Skip to content

docs(spec,drivers,service-analytics): CAST(col AS BLOB) LIKE returning nothing is a compile-option behaviour, not a SQLite fact - #16113

Merged
os-warren merged 6 commits into
mainfrom
claude/issue-15805-cast-blob-compile-option
Sep 6, 2026
Merged

docs(spec,drivers,service-analytics): CAST(col AS BLOB) LIKE returning nothing is a compile-option behaviour, not a SQLite fact#16113
os-warren merged 6 commits into
mainfrom
claude/issue-15805-cast-blob-compile-option

Conversation

@os-warren

@os-warren os-warren commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15805
Fixes #16112

Docs precision only across four files. No behaviour changes, no exported surface moves, and no conclusion is weakened — all four sites still reject CAST(col AS BLOB) LIKE ? and all four still choose GLOB. What changes is the supporting premise, which was stated as a universal fact about SQLite and is not one.

#16112 was filed by this seat mid-round, after a grep showed the sentence had four carriers rather than the two #15805 names. It is absorbed here rather than landed separately, on the PM's ruling: applying #15805's own rule to the corrected population gives four, which is the same rule rather than a widened scope.

The false sentence

Four headers rejected the construct on the strength of one claim — that it "was measured to return NOTHING". Whether LIKE is false for a BLOB operand is not a property of SQLite the language: it is fixed when SQLite is compiled, by SQLITE_LIKE_DOESNT_MATCH_BLOBS. The two SQLite builds this repo ships disagree about it.

Measurement — re-measured by this seat, not carried from the card

Both builds are already dependencies. { name: { $contains: 'acme' } } over the shared FILTER_TEXT_ROWS fixture, compiled as CAST(name AS BLOB) LIKE ?:

build pragma_compile_options result
better-sqlite3 13.0.3 (SQLite 3.53.4) LIKE_DOESNT_MATCH_BLOBS present []
sql.js 1.14.1 (SQLite 3.49.1) absent ['1','2']ACME Corp AND acme corp

Two further readings, now carried in all four headers so the remedy's stability is measured rather than asserted:

  • typeof CAST(name AS BLOB) is 'blob' on both builds — the CAST is not the part that differs; LIKE's blob rule is the compile-time half.
  • name GLOB '*acme*' answers ['2'] on both builds — stable across exactly the axis that disqualifies the rejected construct.

Why this makes the argument stronger

The original premise was a universal claim about return values, which a single counter-example overturns — and this repo ships the counter-example. The replacement needs no particular return value: a construct that means two different things on the two SQLite builds this repo ships — silently answering nothing on one and silently answering exactly the ASCII over-fold the change was made to end on the other — is disqualifying for a read scope (#3948) on its own, because the deciding flag is upstream of us.

The four carriers, and their provenance

git log -S on a non-shallow clone dates the spread to one origin:

31728314c  2026-08-08  #6518 / #6706  planted it in sql-driver.ts AND
                                      remote-transport.ts, in the SAME commit
d063a969d  2026-08-11  #7536 / #7593  copied it into packages/spec
54bb2f125  2026-09-05  #15790         copied it into service-analytics

One origin, four carriers, four weeks.

file note
packages/drivers/driver-sql/src/sql-driver.ts the #6518 header at 2867-2868 — located and verified here, not carried from a note. Where the sentence originated.
packages/drivers/driver-turso/src/remote-transport.ts planted by the same commit. Sharpened for this face: a remote transport cannot pin the build its libSQL server was compiled from, so the flag is not merely upstream, it is across the wire.
packages/spec/src/data/filter.zod.ts the contract layer, and the worst carrier — a reader trusts it because of where it sits. Its own next sentence already lists three SQLite backends without noting they are not the same build; the two measured rows ARE two of the three.
packages/services/service-analytics/src/text-match-sql.ts inherited the sentence most recently.

Confined to those four comment blocks — no code, no signatures, no tests touched.

Changeset — the two-site verdict was re-decided on the widened set, and reversed

This PR originally carried skip-changeset. That was correct while the change was confined to a module-private function and a file header. It does not survive adding packages/spec and driver-turso, so a changeset is included and the label has been removed (confirmed by API read-back; the labels now are documentation, tests, tooling, protocol:data).

  • likePatternToGlobPattern in filter.zod.ts is exported, so its TSDoc reaches published declarations — measured in packages/spec/dist/filter.zod-DGYUwZPS.d.ts and .d.mts.
  • driver-turso ships it in dist/index.d.ts, dist/index.d.mts, and the emitted dist/index.js / dist/index.mjs.
  • @objectstack/spec also publishes the edited source file itself, via its files entry src/**/*.zod.ts.

⇒ Two packages ship changed declarations, which is a release. For driver-sql and service-analytics the change reaches published output only through sourcemaps; they are listed because all four are in one fixed version group.

Leaving the label on would have been worse than a wrong label: it exempts the changeset-check job wholesale, so the changeset gates would have reported skipped — unmeasured, not green.

Does the spec docblock reach a generated reference page? No — established, not assumed. pnpm --filter @objectstack/spec run check:docs exits 0 with the edit in place, reporting 227 generated files in sync with packages/spec, and check:generated reports All 15 generated artifacts are up to date. Affirmatively: gen:docs was run twice, exit 0 both times, with git status --porcelain empty after each — zero files rewritten, so the fixpoint holds and nothing generated was hand-edited.

Verification

Gate family re-derived from the widened change set — ⛔ the earlier 42 was not reused:

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
  -> exit 0; merge base 0ef4f8094; Reconciliation — 71 famil(ies)

The count moved 42 (two sites) to 65 (adding packages/spec) to 71 once the changeset existed. Unless noted, figures ran on 990b3d48d, the pushed head:

what command result
all 71 derived families each from dispatch-gates.mjs --commands exit 0 — 71 of 71, on 990b3d48d
closure build (prerequisite) os-verify-lock.sh -c 'pnpm build' VERDICT command-exit 0, 72/72 tasks
typecheck, 4 packages pnpm --filter ... typecheck exit 0 on 990b3d48d (each echoed its tsc --noEmit)
tests, 4 packages under the verify lock VERDICT command-exit 0, measured on ecea92c17
control bytes grep -naP over all 5 changed files no matches

Test totals: spec 482 files / 12950 tests passed; driver-sql 155 passed + 9 skipped / 2389 passed + 140 skipped; driver-turso 44 files / 1168 tests passed; service-analytics 93 files / 2013 tests passed. The driver-sql skips are pre-existing declared MySQL/Postgres cells — unmeasured, not green, and unchanged here. Those suites ran on ecea92c17; the only change since is a merge of main, whose incoming files (packages/cli, packages/drivers/driver-memory) touch none of the four packages — verified with git diff --name-only.

Six roster-based families whose rosters sit under directories these paths are in — where the derivation warns its own silence is evidence in neither direction — were run explicitly rather than read as clear: check-changeset-fixed, spec check:meta-url-spelling, spec check:spec-changes, check:authz-resolver, check:error-code-casing, check:filter-alias-parity. All exit 0.

Every exit code was captured immediately after a single redirected command, ⛔ never through a pipe. One earlier batch of spec-check readings was discarded rather than reported: a malformed substitution turned the command into a pipeline, so $? was reporting tr's status; those checks were re-run cleanly and are the numbers above.

Branch note

A container restart interrupted this round, and the branch had diverged because this seat rebased onto a newer main after pushing — leaving d53445224 on the remote and its rebased twin locally. Reconciled by merging the pushed commit rather than force-pushing (force-push is barred here, and the PR already pointed at it). Verified before merging that nothing was lost: text-match-sql.ts was byte-identical between the two, and sql-driver.ts differed only by this round's additive typeof paragraph.

That merge left the PR comparing against a stale base, so GitHub briefly attributed 11 files from #16035 to this PR. Merging current main in corrected it; the comparison now lists exactly the five files above. Both pushes were fast-forwards (d53445224..ecea92c17, ecea92c17..990b3d48d).

⛔ This section reports only what was measured locally. It makes no claim about this PR's CI state, which this seat has not read.


Generated by Claude Code

…hing is a compile-option behaviour, not a SQLite fact

Both headers rejected `CAST(col AS BLOB) LIKE ?` as a portable case-exact
construct on the strength of one universal claim: that it "was measured to
return NOTHING at all" on SQLite. That claim is false as stated. Whether LIKE
is false for a BLOB operand is fixed when SQLite is COMPILED, by
SQLITE_LIKE_DOESNT_MATCH_BLOBS, and the two SQLite builds this repo ships
disagree about it.

Measured here over the shared FILTER_TEXT_ROWS fixture, with
{ name: { $contains: 'acme' } } compiled to that construct:

  better-sqlite3 13.0.3 (SQLite 3.53.4, flag compiled in) -> []
  sql.js         1.14.1 (SQLite 3.49.1, flag absent)      -> ['1','2']

Neither file's conclusion changes: both still reject the construct, and the
rejection is now stronger rather than merely hedged. A construct that means
two different things on the two builds this repo ships is disqualifying for a
read scope on its own, without needing any particular return value at all --
one build silently answers nothing, the other silently answers exactly the
ASCII over-fold the change was made to end. GLOB, which both files chose
instead, answers ['2'] on both builds.

Comment text only: no behaviour change, no exported surface, and no emitted
JS or .d.ts difference (the driver-side block documents a module-private
function; the analytics one is a file header).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@github-actions github-actions Bot added the size/s label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 4 changed package(s); no hand-written page names any of them. ⚠️ 3 changed file(s) yielded no anchor (packages/drivers/driver-sql/src/sql-driver.ts, packages/services/service-analytics/src/text-match-sql.ts, packages/spec/src/data/filter.zod.ts), 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
  • 3 changed file(s) yielded no anchor (packages/drivers/driver-sql/src/sql-driver.ts, packages/services/service-analytics/src/text-match-sql.ts, packages/spec/src/data/filter.zod.ts) — pages documenting those are invisible to this run
  • 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.

Coarse fallback — 134 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 932acc3df41e016812cfc17ffe1527fd7b5df538packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2a3d93f2eab8c5d8b11e542600d7497d1d8951bd — the merge of head 990b3d48d926f84339ab8120dc39abb0be68e646 into base 932acc3df41e016812cfc17ffe1527fd7b5df538, 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 2a3d93f2eab8c5d8b11e542600d7497d1d8951bd && git checkout 2a3d93f2eab8c5d8b11e542600d7497d1d8951bd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 932acc3df41e016812cfc17ffe1527fd7b5df538 990b3d48d926f84339ab8120dc39abb0be68e646 && git checkout -B drift-repro 932acc3df41e016812cfc17ffe1527fd7b5df538 && git merge --no-ff 990b3d48d926f84339ab8120dc39abb0be68e646

node scripts/docs-audit/affected-docs.mjs --json 932acc3df41e016812cfc17ffe1527fd7b5df538

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

@os-warren os-warren added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/s labels Sep 6, 2026 — with Claude
…hing is a compile-option behaviour, not a SQLite fact

Both headers rejected `CAST(col AS BLOB) LIKE ?` as a portable case-exact
construct on the strength of one universal claim: that it "was measured to
return NOTHING at all" on SQLite. That claim is false as stated. Whether LIKE
is false for a BLOB operand is fixed when SQLite is COMPILED, by
SQLITE_LIKE_DOESNT_MATCH_BLOBS, and the two SQLite builds this repo ships
disagree about it.

Measured here over the shared FILTER_TEXT_ROWS fixture, with
{ name: { $contains: 'acme' } } compiled to that construct:

  better-sqlite3 13.0.3 (SQLite 3.53.4, flag compiled in) -> []
  sql.js         1.14.1 (SQLite 3.49.1, flag absent)      -> ['1','2']

Neither file's conclusion changes: both still reject the construct, and the
rejection is now stronger rather than merely hedged. A construct that means
two different things on the two builds this repo ships is disqualifying for a
read scope on its own, without needing any particular return value at all --
one build silently answers nothing, the other silently answers exactly the
ASCII over-fold the change was made to end. GLOB, which both files chose
instead, answers ['2'] on both builds.

Comment text only: no behaviour change, no exported surface, and no emitted
JS or .d.ts difference (the driver-side block documents a module-private
function; the analytics one is a file header).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
… two remaining carriers

The false sentence had FOUR carriers in the tree, not the two originally
identified. `git log -S` dates them to one origin:

  3172831  2026-08-08  planted it in sql-driver.ts AND remote-transport.ts
                         in the same commit
  d063a96  2026-08-11  copied it into packages/spec/src/data/filter.zod.ts
  54bb2f1  2026-09-05  copied it into service-analytics/text-match-sql.ts

One origin, four carriers, four weeks. Correcting only some of them is worse
than correcting none: a reader trusts the uncorrected copy precisely because
of where it sits, and packages/spec is the contract layer -- it outranks the
driver layer as an authority.

Both remaining headers now name SQLITE_LIKE_DOESNT_MATCH_BLOBS, carry the two
shipped builds' divergent readings, and let the divergence carry the
rejection rather than any claim about return values. Neither conclusion is
weakened: both still reject the construct and choose GLOB.

The spec header is where this bites hardest, and it already said so without
knowing it: three of the five backends it lists are SQLite underneath, and
those are NOT the same build -- driver-sql runs better-sqlite3 (flag compiled
in) and driver-sqlite-wasm runs sql.js (flag absent). The two measured rows
ARE two of the three it enumerates.

Two readings added to all four headers so the remedy's stability is measured
rather than asserted:

  typeof CAST(name AS BLOB) is 'blob' on BOTH builds -- the CAST is not the
    part that differs; LIKE's blob rule is the compile-time half
  GLOB answers ['2'] on BOTH builds -- stable across exactly the axis that
    disqualifies the rejected construct

Comment text only; no behaviour, no exported surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…form

The branch diverged when this seat rebased onto a newer main AFTER pushing:
d534452 (pushed, based on 64011dd) and da0dae9 (its rebased twin, based
on a4816a7) carry the same two-site correction with different shas.

Merging rather than force-pushing: force-push is barred on this branch, and
the PR already points at d534452. Verified before merging that nothing is
lost -- text-match-sql.ts is byte-identical between the two, and sql-driver.ts
differs only by this round's additive typeof CAST(name AS BLOB) paragraph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

# Conflicts:
#	packages/drivers/driver-sql/src/sql-driver.ts
…blished typings, so it is not skip-changeset

Measured on the built tree rather than assumed: the corrected text reaches
packages/spec/dist/filter.zod-*.d.ts and .d.mts, and driver-turso's
dist/index.d.ts, index.d.mts, index.js and index.mjs. @objectstack/spec also
publishes the edited source file itself through its src/**/*.zod.ts files
entry. Two packages therefore ship changed declarations, which is a release,
so the earlier skip-changeset judgement -- correct while the change was
confined to a module-private function and a file header -- does not survive
the widened set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@os-warren os-warren removed size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Sep 6, 2026 — with Claude
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 6, 2026
@os-warren os-warren changed the title docs(drivers,service-analytics): CAST(col AS BLOB) LIKE returning nothing is a compile-option behaviour, not a SQLite fact docs(spec,drivers,service-analytics): CAST(col AS BLOB) LIKE returning nothing is a compile-option behaviour, not a SQLite fact Sep 6, 2026
@os-warren os-warren added documentation Improvements or additions to documentation tests tooling protocol:data and removed documentation Improvements or additions to documentation tests tooling protocol:data labels Sep 6, 2026 — with Claude
Keeps the PR's diff computable against a base that is an ancestor of this
head, so the comparison shows only this card's five files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@github-actions github-actions Bot added the size/s label Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

PM 验收 · 独立复核了三项,全部成立。⭐ 其中一项推翻了我给的口径,而它是对的。

复核的三项(本地 git 读数,⛔ 未采信报告转述)

① 四个载体全部改到位

$ git grep -c "SQLITE_LIKE_DOESNT_MATCH_BLOBS" 990b3d48d -- 'packages/**/*.ts'
driver-sql/src/sql-driver.ts            2
driver-turso/src/remote-transport.ts    1
service-analytics/src/text-match-sql.ts 1
spec/src/data/filter.zod.ts             2

② 那句普遍断言在全树归零。 measured to match NOTHING 命中 0。(全树尚存的 matched NOTHING 全部是无关语境 —— glob、findOne、operator map,与 CAST/BLOB 无关。)

③ 变更集恰好 5 个文件,+86/-12:四个载体 + 一个 changeset。

⚠️ 这一项我用的是本地 merge-base 比较(git diff --stat $(git merge-base HEAD origin/main) HEAD),⛔ 不是 GitHub 的文件列表 —— 正因为你报告里那条平台读数:一条合并了旧 main 的分支会被 GitHub 拿过期 base 去比,本 PR 一度把 #16035 的 11 个文件算到自己头上。⇒ 本地 merge-base 读数对这个陷阱免疫,这条以后是本席位核对「这张 PR 改了哪些文件」的默认方法。

⭐ 你推翻了我的口径,而且理由比我的强

我裁定纳入 spec 时,只要求你重新判定 skip-changeset,没有预判结论。你判定它并改掉了 —— likePatternToGlobPattern 是导出的,其 TSDoc 进入 spec.d.ts/.d.mts,driver-turso 连 emitted JS 一起带,而 spec 本身还经 src/**/*.zod.ts 直接发布源文件。

⭐ 但真正让这条值得记下来的是第二层理由:

留着它比贴错标签更糟 —— skip-changeset整体豁免 changeset-check,那些门会报 skipped,而 skipped 是未测量,不是绿

⇒ 一个标签可以把一整族门从「绿」变成「看不见」。⭐ 这是本仓那条规则此前没有覆盖到的形态,已收进本席位的常备口径。

changeset 的分级也不是默认出来的:四个包各自 patch,并逐包写明哪两个进入已发布的类型声明、哪两个只经 sourcemap 到达 —— 这正是「判定并陈述理由」该有的样子。

⭐ 你主动作废的那批读数,是本轮最该表扬的一件事

一次拼接失误把 spec 检查循环变成了管道,于是那些 EXITtr 的状态,不是门的状态 —— 正是那个假绿管道陷阱。整批读数作废重跑,报告里只留重跑的数。

⛔ 没有报「都过了」,也没有悄悄修掉不提。⇒ 本席位对这一批不作任何采信,也不需要 —— 你已经把它们排除在证据之外了。

未复核的部分(⛔ 按你的报告记,不冒领)

71 个门族全 0(42 → 65 → 71 的变动过程)、gen:docs 跑两次且 git status 皆空的不动点证明、四个包的套件与 typecheck。⚠️ driver-sql 的 skip 是既有的 MySQL/PG 声明格,未测量,不是绿 —— 你标注正确。

状态

本 PR 现在 Fixes #15805 + Fixes #16112,两张卡随落地一并关闭。⚠️ #16112 已置 pm:dispatched 以防他席重复路由。

⛔ 保持 draft、未 arm:按维护者裁决,等总监契约复审,CI 绿本身不是门槛。

domain:services PM 席位 · 三项本地复核,一项口径被 dev 正确推翻


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 6, 2026 03:46
@os-warren
os-warren enabled auto-merge September 6, 2026 03:46
@os-warren
os-warren added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit d5d8d50 Sep 6, 2026
39 checks passed
@os-warren
os-warren deleted the claude/issue-15805-cast-blob-compile-option branch September 6, 2026 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment