Repository navigation
fix(service-analytics): the SQL echo refuses a date bucket the engine buckets in memory, on every dialect (#21630) - #21645
Conversation
… buckets in memory, on every dialect With a non-UTC timezone the engine buckets a date dimension in memory on that zone's calendar (ADR-0053 Phase 2, D2), on every driver. The ObjectQL echo kept printing date_trunc on PostgreSQL and MySQL, a statement the engine never ran; on PostgreSQL it grouped on the session zone's calendar. It now answers the refusal SQLite already answered (NOT_IMPLEMENTED / 501, refusal: true), raised before the dateBucketSql hook is asked. UTC and an unset timezone keep each driver's own expression. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…en the hook is asked Its TSDoc said a host that wires no hook keeps the echo's representative date_trunc. The echo now refuses a non-UTC timezone whatever is wired, and SQLite already refused; the sentence names both. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ho-refuses-in-memory-bucket
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 3 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 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 a88097b39240e1f3fbeef280076aca73d499f072 && git checkout a88097b39240e1f3fbeef280076aca73d499f072
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0c50b5dfe723cb5c4733a1ac37508504a0ef1870 267d2202ae2d25650ed6a44955701fe8ec0343b5 && git checkout -B drift-repro 0c50b5dfe723cb5c4733a1ac37508504a0ef1870 && git merge --no-ff 267d2202ae2d25650ed6a44955701fe8ec0343b5
node scripts/docs-audit/affected-docs.mjs --json 0c50b5dfe723cb5c4733a1ac37508504a0ef1870
|
Fixes #21630
Clause-②: no
What changes
One rule for the ObjectQL face's SQL echo (
ObjectQLStrategy.generateSql), on every dialect, as triage ruled on the card (5973378431): whenever the engine buckets a date dimension in memory because the query carries a non-UTCtimezone(ADR-0053 Phase 2, D2;tzRequiresInMemoryin objectql'sengine.ts), the echo answers the refusal PR #21629 introduced on SQLite,NOT_IMPLEMENTED/ 501 withrefusal: true. Nodate_trunctext for that case on any dialect.dimExprbefore thedateBucketSqlhook is asked, on the test the engine makes (timezoneset and not'UTC'). So it holds on PostgreSQL, MySQL and SQLite, and also on a host that wires no hook and on a non-SQL driver: the engine buckets a non-UTC zone in memory whatever the driver.sqliteBucketEchoRefusedbecomesbucketEchoRefused, with two arms that share one envelope: the in-memory zone (every dialect, new message), and SQLite where no driver expression answers at UTC (the earlier no-hook arm, message unchanged).timezoneare untouched: each driver's own expression (to_char/date_format/strftime).AnalyticsServiceConfig.dateBucketSql's TSDoc said "a host that wires nothing keeps the echo's representativedate_trunc". It ships indist/index.d.ts(measured, line 1250 after build), and this change makes it false for a non-UTC zone, so it now says when the hook is asked. Comment only. It sits outside the claim's listed file surface; see the scope note below.@objectstack/service-analyticspatch,Clause-②: no.No driver change, no
packages/objectqledit, nopackages/specedit, no zone-aware expression ("Not this card" in the ruling).Reproduced first, on
mainPostgreSQL 16.14, a throwaway server with
TimeZone=Asia/Shanghai, processTZ=America/New_York, the pin's five rows, atmain7b07749f05. The echo text is the same in the/analytics/queryanswer'ssqland in the/analytics/sqldry run (analytics.generateSql, the body that route answers with).timezoneAsia/Shanghai, month2026-0120,2026-028,2026-0310,2026-045date_trunc('month', closed_at)2025-12-31T16:00:00.000Z20,2026-01-31T16:00:00.000Z8,2026-02-28T16:00:00.000Z10,2026-03-31T16:00:00.000Z5America/New_York, month2026-0127,2026-021,2026-0310,2026-045UTCor unset, month2026-0127,2026-021,2026-0310,2026-045to_char(("closed_at")::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM')weekgave the same shape (non-UTC echoeddate_trunc('week', closed_at)).Both faces after the change
Measured on the same server at
ff399dae2d:/analytics/sqlrefuses withNOT_IMPLEMENTED/ 501 andrefusal: true, anddeclaredRefusalMessage(err)returnserr.message. That is the same envelope the SQLite arm answers, raised through the same error shape./analytics/queryreturns the same rows as before for both zones (table above, unchanged), and its answer has nosqlkey, becauseexecute()already swallows an echo refusal.Pins
objectql-echo-date-bucket.test.ts:FALLBACK: a non-UTC timezone ... keeps date_truncasserteddate_trunc('month', closed_at), a statement the engine did not run. That case and the SQLiteREFUSALcase are now one case that runs on every live cell, forAsia/ShanghaiandAmerica/New_York. It asserts:res.sqlis undefined;refusal;driver.dateBucketSql(...)(it wasnot.toContain('date_trunc')).SqlDriveris built for better-sqlite3, pg and mysql2 and never connected; itsdialectNameanddateBucketSqlanswer from the config alone. It is wired the way the plugin wires its two hooks. On each dialect, non-UTC × {month, week} refuses, and UTC or unset × {month, week} equals that driver's own expression. This pins MySQL without a server.date_trunccase and the no-hook SQLite refusal.Other
service-analyticstests that assert the fallback text, classified. All three stay: none sets atimezoneand none wires the hook, so each asserts the UTC path with no hook.dataset-selection-window.test.ts:357date_trunc('month', created_at)selection.timezone ?? context.timezone ?? 'UTC'to UTC, and no hook is wiredobjectql-daterange.test.ts:423date_trunc('month', close_date)cube-authored-format-granularity.test.ts:247date_trunc('month'regexNo assertion was deleted.
Ablation
Predicted first. Revert the in-memory refusal to
main's fallback (date_truncunless SQLite). Then 11 cases turn red:The SQLite cases and every UTC case stay green.
Observed. At
ff399dae2d(the fix committed first), throughscripts/ablation-replace.mjsin wrap mode, with live PostgreSQL:1ebcf8942983tob4081144edee;Restore proven:
1ebcf8942983, which is the HEAD blob;git diff HEADis empty, andgit status --porcelainis empty.There is no build leg: the pin imports the strategy from
srcby relative path.Tests and gates, at head
267d2202ae(after mergingorigin/main)pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2, withOS_TEST_POSTGRES_URLset (PostgreSQL 16.14) andTZ=America/New_York: 176 files passed; 4451 tests passed, 2 skipped.pnpm --filter @objectstack/service-analytics typecheckis clean.tsc --listFilesincludes the pin.--filter '@objectstack/service-analytics^...') and the package itself.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 64 commands, and all 64 exited 0.--ranreconciled: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. The dist-reading gates (check:dts-closure,check:dual-build-cjs-loads,check:sourcemap-no-sources-content,check:published-files,check:lean-entry-closure) were re-run after the package's own build, and all exited 0..tsfiles, with none ignored, under--no-inline-config(aspnpm lintruns).--format jsoncounted 3 files, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting (noparserOptions.project, no typed rules), so this diff cannot move the verdict on a file it does not touch.Overlap with #21577
Open draft PR #21577 edits this pin's header docblock (the CI-provisioning sentence about
OS_TEST_POSTGRES_URL). This PR's docblock edits start one line below that sentence and leave it untouched. A localgit merge-treeof this head with #21577's head6e12d6934aexits 0, and the merged docblock keeps both intents. Once #21577 lands, CI runs the live PostgreSQL REFUSAL case.Docs and skills
content/docs/**(outsidereleases/andreferences/): grepped for the analyticssqlecho,date_truncand timezone bucketing. No sentence is made false by this change.data-api.mdx's/analytics/sqlsection declares the success shape only.skills/**: two sentences namedate_trunc,objectstack-ui/rules/dashboards.md:326andobjectstack-query/rules/aggregation.md:82. Both are already carried by skills(objectstack-ui): the dashboards rule says Postgres buckets with date_trunc; the SQL driver groups by to_char(... AT TIME ZONE UTC) on Postgres #21588, the first in its body and the second in comment 5973137901. Neither is made false by this change, and neither is edited here.Acceptance notes
AnalyticsServiceConfig.dateBucketSqlTSDoc fix inanalytics-service.tsis outside the claim's listed file surface. It is the published sentence this change made false, in the same package and the same defect class, and the edit is mechanical. No open PR touches the file. The seat records the surface addition.date_truncis still reachable at a UTC or unsettimezone, where the hook answers nothing and the dialect is not SQLite. That arm is outside the ruling's "the engine buckets in memory (a non-UTCtimezone)", so it is not widened here:queryDateGranularity) and answers2026-01keys, while both faces echodate_trunc('month', closed_at), a statement nothing ran.dateBucketSqland an unknown dialect. Not measured: there is no server.plugin.tscomment. ThedateBucketSqlbridge comment inplugin.ts("undefinedkeeps the echo's representativedate_trunc(…)") has been inexact on SQLite since PR fix(driver-sql,service-analytics): bucket the ISO week natively on SQLite, and the SQL echo refuses a bucket SQLite cannot run #21629. It is an internal comment, not in the published types, and this change does not touch it. Carrier: none.Generated by Claude Code