Found while measuring the replacement samples for #7244 (docs-only card; this is the code-side divergence that measurement turned up). Filed unassigned for triage. Measured on origin/main @ f188ed60e.
What was measured
applyFieldDefaults (packages/objectql/src/engine.ts:2490) has three interesting branches for a defaultValue:
- an expression envelope
{ dialect, source } — evaluated by ExpressionEngine, and the result assigned verbatim: out[f.name] = result.value;
- the
NOW() token — routed through resolveNowDefault(f.type, now), which produces the value in the storage shape the declared type calls for;
- everything else — the literal.
The CEL branch has no counterpart to resolveNowDefault. daysFromNow(n) / today() / now() return a JS Date (ADR-0053 D1: the reference-tz calendar day as a UTC-midnight Date), so the Date is what lands in storage — while valueSchemaFor says an instant is an ISO-8601 string.
Probe, memory driver, engine insert with due_datetime: { type: 'datetime', defaultValue: { dialect: 'cel', source: 'daysFromNow(7)' } }:
PROBE REPLACEMENTS insert OK. stored due_datetime = "2026-08-17T00:00:00.000Z" | typeof object | ctor Date
PROBE stored due_datetime vs valueSchemaFor(datetime,stored): FAIL :: ["Invalid input: expected string, received Date"]
The same declaration on a date field is the same story, one shape further off:
PROBE date+CEL stored due_date = "2026-08-17T00:00:00.000Z" | typeof object
PROBE stored due_date vs valueSchemaFor(date,stored): FAIL :: ["Invalid input: expected string, received Date"]
The NOW() token on the identical field is the control, and it is clean:
PROBE NOW() token stored created_at = "2026-08-10T07:58:46.907Z" | typeof string
PROBE stored created_at vs valueSchemaFor(datetime,stored): PASS
(JSON.stringify renders a Date as its ISO string, which is why the first two lines read correct — typeof object / ctor Date is the actual shape.)
Why it matters
Not the same defect as #7127 (Blocked-by: nothing). #7127 is about whether the declaration shape is accepted at author time; this is about what the evaluated value is normalized to at store time. A fix for #7127 would still leave a Date in the column.
Suggested shape (not a ruling)
Route the CEL branch's result through the same resolveNowDefault-style per-type normalization the NOW() token uses, so a temporal expression default stores what the declared type's contract names. Whether that normalizer is shared or type-dispatched is a design call for whoever picks this up.
Refs: #7244 (the docs card this came out of), #7127, #4597 / #4560 (the NOW()-token precedent), ADR-0053, ADR-0104.
Generated by Claude Code
Found while measuring the replacement samples for #7244 (docs-only card; this is the code-side divergence that measurement turned up). Filed unassigned for triage. Measured on
origin/main@f188ed60e.What was measured
applyFieldDefaults(packages/objectql/src/engine.ts:2490) has three interesting branches for adefaultValue:{ dialect, source }— evaluated byExpressionEngine, and the result assigned verbatim:out[f.name] = result.value;NOW()token — routed throughresolveNowDefault(f.type, now), which produces the value in the storage shape the declared type calls for;The CEL branch has no counterpart to
resolveNowDefault.daysFromNow(n)/today()/now()return a JSDate(ADR-0053 D1: the reference-tz calendar day as a UTC-midnightDate), so theDateis what lands in storage — whilevalueSchemaForsays an instant is an ISO-8601 string.Probe, memory driver, engine insert with
due_datetime: { type: 'datetime', defaultValue: { dialect: 'cel', source: 'daysFromNow(7)' } }:The same declaration on a
datefield is the same story, one shape further off:The
NOW()token on the identical field is the control, and it is clean:(
JSON.stringifyrenders aDateas its ISO string, which is why the first two lines read correct —typeof object/ctor Dateis the actual shape.)Why it matters
Date), so nothing refuses the write — the divergence is silent.os migrate value-shapeswalks stored values against this veryvalueSchemaFor, so rows defaulted this way are a violation by the platform's own scan.Dateat the wire, the memory driver stores the object as-is. Same declaration, different stored shape per datasource — the exact split data: theNOW()defaultValue token is unresolved on non-SQL datasources — the engine stamps the literal string, and the record validator then rejects the insert #4597 / data: the SQL driver emits thecurrent_userframework token as a literal column DEFAULT, so the database itself writes a non-id into alookup('sys_user')#4560 closed for theNOW()token, reappearing on the CEL branch.defaultValuesamples that are wrong today — a bare CEL source string stored verbatim (and emitted as a column DEFAULT), and a currency object literal the engine stores but the SQL DDL silently drops #7244 lands.Not the same defect as #7127 (
Blocked-by:nothing). #7127 is about whether the declaration shape is accepted at author time; this is about what the evaluated value is normalized to at store time. A fix for #7127 would still leave aDatein the column.Suggested shape (not a ruling)
Route the CEL branch's result through the same
resolveNowDefault-style per-type normalization theNOW()token uses, so a temporal expression default stores what the declared type's contract names. Whether that normalizer is shared or type-dispatched is a design call for whoever picks this up.Refs: #7244 (the docs card this came out of), #7127, #4597 / #4560 (the
NOW()-token precedent), ADR-0053, ADR-0104.Generated by Claude Code