@@ -17,7 +17,8 @@ Go client library for QuestDB ingestion. Three transports:
1717
1818Module path: ` github.com/questdb/go-questdb-client/v4 ` — the ` /v4 ` segment is
1919load-bearing when importing within this repo. Minimum Go: 1.23 (go.mod pins
20- ` go 1.23 ` with a ` 1.24.4 ` toolchain).
20+ ` go 1.23 ` with no ` toolchain ` directive; CI's ` 1.23.x ` /` 1.24.x ` matrix runs
21+ under ` GOTOOLCHAIN=local ` ).
2122
2223## Commands
2324
@@ -88,92 +89,20 @@ encodes a batch into `qwpSfCursorEngine` via `engineAppendBlocking`; the
8889` qwpSfSendLoop ` goroutine drains it to the WebSocket, parses ACKs, advances
8990` engineAckedFsn ` , and owns reconnect + replay from ` engineAckedFsn() + 1 ` .
9091
91- ** Cursor frames are self-sufficient** — full schema definitions plus the full
92- symbol dictionary from id 0, every flush. This is what makes
93- reconnect/replay/orphan-adoption safe across a fresh server connection.
94-
95- ** The wire carries no schema id and no schema mode byte.** A table block is
96- ` table_name, row_count, col_count, inline columns, column data ` ; the inline
97- column definitions are the authoritative schema, repeated on every frame. There
98- is no ` nextSchemaId ` accumulator on the sender, no per-table ` schemaId ` field on
99- the table buffer, no schema-change detection, and no reference mode. (QWP once
100- carried a mode byte + schema id plus a schema-reference optimisation; it was
101- removed across the server and all clients.) On egress, the decoder parses the
102- schema from the first ` RESULT_BATCH ` of a query (` batch_seq == 0 ` ) into
103- ` qwpQueryDecoder.querySchema ` and reuses it for that query's continuation
104- batches; ` qwpEgressIO.dispatcherRun ` calls ` resetQuerySchema ` at the start of
105- every query so a schema never leaks across query boundaries.
106-
107- Symbol-dict tracking (` maxSentSymbolId ` , ` batchMaxSymbolId ` ) is still in place,
108- and both fields are load-bearing. The encoder always passes ` -1 ` as the
109- ` maxSentId ` arg of ` encodeMultiTableWithDeltaDict ` to force "full dict from id
110- 0", but ` batchMaxSymbolId ` is the separate ` batchMaxId ` arg and bounds the dict
111- actually written: ` writeDeltaDict ` emits ` globalDict[0..batchMaxSymbolId] ` , so
112- dropping it would silently truncate the symbol dict. ` maxSentSymbolId ` is the
113- cross-flush high-water mark that ` resetAfterFlush ` rewinds ` batchMaxSymbolId ` to
114- (never to ` -1 ` ), so a later batch reusing only earlier symbols still writes the
115- full dict its rows reference. Both are also read by tests and external
116- observers, but that is incidental to their wire role.
117-
118- ` WithInFlightWindow(n) ` / ` in_flight_window=n ` is ** retained but a no-op** in
119- the cursor architecture — backpressure is governed by the engine's segment-ring
120- + ` engineAppendBlocking ` deadline.
121-
122- ### Java-parity QWP knobs (not in connect-string.md)
123-
124- These connect-string keys are recognised by the Java client
125- (` Sender.java ` ) but are not listed in the
126- [ native-client spec] ( https://github.com/questdb/questdb-enterprise/blob/main/questdb/docs/qwp/connect-string.md ) .
127- We accept them for Java-parity portability — a connect string that
128- works on the Java client must work here. None should ever be
129- considered for removal without a matching change in Java:
130-
131- - ` gorilla=on|off ` — gates the Gorilla timestamp encoding in
132- ` qwp_encoder.go ` (FLAG_GORILLA). Default ` on ` .
133- - ` in_flight_window=N ` — see the "retained but a no-op" note above.
134-
135- ` close_timeout=N ` (millisecond integer) was a v4.0–v4.5 Go-only key
136- for the memory-mode close path. The cursor architecture unified
137- memory and SF onto ` close_flush_timeout_millis ` , which the spec
138- also defines. The parser now rejects ` close_timeout= ` with a
139- migration hint pointing at ` close_flush_timeout_millis ` .
140- ` WithCloseTimeout(d) ` is retained as a deprecated alias that routes
141- positive durations through ` close_flush_timeout_millis ` ; new code
142- should use ` WithCloseFlushTimeout ` directly.
143-
144- Flush semantics: ` Flush ` / ` FlushAndGetSequence ` ** never wait for the server
145- ACK** — they return once the batch is published into the cursor engine (in-RAM
146- for memory mode, on-disk for SF) and the send loop delivers + replays it in the
147- background. This matches the Java spec (` design/qwp-cursor-durability.md `
148- decision #1 : "flush() never waits for ACK; ACKs are async") and is uniform
149- across both the pending-rows and zero-pending branches and auto-flush — all
150- route through ` enqueueCursor ` ; explicit ` Flush ` only additionally surfaces a
151- latched send-loop error eagerly. (` Flush ` was an ACK barrier
152- through v4.2.0; that contract was dropped when the cursor/SF architecture made
153- local persistence, not the ACK, the durability guarantee.) ` FlushAndGetSequence ` returns the
154- published FSN — the upper bound of any ` SenderError.ToFsn ` for that batch;
155- ** pair it with ` AwaitAckedFsn ` for server-ACK confirmation** (the dedicated
156- primitive now that ` Flush ` no longer blocks on ACKs).
157-
158- Orphan-slot adoption (SF mode, ` drain_orphans=on ` ) is implemented in
159- ` qwp_sf_orphan.go ` + ` qwp_sf_drainer.go ` + ` qwp_sf_round_walk.go ` ; drainers run
160- in dedicated goroutines and are visible via ` QwpSender.BackgroundDrainers() ` .
92+ ** ` Flush ` / ` FlushAndGetSequence ` never wait for the server ACK** — they return
93+ once the batch is published to the cursor engine (in-RAM for memory mode,
94+ on-disk for SF); delivery and replay to the server run in the background. For
95+ server-ACK confirmation, pair ` FlushAndGetSequence ` with ` AwaitAckedFsn ` .
16196
16297### Error handling
16398
16499QWP server rejections surface as ` *SenderError ` (` sender_error.go ` is canonical
165- for categories + policy enum). Two paths: async callback registered via
166- ` WithErrorHandler ` , and producer-side typed error via ` errors.As ` after ` Flush `
167- / ` FlushAndGetSequence ` .
168-
169- Policy resolution precedence (highest first): ` WithErrorPolicyResolver ` →
170- ` WithErrorPolicy(category, ...) ` → connect-string ` on_*_error ` →
171- ` on_server_error ` → spec defaults. ` PROTOCOL_VIOLATION ` and ` UNKNOWN ` are never
172- user-configurable — always HALT.
173-
174- A HALT latches the typed error on the I/O loop; ` sendLoopCheckError() ` surfaces
175- it on the next producer call. The sender does not auto-resume — close + rebuild
176- is the supported recovery (matches Java).
100+ for the categories + policy enum). ` PROTOCOL_VIOLATION ` and ` UNKNOWN ` are never
101+ user-configurable — always HALT. A HALT records the typed error on the I/O loop
102+ and surfaces it on the next producer call; the sender does not auto-resume, so
103+ close + rebuild is the supported recovery. Policy precedence, highest first:
104+ ` WithErrorPolicyResolver ` → ` WithErrorPolicy ` → connect-string ` on_*_error ` →
105+ ` on_server_error ` → spec defaults.
177106
178107### Connection pooling
179108
@@ -186,10 +115,6 @@ doesn't participate.
186115QWP unit tests use ` httptest.Server ` to stand in for the QuestDB WebSocket
187116endpoint (` newQwpTestServer ` in ` qwp_sender_test.go ` ). ILP unit tests are pure.
188117
189- ` *_integration_test.go ` files need Docker — they spin up real QuestDB via
190- testcontainers-go; HTTP/TCP suites sometimes launch haproxy via
191- ` test/haproxy.cfg ` .
192-
193118Cross-language conformance: ` interop_test.go ` +
194119` test/interop/questdb-client-test ` (submodule) — ILP vectors shared across
195120QuestDB client libraries.
0 commit comments