Skip to content

Commit 5ede53c

Browse files
authored
Fix getting wrong schema with CommandBehavior.SchemaOnly and autoprepare (#6040)
Fixes #6038
1 parent 5d073da commit 5ede53c

File tree

3 files changed

+78
-14
lines changed

3 files changed

+78
-14
lines changed

src/Npgsql/NpgsqlCommand.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,11 +1118,24 @@ async Task WriteExecuteSchemaOnly(NpgsqlConnector connector, bool async, bool fl
11181118
await new TaskSchedulerAwaitable(ConstrainedConcurrencyScheduler);
11191119

11201120
var batchCommand = InternalBatchCommands[i];
1121+
var pStatement = batchCommand.PreparedStatement;
1122+
1123+
pStatement?.RefreshLastUsed();
1124+
1125+
Debug.Assert(batchCommand.FinalCommandText is not null);
1126+
1127+
if (pStatement != null && !batchCommand.IsPreparing)
1128+
{
1129+
// Prepared, we already have the RowDescription
1130+
Debug.Assert(pStatement.IsPrepared);
1131+
continue;
1132+
}
11211133

1122-
if (batchCommand.PreparedStatement?.State == PreparedState.Prepared)
1123-
continue; // Prepared, we already have the RowDescription
1134+
// We may have a prepared statement that replaces an existing statement - close the latter first.
1135+
if (pStatement?.StatementBeingReplaced != null)
1136+
await connector.WriteClose(StatementOrPortal.Statement, pStatement.StatementBeingReplaced.Name!, async, cancellationToken).ConfigureAwait(false);
11241137

1125-
await connector.WriteParse(batchCommand.FinalCommandText!, batchCommand.StatementName,
1138+
await connector.WriteParse(batchCommand.FinalCommandText, batchCommand.StatementName,
11261139
batchCommand.CurrentParametersReadOnly,
11271140
async, cancellationToken).ConfigureAwait(false);
11281141
await connector.WriteDescribe(StatementOrPortal.Statement, batchCommand.StatementName, async, cancellationToken).ConfigureAwait(false);

src/Npgsql/NpgsqlDataReader.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,11 @@ async Task<bool> NextResultSchemaOnly(bool async, bool isConsuming = false, Canc
713713
break;
714714
case BackendMessageCode.RowDescription:
715715
// We have a resultset
716-
RowDescription = _statements[StatementIndex].Description = (RowDescriptionMessage)msg;
716+
// RowDescription messages are cached on the connector, but if we're auto-preparing, we need to
717+
// clone our own copy which will last beyond the lifetime of this invocation.
718+
RowDescription = _statements[StatementIndex].Description = preparedStatement == null
719+
? (RowDescriptionMessage)msg
720+
: ((RowDescriptionMessage)msg).Clone();
717721
Command.FixupRowDescription(RowDescription, StatementIndex == 0);
718722
break;
719723
default:
@@ -734,17 +738,7 @@ async Task<bool> NextResultSchemaOnly(bool async, bool isConsuming = false, Canc
734738

735739
// Found a resultset
736740
if (RowDescription is not null)
737-
{
738-
if (ColumnInfoCache?.Length >= ColumnCount)
739-
Array.Clear(ColumnInfoCache, 0, ColumnCount);
740-
else
741-
{
742-
if (ColumnInfoCache is { } cache)
743-
ArrayPool<ColumnInfo>.Shared.Return(cache, clearArray: true);
744-
ColumnInfoCache = ArrayPool<ColumnInfo>.Shared.Rent(ColumnCount);
745-
}
746741
return true;
747-
}
748742
}
749743

750744
State = ReaderState.Consumed;

test/Npgsql.Tests/AutoPrepareTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,63 @@ public async Task SchemaOnly()
538538
await cmd.ExecuteScalarAsync();
539539
}
540540

541+
[Test, IssueLink("https://github.com/npgsql/npgsql/issues/6038")]
542+
public async Task Auto_prepared_schema_only_correct_schema()
543+
{
544+
await using var dataSource = CreateDataSource(csb =>
545+
{
546+
csb.MaxAutoPrepare = 1;
547+
csb.AutoPrepareMinUsages = 5;
548+
});
549+
await using var connection = await dataSource.OpenConnectionAsync();
550+
var table1 = await CreateTempTable(connection, "foo int");
551+
var table2 = await CreateTempTable(connection, "bar int");
552+
553+
await using var cmd = connection.CreateCommand();
554+
cmd.CommandText = $"SELECT * FROM {table1}";
555+
for (var i = 0; i < 5; i++)
556+
{
557+
// Make sure we prepare the first query
558+
await using (await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly)) { }
559+
}
560+
561+
cmd.CommandText = $"SELECT * FROM {table2}";
562+
// The second query will load RowDescription, which is a singleton on NpgsqlConnector
563+
// This shouldn't affect the first query, because we create a copy of RowDescription on prepare
564+
await using (await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly)) { }
565+
566+
cmd.CommandText = $"SELECT * FROM {table1}";
567+
// If we indeed made a copy of RowDescription on prepare, we should get the column for the first query and not for the second
568+
await using var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo);
569+
var columns = await reader.GetColumnSchemaAsync();
570+
Assert.That(columns.Count, Is.EqualTo(1));
571+
Assert.That(columns[0].ColumnName, Is.EqualTo("foo"));
572+
}
573+
574+
[Test]
575+
public async Task Auto_prepared_schema_only_replace()
576+
{
577+
await using var dataSource = CreateDataSource(csb =>
578+
{
579+
csb.MaxAutoPrepare = 1;
580+
csb.AutoPrepareMinUsages = 5;
581+
});
582+
await using var connection = await dataSource.OpenConnectionAsync();
583+
584+
await using var cmd = connection.CreateCommand();
585+
cmd.CommandText = "SELECT 1";
586+
for (var i = 0; i < 5; i++)
587+
{
588+
await using (await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly)) { }
589+
}
590+
591+
cmd.CommandText = "SELECT 2";
592+
for (var i = 0; i < 5; i++)
593+
{
594+
await using (await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly)) { }
595+
}
596+
}
597+
541598
[Test]
542599
public async Task Auto_prepared_statement_invalidation()
543600
{

0 commit comments

Comments
 (0)