Skip to content

refactor(qwp): simplify the wire protocol to one version with inline schemas#39

Open
mtopolnik wants to merge 3 commits into
mainfrom
mt_remove-schema-ref
Open

refactor(qwp): simplify the wire protocol to one version with inline schemas#39
mtopolnik wants to merge 3 commits into
mainfrom
mt_remove-schema-ref

Conversation

@mtopolnik
Copy link
Copy Markdown
Contributor

What

Removes the QWP schema-reference mechanism and the protocol-v2 designation from the Java client. Column schemas now always travel inline on the wire, and the client runs a single protocol version.

This is the client (Phase 2) half of a tandem change; the server half is questdb/questdb#7200. The QWP wire format is breaking, but QWP is beta so there is no compatibility window to preserve.

Why

QWP shipped with a schema-reference optimisation: a sender could transmit a column schema once, receive a schema id, and reference that id on later frames. Store-and-forward retry made this unworkable -- a recorded frame can replay against a fresh server connection that never saw the registering frame, leaving the reference dangling. Senders already stopped using reference mode in production, so the wire kept paying for a mechanism nobody used.

Wire format

Ingress table block (sender -> server):

before: name | row_count | col_count | mode | schema_id | [cols if FULL]
after:  name | row_count | col_count | columns (always inline)

Egress RESULT_BATCH (server -> client): the schema rides only the first batch of a query (batch_seq == 0); continuation batches carry rows and bind against the schema parsed there.

The per-frame version byte stays at offset 4, hardcoded to 1 and validated == 1. The X-QWP-Max-Version handshake header and negotiation machinery stay so a future version bump can re-introduce a range.

Notable changes

  • Decoder: the connection-scoped schema registry is replaced by per-query schema state, invalidated when a new query starts. A continuation batch that arrives before its schema-bearing batch_seq == 0 is now rejected rather than binding rows to a stale schema.
  • API removal (breaking): Sender.builder().maxSchemasPerConnection(...) and the max_schemas_per_connection connection-string option are gone. Code or config strings that reference them now fail -- with a compile error and an "unknown option" error respectively.
  • SERVER_INFO is always delivered post-upgrade (no v2 gate), so the failover role filter no longer has a v1 "no role available" branch.

Trade-offs

  • Every query result re-ships its column schema in batch 0, even when the same client re-runs the same query. The old per-connection registry deduped this; the cost is now a few dozen bytes plus a fresh column-name String per column on each batch 0.
  • The maxSchemasPerConnection builder method disappears; external callers that set it will no longer compile. Acceptable for beta.

Test plan

  • mvn -pl core test (client unit + decoder-hardening suites)
  • Schema-reference and schema-id-exhaustion tests removed (mechanism gone); other decoder-hardening coverage (truncation, corrupt zstd, oversized varints) retained.
  • SelfSufficientFramesTest trimmed to its symbol-dict-delta invariant.
  • End-to-end multi-batch / wide-schema coverage lives in the server repo's cutlass-test-suite, which runs this client against the matching server branch.

Coordination

Tandem with questdb/questdb#7200. The wire format is breaking; merge the two together. This PR's branch is named to match the server branch so the client CI builds against the server PR instead of master.

🤖 Generated with Claude Code

mtopolnik and others added 3 commits June 2, 2026 16:48
Mirror the server-side QWP simplification on the Java client. The wire
format drops the per-table-block schema mode byte and schema id; column
schemas always travel inline. Egress RESULT_BATCH carries the schema only
on the first batch (batch_seq == 0); continuation batches reuse it.

Ingress encode: QwpColumnWriter writes columns inline (no mode byte or
schema_id), QwpWebSocketEncoder and QwpUdpSender drop the useSchemaRef
parameter, and QwpWebSocketSender drops the schema-id allocator and the
maxSchemasPerConnection plumbing across its connect overloads.
QwpTableBuffer loses its schemaId field.

Egress decode: QwpResultBatchDecoder parses the inline schema only on
batch_seq == 0 into a reused per-query schema, replacing the connection
schema registry; QwpEgressIoThread invalidates it when a new query starts.
QwpQueryClient always reads SERVER_INFO now that the version gate is gone.

Constants collapse to a single VERSION = 1, and CACHE_RESET keeps only
RESET_MASK_DICT. The Sender builder loses maxSchemasPerConnection() and the
max_schemas_per_connection connect-string key (a breaking change; QWP is
beta). Client unit tests updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up doc cleanup after removing QWP schema references and protocol
v2. No logic changes.

Reword comments that still described the removed two-version,
schema-reference world: the CACHE_RESET doc no longer advertises a
schema-cache mask bit, SERVER_INFO is "every connection" rather than
"every v2 connection", and the capability-trailer comments are
version-neutral instead of naming a nonexistent v2.0/v2.1.

Expand the QwpResultBatchDecoder querySchema comment to record two
consequences of dropping the schema registry: every query's batch 0 now
re-parses the inline schema and allocates a column-name String per
column, and the single schema slot is correct only while the IoThread
runs one query at a time (pipelining would need request_id keying).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Un-glue eight connect() parameter lists in QwpWebSocketSender where
removing the maxSchemasPerConnection parameter had collapsed two
arguments onto a single line with stray indentation. No behaviour
change -- whitespace only.

Repoint three Javadoc/comment references that pointed at the deleted
docs/qwp/*.md design docs to their published equivalents under
https://questdb.com/docs/ -- in QwpColumnBatch, ColumnView, and
WebSocketResponse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mtopolnik
Copy link
Copy Markdown
Contributor Author

[PR Coverage check]

😍 pass : 31 / 37 (83.78%)

file detail

path covered line new line coverage
🔵 io/questdb/client/cutlass/qwp/client/QwpEgressIoThread.java 0 1 00.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpWebSocketSender.java 3 5 60.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpResultBatchDecoder.java 19 22 86.36%
🔵 io/questdb/client/cutlass/qwp/client/QwpWebSocketEncoder.java 4 4 100.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpUdpSender.java 2 2 100.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpColumnWriter.java 2 2 100.00%
🔵 io/questdb/client/cutlass/qwp/client/QwpQueryClient.java 1 1 100.00%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant