Repository navigation
fix(postgres)!: type arrays by their dimensions - #181
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
ℹ️ 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.
Nestingininfer.rs: counts how many levels an array literal,array_aggorARRAY(SELECT …)adds over its innermost arrays, and records the element array type they collect.Expr::Unreadnow carries a subquery column's catalog type so the engine can ask that question of it.ARRAY(SELECT …)asExpr::Array: the subquery's target is now the array's item, soarray(select 1)gets non-null elements.levels::needednow reads the tables' columns for these statements too.nestinpostgres.rs: rendersDescribedColumn::nestedfor result columns. Each added level is closed and non-null, and each element array staysany_depth. A set operation keeps the nesting only where both arms agree.withinDimensionsandelementKindinvalues.ts:.run()and a Standard Query'sdecodethrow on a value deeper than its[]count. Avectorelement counts as one level and ajsonelement can be any depth. Decoders for multi-level kinds now decode the innermost elements by their own kind.- Oracle:
seed.rschecks an array's depth against its type, and checks a NULL against the innermost level's nullability. Adds the newarrays/nested.sqlandarrays/deeper.sqlcases. - Docs and skill: describe the one-dimension rule and the
to_jsonbworkaround.
claude-opus-5-5 | 𝕏
There was a problem hiding this comment.
✅ 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::columninjsonshape.rsnow renders a probed column throughnest. It dropsnullable_itemsoncenesthas applied them, as the result-column path does. The newarrays/json_nested.sqltypesarray_agg(cells)insidejson_build_objectasint4?[][]. - Oracle depth for non-Postgres array levels:
depth_contradictionnow stops at the first level thatpostgres_arrayrejects. Avectorelement therefore adds no braces to the count. A unit test andpgvector_types/nested.sqlcover this. - Message for built arrays:
withinDimensionsgives theto_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.
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.
4eb5627 to
4ed34e9
Compare
There was a problem hiding this comment.
✅ 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.
nesttakes the whole column:postgres.rs::nest(col, registry)now renders the column itself throughrender_typmod(col.type_oid, col.typmod). It also renders each collected element array with the sametypmod, soarray[history, history]over avector(3)[]column is now typedvector(…[3])?[][]instead of losing the size. This holds because an array's typmod in Postgres is its element's. Postgres also reports one forARRAY[…]andARRAY(SELECT …)only when every element shares it, and reports-1forarray_agg.- Callers simplified: the result-column path (
postgres.rs:853) andjsonshape.rsResolveCtx::columnnow pass the column, so the two can't disagree on the modifier. - Oracle:
pgvector_types/expected.txtre-blessed fornested.sqlto carry[3]at the innermost level.
claude-opus-5-5 | 𝕏

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'sdecode) 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:
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, solevels::needednow reads the tables' columns for a statement with anARRAY(SELECT …)too.engine/postgres/src/postgres.rs:nestbuilds the type from it; aUNIONkeeps a nesting both arms agree on.engine/postgres/src/jsonshape.rs: a column inside a JSON shape is nested the same way, sojson_build_object('stacked', array_agg(cells))is typed{ stacked: int4?[][] }.src/values.ts:withinDimensionsis the run-time check, applied inexecute.tsandstandard-query.ts;elementKindfixes 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 (avectoradds none); new statements inoracle/cases/arrays(nested.sql,deeper.sql,json_nested.sql) andoracle/cases/pgvector_types(nested.sql), and a unit test.to_jsonbway around it.Verified
--strict-coverageover every Postgres and SQLite case: 126 cases, 0 failed; no other case's types changed.cargo build --workspace --all-targetswith no warning; the engine, native, CLI and oracle crates' tests.nub run typecheck, andnub run testagainst Postgres 18 and MySQL 8.4 on the release binary: 343 passed, none skipped.