Skip to content

ci: verify skipped migrations target no public schema (register #40) - #62

Merged
tornidomaroc-web merged 1 commit into
mainfrom
ci/gen-db-types-verify-skipped-files
Jul 21, 2026
Merged

ci: verify skipped migrations target no public schema (register #40)#62
tornidomaroc-web merged 1 commit into
mainfrom
ci/gen-db-types-verify-skipped-files

Conversation

@tornidomaroc-web

Copy link
Copy Markdown
Owner

Closes register #40"latent hole in the db-types gate".

The hole

supabase/migration-order.txt skips 002_storage.sql, and the skip is keyed on the filename, not on the file's content. Nothing stopped a public-schema object from being added to a skipped file: it would be silently absent from the generated types while db-types — a required status check — stayed green.

gen-db-types.sh also printed the claim, on every run, with nothing executing it:

Skipped migrations create no objects in the public schema, so they cannot affect the generated types.

That is prose wearing a gate's uniform — the same shape as #23 (a written migration assumed applied) and #39 ("Enforcement WAS prose only and prose already failed once").

The fix

Each skipped file is now statically verified before Postgres starts: every statement must match a form whose target is explicitly qualified to an allowlisted schema (auth, storage). The notice no longer asserts the claim — it states what was verified, and keeps saying what this gate still does not prove.

Allowlist, not a public-token denylist, because a denylist is backwards in both directions:

  • it misses the dangerous case — create table foo (id int) resolves to public through search_path and contains no public token at all;
  • it fires on a harmless one — 002_storage.sql:1 is insert into storage.buckets (id, name, public), where public is the bucket-visibility column. A token scan would have redded a provably clean tree on its first run.

Honest limit (stated in the script's own register)

A fail-closed static heuristic over SQL text, not a proof of application. It has a documented undecidable case: a $$-quoted body, dynamic SQL, or an unterminated literal/comment is rejected rather than guessed at. The only sound proof is execution, and execution is blocked for exactly the reason the skip exists — the storage schema is absent on bare Postgres. Provisioning a stub of it would let the gate claim a full application backed by a fake schema this repo does not own: a louder false green than the one being closed.

Evidence

Real file passes (002_storage.sql — one insert into storage.buckets, two create policy ... on storage.objects, all storage-qualified):

==> Verifying skipped migration 002_storage.sql (targets no public-schema object)
...
==> Applied 13/14 migrations; skipped 1

Fail-closed path fires, through the real script, before docker run (scratch file, not committed):

==> Verifying skipped migration 999_scratch.sql (targets no public-schema object)

ERROR: supabase/migrations/999_scratch.sql is SKIPPED by supabase/migration-order.txt, but it
       cannot be shown to leave the `public` schema untouched.

  - this statement matches no verified-safe form:
        create table foo (id int)
=== script exit: 1 ===

Also exercised: explicit create table public.foo → red; do $$ ... $$ → red with "apply it or split it"; create table foo inside a -- comment → pass; the same text inside a string literal → pass.

Scope

Register #40 only. No migration, no DB, no live access, no app code. database.types.ts is byte-identical; the same 13/14 migrations apply. Placed beside the manifest/disk cross-check so a bad skipped file reds on a text read in seconds rather than after Postgres startup and a partial application.

🤖 Generated with Claude Code

The db-types gate skips 002_storage.sql, and the skip is keyed on the
FILENAME, not on the file's content. Nothing stopped a `public`-schema
object from being added to a skipped file: it would be absent from the
generated types while db-types -- a REQUIRED status check -- stayed
green. Making the check required on 2026-07-17 raised the stakes on that
hole rather than lowering them, because a false green now carries
authority to gate merges.

Worse, gen-db-types.sh PRINTED the claim on every run:

    "Skipped migrations create no objects in the public schema, so they
     cannot affect the generated types."

Nothing executed it. That is prose wearing a gate's uniform -- the same
shape as register #23 (a written migration assumed applied) and register
#39 ("Enforcement WAS prose only and prose already failed once").

Each skipped file is now statically verified before Postgres starts:
every statement must match a form whose target is EXPLICITLY qualified to
an allowlisted schema (auth, storage). The notice no longer asserts the
claim; it states what was verified, and it keeps saying what this gate
still does not prove.

The predicate is an ALLOWLIST, not a `public`-token denylist, because a
denylist is backwards in both directions:

  - it MISSES the dangerous case -- `create table foo (id int)` resolves
    to `public` through search_path and contains no `public` token at
    all; an allowlist reds it by construction;
  - it FIRES on a harmless one -- 002_storage.sql line 1 is
    `insert into storage.buckets (id, name, public)`, where `public` is
    the bucket-visibility COLUMN. A token scan would have redded a
    provably clean tree on its first run.

HONEST LIMIT, stated in the script's own register: this is a fail-closed
STATIC heuristic over SQL text, not a proof of application. It has a
documented undecidable case -- a $$-quoted body, dynamic SQL, or an
unterminated literal/comment is REJECTED rather than guessed at, because
refusing to guess is what keeps the result meaningful. The only sound
proof is execution, and execution is blocked for exactly the reason the
skip exists: the storage schema is absent on bare Postgres. Provisioning
a stub of it would let the gate claim a FULL application backed by a
fake schema this repo does not own -- a louder false green than the one
being closed.

Behaviour on the current tree is unchanged: 002_storage.sql passes (its
three statements are one `insert into storage.buckets` and two
`create policy ... on storage.objects`, all storage-qualified), the same
13/14 migrations apply, and database.types.ts is byte-identical. Placed
beside the manifest/disk cross-check and BEFORE `docker run`, so a bad
skipped file reds on a text read in seconds instead of after Postgres
startup and a partial application.

Register #40 only; no migration, no DB, no live access, no app code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
knowflow Ready Ready Preview, Comment Jul 21, 2026 1:25pm

@tornidomaroc-web
tornidomaroc-web merged commit d8c8fde into main Jul 21, 2026
4 checks passed
@tornidomaroc-web
tornidomaroc-web deleted the ci/gen-db-types-verify-skipped-files branch July 21, 2026 13:42
tornidomaroc-web added a commit that referenced this pull request Jul 21, 2026
…64)

Records three register moves from one purpose: the db-types REQUIRED
check can no longer pass on a wrong result, nor red on a third party's
traffic. Logged under Option C — a single-purpose current entry, with
docs-PR provenance closed in the header lines, not changelog prose.

Four edit regions, nothing else:
- Line 8: PREPEND a gate-integrity current entry (#40 closed, #43
  opened+closed, #44 opened) and advance the "Last updated" stamp to
  2026-07-21. The prior #60/#58 current entry is NOT demoted or
  rewrapped — byte-identical.
- Lines 9-10: advance Active-branch + Main-tip to the merged state. The
  Main-tip provenance chain now names 3378dd8 (PR #63) on top of
  d8c8fde (PR #62) on 71e1c9d (PR #60) via the PR #61 docs merge
  b62cbd5 — this is where PR #62's zero-mention lag is closed, in
  provenance, not in changelog prose.
- Register #40 row: CONVERT IN PLACE to CLOSED (PR #62, merge d8c8fde),
  per the #38 template for a closed row that keeps its history. The lead
  flips to ✅ and preserves the original marker after "Was:". The "Where
  addressed" cell records WHICH branch was taken — the content-aware
  skip; `storage` was NOT provisioned — so the untaken alternative is not
  later mistaken for unfinished work, and carries the honest limit (a
  fail-closed STATIC heuristic with a documented undecidable case, not a
  proof of application). Columns 2 and 3 are byte-identical: both remain
  historically accurate.
- Register rows #43 and #44 APPENDED, strictly sequential. #43 (closed,
  PR #63) carries its residual IN the row per the #38 precedent — Docker
  Hub is also anonymous-quota'd, and a SUPABASE_CLI_VERSION bump can
  silently fall back to an ECR pull. #44 (open, unfixed) records the
  npx integrity gap so the class is not considered "handled" because #43
  closed.

Frozen-history invariant proven, both hashes before == after:
  primary   (Option C region, old entry's opening paren -> EOL8)
            sha256 d0c031b5a7ee2623e7b0443e337a612411563cea5329235adcc57118e69776fd
  secondary (first "Prior update" -> EOL8)
            sha256 22019b90c7ae5884e7531abb7916eb095c41745ed2b80cb01b6ab91a117aaf20
The insert is 2615 bytes and the frozen region shifted right by exactly
that (byte 32 -> 2647); "Prior update" markers stay 15, so nothing was
demoted. The historical "#40" mention inside the frozen region (now byte
11030) still describes #40 as OPEN and is deliberately UNCHANGED: it was
true when written, and editing it would break the invariant.

Not touched: §1 (Phase 7 stays ⬜ NOT STARTED — #40 was bucketed Phase 7
and closed early, the phase is unchanged); register #39 (its repo↔live
parity claim is unaffected by either fix and the row is marked
non-closable); PIVOT_PLAN.md. Docs-only; no code, no migration, no DB.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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