Skip to content

[pull] main from Basekick-Labs:main - #148

Merged
pull[bot] merged 3 commits into
Mu-L:mainfrom
Basekick-Labs:main
Apr 28, 2026
Merged

pull[bot] merged 3 commits into
Mu-L:mainfrom
Basekick-Labs:main

Conversation

@pull

@pull pull Bot commented Apr 28, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

xe-nvdk and others added 3 commits April 28, 2026 09:39
…leanup (#413)

* fix(ingest): 26.05.1 critical hardening — 5 criticals + review-pass cleanup

Five critical bugs surfaced by a 4-agent post-implementation review of
the ingest path; this commit fixes all five PLUS the consolidated
findings from a second 4-agent review of the fix branch.

CRITICAL — bugs that block 26.05.1 GA:

- C1: ArrowBuffer.Close() raced concurrent Write() goroutines via
  close(flushQueue) after cancel(). A writer past shard.mu.Unlock()
  but not yet at the channel send would panic with "send on closed
  channel" during graceful shutdown. Fixed: closing atomic.Bool flag
  set BEFORE cancel(); flushQueue is no longer closed (workers exit
  via b.ctx.Done()); senders short-circuit on the flag with a
  ctx.Done() defense-in-depth select arm. Regression test:
  TestArrowBuffer_CloseRaceWithConcurrentWrites uses wait-for-evidence
  on totalRecordsBuffered (not a fixed Sleep) for stability on slow CI.

- C2: Schema-evolution flush released shard.mu for I/O; a concurrent
  writer could install a third schema in the I/O window and the
  original caller would append records into the racer's schema buffer,
  mixing schemas and producing column mismatches at flush. Fixed:
  bounded loop (8 iterations) re-checks bufferSchemas after the I/O
  returns under re-acquired lock + ctx.Err() check on every iteration
  so a cancelled request isn't starved. Extracted into
  flushOnSchemaChangeLocked helper (was duplicated across both write
  paths). Regression test:
  TestArrowBuffer_SchemaEvolutionConcurrentNoCorruption.

- C3: WAL.AppendRaw / AppendRawWithMeta silently incremented
  DroppedEntries and returned nil on channel-full backpressure,
  making downstream "data preserved in WAL" log lines untrue.
  Operators couldn't distinguish backpressure drops from real I/O
  failures. Fixed: wal.ErrWALDropped sentinel returned on drop. Both
  drop sites consolidated into Writer.tryEnqueue helper.

  Caller-side (ingest): differentiates ErrWALDropped from I/O
  failure via errors.Is. Drops increment a NEW totalWALDropped
  counter (separate from totalWALErrors) and emit a SAMPLED Warn
  (max 1 line/sec via walDropLastLogNano) — the previous code path
  emitted an unsampled Error per record, which was log-spam at ~250
  bytes/event under sustained backpressure.

  Cluster receivers (replication/receiver + sharding/shard_receiver):
  ErrWALDropped is now NON-FATAL — receiver continues to apply the
  entry to the in-memory ingest buffer, increments
  totalLocalWALDropped, emits a warn. The previous behavior would
  have advanced lastSeq past the dropped entry, causing silent
  primary/follower divergence. Durability is preserved by the
  primary's WAL + Phase 2 peer Parquet replication.

- C4: Fiber's c.Body() transparently gunzipped Content-Encoding:
  gzip with NO size cap — a 1 MB gzip payload that decompressed to
  50 GB would OOM the process before any handler-level check fired.
  Fixed in lineprotocol and tle handlers (msgpack was already
  correct): use c.Request().Body() (raw bytes) and our pooled
  decompressGzip helper that enforces a hard size cap before
  allocation. Mirrors the msgpack pattern.

- C5: Ingest endpoints (msgpack, lineprotocol×3, tle, import×4)
  lacked auth.RequireWrite middleware — a read-only token could
  write data via /api/v1/write/* when RBAC was disabled (OSS
  default). Same vuln class CLAUDE.md flagged on CQ/delete. Fixed:
  SetAuthAndRBAC on the four ingest handlers now takes the concrete
  *auth.AuthManager directly (no silent type-assert miss); routes
  use the new withWriteAuth / withAdminAuth helpers in
  internal/api/auth_middleware.go that fall back to a no-op
  middleware when auth is disabled. Bulk import endpoints use
  RequireAdmin (rewriting history). LP /flush also uses RequireAdmin
  (global flush is operationally heavy + spammable).

Refactors / cleanups (review-pass findings):

- Extract Writer.tryEnqueue (eliminates 2 copies of the WAL drop pattern).
- Extract ArrowBuffer.tryEnqueueFlush (eliminates 4 copies of the
  closing-flag + select-with-ctx-arm + queue-full pattern). Returns a
  flushSendOutcome enum so the caller knows queued / closing /
  ctx-canceled / queue-full distinctly.
- Extract ArrowBuffer.flushOnSchemaChangeLocked (eliminates 2 copies
  of the schema-evolution loop).
- Extract withWriteAuth / withAdminAuth helpers + passthroughMiddleware
  in internal/api/auth_middleware.go. Collapses the if/else
  registration pattern in 4 ingest handlers.
- New regression tests with deterministic semantics: WAL drop test
  halts the drainer + manually fills the channel (no Skip on no-drops).
  Schema-evolution test uses iteration count + per-goroutine recover.
  Close-race test waits for evidence of writer activity.

Performance: 60s sustained MessagePack columnar benchmark on this
branch produced 19.04M rec/s @ p99 3.13ms vs baseline 18.6M rec/s @
p99 3.68ms — ~17% p99 improvement, ~2% throughput uplift, 0 errors.
The hot record path is unchanged; all new logic is per-flush or
per-shutdown.

Release notes: RELEASE_NOTES_2026.05.1.md updated with a new
"Ingestion Critical-Path Hardening (26.05.1 Pre-GA)" subsection
under Hardening, covering the five critical fixes and the
performance result.

* docs(readme): bump perf claims to 19M+ rec/s and 3.13ms p99

Reflects the 60s sustained MessagePack columnar benchmark on the
fix/ingest-26.05.1-criticals branch:

  19.04M rec/s @ p99 3.13ms over 1.14B records, 0 errors.

Previous claim was 18.6M rec/s @ p99 3.68ms (still true on baseline).
The other rows in the table (Zstd / GZIP / Line Protocol) were not
re-measured on this branch and are left as-is — only the path the
benchmark actually exercised gets updated.

Marketing site (basekick.net) and docs site (docs.basekick.net) carry
the same claims and will be updated separately.

* fix(ingest): gemini round 1 — restore decompress error detail + zstd on LP/TLE

Addresses all four gemini-code-assist findings on PR #413:

- LP and TLE gzip-decompress responses now include the underlying
  err.Error() so clients can diagnose oversized payloads without
  checking server logs (mirrors msgpack and import handlers).
- Line Protocol and TLE endpoints now accept Content-Encoding: zstd
  via magic-byte detection (0x28 0xB5 0x2F 0xFD), decompressed through
  the package-level zstdDecoderPool shared with msgpack. Same 100MB
  decompressed-size cap as the gzip path. Zstd is 3-5x faster than
  gzip on typical LP and TLE payloads (matters most for satellite-fleet
  TLE senders pushing large catalog refreshes per tick).
- Release notes: expanded the shell-glob endpoint list to explicit
  paths (gemini's prose-clarity suggestion) and added a new
  "Line Protocol and TLE: zstd Decompression Support" subsection.

New helper: decompressZstdPooled in tle.go (returns []byte; LP/TLE
keep their flat-byte handler shape rather than adopting msgpack's
PooledBuffer.Release contract).

* fix(ingest): gemini round 2 — defensive body copy + schema-churn 503

Addresses both gemini-code-assist findings on PR #413 commit 713bbd3:

CRITICAL (security-critical): Use-after-free risk in LP and TLE
handlers. c.Request().Body() returns a fasthttp-owned slice that is
reused after the handler returns. The current parser path produces
fresh strings (no aliasing today), but the contract was undocumented
and brittle. Both handlers now make a one-shot defensive copy at the
localProcessing entry point — append([]byte(nil), c.Request().Body()...).
Cost: one memcpy per request, dwarfed by per-record allocations. This
closes the silent-aliasing footgun if anyone ever adds a fast-path
that retains a sub-slice of the body.

MEDIUM: Schema-evolution iteration cap previously returned nil and
proceeded with a wide schema-mixed buffer. Now returns a typed
ingest.ErrSchemaChurnExceeded sentinel; LP and TLE handlers map it
to HTTP 503 (retryable backpressure). New totalSchemaChurnExceeded
atomic counter surfaced in ArrowBuffer.Stats() so operators can
alert on a non-zero rate. The per-iteration flushes inside the loop
already wrote the older schemas' rows to durable Parquet, so there
is no data loss — only this single request fails under sustained
schema-rotation churn.

New regression tests:
- TestArrowBuffer_SchemaChurnExceededErrorContract: pins the error
  type (errors.Is(err, ErrSchemaChurnExceeded)) and counter/error
  parity under a rotating-schema workload.
- TestArrowBuffer_ErrSchemaChurnExceeded_IsExported: sentinel must
  be exported and unwrappable via errors.Is.

Build clean, full ingest + api test suite passes.

* fix(ingest): gemini round 3 — defer body copy + msgpack 503 + docstring

Addresses all four NEW gemini-code-assist findings on PR #413 commit
3c79ea1. Two earlier findings (zstd consistency + cap-error return)
were stale duplicates already addressed in 713bbd3 / 3c79ea1.

MEDIUM: Defensive body copy in LP and TLE was performed unconditionally
before compression detection, doubling memory for large compressed
payloads (decompression returns a fresh slice, making the prior copy
redundant). Both handlers now use a switch over compression magic
bytes — gzip/zstd return fresh slices directly, only the uncompressed
default branch makes the defensive copy.

MEDIUM: writeMsgPack handler did not specifically handle
ingest.ErrSchemaChurnExceeded — it returned a generic 500 instead of
the retryable 503 used by the LP and TLE handlers. The error message
also dropped the underlying err.Error() detail. Both fixed: msgpack
now mirrors LP/TLE — errors.Is(err, ingest.ErrSchemaChurnExceeded)
maps to 503, and the generic-error response includes err.Error() for
client-side diagnosis (consistent with the gzip-decompress responses).

MEDIUM: flushOnSchemaChangeLocked docstring still listed
"schemaEvolutionMaxIters reached — return nil (best-effort)" but the
implementation in 3c79ea1 changed it to return ErrSchemaChurnExceeded.
Comment updated to match: documents the 503 retryable contract and
that per-iteration flushes already wrote older schemas to durable
Parquet so there is no data loss.

Build clean, full ingest + api test suite passes.

* fix(ingest): gemini round 4 — zstd bomb hardening + decompress helper

Addresses three NEW gemini-code-assist findings on PR #413 commit
0822793. Two earlier-round comments on arrow_writer.go:1440 and
msgpack.go:168 were stale duplicates already addressed in 3c79ea1
and 0822793 respectively (gemini's grep landed on shifted line
numbers; the relevant code now shows the fixed shape).

HIGH/security-high — zstd decompression bomb:
The msgpack zstd path (and the new LP/TLE zstd path added in 713bbd3)
used zstd.Decoder.DecodeAll, which grows the output buffer to fit
the entire decompressed stream regardless of WithDecoderMaxMemory
(that option only bounds the decoder's per-frame window, not output
buffer growth). A high-ratio bomb (e.g. 28KB compressed → 256MB
decompressed, ratio 9361x) would have OOM'd the process before any
post-hoc length check could fire — symmetric to the gzip-bomb fix
earlier in 26.05.1. All three handlers now use streaming
decoder.Reset + io.LimitReader + io.ReadAll with the same hard
100MB output cap as gzip; the bound is enforced *during* decoding.
New regression tests in internal/api/decompress_bomb_test.go
construct a 256MB bomb at a 100MB cap and assert clean rejection
without OOM.

MEDIUM — duplicated dispatch logic:
Extracted shared decompressRequest(rawBody, maxSize) ([]byte, codec, error)
helper in tle.go that handles gzip/zstd/uncompressed dispatch with
the hard output cap and the defensive copy of fasthttp-owned bytes
on the uncompressed branch. LP and TLE handlers now delegate to it
in three lines instead of a 30-line inline switch each.

MEDIUM — duplicated gzip helper:
LineProtocolHandler.decompressGzip method was a near-clone of the
package-level decompressGzipPooled (in tle.go), both pulling from
the same lpGzipReaderPool. The method is now removed; all callers
go through decompressRequest → decompressGzipPooled. Removed unused
bytes/io/gzip imports from lineprotocol.go.

Build clean, full ingest + api test suite passes (incl. 3 new
bomb-regression tests).

* fix(ingest): gemini round 5 — log sampling on follower receivers + formatBytes

Addresses 5 NEW gemini-code-assist findings on PR #413 commit 1aca1e8.
Two earlier-round comments at arrow_writer.go:1440 and msgpack.go:168
remain stale duplicates (already addressed in 3c79ea1 and 0822793 —
gemini's grep keeps landing on shifted lines).

MEDIUM (x4) — sampled Warn for follower-side WAL backpressure:
The replication.Receiver and sharding.ShardReceiver fix from 26.05.1
(cluster-receivers tolerate ErrWALDropped) emitted Warn unsampled.
Under sustained follower-side backpressure that produces a Warn per
replicated record, drowning operator dashboards. Both receivers now
mirror the ArrowBuffer.recordWALError pattern: walDropLastLogNano
atomic timestamp, walDropLogIntervalNano = 1s const, log line
suppressed if (now - last) < interval. Operators continue to read
the rate from totalLocalWALDropped; the Warn is just the degraded-
state signal.

MEDIUM — formatBytes consistency:
decompressZstdPooled and decompressGzipPooled previously formatted
cap errors as "exceeds %dMB limit". Switched to formatBytes(int64(
maxSize)) for consistency with msgpack's decompressZstd and
decompressGzip.

Build clean, full api + replication + sharding suites pass.

* fix(ingest): gemini round 6 — uncompressed cap + msgpack via shared helper

Addresses 2 NEW HIGH findings on PR #413 commit 4a05cfb. Six earlier-
round comments remain stale duplicates (already fixed in 3c79ea1,
0822793, and 4a05cfb — gemini's grep keeps re-flagging on shifted
lines).

HIGH/security-high — uncompressed maxSize cap missing:
decompressRequest enforced the 100MB cap on gzip and zstd via the
streaming LimitReader but had no cap on the uncompressed branch.
A multi-GB raw body would have been both accepted AND defensively
copied, doubling the allocation in the uncompressed-OOM vector.
The helper now defaults maxSize<=0 to 100MB up front and applies
the same ceiling to all three branches; the uncompressed path
rejects over-cap input before the copy. New regression tests:
TestDecompressRequest_UncompressedRejectsOverCap (over-cap rejected),
TestDecompressRequest_UncompressedAcceptsAtCap (at-boundary accepted
+ defensive-copy no-aliasing invariant via mutate-and-check).

HIGH — msgpack missing defensive body copy:
The msgpack handler was the only ingest path still operating on
c.Request().Body() directly without the round-2 defensive copy. The
fork's msgpack decoder does string(b) copies on string fields and
io.ReadFull copies on []byte fields, so today's behavior is
defensible — but documenting that "no future fork update may
introduce zero-copy aliasing" is brittle, and the inconsistency
with LP/TLE was a real maintenance hazard. msgpack now routes
through the shared decompressRequest helper, which also gives it
the new uncompressed-size cap above for free.

Cleanup:
Deleted MsgPackHandler.decompressGzip and decompressZstd (240 lines
of per-handler pooled-buffer codecs replaced by the package-level
streaming helpers in tle.go). PooledBuffer remains in the package
for TestPooledBufferRelease. Removed unused bytes/io/gzip/zstd
imports from msgpack.go and updated handler error-message shape
across LP/TLE so codec-empty rejections produce a clean
"Failed to read request body: ..." (vs the awkward "Failed to
decompress  data: ...").

Build clean, full api + replication + sharding suites pass; 5
regression tests cover bomb / over-cap / boundary / aliasing.

* fix(ingest): gemini round 7 + perf restoration on msgpack hot path

Three changes in this commit:

1) MEDIUM (gemini round 7) — drop poisoned zstd decoder on partial-
decode error. decompressZstdPooled previously called Reset(nil) and
returned the decoder to the pool after an io.ReadAll error, on the
assumption that Reset would clean any residual frame state. After a
mid-stream corruption error that's not a safe assumption — Close()
the decoder entirely; the next request's pool.Get() will allocate a
fresh one.

2) Plan B — msgpack uncompressed fast-path bypasses the defensive
body copy. After round-6 unified all three handlers behind
decompressRequest (which copies on the uncompressed branch), the
sustained MessagePack-columnar bench dropped from 19.04M rec/s to
18.04M rec/s — a 5% regression on the throughput-critical path
where 18M rec/s × ~50KB batches translates to a real allocation
storm. msgpack now inlines the dispatch: gzip/zstd still go through
the shared decompressGzipPooled / decompressZstdPooled (those need
the bomb cap and produce fresh slices anyway), but the uncompressed
branch hands rawBody directly to h.decoder.Decode. The decoder is
synchronous and the Basekick-Labs/msgpack/v6 fork copies every
string via `string(b)` and every []byte via io.ReadFull, so by the
time Decode returns no decoded value still aliases rawBody — fasthttp
can safely reuse the buffer. LP and TLE keep the defensive copy
because their throughput targets don't justify diverging from the
shared helper.

A new regression test (TestMessagePackDecoder_CopiesStringFields_
NoBodyAliasing) decodes a row-format payload, mutates the source
bytes to 0xFF, and asserts the decoded Measurement and Tags values
are unchanged. Locks in the no-aliasing invariant — if a future
fork update introduces zero-copy aliasing the test fails loudly
before production sees corruption.

Bench result with this commit: 19.13M rec/s @ p99 3.06ms over 60s,
slightly above the post-round-1 baseline of 19.04M / 3.13ms.

3) Bumped Basekick-Labs/msgpack/v6 from v6.0.0 to v6.1.0 (user-
released; pre-existing fork upgrade unrelated to gemini findings).
The no-aliasing regression test passes against v6.1.0.

The 7 other comments in gemini round 7 are recurring re-flag noise
on code shapes already fixed in 3c79ea1, 0822793, 4a05cfb, and 6561c86.

Build clean, all suites pass, no-aliasing invariant locked in test.
…414)

* fix(query): six critical-path hardening fixes from query-path review

A 4-agent staff/principal-engineer review of the query execution path
(mirror of the ingest review that produced PR #413) surfaced six
issues we want addressed before 26.05.1 GA. All are fixed here. Bench
delta is within the 5% no-regression budget on a 99.9M-row ClickBench
GROUP BY workload (JSON p95 75.86ms→78.58ms, Arrow p95 76.14ms→78.36ms).

C1 — Expanded SQL denylist + comment-strip + literal-mask normalisation.
The query API guardrail previously gated only DDL/DML keywords. It
now also gates ATTACH/DETACH/COPY/EXPORT/IMPORT DATABASE/PRAGMA/
SET <var>=/LOAD/INSTALL/CALL — session/extension/file-system ops that
don't belong in a read-only query path. ValidateSQLRequest now strips
comments and masks string literals before the regex check, so
`DROP /* */ TABLE x` cannot interleave comments past token boundaries
and `SELECT 'DROP TABLE x'` is not falsely rejected. Removed the dead
(h *QueryHandler).validateSQL method (code-quality finding from the
same review). Test matrix covers every blocked keyword + comment-
injection + quoted-identifier false-positive cases.

C2 — x-arc-database header validation + universal read_parquet path
quoting. The header value lands inside read_parquet('<base>/<db>/...')
storage paths Arc generates internally; in some edge cases a header
containing a single quote or shell-active char could break out of the
literal. New validateHeaderDatabase helper validates at every entry
point (executeQuery, executeQueryArrow, estimateQuery). New quotePath()
helper routes every read_parquet('PATH', ...) interpolation site
through sqlutil.EscapeStringLiteral (added to internal/sql/mask.go as
a single source of truth). Eight read_parquet sites in query.go
converted. Tests cover SQLi vectors (quote, NUL, newline, comma,
backslash, path-traversal) and boundary conditions.

C3 — Reject direct read_parquet() in user SQL + extend CTE name regex.
Arc's transformation layer is the only legitimate source of
read_parquet — in some edge cases, a user query containing
read_parquet directly produced zero extracted table references and
bypassed the (database, measurement) RBAC pair-check. ValidateSQLRequest
now rejects user SQL containing read_parquet(. Companion fix: the
CTE-name extraction regex was extended to recognise the parenthesized
column-list form (WITH foo(c1, c2) AS (...)), so a CTE name doesn't
leak into the table-reference list. Tests cover read_parquet in JOIN,
subqueries, CTEs, plus the new CTE column-list parse.

C4 — Parallel-partition partial-failure now fails the whole request.
When the parallel executor fans out a query across N partitions and
one or more partition queries error, NewMergedRowIterator previously
returned the surviving partitions' rows as a 200/success — a fraction
of the result silently dropped. The handler now inspects per-partition
Error after ExecutePartitioned and fails the request with HTTP 500 on
any partition error. Companion fix in parallel_executor.go: the
goroutine fan-out semaphore is acquired in the launch loop instead of
inside each spawned goroutine, so a 10K-path query bounds in-flight
goroutines at MaxConcurrentPartitions (default 4) instead of spawning
10K goroutines parked on the semaphore.

C5 — Streaming-response error semantics. streamTypedJSON and
streamArrowJSON previously returned only the row count; in some edge
cases (Scan failures mid-stream, deferred *sql.Rows.Err(), context
cancellation after the response envelope was already flushed) the
loop silently `continue`'d and the caller marked the query
Complete(rowCount). Both functions now return (int, error), perform a
ctx.Err() check at every row/batch boundary, and check the iterator's
deferred error after the loop. Callers route any error to
IncQueryErrors, registry Fail() (or TimedOut() on
context.DeadlineExceeded), and an Error log line. The HTTP status
cannot be changed retroactively (headers already flushed) but
operator-side observability is now correct. The Arrow IPC stream loop
in executeQueryArrow adopts the same per-batch ctx-check pattern.

C6 — Arrow IPC streaming memory pinned by deferred Release. The
executeQueryArrow IPC loop used `defer batch.Release()` inside the
for-reader.Next() body. Defers accumulate on the closure stack until
the closure exits — for a 10M-row result with 10K-row batches,
1,000 deferred Release calls held all casted Arrow records alive
until the entire stream completed. Releases each casted batch
explicitly after ipcWriter.Write, restoring constant per-batch
memory. The reader-owned input batch needs no Release; reader.Next()
releases the prior record automatically.

Build clean; full ./internal/api/... ./internal/query/...
./internal/queryregistry/... ./internal/database/... test suites
pass. Smoke test against running Arc verified all 5 attack vectors
are rejected (header SQLi, direct read_parquet, ATTACH RCE, PRAGMA
hardening) and legitimate queries still work.

* fix(query): gemini round 1 — tighten SET/CALL regex + idiomatic ctx select

Addresses 5 NEW gemini findings on PR #414 commit 244849b. One MEDIUM
(G4 — simplify error-collection loop) was a no-op suggestion identical
to the existing code; skipped.

CRITICAL/security-critical (gemini G1) — SET regex bypassable:
The previous \bSET\s+(?:GLOBAL\s+|...)?\w+\s*= form required an
equals sign and a word after the optional scope keyword. DuckDB
also accepts:
  - SET enable_external_access TO true (TO instead of =)
  - SET VARIABLE x = 1            (VARIABLE keyword)
  - RESET enable_external_access  (mutates session state)
All three slipped past the regex. Replaced the multi-form pattern
with bare \bSET\b and added \bRESET\b. The query API is read-only
so any session-state mutation is forbidden — bare-keyword match is
correct here. New regression tests for all three bypass shapes.

CRITICAL/security-critical (gemini G2) — CALL regex bypassable:
\bCALL\s+\w+ required whitespace before the procedure name, but
DuckDB accepts CALL(proc_name) with no space. Replaced with bare
\bCALL\b. Regression test for the no-space form.

MEDIUM (gemini G3) — queryMeasurement streamCtx:
Switched from context.Background() to c.UserContext() so client
disconnects propagate to per-row cancellation. Fasthttp keeps
c.Context() alive across the SetBodyStreamWriter boundary so it's
safe inside the async stream callback (consistent with the
executeQuery and executeQueryArrow patterns).

MEDIUM (gemini G5+G6) — idiomatic select for ctx cancellation:
Replaced 'if ctx.Err() != nil' with 'select { case <-ctx.Done() }'
+ default at three call sites — streamTypedJSON, streamArrowJSON,
and the Arrow IPC stream loop in executeQueryArrow. Required
labeled-break (break scanLoop / batchLoop / streamLoop) because
plain break inside select-in-for would only break the select.
Same semantics, more idiomatic Go.

Bench: two runs on warm state show JSON avg 72.64-73.85ms, p95
74.34-81.31ms; Arrow avg 72.30-73.66ms, p95 74.30-75.50ms — well
within run-to-run jitter of baseline (73ms avg / 76ms p95). The
"+3.6% JSON p95" delta from the initial commit appears to have
been first-restart cold-cache noise, not a real regression.

Build clean (default + -tags=duckdb_arrow); full test sweep across
internal/api/... internal/query/... internal/queryregistry/...
internal/database/... passes.

* docs(release): SET/CALL denylist tightening from gemini round 1

The C1 denylist bullet listed SET <var>=/CALL (the original narrower
regex form). After gemini round 1 found two bypass shapes (SET TO,
SET VARIABLE, RESET, CALL with no whitespace before paren), the
regex simplified to bare-keyword anchors plus added RESET. Update
the release notes to match.
Re-ran sustained_bench across all four ingestion paths after the
26.05.1 hardening landed. Numbers are 30-second runs, 12 workers,
0% errors:

| Protocol               | Old              | New              |
|------------------------|------------------|------------------|
| MessagePack Columnar   | 19.0M / p99 3.13ms | 19.9M / p99 2.95ms |
| MessagePack + Zstd     | 16.8M / p99 3.23ms | 16.5M / p99 2.70ms |
| MessagePack + GZIP     | 15.4M / p99 3.17ms | 16.5M / p99 2.71ms |
| Line Protocol          | 3.7M  / p99 10.63ms | 4.1M  / p99 8.85ms |

Compressed paths now sit within 13% of uncompressed columnar.
GZIP closed the gap to Zstd entirely (the C4 streaming-LimitReader
fix in #413 landed on the gzip path too). Line Protocol improved
~11% throughput / 17% better p99 — likely from the C5 stream-error
work + the C4 ingest hardening.

Bench harness fix: benchmarks/sustained_bench/main.go was
hardcoding /api/v1/write for the lineprotocol path. That endpoint
doesn't exist (404) — the actual route is /api/v1/write/line-protocol.
Fixed both the URL and the banner-printed endpoint label.
@pull pull Bot locked and limited conversation to collaborators Apr 28, 2026
@pull pull Bot added the ⤵️ pull label Apr 28, 2026
@pull
pull Bot merged commit 704f996 into Mu-L:main Apr 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant