Skip to content

Filter AST: like is folded to $contains at the wire — wildcards bind as literals and driver-sql's like/ilike arm is unreachable #7536

Description

@huangyiirene

Symptom

A like predicate carrying SQL wildcards matches nothing, because the % is bound as a literal character:

filter node observed expected
["name","like","%Industries"] 200, 0 rows the rows whose name ENDS WITH Industries
["name","like","Industries"] 200, the 2 matching rows — byte-identical to the $contains control a LIKE 'Industries' (exact) match, not a substring match

The second line is the tell: like and $contains produce the same bytes, so like is not reaching the driver as LIKE at all — it is being lowered onto contains, which wraps the value in %…% and changes what the query means. A caller who writes their own wildcards therefore gets a silently wrong answer in both directions: with wildcards, zero rows; without them, a substring match they did not ask for.

This contradicts canonicalAstOperator's own comment in the same file:

like/ilike share the $contains lowering but are NOT substring matches at the driver: driver-sql passes them to SQL verbatim, so the caller binds the wildcards. Folding them onto contains would silently wrap the value in %…% and change what the query means.

Consequence: driver-sql's like/ilike bind arm has been unreachable from the wire since #5158 — dead code guarding a contract nothing can exercise over HTTP.

Root cause

Located in the report as the like$contains fold on the wire path. In tree (checked 2026-08-11), the fold is the AST_OPERATOR_MAP entry

'like': '$contains',

in packages/spec/src/data/filter.zod.ts, reached via astOperatorLowering() from the AST comparison converter (parseFilterAST / convertComparison) — the path a wire filter actually takes. canonicalAstOperator(), immediately below it in the same file, exempts like/ilike deliberately (that is the comment quoted above), but that exemption only shapes its own canonical-name output; the lowering the wire uses has no such exemption. Fix lands in packages/spec, with driver-side verification that the like/ilike bind arm is exercised again.

Reproduction

  1. Boot showcase on a fresh isolated file DB (SqlDriver / better-sqlite3); authenticate as admin@objectos.ai.
  2. POST /api/v1/data/showcase_account/query with the filter AST ["name","like","%Industries"]200, 0 rows.
  3. Same request with ["name","like","Industries"]200, the 2 matching rows.
  4. Control: the same value through $contains → byte-identical response to step 3.

Source

Extracted from the QA run #7463 (framework a86db17).

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions