Skip to content

Commit 259459d

Browse files
qq9340100claude
andauthored
refactor(spec)!: retire array_agg / string_agg from AggregationFunction — count_distinct kept (#6527)
* refactor(spec)!: retire array_agg / string_agg from AggregationFunction — count_distinct kept (#6188) 维护者 2026-08-07 拆分裁决的 spec 退役半边。`count_distinct` 保留(SQL 实现 是 drivers 侧独立卡,本单未做);mongodb / memory 实现在 #5499 冻结内,未触。 ## 前提核验(对 origin/main @ 1818998) - `packages/spec/src/data/query.zod.ts:108` 的 `AggregationFunction` 仍是 8 成员, 三个未被 SQL 族编译的成员俱在。 - issue #6188 评论区 2026-08-07 16:59Z 维护者裁决在册,未被后续评论推翻;PM 认领 评论(2026-08-08 02:02Z)指定的分支名与本分支一致。 - 前提成立,照裁决执行。 ## 路线判定(这决定了该期待什么读数) 这是**枚举 VALUE 收窄**,不是 authorable KEY 退役。因此按 playbook: - 无 `retiredKey()` 墓碑 —— 处方挂在枚举自己的 error map 上,按 `issue.input` 分派,只有「曾经合法」的两个拼写才被告知 "was removed"(`crypto.hash` / `HookBodyCapability` 先例,#4391);拼错的 `arry_agg` 仍拿 zod 的合法值列表。 - 不进 `RETIRED_KEYS_BY_MAJOR`(那张表登记 authorable key)。 - **四张 ratchet 字节完全相同,这是本路线的正确读数**,不是漏做: `api-surface/`、`authorable-surface/`、`json-schema.manifest/`、 `api-surface-signatures.json` 全部零变化,`check:authorable-surface` / `check:api-surface` 绿。变化的生成物是 `spec-changes.json`、 `docs/protocol-upgrade-guide.md` 与四个 references 页。 - **ADR-0122 pin 注册表未触及**:`type-alias-convention.pin.test.ts` 里没有 `AggregationFunction` 的 pin(在册的是 AggregationStage / AggregationPipeline / AggregationNode / MetricAggregationType,均为 def 级,未变),所以没有要删的行、 也没有 :1491 附近的硬编码计数需要同步。 ## 两个退役键的墓碑说什么 处方(`ARRAY_AGG_RETIRED` / `STRING_AGG_RETIRED`,query.zod.ts)按房规五段式: 反引号全限定名 → `was removed in @objectstack/spec 17 (#6188, ADR-0049 enforce-or-remove)` → 为何本就无效(`SqlDriver.mapAggregateFunc` 与 `RemoteTransport.aggregate` 各只编译五个;v1 dataset runtime 还得按名字把它们减掉, 否则会落到 Cube strategy 的 `default` 返回 `COUNT(*)` —— 拿行数冒充作者要的数值) → 祈使句(删掉这条 aggregation / dataset measure;`string_agg` 额外说明它的方言分歧 最大,分隔符在 PostgreSQL 是第二参数、MySQL 是 `SEPARATOR`、SQL Server 是另一个函数名, 所以从来没有一个可 lower 的形状)→ `Run os migrate meta --from 16`。 ## ADR-0087 登记 - D2 conversion `dataset-measure-array-string-agg-removed`(toMajor 17, `retiredFromLoadPath: true`):唯一的**存量**作者面是 `dataset.measures[].aggregate`。整条 measure 被删除而不是只剥掉 `aggregate` —— 既无 `aggregate` 又无 `derived` 的 measure 过不了 dataset 自己的 superRefine, 产出无法 parse 的条目比不转换更糟;被删 measure 牵连的 derived measure 按不动点 一并清理(derived 可以引用 derived)。每删一条发一条 notice。fixture expectedNotices=4(两个退役 aggregate + 两条被牵连的 derived)。 - D3 semantic entry `query-array-string-agg-retired`:`QueryAST.aggregations[].function` 是**请求面**,从不入 stack metadata(与 `query.joins` / `cursor` / `distinct` 同类), 没有源可改写。 - chain step:id 已挂进 `MIGRATIONS_BY_MAJOR[17].conversionIds`,rationale 已扩写。 ## 消费半径(按规则的 consumption radius 扫,不是按包) 被 tsc 逼出来、必须随动的: - `objectql/in-memory-aggregation.ts`:删掉两条 case 臂(枚举没有的值做 switch case 不过编译),模块能力注释同步;测试改为 pin「fallback 不再计算它们」。 - `service-analytics`:`UNSUPPORTED_AGGREGATES` 现为**空集且保留** —— 它是 lockstep 套件强制的算术的一半(SUPPORTED = spec 词表 − 它),那条算术正是拦住「下一个加进 spec 的 aggregate 悄悄落到 COUNT(*)」的东西;refusal 分支保留为下一个未 lower aggregate 的落点,并在注释里写明当前不可达。 - `driver-sql` / `driver-turso`(**不在 #5499 冻结范围**):两个名字从 class 2 (declared-but-uncompiled → 501)移到 class 1(undeclared → 400)。这两个测试当初 就写好了迎接本单——注释原文写着「a name that leaves the enum when #6188 is decided — fails tsc」,实测正是这样炸出来的。turso 的 parity 表里两个条目从 `declaredAst` 换成 `undeclaredAst`:要守住的性质是两张脸对**新答案**依然逐字一致。 - `rest`:见下面的实测发现。 **未触**(裁决与 #5499 冻结):`driver-mongodb`、`driver-memory` 的实现。两者都用 自己的 `function: string`,不与 spec 枚举耦合,因此也没有编译压力——代码原样保留, 只是不再能通过一个 spec-valid 的请求抵达。 ## 实测发现(不是预测) 1. **REST 路由上这条 refusal 换了 producer**: `POST /analytics/dataset/query` 携带 `aggregate: 'string_agg'`,退役前是 `400 DATASET_INVALID`(dataset-compiler 的信封),退役后是 `400 VALIDATION_FAILED` (路由自己的 `DatasetSchema.parse`)。status 不变,code 变了。 `analytics-dataset-refusal-envelope.test.ts` 的那一行保留(它的 `listEntry` 是 #5367 删掉的五条正则的审计线索),但改为 pin 继任答案,并写明:该行**不再能** 证明路由会给 compiler 的 aggregate refusal 套信封——那条分支在 `UNSUPPORTED_AGGREGATES` 为空时端到端不可达;②–⑤ 行仍然扛着这个保证。 与其造一个合成 aggregate 维持旧形状,不如把真相写下来。 2. **处方在这条路由上被截断**:路由把 parse 错误放进 `detail` 并截到 1000 字符 (`rest-server.ts:7049`)。zod 包装后的消息 `array_agg` 1076 字符、`string_agg` 1304 字符,所以作者必须行动的两句都进得去(`was removed` 在 ~192、 `Delete the aggregation` 在 690 / 918),**收尾的 `os migrate meta --from 16` 一句被截掉**,只在这个面上。tsc 报错、直接 `DatasetSchema.parse`、`os validate` 上处方完整。已加测试把这个截断量到位(断言 detail.length === 1000 且不含 `os migrate meta`),抬 cap 或改结构化 issues 是 REST 车道对**所有**长处方的决定, 不是本词表该顺手做的。见下方范围外发现。 ## 反向验证(方向先写后跑) 预测:只删掉 error map、保留收窄后的枚举 → 四条处方测试变红,其余(名单 pin、 count_distinct 正例)保持绿——证明处方测试测的是 error map,而不只是「拒绝了」。 实测:`4 failed | 82 passed`,红的正是那四条。已还原。 ## 验证读数(全部前台阻塞执行) - `pnpm --filter @objectstack/spec test` → 340 files / 8717 tests passed - spec 门禁全绿:check:generated / authorable-surface / docs / api-surface / spec-changes / upgrade-guide / skill-refs / skill-docs / skill-examples / liveness / empty-state / variant-docs / strictness-ledger - typecheck 绿:spec(含 scripts + test 层)、objectql、lint、client、mcp、 metadata-protocol、driver-sql、driver-turso、driver-mongodb、driver-memory - 测试绿:objectql 146/2449、service-analytics 68/1390、driver-sql 69/959、 driver-turso 27/828、rest 64/885、dogfood expression-conformance 1/3 - `pnpm lint` EXIT=0;`check:adr-0087-registration`、`check:i18n`(补建 CLI 后 9 包 bundle in sync)、check:doc-authoring / role-word / quick-reference-counts / skill-compatibility / adr-anchors / empty-changeset / release-notes / engine-double-contract / error-code-casing / route-envelope 全 PASS - `node scripts/check-nul-bytes.mjs` OK;改动文件控制字符自扫无命中 - ⛔ `content/docs/releases/` 未触 ## 两处需要 PM 裁量的地方 1. **changeset 用了 `major`,不是派单说的 `minor`。** 派单写「@objectstack/spec minor(退役面)」,但 playbook 明写退役是 `major`,且树内三个同类先例 (query-distinct-removed / tool-inert-keys-removed / metadataformat-cachestrategy-single-declaration)都是 `major`;本变更确实是破坏性 收窄(导出枚举少两个取值,`AggregationFunction` 的 TS input 类型随之变窄, `dataset.measures[].aggregate` 不再接受)。写 `minor` 会在发布的 CHANGELOG 里 留下一个失真的 semver 声明,所以按正确语义落了 `major` 并在此声明偏差—— PM 若坚持 minor,改 `.changeset/aggregation-array-string-agg-removed.md` 第 2 行一处即可。 2. **范围外发现,未在本 PR 修**(drivers 车道 / #5907 的措辞): `driver-sql/src/sql-driver.ts:613` 与 `driver-turso/src/remote-transport.ts:590` 的 wire 文案仍写着「whether the declaration itself should stand is #6188 (ADR-0049 enforce-or-remove)」。#6188 已裁决:两个退役、`count_distinct` 保留待 实现,所以这句对唯一剩下的成员已经过时。两张脸的文案受 #5240「一个条件一种措辞」 约束且有逐字 parity 测试,改动属 #5907 那张卡,本单只更新了两处 docblock、 未动 wire 文案。另,上面第 2 条实测的 REST `detail` 1000 字符截断,是对**所有** 长处方生效的路由级行为,同样建议在 REST 车道单独立卡。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wd2qncNzNCfxYYeuNrkc53 * chore(changeset): ADR-0087 disposition marker for the aggregation retirement The two ledger entries were registered in the same commit; the gate requires the answer in writing on the changeset itself (#6148). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011M7UwH25Unfi73UHim7ajY --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 94e749b commit 259459d

32 files changed

Lines changed: 804 additions & 126 deletions
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/objectql": minor
4+
"@objectstack/service-analytics": patch
5+
---
6+
7+
refactor(spec)!: retire `array_agg` / `string_agg` from `AggregationFunction``count_distinct` deliberately kept (#6188, ADR-0049)
8+
9+
`AggregationFunction` declared eight functions; the SQL family compiles five.
10+
`SqlDriver.mapAggregateFunc` and the Turso `RemoteTransport.aggregate` each lower
11+
`count`/`sum`/`avg`/`min`/`max` and route everything else to one refusal, so
12+
three of the eight were declared-but-unenforced against the backends this
13+
platform targets — and, worse, the *set* each backend implemented was different,
14+
so "which aggregations can I use" had no answer an author could read off the
15+
schema.
16+
17+
What makes these two sharper than an ordinary inert declaration is that another
18+
package had to carry a denylist for them. `service-analytics` subtracted
19+
`array_agg` and `string_agg` by name in `UNSUPPORTED_AGGREGATES`, because
20+
without that subtraction they reached the Cube strategy's `default` and came
21+
back as `COUNT(*)`**a row count in place of the value the author asked for**,
22+
with no error and no log (objectui#2945).
23+
24+
**The three unlowered functions were SPLIT, not retired as a block** (maintainer
25+
ruling, 2026-08-07):
26+
27+
- **`count_distinct` STAYS** and takes ADR-0049's *enforce* leg. It is a
28+
dashboard staple with one portable lowering (`COUNT(DISTINCT x)`), and
29+
`service-analytics` lowers it already; the SQL-driver implementation follows
30+
on its own card. Its declaration leads its implementation here by decision,
31+
not by drift.
32+
- **`array_agg` / `string_agg` take the *remove* leg.** Display conveniences
33+
with no measured pull, and `string_agg` never had one shape to lower to at
34+
all: the delimiter is a second argument in PostgreSQL, a `SEPARATOR` clause in
35+
MySQL and a differently named function in SQL Server.
36+
37+
FROM → TO, both authoring surfaces:
38+
39+
| Was | Now |
40+
|:--|:--|
41+
| `aggregations: [{ function: 'array_agg', field: 'tag', alias: 'tags' }]` | no replacement — read the rows with an ordinary `fields` query and shape them in the caller, or materialise the roll-up as a stored field |
42+
| `aggregations: [{ function: 'string_agg', field: 'name', alias: 'names' }]` | as above |
43+
| `measures: [{ name: 'tags', aggregate: 'array_agg', field: 'tag' }]` | delete the measure — `compileDataset` already refused it by name, so it never produced a number |
44+
45+
The retirement kit:
46+
47+
- This is an enum **VALUE** retirement, so there is no `retiredKey()` tombstone:
48+
the enum's own error map carries the prescription, keyed on the received value
49+
so that only the two spellings which used to be legal are told they "were
50+
removed" (the `crypto.hash` / `HookBodyCapability` precedent, #4391). A
51+
mis-spelling still gets zod's list of the legal functions. For the same reason
52+
nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four surface ratchets are
53+
byte-identical — no def and no authorable key changed.
54+
- **ADR-0087 D2 conversion + D3 chain step**
55+
(`dataset-measure-array-string-agg-removed`): `os migrate meta --from 16`
56+
drops any `dataset.measures[]` declaring a retired aggregate, plus any derived
57+
measure the drop strands, with a notice each. The measure is dropped rather
58+
than stripped down because one with neither `aggregate` nor `derived` fails
59+
the dataset's own refinement — a conversion whose output cannot parse is worse
60+
than none.
61+
- **D3 semantic entry** (`query-array-string-agg-retired`) for
62+
`QueryAST.aggregations[].function`: a request surface, never stored, so there
63+
is no source for the chain to rewrite and callers move their own queries.
64+
- The engine's in-memory fallback (`@objectstack/objectql`) drops its arms for
65+
both functions — a `switch` case on a value the enum no longer has does not
66+
type-check, and a dead arm is how a retired vocabulary returns by accident.
67+
- `service-analytics`' `UNSUPPORTED_AGGREGATES` is now **empty and kept**: it is
68+
half of an arithmetic the lockstep suite enforces (`SUPPORTED = spec
69+
vocabulary − this`), which is what stops the next aggregate added to the spec
70+
from silently reaching that `COUNT(*)` default.
71+
72+
**Behaviour that actually changes** — this is the rare narrowing that removes
73+
reachable behaviour, and it is worth stating plainly: on `driver-mongodb` and on
74+
the engine's in-memory fallback these two DID compute. A raw QueryAST
75+
aggregation against those backends returned an array or a joined string and will
76+
now be refused at parse. That unpredictability is precisely what the ruling
77+
ended — an aggregation that worked on one backend and failed on another is not a
78+
capability — and both of those backends are inside the #5499 freeze. Their code
79+
is untouched; it is simply no longer reachable through a spec-valid request. On
80+
the dataset path nothing changes: `compileDataset` refused both by name already.
81+
82+
<!-- adr-0087: registered query-array-string-agg-retired, dataset-measure-array-string-agg-removed -->

content/docs/data-modeling/queries.mdx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -369,16 +369,24 @@ querying the related object directly.
369369
| `min` | Minimum value | `{ function: 'min', field: 'price', alias: 'min_price' }` |
370370
| `max` | Maximum value | `{ function: 'max', field: 'price', alias: 'max_price' }` |
371371
| `count_distinct` | Count unique values | `{ function: 'count_distinct', field: 'category', alias: 'categories' }` |
372-
| `array_agg` | Collect into array | `{ function: 'array_agg', field: 'tag', alias: 'all_tags' }` |
373-
| `string_agg` | Concatenate strings | `{ function: 'string_agg', field: 'name', alias: 'names' }` |
374372

375373
<Callout type="warn">
376-
`count_distinct` / `array_agg` / `string_agg` are only fully supported on the MongoDB driver.
377-
The SQL drivers map only `count`/`sum`/`avg`/`min`/`max` and refuse all three as a
378-
**capability gap**`501 NOT_IMPLEMENTED`, "declared but not implemented by this backend"
379-
— rather than as a caller mistake, because the query is spelled correctly and the gap is
380-
the backend's (#5907). The in-memory driver's aggregator silently returns `null` for them.
381-
Avoid these three on SQL- or memory-backed objects.
374+
`count_distinct` is not yet lowered by the SQL drivers. They map
375+
`count`/`sum`/`avg`/`min`/`max` and refuse it as a **capability gap**
376+
`501 NOT_IMPLEMENTED`, "declared but not implemented by this backend" — rather than as a
377+
caller mistake, because the query is spelled correctly and the gap is the backend's
378+
(#5907). It works on the MongoDB driver and on the engine's in-memory fallback, and its
379+
SQL lowering (`COUNT(DISTINCT field)`) is scheduled.
380+
</Callout>
381+
382+
<Callout type="warn">
383+
**Removed in 17.** `array_agg` and `string_agg` were declared here and compiled by no SQL
384+
backend, which left "what can this backend actually compute" unpredictable to the author.
385+
Both were retired (#6188, ADR-0049 enforce-or-remove): a query carrying either is now
386+
refused at parse with a prescription. There is no replacement in the query vocabulary —
387+
read the rows with an ordinary `fields` query and shape them in the caller, or materialise
388+
the roll-up as a stored field. `os migrate meta --from 16` rewrites affected dataset
389+
measures.
382390
</Callout>
383391

384392
<Callout type="info">

content/docs/kernel/contracts/data-engine.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,7 @@ interface EngineAggregateOptions {
354354
}
355355

356356
interface AggregationNode {
357-
function: 'count' | 'sum' | 'avg' | 'min' | 'max'
358-
| 'count_distinct' | 'array_agg' | 'string_agg';
357+
function: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct';
359358
field?: string; // Field to aggregate (optional for COUNT(*))
360359
alias: string; // Result column alias
361360
distinct?: boolean; // Apply DISTINCT before aggregation

content/docs/protocol/objectql/query-syntax.mdx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ interface SortNode {
117117

118118
// AggregationNode — aggregation definition
119119
interface AggregationNode {
120-
function: 'count' | 'sum' | 'avg' | 'min' | 'max'
121-
| 'count_distinct' | 'array_agg' | 'string_agg';
120+
function: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct';
122121
field?: string; // optional for COUNT(*)
123122
alias: string; // result column alias
124123
distinct?: boolean; // DISTINCT before aggregation — in-memory path only
@@ -715,17 +714,28 @@ const query: QueryAST = {
715714
// { count: 100, total: 5000000, average: 50000, min_amount: 10000, max_amount: 500000 }
716715
```
717716

718-
**Schema enum:** `count`, `sum`, `avg`, `min`, `max`, `count_distinct`, `array_agg`, `string_agg`.
717+
**Schema enum:** `count`, `sum`, `avg`, `min`, `max`, `count_distinct`.
719718

720719
<Callout type="warn">
721-
Only `count`, `sum`, `avg`, `min`, and `max` are portable. `count_distinct`, `array_agg`
722-
and `string_agg` are implemented by the MongoDB driver and by the engine's in-memory
723-
aggregation fallback, but not by the SQL drivers — on `SqlDriver` (and on the Turso
724-
driver, both transports) they are refused as a **capability gap**:
720+
Only `count`, `sum`, `avg`, `min`, and `max` are portable today. `count_distinct` is
721+
declared and is implemented by the MongoDB driver and by the engine's in-memory
722+
aggregation fallback, but not yet by the SQL drivers — on `SqlDriver` (and on the Turso
723+
driver, both transports) it is refused as a **capability gap**:
725724
`501 NOT_IMPLEMENTED`, "declared but not implemented by this backend". That is
726725
deliberately a different answer from a function the schema enum never declared
727726
(`median`), which is `400 INVALID_QUERY` — the caller's mistake — so an author who
728-
wrote `count_distinct` is never told they made a typo (#5907).
727+
wrote `count_distinct` is never told they made a typo (#5907). Its SQL lowering
728+
(`COUNT(DISTINCT field)`) is scheduled: the declaration leads the implementation here
729+
by decision, not by drift.
730+
</Callout>
731+
732+
<Callout type="warn">
733+
**Removed in 17:** `array_agg` and `string_agg` were declared by this enum and compiled
734+
by no SQL backend, so which backend could compute them was unpredictable to the author.
735+
Both were retired (#6188, ADR-0049 enforce-or-remove) and are now refused at parse with
736+
a prescription. There is no replacement in the query vocabulary — read the rows with an
737+
ordinary `fields` query and shape them in the caller, or materialise the roll-up as a
738+
stored field. `os migrate meta --from 16` rewrites affected dataset measures.
729739
</Callout>
730740

731741
### Group By Multiple Fields

content/docs/references/api/contract.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ const result = ApiErrorSchema.parse(data);
412412
| **top** | `number` | optional | Alias for limit (OData compatibility) |
413413
| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
414414
| **joins** | `never` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand``expand: { owner: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and a single related column is a dotted `fields` path (`fields: ['owner.name']`). |
415-
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | Aggregation functions |
415+
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | Aggregation functions |
416416
| **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) |
417417
| **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
418418
| **windowFunctions** | `never` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. |

content/docs/references/data/data-engine.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Options for DataEngine.aggregate operations
6565
| :--- | :--- | :--- | :--- |
6666
| **method** | `'aggregate'` || |
6767
| **object** | `string` || |
68-
| **query** | `{ context?: object; where?: Record<string, any> \| any; groupBy?: string[]; aggregations?: { function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]; … }` || |
68+
| **query** | `{ context?: object; where?: Record<string, any> \| any; groupBy?: string[]; aggregations?: { function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; distinct?: boolean; … }[]; … }` || |
6969

7070

7171
---
@@ -328,7 +328,7 @@ This schema accepts one of the following structures:
328328
| :--- | :--- | :--- | :--- |
329329
| **method** | `'aggregate'` || |
330330
| **object** | `string` || |
331-
| **query** | `{ context?: object; where?: Record<string, any> \| any; groupBy?: string[]; aggregations?: { function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]; … }` || |
331+
| **query** | `{ context?: object; where?: Record<string, any> \| any; groupBy?: string[]; aggregations?: { function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; distinct?: boolean; … }[]; … }` || |
332332

333333
---
334334

@@ -467,7 +467,7 @@ QueryAST-aligned options for DataEngine.aggregate operations
467467
| **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | |
468468
| **where** | `Record<string, any> \| any` | optional | |
469469
| **groupBy** | `string[]` | optional | |
470-
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | |
470+
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | |
471471
| **having** | `any` | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
472472
| **timezone** | `string` | optional | |
473473

content/docs/references/data/query.mdx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ const result = AggregationFunction.parse(data);
3535
* `min`
3636
* `max`
3737
* `count_distinct`
38-
* `array_agg`
39-
* `string_agg`
4038

4139

4240
---
@@ -47,7 +45,7 @@ const result = AggregationFunction.parse(data);
4745

4846
| Property | Type | Required | Description |
4947
| :--- | :--- | :--- | :--- |
50-
| **function** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>` || Aggregation function |
48+
| **function** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` || Aggregation function |
5149
| **field** | `string` | optional | Field to aggregate (optional for COUNT(*)) |
5250
| **alias** | `string` || Result column alias |
5351
| **distinct** | `boolean` | optional | Apply DISTINCT before aggregation |
@@ -134,7 +132,7 @@ Type: `string`
134132
| **top** | `number` | optional | Alias for limit (OData compatibility) |
135133
| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
136134
| **joins** | `never` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand``expand: { owner: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and a single related column is a dotted `fields` path (`fields: ['owner.name']`). |
137-
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | Aggregation functions |
135+
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | Aggregation functions |
138136
| **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) |
139137
| **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
140138
| **windowFunctions** | `never` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. |

0 commit comments

Comments
 (0)