feat(client-v2): decode QBit in the Native format - #2983
Conversation
Reading a plain top-level QBit(Float32|Float64|BFloat16, dimension) column over the Native format previously failed fast: the server transmits QBit there using its internal bit-plane-transposed Tuple(FixedString) layout rather than the Array-like encoding used over RowBinary, which the reader could not decode. Reconstruct the vector by reversing that transposition (the exact inverse of the server's SerializationQBit transpose): read the element_size bit-plane FixedString columns column-major and, for each row, scatter each plane's bits back to their element/bit positions. Values are materialized into the same float[]/double[] ArrayValue representation as the RowBinary path, so a QBit column round-trips identically through either format. Strided QBit(E, dim, stride), Nullable/LowCardinality-wrapped QBit, and QBit nested inside a container remain rejected with a clear error directing callers to a RowBinary format. Implements: #2610
Client V2 CoverageCoverage Report
Class Coverage
|
JDBC V2 CoverageCoverage Report
Class Coverage
|
JDBC V1 CoverageCoverage Report
Class Coverage
|
Client V1 CoverageCoverage Report
Class Coverage
|
TriageCategory: Summary What this impacts
Concerns
Required reviewer action
|
There was a problem hiding this comment.
Pull request overview
Adds client-v2 support for reading plain top-level QBit(Float32|Float64|BFloat16, dimension) columns from ClickHouse’s Native output format by reversing the server’s bit-plane transposition, bringing Native read parity with the existing RowBinary QBit handling.
Changes:
- Implement Native-format
QBitblock decoding inBinaryStreamReaderand wire it intoNativeFormatReaderbehind a strict “decodable QBit” gate. - Add integration tests asserting
Native == RowBinaryparity for multiple element types, dimensions, and multi-row blocks; keep nested/unsupported variants rejected. - Update
docs/features.mdandCHANGELOG.mdto reflect Native read support and remaining limitations.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
client-v2/src/main/java/com/clickhouse/client/api/data_formats/internal/BinaryStreamReader.java |
Adds Native QBit bit-plane transpose inversion to materialize per-row vectors. |
client-v2/src/main/java/com/clickhouse/client/api/data_formats/NativeFormatReader.java |
Decodes top-level float QBit via the new reader method; rejects other QBit shapes early with an error. |
client-v2/src/test/java/com/clickhouse/client/datatypes/DataTypeTests.java |
Adds integration coverage for Native QBit reads (single/multi-row) and preserves nested rejection behavior. |
docs/features.md |
Documents Native read support for plain top-level QBit, plus unsupported variants. |
CHANGELOG.md |
Notes the new Native read capability and limitations in the unreleased section. |
…ard plane-size overflow - Reject message + docs (features.md, CHANGELOG) now state that only a plain top-level QBit with a Float32/Float64/BFloat16 element type is decoded over Native; a QBit with any other element type is rejected alongside the strided/Nullable/LowCardinality/nested cases. The server constrains QBit element types to those three, so the non-float case is only reachable via the client's forward-compat lenient type parser. - Guard the per-plane byte count (nRows * ceil(dimension/8)) against int overflow in readQBitNative: compute it as a long and throw a clear ClientException instead of wrapping to a negative/short readNBytes length that would desynchronize the stream. - Add a unit test pinning the overflow guard.
chernser
left a comment
There was a problem hiding this comment.
coverage of the new code is only 41% - add more test with different values, test whatever fail scenarios possible
there are too many verbose comments - shorten them. create separate document about Qbit encoding in Native and Rowbinary to shortly describe the "problem".
…add encoding doc - Add unit tests for BinaryStreamReader#readQBitNative covering all supported element types, single/two-byte bit planes, multi-row slicing, special float values (NaN/Inf/-0.0), and the unsupported-element-type and int-overflow guards, driven by Native-format QBit bytes captured from the server (readQBitNative new-code coverage 41% -> 100%). - Add NativeFormatReaderQBitTest covering the block-level decode-vs-reject routing (isNativeDecodableQBit / containsQBit) without a live server. - Move the bit-plane transpose math into docs/qbit-encoding.md and trim the inline comments in readQBitNative / NativeFormatReader to short pointers; link the doc from docs/features.md. Addresses @chernser review feedback on #2983.
|
Thanks for the review, @chernser — addressed all three in f71744e: 1. New-code coverage. Root cause of the 41%: the QBit integration tests skip on the coverage build (it runs CH 25.3 via
Local jacoco now reports 2. Verbose comments. Trimmed the byte/bit-math comments in 3. Encoding doc. Added All QBit unit + integration tests are green locally. |
|



Description
Implements #2610 (follow-up to #2939).
client-v2gainedQBitread/write overRowBinaryin #2939, but reading aQBitcolumn over theNativeoutput format still failed fast: overNativethe server transmitsQBitusing its internal bit-plane-transposedTuple(FixedString(...))layout, not theArray(element_type)-like encoding used overRowBinary, and the reader had no decoder for it.This PR decodes a plain top-level
QBit(Float32 | Float64 | BFloat16, dimension)column from theNativeformat, so such a column can now be read throughNativeand round-trips identically toRowBinary.Design
The
NativeQBitlayout was verified against the ClickHouse server source (SerializationQBit::transposeBits) and empirically against server26.5.1:QBitcolumn is the server's nestedTuple(FixedString(S) × element_size)serialized column-major (plane-major):element_sizebit planes (Float32→32,Float64→64,BFloat16→16), eachS = ceil(dimension/8)bytes per row, laid outplane0[all rows] plane1[all rows] ….pcarries float bit(element_size − 1 − p)of every element (most-significant plane first). Within a plane'sSbytes, elementjis at byte(bitIndex >> 3), bit(bitIndex & 7)wherebitIndex = (S*8 − 1) − (j ^ 7).BinaryStreamReader#readQBitColumnreverses that transposition per row and materializes the vector into the samefloat[]/double[]ArrayValue(via the sameconvertArraypath) that the RowBinaryreadQBitproduces — so aQBitread overNativeand overRowBinaryyields equal values through generic records, binary readers, and POJO binding.NativeFormatReaderdecodes the column when it is a plain top-level floatQBit, and otherwise preserves the existing fail-loud rejection.Kept out of scope (still rejected with a clear, actionable
ClientException, as before):QBit(E, dim, stride)— a master-only server feature (server26.5.1rejects the 3-arg type), so it cannot be exercised/verified here;Nullable/LowCardinality-wrappedQBit;QBitnested inside a container (Array/Tuple/Map).These remain readable over a
RowBinaryformat.Changes
client-v2BinaryStreamReader: newreadQBitColumn(column, nRows)— the block-level inverse bit-plane transpose decoder.client-v2NativeFormatReader.readBlock: decode a plain top-level floatQBit; newisNativeDecodableQBitgate; the rejection (andcontainsQBitjavadoc) narrowed to the strided/wrapped/nested variants with an accurate message.DataTypeTests): newtestQBitNativeFormatRead(@DataProvider:Float32/Float64/BFloat16; dims 3, 8, 10, 16 covering single-byte and two-byte / partially-filled planes; negative and fractional values; assertsNative == RowBinaryparity),testQBitNativeFormatMultiRow(column-major per-row slicing forS=1andS=2), andtestQBitNativeFormatRejectednarrowed/renamed totestQBitNativeFormatNestedRejected(top-levelQBitnow decodes; the nested case still rejects — the one existing assertion this feature intentionally changes).CHANGELOG.mdanddocs/features.md: updated theQBitentry to describeNativeread support and the remaining unsupported variants.Test
Run against ClickHouse
26.5.1via the integration harness:testQBitNativeFormatRead(6 cases ×Native+RowBinary),testQBitNativeFormatMultiRow,testQBitNativeFormatNestedRejected, plus the pre-existingtestQBit/testQBitInDynamicColumn— 10/10 green.DataTypeTestsintegration class (101/101) andBinaryStreamReaderTests+BinaryReaderBackedRecordTestunit tests (31/31) — no regression; no existing test weakened.The tests place the
QBitcolumn between a leadingInt64and a trailingInt32and assert the whole row, so a wrong byte count / desync is detected; expected values are derived from the input and cross-checked against the independently-correct RowBinary path.Docs / surface
0.11.0-rc1section as feat(client-v2, jdbc-v2): add QBit data type support #2939).docs/features.mdQBitentry updated.QBit)....data_formats.internalpackage (not tracked public API).Pre-PR validation gate
newBinaryFormatReader)client-v2build + focused tests green; no regressiondocs/features.md+CHANGELOG.mdupdated perAGENTS.md/changes_checklist.mdNo backport needed. No server-side change required.