Skip to content

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

Merged
pull[bot] merged 1 commit into
code:masterfrom
cube-js:master
Aug 16, 2026
Merged

[pull] master from cube-js:master#669
pull[bot] merged 1 commit into
code:masterfrom
cube-js:master

Conversation

@pull

@pull pull Bot commented Aug 16, 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 : )

…1569)

* test(trino-driver): smoke-test LIKE filter escaping against real Trino

Trino has no default escape character for LIKE, so a `contains` filter over
a value like `%` only works if the schema compiler both escapes the value
(`%` -> `\%`) and emits an explicit `ESCAPE '\'` clause. Nothing covered
that end to end, so a regression in either half would have silently turned
user-supplied `%`, `_` or `\` back into wildcards.

Adds an integration test that boots a real Trino, builds the query with the
real PrestodbQuery (the dialect TrinoDriver.dialectClass() resolves to) and
runs it through the real driver, for contains/notContains/startsWith/
endsWith on both the legacy and the Tesseract planner.

Verified the test fails as intended: dropping the `ESCAPE '\'` clause from
PrestodbQuery turns 14 of the 16 cases red on both planners.

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

* test(testing): drive Trino LIKE escaping through the REST API

Replaces the driver-level LIKE escaping test with one that goes through the
REST API against a real Cube server, so the check covers query planning as a
user actually hits it rather than the schema compiler in isolation.

That change of altitude is what surfaced the actual defect. Filters are only
escaped by the dialect that defines the `filters/like_escape_char` template,
and PrestodbQuery is the only one that does. A query answered from a
pre-aggregation is planned for Cube Store instead, the native planner skips
escaping when the template is absent, and the user's `%` reaches the pattern
as a wildcard: `contains '%'` matches every row and `notContains '%'` matches
none.

The fixture therefore carries two cubes over identical data - one queried
straight from Trino, one behind a rollup - and a guard asserts they really are
planned for different engines, so the Cube Store path cannot silently stop
being exercised. The three filters the rollup path gets wrong are marked
`it.failing`: they document the defect without reddening CI, and will demand
attention by failing once the escaping gap is closed.

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

* fix(schema-compiler): escape LIKE wildcards on the native planner

A `contains` filter is supposed to match a literal `%`, but on the native
planner it matched every row instead. Escaping there only happens when the
dialect defines the `filters/like_escape_char` template - the planner skips
it entirely when absent - and PrestodbQuery was the only dialect that
defined it. Every other dialect passed the user's value through untouched,
so `%` and `_` reached the pattern as wildcards. `contains '%'` matched
everything and `notContains '%'` matched nothing.

This bites hardest where it is least visible: a query answered from a
pre-aggregation is planned for Cube Store, so filters that were correct
against the source database silently changed meaning once a rollup started
serving them.

Declares the escape character on the base templates, so every dialect
inherits the escaping the legacy planner has always applied via
BaseFilter.escapeWildcardChars. It is a bare character with no ESCAPE
clause because backslash is already the default in Postgres, MySQL,
BigQuery, ClickHouse and Cube Store - and Cube Store's parser rejects an
explicit clause outright, so a clause in the shared template would break
every pre-aggregation query.

Dialects with no default escape character need the clause to interpret that
escaping, so MSSQL, Oracle and Snowflake now emit one on the native filter
path, matching what each already does on the legacy path. Oracle's was
present but gated on `default_escape`, which the filter path never sets.

Verified end to end against real Trino and real Cube Store, and by SQL shape
across fifteen dialects on both planners.

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

* fix(schema-compiler): cover DuckDB and Pinot, and run the smoke test in CI

Follow-up to review on #11569.

DuckDB and Pinot were missed when auditing which dialects need an explicit
ESCAPE clause, because both live in driver packages rather than in
schema-compiler's adapter directory, and the sweep only covered the latter.
DuckDB gates its `expressions.like` on `default_escape`, which is how this
repo records that a dialect has no default LIKE escape character, and Pinot
already emits the clause on its legacy filter path. Both therefore ended up
escaping the value with nothing to interpret it, turning `contains '%'` from
matching everything into matching nothing.

The new smoke test was also unreachable from CI: `.github/actions/smoke.sh`
invokes each suite by explicit script name, so a script that is not listed
there only ever runs locally. A regression guard nothing runs guards nothing,
so it now sits alongside `smoke:trino`.

Also pins the MSSQL legacy case in the unit test instead of branching around
it. It was asserting nothing while still counting as a passing test, which
made the suite look like it covered a case it did not.

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

* test(duckdb-driver,pinot-driver): pin the LIKE escape clause

Follow-up to review on #11569.

The DuckDB and Pinot ESCAPE clauses were the only lines in the change with
nothing asserting on them, so a later template edit could drop either and
reintroduce exactly the bug the rest of the work exists to prevent. The
dialect matrix in schema-compiler cannot reach them - it sits upstream of the
driver packages - so each driver gets the assertion in its own package,
covering both planners.

Also names DuckDB and Pinot in BaseQuery's enumeration of dialects that carry
an explicit clause. That comment is what a future sweep reads to find them,
and it is precisely what failed here: both live outside src/adapter/, so
enumerating that directory missed them.

Keeps PrestodbQuery's now-duplicate like_escape_char rather than inheriting
it, with a comment explaining why: the ESCAPE clause in its like_pattern
hardcodes the same character, so the two have to change together.

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

* test(duckdb-driver,pinot-driver): make the template tests reachable from CI

Follow-up to review on #11569.

Both new template tests were written where nothing would run them. Pinot's
`unit` script matches `dist/test/unit`, so a file directly under `test/` is
excluded, and nothing invokes `integration:pinot` — there is no Pinot group in
smoke.sh. The DuckDB one was reachable, but only through `integration:duckdb`
in the docker smoke job, which is a slow place to run a test that needs no
database.

Moving both under `test/unit/` puts them in the fast `lerna run unit` job.
DuckDB needs a `unit` script for that, since it had none. Confirmed by running
the CI entry points rather than the files directly:

    pinot   yarn unit -> 2 suites, 11 tests
    duckdb  yarn unit -> 1 suite,   2 tests

This also rescues Pinot's pre-existing sql_table template test, which has
never run in CI for the same reason.

A guard nothing runs guards nothing — the same gap as the smoke script wiring,
one level down, and worth checking by execution rather than by reading.

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

* test(testing-drivers): cover LIKE wildcard escaping across every driver

The escaping fix changes behaviour for every dialect, but the only
behavioural coverage was Trino. These run in the shared driver suite, so they
execute against all thirteen engines the suite supports, and they exercise
both escaping paths: once against a cube with no pre-aggregations, so the
filter reaches the database, and once through a query the ECommerce `SA`
rollup serves, where the filter SQL is generated for the rollup store
instead. The rollup path is the one that broke in production.

No fixture data was added. The existing dataset already discriminates: no
product name contains a percent sign, so `contains '%'` must return nothing
and would return everything if the wildcard leaked, and exactly one product
has a literal underscore, so `contains '_'` must return that one rather than
all twenty-eight.

Asserts exact result sets rather than snapshots, deliberately. The wrong
answer here is a superset of the right one, so a snapshot would have recorded
the broken result as expected — which is how this class of bug survives.

Note the pre-existing `contains with special chars` test does not discriminate:
`%di_Novo%` matches the same single row whether or not the underscore is
escaped, so it would have stayed green throughout.

Verified against real Postgres on the native planner: 124 passed, including
all five, with the rollup queries confirmed to route to the Cube Store
pre-aggregation.

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

* test(testing-drivers): pin which engine answers each LIKE escaping case

Follow-up to review on #11569.

The pre-aggregated pair named the rollup path but never checked it, and
`contains '%'` cannot notice the difference on its own: it returns nothing
whether the rollup or the source database answers. If `SA` ever stopped
matching, both cases would keep passing as duplicates of the Products ones
and the rollup-store escaping path would lose its only coverage here, silently
- the same shape as the two CI-reachability gaps already fixed on this branch.

Asserts `external` on all four cases, in both directions: false for the cubes
with no pre-aggregations, true for the ones the rollup serves. Pinning both
halves is what makes the pair a contrast rather than two similar queries.

The review suggested `usedPreAggregations`, which is the more explicit field
and is what the Trino smoke test would use. It is not available here: the
gateway only emits it in dev mode, and this suite does not enable it, so that
assertion would have failed on all thirteen engines. Confirmed by dumping the
real `/load` response, which carries `external: true` and
`extDbType: cubestore` instead.

Verified against real Postgres: 124 passed, with the guards active.

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

* test(testing-drivers): assert the engine on the notContains case too

Follow-up to review on #11569.

It was the only one of the four Products cases not pinning which engine
answered, and the only one issuing two concurrent loads - so the one most
likely to be edited later without noticing the omission. Harmless today,
since Products has no pre-aggregations in any fixture, but the point of these
guards is that the pairing cannot quietly stop being a pairing.

Verified against real Postgres: 124 passed.

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

* fix(pinot-driver): drop the ESCAPE clause, which Pinot rejects

The new driver test caught this: `contains '_'` returned nothing on Pinot
where it should have returned the one product with a literal underscore.

Verified against Pinot 1.4.0 rather than inferred. An explicit clause fails
at execution:

    'a_b' LIKE '%\_%' ESCAPE '\'   -> Query execution error
    'a_b' LIKE '%\_%'              -> matches
    'axb' LIKE '%\_%'              -> no match

So Pinot already reads backslash as the LIKE escape character and rejects
being told so. The escaping the planner applies to the value takes effect
precisely by emitting no clause.

Removes it from both paths. The clause on the legacy filter predates this
branch and has never worked - nothing asserted that a literal `_` matches, so
it went unnoticed; the earlier commit here propagated the same mistake to the
native path. The unit test now pins the absence of the clause as well as the
escaping of the value, since restoring either half alone breaks it.

One Pinot limitation remains and is not fixable from this side: an escaped
`%` immediately followed by a trailing wildcard is mis-parsed, so
`'50% off' LIKE '%\% off'` matches but `'%\%%'` does not. `contains '%'`
therefore cannot match a literal percent sign on Pinot.

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

* revert(pinot-driver): split Pinot out of the escaping fix

The Pinot job on b6a6465 disproved the theory that commit was built on.
Removing the ESCAPE clause did not restore Pinot's LIKE matching:

    Snapshots:   102 passed, 102 total    <- the 21 `Array []` did not change
    Tests:       1 failed, 110 passed
    filtering Products: contains a literal underscore -> Received []

So the empty snapshots are accurate rather than stale. Pinot's LIKE does not
match a non-constant CONCAT(...) pattern, which is why contains, startsWith
and notContains all return nothing there - independently of escaping, and
since well before this branch. Dropping the ESCAPE clause was necessary but
nowhere near sufficient, and fixing the rest means changing how the pattern is
built for that dialect, which is its own piece of work and needs a Pinot to
test against.

Reverts PinotQuery.ts to master so this PR carries only the escaping change,
and skips the one new case Pinot cannot pass, with the reason recorded in the
fixture's own banner style. The other four escaping cases still run there -
they pass because they expect an empty result, which is noted so nobody reads
those ticks as Pinot being correct.

The test-file move to test/unit/ stays: it is a CI-reachability fix, not a
Pinot behaviour change, and it rescues the pre-existing sql_table test that
had never run.

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

* docs(testing-drivers): correct what the Pinot banner claims about notContains

The banner said the other escaping cases pass on Pinot "only because they
expect an empty result". That holds for the two contains-a-literal-percent
cases and not for notContains, which asserts a non-empty result equal to the
unfiltered count and passes because NOT LIKE over a never-matching pattern
returns every row. It is the one Pinot case here whose assertion still means
something, and describing it as vacuous invites the next reader to skip it.

The banner is what someone reads before adding a name to this list, so it is
worth being exact about which cases are load-bearing. Also points at #11570
rather than saying "tracked separately".

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

* docs(testing-drivers): split the Pinot banner by which engine answers

The previous pass fixed one mis-classification and introduced its mirror.
Calling both contains-a-literal-percent cases vacuous is wrong: the ECommerce
(pre-aggregated) one asserts servedByRollupStore true, so Cube Store generates
and evaluates that pattern and Pinot's LIKE never runs. It is as protective
here as on any other engine. And notContains is not the strongest surviving
case - the pre-aggregated underscore case asserts a specific non-empty row
from the rollup store.

What decides whether a case means anything on Pinot is which engine answers
it, not the shape of its assertion, so the banner now splits on that. It sits
directly above a skip list, and labelling the two genuinely protective cases
as vacuous is the one error most likely to cause real damage there.

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

* test(testing-drivers): move Trino LIKE escaping coverage into the shared suite

The Trino LIKE-escaping regression was covered by a standalone birdbox smoke
test in cubejs-testing. That put the only end-to-end proof of the fix in a
Trino-specific file, so no other engine exercised it and the driver suite -
where every backend runs the same cases - stayed blind to the bug.

Onboard Trino as a driver in cubejs-testing-drivers and delete the smoke test:

  - fixtures/trino.json: trinodb/trino:476 against the in-memory catalog, the
    Presto-family cast rules, and the same pre-aggregation set the other
    fixtures declare. The container raises query.max-stage-count because the
    fixture tables are built from ~1000-branch UNION ALL chains, which blow
    past Trino's 150-stage default.
  - runEnvironment: wait on the image's HEALTHCHECK for Trino. The compose
    environment overwrites each strategy's own timeout with the global one, so
    the global value is what has to be raised.
  - Both planners are wired into drivers-tests.yml. Skip lists are split by
    planner and annotated with the Trino limitation behind each entry: no WEEK
    interval unit, PrestodbQuery rejecting multi-part intervals, ordinal
    GROUP BY in SQL API rollup push-down, and unordered results that cannot be
    snapshotted.

Locally: 128 passed / 11 skipped with Tesseract, 109 passed / 30 skipped on
the legacy planner, no failures on either.

The shared suite also picks up startsWith and endsWith percent-sign cases plus
a pre-aggregated notContains, so the operator coverage the smoke test had is
kept, now running for every driver rather than Trino alone. Verified green on
Postgres under both planners.

buildPreaggs now rejects with the response body when the jobs endpoint answers
with an error instead of a token array. That path previously died on an opaque
TypeError inside a polling interval and failed 120s later complaining about
`tokens`, which hid the pre-aggregation the fixture was actually missing.

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

* fix(testing-drivers): reject with the response body when the jobs endpoint is not JSON

Two follow-ups from review on the pre-aggregation build helper.

A response that is not JSON at all - a proxy error page, or an empty body from
a cube that died during startup - threw inside a `.then` with no rejection
handler. The outer promise never settled, and the 120s backstop is only armed
once there are tokens to poll, so the build hung until jest's own timeout with
the response never printed. The parse is now guarded and rejects with the body,
and the chain carries a `.catch` so a failed request settles too.

The polling round had the same hole one step later, where the backstop reduced
it to `timeout.` with no cause. Guarded the same way. The backstop is also
cleared on the success path now, instead of being left pending.

Also correct the Pinot banner, which the previous commit invalidated: the
escaping group grew to three Cube Store-answered ECommerce cases, and
startsWith and endsWith join contains as vacuous on Pinot, so naming contains
"the only vacuous one" no longer held.

Postgres driver suite re-run green after the rewrite: 127 passed, 20 skipped,
with `must built pre-aggregations` still building all eight rollups.

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

* fix(testing-drivers): stop polling once the pre-aggregation build has settled

The `failure` branch rejected from inside the status tally, where a `failure`
status is neither `done` nor `missing_partition`, so `inProcess` stayed
non-empty and neither timer was cleared. The helper went on POSTing to the jobs
endpoint once a second for the remaining ~120s after the promise had settled,
then the backstop called reject on an already-settled promise. `--forceExit`
kept that from stalling the run, but it interleaved the polling output with
every following test at exactly the moment someone is reading it to find out
why the build failed.

Hoist the check out of the loop so it settles and stops, and route every
settle path through one `stop()` that clears both timers - the two guarded
rejects added in the previous commit cleared the interval but left the 120s
backstop pending.

Postgres driver suite re-run green: 127 passed, 20 skipped, pre-aggregations
still building.

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

* fix(testing-drivers): count outstanding partitions across the whole job list

`hookPreaggs` is the twin of the loop the previous commit fixed, and had the
same defect plus a worse one. `postBuildJobs` returns one token per partition
and the outstanding tally was tested inside the loop over them, so a first
token reporting `done` resolved the build while later partitions were still
scheduled - the suite then queried a rollup table that did not exist yet.
Count over the whole array instead.

The `failure` branch also rejected without stopping the poll, and an empty job
list never settled at all because the check lived in a loop body that never
ran; both follow from the same restructuring.

Verified with the postgres-core suite, which is what exercises this function:
6 passed, snapshots unchanged, so the recorded driver calls are identical.

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

* fix(testing-drivers): guard the jobs poll in hookPreaggs like its twin

The `await preAggregationsJobsGET` was the last unguarded one in this file. The
orchestrator and compiler errors it can raise became unhandled rejections
thrown out of an async interval callback, so `stop()` never ran, the poll kept
going, and the build died 60s later on `timeout.` with the real error never
printed - the failure mode already removed from `buildPreaggs`. The two poll
loops now have the same shape.

postgres-core green again: 6 passed, snapshots unchanged.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Aug 16, 2026
@pull pull Bot added the ⤵️ pull label Aug 16, 2026
@pull
pull Bot merged commit f050c08 into code:master Aug 16, 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