8000 fix(engine)!: narrow sqlite strict, postgres written params and call by colinhacks · Pull Request #186 · colinhacks/reylo · GitHub
[go: up one dir, main page]

Skip to content

fix(engine)!: narrow sqlite strict, postgres written params and call - #186

Open
colinhacks wants to merge 4 commits into
mainfrom
sqlite-strict
Open

colinhacks wants to merge 4 commits into
mainfrom
sqlite-strict

Conversation

@colinhacks
@colinhacks colinhacks commented Nov 5, 2026 •
Copy link
Copy Markdown
Owner

Three places where the engines typed something wider than the type descriptor format says they should, each too wide rather than wrong: a SQLite STRICT column, a Postgres parameter written into a column that has a type modifier, and the rows of a Postgres CALL.

A SQLite STRICT column holds its declared type alone

A column read from a STRICT table, and a rowid in any table, now has its declared type alone. They were typed as the union of everything their affinity stores: nothing wrong could pass, but a consumer of the descriptor had to handle values the database refuses to store.

 create table s (id integer primary key, n integer not null, t text) strict;
 select id, n, t from s;
-{ id: INTEGER(int64 | float | text | bytes), n: INTEGER(int64 | float | text | bytes), t: TEXT(text | bytes)? }
+{ id: INTEGER, n: INTEGER, t: TEXT? }

What holds its declared type alone

  • A column of a STRICT table other than ANY or a generated column.
  • A rowid, under its own name or as an INTEGER PRIMARY KEY alias, in any table.
  • Either of those read through a subquery, a view or a WITH query, or in every arm of a compound.

What keeps the union

  • A generated column of a STRICT table. SQLite 3.54.1 refuses a computed value of another type, but 3.51.0 stores '1abc' in an integer one, virtual or stored, and an app may run an older SQLite than the type check.
  • A column outside a STRICT table that is no rowid. INTEGER PRIMARY KEY DESC, INT PRIMARY KEY and an INTEGER PRIMARY KEY in a WITHOUT ROWID table are no rowids, and each stores 'abc' as it is.
  • A compound with any other arm, VALUES included.
  • The recursive reference of a WITH RECURSIVE query, which is read conservatively.

A Postgres parameter written into a column takes its type modifier

Postgres describes a parameter written into a numeric(10,2) column as plain numeric, and one written into a varchar(20) column as plain varchar. A parameter written whole and bare into a column now takes the column's modifier, as the format says: precision and scale, a size, a tick, an interval's fields, an array's for its elements.

 create table items (id int primary key, name varchar(20), price numeric(10,2));
 insert into items (name, price) values (:name, :price);
-(name: varchar, price: numeric)
+(name: varchar(≤20), price: numeric(10,2))

The server rounds 1.234 written into numeric(10,2) and refuses a longer string for varchar(20), so the column's type is the stricter one, never the looser. A parameter under a cast keeps its cast's type, and one only compared with a column keeps the server's. The column query reads each column's type and modifier, one round as before.

A Postgres CALL returns one row

A procedure with OUT or INOUT parameters returns their values as exactly one row, and it was typed as any number of rows. Its cardinality is now one, so .run() returns the row itself rather than an array of one. A procedure with neither still returns no row.

 call count_users(null);
-many () -> { n: int8? }
+one () -> { n: int8? }

Unchanged

The declarations and the docs' captured types for the first two: SQLite's column types are read by their declared names, and a modifier changes no TypeScript type. .run() changes only for a CALL that returns a row, above.

Tests

  • A new oracle case, sqlite_strict_columns, runs the SQLite rules over seeded edge values: 21 statements, none of them contradicted. 27 other sqlite_* cases' expected.txt change, 161 lines in all, each a rowid or STRICT INTEGER column losing its union, or a STRICT TEXT column losing bytes.
  • type_modifiers gains statements that write parameters by an INSERT, the SELECT that feeds one, an UPDATE and an upsert, cast and bare, and one that only compares them; each runs over the seeded edge values.
  • An engine test for each rule.

Verified

  • cargo build --workspace --all-targets with no warning; the SQLite engine's tests (44) and the Postgres engine's (202).
  • The oracle with --strict-coverage: every sqlite_* case and every Postgres case, none failed. Two other Postgres cases' expected.txt gain a written parameter's modifier: check_unions's tier becomes varchar(≤10, 'free' | 'pro'), and pgvector_types's embedding becomes vector of exactly 3.
  • nub run typecheck, and nub run test against Postgres 18 and MySQL 8.4 on the release binary: 365 passed, none skipped.
  • nub run capture in site/ writes the same files, so no type on the docs changes.

Copilot AI balanced review requested due to automatic review settings November 5, 2026 06:26
@vercel
vercel Bot commented Nov 5, 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 6, 2026 6:31am 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. 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.

Important

A generated column of a STRICT table is now typed as its declared type alone, but SQLite has only enforced STRICT typing on generated columns since 3.51.0. Older SQLite builds still in use (3.45.1 here) store anything in them, so the descriptor is too narrow there.

Reviewed changes

I reviewed the initial commit, which narrows the type descriptor of a SQLite column read as it is when its table is STRICT or when it is a rowid.

  • Analyzer::exacts: each result column gets an exact flag, set for a bare column reference with strict or rowid. The flag follows subqueries, views and WITH queries, and must hold in every arm of a compound. A VALUES arm clears it.
  • descriptor: an exact column with a declared name now gets the single storage class (int64, real, text, bytes) under that name, instead of the affinity union from stored.
  • Oracle: there is a new case, sqlite_strict_columns, and 27 sqlite_* cases are reblessed. I checked every added line in those 27 files: each one only narrows an INTEGER, INT, TEXT, REAL or BLOB column, as the description says.

The rowid half holds up. columnRef only sets rowid for iColumn<0 or iCol==pTab->iPKey, and iPKey is -1 for INTEGER PRIMARY KEY DESC, for a WITHOUT ROWID key and for INT PRIMARY KEY, which the new case covers. Ordinary STRICT columns and ANY (which stays unknown) also hold up.

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

Comment thread engine/sqlite/src/sqlite.rs 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 prior finding on generated columns is addressed.

Reviewed changes

I reviewed ed06f5c, which leaves a generated column of a STRICT table out of the exact typing.

  • exact: now returns (n.strict && !n.generated) || n.rowid. Node reads the generated flag that columnRef in reylo_sqlite.c already emits, and it defaults to false under #[serde(default)]. A rowid is never generated, so the rowid half is unaffected.
  • Tests: the engine test adds a virtual generated g to the STRICT table and expects union. strict_read in sqlite_strict_columns is reblessed, and only g and h widen back to their affinity unions. The comment in setup.sql is corrected.

ℹ️ The PR description still says generated columns are exact

Under "What holds its declared type alone", the first bullet still reads "generated ones included: SQLite checks what a generated column computes as it stores the row, the virtual kind too". Since ed06f5c that is no longer true. The bullet should move to "What keeps the union" before the squash merge.

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

@colinhacks colinhacks changed the title fix(sqlite): type a strict table's column as its declared type alone fix(engine): narrow sqlite strict columns and postgres written params Nov 5, 2026
@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 critical issues, just one minor suggestion inline.

Reviewed changes

I reviewed e66c91b, which gives a Postgres parameter written to a column that column's type modifier.

  • RELATION_COLUMNS_QUERY: the query now also reads atttypid and atttypmod, into a new Engine::column_types, in the same round as before. The shadow pg_attribute in tests/catalog-queries.test.ts already has both columns.
  • columns_of: this was split out of written_columns, so Parsed::bare resolves to columns the same way filled does.
  • Parameter type: a parameter takes render_typmod only when every bare column it is written to has the described type and one shared typmod of 0 or more. A cast parameter, one that is only compared, and a column with no modifier (a domain column included) keep render(o).
  • Tests: there is a new engine test, and four new type_modifiers statements plus compared.sql. check_unions and pgvector_types are reblessed.

Neither the input JSON Schema (engine/core/src/schema.rs) nor .run() reads a Postgres size or precision, so as the description says, nothing changes at run time.

ℹ️ The PR now carries two unrelated changes

The SQLite STRICT change and the Postgres written-parameter change touch separate engines and separate oracle cases. They were batched under a retitled fix(engine): …. AGENTS.md asks for one change per pull request, since each squash-merges as one commit. Splitting e66c91b into its own PR would keep the two changes apart in history and in a revert.

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

Comment thread engine/postgres/src/postgres.rs Resolved
@colinhacks colinhacks changed the title fix(engine): narrow sqlite strict columns and postgres written params fix(engine): narrow sqlite strict, postgres written params and call Nov 5, 2026
@colinhacks colinhacks changed the title fix(engine): narrow sqlite strict, postgres written params and call fix(engine)!: narrow sqlite strict, postgres written params and call Nov 5, 2026
@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.

ℹ️ I found no new issues in 88acbdc. The earlier thread on bpchar(=n) for a written parameter is still open and unchanged by this commit.

Reviewed changes

I reviewed 88acbdc, which types the rows of a Postgres CALL as exactly one.

  • Engine::command: when a statement's first word is call and the describe returns columns, the cardinality is now One instead of Many. A statement with no columns still gets None, and other commands with columns (EXPLAIN, SHOW, …) still get Many. This matches what Postgres does: a procedure with OUT or INOUT parameters sends exactly one tuple, or fails.
  • Oracle: commands' call_out.sql moves from many to one, and nothing else changes. That fits a rule keyed on the statement's kind.

The ! added to the title fits, because .run() now returns the row instead of an array of one. No docs page or skill describes a CALL's rows, so nothing needs updating there.

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

@colinhacks
Copy link
Copy Markdown
Owner Author

On the review's note about two unrelated changes in one pull request: they stay together. Both are places where an engine typed a value wider than the format says, found in one pass, and this repository lets fixes found together share a pull request, a commit each.

This branch has not been deployed

No deployments
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