8000 fix(postgres): count an aggregate holding a subquery as its query's own by colinhacks · Pull Request #69 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(postgres): count an aggregate holding a subquery as its query's own - #69

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

colinhacks merged 1 commit into
mainfrom
pg-agg-sublink

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

On Postgres, an ungrouped scalar subquery over an aggregate returns exactly one row, but only when the aggregate is the subquery's own: one over columns of a query around it belongs to that query. Level::owns decides that, and it gave up whenever the aggregate's argument held a subquery of its own, so the scalar subquery around it was typed nullable. That is the shape of a tree built to a fixed depth, where each level's jsonb_agg holds the next level's subquery:

select c.id, c.name, (
  select coalesce(jsonb_agg(jsonb_build_object(
    'id', k.id,
    'name', k.name,
    'children', (select coalesce(jsonb_agg(jsonb_build_object('id', g.id, 'name', g.name)), '[]') from categories g where g.parent_id = k.id)
  )), '[]')
  from categories k where k.parent_id = c.id
) as children
from categories c
where c.parent_id is null

was typed children: { id: number; name: string; children: { id: number; name: string }[] }[] | null, though it is never null.

Now owns reads the columns the call reads outside any subquery in it, and the expression an IN or ANY subquery is compared with. An aggregate belongs to the innermost query whose column it reads, so one read of this FROM there settles it, whatever the subquery reads. A call that reads no column of this FROM and holds a subquery is still taken as possibly the outer query's, since the subquery may read one.

Statement Before After
the tree above children: …[] | null children: …[]
select (select count(p.id + (select count(name) from comments)) from posts p where false) as n from users n: int8? n: int8
select (select count(p.id in (select post_id from comments)) from posts p where false) as n from users n: int8? n: int8
select (select count(name || (select body from comments limit 1)) from posts where false) as n from users n: int8? n: int8?

Tests:

  • oracle/cases/aggregate_levels: the four statements above (the tree with posts and comments), sound over the seeded edge values; no other case's expected.txt changes.
  • Unit tests in engine/postgres/src/levels.rs, for calls that hold a subquery, owned and not.
  • Run locally: the whole oracle with --strict-coverage, the engine's tests, the workspace build with no warning, the rebuilt artifact's fingerprint, and the loader's typecheck and tests.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 16:10
@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:10pm UTC

Request Review

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 change to how Postgres's Level::owns handles an aggregate call that holds a subquery, along with its new oracle statements and unit tests.

  • references reads through subqueries: it no longer gives up at a SubLink. It collects the column references outside any subquery, plus the SubLink's testexpr, and returns whether it saw a subquery.
  • owns is decided by the call's own reads: one read of this FROM outside a subquery makes the call this query's own. A call with no reads that holds a subquery is still treated as possibly the outer query's.
  • Oracle and unit tests: there are four new aggregate_levels statements: the nested jsonb_agg tree, an IN subquery, an own read beside an outer-level subquery, and an outer read beside a subquery. There are also five new unit cases for owned and not-owned calls.
  • Rebuilt packages/postgres/engine.wasm.

This rule matches how Postgres places an aggregate (check_agg_arguments). The aggregate's level is the lowest level among the variables it reads, and a variable inside a subquery has its level shifted by the subquery's depth. So a read of this FROM outside any subquery always puts the aggregate here. Also:

  • Postgres parses the testexpr of x IN (…) / x op ANY (…) at the call's own level, so reading it is right.
  • The JSON comes from a plain serde::Serialize derive, so the key is testexpr. cardinality.rs already reads that key.
  • Both |= and the fold with | evaluate every branch, so no read is skipped after the first subquery.

In subquery_own_beside_outer_read, the inner count(name) belongs to the users level, because comments has no name column. Postgres accepts that, and the statement still yields 0, not NULL, so n: int8 holds.

I didn't run the engine's tests or the oracle in this run, because the workspace's prepare step wasn't done. CI covers both.

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

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