Uh oh!
There was an error while loading. Please reload this page.
feat(objectql,spec): per-aggregation filter on engine.aggregate — drivers refuse instead of silently dropping (#10413 contract half) - #10771
Conversation
…e; drivers refuse instead of dropping Fixes the contract half of the ObjectQL analytics silent-drop: aggregation entries gain an enforced per-aggregation filter (SQL FILTER (WHERE ...) semantics), lowered in memory by the engine for every driver; the four native driver aggregate faces refuse a directly-delivered filter with NOT_IMPLEMENTED/501; the StrategyContext.executeAggregate bridge carries the new key for #10413 phase 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
…EST_DEBT ratchet stays at 355) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw
📓 Docs Drift CheckThis PR changes 6 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 131 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 e9e2732d311473c5c02ad84e74d77b056df4854c && git checkout e9e2732d311473c5c02ad84e74d77b056df4854c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 01c50322a10843e169d109b789b4f268bdf9122f 3d27f1b3670ecfcd438f5cf585c6fa8b239bba8e && git checkout -B drift-repro 01c50322a10843e169d109b789b4f268bdf9122f && git merge --no-ff 3d27f1b3670ecfcd438f5cf585c6fa8b239bba8e
node scripts/docs-audit/affected-docs.mjs --json 01c50322a10843e169d109b789b4f268bdf9122f
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10576
The contract half of #10413's ruling (maintainer, 2026-08-21, verbatim 「其他接受」 accepting option A: 「给引擎聚合契约加逐聚合过滤,一次修对所有驱动」). #10413 phase 2 (
ObjectQLStrategylowering each measure'sfilterinto this seam, inservice-analytics) consumes this seam next and stays on #10413 — that package is deliberately untouched here.What changed
Contract (
@objectstack/spec)AggregationNodeSchema.filter(data/query.zod.ts) — declared since [P2] data:QueryASTdeclares 12 members no executor runs — the liveness ledger governs metadata types, not the request surface #4286 but marked[EXPERIMENTAL — not enforced]— is now an enforced key: SQLFILTER (WHERE …)semantics, the predicate narrows the SOURCE rows that one aggregation reads while sibling aggregations keep the full group. Describe text + TSDoc rewritten; docs regenerated (content/docs/references/data/query.mdx).StrategyContext.executeAggregate(contracts/analytics-service.ts) — the seam/api/v1/analytics/querystill drops per-measure and dataset-levelfilteron the ObjectQL path —engine.aggregatereceives no filter at all #10413 names — widened: its aggregation entries gainfilter?: FilterCondition.query/aggregations.filterreclassified experimental →livewith executor evidence (packages/spec/liveness/query.json, counts regenerated).Execution (
@objectstack/objectql)engine.aggregateroutes any call whose aggregations carry a non-emptyfilterthrough the in-memory lowering (applyInMemoryAggregation) — the same correct-first two-tier shape as date bucketing and HAVING. No driver compiles conditional aggregation today; a driver that grows native support must advertise a capability flag, at which point this fork becomes its fallback tier.applyInMemoryAggregationevaluates the per-aggregation filter per bucket via the having evaluator's walker (matchesAggregationFilter, having-filter.ts) — one operator vocabulary, one ADR-0112 refusal envelope (INVALID_FILTER/400), the 非否定路径上的$ne/$nin/$notContains:driver-sql 排除 NULL 行,driver-memory / formula 返回它们(#5146 只裁定了$not) #5298 null-safe negation semantics, with refusal wording that names theaggregations[i].filterposition (the having wording is byte-identical via a clause parameter defaulting to the old strings).emptyGroupValueFor(spec data/aggregation-policy.ts), which exists for exactly this case (objectui#3136).wheredoes: comparand-shape gate (数据 API:集合算子not_in/in的比较值是标量(非数组)时答 500 DATABASE_ERROR,而不是带信封的 400 —— 而这个形状是 spec 合法的 ViewFilterRule #5869) and materializable-field gate (The FILTER axis has no unmaterializable verdict: awhereon a virtual formula field returns 0 rows silently, while sort and search refuse the same field with a 400 #8296), each naming the aggregation position; filter tokens (Flow node filters silently blank date macros: the template engine consumes{…}before the query engine sees it #3810) resolve in it copy-on-write. The FLS predicate guard already walkedaggregations[].filter(plugin-security predicate-guard.ts:89) — unchanged.Driver semantics — refuse loudly, no silent drop anywhere
The engine never pushes a filtered aggregation down, so a driver can only meet the key through a direct call — and each native aggregate face now refuses it with
NOT_IMPLEMENTED/501 (the #5907 "declared by the spec, not compiled by this face" class, twin wording first-sentence-for-first-sentence) instead of silently aggregating the UNFILTERED rows:driver-sqlSqlDriver.aggregate(inherited by driver-sqlite-wasm and Turso local)driver-tursoRemoteTransport.aggregatedriver-mongodbbuildAggregationPipeline(whoseAggregationInput.filterhad declared the key since [finding][drivers] driver-mongodb cannot take a structuredGroupByNodeat all — the object stringifies into a"[object Object]"$group._id #6850 and read it never)driver-memoryperformAggregation(guarding both its doors:find()with aggregations andaggregate(AST))Driver-investment freeze judgment (per the dispatch): the driver-memory / driver-mongodb changes are refusal paths only — they close a silent drop; no capability build-out was made in the frozen family (a
$cond-wrapped mongodb accumulator would be one, and was deliberately not built).Acceptance evidence (the card's non-negotiables)
/api/v1/analytics/querystill drops per-measure and dataset-levelfilteron the ObjectQL path —engine.aggregatereceives no filter at all #10413 reproduction at the objectql level —engine-aggregate-filter.test.ts: the measurement's three-measure shape (opp_countcount(*),won_countcount filteredstage: 'closed_won',won_amountsum(amount) with the same filter) reachesengine.aggregateand the numbers CHANGE:[{ opp_count: 6, won_count: 2, won_amount: 700 }]where the pre-fix behaviour answered 6/6/1970. A native-aggregate driver double that drops the filter (the pre-fix behaviour of every real driver) is proven NOT called (nativeCalls() === 0).code+status+ message content: engine/in-memory unknown operator →INVALID_FILTER/400 namingaggregations[i].filter; all four driver faces →NOT_IMPLEMENTED/501 naming the aggregation alias and the engine-lowering remedy (driver-sql, turso remote incl. cross-face wording parity, mongodb builder, memory both doors).filterbehave byte-identically — unfiltered aggregations keep the native pushdown path (nativeCalls() === 1, same results);{}is the vacuous filter (thewhere/havingconvention) and does not break pushdown; equivalent controls pinned on each driver face; all 3999 pre-existing objectql tests and full spec/driver suites green.Verification
Gate union pinned to final HEAD
3d27f1b367: the fast ratchet families (check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:nul-bytes,check:cross-package-test-inputs,check:test-source-alias, changeset-no-major, adr-0087) re-ran green at that sha; the heavy runs (suites +check:type-check-debtre-measure, 33 entries, none above ledger) ran on a working tree byte-identical to it (git status --porcelainempty at commit; no edits between those runs and the commit). Highlights: objectql 226 files / 3999 tests; spec 416 files / 11107 tests + all 14 generated artifacts current + typecheck; driver-sql 104 files, turso 39 (incl. the previously-unbuilt@objectstack/verifyparity file), mongodb 26, memory 39; consumers downstream of the widened types — service-analytics 78 files (tests run as consumer; source fenced for #10413 phase 2), rest 133, runtime 178, qa/dogfoodanalytics-rls.dogfood.test.ts— all green. The report comment on #10576 carries the full command/readout table.Notes for review
contracts/analytics-service.ts,data/query.zod.ts). This PR passes the clause-② enqueue gate before landing, per the card.check-changeset-no-majorgreen../registrymodule-mock is 12 hand-copies of one factory, and they have drifted — one copy omitsgetAllObjects(inert today, the #9002 shape if a path ever reaches it) #10551's 12 registry-mock test files inpackages/objectql/src/**are untouched (new test files only, plus the two non-fenced aggregation test files).Generated by Claude Code