Skip to content

fix(spec, drivers)!: a $like / $ilike pattern holding U+0000 is refused by every driver that answers $like, instead of being cut at the NUL on SQLite (#20041) - #20124

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20041-like-nul-pattern-refused
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20041-like-nul-pattern-refused

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20041

Clause-②: yes (narrowing)

What changed

On the SQLite faces $like / $ilike compile to GLOB, and SQLite reads a pattern only up to its first U+0000. A pattern holding U+0000 was cut there and answered a different question, with nothing raised. There is no NUL-safe SQLite pattern primitive to compile to instead (LIKE cuts the same way, replace() cannot target U+0000, instr() has no wildcards; measured on #20024). So the accept set becomes one contract: such a pattern is refused, the way a pattern ending in a lone unpaired backslash already is.

  • packages/spec: one exported predicate, hasNulInLikePattern(pattern), beside hasDanglingLikeEscape in src/data/filter.zod.ts, with its own docblock. hasDanglingLikeEscape is byte-identical. The converters (likePatternToRegexSource, likePatternToGlobPattern, matchesLikePattern) are unchanged (see H3).
  • Every door that refused a dangling escape now asks the new predicate right after it, in that door's existing envelope, INVALID_FILTER / 400:
door file envelope
SqlDriver's filter walk (classifyFilterKey), before a dialect is chosen driver-sql/src/sql-driver.ts new builder nulLikePatternError, born in the withheld seam (withheldFilterError, the operator map as the node, as for the dangling escape)
RemoteTransport.buildWhereSQL's $like / $ilike arm, before anything is sent driver-turso/src/remote-transport.ts new method nulLikePattern, through withheldRefusal; its withheld sentence is driver-sql's behind the [RemoteTransport] prefix
driver-memory's shape gate (assertFieldConstraintShape: the query path and the reference matcher match()) driver-memory/src/filter-refusal.ts new exported nulLikePatternError, driver-sql's author text word for word
driver-memory's QueryAST comparison like / ilike arm, and the $like translator floor driver-memory/src/memory-driver.ts the same builder
  • The new check runs AFTER the dangling-escape check at every door, so a pattern with both keeps the refusal it already had.
  • Messages. The withheld class statement names neither the operator variant, the field, the path nor the pattern. The author text (and the server log) reads, for example: Operator "$like" on field "v" at filter.v.$like has a pattern holding the NUL character U+0000 ("%\u0000"). ... — the pattern goes through JSON.stringify, so U+0000 reaches no message as a raw byte.
  • Bounded in-place fix, declared: the $like operator's .describe() (LIKE_DESCRIPTION, same file) said "A pattern ending in a lone unpaired backslash is refused (INVALID_FILTER)". It now reads "A pattern ending in a lone unpaired backslash, or holding the NUL character U+0000, is refused (INVALID_FILTER)", so the declared contract names the refusal this PR enforces. content/docs/references/data/filter.mdx is its regeneration (gen:schema && gen:docs): 5 rows, that phrase only.
  • A pending release note corrected, which needs your confirmation: the last bullet of .changeset/20024-sqlite-glob-stored-nul.md (not yet released) said "a pattern holding U+0000 is still cut there". At this head that sentence is false, so it now says the pattern is refused by every driver that answers $like. node scripts/check-empty-changeset.mjs is RED on this by design: its DELIBERATE CORRECTION class says "do NOT restore it -- say so on the PR and get it confirmed". This is that statement. The alternative is to drop the edit and leave that false sentence in the release's changelog beside this PR's own entry.

H1: every face, base vs head (measured, the PM's hypothesis holds on the SQLite faces)

A probe (scratch, not committed) drove the public find of each face over one row set: 13 stored values, 12 non-NULL, U+0000 at the start, middle and end, alone, and none ('a' + U+0000 + 'b', 'ab' + U+0000, U+0000 + 'z', U+0000 alone, 'A' + U+0000 + 'B', plain, '', ab, a, b, axb, AB, NULL). Cases: 10 patterns holding U+0000 ('%'+NUL, NUL+'%', '%'+NUL+'%', 'a'+NUL+'b', 'a'+NUL+'%', '_'+NUL+'_', backslash+NUL, NUL alone, $ilike '%'+NUL+'B' and 'AB'+NUL) and 9 NUL-free controls, each bare and under $not. Base is 8d76c2d38c, head is this branch (dists rebuilt at each).

face U+0000 pattern, base U+0000 pattern, head NUL-free control, base = head
SqlDriver on better-sqlite3 20 of 20 differ from formula, 0 refused 20 of 20 refused, INVALID_FILTER / 400 0 of 18 differ on the NUL-free rows; 12 of 18 differ over rows holding a stored U+0000 (#20024 item 2 (ii), not this card)
SqliteWasmDriver 20 of 20 differ, 0 refused 20 of 20 refused same as above
TursoDriver local 20 of 20 differ, 0 refused 20 of 20 refused same as above
TursoDriver remote, makeLibsqlSqliteStub 20 of 20 differ, 0 refused 20 of 20 refused same as above
TursoDriver remote, real @libsql/client file::memory: 20 of 20 differ, 0 refused 20 of 20 refused same as above
InMemoryDriver.find ($-spelling) 0 of 20 differ (answers correctly), 0 refused 20 of 20 refused 0 of 18 differ
InMemoryDriver.find, QueryAST comparison like / ilike answers (like '%'+NUL gives the one value ending in U+0000) refused like 'ab%' answers the same rows
driver-memory match() answers (true for 'ab'+NUL) refused pinned equal to the query path
@objectstack/formula the oracle unchanged (see H2) the oracle
driver-mongodb translateFilter 38 of 38 cases refused: $like is not translated at all unchanged, not touched (held by draft PR #19947) refused
  • The five SQLite faces gave byte-identical answer lists on every case, at base and at head. The control answers are byte-identical base vs head on all six driver faces.
  • Examples at base, SQLite faces: $like: '%' + U+0000 returned all 12 non-NULL rows, where formula returns the two ending in U+0000; $like: 'a' + U+0000 + 'b' also returned 'a'; $like: '_' + U+0000 + '_' also returned 'a' and 'b'; $ilike: 'AB' + U+0000 also returned 'AB' and 'ab'; $not $like '%' + U+0000 returned only the NULL row.
  • Also measured at base, not touched: objectql applyHaving refuses every $like (INVALID_FILTER / 400, "Unsupported operator '$like' in having"); service-analytics compileScopedFilterToSql refuses it (READ_SCOPE_COMPILE_FAILED / 500, fail-closed) and normalizeAnalyticsFilterTree refuses it (INVALID_FILTER / 400). lowerAnalyticsWhere alone passes the node through; the tree build after it refuses.
  • The Postgres and MySQL arms of driver-sql were not measured live (no server here). At head the refusal fires on the walk before the dialect is chosen, pinned by compiling with the pg and mysql2 clients and no server.

H2: the doors (census)

git grep -n -E '\bNAME\(' HEAD -- 'packages/**/*.ts' ':!**/*.test.ts', comments and the definition excluded:

  • hasDanglingLikeEscape: 7 call sites at base and at head. Five are face doors: filter-refusal.ts (1), memory-driver.ts (2), sql-driver.ts (1), remote-transport.ts (1). Two are the converters' own backstops in filter.zod.ts. Positive control: the PM's list (sql-driver.ts, remote-transport.ts, memory-driver.ts x2, filter-refusal.ts) is exactly the five doors.
  • hasNulInLikePattern: 0 at base, 5 at head, one beside each of the five doors.
  • Every one of the five doors refused a dangling escape at base, and every one must refuse U+0000. formula does NOT refuse a dangling escape: matchesLikePattern throws, and the arm answers false (measured: matchesFilterCondition({ v: 'abc\\' }, { v: { $like: 'abc\\' } }) is false). So by the claim's condition its file is not touched, and it still evaluates a U+0000 pattern.

H3: where the predicate is called, option (a)

Converter consumer census, same grep over non-test sources:

  • likePatternToGlobPattern: 3 calls: sql-driver.ts (likePatternPredicate), remote-transport.ts (pushLikePattern), and driver-memory/src/memory-analytics.ts (globSubstringPattern, the analytics echo of a $contains comparand).
  • likePatternToRegexSource: 3 calls: memory-driver.ts (2) and the spec's own matchesLikePattern.
  • matchesLikePattern: 2 calls: driver-memory/src/memory-matcher.ts and formula/src/matches-filter.ts.
  • 8 calls in all: packages/drivers/** 6, packages/formula 1, packages/spec 1. packages/services/**, packages/objectql and packages/plugins/**: 0 (the same grep, whose drivers count is the positive control).

Option (b), a throw inside the converters, would have changed three consumers beyond this surface: memory-analytics' $contains echo would throw a plain Error on a comparand holding U+0000; formula's $like would answer false (its caught throw) instead of the right rows; the reference matcher the same (though its shape gate runs first). So the predicate is called at each door (a), and filter-like-nul-pattern.test.ts pins that the JS translation keeps its meaning.

H4: the withheld seam

  • nulLikePatternError has a row in sql-driver-compile-refusal-seam.test.ts and nulLikePattern in remote-transport-compile-refusal-seam.test.ts; without the row, "every builder that goes through the seam is driven by a row below" is red. The remote pin's local-vs-remote table gained the class too (the withheld sentence is one sentence on both compilers).
  • policy-marked: INVALID_FILTER / 400, the error's own keys exactly code,status, no field or pattern on the wire, both in the log / sink. 'author': the full text. Unmarked: byte-identical to policy. Merged $and: the refusing arm's mark decides, in both arm orders.
  • Through TursoDriver in remote mode no mark survives toRemoteFilter, so an author gets the class statement there (the fix(driver-sql, driver-turso): every filter-compile refusal goes through the withheld provenance seam, so a read scope refused there names no field or literal (#20039) #20093 fail-closed cost, pinned in turso-20041-like-nul-pattern.test.ts).

H5: declaration

  • Clause-②: yes (narrowing): the PR adds one public export, hasNulInLikePattern on @objectstack/spec/data (api-surface/data.json +1), so the value is yes (AGENTS.md's Clause-② rule, the PR fix(driver-turso)!: a remote TursoDriver answers or refuses every inherited SqlDriver member (#20055) #20104 precedent), and the arm is (narrowing) for the refused patterns. The claim carried no (narrowing); corrected in the patch round after contract review 5828387721. The changeset grades @objectstack/spec, driver-sql, driver-sqlite-wasm, driver-turso and driver-memory minor, with BREAKING and a ! title, following PR fix(driver-turso)!: refuse a remote url beside syncUrl, and any replica not on a local file, instead of running on :memory: #19971's changeset.
  • node scripts/check-changeset-no-major.mjs --base 8d76c2d38c: "✓ This diff introduces no major bump." (exit 0)
  • node scripts/check-adr-0087-registration.mjs --base 8d76c2d38c: "✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition." — [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription) (exit 0). The gate chose not-required: no key, schema, object definition or stored representation moves, and no rewrite of a stored pattern keeps its meaning.

Compile surfaces (references/compile-surfaces.md, re-verified with its grep: 92 hits in non-test sources)

# face this PR
1 driver-sql applyFilterCondition (sql-driver.ts:15953); driver-sqlite-wasm and Turso local by inheritance CHANGED: the walk refuses a U+0000 pattern on every dialect
2 Turso RemoteTransport.buildWhereSQL (remote-transport.ts:2668) CHANGED: the $like / $ilike arm refuses it
3 service-analytics compileScopedFilterToSql (read-scope-sql.ts:602) not touched (claim excludes it): refuses every $like already, measured
4 service-analytics lowerAnalyticsWhere (filter-normalizer.ts:2015) not touched: passes the node through; normalizeAnalyticsFilterTree refuses every $like, measured
5 formula matchesFilterCondition (matches-filter.ts:212) not touched: it refuses no dangling escape, so it is not one of the doors; it evaluates a U+0000 pattern correctly (the oracle above)
half objectql applyHaving / matchesHaving (having-filter.ts:279 / :292) not touched: refuses every $like, measured
thawed driver-memory checkCondition (memory-matcher.ts:361) and the query path CHANGED: the shape gate both run first refuses it; the QueryAST arm and the translator floor too
thawed driver-mongodb translateFieldOperators (mongodb-filter.ts:832) not touched (draft PR #19947 holds it): refuses every $like through its default: arm, measured through translateFilter

Tests

New pins, each asserting code, status and the path (never a bare toThrow()), plus NUL-free controls and the dangling escape beside U+0000:

  • packages/spec/src/data/filter-like-nul-pattern.test.ts (5): the predicate, the escaped U+0000, its independence from the dangling escape, and the converters unchanged.
  • driver-sql/src/sql-driver-20041-like-nul-pattern.test.ts (84): 9 patterns x bare / $not / $or / $and, unmarked (class only, path in the log, through find and count) and author-marked (operator, field, path); the dangling escape keeps its refusal also beside U+0000; 8 controls equal formula; sqlite, pg and mysql2 compiles refuse on the walk.
  • driver-sqlite-wasm/src/sqlite-wasm-20041-like-nul-pattern.test.ts (16), the same through sql.js, controls checked against formula.
  • driver-turso/src/turso-20041-like-nul-pattern.test.ts (17): local, remote over the stub and remote over a real libSQL engine; no FROM statement reaches the engine for a refused filter; 8 controls, equal on all three transports.
  • driver-memory/src/memory-20041-like-nul-pattern.test.ts (21): the query path, the QueryAST spelling and match(), with controls.
  • Consumer pins changed: the two seam enumerations above (one row each; the remote one also one local-vs-remote row). No other consumer pin reads a U+0000 $like: git grep over every *.test.ts that names $like / $ilike and U+0000 finds none outside this PR.

Suites (vitest run --maxWorkers=2), at 5f2e6f246d (code equal to this head but for a comment in memory-driver.ts), dists rebuilt:

  • driver-sql: 186 files passed, 11 skipped; 3024 tests passed, 170 skipped.
  • driver-turso: 69 files, 1629 tests passed.
  • driver-sqlite-wasm: 33 files, 622 tests passed.
  • driver-memory: 53 files, 1269 tests passed (re-run at 049b3a6c95, same).
  • formula: 35 files, 978 tests passed.
  • spec: 570 files, 16369 tests passed, 2 todo.
  • plugin-auth (consumer): 114 files, 2440 tests passed.
  • typecheck exits 0 in spec, driver-sql, driver-sqlite-wasm, driver-turso, driver-memory. tsc --listFiles counts each new driver test once in its package program; the spec test once in tsconfig.test.json, which check:test-typecheck compiles.

Before-red and ablations (one-off, no file left behind)

All from committed state, through scripts/ablation-replace.mjs (anchor 1 -> 0 on disk, blob changed), restored with the blob equal to HEAD, git diff HEAD empty and git status --porcelain empty.

  1. Every driver's predicate replaced by a never-true local (the four source files at once, which is base behaviour at every door). driver-sql rebuilt and ablation-dist-preflight found the marker in dist/ before the run. Predicted and observed exactly:
    • sql-driver-20041: 75 failed, 9 passed (the controls, the dangling escape, and nothing else green);
    • sql-driver-compile-refusal-seam: 4 failed, 91 passed (the new row);
    • sqlite-wasm-20041: 10 failed, 6 passed; turso-20041: 8 failed, 9 passed; remote-transport-compile-refusal-seam: 6 failed, 96 passed; memory-20041: 15 failed, 6 passed;
    • the dangling-escape suites sql-driver-like-pattern (22) and memory-like-pattern (15) stayed green.
      Restored, rebuilt, and ablation-dist-preflight --absent passed with the tree clean.
  2. The new builder bypassing the seam (nulLikePatternError calling unsupportedFilterError, nulLikePattern calling invalidFilterError). Predicted and observed: driver-sql 78 failed of 179 (both enumeration assertions, the row's 4, and the 72 path / log cases of the new suite); remote seam pin 6 failed of 102.

Gates (at 049b3a6c95)

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the real diff (17 paths, merge base 8d76c2d38c): 110 commands, the PM's 79 plus 31 the docs, driver-memory and changeset paths add. All 110 run after a full turbo run build --filter='./packages/*' --filter='./packages/*/*' (72 tasks); --ran: "110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN".
  • 109 exit 0. One exits 1, on purpose: node scripts/check-empty-changeset.mjs --base origin/main, the DELIBERATE CORRECTION of .changeset/20024-sqlite-glob-stored-nul.md above.
  • Roster families beside these paths, run by hand: check-changeset-fixed, check:meta-url-spelling, check:spec-changes, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:engine-double-contract, check:error-status-conformance: all exit 0.
  • pnpm --filter @objectstack/spec check:generated: all 15 artifacts up to date after gen:api-surface, gen:export-origins, gen:schema and gen:docs (api-surface/data.json and export-origins/data.json gain hasNulInLikePattern; authorable-surface.base.json untouched).
  • pnpm check:driver-conformance: "50 covered cell(s), 0 in the DEBT ledger, 0 exempt" at base and at head.
  • node scripts/check-issue-citations.mjs --base 8d76c2d38c: 17 citations across 5 files, all resolve.
  • pnpm check:nul-bytes passes, and a control-byte scan of the changed files finds none. U+0000 is spelled String.fromCharCode(0x00) in every file.
  • Re-run at the merged head b88ca46d90 (merge base 6780e34af5, 17 paths, after a full turbo run build): the derivation gives the same 110 commands; 109 exit 0 and check-empty-changeset exits 1 on the driver-sql / driver-turso (SQLite faces): GLOB still cuts a STORED value at its first U+0000, so a NUL-free $contains / $notContains / $icontains / $endsWith answers wrongly on it; a $like / $ilike pattern holding U+0000 is cut the same way #20024 correction; --ran: "110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN". check-changeset-no-major and check-adr-0087-registration exit 0; check:generated all 15 up to date.
  • ESLint, narrowed: --no-inline-config --format json over the 12 changed .ts files reports 12 files, 0 errors, 0 warnings. eslint.config.mjs enables no type-aware linting (the printed parserOptions for sql-driver.ts are {"ecmaVersion":"latest","sourceType":"module"}), so this diff cannot move the verdict of a file it does not touch. The full pnpm lint is CI's.

Acceptance notes

… every face that refuses a dangling escape

One spec predicate, hasNulInLikePattern, beside hasDanglingLikeEscape, asked
at the driver-sql walk (through the withheld seam), the Turso RemoteTransport
$like arm (through its seam), and driver-memory's shape gate and both of its
translator floors.

Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
…dialect, and enumerate its seam door

Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
… $like refusal on every face and door

Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
…; changeset and generated artifacts

The regenerated api-surface / export-origins shards carry hasNulInLikePattern,
and content/docs/references/data/filter.mdx the one-sentence describe change.
The pending #20024 changeset's last bullet said a pattern holding U+0000 is
still cut on SQLite; it is refused now, so that clause is corrected.

Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
…river-sql's emitter shares

Claude-Session: https://claude.ai/code/session_01Bvd69VPa6puiNzzPUroDBx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

11 anchor(s) derived from 4 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bc80e16260597d52b062b7e1ee810c7b9a99aed2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from aa0e12f57e7910818b253a5b988d1e6e29edfa33 — the merge of head b88ca46d901e0b7d509b89e716b6a89a885d359b into base bc80e16260597d52b062b7e1ee810c7b9a99aed2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin aa0e12f57e7910818b253a5b988d1e6e29edfa33 && git checkout aa0e12f57e7910818b253a5b988d1e6e29edfa33
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bc80e16260597d52b062b7e1ee810c7b9a99aed2 b88ca46d901e0b7d509b89e716b6a89a885d359b && git checkout -B drift-repro bc80e16260597d52b062b7e1ee810c7b9a99aed2 && git merge --no-ff b88ca46d901e0b7d509b89e716b6a89a885d359b

node scripts/docs-audit/affected-docs.mjs --json bc80e16260597d52b062b7e1ee810c7b9a99aed2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 049b3a6c95fa9a8b4743d7d0496c5f0349ead6a7

Scope: 17 files (+1017/−16) on merge base 8d76c2d38c:

  • packages/spec/src/data/filter.zod.ts: one new export plus the LIKE_DESCRIPTION edit, with its three generated artifacts;
  • four driver doors, five new pins and two seam-pin rows;
  • .changeset/20041-like-nul-pattern-refused.md, and one rewritten bullet of the pending .changeset/20024-sqlite-glob-stored-nul.md.

Read: card #20041 and its comments, #20024, PR #20038 and its record, #20039, PR #20093 and its record 5826632400, PR #20104 and its record 5827309177 (the Clause-② precedent), the PR body, the diff, all 35 check-runs, AGENTS.md, contract-review.md, landing-operations.md, clause2-line.mjs, check-empty-changeset.mjs and check-adr-0087-registration.mjs.

Measured in detached worktrees at head and at base, plus a third worktree of origin/main merged with the head.

① Derived judgments

  • Every face, base → head: RIGHT. Each face got 10 U+0000 patterns across positions and marks (80 cells) and 9 NUL-free controls (36 cells), with @objectstack/formula as the oracle.
    • At base, the five SQLite faces (SqlDriver, SqliteWasmDriver, TursoDriver local, remote over the stub, remote over a real libSQL client) differed from formula in 20/20 bare and $not cells and refused none. Their answers were identical across all five.
    • InMemoryDriver.find, its QueryAST like/ilike arm and match() answered correctly at base.
    • At head, all eight driver faces refuse 80/80 with INVALID_FILTER / 400, own keys code,status. The two remote faces send 0 FROM statements, and the pg / mysql2 compile pins refuse on the walk before a dialect.
    • The controls are byte-identical at base and head, 36/36 on every face.
    • A dangling escape keeps its own refusal beside U+0000.
  • The spec predicate: RIGHT. hasNulInLikePattern sits directly after hasDanglingLikeEscape, which is byte-identical. The converters are unchanged.
  • The public surface gains exactly one addition: hasNulInLikePattern on @objectstack/spec/data (api-surface/data.json +1). The driver-side builders are module-private or private.
  • The withheld seam: RIGHT.
  • The ablations reproduce the PR's numbers exactly, each restored by blob:
    • A, the predicate never true: sql-20041 75 failed / 9 passed, the sql seam 4 / 91, wasm 10 / 6, turso 8 / 9, the remote seam 6 / 96, memory 15 / 6. The dangling-escape suites stay green.
    • B, the builders bypassing the seam: driver-sql 78 failed of 179, including both enumeration assertions; the remote seam 6 failed of 102.
  • Census and generated files: RIGHT. hasNulInLikePattern( has 0 call sites at base and 5 at head. check:generated reports all 15 up to date.
  • Pins at head: spec 5/5, driver-sql 201/201, driver-turso 119/119, wasm 16/16, memory 36/36. On the tree merged with current main, the turso pins pass 179/179.
  • CI at this head: 35 runs, 32 success, 2 skipped, 1 failure, and all seven required contexts are success. The one failure is Check Changeset, on the foreign-changeset rule for .changeset/20024-sqlite-glob-stored-nul.md: exactly the declared correction.

② Semver level

  • minor for @objectstack/spec and the four drivers, with BREAKING and a ! title: CONSISTENT with an accept-set narrowing under the launch-window convention.
  • Clause-②: no (narrowing) is a FALSE declaration. It must read Clause-②: yes (narrowing).
  • ADR-0087 not-required (no-migration-prescription): CORRECT. Nothing an author can write is removed or renamed. The narrowing is of pattern values, and the refusal text and the changeset carry the one-line fix.

③ Boundary flags

  • DELIBERATE CORRECTION confirmed for this head (.changeset/20024-sqlite-glob-stored-nul.md, the last bullet of "What does not change"):
    • Base sentence, "… and a pattern holding U+0000 is still cut there": its final clause is FALSE at head. 80/80 U+0000 pattern cells are refused on every SQLite face before any statement is sent.
    • Head sentence 1, "$like and $ilike still compile to GLOB on SQLite, so they still read a stored value only up to its first U+0000": TRUE. The emitters are untouched, and the stored-NUL control cells still differ from formula.
    • Head sentence 2, "A pattern holding U+0000 is no longer cut there: every driver that answers $like now refuses it (INVALID_FILTER / 400), by its own entry in this release": TRUE.
    • The Check Changeset red is this correction and nothing else. The confirmation binds to 049b3a6c95; a new head needs its own at-tier record, which repeats it.
  • Changeset 20041-like-nul-pattern-refused.md: every measured sentence is TRUE except the line Clause-②: no (narrowing) (see ②).
  • PR body: TRUE except two sentences.
  • The dev's out-of-scope findings are REAL and correctly left. query-syntax.mdx names only the dangling escape (incomplete, not false), and so does skills/objectstack-query/rules/filters.md.

Implemented-by: claude/issue-20041-like-nul-pattern-refused
Reviewed-by: session_01Bvd69VPa6puiNzzPUroDBx

VERDICT: FAIL

Must-change (text only; every measured behaviour is RIGHT):

  1. Clause-②: no (narrowing) → Clause-②: yes (narrowing) in the PR body (the header and H5) and in .changeset/20041-like-nul-pattern-refused.md. Keep minor, BREAKING, the ! title and the ADR-0087 marker.
  2. Correct the Acceptance note on main: name the commits and PR fix(driver-turso): a remote $between that is not two bounds is refused INVALID_FILTER / 400 through the transport's withheld seam, naming no field or value (#20094) #20114's two driver-turso paths, with the merged-tree reading, or drop the sentence.
  3. The new head needs its own at-tier record, which repeats the DELIBERATE CORRECTION confirmation, before the Check Changeset red may ride in.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b88ca46d901e0b7d509b89e716b6a89a885d359b

Scope: the delta over FAIL 5828387721 (049b3a6c95), plus the patched PR body and the seat's claim amendment 5828886919. Two commits:

  • 2b8dd90200, a merge of origin/main 6780e34af5;
  • b88ca46d90, one line of .changeset/20041-like-nul-pattern-refused.md: Clause-②: no (narrowing) → yes (narrowing).

Measured in a detached worktree at b88ca46d90, with the driver closures rebuilt.

① Derived judgments

  • Must-change 1 (Clause-②): MET.
    • The changeset and the PR body (the header and H5) read yes (narrowing). minor, BREAKING, the ! title and the ADR-0087 marker are unchanged.
    • check-changeset-no-major --base 6780e34af5 --event <PR payload>: "✓ This diff introduces no major bump." and "✓ LEVEL AXIS: this PR declares clause-② yes (narrowing) …" (exit 0).
    • check-adr-0087-registration --base 6780e34af5: "✓ … 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition." (exit 0).
  • Must-change 2 (the Acceptance note on main): MET, every clause TRUE.
  • The merge is behaviour-neutral for this PR. On the merged head, @objectstack/formula is the oracle:
  • The public surface change is still exactly one export, hasNulInLikePattern on @objectstack/spec/data.
  • CI at this head: 42 runs, 36 success, 4 skipped, 2 failure, 0 pending. All seven required contexts are success.

② Semver level

  • minor for the five packages, with BREAKING and the ! title: CONSISTENT.
  • Clause-②: yes (narrowing): CORRECT, in the changeset, the PR body and the amended claim.
  • ADR-0087 not-required (no-migration-prescription): CORRECT, and unchanged.

③ Boundary flags

  • DELIBERATE CORRECTION confirmed on b88ca46d90 (.changeset/20024-sqlite-glob-stored-nul.md, the last bullet of "What does not change"), re-measured on this head:
  • Every other changed PR-body sentence is TRUE, including the new Gates bullet (110 derived commands, reproduced).
  • Changeset 20041-like-nul-pattern-refused.md: every measured sentence is TRUE. Record 5828387721's only objection is gone.

Implemented-by: claude/issue-20041-like-nul-pattern-refused
Reviewed-by: session_01Bvd69VPa6puiNzzPUroDBx

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 08:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 8a44ce7 Sep 25, 2026
42 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20041-like-nul-pattern-refused branch September 25, 2026 08:34
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…le stored value past a U+0000 (objectstack-ai#20024) (objectstack-ai#20140)

Fixes objectstack-ai#20024

Clause-②: no

Item 2 (ii) of objectstack-ai#20024, direction C (landing record 5823785768). On the
SQLite faces, a `$like` / `$ilike` pattern without U+0000 over a stored
value holding U+0000 now gets the answer `likePatternToRegexSource`
gives, which is also what `@objectstack/formula` and `driver-memory`
return. Item 1 landed as PR objectstack-ai#20038 and item 2 (i) as PR objectstack-ai#20124 (objectstack-ai#20041).
This is the card's last item, so the PR closes it.

`patch` for `@objectstack/driver-sql`, `@objectstack/driver-sqlite-wasm`
(it inherits the fix) and `@objectstack/driver-turso`. Nothing that was
accepted is refused now: the refusal doors, `hasNulInLikePattern` and
`hasDanglingLikeEscape` are untouched. The grid, plan and cost numbers
below were taken at `b424b90f7c` or earlier. Head `89e02a2521` differs
from `b424b90f7c` by one changeset line only, and its emitter code is
byte-identical to `7434e6646e`; every commit since then changed
changeset prose only.

## The defect, measured at base `fe677aeeed`

`glob()` reads the stored value as a C string, only up to its first
U+0000. The grid has 59 stored values (27 holding U+0000, leading,
interior, trailing, doubled, next to multi-byte and U+0001 characters,
plus NULL). It runs 130 queries against them: 81 `$like` and 27 `$ilike`
bare patterns, and 22 `$not` / `$or` / `$and` compositions. That is 7670
cells per face, each compared with `formula`. The four faces answered
identically:

| stored value | cells per face | base: differ from `formula` | head:
differ from `formula` |
|---|---|---|---|
| holds U+0000 (26 values in the Basic Multilingual Plane) | 3380 | 359
| **0** |
| no U+0000, in the Basic Multilingual Plane (28 values) | 3640 | 0 | 0
|
| NULL | 130 | 0 | 0 |
| outside the Basic Multilingual Plane, no U+0000 (3 values) | 390 | 20
| 20 (older than this PR, see H3) |
| outside the Basic Multilingual Plane, with U+0000 (1 value) | 130 | 6
(6 against a code-point oracle) | 2 (0 against a code-point oracle) |

The four faces are `SqlDriver` on better-sqlite3 (SQLite 3.53.4),
`SqliteWasmDriver` on sql.js (3.49.1), `TursoDriver` local, and
`TursoDriver` remote over a real `@libsql/client` `file::memory:` engine
(3.45.1). At base, 74 of the 108 bare patterns got at least one row
holding U+0000 wrong. Some examples:

- `$like: 'a'` returned `'a'` + U+0000 + `'b'`;
- `$like: ''` returned U+0000 + `'z'`;
- `$like: '_'` missed a lone U+0000;
- `$ilike: 'A_B'` missed `'a'` + U+0000 + `'b'`.

**Base vs head, every stored value without U+0000 and NULL: 0 of 16640
cells moved, on any face.**

## What changes

`likePatternPredicate`'s SQLite arm in `sql-driver.ts` now calls
`sqliteLikePatternMatch`. `RemoteTransport.pushLikePattern` in
`remote-transport.ts` restates the same rule for the remote transport.
The PR's own parity test holds the two to the same predicate text and
bindings, the precedent `pushLike` already follows. Sharing one compiler
would need a new export from `driver-sql`'s `index.ts`, which is outside
this claim's file surface. How a pattern compiles:

- **A literal prefix followed only by `%`** (`'ab%'`, `'%'`, `'%%'`)
keeps the bare `col GLOB ?` with the same binding as before. A prefix
free of U+0000 cannot be changed by the cut, the same argument
`$startsWith` stands on.
- **Every other pattern** compiles to `CASE WHEN instr(CAST(col AS
BLOB), X'00') > 0 THEN (a recursive CTE that replaces each U+0000 with
one stand-in character, then GLOB) ELSE col GLOB ? END`.
- A value without U+0000 takes the `ELSE` arm: the same `GLOB` as
before.
- NULL also takes the `ELSE` arm and stays NULL, so the NULL-safe `$not`
wrapper composes unchanged.
- **A case-exact pattern with a literal prefix** leads with `col GLOB
'PREFIX*'`, where PREFIX is the text before the first unescaped
wildcard. Every value the pattern matches satisfies that conjunct, cut
or not, and it keeps the index range. `$ilike` has no index range to
keep, since `lower(col)` is not an indexed column, so it gets no
conjunct.

**Why the rewrite is exact.** The stand-in is the smallest code point
from U+0001 up that is not a literal character of the pattern, not an
ASCII letter and not a surrogate. The pattern holds no U+0000, because
`hasNulInLikePattern` refuses it at every door. So a U+0000 in the value
can only be matched by `%` or `_`. The stand-in is one character, as
U+0000 is, and no literal position of the pattern matches it, just as
none matched U+0000. Because it is not a letter, the `$ilike` fold
cannot turn it into one. So the rewritten value matches exactly when the
original does.

**H2 is measured, not assumed.** On all three engines, `replace(v,
char(0), char(1))` returns its input unchanged, `GLOB` and `LIKE` cut at
U+0000, `lower()` keeps the bytes past U+0000, and `instr()` over BLOB
finds U+0000. A correlated recursive CTE inside a scalar subquery works
on all three and splices every U+0000, with NULL staying NULL. The
earlier sentinel measurement (223 of 2392 cells wrong) is consistent
with this: `replace()` never reached a U+0000 there.

## H3: characters outside the Basic Multilingual Plane under `_` (older
than this PR, not widened)

The spec's regex has no `u` flag, so `_` in `formula` and
`driver-memory` matches one UTF-16 unit, while `GLOB`'s `?` matches one
code point. At base and at head alike, 20 of 390 cells over such values
without U+0000 differ from `formula` on every SQLite face (17 queries).
For example, `$like: '_'` returns a stored '😀' on SQLite and not in
`formula`. This PR's U+0000 arm uses `GLOB` too, so it follows the
code-point reading, consistent with the arm for values without U+0000.
On the astral value holding U+0000, 0 of 130 head cells differ from a
code-point oracle (the spec's regex compiled with the `u` flag). The
divergence is reported to the seat as an out-of-scope finding and not
fixed here.

## H4: plans and cost

`EXPLAIN QUERY PLAN` of the statements the real compilers emit (the
`SqlDriver` compile, and the remote statement captured from a recording
libSQL client) over `plan_probe(id integer primary key, v text)` with an
index on `v`. Identical on better-sqlite3, sql.js and libSQL:

| filter | before | after |
|---|---|---|
| `$like 'ab%'` | SEARCH USING COVERING INDEX (v greater-than ? AND v
less-than ?) | same, same statement |
| `$like 'ab_'`, `'ab%cd'`, `'abc'`, `'a%b%'` | SEARCH USING COVERING
INDEX (range) | the same SEARCH, plus a correlated scalar subquery |
| `$like '%ab'`, `'_b'`, `''` | SCAN | SCAN, plus a correlated scalar
subquery |
| `$like '%'`, `$ilike 'ab%'` | SCAN | SCAN, same statement |
| `$ilike 'ab_'` | SCAN | SCAN, plus a correlated scalar subquery |
| `$eq`, `$startsWith` (controls) | SEARCH | unchanged |

Cost is the median of 7 runs of `count(*)` over 10,000 rows, base `GLOB`
against head. This is a shared box, so read the ratios:

| rows holding U+0000 | `$like '%ab%c'` | `$like '_b%'` | `$like 'ab_%'`
| `$like 'ab%'` | `$ilike '%AB%C'` |
|---|---|---|---|---|---|
| 0% | 1.4–1.8x | 1.7–2.4x | 1.1–2.2x | 1.0x | 1.3–1.4x |
| 10% | 4.2–4.7x | 5.4–7.8x | 3.3–3.7x | 0.9–1.0x | 2.4–2.8x |
| 100% (1–3 each) | 25–37x (23–34 ms) | 26–44x (25–31 ms) | 23–33x (5–7
ms) | 0.7–1.1x | 12–14x (25–35 ms) |

The cost for values without U+0000 is the per-row `instr()` guard. A
text-`instr` spelling of it is also correct on all three engines, but it
measured slower than the BLOB one. Every head count equals `formula`'s
count, and base counts differ wherever U+0000 rows exist.

## H5: the remote face, executed

`TursoDriver` remote over a real `@libsql/client` engine runs the whole
grid in the probe (0 of 3380 cells differ at head) and the whole pin
suite in CI. Its predicate text and bindings equal the local compiler's,
modulo identifier quoting. A real Turso server, and its wire encoding of
a string holding U+0000, are NOT MEASURED.

## H6: two pending changesets, two DELIBERATE CORRECTIONS (please
confirm)

This PR makes one sentence false in each of two pending changesets, and
it corrects each sentence in place. One line changes in each file, and
every other line of both files is byte-identical.

- `Check Changeset` goes RED on both by design.
`scripts/check-empty-changeset.mjs`'s foreign-changeset rule is
content-blind: any `M` row on a changeset present at the merge base is
refused, whichever card owns it.
- Both are the **DELIBERATE CORRECTION** class. Its remedy is to confirm
the correction here. ⛔ Restoring either file would put a false sentence
back.

1. `.changeset/20024-sqlite-glob-stored-nul.md` is this card's own item
1 entry. The last bullet changed (line 29 of 29):
- Before: "- `$like` and `$ilike` still compile to `GLOB` on SQLite, so
they still read a stored value only up to its first U+0000. A pattern
holding U+0000 is no longer cut there: every driver that answers `$like`
now refuses it (`INVALID_FILTER` / 400), by its own entry in this
release."
- After: "- `$like` and `$ilike` are outside this entry. Two other
entries in this release cover them: on SQLite they now read the whole
stored value as well, and every driver that answers `$like` refuses a
pattern holding U+0000 (`INVALID_FILTER` / 400)."
2. `.changeset/20041-like-nul-pattern-refused.md` is objectstack-ai#20041's entry. The
first bullet under **Not changed here** changed (line 36 of 44). The
seat's claim amendment 5830697185 on objectstack-ai#20024 (ruling A) added this bullet
to this PR's surface.
- Before: "- A pattern without U+0000 matched against a STORED value
that holds U+0000 still differs from `formula` on the SQLite faces,
because `GLOB` also reads the stored value only up to its first U+0000.
No refusal of the pattern can reach that half."
- After: "- A pattern without U+0000 matched against a STORED value that
holds U+0000 is not refused: it is well formed, and on the SQLite faces
it reads the whole stored value, by its own entry in this release."
   - The new sentence is true at head `89e02a2521`:
- The refusal doors read only the pattern (a non-string pattern, a
dangling escape, or a pattern holding U+0000).
- The three pin suites pass there: 65 of 65, 22 of 22 and 23 of 23. They
include patterns without U+0000 over stored values holding U+0000 on
every SQLite face, and every such case returns `formula`'s rows and none
is refused.
     - "Its own entry" is `.changeset/20024-like-stored-nul.md`.

The new entry is `.changeset/20024-like-stored-nul.md`.

## Tests (at `7434e6646e`; the code is identical at head)

- New suites:
- `sql-driver-20024-like-stored-nul.test.ts`: 65 of 65 pass. It has 29
literal cases, each pinned and checked against `formula`, a 67-pattern
grid held to `formula` cell by cell, the exact compiled statements, and
an `EXPLAIN QUERY PLAN` pin.
- `sqlite-wasm-20024-like-stored-nul.test.ts`: 22 of 22 pass (19 literal
cases, the grid, a plan pin, and the stored-bytes premise).
- `turso-20024-like-stored-nul.test.ts`: 23 of 23 pass. It runs local,
remote over the stub, and remote over a real libSQL engine, plus the
grid and the emitter-parity case. `driver-turso` does not depend on
`formula`, so its grid is held to the spec's `matchesLikePattern`.
- Full suites:
- `pnpm --filter @objectstack/driver-sql exec vitest run
--maxWorkers=2`: 187 files passed and 11 skipped; 3089 tests passed and
170 skipped.
  - `@objectstack/driver-sqlite-wasm`: 34 of 34 files, 644 of 644 tests.
  - `@objectstack/driver-turso`: 70 of 70 files, 1686 of 1686 tests.
- No existing pin moved: before the new suites were added, all three
full suites were green against the new emitters.
- `pnpm --filter … run typecheck` exits 0 for all three packages, and
`tsc --listFiles` counts each new suite once.
- Ablations, each with a prediction made from the base statement or a
simulation, a mutation proved on disk through
`scripts/ablation-replace.mjs`, a `driver-sql` rebuild, and
`ablation-dist-preflight` (marker present, then `--absent` after
restore). Each restore was proved as the blob hash equal to HEAD, with
`git status --porcelain` empty.
- (1) Dispatch reverted in both emitters, which gives the base
statement. Predicted and observed: driver-sql 25 failed / 40 passed,
sqlite-wasm 15 / 7, turso 16 / 7.
- (2) Stand-in fixed at U+0001 in both emitters. Predicted and observed:
4 / 61, 2 / 20, 2 / 21. The failures are the U+0001 cases, the grid, and
the stand-in statement pin.
- (3) Remote emitter only. Predicted and observed: turso 17 failed / 6
passed, including the parity test.

## Gates (the full union at `b424b90f7c`; the changeset gates re-run at
`89e02a2521`)

At `89e02a2521`, the head that adds the second correction:

- `node scripts/check-empty-changeset.mjs --base origin/main` exits 1.
This is expected and names both files: "This PR changes a changeset it
did not add: .changeset/20024-sqlite-glob-stored-nul.md present on the
merge base and CHANGED by this PR ...
.changeset/20041-like-nul-pattern-refused.md present on the merge base
and CHANGED by this PR". Both are the H6 DELIBERATE CORRECTIONS.
- `node scripts/check-changeset-no-major.mjs --base origin/main
--event`, over this body, exits 0: "This diff introduces no `major`
bump.", and "LEVEL AXIS: this PR declares clause-② `no`, so no package
here is declared to have grown a published surface" (declaration line
`Clause-②: no`, no direction arm).
- `node scripts/check-adr-0087-registration.mjs --base origin/main`
exits 0: "check-adr-0087-registration: this PR adds no declared-breaking
changeset (3 non-breaking changeset(s) seen)."
- `node scripts/check-issue-citations.mjs --base fe677ae` exits 0: 7
citations across 2 files resolve.

At `b424b90f7c`:

- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 62 commands. All 62 were run and
reconciled with `--ran`: 62 derived, 62 run, 0 NOT MEASURED, 0 UNRUN.
- Every command exits 0 except `node scripts/check-empty-changeset.mjs
--base origin/main`, which exits 1 for the H6 correction above.
- `check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:type-check-debt` first exited 3 (PREREQUISITE NOT MET). They exit
0 after `turbo run build` over `./packages/*` and `./packages/*/*` (72
tasks). The dist-reading gates were re-run over that build and are green
too.
- `check:driver-conformance`: 50 covered cells, 0 DEBT, 0 exempt.
- `node scripts/check-issue-citations.mjs --base fe677ae`: 7
citations resolve.
- The rosters beside these paths are green: `check-changeset-fixed`,
`check:authz-resolver`, `check:error-code-casing`,
`check:filter-alias-parity`.
- ESLint, narrowed: `eslint --no-inline-config --format json` over the 5
changed `.ts` files gives 5 files, 0 errors, 0 warnings, 0 ignored.
- The population is every changed `.ts` file, each resolving a config
under `--print-config`.
- `eslint.config.mjs` never enables type-aware linting, so no untouched
file's verdict can move. The full `pnpm lint` run is CI's.

## Compile surfaces

1. `driver-sql` `likePatternPredicate`, SQLite arm: TOUCHED. Measured on
better-sqlite3 3.53.4. `SqliteWasmDriver` (sql.js 3.49.1) and
`TursoDriver` local inherit it, and each is measured by the grid and by
its own suite. The Postgres and MySQL arms are not touched and NOT
MEASURED (no live server here).
2. turso `RemoteTransport.pushLikePattern`: TOUCHED. Measured on
`makeLibsqlSqliteStub` and on a real `@libsql/client` engine (3.45.1). A
real Turso server is NOT MEASURED.
3. and 4. service-analytics `compileScopedFilterToSql` /
`lowerAnalyticsWhere`: not touched. `$like` / `$ilike` are in their
unevaluated set (`preview-unevaluable-operator.test.ts`), so these faces
do not compile them. Rows are NOT MEASURED.
5. `formula` `matchesFilterCondition`: not touched; it is the oracle.
- Half face: objectql `having-filter`: not touched, and `$like` does not
occur in `having-filter.ts`.
- Thawed faces:
- `driver-memory` `checkCondition`: not touched. Measured on the same
grid: 0 of 7670 cells differ from `formula`.
  - `driver-mongodb`: not touched, NOT MEASURED (no mongod).

## Acceptance notes

- H3's divergence outside the Basic Multilingual Plane is reported to
the seat with its measured reach, and is not fixed here.
- The objectstack-ai#20041 changeset: its line 32 ("every `$like` / `$ilike` pattern
without U+0000 answers exactly as before") is scoped to objectstack-ai#20041's own
change and is left as it is. The corrected line 36, in the same section,
states what this PR changes for stored values.
- Draft PR objectstack-ai#20104 edits `RemoteTransport` elsewhere in
`remote-transport.ts`. This PR's hunk is inside `pushLikePattern` only.

Written by `session_01Bvd69VPa6puiNzzPUroDBx` (os-dev, `mode:subagent`)
for the `domain:engine` seat.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

2 participants