Repository navigation
driver-sql + driver-memory: an epoch-millisecond NUMBER against a date field is read by neither driver's storage rule — where: { placed_on: { $gt: 1769940000000 } } returns 6 of 6 rows on SqlDriver and 0 on InMemoryDriver over REST #20203
Description
Activity
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsPointer: a third public-door answer, on live PostgreSQL.
domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T09:14Z. ⛔ Not a claim.PR #20202's at-tier contract review (record 5854540769) measured this card's row on a private live PostgreSQL 16.13 as well. Over REST,
where: { placed_on: { $gt: 1769940000000 } }on adatefield answersDATABASE_ERROR/ 500 ("Internal server error"). So one comparand now has three answers across three drivers: InMemoryDriver 0 rows, SQLite 6 rows, PostgreSQL 500. The readings are identical at PR #20202's base and head.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsPath: an API a customer can call | api-backend.query-contract-matrix | P2
Triage: first grade —
bug·priority:p2·domain:engine·area:api·pm:queueTriage: lands in
packages/core/src/utils/temporal-storage-form.ts, the one storage-form rule that PR #20202 lifted into core (merged 2026-09-27T09:34Z), which both drivers now read (driver-sqltemporalFilterValue,driver-memorycoerceTemporalValue) ⇒domain:engine. Rationale: an epoch-ms number against adatefield is read by no storage rule. One REST request gets three answers: InMemoryDriver 0 rows, SQLite 6 of 6, PostgreSQLDATABASE_ERROR/ 500 (pointer5854552106). The same number on adatetimefield is read as an instant on both drivers (3 = 3). It is a silent wrong answer, plus a 500, at a public door. It is the same family as #20176 (p2, just closed) ⇒ p2.reach:is measured over REST on three drivers.Triage seat (objectstack-wide, seat post #6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T10:11Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card and its pointer, #20176 and PR #20202 (both closed and merged), and #8690.The serial constraint is gone. PR #20202 merged, so the rule this card changes now lives in one place,
temporalStorageForm. It is not folded into #20176: that card is closed, and its rows were the per-aggregation positions, notwhere.Direction. Read or refuse must be decided once for every temporal face, as the card says. On the texts already in force the answer is read:
- the
datetimeface already reads an epoch-ms number as its instant; - the temporal-comparand door judges strings only, 「by design」, so a number is meant to be interpretable;
- An unparseable date comparand on a datetime filter is passed through and compares false — HTTP 200, zero rows, no diagnostic — while an unknown
{placeholder}is correctly rejected 400 (17.0.0 GA) #8690 refuses an unparseable date string, which is a different input.
⇒ Read an epoch-ms number on a
datefield as its UTC calendar day, intemporalStorageForm, the same rule for every caller. Stop valve: if ADR-0053 (the date / datetime semantics record) states that adatecomparand must be a calendar-day string, refuse at the temporal-comparand door instead, cite the sentence, and report the choice here. ⛔ Not a third copy in a driver.Execution notes.
- Pin
$gt/$lt/$eq/$in/$betweenon adatefield with an epoch-ms number, on both drivers, through the engine and REST, with thedatetimecontrol. - Record in
## Acceptance notesthe PostgreSQL reading after the change (the 500 must be gone). Use the live cell PR fix(objectql,core): a per-aggregation filter and having read a temporal comparand by the column's storage rule — one rule in core, shared with both drivers' where #20202's review used, if it is reachable.
- the
- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 27, 2026 objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: PM loop round 22
Session:session_01Bvd69VPa6puiNzzPUroDBx
Account:os-sales(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20203-epoch-ms-date-comparand
Worktree:objectstack-issue-20203
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/core/src/utils/temporal-storage-form.ts: thedaterule. Following triage 5854940483, an epoch-ms NUMBER is read as its UTC calendar day, the same rule for every caller. The stop valve: if ADR-0053 states that adatecomparand must be a calendar-day string, refuse it at the temporal-comparand door instead, cite the sentence, and stop and report;- tests in
packages/core, the two drivers and REST (test side only); .changeset/20203-*.md.
Stop on breach and explain in the report. ⛔ No third copy in a driver:
driver-sqlanddriver-memoryreadtemporalStorageFormsince PR #20202. ⛔ Notpackages/spec. ⛔ Not thedatetime/timerules, which must stay byte-identical.
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5854940483
Serial constraints cleared: at 2026-09-27T10:45Z, the region predecessor PR #20202 (#20176) has landed ase5cf27d65fand is verified onmain(landing record 5854697554). A census of the 12 open PRs finds none ontemporal-storage-form.ts,sql-driver.tsormemory-temporal.ts. The seat runsbatch:3(the maintainer's ruling); this is the only card in flight.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20203,
"status": "done",
"branch": "claude/issue-20203-epoch-ms-date-comparand",
"pr": "#20224",
"session": "session_01Bvd69VPa6puiNzzPUroDBx — mode:subagent; the id the claim 5855157485 names, and the one this container derives (CLAUDE_CODE_REMOTE_SESSION_ID cse_01Bvd69VPa6puiNzzPUroDBx → session_…)",
"premise_still_valid": true,
"summary": "temporalStorageForm (@objectstack/core) now reads a finite number on adatefield as the UTC calendar day of new Date(value): the same conversion a Date takes, so a number and its Date always agree. NaN, ±Infinity and numbers past the Date range come back unchanged. driver-sql (toDateOnly / temporalFilterValue), driver-memory (coerceTemporalValue) and the engine's per-aggregation filter / having already call this rule, so there is no driver source change. One REST request that answered memory 0, SQLite 6 of 6 and PostgreSQL DATABASE_ERROR/500 now answers the date's own count on all three (e.g. $gt 1769940000000 → 1 row), and where = per-aggregation filter = having on every cell. Every datetime, time and non-number date cell is byte-identical base → head. Stop valve: ADR-0053 states no requirement that a date comparand be a calendar-day string, so the read direction was taken. Draft PR #20224; the card had assignee os-sales on arrival, and PR #20224 now carries the same assignee.",
"tests": "Commits: the fix ba5c0de; tests 6cb41f4; changesets 4c8b92e; merge of main 16aedca; final 1c8c5ce (types the three new pins' query options, test files only). Pins: core temporal-storage-form.test.ts 72 passed (file unchanged since 4c8b92e); at 1c8c5ce driver-sql sql-driver-20203-epoch-ms-date-comparand.test.ts on SQLite + live PostgreSQL 16.13 (TZ=America/New_York, server Asia/Shanghai) 46 passed | 1 skipped (MySQL declared unprovisioned); driver-memory memory-20203-epoch-ms-date-comparand.test.ts 23 passed; rest data-query-epoch-ms-date-comparand.test.ts 18 passed. Whole suites at 16aedca, after merging main 4df101c and rebuilding the closure (vitest run, no --project, so both local and repo projects): core 57 files / 1488 passed; driver-memory 55 / 1351 passed; driver-sql with live PostgreSQL 197 passed, 3 skipped files / 3852 passed, 87 skipped (reporter: sqlite RAN, live postgres RAN, live mysql NOT RUN); objectql 319 / 5767 passed; rest (includes every query-route test) 201 / 3582 passed, 1 skipped. Typecheck: core at 16aedca, driver-memory / driver-sql / rest at 1c8c5ce, exit 0 each; every new/changed test file is in a compiled program by --listFiles (tsconfig.json for the drivers, tsconfig.test.json for core and rest); test-typecheck ledgers unchanged (core 4 held, rest 0). Ablation at 4c8b92e (fix committed first; ablation-replace wrap mode with EXIT/INT/TERM restore on the absolute path): anchor "const instant = typeof value === 'number' && Number.isFinite(value) ? new Date(value) : value;" x1 → x0, blob 61a6c0861a → 2a9bd5d997; core rebuilt; ablation-dist-preflight --absent exit 0 (marker gone from dist). Mutated: core 13 failed / 59 passed; memory 12 failed / 11 passed (the empty-object rows); driver-sql 35 failed / 11 passed / 1 skipped (SQLite empties green, PG empties red: the 500 came back); rest 17 failed / 1 passed (the min(datetime) control). Restored by the tool: blob == HEAD 61a6c0861a, git diff HEAD empty, whole-tree git status --porcelain empty; core rebuilt; preflight marker present exit 0; pins 72 / 23 / 46+1 skipped / 18 passed. Reverse-verification direction observed: turned red (the ordinary direction).",
"mcp_calls": "0 — no MCP GitHub tool was called (reads were REST GETs; writes went through scripts/pm and the fleet-write relay)",
"api_writes": "3 — all through the fleet-write relay as objectstack-fleet[bot], each carried by one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) = #20224, relay run 36317832633; (2) assign via scripts/pm/label-write.mjs → POST /repos//issues/20224/assignees [os-sales], relay run 36317881511, read back MATCHES; (3) this os-dev-report → POST /repos//issues/20203/comments via scripts/pm/post-stamped.mjs. Plus git push of the branch (not a REST write). No label was written by this run (the PR's documentation / size/l / tests / tooling labels were applied by the repo's labeler).",
"open_questions": [
{
"question": "The date rule's Date conversion spells a year outside 1000..9999 unpadded (999-06-15, 10000-01-01, -1-01-01), so its text stops ordering as a YYYY-MM-DD day. This PR routes a number through that same conversion (H3: a number and its Date agree), so a REST number now reaches the spelling too. os-dev rule 3 says a published defect this round touches must be fixed; the dispatch ruling says every non-number date comparand stays byte-identical, and fixing the spelling moves Date cells. Which governs?",
"options": [
"A — leave this PR as is (number ≡ Date on the whole Date range); file the spelling as its own class (a) card, whose ruling may move Date cells deliberately (4-digit pad for years 0..999; a decided reading for years past 9999 and below 0, which have no YYYY-MM-DD spelling at all).",
"B — pad the year to 4 digits for numbers only in this PR: the number path becomes right for years 0..999, but a number and its Date disagree there (breaks H3), and years past 9999 / below 0 stay unresolved.",
"C — fix the shared conversion here (pad 0..999) and move the Date cells, overriding the dispatch ruling; still leaves years past 9999 / below 0 without a pinned form."
],
"recommendation": "A. Real business need: epoch-ms numbers before year 1000 or after 9999 on a date field have no measured writer; the measured pull is the card's 2026 numbers, which this PR serves. Long-term soundness: one conversion for Date and number (no third dialect); the unpadded spelling is a Date-arm defect, and its right fix (including the out-of-range years with no YYYY-MM-DD form) is a decision, not a mechanical edit, so the bounded-fix exemption's "shape already pinned" condition fails. Keeping AI from writing wrong code: B would teach two spellings of one day depending on input type, the exact dialect the single rule removed. Startup focus: no new surface; the edge gets its own card with evidence."
}
],
"out_of_scope_findings": [
"class: a · driver-mongodb src/mongodb-temporal.ts storageDateValue (its own copy of the date rule, outside this claim) returns a finite number unchanged: probe storageDateValue(1769940000000) → 1769940000000 while core temporalStorageForm(1769940000000, "date") → "2026-02-01" (a Date and an ISO string agree on both). On MongoDB a number against the YYYY-MM-DD text meets BSON type bracketing, the mechanism behind driver-memory's 0 at base; the live Mongo answer is NOT MEASURED (no server here). Fix shape: re-point driver-mongodb's date/time arms at core's rule, as PR #20202's acceptance note already suggested. · dedupe words: driver-mongodb epoch ms number date field · mongodb storageDateValue number unchanged · mongodb-temporal date rule copy",
"class: a · the date rule's Date conversion (${y}-${m}-${d}from getUTCFullYear, in core temporal-storage-form.ts, formerly each driver's copy) spells a year outside 1000..9999 unpadded, so the text no longer orders or parses as a YYYY-MM-DD day. Measured at head, same six-row fixture, REST number and in-process Date agree: year 999 → memory/SQLite $gt 0 / $lt 6 (PG 6 / 0, correct); year 10000 → memory/SQLite 6 / 0 (PG 0 / 6, correct); year -1 → PG DATABASE_ERROR / 500. Reachable in-process by a Date before this PR, and by a REST number after it. See open_questions[0]. · dedupe words: date rule year below 1000 unpadded · calendar day year outside 4 digits text order · toDateOnly getUTCFullYear padding",
"carrier: 承接者:无 · SqlDriver.toDateOnly's docstring still says "Anything else (and null/undefined) passes through unchanged", and a number no longer does. driver-sql source is outside the claim (test side only). Noted in PR #20224 Acceptance notes, not filed."
],
"gates": {
"derived_at": "1c8c5ce756",
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths); stderr line 1 names objectstack-ai/objectstack at 1c8c5ce; 62 commands (the dispatch clue's 47 at 3875ae6, plus 15 the real diff adds: check-adr-0087-registration x2, check-empty-changeset x2, release-rehearsal-clone --self-test, check:dispatcher-error-vocabulary, check:driver-conformance, check:engine-double-contract, check:objectql-double-limit, check:objectui-changeset, check:pm-changeset-deadline-census, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher)",
"run_at": "1c8c5ce756",
"exit_0": 59,
"exit_1": [
"node scripts/check-empty-changeset.mjs --base origin/main — the deliberate one-clause correction to .changeset/20176-aggregation-temporal-storage-rule.md; red on purpose, needs a person's confirmation on the PR, the file must not be restored from base"
],
"not_measured": [
"pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads every package's built dist, which needs the whole ./packages/* build; CI builds it first",
"pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: --re-measure needs the whole ./packages/* build closure; CI builds it first"
],
"ran_reconcile": "dispatch-gates --ran ran.list: 62 derived, 60 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN",
"fixed_during_run": "check:query-options-erasure was exit 1 at 16aedca (test surface 236 → 241: fiveas anycasts on query options in the new pins); typed as FilterCondition / EngineAggregateOptions in 1c8c5ce, then exit 0 (236, at ceiling)",
"also_run": [
"pnpm check:authz-resolver exit 0",
"pnpm check:error-code-casing exit 0",
"pnpm check:filter-alias-parity exit 0 (the three roster gates whose roster sits under packages/)",
"node scripts/check-issue-citations.mjs --base 4df101c exit 0 (every added citation resolves)",
"control-byte scan grep -naP over the 7 changed files: none"
],
"lint": "declared narrowing, three pieces: (1) population read from eslint's own config: --print-config resolves a config for each of the 5 changed .ts files; (2) counts from --format json at 1c8c5ce: 5 files, 0 errors, 0 warnings, 0 fatal; (3) invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project), so no untouched file's verdict can move. Repo-wide pnpm lint is CI's.",
"type_check_lanes": "per-package typecheck run for core, driver-memory, driver-sql, rest (exit 0 each); the workspace-wide lint.yml Type Check lanes are CI's"
},
"line_budget": "n/a — no skills/** in the diff",
"deviations": [
".changeset/20176-aggregation-temporal-storage-rule.md: one-clause DELIBERATE CORRECTION. "…, so nowhere, write or read answer of either driver changes." → "…, so the lift itself changes nowhere, write or read answer of either driver (#20203, in the same release, then reads an epoch-millisecond number on adatefield as its UTC calendar day)." Nothing else in the note moved; check-empty-changeset red for it by design. The 20148-* notes read true and were not touched.",
"Pin placement: packages/rest has no driver-memory dependency and driver-memory cannot reach the engine, so memory's engine and REST cells are measured by a scratch harness but not committed; memory's where is pinned at its own driver door. PostgreSQL is pinned at the driver door in driver-sql's live matrix (CI Temporal Conformance runs it on PG and MySQL); its engine and REST cells are measured by the harness. Same precedent as PR #20202.",
"Merged origin/main (4df101c, one commit touching objectql and spec) into the branch before the whole suites (AGENTS Multi-agent §10); clean merge, closure rebuilt.",
"The live PostgreSQL used is a private PostgreSQL 16.13 this run started (server zone Asia/Shanghai), not the cell PR #20202's review used, which was not reachable; stopped and removed at the end."
],
"files_changed": [
"packages/core/src/utils/temporal-storage-form.ts",
"packages/core/src/utils/temporal-storage-form.test.ts",
"packages/drivers/driver-sql/src/sql-driver-20203-epoch-ms-date-comparand.test.ts",
"packages/drivers/driver-memory/src/memory-20203-epoch-ms-date-comparand.test.ts",
"packages/rest/src/data-query-epoch-ms-date-comparand.test.ts",
".changeset/20203-epoch-ms-date-comparand.md",
".changeset/20176-aggregation-temporal-storage-rule.md"
],
"rows_legend": "Fixture: six rows, placed_on (date) 2026-01-10, 01-02, 03-01, 02-01, 01-15, 02-01; opened_at (datetime) 01-01T10Z, 01-02T10Z, 02-01T10Z, 02-05T10Z, 02-06T10Z, 03-01T10Z. N = 1769940000000 = 2026-02-01T10:00:00.000Z; $in = [N, 1768057200000 (01-10T15Z)]; $between = [1767394800000 (01-02T23Z), N]; "$eq midnight" = 1769904000000. Counts are rows (per_aggregation_filter: the filtered count m). base = 3875ae6; head = ba5c0de (the fix; the pins re-confirm at 1c8c5ce). engine = engine.find / engine.aggregate; rest = POST /api/v1/data/:object/query with a JSON round-tripped body. date_twin_head = the same comparand as new Date(N), engine door (a Date is not JSON); the driver-door pins assert it on all 11 operators. pg = live PostgreSQL 16.13; DATABASE_ERROR is SQLSTATE 22008 DateTimeParseError from the server's date input parser.",
"rows": {
"$gt": {
"memory": {
"engine": {
"base": 0,
"head": 1
},
"rest": {
"base": 0,
"head": 1
},
"per_aggregation_filter": {
"base": 0,
"head": 1,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 3,
"head": 3
},
"date_twin_head": 1
},
"sqlite": {
"engine": {
"base": 6,
"head": 1
},
"rest": {
"base": 6,
"head": 1
},
"per_aggregation_filter": {
"base": 0,
"head": 1,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 3,
"head": 3
},
"date_twin_head": 1
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 1
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 1
},
"per_aggregation_filter": {
"base": 0,
"head": 1,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 3,
"head": 3
},
"date_twin_head": 1
}
},
"$lt": {
"memory": {
"engine": {
"base": 0,
"head": 3
},
"rest": {
"base": 0,
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 2,
"head": 2
},
"date_twin_head": 3
},
"sqlite": {
"engine": {
"base": 0,
"head": 3
},
"rest": {
"base": 0,
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 2,
"head": 2
},
"date_twin_head": 3
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 3
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 2,
"head": 2
},
"date_twin_head": 3
}
},
"$eq": {
"memory": {
"engine": {
"base": 0,
"head": 2
},
"rest": {
"base": 0,
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 2
},
"sqlite": {
"engine": {
"base": 0,
"head": 2
},
"rest": {
"base": 0,
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 2
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 2
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 2
}
},
"$in": {
"memory": {
"engine": {
"base": 0,
"head": 3
},
"rest": {
"base": 0,
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 3
},
"sqlite": {
"engine": {
"base": 0,
"head": 3
},
"rest": {
"base": 0,
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 3
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 3
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 3
}
},
"$between": {
"memory": {
"engine": {
"base": 0,
"head": 5
},
"rest": {
"base": 0,
"head": 5
},
"per_aggregation_filter": {
"base": 6,
"head": 5,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 5
},
"sqlite": {
"engine": {
"base": 0,
"head": 5
},
"rest": {
"base": 0,
"head": 5
},
"per_aggregation_filter": {
"base": 6,
"head": 5,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 5
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 5
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 5
},
"per_aggregation_filter": {
"base": 6,
"head": 5,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
},
"date_twin_head": 5
}
},
"$gte": {
"memory": {
"engine": {
"base": 0,
"head": 3
},
"rest": {
"base": 0,
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 4,
"head": 4
}
},
"sqlite": {
"engine": {
"base": 6,
"head": 3
},
"rest": {
"base": 6,
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 4,
"head": 4
}
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 3
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 3
},
"per_aggregation_filter": {
"base": 0,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 4,
"head": 4
}
}
},
"$lte": {
"memory": {
"engine": {
"base": 0,
"head": 5
},
"rest": {
"base": 0,
"head": 5
},
"per_aggregation_filter": {
"base": 0,
"head": 5,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 3,
"head": 3
}
},
"sqlite": {
"engine": {
"base": 0,
"head": 5
},
"rest": {
"base": 0,
"head": 5
},
"per_aggregation_filter": {
"base": 0,
"head": 5,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 3,
"head": 3
}
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 5
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 5
},
"per_aggregation_filter": {
"base": 0,
"head": 5,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 3,
"head": 3
}
}
},
"$ne": {
"memory": {
"engine": {
"base": 6,
"head": 4
},
"rest": {
"base": 6,
"head": 4
},
"per_aggregation_filter": {
"base": 6,
"head": 4,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 5,
"head": 5
}
},
"sqlite": {
"engine": {
"base": 6,
"head": 4
},
"rest": {
"base": 6,
"head": 4
},
"per_aggregation_filter": {
"base": 6,
"head": 4,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 5,
"head": 5
}
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 4
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 4
},
"per_aggregation_filter": {
"base": 6,
"head": 4,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 5,
"head": 5
}
}
},
"$nin": {
"memory": {
"engine": {
"base": 6,
"head": 3
},
"rest": {
"base": 6,
"head": 3
},
"per_aggregation_filter": {
"base": 6,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 5,
"head": 5
}
},
"sqlite": {
"engine": {
"base": 6,
"head": 3
},
"rest": {
"base": 6,
"head": 3
},
"per_aggregation_filter": {
"base": 6,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 5,
"head": 5
}
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 3
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 3
},
"per_aggregation_filter": {
"base": 6,
"head": 3,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 5,
"head": 5
}
}
},
"implicit": {
"memory": {
"engine": {
"base": 0,
"head": 2
},
"rest": {
"base": 0,
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
}
},
"sqlite": {
"engine": {
"base": 0,
"head": 2
},
"rest": {
"base": 0,
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
}
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 2
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 1,
"head": 1
}
}
},
"$eq midnight": {
"memory": {
"engine": {
"base": 0,
"head": 2
},
"rest": {
"base": 0,
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 0,
"head": 0
}
},
"sqlite": {
"engine": {
"base": 0,
"head": 2
},
"rest": {
"base": 0,
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": 0,
"head": 0
},
"datetime_control": {
"base": 0,
"head": 0
}
},
"pg": {
"engine": {
"base": "DATABASE_ERROR (engine)",
"head": 2
},
"rest": {
"base": "500 DATABASE_ERROR",
"head": 2
},
"per_aggregation_filter": {
"base": 0,
"head": 2,
"rest_equals_engine": true
},
"empty_table": {
"base": "DATABASE_ERROR (engine)",
"head": 0
},
"datetime_control": {
"base": 0,
"head": 0
}
}
}
},
"having": {
"legend": "groupBy customer_id, max(placed_on) as last_placed, min(opened_at) as first_opened; c1 last 01-10, c2 last 03-01, c3 last 02-01. native = driver aggregates; rows = engine aggregates in memory. Empty object: no group at base and head, every cell.",
"memory/max(date) $gt N/native": {
"base": "(no group)",
"head": "c2",
"rest_equals_engine": true
},
"memory/max(date) $gt N/rows": {
"base": "(no group)",
"head": "c2",
"rest_equals_engine": true
},
"memory/max(date) $eq N/native": {
"base": "(no group)",
"head": "c3",
"rest_equals_engine": true
},
"memory/max(date) $eq N/rows": {
"base": "(no group)",
"head": "c3",
"rest_equals_engine": true
},
"memory/max(date) $in/native": {
"base": "(no group)",
"head": "c1,c3",
"rest_equals_engine": true
},
"memory/max(date) $in/rows": {
"base": "(no group)",
"head": "c1,c3",
"rest_equals_engine": true
},
"memory/min(datetime) $gte N/native": {
"base": "c2,c3",
"head": "c2,c3",
"rest_equals_engine": true
},
"memory/min(datetime) $gte N/rows": {
"base": "c2,c3",
"head": "c2,c3",
"rest_equals_engine": true
},
"sqlite/max(date) $gt N/native": {
"base": "(no group)",
"head": "c2",
"rest_equals_engine": true
},
"sqlite/max(date) $gt N/rows": {
"base": "(no group)",
"head": "c2",
"rest_equals_engine": true
},
"sqlite/max(date) $eq N/native": {
"base": "(no group)",
"head": "c3",
"rest_equals_engine": true
},
"sqlite/max(date) $eq N/rows": {
"base": "(no group)",
"head": "c3",
"rest_equals_engine": true
},
"sqlite/max(date) $in/native": {
"base": "(no group)",
"head": "c1,c3",
"rest_equals_engine": true
},
"sqlite/max(date) $in/rows": {
"base": "(no group)",
"head": "c1,c3",
"rest_equals_engine": true
},
"sqlite/min(datetime) $gte N/native": {
"base": "c2,c3",
"head": "c2,c3",
"rest_equals_engine": true
},
"sqlite/min(datetime) $gte N/rows": {
"base": "c2,c3",
"head": "c2,c3",
"rest_equals_engine": true
},
"pg/max(date) $gt N/native": {
"base": "(no group)",
"head": "c2",
"rest_equals_engine": true
},
"pg/max(date) $gt N/rows": {
"base": "(no group)",
"head": "c2",
"rest_equals_engine": true
},
"pg/max(date) $eq N/native": {
"base": "(no group)",
"head": "c3",
"rest_equals_engine": true
},
"pg/max(date) $eq N/rows": {
"base": "(no group)",
"head": "c3",
"rest_equals_engine": true
},
"pg/max(date) $in/native": {
"base": "(no group)",
"head": "c1,c3",
"rest_equals_engine": true
},
"pg/max(date) $in/rows": {
"base": "(no group)",
"head": "c1,c3",
"rest_equals_engine": true
},
"pg/min(datetime) $gte N/native": {
"base": "c2,c3",
"head": "c2,c3",
"rest_equals_engine": true
},
"pg/min(datetime) $gte N/rows": {
"base": "c2,c3",
"head": "c2,c3",
"rest_equals_engine": true
}
},
"stop_valve": {
"reading": "none found — no sentence in ADR-0053 or packages/spec states that a date comparand must be a calendar-day string; read direction taken",
"quotes": [
"ADR-0053 TL;DR: "date= a timezone-naive calendar day, represented as aYYYY-MM-DDstring end-to-end (storage, query, CEL). It is never converted to an instant and never timezone-shifted." — representation, not an input restriction",
"ADR-0053 Phase 1, decision 1: "ADate, a full ISO string, or aYYYY-MM-DDstring all collapse to the calendar day (UTC calendar day for aDate, matching the existing filter coercion)." — non-day inputs are collapsed, not refused",
"ADR-0053 D-C1 (the sibling time rule): "ADate/ epoch-ms / full-timestamp value folds to its UTC time-of-day"",
"packages/spec/src/data/filter.zod.ts ORDERING_COMPARAND_DESCRIPTION: "Comparand is a number, a Date, a string, or a { $field } reference"",
"packages/core/src/utils/temporal-comparand.ts: "A number is epoch milliseconds, aDateis an instant,nullis a null test. The refusal scopes to strings by ruling.""
],
"adjacent_not_adr": "The engine/REST WRITE doors refuse a number on a date field (VALIDATION_FAILED, "placed_on must be a valid date (ISO-8601)"): the stored-value contract (CalendarDateValueSchema = YYYY-MM-DD), not a comparand sentence; unchanged base → head."
},
"collateral": {
"rule_sweep": "34 shapes × 3 kinds = 102 cells of temporalStorageForm, base vs head dist: 12 moved, all date × a finite number inside the Date range (epoch ms, a UTC midnight, -86400000, -1, a fraction, a negative fraction, 0, -0, ±8.64e15, year 10000, year 0); 90 byte-identical: every datetime and time cell, and on date every Date, Invalid Date, NaN, ±Infinity, 8.64e15+1, MAX_SAFE_INTEGER, bigint, boxed Number, epoch-ms string, bare day, zone-naive, ISO, offset, wall clock, junk, empty, null, undefined, boolean, object, array",
"end_to_end": "466 measured cells (where, per-aggregation filter, having, write, read, Date twin, rule sweep) across memory / SQLite / PostgreSQL: 113 moved, 0 outside "a number on a date field"",
"where_equals_per_aggregation_filter_head": "132 of 132 keys (3 drivers × populated/empty × date/datetime × 11 operators), engine and REST: where count = per-aggregation filter count on every key",
"per_aggregation_filter_base": "date field, all three drivers, both doors: 0 on every operator except $between, $ne, $nin (6); datetime control equal to where at base and head",
"having": "only max(date) moved (both doors, native and rows, all three drivers): $gt N no group → c2; $eq N no group → c3; $in no group → c1, c3; min(datetime) $gte N control c2, c3 at base and head",
"write_paths": "engine.insert and REST create of a number on a date field: VALIDATION_FAILED / 400 at base and head (unchanged). Direct driver create/update of a number on a date field: memory and SQLite stored 1769940000000, PostgreSQL refused 22008 → all three store "2026-02-01". The same number on datetime / time writes "2026-02-01T10:00:00.000Z" / "10:00:00" at base and head.",
"read_paths": "SQLite, a raw INTEGER already in a date column: find, findOne, a groupBy key and distinct presented 1769940000000 → "2026-02-01" (ADR-0053 Phase 1 read repair); aggregate max over a mixed group unchanged ("2026-01-05"); a where day comparand against that raw row unchanged ([]: the date column side has no legacy repair)",
"other_callers": "driver-turso inherits temporalFilterValue from SqlDriver and service-analytics binds through driver.temporalFilterValue, so both follow the rule; not separately measured",
"pending_changesets": "20176-*: one sentence read false in the same release → corrected (deviations[0]); 20148-aggregation-filter-keys-rest and 20148-aggregation-filter-where-doors read true and were not touched"
},
"cleanup": "Private PostgreSQL (initdb /var/lib/postgresql/issue-20203-pg, port 45203, recorded PID 4218) stopped with pg_ctl -m fast; its data dir removed; no process left for it. Worktree /home/user/objectstack-issue-20203: branch pushed (local = remote 1c8c5ce), tree clean, node_modules removed, git worktree remove without --force exit 0, directory gone, git worktree list has no issue-20203 entry. Scratch harnesses were never in the repo (scratchpad issue-20203/). This comment was posted from a scratchpad copy of scripts/ at 1c8c5ce. No background process or monitor was started."
}objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsSeat ruling on the
os-dev-report5855694754 (PR #20224)domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T12:14Z.- Open question 1 (the unpadded year spelling) → A. PR fix(core): read an epoch-ms number on a
datefield as its UTC calendar day, so driver-memory, SQLite and PostgreSQL give one answer #20224 stays as it is: a number and itsDatetake one conversion across the wholeDaterange. The spelling of a year outside 1000..9999 is a defect of theDatearm that predates this card. Years past 9999 and below 0 have noYYYY-MM-DDspelling at all, so fixing it is a decision, not a bounded edit. The seat files it through the filing gate after landing, with its reach re-measured onmainat the REST door. - The declaration stands as
Clause-②: no. No export or payload key moves, and no request that answered now gets refused. The at-tier review judges it. - The
20176-*one-clause DELIBERATE CORRECTION: confirm as written. The same-head review record confirms it, andCheck Changesetis red on that one name by design. - Out-of-scope findings:
- Finding 1 (
driver-mongodb'sstorageDateValuereturns a number unchanged). There is no measured public-door answer: nomongodis reachable here. It is recorded with carrier none, beside PR fix(objectql,core): a per-aggregation filter and having read a temporal comparand by the column's storage rule — one rule in core, shared with both drivers' where #20202's acceptance note on the same copy. Not filed. - Finding 2 (the year spelling): see open question 1.
- Finding 3 (
SqlDriver.toDateOnly's docstring still says a number passes through). It is an acceptance note, carrier none. The claim keeps driver-sql to the test side.
- Finding 1 (
- Open question 1 (the unpadded year spelling) → A. PR fix(core): read an epoch-ms number on a
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsACCEPT: PR #20224 at
1c8c5ce756(#20203)domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T12:38Z. Reviewed on GitHub againstreferences/review-checklist.md, not from the dev'sos-dev-report.- Shape: the first line is
Fixes #20203, and the body declaresClause-②: no. The changeset grades@objectstack/core,driver-sql,driver-memoryandobjectqlpatch. - Scope: 7 files, +499/−9. Not governed; 508 changed lines.
- The one source change is the
datearm oftemporalStorageFormin@objectstack/core: a finite number reads as the UTC calendar day ofnew Date(value). No driver source moves, and there is no third copy. - Also: pins at the core, memory, SQL and REST doors, the
20203-*changeset, and one DELIBERATE CORRECTION (20176-*).
- The one source change is the
- Contract review of record: PASS at
1c8c5ce756(5855881108).- Memory and SQLite: on all 11 operators the two drivers agree, engine = REST, a number = its
Date, and the per-aggregationfilter=where(88/88 keys per driver). Everydatetimecontrol is byte-identical. - The rule sweep moves 14 of 138 cells, all "a finite number on
date". Of 445 / 452 end-to-end cells, 0 moved outside that class. - The write and read paths move inside the class, as ADR-0053 Phase 1 prescribes. The engine and REST write doors are unchanged. No accept set narrows.
- The stop valve does not trip: no ADR-0053 sentence requires a
datecomparand to be a day string. - The ablation reproduces the dev's counts.
- PostgreSQL was not measured locally. Its cell of record is CI's
Temporal Conformance (live PG + MySQL)job at this head (success).
- Memory and SQLite: on all 11 operators the two drivers agree, engine = REST, a number = its
- CI at this head: 34 runs: 30
success, 3skipped, 1 failure. All seven required contexts aresuccess.- The failure is
Check Changeset, red by design on exactly.changeset/20176-aggregation-temporal-storage-rule.md: the DELIBERATE CORRECTION class. The same-head PASS confirms it, and the gate runs onpull_requestonly. - The probe
merge-treeonto currentmainis clean.
- The failure is
- Ruling carried (5855727555): open question 1 → A.
- The review measured the cost. On four extreme-year cells (a day in year 999 or 10000), head answers worse than base: base answered them right only by accident, since a raw number never met the day text. For every such year a number now equals its
Date, and theDatecells are byte-identical. - The defect is the
Datearm's unpadded year spelling, which predates this card. The seat files it after landing, with its reach measured onmain.
- The review measured the cost. On four extreme-year cells (a day in year 999 or 10000), head answers worse than base: base answered them right only by accident, since a raw number never met the day text. For every such year a number now equals its
- Acceptance notes (not cards):
SqlDriver.toDateOnly's docstring ("Anything else … passes through unchanged") is now false for a finite number. Carrier none; it rides on the next driver-sql source PR.- A
wherewith a NUMBER comparand against a raw INTEGER already stored in a SQLitedatecolumn matched at base and does not at head. Only a raw write reaches it; it is inside the class and not in the changeset. driver-mongodb'sstorageDateValuestill returns a number unchanged. Nomongodwas reachable to measure it. Carrier none, beside PR fix(objectql,core): a per-aggregation filter and having read a temporal comparand by the column's storage rule — one rule in core, shared with both drivers' where #20202's note on the same copy.
Landing:
readyplus auto-merge through the queue now.- Shape: the first line is
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsLanded: PR #20224, verified on
maindomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-27T13:01Z.- Verified on main:
- PR fix(core): read an epoch-ms number on a
datefield as its UTC calendar day, so driver-memory, SQLite and PostgreSQL give one answer #20224 merged through the merge queue as615c468746. It has one parent (93cfc3f593) and is an ancestor oforigin/mainafter a fresh fetch. - Its patch-id equals the reviewed diff
4df101c383..1c8c5ce756(PASS 5855881108): 7 files, +499/−9. - The changeset
20203-epoch-ms-date-comparand.mdis present at that commit and absent at its parent. temporal-storage-form.tslanded at the reviewed blob61a6c0861a.- The DELIBERATE CORRECTION of
20176-*landed at the reviewed blob6b9d78d447, as ACCEPT 5855886361 ruled. The redCheck Changesetdoes not run onmerge_group.
- PR fix(core): read an epoch-ms number on a
- This card: closed
completedviaFixes #20203.pm:dispatchedis removed in the same act.- An epoch-ms number on a
datefield now reads as its UTC calendar day in the one storage rule. InMemoryDriver, SQLite and PostgreSQL give one answer through the engine and REST, and PostgreSQL'sDATABASE_ERROR/ 500 is gone. - Declared
Clause-②: no;patchlevels.
- An epoch-ms number on a
- Next, per ruling 5855727555: the seat files the
Datearm's unpadded year spelling (years outside 1000..9999) through the filing gate, with its reach measured onmain.
- Verified on main:
- added 2 commits that reference this issue
on Sep 28, 2026 - added a commit that references this issue
on Sep 29, 2026
Filing gate: ① a defect with a named landing site, where a numeric temporal comparand meets a
datecolumn:packages/core/src/utils/temporal-storage-form.ts(PR fix(objectql,core): a per-aggregation filter and having read a temporal comparand by the column's storage rule — one rule in core, shared with both drivers' where #20202, pending): thedaterule returns a number unchanged;packages/drivers/driver-sql/src/sql-driver.ts:coerceFilterValue/temporalFilterValueon thedatekind;packages/drivers/driver-memory/src/memory-temporal.ts:coerceTemporalValueon thedatekind.Finding class (a).
reach:was measured at the public REST door (below).The
domain:engineexecution seat 1 (session_01Bvd69VPa6puiNzzPUroDBx) filed this from its #20176 dev's report (os-dev-report5854303341 on #20176, PR #20202), out-of-scope finding 2. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.What happens
Measured by the #20176 dev on a real
InMemoryDriverand a realSqlDriver(better-sqlite3:memory:), schema-synced, over a 6-row object whoseplaced_onis adatefield. The readings are identical at basecfe2387a3band at PR #20202's head:where: { placed_on: { $gt: 1769940000000 } }(a JS number, epoch ms)POST /api/v1/data/:object/query, SqlDriverengine.find, both drivers$gtbound, which is a silent wrong answer at a public door.datetimefield IS read by the storage rule: epoch ms converts to the instant, and both drivers count 3.assertTemporalComparandsInterpretable) judges only strings, by design, so a number reaches the drivers unjudged.Suggested shape (⛔ not a ruling)
One of the two, decided once for every temporal face:
datefield as its UTC calendar day, as thedatetimerule already reads it as an instant; orINVALID_FILTER/ 400) at the temporal-comparand door, as an unparseable date string is refused (An unparseable date comparand on a datetime filter is passed through and compares false — HTTP 200, zero rows, no diagnostic — while an unknown{placeholder}is correctly rejected 400 (17.0.0 GA) #8690).The rule lives in ONE place:
temporalStorageFormin@objectstack/coreonce PR #20202 lands. Pin it on both drivers, through the engine and REST, with$gt/$lt/$eq/$in/$between, and with adatetimecontrol.Filing-gate answers
filter(andhaving) compares a temporal comparand type-blind, not by the column's storage rule — an ISO instant on adatefield counts 1 where thewheretwin counts 3 #20176 dev.domain:engine, the owner ofpackages/coreand the SQL / memory drivers). It is sequenced after PR fix(objectql,core): a per-aggregation filter and having read a temporal comparand by the column's storage rule — one rule in core, shared with both drivers' where #20202 (objectql + REST: a per-aggregationfilter(andhaving) compares a temporal comparand type-blind, not by the column's storage rule — an ISO instant on adatefield counts 1 where thewheretwin counts 3 #20176), which lifts the rule this card changes. That is a region order, not aBlocked-by:.closedincluded:epoch milliseconds number comparand date field where drivers disagree sqlite integer text ordering→ 19 hits, top 10 read:datefield;Field.datetime的裸 epoch 从聚合出口漏出去(#3773 同根不同出口) #3797, service-analytics:AVG()over aField.datetimemeasure returns SQLite's text→numeric coercion (an average YEAR) with no error, andderived: { op: 'difference' }renders the difference of two of them as a clean plausible number #16737,os generate migration's audit-stamp columns diverge from driver-sql — the generators emitNOT NULLwhere the driver emits nullable, and the SQL format emitsTIMESTAMPwhere both knex paths yieldtimestamptz#15521, driver-sql: on SQLite the builtin audit columns DEFAULT to zone-naiveCURRENT_TIMESTAMPwhile a declaredField.datetimeNOW() default in the SAME table gets the canonical ISO-8601 form #11321, driver-sql: every Field.date read from PostgreSQL is one day early when the process TZ is east of UTC — toDateOnly() reads UTC components off a local-midnight Date #11389, driver-sql: on MySQL the UPDATE door'supdated_atstamp truncates to whole seconds against aDATETIME(3)column — a row updated in its first second readsupdated_atEARLIER thancreated_at#11224: other temporal columns and doors;numeric comparand on date column temporal comparand door does not judge numbers→ 4 hits, all read: objectql + REST: a per-aggregationfilter(andhaving) compares a temporal comparand type-blind, not by the column's storage rule — an ISO instant on adatefield counts 1 where thewheretwin counts 3 #20176 (the parent), objectqlhaving: a{ $field }reference withaddDaysagainst a non-temporal aggregated column answers by epoch-ms coercion, where SQL push-down refuses the same pair onwhere— the aggregated row declares no temporal class to judge it by #20127, service-analytics:AVG()over aField.datetimemeasure returns SQLite's text→numeric coercion (an average YEAR) with no error, andderived: { op: 'difference' }renders the difference of two of them as a clean plausible number #16737 and An unparseable date comparand on a datetime filter is passed through and compares false — HTTP 200, zero rows, no diagnostic — while an unknown{placeholder}is correctly rejected 400 (17.0.0 GA) #8690 (an unparseable date STRING).datefield.Dedupe words:
epoch ms number date field where drivers disagree·date field numeric comparand sqlite integer text ordering·temporal comparand number date column