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
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
- Boot showcase on a fresh isolated file DB (
SqlDriver / better-sqlite3); authenticate as admin@objectos.ai.
POST /api/v1/data/showcase_account/query with the filter AST ["name","like","%Industries"] → 200, 0 rows.
- Same request with
["name","like","Industries"] → 200, the 2 matching rows.
- Control: the same value through
$contains → byte-identical response to step 3.
Source
Extracted from the QA run #7463 (framework a86db17).
Symptom
A
likepredicate carrying SQL wildcards matches nothing, because the%is bound as a literal character:["name","like","%Industries"]200, 0 rowsnameENDS WITHIndustries["name","like","Industries"]200, the 2 matching rows — byte-identical to the$containscontrolLIKE 'Industries'(exact) match, not a substring matchThe second line is the tell:
likeand$containsproduce the same bytes, solikeis not reaching the driver asLIKEat all — it is being lowered ontocontains, 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:Consequence: driver-sql's
like/ilikebind 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→$containsfold on the wire path. In tree (checked 2026-08-11), the fold is theAST_OPERATOR_MAPentryin
packages/spec/src/data/filter.zod.ts, reached viaastOperatorLowering()from the AST comparison converter (parseFilterAST/convertComparison) — the path a wire filter actually takes.canonicalAstOperator(), immediately below it in the same file, exemptslike/ilikedeliberately (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 inpackages/spec, with driver-side verification that thelike/ilikebind arm is exercised again.Reproduction
SqlDriver/ better-sqlite3); authenticate asadmin@objectos.ai.POST /api/v1/data/showcase_account/querywith the filter AST["name","like","%Industries"]→200, 0 rows.["name","like","Industries"]→200, the 2 matching rows.$contains→ byte-identical response to step 3.Source
Extracted from the QA run #7463 (framework a86db17).