feat(objectql): filtered roll-up summary fields (#1868)#3227
Merged
Conversation
`summaryOperations` gains an optional `filter` — a query `where` FilterCondition evaluated against each child row — so a roll-up `summary` field aggregates only the matching children instead of the whole child collection. This is the piece the cross-object rollup templates were missing: it lets a single child object feed several distinct parent totals (e.g. content_publication.total_signups vs total_clicks over one engagement child, or procurement_order.received_amount summing only received receipt lines in a 3-way match). The engine ANDs the predicate with the parent-FK match when it recomputes, and because the whole filtered aggregate is re-run on every child insert/update/delete, a child that moves in or out of the predicate (a status change) keeps the parent current with no extra wiring. Operator and compound filter forms work too. Purely additive: omitting `filter` aggregates every child exactly as before. - spec: add `summaryOperations.filter` (FilterCondition) + tests; regen reference doc; note the sub-key in the liveness ledger - objectql: thread the filter through SummaryDescriptor / buildSummaryIndex / recomputeSummaries; tests for filtered sum/count, $in/$gte compounds, and in/out-of-filter recompute on update & delete - docs: document `filter` in field-types.mdx and the objectstack-data relationships skill; fix a stale non-canonical summary example in that skill Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbK4FqcHwp9jSwdhTtxYuC
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…d dep
field.zod.ts now imports filter.zod.ts (summaryOperations.filter), so
`check:skill-refs` flagged skills/{objectstack-data,objectstack-platform}/
references/_index.md as stale. Regenerated via `pnpm gen:skill-refs`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbK4FqcHwp9jSwdhTtxYuC
… summaries (#1868) Adds `showcase_expense_report` + `showcase_expense_line` — the headline demo of `summaryOperations.filter`: one expense-line child feeds SIX parent roll-ups, each aggregating only the lines a filter matches: total_amount SUM(amount) every line approved_amount SUM(amount) WHERE status=approved filtered sum (equality) reimbursable_amount SUM(amount) WHERE billable=true filtered sum (boolean) line_count COUNT every line rejected_count COUNT WHERE status=rejected filtered count (equality) over_limit_count COUNT WHERE amount>=500 filtered count (operator) Master-detail with an inline line-item grid (like showcase_invoice), so the interactive story — flip a line's status and watch approved/rejected/ reimbursable diverge from the unfiltered total — is drivable in the app. Seed data is chosen so all six show distinct non-zero values on first boot. Wired into the object registry, seed set, and the Data Model nav group. Verified end-to-end in the running showcase: the six rollups compute the expected values from seed, and recompute when a child line's status flips. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbK4FqcHwp9jSwdhTtxYuC
os-zhuang
marked this pull request as ready for review
July 19, 2026 06:05
This was referenced Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1868.
Context
The native cross-object roll-up
summaryfield already exists and stays current — the engine recomputescount/sum/min/max/avgon the parent whenever a child is inserted/updated/deleted (packages/objectql/src/engine.ts, tests insummary-rollup.test.ts). The one piece of the issue's Expected shape ({ object, field, op, filter? }) that was still missing isfilter?— and without it several of the named templates cannot be expressed:content_publication.total_views/clicks/signups/revenueall roll up the same child collection, differentiated only by a filter.procurement_order.received_amountsums only the child receipt lines whose status isreceived(3-way match), not every line.This is a classic declared-but-incomplete gap (ADR-0078): the summary type is live, but a whole class of real roll-ups is inexpressible.
Change
Add an optional
summaryOperations.filter— a querywhereFilterConditionevaluated against each child row. Only matching children are aggregated; omit it and every child is aggregated exactly as before (purely additive, no migration).The engine
$ands the predicate with the parent-FK match at recompute time. Because the whole filtered aggregate is re-run on every child write, a child that moves in or out of the predicate (e.g. a status change) keeps the parent current with no extra bookkeeping. Operator/compound forms work too (filter: { type: { $in: ['signup','trial'] }, amount: { $gte: 100 } }).Files
summaryOperations.filter(FilterCondition) onFieldSchema; schema tests; regeneratedreferences/data/field.mdx; liveness-ledger note for the new sub-key.filterthroughSummaryDescriptor→buildSummaryIndex→recomputeSummaries(merged into the aggregatewhere).filterinfield-types.mdxand theobjectstack-datarelationships skill; fixed a stale non-canonicalsummaryType/summaryFieldexample in that skill (it would have authored an inert summary — the ADR-0078 failure mode).@objectstack/spec+@objectstack/objectql.Verification
@objectstack/objectqlsummary-rollup.test.ts— 8/8 (4 new filtered tests: filtered sum/count,$in+$gtecompound, in/out-of-filter recompute on update and delete), driven end-to-end through the real engine + a$and/operator-aware matcher.@objectstack/objectqlsummary/aggregation/bulk suites — 34/34;engine-summary-retry— 4/4.@objectstack/specfull suite — 6765/6765;check:docsgate — in sync.🤖 Generated with Claude Code
Generated by Claude Code