Conversation
The first setter of a row waited at most one second for the initial WebSocket handshake and the schema DESCRIBE round trip, then threw SCHEMA_UNAVAILABLE and dropped the row. One second is a per-row bound on what is really a connection-setup cost, so a cold TLS handshake, a GC pause, or an I/O thread busy draining a backlog could push an ordinary lookup over the line. Every reconnect clears the schema cache, so a flaky link turned into a stream of lost rows. QwpWebSocketSender now reads the budget from a single field defaulting to DEFAULT_SCHEMA_WAIT_MILLIS (30 s). Both the per-row deadline in bindingForEffectiveWrite() and the refresh in refreshAfterSchemaRejection() use it. A healthy lookup still completes in milliseconds; the constant only bounds a dead or still-connecting link. setSchemaWaitMillisForTesting() lets the one test that asserts the deadline fires against an unreachable port keep its short budget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDP5ZVN8xijRp9LhQoreMf
Parse textual decimals through the existing Decimal256 scratch and reuse the established decimal and text conversion paths. Preserve row rollback, schema refresh, duplicate ordering, and missing-column scale semantics without adding retained converter state. Cover exact wire values, every decimal width, text targets, nulls, invalid rows, inference, and real-server ingestion. Record the design decisions, validation evidence, and remaining branch-level blockers.
The client carried two double-to-text implementations that disagreed on about 0.3% of values. Numbers.append() used the pre-Ryu algorithm and served the legacy ILP path, while QwpSchemaDoubleFormatter wrapped a private copy of the server's Ryu for schema-aware QWP writes. The same double therefore reached the server as two different strings, depending only on which transport carried it. Numbers.append() now formats through Ryu, copied from the server so that client and server produce identical text. RyuDouble moves to io.questdb.client.std as the single copy, and QwpSchemaBinding calls Numbers.append() directly. QwpSchemaRyuDouble and QwpSchemaDoubleFormatter are gone. RyuDouble calls Math.multiplyHigh, which Java 8 lacks, so Compat gains a multiplyHigh shim: the intrinsic on Java 9+, and a hand-rolled equivalent on Java 8. A differential over 20 million random pairs plus edge cases shows the fallback matches the intrinsic exactly. Retiring the old formatter also retires the FdBig bridge and the jdk.internal.math export flags it needed. The multi-release jar stays, because Compat still supplies currentPid() and onSpinWait() to five other callers. JarPackagingIT now guards the Compat variants rather than the FdBig ones, and still executes the formatter out of the packaged jar on both JDKs. This also fixes two Java 8 breaks that predate the change and that a modern JDK hides. QwpSchemaRyuDouble called Math.multiplyHigh from a shared source root, and QwpSchemaFeedbackResponseTest chained putShort() off position(), which returns Buffer rather than ByteBuffer on Java 8. Both would have failed the Java 8 CI job. The legacy ILP path changes its wire text for roughly 0.3% of doubles. Ryu emits the shortest form that still reads back to the same value, so the output is never longer and never loses precision; over 3 million random values both formatters round-trip to identical doubles. One NumbersTest expectation encoded the old extra digit and is updated. Verified on JDK 8 and JDK 25: 3721 unit tests and the packaging ITs pass on both, and the shared floating-to-text corpus matches on all 40 rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDP5ZVN8xijRp9LhQoreMf
Every row-building setter in QwpWebSocketSender repeated the same two catch blocks: roll the partial row back, run the one-refresh rule for a LineSenderSchemaException, and rethrow everything else. The policy lived in 31 copies, and D045 already recorded one setter (CHAR) that shipped without the schema branch. onRowFailure() now owns that policy. It rolls the whole row back first, so a failed refresh cannot leave a half-written row, then hands a schema rejection to refreshAfterSchemaRejection() and lets every other failure propagate unchanged. All 36 row guards call it from a single catch. The two catches in installSchemaBinding() guard buffer cleanup, not rows, and stay as they are. Behaviour change: the four longArray() overloads had only the generic catch, so their UNSUPPORTED_FEATURE rejection skipped the refresh. They now follow the standard path: the first rejection costs one DESCRIBE round trip and reports SCHEMA_CHANGED when the target column changed. atNow() also routes through the helper, but its null column name keeps it on the no-refresh path. QwpSchemaSenderIntegrationTest covers both longArray() outcomes, asserts whole-row rollback and exactly one refresh, and extends the null no-op test with the four longArray() null forms. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The QWP longArray setters cannot become valid after a schema change. Preserve their UNSUPPORTED_FEATURE rejection without issuing DESCRIBE, while retaining the shared row rollback helper. Cover all four overloads, rollback, changed schemas and unavailable metadata. Keep double-array schema recovery and null no-ops unchanged.
A QWP frame has one wire family, so a flush that held both legacy and schema table blocks needed its own delivery pipeline. flushPendingRowsMixed() duplicated the split path's pre-flight sizing, dictionary-chunk fallback, publish loop, baseline advance and commit bookkeeping, and it encoded every block two to three times. Row-boundary adoption was the only source of such batches. A batch now has one wire contract, selected by its first committed row. bindingForEffectiveWrite() keeps returning the legacy contract while a legacy batch is pending, and the first row after the flush adopts schema mode. The upgrade stays one-way and is delayed by at most one batch. This deletes flushPendingRowsMixed(), mixedFramesFit(), encodeSingleTableFrame() and isMixedSchemaBatch(); the encoder is unchanged. Trade-off: rows written after the supporting handshake but inside the pending legacy batch keep legacy conversion, for example microsecond Instant truncation. Which rows preceded the handshake was already a timing race. The change also fixes an existing defect. A legacy row, a flush, a supporting reconnect and then a row on the same table threw IllegalStateException from the column setter: installSchemaBinding() bound in place whenever the buffer had no rows, but a flushed legacy buffer keeps its column layout. It now binds in place only a buffer with no columns and replaces the buffer otherwise. The two upgrade integration tests now assert the batch-boundary rule and cover the flushed-legacy-buffer path. The design document and the journal record the superseded row-boundary rule. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The ASYNC initial-connect javadoc in Sender still described the superseded rule: once schema support is confirmed, all future rows use strict schema lookup and only a row already in progress keeps its contract. The sender now selects one wire contract per batch, so rows added to a pending legacy batch stay legacy, and the first row after that batch flushes adopts schema mode. The javadoc states that rule. Two passages in the design document still named the row-boundary transition; they now name the batch boundary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
QwpSchemaProtocol.decodePayload() built a case-insensitive column-name index to reject duplicate names and then discarded it. Every QwpSchemaBinding rebuilt the same index in its constructor, and hasSameRelevantTarget() resolved names in the refreshed schema with a third mechanism, a linear equalsIgnoreCase scan. The decoder now hands its index to QwpSchemaResponse, which exposes getColumnIndex() and owns hasSameRelevantTarget(). The binding drops its columns field and its constructor loop, and delegates the comparison to the snapshot. The index is never mutated after the decoder publishes it through a final field, so the I/O thread to user thread handoff stays safe. Each cached QwpSchemaResponse now retains its index for its lifetime. Before, only live bindings held one. QwpSchemaProtocolTest gains direct coverage for the case-insensitive lookup and for the target comparison, which only sender-level tests exercised before: absence on one or both sides, a retyped column, the extension-parameter flag, a moved or removed designated timestamp, and a schema-less RESULT_MISSING response. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 21 QwpSchemaBinding*Test classes each carried a private wire Reader plus their own copies of the schema-response builders: 21 readers in 17 textual variants and two naming dialects (u8/i32/i64 vs byteValue/intValue/longValue), 8 variants of known(), 6 of column(), 4 of decode(). The copies were semantically identical, so a change to the schema envelope or the table header meant hand-editing up to 19 slightly different helpers. QwpSchemaTestFixtures now owns the schema-response builders (known, missing, schemaResult, column, parameterizedColumn, frame, decode), binding(), assertReason(), and the encoded-table header checks (tableHeader, skipTablePrefix, skipTableHeader). It writes the schema envelope by hand, independently of any production encoder. QwpTestWireReader is the single bounds-checked reader, with one naming dialect and a bounded varint; it never calls production decoders. The tests keep tableReader(), wireType(), targetType() and every conversion expectation, because those encode per-test intent. Two shared assertions are stricter than some of the copies they replace. assertReason() always asserts that the rejection is not retryable. tableHeader() always asserts the message magic, version, flags, and table count; six classes only skipped the header before. Four of the old readers read i32/i64 without a bounds check, and only one bounded its varint loop. The test count is unchanged at 139 across the binding classes. The change removes about 2,000 lines of test code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The schema-aware sender no longer swaps a table's schema binding under pending rows. Once a table holds rows in the current batch, every later row of that batch validates against the snapshot the first row pinned; bindingForEffectiveWrite() consults the cache only when the table buffer is row-less, which is the first row after a flush or reset. installSchemaBinding() then clears the buffer and rebinds it in place instead of retiring the old buffer alongside a new one. A failed setter reports its local INVALID_VALUE or UNSUPPORTED_FEATURE directly. onRowFailure() rolls the row back and rethrows; it never performs a network lookup, so a rejection costs no round trip and cannot fail for a transport reason. The cache stays current through the paths that already exist: the server attaches the table's current schema to the ACK of a frame that carries a stale identity, the send loop applies that feedback before it advances the ack watermark, INVALIDATE_ALL empties the cache so the next batch looks the table up again, and a reconnect clears the cache. An explicit refresh API was considered and dropped: a caller cannot tell a stale-snapshot rejection from invalid input, so there is no sound rule for when to call it. This removes retiredTableBuffers and its handling on flush collection, post-flush reset, reset(), close() and byte accounting; the schemaResolutionFresh out-parameter threaded through resolveSchema() and QwpSchemaCoordinator.resolve(); refreshAfterSchemaRejection(); QwpSchemaResponse .hasSameRelevantTarget() and its binding delegate; and the SCHEMA_CHANGED reason, which nothing raises any more. The integration test fixtures now acknowledge like the real server: SchemaHandler and LongTextSchemaHandler attach the current schema to the ACK when the snapshot they last delivered is stale, or answer INVALIDATE_ALL when they cannot describe. The generation rebind tests drive adoption through flushAndGetSequence() and awaitAckedFsn() and read the two generations from two single-block frames via a multi-frame FrameReader. New coverage: an INVALIDATE_ALL ACK makes the next batch surface the typed lookup failure from the setter, a version change does not split the pending batch, and reset() keeps the cached snapshot because it ships no frame. The design doc's "Local errors and stale schemas" and "One schema snapshot per table per batch" sections describe the new contract, and the contract-simplification proposal records proposal 1 as implemented without an explicit refresh. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Server PR #7531 (browser negotiation) already claimed header flag bit 0x02 for FLAG_DURABLE_ACK_POLL, a zero-table control frame. Merging server master into the schema-aware branch produced a clean textual merge, but the ingress dispatch then read a poll frame as a schema-flagged frame on a connection that had not negotiated schema and rejected it. The poll bit is already on server master, while FLAG_SCHEMA is unreleased, so FLAG_SCHEMA moves to the free bit 0x40. The server constant changes in lockstep. The design doc and the journal record the new value and the reason. Store-and-forward frames persisted by an earlier build of this branch carry 0x02 and no longer replay as schema frames. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Retain pending schema lookups when the transport disconnects, including requests that have not reached the wire. Resend on the replacement connection with a fresh request ID while preserving the original lookup deadline, so a brief server outage does not abort row construction. Keep explicit schema errors and malformed-response cancellation intact. Add lifecycle coverage for repeated disconnects, stale replies, unsent requests, timeout, interruption and close, and update the design contract. Validate 296 client tests on Java 8 and Java 25, the original failing fuzz seeds, and 18 server integration tests. All pass.
A schema-negotiated sender could not write to an unseen table during an outage: table() always ran a DESCRIBE and failed with SCHEMA_UNAVAILABLE after 30 s, breaking store-and-forward. The only workaround overloaded initial_connect_retry with the row wire contract. schema_mode=auto|strict|off (default auto) now owns that decision. auto uses the strict contract when a schema is pinned, cached, or a lookup answers while the wire is up, and otherwise writes the row legacy without waiting. strict waits out the budget and fails typed. off never looks schemas up. schema_wait_millis replaces the test-only constant. initial_connect_retry is startup-only again; the capability latch is unchanged. A pending schema batch is flushed before a legacy row starts a new one (one wire family per frame). The send loop exposes isWireUp() and peekSchema(), and abandons an in-flight lookup on disconnect under auto.
Separate schema discovery from frame-driven replay requirements so a schema handshake does not prevent a legacy-only stream from failing over to an older server. Keep reconnect requirements local to each stream and negotiate schema support in OFF mode only for recovered schema backlog. Use current peer capability for AUTO lookups and preserve STRICT policy with independent negotiation history. Classify a lookup reaching a legacy replacement as unavailable so AUTO can use its existing fallback. Add failover, recovery, factory isolation and policy regression coverage. Four regressions fail before the fix and pass afterward. All 128 focused tests pass on Java 8 and Java 25. A broader run passes 332 of 333 tests; the remaining intermittent drainer failure also reproduces on the original code. Independent reviews pass.
Use the existing legacy fallback when AUTO schema lookup reports an unsupported or oversized response. This preserves partial-column writes to tables wider than the schema discovery limit. Keep STRICT lookup failures and local conversion errors unchanged. Add regressions for legacy fallback and publishing a pending schema batch before starting a legacy row. Validation: 108 focused tests passed on Java 8 and 86 schema sender integration tests passed on Java 25.
Guard the string and GEOHASH setters against array type metadata sharing the GEOHASH flag. Unsupported scalar writes now produce the existing schema error instead of an NPE or an unintended NULL. Preserve explicit string null writes to array columns. Add regression coverage across all 32 array ranks. Validate 142 schema-binding tests on Java 8 and 61 surrounding tests on Java 25.
CursorSendEngine latched requiresSchema on the first FLAG_SCHEMA frame and never cleared it, and ReconnectSupplier kept a second one-way copy. After one schema batch was acked, a rollback or failover to an older server left the sender retrying forever while its SF backlog filled, until a JVM restart recomputed the flag from unacked frames. The engine now tracks the FSN of the newest schema-framed frame and requiresSchema() compares it with ackedFsn, so the requirement lasts exactly while such a frame is unacked. The producer records the FSN before publishing the frame, which keeps the post-publication check in trySendOne effective. Recovery seeds the same FSN. Reconnect factories take setSchemaRequired(boolean) and every caller refreshes it from the engine before each attempt. Orphan-tail retirement no longer requires a schema-capable connection. Retirement transmits nothing, and retiring schema-framed tail frames now clears the requirement they imposed, so the background drainer reclaims such a slot before opening a socket. Tests cover the requirement following acks, a live downgrade from a schema peer to a legacy peer, local retirement of a schema orphan tail, and a drainer that retires it without connecting. All fail with the old behaviour restored; 2284 QWP client tests pass.
QwpSchemaBinding re-validated and re-hashed the column name on every value: isValidColumnName, a schema hash lookup, and the buffer lookup, plus the designated, wire-type and parameterized checks. All of these depend only on the column, so every row paid for work whose answer never changes while the binding is attached. QwpTableBuffer.ColumnBuffer now records the schema index the binding resolved it to (-1 when the schema lacks the column). Each write first finds the column through the buffer's sequential cursor; a resolved column skips name validation, the schema lookup and the per-column checks, leaving one name compare plus the value conversion, the same lookup cost as the legacy path. A binding only attaches to an empty buffer and unbinding closes its columns, so a recorded resolution can never outlive its schema. Columns without one fall back to the full path. The parameterized-type check now runs before column creation, so a rejected write no longer leaves an empty column behind. The three copies of the inferred-column logic share isInferredWritable and claimInferred. QwpSchemaBindingBenchmark (10 columns, 5 LONG + 5 DOUBLE) measures the schema path at 42 ns/row against 31 ns/row for legacy, down from 141 ns (2-char names) and 282 ns (14-char names). The remaining ~1 ns per value is the target-type dispatch.
QwpSchemaTimestampParser did work whose outcome was already decided. Every ISO timestamp first ran the PG parser to completion of the day and failed there. parse() fetched an exception on the success path. Each fraction attempt copied a Parsed object, the greedy fraction threw to signal a miss, numeric zones allocated a Zone, and named zones were found by a linear scan over ~4400 names with an iterator per value. The first timestamp with any zone, even 'Z', loaded every zone's rules on the producer thread. appendStringTimestamp also UTF-8 encoded each value into a sink before parsing it back through a byte view. The character after the day now selects the parser: only the PG layout has a space there, and a space fails every ISO layout. DATE dispatches the same way between PG, UTC and a raw number. Fields live in locals, the greedy fraction returns a sentinel, numeric zones return an offset, and named zones sit in a hash table inside a holder class that 'Z' and numeric offsets never initialize. TIMESTAMP text now parses the caller's CharSequence directly. The server parses TIMESTAMP text as UTF-8 bytes, where no non-ASCII byte matches anything, so named-zone lookup rejects non-ASCII for TIMESTAMP; DATE keeps its UTF-16 matching, as the server's fallback does. Zone names are upper-cased with Locale.ROOT instead of the default locale. Under a Turkish default locale about 2240 names containing 'i' could never match; results elsewhere are unchanged. The client corpora, the OSS e2e suites against a real server (22 tests, including named-zone gap, overlap, alias and history cases) and 2471 QWP client tests pass; the binding tests also pass on JDK 8.
QwpSchemaTimestampParser carried a micros copy (TIMESTAMP) and a millis copy (DATE) of most of its arithmetic: local time, year and month starts, day of week, next/previous weekday, zone offsets and recurring zone transitions. The copies differed only in their unit and in three behaviours of the server's two parser families. A Target now holds the unit sizes plus those three differences as named flags: micros year starts saturate on negative overflow, TIMESTAMP zone names must be ASCII (the server matches them as UTF-8 bytes), and DATE recurring zone rules reject negative years. Each function pair becomes one function over a Target. Zone stores its cutoff and offsets in seconds and scales them per unit; the scaling reproduces the old per-unit values exactly, including Instant.ofEpochMilli for historic DATE offsets. Month tables hold day counts instead of per-unit values. The PG DATE tail fallback and the UTC DATE layout stay DATE-specific. The file shrinks from 889 to 752 lines. Flipping any one Target flag fails a pinned corpus case (micro_parsed_long_min, min_int_year, negative_year_named_zone_wrap, micro_nonascii_zone_lookalike). The client corpora, the OSS e2e suites against a real server (22 tests) and 2471 QWP client tests pass; the binding tests also pass on JDK 8.
QwpSchemaCoordinator.resolve() unparked the I/O thread after queueing a DESCRIBE. While the wire was down, that unpark cut connectLoop's backoff park short, so a producer retrying a STRICT lookup drove one reconnect attempt per lookup instead of one per backoff interval: 940 lookups in one second produced 940 connect attempts. The unpark bought nothing. The I/O loop polls hasPendingRequest on every pass, parking at most DEFAULT_PARK_NANOS (50 us) between passes, and connectLoop never services the request anyway. Removing it restores the base invariant that only close() unparks the I/O thread. A lookup now waits up to one idle park before it goes out, which is negligible next to its network round trip. testLookupsDuringOutageDoNotShortenReconnectBackoff drops the wire with a 200 ms backoff and resolves in a tight loop for one second; attempts stay within the backoff budget. 2472 QWP client tests pass.
schema_wait_millis, drain(timeoutMillis) and close_flush_timeout_millis each built an absolute deadline as nanoTime() + millis * 1_000_000. A large value such as Long.MAX_VALUE, the usual "no limit" convention, overflowed that sum into the past, so the wait ended at once: STRICT failed the row with SCHEMA_UNAVAILABLE, AUTO silently fell back to the legacy contract, drain() returned false, and close() skipped its flush wait, which loses unacked rows in memory mode. Each wait now records its start and a saturated budget from TimeUnit.MILLISECONDS.toNanos and compares elapsed time against it, which cannot wrap. QwpSchemaCoordinator and awaitInitialSchemaMode drop their hand-rolled saturating multiplies for the same call. The Sender javadoc states that Long.MAX_VALUE waits without a limit; the builder is unchanged. testUnboundedSchemaWaitStillResolves binds the schema in STRICT and AUTO with an unlimited wait, and QwpWebSocketSenderUnboundedWaitTest holds the server ACK to prove drain() and close() keep waiting. All three fail against the previous code. 2514 client tests pass; the new class also passes on JDK 8.
QwpSchemaCoordinator evicted a TOO_LARGE DESCRIBE answer instead of caching it, so AUTO paid one blocking round trip on the producer thread for every batch of a table too wide to describe (more than 2048 columns, or a schema larger than the server's send buffer), and a pending schema-framed batch was flushed early, splitting the frame. TOO_LARGE now caches like MISSING through one isCacheable rule shared by completeResponse and applyFeedback; DENIED and UNAVAILABLE may clear at any time and are still evicted. The coordinator returns the cached TOO_LARGE response and the sender maps it in one place: AUTO writes the legacy contract, STRICT fails with UNSUPPORTED_FEATURE. An AUTO batch for an oversized table costs a map lookup, with no round trip and no exception. The negative entry stays correct because the server reports each legacy-framed table once per schema version: when the table shrinks, the next legacy ACK carries the describable schema and replaces it. STRICT writes no rows to such a table and so earns no feedback; it asks again when it finds a cached TOO_LARGE, as it did before. Coordinator tests cover caching from a response and from feedback, and that UNAVAILABLE is not cached. testTooLargeSchemaIsDescribedOnce sends five AUTO batches with one DESCRIBE (the old code sent ten) and keeps STRICT failing. 2517 client tests pass; also JDK 8.
QWP WebSocket senders resolve the table's schema eagerly in table(), which widened its contract beyond the old "select the table" javadoc. It does not connect: build() always runs ensureConnected(), so the call inside table() is a no-op for any built sender. It can, however, wait up to schema_wait_millis (with an asynchronous initial connect that includes waiting for the first connection), fail with a schema error in STRICT mode, and publish the pending batch when the table's wire contract differs, which can raise flush-timeout and backpressure errors. The javadoc now states that surface, and that a failure leaves the table selected with no row started, so calling table() again retries. No behaviour changes.
The client's DecimalParser required NaN and Infinity to be the whole token, while the server's DecimalParser matches them as prefixes. So "NaN " or "NaNjunk" became a NULL decimal on every server ingest path, including legacy QWP text into a DECIMAL column, but failed the row in schema mode, where the client converts the text itself. isNanOrInfinite is back to the prefix match it had before this branch, so both paths store NULL. The client keeps its long exponent arithmetic and range guards: they only reject inputs such as "1e2147483647" that the server does not parse into a valid value either. DecimalParserServerCompatibilityTest now pins the prefix behaviour and that truncated tokens are still rejected, and the string-to-decimal corpus expects NULL for its NaN and Infinity junk rows. Both fail against the previous parser. 3205 client tests pass.
testUuidConformanceCorpus parsed every row but never checked what the valid rows encoded, so 8 of its 20 rows, including the case-folding row, asserted nothing. It now compares each valid row's limbs with UUID.fromString(expected); a NULL row accepts a null-bitmap entry or the both-Long.MIN_VALUE UUID null sentinel, which the server reads as NULL. Swapping the limbs in the binding now fails it. The timestamp-inputs, timestamp-units and long-to-numeric loaders had no row-count guard, so an emptied corpus or a filter that skipped every row would pass with zero cases. Each now asserts its exact count. QwpSchemaProtocolTest drops its private frame and column copies for the shared QwpSchemaTestFixtures helpers, so the frame version byte comes from QwpConstants.VERSION instead of a hardcoded 1.
The schema cache was a LinkedHashMap capped at 1_000_000 entries with an eviction loop. The map has no access order, so the eviction was FIFO and would have dropped the hottest tables first; in practice it never fired, because the cache is bounded by the tables the sender writes, like its per-table buffers, each far heavier than an entry. It is now a final HashMap without the cap; lookups allocate nothing. With the map always present, the four null checks, the lazy creation and the put/remove helpers go away, and completeResponse evicts once before its failure switch instead of in each branch. Request ids are now assigned only in requestToSend. resolve() used to check id exhaustion and encode the DESCRIBE itself, duplicating the path requestToSend already ran after a reconnect cleared the id. Every send attempt, first or after a reconnect, now takes a fresh id there. An exhausted id space (2^63 lookups) now fails the lookup at send time with the same error. 2517 QWP client tests, the coordinator tests on JDK 8 and the OSS QWP suite (1745 tests) pass.
Brings in the JDK 26 compatibility fix (#99) and the token-store rename retry fix (#97). Upstream #99 rewrote the Java 11 FdBig bridge around method handles because JDK 26 made jdk.internal.math.FDBigInteger package-private. This branch already retired FdBig entirely: Numbers.append() formats through Ryu over the public Compat.multiplyHigh shim, so no JDK-internal class is needed on any JDK. The merge therefore keeps both FdBig variants deleted, keeps JarPackagingIT asserting the Compat variants instead of FdBig, and takes upstream's removal of the now-unused compiler/javadoc arg properties from the pom. DoubleFormatBenchmark from #99 still benchmarks Numbers.append(); its Javadoc now describes the Ryu formatter instead of the FdBig bridge.
The send loop cleared the schema cache on every reconnect, so the next batch for each table paid a blocking DESCRIBE round trip, one table at a time. The server ingests by column name and uses the frame's schema identity only to decide on feedback, and a new connection reports feedback for every stale table on its first ACK. The cache now survives the reconnect. The clear was also the only way out of one stuck state. When a column changes type, a row that the stale snapshot rejects never reaches the server, so no feedback refreshes the snapshot. QwpSchemaBinding now records when the snapshot type of a known column rejects a value: an unsupported conversion, an out-of-range value, excess decimal precision or an array rank mismatch. The sender's row-failure path then evicts that snapshot if the cache still holds the same identity, so the next row looks the table up again. Inputs that no column type can take, invalid names, nulls and malformed text never evict. Tests cover a reconnect that reuses the cache without a DESCRIBE, and the lookup that follows a type rejection.
A store-and-forward slot recovered with only a deferred (uncommitted) tail of schema-framed frames made requiresSchema() true. The foreground sender set that requirement on its reconnect factory before any send loop existed, so build() failed with QwpSchemaCapabilityMismatchException against every server without schema support, even with schema_mode=off. The tail belongs to an aborted transaction and never reaches the wire. ensureConnected() now calls retireRecoveredOrphanTailIfReady() before it sets the schema requirement, matching BackgroundDrainer. The sender starts against a legacy server and sends new rows as legacy frames. QwpSchemaReplayNetworkTest now asserts that outcome instead of the startup failure.
QwpSchemaCoordinator.resolve() lower-cased the table name into a new String before every cache lookup, hit or miss. In AUTO and STRICT modes the sender resolves twice on the first row of each table in each batch, so a schema-aware sender produced two garbage Strings per table per batch on the producer thread, and one pair per row for callers that flush every row. QwpTableBuffer now computes the lower-cased schema key once, reusing the name itself when it is already lower case. The sender passes that key to resolveSchema(), peekSchema() and refreshSchema(), and normalize() returns an already lower-case String unchanged, so a cache hit no longer allocates. DESCRIBE requests now carry the lower-cased name; the server resolves table names case-insensitively, so the answer is unchanged.
Move FLAG_SCHEMA into its alphabetical slot after FLAG_GORILLA in QwpConstants. Reword the Unsafe comment on the override field offset: it named the FdBig double-formatting bridge, which this branch deleted, so it now names the makeAccessible() callers that the offset protects. No behavior change.
Contributor
[PR Coverage check]😍 pass : 2762 / 2971 (92.97%) file detail
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.