Skip to content

Implement GetBytes and GetChars - #377

Merged
Giorgi merged 4 commits into
Giorgi:developfrom
pengdows:feature/getbytes-getchars
Oct 20, 2026
Merged

Giorgi merged 4 commits into
Giorgi:developfrom
pengdows:feature/getbytes-getchars

Conversation

@alaricd

@alaricd alaricd commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

DuckDBDataReader.GetBytes throws NotImplementedException, and GetChars throws NotSupportedException. Both are abstract members of DbDataReader that are meant to read part of a value into a caller's buffer.

The contract

IDataRecord.GetBytes: "Reads a stream of bytes from the specified column offset into the buffer as an array, starting at the given buffer offset." Returns "The actual number of bytes read." Remarks:

If you pass a buffer that is null, GetBytes returns the length of the row in bytes.

IDataRecord.GetChars: "Reads a stream of characters from the specified column offset into the buffer as an array, starting at the given buffer offset." Remarks:

If you pass a buffer that is null, GetChars returns the length of the field in characters.

Why it matters

These are the standard ways to read large values in chunks, or to ask for a value's length without materialising it, and data layers written against DbDataReader call them. SqlClient, Npgsql, MySqlConnector and Microsoft.Data.Sqlite all implement them.

Change

  • GetBytes reads through the existing GetStream(ordinal), which is an UnmanagedMemoryStream over the blob, so nothing is copied beyond what the caller asks for. A null buffer returns the blob's length. Otherwise it copies min(length, remaining) bytes starting at dataOffset and returns the count, or 0 past the end.
  • GetChars does the same over GetString(ordinal), in UTF-16 characters ('ab¢d' has length 4).
  • A negative dataOffset, a bufferOffset outside the buffer, or a length that doesn't fit after bufferOffset throws ArgumentOutOfRangeException.
  • As the docs say ("No conversions are performed"), GetBytes on a non-blob column throws the same InvalidCastException that GetStream does.

Tests: GetBytesReadsABlobWholeOrInParts, GetCharsReadsAStringWholeOrInParts (with a multi-byte UTF-8 character), GetBytesAndGetCharsRejectOffsetsOutsideTheBuffer. The full suite passes.

Conformance tests: GetBytes_works, GetBytes_returns_length_when_buffer_is_null, GetBytes_reads_part_of_blob, GetChars_works, GetChars_returns_length_when_buffer_is_null, GetChars_reads_part_of_string.

Found by the pengdows ADO.NET conformance suite, which runs the documented System.Data.Common contract against every provider; each failure it reports cites the learn.microsoft.com sentence the provider departs from.

🤖 Generated with Claude Code

GetBytes threw NotImplementedException and GetChars NotSupportedException.
IDataRecord.GetBytes and GetChars read part of a value into a buffer, and
"If you pass a buffer that is null" return the value's length. GetBytes
reads the blob through GetStream; GetChars reads the string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coveralls

coveralls commented Oct 20, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 39428820408

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage at 92.527% (no base build to compare)

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 5 uncovered changes across 2 files (57 of 62 lines covered, 91.94%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
DuckDB.NET.Data/DataChunk/Reader/StringVectorDataReader.cs 36 32 88.89%
DuckDB.NET.Data/DuckDBDataReader.cs 26 25 96.15%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 4080
Covered Lines: 3841
Line Coverage: 94.14%
Relevant Branches: 2343
Covered Branches: 2102
Branch Coverage: 89.71%
Branches in Coverage %: Yes
Coverage Strength: 990063.41 hits per line

💛 - Coveralls

@Giorgi

Giorgi commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks for this. I compared it with Npgsql 10.0.0 on PostgreSQL 18.3, and whole reads, partial reads, reads at and past the end, the null-buffer length, NULL values and surrogate pairs all give the same results.

One request before merging: could GetChars avoid decoding the whole value on every call? Right now each call goes through GetString(ordinal), which decodes the full VARCHAR and allocates a new string, then copies length characters out of it. Reading a large text value in chunks is the main reason to call GetChars, and that pattern then decodes and allocates the whole value once per chunk.

The UTF-8 bytes are already available without a string: StringVectorDataReader has the pointer and length in the DuckDBString, the same way GetStream wraps them for BLOBs. With an internal accessor that returns them as a ReadOnlySpan<byte>, GetChars could:

  • for a null buffer, return Encoding.UTF8.GetCharCount(bytes), which counts without allocating;
  • skip dataOffset UTF-16 units by walking the UTF-8 lead bytes (a 1–3 byte sequence is one unit, a 4-byte sequence is two);
  • decode only what fits with Utf8.ToUtf16(bytes[byteOffset..], buffer.AsSpan(bufferOffset, length), out _, out var written), straight into the caller's buffer.

Two edge cases to keep the results the same as now:

  • a dataOffset that lands in the middle of a surrogate pair has to start with the pair's low surrogate;
  • when only one slot is left and the next character is a surrogate pair, Utf8.ToUtf16 writes neither half, while the current code copies the high surrogate.

Optionally, the reader could remember the last (row, column, char offset → byte offset) position, so that reading a value chunk by chunk doesn't scan from the start on every call.

The vector readers are performance-sensitive: please read DuckDB.NET.Data/DataChunk/Reader/DESIGN.md and DuckDB.NET.Data/DataChunk/AGENTS.md before adding the accessor. A test with a large multi-byte string read in small chunks, including chunk boundaries that fall inside a surrogate pair, would cover the new path.

@alaricd

alaricd commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

The chunked UTF-8 GetBytes/GetChars changes are implemented and verified. Build and CodeQL are passing, with no unresolved review requests from my side. Ready for review.

@Giorgi
Giorgi merged commit b5eba07 into Giorgi:develop Oct 20, 2026
11 of 12 checks passed

This branch is waiting to be deployed

1 waiting deployment
external — 6d85eb0e Waiting Oct 20, 2026 by alaricd via Authorize #697
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.

3 participants