fix(driver-sql): Field.date is a tz-naive YYYY-MM-DD calendar day (ADR-0053 Phase 1)#1968
Merged
Merged
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Implements Phase 1 of ADR-0053: make
Field.datea timezone-naiveYYYY-MM-DDcalendar day end-to-end at the SQL-driver boundary.Field.datetimeis 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:formatInput)Date/ISO kept its time (dev.dbheldclose_date = "2026-07-15T17:24:56.533Z")coerceFilterValue)YYYY-MM-DDformatOutput)So
close_date == '2026-07-15'compared"2026-07-15T17:24Z"against"2026-07-15"→ never equal. The same broke$inof dates anddaysFromNow(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)toDateOnly()— single source of truth for date-only truncation (Date → UTC calendar day; string → leadingYYYY-MM-DD).coerceFilterValuenow delegates to it, so filter/write/read all agree.formatInput): everyField.datevalue collapses toYYYY-MM-DDbefore insert/update, matching the filter's existing contract. Dialect-agnostic.formatOutput):Field.datereturned asYYYY-MM-DD, slicing any stored time — transparently repairs legacy timestamped rows on read, no data migration. Read normalization now runs on thefindpath for every dialect too (previously onlyfindOne); 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,datetimerender-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
sql-driver-date-only.test.ts(8 tests): Date/ISO/date-only write→read round-trips collapse toYYYY-MM-DD;Field.datetimekeeps its instant; the silent-miss regression (date-only equality matches a timestamped write);$inof days; range filters; and legacy-row read-repair + that an in-place rewrite makes a legacy row filter-matchable.driver-sqlsuite green: 160 tests (152 existing + 8 new).pnpm --filter @objectstack/example-crm build✓ (ADR-0032 expression validation passes, so thedays_to_closeformula still compiles).🤖 Generated with Claude Code