Skip to content

Commit f5ac3a8

Browse files
authored
Add a connection string parameter to control NpgsqlException.BatchCommand (#6098)
Closes #6042
1 parent 5fd06df commit f5ac3a8

6 files changed

Lines changed: 47 additions & 10 deletions

File tree

src/Npgsql/NpgsqlConnectionStringBuilder.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,24 @@ public bool IncludeErrorDetail
683683
}
684684
bool _includeErrorDetail;
685685

686+
/// <summary>
687+
/// When enabled, failed statements are included on <see cref="NpgsqlException.BatchCommand" />.
688+
/// </summary>
689+
[Category("Security")]
690+
[Description("When enabled, failed batched commands are included on NpgsqlException.BatchCommand.")]
691+
[DisplayName("Include Failed Batched Command")]
692+
[NpgsqlConnectionStringProperty]
693+
public bool IncludeFailedBatchedCommand
694+
{
695+
get => _includeFailedBatchedCommand;
696+
set
697+
{
698+
_includeFailedBatchedCommand = value;
699+
SetValue(nameof(IncludeFailedBatchedCommand), value);
700+
}
701+
}
702+
bool _includeFailedBatchedCommand;
703+
686704
/// <summary>
687705
/// Controls whether channel binding is required, disabled or preferred, depending on server support.
688706
/// </summary>

src/Npgsql/NpgsqlDataReader.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,8 @@ async Task<bool> NextResult(bool async, bool isConsuming = false, CancellationTo
516516
var statement = _statements[StatementIndex];
517517

518518
// Reference the triggering statement from the exception
519-
postgresException.BatchCommand = statement;
519+
if (Connector.Settings.IncludeFailedBatchedCommand)
520+
postgresException.BatchCommand = statement;
520521

521522
// Prevent the command or batch from being recycled (by the connection) when it's disposed. This is important since
522523
// the exception is very likely to escape the using statement of the command, and by that time some other user may
@@ -754,7 +755,9 @@ async Task<bool> NextResultSchemaOnly(bool async, bool isConsuming = false, Canc
754755
// Reference the triggering statement from the exception
755756
if (e is PostgresException postgresException && StatementIndex >= 0 && StatementIndex < _statements.Count)
756757
{
757-
postgresException.BatchCommand = _statements[StatementIndex];
758+
// Reference the triggering statement from the exception
759+
if (Connector.Settings.IncludeFailedBatchedCommand)
760+
postgresException.BatchCommand = _statements[StatementIndex];
758761

759762
// Prevent the command or batch from being recycled (by the connection) when it's disposed. This is important since
760763
// the exception is very likely to escape the using statement of the command, and by that time some other user may

src/Npgsql/NpgsqlException.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public override bool IsTransient
4646
=> InnerException is IOException or SocketException or TimeoutException or NpgsqlException { IsTransient: true };
4747

4848
/// <inheritdoc cref="DbException.BatchCommand"/>
49+
/// <remarks>This property is <c>null</c> unless <see cref="NpgsqlConnectionStringBuilder.IncludeFailedBatchedCommand"/> in connection string is set to <c>true</c>.</remarks>
4950
public new NpgsqlBatchCommand? BatchCommand { get; set; }
5051

5152
/// <inheritdoc/>

src/Npgsql/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.get -> System.Act
99
Npgsql.NpgsqlConnection.SslClientAuthenticationOptionsCallback.set -> void
1010
Npgsql.NpgsqlConnectionStringBuilder.GssEncryptionMode.get -> Npgsql.GssEncryptionMode
1111
Npgsql.NpgsqlConnectionStringBuilder.GssEncryptionMode.set -> void
12+
Npgsql.NpgsqlConnectionStringBuilder.IncludeFailedBatchedCommand.get -> bool
13+
Npgsql.NpgsqlConnectionStringBuilder.IncludeFailedBatchedCommand.set -> void
1214
Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.get -> string?
1315
Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.set -> void
1416
Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.get -> Npgsql.SslNegotiation

test/Npgsql.Tests/BatchTests.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Npgsql.Tests;
1212
[TestFixture(MultiplexingMode.Multiplexing, CommandBehavior.Default)]
1313
[TestFixture(MultiplexingMode.NonMultiplexing, CommandBehavior.SequentialAccess)]
1414
[TestFixture(MultiplexingMode.Multiplexing, CommandBehavior.SequentialAccess)]
15-
public class BatchTests : MultiplexingTestBase
15+
public class BatchTests : MultiplexingTestBase, IDisposable
1616
{
1717
#region Parameters
1818

@@ -477,7 +477,7 @@ public async Task Batch_with_multiple_errors([Values] bool withErrorBarriers)
477477
public async Task Batch_close_dispose_reader_with_multiple_errors([Values] bool withErrorBarriers, [Values] bool dispose)
478478
{
479479
// Create a temp pool since we dispose the reader (and check the state afterwards) and it can be reused by another connection
480-
await using var dataSource = CreateDataSource();
480+
await using var dataSource = CreateDataSource(x => x.IncludeFailedBatchedCommand = true);
481481
await using var conn = await dataSource.OpenConnectionAsync();
482482
var table = await CreateTempTable(conn, "id INT");
483483

@@ -804,11 +804,16 @@ public async Task Batch_dispose_reuse()
804804
readonly CommandBehavior Behavior;
805805
// ReSharper restore InconsistentNaming
806806

807+
NpgsqlDataSource? _dataSource;
808+
protected override NpgsqlDataSource DataSource => _dataSource ??= CreateDataSource(csb => csb.IncludeFailedBatchedCommand = true);
809+
807810
public BatchTests(MultiplexingMode multiplexingMode, CommandBehavior behavior) : base(multiplexingMode)
808811
{
809812
Behavior = behavior;
810813
IsSequential = (Behavior & CommandBehavior.SequentialAccess) != 0;
811814
}
812815

816+
public void Dispose() => DataSource.Dispose();
817+
813818
#endregion
814819
}

test/Npgsql.Tests/ReaderTests.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -623,9 +623,10 @@ await conn.ExecuteNonQueryAsync($@"
623623
}
624624

625625
[Test, IssueLink("https://github.com/npgsql/npgsql/issues/967")]
626-
public async Task NpgsqlException_references_BatchCommand_with_single_command()
626+
public async Task NpgsqlException_references_BatchCommand_with_single_command([Values] bool includeFailedBatchedCommand)
627627
{
628-
await using var conn = await OpenConnectionAsync();
628+
await using var dataSource = CreateDataSource(x => x.IncludeFailedBatchedCommand = includeFailedBatchedCommand);
629+
await using var conn = await dataSource.OpenConnectionAsync();
629630
var function = await GetTempFunctionName(conn);
630631

631632
await conn.ExecuteNonQueryAsync($@"
@@ -638,7 +639,10 @@ await conn.ExecuteNonQueryAsync($@"
638639
cmd.CommandText = $"SELECT {function}()";
639640

640641
var exception = Assert.ThrowsAsync<PostgresException>(() => cmd.ExecuteReaderAsync(Behavior))!;
641-
Assert.That(exception.BatchCommand, Is.SameAs(cmd.InternalBatchCommands[0]));
642+
if (includeFailedBatchedCommand)
643+
Assert.That(exception.BatchCommand, Is.SameAs(cmd.InternalBatchCommands[0]));
644+
else
645+
Assert.That(exception.BatchCommand, Is.Null);
642646

643647
// Make sure the command isn't recycled by the connection when it's disposed - this is important since internal command
644648
// resources are referenced by the exception above, which is very likely to escape the using statement of the command.
@@ -648,9 +652,10 @@ await conn.ExecuteNonQueryAsync($@"
648652
}
649653

650654
[Test, IssueLink("https://github.com/npgsql/npgsql/issues/967")]
651-
public async Task NpgsqlException_references_BatchCommand_with_multiple_commands()
655+
public async Task NpgsqlException_references_BatchCommand_with_multiple_commands([Values] bool includeFailedBatchedCommand)
652656
{
653-
await using var conn = await OpenConnectionAsync();
657+
await using var dataSource = CreateDataSource(x => x.IncludeFailedBatchedCommand = includeFailedBatchedCommand);
658+
await using var conn = await dataSource.OpenConnectionAsync();
654659
var function = await GetTempFunctionName(conn);
655660

656661
await conn.ExecuteNonQueryAsync($@"
@@ -665,7 +670,10 @@ await conn.ExecuteNonQueryAsync($@"
665670
await using (var reader = await cmd.ExecuteReaderAsync(Behavior))
666671
{
667672
var exception = Assert.ThrowsAsync<PostgresException>(() => reader.NextResultAsync())!;
668-
Assert.That(exception.BatchCommand, Is.SameAs(cmd.InternalBatchCommands[1]));
673+
if (includeFailedBatchedCommand)
674+
Assert.That(exception.BatchCommand, Is.SameAs(cmd.InternalBatchCommands[1]));
675+
else
676+
Assert.That(exception.BatchCommand, Is.Null);
669677
}
670678

671679
// Make sure the command isn't recycled by the connection when it's disposed - this is important since internal command

0 commit comments

Comments
 (0)