Repository navigation
fix(driver-sql): a date field still on a Postgres timestamptz column reads the day it was written - #33000
Conversation
…reads every door in UTC Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxvG52CvKzKfC5CYHB
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxvG52CvKzKfC5CYHB
… them Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxvG52CvKzKfC5CYHB
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cf5510eff72d00bce033326f99888a048864a5c8 && git checkout cf5510eff72d00bce033326f99888a048864a5c8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d4b14a85048f45e52622eb520a00aaea2fe5bda2 6c557952db635f7c4b4575b2d0dbc1c494429c0f && git checkout -B drift-repro d4b14a85048f45e52622eb520a00aaea2fe5bda2 && git merge --no-ff 6c557952db635f7c4b4575b2d0dbc1c494429c0f
node scripts/docs-audit/affected-docs.mjs --json d4b14a85048f45e52622eb520a00aaea2fe5bda2
|
|
domain:server#6 · os-elon-musk · session_016WV4cz8zK9vJDPeeVKJ4XP 维护者速读
席位核对 — ACCEPT · domain:server#6 · head
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 43361233025 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Requested by Elon · project thread
Fixes #32851
Clause-②: no
Metadata-protocol: none
维护者速读
timestamptz;现在这种字段的写入、读取、按日期过滤、按天分组统一按 UTC 日计算,四处答案一致。列类型不动,存量数据不改写。packages/drivers/driver-sql/src/date-on-instant-column.ts(规则与理由),其次sql-driver-rotation.ts里登记哪些列是 timestamptz 的那段,再看sql-driver-filter-helpers.ts的filterColumnExpr新分支。OS_TEST_POSTGRES_URL=… TZ=America/New_York pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-32851-date-on-instant-column.test.ts(服务器时区设为 Asia/Shanghai),预期 Postgres 与 SQLite 两格全绿。What changes
Chosen shape: (b), read every door in a fixed zone (UTC), not (a), narrowing the column.
packages/drivers/driver-sql/src/date-on-instant-column.ts: the registry type, the predicateisDateOnInstantColumn, the write bindingutcMidnightOfDayand the SQLutcCalendarDaySql(((col) AT TIME ZONE 'UTC')::date), with the measurement and the reasoning in its header.SqlDriver.dateFieldsOnInstantColumn(protected registry): per table, the declaredField.datefields whose Postgres column istimestamp with time zone.initObjects(the callsyncSchemaand the publish's storage sync make) records it from thecolumnInfo()it already reads for an existing table; it is reset to empty for every table it syncs, so a table created here, a non-Postgres dialect, deferred DDL or a guest datasource keeps the plaindatepath exactly as before.formatInput): such a field binds the day as its UTC midnight (2026-11-12T00:00:00.000Z) instead of the bare day the server read in its own zone. This is the Oracle arm's rule (oracleDayBinding).filterColumnExpr, so every comparison operator, the object shorthand, cross-field and now-anchor comparisons) and the outside-the-builder twintemporalFilterColumnSql(analytics native SQL): the column is read as its UTC calendar day; the comparand stays theYYYY-MM-DDday it already was.buildDateBucketExpr, soaggregate()and thedateBucketSqlecho): such a field buckets as its UTC calendar day instead of::datein the session zone.temporalStorageForm), which is the zone every other door now uses.Why (b) and not (a):
datetimecarry a time of day, and rows written after the publish on a server ahead of UTC are stored at 16:00Z of the previous day. Narrowing those changes stored values, which the order routes toneeds_decision; skipping them leaves the defect in place. So (a) alone does not fix the reported case, and once (b) is in, (a) adds nothing.datetimebefore the publish, which a comparand-only fix would still miss), and leavesdatetimefields untouched.TimeZone = UTCon every Postgres connection (the MySQLwithUtcSessionshape) was considered and rejected as wider: it moves every raw statement and host query on the pool, and it still would not make an equality filter match a stored instant with a time of day.Existing rows: a day written into such a column before this fix, on a server ahead of UTC, was stored as the previous day's instant (
2026-11-11T16:00:00Zfor2026-11-12). It keeps reading, filtering and bucketing as that previous day, now consistently. It is not repaired: from the column alone it cannot be told apart from adatetime-era value at the same instant, so a repair needs a data migration and a decision (see Acceptance notes). Rows written while the field was adatetimeread, filter and bucket as their UTC day.Tests
packages/drivers/driver-sql/src/sql-driver-32851-date-on-instant-column.test.ts, on every dialect cell of the live matrix (SQLite always; Postgres and MySQL where provisioned, which theTemporal Conformance (live PG + MySQL)job does with both servers atAsia/Shanghaiand the process atAmerica/New_York). It creates the table withdue: datetime, writes adatetime-era row (2026-11-12T20:00:00Z, 04:00 the next day in Shanghai), publishesdue: datethroughsyncSchema+applyPublishedColumnChanges, then:timestamptzand the server's offset is ahead of UTC (fails, with a remedy, if the server is at UTC);create({ due: '2026-11-12' })answers and reads2026-11-12, anupdateto that day reads it back,due = dayand$gte/$lte dayfind both,$lt daydoes not;datetimefield beside it (seen) still reads and filters as its exact instant;datetime-era row reads2026-11-12, the equality filter returns all three rows, andaggregate()by day answers one bucket2026-11-12of 3;temporalFilterColumnSql+temporalFilterValue, and thedateBucketSqlecho, answer the same.Pre-fix measurement on main (b4f6971), live Postgres 16 at Asia/Shanghai, TZ=America/New_York
After the fix:
create answers 2026-11-12, stored2026-11-12 08:00:00+08(UTC midnight), both rows read2026-11-12,filter eq ["a","b"], bucket2026-11-12of 2.Ablation (each leg through scripts/ablation-replace.mjs; restore verified blob == HEAD and git diff HEAD empty)
Acceptance notes
datetime(an instant with a time of day) reads as its day but is not matched by an equality filter on that day, because the comparand is the bare day and the column holds the instant. Postgres now matches it through the UTC-day column read. Reported to the seat in the os-dev report.os migrate plan, a guest datasource withschemaModeother than managed) keeps the pre-fix behaviour for such a column.timestamp without time zonecolumn under adatefield is not recorded: this driver never builds one for adatetimefield.datetimetodateare affected.Verification
origin/mainmerge before the PR (583b1e38, main atc1a645e2; nopackages/drivers/driver-sqlchange came with it).6c557952:node scripts/pm/dispatch-gates.mjs --commandsderived 70 families; every one run after aturbo run buildof the@objectstack/dogfood^...+@objectstack/runtime^...+ driver-sql closure (exit 0), pluspnpm --filter @objectstack/driver-sql typecheckandpnpm lint. All 72 exit 0.--ranreconciliation at6c557952: ✓ dispatch-gates --ran: 70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED.583b1e38was green exceptcheck:query-options-erasure(the new test cast six query bagsas any);6c557952types them and the full union was re-run on it.pnpm --filter @objectstack/driver-sql testwith live Postgres 16 (serverAsia/Shanghai,TZ=America/New_York): 261 files passed, 4 skipped; 5319 tests passed, 122 skipped (run at89ffef34, same driver-sql source as the final head except the pin's casts; the pin re-run on6c557952: 9 passed, 4 skipped).Temporal Conformance (live PG + MySQL)job runs it.!in the title,Clause-②: nowithout a narrowing arm).--ran file (COMMAND :: exit CODE) at 6c55795