Skip to content

[pull] master from cube-js:master - #707

Merged
pull[bot] merged 2 commits into
code:masterfrom
cube-js:master
Sep 2, 2026
Merged

[pull] master from cube-js:master#707
pull[bot] merged 2 commits into
code:masterfrom
cube-js:master

Conversation

@pull

@pull pull Bot commented Sep 2, 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 : )

paveltiunov and others added 2 commits September 1, 2026 18:35
* fix(cubesql): stop reporting `Continue wait` as a SQL API error

A `Continue wait` coming back from a parallelized plan was logged as a
`Cube SQL Error` load event, so Query History showed the queue's "not finished
yet" signal as a failed request.

#10649 suppressed that event, but only for an error whose message is exactly
`Continue wait`. DataFusion's `RepartitionExec` has to hand one error to every
output partition and a boxed error is not `Clone`, so `wait_for_task` flattens
ours to its `Display` string and re-wraps it as `DataFusionError::Execution`:
the typed `CubeError` is gone and the message reads `Execution error: Continue
wait`, which an equality check does not match. `target_partitions` defaults to
`num_cpus::get()` with `repartition_aggregations` / `repartition_windows` on, so
any GROUP BY or window function over a `CubeScan` takes that path.

- add `CubeError::is_continue_wait()`, which matches the cause first and falls
  back to a substring test on the message, and use it at the call sites that each
  spelled the check differently - exact equality in `handle_sql_query` and
  `load_data`, substring in `NodeBridgeTransport`
- restore the `ContinueWait` cause and the canonical message in the DataFusion
  and Arrow conversions, so the cause survives the flattening and the prefixes
  cannot compound through a further wrapping layer
- also try `DataFusionError` when downcasting `DataFusionError::External`, which
  `From<ArrowError>` already did
- classify the streaming path's errors by cause, as `load_data` does, rather than
  prefixing the message

* fix(cubesql): address review on `Continue wait` detection

Follow-up on the review of #11717. The message check is a tail match, not a
substring test - the earlier comment and commit message said "substring", which
is the looser semantics this deliberately avoids.

- `normalize_continue_wait` canonicalizes the message whenever the error is a
  continue wait, not only when the cause was missing. An error can arrive with
  the cause already set and a prefixed message - `CubeScanMemoryStream` sets the
  cause without rewriting the message, and the `CubeError` downcast arm returns
  it unchanged - and JS compares the message exactly in places
  (`gateway.ts`'s `err.message === 'Continue wait'`). Fixing it centrally covers
  the `scan.rs` case too, so that call site stays as it is.
- Match the tail of the *first line*, so a message that arrived over the JS
  bridge with a stack appended still resolves. `errorString` reads `err.error`
  and `err.message` before falling back to `err.stack`, so a continue wait does
  not reach that fallback today; this keeps the check robust if it ever does.
- Compare as ASCII bytes instead of lowercasing both sides. The needle is pure
  ASCII and these messages can quote the whole failing query, so `to_lowercase`
  allocated a copy of it - plus one of the constant - on every call.

Three more tests: a typed continue wait carrying a prefixed message, a message
with a stack appended, and the awkward inputs the byte-tail compare has to
survive (shorter than the needle, and a split landing inside a multi-byte
character).

* fix(cubesql): require `Continue wait` to start a word

Follow-up nit from the review of #11717. The tail match had no word boundary, so
a first line ending in a longer word - `discontinue wait` - was read as the
queue's signal and would have been swallowed.

The phrase now has to start a word. Every real prefix ends in a separator
(`Execution error: `), and an unprefixed message has no preceding character at
all, so both keep matching.

* fix(cubesql): match `Continue wait` as a lowercased substring

Reverts the narrowing this branch picked up over the last two commits - the tail
match, the word boundary, and the ASCII byte compare - back to the check
`NodeBridgeTransport` has used since 2024:

    message.to_lowercase().contains("continue wait")

The message has to survive arbitrary wrapping on both sides: a prefix from
whichever layer re-wrapped the error (`Execution error: `, `Database Execution
Error: `, and these compound), and a suffix when it arrived over the JS bridge
with a stack appended. Anything narrower has to enumerate those shapes, and the
shape is what keeps changing - twice now a continue wait has reached query
history as a failed request because a check was too specific about it.

The cost, written into the doc comment rather than left implicit: a real failure
quoting the phrase - an error naming a column `continue wait` - is read as the
queue's signal and goes unreported. No such message is known, and the opposite
mistake is the one with a track record.

Two tests asserted the narrow behaviour and are updated: the quoting cases drop
out of `a_real_failure_is_left_alone`, and the boundary test becomes
`a_message_without_the_phrase_is_not_a_continue_wait`, keeping the empty, short
and non-ASCII inputs that still must not match.

* fix(cubesql): do not read a quoted `continue wait` as the queue's signal

Review follow-up. The lowercased substring check feeds `normalize_continue_wait`,
which runs on every DataFusion and Arrow conversion and *replaces* the message -
so a false positive does not merely go unreported, it destroys the original error
text and the caller sees a query that appears to poll forever instead of the
error naming their mistake.

These messages interpolate user-controlled SQL, so the realistic false positive
is `No field named 'continue wait'` - and it is always quoted. Skip an occurrence
immediately preceded by `'`, `"` or a backtick. That is one character of context
rather than an enumeration of wrapper shapes: arbitrary prefixes and appended
stacks still match, and the exception is per occurrence, so a message that quotes
the phrase and also carries it unquoted is still a continue wait.

The doc comment previously priced a false positive as "would go unreported",
which was the cost at the original `NodeBridgeTransport` site, not here. Fixed.

* fix(cubesql): record the Postgres cost of a misread `Continue wait`

Review follow-up, doc and a constant - no behaviour change.

The cost paragraph named `normalize_continue_wait` replacing the message but
missed the other consumer the widened check newly reaches. `load_data` held an
equality check until this branch: a real database error misread there is minted
with the `ContinueWait` cause locally, so nothing re-classifies it on the way up
- unlike a genuine continue wait, which the transport retries and never surfaces
on the Postgres path - and `sql/postgres/error.rs` answers the client
`SqlStatementNotYetComplete` (`03000`) instead of their failure.

That also corrects the reachability note in the PR description, which argued the
`ContinueWait` arm there was unreachable. The argument held for a genuine
continue wait and does not cover an error misread into that cause.

Also lifts the lowercased needle into a constant, so the substring test no longer
allocates a copy of `CONTINUE_WAIT_MESSAGE` on every call, with a test keeping
the two spellings in step.

* fix(cubesql): match `Continue wait` by message structure, not substring

Replaces the lowercased `contains` and its quote guard with a check that
follows the structure these messages actually have. Every wrapping layer
prepends its own label and a colon (`Execution error: `, `Database Execution
Error: `, and these compound), and a message that came over the JS bridge can
carry an appended stack, which puts the message on the first line and the
frames after it. So the phrase always lands as a whole `:`- or
newline-delimited part, however many layers wrapped it:

    message
        .split(['\n', ':'])
        .any(|part| part.trim().eq_ignore_ascii_case(CONTINUE_WAIT_MESSAGE))

That matches every wrapped shape without enumerating them - which is what the
substring test bought - while a real failure that only mentions the phrase
inside a larger part keeps its message. The quote guard covered just the shapes
where the phrase is quoted, so `Execution error: continue wait is not a column`
was still read as the queue's signal, and `normalize_continue_wait` replaces
the message, meaning that text was gone before anything downstream saw it.

The trade now runs the other way: a layer that appends rather than prepends
(`Continue wait.`) would stop matching. None does today - the phrase is a wire
constant that gets wrapped, not edited - and the doc comment says so.

Splitting also drops both allocations the old check made per call:
`to_lowercase` on a message that can quote the whole failing query, plus one
for the constant. `CONTINUE_WAIT_MESSAGE_LOWER` and the test keeping it in step
go with them.

Tests: `the_phrase_is_matched_as_a_whole_part` pins the wrapped and
stack-appended shapes on the predicate directly, and `a_real_failure_is_left_alone`
gains the unquoted mid-part mention the quote guard let through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFsWyd58aFEWhLT2CjuiiV

* fix(cubesql): name the one wrapper that appends, and pin it with a test

Review follow-up on the structural match. The doc comment justified the trade
with "No layer produces those today - the phrase is a wire constant that gets
wrapped, not edited", and that is not true as written: the `Rewrite` arm of this
file's own `Display` renders

    Rewrite Error: {}. Please check logs for additional information

where every other arm is `<Label>: {}`. Flattened through `Display` - which is
exactly what `RepartitionExec::wait_for_task` does, the reason this fallback
exists at all - a continue wait with that cause reads `Rewrite Error: Continue
wait. Please check logs for additional information`, whose parts are `Rewrite
Error` and ` Continue wait. Please check logs...`, and the predicate matches
neither.

Not reachable today, and I checked rather than assumed: every `CubeError::rewrite`
call site is inside `compile/rewrite/**` with its own locally-authored message,
while a continue wait originates from `transport.load` at execution time, after
rewriting. So this is a false invariant in a comment, not a live bug - but that
sentence is what the next person adding a cause variant or a wrapper will trust.

The comment now names the exception and why it cannot be hit, and
`every_cause_renders_a_matchable_continue_wait` pins both halves: it renders a
canonical continue wait through every cause and asserts the predicate still
matches, `Rewrite` excepted. Its inner match is exhaustive, so a new cause
variant stops compiling here, and an existing arm taught to append fails the
assertion - either way the next appending wrapper surfaces here instead of
silently swallowing the queue's signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFsWyd58aFEWhLT2CjuiiV

* docs(cubesql): give the right reason the `Rewrite` arm is unreachable

Review follow-up, comments only. The justification added in the previous commit
was that `CubeError::rewrite` is minted only inside the rewrite engine with its
own messages. That is not what makes the arm safe, and it is not true:
`query_engine.rs` re-stamps `e.cause = CubeErrorCauseType::Rewrite(..)` onto
whatever `compiler_cache.rewrite` and `find_best_plan` return, whatever its
origin, and `converter.rs` already wraps a foreign `MemberError` with
`CubeError::rewrite(error.to_string())`. So the cause is not confined to
locally-authored messages, and a reader trusting that sentence would think the
arm safe by construction.

The guarantee is one level down: the rewrite phase never calls the transport. It
works against an already-fetched `MetaContext` / `CompilerCacheEntry`, and
`compile/rewrite/` contains no `transport.` call at all, so nothing there can
produce a continue wait for `query_engine.rs` to re-label. That reason survives
someone adding another foreign-error wrap; the previous one did not.

Both the predicate's doc comment and the mirrored paragraph on
`every_cause_renders_a_matchable_continue_wait` now say this. No behaviour or
test change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFsWyd58aFEWhLT2CjuiiV

* chore(review): cap explanatory comments at three lines

Adds a Comments section to the Claude review prompt. Two rules: an explanatory
comment is three lines at most, and it earns its place only when its absence
would let a later edit reintroduce the bug.

A comment is a claim nothing recompiles, so it stops being checked the moment
the code under it moves, and a long one buries the sentence that was actually
load-bearing. The section also names what to prefer instead - a named constant,
a named intermediate value, an extracted function - and bounds the rule so it is
never raised to ask for a comment to be added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFsWyd58aFEWhLT2CjuiiV

---------

Co-authored-by: Claude <noreply@anthropic.com>
…MPILER_CACHE_SIZE` (#11732)

* feat(server-core): add CUBEJS_COMPILER_CACHE_SIZE env variable

`compilerCacheSize` could only be set through the configuration file. Add a
`CUBEJS_COMPILER_CACHE_SIZE` environment variable that feeds the same option,
so the compiler cache can be sized without a config file change.

The value is resolved in `OptsHandler` alongside the other env-backed options,
so the configuration file keeps taking precedence over the environment
variable, and defaults to the pre-existing 250.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsJnAHifeU7RMbdumCLSDb

* test(backend-shared): align CUBEJS_COMPILER_CACHE_SIZE assertions with env-var

`asIntPositive()` accepts 0 (it rejects negatives and non-integers), which
matches the `min(0)` Joi rule the `compilerCacheSize` option already uses.
Assert on the values it actually rejects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsJnAHifeU7RMbdumCLSDb

* fix(backend-shared): reject CUBEJS_COMPILER_CACHE_SIZE=0 instead of silently defaulting

env-var's `asIntPositive()` lets 0 through, but every consumer of the option
falls back to the default on a falsy value (`server.ts:205`, `server.ts:750`,
`CompilerApi.ts:163/168/173`), so `CUBEJS_COMPILER_CACHE_SIZE=0` was accepted
as valid and then quietly became 250. Fail loudly instead, and say so in the
docs, so 0 can't be mistaken for a way to disable the compiler cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsJnAHifeU7RMbdumCLSDb

* test(backend-shared): isolate CUBEJS_COMPILER_CACHE_SIZE invalid-value cases

Split the bundled invalid-value assertions into a test.each, matching
timezone.test.ts, so a regression in one input doesn't mask the others.
Hoist the env cleanup into beforeEach so the suite is hermetic without
each test repeating the delete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsJnAHifeU7RMbdumCLSDb

* fix(server-core): reject compilerCacheSize of 0 in the config file too

The env variable now rejects 0, but `optionsValidate.ts` still accepted it
from the config file and let it be silently replaced by 250 through the
`|| 250` guards — the same trap, on the other entry point.

Tighten the Joi rule to `min(1)`, matching `scheduledRefreshConcurrency` and
`scheduledRefreshBatchSize` directly above it, so both entry points agree with
the documented range. 0 has never sized the cache to anything but the default,
so nothing can depend on its behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsJnAHifeU7RMbdumCLSDb

* test(backend-shared): drop redundant beforeEach in compilerCacheSize suite

The afterEach already leaves the variable unset for the next test, so the
beforeEach only relocated the redundancy flagged earlier rather than removing
it. The sibling describe blocks in this file use afterEach alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsJnAHifeU7RMbdumCLSDb

* docs: document that CUBEJS_COMPILER_CACHE_SIZE also sizes the SQL API cache

The name is not new: CubeSQL already reads CUBEJS_COMPILER_CACHE_SIZE
(rust/cubesql/cubesql/src/config/mod.rs:192) to size its own compiler LRU,
defaulting to 100 rather than 250. Document both consumers and both defaults,
and note that the compilerCacheSize config option sizes only the data model
compiler cache, since CubeSQL reads the environment variable directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MsJnAHifeU7RMbdumCLSDb

---------

Co-authored-by: Claude <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Sep 2, 2026
@pull pull Bot added the ⤵️ pull label Sep 2, 2026
@pull
pull Bot merged commit 6c75c60 into code:master Sep 2, 2026
3 checks passed
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