Skip to content

fix(pinot-driver,dremio-driver,druid-driver): interpret LIKE wildcard escaping - #11811

Open
waralexrom wants to merge 8 commits into
masterfrom
tesseract-like-pattern-escaping
Open

fix(pinot-driver,dremio-driver,druid-driver): interpret LIKE wildcard escaping#11811
waralexrom wants to merge 8 commits into
masterfrom
tesseract-like-pattern-escaping

Conversation

@waralexrom

@waralexrom waralexrom commented Sep 9, 2026

Copy link
Copy Markdown
Member

Problem

A contains / notContains / startsWith / endsWith filter is supposed to match the
user's value literally: searching for 50%_off must return the rows containing that
string, not every row. That takes two cooperating pieces, and both have to come from the
dialect:

  1. the %, _ and the escape character itself are escaped inside the value;
  2. the emitted statement interprets that escaping — either because the engine reads
    backslash as its default LIKE escape character, or because the statement carries an
    explicit ESCAPE clause.

Having (1) without (2) is the dangerous half: the backslash stops being an escape and
becomes plain data, so the pattern searches for a string nobody has and the filter
silently returns nothing.

The escape character is declared once on the base templates, so escaping now happens for
every dialect. A dialect whose LIKE has no default escape character therefore has to say
so. Three did not, in four places:

dialect planner before
Pinot native LOWER(x) LIKE CONCAT('%', LOWER(?), '%') — legacy already had the clause
Dremio native x ILIKE '%' || ? || '%' — an operator Dremio does not have
Dremio legacy ILIKE(x, CONCAT('%', ?, '%')) — the function takes no escape argument
Druid both LOWER(x) LIKE CONCAT('%', LOWER(?), '%')

Dremio's legacy path carried a second defect in the same function: the negation was
spliced into the first argument of the call — ILIKE(x NOT, CONCAT(...)) — a parse error
rather than a filter, for every notContains, notStartsWith and notEndsWith.

Cause

The native filter path renders tesseract.ilike, which is a separate template from
the expressions.like / expressions.ilike pair the SQL API push down uses. A dialect
that carries the clause on the push-down path — Dremio and DuckDB both do, gated on
default_escape — still has nothing on the filter path.

Pinot was missed even though BaseQuery's enumeration of the dialects that carry a clause
already named it; Dremio and Druid were never in that enumeration at all.

What changed

  • Pinot (tesseract.ilike): carries ESCAPE '\', matching what
    PinotFilter.likeIgnoreCase already emits. It cannot go inside like_pattern because
    Pinot's pattern is wrapped in CONCAT(...).
  • Dremio (tesseract.ilike, new; and DremioFilter.likeIgnoreCase): the case folding
    moves from the ILIKE(expr, pattern) function onto LOWER(...) LIKE LOWER(...), since
    the function takes no escape argument and LIKE does. That also puts the negation beside
    the operator, fixing the parse error.
  • Druid (tesseract.ilike and DruidFilter.likeIgnoreCase): both carry the clause.
    The changed SQL is what its dialect test pins, so that assertion moves with it.
  • BaseQuery: the comment enumerating which dialects carry an explicit clause — the
    thing a future sweep of this area reads — names Dremio and Druid, and names ksqlDB as
    the one dialect the shared escape character is wrong for.

No Rust change. The native planner's own escaping is correct and already covered by unit
tests in cubesqlplanner; this is entirely about the dialect templates it renders through.

How it was verified

By values, against real Postgres. Both planners were run over a table holding
50%_off, 50Xyoff, a\b, aXb, plain, 50%_offer for seven filter shapes. Every
case matches literally and the two planners agree:

contains '%'           MATCH  legacy=["50%_off","50%_offer"]  tess=["50%_off","50%_offer"]
contains '_'           MATCH  legacy=["50%_off","50%_offer"]  tess=["50%_off","50%_offer"]
contains '50%_off'     MATCH  legacy=["50%_off","50%_offer"]  tess=["50%_off","50%_offer"]
contains 'a\b'         MATCH  legacy=["a\\b"]                 tess=["a\\b"]
startsWith '50%'       MATCH  legacy=["50%_off","50%_offer"]  tess=["50%_off","50%_offer"]
endsWith '_off'        MATCH  legacy=["50%_off"]              tess=["50%_off"]
notContains '%'        MATCH  legacy=["50Xyoff","aXb","a\\b","plain"]  tess=[same]

By values, against real Druid. Druid already has a cluster in CI - its own
docker-compose, reached from the integration matrix - but nothing there built a query
through DruidQuery, so the filter family was covered by SQL shape only. It now ingests
rows and asserts exact result sets on both planners. That also settles the one question
the shape tests cannot answer: Druid does honour ESCAPE on a non-literal pattern, but
only over a datasource - over an inline SELECT 'x' AS name it refuses the query with
Function[like] pattern argument must be a literal, which is why the case ingests instead
of selecting constants.

Undoing the clause turns 10 of those 12 cases red against the live cluster: rows expected,
Array [] returned, and notContains '%' returning every row instead of three. The
ordinary-value case stays green, so the escaping cannot be "fixed" by breaking plain
search.

Value-level coverage across every engine the shared driver suite supports, on both the
source-database and the rollup-store escaping paths, already exists in
cubejs-testing-drivers. Pinot is in that suite, but its LIKE cases cannot discriminate:
it does not match a non-constant CONCAT(...) pattern at all (tracked separately). Dremio
is the only one of the three with no engine to test against - it has no compose file and
is commented out of the integration matrix as flaky.

By SQL shape, across every dialect in the repo, for all six LIKE operators on both
planners. Pinot, Dremio and Druid now emit the same predicate on each planner. Those checks
were run while developing the fix but are not committed: coverage here rests on behaviour,
not on the SQL text, and shape assertions largely restate the templates they read.

The value cases fail without the fix. Undoing the clause turns 10 of the 12 Druid cases
red, as above.

Note the coverage this leaves per dialect, which is uneven and worth knowing at merge time:

dialect engine available what covers the clause
Druid own docker-compose cluster, in the integration matrix the value cases added here, both planners
Pinot in cubejs-testing-drivers nothing effective - its LIKE cases cannot discriminate while it refuses non-constant patterns
Dremio none - no compose file, commented out of the matrix as flaky nothing

Risks

  • Druid is now verified by values against a real engine. Pinot and Dremio still rest on
    the repo's own record. For Pinot that record is strong: the exact SQL form is what its
    legacy path already sends to real Pinot, pinned by an existing driver test. For Dremio it
    is sqlTemplates deleting expressions.ilike (no infix operator) plus the
    default_escape gate on expressions.like (no default escape character), and an
    existing driver test that already assumes the LOWER(...) LIKE ... ESCAPE '\' shape.
  • Dremio's native path could not parse before this change and its legacy path could not
    parse for any negated operator, so neither can regress into a worse state.
  • Druid's legacy SQL changes shape for every LIKE filter. LOWER(x) LIKE p ESCAPE '\' is
    the same predicate it emitted before plus the clause, so the only behaviour that changes
    is the one that was wrong.

Left open, deliberately

  • ksqlDB overrides filters.like_pattern but inherits like_escape_char, and its
    LIKE/ILIKE accept no ESCAPE clause at all — so the injected backslashes are matched
    as data and no clause can rescue them. The lever is to stop escaping there rather than to
    add a clause, which is a behaviour change in the opposite direction; with no ksqlDB
    available to confirm its LIKE semantics, this is recorded in the BaseQuery comment instead
    of changed blind.
  • DuckDB's legacy path has the mirror-image gap: it escapes the value and emits no
    clause, while its native path carries one.
  • Hive and SQLite render the base infix ILIKE on the native planner, which neither
    engine has. Different defect class — the statement does not parse rather than quietly
    returning the wrong rows — and not about escaping.

🤖 Generated with Claude Code

@waralexrom
waralexrom requested review from a team as code owners September 9, 2026 10:18
@github-actions github-actions Bot added driver:druid Issues relating to the Druid driver driver:dremio javascript Pull requests that update Javascript code data source driver driver:pinot labels Sep 9, 2026
@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 5m 23s —— View job


Review: sound fix, well-targeted tests — 1 medium (Druid may not accept a non-literal LIKE pattern), 4 low (test strictness, comment volume ×3). No blocking correctness bug found.

Full review — click to expand
  • Read PR diff (git diff origin/master...HEAD)
  • Verify test wiring (module paths, jest config, unit scripts, CI reachability)
  • Verify SQL template correctness per dialect
  • Check existing review threads / dedupe (none existed)
  • Post review

What I verified independently

  • The escaping/interpretation pairing is real. BaseFilter.escapeWildcardChars (BaseFilter.ts:112) escapes \, _, % for every LIKE operator on the legacy path, and the native path does the same in like.rs::escape_like_pattern gated on like_escape_char. Since all three dialects inherit like_escape_char: '\\' and none deletes it, the unconditional ESCAPE in tesseract.ilike is consistent with the value always being escaped — no risk of emitting a clause the value doesn't need.
  • Test wiring resolves. Dremio's require('../../../driver/DremioQuery') is correct from dist/test/unit (rootDir: ".", driver/ is uncompiled JS) and matches the sibling params-escaping.test.ts. Druid's ../../src/DruidQuery resolves to dist/src/DruidQuery.js. Both new files are reached by jest dist/test/unit.
  • CI really runs both planners. .github/workflows/push.yml:137 runs yarn lerna run unit after yarn tsc, with the native addon downloaded as an artifact — so useNativeSqlPlanner: true exercises Tesseract rather than silently falling back. The new Druid unit script is needed exactly as described (integration is jest dist/test/*.js, top-level only).
  • No collateral snapshots. Nothing else pins Dremio's old ILIKE (col NOT, ...) shape, and Pinot's LIKE-family cases are already skipped in fixtures/pinot.json for the non-constant-pattern reason, so the changed Pinot template causes no snapshot churn.
  • Dremio's legacy negation fix is real. ILIKE(col NOT, ...) was a parse error for every notContains/notStartsWith/notEndsWith; moving to LOWER(col) NOT LIKE ... puts the negation beside the operator. Good catch, and the test pins it.

Findings

# Severity Where Issue
1 Medium DruidQuery.ts:22 Druid may not accept a non-literal LIKE pattern — same limitation the PR cites for Pinot — in which case the clause makes the SQL look right without fixing the rows returned
2 Low new unit tests (×3) toContain("ESCAPE '\\'") doesn't tie the clause to the LIKE predicate; only contains is covered, and the negated shape is unpinned for Pinot and Druid
3 Low DremioQuery.js:175-183 8-line comment restates the 5-line one at lines 16-20 sentence for sentence
4 Low PinotQuery.ts:256 Stray space renders LOWER(x) LIKE / NOT LIKE; comment duplicates the BaseQuery block
5 Low BaseQuery.js:4782-4787 Comment now ~24 lines and doubles as the tracker for a known-broken dialect (ksqlDB) — that belongs in an issue

Details are in the five inline comments.

Comment volume, generally

Beyond the three flagged inline, each new test file opens with a 5-6 line prose preamble arguing the same point (DruidQueryTemplates.test.ts:33-38, PinotQueryTemplates.test.ts:53-57, DremioQueryTemplates.test.ts:52-57, plus two more in the Dremio file). Five copies of one rationale spread across three packages. The test names already say what's asserted; the one thing worth keeping near an assertion is the non-obvious bit — this dialect has no default escape character — which fits on one line.

Not run

I couldn't execute the new tests: this checkout has no node_modules and a full monorepo install plus yarn tsc across schema-compiler and the three drivers wasn't available here. The review is static, plus the PR's own recorded failure output. If you want the suites run, allowlisting yarn install/yarn tsc would let me do that.

· branch [`tesseract-like-pattern-escaping`](https://github.com/cube-js/cube/tree/tesseract-like-pattern-escaping)

Comment thread packages/cubejs-druid-driver/src/DruidQuery.ts
Comment thread packages/cubejs-dremio-driver/driver/DremioQuery.js Outdated
Comment thread packages/cubejs-druid-driver/test/unit/DruidQueryTemplates.test.ts Outdated
Comment thread packages/cubejs-pinot-driver/src/PinotQuery.ts Outdated
Comment thread packages/cubejs-schema-compiler/src/adapter/BaseQuery.js Outdated
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.20%. Comparing base (a497bb4) to head (1738851).
⚠️ Report is 10 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11811      +/-   ##
==========================================
+ Coverage   60.16%   60.20%   +0.04%     
==========================================
  Files         239      239              
  Lines       19203    19203              
  Branches     3886     3886              
==========================================
+ Hits        11554    11562       +8     
+ Misses       7099     7089      -10     
- Partials      550      552       +2     
Flag Coverage Δ
cube-backend 60.20% <100.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 2m 37s —— View job


Re-review after 3 new commits: all five prior findings addressed. 1 new medium — DruidQuery.ts deletes expressions.like_escape while adding an ESCAPE clause four lines below.

Full review — click to expand
  • Read the diff since my last review (commits 2aecc55, c60a1ac, 8b58d04, 1738851)
  • Re-check the five prior findings against the current code
  • Resolve my stale review threads (5 resolved)
  • Post updated review

Prior findings — status

# Finding Status
1 Druid non-literal LIKE pattern unverified RecordedDruidQuery.ts:14-15 now says so explicitly ("Unverified against a real broker"), which was the fallback I asked for
2 Tests only pinned ESCAPE '\' as a floating substring, contains only Fixed — all three files now pin the whole predicate and parameterise over contains/notContains/startsWith/endsWith on both planners (8 cases each)
3 Duplicate 8-line Dremio comment Fixed — cut to the 2-line pointer, verbatim from the suggestion
4 Pinot stray space + duplicated comment Fixed{% endif %}LIKE now, comment down to 2 lines
5 BaseQuery comment ~24 lines Improved — 14 → 10 lines. ksqlDB is still tracked in the comment rather than an issue; low, and your call

The tightened tests are a genuine improvement over what I asked for: pinning LOWER("orders".status) NOT LIKE LOWER(CONCAT('%', ?, '%')) ESCAPE '\' catches both the clause drifting and the negation landing back inside the call, and the does not render ILIKE case guards the Dremio operator staying gone on both planners.

Newly verified this round

  • Dremio's new legacy shape agrees with what the package already expects. test/unit/params-escaping.test.ts:9 independently asserts LOWER(name) LIKE '%' || LOWER(?) || '%' ESCAPE '\' — same construct the rewritten DremioFilter.likeIgnoreCase now produces. Nothing anywhere pins the old ILIKE (col NOT, …).
  • Druid's new unit script is wired. tsconfig.json has include: ["src", "test"], rootDir: ".", so test/unit/*.ts compiles to dist/test/unit; jest and @types/jest are already devDependencies and schema-compiler a dependency. integration's dist/test/*.js glob really would have missed the directory.
  • The escape/clause pairing still holds on both paths. BaseFilter.escapeWildcardChars (BaseFilter.ts:112) escapes unconditionally for every operator isWildcardOperator covers, and the native side keys off like_escape_char, which none of the three deletes — so the unconditional clause never over-escapes.

New finding

Medium — packages/cubejs-druid-driver/src/DruidQuery.ts:68. delete templates.expressions.like_escape is how a dialect declares its LIKE takes no ESCAPE clause (CubeSQL gates the push-down rewrite on the template's presence, like_expr.rs:108), and line 72 now emits precisely that clause. If Druid does accept ESCAPE the deletion is stale and needlessly blocking push-down; if it doesn't, the new clause turns every LIKE filter into a parse error. Details inline.

This is distinct from the non-literal-pattern caveat already recorded in the DruidFilter comment: that one is about whether the clause is honoured, this about whether it parses.

Not run

Same limitation as last round — no node_modules in this checkout, so the new suites were not executed. The sql.replace(/\s+/g, ' ') normalisation plus exact-predicate toContain makes the expectations whitespace-fragile against template edits by design, so I'd want a real run of yarn lerna run unit --scope '@cubejs-backend/{druid,dremio,pinot}-driver' before merge to confirm the eight Dremio strings (particularly the ?|| '%' spacing quirks) match what the base like_pattern actually renders.

· branch [`tesseract-like-pattern-escaping`](https://github.com/cube-js/cube/tree/tesseract-like-pattern-escaping)

@@ -64,7 +67,9 @@ export class DruidQuery extends BaseQuery {
templates.expressions.timestamp_literal = 'TIME_PARSE(\'{{ value }}\')';
delete templates.expressions.like_escape;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file now says two contradictory things about Druid's LIKE grammar. Line 68 deletes expressions.like_escape — the base '{{ like_expr }} ESCAPE {{ escape_char }}' — which is how a dialect declares "my LIKE takes no ESCAPE clause" (CubeSQL reads it as a capability gate: like_expr.rs:108 refuses the rewrite when the template is absent). Four lines below, tesseract.ilike emits exactly that clause.

Only one of the two can be right:

  • If Druid does accept ESCAPE (its docs give x LIKE pattern [ESCAPE esc]), the deletion on line 68 is stale and is silently blocking SQL-API push-down of LIKE ... ESCAPE, which this PR is well placed to drop.
  • If it doesn't, the new clause on line 72 makes every LIKE filter fail to parse — a much worse outcome than the wrong-rows bug being fixed.

Whichever way it resolves, worth a line explaining why the two coexist, since the next reader hits both within five lines. Note this is separate from the non-literal-pattern question already recorded in the DruidFilter comment — that one is about whether the clause is honoured, this one about whether it parses.

Fix this →

waralexrom and others added 8 commits September 10, 2026 18:38
…ative planner

Both planners escape `%`, `_` and the escape character itself inside a
user-supplied LIKE value, but that escaping only means anything if the emitted
statement carries the clause that interprets it. Pinot and Dremio both have no
default LIKE escape character, and nothing asserted on either dialect's native
filter path, so a value like `50%_off` could silently stop matching literally
without a single test noticing.

Pinot's case covers both planners, since its legacy path already carries the
clause. Dremio's covers the native planner, where the statement additionally
has to avoid the infix `ILIKE` operator the dialect does not have.

Both tests fail as written:

    pinot   ✕ escapes LIKE wildcards ... on the tesseract planner
            Expected substring: "ESCAPE '\'"
            Received: WHERE ((LOWER("orders".status)  LIKE CONCAT('%', LOWER(?), '%')))

    dremio  ✕ interprets that escaping with an explicit ESCAPE clause
            ✕ does not render ILIKE as an infix operator
            Received: WHERE (("orders".status ILIKE '%' || ?|| '%'))

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e planner

A `contains`, `startsWith` or `endsWith` filter is supposed to match a literal
`%` or `_`, and the value is escaped for exactly that. Neither Pinot nor Dremio
has a default LIKE escape character, so on those two dialects the escaping the
native planner applies reached the engine with nothing to interpret it: the
backslash stayed a plain character and a search for `50%_off` matched nothing
instead of the rows containing it.

Pinot's legacy filter path already emits the clause, so the two planners
disagreed on the same query. Dremio's native path was worse off: it also
inherited the base infix `ILIKE` operator, which the dialect does not have -
that is what deleting `expressions.ilike` records - so it could not carry the
clause and would not parse. Its case folding therefore moves onto `LOWER(...)
LIKE LOWER(...)`, which takes an ESCAPE clause where the `ILIKE(expr, pattern)`
function does not.

The escape character is declared once on the base templates, so a dialect only
has to say that its LIKE needs the explicit clause. BaseQuery's enumeration of
the dialects that carry one is what a future sweep reads, so it names Dremio
too.

Verified by values against real Postgres, where both planners return the rows
containing a literal `%`, `_` and `\` rather than every row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…egacy planner too

Escaping a filter value only means anything if the emitted statement carries the
clause that interprets it, and that is true whichever planner emitted the
statement. Pinning only the native planner locks the divergence in rather than
catching it, and Druid was not covered at all.

Dremio's case now covers both planners, and adds one for the negation: a `NOT`
belongs beside the operator, not spliced into the first argument of a function
call. Druid's covers both planners; its test needs a `unit` script to be
reachable from the fast CI job, since `integration`'s top-level glob leaves the
directory out.

The added cases fail as written:

    dremio  ✕ escapes ... on the legacy planner
            ✕ does not render ILIKE on the legacy planner
            ✕ negates beside the operator on the legacy planner
            Received: WHERE ( ILIKE ("orders".status NOT, CONCAT('%', ?, '%'))
                             OR "orders".status IS NULL)

    druid   ✕ escapes ... on the legacy planner
            ✕ escapes ... on the tesseract planner
            Expected substring: "ESCAPE '\'"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y planner too

Escaping a filter value with a backslash is only half of matching a literal `%`
or `_`; the statement still has to say that the backslash is the escape
character. Neither Dremio nor Druid has a default one, and both left that unsaid
on the legacy filter path, so `contains: ['%']` searched for a literal `\%` and
returned nothing.

Dremio also spliced the negation into the first argument of a function call -
`ILIKE(x NOT, CONCAT(...))` - which is a parse error rather than a filter, for
every notContains, notStartsWith and notEndsWith. Moving the case folding onto
`LOWER(...) LIKE ...` fixes both at once: unlike the ILIKE function, LIKE takes
an escape clause, and the negation lands beside the operator where it belongs.

Druid needed the clause on both planners, and its inherited native template had
none either. The `ESCAPE` clause changes the SQL its dialect test pins, so that
assertion moves with it.

BaseQuery's enumeration of the dialects that carry an explicit clause is what a
future sweep of this area reads, so it names Druid. It also names ksqlDB as the
one dialect the shared escape character is wrong for: its LIKE accepts no
ESCAPE clause at all, so no clause can rescue the backslashes and they are
matched as data. Left as is rather than fixed blind - suppressing the escaping
is a behaviour change in the opposite direction, and no ksqlDB is available here
to confirm it.

Verified by values against real Postgres (unchanged, both planners literal), and
by SQL shape for all six LIKE operators on both planners: Pinot, Dremio and
Druid now emit the same predicate on each.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing comments back

The rationale for the escape clause ended up written out three or four times -
twice inside DremioQuery alone, sentence for sentence, and again in each dialect
on top of the BaseQuery block that already records the mechanism. One statement
of a reason protects a future edit as well as four do; past that it is prose
around a template value, and the BaseQuery block had grown to two dozen lines of
it. Each note is now the load-bearing sentence, and the enumeration of which
dialects carry an explicit clause - the part a sweep of this area actually
reads - survives intact.

Also drops the stray space in Pinot's template, which rendered `LOWER(x)  LIKE`
and `NOT  LIKE`. It made the native predicate differ from the legacy one by
whitespace alone, which any assertion on the whole predicate has to encode.

Records on DruidFilter that whether Druid honours ESCAPE on a non-literal
pattern is unverified here, so the next reader does not take this path for
confirmed. The clause is still strictly better than none: without it the
backslashes are matched as data on any code path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dicate, not just the clause

`toContain("ESCAPE '\'")` proves the substring exists somewhere in the
statement. It does not tie the clause to the LIKE predicate it has to attach
to, so it would stay green with the clause drifting outside the CONCAT(...) or
onto a different filter entirely. Each case now pins the whole predicate.

Coverage also stopped at `contains`, which left the wildcard placement of
`startsWith`/`endsWith` and every negated operator unasserted - and the negated
shape is exactly where Dremio's legacy parse error lived. All four operators now
run on both planners.

Confirmed the stronger form still catches the defect, by undoing the fix: 22 of
the 43 cases go red, against 4 under the previous assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…al Druid

Druid is the one of the three dialects with a real engine already in CI - its
own docker-compose cluster, reached from the integration matrix - but nothing
there built a query through DruidQuery, so the whole filter family was covered
only by SQL shape.

This settles a question the shape tests cannot answer: whether Druid honours
ESCAPE on a non-literal pattern. It does, but only over a datasource. Over an
inline `SELECT 'x' AS name` Druid refuses the query outright with `Function[like]
pattern argument must be a literal`, so the case ingests rows rather than
selecting constants - and waits for the rows to answer, since ingestion
finishing and the segment becoming queryable are separate events.

Asserts exact result sets against what a literal match means, in both
directions: `contains '%'` must return only the rows with a percent sign, and
`notContains '%'` only the rows without one. `contains 'off'` pins that an
ordinary value keeps working, so the escaping cannot be "fixed" by breaking
plain search.

Verified the cases catch the defect, by undoing the clause: 10 of the 12 go red
against the live cluster, with rows expected and `Array []` returned - and
notContains returning every row instead of three. `contains 'off'` stays green,
as it should.

    Tests: 16 passed (29s, ingestion included)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sts, keep the value ones

Coverage for the escape clause now rests on behaviour rather than on the SQL
text. Druid checks it by values against a real cluster; the shape assertions
restated the templates they were reading, so they said little the value cases do
not say better.

Pinot's file and Dremio's params-escaping cases predate this and stay as they
were. Druid's `unit` script existed only for the deleted file, and an empty
directory fails jest, so it goes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@waralexrom
waralexrom force-pushed the tesseract-like-pattern-escaping branch from 036fb5d to e285dad Compare September 10, 2026 16:41
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 3m 18s —— View job


Re-review after 3 new commits: 1 medium — the last commit deleted the Pinot and Dremio tests, leaving both dialects' changes with no coverage. The new Druid value test is good and settles the open ESCAPE question. 1 low, 1 prior thread re-affirmed.

Full review — click to expand
  • Read the diff since the last round (99d56f5, 17444b5, 7dae88d, 3fe0ddd, e285dad)
  • Re-check prior findings against current code
  • Verify the new Druid value-test wiring (CI reachability, native addon, ingestion)
  • Resolve stale threads / dedupe (nothing stale — 5 already resolved, 1 still applies)
  • Post review

Prior findings — status

# Finding Status
1 Druid non-literal LIKE pattern unverified Fixed properly3fe0ddd ingests rows and asserts result sets against the real cluster on both planners, which answers it by behaviour rather than by comment
3 Duplicate 8-line Dremio comment Fixed — 2-line pointer
4 Pinot stray space + duplicated comment Fixed{% endif %}LIKE, 2-line comment
5 BaseQuery comment ~24 lines Fixed — 9 lines, enumeration kept, prose gone
2 Tests pinned only a floating ESCAPE '\', contains only Fixed in 7dae88d, then the tests were deleted in e285dad — see below
(last round) DruidQuery.ts:68 deletes expressions.like_escape while line 72 emits the clause Still open — code unchanged; not re-posting, the thread stands. The new value test now settles it: Druid parses and honours ESCAPE on a CONCAT(...) pattern, so the delete on line 68 is stale and is needlessly blocking SQL-API push-down of LIKE … ESCAPE. This PR is well placed to drop it

Verified this round

  • The Druid value test is reachable in CI and really runs Tesseract. druid is in the integration matrix (push.yml:424) with use_tesseract_sql_planner: true, and the job downloads the backend-native artifact before install — so useNativeSqlPlanner: true reaches buildSqlAndParamsRust (BaseQuery.js:918) rather than falling back. The file is picked up by the existing integration script (jest dist/test/*.js), which is why the unit script added last round could be dropped again.
  • The case set discriminates. contains '_' expecting one row is the sharp one: unescaped, %_% matches every non-empty row, so a missing clause turns 1 into 5. notContains '%' catches the inverse. And the contains 'off' case keeps the escaping from being "fixed" by breaking ordinary search.
  • Waiting on rows rather than on the task is the right call — segment handoff is a separate event from task success, and the try/catch/finally around a fresh driver per poll is correct.
  • prepareCompiler(..., { adapter: 'druid' }) is harmless — adapter is only declared on the options type in PrepareCompiler.ts:39 and not read.
  • Comment volume is now within the repo's 3-line guidance everywhere it was flagged.

Findings

Medium — packages/cubejs-dremio-driver/driver/DremioQuery.js:19 (inline). e285dad deleted DremioQueryTemplates.test.ts and DruidQueryTemplates.test.ts and reverted the additions to PinotQueryTemplates.test.ts. Dropping Druid's is right — the value test supersedes it. Dropping the other two is not: Druid is the only one of the three with an engine in CI, so for Dremio and Pinot the shape test was the only guard, and shape is what the fix consists of.

  • Dremio's likeIgnoreCase rewrite changes the SQL for every LIKE filter and fixes a parse error on every negated one. Dremio has no compose file and is commented out of the matrix (push.yml:426); test/unit/params-escaping.test.ts asserts on a hand-written string passed to applyParams and never calls DremioQuery. Nothing executes or asserts this method.
  • Pinot's tesseract.ilike clause is likewise unpinned — the surviving PinotQueryTemplates.test.ts covers only sql_table FROM rendering and LIMIT/OFFSET ordering, and the driver-suite LIKE cases are skipped in fixtures/pinot.json.

The deleted versions had already been tightened to pin the whole predicate across four operators on both planners, which is the form that catches both the clause drifting and the negation landing back inside the call. Worth restoring those two.

Low — packages/cubejs-druid-driver/test/druid-driver.test.ts:148 (inline). Credentials re-hardcoded next to config.url; and LIKE_ROWS lost the backslash value, so the escape character itself — the third thing escapeWildcardChars escapes — is uncovered.

Housekeeping. The PR description's "How it was verified" and "The tests fail without the fix — they are the first and third commits" now describe tests that are no longer in the branch.

Not run

No node_modules in this checkout, so neither the Druid integration suite (needs Docker + a ~4-minute ingestion) nor the driver unit suites were executed. Review is static plus CI-wiring inspection.

· branch [`tesseract-like-pattern-escaping`](https://github.com/cube-js/cube/tree/tesseract-like-pattern-escaping)

// Dremio's `ILIKE(expr, pattern)` is a function and takes no escape argument,
// so case folding goes on LOWER(...) and matching on LIKE, which takes one.
// Dremio has no default escape character, so the clause is not optional.
likeIgnoreCase(column, not, param, type) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last commit (e285dad) deleted DremioQueryTemplates.test.ts and DruidQueryTemplates.test.ts and reverted the additions to PinotQueryTemplates.test.ts, so Dremio and Pinot now have no test covering anything this PR changes.

The rationale given — "Druid checks it by values against a real cluster" — holds for Druid, but Druid is the one dialect of the three that has an engine in CI. For the other two the shape test was the only guard, and shape is exactly what the fix consists of:

  • This line is a behavioural rewrite of the SQL emitted for every Dremio LIKE filter (ILIKE(col, …)LOWER(col) LIKE LOWER(…) ESCAPE '\') plus the negation-placement parse-error fix. Dremio has no compose file and is commented out of the integration matrix (push.yml:426), so nothing anywhere executes it. test/unit/params-escaping.test.ts asserts on a hand-written SQL string passed to applyParams — it never calls DremioQuery, so it would not notice this method regressing.
  • Pinot's tesseract.ilike clause is likewise unpinned: the surviving PinotQueryTemplates.test.ts only covers sql_table FROM rendering and LIMIT/OFFSET ordering, and the driver-suite LIKE cases are skipped in fixtures/pinot.json.

A shape assertion is a weak test when an engine is available and the only possible test when one is not. Suggest restoring the two dialects' cases (they were already tightened to pin the whole predicate across four operators on both planners, which is the form that catches the clause drifting and the negation landing back inside the call). Dropping Druid's shape file is fine now that the value test supersedes it.

Also worth updating the PR description — its "How it was verified" and "The tests fail without the fix — they are the first and third commits" sections now describe tests that are no longer in the branch.

Fix this →

method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${Buffer.from('admin:password1').toString('base64')}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the credentials are re-hardcoded here while config.user / config.password already hold them (and config.url is read on the line above). If the TEST_DRUID_HOST branch ever points at a cluster with different credentials, the driver queries in this file keep working and only the ingestion helper 401s — a confusing failure mode for a one-line fix.

Suggested change
Authorization: `Basic ${Buffer.from('admin:password1').toString('base64')}`,
Authorization: `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`,

Separately, LIKE_ROWS (line 16) lost the backslash value that an earlier revision carried. The escape character itself is one of the three things escapeWildcardChars escapes, and it is the case where a wrong ESCAPE character shows up as a missing row rather than as too many — adding 'a\\b' alongside an aXb decoy and a contains 'a\\b' case would cover the third character for the cost of one row.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data source driver driver:dremio driver:druid Issues relating to the Druid driver driver:pinot javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant