Skip to content

Commit 12f1677

Browse files
committed
Fix auto-preparation with SchemaOnly (#4420)
Fixes #4404 (cherry picked from commit e914ed7)
1 parent 65a7ec7 commit 12f1677

File tree

3 files changed

+64
-6
lines changed

3 files changed

+64
-6
lines changed

src/Npgsql/NpgsqlCommand.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,9 +1034,8 @@ async Task WriteExecuteSchemaOnly(NpgsqlConnector connector, bool async, bool fl
10341034

10351035
if (batchCommand.PreparedStatement?.State == PreparedState.Prepared)
10361036
continue; // Prepared, we already have the RowDescription
1037-
Debug.Assert(batchCommand.PreparedStatement == null);
10381037

1039-
await connector.WriteParse(batchCommand.FinalCommandText!, string.Empty, batchCommand.PositionalParameters, async, cancellationToken);
1038+
await connector.WriteParse(batchCommand.FinalCommandText!, batchCommand.StatementName, batchCommand.PositionalParameters, async, cancellationToken);
10401039
await connector.WriteDescribe(StatementOrPortal.Statement, batchCommand.StatementName, async, cancellationToken);
10411040
wroteSomething = true;
10421041
}

src/Npgsql/NpgsqlDataReader.cs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,27 @@ async Task<bool> NextResultSchemaOnly(bool async, bool isConsuming = false, Canc
623623
}
624624
else
625625
{
626+
var pStatement = statement.PreparedStatement;
627+
if (pStatement != null)
628+
{
629+
Debug.Assert(!pStatement.IsPrepared);
630+
if (pStatement.StatementBeingReplaced != null)
631+
{
632+
Expect<CloseCompletedMessage>(await Connector.ReadMessage(async), Connector);
633+
pStatement.StatementBeingReplaced.CompleteUnprepare();
634+
pStatement.StatementBeingReplaced = null;
635+
}
636+
}
637+
626638
Expect<ParseCompleteMessage>(await Connector.ReadMessage(async), Connector);
639+
640+
if (statement.IsPreparing)
641+
{
642+
pStatement!.State = PreparedState.Prepared;
643+
Connector.PreparedStatementManager.NumPrepared++;
644+
statement.IsPreparing = false;
645+
}
646+
627647
Expect<ParameterDescriptionMessage>(await Connector.ReadMessage(async), Connector);
628648
var msg = await Connector.ReadMessage(async);
629649
switch (msg.Code)
@@ -659,12 +679,31 @@ async Task<bool> NextResultSchemaOnly(bool async, bool isConsuming = false, Canc
659679
{
660680
State = ReaderState.Consumed;
661681

662-
// Reference the triggering statement from the exception (for batching)
663-
if (e is PostgresException postgresException &&
664-
Command.IsWrappedByBatch &&
665-
StatementIndex >= 0 && StatementIndex < _statements.Count)
682+
// Reference the triggering statement from the exception
683+
if (e is PostgresException postgresException && StatementIndex >= 0 && StatementIndex < _statements.Count)
666684
{
667685
postgresException.BatchCommand = _statements[StatementIndex];
686+
687+
// Prevent the command or batch from by recycled (by the connection) when it's disposed. This is important since
688+
// the exception is very likely to escape the using statement of the command, and by that time some other user may
689+
// already be using the recycled instance.
690+
if (!Command.IsWrappedByBatch)
691+
{
692+
Command.IsCached = false;
693+
}
694+
}
695+
696+
// An error means all subsequent statements were skipped by PostgreSQL.
697+
// If any of them were being prepared, we need to update our bookkeeping to put
698+
// them back in unprepared state.
699+
for (; StatementIndex < _statements.Count; StatementIndex++)
700+
{
701+
var statement = _statements[StatementIndex];
702+
if (statement.IsPreparing)
703+
{
704+
statement.IsPreparing = false;
705+
statement.PreparedStatement!.AbortPrepare();
706+
}
668707
}
669708

670709
throw;

test/Npgsql.Tests/AutoPrepareTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Data;
23
using System.Linq;
34
using System.Threading;
45
using System.Threading.Tasks;
@@ -556,6 +557,25 @@ CREATE OR REPLACE FUNCTION pg_temp.emit_exception() RETURNS VOID AS
556557
Assert.That(conn.ExecuteScalar("SELECT 3"), Is.EqualTo(3));
557558
}
558559

560+
[Test, IssueLink("https://github.com/npgsql/npgsql/issues/4404")]
561+
public async Task SchemaOnly()
562+
{
563+
var csb = new NpgsqlConnectionStringBuilder(ConnectionString)
564+
{
565+
AutoPrepareMinUsages = 2,
566+
MaxAutoPrepare = 10,
567+
};
568+
569+
using var _ = CreateTempPool(csb, out var connString);
570+
await using var conn = await OpenConnectionAsync(connString);
571+
await using var cmd = new NpgsqlCommand("SELECT 1", conn);
572+
573+
for (var i = 0; i < 5; i++)
574+
{
575+
await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly);
576+
}
577+
}
578+
559579
void DumpPreparedStatements(NpgsqlConnection conn)
560580
{
561581
using var cmd = new NpgsqlCommand("SELECT name,statement FROM pg_prepared_statements", conn);

0 commit comments

Comments
 (0)