You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Filed unassigned by the dev seat implementing #8489, per the maintainer's 2026-08-16 ruling on that card: "Option C filed as the durable follow-up (driver-side normalization of own-key-undefined as a storage-contract defect) — lands in packages/drivers/*, its own card in that lane; the implementing dev or the identity seat files it with a backlink."
Follow-up to #8489. ⛔ This records the follow-up only — no fix commitment; the lane grades it.
The shape
InMemoryDriver preserves an explicitly-undefined property through create and hands it back from find as an own key holding undefined. Measured on origin/main in a worktree at 2d11ddbe3, against the built driver-memory dist:
SqlDriver cannot express the defective shape at all: a SQL NULL arrives as null, which is a value. So the divergence is between JS-backed drivers and SQL-backed ones, on a row shape only the former can emit.
Why it is a contract defect and not a curiosity
The two states are supposed to be distinguishable and each has a defined meaning — "the key is absent" versus "the key holds a value". An own key holding undefined is neither, and every consumer has to pick a reading:
CEL, measured against the real @objectstack/formula engine, reads it as absent: has(record.status) is false, and record.status == null faults No such key: status.
materializeDeclaredFields (packages/objectql/src/declared-fields.ts) reads it as absent too, deliberately and by documented design — "undefined counts as absent (not just a missing key): CEL treats an own key holding undefined exactly as it treats no key at all."
So the row shape is a fork in the road that every consumer must independently know about, and the platform's own helper carries a dedicated sentence to survive it. That is the cost this card proposes to remove at the producer.
Why fixing it at the producer is the durable half
#8489 converged plugin-sharing onto the canonical binder, which resolves the disagreement for that seam. It does not stop the shape being emitted, so the next consumer that reasons about key presence re-acquires the bug. Normalising at the driver — a declared field whose stored value is undefined is either dropped from the row or returned as null — would make the two binder implementations provably agree on every reachable input, and would let consumers stop carrying the special case.
Scope, as understood from outside the lane
packages/drivers/*. driver-memory is the measured emitter; driver-mongodb is the other JS-backed driver and is worth measuring the same way before deciding, since the two share the family. Note that driver-memory / driver-mongodb investment was frozen under #5499 — whether that freeze covers this is a lane call, not mine.
⚠️ Severity is for triage to grade, and a filing-time grade is unreliable in both directions. Recording honestly what is and is not established: the shape is reachable through a first-party shipped driver (measured above), but reaching it requires in-process code passing an explicitly-undefined property to create — undefined does not survive JSON, so it cannot arrive over the wire. No production consumer was measured. Direction of the consequence at the one consumer studied so far (#8489's eligibility gate) was fail-closed before that card and is fail-closed after it.
Filed unassigned by the dev seat implementing #8489, per the maintainer's 2026-08-16 ruling on that card: "Option C filed as the durable follow-up (driver-side normalization of own-key-
undefinedas a storage-contract defect) — lands inpackages/drivers/*, its own card in that lane; the implementing dev or the identity seat files it with a backlink."Follow-up to #8489. ⛔ This records the follow-up only — no fix commitment; the lane grades it.
The shape
InMemoryDriverpreserves an explicitly-undefinedproperty throughcreateand hands it back fromfindas an own key holdingundefined. Measured onorigin/mainin a worktree at2d11ddbe3, against the builtdriver-memorydist:The control leg, same driver, same object — a declared field simply never written — behaves correctly and omits the key entirely:
SqlDrivercannot express the defective shape at all: a SQL NULL arrives asnull, which is a value. So the divergence is between JS-backed drivers and SQL-backed ones, on a row shape only the former can emit.Why it is a contract defect and not a curiosity
The two states are supposed to be distinguishable and each has a defined meaning — "the key is absent" versus "the key holds a value". An own key holding
undefinedis neither, and every consumer has to pick a reading:@objectstack/formulaengine, reads it as absent:has(record.status)isfalse, andrecord.status == nullfaultsNo such key: status.materializeDeclaredFields(packages/objectql/src/declared-fields.ts) reads it as absent too, deliberately and by documented design — "undefinedcounts as absent (not just a missing key): CEL treats an own key holdingundefinedexactly as it treats no key at all."name in recordpresence test reads it as present. That is what made it a live divergence in plugin-sharing re-derives materializeDeclaredFields instead of importing it — a second copy of the declared-field binding contract, already diverged #8489:plugin-sharingre-derived the declared-field binder with anincheck, and the two copies disagreed on exactly this one input class.So the row shape is a fork in the road that every consumer must independently know about, and the platform's own helper carries a dedicated sentence to survive it. That is the cost this card proposes to remove at the producer.
Why fixing it at the producer is the durable half
#8489 converged
plugin-sharingonto the canonical binder, which resolves the disagreement for that seam. It does not stop the shape being emitted, so the next consumer that reasons about key presence re-acquires the bug. Normalising at the driver — a declared field whose stored value isundefinedis either dropped from the row or returned asnull— would make the two binder implementations provably agree on every reachable input, and would let consumers stop carrying the special case.Scope, as understood from outside the lane
packages/drivers/*.driver-memoryis the measured emitter;driver-mongodbis the other JS-backed driver and is worth measuring the same way before deciding, since the two share the family. Note thatdriver-memory/driver-mongodbinvestment was frozen under #5499 — whether that freeze covers this is a lane call, not mine.undefinedproperty tocreate—undefineddoes not survive JSON, so it cannot arrive over the wire. No production consumer was measured. Direction of the consequence at the one consumer studied so far (#8489's eligibility gate) was fail-closed before that card and is fail-closed after it.Related
undefined比较数在仓内有五种读法 —— #6050 只在 driver-sql/turso 落了拒收,formula 读作「键缺失」、read-scope-sql 编成= NULL、driver-memory 读作 null #6125 — the same word, the other side of the query:undefinedas a filter COMPARAND had five readings across the repo (closed;read-scope-sqlhalf landed in PR fix(analytics): read scope 里的undefined比较数改为拒收,不再编成绑了 undefined 的合法 SQL (#6125) #6390). This card is aboutundefinedin a RETURNED ROW VALUE, which that card did not cover.$notContains(null 值)、$exists(键在值为 null)、$nin(缺键) #5299 —driver-memoryvsformulaon "the field has no value", also matcher-side (closed).