Known limits¶
Nullability inference limits from the reference, each
pinned by a test in packages/rowx-postgres/src/rowx_postgres/__tests__/integration/describe/test_known_limits.py.
A limit is a case where rowx is
less precise than the database allows. A correct case is a hard-looking shape that rowx gets right.
| Kind | Test | Behaviour |
|---|---|---|
| correct | test_lateral_on_outer_join |
A LATERAL subquery on the null-extended side is nullable (OuterJoin). |
| correct | test_window_over_outer_join |
A window function over an outer join, behind a subquery, is nullable (ScanBoundary). |
| correct | test_nested_cte_chain |
A nested CTE chain over an outer join is nullable (ScanBoundary). |
| correct | test_scan_boundary_over_promotes |
A scan boundary column from the non-null side stays NOT NULL. Computed, duplicate-named, renamed (swapped-alias) and constant outputs, and columns that merge inputs (UNION ALL), stay nullable (ScanBoundary). |
| correct | test_view_columns_are_nullable |
A view column is NOT NULL (ViewColumn) when the view's definition proves it, such as a column from the non-null side of its join. Columns on the nullable side, computed columns and views that fall back (extra rules, unreadable security_invoker views, chains deeper than 8) stay nullable (ForeignOrView). |
| correct | test_set_operation_not_null_is_inferred |
A set operation column is NOT NULL (SetOperation) when the plan proves that output position, through UNION, UNION ALL, EXCEPT and INTERSECT, nested or mixed. |
| correct | test_set_operation_stays_nullable_when_unproven |
A set operation column stays nullable when any branch is unproven: a nullable or NOT VALID column, a NULL or computed branch, a null-extended join side, or a position that only matches by name. |
| limit | test_recursive_cte_is_nullable |
Recursive CTE columns have no table origin, so they default to nullable. |
| limit | test_computed_column_is_nullable |
A computed column outside the whitelist (upper(name), id + 1) is nullable (Computed), even when its inputs are NOT NULL. |
| correct | test_computed_not_null_is_inferred |
Constants, count(...), the ranking window functions, merge_action() and a COALESCE with a proven argument are NOT NULL (ComputedExpr). |
| correct | test_computed_stays_nullable_when_unproven |
NULL::t, count(*) FILTER, other aggregates, arithmetic over a proven value and a constant on the null-extended side of an outer join stay nullable. |
| correct | test_shadowed_function_stays_nullable |
A user-defined count or row_number outside pg_catalog disables the function items for that describe call. Constants and COALESCE over them are unaffected. |
| correct | test_bang_narrows_computed_column |
"col!" forces NOT NULL on a computed column (OverrideBang). |
Checker limits¶
- Array nesting depth is not checked. Postgres does not record dimensions in a column's declared type, so
describecan't report them. The checker acceptslist[X],list[list[X]]and deeper for any array column. A mismatch with the real value's dimensions (such aslist[int]for a two-dimensionalint[]) surfaces at runtime. Pinned bytest_array_depth_is_not_checked.
Named row limits¶
- No nesting. A named type is allowed only as the row. A named type inside a tuple, a list or a union, or as a
field type, is
invalid-row-type, so there are no JSON-shaped fields, composite or record columns, orflatten. - Pydantic-only types are rejected.
EmailStr,AnyUrlandSecretStraren't row types on either side, andconstr()andPositiveInt(constraint aliases of the formAnnotated[<leaf>, ...]) are rejected byrowx checkonly; the runtime strips the metadata and accepts them. The checker compares exact Python types and has no pydantic type table. Annotate the column's Python type and validate withAnnotated[str, AfterValidator(...)]. Lax-mode coercions, such as anumericcolumn into anintfield, are reported too. - Models with unknown names are partly checked. For
AliasChoices,AliasPath, a non-literal alias, amodel_validator(mode="before" | "wrap"), analias_generatoror an unreadablemodel_config, the checker skipsmissing-column,unexpected-column,duplicate-columnandambiguous-columnfor the whole model. Anames not checkednote is attached only to a diagnostic the model emits, and a clean model with unknown names is silent. Type and nullability checks still run on columns that map to a field with known names. At runtime a column that matches no known name passes through with the default decoder, so anintervalcolumn there has noIntervalflag, and two columns with one name collapse to the last one in the dict. - A generic pydantic dataclass is not parametrized.
Box[int]is built by calling the origin class, so pydantic validates the field as the unparametrizedTand values pass through. ATypeVarbound is enforced by pydantic there but not by the checker. - Checker-only rejections beyond pydantic aliases.
Json[...], mixed row type forms (a model with a stdlib dataclass base, or a stdlib and a pydantic dataclass stacked), an undecorated subclass of a pydantic dataclass, a base from the stdlib (abc.ABC,Exception), a field declared in a conditional block, and a plain alias over aTypeVarTuplework at runtime and failrowx check. - Notes print absolute paths. The
field defined atand similar notes show an absolute path (#208). - No column rename for dataclasses and TypedDicts. There is no equivalent of sqlx's
rename, so a column that needs a different name than the field uses a quoted alias in SQL. - Some rejections are checker-only. A class decorator other than the dataclass decorators and
typing.final, ametaclass=or another unknown class keyword, a hand-written__init__on a stdlib dataclass, and a pydantic constraint alias are rejected byrowx check, because it can't see what they do. The runtime accepts them, and a constructor failure surfaces asRowConstructError. - Not supported: attrs, msgspec,
Protocol,collections.namedtuple,RootModel, pydantic v1, a dataclass withinit=Falseor anInitVar(a pydantic dataclass accepts the decorator'sinit=Falsebut rejectsInitVar), a named row with no fields, a nullable named row (User | None, use.optional()), named parameters, and row classes from installed packages (the checker resolves classes in the project only). Pydantic needs 2.11 or later. A plan caches__pydantic_validator__, so a latermodel_rebuild(force=True)isn't seen by a plan that is already built. - Types are not checked at runtime, except by pydantic. A class whose annotations don't match its columns fails
only in
rowx check.
Execution limits¶
- No multi-statement execution. A query is one statement. Postgres prepares one statement at a time, so a string with several statements fails at prepare time. Run them as separate queries.
- No
commandtag inQueryResult.QueryResultcarriesrows_affectedonly. The tag ("INSERT 0 5") is not exposed by the driver. - Extension types are verified once per statement. The owning extension of
citext,hstore,ltree,lqueryandcubeis checked when a statement is prepared.DROP EXTENSIONorALTER EXTENSIONwhile a connection holds a cached statement is not noticed until that statement is prepared again.
Migration limits¶
-- no-transactionis still one implicit transaction. The file is sent as a single simple-protocol query, so Postgres runs a multi-statement file as one transaction block. A statement that can't run in one (CREATE INDEX CONCURRENTLY,DROP INDEX CONCURRENTLY,REINDEX ... CONCURRENTLY,VACUUM) must be the only statement in its file, and an enum value fromALTER TYPE ... ADD VALUEcan't be used in the same file unless the file commits in between with its ownBEGINandCOMMIT.- PgBouncer in transaction mode is unsupported. The migration lock is a session-level advisory lock, which a transaction-mode pooler doesn't keep. Migrate over a direct or session-mode connection.
- A file that ends rowx's transaction is not detected. A transactional file containing
COMMITorROLLBACKruns, but what follows it is outside the transaction rowx opened, and the history row is written after the fact. Mark such files-- no-transaction. - Migrations don't set timeouts.
statement_timeoutandlock_timeoutcome from the role, the database or the URL.SET LOCALworks in a transactional file but can't share a file withCREATE INDEX CONCURRENTLY. - A
SETin a file outlives it on aConnection. A session-levelSET(such assearch_path) stays on that connection after the run, until a pool's reset clears it. UseSET LOCAL, or a schema-qualifiedtable. - A failed run reports no partial progress.
runreturns nothing when it raises, so theAppliedlines of migrations that committed before the failing one are not printed.rowx migrate infoshows the state. rowx check --scratchandrowx.testingexecute migration files.rowx checkdoes so only with--scratch, in the database it creates, and the plugin in its template databases. Migration files are trusted code.- The test plugin is asyncio only. Trio is unsupported, and roles created by a test are cluster-wide, so a database clone doesn't isolate them.
Rollout compatibility covers discoverable query sites and schema effects the checker can describe. It does not execute queries or verify production data or permissions. For example, PostgreSQL 18.3 accepts Parse/Describe and EXPLAIN for an INSERT omitting a new NOT NULL column with no default, even though execution fails.