Skip to content

feat(objectql): filtered roll-up summary fields (#1868) - #3227

Merged
os-zhuang merged 3 commits into
mainfrom
claude/cross-object-rollup-summary-ik6dqe
Jul 19, 2026
Merged

feat(objectql): filtered roll-up summary fields (#1868)#3227
os-zhuang merged 3 commits into
mainfrom
claude/cross-object-rollup-summary-ik6dqe

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#1868.

Context

The native cross-object roll-up summary field already exists and stays current — the engine recomputes count/sum/min/max/avg on the parent whenever a child is inserted/updated/deleted (packages/objectql/src/engine.ts, tests in summary-rollup.test.ts). The one piece of the issue's Expected shape ({ object, field, op, filter? }) that was still missing is filter? — and without it several of the named templates cannot be expressed:

  • content_publication.total_views/clicks/signups/revenue all roll up the same child collection, differentiated only by a filter.
  • procurement_order.received_amount sums only the child receipt lines whose status is received (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 query whereFilterCondition evaluated against each child row. Only matching children are aggregated; omit it and every child is aggregated exactly as before (purely additive, no migration).

// One `engagement` child → distinct filtered totals
total_signups: {type: 'summary',summaryOperations: {object: 'engagement',field: 'id',function: 'count',filter: {type: 'signup'}}}// Sum only received receipt lines
received_amount: {type: 'summary',summaryOperations: {object: 'procurement_receipt',field: 'amount',function: 'sum',filter: {status: 'received'}}}

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

  • specsummaryOperations.filter (FilterCondition) on FieldSchema; schema tests; regenerated references/data/field.mdx; liveness-ledger note for the new sub-key.
  • objectql — thread filter through SummaryDescriptorbuildSummaryIndexrecomputeSummaries (merged into the aggregate where).
  • docs — document filter in field-types.mdx and the objectstack-data relationships skill; fixed a stale non-canonical summaryType/summaryField example in that skill (it would have authored an inert summary — the ADR-0078 failure mode).
  • changeset — minor bump for @objectstack/spec + @objectstack/objectql.

Verification

  • @objectstack/objectqlsummary-rollup.test.ts — 8/8 (4 new filtered tests: filtered sum/count, $in+$gte compound, in/out-of-filter recompute on update and delete), driven end-to-end through the real engine + a $and/operator-aware matcher.
  • @objectstack/objectql summary/aggregation/bulk suites — 34/34; engine-summary-retry — 4/4.
  • @objectstack/spec full suite — 6765/6765; check:docs gate — in sync.

🤖 Generated with Claude Code


Generated by Claude Code

`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
@vercel

vercelBot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specErrorErrorJul 18, 2026 5:28pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling size/m labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0] Add native cross-object rollup/summary capability (parent aggregates of child rows)

2 participants

@os-zhuang@claude