8000 fix(postgres)!: type arrays by their dimensions by colinhacks · Pull Request #181 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(postgres)!: type arrays by their dimensions - #181

Merged
colinhacks merged 6 commits into
mainfrom
pg-flat-arrays
Nov 4, 2026
Merged

colinhacks merged 6 commits into
mainfrom
pg-flat-arrays

Conversation

@colinhacks
@colinhacks colinhacks commented Nov 4, 2026 • 8000
Copy link
Copy Markdown
Owner

Postgres doesn't track an array column's dimensions: one int[] column holds {1,2} and {{1,2},{3,4}} alike, and nothing in the catalog says which a column holds. Nearly every array column holds one dimension, so Reylo types it that way, and now holds the value to it at run time instead of returning nested arrays its type doesn't describe.

What changes for users

  • A Postgres array column is typed with one dimension, as before, and .run() (and a Standard Query's decode) now throws on a value of more, naming the column: Column "cells" holds an array of 2 dimensions, and its type has one: … Select to_jsonb(cells) to read it as JSON. A built array that is deeper than its levels says an array column it collects holds more than one dimension. A vector element counts as one level of its own, and a JSON element may be an array at any depth.

  • An array a statement builds of arrays is typed with each level it adds, where it was typed one-dimensional before though it returned nested arrays:

    Statement Before Now
    array_agg(cells) int4?[] int4?[][]
    array[cells, cells] int4?[] int4?[][]
    array[array[1, 2], array[3, null]] int4?[] int4?[][]
    array(select g.cells from grids g) int4?[] int4?[][]
    array(select array[g.id, g.id] from grids g) int4?[] int4?[][]
    array[array[cells]] int4?[] int4?[][][]
    array(select 1) int4?[] int4[]

    The descriptor keeps each collected column array any_depth, as the format says of a Postgres array column; the levels the statement adds are closed. Where the engine can't tell whether an element is an array, the type stays the column's own, and a deeper value is refused at run time rather than mistyped.

How

  • engine/postgres/src/infer.rs: Nesting { rows, element } says how a built array nests: levels of rows over the innermost arrays, and the type of the arrays those collect, or none where every item is of a type that is no array for certain. ARRAY(SELECT …) is read as an array of the subquery's values, and a column a subquery reads keeps the catalog's type for this one question, so levels::needed now reads the tables' columns for a statement with an ARRAY(SELECT …) too.
  • engine/postgres/src/postgres.rs: nest builds the type from it; a UNION keeps a nesting both arms agree on.
  • engine/postgres/src/jsonshape.rs: a column inside a JSON shape is nested the same way, so json_build_object('stacked', array_agg(cells)) is typed { stacked: int4?[][] }.
  • src/values.ts: withinDimensions is the run-time check, applied in execute.ts and standard-query.ts; elementKind fixes the decoders for a multi-level kind (int8[][]), whose innermost elements were decoded as arrays themselves.
  • oracle/src/seed.rs: the oracle now checks an array's depth against its type, and a NULL element against the innermost level's nullability, counting only the levels that are arrays in Postgres's text (a vector adds none); new statements in oracle/cases/arrays (nested.sql, deeper.sql, json_nested.sql) and oracle/cases/pgvector_types (nested.sql), and a unit test.
  • The Postgres docs page and the agent skill describe the rule and the to_jsonb way around it.

Verified

  • The oracle with --strict-coverage over every Postgres and SQLite case: 126 cases, 0 failed; no other case's types changed.
  • cargo build --workspace --all-targets with no warning; the engine, native, CLI and oracle crates' tests.
  • nub run typecheck, and nub run test against Postgres 18 and MySQL 8.4 on the release binary: 343 passed, none skipped.

@vercel
vercel Bot commented Nov 4, 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 Nov 4, 2026 9:53pm UTC

Request Review

Copilot AI balanced review requested due to automatic review settings November 4, 2026 20:12
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. Try requesting a review at a lower effort level.

@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 blocking issues. Three minor suggestions inline: the new nesting doesn't reach JSON shapes yet, the oracle's depth check counts non-Postgres array levels, and one error message is confusing.

Reviewed changes

Reviewed both commits: the engine now types arrays built from arrays with each level they add, and the run time refuses an array column value with more dimensions than its type has.

  • Nesting in infer.rs: counts how many levels an array literal, array_agg or ARRAY(SELECT …) adds over its innermost arrays, and records the element array type they collect. Expr::Unread now carries a subquery column's catalog type so the engine can ask that question of it.
  • ARRAY(SELECT …) as Expr::Array: the subquery's target is now the array's item, so array(select 1) gets non-null elements. levels::needed now reads the tables' columns for these statements too.
  • nest in postgres.rs: renders DescribedColumn::nested for result columns. Each added level is closed and non-null, and each element array stays any_depth. A set operation keeps the nesting only where both arms agree.
  • withinDimensions and elementKind in values.ts: .run() and a Standard Query's decode throw on a value deeper than its [] count. A vector element counts as one level and a json element can be any depth. Decoders for multi-level kinds now decode the innermost elements by their own kind.
  • Oracle: seed.rs checks an array's depth against its type, and checks a NULL against the innermost level's nullability. Adds the new arrays/nested.sql and arrays/deeper.sql cases.
  • Docs and skill: describe the one-dimension rule and the to_jsonb workaround.

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

Comment thread engine/postgres/src/postgres.rs Resolved
Comment thread oracle/src/seed.rs Resolved
Comment thread src/values.ts Resolved
@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 three commits address each point from the last review.

Reviewed changes

Reviewed the three commits since the last review, each of which addresses one earlier finding.

  • Nested JSON shapes: ResolveCtx::column in jsonshape.rs now renders a probed column through nest. It drops nullable_items once nest has applied them, as the result-column path does. The new arrays/json_nested.sql types array_agg(cells) inside json_build_object as int4?[][].
  • Oracle depth for non-Postgres array levels: depth_contradiction now stops at the first level that postgres_array rejects. A vector element therefore adds no braces to the count. A unit test and pgvector_types/nested.sql cover this.
  • Message for built arrays: withinDimensions gives the to_jsonb(column) hint only for a one-dimension column. A deeper built array now points at the array column it collects. The test matches the whole message.

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

array_agg over an array column, ARRAY[] of arrays, ARRAY(SELECT
arrays) and nested array literals now carry the dimensions they add,
where the engine can tell; the decoders read a multi-level kind's
innermost elements by their scalar kind.
Postgres doesn't enforce an array column's dimensions, and Reylo types
one: .run() and a Standard Query's decode now throw on a value of more,
naming the column, rather than return nested arrays the type doesn't
describe.
@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 branch was rebased onto #180, and one new commit applies the column's type modifier to the arrays a built array collects.

Reviewed changes

Reviewed 4ed34e9. The rest of the incremental delta is the rebase, which adds typmod: -1 / typmod: col.typmod to each DescribedColumn literal and changes the render calls to render_typmod.

  • nest takes the whole column: postgres.rs::nest(col, registry) now renders the column itself through render_typmod(col.type_oid, col.typmod). It also renders each collected element array with the same typmod, so array[history, history] over a vector(3)[] column is now typed vector(…[3])?[][] instead of losing the size. This holds because an array's typmod in Postgres is its element's. Postgres also reports one for ARRAY[…] and ARRAY(SELECT …) only when every element shares it, and reports -1 for array_agg.
  • Callers simplified: the result-column path (postgres.rs:853) and jsonshape.rs ResolveCtx::column now pass the column, so the two can't disagree on the modifier.
  • Oracle: pgvector_types/expected.txt re-blessed for nested.sql to carry [3] at the innermost level.

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

@colinhacks
colinhacks merged commit 30c3b58 into main Nov 4, 2026
15 checks passed
@colinhacks
colinhacks deleted the pg-flat-arrays branch November 4, 2026 22:04
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