Skip to content

Commit 3b6c74c

Browse files
committed
Fix getting wrong schema with CommandBehavior.SchemaOnly and autoprepare (npgsql#6040)
Fixes npgsql#6038 (cherry picked from commit 5ede53c)
1 parent c7bb8bc commit 3b6c74c

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
@@ -1120,11 +1120,24 @@ async Task WriteExecuteSchemaOnly(NpgsqlConnector connector, bool async, bool fl
11201120
await new TaskSchedulerAwaitable(ConstrainedConcurrencyScheduler);
11211121

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

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

1127-
await connector.WriteParse(batchCommand.FinalCommandText!, batchCommand.StatementName,
1140+
await connector.WriteParse(batchCommand.FinalCommandText, batchCommand.StatementName,
11281141
batchCommand.CurrentParametersReadOnly,
11291142
async, cancellationToken).ConfigureAwait(false);
11301143
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)