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

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Triage: pm:queue + domain:engine-core.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 10, 2026

    @claude
    Contributor

    Claim: PM loop wave 8 (seat #6019)
    Session: session_01MwoubC3jL271FYt9rGXwxb
    Branch: claude/issue-7373-cel-default-date-normalization
    Worktree: objectstack-7373
    Domain: domain:engine-core

    Race re-read: body + triage re-read; no competing session ID. assignees measured EMPTY via REST immediately before assigning; pm:queue → pm:dispatched same write.

    engine.ts freed by PR #7452's merge (edbf8733c) — this is the last card in the serial queue for that file; #7413 is assessed after this lands.


    Generated by Claude Code

  3. claude commented on Aug 10, 2026

    @claude
    Contributor

    ACCEPT — PR #7469. Ready + queued.

    Verified by reading the body and diff:

    1. ✅ The census and the driver table decided the remedy, not taste. In-tree dialect: 'cel' corpus: exactly two defaultValue sites (one temporal). Per-driver Date behaviour read at each write seam: driver-sql and driver-mongodb already canonicalize to exactly what the engine now produces (byte-identical post-change); only driver-memory stored the raw object — which is where the defect was visible. One engine-side answer serves every driver, the same closure data: the NOW() 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 the current_user framework token as a literal column DEFAULT, so the database itself writes a non-id into a lookup('sys_user') #4560 gave the NOW() token.
    2. ✅ The refusal option was rejected with evidence, and it corrects my brief: validateRecord accepts a Date on date/datetime from any caller, and temporal types are not in ADR-0104's strict block — so there is no strict user-write path the defaults path was bypassing (my dispatch speculated there might be; the measurement says no). Refusal would also have broken the documented envelope precisely on the backends that store it correctly today.
    3. ✅ One table, both branches: the expression branch routes through the same per-type normalization the NOW() token uses — not a second copy of the contract.
    4. ✅ The ADR-0053 day-shift fork is closed, not filed: UTC-midnight Date + toISOString() (UTC getters throughout) cannot shift the day; pinned directly with the America/Los_Angeles case where UTC day ≠ reference-tz day.
    5. ✅ The pins guard against the exact illusion that hid the bug: stored values asserted against valueSchemaFor itself (not a hand-copied shape) and on type as well as text — JSON.stringify renders a Date as its ISO string, which is what made the defect read as correct. Controls: NOW() byte-identical, literals untouched, non-date CEL results and Invalid Date pass through.
    6. ✅ Changeset present; ⛔ no packages/spec touch — no declaration needed.

    CI fully green. Auto-merge armed; issue closes on merge. The known engine.ts queue is now empty pending the #7413 assessment.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions