Skip to content

Commit c57f3a9

Browse files
Tests | Fix TVP query hint test timeouts against shared Azure SQL DB (#4593)
1 parent 678556d commit c57f3a9

6 files changed

Lines changed: 135 additions & 39 deletions

File tree

src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ protected override void CreateObject(string definition)
3535

3636
protected override void DropObject()
3737
{
38-
using SqlCommand dropCommand = new($"IF USER_ID('{UnescapedName}') IS NOT NULL DROP USER {Name}", Connection);
38+
// NOTE: The name is passed to USER_ID() as a parameter rather than being interpolated into
39+
// a string literal, because it embeds Environment.UserName/MachineName (see
40+
// DatabaseObject.GenerateLongName) and an apostrophe in either would break the batch.
41+
// The identifier in DROP USER is already bracket-quoted by GenerateLongName.
42+
using SqlCommand dropCommand = new($"IF USER_ID(@name) IS NOT NULL DROP USER {Name}", Connection);
43+
44+
dropCommand.Parameters.AddWithValue("@name", UnescapedName);
3945

4046
ExecuteCommandInDatabase(dropCommand);
4147
}

src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,13 @@ protected override void CreateObject(string definition)
9292

9393
protected override void DropObject()
9494
{
95-
using SqlCommand dropCommand = new($"IF SUSER_ID('{UnescapedName}') IS NOT NULL DROP LOGIN {Name}", Connection);
95+
// NOTE: The name is passed to SUSER_ID() as a parameter rather than being interpolated into
96+
// a string literal, because it embeds Environment.UserName/MachineName (see
97+
// DatabaseObject.GenerateLongName) and an apostrophe in either would break the batch.
98+
// The identifier in DROP LOGIN is already bracket-quoted by GenerateLongName.
99+
using SqlCommand dropCommand = new($"IF SUSER_ID(@name) IS NOT NULL DROP LOGIN {Name}", Connection);
100+
101+
dropCommand.Parameters.AddWithValue("@name", UnescapedName);
96102

97103
dropCommand.ExecuteNonQuery();
98104
}

src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ protected override void CreateObject(string definition)
3434

3535
protected override void DropObject()
3636
{
37-
using SqlCommand dropCommand = new($"IF (OBJECT_ID('{Name}') IS NOT NULL) DROP PROCEDURE {Name}", Connection);
37+
// NOTE: The name is passed to OBJECT_ID() as a parameter rather than being interpolated
38+
// into a string literal, because it embeds Environment.UserName/MachineName (see
39+
// DatabaseObject.GenerateLongName) and an apostrophe in either would break the batch.
40+
// The identifier in DROP PROCEDURE is already bracket-quoted by GenerateLongName.
41+
using SqlCommand dropCommand = new($"IF (OBJECT_ID(@name) IS NOT NULL) DROP PROCEDURE {Name}", Connection);
42+
43+
dropCommand.Parameters.AddWithValue("@name", Name);
3844

3945
dropCommand.ExecuteNonQuery();
4046
}

src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ protected override void CreateObject(string definition)
3434

3535
protected override void DropObject()
3636
{
37-
using SqlCommand dropCommand = new($"IF (OBJECT_ID('{Name}') IS NOT NULL) DROP TABLE {Name}", Connection);
37+
// NOTE: The name is passed to OBJECT_ID() as a parameter rather than being interpolated
38+
// into a string literal, because it embeds Environment.UserName/MachineName (see
39+
// DatabaseObject.GenerateLongName) and an apostrophe in either would break the batch.
40+
// The identifier in DROP TABLE is already bracket-quoted by GenerateLongName.
41+
using SqlCommand dropCommand = new($"IF (OBJECT_ID(@name) IS NOT NULL) DROP TABLE {Name}", Connection);
42+
43+
dropCommand.Parameters.AddWithValue("@name", Name);
3844

3945
dropCommand.ExecuteNonQuery();
4046
}

src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/UserDefinedType.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,16 @@ protected override void CreateObject(string definition)
3434

3535
protected override void DropObject()
3636
{
37-
using SqlCommand dropCommand = new($"IF (OBJECT_ID('{Name}') IS NOT NULL) DROP TYPE {Name}", Connection);
37+
// NOTE: User-defined types live in sys.types, not sys.objects, so OBJECT_ID() always
38+
// returns NULL for them. Using it here silently skipped every drop and leaked the type
39+
// into the (shared) test database. TYPE_ID() is the correct lookup.
40+
// NOTE: The name is passed to TYPE_ID() as a parameter rather than being interpolated into
41+
// a string literal, because it embeds Environment.UserName/MachineName (see
42+
// DatabaseObject.GenerateLongName) and an apostrophe in either would break the batch.
43+
// The identifier in DROP TYPE is already bracket-quoted by GenerateLongName.
44+
using SqlCommand dropCommand = new($"IF (TYPE_ID(@typeName) IS NOT NULL) DROP TYPE {Name}", Connection);
45+
46+
dropCommand.Parameters.AddWithValue("@typeName", Name);
3847

3948
dropCommand.ExecuteNonQuery();
4049
}

src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/TvpQueryHintsTests.cs

Lines changed: 97 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,58 +6,121 @@
66
using System.Collections.Generic;
77
using System.Data;
88
using Microsoft.Data.SqlClient.Server;
9+
using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
910
using Xunit;
1011

1112
namespace Microsoft.Data.SqlClient.ManualTesting.Tests
1213
{
1314
/// <summary>
14-
/// Tests for TVP query hints (sort order, uniqueness, default columns).
15+
/// Creates the table type and stored procedure used by <see cref="TvpQueryHintsTests"/> exactly
16+
/// once for the whole test class.
1517
/// </summary>
16-
[Trait("Set", "3")]
17-
public sealed class TvpQueryHintsTests : IDisposable
18+
/// <remarks>
19+
/// Every test in the class needs an identically shaped table type and procedure, so creating them
20+
/// per test only multiplies the amount of DDL issued against the (shared) test database. On Azure
21+
/// SQL Database the resulting schema-modification lock contention was enough to push
22+
/// CREATE/DROP TYPE and CREATE/DROP PROCEDURE past the default 30 second command timeout, which
23+
/// showed up as sporadic "Execution Timeout Expired" failures in the manual test legs. Sharing a
24+
/// single type/procedure via a class fixture cuts the DDL statement count by 5x, and the extended
25+
/// command timeout below absorbs the contention that remains.
26+
/// </remarks>
27+
public sealed class TvpQueryHintsFixture : IDisposable
1828
{
19-
private readonly SqlConnection _conn;
20-
private readonly SqlCommand _cmd;
21-
private readonly SqlParameter _param;
22-
private readonly string _procName;
23-
private readonly string _typeName;
29+
/// <summary>
30+
/// Command timeout (in seconds) applied to every command issued on <see cref="Connection"/>.
31+
/// Deliberately generous: DDL against a shared Azure SQL Database can block for a long time
32+
/// behind concurrently executing test legs.
33+
/// </summary>
34+
private const int CommandTimeoutSeconds = 120;
35+
36+
private readonly UserDefinedType _tableType;
37+
private readonly StoredProcedure _procedure;
2438

25-
public TvpQueryHintsTests()
39+
public SqlConnection Connection { get; }
40+
41+
public string ProcedureName => _procedure.Name;
42+
43+
public TvpQueryHintsFixture()
2644
{
27-
Guid randomizer = Guid.NewGuid();
28-
_typeName = string.Format("dbo.[QHint_{0}]", randomizer);
29-
_procName = string.Format("dbo.[QHint_Proc_{0}]", randomizer);
30-
string createTypeSql = string.Format(
31-
"CREATE TYPE {0} AS TABLE("
45+
SqlConnectionStringBuilder builder = new(DataTestUtility.TCPConnectionString)
46+
{
47+
CommandTimeout = CommandTimeoutSeconds
48+
};
49+
50+
Connection = new SqlConnection(builder.ConnectionString);
51+
52+
// Partial construction must not leak the objects created so far, otherwise a transient
53+
// failure here would orphan a type in the shared database.
54+
try
55+
{
56+
Connection.Open();
57+
58+
_tableType = new UserDefinedType(Connection, "QHint",
59+
"TABLE("
3260
+ " c1 Int DEFAULT -1,"
3361
+ " c2 NVarChar(40) DEFAULT N'DEFUALT',"
3462
+ " c3 DateTime DEFAULT '1/1/2006',"
35-
+ " c4 Int DEFAULT -1)",
36-
_typeName);
37-
string createProcSql = string.Format(
38-
"CREATE PROC {0}(@tvp {1} READONLY) AS SELECT TOP(2) * FROM @tvp ORDER BY c1", _procName, _typeName);
39-
40-
_conn = new SqlConnection(DataTestUtility.TCPConnectionString);
41-
_conn.Open();
63+
+ " c4 Int DEFAULT -1)");
64+
65+
try
66+
{
67+
_procedure = new StoredProcedure(Connection, "QHint_Proc",
68+
$"(@tvp {_tableType.Name} READONLY) AS SELECT TOP(2) * FROM @tvp ORDER BY c1");
69+
}
70+
catch
71+
{
72+
_tableType.Dispose();
73+
throw;
74+
}
75+
}
76+
catch
77+
{
78+
Connection.Dispose();
79+
throw;
80+
}
81+
}
4282

43-
_cmd = new SqlCommand(createTypeSql, _conn);
44-
_cmd.ExecuteNonQuery();
83+
public void Dispose()
84+
{
85+
// Each step runs even if an earlier one fails, so a transient error while dropping the
86+
// procedure cannot leave the type (or the connection) behind.
87+
try
88+
{
89+
_procedure.Dispose();
90+
}
91+
finally
92+
{
93+
try
94+
{
95+
_tableType.Dispose();
96+
}
97+
finally
98+
{
99+
Connection.Dispose();
100+
}
101+
}
102+
}
103+
}
45104

46-
_cmd.CommandText = createProcSql;
47-
_cmd.ExecuteNonQuery();
105+
/// <summary>
106+
/// Tests for TVP query hints (sort order, uniqueness, default columns).
107+
/// </summary>
108+
[Trait("Set", "3")]
109+
public sealed class TvpQueryHintsTests : IClassFixture<TvpQueryHintsFixture>, IDisposable
110+
{
111+
private readonly SqlCommand _cmd;
112+
private readonly SqlParameter _param;
48113

49-
_cmd.CommandText = _procName;
50-
_cmd.CommandType = CommandType.StoredProcedure;
114+
public TvpQueryHintsTests(TvpQueryHintsFixture fixture)
115+
{
116+
_cmd = new SqlCommand(fixture.ProcedureName, fixture.Connection)
117+
{
118+
CommandType = CommandType.StoredProcedure
119+
};
51120
_param = _cmd.Parameters.Add("@tvp", SqlDbType.Structured);
52121
}
53122

54-
public void Dispose()
55-
{
56-
string dropSql = string.Format("DROP PROC {0}; DROP TYPE {1}", _procName, _typeName);
57-
using SqlCommand cmd = new(dropSql, _conn);
58-
cmd.ExecuteNonQuery();
59-
_conn.Dispose();
60-
}
123+
public void Dispose() => _cmd.Dispose();
61124

62125
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
63126
public void SortOrderSimple()

0 commit comments

Comments
 (0)