Skip to content

fix(driver-sql): Field.date is a tz-naive YYYY-MM-DD calendar day (ADR-0053 Phase 1) - #1968

Merged
os-zhuang merged 1 commit into
mainfrom
fix/adr-0053-date-only-storage
Jun 16, 2026
Merged

fix(driver-sql): Field.date is a tz-naive YYYY-MM-DD calendar day (ADR-0053 Phase 1)#1968
os-zhuang merged 1 commit into
mainfrom
fix/adr-0053-date-only-storage

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

Implements Phase 1 of ADR-0053: make Field.date a timezone-naive YYYY-MM-DD calendar day end-to-end at the SQL-driver boundary. Field.datetime is untouched (keeps full-instant UTC-ms semantics).

Refs ADR-0053, #1928.

Why — the silent date-equality miss

A Field.date ("close date", "due date", "birthday") is semantically a calendar day, but the driver treated it as an instant. The three layers disagreed:

LayerOld behaviour
Write (formatInput)stored the value verbatim — a Date/ISO kept its time (dev.db held close_date = "2026-07-15T17:24:56.533Z")
Filter (coerceFilterValue)already normalized the comparand to date-only YYYY-MM-DD
Read (formatOutput)no normalization — returned the stored timestamp

So close_date == '2026-07-15' compared "2026-07-15T17:24Z" against "2026-07-15"never equal. The same broke $in of dates and daysFromNow(n)-style equality comparands. Range filters ($gte/$lt) only worked by accident of lexicographic ISO ordering.

What changed (packages/plugins/driver-sql/src/sql-driver.ts)

  • Shared helpertoDateOnly() — single source of truth for date-only truncation (Date → UTC calendar day; string → leading YYYY-MM-DD). coerceFilterValue now delegates to it, so filter/write/read all agree.
  • Write (formatInput): every Field.date value collapses to YYYY-MM-DD before insert/update, matching the filter's existing contract. Dialect-agnostic.
  • Read (formatOutput): Field.date returned as YYYY-MM-DD, slicing any stored time — transparently repairs legacy timestamped rows on read, no data migration. Read normalization now runs on the find path for every dialect too (previously only findOne); the json/boolean deserialisation stays SQLite-gated internally.

Out of scope (ADR-0053 Phase 2, needs separate review)

Timezone-aware today()/daysFromNow()/daysAgo(), org/user reference timezone, datetime render-time TZ. This PR is the low-risk root fix; it composes with the #1950 build lint (flow-date-equality-filter), which becomes a belt-and-suspenders hint.

Testing

  • New suite sql-driver-date-only.test.ts (8 tests): Date/ISO/date-only write→read round-trips collapse to YYYY-MM-DD; Field.datetime keeps its instant; the silent-miss regression (date-only equality matches a timestamped write); $in of days; range filters; and legacy-row read-repair + that an in-place rewrite makes a legacy row filter-matchable.
  • Full driver-sql suite green: 160 tests (152 existing + 8 new).
  • pnpm --filter @objectstack/example-crm build ✓ (ADR-0032 expression validation passes, so the days_to_close formula still compiles).

⚠️ Note for pg/mysql real DATE columns: a Date returned by the driver collapses on its UTC calendar day (consistent with the pre-existing coerceFilterValue contract). SQLite (TEXT affinity) is fully correct; a native-type parser for pg/mysql is left for a follow-up if needed.

🤖 Generated with Claude Code

…endar day (ADR-0053 Phase 1)
A `Field.date` is semantically a timezone-naive calendar day, but the SQL
driver treated it as an instant: `formatInput` wrote the value verbatim
(keeping its time component) while `coerceFilterValue` already normalized
filter comparands to date-only `YYYY-MM-DD`. That write/filter asymmetry
made date-equality filters (`close_date == '2026-07-15'`, `$in`,
`daysFromNow(n)`-style comparands) silently match nothing.
Align the write/read boundary with the filter's existing date-only contract:
- Extract the date-only truncation into a shared `toDateOnly` helper used by
the filter, write, and read paths so all three agree on what a date is.
- `formatInput`: collapse every `Field.date` value (Date | full-ISO string |
date-only string) to `YYYY-MM-DD` before insert/update. A Date collapses to
its UTC calendar day, matching `coerceFilterValue`.
- `formatOutput`: return `Field.date` as `YYYY-MM-DD`, slicing any stored time
component — transparently repairing legacy timestamped rows on read with no
data migration. Read normalization now runs on the `find` path for every
dialect (previously only `findOne`); the json/boolean deserialisation stays
SQLite-gated internally.
`Field.datetime` is unchanged (full-instant UTC-ms semantics).
Out of scope (ADR-0053 Phase 2): tz-aware today()/daysFromNow()/daysAgo(),
org/user reference timezone, datetime render-time TZ.
Refs ADR-0053, #1928
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercelBot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJun 16, 2026 3:33pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/core/plugins.mdx(via @objectstack/driver-sql)
  • content/docs/concepts/implementation-status.mdx(via @objectstack/driver-sql)
  • content/docs/concepts/terminology.mdx(via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx(via @objectstack/driver-sql)
  • content/docs/guides/driver-configuration.mdx(via @objectstack/driver-sql)
  • content/docs/guides/packages.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/objectos/index.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/driver-sql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang