Skip to content

ci(temporal-conformance): give the non-SQL step the job's live PostgreSQL URL - #21577

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21564-temporal-pg-url
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21564-temporal-pg-url

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21564
Clause-②: no

What this changes

The Temporal Conformance (live PG + MySQL) job's step "Run the non-SQL temporal backends under the skewed process zone" now sets OS_TEST_POSTGRES_URL in its env:, with the same literal the job's three SQL steps use (postgres://postgres:postgres@127.0.0.1:5432/postgres), for the postgres service the job already provisions. Until now that step set only TZ, so every service-analytics live-PostgreSQL cell (describe.skipIf(!config)) was a named skip there while the job read green.

The header clause "no CI step provisions that variable for this package" in 15 service-analytics test files becomes false with this wiring. It is corrected, comment-only, to name the step that now sets it. No other line of those files changes: every changed line in them is a * comment line. The 15th file, objectql-echo-date-bucket.test.ts, landed on main after this PR was opened, and the takeover added it (claim amendment 5973803595).

No other job, no other step, and no timeout changes. No cell is re-skipped.

What the step needed, measured (A2)

Only the one env: line:

  • Build closure. turbo run build --filter=@objectstack/service-analytics... --dry=json selects 16 packages, including @objectstack/driver-sql, @objectstack/objectql and @objectstack/driver-sqlite-wasm (dev dependencies of service-analytics). The step's existing build already builds what the cells import.
  • PostgreSQL client. pg is an optional peer of driver-sql, resolved by pnpm into driver-sql's knex instance; require.resolve('pg') from that knex answers pg@8.22.0. The cells reach pg through SqlDriver, so nothing is added to service-analytics.
  • Database and tables. Every cell connects to the default postgres database and creates and drops its own tables. The table names are distinct across the 17 files that read the URL (os21316_deal, os21505_coercion, os21441_bucket_deal, ...), so parallel vitest workers do not collide.
  • Server zone. The job's existing step "Point both servers at a non-UTC timezone" sets the server to Asia/Shanghai before any test step.

Readings

  • A3. No service-analytics file reads OS_EXPECT_LIVE_DIALECT_MATRIX (git grep exit 1), and no code in the step's other four packages reads it. So the flag is not added to the step.
  • A4. No service-analytics file reads OS_TEST_MYSQL_URL or any other server variable the job provides. Its only process.env reads are OS_TEST_POSTGRES_URL (34, in 17 files), NODE_ENV (10) and TZ (4). Nothing is listed for MySQL.
  • A5. core, formula, driver-memory and driver-mongodb have zero OS_TEST_POSTGRES_URL hits (git grep exit 1 for each). Adding the variable to the step starts no test in them.

Acceptance

Acceptance notes

  • The ruling says each of these cells "asserts a non-UTC server". At this tree, 1 of the 17 files does (read-scope-temporal-coercion.test.ts, show timezone). The other 16 rely on the job's provisioning step for the server's zone. Noted, not changed.
  • packages/plugins/plugin-security/src/rls-boolean-comparand-door.test.ts:40 carries the same "no CI step provisions" clause about its own package. This PR's step does not run that package, so the clause there stays true and is not touched.
  • Stall guard. check:stall-guard-budget reads this step's static budget as window 10m, cap 20m, job budget 30m, slack 10m. The step ran 3m38s on this head.
  • Release. Nothing publishes: .github/workflows/ci.yml, plus comments in test files outside service-analytics's files[]. No changeset; skip-changeset.
  • Governed. check-governed-merges.mjs: 0 of 16 paths hit the register, NOT governed; 118 changed lines (+80 / −38).

Gates

Run on head 6e12d6934a: the merge 1866c0185c of origin/main 5b5e83f446, plus the header commit. Each exit code was recorded before any pipe.

  • dispatch-gates.mjs --commands derived 87 commands. All 87 ran, and all exited 0. --ran reconciled them as 87 derived, 87 run, 0 NOT-MEASURED.
  • check:changeset-gate-self-tests was re-run once, alone, after the container's commit-signing hop answered 503 inside its scratch repository on the first pass. The re-run exited 0.
  • service-analytics: its build closure built, and objectql-echo-date-bucket.test.ts passed without the URL (10 passed, 8 PostgreSQL cells skipped locally). Its full suite with the URL is in Acceptance above.

Opened by domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM); taken over by domain:spec seat 1 (session_01T9u38rswFp5Rw8DswRUReJ, claim 5973375306).

claude added 2 commits October 3, 2026 09:28
…eSQL URL

The step "Run the non-SQL temporal backends under the skewed process zone"
runs service-analytics, whose live-PostgreSQL cells are keyed on
OS_TEST_POSTGRES_URL (describe.skipIf(!config)). The step set only TZ, so
those cells were named skips while the job read green. It now receives the
same URL the job's SQL steps use, for the postgres service the job already
provisions.

The test files' header clause "no CI step provisions that variable for this
package" becomes false with this wiring; it is corrected, comment-only, to
name the step that now sets it.

Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 5b5e83f446bde0bf6e13db304b9f07f115635704 → packageMentionDocs.

This was referenced Oct 3, 2026
…mporal-pg-url

Brings in PR #21611 (440cd32), which buckets a Field.date as its calendar day on PostgreSQL and MySQL -- the fix for the four live-PostgreSQL objectql-face-order-limit cells this branch's wiring exposed.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
claude added 2 commits October 3, 2026 21:52
Brings in objectql-echo-date-bucket.test.ts as main now has it (#21629),
the 17th OS_TEST_POSTGRES_URL reader, so its header clause can be
corrected in the PR that makes it false.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
…visions the PostgreSQL URL

The postgres-cell clause 'No CI step provisions that variable for this package,
so the live cell is red-capable and un-run in CI' is false once the Temporal
Conformance job's non-SQL step sets OS_TEST_POSTGRES_URL. Reworded the way the
same clause reads in the other 14 service-analytics test files. Comment text
only: no assertion, skip or test body changes.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
Merged via the queue into main with commit 0c50b5d Oct 3, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21564-temporal-pg-url branch October 3, 2026 23:20
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…L and MySQL (objectstack-ai#21611)

Fixes objectstack-ai#21485
Clause-②: no

## What changes

`SqlDriver.buildDateBucketExpr` now reads the column's declared type
from the object it already receives. A declared `Field.date` is bucketed
as its own calendar day with no zone conversion. A `Field.datetime`, or
a column with no declaration, keeps the UTC-instant expression byte for
byte.

| dialect | `Field.date` (new) | `Field.datetime` / undeclared
(unchanged) |
|:--|:--|:--|
| PostgreSQL | `to_char((col)::date::timestamp, FMT)` |
`to_char((col)::timestamptz AT TIME ZONE 'UTC', FMT)` |
| MySQL | `date_format(col, FMT)` | `date_format(convert_tz(col,
@@session.time_zone, '+00:00'), FMT)` |
| SQLite | unchanged (`strftime` on TEXT, no zone) | unchanged |

Both callers render this one function, so both follow without a second
edit: `aggregate()`'s GROUP BY (`buildDateBucketExpr(g.field,
g.dateGranularity, table)`, with `table = coercionKey(builder)`), and
the public `dateBucketSql(objectName, …)` member that the analytics SQL
echo reads. No `service-analytics` source is touched.

## The mechanism, measured

PostgreSQL 16.14 with the server's `TimeZone` at `Asia/Shanghai`, at the
expression:

| `date` value | old day | old week | old month | new (all five
granularities) |
|:--|:--|:--|:--|:--|
| `2026-06-01` | `2026-05-31` | `2026-W22` | `2026-05` | `2026-06-01` /
`2026-W23` / `2026-06` / `2026-Q2` / `2026` |
| `2026-01-01` | `2025-12-31` | `2026-W01` | `2025-12` | `2026-01-01` /
`2026-W01` / `2026-01` / `2026-Q1` / `2026` |
| `2024-12-30` | `2024-12-29` | `2024-W52` | `2024-12` | `2024-12-30` /
`2025-W01` / `2024-12` / `2024-Q4` / `2024` |

Casting a `date` to `timestamptz` makes midnight in the session zone;
`AT TIME ZONE 'UTC'` then reads the previous UTC day on any session east
of UTC. A session west of UTC does not shift (midnight local is later
the same UTC day), which is why only east-of-UTC servers show it.

**Why `::date::timestamp` and not the bare `::date` the dispatch
suggested.** `EXPLAIN VERBOSE` shows `to_char((d)::date, …)` resolves to
`to_char((d)::timestamp with time zone, …)` through the implicit cast,
so the bare form still round-trips the session zone. Measured over every
day 1900..2100 in ten zones, that round trip differs from the zone-free
form on exactly one day each in `Pacific/Apia` (`2011-12-30` prints
`2011-12-31`) and `Pacific/Kiritimati` (`1994-12-31` prints
`1995-01-01`), the days those zones skipped. `::date::timestamp`
resolves `to_char(timestamp without time zone, …)` and consults no zone.

**MySQL (H2), measured on MySQL 8.0.46** (the Ubuntu
`mysql-server-core-8.0` binary, run from a private datadir in this
container) at a global `+08:00`. At the expression, `convert_tz(d,
'+08:00', '+00:00')` shifts a `DATE` exactly like PostgreSQL:
`2026-06-01` → day `2026-05-31`, month `2026-05`; `2024-12-30` →
`2024-W52`. Through the driver's default composition it does not,
because the driver pins its own session to `+00:00` (`withUtcSession`),
which makes `convert_tz` the identity. A host `pool.afterCreate` that
sets the session zone (the driver chains it after its own hook) brings
the shift back. Per triage's ruling ("gets the same split if a `DATE`
shifts"; "No server-timezone dependence for a `date` on any dialect"),
the MySQL arm gets the same split.

## Pins

-
`packages/drivers/driver-sql/src/sql-driver-21485-date-bucket-calendar-day.test.ts`
(new). One matrix over `DIALECT_CELLS` through `declareDialectCell`:
`date` and `datetime` × every granularity the dialect buckets in SQL
(declared per dialect and asserted equal to
`supports.queryDateGranularity`, so the iterated set cannot shrink
silently) × session zone. Each cell asserts both doors: `aggregate()`
and the `dateBucketSql()` text run as SQL. The rows sit on month,
quarter, year and ISO-week boundaries, plus the empty bucket.
  - sqlite: the zone-free control.
- live postgres / live mysql, each twice: **as provisioned** (both axes,
after `assertThreeWayZoneSkew`) and **session at +08:00 through a host
`pool.afterCreate`** (the `date` axis, after asserting the session
really is at `+08:00`). The second run is red-capable on any server, and
it is the only red-capable route on MySQL.
- `sql-driver-temporal-dialect.test.ts`: a no-server pin (runs in Test
Core too). On `pg` and `mysql2`, a declared `Field.date` expression
names no `timestamptz`, `time zone`, `convert_tz` or `time_zone`; on
`pg` it carries `::date::timestamp`; the undeclared control keeps the
UTC-instant arm.

**Where the live pins run in CI (H4).** Job `Temporal Conformance (live
PG + MySQL)` (required), step `Run driver-sql suite against both live
servers`. It runs the whole driver-sql package with
`OS_TEST_POSTGRES_URL`, `OS_TEST_MYSQL_URL`,
`OS_EXPECT_LIVE_DIALECT_MATRIX=1` and `TZ=America/New_York`, with
PostgreSQL at `Asia/Shanghai` and MySQL at a global `+08:00`. No
workflow edit is needed. PR objectstack-ai#21577 (objectstack-ai#21564) wires the separate
`service-analytics` step and is not part of this PR.

## Reverse verification and ablation

All runs: PostgreSQL 16.14 at `Asia/Shanghai`, MySQL 8.0.46 at `+08:00`,
process `TZ=America/New_York`, `OS_EXPECT_LIVE_DIALECT_MATRIX=1`; the
new file plus `sql-driver-temporal-dialect.test.ts`.

1. **Before the fix was written** (the test committed alone,
`26b8310fb`): the new file 15 failed / 32 passed of 47. The `date` axis
was red at all five granularities on live postgres as provisioned, live
postgres at +08:00, and live mysql at +08:00. Live mysql as provisioned
(the driver's UTC pin), every `datetime` cell and sqlite were green.
2. **Fix committed (`aa061685b`), then `sql-driver.ts` reverted to the
base text** (`git restore --source=f6b752083`; on-disk anchors
`calendarDay` 3 → 0, the old PostgreSQL month text 0 → 1): **16 failed /
45 passed of 61**. That is the same 15 live cells, plus the no-server
pin. Restored with `git checkout HEAD --
packages/drivers/driver-sql/src/sql-driver.ts`. The `hash-object` was
`be60fd469…`, equal to the HEAD blob; `git diff HEAD` and `git status
--porcelain` were empty.
3. **Fix in place:** 61 of 61 green.
4. **Ablation, the `::timestamp` hop** (`scripts/ablation-replace.mjs`,
anchor 1 → 0): `(??)::date::timestamp` replaced by the bare
`(??)::date`. Predicted: the live pins stay green at `Asia/Shanghai`
(the round trip is the identity there) and only the no-server pin reds.
Observed: **1 failed / 60 passed**, the no-server pin. Restored blob =
HEAD, `git diff HEAD` empty.
5. **The four `service-analytics` cells the card measured**
(`objectql-face-order-limit.test.ts`, which resolves
`@objectstack/driver-sql` through `dist/`): the fix was disabled in
`dist/` (`const calendarDay = false`, rebuilt, `ablation-dist-preflight`
marker present in 2 built files). Result: **4 failed / 22 passed of
26**, exactly the card's four. After restore + rebuild + `--absent`
preflight: the file plus `objectql-echo-date-bucket.test.ts` gave 42 of
42 green against live PostgreSQL at `Asia/Shanghai`. **No assertion
change is needed** in `objectql-face-order-limit.test.ts`, so this PR
does not touch it.

## Dispatch hypotheses

- **H1, holds.** Both call sites pass the object name as the coercion
key (`aggregate`: `coercionKey(builder)`; `dateBucketSql`: `objectName`,
which the analytics echo fills from `extractObjectName(cube)`).
`temporalFieldKind(table, field)` answers `'date'` from `dateFields`,
which only `type: 'date'` populates. No new parameter.
- **H2, measured** (above). PostgreSQL: the card's mechanism, confirmed.
MySQL: the `DATE` shifts under any east-of-UTC session; through the
driver only with a host-set session zone.
- **H3.** Only `type: 'date'` feeds `dateFields`. SQLite stores a `date`
as TEXT, and `strftime` reads it with no zone, so the SQLite arm has no
instance of this class; its sqlite cells are the control. The SQLite
`week` arm is untouched (objectstack-ai#21595).
- **H4,** answered above.

## Driver conformance ledger

`pnpm check:driver-conformance`, before (`f6b752083`) and after
(`c20518cfd7`): **50 covered, 0 DEBT, 0 exempt**, both times. The
dialect axis was 8 suites (7 matrix, 1 named cell), 0 DIALECT ledger,
both times. The new file imports no shared `spec/data` case-set, so it
adds no cell. A spec-level shared case-set would have made every one of
the five drivers consume it or carry DEBT. That is outside this card's
file surface and against the no-new-DEBT commitment, so the matrix is
driver-local.

## Local verification

- `pnpm --filter @objectstack/driver-sql exec vitest run
--maxWorkers=2`, with all three dialects live as in CI, at `eab3f3bde`
(after merging `origin/main` `b610eabf7`): **228 files, 5521 passed, 1
skipped** (the skip is pre-existing, in
`schema-drift.base-type-mismatch.test.ts`). The run printed "all 3
dialects were exercised".
- At the final head `c20518cfd7`, whose last commit only removes the `as
any` casts in the new test: `pnpm --filter @objectstack/driver-sql
typecheck` was green (`--listFiles` lists all 228 test files). The new
file, `sql-driver-temporal-dialect.test.ts` and
`live-dialect-matrix.isolation.test.ts` gave **81 of 81** green with all
three dialects live.
- Gates at `c20518cfd7`: `node scripts/pm/dispatch-gates.mjs --commands`
derived 65, and all 65 were run with exit 0. `--ran` printed: "65
derived famil(ies) accounted for, 65 run, 0 NOT-MEASURED". One gate was
red on the way: `check:query-options-erasure`, because the new test's
`as any` raised the test surface 236 → 237. It was answered by typing
the query and the config, not by raising the number.
- ESLint, a narrowed run: the three changed `.ts` files, all in the lint
population (`--print-config` resolves a config for each), 3 files / 0
errors / 0 warnings in the `--format json` output at `c20518cfd7`.
`eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move any untouched file's
verdict. The full `pnpm lint` is CI's.

## Acceptance notes

- **MySQL `datetime` under a host-set session zone (outside this card's
ruling, not changed).** Measured through the driver on MySQL 8.0.46 with
a host `pool.afterCreate` setting `time_zone = '+08:00'`:
`2026-06-01T03:00:00.000Z` stores `2026-06-01 03:00:00.000` in
`DATETIME(3)`, and `find()` reads it back correctly, but `aggregate()`
by `day` answers `2026-05-31`. `convert_tz(…, @@session.time_zone,
'+00:00')` is right for a legacy `TIMESTAMP` column and wrong for a
`DATETIME(3)` that already holds the UTC wall clock. Under the driver's
default composition it is the identity. No in-repo host sets the session
zone, so this is noted here, not filed.
- An external (federated) object that declares `type: 'date'` over a
remote `timestamptz` column would now bucket by the session-local day
(`::date` on a `timestamptz`). That is an inference only, not measured,
and no producer is named.
- `service-analytics` comments (`strategies/objectql-strategy.ts`,
`strategies/types.ts`) and the `objectql-echo-date-bucket.test.ts`
docblock quote the PostgreSQL `… AT TIME ZONE 'UTC' …` text. That is
still the `datetime` expression; they are not edited, because a
`service-analytics` source edit is outside this PR's surface.
- Raise rule: no hosted or shipped deployment was measured running
PostgreSQL at a non-UTC `TimeZone`. The non-UTC readings here are a
private server in this container.
- Merged `origin/main` `b610eabf7` (two commits, `packages/spec` and
docs only, disjoint from this diff) before the full-suite run.

---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… buckets in memory, on every dialect (objectstack-ai#21630) (objectstack-ai#21645)

Fixes objectstack-ai#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-UTC `timezone` (ADR-0053 Phase 2,
D2; `tzRequiresInMemory` in objectql's `engine.ts`), the echo answers
the refusal PR objectstack-ai#21629 introduced on SQLite, `NOT_IMPLEMENTED` / 501 with
`refusal: true`. No `date_trunc` text for that case on any dialect.

- The check runs in `dimExpr` **before** the `dateBucketSql` hook is
asked, on the test the engine makes (`timezone` set 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.
- `sqliteBucketEchoRefused` becomes `bucketEchoRefused`, 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).
- UTC and an unset `timezone` are 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 representative `date_trunc`". It ships in
`dist/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.
- Changeset: `@objectstack/service-analytics` patch, `Clause-②: no`.

No driver change, no `packages/objectql` edit, no `packages/spec` edit,
no zone-aware expression ("Not this card" in the ruling).

## Reproduced first, on `main`

PostgreSQL 16.14, a throwaway server with `TimeZone` = `Asia/Shanghai`,
process `TZ=America/New_York`, the pin's five rows, at `main`
`7b07749f05`. The echo text is the same in the `/analytics/query`
answer's `sql` and in the `/analytics/sql` dry run
(`analytics.generateSql`, the body that route answers with).

| query `timezone` | face rows | echo printed | that echo, run on
PostgreSQL |
|:--|:--|:--|:--|
| `Asia/Shanghai`, month | `2026-01` 20, `2026-02` 8, `2026-03` 10,
`2026-04` 5 | `date_trunc('month', closed_at)` |
`2025-12-31T16:00:00.000Z` 20, `2026-01-31T16:00:00.000Z` 8,
`2026-02-28T16:00:00.000Z` 10, `2026-03-31T16:00:00.000Z` 5 |
| `America/New_York`, month | `2026-01` 27, `2026-02` 1, `2026-03` 10,
`2026-04` 5 | the same text | 20 / 8 / 10 / 5, on the session zone's
calendar: other groupings than the face |
| `UTC` or unset, month | `2026-01` 27, `2026-02` 1, `2026-03` 10,
`2026-04` 5 | `to_char(("closed_at")::timestamptz AT TIME ZONE 'UTC',
'YYYY-MM')` | identical to the face |

`week` gave the same shape (non-UTC echoed `date_trunc('week',
closed_at)`).

## Both faces after the change

Measured on the same server at `ff399dae2d`:

- **`/analytics/sql`** refuses with `NOT_IMPLEMENTED` / 501 and
`refusal: true`, and `declaredRefusalMessage(err)` returns
`err.message`. That is the same envelope the SQLite arm answers, raised
through the same error shape.
- **`/analytics/query`** returns the same rows as before for both zones
(table above, unchanged), and its answer has no `sql` key, because
`execute()` already swallows an echo refusal.
- **Not re-measured:** the HTTP envelope through the dispatcher. The
route forwards the error object the SQLite arm already raises, so it was
not measured again.

## Pins

`objectql-echo-date-bucket.test.ts`:

- **Turned around.** The PostgreSQL cell's `FALLBACK: a non-UTC timezone
... keeps date_trunc` asserted `date_trunc('month', closed_at)`, a
statement the engine did not run. That case and the SQLite `REFUSAL`
case are now one case that runs on every live cell, for `Asia/Shanghai`
and `America/New_York`. It asserts:
  - the rows, per zone;
  - that `res.sql` is undefined;
  - the dry run's code, status and `refusal`;
- a UTC control that now equals `driver.dateBucketSql(...)` (it was
`not.toContain('date_trunc')`).
- **New: a dialect matrix.** `SqlDriver` is built for better-sqlite3, pg
and mysql2 and never connected; its `dialectName` and `dateBucketSql`
answer 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.
- **New:** a host that wires no hook refuses a non-UTC zone.
- **Kept:** the no-hook UTC `date_trunc` case and the no-hook SQLite
refusal.

Other `service-analytics` tests that assert the fallback text,
classified. All three stay: none sets a `timezone` and none wires the
hook, so each asserts the UTC path with no hook.

| file | assertion | disposition |
|:--|:--|:--|
| `dataset-selection-window.test.ts:357` | `date_trunc('month',
created_at)` | stays: the dataset path resolves `selection.timezone ??
context.timezone ?? 'UTC'` to UTC, and no hook is wired |
| `objectql-daterange.test.ts:423` | `date_trunc('month', close_date)` |
stays: no timezone, no hook |
| `cube-authored-format-granularity.test.ts:247` | `date_trunc('month'`
regex | stays: no timezone, no hook |

No assertion was deleted.

## Ablation

**Predicted first.** Revert the in-memory refusal to `main`'s fallback
(`date_trunc` unless SQLite). Then 11 cases turn red:
- the live PostgreSQL REFUSAL cases (2);
- the matrix's postgres and mysql non-UTC cases (8);
- the no-hook non-UTC case (1).

The SQLite cases and every UTC case stay green.

**Observed.** At `ff399dae2d` (the fix committed first), through
`scripts/ablation-replace.mjs` in wrap mode, with live PostgreSQL:
- the anchor went from 1 hit to 0, and the blob from `1ebcf8942983` to
`b4081144edee`;
- the run answered **11 failed, 37 passed (48)**: exactly the predicted
set.

**Restore proven:**
- the blob is back to `1ebcf8942983`, which is the HEAD blob;
- `git diff HEAD` is empty, and `git status --porcelain` is empty.

There is no build leg: the pin imports the strategy from `src` by
relative path.

## Tests and gates, at head `267d2202ae` (after merging `origin/main`)

- **Full suite.** `pnpm --filter @objectstack/service-analytics exec
vitest run --maxWorkers=2`, with `OS_TEST_POSTGRES_URL` set (PostgreSQL
16.14) and `TZ=America/New_York`: 176 files passed; 4451 tests passed, 2
skipped.
- **The pin, verbose:** 48 passed. The live PostgreSQL cell ran.
- **Typecheck.** `pnpm --filter @objectstack/service-analytics
typecheck` is clean. `tsc --listFiles` includes the pin.
- **Build.** The dependency closure (`--filter
'@objectstack/service-analytics^...'`) and the package itself.
- **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 64 commands, and all 64
exited 0. `--ran` reconciled: 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.
- **Lint, a declared narrowing.** These three facts together make it a
measurement:
1. eslint's own config admitted the 3 changed `.ts` files, with none
ignored, under `--no-inline-config` (as `pnpm lint` runs).
  2. `--format json` counted 3 files, 0 errors, 0 warnings.
3. `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, no typed rules), so this diff cannot move the
verdict on a file it does not touch.
- **NOT MEASURED: MySQL live rows,** because there is no MySQL server in
the container. The matrix pins the echo there. The rows of a non-UTC
query are bucketed by the engine in memory on every dialect.

## Overlap with objectstack-ai#21577

Open draft PR objectstack-ai#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 local `git merge-tree` of this head with objectstack-ai#21577's head
`6e12d6934a` exits 0, and the merged docblock keeps both intents. Once
objectstack-ai#21577 lands, CI runs the live PostgreSQL REFUSAL case.

## Docs and skills

- **`content/docs/**`** (outside `releases/` and `references/`): grepped
for the analytics `sql` echo, `date_trunc` and timezone bucketing. No
sentence is made false by this change. `data-api.mdx`'s `/analytics/sql`
section declares the success shape only.
- **`skills/**`:** two sentences name `date_trunc`,
`objectstack-ui/rules/dashboards.md:326` and
`objectstack-query/rules/aggregation.md:82`. Both are already carried by
objectstack-ai#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

- **Scope note.** The `AnalyticsServiceConfig.dateBucketSql` TSDoc fix
in `analytics-service.ts` is 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_trunc` is still reachable at a UTC or unset `timezone`,**
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-UTC
`timezone`)", so it is not widened here:
- On a **driver-memory** datasource it was measured at the service seam:
the engine buckets in memory (the driver advertises no
`queryDateGranularity`) and answers `2026-01` keys, while both faces
echo `date_trunc('month', closed_at)`, a statement nothing ran.
- MongoDB takes the same code path, with no `dateBucketSql` and an
unknown dialect. Not measured: there is no server.
  - The no-hook pin keeps this arm on purpose.
  - Reported to the seat as a finding.
- **`plugin.ts` comment.** The `dateBucketSql` bridge comment in
`plugin.ts` ("`undefined` keeps the echo's representative
`date_trunc(…)`") has been inexact on SQLite since PR objectstack-ai#21629. It is an
internal comment, not in the published types, and this change does not
touch it. Carrier: none.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

3 participants