Skip to content

Comments

fix(db): support aggregates nested inside expressions (#720)#1274

Merged
kevin-dp merged 9 commits intomainfrom
nested-agg
Feb 23, 2026
Merged

fix(db): support aggregates nested inside expressions (#720)#1274
kevin-dp merged 9 commits intomainfrom
nested-agg

Conversation

@kevin-dp
Copy link
Contributor

@kevin-dp kevin-dp commented Feb 19, 2026

Summary

Fixes #720.

Aggregates wrapped inside other expressions (e.g. coalesce(count(...), 0)) threw QueryCompilationError: Unknown expression type: agg because the compiler only recognized top-level aggregates in SELECT, not aggregates nested inside function expressions.

Approach

HAVING clauses already solve a similar problem — replaceAggregatesByRefs walks the HAVING expression tree, matches aggregates against SELECT entries, and replaces them with refs to computed values. But for SELECT, the aggregates are buried inside other expressions rather than existing as top-level entries, so there's nothing to match against.

The fix adds an extract-register-replace pass: before groupBy processing, nested aggregates are extracted from wrapper expressions and registered under synthetic aliases (e.g. __agg_0). The aggregates in the original expression are replaced with PropRef references to these aliases. After groupBy computes the aggregate values, a second pass evaluates the wrapper expressions against the populated results.

Changes

  • select.ts: Defer expressions containing nested aggregates to processGroupBy (same as plain aggregates)
  • group-by.ts: Add containsAggregate / extractAndReplaceAggregates helpers; update both single-group and multi-group paths to extract, register, and evaluate wrapped aggregates
  • index.ts: Detect nested aggregates for implicit single-group aggregation

Reproduction tests

  • 5 tests in @tanstack/db (packages/db/tests/query/group-by.test.ts): coalesce(count(...)), coalesce(sum(...)), add(count(...), count(...)), mixed plain + wrapped, subquery join source
  • 3 tests in @tanstack/react-db (packages/react-db/tests/useLiveQuery.test.tsx): coalesce(count(...)), coalesce(sum(...)), subquery join source — confirming the bug also affects useLiveQuery

Test plan

  • All 8 new tests pass (previously threw Unknown expression type: agg)
  • All existing tests pass (94 group-by, 32 useLiveQuery)
  • TypeScript builds cleanly (tsc --noEmit)

🤖 Generated with Claude Code

Add failing tests that reproduce the issue where aggregates wrapped
inside other expressions (e.g. coalesce(count(...), 0)) throw
QueryCompilationError: Unknown expression type: agg.

Tests cover coalesce wrapping count/sum, add combining two aggregates,
mixed plain and wrapped aggregates, and subquery join sources — in both
@tanstack/db and useLiveQuery.

Ref: #720

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Feb 19, 2026

🦋 Changeset detected

Latest commit: fd16336

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@tanstack/db Patch
@tanstack/angular-db Patch
@tanstack/electric-db-collection Patch
@tanstack/offline-transactions Patch
@tanstack/powersync-db-collection Patch
@tanstack/query-db-collection Patch
@tanstack/react-db Patch
@tanstack/rxdb-db-collection Patch
@tanstack/solid-db Patch
@tanstack/svelte-db Patch
@tanstack/trailbase-db-collection Patch
@tanstack/vue-db Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 19, 2026

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1274

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1274

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1274

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1274

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1274

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1274

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1274

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1274

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1274

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1274

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1274

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1274

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1274

commit: fd16336

@github-actions
Copy link
Contributor

github-actions bot commented Feb 19, 2026

Size Change: +464 B (+0.5%)

Total Size: 92.5 kB

Filename Size Change
./packages/db/dist/esm/query/compiler/group-by.js 2.23 kB +418 B (+23.03%) 🚨
./packages/db/dist/esm/query/compiler/index.js 2.04 kB +18 B (+0.89%)
./packages/db/dist/esm/query/compiler/select.js 1.09 kB +28 B (+2.63%)
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/changes.js 1.22 kB
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.32 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/lifecycle.js 1.75 kB
./packages/db/dist/esm/collection/mutations.js 2.34 kB
./packages/db/dist/esm/collection/state.js 3.49 kB
./packages/db/dist/esm/collection/subscription.js 3.71 kB
./packages/db/dist/esm/collection/sync.js 2.41 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.7 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/index.js 2.69 kB
./packages/db/dist/esm/indexes/auto-index.js 742 B
./packages/db/dist/esm/indexes/base-index.js 766 B
./packages/db/dist/esm/indexes/btree-index.js 2.17 kB
./packages/db/dist/esm/indexes/lazy-index.js 1.1 kB
./packages/db/dist/esm/indexes/reverse-index.js 538 B
./packages/db/dist/esm/local-only.js 808 B
./packages/db/dist/esm/local-storage.js 2.1 kB
./packages/db/dist/esm/optimistic-action.js 359 B
./packages/db/dist/esm/paced-mutations.js 496 B
./packages/db/dist/esm/proxy.js 3.75 kB
./packages/db/dist/esm/query/builder/functions.js 733 B
./packages/db/dist/esm/query/builder/index.js 4.09 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 1.05 kB
./packages/db/dist/esm/query/compiler/evaluators.js 1.43 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/joins.js 2.11 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.45 kB
./packages/db/dist/esm/query/expression-helpers.js 1.43 kB
./packages/db/dist/esm/query/ir.js 673 B
./packages/db/dist/esm/query/live-query-collection.js 360 B
./packages/db/dist/esm/query/live/collection-config-builder.js 5.44 kB
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/live/collection-subscriber.js 2.42 kB
./packages/db/dist/esm/query/live/internal.js 145 B
./packages/db/dist/esm/query/optimizer.js 2.62 kB
./packages/db/dist/esm/query/predicate-utils.js 2.97 kB
./packages/db/dist/esm/query/subset-dedupe.js 921 B
./packages/db/dist/esm/scheduler.js 1.3 kB
./packages/db/dist/esm/SortedMap.js 1.3 kB
./packages/db/dist/esm/strategies/debounceStrategy.js 247 B
./packages/db/dist/esm/strategies/queueStrategy.js 428 B
./packages/db/dist/esm/strategies/throttleStrategy.js 246 B
./packages/db/dist/esm/transactions.js 2.9 kB
./packages/db/dist/esm/utils.js 924 B
./packages/db/dist/esm/utils/browser-polyfills.js 304 B
./packages/db/dist/esm/utils/btree.js 5.61 kB
./packages/db/dist/esm/utils/comparison.js 952 B
./packages/db/dist/esm/utils/cursor.js 457 B
./packages/db/dist/esm/utils/index-optimization.js 1.51 kB
./packages/db/dist/esm/utils/type-guards.js 157 B

compressed-size-action::db-package-size

@github-actions
Copy link
Contributor

github-actions bot commented Feb 19, 2026

Size Change: 0 B

Total Size: 3.7 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 225 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.17 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.34 kB
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 559 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

Extract nested aggregates (e.g. coalesce(count(...), 0)) into synthetic
aliases, register them with the groupBy operator, and evaluate the
wrapper expression post-aggregation. This follows the same pattern used
for HAVING clauses via replaceAggregatesByRefs.

Changes:
- select.ts: defer expressions containing nested aggregates to groupBy
- group-by.ts: extract, register, and evaluate wrapped aggregates in
  both single-group and multi-group paths
- index.ts: detect nested aggregates for implicit single-group aggregation

Fixes: #720

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kevin-dp kevin-dp changed the title test: reproduce aggregates nested inside expressions (#720) fix(db): support aggregates nested inside expressions (#720) Feb 19, 2026
@kevin-dp kevin-dp requested a review from samwillis February 19, 2026 15:05
kevin-dp and others added 2 commits February 19, 2026 16:07
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Collaborator

@samwillis samwillis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think __agg_{n} is exposed on the projected results rows? unless I am missing something.

None of the new tests check for the exact data on the projected rows, and so don't validate this, and as this is new functionality the __agg_ would not appear on any rows on old tests.

Three options - not sure whats best:

  • do nothing, and accept that the aggregates are there (but not on the types)
  • have a cleanup pass to remove them
  • use a weakMap to store them with the result row as the key

Comment on lines 274 to 283
// Add synthetic aggregate values so wrapped expressions can reference them
for (const key of Object.keys(aggregatedRow)) {
if (key.startsWith(`__agg_`)) {
finalResults[key] = aggregatedRow[key]
}
}
// Second pass: evaluate wrapped-aggregate expressions
for (const [alias, evaluator] of Object.entries(wrappedAggExprs)) {
finalResults[alias] = evaluator({ $selected: finalResults })
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is exactly the same code as on 134

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — extracted into a shared evaluateWrappedAggregates helper.

kevin-dp and others added 3 commits February 23, 2026 10:14
Remove temporary __agg_N keys from finalResults after wrapped-aggregate
expressions have been evaluated. Without this cleanup, internal synthetic
aliases leak onto user-visible projected result rows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deduplicate the synthetic-value copy, evaluate, and cleanup logic that
was repeated in both the single-group and multi-group paths of
processGroupBy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use toEqual to verify that projected rows contain only the expected
keys and no leaked internal state like __agg_N synthetic aliases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kevin-dp kevin-dp requested a review from samwillis February 23, 2026 09:22
@kevin-dp
Copy link
Contributor Author

@samwillis Good catch on all three points. Fixed in the latest commits:

  1. Synthetic keys leaking onto result rows — Added a cleanup pass in evaluateWrappedAggregates that deletes all __agg_* keys from finalResults after the wrapped expressions have been evaluated. They're only needed temporarily during that evaluation step.

  2. Tests not validating exact row shape — Replaced per-field assertions (expect(row?.field).toBe(...)) with toEqual checks on the full row object. This ensures no extra keys (like __agg_0) are present on projected results.

  3. Duplicated code between single-group and multi-group paths — Extracted the synthetic-value copy, evaluate, and cleanup logic into a shared evaluateWrappedAggregates helper used by both paths.

Copy link
Collaborator

@samwillis samwillis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kevin-dp kevin-dp merged commit ac4ce67 into main Feb 23, 2026
8 checks passed
@kevin-dp kevin-dp deleted the nested-agg branch February 23, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate inside expression (e.g. coalesce(count(...), 0)) throws QueryCompilationError: Unknown expression type: agg

2 participants