From e184e80b691dfdc47353922b5ec31b2bd10dca2a Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 25 Aug 2026 11:16:56 -0700 Subject: [PATCH 01/16] Fix SQL Server and certificate resource leaks in test suites Many test fixtures create GUID-named server objects (tables, stored procedures, table types, CMKs, CEKs, logins, queues, services, databases) and certificates, but leave them behind when setup or cleanup fails. Because every name is unique, nothing ever reclaims them: the shared test database accumulates objects until it hits error 3807 ("all available identifiers have been exhausted"), and Windows agents accumulate certificates and persisted private key containers. Three recurring root causes are addressed: 1. xUnit never calls Dispose when a constructor throws, so any object created before the throw is leaked. Affected fixtures now wrap setup in try/catch, invoke their own cleanup, and rethrow. 2. Cleanup ran as a single unguarded batch, so the first failure aborted the rest. Drops are now IF EXISTS / OBJECT_ID guarded and executed independently, best-effort. 3. Certificates created with PersistKeySet leave a key container on disk that store removal does not delete. CertificateFixtureBase now tracks every certificate it creates and deletes the backing CNG/CSP key container on cleanup. Also fixes SQLSetupStrategy and EnclaveAzureDatabaseTests reversing the tracked-object list in place (which would drop keys before their dependents on a second pass), AKV keys being deleted without awaiting completion, NativeColumnEncryptionKeyCertificateBaselineFixture disposing its certificate before the store could remove it, and CertificateTestWithTdsServer skipping the ForceEncryption registry reset when certificate removal failed. No product code changes; test infrastructure only. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../Fixtures/AzureKeyVaultKeyFixtureBase.cs | 38 ++++++-- .../Common/Fixtures/CertificateFixtureBase.cs | 92 +++++++++++++++++- .../ColumnEncryptionCertificateFixture.cs | 38 +++++--- .../ColumnMasterKeyCertificateFixture.cs | 20 +++- .../Common/Fixtures/CspCertificateFixture.cs | 18 +++- .../EnclaveAzureDatabaseTests.cs | 50 ++++++++-- .../AlwaysEncrypted/ExceptionsGenericError.cs | 58 +++++++++-- .../AlwaysEncrypted/SqlNullValues.cs | 82 +++++++++++----- .../TestFixtures/ConversionTestFixture.cs | 38 ++++++-- .../TestFixtures/SQLSetupStrategy.cs | 76 +++++++++++++-- .../SQLSetupStrategyAzureKeyVault.cs | 47 ++++++--- .../SQLSetupStrategyCertStoreProvider.cs | 2 +- .../TestFixtures/Setup/ColumnEncryptionKey.cs | 6 +- .../TestFixtures/Setup/ColumnMasterKey.cs | 6 +- .../TestFixtures/Setup/Table.cs | 6 +- .../SqlSetupStrategyCspProvider.cs | 45 +++++---- .../tests/ManualTests/BulkCopy/Bug84548.cs | 4 +- .../tests/ManualTests/BulkCopy/Bug85007.cs | 4 +- .../tests/ManualTests/BulkCopy/Bug903514.cs | 21 ++-- .../tests/ManualTests/BulkCopy/Bug98182.cs | 5 +- .../ManualTests/BulkCopy/CacheMetadata.cs | 3 +- .../ManualTests/BulkCopy/CheckConstraints.cs | 4 +- .../ManualTests/BulkCopy/ColumnCollation.cs | 63 ++++++------ .../ManualTests/BulkCopy/CopyVariants.cs | 4 +- .../DataConversionErrorMessageTest.cs | 21 +++- .../DestinationTableNameWithSpecialChar.cs | 5 +- .../tests/ManualTests/BulkCopy/FireTrigger.cs | 9 +- .../tests/ManualTests/BulkCopy/Helpers.cs | 95 ++++++++++++++++++- .../tests/ManualTests/BulkCopy/KeepNulls.cs | 29 +++--- .../BulkCopy/SpecialCharacterNames.cs | 7 +- .../tests/ManualTests/BulkCopy/TableLock.cs | 30 +++--- .../ManualTests/BulkCopy/UnprivilegedLogin.cs | 50 +++++++--- .../CertificateTestWithTdsServer.cs | 69 +++++++++++--- .../SQL/ConnectivityTests/ConnectivityTest.cs | 21 +++- .../SQL/JsonTest/JsonBulkCopyTest.cs | 33 ++++++- .../ParallelTransactionsTest.cs | 3 +- .../ManualTests/SQL/ParameterTest/TvpTest.cs | 69 +++++++++----- .../SqlCredentialTest/SqlCredentialTest.cs | 28 +++++- .../SqlNotificationTest.cs | 37 +++++++- .../SQL/TransactionTest/TransactionTest.cs | 21 +++- .../SQL/VectorTest/NativeVectorTestsBase.cs | 40 ++++++-- .../VectorBackwardCompatTestBase.cs | 43 +++++++-- ...EncryptionKeyCertificateBaselineFixture.cs | 21 +++- 43 files changed, 1062 insertions(+), 299 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/AzureKeyVaultKeyFixtureBase.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/AzureKeyVaultKeyFixtureBase.cs index b2232b3f69..8feb0e38f0 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/AzureKeyVaultKeyFixtureBase.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/AzureKeyVaultKeyFixtureBase.cs @@ -75,18 +75,38 @@ public void Dispose() protected virtual void Dispose(bool disposing) { - foreach (KeyVaultKey key in _createdKeys) + try { - try - { - _keyClient.StartDeleteKey(key.Name).WaitForCompletion(); - } - catch (Exception) + foreach (KeyVaultKey key in _createdKeys) { - continue; + try + { + _keyClient.StartDeleteKey(key.Name).WaitForCompletion(); + } + catch (Exception) + { + continue; + } + + // A deleted key remains in a soft-deleted state (and continues to consume the name) + // until it is purged or the retention period expires. Purging is best-effort: the + // test principal may not have permission, and the vault may have purge protection + // enabled, in which case the soft-deleted key simply expires on its own. + try + { + _keyClient.PurgeDeletedKey(key.Name); + } + catch (Exception) + { + continue; + } } - } - _randomGenerator.Dispose(); + _createdKeys.Clear(); + } + finally + { + _randomGenerator.Dispose(); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs index 5081bc8b18..b8d081cf01 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs @@ -50,7 +50,23 @@ public CertificateStoreContext(StoreLocation location, StoreName name) private readonly List _certificateStoreModifications = new List(); + /// + /// Every certificate handed out by . Certificates are created with + /// , which writes the private key to a key container on + /// disk. Removing the certificate from a store does not remove that container, so the containers must + /// be deleted explicitly or they accumulate indefinitely across test runs. + /// + private readonly List _createdCertificates = new List(); + protected X509Certificate2 CreateCertificate(string subjectName, IEnumerable dnsNames, IEnumerable ipAddresses, bool forceCsp = false) + { + X509Certificate2 certificate = CreateCertificateCore(subjectName, dnsNames, ipAddresses, forceCsp); + + _createdCertificates.Add(certificate); + return certificate; + } + + private X509Certificate2 CreateCertificateCore(string subjectName, IEnumerable dnsNames, IEnumerable ipAddresses, bool forceCsp = false) { // This will always generate a certificate with: // * Start date: 24hrs ago @@ -281,21 +297,39 @@ public void Dispose() protected virtual void Dispose(bool disposing) { + // Collect everything that needs disposing before touching any of it: removal from a store and + // deletion of the persisted key are both best-effort, but the handles must always be released. + List certificates = new List(_createdCertificates); + + // Remove the certificates from any store they were added to. This must happen before the + // certificates are disposed, because a disposed certificate cannot be matched against a store. foreach (CertificateStoreContext storeContext in _certificateStoreModifications) { using X509Store store = new X509Store(storeContext.Name, storeContext.Location); + bool opened; try { store.Open(OpenFlags.ReadWrite); + opened = true; } catch (Exception) { - continue; + opened = false; } foreach (X509Certificate2 cert in storeContext.Certificates) { + if (!certificates.Contains(cert)) + { + certificates.Add(cert); + } + + if (!opened) + { + continue; + } + try { if (store.Certificates.Contains(cert)) @@ -307,11 +341,65 @@ protected virtual void Dispose(bool disposing) { continue; } + } + storeContext.Certificates.Clear(); + } + + _certificateStoreModifications.Clear(); + + foreach (X509Certificate2 cert in certificates) + { + DeletePersistedPrivateKey(cert); + + try + { cert.Dispose(); } + catch (Exception) + { + // Nothing further can be done about a certificate that refuses to release its handle. + } + } - storeContext.Certificates.Clear(); + _createdCertificates.Clear(); + } + + /// + /// Deletes the on-disk key container backing a certificate's private key, if there is one. + /// + /// + /// This is best-effort: the certificate may have no private key, the key may be ephemeral, or the + /// current user may lack permission to delete it (for example, for a machine-scoped key). + /// + private static void DeletePersistedPrivateKey(X509Certificate2 certificate) + { + try + { + if (!certificate.HasPrivateKey) + { + return; + } + + using RSA? privateKey = certificate.GetRSAPrivateKey(); + + switch (privateKey) + { + case RSACryptoServiceProvider csp: + csp.PersistKeyInCsp = false; + break; +#if NET + case RSACng cng: + cng.Key.Delete(); + break; +#endif + default: + break; + } + } + catch (Exception) + { + // Best-effort; a stale key container is preferable to failing the test run during cleanup. } } } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnEncryptionCertificateFixture.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnEncryptionCertificateFixture.cs index c5b527c690..ff2dc0719b 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnEncryptionCertificateFixture.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnEncryptionCertificateFixture.cs @@ -28,29 +28,41 @@ public sealed class ColumnEncryptionCertificateFixture : CertificateFixtureBase public ColumnEncryptionCertificateFixture() { - PrimaryColumnEncryptionCertificate = CreateCertificate(nameof(PrimaryColumnEncryptionCertificate), Array.Empty(), Array.Empty()); - SecondaryColumnEncryptionCertificate = CreateCertificate(nameof(SecondaryColumnEncryptionCertificate), Array.Empty(), Array.Empty()); - _currentUserCertificate = CreateCertificate(nameof(_currentUserCertificate), Array.Empty(), Array.Empty()); - using (X509Certificate2 createdCertificate = CreateCertificate(nameof(CertificateWithoutPrivateKey), Array.Empty(), Array.Empty())) + // NOTE: If this constructor throws, xUnit never calls Dispose, so any certificate already + // placed in a store (and its persisted key container) would be leaked. + try { + PrimaryColumnEncryptionCertificate = CreateCertificate(nameof(PrimaryColumnEncryptionCertificate), Array.Empty(), Array.Empty()); + SecondaryColumnEncryptionCertificate = CreateCertificate(nameof(SecondaryColumnEncryptionCertificate), Array.Empty(), Array.Empty()); + _currentUserCertificate = CreateCertificate(nameof(_currentUserCertificate), Array.Empty(), Array.Empty()); + + // NOTE: The source certificate is intentionally not disposed here; it is tracked by the base + // fixture, which deletes its persisted key container and disposes it on cleanup. + X509Certificate2 createdCertificate = CreateCertificate(nameof(CertificateWithoutPrivateKey), Array.Empty(), Array.Empty()); + // This will strip the private key away from the created certificate #if NET9_0_OR_GREATER - CertificateWithoutPrivateKey = X509CertificateLoader.LoadCertificate(createdCertificate.Export(X509ContentType.Cert)); + CertificateWithoutPrivateKey = X509CertificateLoader.LoadCertificate(createdCertificate.Export(X509ContentType.Cert)); #else CertificateWithoutPrivateKey = new X509Certificate2(createdCertificate.Export(X509ContentType.Cert)); #endif AddToStore(CertificateWithoutPrivateKey, StoreLocation.CurrentUser, StoreName.My); - } - AddToStore(PrimaryColumnEncryptionCertificate, StoreLocation.CurrentUser, StoreName.My); - AddToStore(SecondaryColumnEncryptionCertificate, StoreLocation.CurrentUser, StoreName.My); - AddToStore(_currentUserCertificate, StoreLocation.CurrentUser, StoreName.My); + AddToStore(PrimaryColumnEncryptionCertificate, StoreLocation.CurrentUser, StoreName.My); + AddToStore(SecondaryColumnEncryptionCertificate, StoreLocation.CurrentUser, StoreName.My); + AddToStore(_currentUserCertificate, StoreLocation.CurrentUser, StoreName.My); - if (IsAdmin) - { - _localMachineCertificate = CreateCertificate(nameof(_localMachineCertificate), Array.Empty(), Array.Empty()); + if (IsAdmin) + { + _localMachineCertificate = CreateCertificate(nameof(_localMachineCertificate), Array.Empty(), Array.Empty()); - AddToStore(_localMachineCertificate, StoreLocation.LocalMachine, StoreName.My); + AddToStore(_localMachineCertificate, StoreLocation.LocalMachine, StoreName.My); + } + } + catch + { + Dispose(); + throw; } } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnMasterKeyCertificateFixture.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnMasterKeyCertificateFixture.cs index d396251103..5cb70cc27a 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnMasterKeyCertificateFixture.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/ColumnMasterKeyCertificateFixture.cs @@ -28,11 +28,21 @@ protected ColumnMasterKeyCertificateFixture(bool createCertificate) { if (createCertificate) { - ColumnMasterKeyCertificate = CreateCertificate(nameof(ColumnMasterKeyCertificate), Array.Empty(), Array.Empty()); - - AddToStore(ColumnMasterKeyCertificate, StoreLocation.CurrentUser, StoreName.My); - - ColumnMasterKeyCertificatePath = $"{StoreLocation.CurrentUser}/{StoreName.My}/{ColumnMasterKeyCertificate.Thumbprint}"; + // NOTE: If this constructor throws, xUnit never calls Dispose, so the certificate placed in + // the store (and its persisted key container) would be leaked. + try + { + ColumnMasterKeyCertificate = CreateCertificate(nameof(ColumnMasterKeyCertificate), Array.Empty(), Array.Empty()); + + AddToStore(ColumnMasterKeyCertificate, StoreLocation.CurrentUser, StoreName.My); + + ColumnMasterKeyCertificatePath = $"{StoreLocation.CurrentUser}/{StoreName.My}/{ColumnMasterKeyCertificate.Thumbprint}"; + } + catch + { + Dispose(); + throw; + } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CspCertificateFixture.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CspCertificateFixture.cs index 4d46032ce2..ac8e847656 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CspCertificateFixture.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CspCertificateFixture.cs @@ -19,12 +19,22 @@ public class CspCertificateFixture : CertificateFixtureBase { public CspCertificateFixture() { - CspCertificate = CreateCertificate(nameof(CspCertificate), Array.Empty(), Array.Empty(), true); + // NOTE: If this constructor throws, xUnit never calls Dispose, so the certificate placed in the + // store (and its persisted CSP key container) would be leaked. + try + { + CspCertificate = CreateCertificate(nameof(CspCertificate), Array.Empty(), Array.Empty(), true); - AddToStore(CspCertificate, StoreLocation.CurrentUser, StoreName.My); + AddToStore(CspCertificate, StoreLocation.CurrentUser, StoreName.My); - CspCertificatePath = $"{StoreLocation.CurrentUser}/{StoreName.My}/{CspCertificate.Thumbprint}"; - CspKeyPath = GetCspPathFromCertificate(); + CspCertificatePath = $"{StoreLocation.CurrentUser}/{StoreName.My}/{CspCertificate.Thumbprint}"; + CspKeyPath = GetCspPathFromCertificate(); + } + catch + { + Dispose(); + throw; + } } public X509Certificate2 CspCertificate { get; } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/EnclaveAzureDatabaseTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/EnclaveAzureDatabaseTests.cs index 4de82d39b7..7fca0a7abb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/EnclaveAzureDatabaseTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/EnclaveAzureDatabaseTests.cs @@ -50,14 +50,25 @@ public EnclaveAzureDatabaseTests(AzureKeyVaultKeyFixture keyVaultKeyFixture) connStrings.Add(connString1.ToString()); connStrings.Add(connString2.ToString()); - foreach (string connString in connStrings) + // NOTE: If creation fails part way through (for example on the second database), this + // constructor never returns, so xUnit never calls Dispose and the keys created so far + // would be leaked. Their names embed a GUID, so nothing would ever reclaim them. + try { - using (SqlConnection connection = new SqlConnection(connString)) + foreach (string connString in connStrings) { - connection.Open(); - databaseObjects.ForEach(o => o.Create(connection)); + using (SqlConnection connection = new SqlConnection(connString)) + { + connection.Open(); + databaseObjects.ForEach(o => o.Create(connection)); + } } } + catch + { + Dispose(); + throw; + } } } @@ -162,13 +173,36 @@ public void Dispose() { if (DataTestUtility.IsEnclaveAzureDatabaseSetup()) { - databaseObjects.Reverse(); + // NOTE: Reverse a copy - reversing the field in place would restore creation order if + // this ever ran twice, dropping the master key before the encryption key that depends + // on it. Every drop is best-effort so that one failure cannot leak the rest. + List objectsToDrop = new List(databaseObjects); + objectsToDrop.Reverse(); + foreach (string connStr in connStrings) { - using (SqlConnection sqlConnection = new SqlConnection(connStr)) + try + { + using (SqlConnection sqlConnection = new SqlConnection(connStr)) + { + sqlConnection.Open(); + + foreach (DbObject databaseObject in objectsToDrop) + { + try + { + databaseObject.Drop(sqlConnection); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(EnclaveAzureDatabaseTests)}: failed to drop '{databaseObject.Name}': {ex.Message}"); + } + } + } + } + catch (Exception ex) { - sqlConnection.Open(); - databaseObjects.ForEach(o => o.Drop(sqlConnection)); + Console.WriteLine($"{nameof(EnclaveAzureDatabaseTests)}: cleanup connection failed: {ex.Message}"); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs index 00e45951db..9a0d2b43d5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs @@ -119,7 +119,19 @@ public sealed class ExceptionGenericErrorFixture : IDisposable public ExceptionGenericErrorFixture() { SqlConnection.ColumnEncryptionQueryMetadataCacheEnabled = false; - CreateAndPopulateSimpleTable(); + + // NOTE: If setup fails part way through, this constructor never returns, so xUnit never + // calls Dispose and the objects created so far would be leaked. Their names embed a + // GUID, so anything left behind stays in the shared test database forever. + try + { + CreateAndPopulateSimpleTable(); + } + catch + { + Dispose(); + throw; + } } private void CreateAndPopulateSimpleTable() @@ -155,25 +167,55 @@ public void Dispose() foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) { SqlConnectionStringBuilder sb = new SqlConnectionStringBuilder(connectionStr); - using (SqlConnection conn = CertificateUtility.GetOpenConnection(false, sb)) + + // NOTE: Cleanup is best-effort and guarded. Previously the drops shared one command + // with no IF EXISTS guard, so a failure to drop the table leaked the procedure and + // skipped the server TCE setting reset for every remaining connection string. + try { - using (SqlCommand cmd = new SqlCommand($"drop table {encryptedTableName}", conn)) + using (SqlConnection conn = CertificateUtility.GetOpenConnection(false, sb)) { - cmd.CommandType = CommandType.Text; - cmd.ExecuteNonQuery(); + using (SqlCommand cmd = conn.CreateCommand()) + { + cmd.CommandType = CommandType.Text; - cmd.CommandText = $"drop procedure {encryptedProcedureName}"; - cmd.ExecuteNonQuery(); + TryExecute(cmd, $"IF (OBJECT_ID('{encryptedTableName}') IS NOT NULL) DROP TABLE {encryptedTableName}"); + TryExecute(cmd, $"IF (OBJECT_ID('{encryptedProcedureName}') IS NOT NULL) DROP PROCEDURE {encryptedProcedureName}"); + } } } + catch (Exception ex) + { + Console.WriteLine($"{nameof(ExceptionGenericErrorFixture)}: cleanup failed: {ex.Message}"); + } // Only use traceoff for non-sysadmin role accounts, Azure accounts does not have the permission. if (DataTestUtility.IsNotAzureServer()) { - CertificateUtility.ChangeServerTceSetting(true, sb); + try + { + CertificateUtility.ChangeServerTceSetting(true, sb); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(ExceptionGenericErrorFixture)}: failed to reset TCE setting: {ex.Message}"); + } } } } + + private static void TryExecute(SqlCommand command, string commandText) + { + try + { + command.CommandText = commandText; + command.ExecuteNonQuery(); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(ExceptionGenericErrorFixture)}: cleanup statement failed ({commandText}): {ex.Message}"); + } + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlNullValues.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlNullValues.cs index f409a7db77..9b8ff77494 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlNullValues.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlNullValues.cs @@ -26,35 +26,45 @@ public SqlNullValuesTests(SQLSetupStrategyCertStoreProvider context) // Disable the cache to avoid false failures. SqlConnection.ColumnEncryptionQueryMetadataCacheEnabled = false; - foreach (string connStr in DataTestUtility.AEConnStringsSetup) + // NOTE: If setup fails part way through, this constructor never returns, so xUnit never calls + // Dispose and the functions created so far (whose names embed a GUID) would be leaked. + try { - // Insert data and create functions for SqlNullValues test. - using (SqlConnection sqlConnection = new SqlConnection(connStr)) + foreach (string connStr in DataTestUtility.AEConnStringsSetup) { - sqlConnection.Open(); - - using (SqlCommand cmd = new SqlCommand(string.Format("INSERT INTO [{0}] (c1) VALUES (@c1)", tableName), sqlConnection, null, SqlCommandColumnEncryptionSetting.Enabled)) + // Insert data and create functions for SqlNullValues test. + using (SqlConnection sqlConnection = new SqlConnection(connStr)) { - SqlParameter param = cmd.Parameters.Add("@c1", SqlDbType.Int); - param.Value = DBNull.Value; - cmd.ExecuteNonQuery(); + sqlConnection.Open(); - param.Value = 10; - cmd.ExecuteNonQuery(); - } + using (SqlCommand cmd = new SqlCommand(string.Format("INSERT INTO [{0}] (c1) VALUES (@c1)", tableName), sqlConnection, null, SqlCommandColumnEncryptionSetting.Enabled)) + { + SqlParameter param = cmd.Parameters.Add("@c1", SqlDbType.Int); + param.Value = DBNull.Value; + cmd.ExecuteNonQuery(); - string sql1 = $"CREATE FUNCTION {UdfName}() RETURNS INT AS \n BEGIN \n RETURN (SELECT c1 FROM [{tableName}] WHERE c1 IS NULL)\n END"; - string sql2 = $"CREATE FUNCTION {UdfNameNotNull}() RETURNS INT AS \n BEGIN \n RETURN (SELECT c1 FROM [{tableName}] WHERE c1 IS NOT NULL)\n END"; - using (SqlCommand cmd = sqlConnection.CreateCommand()) - { - cmd.CommandText = sql1; - cmd.ExecuteNonQuery(); + param.Value = 10; + cmd.ExecuteNonQuery(); + } - cmd.CommandText = sql2; - cmd.ExecuteNonQuery(); + string sql1 = $"CREATE FUNCTION {UdfName}() RETURNS INT AS \n BEGIN \n RETURN (SELECT c1 FROM [{tableName}] WHERE c1 IS NULL)\n END"; + string sql2 = $"CREATE FUNCTION {UdfNameNotNull}() RETURNS INT AS \n BEGIN \n RETURN (SELECT c1 FROM [{tableName}] WHERE c1 IS NOT NULL)\n END"; + using (SqlCommand cmd = sqlConnection.CreateCommand()) + { + cmd.CommandText = sql1; + cmd.ExecuteNonQuery(); + + cmd.CommandText = sql2; + cmd.ExecuteNonQuery(); + } } } } + catch + { + Dispose(); + throw; + } } [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] @@ -158,15 +168,37 @@ public void Dispose() { foreach (string connStrAE in DataTestUtility.AEConnStringsSetup) { - using (SqlConnection sqlConnection = new SqlConnection(connStrAE)) + // Each step is best-effort so that one failure cannot leak the remaining functions or + // skip cleanup for the remaining connection strings. + try + { + using (SqlConnection sqlConnection = new SqlConnection(connStrAE)) + { + sqlConnection.Open(); + + TryCleanup(() => Table.DeleteData(fixture.SqlNullValuesTable.Name, sqlConnection)); + TryCleanup(() => DataTestUtility.DropFunction(sqlConnection, UdfName)); + TryCleanup(() => DataTestUtility.DropFunction(sqlConnection, UdfNameNotNull)); + } + } + catch (Exception ex) { - sqlConnection.Open(); - Table.DeleteData(fixture.SqlNullValuesTable.Name, sqlConnection); - DataTestUtility.DropFunction(sqlConnection, UdfName); - DataTestUtility.DropFunction(sqlConnection, UdfNameNotNull); + Console.WriteLine($"{nameof(SqlNullValuesTests)}: cleanup connection failed: {ex.Message}"); } } } + + private static void TryCleanup(Action cleanupAction) + { + try + { + cleanupAction(); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(SqlNullValuesTests)}: cleanup step failed: {ex.Message}"); + } + } } public class NullValueTestsData : IEnumerable diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/ConversionTestFixture.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/ConversionTestFixture.cs index abae589981..7b34a6314e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/ConversionTestFixture.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/ConversionTestFixture.cs @@ -46,16 +46,38 @@ public ConversionTestFixture() _columnMasterKey, _certStoreProvider); - foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) + // NOTE: If creation fails part way through (for example on the second connection string), + // this constructor never returns, so xUnit never disposes the fixture and the keys + // created so far would be leaked into the shared test database forever. Dispose + // explicitly before rethrowing; the drops are guarded, so dropping a key that was + // never created is a no-op. + try { - SqlConnectionStringBuilder connectionString = new SqlConnectionStringBuilder(connectionStr); - // The AE setup often fails with a connect timeout here; ensure a reasonable minimum. - connectionString.ConnectTimeout = Math.Max(connectionString.ConnectTimeout, 30); + foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) + { + SqlConnectionStringBuilder connectionString = new SqlConnectionStringBuilder(connectionStr); + // The AE setup often fails with a connect timeout here; ensure a reasonable minimum. + connectionString.ConnectTimeout = Math.Max(connectionString.ConnectTimeout, 30); + + using SqlConnection sqlConnection = new SqlConnection(connectionString.ConnectionString); + sqlConnection.Open(); + _columnMasterKey.Create(sqlConnection); + ColumnEncryptionKey.Create(sqlConnection); + } + } + catch + { + try + { + Dispose(); + } + catch (Exception disposeException) + { + Console.WriteLine( + $"ConversionTestFixture: cleanup after failed setup did not complete: {disposeException.Message}"); + } - using SqlConnection sqlConnection = new SqlConnection(connectionString.ConnectionString); - sqlConnection.Open(); - _columnMasterKey.Create(sqlConnection); - ColumnEncryptionKey.Create(sqlConnection); + throw; } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs index 9929db55d8..5cf8a7b22b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs @@ -69,6 +69,38 @@ protected SQLSetupStrategy(string customKeyPath) ColumnMasterKeyPath = customKeyPath; } + /// + /// Runs the supplied setup action, disposing this fixture if the action throws. + /// + /// + /// Derived types perform their setup from their constructors. When a constructor throws, + /// xUnit never receives the instance and therefore never disposes it, so every column master + /// key, column encryption key and table created before the failure would be leaked into the + /// (shared, long lived) test database. Because the object names embed a GUID, those leaks are + /// permanent and accumulate on every run, eventually exhausting SQL Server's per-database + /// object identifiers (error 3807). + /// + protected void SetupOrCleanUp(Action setupAction) + { + try + { + setupAction(); + } + catch + { + try + { + Dispose(); + } + catch (Exception disposeException) + { + Console.WriteLine($"{GetType().Name}: cleanup after failed setup did not complete: {disposeException}"); + } + + throw; + } + } + internal virtual void SetupDatabase() { foreach (string value in DataTestUtility.AEConnStringsSetup) @@ -274,16 +306,48 @@ protected List CreateTables(IList columnEncryptionKe protected override void Dispose(bool disposing) { - databaseObjects.Reverse(); - foreach (string value in DataTestUtility.AEConnStringsSetup) + try { - using (SqlConnection sqlConnection = new SqlConnection(value)) + foreach (string value in DataTestUtility.AEConnStringsSetup) { - sqlConnection.Open(); - databaseObjects.ForEach(o => o.Drop(sqlConnection)); + try + { + using (SqlConnection sqlConnection = new SqlConnection(value)) + { + sqlConnection.Open(); + + // NOTE: Objects are dropped in reverse creation order so that dependants + // (tables) are removed before their dependencies (keys). A local copy is + // reversed rather than the field itself, so that repeated disposal (which + // can happen when setup fails) does not restore the original order. + List objectsToDrop = new List(databaseObjects); + objectsToDrop.Reverse(); + + foreach (DbObject databaseObject in objectsToDrop) + { + // Each drop is best-effort: one failure must not prevent the remaining + // objects from being dropped, or the certificate from being removed. + try + { + databaseObject.Drop(sqlConnection); + } + catch (Exception ex) + { + Console.WriteLine($"{GetType().Name}: failed to drop '{databaseObject.Name}': {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"{GetType().Name}: failed to clean up database objects: {ex.Message}"); + } } } - base.Dispose(disposing); + finally + { + base.Dispose(disposing); + } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyAzureKeyVault.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyAzureKeyVault.cs index f1ebc3a93e..eac615af55 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyAzureKeyVault.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyAzureKeyVault.cs @@ -35,8 +35,12 @@ public SQLSetupStrategyAzureKeyVault() : base() { RegisterGlobalProviders(AkvStoreProvider); } - SetupAzureKeyVault(); - SetupDatabase(); + + SetupOrCleanUp(() => + { + SetupAzureKeyVault(); + SetupDatabase(); + }); } public static void RegisterGlobalProviders(SqlColumnEncryptionAzureKeyVaultProvider akvProvider) @@ -109,18 +113,39 @@ internal override void SetupDatabase() protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - foreach (string keyName in _akvKeyNames) + try { - try - { - _keyClient.StartDeleteKey(keyName); - } - catch (Exception) + base.Dispose(disposing); + } + finally + { + foreach (string keyName in _akvKeyNames) { - continue; + try + { + // NOTE: The deletion is awaited so that the key is actually removed before the + // process exits; StartDeleteKey only begins the (long running) operation. + _keyClient.StartDeleteKey(keyName).WaitForCompletion(); + + // A deleted key stays soft-deleted (still consuming its name and vault quota) + // until purged. Purging is best-effort: the principal may lack permission, or + // the vault may have purge protection enabled. + try + { + _keyClient.PurgeDeletedKey(keyName); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(SQLSetupStrategyAzureKeyVault)}: failed to purge AKV key '{keyName}': {ex.Message}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(SQLSetupStrategyAzureKeyVault)}: failed to delete AKV key '{keyName}': {ex.Message}"); + } } + + _akvKeyNames.Clear(); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyCertStoreProvider.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyCertStoreProvider.cs index db935a5fed..eb2b6c3e32 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyCertStoreProvider.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyCertStoreProvider.cs @@ -16,7 +16,7 @@ public class SQLSetupStrategyCertStoreProvider : SQLSetupStrategy public SQLSetupStrategyCertStoreProvider() : base() { CertStoreProvider = new SqlColumnEncryptionCertificateStoreProvider(); - SetupDatabase(); + SetupOrCleanUp(SetupDatabase); } internal override void SetupDatabase() diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnEncryptionKey.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnEncryptionKey.cs index f9b008ddbf..7b6338b7b2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnEncryptionKey.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnEncryptionKey.cs @@ -43,12 +43,16 @@ WITH VALUES ( public override void Drop(SqlConnection sqlConnection) { - string sql = $"DROP COLUMN ENCRYPTION KEY [{Name}]"; + // NOTE: The drop is guarded so that cleanup is idempotent. An unguarded DROP throws when + // the key was never created (for example when setup failed part way through), which + // would abort the enclosing drop loop and leak every remaining object. + string sql = $"IF EXISTS (SELECT 1 FROM sys.column_encryption_keys WHERE name = @name) DROP COLUMN ENCRYPTION KEY [{Name}]"; using (SqlCommand command = sqlConnection.CreateCommand()) { command.CommandText = sql; command.CommandTimeout = 60; + command.Parameters.AddWithValue("@name", Name); command.ExecuteNonQuery(); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs index 2e8d67b95c..155d36bc29 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs @@ -50,12 +50,16 @@ public override void Create(SqlConnection sqlConnection) public override void Drop(SqlConnection sqlConnection) { - string sql = $"DROP COLUMN MASTER KEY [{Name}];"; + // NOTE: The drop is guarded so that cleanup is idempotent. An unguarded DROP throws when + // the key was never created (for example when setup failed part way through), which + // would abort the enclosing drop loop and leak every remaining object. + string sql = $"IF EXISTS (SELECT 1 FROM sys.column_master_keys WHERE name = @name) DROP COLUMN MASTER KEY [{Name}];"; using (SqlCommand command = sqlConnection.CreateCommand()) { command.CommandText = sql; command.CommandTimeout = 60; + command.Parameters.AddWithValue("@name", Name); command.ExecuteNonQuery(); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs index b18e5a0ae8..a037087d64 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs @@ -12,11 +12,15 @@ protected Table(string name) : base(name) public override void Drop(SqlConnection sqlConnection) { - string sql = $"DROP TABLE [{Name}];"; + // NOTE: The drop is guarded so that cleanup is idempotent. An unguarded DROP throws when + // the object was never created (for example when setup failed part way through), which + // would abort the enclosing drop loop and leak every remaining object. + string sql = $"IF (OBJECT_ID(@name) IS NOT NULL) DROP TABLE [{Name}];"; using (SqlCommand command = sqlConnection.CreateCommand()) { command.CommandText = sql; + command.Parameters.AddWithValue("@name", $"[{Name}]"); command.ExecuteNonQuery(); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SqlSetupStrategyCspProvider.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SqlSetupStrategyCspProvider.cs index 08cdfe48ec..da6b06d5d8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SqlSetupStrategyCspProvider.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SqlSetupStrategyCspProvider.cs @@ -27,7 +27,7 @@ public SQLSetupStrategyCspProvider(CspParameters cspParameters) _cspKeyParameters.Add(cspParameters); CspProvider = new SqlColumnEncryptionCspProvider(); - SetupDatabase(); + SetupOrCleanUp(SetupDatabase); } public SqlColumnEncryptionCspProvider CspProvider { get; } @@ -50,28 +50,35 @@ internal override void SetupDatabase() protected override void Dispose(bool disposing) { - foreach (CspParameters cspParameters in _cspKeyParameters) + try { - try + foreach (CspParameters cspParameters in _cspKeyParameters) { - // Create a new instance of RSACryptoServiceProvider. - // Pass the CspParameters class to use the - // key in the container. - using RSACryptoServiceProvider rsaAlg = new RSACryptoServiceProvider(cspParameters); - - // Delete the key entry in the container. - rsaAlg.PersistKeyInCsp = false; - - // Call Clear to release resources and delete the key from the container. - rsaAlg.Clear(); - } - catch (Exception) - { - continue; + try + { + // Create a new instance of RSACryptoServiceProvider. + // Pass the CspParameters class to use the + // key in the container. + using RSACryptoServiceProvider rsaAlg = new RSACryptoServiceProvider(cspParameters); + + // Delete the key entry in the container. + rsaAlg.PersistKeyInCsp = false; + + // Call Clear to release resources and delete the key from the container. + rsaAlg.Clear(); + } + catch (Exception) + { + continue; + } } - } - base.Dispose(disposing); + _cspKeyParameters.Clear(); + } + finally + { + base.Dispose(disposing); + } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs index fad361ff9e..349913171b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs @@ -78,8 +78,8 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + targettable); - Helpers.TryExecute(dstCmd, "drop table " + targetCustomerTable); + Helpers.DropTable(dstCmd, targettable); + Helpers.DropTable(dstCmd, targetCustomerTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs index b3de348137..42f4daf77a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs @@ -106,8 +106,8 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); - Helpers.TryExecute(dstCmd, "drop table " + targetCustomerTable); + Helpers.DropTable(dstCmd, dstTable); + Helpers.DropTable(dstCmd, targetCustomerTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs index d720fd9ebc..f8fa2252a0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs @@ -25,15 +25,22 @@ public void Test() Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 varchar(7000))"); } - DoBulkCopy(constr, dstTable, 2); - DoBulkCopy(constr, dstTable, 0); - - using (SqlConnection dstConn = new SqlConnection(constr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) + // NOTE: The table name embeds a GUID, so it must be dropped even when the bulk copy or an + // assertion below fails, otherwise it is left in the shared test database forever. + try { - dstConn.Open(); + DoBulkCopy(constr, dstTable, 2); + DoBulkCopy(constr, dstTable, 0); + } + finally + { + using (SqlConnection dstConn = new SqlConnection(constr)) + using (SqlCommand dstCmd = dstConn.CreateCommand()) + { + dstConn.Open(); - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.TryExecute(dstCmd, "drop table " + dstTable); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug98182.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug98182.cs index f611e2e3b1..fc0c10cec9 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug98182.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug98182.cs @@ -62,7 +62,10 @@ public void Test() } finally { - Helpers.ProcessCommandBatch(typeof(SqlConnection), constr, prologue); + // NOTE: Each drop is run independently so that a failure to drop the source table + // does not leak the destination table (the names embed a GUID, so anything left + // behind stays in the shared test database forever). + Helpers.ProcessCleanupBatch(typeof(SqlConnection), constr, prologue); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs index 7443a99734..ce707e9408 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs @@ -186,8 +186,7 @@ public void Test() } finally { - Helpers.TryDropTable(dstConstr, dstTable1); - Helpers.TryDropTable(dstConstr, dstTable2); + Helpers.DropTables(dstConstr, dstTable1, dstTable2); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CheckConstraints.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CheckConstraints.cs index ede548f9cf..b5ec0e0dfc 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CheckConstraints.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CheckConstraints.cs @@ -60,8 +60,8 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); - Helpers.TryExecute(dstCmd, "drop table " + srctable); + Helpers.DropTable(dstCmd, dstTable); + Helpers.DropTable(dstCmd, srctable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs index 4cffae2fb2..64f7bcc511 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs @@ -24,44 +24,45 @@ public void Test() Helpers.TryExecute(dstCmd, "create table " + dstTable + " (name_jp varchar(20) collate Japanese_CI_AS, " + "name_ru varchar(20) collate Cyrillic_General_CI_AS)"); - string s_jp = "江戸糸あやつり人形"; - string s_ru = "проверка"; + // NOTE: The table name embeds a GUID, so it must be dropped even when an assertion + // below fails, otherwise it is left in the shared test database forever. + try + { + string s_jp = "江戸糸あやつり人形"; + string s_ru = "проверка"; - DataTable table = new DataTable(); - table.Columns.Add("name_jp", typeof(string)); - table.Columns.Add("name_ru", typeof(string)); - DataRow row = table.NewRow(); - row["name_jp"] = s_jp; - row["name_ru"] = s_ru; - table.Rows.Add(row); + DataTable table = new DataTable(); + table.Columns.Add("name_jp", typeof(string)); + table.Columns.Add("name_ru", typeof(string)); + DataRow row = table.NewRow(); + row["name_jp"] = s_jp; + row["name_ru"] = s_ru; + table.Rows.Add(row); - using (SqlBulkCopy bcp = new SqlBulkCopy(dstConn)) - { - bcp.DestinationTableName = dstTable; - bcp.WriteToServer(table); - } + using (SqlBulkCopy bcp = new SqlBulkCopy(dstConn)) + { + bcp.DestinationTableName = dstTable; + bcp.WriteToServer(table); + } - using (SqlDataReader reader = (new SqlCommand("select * from " + dstTable, dstConn)).ExecuteReader()) - { - while (reader.Read()) + using (SqlDataReader reader = (new SqlCommand("select * from " + dstTable, dstConn)).ExecuteReader()) { - DataTestUtility.AssertEqualsWithDescription( - 0, string.CompareOrdinal(s_jp, reader["name_jp"] as string), - "Unexpected value: " + reader["name_jp"]); + while (reader.Read()) + { + DataTestUtility.AssertEqualsWithDescription( + 0, string.CompareOrdinal(s_jp, reader["name_jp"] as string), + "Unexpected value: " + reader["name_jp"]); - DataTestUtility.AssertEqualsWithDescription( - 0, string.CompareOrdinal(s_ru, reader["name_ru"] as string), - "Unexpected value: " + reader["name_ru"]); + DataTestUtility.AssertEqualsWithDescription( + 0, string.CompareOrdinal(s_ru, reader["name_ru"] as string), + "Unexpected value: " + reader["name_ru"]); + } } } - - } - - using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) - { - dstConn.Open(); - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + finally + { + Helpers.TryExecute(dstCmd, "drop table " + dstTable); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyVariants.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyVariants.cs index d690f849ca..423440a193 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyVariants.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyVariants.cs @@ -74,8 +74,8 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable + "_src"); - Helpers.TryExecute(dstCmd, "drop table " + dstTable + "_dst"); + Helpers.DropTable(dstCmd, dstTable + "_src"); + Helpers.DropTable(dstCmd, dstTable + "_dst"); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DataConversionErrorMessageTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DataConversionErrorMessageTest.cs index 11c4a262a1..1ffa69e5c4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DataConversionErrorMessageTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DataConversionErrorMessageTest.cs @@ -70,7 +70,9 @@ private void DropTable(SqlConnection sqlConnection, string targetTable) using (var command = new SqlCommand()) { command.Connection = sqlConnection; - command.CommandText = string.Format("DROP TABLE {0}", targetTable); + // NOTE: The drop is guarded so that cleanup is idempotent; the table name embeds a + // GUID, so an unguarded drop that throws would leave it behind forever. + command.CommandText = string.Format("IF (OBJECT_ID('{0}') IS NOT NULL) DROP TABLE {0}", targetTable); command.CommandType = CommandType.Text; command.ExecuteNonQuery(); } @@ -79,10 +81,19 @@ private void DropTable(SqlConnection sqlConnection, string targetTable) public void Dispose() { - DropTable(Connection, TableName); - - Connection.Close(); - Connection.Dispose(); + try + { + DropTable(Connection, TableName); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(InitialDatabase)}: failed to drop '{TableName}': {ex.Message}"); + } + finally + { + Connection.Close(); + Connection.Dispose(); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DestinationTableNameWithSpecialChar.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DestinationTableNameWithSpecialChar.cs index 230d450f80..014a983d95 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DestinationTableNameWithSpecialChar.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DestinationTableNameWithSpecialChar.cs @@ -70,7 +70,10 @@ public void Test() } finally { - Helpers.ProcessCommandBatch(typeof(SqlConnection), constr, prologue); + // NOTE: Each drop is run independently so that a failure to drop the source table + // does not leak the destination table (the names embed a GUID, so anything left + // behind stays in the shared test database forever). + Helpers.ProcessCleanupBatch(typeof(SqlConnection), constr, prologue); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/FireTrigger.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/FireTrigger.cs index 6353197634..1906c977fc 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/FireTrigger.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/FireTrigger.cs @@ -40,10 +40,13 @@ public void Test() using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - Helpers.ProcessCommandBatch(dstCmd, prologue); try { + // NOTE: Setup runs inside the try so that a partial failure (for example the + // trigger failing to create) still runs the epilogue and drops the tables. + Helpers.ProcessCommandBatch(dstCmd, prologue); + using (SqlConnection srcConn = new SqlConnection(srcConstr)) using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) { @@ -73,7 +76,9 @@ public void Test() } finally { - Helpers.ProcessCommandBatch(dstCmd, epilogue); + // NOTE: Each drop is run independently so that one failure does not leak the + // remaining objects (in particular the trigger). + Helpers.ProcessCleanupBatch(dstCmd, epilogue); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Helpers.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Helpers.cs index 274750bc09..0c197b355f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Helpers.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Helpers.cs @@ -34,16 +34,109 @@ internal static void ProcessCommandBatch(DbCommand cmd, string[] batch) } } + /// + /// Executes a batch of cleanup statements, running each one independently. + /// + /// + /// Unlike , a statement that fails does + /// not prevent the remaining statements from running. Cleanup batches typically remove several + /// objects (for example a table and the schema or trigger that depends on it), and aborting on + /// the first failure leaks everything that follows into the shared test database. + /// + internal static void ProcessCleanupBatch(DbCommand cmd, string[] batch) + { + foreach (string cmdtext in batch) + { + TryCleanup(cmd, cmdtext); + } + } + + /// + /// Executes a batch of cleanup statements on a new connection, running each one independently. + /// + internal static void ProcessCleanupBatch(Type connType, string constr, string[] batch) + { + if (batch.Length == 0) + { + return; + } + + try + { + using DbConnection conn = (DbConnection)Activator.CreateInstance(connType, new object[] { constr }); + conn.Open(); + + using DbCommand cmd = conn.CreateCommand(); + ProcessCleanupBatch(cmd, batch); + } + catch (Exception e) + { + Console.WriteLine($"Cleanup batch could not be run: {e.Message}"); + } + } + + /// + /// Executes a single cleanup statement, best-effort. + /// + internal static void TryCleanup(DbCommand cmd, string statement) + { + try + { + TryExecute(cmd, statement); + } + catch (Exception e) + { + Console.WriteLine($"Cleanup statement failed ({statement}): {e.Message}"); + } + } + + /// + /// Drops a table if it exists, best-effort. + /// + /// + /// Test table names embed a GUID, so anything that is not dropped stays in the shared test + /// database forever. The drop is therefore both guarded (so that dropping a table which was + /// never created is a no-op) and best-effort (so that one failure does not prevent subsequent + /// cleanup from running). + /// + public static void DropTable(DbCommand cmd, string tableName) => + TryCleanup(cmd, GetDropTableStatement(tableName)); + public static int TryDropTable(string dstConstr, string tableName) { using (SqlConnection dropConn = new SqlConnection(dstConstr)) using (SqlCommand dropCmd = dropConn.CreateCommand()) { dropConn.Open(); - return Helpers.TryExecute(dropCmd, "drop table " + tableName); + return Helpers.TryExecute(dropCmd, GetDropTableStatement(tableName)); + } + } + + /// + /// Drops the supplied tables if they exist, best-effort, on a new connection. + /// + public static void DropTables(string dstConstr, params string[] tableNames) + { + try + { + using SqlConnection dropConn = new SqlConnection(dstConstr); + dropConn.Open(); + + using SqlCommand dropCmd = dropConn.CreateCommand(); + foreach (string tableName in tableNames) + { + DropTable(dropCmd, tableName); + } + } + catch (Exception e) + { + Console.WriteLine($"Tables could not be dropped: {e.Message}"); } } + private static string GetDropTableStatement(string tableName) => + $"IF (OBJECT_ID('{tableName.Replace("'", "''")}') IS NOT NULL) DROP TABLE {tableName}"; + public static int TryExecute(DbCommand cmd, string strText) { cmd.CommandText = strText; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/KeepNulls.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/KeepNulls.cs index 5228c1a35d..de359bea7a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/KeepNulls.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/KeepNulls.cs @@ -22,21 +22,25 @@ public void Test() destConn.Open(); using SqlCommand dstcmd = destConn.CreateCommand(); - Helpers.TryExecute(dstcmd, "create table " + srctable + " (col1 int, col2 text, col3 text)"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col3) values (1, 'Michael')"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2, col3) values (2, 'Quark', 'Astrid')"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2) values (66, 'K�se');"); - Helpers.TryExecute(dstcmd, "create table " + dsttable + " (col1 int identity(1,1), col2 text default 'Jogurt', col3 text)"); + // NOTE: Setup runs inside the try so that a failure part way through (for example while + // creating the destination table) still drops the objects created before it. The table + // names embed a GUID, so anything left behind stays in the shared test database forever. + try + { + Helpers.TryExecute(dstcmd, "create table " + srctable + " (col1 int, col2 text, col3 text)"); + Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col3) values (1, 'Michael')"); + Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2, col3) values (2, 'Quark', 'Astrid')"); + Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2) values (66, 'K�se');"); - using SqlConnection sourceConn = new(srcconstr); - sourceConn.Open(); + Helpers.TryExecute(dstcmd, "create table " + dsttable + " (col1 int identity(1,1), col2 text default 'Jogurt', col3 text)"); - using SqlCommand srccmd = new("select * from " + srctable, sourceConn); - using IDataReader reader = srccmd.ExecuteReader(); + using SqlConnection sourceConn = new(srcconstr); + sourceConn.Open(); + + using SqlCommand srccmd = new("select * from " + srctable, sourceConn); + using IDataReader reader = srccmd.ExecuteReader(); - try - { using SqlBulkCopy bulkcopy = new(destConn, SqlBulkCopyOptions.KeepNulls, null); bulkcopy.DestinationTableName = dsttable; SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; @@ -49,8 +53,7 @@ public void Test() } finally { - Helpers.TryDropTable(dstconstr, srctable); - Helpers.TryDropTable(dstconstr, dsttable); + Helpers.DropTables(dstconstr, srctable, dsttable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs index 1c10feebad..28a62466b2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs @@ -56,8 +56,11 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); - Helpers.TryExecute(dstCmd, "drop schema " + dstschema); + // NOTE: Each statement is run independently. Previously a failed "drop table" + // (for example when the create failed) aborted the cleanup and leaked the + // schema, while also masking the original exception. + Helpers.TryCleanup(dstCmd, "drop table " + dstTable); + Helpers.TryCleanup(dstCmd, "drop schema " + dstschema); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TableLock.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TableLock.cs index 69c090d655..c90280d6d8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TableLock.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TableLock.cs @@ -22,20 +22,25 @@ public void Test() destConn.Open(); using SqlCommand dstcmd = destConn.CreateCommand(); - Helpers.TryExecute(dstcmd, "create table " + srctable + " (col1 int, col2 text, col3 text)"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col3) values (1, 'Michael')"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2, col3) values (2, 'Quark', 'Astrid')"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2) values (66, 'K�se');"); - Helpers.TryExecute(dstcmd, "create table " + dsttable + " (col1 int identity(1,1), col2 text default 'Jogurt', col3 text)"); - - using SqlConnection sourceConn = new(srcconstr); - sourceConn.Open(); - - using SqlCommand srccmd = new SqlCommand("select * from " + srctable, sourceConn); - using IDataReader reader = srccmd.ExecuteReader(); + // NOTE: Setup runs inside the try so that a failure part way through (for example while + // creating the destination table) still drops the objects created before it. The table + // names embed a GUID, so anything left behind stays in the shared test database forever. try { + Helpers.TryExecute(dstcmd, "create table " + srctable + " (col1 int, col2 text, col3 text)"); + Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col3) values (1, 'Michael')"); + Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2, col3) values (2, 'Quark', 'Astrid')"); + Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2) values (66, 'K�se');"); + + Helpers.TryExecute(dstcmd, "create table " + dsttable + " (col1 int identity(1,1), col2 text default 'Jogurt', col3 text)"); + + using SqlConnection sourceConn = new(srcconstr); + sourceConn.Open(); + + using SqlCommand srccmd = new SqlCommand("select * from " + srctable, sourceConn); + using IDataReader reader = srccmd.ExecuteReader(); + using SqlBulkCopy bulkcopy = new(destConn, SqlBulkCopyOptions.TableLock, null); bulkcopy.DestinationTableName = dsttable; SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; @@ -48,8 +53,7 @@ public void Test() } finally { - Helpers.TryDropTable(dstconstr, srctable); - Helpers.TryDropTable(dstconstr, dsttable); + Helpers.DropTables(dstconstr, srctable, dsttable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/UnprivilegedLogin.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/UnprivilegedLogin.cs index 7fd5b36237..99804a0ecb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/UnprivilegedLogin.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/UnprivilegedLogin.cs @@ -54,17 +54,28 @@ public UnprivilegedLogin() _managementConnection = new SqlConnection(DataTestUtility.TCPConnectionString); _managementConnection.Open(); - _unprivilegedLogin = new ServerLogin(_managementConnection, nameof(UnprivilegedLogin), _managementConnection.Database); - _unprivilegedAppUser = new DatabaseUser(_managementConnection, _managementConnection.Database, _unprivilegedLogin); - _unprivilegedMasterUser = new DatabaseUser(_managementConnection, "master", _unprivilegedLogin); - - using (SqlCommand permissionsModificationCommand = _managementConnection.CreateCommand()) + // NOTE: If setup fails part way through, this constructor never returns, so xUnit never calls + // Dispose and the login/users created so far would be leaked. A server login in particular + // is instance-wide, so it survives long after the test database is recreated. + try { - permissionsModificationCommand.CommandText = $"DENY SELECT ON [master].[sys].[all_columns] TO {_unprivilegedMasterUser.Name}"; - permissionsModificationCommand.ExecuteNonQuery(); + _unprivilegedLogin = new ServerLogin(_managementConnection, nameof(UnprivilegedLogin), _managementConnection.Database); + _unprivilegedAppUser = new DatabaseUser(_managementConnection, _managementConnection.Database, _unprivilegedLogin); + _unprivilegedMasterUser = new DatabaseUser(_managementConnection, "master", _unprivilegedLogin); + + using (SqlCommand permissionsModificationCommand = _managementConnection.CreateCommand()) + { + permissionsModificationCommand.CommandText = $"DENY SELECT ON [master].[sys].[all_columns] TO {_unprivilegedMasterUser.Name}"; + permissionsModificationCommand.ExecuteNonQuery(); - permissionsModificationCommand.CommandText = $"DENY SELECT ON [{_managementConnection.Database}].[sys].[all_columns] TO {_unprivilegedAppUser.Name}"; - permissionsModificationCommand.ExecuteNonQuery(); + permissionsModificationCommand.CommandText = $"DENY SELECT ON [{_managementConnection.Database}].[sys].[all_columns] TO {_unprivilegedAppUser.Name}"; + permissionsModificationCommand.ExecuteNonQuery(); + } + } + catch + { + Dispose(); + throw; } SqlConnectionStringBuilder tcpConnectionBuilder = new(DataTestUtility.TCPConnectionString) @@ -205,9 +216,24 @@ public void BulkCopyWithoutMetadataPermission_FailsWhenUsingAliases() public void Dispose() { - _unprivilegedAppUser?.Dispose(); - _unprivilegedMasterUser?.Dispose(); - _unprivilegedLogin?.Dispose(); + // Each drop is best-effort: a failure to remove one object must not prevent the others (in + // particular the instance-wide server login) from being removed. + DisposeSafely(_unprivilegedAppUser); + DisposeSafely(_unprivilegedMasterUser); + DisposeSafely(_unprivilegedLogin); + _managementConnection?.Dispose(); } + + private static void DisposeSafely(IDisposable? disposable) + { + try + { + disposable?.Dispose(); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(UnprivilegedLogin)}: cleanup failed: {ex.Message}"); + } + } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs index 828a87d63e..0dbb7cca8a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs @@ -64,7 +64,18 @@ public CertificateTestWithTdsServer() Directory.CreateDirectory(s_fullPathToClientCert); } - RunPowershellScript(s_fullPathToPowershellScript); + // NOTE: The setup script installs a certificate into the LocalMachine root store and writes + // the scratch files. If it fails part way through, xUnit never calls Dispose, so run the + // cleanup here to avoid leaving the certificate behind. + try + { + RunPowershellScript(s_fullPathToPowershellScript); + } + catch + { + Dispose(); + throw; + } } private static bool IsLocalHost() @@ -254,18 +265,43 @@ private X509Certificate2 GetEncryptionCertificate(string fileName, string? passw private void RemoveCertificate() { - string thumbprint = File.ReadAllText(s_fullPathTothumbprint); - using X509Store certStore = new(StoreName.Root, StoreLocation.LocalMachine); - certStore.Open(OpenFlags.ReadWrite); - X509Certificate2Collection certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); - if (certCollection.Count > 0) + // NOTE: Each step is independent. Previously a missing thumbprint file (or a failed store + // removal) aborted the whole cleanup, leaving the certificate in the LocalMachine root + // store and the scratch directory on disk. + try + { + if (File.Exists(s_fullPathTothumbprint)) + { + string thumbprint = File.ReadAllText(s_fullPathTothumbprint); + using X509Store certStore = new(StoreName.Root, StoreLocation.LocalMachine); + certStore.Open(OpenFlags.ReadWrite); + X509Certificate2Collection certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false); + if (certCollection.Count > 0) + { + certStore.Remove(certCollection[0]); + } + certStore.Close(); + } + } + catch (Exception ex) { - certStore.Remove(certCollection[0]); + Console.WriteLine($"{nameof(CertificateTestWithTdsServer)}: failed to remove certificate: {ex.Message}"); } - certStore.Close(); - File.Delete(s_fullPathTothumbprint); - Directory.Delete(s_fullPathToClientCert, true); + TryCleanup(() => File.Delete(s_fullPathTothumbprint)); + TryCleanup(() => Directory.Delete(s_fullPathToClientCert, true)); + } + + private static void TryCleanup(Action cleanupAction) + { + try + { + cleanupAction(); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(CertificateTestWithTdsServer)}: cleanup step failed: {ex.Message}"); + } } private static void RemoveForceEncryptionFromRegistryPath(string registryPath) @@ -292,8 +328,17 @@ protected virtual void Dispose(bool disposing) { if (disposing && !string.IsNullOrEmpty(s_fullPathTothumbprint)) { - RemoveCertificate(); - RemoveForceEncryptionFromRegistryPath(ForceEncryptionRegistryPath); + // NOTE: The registry/service reset must run even if certificate removal fails, + // otherwise the SQL Server instance is left with ForceEncryption enabled and a + // reference to a certificate that may no longer exist. + try + { + RemoveCertificate(); + } + finally + { + RemoveForceEncryptionFromRegistryPath(ForceEncryptionRegistryPath); + } } } else diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs index fbbee8db26..3b42d7b1c8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -24,10 +24,10 @@ public static class ConnectivityTest private static readonly string s_dbConnectionString = new SqlConnectionStringBuilder(s_connectionString) { InitialCatalog = s_databaseName }.ConnectionString; private static readonly string s_createDatabaseCmd = $"CREATE DATABASE {s_databaseName}"; private static readonly string s_createTableCmd = $"CREATE TABLE {s_tableName} (NAME NVARCHAR(40), AGE INT)"; - private static readonly string s_alterDatabaseSingleCmd = $"ALTER DATABASE {s_databaseName} SET SINGLE_USER WITH ROLLBACK IMMEDIATE;"; - private static readonly string s_alterDatabaseMultiCmd = $"ALTER DATABASE {s_databaseName} SET MULTI_USER WITH ROLLBACK IMMEDIATE;"; + private static readonly string s_alterDatabaseSingleCmd = $"IF (EXISTS(SELECT 1 FROM sys.databases WHERE name = '{s_databaseName}')) ALTER DATABASE {s_databaseName} SET SINGLE_USER WITH ROLLBACK IMMEDIATE;"; + private static readonly string s_alterDatabaseMultiCmd = $"IF (EXISTS(SELECT 1 FROM sys.databases WHERE name = '{s_databaseName}')) ALTER DATABASE {s_databaseName} SET MULTI_USER WITH ROLLBACK IMMEDIATE;"; private static readonly string s_selectTableCmd = $"SELECT COUNT(*) FROM {s_tableName}"; - private static readonly string s_dropDatabaseCmd = $"DROP DATABASE {s_databaseName}"; + private static readonly string s_dropDatabaseCmd = $"IF (EXISTS(SELECT 1 FROM sys.databases WHERE name = '{s_databaseName}')) DROP DATABASE {s_databaseName}"; // Synapse: Stored procedure sp_who2 does not exist or is not supported. // Synapse: SqlConnection.ServerProcessId is always retrieved as 0. @@ -263,8 +263,19 @@ public static void ConnectionKilledTest() } finally { - // Kill all the connections, set Database to SINGLE_USER Mode and drop Database - DataTestUtility.RunNonQuery(s_connectionString, s_alterDatabaseSingleCmd, 4); + // Kill all the connections, set Database to SINGLE_USER Mode and drop Database. + // NOTE: The database name embeds a GUID, so failing to drop it leaks a database that + // nothing will ever reclaim. Switching to SINGLE_USER is only an optimization to + // evict other sessions, so its failure must not prevent the drop from running. + try + { + DataTestUtility.RunNonQuery(s_connectionString, s_alterDatabaseSingleCmd, 4); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(ConnectionKilledTest)}: failed to set '{s_databaseName}' to SINGLE_USER: {ex.Message}"); + } + DataTestUtility.RunNonQuery(s_connectionString, s_dropDatabaseCmd, 4); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs index 4e5fa29c7d..629ce3785e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/JsonTest/JsonBulkCopyTest.cs @@ -15,7 +15,7 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SQL.JsonTest { [Trait("Set", "3")] - public class JsonBulkCopyTest + public class JsonBulkCopyTest : IDisposable { private readonly ITestOutputHelper _output; private static readonly string _generatedJsonFile = DataTestUtility.GetShortName("randomRecords"); @@ -28,6 +28,37 @@ public JsonBulkCopyTest(ITestOutputHelper output) _output = output; } + /// + /// Drops the tables and removes the scratch files created by the tests. + /// + /// + /// The table names embed a GUID, so without this the tests left two tables behind in the + /// shared test database on every run. + /// + public void Dispose() + { + try + { + if (DataTestUtility.AreConnStringsSetup() && DataTestUtility.IsJsonSupported) + { + using SqlConnection connection = new SqlConnection(DataTestUtility.TCPConnectionString); + connection.Open(); + + DataTestUtility.DropTable(connection, _sourceTableName); + DataTestUtility.DropTable(connection, _destinationTableName); + } + } + catch (Exception ex) + { + _output.WriteLine($"{nameof(JsonBulkCopyTest)}: failed to drop test tables: {ex.Message}"); + } + finally + { + DeleteFile(_generatedJsonFile); + DeleteFile(_outputFile); + } + } + public static IEnumerable JsonBulkCopyTestData() { yield return new object[] { CommandBehavior.Default, false, 30, 10 }; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs index 6f3f74fe4e..a51f64713d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs @@ -152,7 +152,8 @@ private static void DropTempTable(string connectionString, string tempTableName) using (SqlConnection con1 = new SqlConnection(connectionString)) { con1.Open(); - SqlCommand cmd = new SqlCommand("Drop table " + tempTableName, con1); + SqlCommand cmd = new SqlCommand( + string.Format("IF (OBJECT_ID('{0}') IS NOT NULL) DROP TABLE {0}", tempTableName), con1); cmd.ExecuteNonQuery(); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/TvpTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/TvpTest.cs index a8baee786c..2373171a04 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/TvpTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/TvpTest.cs @@ -295,26 +295,16 @@ private void TestTVPPermutations(SteStructuredTypeBoundaries bounds, bool runOnl continue; } - // Send list of SqlDataRecords as value - Console.WriteLine("------IEnumerable---------"); + // NOTE: The server objects created above (a table type and a stored procedure) must be + // dropped even when the body below throws. Their names are unique per permutation, + // so anything left behind stays in the shared test database forever. try { - param.Value = CreateListOfRecords(tvpPerm, baseValues); - ExecuteAndVerify(cmd, tvpPerm, baseValues, null); - } - catch (ArgumentException ae) - { - // some argument exceptions expected and should be swallowed - Console.WriteLine("Argument exception in value setup: {0}", ae.Message); - } - - if (!runOnlyDataRecordTest) - { - // send DbDataReader - Console.WriteLine("------DbDataReader---------"); + // Send list of SqlDataRecords as value + Console.WriteLine("------IEnumerable---------"); try { - param.Value = new TvpRestartableReader(CreateListOfRecords(tvpPerm, baseValues)); + param.Value = CreateListOfRecords(tvpPerm, baseValues); ExecuteAndVerify(cmd, tvpPerm, baseValues, null); } catch (ArgumentException ae) @@ -323,17 +313,35 @@ private void TestTVPPermutations(SteStructuredTypeBoundaries bounds, bool runOnl Console.WriteLine("Argument exception in value setup: {0}", ae.Message); } - // send datasets - Console.WriteLine("------DataTables---------"); - foreach (DataTable d in dtList) + if (!runOnlyDataRecordTest) { - param.Value = d; - ExecuteAndVerify(cmd, tvpPerm, null, d); + // send DbDataReader + Console.WriteLine("------DbDataReader---------"); + try + { + param.Value = new TvpRestartableReader(CreateListOfRecords(tvpPerm, baseValues)); + ExecuteAndVerify(cmd, tvpPerm, baseValues, null); + } + catch (ArgumentException ae) + { + // some argument exceptions expected and should be swallowed + Console.WriteLine("Argument exception in value setup: {0}", ae.Message); + } + + // send datasets + Console.WriteLine("------DataTables---------"); + foreach (DataTable d in dtList) + { + param.Value = d; + ExecuteAndVerify(cmd, tvpPerm, null, d); + } } } - - // And clean up - DropServerObjects(tvpPerm); + finally + { + // And clean up + DropServerObjects(tvpPerm); + } iter++; } @@ -892,11 +900,20 @@ private bool DoesRowMatchMetadata(object[] row, DataTable table) private void DropServerObjects(StePermutation tvpPerm) { - string dropText = "DROP PROC " + GetProcName(tvpPerm) + "; DROP TYPE " + GetTypeName(tvpPerm); using SqlConnection conn = new(_connStr); conn.Open(); - SqlCommand cmd = new(dropText, conn); + // NOTE: The procedure and the type are dropped by separate, individually guarded commands. + // Previously both drops shared a single batch, so when the procedure did not exist (the + // CREATE PROC step failed after CREATE TYPE succeeded) the batch aborted on the first + // statement and the table type was leaked into the shared test database. + DropServerObject(conn, "DROP PROC IF EXISTS " + GetProcName(tvpPerm)); + DropServerObject(conn, "DROP TYPE IF EXISTS " + GetTypeName(tvpPerm)); + } + + private static void DropServerObject(SqlConnection conn, string dropText) + { + using SqlCommand cmd = new(dropText, conn); try { cmd.ExecuteNonQuery(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs index 38295bd953..628ebfefe6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs @@ -198,18 +198,36 @@ private static void CreateTestUser(string username, string password) private static void DropTestUser(string username) { // Removes a created test user. - string dropUserCmd = $"IF EXISTS (SELECT * FROM sys.schemas WHERE name = '{username}') BEGIN DROP SCHEMA {username} END;" - + $"IF EXISTS (SELECT * FROM sys.database_principals WHERE type = 'S' AND name = '{username}') BEGIN DROP USER {username} END;" - + $"DROP LOGIN {username}"; + // NOTE: Each statement runs independently and is guarded so that a failure to drop the + // schema or database user does not leak the login, which is instance-wide and would + // otherwise survive well beyond the test run. + string[] dropStatements = + { + $"IF EXISTS (SELECT * FROM sys.schemas WHERE name = '{username}') BEGIN DROP SCHEMA {username} END;", + $"IF EXISTS (SELECT * FROM sys.database_principals WHERE type = 'S' AND name = '{username}') BEGIN DROP USER {username} END;", + $"IF EXISTS (SELECT * FROM sys.server_principals WHERE name = '{username}') BEGIN DROP LOGIN {username} END;", + }; // Pool must be cleared to prevent DROP LOGIN failure. SqlConnection.ClearAllPools(); using (var conn = new SqlConnection(DataTestUtility.TCPConnectionString)) - using (var cmd = new SqlCommand(dropUserCmd, conn)) + using (var cmd = new SqlCommand(string.Empty, conn)) { conn.Open(); - cmd.ExecuteNonQuery(); + + foreach (string statement in dropStatements) + { + try + { + cmd.CommandText = statement; + cmd.ExecuteNonQuery(); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(SqlCredentialTest)}: cleanup statement failed ({statement}): {ex.Message}"); + } + } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlNotificationTest/SqlNotificationTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlNotificationTest/SqlNotificationTest.cs index 3701b503a3..8447b17300 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlNotificationTest/SqlNotificationTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlNotificationTest/SqlNotificationTest.cs @@ -31,7 +31,19 @@ public SqlNotificationTest() _schemaQueue = $"[{_queueName}]"; - Setup(); + // NOTE: If setup fails part way through (for example the queue is created but the service + // is not), this constructor never returns, so xUnit never calls Dispose and the objects + // created so far would be leaked. Their names embed a GUID and xUnit creates one + // instance per test method, so those leaks accumulate quickly. + try + { + Setup(); + } + catch + { + Cleanup(); + throw; + } } public void Dispose() @@ -310,10 +322,13 @@ private static string[] CreateSqlSetupStatements(string tableName, string queueN private static string[] CreateSqlCleanupStatements(string tableName, string queueName, string serviceName) { + // NOTE: Every statement is guarded so that cleanup is idempotent and so that a failure to + // remove one object does not leave the others (in particular the Service Broker queue + // and service) behind in the shared test database. return new string[] { - string.Format("DROP TABLE {0}", tableName), - string.Format("DROP SERVICE [{0}]", serviceName), - string.Format("DROP QUEUE {0}", queueName) + string.Format("IF (OBJECT_ID('{0}') IS NOT NULL) DROP TABLE {0}", tableName), + string.Format("IF EXISTS (SELECT 1 FROM sys.services WHERE name = '{0}') DROP SERVICE [{0}]", serviceName), + string.Format("IF (OBJECT_ID('{0}') IS NOT NULL) DROP QUEUE {0}", queueName) }; } @@ -324,7 +339,19 @@ private void Setup() private void Cleanup() { - RunSQL(CreateSqlCleanupStatements(_tableName, _schemaQueue, _serviceName)); + // Each statement is executed independently: one failure must not prevent the remaining + // objects from being dropped. + foreach (string statement in CreateSqlCleanupStatements(_tableName, _schemaQueue, _serviceName)) + { + try + { + RunSQL(statement); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(SqlNotificationTest)}: cleanup statement failed ({statement}): {ex.Message}"); + } + } } private int RunSQL(params string[] stmts) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionTest.cs index 9aea760e56..91742465e3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionTest.cs @@ -174,11 +174,28 @@ private void DropTempTables() { using (var conn = new SqlConnection(_connectionString)) { - SqlCommand command = new SqlCommand( - string.Format("DROP TABLE [{0}]; DROP TABLE [{1}]", _tempTableName1, _tempTableName2), conn); conn.Open(); + + // NOTE: The drops are guarded and issued separately. Previously they shared a + // single unguarded batch, so a failure to drop the first table also leaked + // the second one. + DropTempTable(conn, _tempTableName1); + DropTempTable(conn, _tempTableName2); + } + } + + private static void DropTempTable(SqlConnection connection, string tableName) + { + try + { + using SqlCommand command = new SqlCommand( + string.Format("IF (OBJECT_ID('[{0}]') IS NOT NULL) DROP TABLE [{0}]", tableName), connection); command.ExecuteNonQuery(); } + catch (Exception ex) + { + Console.WriteLine($"TransactionTest: failed to drop table '{tableName}': {ex.Message}"); + } } public void ResetTables() diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorTestsBase.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorTestsBase.cs index 5c9970f6ed..a69a943e3d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorTestsBase.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/NativeVectorTestsBase.cs @@ -110,11 +110,16 @@ public NativeVectorTestsBase() _connectionString = DataTestUtility.TCPConnectionString; _managementConnection = new SqlConnection(_connectionString); - _vectorTable = new Table(_managementConnection, "VectorTestTable", tableDefinition); - _bulkCopySourceTable = new Table(_managementConnection, "VectorBulkCopyTestTable", tableDefinition); - _vectorProcedure = new StoredProcedure(_managementConnection, - prefix: "VectorsAsVarcharSp", - definition: $@" + + // NOTE: If this constructor throws, xUnit never calls Dispose, so any object already created + // (each of which has a GUID-based name) would be left in the database permanently. + try + { + _vectorTable = new Table(_managementConnection, "VectorTestTable", tableDefinition); + _bulkCopySourceTable = new Table(_managementConnection, "VectorBulkCopyTestTable", tableDefinition); + _vectorProcedure = new StoredProcedure(_managementConnection, + prefix: "VectorsAsVarcharSp", + definition: $@" {VectorParameterName} vector({vectorDimensions}, {TestDataInstance.SqlServerTypeName}), -- Input: Serialized TElement[] as JSON string {VectorOutputParameterName} vector({vectorDimensions}, {TestDataInstance.SqlServerTypeName}) OUTPUT -- Output: Echoed back from latest inserted row AS @@ -130,6 +135,12 @@ public NativeVectorTestsBase() FROM {_vectorTable.Name} ORDER BY Id DESC; END;"); + } + catch + { + Dispose(); + throw; + } _selectCommand = $"SELECT {VectorColumnName} FROM {_vectorTable.Name} ORDER BY Id DESC"; _insertCommand = $"INSERT INTO {_vectorTable.Name} ({VectorColumnName}) VALUES ({VectorParameterName})"; @@ -150,15 +161,28 @@ protected virtual void Dispose(bool disposing) if (disposing) { - _vectorProcedure?.Dispose(); - _bulkCopySourceTable?.Dispose(); - _vectorTable?.Dispose(); + // Each drop is best-effort: failing to drop one object must not leak the others. + DisposeSafely(_vectorProcedure); + DisposeSafely(_bulkCopySourceTable); + DisposeSafely(_vectorTable); _managementConnection?.Dispose(); } _disposed = true; } + private static void DisposeSafely(IDisposable? disposable) + { + try + { + disposable?.Dispose(); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(NativeVectorTestsBase)}: cleanup failed: {ex.Message}"); + } + } + ~NativeVectorTestsBase() => Dispose(false); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs index 1b2d4c792f..26817381d3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs @@ -49,13 +49,17 @@ protected VectorBackwardCompatTestBase( _connection = new SqlConnection(s_connectionString); _connection.Open(); - _vectorTable = new Table(_connection, namePrefix + "TestTable", - $"(Id INT PRIMARY KEY IDENTITY, VectorData {columnDefinition} NULL)"); + // NOTE: If this constructor throws, xUnit never calls Dispose, so the objects created so far + // (each with a GUID-based name) would be left in the database permanently. + try + { + _vectorTable = new Table(_connection, namePrefix + "TestTable", + $"(Id INT PRIMARY KEY IDENTITY, VectorData {columnDefinition} NULL)"); - _bulkCopySrcTable = new Table(_connection, namePrefix + "BulkCopyTestTable", - "(Id INT PRIMARY KEY IDENTITY, VectorData varchar(max) NULL)"); + _bulkCopySrcTable = new Table(_connection, namePrefix + "BulkCopyTestTable", + "(Id INT PRIMARY KEY IDENTITY, VectorData varchar(max) NULL)"); - string storedProcBody = $@" + string storedProcBody = $@" @InputVectorJson VARCHAR(MAX), -- Input: Serialized float[] as JSON string @OutputVectorJson VARCHAR(MAX) OUTPUT -- Output: Echoed back from latest inserted row AS @@ -72,7 +76,13 @@ @OutputVectorJson VARCHAR(MAX) OUTPUT -- Output: Echoed back from latest insert ORDER BY Id DESC; END;"; - _storedProc = new StoredProcedure(_connection, namePrefix + "AsVarcharSp", storedProcBody); + _storedProc = new StoredProcedure(_connection, namePrefix + "AsVarcharSp", storedProcBody); + } + catch + { + Dispose(); + throw; + } _selectCmdString = $"SELECT VectorData FROM {_vectorTable.Name} ORDER BY Id DESC"; _insertCmdString = $"INSERT INTO {_vectorTable.Name} (VectorData) VALUES (@VectorData)"; @@ -80,13 +90,26 @@ @OutputVectorJson VARCHAR(MAX) OUTPUT -- Output: Echoed back from latest insert public void Dispose() { - // RAII objects drop themselves on Dispose in reverse order. - _storedProc?.Dispose(); - _bulkCopySrcTable?.Dispose(); - _vectorTable?.Dispose(); + // RAII objects drop themselves on Dispose in reverse order. Each drop is best-effort so + // that one failure cannot leak the remaining objects. + DisposeSafely(_storedProc); + DisposeSafely(_bulkCopySrcTable); + DisposeSafely(_vectorTable); _connection?.Dispose(); } + private static void DisposeSafely(IDisposable disposable) + { + try + { + disposable?.Dispose(); + } + catch (Exception ex) + { + Console.WriteLine($"{nameof(VectorBackwardCompatTestBase)}: cleanup failed: {ex.Message}"); + } + } + #region Shared Helpers /// diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Fixtures/AlwaysEncrypted/NativeColumnEncryptionKeyCertificateBaselineFixture.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Fixtures/AlwaysEncrypted/NativeColumnEncryptionKeyCertificateBaselineFixture.cs index d0cfc2a842..8994316343 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Fixtures/AlwaysEncrypted/NativeColumnEncryptionKeyCertificateBaselineFixture.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Fixtures/AlwaysEncrypted/NativeColumnEncryptionKeyCertificateBaselineFixture.cs @@ -51,15 +51,28 @@ public NativeColumnEncryptionKeyCertificateBaselineFixture() : base() { byte[] nativeCertificateBaseline = Resources.AlwaysEncrypted_NativeColumnEncryptionKeyBaseline_Certificate; + + // NOTE: The certificate is deliberately not disposed here. It is owned by the base fixture, which + // removes it from the store on cleanup - and a disposed certificate cannot be matched against a + // store, so disposing it early left it behind in the current user's store permanently. #if NET9_0_OR_GREATER - using X509Certificate2 certificate = X509CertificateLoader.LoadPkcs12(nativeCertificateBaseline, NativeCertificatePassword, + X509Certificate2 certificate = X509CertificateLoader.LoadPkcs12(nativeCertificateBaseline, NativeCertificatePassword, keyStorageFlags: X509KeyStorageFlags.PersistKeySet); #else - using X509Certificate2 certificate = new(nativeCertificateBaseline, NativeCertificatePassword, + X509Certificate2 certificate = new(nativeCertificateBaseline, NativeCertificatePassword, X509KeyStorageFlags.PersistKeySet); #endif - Thumbprint = certificate.Thumbprint; - AddToStore(certificate, StoreLocation.CurrentUser, StoreName.My); + try + { + Thumbprint = certificate.Thumbprint; + AddToStore(certificate, StoreLocation.CurrentUser, StoreName.My); + } + catch + { + Dispose(); + certificate.Dispose(); + throw; + } } } From c7ec9ab66873218eeab858809347b649975931f3 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 25 Aug 2026 11:36:05 -0700 Subject: [PATCH 02/16] Address review feedback on cleanup guards Schema-qualify the Always Encrypted Table.Drop guard and DROP to [dbo] to match the CREATE TABLE statements in the derived classes. An unqualified name resolves against the connection's default schema, so if that is not dbo the OBJECT_ID guard would return NULL and silently skip the drop. Replace the remaining unguarded "drop table" cleanup statements in the BulkCopy suite with Helpers.DropTable. TryExecute can throw from a finally block, which both masks the original test failure and leaks the table; DropTable is guarded and best-effort. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../AlwaysEncrypted/TestFixtures/Setup/Table.cs | 8 ++++++-- .../tests/ManualTests/BulkCopy/Bug903514.cs | 8 +------- .../tests/ManualTests/BulkCopy/CacheMetadata.cs | 16 ++++++++-------- .../ManualTests/BulkCopy/ColumnCollation.cs | 2 +- .../ManualTests/BulkCopy/CopyAllFromReader.cs | 2 +- .../ManualTests/BulkCopy/CopyAllFromReader1.cs | 2 +- .../BulkCopy/CopyAllFromReaderAsync.cs | 2 +- .../BulkCopy/CopyAllFromReaderCancelAsync.cs | 2 +- .../ManualTests/BulkCopy/CopyMultipleReaders.cs | 2 +- .../BulkCopy/CopySomeFromDatatable.cs | 2 +- .../BulkCopy/CopySomeFromDatatableAsync.cs | 2 +- .../ManualTests/BulkCopy/CopySomeFromReader.cs | 2 +- .../ManualTests/BulkCopy/CopySomeFromRowArray.cs | 2 +- .../BulkCopy/CopySomeFromRowArrayAsync.cs | 2 +- .../tests/ManualTests/BulkCopy/CopyWithEvent.cs | 2 +- .../tests/ManualTests/BulkCopy/CopyWithEvent1.cs | 2 +- .../ManualTests/BulkCopy/CopyWithEventAsync.cs | 2 +- .../ManualTests/BulkCopy/MissingTargetColumn.cs | 2 +- .../ManualTests/BulkCopy/MissingTargetColumns.cs | 2 +- .../tests/ManualTests/BulkCopy/Transaction.cs | 2 +- .../tests/ManualTests/BulkCopy/Transaction1.cs | 2 +- .../tests/ManualTests/BulkCopy/Transaction2.cs | 2 +- .../tests/ManualTests/BulkCopy/Transaction3.cs | 2 +- .../tests/ManualTests/BulkCopy/Transaction4.cs | 2 +- .../ManualTests/BulkCopy/TransactionTestAsync.cs | 2 +- 25 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs index a037087d64..35ab7f6486 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs @@ -15,12 +15,16 @@ public override void Drop(SqlConnection sqlConnection) // NOTE: The drop is guarded so that cleanup is idempotent. An unguarded DROP throws when // the object was never created (for example when setup failed part way through), which // would abort the enclosing drop loop and leak every remaining object. - string sql = $"IF (OBJECT_ID(@name) IS NOT NULL) DROP TABLE [{Name}];"; + // NOTE: Both the lookup and the DROP are schema-qualified to [dbo] to match the CREATE + // TABLE statements in the derived classes. An unqualified name resolves against the + // connection's default schema, so if that is not dbo the guard would return NULL and + // silently skip the drop, leaking the table. + string sql = $"IF (OBJECT_ID(@name) IS NOT NULL) DROP TABLE [dbo].[{Name}];"; using (SqlCommand command = sqlConnection.CreateCommand()) { command.CommandText = sql; - command.Parameters.AddWithValue("@name", $"[{Name}]"); + command.Parameters.AddWithValue("@name", $"[dbo].[{Name}]"); command.ExecuteNonQuery(); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs index f8fa2252a0..29df411186 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs @@ -34,13 +34,7 @@ public void Test() } finally { - using (SqlConnection dstConn = new SqlConnection(constr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) - { - dstConn.Open(); - - Helpers.TryExecute(dstCmd, "drop table " + dstTable); - } + Helpers.DropTables(constr, dstTable); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs index ce707e9408..c296b99969 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs @@ -70,7 +70,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } @@ -127,7 +127,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } @@ -241,7 +241,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } @@ -288,7 +288,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } @@ -364,7 +364,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } @@ -428,7 +428,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } @@ -500,7 +500,7 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } @@ -554,7 +554,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs index 64f7bcc511..68cdc097cb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs @@ -61,7 +61,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs index 7c82e6dd21..e12f0f131f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs @@ -78,7 +78,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs index 78bc18f17a..bd7a925e22 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs @@ -51,7 +51,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs index d9cc530472..e2c26a9f0f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs @@ -66,7 +66,7 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs index 4329f7f5f4..45f009aae0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs @@ -59,7 +59,7 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs index 043c0f75f5..7eabde9fa3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs @@ -61,7 +61,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs index 324ddad5c8..a9f09bfc81 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs @@ -73,7 +73,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs index 7a5eb6d8e7..fa4a3be07f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs @@ -80,7 +80,7 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs index 9d52892baf..fd29ce5488 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs @@ -53,7 +53,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs index be48bf4351..94ccc46a41 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs @@ -64,7 +64,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs index b74144fc77..fa6b594137 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs @@ -78,7 +78,7 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs index fedfde00b9..77a00f7dc3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs @@ -78,7 +78,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs index 4388594033..d6026ef135 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs @@ -78,7 +78,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs index f552b0610b..45068cda95 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs @@ -92,7 +92,7 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs index ac5651f34f..87e9d909e5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs @@ -51,7 +51,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs index 2728b1efb4..2f13d8947c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs @@ -51,7 +51,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs index b5f5e32b53..f8746382dd 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs @@ -52,7 +52,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs index 665b5a5108..655e336b6c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs @@ -56,7 +56,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs index 9c3f69e28a..36dcb917cd 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs @@ -62,7 +62,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs index f28aaad8c3..5ba310c502 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs @@ -57,7 +57,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs index 2c48368483..48a76a5dcf 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs @@ -44,7 +44,7 @@ public void Test() } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs index 537f68d8f0..af88310e9f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs @@ -59,7 +59,7 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d } finally { - Helpers.TryExecute(dstCmd, "drop table " + dstTable); + Helpers.DropTable(dstCmd, dstTable); } } } From f1f7f4a24007fa353ba4d74c388221a04c1182bb Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 25 Aug 2026 12:05:26 -0700 Subject: [PATCH 03/16] Harden DatabaseObject cleanup against ambiguous-completion and broken connections Captures review feedback raised on #4594 against TvpQueryHintsFixture. Both findings apply verbatim to main's copy of that fixture, and to every other consumer of the shared DatabaseObject fixture base, so they are fixed at the base rather than in one test class: - A CREATE that fails *after* the server committed it (command timeout, dropped connection) left the object behind. Creation failure now makes a best-effort drop before rethrowing the original exception. - A DROP that failed left the object orphaned forever, because names embed a GUID and the connection was disposed immediately afterwards. Dispose now retries once on a reconnected connection, rethrowing the original exception only if the retry also fails. The retry closes and reopens the existing SqlConnection rather than building a new one from its connection string: Persist Security Info defaults to false, so the password is no longer readable once the connection has been opened. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../DatabaseObjects/DatabaseObject.cs | 88 ++++++++++++++++++- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs index a74da79697..49557803d7 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs @@ -38,7 +38,21 @@ protected DatabaseObject(SqlConnection connection, string name, string definitio { EnsureConnectionOpen(); DropObject(); - CreateObject(definition); + + try + { + CreateObject(definition); + } + catch + { + // CREATE can fail *after* the server has created the object: a command timeout or a + // dropped connection reports failure for a statement that may already have committed. + // Every generated name embeds a GUID, so anything left behind is orphaned forever in + // the (shared) test database. Make a best-effort attempt to remove it, but let the + // original failure surface. + TryDropBestEffort(); + throw; + } } } @@ -251,7 +265,9 @@ public static string GenerateShortName(string prefix, bool escape = true) /// /// /// By the time this is called, will be open. - /// Must not throw an exception if the object does not exist. + /// Must not throw an exception if the object does not exist, and must be safe to call more than + /// once: a failed drop is retried on a fresh connection, and a failed create attempts a drop to + /// avoid leaking an object whose creation may nonetheless have committed on the server. /// protected abstract void DropObject(); @@ -259,14 +275,78 @@ public void Dispose() { if (_shouldDrop) { - EnsureConnectionOpen(); - DropObject(); + try + { + EnsureConnectionOpen(); + DropObject(); + } + catch + { + // The drop is all that stands between a failed run and an object orphaned forever + // in the shared test database, so it gets one retry on a healthy connection. A bare + // `throw` preserves the original exception (and its stack) if that retry also fails. + if (!TryDropAfterReconnect()) + { + throw; + } + } } // This explicitly does not drop the wrapped SqlConnection; this is sometimes // used in a loop to create multiple UDTs. GC.SuppressFinalize(this); } + + /// + /// Drops the object, swallowing any failure. + /// + /// + /// Only for use on paths that are already unwinding because of a more interesting failure, + /// where a cleanup error must not replace the exception in flight. + /// + private void TryDropBestEffort() + { + try + { + EnsureConnectionOpen(); + DropObject(); + } + catch + { + TryDropAfterReconnect(); + } + } + + /// + /// Re-attempts the drop on a healthy connection. + /// + /// true if the object was dropped; otherwise false. + /// + /// A drop usually fails because itself has gone bad: a command timeout + /// can leave it unusable, and a transport failure kills it outright. Closing and reopening + /// returns the broken connection to the pool and acquires a healthy one, which is the difference + /// between a transient blip and an object orphaned forever in the shared test database. + /// + /// This deliberately reuses the existing rather than constructing a + /// new one from its connection string: `Persist Security Info` defaults to false, so the password + /// is no longer readable from once it has been + /// opened, and a copy would fail to authenticate. + /// + private bool TryDropAfterReconnect() + { + try + { + Connection.Close(); + Connection.Open(); + DropObject(); + + return true; + } + catch + { + return false; + } + } } /// From 42d41f89d915e704b5d257e90b595ea00dab5f51 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 25 Aug 2026 12:13:49 -0700 Subject: [PATCH 04/16] Move connection Open() inside constructor cleanup guards Addresses two suppressed findings from Copilot's re-review. In both VectorBackwardCompatTestBase and UnprivilegedLogin the SqlConnection was opened before the constructor's try/catch, so a failure in Open() escaped the ctor with the connection instance already allocated. xUnit does not call Dispose when a constructor throws, so nothing would ever dispose it -- the exact leak the surrounding guard exists to prevent. Swept the rest of the changed files for the same shape; the only other matches open their connections inside the try on `using` scopes already. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../tests/ManualTests/BulkCopy/UnprivilegedLogin.cs | 9 ++++++--- .../SQL/VectorTest/VectorBackwardCompatTestBase.cs | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/UnprivilegedLogin.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/UnprivilegedLogin.cs index 99804a0ecb..f78ea7334d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/UnprivilegedLogin.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/UnprivilegedLogin.cs @@ -52,13 +52,16 @@ public UnprivilegedLogin() // the actual tests. The user associated with the latter connection will be denied SELECT permissions // over master.sys.all_columns. _managementConnection = new SqlConnection(DataTestUtility.TCPConnectionString); - _managementConnection.Open(); // NOTE: If setup fails part way through, this constructor never returns, so xUnit never calls - // Dispose and the login/users created so far would be leaked. A server login in particular - // is instance-wide, so it survives long after the test database is recreated. + // Dispose and the connection plus the login/users created so far would be leaked. A server + // login in particular is instance-wide, so it survives long after the test database is + // recreated. Opening the connection is inside the try for the same reason: a failed Open + // still leaves a SqlConnection instance that nothing else will ever dispose. try { + _managementConnection.Open(); + _unprivilegedLogin = new ServerLogin(_managementConnection, nameof(UnprivilegedLogin), _managementConnection.Database); _unprivilegedAppUser = new DatabaseUser(_managementConnection, _managementConnection.Database, _unprivilegedLogin); _unprivilegedMasterUser = new DatabaseUser(_managementConnection, "master", _unprivilegedLogin); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs index 26817381d3..7cf2f19339 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs @@ -47,12 +47,15 @@ protected VectorBackwardCompatTestBase( { Output = output; _connection = new SqlConnection(s_connectionString); - _connection.Open(); - // NOTE: If this constructor throws, xUnit never calls Dispose, so the objects created so far - // (each with a GUID-based name) would be left in the database permanently. + // NOTE: If this constructor throws, xUnit never calls Dispose, so the connection and the + // objects created so far (each with a GUID-based name) would be left behind permanently. + // Opening the connection is inside the try for the same reason: a failed Open still + // leaves a SqlConnection instance that nothing else will ever dispose. try { + _connection.Open(); + _vectorTable = new Table(_connection, namePrefix + "TestTable", $"(Id INT PRIMARY KEY IDENTITY, VectorData {columnDefinition} NULL)"); From e91695e477f4900296064ec70ff81b4627015412 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 25 Aug 2026 12:20:09 -0700 Subject: [PATCH 05/16] Annotate VectorBackwardCompatTestBase.DisposeSafely parameter as nullable Addresses a suppressed finding from Copilot's re-review: DisposeSafely accepted nulls at runtime via `disposable?.Dispose()` while declaring its parameter non-nullable. The nullability is not incidental -- on the constructor-failure path Dispose runs with some fields still null, so accepting null is the contract. The suggestion as written does not compile: this file is not in a nullable annotations context, so a bare `IDisposable?` raises CS8632, which is an error in this project. Scoped the annotation with `#nullable enable`/`restore` around the method instead, which honours the intent without enabling nullable across a file that is not ready for it. This matches the sibling helpers in NativeVectorTestsBase and UnprivilegedLogin, which are already annotated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../SQL/VectorTest/VectorBackwardCompatTestBase.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs index 7cf2f19339..03298d3e37 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/VectorTest/VectorBackwardCompatTestBase.cs @@ -101,7 +101,13 @@ public void Dispose() _connection?.Dispose(); } - private static void DisposeSafely(IDisposable disposable) + // NOTE: This file is not in a nullable annotations context, so the parameter is annotated + // under a scoped `#nullable enable`. The nullability is not incidental: on the + // constructor-failure path Dispose runs with some of these fields still null, so accepting + // null is the contract rather than a defensive afterthought. +#nullable enable + private static void DisposeSafely(IDisposable? disposable) +#nullable restore { try { From b87a188acf79dc6e5d4e1898330dc54341059500 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 25 Aug 2026 14:08:20 -0700 Subject: [PATCH 06/16] Consolidate BulkCopy tests onto shared DatabaseObject RAII types Addresses review feedback that the PR added a fourth independent set of object create/delete helpers instead of reusing the existing RAII types. - Convert all BulkCopy tests from manual create + try/finally drop to 'using Table' / 'using Schema' declarations, so cleanup is scope-based and drop ordering falls out of reverse declaration order. - Add Table.WithName / Table.AdoptExisting and a new Schema object, so tests whose names are themselves under test (special characters) or which must address one table over several connections can use the shared types too. - Delete the now-dead drop helpers from BulkCopy/Helpers.cs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../Common/Fixtures/DatabaseObjects/Schema.cs | 74 +++ .../Common/Fixtures/DatabaseObjects/Table.cs | 34 ++ .../tests/ManualTests/BulkCopy/Bug84548.cs | 97 ++-- .../tests/ManualTests/BulkCopy/Bug85007.cs | 143 +++-- .../tests/ManualTests/BulkCopy/Bug903514.cs | 26 +- .../tests/ManualTests/BulkCopy/Bug98182.cs | 55 +- .../ManualTests/BulkCopy/CacheMetadata.cs | 528 ++++++++---------- .../ManualTests/BulkCopy/CheckConstraints.cs | 61 +- .../ManualTests/BulkCopy/ColumnCollation.cs | 64 +-- .../ManualTests/BulkCopy/CopyAllFromReader.cs | 82 ++- .../BulkCopy/CopyAllFromReader1.cs | 43 +- .../BulkCopy/CopyAllFromReaderAsync.cs | 42 +- .../BulkCopy/CopyAllFromReaderCancelAsync.cs | 36 +- .../CopyAllFromReaderConnectionCloseAsync.cs | 40 +- ...llFromReaderConnectionCloseOnEventAsync.cs | 58 +- .../BulkCopy/CopyMultipleReaders.cs | 61 +- .../BulkCopy/CopySomeFromDatatable.cs | 76 ++- .../BulkCopy/CopySomeFromDatatableAsync.cs | 72 ++- .../BulkCopy/CopySomeFromReader.cs | 43 +- .../BulkCopy/CopySomeFromRowArray.cs | 57 +- .../BulkCopy/CopySomeFromRowArrayAsync.cs | 63 +-- .../ManualTests/BulkCopy/CopyVariants.cs | 95 ++-- .../ManualTests/BulkCopy/CopyWithEvent.cs | 65 +-- .../ManualTests/BulkCopy/CopyWithEvent1.cs | 51 +- .../BulkCopy/CopyWithEventAsync.cs | 71 ++- .../DestinationTableNameWithSpecialChar.cs | 56 +- .../tests/ManualTests/BulkCopy/FireTrigger.cs | 72 +-- .../tests/ManualTests/BulkCopy/Helpers.cs | 129 +---- .../BulkCopy/InvalidAccessFromEvent.cs | 49 +- .../tests/ManualTests/BulkCopy/KeepNulls.cs | 59 +- .../BulkCopy/MissingTargetColumn.cs | 41 +- .../BulkCopy/MissingTargetColumns.cs | 41 +- .../BulkCopy/SpecialCharacterNames.cs | 38 +- .../tests/ManualTests/BulkCopy/TableLock.cs | 59 +- .../tests/ManualTests/BulkCopy/Transaction.cs | 43 +- .../ManualTests/BulkCopy/Transaction1.cs | 49 +- .../ManualTests/BulkCopy/Transaction2.cs | 59 +- .../ManualTests/BulkCopy/Transaction3.cs | 49 +- .../ManualTests/BulkCopy/Transaction4.cs | 35 +- .../BulkCopy/TransactionTestAsync.cs | 47 +- 40 files changed, 1227 insertions(+), 1636 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Schema.cs diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Schema.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Schema.cs new file mode 100644 index 0000000000..8ccef04bcd --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Schema.cs @@ -0,0 +1,74 @@ +// 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; + +/// +/// A transient schema, created at the start of its scope and dropped when disposed. +/// +/// +/// A schema can only be dropped once it is empty, so any object created inside it must be +/// declared after the schema and therefore disposed before it. +/// +public sealed class Schema : DatabaseObject +{ + /// + /// Initializes a new instance of the Schema class using the specified SQL connection and name prefix. + /// + /// The SQL connection used to interact with the database. + /// The prefix for the schema name. + public Schema(SqlConnection connection, string prefix) + : base(connection, GenerateLongName(prefix), definition: string.Empty, shouldCreate: true, shouldDrop: true) + { + } + + /// + /// Distinguishes the verbatim-name constructor from the prefix-based one, which would + /// otherwise have an identical signature. + /// + private enum NameIsVerbatim + { + Yes + } + + private Schema(SqlConnection connection, string name, NameIsVerbatim _) + : base(connection, name, definition: string.Empty, shouldCreate: true, shouldDrop: true) + { + } + + /// + /// Creates a schema using the caller-supplied name verbatim, instead of generating one. + /// + /// + /// Prefer the prefix-based constructor. This overload exists for tests in which the name + /// itself is under test, for example one containing special characters. + /// + /// The SQL connection used to interact with the database. + /// The schema name, already quoted/escaped by the caller if it needs to be. + public static Schema WithName(SqlConnection connection, string name) + => new(connection, name, NameIsVerbatim.Yes); + + protected override void CreateObject(string definition) + { + // NOTE: CREATE SCHEMA must be the first statement in its batch, so it cannot be guarded + // by an IF the way the other objects are. The base class drops before creating, which + // covers the (vanishingly unlikely) case of a name collision. + using SqlCommand createCommand = new($"CREATE SCHEMA {Name}", Connection); + + createCommand.ExecuteNonQuery(); + } + + protected override void DropObject() + { + // NOTE: The name is passed to SCHEMA_ID() as a parameter rather than being interpolated + // into a string literal, because it may embed Environment.UserName/MachineName (see + // DatabaseObject.GenerateLongName) and an apostrophe in either would break the batch. + // The identifier in DROP SCHEMA is already bracket-quoted. + using SqlCommand dropCommand = new($"IF (SCHEMA_ID(@name) IS NOT NULL) DROP SCHEMA {Name}", Connection); + + dropCommand.Parameters.AddWithValue("@name", UnescapedName); + + dropCommand.ExecuteNonQuery(); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs index 42d07a8fba..1ea15d8a24 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs @@ -25,6 +25,40 @@ public Table(SqlConnection connection, string prefix, string definition) { } + private Table(SqlConnection connection, string name, string definition, bool shouldCreate) + : base(connection, name, definition, shouldCreate, shouldDrop: true) + { + } + + /// + /// Creates a table using the caller-supplied name verbatim, instead of generating one. + /// + /// + /// Prefer the prefix-based constructor: generated names embed a GUID and so cannot collide + /// between concurrent test runs against a shared database. This overload exists for the + /// minority of tests that must control the name exactly - either because the name itself is + /// under test (for example, one containing special characters), or because the same table has + /// to be addressed through several different connections. + /// + /// The SQL connection used to interact with the database. + /// The table name, already quoted/escaped by the caller if it needs to be. + /// The SQL definition describing the structure of the table, including columns and data types. + public static Table WithName(SqlConnection connection, string name, string definition) + => new(connection, name, definition, shouldCreate: true); + + /// + /// Adopts an already-existing table so that it is dropped when the returned instance is + /// disposed. No table is created. + /// + /// + /// Useful when a table is created by other means (for example, by a helper that also populates + /// it, or over a different connection) but still needs deterministic cleanup. + /// + /// The SQL connection used to drop the table. + /// The table name, already quoted/escaped by the caller if it needs to be. + public static Table AdoptExisting(SqlConnection connection, string name) + => new(connection, name, definition: string.Empty, shouldCreate: false); + protected override void CreateObject(string definition) { using SqlCommand createCommand = new($"CREATE TABLE {Name} {definition}", Connection); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs index 349913171b..4e387ce653 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs @@ -4,6 +4,7 @@ using System.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,73 +17,57 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string targettable = DataTestUtility.GetShortName("SqlBulkCopyTest_Bug84548", false); - string targetCustomerTable = targettable + "_customer"; - using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) - { - dstConn.Open(); - try - { - Helpers.TryExecute(dstCmd, "CREATE TABLE [" + targetCustomerTable + "] ([CustomerID] [nchar] (5) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, CONSTRAINT [PK_" + targetCustomerTable + "] PRIMARY KEY CLUSTERED (CustomerID) ON [PRIMARY]) ON [PRIMARY]"); + using SqlConnection dstConn = new SqlConnection(dstConstr); + dstConn.Open(); - Helpers.TryExecute(dstCmd, - "CREATE TABLE [" + targettable + "] ([OrderID] [int] NOT NULL , " + - " [CustomerID] [nchar] (5) COLLATE SQL_Latin1_General_CP1_CI_AS NULL , " + - " CONSTRAINT [PK_" + targettable + "] PRIMARY KEY CLUSTERED " + - " (" + - " [OrderID]" + - " ) ON [PRIMARY] ," + - " CONSTRAINT [FK_" + targettable + "_Customers] FOREIGN KEY " + - " (" + - " [CustomerID]" + - " ) REFERENCES [" + targetCustomerTable + "] (" + - " [CustomerID]" + - " )" + - ") ON [PRIMARY]"); + // The order table takes a foreign key on the customer table, so it must be dropped first. + // Disposal runs in reverse declaration order, which gives that for free. + using Table customerTable = new Table(dstConn, "SqlBulkCopyTest_Bug84548_customer", + "([CustomerID] [nchar] (5) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL," + + " PRIMARY KEY CLUSTERED (CustomerID) ON [PRIMARY]) ON [PRIMARY]"); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - { - srcConn.Open(); + using Table orderTable = new Table(dstConn, "SqlBulkCopyTest_Bug84548", + "([OrderID] [int] NOT NULL," + + " [CustomerID] [nchar] (5) COLLATE SQL_Latin1_General_CP1_CI_AS NULL," + + " PRIMARY KEY CLUSTERED ([OrderID]) ON [PRIMARY]," + + $" FOREIGN KEY ([CustomerID]) REFERENCES {customerTable.Name} ([CustomerID])" + + ") ON [PRIMARY]"); - // First copy the customer ID list across - SqlCommand customerCommand = new SqlCommand("SELECT CustomerID from Customers", srcConn); - using (DbDataReader reader = customerCommand.ExecuteReader()) - { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = targetCustomerTable; - bulkcopy.WriteToServer(reader); - } - } + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + { + srcConn.Open(); - SqlCommand srcCmd = new SqlCommand("select OrderID, CustomerID from Orders where OrderId = 10643", srcConn); - using (DbDataReader reader = srcCmd.ExecuteReader()) - { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = targettable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + // First copy the customer ID list across + SqlCommand customerCommand = new SqlCommand("SELECT CustomerID from Customers", srcConn); + using (DbDataReader reader = customerCommand.ExecuteReader()) + { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = customerTable.Name; + bulkcopy.WriteToServer(reader); + } + } - ColumnMappings.Add("OrderID", "OrderID"); - ColumnMappings.Add("CustomerID", "CustomerID"); + SqlCommand srcCmd = new SqlCommand("select OrderID, CustomerID from Orders where OrderId = 10643", srcConn); + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = orderTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - bulkcopy.WriteToServer(reader); + ColumnMappings.Add("OrderID", "OrderID"); + ColumnMappings.Add("CustomerID", "CustomerID"); - DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 1, "Unexpected number of rows."); - } - } + bulkcopy.WriteToServer(reader); + + DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 1, "Unexpected number of rows."); } - Helpers.VerifyResults(dstConn, targettable, 2, 1); - } - finally - { - Helpers.DropTable(dstCmd, targettable); - Helpers.DropTable(dstCmd, targetCustomerTable); } } + + Helpers.VerifyResults(dstConn, orderTable.Name, 2, 1); } } } - diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs index 42f4daf77a..b28872890d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,99 +17,91 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Bug85007", false); - string targetCustomerTable = dstTable + "_customer"; - using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try - { - Helpers.TryExecute(dstCmd, "CREATE TABLE [" + targetCustomerTable + "] ([CustomerID] [nchar] (5) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, CONSTRAINT [PK_" + targetCustomerTable + "] PRIMARY KEY CLUSTERED (CustomerID) ON [PRIMARY]) ON [PRIMARY]"); + // Declared before the order table so it is dropped last: the order table holds a + // foreign key referencing it. + using Table targetCustomerTable = new(dstConn, "SqlBulkCopyTest_Bug85007_customer", + "([CustomerID] [nchar] (5) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, PRIMARY KEY CLUSTERED (CustomerID) ON [PRIMARY]) ON [PRIMARY]"); - Helpers.TryExecute(dstCmd, - "CREATE TABLE [" + dstTable + "] (" + - " [OrderID] [int] IDENTITY (1, 1) NOT NULL ," + - " [CustomerID] [nchar] (5) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + - " [EmployeeID] [int] NULL ," + - " [OrderDate] [datetime] NULL ," + - " [RequiredDate] [datetime] NULL ," + - " [ShippedDate] [datetime] NULL ," + - " [ShipVia] [int] NULL ," + - " [Freight] [money] NULL CONSTRAINT [DF_" + dstTable + "_Freight] DEFAULT (0)," + - " [ShipName] [nvarchar] (40) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + - " [ShipAddress] [nvarchar] (60) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + - " [ShipCity] [nvarchar] (15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + - " [ShipRegion] [nvarchar] (15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + - " [ShipPostalCode] [nvarchar] (10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + - " [ShipCountry] [nvarchar] (15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + - " CONSTRAINT [PK_" + dstTable + "] PRIMARY KEY CLUSTERED " + - " (" + - " [OrderID]" + - " ) ON [PRIMARY] ," + - " CONSTRAINT [FK_" + dstTable + "_Customers] FOREIGN KEY " + - " (" + - " [CustomerID]" + - " ) REFERENCES [" + targetCustomerTable + "] (" + - " [CustomerID]" + - " )" + - ") ON [PRIMARY]"); + using Table dstTable = new(dstConn, "SqlBulkCopyTest_Bug85007", + "(" + + " [OrderID] [int] IDENTITY (1, 1) NOT NULL ," + + " [CustomerID] [nchar] (5) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + + " [EmployeeID] [int] NULL ," + + " [OrderDate] [datetime] NULL ," + + " [RequiredDate] [datetime] NULL ," + + " [ShippedDate] [datetime] NULL ," + + " [ShipVia] [int] NULL ," + + " [Freight] [money] NULL DEFAULT (0)," + + " [ShipName] [nvarchar] (40) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + + " [ShipAddress] [nvarchar] (60) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + + " [ShipCity] [nvarchar] (15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + + " [ShipRegion] [nvarchar] (15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + + " [ShipPostalCode] [nvarchar] (10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + + " [ShipCountry] [nvarchar] (15) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ," + + " PRIMARY KEY CLUSTERED " + + " (" + + " [OrderID]" + + " ) ON [PRIMARY] ," + + " FOREIGN KEY " + + " (" + + " [CustomerID]" + + " ) REFERENCES " + targetCustomerTable.Name + " (" + + " [CustomerID]" + + " )" + + ") ON [PRIMARY]"); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand customerCmd = new SqlCommand("SELECT CustomerID from Customers", srcConn)) - { - srcConn.Open(); + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand customerCmd = new SqlCommand("SELECT CustomerID from Customers", srcConn)) + { + srcConn.Open(); - // First copy the customer ID list across - using (DbDataReader reader = customerCmd.ExecuteReader()) + // First copy the customer ID list across + using (DbDataReader reader = customerCmd.ExecuteReader()) + { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = targetCustomerTable; - bulkcopy.WriteToServer(reader); - } + bulkcopy.DestinationTableName = targetCustomerTable.Name; + bulkcopy.WriteToServer(reader); } + } - SqlCommand srcCmd = new SqlCommand("select * from orders", srcConn); - using (DbDataReader reader = srcCmd.ExecuteReader()) - { + SqlCommand srcCmd = new SqlCommand("select * from orders", srcConn); + using (DbDataReader reader = srcCmd.ExecuteReader()) + { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - bulkcopy.BatchSize = 6; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.BatchSize = 6; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("OrderID", "OrderID"); - ColumnMappings.Add("CustomerID", "CustomerID"); - ColumnMappings.Add("EmployeeID", "EmployeeID"); - ColumnMappings.Add("RequiredDate", "RequiredDate"); - ColumnMappings.Add("ShippedDate", "ShippedDate"); - ColumnMappings.Add("ShipVia", "ShipVia"); - ColumnMappings.Add("Freight", "Freight"); - ColumnMappings.Add("ShipName", "ShipName"); - ColumnMappings.Add("ShipAddress", "ShipAddress"); - ColumnMappings.Add("ShipCity", "ShipCity"); - ColumnMappings.Add("ShipRegion", "ShipRegion"); - ColumnMappings.Add("ShipPostalCode", "ShipPostalCode"); - ColumnMappings.Add("ShipCountry", "ShipCountry"); + ColumnMappings.Add("OrderID", "OrderID"); + ColumnMappings.Add("CustomerID", "CustomerID"); + ColumnMappings.Add("EmployeeID", "EmployeeID"); + ColumnMappings.Add("RequiredDate", "RequiredDate"); + ColumnMappings.Add("ShippedDate", "ShippedDate"); + ColumnMappings.Add("ShipVia", "ShipVia"); + ColumnMappings.Add("Freight", "Freight"); + ColumnMappings.Add("ShipName", "ShipName"); + ColumnMappings.Add("ShipAddress", "ShipAddress"); + ColumnMappings.Add("ShipCity", "ShipCity"); + ColumnMappings.Add("ShipRegion", "ShipRegion"); + ColumnMappings.Add("ShipPostalCode", "ShipPostalCode"); + ColumnMappings.Add("ShipCountry", "ShipCountry"); - bulkcopy.WriteToServer(reader); + bulkcopy.WriteToServer(reader); - DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 830, "Unexpected number of rows."); - } - Helpers.VerifyResults(dstConn, dstTable, 14, 830); + DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 830, "Unexpected number of rows."); } + Helpers.VerifyResults(dstConn, dstTable.Name, 14, 830); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - Helpers.DropTable(dstCmd, targetCustomerTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs index 29df411186..d07f9d8cac 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug903514.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,26 +17,13 @@ public class Bug903514 public void Test() { string constr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Bug903514", false); - using (SqlConnection dstConn = new SqlConnection(constr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) - { - dstConn.Open(); + using SqlConnection dstConn = new SqlConnection(constr); + dstConn.Open(); - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 varchar(7000))"); - } + using Table dstTable = new(dstConn, "SqlBulkCopyTest_Bug903514", "(col1 int, col2 varchar(7000))"); - // NOTE: The table name embeds a GUID, so it must be dropped even when the bulk copy or an - // assertion below fails, otherwise it is left in the shared test database forever. - try - { - DoBulkCopy(constr, dstTable, 2); - DoBulkCopy(constr, dstTable, 0); - } - finally - { - Helpers.DropTables(constr, dstTable); - } + DoBulkCopy(constr, dstTable.Name, 2); + DoBulkCopy(constr, dstTable.Name, 0); } private static void DoBulkCopy(string dstConstr, string dstTable, int timeout) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug98182.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug98182.cs index fc0c10cec9..f9764b8de4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug98182.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug98182.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -19,54 +20,38 @@ public void Test() string srctable = "[" + dstTable + " src]"; dstTable = "[" + dstTable + "]"; - string[] epilogue = { - "create table " + srctable + "([col 1] int primary key, [col 2] text)", - "insert into " + srctable + " values (33, 'Michael')", - "create table " + dstTable + "([col 1] int primary key, [col 2] text)", - }; - string[] prologue = { - "drop table " + srctable, - "drop table " + dstTable, - }; - using (SqlConnection dstConn = new SqlConnection(constr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + + using Table srcTableObject = Table.WithName(dstConn, srctable, "([col 1] int primary key, [col 2] text)"); + using Table dstTableObject = Table.WithName(dstConn, dstTable, "([col 1] int primary key, [col 2] text)"); + + Helpers.TryExecute(dstCmd, "insert into " + srctable + " values (33, 'Michael')"); + + using (SqlConnection srcConn = new SqlConnection(constr)) + using (SqlCommand srcCmd = new SqlCommand(string.Format("select * from {0} ", srctable), srcConn)) { - Helpers.ProcessCommandBatch(typeof(SqlConnection), constr, epilogue); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(constr)) - using (SqlCommand srcCmd = new SqlCommand(string.Format("select * from {0} ", srctable), srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) { - srcConn.Open(); - - using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; + bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("[col 1]", "col 1"); - ColumnMappings.Add("col 2", "[col 2]"); + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + ColumnMappings.Add("[col 1]", "col 1"); + ColumnMappings.Add("col 2", "[col 2]"); - bulkcopy.WriteToServer(reader); + bulkcopy.WriteToServer(reader); - DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 1, "Unexpected number of rows."); - } - Helpers.VerifyResults(dstConn, dstTable, 2, 1); + DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 1, "Unexpected number of rows."); } + Helpers.VerifyResults(dstConn, dstTable, 2, 1); } } - finally - { - // NOTE: Each drop is run independently so that a failure to drop the source table - // does not leak the destination table (the names embed a GUID, so anything left - // behind stays in the shared test database forever). - Helpers.ProcessCleanupBatch(typeof(SqlConnection), constr, prologue); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs index c296b99969..dc72a41a14 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CacheMetadata.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Data; using System.Threading.Tasks; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -14,7 +15,7 @@ namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy public class CacheMetadata { private static readonly string sourceTable = "employees"; - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"; private static readonly string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; // Test that CacheMetadata option works for multiple WriteToServer calls to the same table. @@ -23,55 +24,46 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadata", false); string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using SqlConnection dstConn = new(dstConstr); using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try + using Table dstTable = new(dstConn, "SqlBulkCopyTest_CacheMetadata", tableDefinition); + + using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); + bulkcopy.DestinationTableName = dstTable.Name; + + // First WriteToServer: metadata is queried and cached. + using (SqlConnection srcConn = new(srcConstr)) + { + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); + } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 5); + + // Second WriteToServer: should reuse cached metadata. + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.TryExecute(dstCmd, initialQuery); - - using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); - bulkcopy.DestinationTableName = dstTable; - - // First WriteToServer: metadata is queried and cached. - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 5); - - // Second WriteToServer: should reuse cached metadata. - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 10); - - // Third WriteToServer: should still reuse cached metadata. - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 15); + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); } - finally + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 10); + + // Third WriteToServer: should still reuse cached metadata. + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.DropTable(dstCmd, dstTable); + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 15); } } @@ -79,7 +71,7 @@ public void Test() public class CacheMetadataInvalidate { private static readonly string sourceTable = "employees"; - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"; private static readonly string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; // Test that ClearCachedMetadata forces a fresh metadata query. @@ -88,47 +80,38 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataInvalidate", false); string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using SqlConnection dstConn = new(dstConstr); using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try + using Table dstTable = new(dstConn, "SqlBulkCopyTest_CacheMetadataInvalidate", tableDefinition); + + using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); + bulkcopy.DestinationTableName = dstTable.Name; + + // First WriteToServer: metadata is queried and cached. + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.TryExecute(dstCmd, initialQuery); - - using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); - bulkcopy.DestinationTableName = dstTable; - - // First WriteToServer: metadata is queried and cached. - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 5); - - // Invalidate the cache and write again: should still succeed after re-querying metadata. - bulkcopy.ClearCachedMetadata(); - - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 10); + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); } - finally + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 5); + + // Invalidate the cache and write again: should still succeed after re-querying metadata. + bulkcopy.ClearCachedMetadata(); + + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.DropTable(dstCmd, dstTable); + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 10); } } @@ -136,7 +119,7 @@ public void Test() public class CacheMetadataDestinationChange { private static readonly string sourceTable = "employees"; - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"; private static readonly string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; // Test that changing DestinationTableName invalidates the cache and works correctly with a new table. @@ -145,49 +128,38 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable1 = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataDstChange0", false); - string dstTable2 = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataDstChange1", false); string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); - string initialQuery1 = string.Format(initialQueryTemplate, dstTable1); - string initialQuery2 = string.Format(initialQueryTemplate, dstTable2); using SqlConnection dstConn = new(dstConstr); using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try + using Table dstTable1 = new(dstConn, "SqlBulkCopyTest_CacheMetadataDstChange0", tableDefinition); + using Table dstTable2 = new(dstConn, "SqlBulkCopyTest_CacheMetadataDstChange1", tableDefinition); + + using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); + + // Write to first table. + bulkcopy.DestinationTableName = dstTable1.Name; + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.TryExecute(dstCmd, initialQuery1); - Helpers.TryExecute(dstCmd, initialQuery2); - - using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); - - // Write to first table. - bulkcopy.DestinationTableName = dstTable1; - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable1, 3, 5); - - // Change destination table: cache should be invalidated automatically. - bulkcopy.DestinationTableName = dstTable2; - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable2, 3, 5); + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); } - finally + Helpers.VerifyResults(dstConn, dstTable1.Name, 3, 5); + + // Change destination table: cache should be invalidated automatically. + bulkcopy.DestinationTableName = dstTable2.Name; + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.DropTables(dstConstr, dstTable1, dstTable2); + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); } + Helpers.VerifyResults(dstConn, dstTable2.Name, 3, 5); } } @@ -195,7 +167,7 @@ public void Test() public class CacheMetadataWithoutFlag { private static readonly string sourceTable = "employees"; - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"; private static readonly string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; // Test that without the CacheMetadata flag, multiple writes still work (no regression). @@ -204,60 +176,49 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataNoFlag", false); string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using SqlConnection dstConn = new(dstConstr); using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try + using Table dstTable = new(dstConn, "SqlBulkCopyTest_CacheMetadataNoFlag", tableDefinition); + + using SqlBulkCopy bulkcopy = new(dstConn); + bulkcopy.DestinationTableName = dstTable.Name; + + // First WriteToServer without CacheMetadata. + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.TryExecute(dstCmd, initialQuery); - - using SqlBulkCopy bulkcopy = new(dstConn); - bulkcopy.DestinationTableName = dstTable; - - // First WriteToServer without CacheMetadata. - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 5); - - // Second WriteToServer without CacheMetadata. - using (SqlConnection srcConn = new(srcConstr)) - { - srcConn.Open(); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = srcCmd.ExecuteReader(); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 10); + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); } - finally + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 5); + + // Second WriteToServer without CacheMetadata. + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.DropTable(dstCmd, dstTable); + srcConn.Open(); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = srcCmd.ExecuteReader(); + bulkcopy.WriteToServer(reader); } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 10); } } [Trait("Set", "2")] public class CacheMetadataWithDataTable { - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(50), col3 nvarchar(50))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(50), col3 nvarchar(50))"; // Test that CacheMetadata works with DataTable source as well as IDataReader. [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] public void Test() { string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataDT", false); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using DataTable sourceData = new(); sourceData.Columns.Add("col1", typeof(int)); @@ -271,32 +232,25 @@ public void Test() using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try - { - Helpers.TryExecute(dstCmd, initialQuery); + using Table dstTable = new(dstConn, "SqlBulkCopyTest_CacheMetadataDT", tableDefinition); - using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); - bulkcopy.DestinationTableName = dstTable; + using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); + bulkcopy.DestinationTableName = dstTable.Name; - // First WriteToServer with DataTable: metadata is queried and cached. - bulkcopy.WriteToServer(sourceData); - Helpers.VerifyResults(dstConn, dstTable, 3, 3); + // First WriteToServer with DataTable: metadata is queried and cached. + bulkcopy.WriteToServer(sourceData); + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 3); - // Second WriteToServer with DataTable: should reuse cached metadata. - bulkcopy.WriteToServer(sourceData); - Helpers.VerifyResults(dstConn, dstTable, 3, 6); - } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } + // Second WriteToServer with DataTable: should reuse cached metadata. + bulkcopy.WriteToServer(sourceData); + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 6); } } [Trait("Set", "2")] public class CacheMetadataColumnMappingsChange { - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(50), col3 nvarchar(50))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(50), col3 nvarchar(50))"; // Test that changing ColumnMappings between WriteToServer calls works correctly with CacheMetadata. // The cached metadata describes the destination table schema, not the column mappings, @@ -305,8 +259,6 @@ public class CacheMetadataColumnMappingsChange public void Test() { string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataColMap", false); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using DataTable sourceData = new DataTable(); sourceData.Columns.Add("id", typeof(int)); @@ -319,52 +271,45 @@ public void Test() using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try + using Table dstTable = new(dstConn, "SqlBulkCopyTest_CacheMetadataColMap", tableDefinition); + + using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); + bulkcopy.DestinationTableName = dstTable.Name; + + // First write: map firstName -> col2, lastName -> col3. + bulkcopy.ColumnMappings.Add("id", "col1"); + bulkcopy.ColumnMappings.Add("firstName", "col2"); + bulkcopy.ColumnMappings.Add("lastName", "col3"); + bulkcopy.WriteToServer(sourceData); + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 2); + + // Verify first mapping: col2 should contain firstName values. + using (SqlCommand verifyCmd = new("select col2 from " + dstTable.Name + " where col1 = 1", dstConn)) { - Helpers.TryExecute(dstCmd, initialQuery); - - using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); - bulkcopy.DestinationTableName = dstTable; - - // First write: map firstName -> col2, lastName -> col3. - bulkcopy.ColumnMappings.Add("id", "col1"); - bulkcopy.ColumnMappings.Add("firstName", "col2"); - bulkcopy.ColumnMappings.Add("lastName", "col3"); - bulkcopy.WriteToServer(sourceData); - Helpers.VerifyResults(dstConn, dstTable, 3, 2); - - // Verify first mapping: col2 should contain firstName values. - using (SqlCommand verifyCmd = new("select col2 from " + dstTable + " where col1 = 1", dstConn)) - { - object result = verifyCmd.ExecuteScalar(); - Assert.Equal("Alice", result); - } - - // Change mappings: swap col2 and col3 targets. - bulkcopy.ColumnMappings.Clear(); - bulkcopy.ColumnMappings.Add("id", "col1"); - bulkcopy.ColumnMappings.Add("firstName", "col3"); - bulkcopy.ColumnMappings.Add("lastName", "col2"); - bulkcopy.WriteToServer(sourceData); - Helpers.VerifyResults(dstConn, dstTable, 3, 4); - - // Verify second mapping: col3 should now contain firstName values for the new rows. - using (SqlCommand verifyCmd = new("select col3 from " + dstTable + " where col1 = 1 order by col2", dstConn)) - { - using SqlDataReader reader = verifyCmd.ExecuteReader(); - - // First row (from first write): col3 = "Smith" (lastName). - Assert.True(reader.Read()); - Assert.Equal("Smith", reader.GetString(0)); - - // Second row (from second write): col3 = "Alice" (firstName). - Assert.True(reader.Read()); - Assert.Equal("Alice", reader.GetString(0)); - } + object result = verifyCmd.ExecuteScalar(); + Assert.Equal("Alice", result); } - finally + + // Change mappings: swap col2 and col3 targets. + bulkcopy.ColumnMappings.Clear(); + bulkcopy.ColumnMappings.Add("id", "col1"); + bulkcopy.ColumnMappings.Add("firstName", "col3"); + bulkcopy.ColumnMappings.Add("lastName", "col2"); + bulkcopy.WriteToServer(sourceData); + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 4); + + // Verify second mapping: col3 should now contain firstName values for the new rows. + using (SqlCommand verifyCmd = new("select col3 from " + dstTable.Name + " where col1 = 1 order by col2", dstConn)) { - Helpers.DropTable(dstCmd, dstTable); + using SqlDataReader reader = verifyCmd.ExecuteReader(); + + // First row (from first write): col3 = "Smith" (lastName). + Assert.True(reader.Read()); + Assert.Equal("Smith", reader.GetString(0)); + + // Second row (from second write): col3 = "Alice" (firstName). + Assert.True(reader.Read()); + Assert.Equal("Alice", reader.GetString(0)); } } } @@ -372,7 +317,7 @@ public void Test() [Trait("Set", "2")] public class CacheMetadataColumnSubsetChange { - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(50), col3 nvarchar(50))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(50), col3 nvarchar(50))"; // Test that mapping a subset of columns on the first call, then all columns on the // second call, works correctly with CacheMetadata. This verifies that null-pruning of @@ -382,8 +327,6 @@ public class CacheMetadataColumnSubsetChange public void Test() { string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataSubset", false); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using DataTable sourceData = new DataTable(); sourceData.Columns.Add("id", typeof(int)); @@ -396,39 +339,32 @@ public void Test() using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try - { - Helpers.TryExecute(dstCmd, initialQuery); - - using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); - bulkcopy.DestinationTableName = dstTable; - - // First write: map only col1 and col2 (col3 is unmatched and will be pruned). - bulkcopy.ColumnMappings.Add("id", "col1"); - bulkcopy.ColumnMappings.Add("firstName", "col2"); - bulkcopy.WriteToServer(sourceData); - Helpers.VerifyResults(dstConn, dstTable, 3, 2); - - // Second write: map all three columns including col3. - // Without the clone fix, this would fail because col3 metadata was - // permanently nulled in the cache during the first call. - bulkcopy.ColumnMappings.Clear(); - bulkcopy.ColumnMappings.Add("id", "col1"); - bulkcopy.ColumnMappings.Add("firstName", "col2"); - bulkcopy.ColumnMappings.Add("lastName", "col3"); - bulkcopy.WriteToServer(sourceData); - Helpers.VerifyResults(dstConn, dstTable, 3, 4); - - // Verify col3 has the expected data from the second write. - using (SqlCommand verifyCmd = new("select col3 from " + dstTable + " where col1 = 1 and col3 is not null", dstConn)) - { - object result = verifyCmd.ExecuteScalar(); - Assert.Equal("Smith", result); - } - } - finally + using Table dstTable = new(dstConn, "SqlBulkCopyTest_CacheMetadataSubset", tableDefinition); + + using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); + bulkcopy.DestinationTableName = dstTable.Name; + + // First write: map only col1 and col2 (col3 is unmatched and will be pruned). + bulkcopy.ColumnMappings.Add("id", "col1"); + bulkcopy.ColumnMappings.Add("firstName", "col2"); + bulkcopy.WriteToServer(sourceData); + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 2); + + // Second write: map all three columns including col3. + // Without the clone fix, this would fail because col3 metadata was + // permanently nulled in the cache during the first call. + bulkcopy.ColumnMappings.Clear(); + bulkcopy.ColumnMappings.Add("id", "col1"); + bulkcopy.ColumnMappings.Add("firstName", "col2"); + bulkcopy.ColumnMappings.Add("lastName", "col3"); + bulkcopy.WriteToServer(sourceData); + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 4); + + // Verify col3 has the expected data from the second write. + using (SqlCommand verifyCmd = new("select col3 from " + dstTable.Name + " where col1 = 1 and col3 is not null", dstConn)) { - Helpers.DropTable(dstCmd, dstTable); + object result = verifyCmd.ExecuteScalar(); + Assert.Equal("Smith", result); } } } @@ -437,7 +373,7 @@ public void Test() public class CacheMetadataAsync { private static readonly string sourceTable = "employees"; - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"; private static readonly string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; // Test that CacheMetadata works correctly with WriteToServerAsync. @@ -446,77 +382,66 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataAsync", false); - Task t = TestAsync(srcConstr, dstConstr, dstTable); + Task t = TestAsync(srcConstr, dstConstr); t.Wait(); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } - private static async Task TestAsync(string srcConstr, string dstConstr, string dstTable) + private static async Task TestAsync(string srcConstr, string dstConstr) { string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using SqlConnection dstConn = new(dstConstr); using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try + using Table dstTable = new(dstConn, "SqlBulkCopyTest_CacheMetadataAsync", tableDefinition); + + using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); + bulkcopy.DestinationTableName = dstTable.Name; + + // First WriteToServerAsync: metadata is queried and cached. + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.TryExecute(dstCmd, initialQuery); - - using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata, null); - bulkcopy.DestinationTableName = dstTable; - - // First WriteToServerAsync: metadata is queried and cached. - using (SqlConnection srcConn = new(srcConstr)) - { - await srcConn.OpenAsync().ConfigureAwait(false); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = await srcCmd.ExecuteReaderAsync().ConfigureAwait(false); - await bulkcopy.WriteToServerAsync(reader).ConfigureAwait(false); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 5); - - // Second WriteToServerAsync: should reuse cached metadata. - using (SqlConnection srcConn = new(srcConstr)) - { - await srcConn.OpenAsync().ConfigureAwait(false); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = await srcCmd.ExecuteReaderAsync().ConfigureAwait(false); - await bulkcopy.WriteToServerAsync(reader).ConfigureAwait(false); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 10); - - // Third WriteToServerAsync: should still reuse cached metadata. - using (SqlConnection srcConn = new(srcConstr)) - { - await srcConn.OpenAsync().ConfigureAwait(false); - using SqlCommand srcCmd = new(sourceQuery, srcConn); - using IDataReader reader = await srcCmd.ExecuteReaderAsync().ConfigureAwait(false); - await bulkcopy.WriteToServerAsync(reader).ConfigureAwait(false); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 15); + await srcConn.OpenAsync().ConfigureAwait(false); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = await srcCmd.ExecuteReaderAsync().ConfigureAwait(false); + await bulkcopy.WriteToServerAsync(reader).ConfigureAwait(false); } - finally + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 5); + + // Second WriteToServerAsync: should reuse cached metadata. + using (SqlConnection srcConn = new(srcConstr)) + { + await srcConn.OpenAsync().ConfigureAwait(false); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = await srcCmd.ExecuteReaderAsync().ConfigureAwait(false); + await bulkcopy.WriteToServerAsync(reader).ConfigureAwait(false); + } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 10); + + // Third WriteToServerAsync: should still reuse cached metadata. + using (SqlConnection srcConn = new(srcConstr)) { - Helpers.DropTable(dstCmd, dstTable); + await srcConn.OpenAsync().ConfigureAwait(false); + using SqlCommand srcCmd = new(sourceQuery, srcConn); + using IDataReader reader = await srcCmd.ExecuteReaderAsync().ConfigureAwait(false); + await bulkcopy.WriteToServerAsync(reader).ConfigureAwait(false); } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 15); } } [Trait("Set", "2")] public class CacheMetadataCombinedWithKeepNulls { - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(50) default 'DefaultVal', col3 nvarchar(50))"; + private static readonly string tableDefinition = "(col1 int, col2 nvarchar(50) default 'DefaultVal', col3 nvarchar(50))"; // Test that CacheMetadata works correctly when combined with other SqlBulkCopyOptions. [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] public void Test() { string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CacheMetadataKeepNulls", false); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using DataTable sourceData = new(); sourceData.Columns.Add("col1", typeof(int)); @@ -529,33 +454,26 @@ public void Test() using SqlCommand dstCmd = dstConn.CreateCommand(); dstConn.Open(); - try - { - Helpers.TryExecute(dstCmd, initialQuery); + using Table dstTable = new(dstConn, "SqlBulkCopyTest_CacheMetadataKeepNulls", tableDefinition); - using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata | SqlBulkCopyOptions.KeepNulls, null); - bulkcopy.DestinationTableName = dstTable; - bulkcopy.ColumnMappings.Add("col1", "col1"); - bulkcopy.ColumnMappings.Add("col2", "col2"); - bulkcopy.ColumnMappings.Add("col3", "col3"); + using SqlBulkCopy bulkcopy = new(dstConn, SqlBulkCopyOptions.CacheMetadata | SqlBulkCopyOptions.KeepNulls, null); + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.ColumnMappings.Add("col1", "col1"); + bulkcopy.ColumnMappings.Add("col2", "col2"); + bulkcopy.ColumnMappings.Add("col3", "col3"); - // First write with CacheMetadata | KeepNulls. - bulkcopy.WriteToServer(sourceData); - Helpers.VerifyResults(dstConn, dstTable, 3, 2); + // First write with CacheMetadata | KeepNulls. + bulkcopy.WriteToServer(sourceData); + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 2); - // Verify nulls were kept (not replaced by default values). - using SqlCommand verifyCmd = new("select col2 from " + dstTable + " where col1 = 1", dstConn); - object result = verifyCmd.ExecuteScalar(); - Assert.Equal(System.DBNull.Value, result); + // Verify nulls were kept (not replaced by default values). + using SqlCommand verifyCmd = new("select col2 from " + dstTable.Name + " where col1 = 1", dstConn); + object result = verifyCmd.ExecuteScalar(); + Assert.Equal(System.DBNull.Value, result); - // Second write should reuse cached metadata. - bulkcopy.WriteToServer(sourceData); - Helpers.VerifyResults(dstConn, dstTable, 3, 4); - } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } + // Second write should reuse cached metadata. + bulkcopy.WriteToServer(sourceData); + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 4); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CheckConstraints.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CheckConstraints.cs index b5ec0e0dfc..f99b37c98a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CheckConstraints.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CheckConstraints.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -15,54 +16,44 @@ public class CheckConstraints public void Test() { string constr = DataTestUtility.TCPConnectionString; - string srctable = DataTestUtility.GetShortName("SqlBulkCopyTest_Extensionsrc", false); - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Extensiondst", false); using (SqlConnection dstConn = new SqlConnection(constr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try - { - // create the source table - Helpers.TryExecute(dstCmd, "create table " + srctable + " (col1 int , col2 int, col3 text)"); - Helpers.TryExecute(dstCmd, "insert into " + srctable + " values (33, 498, 'Michael')"); - Helpers.TryExecute(dstCmd, "insert into " + srctable + " values (34, 499, 'Astrid')"); - Helpers.TryExecute(dstCmd, "insert into " + srctable + " values (65, 500, 'alles Käse')"); - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int primary key, col2 int CONSTRAINT CK_" + dstTable + " CHECK (col2 < 500), col3 text)"); + using Table srctable = new(dstConn, "SqlBulkCopyTest_Extensionsrc", "(col1 int , col2 int, col3 text)"); + using Table dstTable = new(dstConn, "SqlBulkCopyTest_Extensiondst", "(col1 int primary key, col2 int CHECK (col2 < 500), col3 text)"); + + Helpers.TryExecute(dstCmd, "insert into " + srctable.Name + " values (33, 498, 'Michael')"); + Helpers.TryExecute(dstCmd, "insert into " + srctable.Name + " values (34, 499, 'Astrid')"); + Helpers.TryExecute(dstCmd, "insert into " + srctable.Name + " values (65, 500, 'alles Käse')"); - using (SqlConnection srcConn = new SqlConnection(constr)) - using (SqlCommand srcCmd = new SqlCommand("select * from " + srctable, srcConn)) + using (SqlConnection srcConn = new SqlConnection(constr)) + using (SqlCommand srcCmd = new SqlCommand("select * from " + srctable.Name, srcConn)) + { + srcConn.Open(); + using (DbDataReader reader = srcCmd.ExecuteReader()) { - srcConn.Open(); - using (DbDataReader reader = srcCmd.ExecuteReader()) + try { - try + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.CheckConstraints, null)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.CheckConstraints, null)) - { - bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + bulkcopy.DestinationTableName = dstTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("col1", "col1"); - ColumnMappings.Add("col2", "col2"); - ColumnMappings.Add("col3", "col3"); - bulkcopy.WriteToServer(reader); - } - } - catch (SqlException sqlEx) - { - // Error 547 == The %ls statement conflicted with the %ls constraint "%.*ls". - DataTestUtility.AssertEqualsWithDescription(547, sqlEx.Number, "Unexpected error number."); + ColumnMappings.Add("col1", "col1"); + ColumnMappings.Add("col2", "col2"); + ColumnMappings.Add("col3", "col3"); + bulkcopy.WriteToServer(reader); } } + catch (SqlException sqlEx) + { + // Error 547 == The %ls statement conflicted with the %ls constraint "%.*ls". + DataTestUtility.AssertEqualsWithDescription(547, sqlEx.Number, "Unexpected error number."); + } } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - Helpers.DropTable(dstCmd, srctable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs index 68cdc097cb..ef8a108e37 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -15,56 +16,45 @@ public class ColumnCollation public void Test() { string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_ColumnCollation", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (name_jp varchar(20) collate Japanese_CI_AS, " + - "name_ru varchar(20) collate Cyrillic_General_CI_AS)"); + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_ColumnCollation", + "(name_jp varchar(20) collate Japanese_CI_AS, name_ru varchar(20) collate Cyrillic_General_CI_AS)"); - // NOTE: The table name embeds a GUID, so it must be dropped even when an assertion - // below fails, otherwise it is left in the shared test database forever. - try - { - string s_jp = "江戸糸あやつり人形"; - string s_ru = "проверка"; + string s_jp = "江戸糸あやつり人形"; + string s_ru = "проверка"; - DataTable table = new DataTable(); - table.Columns.Add("name_jp", typeof(string)); - table.Columns.Add("name_ru", typeof(string)); - DataRow row = table.NewRow(); - row["name_jp"] = s_jp; - row["name_ru"] = s_ru; - table.Rows.Add(row); + DataTable table = new DataTable(); + table.Columns.Add("name_jp", typeof(string)); + table.Columns.Add("name_ru", typeof(string)); + DataRow row = table.NewRow(); + row["name_jp"] = s_jp; + row["name_ru"] = s_ru; + table.Rows.Add(row); - using (SqlBulkCopy bcp = new SqlBulkCopy(dstConn)) - { - bcp.DestinationTableName = dstTable; - bcp.WriteToServer(table); - } + using (SqlBulkCopy bcp = new SqlBulkCopy(dstConn)) + { + bcp.DestinationTableName = dstTable.Name; + bcp.WriteToServer(table); + } - using (SqlDataReader reader = (new SqlCommand("select * from " + dstTable, dstConn)).ExecuteReader()) + using (SqlDataReader reader = (new SqlCommand("select * from " + dstTable.Name, dstConn)).ExecuteReader()) + { + while (reader.Read()) { - while (reader.Read()) - { - DataTestUtility.AssertEqualsWithDescription( - 0, string.CompareOrdinal(s_jp, reader["name_jp"] as string), - "Unexpected value: " + reader["name_jp"]); + DataTestUtility.AssertEqualsWithDescription( + 0, string.CompareOrdinal(s_jp, reader["name_jp"] as string), + "Unexpected value: " + reader["name_jp"]); - DataTestUtility.AssertEqualsWithDescription( - 0, string.CompareOrdinal(s_ru, reader["name_ru"] as string), - "Unexpected value: " + reader["name_ru"]); - } + DataTestUtility.AssertEqualsWithDescription( + 0, string.CompareOrdinal(s_ru, reader["name_ru"] as string), + "Unexpected value: " + reader["name_ru"]); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } - } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs index e12f0f131f..e705adb738 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Data.Common; using System.Diagnostics; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -13,73 +14,62 @@ namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy [Trait("Set", "2")] public class CopyAllFromReader { - private static readonly string destinationTable = null; private static readonly string sourceTable = "employees"; - private static readonly string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; + private const string TableDefinition = "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"; private static readonly string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CopyAllFromReader", false); Debug.Assert((int)SqlBulkCopyOptions.UseInternalTransaction == 1 << 5, "Compiler screwed up the options"); - dstTable = destinationTable != null ? destinationTable : dstTable; - string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopyAllFromReader", TableDefinition); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) { - Helpers.TryExecute(dstCmd, initialQuery); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) - { - srcConn.Open(); + srcConn.Open(); - using (DbDataReader reader = srcCmd.ExecuteReader()) + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + IDictionary stats; + long expectedSelectCount = DataTestUtility.IsAzureSynapse ? 4 : 13; + long expectedSelectRows = DataTestUtility.IsAzureSynapse ? 4 : 15; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - IDictionary stats; - long expectedSelectCount = DataTestUtility.IsAzureSynapse ? 4 : 13; - long expectedSelectRows = DataTestUtility.IsAzureSynapse ? 4 : 15; - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - dstConn.StatisticsEnabled = true; - bulkcopy.WriteToServer(reader); - dstConn.StatisticsEnabled = false; - stats = dstConn.RetrieveStatistics(); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 5); + bulkcopy.DestinationTableName = dstTable.Name; + dstConn.StatisticsEnabled = true; + bulkcopy.WriteToServer(reader); + dstConn.StatisticsEnabled = false; + stats = dstConn.RetrieveStatistics(); + } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 5); - Assert.True(0 < (long)stats["BytesReceived"], "BytesReceived is non-positive."); - Assert.True(0 < (long)stats["BytesSent"], "BytesSent is non-positive."); - DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["UnpreparedExecs"], "Non-zero UnpreparedExecs value: " + (long)stats["UnpreparedExecs"]); - DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["PreparedExecs"], "Non-zero PreparedExecs value: " + (long)stats["PreparedExecs"]); - DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["Prepares"], "Non-zero Prepares value: " + (long)stats["Prepares"]); - DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["CursorOpens"], "Non-zero CursorOpens value: " + (long)stats["CursorOpens"]); - DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["IduRows"], "Non-zero IduRows value: " + (long)stats["IduRows"]); + Assert.True(0 < (long)stats["BytesReceived"], "BytesReceived is non-positive."); + Assert.True(0 < (long)stats["BytesSent"], "BytesSent is non-positive."); + DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["UnpreparedExecs"], "Non-zero UnpreparedExecs value: " + (long)stats["UnpreparedExecs"]); + DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["PreparedExecs"], "Non-zero PreparedExecs value: " + (long)stats["PreparedExecs"]); + DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["Prepares"], "Non-zero Prepares value: " + (long)stats["Prepares"]); + DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["CursorOpens"], "Non-zero CursorOpens value: " + (long)stats["CursorOpens"]); + DataTestUtility.AssertEqualsWithDescription((long)0, (long)stats["IduRows"], "Non-zero IduRows value: " + (long)stats["IduRows"]); - DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersReceived"], "Unexpected BuffersReceived value."); - DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersSent"], "Unexpected BuffersSent value."); - DataTestUtility.AssertEqualsWithDescription((long)0, stats["IduCount"], "Unexpected IduCount value."); - DataTestUtility.AssertEqualsWithDescription(expectedSelectCount, stats["SelectCount"], "Unexpected SelectCount value."); - DataTestUtility.AssertEqualsWithDescription((long)3, stats["ServerRoundtrips"], "Unexpected ServerRoundtrips value."); - DataTestUtility.AssertEqualsWithDescription(expectedSelectRows, stats["SelectRows"], "Unexpected SelectRows value."); - DataTestUtility.AssertEqualsWithDescription((long)2, stats["SumResultSets"], "Unexpected SumResultSets value."); - DataTestUtility.AssertEqualsWithDescription((long)0, stats["Transactions"], "Unexpected Transactions value."); - } + DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersReceived"], "Unexpected BuffersReceived value."); + DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersSent"], "Unexpected BuffersSent value."); + DataTestUtility.AssertEqualsWithDescription((long)0, stats["IduCount"], "Unexpected IduCount value."); + DataTestUtility.AssertEqualsWithDescription(expectedSelectCount, stats["SelectCount"], "Unexpected SelectCount value."); + DataTestUtility.AssertEqualsWithDescription((long)3, stats["ServerRoundtrips"], "Unexpected ServerRoundtrips value."); + DataTestUtility.AssertEqualsWithDescription(expectedSelectRows, stats["SelectRows"], "Unexpected SelectRows value."); + DataTestUtility.AssertEqualsWithDescription((long)2, stats["SumResultSets"], "Unexpected SumResultSets value."); + DataTestUtility.AssertEqualsWithDescription((long)0, stats["Transactions"], "Unexpected Transactions value."); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs index bd7a925e22..04241eaf5c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,43 +17,35 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CopyAllFromReader1", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try - { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopyAllFromReader1", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 * from employees", srcConn)) + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 * from employees", srcConn)) + { + srcConn.Open(); + using (DbDataReader reader = srcCmd.ExecuteReader()) { - srcConn.Open(); - using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + bulkcopy.DestinationTableName = dstTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("EmployeeID", "col1"); - ColumnMappings.Add("LastName", "col2"); - ColumnMappings.Add("FirstName", "col3"); + ColumnMappings.Add("EmployeeID", "col1"); + ColumnMappings.Add("LastName", "col2"); + ColumnMappings.Add("FirstName", "col3"); - bulkcopy.WriteToServer(reader); + bulkcopy.WriteToServer(reader); - DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 5, "Unexpected number of rows."); - DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied64, (long)5, "Unexpected number of rows."); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 5); + DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 5, "Unexpected number of rows."); + DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied64, (long)5, "Unexpected number of rows."); } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 5); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs index e2c26a9f0f..96b3bebf6d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -18,56 +19,47 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_AsyncTest1", false); // Use this semaphore to ensure that results are written to the log in the correct order SemaphoreSlim outputSemaphore = new SemaphoreSlim(0, 1); - Task t = TestAsync(srcConstr, dstConstr, dstTable, outputSemaphore); + Task t = TestAsync(srcConstr, dstConstr, outputSemaphore); outputSemaphore.Release(); t.Wait(); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } - private static async Task TestAsync(string srcConstr, string dstConstr, string dstTable, SemaphoreSlim outputSemaphore) + private static async Task TestAsync(string srcConstr, string dstConstr, SemaphoreSlim outputSemaphore) { - string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; string srcTable = "employees"; string sourceQuery = string.Format(sourceQueryTemplate, srcTable); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_AsyncTest1", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) { - Helpers.TryExecute(dstCmd, initialQuery); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) + srcConn.Open(); + using (DbDataReader reader = srcCmd.ExecuteReader()) { - srcConn.Open(); - using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; + bulkcopy.DestinationTableName = dstTable.Name; - await bulkcopy.WriteToServerAsync(reader); - await outputSemaphore.WaitAsync(); + await bulkcopy.WriteToServerAsync(reader); + await outputSemaphore.WaitAsync(); - DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 5, "Unexpected number of rows."); - DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied64, (long)5, "Unexpected number of rows."); - } - Helpers.VerifyResults(dstConn, dstTable, 3, 5); + DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied, 5, "Unexpected number of rows."); + DataTestUtility.AssertEqualsWithDescription(bulkcopy.RowsCopied64, (long)5, "Unexpected number of rows."); } + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 5); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs index 45f009aae0..fdcc2290d1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -20,47 +21,38 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_AsyncTest5", false); cts = new CancellationTokenSource(); cts.Cancel(); - Task t = TestAsync(srcConstr, dstConstr, dstTable, cts.Token); + Task t = TestAsync(srcConstr, dstConstr, cts.Token); DataTestUtility.AssertThrowsInner(() => t.Wait()); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } - private static async Task TestAsync(string srcConstr, string dstConstr, string dstTable, CancellationToken ctoken) + private static async Task TestAsync(string srcConstr, string dstConstr, CancellationToken ctoken) { - string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; string srcTable = "employees"; string sourceQuery = string.Format(sourceQueryTemplate, srcTable); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_AsyncTest5", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) { - Helpers.TryExecute(dstCmd, initialQuery); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) - { - srcConn.Open(); + srcConn.Open(); - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - await bulkcopy.WriteToServerAsync(reader, ctoken); - } + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable.Name; + await bulkcopy.WriteToServerAsync(reader, ctoken); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs index 44b9c89dd7..845832967f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Data.Common; using System.Threading.Tasks; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -18,46 +19,39 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_AsyncTest6", false); - Task t = TestAsync(srcConstr, dstConstr, dstTable); + Task t = TestAsync(srcConstr, dstConstr); DataTestUtility.AssertThrowsInner(() => t.Wait()); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } - private static async Task TestAsync(string srcConstr, string dstConstr, string dstTable) + private static async Task TestAsync(string srcConstr, string dstConstr) { - string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10))"; + string tableDefinition = "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"; string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; string sourceTable = "employees"; string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); - string initialQuery = string.Format(initialQueryTemplate, dstTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + + using Table dstTable = new(dstConn, "SqlBulkCopyTest_AsyncTest6", tableDefinition); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) { - Helpers.TryExecute(dstCmd, initialQuery); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) - { - srcConn.Open(); + srcConn.Open(); - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - dstConn.Close(); - await bulkcopy.WriteToServerAsync(reader); - } + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable.Name; + dstConn.Close(); + await bulkcopy.WriteToServerAsync(reader); } } - finally - { - Helpers.TryDropTable(dstConstr, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs index e48bf5906a..8cef07cfa3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Data.Common; using System.IO; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -19,55 +20,48 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_AsyncTest7", false); #if DEBUG - string initialQueryTemplate = "create table {0} (col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 varchar(8000))"; + string tableDefinition = "(col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 varchar(8000))"; string sourceQuery = "select EmployeeID, LastName, FirstName, REPLICATE('a', 8000) from employees"; - string initialQuery = string.Format(initialQueryTemplate, dstTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + + using Table dstTable = new(dstConn, "SqlBulkCopyTest_AsyncTest7", tableDefinition); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) { - Helpers.TryExecute(dstCmd, initialQuery); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) - { - srcConn.Open(); + srcConn.Open(); - using (DbDataReader reader = srcCmd.ExecuteReader()) + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; + bulkcopy.DestinationTableName = dstTable.Name; - // Close the bulk copy's connection when it notifies us - bulkcopy.NotifyAfter = 1; - bulkcopy.SqlRowsCopied += (sender, e) => - { - dstConn.Close(); - }; + // Close the bulk copy's connection when it notifies us + bulkcopy.NotifyAfter = 1; + bulkcopy.SqlRowsCopied += (sender, e) => + { + dstConn.Close(); + }; - using (AsyncDebugScope debugScope = new AsyncDebugScope()) - { - // Force all writes to pend, this will guarantee that we will go through the correct code path - debugScope.ForceAsyncWriteDelay = 1; + using (AsyncDebugScope debugScope = new AsyncDebugScope()) + { + // Force all writes to pend, this will guarantee that we will go through the correct code path + debugScope.ForceAsyncWriteDelay = 1; - // Check that the copying fails - string message = string.Format(SystemDataResourceManager.Instance.ADP_OpenConnectionRequired, "WriteToServer", SystemDataResourceManager.Instance.ADP_ConnectionStateMsg_Closed); - DataTestUtility.AssertThrowsInnerWithAlternate(() => bulkcopy.WriteToServerAsync(reader).Wait(5000), innerExceptionMessage: message); - } + // Check that the copying fails + string message = string.Format(SystemDataResourceManager.Instance.ADP_OpenConnectionRequired, "WriteToServer", SystemDataResourceManager.Instance.ADP_ConnectionStateMsg_Closed); + DataTestUtility.AssertThrowsInnerWithAlternate(() => bulkcopy.WriteToServerAsync(reader).Wait(5000), innerExceptionMessage: message); } } } } - finally - { - Helpers.TryDropTable(dstConstr, dstTable); - } } #endif } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs index 7eabde9fa3..3927a8e6b9 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,53 +17,45 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CopyMultipleReaders", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopyMultipleReaders", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = srcConn.CreateCommand()) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = srcConn.CreateCommand()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - srcConn.Open(); - - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + bulkcopy.DestinationTableName = dstTable.Name; + srcCmd.CommandText = "select EmployeeID, LastName from employees where LastName < 'E%'"; + using (DbDataReader reader = srcCmd.ExecuteReader()) { - bulkcopy.DestinationTableName = dstTable; - srcCmd.CommandText = "select EmployeeID, LastName from employees where LastName < 'E%'"; - using (DbDataReader reader = srcCmd.ExecuteReader()) - { - bulkcopy.WriteToServer(reader); - } - DataTestUtility.AssertEqualsWithDescription(0, bulkcopy.ColumnMappings.Count, "Unexpected ColumnMappings count."); - - srcCmd.CommandText = "select EmployeeID, LastName, FirstName from employees where LastName > 'D%'"; - using (DbDataReader reader = srcCmd.ExecuteReader()) - { - bulkcopy.WriteToServer(reader); - } - DataTestUtility.AssertEqualsWithDescription(0, bulkcopy.ColumnMappings.Count, "Unexpected ColumnMappings count."); + bulkcopy.WriteToServer(reader); + } + DataTestUtility.AssertEqualsWithDescription(0, bulkcopy.ColumnMappings.Count, "Unexpected ColumnMappings count."); - srcCmd.CommandText = "select EmployeeID, FirstName from employees where LastName < 'E%'"; - using (DbDataReader reader = srcCmd.ExecuteReader()) - { - bulkcopy.WriteToServer(reader); - } - DataTestUtility.AssertEqualsWithDescription(0, bulkcopy.ColumnMappings.Count, "Unexpected ColumnMappings count."); + srcCmd.CommandText = "select EmployeeID, LastName, FirstName from employees where LastName > 'D%'"; + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkcopy.WriteToServer(reader); + } + DataTestUtility.AssertEqualsWithDescription(0, bulkcopy.ColumnMappings.Count, "Unexpected ColumnMappings count."); - Helpers.VerifyResults(dstConn, dstTable, 3, 15); + srcCmd.CommandText = "select EmployeeID, FirstName from employees where LastName < 'E%'"; + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkcopy.WriteToServer(reader); } + DataTestUtility.AssertEqualsWithDescription(0, bulkcopy.ColumnMappings.Count, "Unexpected ColumnMappings count."); + + Helpers.VerifyResults(dstConn, dstTable.Name, 3, 15); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs index a9f09bfc81..fa81839764 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,7 +17,6 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CopySomeFromDataTable", false); DataSet dataset; SqlDataAdapter adapter; DataTable datatable; @@ -26,54 +26,48 @@ public void Test() { dstConn.Open(); - try - { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) - { - srcConn.Open(); + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopySomeFromDataTable", "(col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); - dataset = new DataSet("MyDataSet"); - adapter = new SqlDataAdapter(srcCmd); - adapter.Fill(dataset); - datatable = dataset.Tables[0]; + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) + { + srcConn.Open(); - string columnname; + dataset = new DataSet("MyDataSet"); + adapter = new SqlDataAdapter(srcCmd); + adapter.Fill(dataset); + datatable = dataset.Tables[0]; - foreach (DataColumn column in datatable.Columns) - { - columnname = column.ColumnName; - } + string columnname; - datatable.Rows[0].BeginEdit(); - datatable.Rows[0][0] = 333; - datatable.Rows[0].EndEdit(); + foreach (DataColumn column in datatable.Columns) + { + columnname = column.ColumnName; + } - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - bulkcopy.BatchSize = 7; + datatable.Rows[0].BeginEdit(); + datatable.Rows[0][0] = 333; + datatable.Rows[0].EndEdit(); - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.BatchSize = 7; - ColumnMappings.Add(0, "col1"); - ColumnMappings.Add(1, "col2"); - ColumnMappings.Add(2, "col3"); - bulkcopy.WriteToServer(datatable, DataRowState.Unchanged); - datatable.Rows.GetEnumerator().Reset(); - bulkcopy.WriteToServer(datatable, DataRowState.Modified); - datatable.Rows.GetEnumerator().Reset(); - bulkcopy.WriteToServer(datatable, DataRowState.Deleted); - bulkcopy.Close(); - } + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - Helpers.VerifyResults(dstConn, dstTable, 4, 18); + ColumnMappings.Add(0, "col1"); + ColumnMappings.Add(1, "col2"); + ColumnMappings.Add(2, "col3"); + bulkcopy.WriteToServer(datatable, DataRowState.Unchanged); + datatable.Rows.GetEnumerator().Reset(); + bulkcopy.WriteToServer(datatable, DataRowState.Modified); + datatable.Rows.GetEnumerator().Reset(); + bulkcopy.WriteToServer(datatable, DataRowState.Deleted); + bulkcopy.Close(); } - } - finally - { - Helpers.DropTable(dstCmd, dstTable); + + Helpers.VerifyResults(dstConn, dstTable.Name, 4, 18); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs index fa4a3be07f..51fe1a5f66 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -18,17 +19,16 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_AsyncTest3", false); // Use this semaphore to ensure that results are written to the log in the correct order SemaphoreSlim outputSemaphore = new SemaphoreSlim(0, 1); - Task t = TestAsync(srcConstr, dstConstr, dstTable, outputSemaphore); + Task t = TestAsync(srcConstr, dstConstr, outputSemaphore); outputSemaphore.Release(); t.Wait(); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } - private static async Task TestAsync(string srcConstr, string dstConstr, string dstTable, SemaphoreSlim outputSemaphore) + private static async Task TestAsync(string srcConstr, string dstConstr, SemaphoreSlim outputSemaphore) { DataSet dataset; SqlDataAdapter adapter; @@ -39,48 +39,42 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d { dstConn.Open(); - try - { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) - { - srcConn.Open(); + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_AsyncTest3", "(col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); - dataset = new DataSet("MyDataSet"); - adapter = new SqlDataAdapter(srcCmd); - adapter.Fill(dataset); - datatable = dataset.Tables[0]; + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) + { + srcConn.Open(); - datatable.Rows[0].BeginEdit(); - datatable.Rows[0][0] = 333; - datatable.Rows[0].EndEdit(); + dataset = new DataSet("MyDataSet"); + adapter = new SqlDataAdapter(srcCmd); + adapter.Fill(dataset); + datatable = dataset.Tables[0]; - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - bulkcopy.BatchSize = 7; + datatable.Rows[0].BeginEdit(); + datatable.Rows[0][0] = 333; + datatable.Rows[0].EndEdit(); - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.BatchSize = 7; - ColumnMappings.Add(0, "col1"); - ColumnMappings.Add(1, "col2"); - ColumnMappings.Add(2, "col3"); - bulkcopy.WriteToServer(datatable, DataRowState.Unchanged); - datatable.Rows.GetEnumerator().Reset(); - await bulkcopy.WriteToServerAsync(datatable, DataRowState.Modified); - datatable.Rows.GetEnumerator().Reset(); - await bulkcopy.WriteToServerAsync(datatable, DataRowState.Deleted); - bulkcopy.Close(); - } + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - await outputSemaphore.WaitAsync(); - Helpers.VerifyResults(dstConn, dstTable, 4, 18); + ColumnMappings.Add(0, "col1"); + ColumnMappings.Add(1, "col2"); + ColumnMappings.Add(2, "col3"); + bulkcopy.WriteToServer(datatable, DataRowState.Unchanged); + datatable.Rows.GetEnumerator().Reset(); + await bulkcopy.WriteToServerAsync(datatable, DataRowState.Modified); + datatable.Rows.GetEnumerator().Reset(); + await bulkcopy.WriteToServerAsync(datatable, DataRowState.Deleted); + bulkcopy.Close(); } - } - finally - { - Helpers.DropTable(dstCmd, dstTable); + + await outputSemaphore.WaitAsync(); + Helpers.VerifyResults(dstConn, dstTable.Name, 4, 18); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs index fd29ce5488..4cb71a091b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,45 +17,37 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CopySomeFromReader", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopySomeFromReader", "(col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) { - srcConn.Open(); - - using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - bulkcopy.BatchSize = 6; + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.BatchSize = 6; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("EmployeeID", "col1"); - ColumnMappings.Add("BirthDate", "col4"); - ColumnMappings.Add("FirstName", "col2"); - ColumnMappings.Add("LastName", "col3"); + ColumnMappings.Add("EmployeeID", "col1"); + ColumnMappings.Add("BirthDate", "col4"); + ColumnMappings.Add("FirstName", "col2"); + ColumnMappings.Add("LastName", "col3"); - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable, 4, 9); + bulkcopy.WriteToServer(reader); } + Helpers.VerifyResults(dstConn, dstTable.Name, 4, 9); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs index 94ccc46a41..1eddc051e4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,7 +17,6 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CopySomeFromRowArray", false); DataSet dataset; SqlDataAdapter adapter; DataTable datatable; @@ -27,44 +27,37 @@ public void Test() { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopySomeFromRowArray", "(col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) + dataset = new DataSet("MyDataSet"); + adapter = new SqlDataAdapter(srcCmd); + adapter.Fill(dataset); + datatable = dataset.Tables[0]; + rows = new DataRow[datatable.Rows.Count]; + for (int i = 0; i < rows.Length; i++) { - srcConn.Open(); - - dataset = new DataSet("MyDataSet"); - adapter = new SqlDataAdapter(srcCmd); - adapter.Fill(dataset); - datatable = dataset.Tables[0]; - rows = new DataRow[datatable.Rows.Count]; - for (int i = 0; i < rows.Length; i++) - { - rows[i] = datatable.Rows[i]; - } + rows[i] = datatable.Rows[i]; + } - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - bulkcopy.BatchSize = 4; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.BatchSize = 4; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add(0, "col1"); - ColumnMappings.Add(2, "col3"); + ColumnMappings.Add(0, "col1"); + ColumnMappings.Add(2, "col3"); - bulkcopy.WriteToServer(rows); - bulkcopy.Close(); - } - Helpers.VerifyResults(dstConn, dstTable, 4, 9); + bulkcopy.WriteToServer(rows); + bulkcopy.Close(); } - } - finally - { - Helpers.DropTable(dstCmd, dstTable); + Helpers.VerifyResults(dstConn, dstTable.Name, 4, 9); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs index fa6b594137..2fc56755c1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -18,17 +19,16 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_AsyncTest2", false); // Use this semaphore to ensure that results are written to the log in the correct order SemaphoreSlim outputSemaphore = new SemaphoreSlim(0, 1); - Task t = TestAsync(srcConstr, dstConstr, dstTable, outputSemaphore); + Task t = TestAsync(srcConstr, dstConstr, outputSemaphore); outputSemaphore.Release(); t.Wait(); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } - private static async Task TestAsync(string srcConstr, string dstConstr, string dstTable, SemaphoreSlim outputSemaphore) + private static async Task TestAsync(string srcConstr, string dstConstr, SemaphoreSlim outputSemaphore) { DataSet dataset; SqlDataAdapter adapter; @@ -40,45 +40,38 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_AsyncTest2", "(col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10), col4 datetime)"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select * from employees", srcConn)) + dataset = new DataSet("MyDataSet"); + adapter = new SqlDataAdapter(srcCmd); + adapter.Fill(dataset); + datatable = dataset.Tables[0]; + rows = new DataRow[datatable.Rows.Count]; + for (int i = 0; i < rows.Length; i++) { - srcConn.Open(); - - dataset = new DataSet("MyDataSet"); - adapter = new SqlDataAdapter(srcCmd); - adapter.Fill(dataset); - datatable = dataset.Tables[0]; - rows = new DataRow[datatable.Rows.Count]; - for (int i = 0; i < rows.Length; i++) - { - rows[i] = datatable.Rows[i]; - } + rows[i] = datatable.Rows[i]; + } - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - bulkcopy.BatchSize = 4; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.BatchSize = 4; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add(0, "col1"); - ColumnMappings.Add(2, "col3"); + ColumnMappings.Add(0, "col1"); + ColumnMappings.Add(2, "col3"); - await bulkcopy.WriteToServerAsync(rows); - bulkcopy.Close(); - } - await outputSemaphore.WaitAsync(); - Helpers.VerifyResults(dstConn, dstTable, 4, 9); + await bulkcopy.WriteToServerAsync(rows); + bulkcopy.Close(); } - } - finally - { - Helpers.DropTable(dstCmd, dstTable); + await outputSemaphore.WaitAsync(); + Helpers.VerifyResults(dstConn, dstTable.Name, 4, 9); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyVariants.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyVariants.cs index 423440a193..f0654c57a7 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyVariants.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyVariants.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -15,68 +16,60 @@ public class CopyVariants public void Test() { string constr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Variants", false); - string[] prologue = - { - "create table " + dstTable + "_src (col_1 int primary key, col_2 sql_variant)", - - "insert into " + dstTable + "_src values (0, null)", - "insert into " + dstTable + "_src values (1, convert(int, 0))", - "insert into " + dstTable + "_src values (2, convert(smallint, -32768))", - "insert into " + dstTable + "_src values (3, convert(real, 2.2))", - "insert into " + dstTable + "_src values (4, convert(float, -3303.33303))", - "insert into " + dstTable + "_src values (5, convert(decimal(28,4), 44404.4404))", - "insert into " + dstTable + "_src values (6, convert(money, $555505.5505) )", - "insert into " + dstTable + "_src values (7, convert(smallmoney, $-6.6606) )", - "insert into " + dstTable + "_src values (8, convert(bit, 1) )", - "insert into " + dstTable + "_src values (9, convert(tinyint, 8) )", - "insert into " + dstTable + "_src values (10, convert(uniqueidentifier, '00000000-0000-0000-0000-000000000009') )", - "insert into " + dstTable + "_src values (11, convert(varbinary(756), 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0A) )", - "insert into " + dstTable + "_src values (12, convert(varchar(756), '111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111101') )", - "insert into " + dstTable + "_src values (13, convert(nvarchar(756), N'???a???????????????????????????????üböuaäZßABCÄboÜOUÖvrhÃã??z?????????????????z?????????A?????a???????????????????????????????üböuaäZßABCÄboÜOUÖvrhÃã??z?????????????????z?????????A?????a???????????????????????????????üböuaäZßABCÄboÜOUÖvrhÃã??z?????????????????z?????????A?????a???????????????????????????????üböuaäZßABCÄboÜOUÖvrhÃã??z?????????????????z?????????A?????a?????') )", - "insert into " + dstTable + "_src values (14, convert(datetime, {ts '2003-01-11 12:54:01.133'}) )", - "insert into " + dstTable + "_src values (15, convert(bigint, 444444444444404) )", - "insert into " + dstTable + "_src values (16, convert(int, -555505) )", - "insert into " + dstTable + "_src values (17, convert(smallint, 16) )", - "insert into " + dstTable + "_src values (18, convert(real, 777707.7) )", - "insert into " + dstTable + "_src values (19, convert(float, -888888808.88018) )", - "insert into " + dstTable + "_src values (20, convert(decimal(28,4), 99999999999999999909.9019) )", - - "create table " + dstTable + "_dst (col_1 int primary key, col_2 sql_variant)", - }; using (SqlConnection dstConn = new SqlConnection(constr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table srcTable = new(dstConn, "SqlBulkCopyTest_Variants_src", "(col_1 int primary key, col_2 sql_variant)"); + using Table dstTable = new(dstConn, "SqlBulkCopyTest_Variants_dst", "(col_1 int primary key, col_2 sql_variant)"); + + string[] prologue = { - foreach (string cmdtext in prologue) - { - Helpers.TryExecute(dstCmd, cmdtext); - } - using (SqlConnection srcConn = new SqlConnection(constr)) - using (SqlCommand srcCmd = new SqlCommand("select * from " + dstTable + "_src", srcConn)) - { - srcConn.Open(); + "insert into " + srcTable.Name + " values (0, null)", + "insert into " + srcTable.Name + " values (1, convert(int, 0))", + "insert into " + srcTable.Name + " values (2, convert(smallint, -32768))", + "insert into " + srcTable.Name + " values (3, convert(real, 2.2))", + "insert into " + srcTable.Name + " values (4, convert(float, -3303.33303))", + "insert into " + srcTable.Name + " values (5, convert(decimal(28,4), 44404.4404))", + "insert into " + srcTable.Name + " values (6, convert(money, $555505.5505) )", + "insert into " + srcTable.Name + " values (7, convert(smallmoney, $-6.6606) )", + "insert into " + srcTable.Name + " values (8, convert(bit, 1) )", + "insert into " + srcTable.Name + " values (9, convert(tinyint, 8) )", + "insert into " + srcTable.Name + " values (10, convert(uniqueidentifier, '00000000-0000-0000-0000-000000000009') )", + "insert into " + srcTable.Name + " values (11, convert(varbinary(756), 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0A) )", + "insert into " + srcTable.Name + " values (12, convert(varchar(756), '111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111101') )", + "insert into " + srcTable.Name + " values (13, convert(nvarchar(756), N'???a???????????????????????????????üböuaäZßABCÄboÜOUÖvrhÃã??z?????????????????z?????????A?????a???????????????????????????????üböuaäZßABCÄboÜOUÖvrhÃã??z?????????????????z?????????A?????a???????????????????????????????üböuaäZßABCÄboÜOUÖvrhÃã??z?????????????????z?????????A?????a???????????????????????????????üböuaäZßABCÄboÜOUÖvrhÃã??z?????????????????z?????????A?????a?????') )", + "insert into " + srcTable.Name + " values (14, convert(datetime, {ts '2003-01-11 12:54:01.133'}) )", + "insert into " + srcTable.Name + " values (15, convert(bigint, 444444444444404) )", + "insert into " + srcTable.Name + " values (16, convert(int, -555505) )", + "insert into " + srcTable.Name + " values (17, convert(smallint, 16) )", + "insert into " + srcTable.Name + " values (18, convert(real, 777707.7) )", + "insert into " + srcTable.Name + " values (19, convert(float, -888888808.88018) )", + "insert into " + srcTable.Name + " values (20, convert(decimal(28,4), 99999999999999999909.9019) )", + + }; - using (DbDataReader reader = srcCmd.ExecuteReader()) + foreach (string cmdtext in prologue) + { + Helpers.TryExecute(dstCmd, cmdtext); + } + using (SqlConnection srcConn = new SqlConnection(constr)) + using (SqlCommand srcCmd = new SqlCommand("select * from " + srcTable.Name, srcConn)) + { + srcConn.Open(); + + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable + "_dst"; - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, dstTable + "_dst", 2, 21); + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.WriteToServer(reader); } + Helpers.VerifyResults(dstConn, dstTable.Name, 2, 21); } } - finally - { - Helpers.DropTable(dstCmd, dstTable + "_src"); - Helpers.DropTable(dstCmd, dstTable + "_dst"); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs index 77a00f7dc3..c44df14d1a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -24,7 +25,6 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CopyWithEvent", false); DataSet dataset; SqlDataAdapter adapter; DataTable datatable; @@ -35,51 +35,44 @@ public void Test() { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopyWithEvent", "(orderid int, customerid nchar(5), rdate datetime, freight money, shipname nvarchar(40))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 100 * from orders", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (orderid int, customerid nchar(5), rdate datetime, freight money, shipname nvarchar(40))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 100 * from orders", srcConn)) + dataset = new DataSet("MyDataSet"); + adapter = new SqlDataAdapter(srcCmd); + adapter.Fill(dataset); + datatable = dataset.Tables[0]; + rows = new DataRow[datatable.Rows.Count]; + for (int i = 0; i < rows.Length; i++) { - srcConn.Open(); - - dataset = new DataSet("MyDataSet"); - adapter = new SqlDataAdapter(srcCmd); - adapter.Fill(dataset); - datatable = dataset.Tables[0]; - rows = new DataRow[datatable.Rows.Count]; - for (int i = 0; i < rows.Length; i++) - { - rows[i] = datatable.Rows[i]; - } + rows[i] = datatable.Rows[i]; } + } - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { - bulkcopy.SqlRowsCopied += new SqlRowsCopiedEventHandler(OnRowCopied); + bulkcopy.SqlRowsCopied += new SqlRowsCopiedEventHandler(OnRowCopied); - bulkcopy.DestinationTableName = dstTable; - bulkcopy.NotifyAfter = 50; + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.NotifyAfter = 50; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add(0, "orderid"); - ColumnMappings.Add(1, "customerid"); - ColumnMappings.Add(4, "rdate"); - ColumnMappings.Add(7, "freight"); - ColumnMappings.Add(8, "shipname"); + ColumnMappings.Add(0, "orderid"); + ColumnMappings.Add(1, "customerid"); + ColumnMappings.Add(4, "rdate"); + ColumnMappings.Add(7, "freight"); + ColumnMappings.Add(8, "shipname"); - bulkcopy.WriteToServer(rows); - bulkcopy.SqlRowsCopied -= new SqlRowsCopiedEventHandler(OnRowCopied); - } - Helpers.VerifyResults(dstConn, dstTable, 5, 100); - } - finally - { - Helpers.DropTable(dstCmd, dstTable); + bulkcopy.WriteToServer(rows); + bulkcopy.SqlRowsCopied -= new SqlRowsCopiedEventHandler(OnRowCopied); } + Helpers.VerifyResults(dstConn, dstTable.Name, 5, 100); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs index d6026ef135..d0e37683cc 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -38,48 +39,40 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_CopyWithEvent1", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopyWithEvent1", "(orderid int, customerid nchar(5), rdate datetime, freight money, shipname nvarchar(40))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 100 * from orders", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (orderid int, customerid nchar(5), rdate datetime, freight money, shipname nvarchar(40))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 100 * from orders", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, null)) { - srcConn.Open(); - - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, null)) - { - bulkcopy.SqlRowsCopied += new SqlRowsCopiedEventHandler(OnRowCopied); + bulkcopy.SqlRowsCopied += new SqlRowsCopiedEventHandler(OnRowCopied); - bulkcopy.DestinationTableName = dstTable; - bulkcopy.NotifyAfter = 50; + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.NotifyAfter = 50; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("OrderID", "orderid"); - ColumnMappings.Add("CustomerID", "customerid"); - ColumnMappings.Add("RequiredDate", "rdate"); - ColumnMappings.Add("Freight", "freight"); - ColumnMappings.Add("ShipName", "shipname"); + ColumnMappings.Add("OrderID", "orderid"); + ColumnMappings.Add("CustomerID", "customerid"); + ColumnMappings.Add("RequiredDate", "rdate"); + ColumnMappings.Add("Freight", "freight"); + ColumnMappings.Add("ShipName", "shipname"); - bulkcopy.NotifyAfter = 3; - DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); - bulkcopy.SqlRowsCopied -= new SqlRowsCopiedEventHandler(OnRowCopied); - bulkcopy.Close(); - } + bulkcopy.NotifyAfter = 3; + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); + bulkcopy.SqlRowsCopied -= new SqlRowsCopiedEventHandler(OnRowCopied); + bulkcopy.Close(); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs index 45068cda95..98122e6405 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -26,17 +27,16 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_AsyncTest4", false); // Use this semaphore to ensure that results are written to the log in the correct order SemaphoreSlim outputSemaphore = new SemaphoreSlim(0, 1); - Task t = TestAsync(srcConstr, dstConstr, dstTable, outputSemaphore); + Task t = TestAsync(srcConstr, dstConstr, outputSemaphore); outputSemaphore.Release(); t.Wait(); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } - private static async Task TestAsync(string srcConstr, string dstConstr, string dstTable, SemaphoreSlim outputSemaphore) + private static async Task TestAsync(string srcConstr, string dstConstr, SemaphoreSlim outputSemaphore) { DataSet dataset; SqlDataAdapter adapter; @@ -48,52 +48,45 @@ private static async Task TestAsync(string srcConstr, string dstConstr, string d { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_AsyncTest4", "(orderid int, customerid nchar(5), rdate datetime, freight money, shipname nvarchar(40))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 100 * from orders", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (orderid int, customerid nchar(5), rdate datetime, freight money, shipname nvarchar(40))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 100 * from orders", srcConn)) + dataset = new DataSet("MyDataSet"); + adapter = new SqlDataAdapter(srcCmd); + adapter.Fill(dataset); + datatable = dataset.Tables[0]; + rows = new DataRow[datatable.Rows.Count]; + for (int i = 0; i < rows.Length; i++) { - srcConn.Open(); - - dataset = new DataSet("MyDataSet"); - adapter = new SqlDataAdapter(srcCmd); - adapter.Fill(dataset); - datatable = dataset.Tables[0]; - rows = new DataRow[datatable.Rows.Count]; - for (int i = 0; i < rows.Length; i++) - { - rows[i] = datatable.Rows[i]; - } + rows[i] = datatable.Rows[i]; } + } - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { - bulkcopy.SqlRowsCopied += new SqlRowsCopiedEventHandler(OnRowCopied); + bulkcopy.SqlRowsCopied += new SqlRowsCopiedEventHandler(OnRowCopied); - bulkcopy.DestinationTableName = dstTable; - bulkcopy.NotifyAfter = 50; + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.NotifyAfter = 50; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add(0, "orderid"); - ColumnMappings.Add(1, "customerid"); - ColumnMappings.Add(4, "rdate"); - ColumnMappings.Add(7, "freight"); - ColumnMappings.Add(8, "shipname"); + ColumnMappings.Add(0, "orderid"); + ColumnMappings.Add(1, "customerid"); + ColumnMappings.Add(4, "rdate"); + ColumnMappings.Add(7, "freight"); + ColumnMappings.Add(8, "shipname"); - await bulkcopy.WriteToServerAsync(rows); - bulkcopy.SqlRowsCopied -= new SqlRowsCopiedEventHandler(OnRowCopied); - } - await outputSemaphore.WaitAsync(); - Helpers.VerifyResults(dstConn, dstTable, 5, 100); - } - finally - { - Helpers.DropTable(dstCmd, dstTable); + await bulkcopy.WriteToServerAsync(rows); + bulkcopy.SqlRowsCopied -= new SqlRowsCopiedEventHandler(OnRowCopied); } + await outputSemaphore.WaitAsync(); + Helpers.VerifyResults(dstConn, dstTable.Name, 5, 100); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DestinationTableNameWithSpecialChar.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DestinationTableNameWithSpecialChar.cs index 014a983d95..1406ae382a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DestinationTableNameWithSpecialChar.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/DestinationTableNameWithSpecialChar.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -26,55 +27,36 @@ public void Test() "[dbo]." + "[" + dsttable + "]", // [dbo].[@sometablename] }; - string[] epilogue = - { - "create table " + srctable + "([col1] int)", - "insert into " + srctable + " values (33)", - "create table [" + dsttable + "]([col1] int)", - }; - - string[] prologue = - { - "drop table " + srctable, - "drop table [" + dsttable + "]", - }; - using (SqlConnection dstConn = new SqlConnection(constr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + + using Table srcTableObject = Table.WithName(dstConn, srctable, "([col1] int)"); + using Table dstTableObject = Table.WithName(dstConn, "[" + dsttable + "]", "([col1] int)"); + + Helpers.TryExecute(dstCmd, "insert into " + srctable + " values (33)"); + + using (SqlConnection srcConn = new SqlConnection(constr)) + using (SqlCommand srcCmd = new SqlCommand(string.Format("select * from {0} ", srctable), srcConn)) { - Helpers.ProcessCommandBatch(typeof(SqlConnection), constr, epilogue); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(constr)) - using (SqlCommand srcCmd = new SqlCommand(string.Format("select * from {0} ", srctable), srcConn)) + int expRows = 1; + foreach (string dsttablename in dsttablecombo) { - srcConn.Open(); - - int expRows = 1; - foreach (string dsttablename in dsttablecombo) + using (DbDataReader reader = srcCmd.ExecuteReader()) { - using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dsttablename; - bulkcopy.WriteToServer(reader); - } - Helpers.VerifyResults(dstConn, "[" + dsttable + "]", 1, expRows); + bulkcopy.DestinationTableName = dsttablename; + bulkcopy.WriteToServer(reader); } - expRows++; + Helpers.VerifyResults(dstConn, "[" + dsttable + "]", 1, expRows); } + expRows++; } } - finally - { - // NOTE: Each drop is run independently so that a failure to drop the source table - // does not leak the destination table (the names embed a GUID, so anything left - // behind stays in the shared test database forever). - Helpers.ProcessCleanupBatch(typeof(SqlConnection), constr, prologue); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/FireTrigger.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/FireTrigger.cs index 1906c977fc..bf030ecfff 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/FireTrigger.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/FireTrigger.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,22 +17,6 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_FireTrigger", false); - string dstTable1 = dstTable + "_1"; // this table will receive a value if the trigger fires! - string trigger = dstTable + "_2"; - string[] prologue = - { - "create table " + dstTable + "(col1 int, col2 nvarchar(20), col3 nvarchar(10))", - "create table " + dstTable1 + " (col1 int);", - "create trigger " + trigger + " on " + dstTable + " for INSERT as insert into " + dstTable1 + " values (333)" - }; - string[] epilogue = - { - "drop table " + dstTable1 + " ", - "drop trigger " + trigger + " ", - "drop table " + dstTable - }; - string sourceTable = "employees"; string sourceQueryTemplate = "select top 5 EmployeeID, LastName, FirstName from {0}"; string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); @@ -41,45 +26,42 @@ public void Test() { dstConn.Open(); - try + // Dropping a table also drops the triggers defined on it, so the trigger below + // needs no separate cleanup. dstTable is declared last so that it - and its + // trigger - are dropped before the table the trigger writes into. + using Table dstTable1 = new(dstConn, "SqlBulkCopyTest_FireTrigger_1", "(col1 int)"); + using Table dstTable = new(dstConn, "SqlBulkCopyTest_FireTrigger", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + Helpers.TryExecute(dstCmd, + "create trigger " + DataTestUtility.GetShortName("SqlBulkCopyTest_FireTrigger_2", false) + + " on " + dstTable.Name + " for INSERT as insert into " + dstTable1.Name + " values (333)"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) { - // NOTE: Setup runs inside the try so that a partial failure (for example the - // trigger failing to create) still runs the epilogue and drops the tables. - Helpers.ProcessCommandBatch(dstCmd, prologue); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) { - srcConn.Open(); + SqlBulkCopyOptions option = SqlBulkCopyOptions.FireTriggers; - using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, option, null)) { - SqlBulkCopyOptions option = SqlBulkCopyOptions.FireTriggers; - - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, option, null)) - { - bulkcopy.DestinationTableName = dstTable; - bulkcopy.WriteToServer(reader); - } + bulkcopy.DestinationTableName = dstTable.Name; + bulkcopy.WriteToServer(reader); } + } - dstCmd.CommandText = "select top 2 * from " + dstTable1 + " "; - using (DbDataReader reader2 = dstCmd.ExecuteReader()) - { - Assert.True(reader2.Read(), "Failed to read!"); + dstCmd.CommandText = "select top 2 * from " + dstTable1.Name; + using (DbDataReader reader2 = dstCmd.ExecuteReader()) + { + Assert.True(reader2.Read(), "Failed to read!"); - Assert.True(reader2[0] is int, "Unexpected Field(0) type: " + reader2[0].GetType()); + Assert.True(reader2[0] is int, "Unexpected Field(0) type: " + reader2[0].GetType()); - Assert.True((int)(reader2[0]) == 333, "Unexpected Field(0) value: " + reader2[0]); - } + Assert.True((int)(reader2[0]) == 333, "Unexpected Field(0) value: " + reader2[0]); } } - finally - { - // NOTE: Each drop is run independently so that one failure does not leak the - // remaining objects (in particular the trigger). - Helpers.ProcessCleanupBatch(dstCmd, epilogue); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Helpers.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Helpers.cs index 0c197b355f..0a447da8b8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Helpers.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Helpers.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -10,133 +10,6 @@ namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy { public class Helpers { - internal static void ProcessCommandBatch(Type connType, string constr, string[] batch) - { - if (batch.Length > 0) - { - object[] activatorArgs = new object[1]; - activatorArgs[0] = constr; - using (DbConnection conn = (DbConnection)Activator.CreateInstance(connType, activatorArgs)) - { - conn.Open(); - DbCommand cmd = conn.CreateCommand(); - - ProcessCommandBatch(cmd, batch); - } - } - } - - internal static void ProcessCommandBatch(DbCommand cmd, string[] batch) - { - foreach (string cmdtext in batch) - { - Helpers.TryExecute(cmd, cmdtext); - } - } - - /// - /// Executes a batch of cleanup statements, running each one independently. - /// - /// - /// Unlike , a statement that fails does - /// not prevent the remaining statements from running. Cleanup batches typically remove several - /// objects (for example a table and the schema or trigger that depends on it), and aborting on - /// the first failure leaks everything that follows into the shared test database. - /// - internal static void ProcessCleanupBatch(DbCommand cmd, string[] batch) - { - foreach (string cmdtext in batch) - { - TryCleanup(cmd, cmdtext); - } - } - - /// - /// Executes a batch of cleanup statements on a new connection, running each one independently. - /// - internal static void ProcessCleanupBatch(Type connType, string constr, string[] batch) - { - if (batch.Length == 0) - { - return; - } - - try - { - using DbConnection conn = (DbConnection)Activator.CreateInstance(connType, new object[] { constr }); - conn.Open(); - - using DbCommand cmd = conn.CreateCommand(); - ProcessCleanupBatch(cmd, batch); - } - catch (Exception e) - { - Console.WriteLine($"Cleanup batch could not be run: {e.Message}"); - } - } - - /// - /// Executes a single cleanup statement, best-effort. - /// - internal static void TryCleanup(DbCommand cmd, string statement) - { - try - { - TryExecute(cmd, statement); - } - catch (Exception e) - { - Console.WriteLine($"Cleanup statement failed ({statement}): {e.Message}"); - } - } - - /// - /// Drops a table if it exists, best-effort. - /// - /// - /// Test table names embed a GUID, so anything that is not dropped stays in the shared test - /// database forever. The drop is therefore both guarded (so that dropping a table which was - /// never created is a no-op) and best-effort (so that one failure does not prevent subsequent - /// cleanup from running). - /// - public static void DropTable(DbCommand cmd, string tableName) => - TryCleanup(cmd, GetDropTableStatement(tableName)); - - public static int TryDropTable(string dstConstr, string tableName) - { - using (SqlConnection dropConn = new SqlConnection(dstConstr)) - using (SqlCommand dropCmd = dropConn.CreateCommand()) - { - dropConn.Open(); - return Helpers.TryExecute(dropCmd, GetDropTableStatement(tableName)); - } - } - - /// - /// Drops the supplied tables if they exist, best-effort, on a new connection. - /// - public static void DropTables(string dstConstr, params string[] tableNames) - { - try - { - using SqlConnection dropConn = new SqlConnection(dstConstr); - dropConn.Open(); - - using SqlCommand dropCmd = dropConn.CreateCommand(); - foreach (string tableName in tableNames) - { - DropTable(dropCmd, tableName); - } - } - catch (Exception e) - { - Console.WriteLine($"Tables could not be dropped: {e.Message}"); - } - } - - private static string GetDropTableStatement(string tableName) => - $"IF (OBJECT_ID('{tableName.Replace("'", "''")}') IS NOT NULL) DROP TABLE {tableName}"; - public static int TryExecute(DbCommand cmd, string strText) { cmd.CommandText = strText; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/InvalidAccessFromEvent.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/InvalidAccessFromEvent.cs index 19bdf2193e..12411f52b5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/InvalidAccessFromEvent.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/InvalidAccessFromEvent.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -54,9 +55,7 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_InvalidAccessFromEvent", false); _dstConstr = dstConstr; - _dstTable = dstTable; DataSet dataset; SqlDataAdapter adapter; @@ -77,30 +76,26 @@ public void Test() _dstConn.Open(); _dstcmd = _dstConn.CreateCommand(); - try - { - Helpers.TryExecute(_dstcmd, "create table " + dstTable + " (orderid int, customerid nchar(5), rdate datetime, freight money, shipname nvarchar(40))"); - _dstcmd.CommandText = "truncate table " + dstTable; - - expectedErrorMsg = SystemDataResourceManager.Instance.SQL_ConnectionLockedForBcpEvent; - InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedRollback)); - InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedCommit)); - InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedChangeDatabase)); - InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedExecute)); - InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedBulkCopy)); - - // this will close the connect which is valid so it must be the last test! - expectedErrorMsg = string.Format( - SystemDataResourceManager.Instance.ADP_OpenConnectionRequired, - "WriteToServer", - SystemDataResourceManager.Instance.ADP_ConnectionStateMsg_Closed); - InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedClose)); - } - finally - { - // the original connection is probably trashed - Helpers.TryDropTable(dstConstr, dstTable); - } + // Table disposal falls back to a fresh connection, which matters here because the + // test deliberately leaves the original connection unusable. + using Table dstTable = new(_dstConn, "SqlBulkCopyTest_InvalidAccessFromEvent", + "(orderid int, customerid nchar(5), rdate datetime, freight money, shipname nvarchar(40))"); + _dstTable = dstTable.Name; + _dstcmd.CommandText = "truncate table " + dstTable.Name; + + expectedErrorMsg = SystemDataResourceManager.Instance.SQL_ConnectionLockedForBcpEvent; + InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedRollback)); + InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedCommit)); + InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedChangeDatabase)); + InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedExecute)); + InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedBulkCopy)); + + // this will close the connect which is valid so it must be the last test! + expectedErrorMsg = string.Format( + SystemDataResourceManager.Instance.ADP_OpenConnectionRequired, + "WriteToServer", + SystemDataResourceManager.Instance.ADP_ConnectionStateMsg_Closed); + InnerTest(new SqlRowsCopiedEventHandler(OnRowCopiedClose)); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/KeepNulls.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/KeepNulls.cs index de359bea7a..4e0c663610 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/KeepNulls.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/KeepNulls.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,45 +17,33 @@ public void Test() { string srcconstr = DataTestUtility.TCPConnectionString; string dstconstr = DataTestUtility.TCPConnectionString; - string srctable = DataTestUtility.GetShortName("SqlBulkCopyTest_KeepNulls0", false); - string dsttable = DataTestUtility.GetShortName("SqlBulkCopyTest_KeepNulls1", false); using SqlConnection destConn = new(dstconstr); destConn.Open(); using SqlCommand dstcmd = destConn.CreateCommand(); - // NOTE: Setup runs inside the try so that a failure part way through (for example while - // creating the destination table) still drops the objects created before it. The table - // names embed a GUID, so anything left behind stays in the shared test database forever. - try - { - Helpers.TryExecute(dstcmd, "create table " + srctable + " (col1 int, col2 text, col3 text)"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col3) values (1, 'Michael')"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2, col3) values (2, 'Quark', 'Astrid')"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2) values (66, 'K�se');"); - - Helpers.TryExecute(dstcmd, "create table " + dsttable + " (col1 int identity(1,1), col2 text default 'Jogurt', col3 text)"); - - using SqlConnection sourceConn = new(srcconstr); - sourceConn.Open(); - - using SqlCommand srccmd = new("select * from " + srctable, sourceConn); - using IDataReader reader = srccmd.ExecuteReader(); - - using SqlBulkCopy bulkcopy = new(destConn, SqlBulkCopyOptions.KeepNulls, null); - bulkcopy.DestinationTableName = dsttable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("col1", "col1"); - ColumnMappings.Add("col2", "col2"); - ColumnMappings.Add("col3", "col3"); - - bulkcopy.WriteToServer(reader); - Helpers.VerifyResults(destConn, dsttable, 3, 3); - } - finally - { - Helpers.DropTables(dstconstr, srctable, dsttable); - } + using Table srctable = new(destConn, "SqlBulkCopyTest_KeepNulls0", "(col1 int, col2 text, col3 text)"); + using Table dsttable = new(destConn, "SqlBulkCopyTest_KeepNulls1", "(col1 int identity(1,1), col2 text default 'Jogurt', col3 text)"); + + Helpers.TryExecute(dstcmd, "insert into " + srctable.Name + "(col1, col3) values (1, 'Michael')"); + Helpers.TryExecute(dstcmd, "insert into " + srctable.Name + "(col1, col2, col3) values (2, 'Quark', 'Astrid')"); + Helpers.TryExecute(dstcmd, "insert into " + srctable.Name + "(col1, col2) values (66, 'K�se');"); + + using SqlConnection sourceConn = new(srcconstr); + sourceConn.Open(); + + using SqlCommand srccmd = new("select * from " + srctable.Name, sourceConn); + using IDataReader reader = srccmd.ExecuteReader(); + + using SqlBulkCopy bulkcopy = new(destConn, SqlBulkCopyOptions.KeepNulls, null); + bulkcopy.DestinationTableName = dsttable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + ColumnMappings.Add("col1", "col1"); + ColumnMappings.Add("col2", "col2"); + ColumnMappings.Add("col3", "col3"); + + bulkcopy.WriteToServer(reader); + Helpers.VerifyResults(destConn, dsttable.Name, 3, 3); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs index 87e9d909e5..0dbcd2c45b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -17,42 +18,34 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_MissingTargetColumn", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_MissingTargetColumn", "(col1 int, col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col3 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - srcConn.Open(); - - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + bulkcopy.DestinationTableName = dstTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("EmployeeID", "col1"); - ColumnMappings.Add("LastName", "col2"); // this column does not exist - ColumnMappings.Add("FirstName", "col3"); + ColumnMappings.Add("EmployeeID", "col1"); + ColumnMappings.Add("LastName", "col2"); // this column does not exist + ColumnMappings.Add("FirstName", "col3"); - string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadNonMatchingColumnName; - errorMsg = string.Format(errorMsg, "col2"); + string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadNonMatchingColumnName; + errorMsg = string.Format(errorMsg, "col2"); - DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: errorMsg); - } + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: errorMsg); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs index 2f13d8947c..3fcb73e046 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -17,42 +18,34 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_MissingTargetColumns", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_MissingTargetColumns", "(col1 int, col2 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - srcConn.Open(); - - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + bulkcopy.DestinationTableName = dstTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("EmployeeID", "col1"); - ColumnMappings.Add("LastName", "col3"); // this column does not exist - ColumnMappings.Add("FirstName", "col4"); // this column does not exist + ColumnMappings.Add("EmployeeID", "col1"); + ColumnMappings.Add("LastName", "col3"); // this column does not exist + ColumnMappings.Add("FirstName", "col4"); // this column does not exist - string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadNonMatchingColumnName; - errorMsg = string.Format(errorMsg, "col3,col4"); + string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadNonMatchingColumnName; + errorMsg = string.Format(errorMsg, "col3,col4"); - DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: errorMsg); - } + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: errorMsg); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs index 28a62466b2..ec782d6211 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs @@ -1,8 +1,9 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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 Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -32,35 +33,26 @@ public void Test() { dstConn.Open(); - try + // The table is declared after the schema so that it is dropped first: a schema + // cannot be dropped while it still contains objects. + using Schema schema = Schema.WithName(dstConn, dstschema); + using Table table = Table.WithName(dstConn, dstTable, "(orderid int, customerid nchar(5))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 2 orderid, customerid from orders", srcConn)) { - Helpers.TryExecute(dstCmd, "create schema " + dstschema); - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (orderid int, customerid nchar(5))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 2 orderid, customerid from orders", srcConn)) + using (SqlDataReader srcreader = srcCmd.ExecuteReader()) { - srcConn.Open(); - - using (SqlDataReader srcreader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) { - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = dstTable; + bulkcopy.DestinationTableName = dstTable; - bulkcopy.WriteToServer(srcreader); - } + bulkcopy.WriteToServer(srcreader); } - Helpers.VerifyResults(dstConn, dstTable, 2, 2); } - } - finally - { - // NOTE: Each statement is run independently. Previously a failed "drop table" - // (for example when the create failed) aborted the cleanup and leaked the - // schema, while also masking the original exception. - Helpers.TryCleanup(dstCmd, "drop table " + dstTable); - Helpers.TryCleanup(dstCmd, "drop schema " + dstschema); + Helpers.VerifyResults(dstConn, dstTable, 2, 2); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TableLock.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TableLock.cs index c90280d6d8..f542edc215 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TableLock.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TableLock.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,45 +17,33 @@ public void Test() { string srcconstr = DataTestUtility.TCPConnectionString; string dstconstr = DataTestUtility.TCPConnectionString; - string srctable = DataTestUtility.GetShortName("SqlBulkCopyTest_TableLock0", false); - string dsttable = DataTestUtility.GetShortName("SqlBulkCopyTest_TableLock1", false); using SqlConnection destConn = new(dstconstr); destConn.Open(); using SqlCommand dstcmd = destConn.CreateCommand(); - // NOTE: Setup runs inside the try so that a failure part way through (for example while - // creating the destination table) still drops the objects created before it. The table - // names embed a GUID, so anything left behind stays in the shared test database forever. - try - { - Helpers.TryExecute(dstcmd, "create table " + srctable + " (col1 int, col2 text, col3 text)"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col3) values (1, 'Michael')"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2, col3) values (2, 'Quark', 'Astrid')"); - Helpers.TryExecute(dstcmd, "insert into " + srctable + "(col1, col2) values (66, 'K�se');"); - - Helpers.TryExecute(dstcmd, "create table " + dsttable + " (col1 int identity(1,1), col2 text default 'Jogurt', col3 text)"); - - using SqlConnection sourceConn = new(srcconstr); - sourceConn.Open(); - - using SqlCommand srccmd = new SqlCommand("select * from " + srctable, sourceConn); - using IDataReader reader = srccmd.ExecuteReader(); - - using SqlBulkCopy bulkcopy = new(destConn, SqlBulkCopyOptions.TableLock, null); - bulkcopy.DestinationTableName = dsttable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("col1", "col1"); - ColumnMappings.Add("col2", "col2"); - ColumnMappings.Add("col3", "col3"); - - bulkcopy.WriteToServer(reader); - Helpers.VerifyResults(destConn, dsttable, 3, 3); - } - finally - { - Helpers.DropTables(dstconstr, srctable, dsttable); - } + using Table srctable = new(destConn, "SqlBulkCopyTest_TableLock0", "(col1 int, col2 text, col3 text)"); + using Table dsttable = new(destConn, "SqlBulkCopyTest_TableLock1", "(col1 int identity(1,1), col2 text default 'Jogurt', col3 text)"); + + Helpers.TryExecute(dstcmd, "insert into " + srctable.Name + "(col1, col3) values (1, 'Michael')"); + Helpers.TryExecute(dstcmd, "insert into " + srctable.Name + "(col1, col2, col3) values (2, 'Quark', 'Astrid')"); + Helpers.TryExecute(dstcmd, "insert into " + srctable.Name + "(col1, col2) values (66, 'K�se');"); + + using SqlConnection sourceConn = new(srcconstr); + sourceConn.Open(); + + using SqlCommand srccmd = new SqlCommand("select * from " + srctable.Name, sourceConn); + using IDataReader reader = srccmd.ExecuteReader(); + + using SqlBulkCopy bulkcopy = new(destConn, SqlBulkCopyOptions.TableLock, null); + bulkcopy.DestinationTableName = dsttable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + ColumnMappings.Add("col1", "col1"); + ColumnMappings.Add("col2", "col2"); + ColumnMappings.Add("col3", "col3"); + + bulkcopy.WriteToServer(reader); + Helpers.VerifyResults(destConn, dsttable.Name, 3, 3); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs index f8746382dd..54a9b4f985 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -17,43 +18,35 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Transaction0", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_Transaction0", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, null)) { - srcConn.Open(); + bulkcopy.DestinationTableName = dstTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, null)) + SqlTransaction myTrans = dstConn.BeginTransaction(); + try { - bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - - SqlTransaction myTrans = dstConn.BeginTransaction(); - try - { - DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); - } - finally - { - myTrans.Rollback(); - } + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); + } + finally + { + myTrans.Rollback(); } } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs index 655e336b6c..1b9d430ed3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -17,47 +18,39 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Transaction1", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_Transaction1", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, null)) { - srcConn.Open(); + bulkcopy.DestinationTableName = dstTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, null)) - { - bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + SqlCommand myCmd = dstConn.CreateCommand(); + myCmd.CommandText = "begin transaction"; + myCmd.ExecuteNonQuery(); - SqlCommand myCmd = dstConn.CreateCommand(); - myCmd.CommandText = "begin transaction"; + try + { + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); + } + finally + { + myCmd.CommandText = "rollback transaction"; myCmd.ExecuteNonQuery(); - - try - { - DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); - } - finally - { - myCmd.CommandText = "rollback transaction"; - myCmd.ExecuteNonQuery(); - } } } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs index 36dcb917cd..86b02e5cf2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs @@ -1,9 +1,10 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -16,54 +17,46 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Transaction2", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_Transaction2", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) { - srcConn.Open(); - - using (DbDataReader reader = srcCmd.ExecuteReader()) + SqlTransaction myTrans = dstConn.BeginTransaction(); + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.Default, myTrans)) { - SqlTransaction myTrans = dstConn.BeginTransaction(); - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.Default, myTrans)) - { - bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + bulkcopy.DestinationTableName = dstTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - try - { - bulkcopy.WriteToServer(reader); - SqlCommand myCmd = dstConn.CreateCommand(); - myCmd.CommandText = "select * from " + dstTable; - myCmd.Transaction = myTrans; - using (DbDataReader reader1 = myCmd.ExecuteReader()) - { - Assert.True(reader1.HasRows, "Expected reader to have rows."); - } - } - finally + try + { + bulkcopy.WriteToServer(reader); + SqlCommand myCmd = dstConn.CreateCommand(); + myCmd.CommandText = "select * from " + dstTable.Name; + myCmd.Transaction = myTrans; + using (DbDataReader reader1 = myCmd.ExecuteReader()) { - myTrans.Rollback(); + Assert.True(reader1.HasRows, "Expected reader to have rows."); } } - - Helpers.CheckTableRows(dstConn, dstTable, false); + finally + { + myTrans.Rollback(); + } } + + Helpers.CheckTableRows(dstConn, dstTable.Name, false); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs index 5ba310c502..539d276bfe 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -17,48 +18,40 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Transaction3", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_Transaction3", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlConnection conn3 = new SqlConnection(srcConstr)) { - srcConn.Open(); - - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlConnection conn3 = new SqlConnection(srcConstr)) + conn3.Open(); + // Start a local transaction on the wrong connection. + SqlTransaction myTrans = conn3.BeginTransaction(); + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.Default, myTrans)) { - conn3.Open(); - // Start a local transaction on the wrong connection. - SqlTransaction myTrans = conn3.BeginTransaction(); - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.Default, myTrans)) - { - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - bulkcopy.DestinationTableName = dstTable; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + bulkcopy.DestinationTableName = dstTable.Name; - string exceptionMsg = SystemDataResourceManager.Instance.ADP_TransactionConnectionMismatch; - DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: exceptionMsg); + string exceptionMsg = SystemDataResourceManager.Instance.ADP_TransactionConnectionMismatch; + DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: exceptionMsg); - SqlCommand myCmd = dstConn.CreateCommand(); - myCmd.CommandText = "select * from " + dstTable; - myCmd.Transaction = myTrans; + SqlCommand myCmd = dstConn.CreateCommand(); + myCmd.CommandText = "select * from " + dstTable.Name; + myCmd.Transaction = myTrans; - DataTestUtility.AssertThrows(() => myCmd.ExecuteReader(), exceptionMessage: exceptionMsg); - } + DataTestUtility.AssertThrows(() => myCmd.ExecuteReader(), exceptionMessage: exceptionMsg); } } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs index 48a76a5dcf..30360dcb31 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs @@ -1,10 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -17,35 +18,27 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_Transaction4", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_Transaction4", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlConnection conn3 = new SqlConnection(srcConstr)) { - srcConn.Open(); - - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlConnection conn3 = new SqlConnection(srcConstr)) - { - conn3.Open(); - // Start a local transaction on the wrong connection. - SqlTransaction myTrans = conn3.BeginTransaction(); - string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadConflictingTransactionOption; - DataTestUtility.AssertThrows(() => new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, myTrans), exceptionMessage: errorMsg); - } + conn3.Open(); + // Start a local transaction on the wrong connection. + SqlTransaction myTrans = conn3.BeginTransaction(); + string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadConflictingTransactionOption; + DataTestUtility.AssertThrows(() => new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, myTrans), exceptionMessage: errorMsg); } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs index af88310e9f..2acfbd893f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -6,6 +6,7 @@ using System.Data.Common; using System.Threading.Tasks; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -18,49 +19,41 @@ public void Test() { string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; - string dstTable = DataTestUtility.GetShortName("SqlBulkCopyTest_TransactionTestAsync", false); - Task t = TestAsync(srcConstr, dstConstr, dstTable); + Task t = TestAsync(srcConstr, dstConstr); DataTestUtility.AssertThrowsInner(() => t.Wait()); Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); } - private static async Task TestAsync(string srcConstr, string dstConstr, string dstTable) + private static async Task TestAsync(string srcConstr, string dstConstr) { using (SqlConnection dstConn = new SqlConnection(dstConstr)) using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - try + using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_TransactionTestAsync", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) { - Helpers.TryExecute(dstCmd, "create table " + dstTable + " (col1 int, col2 nvarchar(20), col3 nvarchar(10))"); + srcConn.Open(); - using (SqlConnection srcConn = new SqlConnection(srcConstr)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, LastName, FirstName from employees", srcConn)) + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, null)) { - srcConn.Open(); + bulkcopy.DestinationTableName = dstTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, null)) + SqlTransaction myTrans = dstConn.BeginTransaction(); + try { - bulkcopy.DestinationTableName = dstTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - - SqlTransaction myTrans = dstConn.BeginTransaction(); - try - { - await bulkcopy.WriteToServerAsync(reader); - } - finally - { - myTrans.Rollback(); - } + await bulkcopy.WriteToServerAsync(reader); + } + finally + { + myTrans.Rollback(); } } } - finally - { - Helpers.DropTable(dstCmd, dstTable); - } } } } From 969eddfd118e1673f04ad24225e48bd75dadcb77 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 25 Aug 2026 14:12:57 -0700 Subject: [PATCH 07/16] Convert AE fixtures onto shared DatabaseObject types - Add ScalarFunction, and WithName overloads on StoredProcedure, so the Always Encrypted fixtures - which must create the same object name behind each of several AE connection strings - can use the shared RAII types instead of hand-rolled create/drop pairs. - Convert ExceptionGenericErrorFixture and SqlNullValuesTests to hold their objects as disposables, dropped in reverse creation order. - Explain, consistently across the three AE Setup types, why the object name is a parameter in the guard but interpolated into the DROP. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../DatabaseObjects/ScalarFunction.cs | 68 +++++++++++++++ .../DatabaseObjects/StoredProcedure.cs | 20 +++++ .../AlwaysEncrypted/ExceptionsGenericError.cs | 86 +++++++++---------- .../AlwaysEncrypted/SqlNullValues.cs | 45 ++++++---- .../TestFixtures/Setup/ColumnEncryptionKey.cs | 4 + .../TestFixtures/Setup/ColumnMasterKey.cs | 4 + .../TestFixtures/Setup/Table.cs | 4 + 7 files changed, 171 insertions(+), 60 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ScalarFunction.cs diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ScalarFunction.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ScalarFunction.cs new file mode 100644 index 0000000000..63dcf26607 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ScalarFunction.cs @@ -0,0 +1,68 @@ +// 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; + +/// +/// A transient scalar user-defined function, created at the start of its scope and dropped when +/// disposed. +/// +public sealed class ScalarFunction : DatabaseObject +{ + /// + /// Initializes a new instance of the ScalarFunction class using the specified SQL connection, + /// name prefix and definition. + /// + /// + /// If a function with the specified name already exists, it will be dropped automatically + /// before creation. + /// + /// The SQL connection used to interact with the database. + /// The prefix for the function name. + /// The SQL definition of the function, following the function name. + public ScalarFunction(SqlConnection connection, string prefix, string definition) + : base(connection, GenerateLongName(prefix), definition, shouldCreate: true, shouldDrop: true) + { + } + + private ScalarFunction(SqlConnection connection, string name, string definition, bool shouldCreate) + : base(connection, name, definition, shouldCreate, shouldDrop: true) + { + } + + /// + /// Creates a function using the caller-supplied name verbatim, instead of generating one. + /// + /// + /// Prefer the prefix-based constructor: generated names embed a GUID and so cannot collide + /// between concurrent test runs against a shared database. This overload exists for the + /// minority of tests that must control the name exactly, for example because the same function + /// has to be created and addressed over several different connections. + /// + /// The SQL connection used to interact with the database. + /// The function name, already quoted/escaped by the caller if it needs to be. + /// The SQL definition of the function, following the function name. + public static ScalarFunction WithName(SqlConnection connection, string name, string definition) + => new(connection, name, definition, shouldCreate: true); + + protected override void CreateObject(string definition) + { + using SqlCommand createCommand = new($"CREATE FUNCTION {Name} {definition}", Connection); + + createCommand.ExecuteNonQuery(); + } + + protected override void DropObject() + { + // NOTE: The name is passed to OBJECT_ID() as a parameter rather than being interpolated + // into a string literal, because it may embed Environment.UserName/MachineName (see + // DatabaseObject.GenerateLongName) and an apostrophe in either would break the batch. + // The identifier in DROP FUNCTION is already bracket-quoted. + using SqlCommand dropCommand = new($"IF (OBJECT_ID(@name) IS NOT NULL) DROP FUNCTION {Name}", Connection); + + dropCommand.Parameters.AddWithValue("@name", Name); + + dropCommand.ExecuteNonQuery(); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs index 0ebbd6cb5e..eec97fea70 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs @@ -25,6 +25,26 @@ public StoredProcedure(SqlConnection connection, string prefix, string definitio { } + private StoredProcedure(SqlConnection connection, string name, string definition, bool shouldCreate) + : base(connection, name, definition, shouldCreate, shouldDrop: true) + { + } + + /// + /// Creates a stored procedure using the caller-supplied name verbatim, instead of generating one. + /// + /// + /// Prefer the prefix-based constructor: generated names embed a GUID and so cannot collide + /// between concurrent test runs against a shared database. This overload exists for the + /// minority of tests that must control the name exactly, for example because the same + /// procedure has to be created and addressed over several different connections. + /// + /// The SQL connection used to interact with the database. + /// The procedure name, already quoted/escaped by the caller if it needs to be. + /// The SQL definition of the stored procedure. + public static StoredProcedure WithName(SqlConnection connection, string name, string definition) + => new(connection, name, definition, shouldCreate: true); + protected override void CreateObject(string definition) { using SqlCommand createCommand = new($"CREATE PROCEDURE {Name} {definition}", Connection); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs index 9a0d2b43d5..a7cac6b476 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionsGenericError.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Data; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted @@ -116,6 +118,9 @@ public sealed class ExceptionGenericErrorFixture : IDisposable static public string encryptedTableName; static public string encryptedProcedureName; + private readonly List _databaseObjects = new(); + private readonly List _connections = new(); + public ExceptionGenericErrorFixture() { SqlConnection.ColumnEncryptionQueryMetadataCacheEnabled = false; @@ -138,63 +143,54 @@ private void CreateAndPopulateSimpleTable() { encryptedTableName = DatabaseHelper.GenerateUniqueName("encrypted"); encryptedProcedureName = DatabaseHelper.GenerateUniqueName("encrypted"); + + // The same table and procedure name has to exist behind every AE connection string, so + // the objects are created with an explicit name rather than a generated one, and the + // connection each was created on is held open for the lifetime of the fixture. foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) { - using (SqlConnection conn = CertificateUtility.GetOpenConnection(false, new SqlConnectionStringBuilder(connectionStr))) + SqlConnection conn = CertificateUtility.GetOpenConnection(false, new SqlConnectionStringBuilder(connectionStr)); + _connections.Add(conn); + + _databaseObjects.Add(Table.WithName(conn, encryptedTableName, "(c1 int)")); + + using (SqlCommand cmdInsert = new SqlCommand($"insert into {encryptedTableName} values(1)", conn)) { - using (SqlCommand cmdCreate = new SqlCommand($"create table {encryptedTableName}(c1 int)", conn)) - { - cmdCreate.CommandType = CommandType.Text; - cmdCreate.ExecuteNonQuery(); - } - using (SqlCommand cmdInsert = new SqlCommand($"insert into {encryptedTableName} values(1)", conn)) - { - cmdInsert.CommandType = CommandType.Text; - cmdInsert.ExecuteNonQuery(); - } - using (SqlCommand cmdCreateProc = new SqlCommand($"create procedure {encryptedProcedureName}(@c1 int) as insert into {encryptedTableName} values (@c1)", conn)) - { - cmdCreateProc.CommandType = CommandType.Text; - cmdCreateProc.ExecuteNonQuery(); - } + cmdInsert.CommandType = CommandType.Text; + cmdInsert.ExecuteNonQuery(); } + + _databaseObjects.Add(StoredProcedure.WithName( + conn, encryptedProcedureName, $"(@c1 int) as insert into {encryptedTableName} values (@c1)")); } } public void Dispose() { // Do NOT remove certificate for concurrent consistency. Certificates are used for other test cases as well. - foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) - { - SqlConnectionStringBuilder sb = new SqlConnectionStringBuilder(connectionStr); - // NOTE: Cleanup is best-effort and guarded. Previously the drops shared one command - // with no IF EXISTS guard, so a failure to drop the table leaked the procedure and - // skipped the server TCE setting reset for every remaining connection string. - try - { - using (SqlConnection conn = CertificateUtility.GetOpenConnection(false, sb)) - { - using (SqlCommand cmd = conn.CreateCommand()) - { - cmd.CommandType = CommandType.Text; + // Disposed in reverse creation order so that each procedure is dropped before the table + // it writes into. + for (int i = _databaseObjects.Count - 1; i >= 0; i--) + { + DisposeSafely(_databaseObjects[i]); + } + _databaseObjects.Clear(); - TryExecute(cmd, $"IF (OBJECT_ID('{encryptedTableName}') IS NOT NULL) DROP TABLE {encryptedTableName}"); - TryExecute(cmd, $"IF (OBJECT_ID('{encryptedProcedureName}') IS NOT NULL) DROP PROCEDURE {encryptedProcedureName}"); - } - } - } - catch (Exception ex) - { - Console.WriteLine($"{nameof(ExceptionGenericErrorFixture)}: cleanup failed: {ex.Message}"); - } + foreach (SqlConnection conn in _connections) + { + DisposeSafely(conn); + } + _connections.Clear(); - // Only use traceoff for non-sysadmin role accounts, Azure accounts does not have the permission. - if (DataTestUtility.IsNotAzureServer()) + // Only use traceoff for non-sysadmin role accounts, Azure accounts does not have the permission. + if (DataTestUtility.IsNotAzureServer()) + { + foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) { try { - CertificateUtility.ChangeServerTceSetting(true, sb); + CertificateUtility.ChangeServerTceSetting(true, new SqlConnectionStringBuilder(connectionStr)); } catch (Exception ex) { @@ -204,18 +200,18 @@ public void Dispose() } } - private static void TryExecute(SqlCommand command, string commandText) + private static void DisposeSafely(IDisposable disposable) { try { - command.CommandText = commandText; - command.ExecuteNonQuery(); + disposable.Dispose(); } catch (Exception ex) { - Console.WriteLine($"{nameof(ExceptionGenericErrorFixture)}: cleanup statement failed ({commandText}): {ex.Message}"); + Console.WriteLine($"{nameof(ExceptionGenericErrorFixture)}: cleanup failed: {ex.Message}"); } } + } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlNullValues.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlNullValues.cs index 9b8ff77494..0ae70c5aca 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlNullValues.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlNullValues.cs @@ -7,6 +7,9 @@ using System.Collections.Generic; using System.Data; using Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; +// NOTE: Aliased because the AE test fixtures declare their own Table type alongside the shared one. +using SetupTable = Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup.Table; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted @@ -16,6 +19,8 @@ public sealed class SqlNullValuesTests : IClassFixture _databaseObjects = new(); + private readonly List _connections = new(); private string UdfName = DatabaseHelper.GenerateUniqueName("SqlNullValuesRetVal"); private string UdfNameNotNull = DatabaseHelper.GenerateUniqueName("SqlNullValuesRetValNotNull"); @@ -47,17 +52,19 @@ public SqlNullValuesTests(SQLSetupStrategyCertStoreProvider context) cmd.ExecuteNonQuery(); } - string sql1 = $"CREATE FUNCTION {UdfName}() RETURNS INT AS \n BEGIN \n RETURN (SELECT c1 FROM [{tableName}] WHERE c1 IS NULL)\n END"; - string sql2 = $"CREATE FUNCTION {UdfNameNotNull}() RETURNS INT AS \n BEGIN \n RETURN (SELECT c1 FROM [{tableName}] WHERE c1 IS NOT NULL)\n END"; - using (SqlCommand cmd = sqlConnection.CreateCommand()) - { - cmd.CommandText = sql1; - cmd.ExecuteNonQuery(); - - cmd.CommandText = sql2; - cmd.ExecuteNonQuery(); - } } + + // The same function names have to exist behind every AE connection string, so + // they are created with an explicit name rather than a generated one, and the + // connection each was created on is held open until cleanup. + SqlConnection functionConnection = new SqlConnection(connStr); + functionConnection.Open(); + _connections.Add(functionConnection); + + _databaseObjects.Add(ScalarFunction.WithName(functionConnection, UdfName, + $"() RETURNS INT AS \n BEGIN \n RETURN (SELECT c1 FROM [{tableName}] WHERE c1 IS NULL)\n END")); + _databaseObjects.Add(ScalarFunction.WithName(functionConnection, UdfNameNotNull, + $"() RETURNS INT AS \n BEGIN \n RETURN (SELECT c1 FROM [{tableName}] WHERE c1 IS NOT NULL)\n END")); } } catch @@ -166,19 +173,27 @@ public void NullValueTests(string connString, ConnStringColumnEncryptionSetting public void Dispose() { + for (int i = _databaseObjects.Count - 1; i >= 0; i--) + { + TryCleanup(_databaseObjects[i].Dispose); + } + _databaseObjects.Clear(); + + foreach (SqlConnection connection in _connections) + { + TryCleanup(connection.Dispose); + } + _connections.Clear(); + foreach (string connStrAE in DataTestUtility.AEConnStringsSetup) { - // Each step is best-effort so that one failure cannot leak the remaining functions or - // skip cleanup for the remaining connection strings. try { using (SqlConnection sqlConnection = new SqlConnection(connStrAE)) { sqlConnection.Open(); - TryCleanup(() => Table.DeleteData(fixture.SqlNullValuesTable.Name, sqlConnection)); - TryCleanup(() => DataTestUtility.DropFunction(sqlConnection, UdfName)); - TryCleanup(() => DataTestUtility.DropFunction(sqlConnection, UdfNameNotNull)); + TryCleanup(() => SetupTable.DeleteData(fixture.SqlNullValuesTable.Name, sqlConnection)); } } catch (Exception ex) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnEncryptionKey.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnEncryptionKey.cs index 7b6338b7b2..90379e7bbf 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnEncryptionKey.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnEncryptionKey.cs @@ -46,6 +46,10 @@ public override void Drop(SqlConnection sqlConnection) // NOTE: The drop is guarded so that cleanup is idempotent. An unguarded DROP throws when // the key was never created (for example when setup failed part way through), which // would abort the enclosing drop loop and leak every remaining object. + // NOTE: T-SQL cannot parameterize an identifier, so the name is parameterized in the + // guard - where it is compared as a string - but must be interpolated into the DROP + // itself. The interpolated identifier is bracket-quoted, and the value only ever + // comes from the test's own generated name. string sql = $"IF EXISTS (SELECT 1 FROM sys.column_encryption_keys WHERE name = @name) DROP COLUMN ENCRYPTION KEY [{Name}]"; using (SqlCommand command = sqlConnection.CreateCommand()) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs index 155d36bc29..5de2bf4d1f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs @@ -53,6 +53,10 @@ public override void Drop(SqlConnection sqlConnection) // NOTE: The drop is guarded so that cleanup is idempotent. An unguarded DROP throws when // the key was never created (for example when setup failed part way through), which // would abort the enclosing drop loop and leak every remaining object. + // NOTE: T-SQL cannot parameterize an identifier, so the name is parameterized in the + // guard - where it is compared as a string - but must be interpolated into the DROP + // itself. The interpolated identifier is bracket-quoted, and the value only ever + // comes from the test's own generated name. string sql = $"IF EXISTS (SELECT 1 FROM sys.column_master_keys WHERE name = @name) DROP COLUMN MASTER KEY [{Name}];"; using (SqlCommand command = sqlConnection.CreateCommand()) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs index 35ab7f6486..70def1b1d4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/Table.cs @@ -19,6 +19,10 @@ public override void Drop(SqlConnection sqlConnection) // TABLE statements in the derived classes. An unqualified name resolves against the // connection's default schema, so if that is not dbo the guard would return NULL and // silently skip the drop, leaking the table. + // NOTE: T-SQL cannot parameterize an identifier, so the name is parameterized in the + // guard - where it is compared as a string - but must be interpolated into the DROP + // itself. The interpolated identifier is bracket-quoted, and the value only ever + // comes from the test's own generated name. string sql = $"IF (OBJECT_ID(@name) IS NOT NULL) DROP TABLE [dbo].[{Name}];"; using (SqlCommand command = sqlConnection.CreateCommand()) From 949ad0701310b12b26d33dd9f4fb96b49e9f68a9 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 05:07:46 -0700 Subject: [PATCH 08/16] Report orphaned objects when the final drop attempt fails TryDropAfterReconnect is the last chance to remove a transient test object. Its failure was silent on the TryDropBestEffort path, and on the Dispose path the rethrown exception does not identify which object was left behind. Log the object type and name so a leak is attributable. Also correct the drop-ordering comment in SpecialCharacterNames: the table is not created inside the schema, so note that the ordering is defensive rather than implying a containment relationship that does not exist. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../Common/Fixtures/DatabaseObjects/DatabaseObject.cs | 9 ++++++++- .../tests/ManualTests/BulkCopy/SpecialCharacterNames.cs | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs index 49557803d7..0141093409 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs @@ -2,6 +2,7 @@ // 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; namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; @@ -342,8 +343,14 @@ private bool TryDropAfterReconnect() return true; } - catch + catch (Exception ex) { + // This is the last chance to remove the object, so report the leak. Callers either + // rethrow (surfacing the original failure, which on its own does not say *which* object + // was left behind) or swallow the failure entirely, which would otherwise orphan the + // object silently. + Console.WriteLine($"Failed to drop {GetType().Name} '{Name}'; it may be orphaned in the test database. {ex}"); + return false; } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs index ec782d6211..d335b6ae98 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs @@ -33,8 +33,9 @@ public void Test() { dstConn.Open(); - // The table is declared after the schema so that it is dropped first: a schema - // cannot be dropped while it still contains objects. + // The table is not currently created inside the schema, but it is declared second so + // that it is dropped first regardless: a schema cannot be dropped while it still + // contains objects. using Schema schema = Schema.WithName(dstConn, dstschema); using Table table = Table.WithName(dstConn, dstTable, "(orderid int, customerid nchar(5))"); From ea7ed97c436bb1e365dedd57fc85153e0ad41f19 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 05:17:20 -0700 Subject: [PATCH 09/16] Make DatabaseObject.Dispose best-effort so cleanup cannot mask a test failure Dispose rethrew when both the initial drop and the reconnect retry failed. In a using block that exception surfaces in place of one already in flight, replacing a real test failure with a cleanup error - the exact problem these types exist to remove, and one this PR broadened by converting many tests to using. Dispose now delegates to TryDropBestEffort, collapsing two near-identical paths into one. The leak is still reported: TryDropAfterReconnect names the object, so it stays attributable without destroying the diagnosis of the failure that caused it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../DatabaseObjects/DatabaseObject.cs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs index 0141093409..50f02dbe8e 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs @@ -272,25 +272,19 @@ public static string GenerateShortName(string prefix, bool escape = true) /// protected abstract void DropObject(); + /// + /// This never throws. These objects are overwhelmingly consumed via using, so a throwing + /// Dispose would surface in place of an exception already in flight and replace a real + /// test failure with a cleanup error — the very "cleanup masks the real failure" problem these + /// types exist to remove. A drop that cannot be completed is reported by + /// instead, which names the object so the leak stays + /// attributable without destroying the diagnosis of the failure that caused it. + /// public void Dispose() { if (_shouldDrop) { - try - { - EnsureConnectionOpen(); - DropObject(); - } - catch - { - // The drop is all that stands between a failed run and an object orphaned forever - // in the shared test database, so it gets one retry on a healthy connection. A bare - // `throw` preserves the original exception (and its stack) if that retry also fails. - if (!TryDropAfterReconnect()) - { - throw; - } - } + TryDropBestEffort(); } // This explicitly does not drop the wrapped SqlConnection; this is sometimes // used in a loop to create multiple UDTs. @@ -302,8 +296,11 @@ public void Dispose() /// Drops the object, swallowing any failure. /// /// - /// Only for use on paths that are already unwinding because of a more interesting failure, - /// where a cleanup error must not replace the exception in flight. + /// The drop is all that stands between a failed run and an object orphaned forever in the + /// shared test database, so it gets one retry on a healthy connection before giving up. A + /// failure is never propagated: this runs either during or on a path + /// already unwinding because of a more interesting failure, and in both cases a cleanup error + /// must not replace the exception in flight. /// private void TryDropBestEffort() { @@ -345,10 +342,9 @@ private bool TryDropAfterReconnect() } catch (Exception ex) { - // This is the last chance to remove the object, so report the leak. Callers either - // rethrow (surfacing the original failure, which on its own does not say *which* object - // was left behind) or swallow the failure entirely, which would otherwise orphan the - // object silently. + // This is the last chance to remove the object, and no caller propagates the failure, + // so this report is the only trace the leak will leave. Naming the object matters: + // without it there is nothing to tell a maintainer *which* object was orphaned. Console.WriteLine($"Failed to drop {GetType().Name} '{Name}'; it may be orphaned in the test database. {ex}"); return false; From e09d7d23acbbf88826731756297e497a1680c83c Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 05:27:43 -0700 Subject: [PATCH 10/16] Remove dstCmd locals orphaned by the RAII table conversion These SqlCommand locals existed to run the CREATE and DROP statements for the destination table. Moving that DDL into the Table RAII type left them unused, so 28 BulkCopy tests were still opening a command they never touched. Verified each file contains exactly one dstCmd reference (the declaration) and that it is always the second clause of a using chain whose first clause keeps the block, so removing the line is purely subtractive. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../tests/ManualTests/BulkCopy/Bug85007.cs | 1 - .../tests/ManualTests/BulkCopy/ColumnCollation.cs | 1 - .../tests/ManualTests/BulkCopy/CopyAllFromReader.cs | 1 - .../tests/ManualTests/BulkCopy/CopyAllFromReader1.cs | 1 - .../tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs | 1 - .../tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs | 1 - .../BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs | 1 - .../BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs | 1 - .../tests/ManualTests/BulkCopy/CopyMultipleReaders.cs | 1 - .../tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs | 1 - .../tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs | 1 - .../tests/ManualTests/BulkCopy/CopySomeFromReader.cs | 1 - .../tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs | 1 - .../tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs | 1 - .../tests/ManualTests/BulkCopy/CopyWithEvent.cs | 1 - .../tests/ManualTests/BulkCopy/CopyWithEvent1.cs | 1 - .../tests/ManualTests/BulkCopy/CopyWithEventAsync.cs | 1 - .../tests/ManualTests/BulkCopy/HiddenTargetColumn.cs | 1 - .../tests/ManualTests/BulkCopy/MissingTargetColumn.cs | 1 - .../tests/ManualTests/BulkCopy/MissingTargetColumns.cs | 1 - .../tests/ManualTests/BulkCopy/MissingTargetTable.cs | 1 - .../tests/ManualTests/BulkCopy/SpecialCharacterNames.cs | 1 - .../tests/ManualTests/BulkCopy/Transaction.cs | 1 - .../tests/ManualTests/BulkCopy/Transaction1.cs | 1 - .../tests/ManualTests/BulkCopy/Transaction2.cs | 1 - .../tests/ManualTests/BulkCopy/Transaction3.cs | 1 - .../tests/ManualTests/BulkCopy/Transaction4.cs | 1 - .../tests/ManualTests/BulkCopy/TransactionTestAsync.cs | 1 - 28 files changed, 28 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs index b28872890d..6c8bd1347d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs @@ -18,7 +18,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs index ef8a108e37..9ffebcd0a2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ColumnCollation.cs @@ -17,7 +17,6 @@ public void Test() { string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs index e705adb738..6113fadcb1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader.cs @@ -27,7 +27,6 @@ public void Test() string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopyAllFromReader", TableDefinition); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs index 04241eaf5c..0b731df803 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReader1.cs @@ -18,7 +18,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_CopyAllFromReader1", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs index 96b3bebf6d..ba1c4f0bbf 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderAsync.cs @@ -36,7 +36,6 @@ private static async Task TestAsync(string srcConstr, string dstConstr, Semaphor string sourceQuery = string.Format(sourceQueryTemplate, srcTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_AsyncTest1", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs index fdcc2290d1..0ed67a7aba 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderCancelAsync.cs @@ -36,7 +36,6 @@ private static async Task TestAsync(string srcConstr, string dstConstr, Cancella string sourceQuery = string.Format(sourceQueryTemplate, srcTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_AsyncTest5", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs index 845832967f..daf14f4c95 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseAsync.cs @@ -33,7 +33,6 @@ private static async Task TestAsync(string srcConstr, string dstConstr) string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs index 8cef07cfa3..eccc1de75d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyAllFromReaderConnectionCloseOnEventAsync.cs @@ -25,7 +25,6 @@ public void Test() string sourceQuery = "select EmployeeID, LastName, FirstName, REPLICATE('a', 8000) from employees"; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs index 3927a8e6b9..7a670d207b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyMultipleReaders.cs @@ -18,7 +18,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs index fa81839764..55b7f34152 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatable.cs @@ -22,7 +22,6 @@ public void Test() DataTable datatable; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs index 51fe1a5f66..65531fc03c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromDatatableAsync.cs @@ -35,7 +35,6 @@ private static async Task TestAsync(string srcConstr, string dstConstr, Semaphor DataTable datatable; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs index 4cb71a091b..02d101f4e9 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromReader.cs @@ -18,7 +18,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs index 1eddc051e4..4e6aa12cda 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArray.cs @@ -23,7 +23,6 @@ public void Test() DataRow[] rows; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs index 2fc56755c1..184e109c7f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopySomeFromRowArrayAsync.cs @@ -36,7 +36,6 @@ private static async Task TestAsync(string srcConstr, string dstConstr, Semaphor DataRow[] rows; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs index c44df14d1a..80d51af2e8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent.cs @@ -31,7 +31,6 @@ public void Test() DataRow[] rows; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs index d0e37683cc..63adeb7f41 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEvent1.cs @@ -40,7 +40,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs index 98122e6405..1428aa7683 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/CopyWithEventAsync.cs @@ -44,7 +44,6 @@ private static async Task TestAsync(string srcConstr, string dstConstr, Semaphor DataRow[] rows; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/HiddenTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/HiddenTargetColumn.cs index 0f18df215e..3dcf609521 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/HiddenTargetColumn.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/HiddenTargetColumn.cs @@ -19,7 +19,6 @@ public void WriteToServer_CopyToHiddenTargetColumn_ThrowsSqlException() string destinationHistoryTable = DataTestUtility.GetShortName("HiddenTargetColumn_History"); using (SqlConnection dstConn = new SqlConnection(connectionString)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs index 0dbcd2c45b..85c0a4431b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumn.cs @@ -19,7 +19,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs index 3fcb73e046..d1f035f1bd 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetColumns.cs @@ -19,7 +19,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetTable.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetTable.cs index af103c1c0a..a8d255224e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetTable.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/MissingTargetTable.cs @@ -19,7 +19,6 @@ public void Test() string dstConstr = DataTestUtility.TCPConnectionString; string targetTable = DataTestUtility.GetShortName("@SqlBulkCopyTest_MissingTargetTable", false); using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs index d335b6ae98..f1d82e71c1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/SpecialCharacterNames.cs @@ -29,7 +29,6 @@ public void Test() dstTable = EscapeIdentifier(dstTable); using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs index 54a9b4f985..1e79ff6410 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs @@ -19,7 +19,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs index 1b9d430ed3..9aedc9633d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs @@ -19,7 +19,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs index 86b02e5cf2..00a9dbbcab 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs @@ -18,7 +18,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs index 539d276bfe..58dd0608ba 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs @@ -19,7 +19,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs index 30360dcb31..13d23ab6db 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs @@ -19,7 +19,6 @@ public void Test() string srcConstr = DataTestUtility.TCPConnectionString; string dstConstr = DataTestUtility.TCPConnectionString; using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_Transaction4", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs index 2acfbd893f..09fc9d8be6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs @@ -27,7 +27,6 @@ public void Test() private static async Task TestAsync(string srcConstr, string dstConstr) { using (SqlConnection dstConn = new SqlConnection(dstConstr)) - using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); using Table dstTable = new Table(dstConn, "SqlBulkCopyTest_TransactionTestAsync", "(col1 int, col2 nvarchar(20), col3 nvarchar(10))"); From bbc34471cfa8f2dc7fc1c1e602c431bf95a0f47f Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 05:35:16 -0700 Subject: [PATCH 11/16] Dispose SqlCommands in ParallelTransactionsTest exception-safely CreateTempTable and DropTempTable never disposed their commands at all, and the two parallel-transaction tests disposed theirs only on the happy path, so any throw from ExecuteNonQuery or Rollback leaked them. Converted all nine to using declarations. reader4 keeps an explicit scope rather than becoming a using declaration: MARS is off here, so it has to be closed before the rollback that follows, which would otherwise run against a connection that still has an open reader. Transaction handling is left untouched; the double rollback of trans1 is load-bearing for what this test exercises. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../ParallelTransactionsTest.cs | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs index a51f64713d..20d0c76b92 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs @@ -43,25 +43,21 @@ private static void BasicParallelTest(string connectionString, string tempTableN SqlTransaction trans2 = connection.BeginTransaction(); SqlTransaction trans3 = connection.BeginTransaction(); - SqlCommand com1 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); + using SqlCommand com1 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); com1.Transaction = trans1; com1.ExecuteNonQuery(); - SqlCommand com2 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); + using SqlCommand com2 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); com2.Transaction = trans2; com2.ExecuteNonQuery(); - SqlCommand com3 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); + using SqlCommand com3 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); com3.Transaction = trans3; com3.ExecuteNonQuery(); trans1.Rollback(); trans2.Rollback(); trans3.Rollback(); - - com1.Dispose(); - com2.Dispose(); - com3.Dispose(); } } @@ -100,15 +96,15 @@ private static void MultipleExecutesInSameTransactionTest(string connectionStrin SqlTransaction trans2 = connection.BeginTransaction(); SqlTransaction trans3 = connection.BeginTransaction(); - SqlCommand com1 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); + using SqlCommand com1 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); com1.Transaction = trans1; com1.ExecuteNonQuery(); - SqlCommand com2 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); + using SqlCommand com2 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); com2.Transaction = trans2; com2.ExecuteNonQuery(); - SqlCommand com3 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); + using SqlCommand com3 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); com3.Transaction = trans3; com3.ExecuteNonQuery(); @@ -116,15 +112,14 @@ private static void MultipleExecutesInSameTransactionTest(string connectionStrin trans2.Rollback(); trans3.Rollback(); - com1.Dispose(); - com2.Dispose(); - com3.Dispose(); - - SqlCommand com4 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); + using SqlCommand com4 = new SqlCommand("select top 1 EmployeeID from " + tempTableName, connection); com4.Transaction = trans1; - SqlDataReader reader4 = com4.ExecuteReader(); - reader4.Dispose(); - com4.Dispose(); + using (SqlDataReader reader4 = com4.ExecuteReader()) + { + // Scoped deliberately: MARS is off here, so the reader must be closed before + // the rollback below, which would otherwise fail on a connection that still + // has an open reader. + } trans1.Rollback(); } @@ -138,7 +133,7 @@ private static string CreateTempTable(string connectionString) using (var conn = new SqlConnection(connectionString)) { conn.Open(); - SqlCommand cmd = new SqlCommand(string.Format("SELECT EmployeeID, LastName, FirstName, Title, Address, City, Region, PostalCode, Country into {0} from Employees", tempTableName), conn); + using SqlCommand cmd = new SqlCommand(string.Format("SELECT EmployeeID, LastName, FirstName, Title, Address, City, Region, PostalCode, Country into {0} from Employees", tempTableName), conn); cmd.ExecuteNonQuery(); cmd.CommandText = string.Format("alter table {0} add constraint EmployeeID_{1} primary key (EmployeeID)", tempTableName, uniqueKey); cmd.ExecuteNonQuery(); @@ -152,7 +147,7 @@ private static void DropTempTable(string connectionString, string tempTableName) using (SqlConnection con1 = new SqlConnection(connectionString)) { con1.Open(); - SqlCommand cmd = new SqlCommand( + using SqlCommand cmd = new SqlCommand( string.Format("IF (OBJECT_ID('{0}') IS NOT NULL) DROP TABLE {0}", tempTableName), con1); cmd.ExecuteNonQuery(); } From 2c6fd9b888b778612a3470e3e905b969c4d5e6e2 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 05:44:33 -0700 Subject: [PATCH 12/16] Dispose commands and transactions in the BulkCopy transaction tests Thirteen SqlCommand and SqlTransaction instances were created and never disposed. Where a rollback existed it ran outside any using, so a throw from WriteToServer or ExecuteReader leaked the object regardless. ErrorOnRowsMarkedAsDeleted needed more than a using declaration: its finally reassigned cmd to a fresh command for the DROP, orphaning both. The drop now gets its own scoped command. Disposal ordering was checked per site so a transaction is always released before the connection that owns it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../tests/ManualTests/BulkCopy/Bug84548.cs | 4 ++-- .../tests/ManualTests/BulkCopy/Bug85007.cs | 2 +- .../ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs | 8 ++++---- .../tests/ManualTests/BulkCopy/OrderHintTransaction.cs | 2 +- .../tests/ManualTests/BulkCopy/Transaction.cs | 2 +- .../tests/ManualTests/BulkCopy/Transaction1.cs | 2 +- .../tests/ManualTests/BulkCopy/Transaction2.cs | 4 ++-- .../tests/ManualTests/BulkCopy/Transaction3.cs | 4 ++-- .../tests/ManualTests/BulkCopy/Transaction4.cs | 2 +- .../tests/ManualTests/BulkCopy/TransactionTestAsync.cs | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs index 4e387ce653..5c8257999b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug84548.cs @@ -39,7 +39,7 @@ public void Test() srcConn.Open(); // First copy the customer ID list across - SqlCommand customerCommand = new SqlCommand("SELECT CustomerID from Customers", srcConn); + using SqlCommand customerCommand = new SqlCommand("SELECT CustomerID from Customers", srcConn); using (DbDataReader reader = customerCommand.ExecuteReader()) { using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) @@ -49,7 +49,7 @@ public void Test() } } - SqlCommand srcCmd = new SqlCommand("select OrderID, CustomerID from Orders where OrderId = 10643", srcConn); + using SqlCommand srcCmd = new SqlCommand("select OrderID, CustomerID from Orders where OrderId = 10643", srcConn); using (DbDataReader reader = srcCmd.ExecuteReader()) { using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs index 6c8bd1347d..53501c3819 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Bug85007.cs @@ -69,7 +69,7 @@ public void Test() } } - SqlCommand srcCmd = new SqlCommand("select * from orders", srcConn); + using SqlCommand srcCmd = new SqlCommand("select * from orders", srcConn); using (DbDataReader reader = srcCmd.ExecuteReader()) { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs index 3ac8636467..54e7a0a9d5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs @@ -102,7 +102,7 @@ private static void RunCase(SqlConnection conn, string caseName, SqlBulkCopyInpu } // create SQL table with one int field, similar to the above DataTable - SqlCommand cmd = conn.CreateCommand(); + using SqlCommand cmd = conn.CreateCommand(); cmd.CommandText = "CREATE TABLE [" + tableName + "] (IntVal int)"; cmd.ExecuteNonQuery(); @@ -146,9 +146,9 @@ private static void RunCase(SqlConnection conn, string caseName, SqlBulkCopyInpu finally { // delete the table - cmd = conn.CreateCommand(); - cmd.CommandText = "DROP TABLE [" + tableName + "]"; - cmd.ExecuteNonQuery(); + using SqlCommand dropCmd = conn.CreateCommand(); + dropCmd.CommandText = "DROP TABLE [" + tableName + "]"; + dropCmd.ExecuteNonQuery(); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/OrderHintTransaction.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/OrderHintTransaction.cs index e8a0571467..3e8b70facc 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/OrderHintTransaction.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/OrderHintTransaction.cs @@ -27,7 +27,7 @@ public void Test() using (SqlCommand dstCmd = dstConn.CreateCommand()) { dstConn.Open(); - SqlTransaction txn = dstConn.BeginTransaction(); + using SqlTransaction txn = dstConn.BeginTransaction(); dstCmd.Transaction = txn; Helpers.TryExecute(dstCmd, initialQuery); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs index 1e79ff6410..1df72a8dee 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction.cs @@ -35,7 +35,7 @@ public void Test() bulkcopy.DestinationTableName = dstTable.Name; SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - SqlTransaction myTrans = dstConn.BeginTransaction(); + using SqlTransaction myTrans = dstConn.BeginTransaction(); try { DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader)); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs index 9aedc9633d..863be57734 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction1.cs @@ -35,7 +35,7 @@ public void Test() bulkcopy.DestinationTableName = dstTable.Name; SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - SqlCommand myCmd = dstConn.CreateCommand(); + using SqlCommand myCmd = dstConn.CreateCommand(); myCmd.CommandText = "begin transaction"; myCmd.ExecuteNonQuery(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs index 00a9dbbcab..a99ff3607a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction2.cs @@ -30,7 +30,7 @@ public void Test() using (DbDataReader reader = srcCmd.ExecuteReader()) { - SqlTransaction myTrans = dstConn.BeginTransaction(); + using SqlTransaction myTrans = dstConn.BeginTransaction(); using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.Default, myTrans)) { bulkcopy.DestinationTableName = dstTable.Name; @@ -39,7 +39,7 @@ public void Test() try { bulkcopy.WriteToServer(reader); - SqlCommand myCmd = dstConn.CreateCommand(); + using SqlCommand myCmd = dstConn.CreateCommand(); myCmd.CommandText = "select * from " + dstTable.Name; myCmd.Transaction = myTrans; using (DbDataReader reader1 = myCmd.ExecuteReader()) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs index 58dd0608ba..4735e7aa3c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction3.cs @@ -34,7 +34,7 @@ public void Test() { conn3.Open(); // Start a local transaction on the wrong connection. - SqlTransaction myTrans = conn3.BeginTransaction(); + using SqlTransaction myTrans = conn3.BeginTransaction(); using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, SqlBulkCopyOptions.Default, myTrans)) { SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; @@ -43,7 +43,7 @@ public void Test() string exceptionMsg = SystemDataResourceManager.Instance.ADP_TransactionConnectionMismatch; DataTestUtility.AssertThrows(() => bulkcopy.WriteToServer(reader), exceptionMessage: exceptionMsg); - SqlCommand myCmd = dstConn.CreateCommand(); + using SqlCommand myCmd = dstConn.CreateCommand(); myCmd.CommandText = "select * from " + dstTable.Name; myCmd.Transaction = myTrans; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs index 13d23ab6db..7e86f8b640 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/Transaction4.cs @@ -33,7 +33,7 @@ public void Test() { conn3.Open(); // Start a local transaction on the wrong connection. - SqlTransaction myTrans = conn3.BeginTransaction(); + using SqlTransaction myTrans = conn3.BeginTransaction(); string errorMsg = SystemDataResourceManager.Instance.SQL_BulkLoadConflictingTransactionOption; DataTestUtility.AssertThrows(() => new SqlBulkCopy(dstConn, SqlBulkCopyOptions.UseInternalTransaction, myTrans), exceptionMessage: errorMsg); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs index 09fc9d8be6..76ce1f9ffd 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/TransactionTestAsync.cs @@ -42,7 +42,7 @@ private static async Task TestAsync(string srcConstr, string dstConstr) bulkcopy.DestinationTableName = dstTable.Name; SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - SqlTransaction myTrans = dstConn.BeginTransaction(); + using SqlTransaction myTrans = dstConn.BeginTransaction(); try { await bulkcopy.WriteToServerAsync(reader); From 9ab0256fdb45bcaa7edaf94ee07e4c337b6246fd Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 05:54:05 -0700 Subject: [PATCH 13/16] Report certificate store cleanup failures instead of swallowing them Both failure paths in the store cleanup were silent. A failed store open skips every certificate destined for that store, and a failed remove leaves that one certificate behind; in either case the certificates persist across runs with no trace of why. Both now name what was left behind, matching the leak reporting added to DatabaseObject. The best-effort behaviour is unchanged - cleanup still never fails the run. The private key deletion catch is deliberately left silent: it is expected to fail routinely for certificates with no private key, ephemeral keys, or machine-scoped keys the run cannot delete, so logging there would be noise. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../Common/Fixtures/CertificateFixtureBase.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs index b8d081cf01..88b3a2c7b4 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/CertificateFixtureBase.cs @@ -313,8 +313,14 @@ protected virtual void Dispose(bool disposing) store.Open(OpenFlags.ReadWrite); opened = true; } - catch (Exception) + catch (Exception ex) { + // Every certificate destined for this store is about to be skipped, so this is the + // only chance to say so. Without it the certificates leak across runs with no trace. + Console.WriteLine( + $"Failed to open certificate store '{storeContext.Name}' in '{storeContext.Location}' " + + $"for cleanup; {storeContext.Certificates.Count} certificate(s) may be left behind. {ex}"); + opened = false; } @@ -337,8 +343,14 @@ protected virtual void Dispose(bool disposing) store.Remove(cert); } } - catch (Exception) + catch (Exception ex) { + // Same reasoning as the store-open failure above: report the leak rather than + // letting the certificate linger in the store silently. + Console.WriteLine( + $"Failed to remove certificate '{cert.Subject}' from store '{storeContext.Name}' " + + $"in '{storeContext.Location}'; it may be left behind. {ex}"); + continue; } } From 47a6eefcbc3217e01686d10c36e17bfa0c90a99b Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 06:02:32 -0700 Subject: [PATCH 14/16] Make the two remaining finally-block drops best-effort DropTempTable and the ErrorOnRowsMarkedAsDeleted teardown both run from a finally, so a throw from either surfaces in place of the test failure already propagating and hides the real result. Both now swallow the failure and name the table instead, matching DatabaseObject. The ErrorOnRowsMarkedAsDeleted drop was also unguarded; it now uses the OBJECT_ID check already used elsewhere in these tests rather than DROP TABLE IF EXISTS, which needs SQL Server 2016 or later. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../BulkCopy/ErrorOnRowsMarkedAsDeleted.cs | 16 ++++++++++---- .../ParallelTransactionsTest.cs | 22 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs index 54e7a0a9d5..8d1b2e90fc 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/ErrorOnRowsMarkedAsDeleted.cs @@ -145,10 +145,18 @@ private static void RunCase(SqlConnection conn, string caseName, SqlBulkCopyInpu } finally { - // delete the table - using SqlCommand dropCmd = conn.CreateCommand(); - dropCmd.CommandText = "DROP TABLE [" + tableName + "]"; - dropCmd.ExecuteNonQuery(); + // Best-effort: this runs while a test failure may already be propagating, so a + // failed drop must not surface in its place. The table is named instead. + try + { + using SqlCommand dropCmd = conn.CreateCommand(); + dropCmd.CommandText = "IF (OBJECT_ID('[" + tableName + "]') IS NOT NULL) DROP TABLE [" + tableName + "]"; + dropCmd.ExecuteNonQuery(); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to drop table '{tableName}'; it may be orphaned in the test database. {ex}"); + } } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs index 20d0c76b92..4f59823b79 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParallelTransactionsTest/ParallelTransactionsTest.cs @@ -142,14 +142,26 @@ private static string CreateTempTable(string connectionString) return tempTableName; } + /// + /// Best-effort: both callers invoke this from a finally, so a throw here would surface + /// in place of the failure that is already propagating and hide the real test result. The + /// table is named on failure so the leak stays attributable. + /// private static void DropTempTable(string connectionString, string tempTableName) { - using (SqlConnection con1 = new SqlConnection(connectionString)) + try { - con1.Open(); - using SqlCommand cmd = new SqlCommand( - string.Format("IF (OBJECT_ID('{0}') IS NOT NULL) DROP TABLE {0}", tempTableName), con1); - cmd.ExecuteNonQuery(); + using (SqlConnection con1 = new SqlConnection(connectionString)) + { + con1.Open(); + using SqlCommand cmd = new SqlCommand( + string.Format("IF (OBJECT_ID('{0}') IS NOT NULL) DROP TABLE {0}", tempTableName), con1); + cmd.ExecuteNonQuery(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to drop temp table '{tempTableName}'; it may be orphaned in the test database. {ex}"); } } } From 8db1e6d2a1fb9b6753f704b8f5118af7aa8266fe Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 06:09:35 -0700 Subject: [PATCH 15/16] Simplify DatabaseObject RAII contract per review Removes the shouldCreate/shouldDrop constructor flags. Table.AdoptExisting() was the only caller that ever passed shouldCreate:false and it had no call sites, so both flags were universally true and carried no information. The private verbatim-name constructors that relied on shouldCreate as a de facto overload discriminator now use a shared NameIsVerbatim enum, promoted out of Schema where it was already doing that job. Dispose() is now idempotent, and construction failures unwind through it instead of duplicating the drop, so all cleanup lives in one place. Guarantees that no drop failure can escape: TryDrop() swallows the retry path as well, and the leak report captures the data source and database up front (reading them after a failed reconnect can itself throw) so an orphaned object can be removed manually. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../DatabaseObjects/ColumnEncryptionKey.cs | 2 +- .../DatabaseObjects/ColumnMasterKey.cs | 2 +- .../DatabaseObjects/DatabaseObject.cs | 116 ++++++++++++------ .../Fixtures/DatabaseObjects/DatabaseUser.cs | 2 +- .../DatabaseObjects/NameIsVerbatim.cs | 20 +++ .../DatabaseObjects/ScalarFunction.cs | 8 +- .../Common/Fixtures/DatabaseObjects/Schema.cs | 13 +- .../Fixtures/DatabaseObjects/ServerLogin.cs | 2 +- .../DatabaseObjects/StoredProcedure.cs | 8 +- .../Common/Fixtures/DatabaseObjects/Table.cs | 21 +--- .../DatabaseObjects/UserDefinedType.cs | 2 +- 11 files changed, 115 insertions(+), 81 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/NameIsVerbatim.cs diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ColumnEncryptionKey.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ColumnEncryptionKey.cs index af402428b0..bdad3fbdd5 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ColumnEncryptionKey.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ColumnEncryptionKey.cs @@ -27,7 +27,7 @@ public sealed class ColumnEncryptionKey : DatabaseObject /// The column master key which backs this encryption key. public ColumnEncryptionKey(SqlConnection connection, string namePrefix, ColumnMasterKey cmkOrigin) : base(connection, GenerateLongName(namePrefix), definition: DefinitionTemplate, - state: cmkOrigin, shouldCreate: true, shouldDrop: true) + state: cmkOrigin) { } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ColumnMasterKey.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ColumnMasterKey.cs index de607c0f68..0802cbbe53 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ColumnMasterKey.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ColumnMasterKey.cs @@ -37,7 +37,7 @@ internal CreationParameters(SqlColumnEncryptionKeyStoreProvider provider, protected ColumnMasterKey(SqlConnection connection, string namePrefix, CreationParameters creationParameters) : base(connection, name: GenerateLongName(namePrefix), definition: DefinitionTemplate, - state: creationParameters, shouldCreate: true, shouldDrop: true) + state: creationParameters) { } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs index 50f02dbe8e..8da00090e6 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs @@ -17,7 +17,7 @@ namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; /// public abstract class DatabaseObject : IDisposable { - private readonly bool _shouldDrop; + private bool _disposed; protected SqlConnection Connection { get; } @@ -27,33 +27,32 @@ public abstract class DatabaseObject : IDisposable public string UnescapedName => Name.Substring(1, Name.Length - 2).Replace("]]", "]"); - protected DatabaseObject(SqlConnection connection, string name, string definition, TState state, bool shouldCreate, bool shouldDrop) + protected DatabaseObject(SqlConnection connection, string name, string definition, TState state) { - _shouldDrop = shouldDrop; - Connection = connection; State = state; Name = name; - if (shouldCreate) - { - EnsureConnectionOpen(); - DropObject(); + EnsureConnectionOpen(); - try - { - CreateObject(definition); - } - catch - { - // CREATE can fail *after* the server has created the object: a command timeout or a - // dropped connection reports failure for a statement that may already have committed. - // Every generated name embeds a GUID, so anything left behind is orphaned forever in - // the (shared) test database. Make a best-effort attempt to remove it, but let the - // original failure surface. - TryDropBestEffort(); - throw; - } + // Remove any object left behind by an earlier run before creating this one. Best-effort: + // if it cannot be removed, CREATE will report the collision far more clearly than a drop + // failure would. + TryDrop(); + + try + { + CreateObject(definition); + } + catch + { + // CREATE can fail *after* the server has created the object: a command timeout or a + // dropped connection reports failure for a statement that may already have committed. + // Every generated name embeds a GUID, so anything left behind is orphaned forever in + // the (shared) test database. Unwind through Dispose so that cleanup lives in exactly + // one place, then let the original failure surface. + Dispose(); + throw; } } @@ -266,26 +265,35 @@ public static string GenerateShortName(string prefix, bool escape = true) /// /// /// By the time this is called, will be open. - /// Must not throw an exception if the object does not exist, and must be safe to call more than - /// once: a failed drop is retried on a fresh connection, and a failed create attempts a drop to - /// avoid leaking an object whose creation may nonetheless have committed on the server. + /// + /// Implementations must be safe to call more than once and must not throw when the object does + /// not exist. They are not required to be exception-free beyond that: the guard against a + /// leftover object is inherently racy (the existence check and the DROP are separate + /// statements), and the connection itself can fail at any moment. owns + /// that problem — it retries on a fresh connection and reports anything it cannot remove — so + /// no failure from here ever escapes to a caller. /// protected abstract void DropObject(); /// - /// This never throws. These objects are overwhelmingly consumed via using, so a throwing - /// Dispose would surface in place of an exception already in flight and replace a real - /// test failure with a cleanup error — the very "cleanup masks the real failure" problem these - /// types exist to remove. A drop that cannot be completed is reported by - /// instead, which names the object so the leak stays - /// attributable without destroying the diagnosis of the failure that caused it. + /// Idempotent, and never throws. These objects are overwhelmingly consumed via using, so + /// a throwing Dispose would surface in place of an exception already in flight and + /// replace a real test failure with a cleanup error — the very "cleanup masks the real failure" + /// problem these types exist to remove. A drop that cannot be completed is reported by + /// instead, which identifies the object and where it lives + /// so the leak stays actionable without destroying the diagnosis of the failure that caused it. /// public void Dispose() { - if (_shouldDrop) + if (_disposed) { - TryDropBestEffort(); + return; } + + _disposed = true; + + TryDrop(); + // This explicitly does not drop the wrapped SqlConnection; this is sometimes // used in a loop to create multiple UDTs. @@ -302,7 +310,7 @@ public void Dispose() /// already unwinding because of a more interesting failure, and in both cases a cleanup error /// must not replace the exception in flight. /// - private void TryDropBestEffort() + private void TryDrop() { try { @@ -311,7 +319,15 @@ private void TryDropBestEffort() } catch { - TryDropAfterReconnect(); + try + { + TryDropAfterReconnect(); + } + catch + { + // Nothing left to try, and nowhere to report it: even the reporting path failed. + // Swallowing is the whole point — see the remarks above. + } } } @@ -332,6 +348,10 @@ private void TryDropBestEffort() /// private bool TryDropAfterReconnect() { + // Captured before the reconnect attempt: once the connection has been closed or disposed + // these can throw, and this is the only record the leak will leave. + string location = DescribeLocation(); + try { Connection.Close(); @@ -343,13 +363,29 @@ private bool TryDropAfterReconnect() catch (Exception ex) { // This is the last chance to remove the object, and no caller propagates the failure, - // so this report is the only trace the leak will leave. Naming the object matters: - // without it there is nothing to tell a maintainer *which* object was orphaned. - Console.WriteLine($"Failed to drop {GetType().Name} '{Name}'; it may be orphaned in the test database. {ex}"); + // so this report is the only trace the leak will leave. It names the object *and* + // where it lives, so a human or agent can drop it manually without having to work out + // which server and database the run was pointed at. + Console.WriteLine($"Failed to drop {GetType().Name} '{Name}' {location}; it may be orphaned there. {ex}"); return false; } } + + /// + /// Describes where the object lives, for the leak report. + /// + private string DescribeLocation() + { + try + { + return $"on data source '{Connection.DataSource}', database '{Connection.Database}'"; + } + catch (Exception ex) + { + return $"on an unknown data source ({ex.GetType().Name} reading the connection)"; + } + } } /// @@ -358,8 +394,8 @@ private bool TryDropAfterReconnect() /// public abstract class DatabaseObject : DatabaseObject { - protected DatabaseObject(SqlConnection connection, string name, string definition, bool shouldCreate, bool shouldDrop) - : base(connection, name, definition, state: null, shouldCreate, shouldDrop) + protected DatabaseObject(SqlConnection connection, string name, string definition) + : base(connection, name, definition, state: null) { } } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs index a757e9c9e0..20182d1ba3 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseUser.cs @@ -22,7 +22,7 @@ public sealed class DatabaseUser : DatabaseObject /// The name of the database where the user will be created. /// The server login which the database user will be associated with. public DatabaseUser(SqlConnection connection, string database, ServerLogin login) - : base(connection, login.Name, $"FOR LOGIN {login.Name}", state: database, shouldCreate: true, shouldDrop: true) + : base(connection, login.Name, $"FOR LOGIN {login.Name}", state: database) { } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/NameIsVerbatim.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/NameIsVerbatim.cs new file mode 100644 index 0000000000..1ad15651e7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/NameIsVerbatim.cs @@ -0,0 +1,20 @@ +// 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; + +/// +/// Distinguishes a constructor that takes a caller-supplied name verbatim from the prefix-based +/// one, which would otherwise have an identical signature. +/// +/// +/// The two overloads differ only in how the string is interpreted — as a name or as a prefix to +/// generate one from — which the type system cannot express. This makes the private constructor +/// unambiguous without inventing a parameter that carries no meaning; the corresponding public +/// entry point is the WithName factory, where the distinction is stated in the name. +/// +internal enum NameIsVerbatim +{ + Yes +} diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ScalarFunction.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ScalarFunction.cs index 63dcf26607..4044241e76 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ScalarFunction.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ScalarFunction.cs @@ -22,12 +22,12 @@ public sealed class ScalarFunction : DatabaseObject /// The prefix for the function name. /// The SQL definition of the function, following the function name. public ScalarFunction(SqlConnection connection, string prefix, string definition) - : base(connection, GenerateLongName(prefix), definition, shouldCreate: true, shouldDrop: true) + : base(connection, GenerateLongName(prefix), definition) { } - private ScalarFunction(SqlConnection connection, string name, string definition, bool shouldCreate) - : base(connection, name, definition, shouldCreate, shouldDrop: true) + private ScalarFunction(SqlConnection connection, string name, string definition, NameIsVerbatim _) + : base(connection, name, definition) { } @@ -44,7 +44,7 @@ private ScalarFunction(SqlConnection connection, string name, string definition, /// The function name, already quoted/escaped by the caller if it needs to be. /// The SQL definition of the function, following the function name. public static ScalarFunction WithName(SqlConnection connection, string name, string definition) - => new(connection, name, definition, shouldCreate: true); + => new(connection, name, definition, NameIsVerbatim.Yes); protected override void CreateObject(string definition) { diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Schema.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Schema.cs index 8ccef04bcd..7f316520b5 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Schema.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Schema.cs @@ -19,21 +19,12 @@ public sealed class Schema : DatabaseObject /// The SQL connection used to interact with the database. /// The prefix for the schema name. public Schema(SqlConnection connection, string prefix) - : base(connection, GenerateLongName(prefix), definition: string.Empty, shouldCreate: true, shouldDrop: true) + : base(connection, GenerateLongName(prefix), definition: string.Empty) { } - /// - /// Distinguishes the verbatim-name constructor from the prefix-based one, which would - /// otherwise have an identical signature. - /// - private enum NameIsVerbatim - { - Yes - } - private Schema(SqlConnection connection, string name, NameIsVerbatim _) - : base(connection, name, definition: string.Empty, shouldCreate: true, shouldDrop: true) + : base(connection, name, definition: string.Empty) { } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs index ce264a9d14..721cad1b3b 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ServerLogin.cs @@ -26,7 +26,7 @@ public ServerLogin(SqlConnection connection, string namePrefix, string? defaultD } private ServerLogin(SqlConnection connection, string namePrefix, string password, string? defaultDatabase) - : base(connection, namePrefix, GenerateDefinition(password, defaultDatabase), state: password, shouldCreate: true, shouldDrop: true) + : base(connection, namePrefix, GenerateDefinition(password, defaultDatabase), state: password) { } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs index eec97fea70..826c2ccc10 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/StoredProcedure.cs @@ -21,12 +21,12 @@ public sealed class StoredProcedure : DatabaseObject /// The stored procedure name. Can begin with '#' or '##' to indicate a temporary procedure. /// The SQL definition of the stored procedure. public StoredProcedure(SqlConnection connection, string prefix, string definition) - : base(connection, GenerateLongName(prefix), definition, shouldCreate: true, shouldDrop: true) + : base(connection, GenerateLongName(prefix), definition) { } - private StoredProcedure(SqlConnection connection, string name, string definition, bool shouldCreate) - : base(connection, name, definition, shouldCreate, shouldDrop: true) + private StoredProcedure(SqlConnection connection, string name, string definition, NameIsVerbatim _) + : base(connection, name, definition) { } @@ -43,7 +43,7 @@ private StoredProcedure(SqlConnection connection, string name, string definition /// The procedure name, already quoted/escaped by the caller if it needs to be. /// The SQL definition of the stored procedure. public static StoredProcedure WithName(SqlConnection connection, string name, string definition) - => new(connection, name, definition, shouldCreate: true); + => new(connection, name, definition, NameIsVerbatim.Yes); protected override void CreateObject(string definition) { diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs index 1ea15d8a24..e5ff86a8ea 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs @@ -21,12 +21,12 @@ public sealed class Table : DatabaseObject /// The prefix for the table name. Can begin with '#' or '##' to indicate a temporary table. /// The SQL definition describing the structure of the table, including columns and data types. public Table(SqlConnection connection, string prefix, string definition) - : base(connection, GenerateLongName(prefix), definition, shouldCreate: true, shouldDrop: true) + : base(connection, GenerateLongName(prefix), definition) { } - private Table(SqlConnection connection, string name, string definition, bool shouldCreate) - : base(connection, name, definition, shouldCreate, shouldDrop: true) + private Table(SqlConnection connection, string name, string definition, NameIsVerbatim _) + : base(connection, name, definition) { } @@ -44,20 +44,7 @@ private Table(SqlConnection connection, string name, string definition, bool sho /// The table name, already quoted/escaped by the caller if it needs to be. /// The SQL definition describing the structure of the table, including columns and data types. public static Table WithName(SqlConnection connection, string name, string definition) - => new(connection, name, definition, shouldCreate: true); - - /// - /// Adopts an already-existing table so that it is dropped when the returned instance is - /// disposed. No table is created. - /// - /// - /// Useful when a table is created by other means (for example, by a helper that also populates - /// it, or over a different connection) but still needs deterministic cleanup. - /// - /// The SQL connection used to drop the table. - /// The table name, already quoted/escaped by the caller if it needs to be. - public static Table AdoptExisting(SqlConnection connection, string name) - => new(connection, name, definition: string.Empty, shouldCreate: false); + => new(connection, name, definition, NameIsVerbatim.Yes); protected override void CreateObject(string definition) { diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/UserDefinedType.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/UserDefinedType.cs index be68e649dd..95c9f8ebf0 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/UserDefinedType.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/UserDefinedType.cs @@ -21,7 +21,7 @@ public sealed class UserDefinedType : DatabaseObject /// The type name. /// The SQL definition of the type. public UserDefinedType(SqlConnection connection, string prefix, string definition) - : base(connection, "[dbo]." + GenerateLongName(prefix), definition, shouldCreate: true, shouldDrop: true) + : base(connection, "[dbo]." + GenerateLongName(prefix), definition) { } From 36a765cab5bc33bb160604a3a6f26f0a12d6bed6 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 26 Aug 2026 09:32:50 -0700 Subject: [PATCH 16/16] Add TemporalTable primitive; restore object adoption edwardneal pointed out that removing Table.AdoptExisting() dropped support for a real case: a temporal table's history table is created by the server as a side effect of CREATE TABLE, so it has no CREATE of its own but still has to be dropped. Rather than reinstate the shouldCreate flag, adoption is now expressed as a separate constructor selected by an ExistingObject discriminator, so no call site passes a bool whose meaning has to be looked up. shouldDrop stays gone; it was true everywhere. Adds the TemporalTable primitive edwardneal originally had in mind, which creates the main table, adopts the history table, and drops both in the required order (system versioning off, then period, then tables). Converts HiddenTargetColumn to use it, which also removes that test's leak vector: its finally block ran ALTER statements before the drops, so a failure there skipped both DROP TABLE calls. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2327ee86-547d-468a-815a-04146f5fd7a5 --- .../DatabaseObjects/DatabaseObject.cs | 23 +++++ .../DatabaseObjects/ExistingObject.cs | 20 +++++ .../Common/Fixtures/DatabaseObjects/Table.cs | 19 ++++ .../Fixtures/DatabaseObjects/TemporalTable.cs | 88 +++++++++++++++++++ .../BulkCopy/HiddenTargetColumn.cs | 53 +++++------ 5 files changed, 170 insertions(+), 33 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ExistingObject.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/TemporalTable.cs diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs index 8da00090e6..a93df61fca 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs @@ -56,6 +56,24 @@ protected DatabaseObject(SqlConnection connection, string name, string definitio } } + /// + /// Adopts an object that already exists, so that it is dropped when this instance is disposed. + /// Nothing is created. + /// + /// + /// Necessary for objects the server creates as a side effect of creating something else, which + /// therefore have no CREATE statement of their own but still have to be dropped. The temporal + /// history table behind is the motivating case. + /// + protected DatabaseObject(SqlConnection connection, string name, TState state, ExistingObject _) + { + Connection = connection; + State = state; + Name = name; + + EnsureConnectionOpen(); + } + private void EnsureConnectionOpen() { const int MaxWaits = 2; @@ -398,4 +416,9 @@ protected DatabaseObject(SqlConnection connection, string name, string definitio : base(connection, name, definition, state: null) { } + + protected DatabaseObject(SqlConnection connection, string name, ExistingObject adopt) + : base(connection, name, state: null, adopt) + { + } } diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ExistingObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ExistingObject.cs new file mode 100644 index 0000000000..93ea622ae8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/ExistingObject.cs @@ -0,0 +1,20 @@ +// 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; + +/// +/// Selects the constructor that adopts an object which already exists on the server, rather than +/// creating one. +/// +/// +/// The distinction — create this object, or take ownership of one that is already there — cannot be +/// expressed in the signature, since both take the same arguments. A discriminator states it at the +/// call site, which a bool would not; the corresponding public entry point is the +/// AdoptExisting factory. +/// +public enum ExistingObject +{ + Adopt +} diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs index e5ff86a8ea..b628c3b0fd 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/Table.cs @@ -30,6 +30,11 @@ private Table(SqlConnection connection, string name, string definition, NameIsVe { } + private Table(SqlConnection connection, string name, ExistingObject adopt) + : base(connection, name, adopt) + { + } + /// /// Creates a table using the caller-supplied name verbatim, instead of generating one. /// @@ -46,6 +51,20 @@ private Table(SqlConnection connection, string name, string definition, NameIsVe public static Table WithName(SqlConnection connection, string name, string definition) => new(connection, name, definition, NameIsVerbatim.Yes); + /// + /// Adopts a table that already exists, so that it is dropped when the returned instance is + /// disposed. No table is created. + /// + /// + /// For tables the server creates as a side effect of creating something else, and which + /// therefore have no CREATE statement of their own but must still be dropped — the history + /// table behind a being the motivating case. + /// + /// The SQL connection used to drop the table. + /// The table name, already quoted/escaped by the caller if it needs to be. + public static Table AdoptExisting(SqlConnection connection, string name) + => new(connection, name, ExistingObject.Adopt); + protected override void CreateObject(string definition) { using SqlCommand createCommand = new($"CREATE TABLE {Name} {definition}", Connection); diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/TemporalTable.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/TemporalTable.cs new file mode 100644 index 0000000000..6c225938b8 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/TemporalTable.cs @@ -0,0 +1,88 @@ +// 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; + +/// +/// A transient system-versioned (temporal) table, created at the start of its scope and dropped — +/// together with its history table — when disposed. +/// +/// +/// A temporal table cannot be dropped directly. System versioning has to be switched off first, +/// which severs the link to the history table and leaves that table behind as an ordinary one that +/// must then be dropped in its own right. That history table is created by the server as a side +/// effect, so it is adopted rather than created: this type +/// owns it and drops it, but never issues a CREATE for it. +/// +public sealed class TemporalTable : DatabaseObject +{ + /// + /// The history table backing this temporal table. Dropped along with it. + /// + public Table HistoryTable { get; } + + /// + /// Initializes a new system-versioned table and adopts the history table created alongside it. + /// + /// The SQL connection used to interact with the database. + /// The prefix for the table name. + /// + /// The prefix for the history table name. SYSTEM_VERSIONING requires a schema-qualified history + /// table, so the generated name is qualified with [dbo]. + /// + /// + /// The column definitions, including the PERIOD FOR SYSTEM_TIME clause, in parentheses. The + /// SYSTEM_VERSIONING option naming the history table is appended automatically. + /// + public TemporalTable(SqlConnection connection, string prefix, string historyPrefix, string columns) + : this(connection, GenerateLongName(prefix), $"[dbo].{GenerateLongName(historyPrefix)}", columns, NameIsVerbatim.Yes) + { + } + + private TemporalTable(SqlConnection connection, string name, string historyName, string columns, NameIsVerbatim _) + : base(connection, name, $"{columns} WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyName}))") + { + // Adopted only once the CREATE above has succeeded, since that statement is what brings the + // history table into existence. If it throws, the base constructor disposes this instance, + // and the null check in DropObject keeps that path safe. + HistoryTable = Table.AdoptExisting(connection, historyName); + } + + protected override void CreateObject(string definition) + { + using SqlCommand createCommand = new($"CREATE TABLE {Name} {definition}", Connection); + + createCommand.ExecuteNonQuery(); + } + + protected override void DropObject() + { + // NOTE: The name is passed to OBJECT_ID() as a parameter rather than being interpolated + // into a string literal, because it embeds Environment.UserName/MachineName (see + // DatabaseObject.GenerateLongName) and an apostrophe in either would break the batch. + // The identifier in ALTER/DROP TABLE is already bracket-quoted by GenerateLongName. + // + // Ordering is load-bearing: SYSTEM_VERSIONING must be switched off before the period can + // be dropped, and the period before the table itself. + using (SqlCommand dropCommand = new($""" + IF (OBJECT_ID(@name) IS NOT NULL) + BEGIN + ALTER TABLE {Name} SET (SYSTEM_VERSIONING = OFF); + ALTER TABLE {Name} DROP PERIOD FOR SYSTEM_TIME; + DROP TABLE {Name}; + END + """, Connection)) + { + dropCommand.Parameters.AddWithValue("@name", Name); + + dropCommand.ExecuteNonQuery(); + } + + // Only reachable once versioning is off, which is what turns the history table back into an + // ordinary droppable one. Null both during the pre-emptive drop the base constructor runs + // before CREATE, and while it unwinds a failed CREATE — in neither case is there an adopted + // history table to drop. + HistoryTable?.Dispose(); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/HiddenTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/HiddenTargetColumn.cs index 3dcf609521..08c7204127 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/HiddenTargetColumn.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/BulkCopy/HiddenTargetColumn.cs @@ -4,6 +4,7 @@ using System.Data.Common; using Microsoft.Data.SqlClient.ManualTesting.Tests; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects; using Xunit; namespace Microsoft.Data.SqlClient.ManualTests.BulkCopy @@ -15,16 +16,13 @@ public class HiddenTargetColumn public void WriteToServer_CopyToHiddenTargetColumn_ThrowsSqlException() { string connectionString = DataTestUtility.TCPConnectionString; - string destinationTable = DataTestUtility.GetShortName("HiddenTargetColumn"); - string destinationHistoryTable = DataTestUtility.GetShortName("HiddenTargetColumn_History"); using (SqlConnection dstConn = new SqlConnection(connectionString)) { dstConn.Open(); - try - { - DataTestUtility.CreateTable(dstConn, destinationTable, $""" + // Drops the history table as well as the table itself, in the required order. + using TemporalTable destinationTable = new(dstConn, "HiddenTargetColumn", "HiddenTargetColumn_History", """ ( Column1 int primary key not null, Column2 nvarchar(10) not null, @@ -33,42 +31,31 @@ Column2 nvarchar(10) not null, ValidTo datetime2 generated always as row end hidden not null, period for system_time (ValidFrom, ValidTo) ) -with (system_versioning = on(history_table = dbo.{destinationHistoryTable})); """); - using (SqlConnection srcConn = new SqlConnection(connectionString)) - using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, FirstName, LastName, HireDate, sysdatetime() as CurrentDate from employees", srcConn)) - { - srcConn.Open(); + using (SqlConnection srcConn = new SqlConnection(connectionString)) + using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, FirstName, LastName, HireDate, sysdatetime() as CurrentDate from employees", srcConn)) + { + srcConn.Open(); - using (DbDataReader reader = srcCmd.ExecuteReader()) - using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) - { - bulkcopy.DestinationTableName = destinationTable; - SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; + using (DbDataReader reader = srcCmd.ExecuteReader()) + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = destinationTable.Name; + SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings; - ColumnMappings.Add("EmployeeID", "Column1"); - ColumnMappings.Add("LastName", "Column2"); - ColumnMappings.Add("FirstName", "Employee's First Name"); - ColumnMappings.Add("HireDate", "ValidFrom"); - ColumnMappings.Add("CurrentDate", "ValidTo"); + ColumnMappings.Add("EmployeeID", "Column1"); + ColumnMappings.Add("LastName", "Column2"); + ColumnMappings.Add("FirstName", "Employee's First Name"); + ColumnMappings.Add("HireDate", "ValidFrom"); + ColumnMappings.Add("CurrentDate", "ValidTo"); - SqlException sqlEx = Assert.Throws(() => bulkcopy.WriteToServer(reader)); + SqlException sqlEx = Assert.Throws(() => bulkcopy.WriteToServer(reader)); - Assert.Equal(13536, sqlEx.Number); - Assert.StartsWith("Cannot insert an explicit value into a GENERATED ALWAYS column in table", sqlEx.Message); - } + Assert.Equal(13536, sqlEx.Number); + Assert.StartsWith("Cannot insert an explicit value into a GENERATED ALWAYS column in table", sqlEx.Message); } } - finally - { - DataTestUtility.RunNonQuery(connectionString, $""" -alter table {destinationTable} set (system_versioning = off); -alter table {destinationTable} drop period for system_time; -"""); - DataTestUtility.DropTable(dstConn, destinationTable); - DataTestUtility.DropTable(dstConn, destinationHistoryTable); - } } } }