|
| 1 | +--- |
| 2 | +"@objectstack/core": patch |
| 3 | +"@objectstack/driver-sql": patch |
| 4 | +"@objectstack/driver-memory": patch |
| 5 | +"@objectstack/objectql": patch |
| 6 | +--- |
| 7 | + |
| 8 | +fix(core): an epoch-millisecond number compared against a `date` field is read as the UTC calendar day of its instant, by every driver and at every position that compares it (#20203) |
| 9 | + |
| 10 | +Clause-②: no — no key, export or operator moves, and no comparand that was accepted is now refused: a number was already an accepted comparand on every `date` position, and its answer moves to the storage rule's reading. |
| 11 | + |
| 12 | +`temporalStorageForm(value, 'date')` in `@objectstack/core` returned a finite number unchanged, so each face compared it by its own type rules and they disagreed. Over six rows, with `1769940000000` (2026-02-01T10:00:00.000Z) against a `date` field: |
| 13 | + |
| 14 | +| face | `$gt` | `$lt` | `$eq` | `$in` (with a Jan 10 member) | `$between` (from Jan 2) | |
| 15 | +|:--|:--|:--|:--|:--|:--| |
| 16 | +| `where` on `driver-memory` | 0 | 0 | 0 | 0 | 0 | |
| 17 | +| `where` on `driver-sql`, SQLite | 6 | 0 | 0 | 0 | 0 | |
| 18 | +| `where` on `driver-sql`, PostgreSQL | `DATABASE_ERROR`, a 500 at REST | the same | the same | the same | the same | |
| 19 | +| a per-aggregation `filter` on `engine.aggregate` | 0 | 0 | 0 | 0 | 6 | |
| 20 | +| **now, on every face above** | **1** | **3** | **2** | **3** | **5** | |
| 21 | + |
| 22 | +The same holds through `engine.find` and `POST /data/:object/query`, and for `$gte`, `$lte`, `$ne`, `$nin` and implicit equality. PostgreSQL's server refused the bound number itself (`22008`, date/time field value out of range), on an empty table too. `having` over `max` of a `date` field kept no group for `$gt`, `$eq` or `$in`; it now keeps the groups whose day compares. |
| 23 | + |
| 24 | +A finite number is now read as the `datetime` rule already reads it, as epoch milliseconds. It takes the UTC calendar day of that instant: the day `new Date(value)` names, through the same conversion a `Date` takes. So a number and its `Date` always answer alike. A time of day is dropped, never rounded, a negative number is a day before 1970, and a fraction truncates toward zero as the `Date` constructor does. `driver-sql` (`toDateOnly`, `temporalFilterValue`), `driver-memory` (`coerceTemporalValue`) and the engine's per-aggregation `filter` and `having` all call this rule, so they now agree. |
| 25 | + |
| 26 | +The rule is shared by the drivers' write and read paths too: |
| 27 | + |
| 28 | +- `create()` / `update()` on either driver, given a number for a `date` field, stores its UTC day. Before, `driver-memory` and SQLite stored the number, and PostgreSQL refused the statement. The engine and REST write doors refuse a number on a `date` field before a driver sees it (`VALIDATION_FAILED`), as before. |
| 29 | +- A number already stored in a SQLite `date` column is read back as its UTC day by `find()`, a `groupBy` key and `distinct()`. Only a direct driver write could have put one there. |
| 30 | + |
| 31 | +Not changed, measured identical before and after: `NaN`, ±Infinity, a number outside the `Date` range (past ±8.64e15), a bigint, an epoch-millisecond string, every `Date` and every string on a `date` field, and every `datetime` and `time` reading. `driver-mongodb` keeps its own copy of the `date` rule and is not changed here. |
0 commit comments