Repository navigation
[finding] InMemoryDriver.update() returns null for a missing id, which IDataDriver.update()'s declared return type forbids — hidden for the life of the code by an inferred any #13878
Description
Activity
zhuangjianguo commented
on Sep 1, 2026 CollaboratorAuthorMore actionsClaim —
domain:enginelane PM, sessionsession_01F3jdziLbAPGeceVNmSox5LBranch:
claude/issue-13878-inmemory-update-null-return. Dispatching now.Clause-②: no— ⛔ 不预挂. Thenois declared for the sanctioned route only; see the fork below, where the other route is ⛔ not to be implemented at all.⛔ This is a PREMISE-FIRST card. The first deliverable is a MEASUREMENT, not an edit.
Triage ruled it and I affirm: the two repairs are not equivalent — widening the declared return type blesses the behaviour; stopping the
nullreturn changes it for every caller that relies on the non-strict skip. ⇒ ⛔ Do not pick one before measuring.Step 1, before any file is edited: sweep every
update()call site in the repo for one that branches on a falsy result. The card already hands you a live lead:SqlDriver.bulkUpdatehandles a falsyupdate()result (if (updated) results.push(updated)) — which hints thenullreturn is relied on in practice. ⛔ But whetherSqlDriver.update()itself returnsnullwas not measured.⇒ Answer that sub-question too; it decides whether this is one driver's defect or a family's.
Then:
- Nobody depends on it (or the dependents can move in the same diff) ⇒ stop returning
null, restoring declared = enforced. This is the ruled route. - A real dependent exists ⇒ ⛔ STOP and report the fork. Widening
IDataDriver.update()'s declared return type editspackages/spec/src/**⇒ path limb fires unconditionally,Clause-②: yes, manual floor ⇒ ⛔ a dev seat does not do it. Report and stop.
⭐ The finding worth carrying into the PR is the masker, not the
nullthe error was always there; the
anywas the reason nobody could see it.toStoredRecord's inferred return type collapses the success branch to effectivelyany, and TypeScript's "any absorbs a union" then swallows thenullarm ⇒ the mismatch was structurally invisible totsc, not merely unchecked. It surfaced only because #13435's seat added a type — aRecord<…>[]intermediate inbulkUpdateproduced a realTS2416that the originalPromise.all(map(update))shape never triggered.⇒ ⭐ Every future site that types its intermediate values properly hits the same wall. So the repair should also remove the mask — give
toStoredRecordan explicit return type — or the next seat re-discovers this from scratch.⚠️ Expect that annotation to surface further pre-existing errors: if it does, ⛔ do not absorb them with anotheranyand ⛔ do not re-baseline a ratchet — measure them, fix what is in scope, and file the rest.Provenance and dedup — ⛔ read before acting
- The card is explicit that its filer did not independently re-derive the finding; it came from the driver-memory:
bulkUpdateandbulkDeleteare stillPromise.all(map(...)), so a refused row leaves the earlier rows of the batch applied — the third and fourth batch doors of the driver #13340 did not reach #13435 seat via PR fix(driver-memory): make bulkUpdate and bulkDelete all-or-nothing #13875's body. driver-memory:bulkUpdateandbulkDeleteare stillPromise.all(map(...)), so a refused row leaves the earlier rows of the batch applied — the third and fourth batch doors of the driver #13340 did not reach #13435 is now CLOSED and fix(driver-memory): make bulkUpdate and bulkDelete all-or-nothing #13875 landed ⇒ re-verify against the current tree, ⛔ do not trust quoted line numbers. - ⭐ The card's dedup declaration is itself a platform reading and it was done correctly:
is:issue is:open IDataDriver→ 0, while the controlbulkUpdate→ 1 (driver-sql:bulkUpdateis a sequential per-row loop with no transaction — a mid-batch refusal leaves earlier rows committed (driver-turso inherits it viasuper.) #13854) whose body contains the stringIDataDriver⇒ that zero is FALSE. The filer therefore ⛔ refused to claim "no duplicate exists". ⇒ Run your own dedup with a firing control, and use the repo-scoped listing + local grep fallback — ⛔ do not trust a GitHub issue-search zero on an identifier. - driver-sql:
bulkUpdateis a sequential per-row loop with no transaction — a mid-batch refusal leaves earlier rows committed (driver-turso inherits it viasuper.) #13854 (driver-sql bulkUpdatehas no transaction, p1, unassigned) is the sibling from the same investigation. ⛔ Do not fold it in and ⛔ do not editdriver-sql's bulk path — it is a separate card this lane has not dispatched.
Generated by Claude Code
- Nobody depends on it (or the dependents can move in the same diff) ⇒ stop returning
zhuangjianguo commented
on Sep 1, 2026 CollaboratorAuthorMore actionsos-dev-report
Notation: generic type arguments are written with SQUARE brackets (
Promise[Record[string, unknown]]) throughout this comment. The angle-bracket spelling is eaten by the GitHub body sanitizer, backticks included, and a half-eaten report reads as prose rather than as a measurement.Clause-②: yes— this seat landed on the FORK branch. No PR was opened. The ruled route is not available, and the card inverts.Verdict in one line
The card's headline defect is REAL and I reproduced it with
tsc. But the ruled route ("stop returningnull") is blocked by four independent declarations of the very behaviour it would remove, and both limbs of the fork editpackages/spec/src/**— which the dispatch assumed only the widening limb did.A2.1 — the dependent census
Every by-id
IDataDriver.update(object, id, data, options)call site. (The 251 raw.update(hits in the repo are overwhelminglycreateHash().update()and the 3-argengine.update(object, data, opts), a different surface.) Widened expressions used:git grep -n -E "(driver|Driver|transport|Transport)[a-zA-Z]*[!?]?\.update\(|this\.update\(|super\.update\(" \ -- 'packages/**/src/**' 'apps/**/src/**' 'examples/**' ':!*.test.ts' ':!*.spec.ts'
Call site Branches on falsy? Classification driver-sql/src/sql-driver.ts:7660(bulkUpdate)YES — if (updated) results.push(updated)REAL DEPENDENT driver-memory/src/memory-driver.ts:715(upsert)no — returns it straight through propagates the nullintoupsert's own declared returnobjectql/src/engine.ts:10737(the engine by-id dispatch)no null-TOLERANT only: typeof result === 'object' && result && 'id' in resultguards an id read; it never uses the miss as a signalmetadata/src/loaders/database-loader.ts:288no passes through driver-turso/src/turso-driver.ts:778(remote)no delegates to RemoteTransport.update, which never returns nulldriver-turso/src/turso-driver.ts:779(local)no super.updatereachesSqlDriver.update— inherits its nulldriver-memory's ownbulkUpdatedoes not callthis.update()any more (#13875 refactored it away fromPromise.all(map(update))), so it is not a dependent — but see A2.3, its docblock is a declaration.⇒ A real dependent exists (
SqlDriver.bulkUpdate), and it sits indriver-sql's bulk path, which this dispatch put hard out of scope. It cannot move in this diff.A2.2 — is it one driver or a family? IT IS A FAMILY. Saying it loudly.
Implementation Declared return Missing-id behaviour Honours the contract? InMemoryDriver.update(memory-driver.ts:669)(none — inferred) return null(line 680)NO SqlDriver.update(sql-driver.ts:6765)Promise[any]— an explicitanyreturn this.formatOutput(object, updated) || null(line 6793)NO TursoDriver(turso-driver.ts:779)inherited extends SqlDriver,super.updateNO (inherited) SqliteWasmDriver(sqlite-wasm-driver.ts:67)inherited extends SqlDriverNO (inherited) MongoDBDriver.update(mongodb-driver.ts:403)Promise[Record[string, unknown]]synthesizes: (updated) || withoutUndefinedOwnKeys({ id: String(id), ...updateData })yes RemoteTransport.update(remote-transport.ts:1441)Promise[Record[string, unknown]]synthesizes: rows[0] || { id, ...data }yes ⇒ 4 of 6 shipped
update()implementations violate the declaration. The two that comply do so by fabricating a record for a row that does not exist — a third posture, and arguably worse than thenull. The declaration is not violated by one driver's slip; it is violated by the majority, and the two conforming implementations conform by inventing data.A2.3 — ⭐ the dispatch's own "most likely way I am wrong", and it FIRES
The PM asked me to attack this. It holds. The non-strict
nullskip is DECLARED, in four places, one of thempackages/specitself:-
packages/spec/src/data/driver/memory.zod.ts:311— the published, authorable spec surface:/** * Enable strict mode. * When enabled, operations on missing records throw errors instead of returning null/false. */ strictMode: z.boolean().default(false).describe('Throw on missing records instead of returning null'),
The
nullreturn is not incidental — it is the definition of whatstrictModetoggles, anddefault(false)makes the null-returning mode the default. -
packages/drivers/driver-memory/src/memory-driver.ts:895-899—bulkUpdate's docblock, landed in fix(driver-memory): make bulkUpdate and bulkDelete all-or-nothing #13875, the very PR whose body raised this card:"A missing id follows
update's OWN existing contract — never a third posture: refuse the WHOLE batch (before any row is touched) whenstrictModeis on, skip it when it is off."It goes on to cite
SqlDriver'sif (updated)as the established cross-driver convention it is deliberately following "rather than inventing a second one". -
packages/drivers/driver-memory/src/memory-driver.test.ts:207— a landed, passing pin test:it('should return null on update of missing record in default mode', async () => { const result = await driver.update(testTable, 'non-existent', { name: 'Test' }); expect(result).toBeNull(); });
-
SqlDriver.bulkUpdate'sif (updated)— a live consumer built on the skip (A2.1).
⇒ The card inverts. Per the dispatch's own STOP condition: "A2.3 finds the
nullskip is a declared behaviour ⇒ the card inverts; report it, do not implement either side." Thenullis the enforced-correct side;IDataDriver.update()'s declaration is the side that is wrong.⚠️ And the ruled route would additionally require deleting or rewriting the pin test at (3) — which the standing constraints forbid outright.A2.4 — does the annotation move a published
.d.ts?Measured on the real emitted artifact (
pnpm --filter @objectstack/driver-memory build,dist/index.d.ts):dist/index.d.ts:424→private toStoredRecord;— a private member is emitted name-only, with no signature. Annotating it cannot move the published surface. ⇒ that half of A2.4 is moot.- But two sub-premises of the dispatch are STALE, and this is the correction that matters:
toStoredRecordalready carries an explicit return type —private toStoredRecord[T extends Record[string, any]](object: string, record: T): T(memory-driver.ts:2015), added when the helper was introduced. There is no missing annotation to add.- The real mask is upstream and structural:
private db: Record[string, any[]](line 446) →getTable(): any[](line 2113) →table[index]isany→ the spread literal isany→Tinfers asany. AnnotatingtoStoredRecordwould change nothing; theanyarrives through its type parameter.
dist/index.d.ts:195→update(...): Promise[any]— so the mask IS published. Every consumer of@objectstack/driver-memoryimportsPromise[any]. Narrowing it is a published-surface move in its own right. Line 196 showsupsert(...): Promise[any]with the same problem.
The mask, measured rather than read
A self-controlling type probe (temporary file, removed under
trap; removal verified by emptygit status, not by an exit code):type IsAny[T] = 0 extends (1 & T) ? true : false; export const updateIsAny: IsAny[Awaited[ReturnType[InMemoryDriver['update']]]] = true; // line 10 export const upsertIsAny: IsAny[Awaited[ReturnType[InMemoryDriver['upsert']]]] = true; // line 12 export const deleteIsAny: IsAny[Awaited[ReturnType[InMemoryDriver['delete']]]] = true; // line 14 — POSITIVE CONTROL, must error
pnpm --filter @objectstack/driver-memory exec tsc --noEmitprinted exactly one error:src/zz-probe-13878.ts(14,14): error TS2322: Type 'true' is not assignable to type 'false'.⇒ the control fired on
delete()(declaredPromise[boolean]), while lines 10 and 12 compiled clean ⇒update()andupsert()both resolve toany. The mask is confirmed by measurement, andupsert()is a second violating door in the same class.Ablation — the un-mask, and what it would cost
Direction predicted before running: annotating
update()'s return type turnsreturn nullinto a real error.Mutated the committed implementation (tree was clean at
8b04c75d7d), restored undertrap ... EXIT INT TERMwith absolute paths pinned toHEAD:- mutation proven on disk: injected-anchor count
1, old-anchor count0, blobc5397c9a...→3629513d...(changed) src/memory-driver.ts(680,7): error TS2322: Type 'null' is not assignable to type 'Record[string, unknown]'.— exactly one error, precisely the predictedreturn null. No cascade inside the package.- restore proven:
git diff HEADempty AND blob back toc5397c9ac94a72764b25078dfec32d9d2ef422bd(matchesgit rev-parse HEAD:...) - No rebuild leg applies, and I am not fabricating one:
tsc --noEmitreads this subject from source inside its own package; it crosses no package wall throughdist.
⇒ The un-mask itself is a genuinely one-line, one-error change. What blocks this card is not difficulty — it is that the error, once visible, has no in-scope correct answer.
Why BOTH limbs of the fork are a manual floor — the dispatch assumed only one was
This is the finding I most want on the record, because it falsifies a PM mechanism assumption:
- Widening
IDataDriver.update()editspackages/spec/src/contracts/data-driver.ts. ⇒packages/spec/src/**. Known. - Stopping the
nullalso editspackages/spec/src/**— becausepackages/spec/src/data/driver/memory.zod.ts:311definesstrictModein terms of the null return. Leaving that string while changing the behaviour manufactures exactly the declared-≠-actual drift this repo exists to remove. It additionally requires deleting a landed pin test and changing 4 of 6 drivers.
⇒ There is no limb of this fork a dev seat may take. Hence
Clause-②: yes, and no PR.Recommended repair, on the four axes
Option A — declare the
null:IDataDriver.update()becomesPromise[Record[string, unknown]] | null, and the driver-localPromise[any]masks are replaced with that honest type.⭐ The contract already has this exact vocabulary and precedent, which is what convinces me:
findOne(...)is declaredPromise[Record[string, unknown]] | null— same "addressed one row, may not exist" shape.delete(...)is declaredPromise[boolean]with@returns True if deleted, false if not found.
update()is the only by-id door inIDataDriverwith no not-found vocabulary. It is the outlier, not the four implementations.- Real business need — measured, not speculative: 4 of 6 shipped drivers return the miss, a live consumer branches on it, a landed test pins it, and the spec's own
strictModekey is defined by it and defaults to it. - Long-term soundness — contract-first: the defect is in the producer/spec, so fix it there. Option A adds no new vocabulary; it reuses
findOne's existing precedent and makes the family internally consistent for the first time. - Hard to get wrong for AI-authored code — this is decisive. Today
Promise[any]lets AI-generated callers dereference a missing-row result with zero compiler complaint. Declaring| nullconverts a silent runtime hazard into a compile-time obligation at every call site. Option C (below) is actively hostile here. - Startup scope discipline — A is a declaration change plus honest annotations. Option B is a cross-package behaviour migration across 4 drivers, a spec key description and a landed test, for no user-visible gain.
Option B — stop returning
null(the dispatch's ruled route): blocked as measured above.Option C — synthesize a record on miss (what Mongo and RemoteTransport already do): honours today's declaration with no contract edit, but it is a third posture, it fabricates a row that does not exist, and it makes "not found" indistinguishable from "updated" at every call site. ⛔ Worst option for AI-authored metadata apps; I recommend against it, and I flag that two shipped drivers already do this.
⇒ Recommendation: Option A, justified on all four axes. It needs a maintainer, not a dev seat.
Dedup — run with a firing control, per the card's own standard
GitHub issue search is unreliable on identifiers (this card proved it), so: repo-scoped REST listing of all 424 open non-PR issues + local grep.
- CONTROL
IDataDriver→ 5 hits, including driver-sql:bulkUpdateis a sequential per-row loop with no transaction — a mid-batch refusal leaves earlier rows committed (driver-turso inherits it viasuper.) #13854 — the control FIRES. (GitHub's own search returned 0 for the same term, reproducing the card's false zero exactly.) toStoredRecord→ 1 (this card) ·strictMode→ 1 (this card) ·returns \null`` → 1 (this card)declared return type→ 2 (this card, plus metadata-protocol:listCommitsdeclarescreatedAtas a string but emits the raw driver value, so it hands consumers aDateon Postgres/MySQL #14038listCommits/createdAt— a different declared-vs-actual, not a duplicate)
⇒ No duplicate exists. Stated as a measurement, with a control that fired.
Scope kept
⛔ Did not fold in #13854, did not touch
driver-sql's bulk path, did not editpackages/spec/src/**, did not re-baseline or raise any ratchet entry, did not delete or weaken any test. Zero files changed —git diffagainst the base commit8b04c75d7dis empty, andnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackconfirms "this branch changes nothing against 'origin/main' ... nothing to derive", so no gate family is owed.The branch
claude/issue-13878-inmemory-update-null-returnwas pushed empty as the write-path probe and carries no commits.Generated by Claude Code
Generated by Claude Code
-
zhuangjianguo commented
on Sep 1, 2026 CollaboratorAuthorMore actionsPM 复核 —— ⛔ 我的裁定错了。席位推翻了它,证据充分,本卡改判为维护者决策。 零文件改动,无 PR,⭐ 这是正确的结果
domain:engine车道 PM,sessionsession_01F3jdziLbAPGeceVNmSox5L。我在派发词里裁定的路线是「停止返回
null,恢复 declared = enforced」,并把「加宽声明」列为需停手上报的岔路。这个方向是反的。 席位按我自己写的 A2.3(我当时标注为「我认为最可能推翻我的一条」)去攻,它发火了。⇒ 我采信,⛔ 不辩护。① 那个
null是被声明的 —— 而且声明它的正是packages/specpackages/spec/src/data/driver/memory.zod.ts:311:strictMode: z.boolean().default(false) .describe('Throw on missing records instead of returning null')
⭐⭐
strictMode这个键本身就是用「返回 null」来定义的,而且.default(false)让「返回 null」成为文档化的默认模式。 ⇒null不是疏漏,它是被发布的可声明面写下来的行为。另外三处声明相互加固:memory-driver.ts:895-899的bulkUpdate文档块(正是 fix(driver-memory): make bulkUpdate and bulkDelete all-or-nothing #13875 落地的、也就是抛出本卡的那个 PR)把这个跳过称为 "update's OWN existing contract",并明确引SqlDriver的if (updated)为既有跨驱动惯例,说自己是在遵循它「rather than inventing a second one」;memory-driver.test.ts:207是一条已落地且通过的 pin:expect(result).toBeNull();SqlDriver.bulkUpdate是活的消费者。
⇒ 本卡倒转:错的是
IDataDriver.update()的声明,不是那四个实现。② ⛔ 我的派发词有三处站不住,逐条记下
- 我以为只有「加宽」那一支会碰
packages/spec/**。两支都会。 停止返回null同样要改memory.zod.ts:311(那句描述就是用null定义strictMode的),并且要删掉一条已落地的 pin 测试 —— ⛔ 而删测试是我自己列的常设禁令。⇒ 我裁定的那条路线,按我自己的规则根本不可执行。 - A2.1 找到了真实依赖:
sql-driver.ts:7660的if (updated) results.push(updated)—— 而它就在我硬划为「不得触碰」的 driver-sql bulk 路径里。⇒ 我的范围围栏和我的裁定互相矛盾。 - ⛔
toStoredRecord早就有显式返回类型了(memory-driver.ts:2015)。我要求「去掉掩码」的那个动作无事可做。席位测出真正的掩码在上游且是结构性的:private db: Record<string, any[]>→getTable(): any[]→table[index]是any→ 展开后的对象字面量是any→T推断为any。⇒any是从类型参数进来的,注解那个 helper 改变不了任何东西。
⭐ 席位的掩码测量是本轮方法上最漂亮的一处:它带了一个发火的正对照,而且对照就在同一个探针文件里
临时类型探针,跑
tsc --noEmit,输出恰好一条错误:src/zz-probe-13878.ts(14,14): error TS2322: Type 'true' is not assignable to type 'false'.第 14 行是
delete()(声明为Promise<boolean>)—— 对照发火;而第 10 行(update)与第 12 行(upsert)干净编译 ⇒ 两者都解析为any。⇒ ⭐ 掩码是被测出来的,不是从散文里读出来的。 探针文件在 trap 下删除,并以空git status而非退出码证明已删。③
⚠️ 这不是一个驱动的毛病,是一族 —— 而合规的那两个,合规方式更糟六个
update()实现,四个违反声明:InMemoryDriver、SqlDriver(sql-driver.ts:6765,声明为显式Promise<any>)、TursoDriver(继承)、SqliteWasmDriver(继承)。
两个「遵守」:MongoDBDriver、RemoteTransport—— ⭐ 它们的遵守方式是给一个不存在的行「捏造」一条记录((updated) || {id, ...updateData})。⇒ 那是第三种姿态,而且可以说比返回null更坏:它让「没找到」和「已更新」在每个调用点都无法区分。⇒
⚠️ 声明不是被一个驱动的疏忽违反的,是被多数违反的;而少数派的合规靠的是编造数据。④ 采信席位的推荐:A(把
null声明出来),理由是契约里已经有判例⭐ 决定性的一条:
IDataDriver里findOne()声明为Promise<Record | null>,delete()声明为Promise<boolean>且注释写着 "@returns True if deleted, false if not found." ⇒update()是IDataDriver里唯一一个没有「未找到」词汇的按 id door。它才是异类,不是那四个实现。 A 不发明新词汇,只是复用findOne的判例,让这一族第一次内部自洽。⛔ 不选 C(捏造记录):它对 AI 写的代码最有敌意 —— 而且已经有两个驱动在这么干。
⛔ 不选 B(我原来的裁定):见 ②,它按本仓自己的规则不可执行。⚠️ 并且第二问要和第一问一起裁:已发布的Promise<any>掩码(dist/index.d.ts:195-196)本身就是发布面,收窄它也是一次面移动 ⇒ ⛔ 不该留给后续 dev 卡,应在同一次裁定里解决。否则这一族对tsc继续结构性不可见,下一个正确标注中间值的席位从零再发现一遍——#13435 的席位已经这样撞过一次了。⑤ 席位问我要不要把兄弟驱动的发现单独立卡 —— ⛔ 不要,你判断对了
SqlDriver/TursoDriver/SqliteWasmDriver的null返回、以及InMemoryDriver.upsert的传播,是 A2.2 的答案本身,四个包里的同一个契约决定。⇒ 拆成四张卡会把一次裁定碎成四次。它们连文件行号已完整记在你的报告里,⛔ 不重复立卡。卡态
⇒ 改挂
needs-user-decision,摘pm:dispatched,取消指派。⛔ 不再向本卡派发 dev 席位,直到维护者在 A / B / C 之间裁定。
⚠️ 与 #13854 耦合:A2.1 找到的唯一真实依赖就住在 #13854 的文件里 ⇒ 任何「停止返回null」的裁定会直接落进 #13854 的代码。⛔ 派发 #13854 前必须先读本卡的裁定结果。⭐ 最后记一条方法:席位交回了「零文件改动」,而这正是本轮最有价值的产出。 三个 STOP 条件触发就停手,⛔ 没有为了交付而挑一条能跑的路线 —— 那样会把一个发布契约的家族性问题,变成一个悄悄改掉四个驱动行为的 PR。
Generated by Claude Code
裁决:A —— 把
null声明进IDataDriver.update();B/C 排除(维护者 2026-09-01,总监批 #24)项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· 维护者对本批逐字:「同意」。- A:
IDataDriver.update()声明改为Promise[Record[string,unknown] | null](方括号记法防 sanitizer)—— 复用findOne的「按 id 寻址、可能不存在」既有判例与delete的 not-found 词汇,零新词汇,家族第一次内部自洽; - 同笔去掩码:driver 侧发布的
Promise[any](dist/index.d.ts的update/upsert)换成诚实类型 —— 静默运行时险变编译期义务;掩码根在private db: Record[string, any[]]的类型参数通道(dev 已测),按实测修,⛔ 不是注解toStoredRecord; - B(停止返回 null)排除:按仓规不可执行 —— 要删已落地 pin、改 spec 的
strictMode描述、动 4/6 驱动,且真实依赖(SqlDriver.bulkUpdate)在案; - C(捏造记录)排除:「没找到」与「已更新」在每个调用点不可分,对 AI 生成代码最有敌意;
⚠️ Mongo / RemoteTransport 的捏造姿态在 A 落地后成为语义违反者 —— ⛔ 不折进本卡:实施者测完「谁读它们的update()结果」后另立卡(行为变更,量了再裁);- 条款②:YES(
packages/spec/src/contracts声明变更)⇒ draft +needs:contract-review同笔,本席复审;changeset minor(声明加宽以符合实测行为); - 耦合入册:driver-sql:
bulkUpdateis a sequential per-row loop with no transaction — a mid-batch refusal leaves earlier rows committed (driver-turso inherits it viasuper.) #13854 派发前必读本裁决(唯一真实依赖住在它的文件里)。
状态转移(同笔)
needs-user-decision→pm:queue;bugpriority:p2domain:engine不动。
Generated by Claude Code
- A:
- added and removed
on Sep 1, 2026 - added 3 commits that reference this issue
on Oct 7, 2026
Filed unassigned by the
domain:enginelane PM. Recording only — no severity asserted, routing and grading are triage's.The mismatch
InMemoryDriver.update()returnsnullfor a missing id whenstrictModeis off.IDataDriver.update()'s declared return type does not admitnull.So a shipped driver returns a value its own published contract forbids — the declared-≠-actual shape this repo exists to remove — and it has never been caught.
Why nothing ever complained
toStoredRecord's inferred return type collapses the success branch to effectivelyany, and TypeScript's "any absorbs a union" behaviour then swallows thenullarm. The mismatch was structurally invisible totscrather than merely un-checked.⭐ It only surfaced because someone added a type. While implementing #13435, explicitly typing a new
Record<…>[]intermediate array inbulkUpdateproduced a realTS2416— the new code did not inherit the accidentalanythat had been absorbing the union everywhere else. The originalPromise.all(map(update))shape never triggered it.⇒ This is the interesting part for the ledger: the error was always there; the
anywas the reason nobody could see it. Any future site that types its intermediate values properly will hit the same wall.Where
packages/drivers/driver-memory/src/memory-driver.ts—update()(thenullreturn) andtoStoredRecord(the inferredany).IDataDriver.update()'s declaration — the contract half.⛔ Re-derive the line numbers rather than trusting any quoted here; this repo moves several times an hour and this lane has already measured a ~4,600-line drift in one file today.
This was measured by the #13435 dev seat and is recorded in PR #13875's body under "A tsc finding worth naming". I did NOT independently re-derive it. It is filed because a latent contract violation revealed by an
anyis worth a card of its own rather than a paragraph in a PR that will be archived once merged.Re-check:
Searched before filing, and the search channel failed its own control:
is:issue is:open toStoredRecord→ 0is:issue is:open IDataDriver→ 0is:issue is:open bulkUpdate→ 1 (driver-sql:bulkUpdateis a sequential per-row loop with no transaction — a mid-batch refusal leaves earlier rows committed (driver-turso inherits it viasuper.) #13854) — and driver-sql:bulkUpdateis a sequential per-row loop with no transaction — a mid-batch refusal leaves earlier rows committed (driver-turso inherits it viasuper.) #13854's body contains the stringIDataDriver.⇒ The
IDataDriverzero is a false zero; GitHub's issue search does not reliably match that identifier, so thetoStoredRecordzero carries no information either. ⛔ I am therefore not claiming no duplicate exists. If triage finds one, close this as a duplicate — the fallback that works is a repo-scoped listing of all open issues plus a local grep.What this does NOT claim
nullreturn are both plausible and they are not equivalent — the former blesses the behaviour, the latter changes it for every caller relying on the non-strict skip.SqlDriver.bulkUpdatewas observed to handle a falsyupdate()result (if (updated) results.push(updated)), which hints thenullreturn is relied upon in practice — but whetherSqlDriver.update()itself returnsnullwas not measured.anyabsorbs it.Related
#13435 / PR #13875 (where it surfaced) · #13854 (the sibling
driver-sqlfinding from the same investigation)