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 where FilterCondition 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/objectql summary-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

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Error Error Jul 18, 2026 5:28pm

Request Review

@github-actions github-actions Bot 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

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