Repository navigation
Postgres parity from 250 real project migrations: transactional DDL, type checking, RLS fixes - #12
Merged
Merged
Conversation
…s against PGlite Adds tools/corpus-diff, which runs real migrations + seeds through pg-mem and PGlite side by side (catalogue, data, RLS, triggers, cascades, and generated reject/DDL probes). Across 150 projects, divergence went from every project to 1-4 per 50 (remaining: cosmetic default text, FK action ordering, non-transactional DDL). Validator strictness (pg-mem accepted what Postgres rejects): - typed values no longer cross type categories implicitly (uuid = text, bool = text, ...); untyped literals and parameters still coerce; date/timestamptz and int/bigint now widen - CREATE POLICY binds its predicates (unknown columns, bad operators, non-boolean, USING on INSERT / WITH CHECK on SELECT|DELETE) - ALTER COLUMN TYPE / DROP COLUMN refuse columns a policy (on any table) or FK depends on - CREATE TRIGGER resolves its function; GRANT / CREATE POLICY ... TO check the role exists - int4/int2/int8 range checks; strict text-to-number parsing Things pg-mem rejected or got wrong: - BEGIN ... ROLLBACK across query calls actually rolls back; aborted blocks behave like Postgres - CREATE TABLE IF NOT EXISTS on an existing table no longer "Not supported" - unnamed constraints/indexes get Postgres names (t_col_check, t_a_b_key, t_a_b_idx) - NULL IN (...) is NULL, so CHECK (col IN (...)) accepts NULL - now()/current_timestamp/current_date are transaction-stable; current_date is a date - SECURITY DEFINER functions run as their owner - plpgsql: multi-char operators (->>) in bodies, %ROWTYPE / %TYPE variables - timestamp - timestamp, date - date (integer), interval::text, record::text, jsonb ? ?| ?& - text->numeric stores the canonical decimal string (fixes "text.trim is not a function") - information_schema.columns reports Postgres type names; DROP TABLE frees index names and removes dependent FKs from the catalogues; DROP VIEW; Postgres FK error messages Needs @tinbase/pgsql-ast-parser with the matching grammar changes (SET search_path, DROP VIEW). Co-Authored-By: Claude Opus 5.5 <[email protected]>
…vergence on 250 projects Second round from the corpus diff (tools/corpus-diff). All five sets of 50 real projects (250, ~46k checks) now match PGlite. - DDL is transactional: a failed migration or ROLLBACK / ROLLBACK TO undoes created, altered and dropped tables, columns, constraints, indexes, policies, triggers and functions (schema-snapshot.ts captures the schema metadata before a transaction's first DDL; row data already rolls back with the transaction). DDL no longer force-commits mid-transaction, and commit rebases onto a parent that moved (CREATE SCHEMA registers catalogues in root). - referential actions run breadth-first like postgres RI triggers (ri-queue.ts): a direct NO ACTION reference is checked before a grandchild cascade - security: RLS was skipped on index lookups - `select ... where id = 2` returned rows no policy allowed, and UPDATE/DELETE ... WHERE <pk> reached them. Filters on a policed table now apply the policies on top. - "infinite recursion detected in policy for relation", following postgres' expansion rules (only relations whose applicable policies have subqueries; UPDATE/DELETE that read columns also expand SELECT policies) - CREATE OR REPLACE TRIGGER; replacing a trigger function no longer "changes its return type" - information_schema.columns.column_default reads like pg_get_expr (0.30, ARRAY[...], (now() + '2 days'::interval), '-1') - fix: count(*) with a filter on information_schema.columns / pg_constraint crashed Requires @tinbase/pgsql-ast-parser 12.2.0 (CREATE OR REPLACE TRIGGER, numeric literal text). Co-Authored-By: Claude Opus 5.5 <[email protected]>
…pe check - CREATE POLICY's predicate check left its build in the expression cache, keyed on (selection, AST); enforcement then reused a subquery compiled under the other table's policies as they were at CREATE POLICY time (empty results in normal migration order, or rows a narrowed policy hides). Policy create/drop and RLS enable/disable now count as schema changes, and a schema change clears the build cache. - That also keys the recursion cache correctly: replacing a recursive policy no longer keeps reporting "infinite recursion", and adding one after a clean query reports it. - With the cache no longer short-circuiting it, compiling a policy that reads its own table recursed until the stack overflowed. A re-entrant compile now defers to the run-time recursion check (42P17 for the roles the policy applies to, as in postgres). - `x IN / NOT IN (select col ...)` checks x against the column's type category: `auth.uid() in (select <text col>)` is rejected like `=` is. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- INSERT ... ON CONFLICT DO UPDATE checks the conflicting row against the UPDATE and SELECT USING policies (42501, as in postgres) and the updated row against UPDATE WITH CHECK; it updated other users' rows. - ON CONFLICT (cols) DO NOTHING no longer returns the existing row from RETURNING (the untargeted form already returned nothing). - ROLLBACK TO SAVEPOINT in an aborted block rewinds to the savepoint and keeps the block, instead of being taken as a full ROLLBACK that ended it - after any error, a savepoint-based retry lost the whole block. - A top-level statement that fails to compile (unknown column, uuid = text, ...) inside a BEGIN block aborts it, as one failing at run time does, so COMMIT no longer keeps the block's earlier writes. - SAVEPOINT no longer copies the schema: the first DDL after it does. RELEASE / rollback to an earlier savepoint drop the copies. 200 savepoints on a 150-table schema: 645 ms / +323 MB -> 69 ms / +5 MB. A re-declared savepoint name becomes the newest savepoint. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
tools/corpus-diffruns real RapidNative project migrations and seeds through pg-mem and PGlite side by side. It compares migration and seed outcomes, the catalogue reads the agent validator makes, data, RLS as anon/user/stranger (including primary-key lookups), triggers, cascades, and generated probes that Postgres must reject. PGlite (real Postgres) is the oracle. The corpus is user project code and is never committed; the README covers sampling it.The five sets add up to about 46k checks. Every set after A was also used to fix things, so the honest held-out signal is the score each one got the first time it was run (C 3/50, D 3/50, E 2/50).
Validator strictness (pg-mem accepted what Postgres rejects)
uuid = text, the bug that sent the agent validator to PGlite.bool = text,IN,COALESCE,LIKEand function arguments are covered too. Untyped literals and parameters still coerce. date/timestamptz and int/bigint now widen.CREATE POLICYbinds its predicates: unknown columns, bad operators, non-boolean predicates,USINGon INSERT,WITH CHECKon SELECT/DELETE.ALTER COLUMN TYPEandDROP COLUMNrefuse columns that a policy (on any table, scope-aware) or an FK depends on.CREATE TRIGGERresolves its function.GRANTandCREATE POLICY ... TOcheck that the role exists.Correctness
ROLLBACK, orROLLBACK TO SAVEPOINTundoes schema changes.schema-snapshot.tscaptures schema metadata before a transaction's first DDL; row data already rolled back with the transaction.BEGIN … ROLLBACKacross separate query calls actually rolls back, and aborted blocks behave like Postgres.ri-queue.ts).select … where id = 2returned rows no policy allowed, andUPDATE/DELETE … WHERE <pk>reached them.SECURITY DEFINERruns as the owner.->>inside bodies,%ROWTYPE/%TYPE.now()/current_dateare transaction-stable.create table if not existson an existing table,NULL IN (...), andCREATE OR REPLACE TRIGGER.information_schema.columnsreports Postgres type names andpg_get_expr-style defaults.interval::text,record::text,jsonb ? ?| ?&.text → numericstores the canonical decimal (this fixes "text.trim is not a function").Footprint (Node 24, real projects; the agent validator's workload)
Before merging
@tinbase/pgsql-ast-parser12.2.0 (Supabase DDL found in real project migrations (12.2.0) sanketsahu/pgsql-ast-parser#1).package.jsonalready points at^12.2.0, so install fails until that's published.bun.lockbneeds regenerating after.Tests: 1320 passing.
src/tests/corpus-parity.spec.tshas a minimal repro for each gap, with expected values checked against PGlite.🤖 Generated with Claude Code