8000 feat(postgres): type json a lateral subquery or one case branch builds by colinhacks · Pull Request #55 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

feat(postgres): type json a lateral subquery or one case branch builds - #55

Merged
colinhacks merged 4 commits into
mainfrom
json-shapes
Oct 23, 2026
Merged

colinhacks merged 4 commits into
mainfrom
json-shapes

Conversation

@colinhacks
Copy link
Copy Markdown
Owner

Replaces #16, which GitHub closed when its base branch merged. It builds on #53 (the scoping fix), so this PR's base is that branch, and the diff here is this PR's commit alone. Merge #53 first.

#3 now types JSON that a WITH query or a plain derived table builds, through its own item statements. What it still left unknown was JSON built in a lateral subquery and JSON built in one branch of a case. Both are typed now:

Statement Before Now
select m.id, x.items from member m left join lateral (select jsonb_agg(jsonb_build_object('name', t.name)) as items from team t where t.id = m.id) x on true items: json? items: [{ name: text }]?
select x.snapshot from member m join lateral (select jsonb_build_object('name', t.name) as snapshot from team t where t.id = m.id) x on true json { name: text }
select case when m.id > 1 then jsonb_build_object('n', m.first_name) end as o from member m json? { n: text }?
  • LATERAL (jsonshape.rs, lateral_target and lateral): a column of a lateral subquery whose target is a JSON constructor is read in the subquery's own context, its from and where. The wider probe reads only the from items left of the subquery (left_of), since the join that reads the subquery, a left join above all, would null the subquery's own tables there. A NOT NULL column inside stays non-null. An outer join over the subquery, or a grouping set of the statement, makes the value nullable. A subquery with a WITH of its own, a set operation, a * or grouping sets is not read, and a column list (x (b, a)) renames by position.
  • CASE (case_branch): a case whose one branch builds JSON and whose others are NULL has that branch's shape, nullable where a branch is NULL. Two branches that build JSON may build two shapes, so they're not read.
  • The Postgres page says both.

Pullfrog's two findings on #16, WITH-query scoping and alias column lists, concerned the WITH and derived-table code that #3 superseded. The scoping fixes are in #53, which covers the cases those findings named. This PR reads only lateral subqueries and handles their column lists.

Tests: unit tests for the LATERAL probe and the CASE reading; the oracle case json_lateral_case (17 statements, sound over seeded rows); json_item_shapes/lateral.sql re-blessed from json? to { j: { id: int8 } }; the full oracle with --strict-coverage, 102 cases, nothing else changed.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 00:11
@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 2:38pm 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 reached their quota limit.

@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.

Important

One narrow soundness hole in left_of: it can scope a LATERAL probe to the wrong relation and type a nullable field as non-null. It's a one-line fix, verified below. Apart from that, the LATERAL and CASE reading looks sound.

Reviewed changes

I reviewed the PR's single commit: the LATERAL and CASE paths in jsonshape.rs, the new oracle case, the re-blessed json_item_shapes/lateral.sql and the docs line.

  • LATERAL subqueries: lateral_target resolves x.col (or a bare col of a one-item FROM) to a LATERAL subquery's constructor target. lateral walks that target in the subquery's own FROM and WHERE, and the wider probe reads only left_of the subquery. The value is nullable under an outer join or a statement grouping set.
  • CASE: case_branch reads a CASE whose one branch builds JSON and whose other branches are NULL. is_constructor now accepts such a CASE.
  • Oracle and docs: a new json_lateral_case case with 17 statements, lateral.sql going from json? to { j: { id: int8 } }, and the Postgres page updated for both.

These paths check out:

  • The narrow probe runs first and the wider one only if it fails (postgres.rs around line 708).
  • A left_of that returns None falls back to the whole parent FROM, which can only widen nullability.
  • MERGE's missing source is a LEFT JOIN wrapper that from_items reads as nulled.
  • Where figure can't name a column the way Postgres does, it gives ?column?, never another real name.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread engine/postgres/src/jsonshape.rs Resolved
Comment thread engine/postgres/src/jsonshape.rs Resolved
@pullfrog pullfrog Bot left a comment
Copy link
8000 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. Both earlier findings are fixed, and I've resolved their threads.

Reviewed changes

I reviewed the two commits added since the last review. The range-diff also shows changes from #53's branch, which this PR was rebased onto. Those belong to #53 and I left them out of this review.

  • Aliased joins in left_of: left_in now stops at a join that has its own alias, the same way from_items does. The probe now reads the m that the subquery reads. The new oracle statement lateral_beside_aliased_join.sql and a unit assertion on the probe's FROM cover this, and the statement is now typed { o: { n: text? } }.
  • Simplified case_branch: it now returns Option<&Node>, and the walk always makes the branch nullable. The doc comment explains why: a CASE has at least one WHEN, so if it has one branch that builds JSON, it also has a NULL branch.

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

@colinhacks
colinhacks force-pushed the json-scoping branch 3 times, most recently from d9cc88c to 8bb2025 Compare October 23, 2026 04:08
@colinhacks
colinhacks changed the base branch from json-scoping to main October 23, 2026 05:00
@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. This push rebases the PR onto main now that #53 has merged. The only content change is to the docs.

Reviewed changes

I compared the new commits with the previously reviewed head 517218c using a range-diff. ccccdea and f49bf30 are identical to the commits I reviewed before. f479605 changes only the docs. jsonshape.rs, the oracle cases and engine.wasm did not change.

  • Moved the docs into main's new postgres.mdx layout: that layout uses <Rules> sections with bold headings. The lateral wording is now part of the "Through a with query or a subquery" rule, so a lateral subquery is no longer listed as unknown. No other page or the skill still says it is.
  • Added a "Through a case" rule with a captured example: pg_json_case.sql is captured as big_order: { total: number | "NaN" | … } | null in site/lib/sql-modules.json. That matches the page's schema, where orders.total is a non-null amount (numeric), and how the page says a numeric appears inside JSON.

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

@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. The only new commit changes the docs example.

Reviewed changes

I reviewed the one commit added since f49bf30, 6361c95.

  • Changed the case example to build an integer field: pg_json_case.sql now builds jsonb_build_object('id', id) instead of total, and its capture in site/lib/sql-modules.json is now big_order: { id: number } | null. That fits the page's schema, where orders.id is a bigserial, and the page's own rule that a bigint inside JSON is a number. The recapture changed only this entry.

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

@colinhacks
colinhacks merged commit dcb8323 into main Oct 23, 2026
6 checks passed
@colinhacks
colinhacks deleted the json-shapes branch October 23, 2026 14:44
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