Skip to content

[6.1] Port #3912 to release/6.1#3947

Open
samsharma2700 wants to merge 1 commit intorelease/6.1from
dev/samsharma2700/backport_3912_61
Open

[6.1] Port #3912 to release/6.1#3947
samsharma2700 wants to merge 1 commit intorelease/6.1from
dev/samsharma2700/backport_3912_61

Conversation

@samsharma2700
Copy link
Contributor

Description

Port #3912 to release/6.1 to fix Execute Scalar issue #3736

Issues

Fixes #3736

Copilot AI review requested due to automatic review settings February 10, 2026 23:51
@samsharma2700 samsharma2700 requested a review from a team as a code owner February 10, 2026 23:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Ports the fix for #3736 into release/6.1 so ExecuteScalar()/ExecuteScalarAsync() no longer silently ignores server-side errors that arrive after the first row/result, preventing transactional “zombie” scenarios.

Changes:

  • Drain remaining result sets in SqlCommand.CompleteExecuteScalar() (sync) and the ExecuteScalarAsync path to force pending error tokens to surface.
  • Add new manual regression tests for ExecuteScalar/ExecuteScalarAsync, including transactional rollback scenarios.
  • Update an existing multiple-results manual test to reflect the new error-propagation behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs Ensures ExecuteScalar (sync/async) drains remaining results so deferred server errors are propagated.
src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs Same ExecuteScalar draining behavior ported to the .NET Framework implementation.
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandExecuteScalarTest.cs Adds manual regression coverage for conversion-error-after-data and transaction rollback behavior.
src/Microsoft.Data.SqlClient/tests/ManualTests/ProviderAgnostic/MultipleResultsTest/MultipleResultsTest.cs Updates ExecuteScalar test expectations now that ExecuteScalar drains and throws on later errors.
src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj Includes the new manual test file in the test project.

Comment on lines +29 to +33
// Arrange
// Insert valid VARCHAR values - '42-43' is a valid string, not an invalid number
DataTestUtility.CreateTable(connection, tableName, "(Id INT IDENTITY(1,1) NOT NULL, Val VARCHAR(10) NOT NULL)");
using (SqlCommand insertCmd = connection.CreateCommand())
{
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

These regression tests rely on SQL Server returning at least one row before hitting the conversion error (data token followed by error token). With the current table definition (heap without a clustered key), scan order isn't guaranteed and the conversion error can occur before any row is returned, which may not exercise the specific failure mode this PR targets. Consider adding a clustered primary key (e.g., on Id) so the scan order is deterministic (Id 1 row returned before the Id 2 row triggers error 245).

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +2894 to +2898

// Drain remaining results to ensure all error tokens are processed
// before returning the result (fix for GH issue #3736).
while (await reader.NextResultAsync(cancellationToken).ConfigureAwait(false))
{ }
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

In this async path, draining remaining result sets uses the user-provided cancellationToken. If the token is canceled after the first ReadAsync completes, NextResultAsync will typically throw OperationCanceledException which is currently caught by the generic catch and reported via source.SetException(e), producing a faulted task rather than a canceled task. Consider handling OperationCanceledException/TaskCanceledException explicitly (or checking cancellationToken.IsCancellationRequested) and calling source.SetCanceled() to preserve ExecuteScalarAsync cancellation semantics.

Copilot uses AI. Check for mistakes.
Comment on lines +3112 to +3115
// Drain remaining results to ensure all error tokens are processed
// before returning the result (fix for GH issue #3736).
while (await reader.NextResultAsync(cancellationToken).ConfigureAwait(false))
{ }
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

In this async path, the added drain loop awaits NextResultAsync(cancellationToken). If cancellation is requested after the initial ReadAsync has completed, NextResultAsync can throw OperationCanceledException which is currently handled by the generic catch and surfaced via source.SetException(e), yielding a faulted task instead of a canceled task. Consider catching OperationCanceledException/TaskCanceledException (or checking cancellationToken.IsCancellationRequested) and calling source.SetCanceled() to keep ExecuteScalarAsync cancellation behavior consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +93
ConcurrentQueue<string> messages = new ConcurrentQueue<string>();

connection.InfoMessage += (object sender, SqlInfoMessageEventArgs args) =>
messages.Enqueue(args.Message);

connection.Open();

command.CommandText = s_sqlStatement;

// ExecuteScalar will select the first result set and the info message preceding it, then stop.
command.ExecuteScalar();
Assert.True(messages.TryDequeue(out string lastMessage));
Assert.Empty(messages);
Assert.Equal(ResultSet1_Message, lastMessage);
// ExecuteScalar now drains all result sets to ensure errors are not silently ignored (GH #3736 fix).
// Since the SQL statement contains RAISERRORs after the first result set, an exception is thrown.
SqlException ex = Assert.Throws<SqlException>(() => command.ExecuteScalar());
Assert.Contains("Error 1", ex.Message);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

messages and the InfoMessage event subscription are now unused in this test after switching to Assert.Throws for ExecuteScalar. Consider removing the queue and handler (or asserting on the collected messages) to avoid dead code and keep the test focused on the new behavior.

Copilot uses AI. Check for mistakes.
@paulmedynski paulmedynski self-assigned this Feb 11, 2026
try
{
// Arrange
// Insert valid VARCHAR values - '42-43' is a valid string, not an invalid number
Copy link
Contributor

Choose a reason for hiding this comment

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

... is a valid string, but not a valid number.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants