Skip to content

Add aggregate strategies support (lateral & grouped) - #1

Draft
wtsnz wants to merge 12 commits into
mainfrom
feat/aggregate-strategies-split
Draft

Add aggregate strategies support (lateral & grouped)#1
wtsnz wants to merge 12 commits into
mainfrom
feat/aggregate-strategies-split

Conversation

@wtsnz

@wtsnz wtsnz commented May 21, 2026

Copy link
Copy Markdown
Owner

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

Ultimately this came out of my last pr to ash_sqlite that adds aggregate support. After talking to Zach he brought up the idea that the different aggregate strategies should probably live in ash_sql instead of specifically inside of ash_sqlite. SQLite doesn't support aggregates based on lateral queries so instead we must implement a different strategy based on windows.

Two possible approaches going forward 1 OR 2:

  1. Keep the sqlite aggregate support in my last pr
    a. Add support for Aggregates ash-project/ash_sqlite#213
  2. Use the newer approach (PRs) that add aggregate strategies to ash_sql and updates ash_sqlite to implement them)
    a. This PR that adds the multiple aggregate strategies to ash_sql
    b. A PR that adds the consumption of the above into ash_sqlite Add Aggregate Support using ash_sql implementation ash_sqlite#5
    c. A tiny pr to ash_postgres that explicitly states the aggregate_strategy (wtsnz/ash_postgres@c02e86f)

The direction depends on what ash community want - I'll defer to them after asking

@wtsnz
wtsnz marked this pull request as draft May 21, 2026 21:42
@wtsnz
wtsnz force-pushed the feat/aggregate-strategies-split branch 2 times, most recently from b19bc87 to 5eedd0a Compare July 22, 2026 01:17
wtsnz added 11 commits July 21, 2026 19:50
Problem:
The new grouped query path implemented `first` as a raw ordered `LIMIT 1` query. It could return nil when `include_nil?` was false, skip the aggregate default for empty results, and pass Ash null-ordering atoms directly to Ecto.

Change:
Filter nil values unless they are requested, translate every supported Ash sort order to the matching Ecto order, and apply the aggregate default when the selected result is nil.

Provenance:
This defect was introduced by the new grouped query aggregate implementation. The fix makes grouped `first` behavior match Ash aggregate semantics.
Problem:
Fieldless grouped counts with `uniq?: true` hard-coded an `:id` field. Ash resources can use a differently named key, a composite key, or no primary key, so the generated query could reference a field that does not exist.

Change:
Read the resource primary-key metadata and count the single key when one exists. Return a clear unsupported error for resources with no primary key or a composite primary key.

Provenance:
The `:id` assumption was introduced by the new grouped query aggregate implementation and was not copied from the previous AshSQL path.
Problem:
Three moved aggregate paths produced avoidable compiler feedback: the grouped support predicate tested `related?` indirectly, the lateral module required `Ash.Query` without using its macros, and a list-default clause required a nil default even though Ash normally constructs list aggregates with a default of `[]`.

Change:
Match related aggregates directly, remove the unused requirement, and remove the nil-only list-default path. These changes make the accepted shapes explicit and remove code that normal Ash aggregate construction cannot reach.

Provenance:
These are non-functional hygiene changes in moved code. The predicate and list-default path came from the earlier SQLite implementation, while the unused requirement came from upstream lateral code.
Problem:
Grouped list and first aggregates built parameterized Ecto types with an empty constraint list. Parameterized Ash types can depend on aggregate constraints, so dropping them can select or decode values with the wrong type configuration.

Change:
Pass both `aggregate.type` and `aggregate.constraints` through `AshSql.Expr.parameterized_type/4` for window and loaded aggregate values.

Provenance:
The empty constraint list originated in the earlier SQLite implementation. The extraction moved and generalized that code but did not introduce the defect.
Problem:
The grouped query path wrapped aggregate inputs only for `distinct` or `limit`. An ordered query with only an offset could therefore aggregate the wrong input set, and SQLite cannot express a standalone `OFFSET` without a limit.

Change:
Wrap offset-only grouped inputs, preserve ordering when a limit or offset depends on it, and add SQLite's `LIMIT -1` sentinel when no explicit limit exists.

Provenance:
The missing offset condition was inherited from existing AshSQL wrapping logic and copied into the new grouped path. Fixing the grouped copy is required for the grouped aggregate feature; the separate lateral bug is tracked in a follow-up branch.
@wtsnz
wtsnz force-pushed the feat/aggregate-strategies-split branch from 5eedd0a to f96581e Compare July 22, 2026 02:58
Problem:
The grouped strategy discarded relationship limits and offsets, treated aggregate fields as database-column atoms, and let root first and exists bypass the limited aggregate input. The extraction also added a redundant context wrapper, duplicate binding setup, manual relationship traversal, and SQLite-specific list SQL in the shared strategy.

Change:
Rank related rows per parent before aggregation, resolve fields and sorts through AshSQL expression helpers, and route all root aggregates through the common input wrapper. Reuse AshSQL relationship traversal, remove the unused context and duplicate initialization, and delegate grouped list expressions to the adapter.

Why:
Ash values can be calculations or aggregates, and relationship bounds must apply before rows are folded. Reusing the existing AshSQL paths keeps grouped behavior aligned with lateral behavior and avoids silent wrong results.

Provenance:
These correctness defects and cleanup issues were introduced by the grouped aggregate extraction.

Validation:
The four focused SQLite regressions pass. Full suites pass in AshSQL (1 test), AshSQLite (210 tests), and AshPostgres (851 tests, 21 excluded).
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.

1 participant