Skip to content

A CEL defaultValue stores a raw Date into datetime/date fields — the stored-value contract says ISO-8601 string, and the NOW() token normalizes but the CEL branch does not #7373

Description

@os-help

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions