From f255ee58f4fa56f429ee01c72e87cd081a9c7635 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:21:56 -0300 Subject: [PATCH 1/2] AE tests: create ConversionTests keys once per class to slow error 3807 identifier exhaustion (#4478) --- .../AlwaysEncrypted/ConversionTests.cs | 46 ++------- .../TestFixtures/ConversionTestFixture.cs | 95 +++++++++++++++++++ 2 files changed, 105 insertions(+), 36 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/ConversionTestFixture.cs diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ConversionTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ConversionTests.cs index f54609e2c0..00c0980e71 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ConversionTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ConversionTests.cs @@ -11,14 +11,12 @@ using System.Linq; using System.Reflection; using System.Text; -using System.Security.Cryptography.X509Certificates; using Xunit; using Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; -using Microsoft.Data.SqlClient.Tests.Common.Fixtures; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted { - public sealed class ConversionTests : IDisposable, IClassFixture + public sealed class ConversionTests : IDisposable, IClassFixture { private const string IdentityColumnName = "IdentityColumn"; @@ -29,9 +27,7 @@ public sealed class ConversionTests : IDisposable, IClassFixture _databaseObjects = new List(); + private readonly ColumnEncryptionKey columnEncryptionKey; private List _tables = new(); private class ColumnMetaData @@ -52,33 +48,12 @@ public ColumnMetaData(SqlDbType columnType, int columnSize, int precision, int s public bool UseMax { get; set; } } - public ConversionTests(ColumnMasterKeyCertificateFixture fixture) + public ConversionTests(ConversionTestFixture fixture) { - X509Certificate2 certificate = fixture.ColumnMasterKeyCertificate; - ColumnMasterKey columnMasterKey1 = new CspColumnMasterKey( - DatabaseHelper.GenerateUniqueName("CMK"), - certificate.Thumbprint, - certStoreProvider, - DataTestUtility.EnclaveEnabled); - _databaseObjects.Add(columnMasterKey1); - - columnEncryptionKey = new ColumnEncryptionKey( - DatabaseHelper.GenerateUniqueName("CEK"), - columnMasterKey1, - certStoreProvider); - _databaseObjects.Add(columnEncryptionKey); - - foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) - { - var connectionString = new SqlConnectionStringBuilder(connectionStr); - connectionString.ConnectTimeout = Math.Max(connectionString.ConnectTimeout, 30); // The AE tests often fail with a connect timeout in this constructor. Making sure we have a reasonable timeout. - - using (SqlConnection sqlConnection = new SqlConnection(connectionString.ConnectionString)) - { - sqlConnection.Open(); - _databaseObjects.ForEach(o => o.Create(sqlConnection)); - } - } + // The Column Master Key and Column Encryption Key are created once per test class by + // the fixture, rather than once per test case, to avoid rapidly exhausting SQL Server's + // per-database identifier allocator (error 3807) on long-lived shared databases. + columnEncryptionKey = fixture.ColumnEncryptionKey; } [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE))] @@ -621,7 +596,7 @@ private void AdjustSizePrecisionAndScale(ref ColumnMetaData largeColumnMeta, ref if (TypeHasSize(largeColumnMeta.ColumnType)) { - // 20% of the time use (max) as the length. + // 20% of the time use (max) as the length. largeColumnMeta.UseMax = (largeColumnMeta.ColumnType is SqlDbType.VarChar || largeColumnMeta.ColumnType is SqlDbType.NVarChar || largeColumnMeta.ColumnType is SqlDbType.VarBinary) && @@ -683,7 +658,7 @@ largeColumnMeta.ColumnType is SqlDbType.NVarChar || { smallColumnMeta.Precision = 0; - // For Time / DateTime2 / DateTimeOffset types, actual scale is set to 7 when parameter.scale is zero. + // For Time / DateTime2 / DateTimeOffset types, actual scale is set to 7 when parameter.scale is zero. // Active Issue in SQLParameter.cs when user wants to specify zero as the actual scale. smallColumnMeta.Scale = random.Next(minScale, largeColumnMeta.Scale); } @@ -1354,7 +1329,7 @@ private void SetParamSizeScalePrecision(ref SqlParameter param, ColumnMetaData c public void Dispose() { - _databaseObjects.Reverse(); + // Only the per-test tables are dropped here; the CMK/CEK are owned by the class fixture. foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) { using (SqlConnection sqlConnection = new SqlConnection(connectionStr)) @@ -1364,7 +1339,6 @@ public void Dispose() { DropTableIfExists(sqlConnection, table); } - _databaseObjects.ForEach(o => o.Drop(sqlConnection)); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/ConversionTestFixture.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/ConversionTestFixture.cs new file mode 100644 index 0000000000..abae589981 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/ConversionTestFixture.cs @@ -0,0 +1,95 @@ +// 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 Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; +using Microsoft.Data.SqlClient.Tests.Common.Fixtures; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted +{ + /// + /// Class fixture for . Creates a single Column Master Key (CMK) and + /// Column Encryption Key (CEK) once for the whole test class instead of once per test case. + /// + /// + /// xUnit constructs a test class instance for every test case, so creating the keys in the + /// constructor issued a CREATE COLUMN MASTER KEY and + /// CREATE COLUMN ENCRYPTION KEY for each of the dozens of cases in the class. On a + /// long-lived shared database that rapidly advances SQL Server's internal per-database + /// identifier allocator, which is never reclaimed on DROP, eventually causing error 3807 + /// ("Create failed because all available identifiers have been exhausted"). Creating the keys + /// once per class via reduces that churn by roughly + /// two orders of magnitude. + /// + public sealed class ConversionTestFixture : ColumnMasterKeyCertificateFixture + { + private readonly ColumnMasterKey _columnMasterKey; + private readonly SqlColumnEncryptionCertificateStoreProvider _certStoreProvider = + new SqlColumnEncryptionCertificateStoreProvider(); + + /// + /// The single Column Encryption Key shared by all cases in . + /// + public ColumnEncryptionKey ColumnEncryptionKey { get; } + + public ConversionTestFixture() + { + _columnMasterKey = new CspColumnMasterKey( + DatabaseHelper.GenerateUniqueName("CMK"), + ColumnMasterKeyCertificate.Thumbprint, + _certStoreProvider, + DataTestUtility.EnclaveEnabled); + + ColumnEncryptionKey = new ColumnEncryptionKey( + DatabaseHelper.GenerateUniqueName("CEK"), + _columnMasterKey, + _certStoreProvider); + + 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); + } + } + + protected override void Dispose(bool disposing) + { + try + { + foreach (string connectionStr in DataTestUtility.AEConnStringsSetup) + { + SqlConnectionStringBuilder connectionString = new SqlConnectionStringBuilder(connectionStr); + // Match the constructor's minimum timeout; AE teardown is prone to connect timeouts, + // and skipping cleanup would leave the shared CMK/CEK behind. + connectionString.ConnectTimeout = Math.Max(connectionString.ConnectTimeout, 30); + + // Key drops are best-effort: a failure here must not prevent the base fixture + // from removing the test certificate from the certificate store (see finally). + try + { + using SqlConnection sqlConnection = new SqlConnection(connectionString.ConnectionString); + sqlConnection.Open(); + ColumnEncryptionKey.Drop(sqlConnection); + _columnMasterKey.Drop(sqlConnection); + } + catch (Exception ex) + { + Console.WriteLine( + $"ConversionTestFixture: failed to drop keys on '{connectionString.DataSource}': {ex.Message}"); + } + } + } + finally + { + base.Dispose(disposing); + } + } + } +} From e87af5a589b132b15eff1eefa3876f62fed8c522 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:10:24 -0300 Subject: [PATCH 2/2] Add conversion test fixture to AE compile list --- .../ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj index 3d9418992e..3d77be49d9 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj @@ -306,6 +306,7 @@ +