8000 fix(postgres): count the other built-in aggregates as one row by colinhacks · Pull Request #71 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(postgres): count the other built-in aggregates as one row - #71

Merged
colinhacks merged 1 commit into
mainfrom
pg-agg-names
Oct 23, 2026
Merged

colinhacks merged 1 commit into
mainfrom
pg-agg-names

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

On Postgres, an aggregate without a GROUP BY returns exactly one row, which types the statement's cardinality as one and a scalar subquery over it as one value. The engine counted only count, sum, avg, min, max, json_agg and jsonb_agg as aggregates there, so a query over any other built-in aggregate was typed many, and a scalar subquery over one stayed nullable under a coalesce with a non-null default:

Statement Before After
select string_agg(title, ', ') as titles from posts many one
select array_agg(id) as ids from posts many one
select bool_and(title <> '') as all_titled, bool_or(user_id = 1) as any_first, every(id > 0) as all_positive from posts many one
select (select coalesce(string_agg(p.title, ', '), '') from posts p where p.user_id = u.id) as titles from users u titles: string | null titles: string
select (select coalesce(array_agg(p.id), '{}') from posts p where p.user_id = u.id) as ids from users u ids: (bigint | null)[] | null ids: (bigint | null)[]
select (select string_agg(p.title, ', ') from posts p where p.user_id = u.id) as titles from users u titles: string | null titles: string | null

Now the aggregates the engine's rules already read are counted too, each at its one arity: array_agg, bit_and, bit_or, bool_and, bool_or and every of one argument, and string_agg, json_object_agg and jsonb_object_agg of two. Each is a name the trust query already asks the catalog about, so one a user or an extension has also defined is still not taken for the built-in.

Tests:

  • oracle/cases/aggregate_names: each aggregate without a GROUP BY, over rows and over a table that never holds one, and in scalar subqueries with and without a coalesce, sound over the seeded edge values.
  • Two statements elsewhere are now one, as they should be: param_keys/aggregate_order.sql (string_agg(… order by :o::text)) and precision_rules/empty_array.sql (coalesce(array_agg(id), array[]::int[])).
  • Run locally: the whole oracle with --strict-coverage, the engine's tests, the workspace build with no warning, the rebuilt artifact's fingerprint, the loader's typecheck and tests, the Postgres labs and the docs site.

@vercel
vercel Bot commented Oct 23, 2026 •
Copy link
Copy Markdown

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

Project Deployment Actions Updated
reylo Ready Ready Preview Oct 23, 2026 4:31pm UTC

Request Review

Copilot AI balanced review requested due to automatic review settings October 23, 2026 16:30
Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@pullfrog pullfrog Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

I reviewed the one commit that widens what the Postgres engine treats as an ungrouped aggregate, plus its oracle coverage and the rebuilt artifact.

  • More aggregates count in Reader::aggregates: array_agg, bit_and, bit_or, bool_and, bool_or and every with one argument, and string_agg, json_object_agg and jsonb_object_agg with two, now count on Postgres only. This affects the statement's cardinality (cardinality.rs, through infer::aggregates) and whether a scalar subquery over one of them can be non-null (Reader::sublink).
  • Shadowing still applies: every new name is already in postgres_function_names() through POSTGRES_GROUPED, POSTGRES_GROUPED_NEVER_NULL and POSTGRES_GROUPED_JSON. So a same-named function defined by a user or an extension still shadows it through trust.builtin.
  • No false one from set-returning wrappers: on the cardinality path, calls_at_this_level(...).set_returning rejects a statement like select unnest(array_agg(id)) from posts before it looks for an aggregate. On the subquery path, one_value does the same job. An aggregate over only outer columns is still left to Level::owns.
  • Oracle: the new aggregate_names case covers each aggregate over seeded rows and over an always-empty table. It also covers a grouped query and scalar subqueries with and without coalesce. Two existing expected.txt lines go from many to one, and both are correct.
  • Docs: postgres.mdx and 03-inference.mdx already say "an aggregate without a group by" returns one row, so this change brings the engine in line with them and no docs edit is needed.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
colinhacks merged commit bff4518 into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the pg-agg-names branch October 23, 2026 16:40
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.

2 participants

0