Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fbdcc90
Backporting SqlDataAdapter nullreference exception fix
priyankatiwari08 Dec 15, 2025
d98ee90
Backporting SqlDataAdapter null reference exception to MDS v6.1
priyankatiwari08 Dec 15, 2025
aed9e25
Remove SqlDataAdapterBatchUpdateTests from project
priyankatiwari08 Dec 16, 2025
328fa35
Fixing compilation for SqlDataAdapterBatchUpdateTests.cs
priyankatiwari08 Dec 16, 2025
7160abe
Merge branch 'dev/prtiwar/6.1-SqlDataAdapterIssue' of https://github.…
priyankatiwari08 Dec 16, 2025
b78f299
fix
priyankatiwari08 Dec 16, 2025
c9677a8
changes per copilot's comment on PR
priyankatiwari08 Dec 16, 2025
c2dde41
Making changes to fix issue with enclave in pipeline
priyankatiwari08 Dec 16, 2025
8ee7c01
.
priyankatiwari08 Dec 16, 2025
560d2c6
.
priyankatiwari08 Dec 16, 2025
48fa6a8
Update Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
priyankatiwari08 Dec 16, 2025
03398c7
Update Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
priyankatiwari08 Dec 16, 2025
c9cfd01
Remove EnsureBuyerSellerObjectsExist from tests
priyankatiwari08 Dec 16, 2025
f0f4d43
Refactor SqlDataAdapterBatchUpdateTests for clarity
priyankatiwari08 Dec 16, 2025
3824beb
testcase fixes related to creating unique table and stored procedures
priyankatiwari08 Dec 18, 2025
bbcca93
.
priyankatiwari08 Dec 18, 2025
17bf6a5
Refactor SqlDataAdapterBatchUpdateTests for clarity
priyankatiwari08 Dec 18, 2025
d625cd2
fix for pipeline failure - changing from truncate to delete
priyankatiwari08 Dec 18, 2025
ba78044
changing ids of the data being inserted
priyankatiwari08 Dec 18, 2025
84f704f
adding unique ids with data
priyankatiwari08 Dec 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
// 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<SQLSetupStrategyCertStoreProvider>, IDisposable
{
private readonly SQLSetupStrategy _fixture;
private readonly Dictionary<string, string> tableNames = new();
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated

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.
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
tableNames["BuyerSeller"] = "BuyerSeller";
tableNames["ProcInsertBuyerSeller"] = "InsertBuyerSeller";
tableNames["ProcUpdateBuyerSeller"] = "UpdateBuyerSeller";
}

Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
// ---------- TESTS ----------

Comment thread
priyankatiwari08 marked this conversation as resolved.
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore))]
[ClassData(typeof(AEConnectionStringProvider))]
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)[] {
(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
EnsureBuyerSellerObjectsExist(connectionString);
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)),
Comment thread
priyankatiwari08 marked this conversation as resolved.

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'DataColumn' is created but not disposed.

Copilot uses AI. Check for mistakes.
new DataColumn("SSN1", typeof(string)),
Comment thread
priyankatiwari08 marked this conversation as resolved.

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'DataColumn' is created but not disposed.

Copilot uses AI. Check for mistakes.
new DataColumn("SSN2", typeof(string)),
Comment thread
priyankatiwari08 marked this conversation as resolved.

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'DataColumn' is created but not disposed.

Copilot uses AI. Check for mistakes.
});
Comment on lines +131 to +136

Copilot AI Dec 18, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'DataColumn' is created but not disposed.

Suggested change
dt.Columns.AddRange(new[]
{
new DataColumn("BuyerSellerID", typeof(int)),
new DataColumn("SSN1", typeof(string)),
new DataColumn("SSN2", typeof(string)),
});
dt.Columns.Add("BuyerSellerID", typeof(int));
dt.Columns.Add("SSN1", typeof(string));
dt.Columns.Add("SSN2", typeof(string));

Copilot uses AI. Check for mistakes.
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(2000, 01, 01, 12, 34, 56);
string timeStr = fixedTime.ToString("HHmm");
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
foreach (DataRow row in dt.Rows)
{
i++;
row["SSN1"] = $"{i:000}-11-{timeStr}";
row["SSN2"] = $"{i:000}-22-{timeStr}";
Comment thread
priyankatiwari08 marked this conversation as resolved.
Comment thread
priyankatiwari08 marked this conversation as resolved.
}
}

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);
}
}

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
}
}
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated

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);
}
}
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);

}
Comment thread
priyankatiwari08 marked this conversation as resolved.
Outdated
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<Compile Include="AlwaysEncrypted\TestFixtures\SQLSetupStrategyCspProvider.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TestSet)' == '' OR '$(TestSet)' == 'AE'">
<Compile Include="AlwaysEncrypted\SqlDataAdapterBatchUpdateTests.cs" />
Comment thread
priyankatiwari08 marked this conversation as resolved.
<Compile Include="AlwaysEncrypted\AKVTests.cs" />
<Compile Include="AlwaysEncrypted\AKVUnitTests.cs" />
<Compile Include="AlwaysEncrypted\EnclaveAzureDatabaseTests.cs" />
Expand Down
Loading