From 6e58cc115c8673bf897ad19c837c1c6e6603757c Mon Sep 17 00:00:00 2001 From: Nikita Kazmin Date: Tue, 25 Aug 2026 15:44:15 +0300 Subject: [PATCH] Optimize NpgsqlDataReader.NextResult async state machine --- src/Npgsql/NpgsqlDataReader.cs | 97 ++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/src/Npgsql/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index b72d8fc284..596e7fab8f 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -472,50 +472,7 @@ async Task NextResult(bool async, bool isConsuming = false, CancellationTo if ((Command.WrappingBatch is not null || StatementIndex is 0) && Command.InternalBatchCommands[StatementIndex] is { HasOutputParameters: true } command) { - // If output parameters are present and this is the first row of the resultset, - // we must always read it in non-sequential mode because it will be traversed twice (once - // here for the parameters, then as a regular row). - msg = await Connector.ReadMessage(async, dataRowLoadingMode: DataRowLoadingMode.NonSequential).ConfigureAwait(false); - ProcessMessage(msg); - if (msg.Code == BackendMessageCode.DataRow) - { - Debug.Assert(RowDescription != null); - Debug.Assert(State == ReaderState.BeforeResult); - - try - { - // Temporarily set our state to InResult and non-sequential to allow us to read the values, and in any order. - var isSequential = _isSequential; - var currentPosition = Buffer.ReadPosition; - State = ReaderState.InResult; - _isSequential = false; - try - { - command.PopulateOutputParameters(this, _commandLogger); - - // On success we want to revert any row and column state for the user to be able to read the same row again. - if (async) - await PgReader.CommitAsync().ConfigureAwait(false); - else - PgReader.Commit(); - - State = ReaderState.BeforeResult; // Set the state back - Buffer.ReadPosition = currentPosition; // Restore position - _column = -1; - } - finally - { - // To be on the safe side we always revert this CommandBehavior state change, including on failure. - _isSequential = isSequential; - } - } - catch (Exception e) - { - // TODO: ideally we should flow down to global exception filter and consume there - await Consume(async, firstException: e).ConfigureAwait(false); - throw; - } - } + msg = await ReadMessageWithOutputParameters(async, command).ConfigureAwait(false); } else { @@ -647,6 +604,56 @@ async ValueTask ConsumeResultSet(bool async) } } + async ValueTask ReadMessageWithOutputParameters(bool async, NpgsqlBatchCommand command) + { + // If output parameters are present and this is the first row of the resultset, + // we must always read it in non-sequential mode because it will be traversed twice (once + // here for the parameters, then as a regular row). + var msg = await Connector.ReadMessage(async, dataRowLoadingMode: DataRowLoadingMode.NonSequential).ConfigureAwait(false); + ProcessMessage(msg); + if (msg.Code == BackendMessageCode.DataRow) + { + Debug.Assert(RowDescription != null); + Debug.Assert(State == ReaderState.BeforeResult); + + try + { + // Temporarily set our state to InResult and non-sequential to allow us to read the values, and in any order. + var isSequential = _isSequential; + var currentPosition = Buffer.ReadPosition; + State = ReaderState.InResult; + _isSequential = false; + try + { + command.PopulateOutputParameters(this, _commandLogger); + + // On success we want to revert any row and column state for the user to be able to read the same row again. + if (async) + await PgReader.CommitAsync().ConfigureAwait(false); + else + PgReader.Commit(); + + State = ReaderState.BeforeResult; // Set the state back + Buffer.ReadPosition = currentPosition; // Restore position + _column = -1; + } + finally + { + // To be on the safe side we always revert this CommandBehavior state change, including on failure. + _isSequential = isSequential; + } + } + catch (Exception e) + { + // TODO: ideally we should flow down to global exception filter and consume there + await Consume(async, firstException: e).ConfigureAwait(false); + throw; + } + } + + return msg; + } + /// /// Note that in SchemaOnly mode there are no resultsets, and we read nothing from the backend (all /// RowDescriptions have already been processed and are available) @@ -971,7 +978,7 @@ public override int FieldCount /// Consumes all result sets for this reader, leaving the connector ready for sending and processing further /// queries /// - async Task Consume(bool async, Exception? firstException = null) + async ValueTask Consume(bool async, Exception? firstException = null) { var exceptions = firstException is null ? null : new List { firstException };