You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* fix(formula,spec,core): the RLS write-side check evaluator honours calendar-day upper bounds
matchesFilterCondition — the evaluator behind RLS write-side `check`
policies (ADR-0058 D4) — compared a bare YYYY-MM-DD $lte bound
literally. On a datetime post-image a policy of the shape
{ signed_on: { $lte: '{today}' } } therefore DENIED every write made
after 00:00: the write-side twin of the read-side loss #3777 fixed, and
the last of the platform's filter backends that disagreed about what a
bare day denotes as a bound. The failure direction is what makes it
worth its own change — a rejected write, not a missing row.
$lte and a $between max now evaluate half-open against the next
calendar day, matching the SQL compiler, the memory and mongo drivers
and the analytics preview evaluator. Unchanged per the same semantics
table: full-ISO bounds keep exact-instant semantics, $gte/$gt/$lt keep
their midnight anchoring, a plain YYYY-MM-DD value compares identically
(string ordering makes the forms equivalent), and the evaluator stays
fail-closed on a null bound.
Where the rule lives: nextUtcCalendarDay moves from @objectstack/core to
@objectstack/spec/data, beside date-macros.zod.ts whose vocabulary it
interprets — the macros define which bare days an author can NAME, this
defines what such a day DENOTES as a bound, and both are protocol. Same
species as the pure helpers spec/data already owns, so PD#2 holds.
Chosen over @objectstack/types because it adds no dependency edge: every
consumer already depends on spec, while core does not depend on types.
core re-exports the symbol, so the published surface the drivers and
analytics strategies import from is unchanged.
Rejected: a private copy in formula — the precedent it would cite
(formula's own today()) is debt, not a pattern, and a second copy of
this rule is exactly the divergence #3777 catalogued. Six backends now
share one definition.
Tests: 10 alignment cases in formula (whole-day admit, next-day stop,
plain-date equivalence, full-ISO not widened, gte/gt/lt invariance,
$between min still bounded, impossible day not rolled over, null bound
fail-closed); the primitive's thorough coverage moves to spec, with core
keeping a re-export gate. spec 6943 / core 414 / formula 273 /
driver-sql 508 / driver-memory 194 / driver-mongodb 107 /
service-analytics 329, all green. ADR-0053 gains D-D2 and its D-E4 open
item is closed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f
* docs(queries): a bare calendar day as an upper bound means the whole day
The comparand table said a bare YYYY-MM-DD on a datetime field means
"midnight UTC" full stop. That is true for $gte/$gt/$lt and wrong for
$lte and a $between max, where the day denotes the whole day and
compiles half-open (ADR-0053 D-D). Documents the operator split, when to
pass a full ISO instant instead, and that the rule holds on every
backend including the RLS write-side check evaluator.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f
* chore(spec): refresh the public API-surface snapshot for nextUtcCalendarDay
The API-surface gate diffs spec's built exports against a committed
snapshot; adding `nextUtcCalendarDay` to ./data made it stale and turned
the TypeScript Type Check job red. Regenerated with the gate's own
`gen:api-surface`, so the diff is exactly the one addition — 0 breaking,
1 added, which is what a new export should read as.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/data-modeling/queries.mdx
+27-1Lines changed: 27 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -83,7 +83,7 @@ disagree about shape:
83
83
| Field type | Canonical comparand | Semantics |
84
84
|:---|:---|:---|
85
85
|`date`|`YYYY-MM-DD`| Timezone-naive calendar day. A `Date` collapses to its UTC day; a longer ISO string is truncated to its leading date. |
86
-
|`datetime`|`YYYY-MM-DDTHH:MM:SS.sssZ`| A UTC instant. A `Date`, an epoch-millisecond number, a bare `YYYY-MM-DD` (→ midnight UTC) and a zone-naive `YYYY-MM-DD HH:MM:SS` (→ read as UTC) all fold to this one form. |
86
+
|`datetime`|`YYYY-MM-DDTHH:MM:SS.sssZ`| A UTC instant. A `Date`, an epoch-millisecond number, a bare `YYYY-MM-DD` (→ midnight UTC, but see the whole-day rule below) and a zone-naive `YYYY-MM-DD HH:MM:SS` (→ read as UTC) all fold to this one form. |
87
87
|`time`|`HH:MM:SS`, with `.fff`**only** when the milliseconds are non-zero | Timezone-naive wall clock. `'14:30'` and `'14:30:00'` canonicalise identically, so they are the same filter. |
88
88
89
89
```typescript
@@ -97,6 +97,32 @@ disagree about shape:
97
97
}
98
98
```
99
99
100
+
### A bare calendar day as an UPPER bound means the whole day
101
+
102
+
Canonicalisation settles a comparand's *shape*; which **instant** a bare
103
+
`YYYY-MM-DD` denotes additionally depends on the side of the comparison it sits on:
104
+
105
+
| Operator | A bare `YYYY-MM-DD` means |
106
+
|:---|:---|
107
+
|`$gte` / `$gt` / `$lt`| that day's `00:00:00.000`|
108
+
|`$lte`, and the max of a `$between`| the **whole** day — compiled half-open, as `< next day`|
109
+
110
+
So `{ created_at: { $gte: '2026-01-01', $lte: '2026-03-31' } }` includes
111
+
everything recorded *on* March 31st, not just the instant it began. Without
112
+
that rule a dashboard window ending "today" silently dropped every row created
113
+
after midnight (#3777). Pass a full ISO timestamp when you want an exact
114
+
instant — only the day-granular *string* carries whole-day intent:
115
+
116
+
```typescript
117
+
{ created_at: { $lte: '2026-03-31' } } // through all of March 31st
118
+
{ created_at: { $lte: '2026-03-31T12:00:00.000Z' } } // up to noon exactly
119
+
```
120
+
121
+
On a `date` field the two forms are equivalent (`< next day` and `<= day` order
122
+
identically over `YYYY-MM-DD` text), so the rule needs no special handling
123
+
there. It applies uniformly across the SQL drivers, the in-memory and MongoDB
124
+
drivers, analytics windows, and the RLS write-side `check` evaluator.
125
+
100
126
<Callouttype="info">
101
127
Canonicalisation runs on **every SQL dialect**, not just SQLite — a zone-naive string bound
102
128
into a Postgres `timestamptz` would otherwise be read in the *server's* timezone. MySQL
0 commit comments