Skip to content

fix: locked-down tables stay locked, quoted names, views, --json, data fidelity - #124

Merged
jasdeepkhalsa merged 15 commits into
mainfrom
fix/retest-findings
Oct 8, 2026
Merged

jasdeepkhalsa merged 15 commits into
mainfrom
fix/retest-findings

Conversation

@jasdeepkhalsa

@jasdeepkhalsa jasdeepkhalsa commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Fixes from retesting 0.0.29 against Supabase, plain PostgreSQL and a PostgreSQL 15/17 pair. Every fix has a test that fails on the previous code and passes with the fix.

High

A new table the source keeps away from the Data API stays that way after one sync.

  • The bug: a table protected by REVOKE ALL … FROM anon, authenticated (RLS off) and created on a Supabase target got everything the target's default privileges grant. It stayed readable and writable with the anon key until a second sync.
  • The fix: the roles check now works out what the target's defaults will grant on each table it lacks, and revokes whatever the source does not grant. The revoke runs after the table is created, in the same apply.
  • Severity: extra grants to anon, authenticated or PUBLIC are now critical, and extra grants to any other role a warning. Both were info.
  • Tests: test/e2e/grants-on-new-tables.test.ts runs one sync against Supabase-style default privileges and checks the grants against the source's.

Medium

Names holding a double quote.

  • The bug: say "hi" there was written between plain quotes, a syntax error. A restore holding one rolled back entirely, and diff --apply applied nothing.
  • The fix:
    • Policy SQL for the RLS, storage and Realtime checks and for the snapshot is now built in one place, quoting the policy, schema, table and every role in TO.
    • Webhook, RLS coverage, extension, publication, grant and database names are quoted the same way.
    • A role list arriving as an array literal is parsed properly.
    • Titles, and the keys that stop the schema check and the RLS check creating the same policy twice, read doubled quotes as one name.
  • Tests:

A view dropped and recreated is one "View modified" warning, not a critical "Extra view". Materialized views are handled the same way.

  • The cause of most such findings, PostgreSQL 15 and 16+ printing the same view differently, is fixed in fix(postgres): quoted names, views across major versions, a replaced view's grants DBDiff/DBDiff#254, which also keeps a replaced view's grants.
  • The scenario runner now judges a cross-version target against the source's SQL built on the target's server, as --prove does.
    • With rc.23 the PG17→PG15 known gaps are unchanged.
    • With DBDiff's fix, all 15 of them pass. They come out of known-gaps.json in the bump.

Low

  • --json is JSON alone:
    • every other line now goes to stderr; migrate list and clone used to print their preflight checks on stdout;
    • a clone preview prints a JSON result too.
  • migrate list without read access to the history now shows the status as unknown (applied: null) and exits 1, instead of calling every migration pending.
  • Reference data keeps json exactly:
    • each column is captured as PostgreSQL's own text and restored with a cast, so json keeps its spacing and duplicate keys, and a JSON null stays distinct from SQL NULL;
    • snapshots in the earlier format still restore.
  • Folding into the creating finding:
    • a comment on a new function folds into "Function missing" even when its signature is spelled differently;
    • a new table's ENABLE ROW LEVEL SECURITY folds into "Table missing".
  • Restore reports:
    • a skip names only the missing extension its error is about;
    • grants are snapshotted one statement per role and object, so a skipped relation is one line per role;
    • a rolled-back restore no longer also says incomplete.

Also

  • A recreated object's comments (dbdiff sets them again) fold into its "modified" finding instead of reading as "Comment changed".
  • The RLS check's ENABLE/DISABLE ROW LEVEL SECURITY and the extensions check's SQL quote names properly.
  • No TypeScript warning for users. Under NODE_ENV=development or test, as in many projects running npx supaforge, the published package printed oclif's "Could not find typescript" warning on every command. The compiled bin now turns that lookup off. The release dry run installs the packed package and fails on any output to stderr.
  • scripts/test-release-candidates.sh builds DBDiff's PHAR and static binary from a checkout as a release does, packs @dbdiff/cli, @dbdiff/cli-linux-x64 and pg-conformance, and installs SupaForge against them as a user would. It then runs every suite against that install. SUPAFORGE_E2E_CLI points the harness at the installed CLI. Building from DBDiff needs fix(build): build-local.sh copies the PHAR out of its build copy DBDiff/DBDiff#255, a fix to its build-local.sh.

Testing

  • Local suites, with the released @dbdiff/cli rc.23:
Suite Result
Unit 1742 passed
Integration 97 passed
Database e2e 132 passed
Scenarios, twin servers 151 passed
Scenarios, 17 → 15 139 passed, with the known gaps unchanged

Lint is clean.

  • Against a Supabase stack: the same scenarios as the tests above fail on 0.0.29. On this branch the grants, json and restore cases pass. The quoted-policy sync passes with DBDiff#254.

🤖 Generated with Claude Code

jasdeepkhalsa and others added 8 commits October 7, 2026 21:14
…efaults

A table the source keeps away from the Data API (REVOKE ALL ... FROM
anon, authenticated) and created on a Supabase target by the schema fix
got everything the target's default privileges grant. Nothing took that
back until a second sync, so one sync left it open to the anon key.

The roles check now works out what the target's defaults will grant on
each table it lacks, and revokes whatever the source does not grant. The
revoke runs with the grants, after the table is created. An extra grant
to anon, authenticated or PUBLIC is critical, and one to any other role
a warning; both were info. Role, schema and table names in the generated
SQL are now quoted with their own quotes doubled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A policy named say "hi" there was written between plain quotes, a
syntax error: a restore holding one rolled back entirely, and diff --apply
applied nothing from that run. Policy SQL is now built in one place for
the RLS, storage and Realtime checks and the snapshot, with the policy,
schema, table and every role in TO quoted. Webhook, RLS coverage,
extension, publication, grant and database names are quoted the same way.

A role list read as an array literal ({"Admins \"A\"",anon}) is parsed
properly, so such a role is not quoted twice, and titles read a name with
doubled quotes as one name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A changed view arrives from dbdiff as DROP VIEW + CREATE VIEW. Read by
its DROP it was a critical 'Extra view' for a view both sides have. It
is now one 'View modified' warning, materialized views alike, as
triggers, policies and routines already are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'pending'

Under --json every line a command prints besides its result now goes to
stderr: migrate list and clone printed their preflight checks, and clone
its transfer meter, on stdout before the JSON. Each command's JSON goes
through one helper, and a clone preview prints a JSON result too.

migrate list, connected as a role that cannot read the migration
history, warned and then listed every migration as pending, exiting 0.
It now shows the status as unknown (applied: null under --json) and
exits 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows were captured as JSON values, so a json column came back
re-rendered (pretty-printed, keys reordered, a duplicate key collapsed)
and a jsonb holding JSON null came back as SQL NULL. Each column is now
captured as PostgreSQL's own text for it, which its type reads back
exactly, and restored with a cast per column. Snapshots in the earlier
format still restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reports

- A comment on a new function folds into its 'Function missing' finding,
  matched by name when only one new routine has it: dbdiff spells the
  signature with argument types in the comment and with the parameter
  list in the create, so the two never matched.
- A new table's ENABLE/FORCE ROW LEVEL SECURITY folds into its creation
  instead of being a 'Table altered' finding for a table not there yet.
- A restore skip names only the missing extension or schema its error is
  about, and the snapshot groups a role's privileges on an object into
  one GRANT, so a skipped relation is one line per role, not one per
  privilege.
- A restore that rolled back no longer also reports incomplete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t on its server

PostgreSQL prints the same objects differently from one major version to
the next (15 table-qualifies a view's columns, 16+ does not), so a
migrated target never matched a source on another version however right
it was. Across major versions the runner now builds the source's SQL on
the target's server and compares the target with that, as --prove does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ads them

A policy name with a double quote in it is written with it doubled.
Matched as text, the schema check's CREATE POLICY and the RLS check's for
the same policy got different keys, so both were applied and the second
failed with 'policy ... already exists'. Created and dropped policies and
created triggers are now keyed from the parse tree, and the text
fallback reads doubled quotes as part of one name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
import { BRANCHES_FILE, PG_PIPELINE_TIMEOUT_MS, CLONE_PROGRESS_INTERVAL_MS } from './constants'
import { CLONE_STUBS_SQL } from './stubs'
import { quoteIdent } from './utils/sql'
import { quoteIdent, quoteName } from './utils/sql'
* therefore readable and writable with the anon key until a second sync took
* the grants back. Both servers here carry the same defaults Supabase does.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
* role that cannot read the history table, warned and then listed every
* applied migration as pending, exiting 0.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
* error: the restore holding it rolled back entirely, and `diff --apply`
* applied nothing from that run, unrelated fixes included.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
jasdeepkhalsa and others added 6 commits October 8, 2026 07:34
dbdiff sets the comments of a view, function, policy or trigger it
recreates again. Apart from the 'modified' finding they read as
'Comment changed' on a comment that had not. The grants e2e now covers
a new locked-down view as well as tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/test-release-candidates.sh builds DBDiff's PHAR and static
binary from a checkout the way a release does, packs @dbdiff/cli,
@dbdiff/cli-linux-x64 and pg-conformance, builds and packs SupaForge
against them and installs it as a user would. It then runs the unit,
integration, database e2e and scenario suites (twin servers and 17→15)
against that installed package. SUPAFORGE_E2E_CLI points the harness at
it. A fix spanning two repos can then be tested together before either
is released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by running the scenario suites against packed release candidates:
- The RLS check's ENABLE/DISABLE ROW LEVEL SECURITY and the extensions
  check's CREATE, DROP and ALTER EXTENSION wrote names between plain
  quotes, and an extension version unescaped.
- Under NODE_ENV=development or test, as in many projects running
  npx supaforge, the published package warned on every command that it
  could not find TypeScript. The compiled bin turns oclif's TypeScript
  lookup off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Grouping a role's privileges into one GRANT put PostgreSQL 17's MAINTAIN
alongside the rest, so restoring a 17 snapshot into 15 failed on the
whole statement and rolled back, where before only the MAINTAIN line
was skipped. Found by the release smoke against the packed candidate.
A new scenario restores a snapshot across every pair of servers the
scenario suites run on, 17 → 15 included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* a 17 privilege: grouped into one GRANT with the others, the 15 target
* rejected the whole statement and the restore rolled back.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
… only

Off everywhere, a checkout could no longer run its e2e suites before
building dist, which CI does. The published package ships no src/, so
that is what the bin checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jasdeepkhalsa
jasdeepkhalsa merged commit 3580c8e into main Oct 8, 2026
17 checks passed
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.

1 participant