feat: aggregate functions and GROUP BY for the table query API#4
Merged
Merged
Conversation
Support SUM/COUNT/AVG/MIN/MAX in the `fields` parameter plus a new `group` parameter on the list/query endpoints, so callers can compute per-group aggregates server-side instead of paging the full result set and summing client-side. - query: ParseProjection parses a mixed plain/aggregate field list into []SelectItem; BuildSelectList/BuildGroupBy render the clauses; ValidateGroupedProjection rejects non-portable mixes (a bare column next to an aggregate must appear in GROUP BY; a group needs an explicit field list). - connector: SelectRequest gains Projection and GroupBy; all six dialect builders emit GROUP BY (between WHERE and ORDER BY) via the shared select-list renderer. - handler + mcp: parse `group`, build a structured projection, validate consistency; grouped queries skip include_count (a plain COUNT(*) would count underlying rows, not groups). - openapi: document the `group` parameter and aggregate `fields` syntax. Order by an aggregate's alias (default `<func>_<column>`, or COUNT(*) -> `count`; override with `AS alias`). Security: aggregate function names come from a fixed allowlist and every column and alias is validated as a SQL identifier and quoted, so the feature adds no new injection surface. Tests cover projection parsing (incl. injection attempts), grouped validation, clause rendering, and dialect SQL generation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
|
@jonas136 Thank you for your contribution! |
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.
What
Adds aggregate functions and
GROUP BYto the table query API. Callers can nowrequest
SUM/COUNT/AVG/MIN/MAXinfieldsand group with a newgroupparameter, computing per-group results server-side instead of paging the whole
table and aggregating in the client.
{ "resource": [ { "region": "north", "total": 48210, "count": 7 }, ... ] }fields=SUM(amount).<func>_<column>(e.g.sum_amount),or
countforCOUNT(*); override with... AS alias.faucet_querytool (newgroupargument).Why
Today the only way to answer "total per customer" or "count by status" is to pull every
matching row and reduce it client-side. For large tables that means many paginated
requests and a lot of transferred JSON to produce a handful of numbers. The aggregation
is pushed down to the database where it belongs.
Design & safety
internal/query/aggregate.go:ParseProjectionturns a mixed plain/aggregatefield list into
[]SelectItem;BuildSelectList/BuildGroupByrender the SQL;ValidateGroupedProjectionrejects non-portable queries (a bare column beside anaggregate must appear in
GROUP BY; agrouprequires an explicit field list).connector.SelectRequestgainsProjectionandGroupBy. All six dialect buildersemit
GROUP BYbetweenWHEREandORDER BYand share the select-list renderer, sobehavior is consistent across SQLite/Postgres/MySQL/SQL Server/Oracle/Snowflake.
every column and alias is validated with the existing
ValidateIdentifierand quotedwith the dialect's quote function. Values remain parameterized. Injection attempts in
an aggregate argument or alias are rejected with 400.
include_countis skipped for grouped queries (a plainCOUNT(*)would countunderlying rows, not result groups).
Tests
internal/query/aggregate_test.go: projection parsing (plain, all aggregates, default*-only-for-COUNT rule, reserved-word aliases,injection attempts, grouped-projection validation, and clause rendering.
internal/connector/sqlite/aggregate_select_test.go: end-to-endBuildSelectSQL forgrouped aggregates, alias ordering, multi-column group, and whole-table rollups.
make test(race) green;golangci-lint runclean.Notes
main;CONTRIBUTING.mdmentions adevbranch but the repo currentlyonly has
main— happy to retarget.HAVING(filtering on aggregate results) is a natural follow-up and intentionallyleft out to keep this PR focused.
SUM(...) ... GROUP BYquery that previously required paging the full table andreducing client-side now returns the same result in a single request.