From fbdcc90ad137e4c4ceeed5d54e48dd05c8ce209e Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Mon, 15 Dec 2025 19:32:52 +0530 Subject: [PATCH 01/17] Backporting SqlDataAdapter nullreference exception fix --- .../Microsoft/Data/SqlClient/SqlCommand.cs | 2 +- .../Microsoft/Data/SqlClient/SqlCommand.cs | 2 +- .../SqlDataAdapterBatchUpdateTests.cs | 255 ++++++++++++++++++ ....Data.SqlClient.ManualTesting.Tests.csproj | 7 + 4 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index 0f7d4c164c..0497cd76b4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -4180,7 +4180,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, { // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_RPCList[i].systemParams.Length > 1) + if (_RPCList[i].systemParams != null && _RPCList[i].systemParams.Length > 1) { _RPCList[i].needsFetchParameterEncryptionMetadata = true; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index 96ce941f39..5418fc22f2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -4304,7 +4304,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, { // In _batchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_RPCList[i].systemParams.Length > 1) + if (_RPCList[i].systemParams != null && _RPCList[i].systemParams.Length > 1) { _RPCList[i].needsFetchParameterEncryptionMetadata = true; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs new file mode 100644 index 0000000000..0622f4b9f6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -0,0 +1,255 @@ +// 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 System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted +{ + public sealed class SqlDataAdapterBatchUpdateTests : IClassFixture, IDisposable + { + private readonly SQLSetupStrategy _fixture; + private readonly Dictionary tableNames = new(); + + public SqlDataAdapterBatchUpdateTests(SQLSetupStrategyCertStoreProvider context) + { + _fixture = context; + + // Provide table names to mirror repo patterns. + // If your fixture already exposes specific names for BuyerSeller and procs, wire them here. + // Otherwise use literal names as below. + tableNames["BuyerSeller"] = "BuyerSeller"; + tableNames["ProcInsertBuyerSeller"] = "InsertBuyerSeller"; + tableNames["ProcUpdateBuyerSeller"] = "UpdateBuyerSeller"; + } + + // ---------- TESTS ---------- + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] + [ClassData(typeof(AEConnectionStringProvider))] + public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connectionString) + { + // Arrange + // Ensure baseline rows exist + TruncateTables("BuyerSeller", connectionString); + PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + (1, "123-45-6789", "987-65-4321"), + (2, "234-56-7890", "876-54-3210"), + (3, "345-67-8901", "765-43-2109"), + (4, "456-78-9012", "654-32-1098"), + }, connectionString); + + using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + await conn.OpenAsync(); + + using var adapter = CreateAdapter(conn, updateBatchSize: 10); // failure repro: > 1 + var dataTable = BuildBuyerSellerDataTable(); + LoadCurrentRowsIntoDataTable(dataTable, conn); + + // Mutate values for update + MutateForUpdate(dataTable); + + // Act - With batch updates (UpdateBatchSize > 1), this previously threw NullReferenceException due to null systemParams in batch RPC mode + var updated = await Task.Run(() => adapter.Update(dataTable)); + + // Assert + Assert.Equal(dataTable.Rows.Count, updated); + + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] + [ClassData(typeof(AEConnectionStringProvider))] + public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) + { + // Arrange + TruncateTables("BuyerSeller", connectionString); + PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + (1, "123-45-6789", "987-65-4321"), + (2, "234-56-7890", "876-54-3210"), + (3, "345-67-8901", "765-43-2109"), + (4, "456-78-9012", "654-32-1098"), + }, connectionString); + + using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + await conn.OpenAsync(); + + using var adapter = CreateAdapter(conn, updateBatchSize: 1); // success path + var dataTable = BuildBuyerSellerDataTable(); + LoadCurrentRowsIntoDataTable(dataTable, conn); + + MutateForUpdate(dataTable); + + // Act (should not throw) + var updatedRows = await Task.Run(() => adapter.Update(dataTable)); + + // Assert + Assert.Equal(dataTable.Rows.Count, updatedRows); + + } + + // ---------- HELPERS ---------- + + private SqlDataAdapter CreateAdapter(SqlConnection connection, int updateBatchSize) + { + // Insert + var insertCmd = new SqlCommand(tableNames["ProcInsertBuyerSeller"], connection) + { + CommandType = CommandType.StoredProcedure + }; + insertCmd.Parameters.AddRange(new[] + { + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + }); + insertCmd.UpdatedRowSource = UpdateRowSource.None; + + // Update + var updateCmd = new SqlCommand(tableNames["ProcUpdateBuyerSeller"], connection) + { + CommandType = CommandType.StoredProcedure + }; + updateCmd.Parameters.AddRange(new[] + { + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + }); + updateCmd.UpdatedRowSource = UpdateRowSource.None; + + return new SqlDataAdapter + { + InsertCommand = insertCmd, + UpdateCommand = updateCmd, + UpdateBatchSize = updateBatchSize + }; + } + + private DataTable BuildBuyerSellerDataTable() + { + var dt = new DataTable(tableNames["BuyerSeller"]); + dt.Columns.AddRange(new[] + { + new DataColumn("BuyerSellerID", typeof(int)), + new DataColumn("SSN1", typeof(string)), + new DataColumn("SSN2", typeof(string)), + }); + dt.PrimaryKey = new[] { dt.Columns["BuyerSellerID"] }; + return dt; + } + + private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) + { + using var cmd = new SqlCommand($"SELECT BuyerSellerID, SSN1, SSN2 FROM [dbo].[{tableNames["BuyerSeller"]}] ORDER BY BuyerSellerID", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + dt.Rows.Add(reader.GetInt32(0), reader.GetString(1), reader.GetString(2)); + } + } + + private void MutateForUpdate(DataTable dt) + { + int i = 0; + var fixedTime = new DateTime(2023, 01, 01, 12, 34, 56); // Use any fixed value + string timeStr = fixedTime.ToString("HHmm"); + foreach (DataRow row in dt.Rows) + { + i++; + row["SSN1"] = $"{i:000}-11-{timeStr}"; + row["SSN2"] = $"{i:000}-22-{timeStr}"; + } + } + + internal void TruncateTables(string tableName, string connectionString) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + } + + internal void ExecuteQuery(SqlConnection connection, string commandText) + { + // Mirror AE-enabled command execution style used in repo tests + using var cmd = new SqlCommand( + commandText, + connection: connection, + transaction: null, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled); + cmd.ExecuteNonQuery(); + } + + internal void PopulateTable(string tableName, (int id, string s1, string s2)[] rows, string connectionString) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + + foreach (var (id, s1, s2) in rows) + { + using var cmd = new SqlCommand( + $@"INSERT INTO [dbo].[{tableNames[tableName]}] (BuyerSellerID, SSN1, SSN2) VALUES (@id, @s1, @s2)", + connection, + null, + SqlCommandColumnEncryptionSetting.Enabled); + + cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int) { Value = id }); + cmd.Parameters.Add(new SqlParameter("@s1", SqlDbType.VarChar, 255) { Value = s1 }); + cmd.Parameters.Add(new SqlParameter("@s2", SqlDbType.VarChar, 255) { Value = s2 }); + + cmd.ExecuteNonQuery(); + } + } + + public string GetOpenConnectionString(string baseConnectionString, bool encryptionEnabled) + { + var builder = new SqlConnectionStringBuilder(baseConnectionString) + { + // TrustServerCertificate can be set based on environment; mirror repo’s AE toggling idiom + ColumnEncryptionSetting = encryptionEnabled + ? SqlConnectionColumnEncryptionSetting.Enabled + : SqlConnectionColumnEncryptionSetting.Disabled + }; + return builder.ToString(); + } + + internal void SilentRunCommand(string commandText, SqlConnection connection) + { + try + { ExecuteQuery(connection, commandText); } + catch (SqlException ex) + { + // Only swallow "object does not exist" (error 208), log others + bool onlyObjectNotExist = true; + foreach (SqlError err in ex.Errors) + { + if (err.Number != 208) + { + onlyObjectNotExist = false; + break; + } + } + if (!onlyObjectNotExist) + { + Console.WriteLine($"SilentRunCommand: Unexpected SqlException during cleanup: {ex}"); + } + // Swallow all exceptions, but log unexpected ones + } + } + + public void Dispose() + { + foreach (string connectionString in DataTestUtility.AEConnStringsSetup) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + SilentRunCommand($"DELETE FROM [dbo].[{tableNames["BuyerSeller"]}]", connection); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 627d123834..db713e9121 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -33,6 +33,9 @@ + + + @@ -278,6 +281,7 @@ + @@ -387,4 +391,7 @@ xunit.runner.json + + + From d98ee9018118693025e4bd2cf55da8b968a4e458 Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Mon, 15 Dec 2025 19:49:55 +0530 Subject: [PATCH 02/17] Backporting SqlDataAdapter null reference exception to MDS v6.1 --- .../Microsoft/Data/SqlClient/SqlCommand.cs | 2 +- .../Microsoft/Data/SqlClient/SqlCommand.cs | 2 +- .../SqlDataAdapterBatchUpdateTests.cs | 255 ++++++++++++++++++ ....Data.SqlClient.ManualTesting.Tests.csproj | 4 + 4 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index 0f7d4c164c..0497cd76b4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -4180,7 +4180,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, { // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_RPCList[i].systemParams.Length > 1) + if (_RPCList[i].systemParams != null && _RPCList[i].systemParams.Length > 1) { _RPCList[i].needsFetchParameterEncryptionMetadata = true; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index 96ce941f39..5418fc22f2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -4304,7 +4304,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, { // In _batchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_RPCList[i].systemParams.Length > 1) + if (_RPCList[i].systemParams != null && _RPCList[i].systemParams.Length > 1) { _RPCList[i].needsFetchParameterEncryptionMetadata = true; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs new file mode 100644 index 0000000000..0622f4b9f6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -0,0 +1,255 @@ +// 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 System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted +{ + public sealed class SqlDataAdapterBatchUpdateTests : IClassFixture, IDisposable + { + private readonly SQLSetupStrategy _fixture; + private readonly Dictionary tableNames = new(); + + public SqlDataAdapterBatchUpdateTests(SQLSetupStrategyCertStoreProvider context) + { + _fixture = context; + + // Provide table names to mirror repo patterns. + // If your fixture already exposes specific names for BuyerSeller and procs, wire them here. + // Otherwise use literal names as below. + tableNames["BuyerSeller"] = "BuyerSeller"; + tableNames["ProcInsertBuyerSeller"] = "InsertBuyerSeller"; + tableNames["ProcUpdateBuyerSeller"] = "UpdateBuyerSeller"; + } + + // ---------- TESTS ---------- + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] + [ClassData(typeof(AEConnectionStringProvider))] + public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connectionString) + { + // Arrange + // Ensure baseline rows exist + TruncateTables("BuyerSeller", connectionString); + PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + (1, "123-45-6789", "987-65-4321"), + (2, "234-56-7890", "876-54-3210"), + (3, "345-67-8901", "765-43-2109"), + (4, "456-78-9012", "654-32-1098"), + }, connectionString); + + using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + await conn.OpenAsync(); + + using var adapter = CreateAdapter(conn, updateBatchSize: 10); // failure repro: > 1 + var dataTable = BuildBuyerSellerDataTable(); + LoadCurrentRowsIntoDataTable(dataTable, conn); + + // Mutate values for update + MutateForUpdate(dataTable); + + // Act - With batch updates (UpdateBatchSize > 1), this previously threw NullReferenceException due to null systemParams in batch RPC mode + var updated = await Task.Run(() => adapter.Update(dataTable)); + + // Assert + Assert.Equal(dataTable.Rows.Count, updated); + + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] + [ClassData(typeof(AEConnectionStringProvider))] + public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) + { + // Arrange + TruncateTables("BuyerSeller", connectionString); + PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + (1, "123-45-6789", "987-65-4321"), + (2, "234-56-7890", "876-54-3210"), + (3, "345-67-8901", "765-43-2109"), + (4, "456-78-9012", "654-32-1098"), + }, connectionString); + + using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + await conn.OpenAsync(); + + using var adapter = CreateAdapter(conn, updateBatchSize: 1); // success path + var dataTable = BuildBuyerSellerDataTable(); + LoadCurrentRowsIntoDataTable(dataTable, conn); + + MutateForUpdate(dataTable); + + // Act (should not throw) + var updatedRows = await Task.Run(() => adapter.Update(dataTable)); + + // Assert + Assert.Equal(dataTable.Rows.Count, updatedRows); + + } + + // ---------- HELPERS ---------- + + private SqlDataAdapter CreateAdapter(SqlConnection connection, int updateBatchSize) + { + // Insert + var insertCmd = new SqlCommand(tableNames["ProcInsertBuyerSeller"], connection) + { + CommandType = CommandType.StoredProcedure + }; + insertCmd.Parameters.AddRange(new[] + { + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + }); + insertCmd.UpdatedRowSource = UpdateRowSource.None; + + // Update + var updateCmd = new SqlCommand(tableNames["ProcUpdateBuyerSeller"], connection) + { + CommandType = CommandType.StoredProcedure + }; + updateCmd.Parameters.AddRange(new[] + { + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + }); + updateCmd.UpdatedRowSource = UpdateRowSource.None; + + return new SqlDataAdapter + { + InsertCommand = insertCmd, + UpdateCommand = updateCmd, + UpdateBatchSize = updateBatchSize + }; + } + + private DataTable BuildBuyerSellerDataTable() + { + var dt = new DataTable(tableNames["BuyerSeller"]); + dt.Columns.AddRange(new[] + { + new DataColumn("BuyerSellerID", typeof(int)), + new DataColumn("SSN1", typeof(string)), + new DataColumn("SSN2", typeof(string)), + }); + dt.PrimaryKey = new[] { dt.Columns["BuyerSellerID"] }; + return dt; + } + + private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) + { + using var cmd = new SqlCommand($"SELECT BuyerSellerID, SSN1, SSN2 FROM [dbo].[{tableNames["BuyerSeller"]}] ORDER BY BuyerSellerID", conn); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + dt.Rows.Add(reader.GetInt32(0), reader.GetString(1), reader.GetString(2)); + } + } + + private void MutateForUpdate(DataTable dt) + { + int i = 0; + var fixedTime = new DateTime(2023, 01, 01, 12, 34, 56); // Use any fixed value + string timeStr = fixedTime.ToString("HHmm"); + foreach (DataRow row in dt.Rows) + { + i++; + row["SSN1"] = $"{i:000}-11-{timeStr}"; + row["SSN2"] = $"{i:000}-22-{timeStr}"; + } + } + + internal void TruncateTables(string tableName, string connectionString) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + } + + internal void ExecuteQuery(SqlConnection connection, string commandText) + { + // Mirror AE-enabled command execution style used in repo tests + using var cmd = new SqlCommand( + commandText, + connection: connection, + transaction: null, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled); + cmd.ExecuteNonQuery(); + } + + internal void PopulateTable(string tableName, (int id, string s1, string s2)[] rows, string connectionString) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + + foreach (var (id, s1, s2) in rows) + { + using var cmd = new SqlCommand( + $@"INSERT INTO [dbo].[{tableNames[tableName]}] (BuyerSellerID, SSN1, SSN2) VALUES (@id, @s1, @s2)", + connection, + null, + SqlCommandColumnEncryptionSetting.Enabled); + + cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int) { Value = id }); + cmd.Parameters.Add(new SqlParameter("@s1", SqlDbType.VarChar, 255) { Value = s1 }); + cmd.Parameters.Add(new SqlParameter("@s2", SqlDbType.VarChar, 255) { Value = s2 }); + + cmd.ExecuteNonQuery(); + } + } + + public string GetOpenConnectionString(string baseConnectionString, bool encryptionEnabled) + { + var builder = new SqlConnectionStringBuilder(baseConnectionString) + { + // TrustServerCertificate can be set based on environment; mirror repo’s AE toggling idiom + ColumnEncryptionSetting = encryptionEnabled + ? SqlConnectionColumnEncryptionSetting.Enabled + : SqlConnectionColumnEncryptionSetting.Disabled + }; + return builder.ToString(); + } + + internal void SilentRunCommand(string commandText, SqlConnection connection) + { + try + { ExecuteQuery(connection, commandText); } + catch (SqlException ex) + { + // Only swallow "object does not exist" (error 208), log others + bool onlyObjectNotExist = true; + foreach (SqlError err in ex.Errors) + { + if (err.Number != 208) + { + onlyObjectNotExist = false; + break; + } + } + if (!onlyObjectNotExist) + { + Console.WriteLine($"SilentRunCommand: Unexpected SqlException during cleanup: {ex}"); + } + // Swallow all exceptions, but log unexpected ones + } + } + + public void Dispose() + { + foreach (string connectionString in DataTestUtility.AEConnStringsSetup) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + SilentRunCommand($"DELETE FROM [dbo].[{tableNames["BuyerSeller"]}]", connection); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 627d123834..43574100b7 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -12,6 +12,9 @@ $(BinFolder)$(Configuration).$(Platform).$(AssemblyName) true + + + @@ -278,6 +281,7 @@ + From aed9e25952d21cd827fb84bcb7388bffd4af4a23 Mon Sep 17 00:00:00 2001 From: priyankatiwari08 Date: Tue, 16 Dec 2025 15:45:31 +0530 Subject: [PATCH 03/17] Remove SqlDataAdapterBatchUpdateTests from project Removed unused SqlDataAdapterBatchUpdateTests.cs file from project. --- .../Microsoft.Data.SqlClient.ManualTesting.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 43574100b7..9a46642758 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -281,7 +281,6 @@ - From 328fa355aa36c9cb7e3a4e52796267f8c5fc5626 Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Tue, 16 Dec 2025 16:15:49 +0530 Subject: [PATCH 04/17] Fixing compilation for SqlDataAdapterBatchUpdateTests.cs --- .../Microsoft.Data.SqlClient.ManualTesting.Tests.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 43574100b7..36754cc234 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -12,9 +12,6 @@ $(BinFolder)$(Configuration).$(Platform).$(AssemblyName) true - - - @@ -37,6 +34,7 @@ + @@ -281,7 +279,6 @@ - @@ -391,4 +388,7 @@ xunit.runner.json + + + From b78f299599e90e0c13872a6d816915ec13177c43 Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Tue, 16 Dec 2025 16:17:50 +0530 Subject: [PATCH 05/17] fix --- .../Microsoft.Data.SqlClient.ManualTesting.Tests.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 36754cc234..d949316bdb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -388,7 +388,4 @@ xunit.runner.json - - - From c9677a8e50c1819ad3b45e60ba8b0ce0fb6e71a4 Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Tue, 16 Dec 2025 17:21:34 +0530 Subject: [PATCH 06/17] changes per copilot's comment on PR --- .../SqlDataAdapterBatchUpdateTests.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index 0622f4b9f6..53e4dc9f83 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -37,6 +37,7 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti { // Arrange // Ensure baseline rows exist + EnsureBuyerSellerObjectsExist(connectionString); TruncateTables("BuyerSeller", connectionString); PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), @@ -68,6 +69,7 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) { // Arrange + EnsureBuyerSellerObjectsExist(connectionString); TruncateTables("BuyerSeller", connectionString); PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), @@ -157,7 +159,7 @@ private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) private void MutateForUpdate(DataTable dt) { int i = 0; - var fixedTime = new DateTime(2023, 01, 01, 12, 34, 56); // Use any fixed value + var fixedTime = new DateTime(2000, 01, 01, 12, 34, 56); string timeStr = fixedTime.ToString("HHmm"); foreach (DataRow row in dt.Rows) { @@ -251,5 +253,45 @@ public void Dispose() SilentRunCommand($"DELETE FROM [dbo].[{tableNames["BuyerSeller"]}]", connection); } } + private void EnsureBuyerSellerObjectsExist(string connectionString) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + + // Create table if not exists + SilentRunCommand(@" + IF OBJECT_ID('dbo.BuyerSeller', 'U') IS NULL + CREATE TABLE [dbo].[BuyerSeller] ( + [BuyerSellerID] INT PRIMARY KEY, + [SSN1] VARCHAR(255), + [SSN2] VARCHAR(255) + )", connection); + + // Create Insert proc if not exists + SilentRunCommand(@" + IF OBJECT_ID('dbo.InsertBuyerSeller', 'P') IS NULL + EXEC('CREATE PROCEDURE [dbo].[InsertBuyerSeller] + @BuyerSellerID INT, + @SSN1 VARCHAR(255), + @SSN2 VARCHAR(255) + AS + INSERT INTO [dbo].[BuyerSeller] (BuyerSellerID, SSN1, SSN2) + VALUES (@BuyerSellerID, @SSN1, @SSN2)') + ", connection); + + // Create Update proc if not exists + SilentRunCommand(@" + IF OBJECT_ID('dbo.UpdateBuyerSeller', 'P') IS NULL + EXEC('CREATE PROCEDURE [dbo].[UpdateBuyerSeller] + @BuyerSellerID INT, + @SSN1 VARCHAR(255), + @SSN2 VARCHAR(255) + AS + UPDATE [dbo].[BuyerSeller] + SET SSN1 = @SSN1, SSN2 = @SSN2 + WHERE BuyerSellerID = @BuyerSellerID') + ", connection); + + } } } From c2dde417863698fca00bffffdbe19d250f1ef85d Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Tue, 16 Dec 2025 21:34:53 +0530 Subject: [PATCH 07/17] Making changes to fix issue with enclave in pipeline --- .../SqlDataAdapterBatchUpdateTests.cs | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index 0622f4b9f6..70bf2cb9e3 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -37,6 +37,7 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti { // Arrange // Ensure baseline rows exist + EnsureBuyerSellerObjectsExist(connectionString); TruncateTables("BuyerSeller", connectionString); PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), @@ -68,6 +69,7 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) { // Arrange + EnsureBuyerSellerObjectsExist(connectionString); TruncateTables("BuyerSeller", connectionString); PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), @@ -171,7 +173,15 @@ internal void TruncateTables(string tableName, string connectionString) { using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + try + { + SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + } + catch + { + // Fallback to DELETE if TRUNCATE fails (e.g., due to FK constraints) + SilentRunCommand($@"DELETE FROM [dbo].[{tableNames[tableName]}]", connection); + } } internal void ExecuteQuery(SqlConnection connection, string commandText) @@ -251,5 +261,44 @@ public void Dispose() SilentRunCommand($"DELETE FROM [dbo].[{tableNames["BuyerSeller"]}]", connection); } } + private void EnsureBuyerSellerObjectsExist(string connectionString) + { + using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + connection.Open(); + + // Create table if not exists + SilentRunCommand(@" + IF OBJECT_ID('dbo.BuyerSeller', 'U') IS NULL + CREATE TABLE [dbo].[BuyerSeller] ( + [BuyerSellerID] INT PRIMARY KEY, + [SSN1] VARCHAR(255), + [SSN2] VARCHAR(255) + )", connection); + + // Create Insert proc if not exists + SilentRunCommand(@" + IF OBJECT_ID('dbo.InsertBuyerSeller', 'P') IS NULL + EXEC('CREATE PROCEDURE [dbo].[InsertBuyerSeller] + @BuyerSellerID INT, + @SSN1 VARCHAR(255), + @SSN2 VARCHAR(255) + AS + INSERT INTO [dbo].[BuyerSeller] (BuyerSellerID, SSN1, SSN2) + VALUES (@BuyerSellerID, @SSN1, @SSN2)') + ", connection); + + // Create Update proc if not exists + SilentRunCommand(@" + IF OBJECT_ID('dbo.UpdateBuyerSeller', 'P') IS NULL + EXEC('CREATE PROCEDURE [dbo].[UpdateBuyerSeller] + @BuyerSellerID INT, + @SSN1 VARCHAR(255), + @SSN2 VARCHAR(255) + AS + UPDATE [dbo].[BuyerSeller] + SET SSN1 = @SSN1, SSN2 = @SSN2 + WHERE BuyerSellerID = @BuyerSellerID') + ", connection); + } } } From 560d2c6f9c2db2e70e065d2aa5b58734ba6f31a4 Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Tue, 16 Dec 2025 21:37:20 +0530 Subject: [PATCH 08/17] . --- .../AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index 53e4dc9f83..6c90dd6724 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -173,7 +173,15 @@ internal void TruncateTables(string tableName, string connectionString) { using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + try + { + SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + } + catch + { + // Fallback to DELETE if TRUNCATE fails (e.g., due to FK constraints) + SilentRunCommand($@"DELETE FROM [dbo].[{tableNames[tableName]}]", connection); + } } internal void ExecuteQuery(SqlConnection connection, string commandText) From 48fa6a8a48f078bf23d559e011dd03672776b002 Mon Sep 17 00:00:00 2001 From: priyankatiwari08 Date: Tue, 16 Dec 2025 21:39:12 +0530 Subject: [PATCH 09/17] Update Microsoft.Data.SqlClient.ManualTesting.Tests.csproj --- .../Microsoft.Data.SqlClient.ManualTesting.Tests.csproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 9c2019b85d..5928f096c5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -33,9 +33,6 @@ - - - @@ -392,7 +389,4 @@ xunit.runner.json - - - From 03398c749e42e8d481bd70d506d2f39f48163e77 Mon Sep 17 00:00:00 2001 From: priyankatiwari08 Date: Tue, 16 Dec 2025 21:39:59 +0530 Subject: [PATCH 10/17] Update Microsoft.Data.SqlClient.ManualTesting.Tests.csproj --- .../Microsoft.Data.SqlClient.ManualTesting.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 5928f096c5..d949316bdb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -279,7 +279,6 @@ - From c9cfd01885d212bf7a1d4aef23c3923effd3f0bd Mon Sep 17 00:00:00 2001 From: priyankatiwari08 Date: Tue, 16 Dec 2025 22:14:41 +0530 Subject: [PATCH 11/17] Remove EnsureBuyerSellerObjectsExist from tests Removed EnsureBuyerSellerObjectsExist calls from tests. --- .../AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index 6c90dd6724..b9090a1551 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -37,7 +37,6 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti { // Arrange // Ensure baseline rows exist - EnsureBuyerSellerObjectsExist(connectionString); TruncateTables("BuyerSeller", connectionString); PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), @@ -69,7 +68,6 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) { // Arrange - EnsureBuyerSellerObjectsExist(connectionString); TruncateTables("BuyerSeller", connectionString); PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), From f0f4d437a9ea03dae560b2144a666512cb2ad287 Mon Sep 17 00:00:00 2001 From: priyankatiwari08 Date: Tue, 16 Dec 2025 22:46:14 +0530 Subject: [PATCH 12/17] Refactor SqlDataAdapterBatchUpdateTests for clarity --- .../SqlDataAdapterBatchUpdateTests.cs | 54 ++----------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index b9090a1551..18d98664a8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -140,7 +140,7 @@ private DataTable BuildBuyerSellerDataTable() new DataColumn("SSN1", typeof(string)), new DataColumn("SSN2", typeof(string)), }); - dt.PrimaryKey = new[] { dt.Columns["BuyerSellerID"] }; + dt.PrimaryKey = new[] { dt.Columns["BuyerSellerID"] }; return dt; } @@ -157,7 +157,7 @@ private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) private void MutateForUpdate(DataTable dt) { int i = 0; - var fixedTime = new DateTime(2000, 01, 01, 12, 34, 56); + var fixedTime = new DateTime(2023, 01, 01, 12, 34, 56); // Use any fixed value string timeStr = fixedTime.ToString("HHmm"); foreach (DataRow row in dt.Rows) { @@ -171,15 +171,7 @@ internal void TruncateTables(string tableName, string connectionString) { using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - try - { - SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); - } - catch - { - // Fallback to DELETE if TRUNCATE fails (e.g., due to FK constraints) - SilentRunCommand($@"DELETE FROM [dbo].[{tableNames[tableName]}]", connection); - } + SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); } internal void ExecuteQuery(SqlConnection connection, string commandText) @@ -259,45 +251,5 @@ public void Dispose() SilentRunCommand($"DELETE FROM [dbo].[{tableNames["BuyerSeller"]}]", connection); } } - private void EnsureBuyerSellerObjectsExist(string connectionString) - { - using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); - connection.Open(); - - // Create table if not exists - SilentRunCommand(@" - IF OBJECT_ID('dbo.BuyerSeller', 'U') IS NULL - CREATE TABLE [dbo].[BuyerSeller] ( - [BuyerSellerID] INT PRIMARY KEY, - [SSN1] VARCHAR(255), - [SSN2] VARCHAR(255) - )", connection); - - // Create Insert proc if not exists - SilentRunCommand(@" - IF OBJECT_ID('dbo.InsertBuyerSeller', 'P') IS NULL - EXEC('CREATE PROCEDURE [dbo].[InsertBuyerSeller] - @BuyerSellerID INT, - @SSN1 VARCHAR(255), - @SSN2 VARCHAR(255) - AS - INSERT INTO [dbo].[BuyerSeller] (BuyerSellerID, SSN1, SSN2) - VALUES (@BuyerSellerID, @SSN1, @SSN2)') - ", connection); - - // Create Update proc if not exists - SilentRunCommand(@" - IF OBJECT_ID('dbo.UpdateBuyerSeller', 'P') IS NULL - EXEC('CREATE PROCEDURE [dbo].[UpdateBuyerSeller] - @BuyerSellerID INT, - @SSN1 VARCHAR(255), - @SSN2 VARCHAR(255) - AS - UPDATE [dbo].[BuyerSeller] - SET SSN1 = @SSN1, SSN2 = @SSN2 - WHERE BuyerSellerID = @BuyerSellerID') - ", connection); - - } } } From 3824bebf2d8d4be2efe9a05aa908cb631c81ffd0 Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Thu, 18 Dec 2025 14:16:36 +0530 Subject: [PATCH 13/17] testcase fixes related to creating unique table and stored procedures --- .../SqlDataAdapterBatchUpdateTests.cs | 128 +++++------------- .../TestFixtures/SQLSetupStrategy.cs | 5 + .../TestFixtures/Setup/BuyerSellerTable.cs | 107 +++++++++++++++ ....Data.SqlClient.ManualTesting.Tests.csproj | 4 + 4 files changed, 153 insertions(+), 91 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/BuyerSellerTable.cs diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index 53e4dc9f83..b44b1acb0b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -15,18 +15,14 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted public sealed class SqlDataAdapterBatchUpdateTests : IClassFixture, IDisposable { private readonly SQLSetupStrategy _fixture; - private readonly Dictionary tableNames = new(); + private readonly string _tableName; + private readonly BuyerSellerTable _buyerSellerTable; public SqlDataAdapterBatchUpdateTests(SQLSetupStrategyCertStoreProvider context) { _fixture = context; - - // Provide table names to mirror repo patterns. - // If your fixture already exposes specific names for BuyerSeller and procs, wire them here. - // Otherwise use literal names as below. - tableNames["BuyerSeller"] = "BuyerSeller"; - tableNames["ProcInsertBuyerSeller"] = "InsertBuyerSeller"; - tableNames["ProcUpdateBuyerSeller"] = "UpdateBuyerSeller"; + _buyerSellerTable = _fixture.BuyerSellerTable as BuyerSellerTable; + _tableName = _fixture.BuyerSellerTable.Name; } // ---------- TESTS ---------- @@ -36,24 +32,21 @@ public SqlDataAdapterBatchUpdateTests(SQLSetupStrategyCertStoreProvider context) public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connectionString) { // Arrange - // Ensure baseline rows exist - EnsureBuyerSellerObjectsExist(connectionString); - TruncateTables("BuyerSeller", connectionString); - PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + TruncateTable(connectionString); + PopulateTable(new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), (2, "234-56-7890", "876-54-3210"), (3, "345-67-8901", "765-43-2109"), (4, "456-78-9012", "654-32-1098"), }, connectionString); - using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var conn = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); await conn.OpenAsync(); - using var adapter = CreateAdapter(conn, updateBatchSize: 10); // failure repro: > 1 + using var adapter = CreateAdapter(conn, updateBatchSize: 10); var dataTable = BuildBuyerSellerDataTable(); LoadCurrentRowsIntoDataTable(dataTable, conn); - // Mutate values for update MutateForUpdate(dataTable); // Act - With batch updates (UpdateBatchSize > 1), this previously threw NullReferenceException due to null systemParams in batch RPC mode @@ -61,7 +54,6 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti // Assert Assert.Equal(dataTable.Rows.Count, updated); - } [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] @@ -69,16 +61,15 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) { // Arrange - EnsureBuyerSellerObjectsExist(connectionString); - TruncateTables("BuyerSeller", connectionString); - PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + TruncateTable(connectionString); + PopulateTable(new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), (2, "234-56-7890", "876-54-3210"), (3, "345-67-8901", "765-43-2109"), (4, "456-78-9012", "654-32-1098"), }, connectionString); - using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var conn = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); await conn.OpenAsync(); using var adapter = CreateAdapter(conn, updateBatchSize: 1); // success path @@ -87,41 +78,38 @@ public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) MutateForUpdate(dataTable); - // Act (should not throw) + // Act var updatedRows = await Task.Run(() => adapter.Update(dataTable)); // Assert Assert.Equal(dataTable.Rows.Count, updatedRows); - } // ---------- HELPERS ---------- private SqlDataAdapter CreateAdapter(SqlConnection connection, int updateBatchSize) { - // Insert - var insertCmd = new SqlCommand(tableNames["ProcInsertBuyerSeller"], connection) + var insertCmd = new SqlCommand(_buyerSellerTable.InsertProcedureName, connection) { CommandType = CommandType.StoredProcedure }; insertCmd.Parameters.AddRange(new[] { - new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, - new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, - new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, }); insertCmd.UpdatedRowSource = UpdateRowSource.None; - // Update - var updateCmd = new SqlCommand(tableNames["ProcUpdateBuyerSeller"], connection) + var updateCmd = new SqlCommand(_buyerSellerTable.UpdateProcedureName, connection) { CommandType = CommandType.StoredProcedure }; updateCmd.Parameters.AddRange(new[] { - new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, - new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, - new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, }); updateCmd.UpdatedRowSource = UpdateRowSource.None; @@ -135,7 +123,7 @@ private SqlDataAdapter CreateAdapter(SqlConnection connection, int updateBatchSi private DataTable BuildBuyerSellerDataTable() { - var dt = new DataTable(tableNames["BuyerSeller"]); + var dt = new DataTable(_tableName); dt.Columns.AddRange(new[] { new DataColumn("BuyerSellerID", typeof(int)), @@ -148,7 +136,7 @@ private DataTable BuildBuyerSellerDataTable() private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) { - using var cmd = new SqlCommand($"SELECT BuyerSellerID, SSN1, SSN2 FROM [dbo].[{tableNames["BuyerSeller"]}] ORDER BY BuyerSellerID", conn); + using var cmd = new SqlCommand($"SELECT BuyerSellerID, SSN1, SSN2 FROM [dbo].[{_tableName}] ORDER BY BuyerSellerID", conn); using var reader = cmd.ExecuteReader(); while (reader.Read()) { @@ -159,7 +147,7 @@ private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) private void MutateForUpdate(DataTable dt) { int i = 0; - var fixedTime = new DateTime(2000, 01, 01, 12, 34, 56); + var fixedTime = new DateTime(2023, 01, 01, 12, 34, 56); string timeStr = fixedTime.ToString("HHmm"); foreach (DataRow row in dt.Rows) { @@ -169,16 +157,15 @@ private void MutateForUpdate(DataTable dt) } } - internal void TruncateTables(string tableName, string connectionString) + private void TruncateTable(string connectionString) { - using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + SilentRunCommand($"TRUNCATE TABLE [dbo].[{_tableName}]", connection); } - internal void ExecuteQuery(SqlConnection connection, string commandText) + private void ExecuteQuery(SqlConnection connection, string commandText) { - // Mirror AE-enabled command execution style used in repo tests using var cmd = new SqlCommand( commandText, connection: connection, @@ -187,15 +174,15 @@ internal void ExecuteQuery(SqlConnection connection, string commandText) cmd.ExecuteNonQuery(); } - internal void PopulateTable(string tableName, (int id, string s1, string s2)[] rows, string connectionString) + private void PopulateTable((int id, string s1, string s2)[] rows, string connectionString) { - using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); foreach (var (id, s1, s2) in rows) { using var cmd = new SqlCommand( - $@"INSERT INTO [dbo].[{tableNames[tableName]}] (BuyerSellerID, SSN1, SSN2) VALUES (@id, @s1, @s2)", + $"INSERT INTO [dbo].[{_tableName}] (BuyerSellerID, SSN1, SSN2) VALUES (@id, @s1, @s2)", connection, null, SqlCommandColumnEncryptionSetting.Enabled); @@ -208,11 +195,10 @@ internal void PopulateTable(string tableName, (int id, string s1, string s2)[] r } } - public string GetOpenConnectionString(string baseConnectionString, bool encryptionEnabled) + private string GetConnectionString(string baseConnectionString, bool encryptionEnabled) { var builder = new SqlConnectionStringBuilder(baseConnectionString) { - // TrustServerCertificate can be set based on environment; mirror repo’s AE toggling idiom ColumnEncryptionSetting = encryptionEnabled ? SqlConnectionColumnEncryptionSetting.Enabled : SqlConnectionColumnEncryptionSetting.Disabled @@ -220,13 +206,14 @@ public string GetOpenConnectionString(string baseConnectionString, bool encrypti return builder.ToString(); } - internal void SilentRunCommand(string commandText, SqlConnection connection) + private void SilentRunCommand(string commandText, SqlConnection connection) { try - { ExecuteQuery(connection, commandText); } + { + ExecuteQuery(connection, commandText); + } catch (SqlException ex) { - // Only swallow "object does not exist" (error 208), log others bool onlyObjectNotExist = true; foreach (SqlError err in ex.Errors) { @@ -240,7 +227,6 @@ internal void SilentRunCommand(string commandText, SqlConnection connection) { Console.WriteLine($"SilentRunCommand: Unexpected SqlException during cleanup: {ex}"); } - // Swallow all exceptions, but log unexpected ones } } @@ -248,50 +234,10 @@ public void Dispose() { foreach (string connectionString in DataTestUtility.AEConnStringsSetup) { - using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($"DELETE FROM [dbo].[{tableNames["BuyerSeller"]}]", connection); + SilentRunCommand($"DELETE FROM [dbo].[{_tableName}]", connection); } } - private void EnsureBuyerSellerObjectsExist(string connectionString) - { - using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); - connection.Open(); - - // Create table if not exists - SilentRunCommand(@" - IF OBJECT_ID('dbo.BuyerSeller', 'U') IS NULL - CREATE TABLE [dbo].[BuyerSeller] ( - [BuyerSellerID] INT PRIMARY KEY, - [SSN1] VARCHAR(255), - [SSN2] VARCHAR(255) - )", connection); - - // Create Insert proc if not exists - SilentRunCommand(@" - IF OBJECT_ID('dbo.InsertBuyerSeller', 'P') IS NULL - EXEC('CREATE PROCEDURE [dbo].[InsertBuyerSeller] - @BuyerSellerID INT, - @SSN1 VARCHAR(255), - @SSN2 VARCHAR(255) - AS - INSERT INTO [dbo].[BuyerSeller] (BuyerSellerID, SSN1, SSN2) - VALUES (@BuyerSellerID, @SSN1, @SSN2)') - ", connection); - - // Create Update proc if not exists - SilentRunCommand(@" - IF OBJECT_ID('dbo.UpdateBuyerSeller', 'P') IS NULL - EXEC('CREATE PROCEDURE [dbo].[UpdateBuyerSeller] - @BuyerSellerID INT, - @SSN1 VARCHAR(255), - @SSN2 VARCHAR(255) - AS - UPDATE [dbo].[BuyerSeller] - SET SSN1 = @SSN1, SSN2 = @SSN2 - WHERE BuyerSellerID = @BuyerSellerID') - ", connection); - - } } } 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 d08d2a86be..6beed55e74 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategy.cs @@ -19,6 +19,8 @@ public class SQLSetupStrategy : ColumnMasterKeyCertificateFixture public string ColumnMasterKeyPath { get; } public Table ApiTestTable { get; private set; } + public Table BuyerSellerTable { get; private set; } + public Table BulkCopyAEErrorMessageTestTable { get; private set; } public Table BulkCopyAETestTable { get; private set; } public Table ColumnDecryptErrorTestTable { get; private set; } @@ -133,6 +135,9 @@ protected List CreateTables(IList columnEncryptionKe ApiTestTable = new ApiTestTable(GenerateUniqueName("ApiTestTable"), columnEncryptionKeys[0], columnEncryptionKeys[1]); tables.Add(ApiTestTable); + BuyerSellerTable = new BuyerSellerTable(GenerateUniqueName("BuyerSellerTable"), columnEncryptionKeys[0], columnEncryptionKeys[1]); + tables.Add(BuyerSellerTable); + BulkCopyAEErrorMessageTestTable = new BulkCopyAEErrorMessageTestTable(GenerateUniqueName("BulkCopyAEErrorMessageTestTable"), columnEncryptionKeys[0], columnEncryptionKeys[1]); tables.Add(BulkCopyAEErrorMessageTestTable); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/BuyerSellerTable.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/BuyerSellerTable.cs new file mode 100644 index 0000000000..a6f1d5ac7f --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/BuyerSellerTable.cs @@ -0,0 +1,107 @@ +// 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; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup +{ + public class BuyerSellerTable : Table + { + private const string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; + private readonly ColumnEncryptionKey _columnEncryptionKey1; + private readonly ColumnEncryptionKey _columnEncryptionKey2; + + // ✅ ADD: Unique stored procedure names based on table name + public string InsertProcedureName => $"InsertBuyerSeller_{Name}"; + public string UpdateProcedureName => $"UpdateBuyerSeller_{Name}"; + + public BuyerSellerTable(string tableName, ColumnEncryptionKey columnEncryptionKey1, ColumnEncryptionKey columnEncryptionKey2) + : base(tableName) + { + _columnEncryptionKey1 = columnEncryptionKey1; + _columnEncryptionKey2 = columnEncryptionKey2; + } + + public override void Create(SqlConnection sqlConnection) + { + // Create the table with encrypted columns + string createTableSql = $@" + CREATE TABLE [dbo].[{Name}] + ( + [BuyerSellerID] [int] NOT NULL PRIMARY KEY, + [SSN1] [varchar](255) COLLATE Latin1_General_BIN2 ENCRYPTED WITH ( + COLUMN_ENCRYPTION_KEY = [{_columnEncryptionKey1.Name}], + ENCRYPTION_TYPE = DETERMINISTIC, + ALGORITHM = '{ColumnEncryptionAlgorithmName}' + ), + [SSN2] [varchar](255) COLLATE Latin1_General_BIN2 ENCRYPTED WITH ( + COLUMN_ENCRYPTION_KEY = [{_columnEncryptionKey2.Name}], + ENCRYPTION_TYPE = DETERMINISTIC, + ALGORITHM = '{ColumnEncryptionAlgorithmName}' + ) + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = createTableSql; + command.ExecuteNonQuery(); + } + + // ✅ CHANGED: Use unique SP names + string createInsertProcSql = $@" + CREATE PROCEDURE [dbo].[{InsertProcedureName}] + @BuyerSellerID int, + @SSN1 varchar(255), + @SSN2 varchar(255) + AS + BEGIN + INSERT INTO [dbo].[{Name}] (BuyerSellerID, SSN1, SSN2) + VALUES (@BuyerSellerID, @SSN1, @SSN2) + END"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = createInsertProcSql; + command.ExecuteNonQuery(); + } + + // ✅ CHANGED: Use unique SP names + string createUpdateProcSql = $@" + CREATE PROCEDURE [dbo].[{UpdateProcedureName}] + @BuyerSellerID int, + @SSN1 varchar(255), + @SSN2 varchar(255) + AS + BEGIN + UPDATE [dbo].[{Name}] + SET SSN1 = @SSN1, SSN2 = @SSN2 + WHERE BuyerSellerID = @BuyerSellerID + END"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = createUpdateProcSql; + command.ExecuteNonQuery(); + } + } + + public override void Drop(SqlConnection sqlConnection) + { + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = $"IF OBJECT_ID('[dbo].[{InsertProcedureName}]', 'P') IS NOT NULL DROP PROCEDURE [dbo].[{InsertProcedureName}]"; + command.ExecuteNonQuery(); + } + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = $"IF OBJECT_ID('[dbo].[{UpdateProcedureName}]', 'P') IS NOT NULL DROP PROCEDURE [dbo].[{UpdateProcedureName}]"; + command.ExecuteNonQuery(); + } + + // Drop table + base.Drop(sqlConnection); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index d949316bdb..33ae4fd3d0 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -54,6 +54,7 @@ + @@ -388,4 +389,7 @@ xunit.runner.json + + + From 17bf6a5b564f4054d157a2b1e75baa0c55a69aed Mon Sep 17 00:00:00 2001 From: priyankatiwari08 Date: Thu, 18 Dec 2025 14:20:38 +0530 Subject: [PATCH 14/17] Refactor SqlDataAdapterBatchUpdateTests for clarity --- .../SqlDataAdapterBatchUpdateTests.cs | 90 ++++++++----------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index 18d98664a8..c134e53009 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.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. @@ -15,18 +15,14 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted public sealed class SqlDataAdapterBatchUpdateTests : IClassFixture, IDisposable { private readonly SQLSetupStrategy _fixture; - private readonly Dictionary tableNames = new(); + private readonly string _tableName; + private readonly BuyerSellerTable _buyerSellerTable; public SqlDataAdapterBatchUpdateTests(SQLSetupStrategyCertStoreProvider context) { _fixture = context; - - // Provide table names to mirror repo patterns. - // If your fixture already exposes specific names for BuyerSeller and procs, wire them here. - // Otherwise use literal names as below. - tableNames["BuyerSeller"] = "BuyerSeller"; - tableNames["ProcInsertBuyerSeller"] = "InsertBuyerSeller"; - tableNames["ProcUpdateBuyerSeller"] = "UpdateBuyerSeller"; + _buyerSellerTable = _fixture.BuyerSellerTable as BuyerSellerTable; + _tableName = _fixture.BuyerSellerTable.Name; } // ---------- TESTS ---------- @@ -36,23 +32,21 @@ public SqlDataAdapterBatchUpdateTests(SQLSetupStrategyCertStoreProvider context) public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connectionString) { // Arrange - // Ensure baseline rows exist - TruncateTables("BuyerSeller", connectionString); - PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + TruncateTable(connectionString); + PopulateTable(new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), (2, "234-56-7890", "876-54-3210"), (3, "345-67-8901", "765-43-2109"), (4, "456-78-9012", "654-32-1098"), }, connectionString); - using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var conn = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); await conn.OpenAsync(); - using var adapter = CreateAdapter(conn, updateBatchSize: 10); // failure repro: > 1 + using var adapter = CreateAdapter(conn, updateBatchSize: 10); var dataTable = BuildBuyerSellerDataTable(); LoadCurrentRowsIntoDataTable(dataTable, conn); - // Mutate values for update MutateForUpdate(dataTable); // Act - With batch updates (UpdateBatchSize > 1), this previously threw NullReferenceException due to null systemParams in batch RPC mode @@ -60,7 +54,6 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti // Assert Assert.Equal(dataTable.Rows.Count, updated); - } [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))] @@ -68,15 +61,15 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) { // Arrange - TruncateTables("BuyerSeller", connectionString); - PopulateTable("BuyerSeller", new (int id, string s1, string s2)[] { + TruncateTable(connectionString); + PopulateTable(new (int id, string s1, string s2)[] { (1, "123-45-6789", "987-65-4321"), (2, "234-56-7890", "876-54-3210"), (3, "345-67-8901", "765-43-2109"), (4, "456-78-9012", "654-32-1098"), }, connectionString); - using var conn = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var conn = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); await conn.OpenAsync(); using var adapter = CreateAdapter(conn, updateBatchSize: 1); // success path @@ -85,41 +78,38 @@ public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) MutateForUpdate(dataTable); - // Act (should not throw) + // Act var updatedRows = await Task.Run(() => adapter.Update(dataTable)); // Assert Assert.Equal(dataTable.Rows.Count, updatedRows); - } // ---------- HELPERS ---------- private SqlDataAdapter CreateAdapter(SqlConnection connection, int updateBatchSize) { - // Insert - var insertCmd = new SqlCommand(tableNames["ProcInsertBuyerSeller"], connection) + var insertCmd = new SqlCommand(_buyerSellerTable.InsertProcedureName, connection) { CommandType = CommandType.StoredProcedure }; insertCmd.Parameters.AddRange(new[] { - new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, - new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, - new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, }); insertCmd.UpdatedRowSource = UpdateRowSource.None; - // Update - var updateCmd = new SqlCommand(tableNames["ProcUpdateBuyerSeller"], connection) + var updateCmd = new SqlCommand(_buyerSellerTable.UpdateProcedureName, connection) { CommandType = CommandType.StoredProcedure }; updateCmd.Parameters.AddRange(new[] { - new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, - new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, - new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, + new SqlParameter("@BuyerSellerID", SqlDbType.Int) { SourceColumn = "BuyerSellerID" }, + new SqlParameter("@SSN1", SqlDbType.VarChar, 255) { SourceColumn = "SSN1" }, + new SqlParameter("@SSN2", SqlDbType.VarChar, 255) { SourceColumn = "SSN2" }, }); updateCmd.UpdatedRowSource = UpdateRowSource.None; @@ -133,20 +123,20 @@ private SqlDataAdapter CreateAdapter(SqlConnection connection, int updateBatchSi private DataTable BuildBuyerSellerDataTable() { - var dt = new DataTable(tableNames["BuyerSeller"]); + var dt = new DataTable(_tableName); dt.Columns.AddRange(new[] { new DataColumn("BuyerSellerID", typeof(int)), new DataColumn("SSN1", typeof(string)), new DataColumn("SSN2", typeof(string)), }); - dt.PrimaryKey = new[] { dt.Columns["BuyerSellerID"] }; + dt.PrimaryKey = new[] { dt.Columns["BuyerSellerID"] }; return dt; } private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) { - using var cmd = new SqlCommand($"SELECT BuyerSellerID, SSN1, SSN2 FROM [dbo].[{tableNames["BuyerSeller"]}] ORDER BY BuyerSellerID", conn); + using var cmd = new SqlCommand($"SELECT BuyerSellerID, SSN1, SSN2 FROM [dbo].[{_tableName}] ORDER BY BuyerSellerID", conn); using var reader = cmd.ExecuteReader(); while (reader.Read()) { @@ -157,7 +147,7 @@ private void LoadCurrentRowsIntoDataTable(DataTable dt, SqlConnection conn) private void MutateForUpdate(DataTable dt) { int i = 0; - var fixedTime = new DateTime(2023, 01, 01, 12, 34, 56); // Use any fixed value + var fixedTime = new DateTime(2023, 01, 01, 12, 34, 56); string timeStr = fixedTime.ToString("HHmm"); foreach (DataRow row in dt.Rows) { @@ -167,16 +157,15 @@ private void MutateForUpdate(DataTable dt) } } - internal void TruncateTables(string tableName, string connectionString) + private void TruncateTable(string connectionString) { - using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($@"TRUNCATE TABLE [dbo].[{tableNames[tableName]}]", connection); + SilentRunCommand($"TRUNCATE TABLE [dbo].[{_tableName}]", connection); } - internal void ExecuteQuery(SqlConnection connection, string commandText) + private void ExecuteQuery(SqlConnection connection, string commandText) { - // Mirror AE-enabled command execution style used in repo tests using var cmd = new SqlCommand( commandText, connection: connection, @@ -185,15 +174,15 @@ internal void ExecuteQuery(SqlConnection connection, string commandText) cmd.ExecuteNonQuery(); } - internal void PopulateTable(string tableName, (int id, string s1, string s2)[] rows, string connectionString) + private void PopulateTable((int id, string s1, string s2)[] rows, string connectionString) { - using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); foreach (var (id, s1, s2) in rows) { using var cmd = new SqlCommand( - $@"INSERT INTO [dbo].[{tableNames[tableName]}] (BuyerSellerID, SSN1, SSN2) VALUES (@id, @s1, @s2)", + $"INSERT INTO [dbo].[{_tableName}] (BuyerSellerID, SSN1, SSN2) VALUES (@id, @s1, @s2)", connection, null, SqlCommandColumnEncryptionSetting.Enabled); @@ -206,11 +195,10 @@ internal void PopulateTable(string tableName, (int id, string s1, string s2)[] r } } - public string GetOpenConnectionString(string baseConnectionString, bool encryptionEnabled) + private string GetConnectionString(string baseConnectionString, bool encryptionEnabled) { var builder = new SqlConnectionStringBuilder(baseConnectionString) { - // TrustServerCertificate can be set based on environment; mirror repo’s AE toggling idiom ColumnEncryptionSetting = encryptionEnabled ? SqlConnectionColumnEncryptionSetting.Enabled : SqlConnectionColumnEncryptionSetting.Disabled @@ -218,13 +206,14 @@ public string GetOpenConnectionString(string baseConnectionString, bool encrypti return builder.ToString(); } - internal void SilentRunCommand(string commandText, SqlConnection connection) + private void SilentRunCommand(string commandText, SqlConnection connection) { try - { ExecuteQuery(connection, commandText); } + { + ExecuteQuery(connection, commandText); + } catch (SqlException ex) { - // Only swallow "object does not exist" (error 208), log others bool onlyObjectNotExist = true; foreach (SqlError err in ex.Errors) { @@ -238,7 +227,6 @@ internal void SilentRunCommand(string commandText, SqlConnection connection) { Console.WriteLine($"SilentRunCommand: Unexpected SqlException during cleanup: {ex}"); } - // Swallow all exceptions, but log unexpected ones } } @@ -246,9 +234,9 @@ public void Dispose() { foreach (string connectionString in DataTestUtility.AEConnStringsSetup) { - using var connection = new SqlConnection(GetOpenConnectionString(connectionString, encryptionEnabled: true)); + using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($"DELETE FROM [dbo].[{tableNames["BuyerSeller"]}]", connection); + SilentRunCommand($"DELETE FROM [dbo].[{_tableName}]", connection); } } } From d625cd24785f4604d5aded98ce013dba560d4e4d Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Thu, 18 Dec 2025 15:09:34 +0530 Subject: [PATCH 15/17] fix for pipeline failure - changing from truncate to delete --- .../AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index c134e53009..ac1780697e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -161,7 +161,7 @@ private void TruncateTable(string connectionString) { using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($"TRUNCATE TABLE [dbo].[{_tableName}]", connection); + SilentRunCommand($"DELETE FROM [dbo].[{_tableName}]", connection); } private void ExecuteQuery(SqlConnection connection, string commandText) From ba78044bebe371996d0beb235d9907b5fef23d29 Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Thu, 18 Dec 2025 15:38:00 +0530 Subject: [PATCH 16/17] changing ids of the data being inserted --- .../SqlDataAdapterBatchUpdateTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index ac1780697e..c070dcd54f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -34,10 +34,10 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti // Arrange TruncateTable(connectionString); PopulateTable(new (int id, string s1, string s2)[] { - (1, "123-45-6789", "987-65-4321"), - (2, "234-56-7890", "876-54-3210"), - (3, "345-67-8901", "765-43-2109"), - (4, "456-78-9012", "654-32-1098"), + (10, "123-45-6789", "987-65-4321"), + (20, "234-56-7890", "876-54-3210"), + (30, "345-67-8901", "765-43-2109"), + (40, "456-78-9012", "654-32-1098"), }, connectionString); using var conn = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); @@ -63,10 +63,10 @@ public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) // Arrange TruncateTable(connectionString); PopulateTable(new (int id, string s1, string s2)[] { - (1, "123-45-6789", "987-65-4321"), - (2, "234-56-7890", "876-54-3210"), - (3, "345-67-8901", "765-43-2109"), - (4, "456-78-9012", "654-32-1098"), + (100, "123-45-6789", "987-65-4321"), + (200, "234-56-7890", "876-54-3210"), + (300, "345-67-8901", "765-43-2109"), + (400, "456-78-9012", "654-32-1098"), }, connectionString); using var conn = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); From 84f704f3e52707422c29af304b6a162f981ac9b0 Mon Sep 17 00:00:00 2001 From: Priyanka Tiwari Date: Thu, 18 Dec 2025 16:29:32 +0530 Subject: [PATCH 17/17] adding unique ids with data --- .../SqlDataAdapterBatchUpdateTests.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs index c070dcd54f..063bdd3a45 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/SqlDataAdapterBatchUpdateTests.cs @@ -33,11 +33,12 @@ public async Task AdapterUpdate_BatchSizeGreaterThanOne_Succeeds(string connecti { // Arrange TruncateTable(connectionString); + int idBase = GetUniqueIdBase(); PopulateTable(new (int id, string s1, string s2)[] { - (10, "123-45-6789", "987-65-4321"), - (20, "234-56-7890", "876-54-3210"), - (30, "345-67-8901", "765-43-2109"), - (40, "456-78-9012", "654-32-1098"), + (idBase + 10, "123-45-6789", "987-65-4321"), + (idBase + 20, "234-56-7890", "876-54-3210"), + (idBase + 30, "345-67-8901", "765-43-2109"), + (idBase + 40, "456-78-9012", "654-32-1098"), }, connectionString); using var conn = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); @@ -62,11 +63,12 @@ public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) { // Arrange TruncateTable(connectionString); + int idBase = GetUniqueIdBase(); PopulateTable(new (int id, string s1, string s2)[] { - (100, "123-45-6789", "987-65-4321"), - (200, "234-56-7890", "876-54-3210"), - (300, "345-67-8901", "765-43-2109"), - (400, "456-78-9012", "654-32-1098"), + (idBase + 100, "123-45-6789", "987-65-4321"), + (idBase + 200, "234-56-7890", "876-54-3210"), + (idBase + 300, "345-67-8901", "765-43-2109"), + (idBase + 400, "456-78-9012", "654-32-1098"), }, connectionString); using var conn = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); @@ -87,6 +89,8 @@ public async Task AdapterUpdate_BatchSizeOne_Succeeds(string connectionString) // ---------- HELPERS ---------- + private int GetUniqueIdBase() => Math.Abs(Guid.NewGuid().GetHashCode()) % 1000000; + private SqlDataAdapter CreateAdapter(SqlConnection connection, int updateBatchSize) { var insertCmd = new SqlCommand(_buyerSellerTable.InsertProcedureName, connection) @@ -161,7 +165,7 @@ private void TruncateTable(string connectionString) { using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($"DELETE FROM [dbo].[{_tableName}]", connection); + ExecuteQuery(connection, $"DELETE FROM [dbo].[{_tableName}]"); } private void ExecuteQuery(SqlConnection connection, string commandText) @@ -236,7 +240,7 @@ public void Dispose() { using var connection = new SqlConnection(GetConnectionString(connectionString, encryptionEnabled: true)); connection.Open(); - SilentRunCommand($"DELETE FROM [dbo].[{_tableName}]", connection); + ExecuteQuery(connection, $"DELETE FROM [dbo].[{_tableName}]"); } } }