-
Notifications
You must be signed in to change notification settings - Fork 328
Tests | Introduce RAII SQL object primitives #4050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mdaigle
merged 9 commits into
dotnet:main
from
edwardneal:tests/transient-database-objects
Apr 17, 2026
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
dca9347
Create DatabaseObject base class for fixture
edwardneal b67c750
Create derived types, use these in a few locations
edwardneal fd92ae9
Further removal of ad-hoc utility methods in ParametersTest
edwardneal c9248eb
Further removal of ad-hoc CREATE/DROP scripts in ParametersTest
edwardneal 7364e6e
Use Table type in SqlGraphTables.cs
edwardneal e454306
Use Table and StoredProcedure types in JsonTest.cs
edwardneal 09ddb81
Use Table type in JsonStreamTest.cs
edwardneal 543e0ca
Remove duplicate helper function from ApiShould.cs
edwardneal c6e7fb5
Merge branch 'main' into tests/transient-database-objects
edwardneal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
263 changes: 263 additions & 0 deletions
263
src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,263 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Text; | ||
| using System.Threading; | ||
|
|
||
| namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; | ||
|
|
||
| /// <summary> | ||
| /// Base class for a transient database object (such as a table, type or | ||
| /// stored procedure.) | ||
| /// </summary> | ||
| public abstract class DatabaseObject : IDisposable | ||
| { | ||
| private readonly bool _shouldDrop; | ||
|
|
||
| protected SqlConnection Connection { get; } | ||
|
|
||
| public string Name { get; } | ||
|
|
||
| protected DatabaseObject(SqlConnection connection, string name, string definition, bool shouldCreate, bool shouldDrop) | ||
| { | ||
| _shouldDrop = shouldDrop; | ||
|
|
||
| Connection = connection; | ||
| Name = name; | ||
|
|
||
| if (shouldCreate) | ||
| { | ||
| EnsureConnectionOpen(); | ||
| DropObject(); | ||
| CreateObject(definition); | ||
| } | ||
| } | ||
|
|
||
| private void EnsureConnectionOpen() | ||
| { | ||
| const int MaxWaits = 2; | ||
| int counter = MaxWaits; | ||
|
|
||
| if (Connection.State is System.Data.ConnectionState.Closed) | ||
| { | ||
| Connection.Open(); | ||
| } | ||
| while (counter-- > 0 && Connection.State is System.Data.ConnectionState.Connecting) | ||
| { | ||
| Thread.Sleep(80); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Generate a new GUID and return the characters from its 1st and 4th | ||
| /// parts, as shown here: | ||
| /// | ||
| /// <code> | ||
| /// 7ff01cb8-88c7-11f0-b433-00155d7e531e | ||
| /// ^^^^^^^^ ^^^^ | ||
| /// </code> | ||
| /// | ||
| /// These 12 characters are concatenated together without any | ||
| /// separators. These 2 parts typically comprise a timestamp and clock | ||
| /// sequence, most likely to be unique for tests that generate names in | ||
| /// quick succession. | ||
| /// </summary> | ||
| private static string GetGuidParts() | ||
| { | ||
| var guid = Guid.NewGuid().ToString(); | ||
| // GOTCHA: The slice operator is inclusive of the start index and | ||
| // exclusive of the end index! | ||
| return guid.Substring(0, 8) + guid.Substring(19, 4); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Generate a long unique database object name, whose maximum length is | ||
| /// 96 characters, with the format: | ||
| /// | ||
| /// <c>{Prefix}_{GuidParts}_{UserName}_{MachineName}</c> | ||
| /// | ||
| /// The Prefix will be truncated to satisfy the overall maximum length. | ||
| /// | ||
| /// The GUID Parts will be the characters from the 1st and 4th blocks | ||
| /// from a traditional string representation, as shown here: | ||
| /// | ||
| /// <code> | ||
| /// 7ff01cb8-88c7-11f0-b433-00155d7e531e | ||
| /// ^^^^^^^^ ^^^^ | ||
| /// </code> | ||
| /// | ||
| /// These 2 parts typically comprise a timestamp and clock sequence, | ||
| /// most likely to be unique for tests that generate names in quick | ||
| /// succession. The 12 characters are concatenated together without any | ||
| /// separators. | ||
| /// | ||
| /// The UserName and MachineName are obtained from the Environment, | ||
| /// and will be truncated to satisfy the maximum overall length. | ||
| /// </summary> | ||
| /// | ||
| /// <param name="prefix"> | ||
| /// The prefix to use when generating the unique name, truncated to at | ||
| /// most 32 characters. | ||
| /// | ||
| /// This should not contain any characters that cannot be used in | ||
| /// database object names. See: | ||
| /// | ||
| /// https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17#rules-for-regular-identifiers | ||
| /// </param> | ||
| /// | ||
| /// <param name="escape"> | ||
| /// When true, the entire generated name will be enclosed in square | ||
| /// brackets, for example: | ||
| /// | ||
| /// <c>[MyPrefix_7ff01cb811f0_test_user_ci_agent_machine_name]</c> | ||
| /// </param> | ||
| /// | ||
| /// <returns> | ||
| /// A unique database object name, no more than 96 characters long. | ||
| /// </returns> | ||
| public static string GenerateLongName(string prefix, bool escape = true) | ||
| { | ||
| StringBuilder name = new(96); | ||
|
|
||
| if (escape) | ||
| { | ||
| name.Append('['); | ||
| } | ||
|
|
||
| if (prefix.Length > 32) | ||
| { | ||
| prefix = prefix.Substring(0, 32); | ||
| } | ||
|
|
||
| name.Append(prefix); | ||
| name.Append('_'); | ||
| name.Append(GetGuidParts()); | ||
| name.Append('_'); | ||
|
|
||
| var suffix = | ||
| Environment.UserName + '_' + | ||
| Environment.MachineName; | ||
|
|
||
| int maxSuffixLength = 96 - name.Length; | ||
| if (escape) | ||
| { | ||
| --maxSuffixLength; | ||
| } | ||
| if (suffix.Length > maxSuffixLength) | ||
| { | ||
| suffix = suffix.Substring(0, maxSuffixLength); | ||
| } | ||
|
|
||
| name.Append(suffix); | ||
|
|
||
| if (escape) | ||
| { | ||
| name.Append(']'); | ||
| } | ||
|
|
||
| return name.ToString(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Generate a short unique database object name, whose maximum length | ||
| /// is 30 characters, with the format: | ||
| /// | ||
| /// <c>{Prefix}_{GuidParts}</c> | ||
| /// | ||
| /// The Prefix will be truncated to satisfy the overall maximum length. | ||
| /// | ||
| /// The GUID parts will be the characters from the 1st and 4th blocks | ||
| /// from a traditional string representation, as shown here: | ||
| /// | ||
| /// <code> | ||
| /// 7ff01cb8-88c7-11f0-b433-00155d7e531e | ||
| /// ^^^^^^^^ ^^^^ | ||
| /// </code> | ||
| /// | ||
| /// These 2 parts typically comprise a timestamp and clock sequence, | ||
| /// most likely to be unique for tests that generate names in quick | ||
| /// succession. The 12 characters are concatenated together without any | ||
| /// separators. | ||
| /// </summary> | ||
| /// | ||
| /// <param name="prefix"> | ||
| /// The prefix to use when generating the unique name, truncated to at | ||
| /// most 18 characters when withBracket is false, and 16 characters when | ||
| /// withBracket is true. | ||
| /// | ||
| /// This should not contain any characters that cannot be used in | ||
| /// database object names. See: | ||
| /// | ||
| /// https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17#rules-for-regular-identifiers | ||
| /// </param> | ||
| /// | ||
| /// <param name="escape"> | ||
| /// When true, the entire generated name will be enclosed in square | ||
| /// brackets, for example: | ||
| /// | ||
| /// <c>[MyPrefix_7ff01cb811f0]</c> | ||
| /// </param> | ||
| /// | ||
| /// <returns> | ||
| /// A unique database object name, no more than 30 characters long. | ||
| /// </returns> | ||
| public static string GenerateShortName(string prefix, bool escape = true) | ||
| { | ||
| StringBuilder name = new(30); | ||
|
|
||
| if (escape) | ||
| { | ||
| name.Append('['); | ||
| } | ||
|
|
||
| int maxPrefixLength = escape ? 16 : 18; | ||
| if (prefix.Length > maxPrefixLength) | ||
| { | ||
| prefix = prefix.Substring(0, maxPrefixLength); | ||
| } | ||
|
|
||
| name.Append(prefix); | ||
| name.Append('_'); | ||
| name.Append(GetGuidParts()); | ||
|
|
||
| if (escape) | ||
| { | ||
| name.Append(']'); | ||
| } | ||
|
|
||
| return name.ToString(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates the object with a given definition. | ||
| /// </summary> | ||
| /// <param name="definition">Definition of the object to create.</param> | ||
| /// <remarks> | ||
| /// By the time this is called, <see cref="Connection"/> will be open. | ||
| /// </remarks> | ||
| protected abstract void CreateObject(string definition); | ||
|
|
||
| /// <summary> | ||
| /// Drops the object created by <see cref="CreateObject"/>. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// By the time this is called, <see cref="Connection"/> will be open. | ||
| /// Must not throw an exception if the object does not exist. | ||
| /// </remarks> | ||
| protected abstract void DropObject(); | ||
|
|
||
| public void Dispose() | ||
| { | ||
| if (_shouldDrop) | ||
| { | ||
| EnsureConnectionOpen(); | ||
| DropObject(); | ||
| } | ||
| // This explicitly does not drop the wrapped SqlConnection; this is sometimes | ||
| // used in a loop to create multiple UDTs. | ||
|
|
||
| GC.SuppressFinalize(this); | ||
| } | ||
| } | ||
41 changes: 41 additions & 0 deletions
41
src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; | ||
|
|
||
| /// <summary> | ||
| /// A transient stored procedure, created at the start of its scope and dropped when disposed. | ||
| /// </summary> | ||
| public sealed class StoredProcedure : DatabaseObject | ||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the StoredProcedure class using the specified SQL connection, | ||
| /// name and definition. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// If a stored procedure with the specified name already exists, it will be dropped automatically | ||
| /// before creation. | ||
| /// </remarks> | ||
| /// <param name="connection">The SQL connection used to interact with the database.</param> | ||
| /// <param name="prefix">The stored procedure name. Can begin with '#' or '##' to indicate a temporary procedure.</param> | ||
| /// <param name="definition">The SQL definition of the stored procedure.</param> | ||
| public StoredProcedure(SqlConnection connection, string prefix, string definition) | ||
| : base(connection, GenerateLongName(prefix), definition, shouldCreate: true, shouldDrop: true) | ||
| { | ||
| } | ||
|
|
||
| protected override void CreateObject(string definition) | ||
| { | ||
| using SqlCommand createCommand = new($"CREATE PROCEDURE {Name} {definition}", Connection); | ||
|
|
||
| createCommand.ExecuteNonQuery(); | ||
| } | ||
|
|
||
| protected override void DropObject() | ||
| { | ||
| using SqlCommand dropCommand = new($"IF (OBJECT_ID('{Name}') IS NOT NULL) DROP PROCEDURE {Name}", Connection); | ||
|
|
||
| dropCommand.ExecuteNonQuery(); | ||
| } | ||
| } |
61 changes: 61 additions & 0 deletions
61
src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; | ||
|
|
||
| /// <summary> | ||
| /// A transient table, created at the start of its scope and dropped when disposed. | ||
| /// </summary> | ||
| public sealed class Table : DatabaseObject | ||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the Table class using the specified SQL connection, table name prefix, and table | ||
| /// definition. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// If a table with the specified name already exists, it will be dropped automatically before | ||
| /// creation. | ||
| /// </remarks> | ||
| /// <param name="connection">The SQL connection used to interact with the database.</param> | ||
| /// <param name="prefix">The prefix for the table name. Can begin with '#' or '##' to indicate a temporary table.</param> | ||
| /// <param name="definition">The SQL definition describing the structure of the table, including columns and data types.</param> | ||
| public Table(SqlConnection connection, string prefix, string definition) | ||
| : base(connection, GenerateLongName(prefix), definition, shouldCreate: true, shouldDrop: true) | ||
| { | ||
| } | ||
|
|
||
| protected override void CreateObject(string definition) | ||
| { | ||
| using SqlCommand createCommand = new($"CREATE TABLE {Name} {definition}", Connection); | ||
|
|
||
| createCommand.ExecuteNonQuery(); | ||
| } | ||
|
|
||
| protected override void DropObject() | ||
| { | ||
| using SqlCommand dropCommand = new($"IF (OBJECT_ID('{Name}') IS NOT NULL) DROP TABLE {Name}", Connection); | ||
|
|
||
| dropCommand.ExecuteNonQuery(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Deletes all data from the table. | ||
| /// </summary> | ||
| public void DeleteData() | ||
| { | ||
| using SqlCommand deleteCommand = new($"DELETE FROM {Name}", Connection); | ||
|
|
||
| deleteCommand.ExecuteNonQuery(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Truncates the table. | ||
| /// </summary> | ||
| public void Truncate() | ||
| { | ||
| using SqlCommand truncateCommand = new($"TRUNCATE TABLE {Name}", Connection); | ||
|
|
||
| truncateCommand.ExecuteNonQuery(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.