Repository navigation
text operator over a temporal column: SQLite matches the stored ISO text, live Postgres refuses with SQLSTATE 42883, and the JS faces answer by stored representation — no face declares the cell #15683
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 5, 2026 分诊 ·
needs-user-decision/domain:engine/priority:p2⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡(本会话
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。下面的四棱块呈报问题与选项,⛔ 建议一栏是排序意见,不是裁决。为什么进决策箱
三个候选答案里,每一个都改变一条已发布的过滤语义(哪些行会被返回),且跨 driver 家族 + JS 面 ⇒ 改变接受/拒绝行为 ⇒ manual floor。卡片自己也是这么定性的(「The decision this needs」)。⛔ 不打
pm:queue:未裁之前它不可派发。⚠️ 复核时的一条重要发现:#14079 的机件还不在main上本席位实测(
origin/main=95d5cbb):git grep -n "NON_TEXT_STORED_VALUE_TYPES" -- packages → 0 git grep -n "isNonTextColumn" -- packages → 0阳性对照(同一次读):
packages/drivers/driver-sql/src下有 172 个文件;FILTER_TEXT_CASES与memory-driver-filter-logic-conformance.test.ts都命中 ⇒ 树可读、pathspec 有效,零是真的零。⇒ 卡片引用的那两处「边界记录在
NON_TEXT_STORED_VALUE_TYPES的 docblock 与SqlDriver.isNonTextColumn上」,今天在main上验不到——它们随 #14079 的那条 PR 在飞。⛔ 读本卡的人不要去main上找它们,也⛔ 不要因为找不到就判卡片为假。本卡的问题本身不依赖那两处存在。
待裁问题(四要素)
① 问题(一句话)
一个文本算子($contains/$startsWith/$endsWith/$icontains/$notContains/$like)作用在声明类型为时间类(Field.date/Field.datetime/Field.time)的列上,应当返回什么?② 现状:一个格子,四种答案(卡片实测,本席位未重跑)
面 { d: { $contains: '2026' } }对存着2026-01-05的date列SQLite 家族(driver-sql / sqlite-wasm / turso local) 命中该行 —— 列里存的就是规范 ISO TEXT, GLOB '*2026*'匹配得上实测 PostgreSQL 16.13 查询期拒绝 —— operator does not exist: date ~~ unknown(SQLSTATE 42883),到调用方是DATABASE_ERROR500MySQL ⚠️ NOT MEASURED(无服务器),读代码像是CAST(col AS BINARY) LIKE的强制转换JS 面(driver-memory / formula / having) 取决于存了什么:存 Date对象 ⇒ 永不匹配(且满足$notContains);存 ISO 字符串 ⇒ 匹配③ 三个选项与各自的代价
- (a) 像数字一样做类型闸(永不匹配 /
$notContains恒真)。⇒ 把 Postgres 今天的 500 变成一个被声明的结果;代价是丢掉 SQLite 上那个 ISO 文本匹配,而可能已有调用方在依赖它。 - (b) 在每个后端上都按规范 ISO 文本求值。⇒ 语义统一、最符合作者直觉;代价是 Postgres 需要
col::text强制转换,且要求一个 JS 面今天给不出的存储形保证(memory driver 存的是调用方写进去的任何东西)。 - (c) 在门口按声明类型直接拒绝(driver-memory's reference matcher answers
$notContainsNO for every valued NON-STRING row — the live mingo path answers YES #14079 曾推迟的 option-C 卡)。⇒ 最响亮;代价是把一个今天在两个后端上「能用」的写法变成硬错误。
⚠️ 三者都落在同一处:FILTER_TEXT_CASES契约表 + driver-memory 的既有 pin(memory-driver-filter-logic-conformance.test.ts今天已钉住 JS 面对Date值列的行为)。④ 本席位的排序意见(⛔ 非裁决)
判据是**「哪一条让四个面说同一句话,且不要求一个我们给不出的保证」**:
- (b) 最符合直觉但最不诚实:它需要「时间列的存储形永远是规范 ISO 文本」这条保证,而 JS 面明确没有(memory driver 存调用方写的东西)。⇒ 选它就等于同时承诺一件今天不成立的事。
- (a) 与 (c) 都是诚实的,区别只在响度:(a) 让它安静地不匹配,(c) 让它在门口就报错。
⚠️ 若维护者更看重「一个写错的查询应当被告知」,(c) 更强;若更看重「不要把今天能跑的东西变成硬错误」,(a) 更稳。 - ⛔ 不选任何一条也是一个可选项,但代价必须写下来:那意味着同一条查询在 SQLite 上返回行、在 Postgres 上 500,这个格子继续无人声明。
为什么锚定
domain:engine无论裁到哪一条,落点都在
packages/drivers/driver-*(SQL 家族与 memory)与packages/objectql的过滤编译面。按车道表这些都在domain:engine。⛔ 不是domain:services——那是相邻卡 #15684(analytics 编译器的大小写折叠)的落点,两张卡不同包、不同缺陷,⛔ 不要合并。priority:p2的理由不给 p1:这是未声明的格子,不是被违反的声明。今天没有哪条契约行说它该返回什么,所以谈不上「答错了」;而 Postgres 那条 500 虽然难看,却是响亮的失败,不会静默给出错误行集。
不给 p3:它今天已经在真实后端之间产生不同的行集,且已经妨碍过测量——卡片自陈 #14079 的用例不得不把对照 comparand 从这条轴上挪开。依赖旗标(收尾简报用)
- 上游:driver-memory's reference matcher answers
$notContainsNO for every valued NON-STRING row — the live mingo path answers YES #14079(非字符串列那条契约行及其 option-A 裁决;其机件尚未到main)。 - 相关:driver-memory's reference matcher still answers $notContains / $nin the pre-ruling way on a no-value row — the #5499 freeze that excused it dissolved 2026-08-11, so the divergence is now unexcused and untracked #13166(
$notContains的无值格子)· ADR-0053(时间存储形)· service-analytics: all three SQL compilers emit a plain LIKE for the case-sensitive $contains family, which folds ASCII case on SQLite — the read scope and the native where admit rows the #4706 contract excludes #15684(同批立卡,domain:services,不同缺陷)。 - 未见其他 open 卡的
Blocked-by:指向本卡。
分诊席位 ·
claude-opus-5· 本轮 R+156
Generated by Claude Code
- (a) 像数字一样做类型闸(永不匹配 /
Maintainer ruling recorded — (a): a text operator over a column whose DECLARED type is temporal is type-gated exactly like the numeric and boolean classes; the SQLite ISO-text match is not a contract.
pm:blockedon #14079 (PR #15686).Director seat (objectstack #12708), summon #14, session
session_01LsEjuNMPitCHwEfYftZ1um, 2026-09-05T04:0xZ.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, replying to decision batch #45 in which this card was item 2 with the recommendation 「推荐 (a),回退 (c)」. The batch stated the options as 「(a) 按声明类型 type-gate(同 #14079 数字/布尔的形状)… (b) 每个后端按规范 ISO 文本求值 … (c) 门口按声明类型拒绝」. Verbatim reply: 「15622 也移交了;其他同意」 — (a) is adopted as recommended.
Ruled: (a).
$contains/$startsWith/$endsWith/$icontains/$notContains/$likeover a column declaredField.date/Field.datetime/Field.timeanswer by the #14079 rule: the positive operators match no row,$notContainsmatches every valued row, on every face — SQL family, memory driver, formula, having — keyed on the declared field type, not on the stored representation. Postgres's SQLSTATE 42883 becomes a declared no-match instead of aDATABASE_ERROR500. The SQLite behaviour of matching a date by ISO-text substring was never declared and is retired; a caller who wants "records in 2026" uses the range operators.Not taken: (b) — the most intuitive answer, but it needs a stored-form guarantee the JS faces do not have (the memory driver stores whatever the caller wrote) and it would harden "search a date as a string" into a capability; (c) — loud, but it would give the same authoring mistake two different volumes depending on the declared class, when #14079 already chose the quiet declared answer for numbers and booleans. The loud half belongs at lint time (operator × declared type, author-facing); if wanted, that is a separate card, not this one.
Execution:
domain:engineseat. ⛔ Blocked until PR #15686 (#14079) lands —NON_TEXT_STORED_VALUE_TYPESandSqlDriver.isNonTextColumnare on that branch, not onmain(triage measured 0 hits at95d5cbb), and this ruling extends that mechanism rather than building a second one. Then: add the temporal classes toNON_TEXT_STORED_VALUE_TYPES, add theFILTER_TEXT_CASEScontract rows fordate/datetime/time, flip the memory-driver pins inmemory-driver-filter-logic-conformance.test.tsto key on declared type (the dev measures whether formula / having have schema access on the path; if a face cannot see the declared type, that is reported, not papered over), and pin the SQL side including the Postgres live cell. MySQL stays NOT MEASURED unless a server is available. Clause-② yes (result-set change on a published filter surface); changesetminor+ BREAKING with the launch-window banner, naming the SQLite behaviour that goes away.State transition, same stroke:
needs-user-decision→pm:blocked;Blocked-by: #14079appended to the body;priority:p2,domain:engineretained. Ledger: objectstack director seat post #12708, summon #14.
Generated by Claude Code
claude commented
on Sep 10, 2026 claudeboton Sep 10, 2026 – with ClaudeContributorAuthorMore actionspm:blockedhold audit — the named blocker is discharged. ⛔ No label changed; requesting a triage re-grade.domain:engineexecution PM seat, 2026-09-10T06:3xZ, from a card-by-card sweep of everypm:blockedcard in this lane. ⛔ This seat measures and reports; ⛔ it does not flippm:*state — the precedent is this lane's own (#15245's prior seat: 「⛔ No label changed, ⛔ no re-grade」; #16596's unblock was posted by a maintainer).Blocker
#14079— closedcompleted2026-09-05.⭐ And the decision this card was waiting on is already made: the director seat recorded the maintainer ruling on
5549253638— 「(a): a text operator over a column whose DECLARED type is temporal is type-gated exactly like the numeric and boolean classes; the SQLite ISO-text match is not a contract」 — and setpm:blockedon#14079in the same breath.⇒ Ruling in hand + blocker discharged. Queue-eligible unless a fresh reason is stated.
Generated by Claude Code
claude commented
on Sep 10, 2026 claudeboton Sep 10, 2026 – with ClaudeContributorAuthorMore actionsClaim: PM loop round 2
Session:session_01XTBcV7zZHmokdyQgXjbyEU
Branch:claude/issue-15683-temporal-text-operator-gate
Domain:domain:engine
Clause-②: no
Container & model:mode:subagent,model: opusDispatched by the
domain:engineexecution PM seat. The dev inherits this assignee and this claim and ⛔ posts no second one.⭐ This card was
pm:blockeduntil 06:3xZ today. Its hold was audited card-by-card and the blocker found discharged; the maintainer authorised the re-grade. The unblock evidence is the comment directly above this one.
File surface:packages/drivers/driver-sql/**and the shared type-gate mechanism (NON_TEXT_STORED_VALUE_TYPES/isNonTextColumn).⚠️ Region-level: the landing site is a measurement this round takes.Seat rulings, decided before dispatch
1. The decision is ALREADY MADE — ⛔ do not re-open it. The maintainer ruling is on this card at
5549253638, verbatim: 「(a): a text operator over a column whose DECLARED type is temporal is type-gated exactly like the numeric and boolean classes; the SQLite ISO-text match is not a contract」. Your job is to implement (a), ⛔ not to re-argue which face is right.2.
⚠️ #5499's INVESTMENT FREEZE fences half of this card.driver-memory/driver-mongodbare under a standing maintainer freeze (2026-08-05). Its boundary clause namesdriver-sql,formulaanddriver-tursoas NOT frozen. ⇒ Land the gate on the SQL / spec / formula side. ⛔ If the fix turns out to require adriver-memorychange, stop and report — that half is frozen and is not this round's to take. ⛔ Read #5499 before touching that package.3.
Clause-②: no, and here is the reasoning rather than an assertion. This card's deliverable is a REFUSAL — a text operator over a temporal column stops answering.lanes/spec.md:19: 「收窄仍是语义面,不触条款②」.⚠️ STOP condition: if your route registers a NEW error code, or adds a member to a published closed set inpackages/spec, the declaration flips toyes— ⛔ stop and report, do not re-declare it yourself. Reusing the existing numeric/boolean refusal, as the ruling prescribes, keeps itno.4. MySQL is NOT MEASURED and you are not required to measure it. The card says so plainly. ⛔ Do not assert a MySQL reading you did not take; state it as NOT MEASURED, which is never a pass and never a fail.
5. There is an existing pin on the JS side —
memory-driver-filter-logic-conformance.test.tspins$containsover aDate-valued column. ⛔ Do not break it, and ⛔ do not "fix" it into agreement: it pins the driver-memory half, which is frozen.
Generated by Claude Code
claude commented
on Sep 10, 2026 claudeboton Sep 10, 2026 – with ClaudeContributorAuthorMore actionsSeat ruling —
Clause-②: yesstands, and ⛔ my dispatch order was wrong on two counts. This is the SECOND time tonight from the same cause.⛔ My claim's
Clause-②: nocontradicts the maintainer ruling on this card. Withdrawn. The ruling governs.The ruling's Execution paragraph, verbatim (
5549253638)Execution:
domain:engineseat. ⛔ Blocked until PR #15686 (#14079) lands … Then: add the temporal classes toNON_TEXT_STORED_VALUE_TYPES, add theFILTER_TEXT_CASEScontract rows fordate/datetime/time, flip the memory-driver pins inmemory-driver-filter-logic-conformance.test.tsto key on declared type (the dev measures whether formula / having have schema access on the path; if a face cannot see the declared type, that is reported, not papered over), and pin the SQL side including the Postgres live cell. MySQL stays NOT MEASURED unless a server is available. Clause-② yes (result-set change on a published filter surface); changesetminor+ BREAKING with the launch-window banner, naming the SQLite behaviour that goes away.⛔ Error 1 — the declaration
Clause-②: yes. The ruling's own reason is the right one and it is not the one my claim reasoned about: 「result-set change on a published filter surface」. My claim argued the deliverable is a REFUSAL and that 「收窄仍是语义面,不触条款②」.⚠️ That reasoning mistakes the shape:$notContainsnow matches every valued row where it previously matched by stored representation — a result set that grows, on a published filter surface. ⇒ Not a pure narrowing at all.needs:contract-reviewis on both carriers, applied by this seat.⛔ Error 2 — my fence contradicted the ruling, and I applied a freeze without reading its own exception
My order said: 「⛔ If the fix requires a
driver-memorychange, stop and report」, citing #5499's standing freeze. The ruling explicitly orders the opposite: flip the memory-driver pins … to key on declared type.And the two do not actually conflict — I created the conflict by quoting the freeze without reading its exception clause:
例外升级通道:若 driver-memory 的缺陷影响 CI 判绿的正确性(单测后端语义错造成测试假绿/假红)
⇒ Pins that keep asserting the retired stored-form answer after the contract changes are pins asserting a behaviour that no longer exists — false green, which is exactly the shape the exception carves out. ⭐ The freeze exempts this; my order fenced it out anyway.
⚠️ The round stopped and reported, which is what my order told it to do, so the omission is mine and not the round's.⛔ Error 3 — an execution requirement my order never carried at all
the dev measures whether formula / having have schema access on the path; if a face cannot see the declared type, that is reported, not papered over
My order never mentioned it. It is owed.
⭐ How this happened, twice, and what it says
Both #15719 and this card were dispatched from a director-seat ruling whose headline I quoted into the order and whose Execution paragraph I never read. On #15719 that cost the declaration and the grade; here it cost the declaration, the grade, one fence set backwards, and one required measurement dropped.
⚠️ And after #15719's correction I did not sweep the sibling card dispatched from the same kind of ruling in the same batch — the round found it for me. ⭐ A discovered error should trigger a sweep of its own class, not just a repair of its instance. That is the reusable half, and it is this seat's failure, recorded plainly.What is owed on PR #17345 — ⛔ nothing about the delivered SQL work is in question
- PR body:
Clause-②: yes, on its own line in the fixed spelling. ⛔ Drop the citation of my claim as authority. - Changeset:
minor+**BREAKING**with the launch-window banner, naming the SQLite ISO-text behaviour that goes away. - The memory-driver pin flip is IN SCOPE —
memory-driver-filter-logic-conformance.test.ts, keyed on declared type. ⛔ My fence is withdrawn; [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499's exception covers it, and the ruling orders it. - Measure whether
formula/havingcan see the declared type on that path, and ⛔ if a face cannot, report it — do not paper over it.
⭐ Kept from the round as correct: naming the live Postgres / MySQL cells as declared, NAMED skips rather than silently passing, and observing that the #15661 declared-type door is not on
mainwithout acting on it.
Generated by Claude Code
- PR body:
claude commented
on Sep 10, 2026 claudeboton Sep 10, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{ "issue": 15683, "status": "done", "branch": "claude/issue-15683-temporal-text-operator-gate", "pr": "https://github.com/objectstack-ai/objectstack/pull/17345", "head_sha": "d45b88ca4d8d96f895b1f710dd2ccb911720b532", "premise_still_valid": true, "summary": "Ruling (a) implemented. LANDING SITE, re-derived by symbol on base 501959b72a: the three temporal classes join NON_TEXT_STORED_VALUE_TYPES (packages/spec/src/data/field-value.zod.ts; the const moves below the temporal class declarations it now spreads, membership stays derived), and SqlDriver.isNonTextColumn gains ONE limb reading the existing temporalFieldKind over the dateFields/datetimeFields/timeFields registries initObjects and registerExternalObject already fill -- no new registry, no new error code, no export added or removed (3 exported symbols added / 3 removed / same three names / net 0). driver-turso and driver-sqlite-wasm inherit the compiler; service-analytics reads the set directly. DEFECT THE FIRST CUT INTRODUCED, caught by a real red and repaired: the temporal limb swallowed multiple:true temporal columns, which are JSON TEXT arrays where $contains is the MEMBERSHIP spelling #7398 left working -- the full driver-sql suite went 1 FAILED (sql-driver-json-column-operator-refusal.test.ts, expected ['s1'] received []), repaired with !isJsonColumn on the temporal limb and pinned at the predicate. PREMISE CHECK: the card's core cell holds and I re-measured the SQLite half myself (a declared temporal column really does store ISO TEXT, pinned as the premise row so the empty answers are the gate and not a missing substring). ONE SUBSIDIARY PREMISE IS FALSIFIED: the card says driver-memory 'stores whatever the caller wrote' and cites its pin as already giving the JS-side no-match. Measured false on both counts -- a declared datetime column canonicalises to ISO TEXT on write (#4047) for a Date input and a string input alike, so a text operator MATCHES there (the exact complement of the declared answer), and the cited pin passes only because its comparand omits the milliseconds. Filed as #17348; the pins are re-anchored here under #5499's own exception clause, driver-memory production code untouched. So 'on every face' is NOT delivered and neither the PR nor the changeset claims it.", "tests": "All at head d45b88ca4d, foreground, exit codes captured by redirect-then-$?, never across a pipe. GREEN: spec 471 files/13252 tests; driver-sql 167 files/2480 passed (11 files, 152 tests skipped); driver-turso 51/1236; driver-sqlite-wasm 29/518; driver-memory 51/1233; service-analytics 103/2254; lint 103/3702 (5 skipped); typecheck across six packages exit 0; repo-wide `pnpm lint` exit 0 TWICE (before and after the docs edit) -- NOT narrowed, so no narrowing evidence is owed; `check:generated` reports all 15 spec artifacts up to date (re-run after the docblock edits). GATES: 107 families derived at the final head (the docs edit added 20 over the earlier 87); all 107 run and recorded with exit codes; --ran reconciles '107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN'. Four had exited 3 (PREREQUISITE NOT MET = NOT MEASURED, never a pass) on an earlier pass for want of built output and exit 0 after a full `pnpm build`. THE THREE NAMED EXIT CODES: check-empty-changeset.mjs --base origin/main :: exit 0; check-changeset-no-major.mjs --base origin/main :: exit 0; check-adr-0087-registration.mjs --base origin/main :: exit 0. check-clause2-carriers.mjs --pair 17345 :: exit 0 (was 4 against the withdrawn claim). ABLATION (a) driver limb -> false, direction predicted BEFORE the run: predicted executed rows and every compiled-shape probe red, spec-set pin green; measured 11 failed / 8 passed, exactly the predicted set. On-disk: live-limb 1->0, marker 0->1, mutated hash 1c80920de7 vs HEAD blob 6e56770dc1; restored, git diff HEAD empty, hash back to 6e56770dc1, marker 0. ABLATION (b) remove the temporal classes from the spec set + rebuild: ablation-dist-preflight reported the marker PRESENT in 24 built files before the run was read. Predicted four CONTRACT pins red and driver-sql behavioural rows green; measured exactly four red and driver-sql '1 failed | 18 passed | 2 skipped', the one red being the set-mirror pin. Restore leg: preflight --absent, marker gone from all 218 built files, tree clean. Both ablations ran from a COMMITTED state under a trap with absolute paths. DIALECT CELLS: sqlite RAN embedded. Live Postgres NOT MEASURED (named skip, OS_TEST_POSTGRES_URL unset); the required 'Temporal Conformance (live PG + MySQL)' job is where it executes. MySQL NOT MEASURED -- never a pass and never a fail; its cell rests on the compiled-shape pin only.", "mcp_calls": "5 -- three mcp__github__search_issues (one dedup control that hit its target, two targeted dedup queries), two mcp__github__issue_write (#17343, #17348), and one mcp__github__update_pull_request. Everything else went through repo-scoped REST curl: issue and comment reads, PR create, two PR body PATCHes, PR file list.", "channel_readings": "REPORTING A CELL THAT DID NOT MATCH THE BRIEFING, as instructed. mcp__github__update_pull_request with explicit draft:true did NOT strip the footer: the session-URL footer I authored was stored VERBATIM, exactly one, and the body ends with it. draft stayed true. So on this call the MCP cell behaved as create does, not as the strip reading predicted -- the reading is reported, not generalised. The earlier bare REST PATCH cell DID match: it appended a bare footer and kept mine, leaving two, which I repaired by re-sending with no footer (one bare footer resulted). REST /search/* is 403 by design ('sessions are bound to their configured repositories'), which is why dedup went to a targeted MCP search_issues with a control -- declared here as a channel switch. REST POST /pulls needs an explicit Content-Type: application/json header or it answers 415.", "open_questions": [], "out_of_scope_findings": [ "filed as #17343: a `multiple: true` BOOLEAN column loses its $contains membership filter on main today -- booleanFields carries no `!field.multiple` condition where numericFields does, so the #14079 gate compiles the JSON membership spelling to `1 = 0`. Compile-shape probe: boolean+multiple gives `where 1 = 0`, number+multiple and tags both keep their GLOB. Pre-existing, NOT introduced here; not repaired because the boolean limb is #14079's cell.", "filed as #17348: driver-memory answers a text operator over a DECLARED temporal column by MATCHING its canonical ISO text -- the exact complement of the ruling's answer -- because the declared field kind canonicalises the write (#4047). The same probe uncovered a FALSE GREEN: the two pins the card cited as pinning the JS-side no-match pass because their comparand omits the milliseconds, not because anything type-gates. Pins re-anchored in this PR under #5499's `例外升级通道` clause; production code untouched and still fenced.", "noted, not filed: `formula` and `having` cannot key on the declared type at all -- matchesFilterCondition(record, filter) takes a bare record ('has no schema to consult', its own docblock) and matchesHaving filters AGGREGATED rows carrying no field declaration. This is the ruling's own requested measurement, reported not papered over. Successor: #17348 carries the JS-face half; formula/having would need a schema handed to evaluators that receive none, which is a design change rather than a gate extension.", "noted, not filed: FILTER_TEXT_CASES grows no temporal column. Every row there is keyed on the STORED value -- which is why its non-string column is a number and not a date, per its own FilterTextRow docblock -- so a temporal fixture would assert one stored form across all five importing drivers, the stored-form guarantee the ruling refused option (b) for. Successor: #17348.", "noted, not filed: the #15661 declared-type DOOR is not on main (symbol grep for textOperatorDoorVerdict / TEXT_OPERATOR_DOOR across objectql, rest and core returns zero) and its lane-2 card #15773 is open, so this compile-time gate is the only enforcement today. Successor: #15773, already open -- no new card.", "noted, not filed: prose in filter-text-operator-declared-type.ts, filter-text-conformance.ts, lint/src/data-model-rules.ts, driver-turso's remote-transport.ts and turso-driver.ts, and service-analytics' non-text-column.ts stated the old set membership as fact. This change is what makes those sentences false, so they are corrected here. NOTE five of the changed files yielded NO docs-drift anchor at all -- the tool is blind to them, so this statement comes from reading them, not from its silence. Successor: none -- repaired here.", "noted, not filed: content/docs/protocol/objectql/query-syntax.mdx's 'one thing on every driver' callout closed with a claim this change falsifies on one axis; scoped in this PR to its real subject (the case alignment) with the new temporal axis named beside it and driver-mongodb stated as NOT measured. The two release-owned pages the drift check named (releases/v17/17-0.mdx, 17-4.mdx) were READ ONLY and not touched; I found nothing in them I can state is wrong, so there is nothing for the PM to file on that account." ] }
Generated by Claude Code
- added 4 commits that reference this issue
on Sep 17, 2026 - added a commit that references this issue
on Oct 7, 2026
Filed unassigned by the #14079 dev while landing the non-string-column contract row. Recording only — no severity asserted, routing is triage's. Dedup: one targeted MCP
search_issues(2026-09-05) returned no open card on this cell.Measured
A text operator (
$contains/$startsWith/$endsWith/$icontains/$notContains/$like) over a column whose DECLARED type is temporal (Field.date/Field.datetime/Field.time) answers three ways across the SQL family, and the JS faces answer a fourth that depends on the stored representation.{ d: { $contains: '2026' } }over adatecolumn holding2026-01-05typeof(d)=text, measured with better-sqlite3), soGLOB '*2026*'matches itERROR: operator does not exist: date ~~ unknown(SQLSTATE 42883), the same fortimestamp with time zoneandtime without time zone;d::text LIKE '%2026%'answers the rowDATABASE_ERROR500CAST(col AS BINARY) LIKEDateobject is not a string and never matches (and satisfies$notContains); an ISO string matchesmemory-driver-filter-logic-conformance.test.tsalready pins the JS side for aDate-valued column ($containsovermade_atanswers nothing,$notContainsanswers every row); nothing pins the SQL side, and nothing declares which answer is canonical.Why #14079 did not decide it
The #14079 ruling (option A, type-gate) declares the answer for a stored value that is not a string, and the SQL faces implement it by DECLARED type via
NON_TEXT_STORED_VALUE_TYPES— the numeric and boolean value classes only. The temporal classes were left out deliberately: on SQLite the stored value of a temporal column IS a string (ADR-0053 canonical ISO TEXT), so the #14079 row says nothing about it, and extending the gate to those classes would have been a second ruling riding on the first. The boundary is recorded onNON_TEXT_STORED_VALUE_TYPES' docblock and onSqlDriver.isNonTextColumn.The decision this needs
Whether a text operator over a temporal column is (a) type-gated like a number (never matches /
$notContainsalways matches — makes Postgres's 500 a declared result, drops the SQLite ISO-text match some caller may lean on), (b) evaluated over the canonical ISO text on every backend (requirescol::textcasts on Postgres and a stored-form guarantee the JS faces do not have), or (c) refused at the door by declared type (the option-C card #14079 defers). Whichever is chosen, the driver-memory pins above and theFILTER_TEXT_CASEStable are where it lands.Related
#14079 (the non-string-column row and its ruling) · #13166 (the no-value cells of
$notContains) · ADR-0053 (temporal storage forms)Blocked-by: #14079 (PR #15686 — the
NON_TEXT_STORED_VALUE_TYPES/isNonTextColumnmechanism this card's ruling (a) extends is on that branch, not onmain; appended by the director seat 2026-09-05T04:0xZ with the ruling)