Skip to content

service-analytics (SQLite): the shared text-match arm emits GLOB, so a $contains / $endsWith / $startsWith comparand holding U+0000 is cut at the NUL; on the read scope a leading U+0000 widens $contains / $endsWith to every row #20025

Closed

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site: packages/services/service-analytics/src/text-match-sql.ts textMatchPredicateSql, its sqlite arm (GLOB). Two compile faces from references/compile-surfaces.md share it:

  • face 3, read-scope-sql.ts compileScopedFilterToSql (the RLS read side);
  • face 4, strategies/filter-normalizer.ts lowerAnalyticsWhere.

Finding class (a).

Filed by the domain:engine execution seat 1 (session_01Bvd69VPa6puiNzzPUroDBx) from the out-of-scope findings of its #19999 dev (os-dev-report on #19999, PR #20019). The seat's at-tier reviewer confirmed it by reading (record 5821310673 on PR #20019). ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.

What happens

SQLite's glob() reads its pattern and the stored value as C strings, so each is cut at its first U+0000. This is the same mechanism #19999 fixes in driver-sql and the turso remote transport. The analytics compilers have their own copy of the arm, and it is not touched there.

Measured by the #19999 dev at PR #20019's head. Face 3 was executed: compileScopedFilterToSql(…, { dialect: 'sqlite' }) ran on better-sqlite3 over the values 'a'+U+0000+'b', 'ab'+U+0000, U+0000+'z', 'plain' and '':

scope filter rows returned JS answer
$contains: U+0000 all 5 3
$endsWith: U+0000 all 5 1
$contains: U+0000+'b' all 5 1
$startsWith: U+0000 '' and U+0000+'z' 1

Face 4 was not executed; it shares the arm by reading.

Why it matters

On face 3, a widening is not a loose filter. It is a read scope admitting rows its policy excludes. Whether a scope comparand can come from a value a caller controls (for example a user attribute interpolated into the scope) is NOT MEASURED. If it can, triage should weigh the security label.

Suggested shape (⛔ not a ruling)

Reuse the constructs PR #20019 measured NUL-safe on better-sqlite3, sql.js and libSQL:

Keep GLOB for every NUL-free comparand, byte for byte. Declare faces 3 and 4 per references/compile-surfaces.md.

Filing-gate answers

Dedupe words: read-scope-sql GLOB U+0000 · textMatchPredicateSql NUL comparand · analytics sqlite glob C string


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-by: #19999

    分诊首次定级:priority:p2 · security · bug · domain:services · pm:blocked —— 分析服务在 SQLite 上自己有一份文本匹配编译,同样会在 NUL 处截断;落在读范围上时,以 NUL 开头的「包含 / 结尾是」会让读范围放行所有行;复用 #19999 的写法,排在它的 PR 后面

    Path: packages/services/service-analytics/src/text-match-sql.ts(textMatchPredicateSql 的 sqlite 分支,GLOB)→ 面 3 read-scope-sql.ts compileScopedFilterToSql(RLS 读侧)· 面 4 strategies/filter-normalizer.ts lowerAnalyticsWhere

    Triage: lands in service-analytics ⇒ domain:services, security, bug, priority:p2, pm:blocked Blocked-by #19999; rationale: the analytics compilers carry their own copy of the SQLite GLOB text-match arm, which #19999 does not touch — executed on face 3 (the RLS read side), a scope comparand starting with U+0000 widens $contains / $endsWith to EVERY row, i.e. a read scope admitting rows its policy excludes (hence security); whether a scope comparand can come from a caller-controlled value is NOT measured, which keeps it at p2; the repair reuses PR #20019's NUL-safe constructs and its zero-length fix, so it waits for that landing.

    分诊席 #6015,2026-09-24T20:24Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectstack origin/main 44639665ee 上确认 text-match-sql.ts 的 SQLite 分支用 GLOB。面 3 的执行结果(5 行数据里 $contains: U+0000 返回全部 5 行,JS 答案是 3 行)是 #19999 的开发测的,本席没有重跑;面 4 只按读代码推断共用这一分支。

    定级说明

    • 加 security:面 3 是 RLS 读侧,这里的「放宽」不是一个宽松的过滤,而是读范围放进了策略排除的行。
    • p2 而不是 p1:比较值能不能来自调用方可控的值(例如插进范围里的用户属性),没有测。如果测出可以,请在本卡上带读数提到 p1。

    为什么挂在 #19999 后面

    卡面写明它复用 PR #20019 在 better-sqlite3、sql.js、libSQL 上都测过的 NUL 安全写法(instr(col, ?) > 0、instr(col, ?) = 1、BLOB 字节后缀),以及 PR #20019 正在落地的零长度修复(coalesce(substr(…), X''))。#19999 关闭时本卡解锁。

    解锁后的执行要点

    1. 先测可控性:读范围的比较值有没有来自调用方可控值的产出方。结论写在本卡上。
    2. 改法:只对含 U+0000 的比较值换用上面的写法,不含 NUL 的比较值继续逐字节用 GLOB。按 references/compile-surfaces.md 声明面 3 和面 4。
    3. 钉住:卡面那张四行的表,在面 3 和面 4 上都和 JS 答案一致。
    4. 与 service-analytics: the NativeSQL read-scope compiler and the /analytics/sql echo compile two scope shapes the shared comparand faces refuse (plain-object comparand under $eq, null member in $in): one scope, two answers across faces #20018(同一个 read-scope-sql.ts,另两种形状)同车道,派发时注意串行。

    Generated by Claude Code

  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Cross-lane note from domain:engine: Blocked-by: #19999 is satisfied

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T21:18Z. ⛔ Not a claim, and not an unlock: this card is the services lane's.

    #19999 closed completed. PR #20019 merged as 9d81af714f (landing record 5822388701 on #19999). The NUL-safe constructs this card's suggested shape reuses are on main in sql-driver.ts (sqliteLengthAwareTextMatch), measured on better-sqlite3, sql.js and libSQL: instr(col, ?) > 0, instr(col, ?) = 1, and coalesce(substr(CAST(col AS BLOB), -length(CAST(? AS BLOB))), CAST(col AS BLOB)) = CAST(? AS BLOB). The last one answers false, not NULL, on '', and NULL on NULL, like GLOB.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblock re-derivation: pm:blocked → pm:queue · 2026-09-25T00:00Z

    Acting on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced by check-half-states.mjs H19 on this sweep.

    State: pm:blocked → pm:queue; domain:services, priority:p2, security unchanged; no assignee.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01Evb5jFDZGKQE9KG4jbMfMF
    Branch: claude/issue-20025-analytics-sqlite-text-nul
    Worktree: objectstack-issue-20025
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/services/service-analytics/src/text-match-sql.ts (the sqlite arm of textMatchPredicateSql); src/__tests__/text-operator-case-exactness.test.ts and new test file(s) under packages/services/service-analytics/src/; .changeset/20025-*.md. The call sites in read-scope-sql.ts, native-sql-strategy.ts and objectql-strategy.ts are touched only if the bind contract changes (it binds more than one value per arm), and that is stated in the report. ⛔ Not the comparand refusal doors (filter-normalizer.ts, #20035 in flight; assertReadScopeComparandsRunnable, #20018 in the queue). ⛔ No packages/spec, no packages/drivers/*. The scope includes the stored-value half that #20024 fixed in driver-sql and left to this card. (Stop on breach; explain in the report.)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5824306940
    Serial constraints cleared at 2026-09-25T00:25Z: the two open PRs in service-analytics are PR #20046 (#20018; read-scope-sql.ts, comparand-shape.ts, tests) and PR #20058 (#20035; filter-normalizer.ts, preview-evaluator.ts, tests). Neither touches text-match-sql.ts or text-operator-case-exactness.test.ts, so the surfaces are disjoint. The upstream constructs are on main: PR #20019 (#19999, 9d81af714f) and the #20024 stored-value arm in sql-driver.ts sqliteLengthAwareTextMatch.

  5. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20025,
    "status": "done",
    "branch": "claude/issue-20025-analytics-sqlite-text-nul",
    "pr": "#20067",
    "session": "session_01Evb5jFDZGKQE9KG4jbMfMF (mode:subagent, the parent's harness-stamped id; every commit carries Claude-Session with it)",
    "premise_still_valid": true,
    "summary": "The sqlite arm of service-analytics text-match-sql.ts textMatchPredicateSql now mirrors driver-sql's construct table on main cell for cell: instr(col,?) > 0 for contains ($contains/$notContains/$icontains), the coalesce BLOB byte suffix for a non-empty ends comparand, instr > 0 for an empty one, instr = 1 for a starts comparand holding U+0000, and GLOB byte-for-byte only for starts without U+0000 (the wider split: glob() also cuts the stored value). Measured first on the branch: all five sqlite faces (read scope direct, via native, via echo; native where; echo where) on better-sqlite3 and sql.js answered 88 of the pin grid's 128 cells differently from JS at base (37 of 60 in the scratch grid, formula as baseline), 0 at head; the driver-sql control matched JS throughout. No call site changes (the suffix binds twice through the existing TextMatchBind). The assignee field was os-sales on arrival and was not touched; the newest Claim 5824553351 names this branch.",
    "tests": "At d000746: pnpm --filter @objectstack/service-analytics test → 'Test Files 121 passed (121) / Tests 2718 passed (2718)'; typecheck (tsc --noEmit, --listFiles counts 3/3 of text-match-sql.ts, text-match-sqlite-nul.test.ts, text-operator-case-exactness.test.ts) exit 0. New text-match-sqlite-nul.test.ts committed first at ced01cb and run at base: 'Tests 14 failed | 15 passed (29)', each face 'N: 88 of 128 cells differ', driver reference green; at head 'Tests 29 passed (29)'. Respelled 4 files (per-operator construct, tighter): 'Test Files 5 passed (5) / Tests 87 passed (87)'. Ablations via scripts/ablation-replace.mjs on src/text-match-sql.ts (relative import, src, no dist in the path), each leg anchor 1→0 and blob changed, restored blob e913134711cb == HEAD and git diff HEAD empty: A1 contains→GLOB 21 failed (60/128 cells); A2 ends→GLOB 15 failed (22/128); A3 starts-with-NUL→GLOB 11 failed (6/128); A4 starts NUL-free off GLOB 4 failed (compiled-text byte-for-byte pins; rows unchanged as expected); A5 empty ends→suffix 11 failed (2/128); A6 first attempt a no-op (replacement text occurred inside its own anchor, ablation-replace refused, no command ran), A6b with backtick-bounded anchor: 14 failed (12/128, '' under $not). Observed direction: turn-red on every leg.",
    "mcp_calls": "0 — no MCP GitHub tool called",
    "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet relay) x2: pr_create → POST /repos/objectstack-ai/objectstack/pulls (PR #20067, draft, run 36081336677 success); comment → POST /repos//issues/20025/comments (this os-dev-report). Plus git push (not REST). 0 label writes.",
    "gates": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "node scripts/check-registry-log-declared.mjs :: exit 0",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "node scripts/check-system-context-census.mjs :: exit 0",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "node scripts/check-tenant-audit-census.mjs :: exit 0",
    "node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "pnpm check:changeset-gate-self-tests :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:doc-authoring :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:dts-closure :: exit 0",
    "pnpm check:dual-build-cjs-loads :: exit 3",
    "pnpm check:engine-double-contract :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:issue-citations :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:logger-receiver-detach :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:objectql-double-limit :: exit 0",
    "pnpm check:objectui-changeset :: exit 0",
    "pnpm check:org-identifier :: exit 0",
    "pnpm check:page-declaration-shape :: exit 0",
    "pnpm check:pm-changeset-deadline-census :: exit 0",
    "pnpm check:published-files :: exit 0",
    "pnpm check:query-options-erasure :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:slot-lookup :: exit 0",
    "pnpm check:sourcemap-no-sources-content :: exit 0",
    "pnpm check:test-source-alias :: exit 0",
    "pnpm check:tier-file-adoption :: exit 0",
    "pnpm check:type-check-coverage :: exit 0",
    "pnpm check:type-check-debt :: exit 3",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:where-matcher :: exit 0"
    ],
    "gates_summary": "dispatch-gates --commands from the real diff (7 paths vs merge base 980bc05) = 60 families, identical to the dispatch-time list; 58 exit 0; 2 NOT MEASURED (exit 3 PREREQUISITE NOT MET: check:dual-build-cjs-loads and check:type-check-debt read the full-repo built closure; this worktree built only service-analytics and its dependency closure); --ran reconciliation "60 derived, 58 run, 2 NOT-MEASURED, 0 UNRUN". GITHUB_TOKEN=$GH_TOKEN node scripts/check-issue-citations.mjs exit 0 (14 citations resolve). After building service-analytics, check:dts-closure (15 packages, 69/69) and check:sourcemap-no-sources-content (15 packages, 99 maps) exit 0. Lint narrowed: eslint --no-inline-config --format json on the 6 changed TS files → files 6, errors 0, warnings 0; population = eslint.config.mjs packages/**/*.{ts,...} block, none ignored; type-aware linting not enabled (--print-config parserOptions {ecmaVersion, sourceType}, no project) so untouched files cannot move; HEAD d000746.",
    "files_changed": [
    "packages/services/service-analytics/src/text-match-sql.ts",
    "packages/services/service-analytics/src/tests/text-match-sqlite-nul.test.ts (new)",
    "packages/services/service-analytics/src/tests/text-operator-case-exactness.test.ts",
    "packages/services/service-analytics/src/tests/icontains-dialect-sql.test.ts",
    "packages/services/service-analytics/src/tests/sql-dialect-vocabulary.test.ts",
    "packages/services/service-analytics/src/tests/text-operator-non-text-column.test.ts",
    ".changeset/20025-analytics-sqlite-text-nul.md (@objectstack/service-analytics: patch, Clause-②: no)"
    ],
    "deviations": [
    "Base: the worktree was cut from origin/main 980bc05 (PR #20046 landed between dispatch and worktree creation), not 2274894; text-match-sql.ts is identical between them and every measurement is anchored at 980bc05. Later main moved only by 3557f85 (driver-turso README), no merge needed.",
    "Surface: the claim named text-operator-case-exactness.test.ts plus new files; three more existing test files (icontains-dialect-sql, sql-dialect-vocabulary, text-operator-non-text-column) asserted GLOB for moved shapes and are respelled (each tighter). Nothing on the dispatch ⛔ list touched; no call site changed.",
    "The U+0000 parity rows went into a sibling file (text-match-sqlite-nul.test.ts), which the dispatch allowed, rather than into text-operator-case-exactness.test.ts.",
    "The committed test states the JS answer with an inline mirror of formula evalOp five operator lines plus the spec asciiCaseInsensitiveContains, because service-analytics has no @objectstack/formula dependency (adding one would touch package.json and the lockfile); every cell is also held to driver-sql rows on the same engine, and the scratch measurement used formula matchesFilterCondition itself.",
    "The issue comment JSON and this final JSON are not byte-identical: per the dispatch security discipline the comment carries the controllability verdicts only; controllability_evidence (exact commands) and pr_body_new are in the final report only.",
    "Labels: zero writes. The dispatch named no label, and skip-changeset does not apply (the changeset publishes a patch).",
    "Commit trailers use the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude) rather than the harness reminder model-named Co-Authored-By line; AGENTS.md takes precedence and the pre-push hook refuses a model identifier.",
    "Ablation A6 first attempt was a no-op (declared above) and was re-run with a different anchor.",
    "Two derived gates are NOT MEASURED locally (full-repo build prerequisite); CI owns them."
    ],
    "face_table": {
    "anchor": "objectstack 980bc05 (origin/main after PR #20046; text-match-sql.ts identical to dispatch base 2274894); engines better-sqlite3 (SQLite 3.53.4) via SqlDriver.execute and sql.js (3.49.1) via SqliteWasmDriver.execute; JS baseline @objectstack/formula matchesFilterCondition (packages/formula/dist)",
    "stored_values": "short ASCII strings with U+0000 at the start, middle and end, the same without it, '' and NULL (exact fixture in the committed test and the PM report)",
    "comparands": "without U+0000 (including '') and holding U+0000 at the start, middle and end",
    "base_dialect_sqlite_cells_differing_from_JS": {
    "F3 read-scope (compileScopedFilterToSql)": "37 of 60",
    "F3 via NativeSQLStrategy applyReadScope": "37 of 60",
    "F3 via ObjectQL echo read scope": "37 of 60",
    "F4 NativeSQLStrategy where": "37 of 60",
    "F5 ObjectQL echo where": "37 of 60",
    "CONTROL driver.find (ObjectQL execute face)": "1 of 60 ($icontains '' refused INVALID_FILTER vs JS false; by design)",
    "same_on_both_engines": true,
    "same_under_$not": "yes (NOT=1 re-run: identical 37/60 per face, $startsWith NUL-free 0/7)"
    },
    "base_per_operator_every_analytics_face": {
    "$contains noNUL": "5/7 (narrows: match after a stored NUL missed)",
    "$contains NUL": "3/5 (widens: NUL-leading comparand → every non-NULL row)",
    "$notContains noNUL": "5/7 (widens: admits values containing the comparand after a stored NUL)",
    "$notContains NUL": "3/5 (narrows: only NULL rows)",
    "$startsWith noNUL": "0/7 (matches JS)",
    "$startsWith NUL": "3/5 (widens: '' and NUL-led rows)",
    "$endsWith noNUL": "5/7 (widens and narrows)",
    "$endsWith NUL": "4/5 (widens)",
    "$icontains noNUL": "6/7 (narrows; plus $icontains '' answers every non-NULL row vs JS false, pre-existing, not a NUL cell)",
    "$icontains NUL": "3/5 (widens)"
    },
    "head_dialect_sqlite": "every analytics face on both engines: 1 of 60 differs, the $icontains '' cell only (unchanged by this PR); committed pin grid: 0 of 128 on every face and engine",
    "unknown_dialect_arm_base": "no sqlDialect hook (unknown → LIKE), all five faces both engines: 46 of 60 differ (includes the LIKE ASCII-fold residue); NUL cells: $contains NUL 5/5, $notContains NUL 5/5, $startsWith NUL 5/5, $endsWith NUL 5/5; primitive: (a+NUL+b) LIKE %b% = 0 and plain LIKE %+NUL+% = 1 on both engines",
    "like_ilike": "refused on every analytics face at base and head: read scope READ_SCOPE_COMPILE_FAILED/500, native and echo where INVALID_FILTER/400 (pattern with and without NUL) → no raw-pattern operator compiled on the sqlite arm, item 5 not applicable",
    "explain_query_plan": "indexed TEXT column, both engines: base GLOB contains/ends SCAN, starts SEARCH USING INDEX; head contains/ends/notContains/icontains/starts-with-NUL SCAN, starts NUL-free SEARCH USING INDEX"
    },
    "controllability": {
    "comparand_side": "no caller-controlled producer measured. RLS string-method arguments resolve from admin-authored policy literals or current_user string attributes (id, organization id, email); ids are system-generated, email is refused with U+0000 at the auth entries measured and read-only on the user object; membership sets are arrays, which the string methods refuse; plugin-sharing emits id/owner membership scopes, not text operators, and sharing-rule criteria go through engine.find (the fixed driver face).",
    "stored_value_side": "a caller-controlled producer exists: any persona holding create/edit on an object with a text field; the engine write path stores U+0000 unchanged under a non-system context (executed). Reaching a read scope also needs a deployment-authored using predicate with a string method over such a field; no shipped example authors one.",
    "recommendation_input_for_p1_question": "the widening needs a deployment-authored text-operator RLS predicate plus a writer of that field; no comparand-side producer measured"
    },
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · the unknown-dialect arm (LIKE) on a SQLite engine is cut at U+0000 on pattern and stored value: with no sqlDialect hook, all five faces on better-sqlite3 3.53.4 and sql.js 3.49.1 at 980bc05 answer 46 of 60 formula cells differently (NUL-comparand cells 5/5 for $contains/$notContains/$startsWith/$endsWith; primitive (a+NUL+b) LIKE %b% = 0, plain LIKE %+NUL+% = 1); reachable through the four embedder compositions text-match-sql.ts names (SqlDriver with a class client or unrecognised spelling, a host hook answering 'sqlite3', AnalyticsService without sqlDialect, a data service without getDriverForObject); not fixable in the arm without a dialect guess (PostgreSQL has no instr) · carrier: the domain:services seat · dedupe words: analytics unknown dialect LIKE U+0000 sqlite · text-match-sql unknown arm NUL cut · normalizeSqlDialect unknown sqlite LIKE NUL",
    "class: b · $icontains with an empty comparand answers every non-NULL row on the service-analytics faces (compileScopedFilterToSql, NativeSQLStrategy where, echo; and AnalyticsService.query in-process executed 'instr(lower(v), lower($1)) > 0' with ['']), before and after this PR; contract: spec FILTER_TEXT_CASES 'declares two REJECTION rows for this operator (an EMPTY comparand and a NON-STRING one, code: INVALID_FILTER, mustMention: [$icontains])' (filter.zod.ts checkFilterConditionComparands docblock) and driver-sql refuses it (#5702); Seam: spec:FILTER_TEXT_CASES / isRefusedTextComparand → runtime:service-analytics AnalyticsService.query where + read-scope-sql compileScopedFilterToSql; REST-door reachability NOT MEASURED · carrier: PR #20058 (#20035) touches the analytics where door, coverage of this cell not verified · dedupe words: analytics icontains empty comparand · isRefusedTextComparand service-analytics · FILTER_TEXT_CASES rejection analytics",
    "noted, not filed (PR Acceptance notes): stale GLOB prose in like-pattern.ts, read-scope-sql.ts docblocks, native-sql-strategy.ts, objectql-strategy.ts, strategies/types.ts · carrier: none"
    ]
    }

  6. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Review — ACCEPT (landing gated on CI) · PR #20067 · head d000746b1d · domain:services seat · 2026-09-25T01:24Z

    Seat session_01Evb5jFDZGKQE9KG4jbMfMF. Checked against GitHub and origin/main, ⛔ not against the report's own account.

    Checklist

    • PR shape. Draft, targeting main. The first line is Fixes #20025, and Clause-②: no starts its own line. No other closing keyword.

    • Scope. 7 files, +568 / −52: text-match-sql.ts, one new 29-case test file, four respelled pin files and the changeset. No call site, no refusal door, no driver, spec or formula file. NOT governed. It merges clean with origin/main.

    • Diff, read line by line. The sqlite arm of textMatchPredicateSql now matches driver-sql's textMatchPredicate / sqliteLengthAwareTextMatch on main cell for cell:

      • instr(col, ?) > 0 for contains;
      • the coalesce(substr(CAST … AS BLOB), -length(…)), CAST … AS BLOB) = CAST(? AS BLOB) suffix for a non-empty ends, and instr > 0 for an empty one;
      • instr = 1 for a starts comparand holding U+0000;
      • GLOB byte for byte only for starts without U+0000.

      This is the wider split 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 set, not the card's narrower suggestion. The suffix binds its comparand twice through the existing TextMatchBind, in placeholder order. The fold wraps lower() around both sides. The negation is NOT (…), so 非否定路径上的 $ne / $nin / $notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了 $not) #5298's wrapper composes unchanged.

    • Measurement. Executed at base on better-sqlite3 and sql.js, through each driver's execute(). Every analytics face answered 88 of the 128 pin cells differently from JS; the driver-sql control answered 0. At head, 0 on every face and engine. $startsWith without U+0000 keeps its index SEARCH.

    • Tests. The new file was committed first and ran red at base (14 red / 15 green). At head: package 2718 green, typecheck 0. The four respelled files are each tighter, naming the construct per operator where they had one /GLOB/. Ablations A1–A6 each turn red; A6's void first attempt is declared, and every restore is blob-identical.

    Controllability (triage's point 1) → the grade stays priority:p2.

    • Comparand side: no caller-controlled producer measured. The RLS string-method arguments are policy literals or system-owned current_user attributes.
    • Stored-value side: any writer of a text field can store U+0000. But reaching a read scope also needs a deployment-authored text-operator using predicate, and no shipped one exists.

    Triage's p1 condition was a caller-controlled comparand, and it is not met.

    Deviations accepted.

    • The base is 980bc05e5b; text-match-sql.ts is identical to the dispatch base.
    • The parity rows are in a sibling file.
    • The test's JS answer is an inline mirror of five evaluator lines (the package has no formula dependency), and every cell is also held to driver-sql's rows.
    • The claim's file surface is revised to include icontains-dialect-sql.test.ts, sql-dialect-vocabulary.test.ts and text-operator-non-text-column.test.ts (this line is the revision).

    Out-of-scope findings, one line each:

    Landing. No Clause-② limb is hit: no packages/spec/src, a patch, and the fix makes an existing operator answer its declared meaning. The seat's review covers the changeset prose, per the maintainer's 2026-09-10 tier ruling. The PR goes ready and auto-merge when every check is green.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat · session_01Evb5jFDZGKQE9KG4jbMfMF · 2026-09-25T02:14Z


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions