Skip to content

Commit e70faaa

Browse files
committed
docs(adr-0053): amend D-F1 — findWithWindowFunctions is a covered read door
PR #16716 (card #16609) routes findWithWindowFunctions rows through the same `formatOutput` pass `find()` runs, so ADR-0053's three statements that the door applies no read presentation are false on the tree as of that merge. Three carriers amended, all prose, all re-derived by symbol: - `:3` the Status line — the "but `findWithWindowFunctions`" exception, which is the ADR's summary for a reader who never opens the addendum - `:1081` D-F1's body — "is not one of these doors" - `:1157` the Consequences bullet — "Not covered: ... applies no read presentation of any kind today" Each amendment states separately what the door moves (the columnMap row-KEY rename, JSON, numeric strings, the two instant classes, boolean, date, time) and which of it D-F1 governs (the two instant classes only), so the correction does not replace one overstatement with another. The window ALIAS carve-out is recorded at each site: a computed alias wins the key and its value stays raw. D-F3's Invalid `Date` carve-out is untouched, verbatim, at both sites that carry it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
1 parent d4401f7 commit e70faaa

1 file changed

Lines changed: 26 additions & 5 deletions

File tree

docs/adr/0053-date-and-datetime-semantics.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0053: `date` is a timezone-naive calendar day; `datetime` is an instant rendered in a reference timezone
22

3-
**Status**: Accepted (2026-06-16) — Phase 1 + addendum D-A1 implemented (`sql-driver.ts` `toDateOnly` write/read/filter normalization; analytics `coerceTemporalFilterValue`), Phase 2 landing incrementally; D-A2 resolved 2026-07-30: `temporalFilterValue` + `temporalFilterColumnSql` are optional `IDataDriver` contract members with identity semantics, and analytics types its driver seam from the contract. **Partly superseded (2026-07-29, addendum D-B1..D-B4):** Phase 1's "`Field.datetime` stays stored as UTC epoch ms" is replaced by one canonical UTC instant per dialect — `YYYY-MM-DDTHH:MM:SS.sssZ` text on SQLite, `timestamptz` on Postgres, `DATETIME(3)` on MySQL — applied on write and to filter comparands alike (#3912, #3942). **Extended (2026-07-30, addendum D-C1..D-C3):** `Field.time` takes the same construction — canonical `HH:MM:SS[.fff]` wall-clock text, one function on write/filter/read, `TIME(3)` on MySQL, UTC `NOW()` defaults on every dialect (#3994). **Extended (2026-09-07, addendum D-F1..D-F3):** the READ side takes the same canon — every `@objectstack/driver-sql` record read door but `findWithWindowFunctions` (#16609) presents `Field.datetime` values and the builtin `created_at` / `updated_at` audit stamps as the canonical `YYYY-MM-DDTHH:MM:SS.sssZ` text on every dialect, folded at the driver's read boundary with the client parsers untouched; those doors never hand out a JS `Date` for those columns, save an Invalid `Date`, which has no canonical text and passes through unchanged (#13973, maintainer ruling B1 narrow, 2026-09-02).
3+
**Status**: Accepted (2026-06-16) — Phase 1 + addendum D-A1 implemented (`sql-driver.ts` `toDateOnly` write/read/filter normalization; analytics `coerceTemporalFilterValue`), Phase 2 landing incrementally; D-A2 resolved 2026-07-30: `temporalFilterValue` + `temporalFilterColumnSql` are optional `IDataDriver` contract members with identity semantics, and analytics types its driver seam from the contract. **Partly superseded (2026-07-29, addendum D-B1..D-B4):** Phase 1's "`Field.datetime` stays stored as UTC epoch ms" is replaced by one canonical UTC instant per dialect — `YYYY-MM-DDTHH:MM:SS.sssZ` text on SQLite, `timestamptz` on Postgres, `DATETIME(3)` on MySQL — applied on write and to filter comparands alike (#3912, #3942). **Extended (2026-07-30, addendum D-C1..D-C3):** `Field.time` takes the same construction — canonical `HH:MM:SS[.fff]` wall-clock text, one function on write/filter/read, `TIME(3)` on MySQL, UTC `NOW()` defaults on every dialect (#3994). **Extended (2026-09-07, addendum D-F1..D-F3):** the READ side takes the same canon — every `@objectstack/driver-sql` record read door ~~but `findWithWindowFunctions` (#16609)~~ presents `Field.datetime` values and the builtin `created_at` / `updated_at` audit stamps as the canonical `YYYY-MM-DDTHH:MM:SS.sssZ` text on every dialect, folded at the driver's read boundary with the client parsers untouched; those doors never hand out a JS `Date` for those columns, save an Invalid `Date`, which has no canonical text and passes through unchanged (#13973, maintainer ruling B1 narrow, 2026-09-02). **Corrected 2026-09-08 (#16609 / PR #16716):** that exception is gone — `findWithWindowFunctions` now routes each row through the same `formatOutput` pass, so those two column classes present as the same canonical text there, with the window ALIAS columns carved out (a computed alias wins the key and its value stays raw). D-F1 rules those two classes and no more: the other presentations that pass applies at that door are #16609's contract, not this ADR's.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0032](./0032-unified-expression-layer.md) (unified expression layer — CEL dialect, `today()`/`daysFromNow()`), [ADR-0014](./0014-record-form-field-type.md) (field types)
66
**Consumers**: `@objectstack/spec` (`Field.date`/`Field.datetime`), `@objectstack/driver-sql` (`coerceFilterValue`, `formatInput`/`formatOutput`, `dateFields`/`datetimeFields`), `@objectstack/formula` (`stdlib` time functions, `cel-engine` hydration), `@objectstack/objectql` (`applyFormulaPlan`), schedule/cron executors, report/analytics date bucketing, `sys-user-preference.timezone`.
@@ -1078,8 +1078,21 @@ Postgres and MySQL alike, exactly as SQLite always did:
10781078

10791079
None of these doors hands out a JS `Date` for these columns, save the one
10801080
shape D-F3 names: an Invalid `Date`, which has no canonical text and passes
1081-
through unchanged. `findWithWindowFunctions` is not one of these doors (see
1082-
Consequences; #16609). Declared = enforced:
1081+
through unchanged. ~~`findWithWindowFunctions` is not one of these doors (see
1082+
Consequences; #16609).~~**corrected 2026-09-08 (#16609 / PR #16716): it is
1083+
one of them now, for these two column classes.** It routes each row through the
1084+
same `formatOutput` pass `find()` runs, minus the window ALIAS columns: a
1085+
computed alias wins the key and its value stays raw (`select *` plus an
1086+
`... as ok` window projection yields two `ok` columns and the row object keeps
1087+
the LAST), so no declared field's presentation rule is ever applied to a
1088+
computed value. That pass moves more than this addendum rules — the
1089+
`external.columnMap` row-KEY rename, `Field.object`/JSON, numeric strings,
1090+
`Field.boolean`, `Field.date` and `Field.time` — and D-F1 governs, at that door
1091+
exactly as at the doors listed above, ONLY the two instant classes named here;
1092+
the rest of that pass is #16609's contract, pinned by
1093+
`sql-driver-window-function-output.test.ts` (door-to-door agreement with
1094+
`find()` on SQLite; the Postgres and MySQL cells under `Temporal Conformance
1095+
(live PG + MySQL)`). Declared = enforced:
10831096
`sql-driver-13973-canonical-iso-read-door.test.ts` asserts it per cell of the
10841097
D-A3 driver axis for every door listed — `bulkCreate()` over the rows a
10851098
dialect's bulk insert returns (MySQL, with no `RETURNING`, returns none, and
@@ -1154,9 +1167,17 @@ itself.
11541167
`Date` arm sits beside it.
11551168
- D-A3's matrix gains the read-shape cell. `Temporal Conformance (live PG +
11561169
MySQL)` is the job that proves it; its package set is not widened.
1157-
- Not covered: `findWithWindowFunctions`, which applies no read presentation of
1170+
- ~~Not covered: `findWithWindowFunctions`, which applies no read presentation of
11581171
any kind today (booleans, dates and JSON included) — a pre-existing gap of its
1159-
own, recorded rather than folded in.
1172+
own, recorded rather than folded in.~~**corrected 2026-09-08 (#16609 / PR
1173+
#16716): that gap was a card of its own and it landed.** The door now runs
1174+
each row through the same `formatOutput` pass `find()` runs, so it presents
1175+
the two instant classes D-F1 rules exactly as the doors listed above do — and
1176+
also the classes D-F1 does NOT rule (the `external.columnMap` row-KEY rename,
1177+
`Field.object`/JSON, numeric strings, `Field.boolean`, `Field.date`,
1178+
`Field.time`), which are #16609's contract rather than this ADR's. Carved out
1179+
at that door: the window ALIAS columns — a computed alias wins the key and its
1180+
value stays raw.
11601181

11611182
### Options not taken
11621183

0 commit comments

Comments
 (0)