Skip to content

Decouple PgReader GetTextReader and GetChars #6490

Draft
NinoFloris wants to merge 10 commits intomainfrom
decouple-textreader
Draft

Decouple PgReader GetTextReader and GetChars #6490
NinoFloris wants to merge 10 commits intomainfrom
decouple-textreader

Conversation

@NinoFloris
Copy link
Copy Markdown
Member

@NinoFloris NinoFloris commented Mar 12, 2026

Based on #5480

Loosening restrictions on concurrently active operations in non-sequential mode a bit. Needed to make one assert conditional on sequential mode for tests to pass, though the assert message already implied it should have been conditional from the start.

This PR basically changes the fact that GetChars needs a column stream to an internal implementation detail (it's still being cleaned up on column commit etc).

It will help with future cleanup work around PgReader.GetStream, and it makes behavior a bit more regular in non-sequential mode, as GetBytes doesn't dispose column streams returned to users either.

Copilot AI review requested due to automatic review settings March 12, 2026 08:13
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR relaxes some restrictions around concurrently active read operations in non-sequential mode by decoupling PgReader’s GetTextReader implementation from column-stream tracking (and by treating GetChars’ internal stream usage as an implementation detail). This aligns behavior more closely with GetBytes semantics and supports future cleanup work around PgReader.GetStream.

Changes:

  • Adjusts the GetTextReader test to only expect “second text reader on same column throws” under sequential access.
  • Refactors PreparedTextReader to no longer depend on / dispose an underlying ColumnStream.
  • Updates PgReader to separate “user-facing stream” tracking from GetChars internal reading, and ensures cleanup also disposes _preparedTextReader.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
test/Npgsql.Tests/ReaderTests.cs Makes the “GetTextReader twice” assertion conditional on sequential mode.
src/Npgsql/PreparedTextReader.cs Removes ColumnStream coupling and stream-disposal behavior from PreparedTextReader.
src/Npgsql/Internal/PgReader.cs Adjusts stream creation/tracking and GetTextReader/GetChars plumbing; improves cleanup logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings March 25, 2026 12:00
@NinoFloris NinoFloris force-pushed the decouple-textreader branch from 29451ea to d668b13 Compare March 25, 2026 12:00
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/Npgsql/Internal/PgReader.cs:674

  • Restart() no longer calls Cleanup() when switching from resumable to non-resumable and rewinding the field. If a user-facing stream/text reader is still open, Restart will rewind the underlying buffer position without invalidating that stream, leaving it in an inconsistent state. Suggest explicitly handling active user streams here (dispose/throw) or otherwise guaranteeing that any user-facing readers are independent of PgReader buffer position before allowing rewinds.
        // From this point on we're not resuming, we're resetting any previous converter state and rewinding our position.

        if (NestedInitialized)
            ResetCurrent();

        _fieldConsumed = false;
        _resumable = resumable;
        RewindCore(FieldOffset);


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 207 to +221
// This will cause any previously handed out StreamReaders etc to throw, as intended.
if (_userActiveStream is not null)
if (!forGetChars && StreamActive)
DisposeUserActiveStream(async: false).GetAwaiter().GetResult();

length ??= CurrentRemaining;
CheckBounds(length.GetValueOrDefault());
return _userActiveStream = _buffer.CreateStream(length.GetValueOrDefault(), canSeek && length <= _buffer.ReadBytesLeft, consumeOnDispose: false);
var stream = _buffer.CreateStream(length.GetValueOrDefault(), canSeek && length <= _buffer.ReadBytesLeft, consumeOnDispose: false);

// GetChars isn't a user facing stream and handles its own tracking and cleanup.
if (!forGetChars)
{
_requiresCleanup = true;
_userActiveStream = stream;
}
return stream;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetColumnStream(forGetChars: true) can now create a second ColumnStream while a user-facing stream/text reader is still active. Since ColumnStream reads by advancing the shared NpgsqlReadBuffer.ReadPosition, having two active streams (or rewinding the buffer while one is active) can corrupt the other reader/stream (e.g., a large GetTextReader that returns StreamReader, then GetChars triggers a restart/rewind). Consider either rejecting this scenario (throw) or ensuring GetChars uses an independent buffered source (no shared ReadPosition) when a user stream/text reader is active.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should switch to an isolated implementation for non-sequential streams, as we know there won't be any I/O.

Comment on lines +1717 to +1726
if (IsSequential)
{
Assert.That(async () => await textReaderGetter(reader, 0),
Throws.Exception.TypeOf<InvalidOperationException>(),
"Sequential text reader twice on same column");
}
else
{
Assert.That(reader.GetChars(0, 0, actual, 4, 1), Is.EqualTo(1));
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-sequential branch now exercises GetChars while a TextReader is open, but only with a very small value that will go through the PreparedTextReader path. The behavior being changed is most fragile when GetTextReader returns a StreamReader (value > PgReader.MaxPreparedTextReaderSize), so adding a variant of this test with a >64KB string would better cover the new concurrency behavior.

Copilot uses AI. Check for mistakes.
# Conflicts:
#	src/Npgsql/Internal/PgReader.cs
We haven't left the column so we can keep open any resources pointing into it

# Conflicts:
#	src/Npgsql/Internal/PgReader.cs
@NinoFloris NinoFloris force-pushed the decouple-textreader branch from d668b13 to a4f9813 Compare March 25, 2026 13:41
@NinoFloris NinoFloris requested a review from Brar as a code owner March 25, 2026 13:41
@NinoFloris
Copy link
Copy Markdown
Member Author

Taking this to draft until I have some time to rework our non-sequential column streams.

@NinoFloris NinoFloris marked this pull request as draft March 25, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants