8000 fix(postgres): keep a table's not null in a subquery's array_agg by colinhacks · Pull Request #99 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(postgres): keep a table's not null in a subquery's array_agg - #99

Merged
colinhacks merged 2 commits into
mainfrom
fix/pg-subquery-array-agg
Oct 24, 2026
Merged

colinhacks merged 2 commits into
mainfrom
fix/pg-subquery-array-agg

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

An array_agg of a table's whole row inside a scalar subquery typed every field of its element row as nullable. The top-level array_agg(e) already keeps the table's NOT NULL, and so does jsonb_agg(q) in the same position:

select users.id, (select coalesce(array_agg(q), '{}') from posts q where q.user_id = users.id) as rows from users
  • Before: rows: posts{ id: int8?, user_id: int8?, title: text? }[]
  • After: rows: posts{ id: int8, user_id: int8, title: text }[]

The rule

The column carries the same stored-rows mark that a top-level array_agg(e) gets. A scalar subquery's own target may now be:

  • array_agg(q) or array_agg(q.*), where q is an item of the subquery's FROM;
  • that call under coalesce(…, <empty array literal>), at the top level too.

The empty array literal is '{}' or array[], cast or not. All the existing conditions of the mark still hold:

  • no table the statement reads has a column q, so the aggregate is the subquery's and q is its row;
  • every FROM item is a plain table;
  • the column's type is an array of that table's row type;
  • array_agg is the built-in one.

These are left as they were:

  • a default that may hold other rows (array[row(…)::posts]);
  • ARRAY(select array_agg(q) …);
  • a built row value (array_agg(row(…)::posts)).

Oracle (json_subqueries)

  • scalar_row_array_agg_coalesce: rows: posts{ id: int8?, user_id: int8?, title: text? }[] → rows: posts{ id: int8, user_id: int8, title: text }[]
  • New statements, each checked over seeded rows:
    • _uncorrelated
    • _bare (stays nullable, fields their own)
    • _outer_joined (an outer join inside the subquery)
    • _in_json (inside jsonb_build_object, so the oracle checks each field)
  • New guards that stay as they were:
    • _built and _default_rows stay all-nullable;
    • _outer_column, where a bare p is users.p, stays text[]?.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 23:21
@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 24, 2026 12:37am 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 correctness issues found. One stale doc comment is noted below.

Reviewed changes

I reviewed the whole PR: the engine rule, its unit tests, the oracle case and the rebuilt artifact.

  • aggregated and empty helpers in sublink_rows: these find a built-in array_agg(e) or array_agg(e.*) of a FROM item's whole row. The call can stand alone or sit inside a two-argument coalesce(…, <empty array literal>). Nested coalesce, non-empty defaults, '{}' as the first argument and array_agg(…) || '{}' are all rejected.
  • Scalar subquery's own array_agg: an ExprSublink whose one target is aggregated 8000 over its own FROM now gets the same stored-rows mark as a top-level array_agg. ARRAY(…) stays excluded, because its result would also be typed posts[], as a 2-D array. The existing guards in Engine::stored_sublinks still apply: the type must be an array of the table's row type, no table in the statement may have a column named like the alias, and array_agg must be the built-in one. The _outer_column case shows these guards keep a bare p read as users.p.
  • Top-level coalesce(array_agg(p), '{}'): this is now marked too. The JSON case _in_json uses this path, because jsonshape's probe hands the subquery to stored_sublinks.
  • Oracle json_subqueries: the _coalesce fields become non-null. The new statements cover the uncorrelated, bare, outer-joined and in-JSON cases, plus guards that stay nullable.
  • packages/postgres/engine.wasm: rebuilt. tests/fingerprint.test.ts passes against the PR's sources.

ℹ️ The doc comments on SublinkRows and sublink_rows still describe the old rule

These comments sit outside the diff hunks, so they can't take an inline comment. They still say an array_agg counts only over "the SELECT's FROM" (SublinkRows.targets) or "the SELECT's own" (sublink_rows). Neither mentions a scalar subquery's own array_agg or the coalesce(…, '{}') form, which the code now accepts.

Technical details
# Stale doc comments after widening the array_agg rule

## Affected sites
- engine/postgres/src/postgres.rs:1904-1907 — `SublinkRows.targets` describes an `array_agg` only "of a table of the SELECT's FROM"
- engine/postgres/src/postgres.rs:1935-1937 — `sublink_rows` names only `array_agg(e)` / `array_agg(e.*)` "of one of the SELECT's own"

## Required outcome
- Both comments also cover a scalar subquery whose one target is an `array_agg` of a whole row of its own FROM, and the `coalesce(…, <empty array literal>)` wrapping at either level.

Pullfrog  | Fix it ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

@colinhacks
Copy link
Copy Markdown
Owner Author

Updated both doc comments, on SublinkRows.targets and sublink_rows. They now name a scalar subquery's own array_agg and the coalesce(…, <empty array literal>) form at either level. Rebased on main after #95, artifacts rebuilt, whole oracle passing.

@colinhacks
colinhacks force-pushed the fix/pg-subquery-array-agg branch from 65a2bbf to cd1c797 Compare October 23, 2026 23:39
@colinhacks
colinhacks force-pushed the fix/pg-subquery-array-agg branch from cd1c797 to 85f4acf Compare October 24, 2026 00:36
@colinhacks
colinhacks merged commit f1394c1 into main Oct 24, 2026
7 checks passed
@colinhacks
colinhacks deleted the fix/pg-subquery-array-agg branch October 24, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Assignees

No one assigned

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0