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

Closed

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. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 10, 2026
  3. 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

  4. 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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions