You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(objectql,spec,client,metadata-protocol)!: #4286 收官 — having 落地执行,cursor/distinct 连同 SDK 生产者移除 (#4307)
* feat(objectql,spec,metadata-protocol)!: enforce query.having — the engine applies it after aggregation (#4286 step 3)
ADR-0049 resolved to ENFORCE: having was the one declared member every
SQL-literate author expects to work next to groupBy/aggregations, and its
gap was structural (finding 1 — aggregate() rebuilt the driver AST without
it, and the findData aggregate branch dropped it on the wire).
- applyHaving() (objectql/src/having-filter.ts) runs AFTER aggregation on
the native-driver path and the in-memory fallback alike; namespace is the
aggregated row's own columns (aggregation aliases + groupBy projections).
An unknown operator rejects loudly — ignoring one would silently return
unfiltered aggregates, the ADR-0078 failure enforcement exists to end.
- EngineAggregateOptionsSchema declares having; findData's aggregate branch
forwards it; the ast carries it so the FLS predicate guard (which already
walked having references) sees caller input.
- Native SQL HAVING pushdown can come later behind a driver capability flag
(the dateGranularity two-tier pattern) without changing semantics.
- Ledger: query.having flips dead → live with end-to-end evidence; docs and
the objectstack-query skill stop teaching the app-code post-filter
workaround.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K2BX3WQwPBA2ZBehqJUBX
* refactor(spec,client,metadata-protocol)!: remove query.cursor and query.distinct with their SDK producers (#4286 step 4)
ADR-0049 resolved both to REMOVE. Neither ever had an executor:
- cursor promised keyset pagination no driver implemented — accepted and
ignored, every page identical, so a caller looping "until hasMore" never
terminated. Tombstoned on QuerySchema and EngineQueryOptionsSchema (one
shared prescription); QueryBuilder.cursor() deleted. The manual keyset (a
where predicate on the sort key) is the documented pattern; a first-class
cursor, if ever designed, will be a response-minted opaque token. The
cursor params on listRevisions/flow-runs/notifications are those endpoints'
own live tokens — untouched.
- distinct was MIS-WIRED, not merely dead (finding 2): its only observable
effect was suppressing the REST list count, so callers got duplicate rows
AND degraded total/hasMore — a side effect that "confirmed" a capability
that never ran. Tombstoned on both schemas plus the ?distinct querystring
spelling (HttpFindQueryParamsSchema); QueryBuilder.distinct() deleted; the
countable suppression branch deleted — total is truthful again (the
observable REST change, carried in the changeset). AggregationNode.distinct
(per-aggregation dedupe) is a different, live member and stays.
Both register as protocol-18 semantic migrations (query-cursor-retired,
query-distinct-retired) — request shapes, nothing stored to rewrite. Ledger
entries flip to REMOVED notes (rows stay: retiredKey keeps each key in the
walked shape); docs and the objectstack-query skill teach the live spellings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K2BX3WQwPBA2ZBehqJUBX
* docs(deployment): the troubleshooting pagination example stops teaching the removed cursor key (#4286)
Found via the docs-drift advisory on #4307: the 'Query is slow' checklist
still demonstrated cursor: { id: lastSeenId } — the key the same PR
tombstones. The example now expresses the keyset as a where predicate on
the sort key, matching the pattern every other page teaches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K2BX3WQwPBA2ZBehqJUBX
---------
Co-authored-by: Claude <noreply@anthropic.com>
|`having`| Never read: `engine.aggregate()` forwards only `object`/`where`/`groupBy`/`aggregations`, and the in-memory fallback has no HAVING stage |
94
-
|`cursor`| Accepted by `EngineQueryOptions`, but no driver implements keyset pagination |
95
-
|`distinct`| Not applied by `find()`; the SQL and in-memory drivers expose a separate `distinct(object, field, filters?)` method instead |
96
91
|`aggregations[].filter`|`[EXPERIMENTAL — not enforced]` — a SQL `FILTER (WHERE …)` affordance neither the SQL builders nor the in-memory fallback applies |
97
92
|`search.fuzzy` / `boost` / `operator` / `minScore` / `language` / `highlight`|`[EXPERIMENTAL — not enforced]` — only `query` and `fields` drive the expansion |
98
93
99
94
`top` is the exception that *is* honored — the engine normalises it to `limit`.
100
95
101
-
Two members left this table by being **removed** (#4286, ADR-0049 enforce-or-remove):
102
-
`joins` and `windowFunctions` are tombstoned in `@objectstack/spec` 18 — a query
103
-
carrying either fails to parse with the upgrade prescription, and authoring one is a
104
-
`tsc` error. Related records are read through `expand`; window functions remain a
105
-
SQL-driver door (`SqlDriver.findWithWindowFunctions()`). The inert members that
106
-
*remain* declared (`having`, `cursor`, `distinct`, and the experimental flags above)
107
-
are tracked in the liveness ledger (`packages/spec/liveness/query.json`) pending
108
-
their #4286 dispositions.
96
+
The #4286 sweep (ADR-0049 enforce-or-remove) settled every other declared-but-inert
97
+
member. **Removed** — tombstoned in `@objectstack/spec` 18, so a query carrying one
98
+
fails to parse with the upgrade prescription and authoring it is a `tsc` error:
99
+
`joins` (related records are read through `expand`), `windowFunctions` (a SQL-driver
100
+
door remains: `SqlDriver.findWithWindowFunctions()`), `cursor` (express the keyset as
101
+
a `where` predicate on the sort key — §7), and `distinct` (unique values via
102
+
`groupBy` / `count_distinct` / the drivers' `distinct()` door; its only observable
103
+
effect was suppressing the REST list count, which is truthful again). **Enforced**:
104
+
`having` (§5). The experimental flags above are tracked in the liveness ledger
105
+
(`packages/spec/liveness/query.json`).
109
106
</Callout>
110
107
111
108
### Key Types
@@ -659,23 +656,30 @@ const query: QueryAST = {
659
656
};
660
657
```
661
658
662
-
### HAVING Clause (protocol only)
659
+
### HAVING Clause
663
660
664
-
`QuerySchema` defines a `having` property, but **nothing executes it**:
665
-
`engine.aggregate()` forwards only `object` / `where` / `groupBy` / `aggregations` to
666
-
the driver, no driver reads `query.having`, and the in-memory aggregation fallback has
667
-
no HAVING stage. Filter the aggregated rows in application code:
661
+
**Enforced since #4286** (ADR-0049, resolved to enforce). The engine applies `having`
662
+
itself, AFTER aggregation, identically on the native-driver path and the in-memory
663
+
fallback (`packages/objectql/src/having-filter.ts`) — the same correct-first /
664
+
optimize-later two-tier shape date bucketing uses; native SQL `HAVING` pushdown can come
665
+
later behind a driver capability flag without changing these semantics. The REST
666
+
`findData()` aggregate branch forwards the clause.
667
+
668
+
`having` references the **aggregated row's own columns** — aggregation aliases and
669
+
groupBy projections — with the ordinary FilterCondition operators and
670
+
`$and` / `$or` / `$not`. An unknown operator rejects the query loudly rather than being
671
+
ignored (an ignored operator would silently return unfiltered aggregates — the ADR-0078
0 commit comments