Repository navigation
Commit 31e3e00
Part of #21647
Clause-②: no
The ObjectQL face's SQL echo now prints a date-bucketed dimension in one
thing only: the expression the driver itself renders for it (the
`dateBucketSql` hook), at a `timezone` of `UTC` or none. Everywhere else
it answers the declared refusal (`NOT_IMPLEMENTED` / 501, `refusal:
true`) that the non-UTC arm and SQLite already gave. The
`date_trunc('GRANULARITY', col)` fallback is gone, and so is the
docblock that declared it "representative", per triage's ruling
`5974759561`.
## Reproduction, on `main` `1968d5e812`, real `driver-memory`
A throwaway test (never committed, deleted) served `analytics.query` and
`analytics.generateSql`, the seams `POST /api/v1/analytics/query` and
`POST /api/v1/analytics/sql` forward, through the real
`AnalyticsServicePlugin` and a real `ObjectQL` engine over
`InMemoryDriver`, with the driver's `aggregate` and `find` spied.
| timezone | granularity | driver asked | face rows | `/analytics/query`
`sql` | `/analytics/sql` |
|:--|:--|:--|:--|:--|:--|
| UTC | month | `find` only | `2026-01` 27, `2026-02` 1 |
`date_trunc('month', closed_at)` | `date_trunc('month', closed_at)` |
| UTC | week | `find` only | `2026-W02` 20, `2026-W05` 7, `2026-W06` 1 |
`date_trunc('week', closed_at)` | `date_trunc('week', closed_at)` |
| unset | month, week | `find` only | the same | the same | the same |
After the fix (`ee0abf936e`), the same harness answered the same rows,
with no `sql` on the query and `NOT_IMPLEMENTED` / 501 on the dry run,
in all four cells.
## The rule, and how it relates to the engine
`dimExpr` in `ObjectQLStrategy.generateSql`:
1. a non-UTC `timezone` refuses (the #21630 arm, unchanged);
2. otherwise the hook's answer is printed;
3. otherwise it refuses, on every dialect. The SQLite arm of #21595 is
now just the first case of this.
The refusal names its cause. The zone arm keeps its message. The
no-expression arm says either that the driver names no SQL dialect (the
in-memory and MongoDB drivers, or a host with no hook), or that no
expression is known on this dialect (the engine buckets it in memory, or
the host wires no hook).
**The engine's own predicate is not reachable from this package.**
`engine.aggregate` decides pushdown inline: `typeof driver.aggregate ===
'function'`, every granularity in `supports.queryDateGranularity`,
`!tzRequiresInMemory`, and no `aggregations[].filter`. No `IDataEngine`
member exposes that decision, and this card allows no `objectql` edit.
So the strategy reads the zone (the same test on the same value as
`tzRequiresInMemory`) and the hook. Why the hook is the engine's
capability signal: every `driver-sql` dialect's `dateBucketSql` answers
`null` exactly where its `supports.queryDateGranularity` is false. Both
read `dateGranularityCapabilities`, the first line of
`buildDateBucketExpr`. Where this reading and the engine's predicate
differ, the cases are named, not hidden:
- **The engine buckets in memory, and the echo still prints**, because
the hook answers and the expression, run on that dialect, answers the
face's keys:
- a measure carrying its own `filter`: the engine aggregates every such
query in memory. The existing echo pin measures that running the echo
reproduces the face's rows, live on SQLite and PostgreSQL;
- `driver-turso`'s remote face: it advertises `queryDateGranularity:
{}`, inherits the SQLite expression (`REMOTE_FACE_ANSWERS.dateBucketSql:
'inherited'`), and libSQL runs it.
See the open question in the report: the literal headline of triage's
ruling covers both.
- **The engine pushes down, and the echo refuses:** `driver-mongodb` (by
code path) publishes `queryDateGranularity` for all five granularities
(`MONGODB_DATE_GRANULARITIES`), so its own pipeline groups the bucket.
This corrects the card's premise that MongoDB buckets in memory. MongoDB
runs no SQL, and triage's ruling names the refusal as the honest answer
on a non-SQL driver. A SQL host that wires no hook refuses too: it
cannot name the expression.
## The enumeration pin: `objectql-echo-bucket-enumeration.test.ts`
Driver x timezone class x granularity, 200 cells. Every cell runs
through the real plugin bridges and a real engine with the driver's two
data doors spied. Each cell therefore **observes** where the engine put
the bucket, and its expected echo is derived from that observation:
- pushed down to a driver that renders an expression for the bucket:
that expression on both faces;
- anything else: the refusal on `/analytics/sql`, and no `sql` on
`/analytics/query`.
The single named exception is Turso's remote face.
- **Driver axis:** `BUILTIN_DRIVER_IDS` from `@objectstack/spec/data`.
`DRIVER_ROWS` satisfies a `Record` keyed by `BuiltinDriverId`, and a
case asserts the key sets are equal, so a new builtin with no row fails
both typecheck and the file.
- Real: `sqlite` (better-sqlite3), `sqlite-wasm` (sql.js), and
`postgres` / `mysql` (`driver-sql`, never connected).
- By code path: `memory` (`supports = {}`, no `dialectName`, no
`dateBucketSql`), `mongodb`, and `turso`'s local and remote faces.
- **Timezone axis:** `UTC`, unset, and any other zone (`Asia/Shanghai`,
`America/New_York`, `Etc/UTC`). No enum declares these classes; the
engine's inline test does. So a case asserts each probe's tier on the
engine, and a zone that moves class turns red. `Etc/UTC` is in the
in-memory class because the engine treats only the literal `UTC` as UTC.
- **Granularity axis:** `TimeUpdateInterval.options`, the enum
`timeDimensions[].granularity` parses. The cells are generated from it,
so a new granularity has its cells the moment it is declared.
Observed matrix at `1ef5b5e198`, with a temporary log line, since
removed:
| face | UTC / unset | any other zone |
|:--|:--|:--|
| memory (by code path) | in memory, refusal | in memory, refusal |
| mongodb (by code path) | pushdown, refusal | in memory, refusal |
| sqlite, sqlite-wasm, postgres, mysql, turso local | pushdown, the
driver's expression | in memory, refusal |
| turso remote (by code path) | in memory, the driver's expression (the
named exception) | in memory, refusal |
A rows case serves real rows through the engine over `driver-memory`'s
surface. These are the rows measured above: `2026-01` 27 and `2026-02`
1; `2026-W02` 20, `2026-W05` 7 and `2026-W06` 1. `/analytics/query`
answers them with no `sql`, and the dry run refuses.
## Pins turned (no assertion deleted)
| pin | asserted | now | why |
|:--|:--|:--|:--|
| `objectql-echo-date-bucket.test.ts`, FALLBACK block |
`date_trunc('month', closed_on)` on a host with no hook, at UTC | the
refusal, at UTC and unset | the no-hook UTC arm is the case this card
retires |
| `dataset-selection-window.test.ts` (was :350) | `date_trunc('month',
created_at)` | with a hook: the hook's expression in `SELECT` and `GROUP
BY`; with no hook: the rows and no `sql` | its subject, "a bucket
expression instead of the bare column", needs a host that has one |
| `objectql-daterange.test.ts` (was :423) | `date_trunc('month',
close_date)` beside the window | the hook's expression beside the same
window | without a hook the dry run refuses, and the case's subject, the
window, would go with it |
| `cube-authored-format-granularity.test.ts` (was :247) |
`date_trunc('month'` | the hook is asked for `placed_at:month` and its
expression groups; the no-hook host refuses; the undeclared dimension
renders identically on both hosts | its subject is the declared default
granularity reaching the dry run |
The measure-filter case in the echo pin keeps its assertion, with a
comment naming it as one of the two in-memory cells that print.
## Ablation
The prediction was written before running. Restoring `main`'s fallback
(SQLite refuses, any other dialect prints `date_trunc`) in place of the
no-expression refusal should turn exactly 28 cases red:
- enumeration: memory UTC and unset, 10; mongodb UTC and unset, 10; the
rows case, 4;
- echo pin: the no-hook UTC and unset cases, 2;
- `dataset-selection-window`: 1;
- `cube-authored-format-granularity`: 1.
Everything else should stay green: every SQL-driver UTC/unset cell,
every non-UTC cell, the SQLite no-hook case, the Turso exception and
`objectql-daterange`.
Observed at `1ef5b5e198` (fix committed first), through
`scripts/ablation-replace.mjs` in wrap mode under the verify lock:
- the anchor went from 1 hit to 0, and the replacement from 0 to 1; the
blob went `b6ff007d3371` to `895dda1491ae`;
- **28 failed, 275 passed, 9 skipped (312)**, exactly the predicted set.
Restore: the blob after restore is `b6ff007d3371`, equal to HEAD; `git
diff HEAD` is empty; `git status --porcelain` is empty; there are 0
marker hits. The same result was observed earlier at `d0875f3a82`, when
the memory row was the real driver. There is no dist leg: every subject
is imported from `src` by relative path.
## Tests, at `1ef5b5e198`
- **Full suite:** `pnpm --filter @objectstack/service-analytics exec
vitest run --maxWorkers=2`: 177 files passed; 4403 tests passed, 262
skipped. The skips are the live-server cells.
- **Live PostgreSQL 16.14** (throwaway cluster, since stopped and
deleted), `OS_TEST_POSTGRES_URL` set: the echo pin plus the enumeration,
260 of 260 passed, 0 skipped, the live postgres cell included.
- **Typecheck:** `pnpm --filter @objectstack/service-analytics
typecheck` exits 0, and `tsc --listFiles` includes all five touched test
files.
- **Build:** `pnpm --filter @objectstack/service-analytics build` exits
0. `dist/index.d.ts` carries the corrected
`AnalyticsServiceConfig.dateBucketSql` TSDoc, and the old
"representative `date_trunc`" text has 0 hits.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 64 commands. All 64
exited 0. `check:dual-build-cjs-loads` first exited 3 (`PREREQUISITE NOT
MET`) and exited 0 after a full `turbo run build`, which had 71 of 73
tasks cached. `--ran` with exit codes reports 64 derived, 64 run, 0
NOT-MEASURED, 0 UNRUN.
- **Lint, a declared narrowing:**
1. eslint's own config admits all 8 changed `.ts` files
(`--print-config` answers for each; none is reported ignored);
2. `--no-inline-config --format json`: 8 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 an untouched file.
## Docs
`content/docs/**` (outside `releases/`) has no sentence this makes
false. `data-api.mdx` says the dry run's "support depends on the
underlying driver/strategy". In `skills/**`,
`objectstack-ui/rules/dashboards.md:326` and
`objectstack-query/rules/aggregation.md:82` name `date_trunc` for the
engine's pushdown, not for the echo. Both are carried by #21588, and
neither is edited here.
## Acceptance notes
- The PM's assumption to pin `/analytics/query` on the real
`driver-memory` was falsified by `check:driver-memory-census`. Every
consumer of that package is ledgered, and a new one is a maintainer
ruling, not a ledger edit. The memory row and the rows case are pinned
by code path under the real engine, and the real driver was measured
before and after the change in an uncommitted repro. An intermediate
commit (`d0875f3a82`) added the devDependency; `1ef5b5e198` withdrew it,
and `package.json`, `vitest.config.ts` and `pnpm-lock.yaml` are
byte-identical to `main`.
- `native-sql-strategy.ts`, around `canHandle`, says the engine buckets
with "native date_trunc when UTC-safe". The drivers bucket with
`to_char`, `date_format` and `strftime`. The comment predates this PR,
this PR does not make it false, and nobody has been named to carry it.
- `main` gained two commits after this branch merged `759dbe9ed`: a spec
inline-grid retirement and a service-automation fix. Both are disjoint
from this package. The PR's CI runs on the merge ref.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72f3c74 commit 31e3e00
13 files changed
Lines changed: 688 additions & 89 deletions
File tree
- .changeset
- packages
- client/src
- runtime/src
- services/service-analytics/src
- __tests__
- strategies
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
493 | 493 | | |
494 | 494 | | |
495 | 495 | | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
496 | 502 | | |
497 | 503 | | |
498 | 504 | | |
| |||
699 | 705 | | |
700 | 706 | | |
701 | 707 | | |
702 | | - | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
703 | 712 | | |
704 | 713 | | |
705 | 714 | | |
706 | 715 | | |
| 716 | + | |
707 | 717 | | |
708 | 718 | | |
709 | 719 | | |
| |||
751 | 761 | | |
752 | 762 | | |
753 | 763 | | |
754 | | - | |
| 764 | + | |
755 | 765 | | |
756 | 766 | | |
757 | 767 | | |
758 | | - | |
| 768 | + | |
| 769 | + | |
759 | 770 | | |
760 | 771 | | |
761 | 772 | | |
| |||
Lines changed: 20 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
147 | | - | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
148 | 160 | | |
149 | 161 | | |
150 | 162 | | |
| |||
153 | 165 | | |
154 | 166 | | |
155 | 167 | | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
160 | 175 | | |
161 | 176 | | |
Lines changed: 33 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
90 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
91 | 98 | | |
92 | 99 | | |
93 | 100 | | |
| |||
97 | 104 | | |
98 | 105 | | |
99 | 106 | | |
| 107 | + | |
100 | 108 | | |
101 | 109 | | |
102 | 110 | | |
| |||
238 | 246 | | |
239 | 247 | | |
240 | 248 | | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
241 | 255 | | |
242 | | - | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
243 | 261 | | |
244 | 262 | | |
245 | 263 | | |
246 | 264 | | |
247 | | - | |
248 | | - | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
249 | 277 | | |
250 | 278 | | |
251 | 279 | | |
| |||
Lines changed: 33 additions & 10 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
51 | 54 | | |
52 | 55 | | |
53 | 56 | | |
54 | 57 | | |
55 | | - | |
| 58 | + | |
56 | 59 | | |
57 | 60 | | |
58 | 61 | | |
| |||
61 | 64 | | |
62 | 65 | | |
63 | 66 | | |
| 67 | + | |
64 | 68 | | |
65 | 69 | | |
66 | 70 | | |
| |||
347 | 351 | | |
348 | 352 | | |
349 | 353 | | |
350 | | - | |
351 | | - | |
352 | | - | |
353 | | - | |
354 | | - | |
355 | | - | |
356 | | - | |
357 | | - | |
358 | | - | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
359 | 375 | | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
360 | 383 | | |
361 | 384 | | |
362 | 385 | | |
| |||
Lines changed: 11 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
402 | 402 | | |
403 | 403 | | |
404 | 404 | | |
405 | | - | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
406 | 415 | | |
407 | 416 | | |
408 | 417 | | |
| |||
420 | 429 | | |
421 | 430 | | |
422 | 431 | | |
423 | | - | |
| 432 | + | |
424 | 433 | | |
425 | 434 | | |
426 | 435 | | |
| |||
0 commit comments