fix(pinot-driver,dremio-driver,druid-driver): interpret LIKE wildcard escaping - #11811
fix(pinot-driver,dremio-driver,druid-driver): interpret LIKE wildcard escaping#11811waralexrom wants to merge 8 commits into
Conversation
|
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
What I verified independently
Findings
Details are in the five inline comments. Comment volume, generallyBeyond the three flagged inline, each new test file opens with a 5-6 line prose preamble arguing the same point ( Not runI couldn't execute the new tests: this checkout has no |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Claude finished @waralexrom's task in 2m 37s —— View job Re-review after 3 new commits: all five prior findings addressed. 1 new medium —
|
| # | Finding | Status |
|---|---|---|
| 1 | Druid non-literal LIKE pattern unverified | Recorded — DruidQuery.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:9independently assertsLOWER(name) LIKE '%' || LOWER(?) || '%' ESCAPE '\'— same construct the rewrittenDremioFilter.likeIgnoreCasenow produces. Nothing anywhere pins the oldILIKE (col NOT, …). - Druid's new
unitscript is wired.tsconfig.jsonhasinclude: ["src", "test"],rootDir: ".", sotest/unit/*.tscompiles todist/test/unit;jestand@types/jestare already devDependencies andschema-compilera dependency.integration'sdist/test/*.jsglob really would have missed the directory. - The escape/clause pairing still holds on both paths.
BaseFilter.escapeWildcardChars(BaseFilter.ts:112) escapes unconditionally for every operatorisWildcardOperatorcovers, and the native side keys offlike_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.
| @@ -64,7 +67,9 @@ export class DruidQuery extends BaseQuery { | |||
| templates.expressions.timestamp_literal = 'TIME_PARSE(\'{{ value }}\')'; | |||
| delete templates.expressions.like_escape; | |||
There was a problem hiding this comment.
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 givex LIKE pattern [ESCAPE esc]), the deletion on line 68 is stale and is silently blocking SQL-API push-down ofLIKE ... 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.
…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>
036fb5d to
e285dad
Compare
|
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
|
| # | Finding | Status |
|---|---|---|
| 1 | Druid non-literal LIKE pattern unverified | Fixed properly — 3fe0ddd 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.
druidis in the integration matrix (push.yml:424) withuse_tesseract_sql_planner: true, and the job downloads thebackend-nativeartifact before install — souseNativeSqlPlanner: truereachesbuildSqlAndParamsRust(BaseQuery.js:918) rather than falling back. The file is picked up by the existingintegrationscript (jest dist/test/*.js), which is why theunitscript 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 thecontains '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/finallyaround a fresh driver per poll is correct. prepareCompiler(..., { adapter: 'druid' })is harmless —adapteris only declared on the options type inPrepareCompiler.ts:39and 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
likeIgnoreCaserewrite 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.tsasserts on a hand-written string passed toapplyParamsand never callsDremioQuery. Nothing executes or asserts this method. - Pinot's
tesseract.ilikeclause is likewise unpinned — the survivingPinotQueryTemplates.test.tscovers onlysql_tableFROM rendering and LIMIT/OFFSET ordering, and the driver-suite LIKE cases are skipped infixtures/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.
| // 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) { |
There was a problem hiding this comment.
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.tsasserts on a hand-written SQL string passed toapplyParams— it never callsDremioQuery, so it would not notice this method regressing. - Pinot's
tesseract.ilikeclause is likewise unpinned: the survivingPinotQueryTemplates.test.tsonly coverssql_tableFROM rendering and LIMIT/OFFSET ordering, and the driver-suite LIKE cases are skipped infixtures/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.
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Authorization: `Basic ${Buffer.from('admin:password1').toString('base64')}`, |
There was a problem hiding this comment.
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.
| 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.
Problem
A
contains/notContains/startsWith/endsWithfilter is supposed to match theuser's value literally: searching for
50%_offmust return the rows containing thatstring, not every row. That takes two cooperating pieces, and both have to come from the
dialect:
%,_and the escape character itself are escaped inside the value;backslash as its default LIKE escape character, or because the statement carries an
explicit
ESCAPEclause.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:
LOWER(x) LIKE CONCAT('%', LOWER(?), '%')— legacy already had the clausex ILIKE '%' || ? || '%'— an operator Dremio does not haveILIKE(x, CONCAT('%', ?, '%'))— the function takes no escape argumentLOWER(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 errorrather than a filter, for every
notContains,notStartsWithandnotEndsWith.Cause
The native filter path renders
tesseract.ilike, which is a separate template fromthe
expressions.like/expressions.ilikepair the SQL API push down uses. A dialectthat 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 clausealready named it; Dremio and Druid were never in that enumeration at all.
What changed
tesseract.ilike): carriesESCAPE '\', matching whatPinotFilter.likeIgnoreCasealready emits. It cannot go insidelike_patternbecausePinot's pattern is wrapped in
CONCAT(...).tesseract.ilike, new; andDremioFilter.likeIgnoreCase): the case foldingmoves from the
ILIKE(expr, pattern)function ontoLOWER(...) LIKE LOWER(...), sincethe function takes no escape argument and
LIKEdoes. That also puts the negation besidethe operator, fixing the parse error.
tesseract.ilikeandDruidFilter.likeIgnoreCase): both carry the clause.The changed SQL is what its dialect test pins, so that assertion moves with it.
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%_offerfor seven filter shapes. Everycase matches literally and the two planners agree:
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 ingestsrows and asserts exact result sets on both planners. That also settles the one question
the shape tests cannot answer: Druid does honour
ESCAPEon a non-literal pattern, butonly over a datasource - over an inline
SELECT 'x' AS nameit refuses the query withFunction[like] pattern argument must be a literal, which is why the case ingests insteadof selecting constants.
Undoing the clause turns 10 of those 12 cases red against the live cluster: rows expected,
Array []returned, andnotContains '%'returning every row instead of three. Theordinary-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). Dremiois 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:
cubejs-testing-driversRisks
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
sqlTemplatesdeletingexpressions.ilike(no infix operator) plus thedefault_escapegate onexpressions.like(no default escape character), and anexisting driver test that already assumes the
LOWER(...) LIKE ... ESCAPE '\'shape.parse for any negated operator, so neither can regress into a worse state.
LOWER(x) LIKE p ESCAPE '\'isthe same predicate it emitted before plus the clause, so the only behaviour that changes
is the one that was wrong.
Left open, deliberately
filters.like_patternbut inheritslike_escape_char, and itsLIKE/ILIKEaccept noESCAPEclause at all — so the injected backslashes are matchedas 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.
clause, while its native path carries one.
ILIKEon the native planner, which neitherengine has. Different defect class — the statement does not parse rather than quietly
returning the wrong rows — and not about escaping.
🤖 Generated with Claude Code