supaschema 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/.agents/skills/supaschema/SKILL.md +61 -0
  2. package/.claude/hooks/block-generated-migration-edits.mjs +32 -0
  3. package/.claude/rules/supaschema.md +22 -0
  4. package/.claude/settings.json +16 -0
  5. package/.claude/skills/supaschema/SKILL.md +61 -0
  6. package/.codex/hooks/supaschema-tool-gate.mjs +73 -0
  7. package/.codex/hooks.json +16 -0
  8. package/.codex/rules/supaschema.rules +22 -0
  9. package/AGENTS.md +40 -0
  10. package/LICENSE +661 -0
  11. package/LICENSE-COMMERCIAL.md +35 -0
  12. package/README.md +249 -0
  13. package/benchmarks/README.md +104 -0
  14. package/benchmarks/compare.js +489 -0
  15. package/benchmarks/fixtures/additive/from.sql +8 -0
  16. package/benchmarks/fixtures/additive/manifest.json +1 -0
  17. package/benchmarks/fixtures/additive/to.sql +9 -0
  18. package/benchmarks/fixtures/functions-policies/from.sql +24 -0
  19. package/benchmarks/fixtures/functions-policies/manifest.json +5 -0
  20. package/benchmarks/fixtures/functions-policies/to.sql +24 -0
  21. package/benchmarks/plot-lib.js +234 -0
  22. package/benchmarks/plot-svg.js +339 -0
  23. package/benchmarks/plot.js +154 -0
  24. package/benchmarks/tools/bench-all.sh +49 -0
  25. package/benchmarks/tools/build-project-fixture.mjs +245 -0
  26. package/benchmarks/tools/compare-db.mjs +101 -0
  27. package/benchmarks/tools/compare-fixtures.mjs +84 -0
  28. package/benchmarks/tools/compare-report.mjs +90 -0
  29. package/benchmarks/tools/compare-supabase.mjs +67 -0
  30. package/benchmarks/tools/registry.js +266 -0
  31. package/benchmarks/tools/run-workflow.mjs +77 -0
  32. package/bin/postinstall.mjs +26 -0
  33. package/bin/supaschema +2 -0
  34. package/config-schema.json +208 -0
  35. package/corpus/supabase-style/corpus.json +6 -0
  36. package/corpus/supabase-style/migrations/20260101000000_init.sql +28 -0
  37. package/corpus/supabase-style/migrations/20260102000000_noise.sql +13 -0
  38. package/corpus/supabase-style/migrations/20260103000000_churn.sql +4 -0
  39. package/corpus/supabase-style/migrations/20260104000000_triggers.sql +17 -0
  40. package/corpus/supabase-style/roles.sql +13 -0
  41. package/corpus/supabase-style/tree/functions.sql +26 -0
  42. package/corpus/supabase-style/tree/policies.sql +4 -0
  43. package/corpus/supabase-style/tree/schema.sql +4 -0
  44. package/corpus/supabase-style/tree/tables.sql +20 -0
  45. package/corpus/supabase-style/tree/triggers.sql +3 -0
  46. package/corpus/supabase-style/tree/types.sql +2 -0
  47. package/corpus/supabase-style/tree/views.sql +6 -0
  48. package/dist/audit.d.ts +20 -0
  49. package/dist/audit.d.ts.map +1 -0
  50. package/dist/audit.js +68 -0
  51. package/dist/benchmark-db.d.ts +5 -0
  52. package/dist/benchmark-db.d.ts.map +1 -0
  53. package/dist/benchmark-db.js +71 -0
  54. package/dist/benchmark-fixtures.d.ts +10 -0
  55. package/dist/benchmark-fixtures.d.ts.map +1 -0
  56. package/dist/benchmark-fixtures.js +201 -0
  57. package/dist/benchmark.d.ts +2 -0
  58. package/dist/benchmark.d.ts.map +1 -0
  59. package/dist/benchmark.js +308 -0
  60. package/dist/catalog-comments.d.ts +9 -0
  61. package/dist/catalog-comments.d.ts.map +1 -0
  62. package/dist/catalog-comments.js +194 -0
  63. package/dist/catalog-extras.d.ts +12 -0
  64. package/dist/catalog-extras.d.ts.map +1 -0
  65. package/dist/catalog-extras.js +408 -0
  66. package/dist/catalog-foreign.d.ts +15 -0
  67. package/dist/catalog-foreign.d.ts.map +1 -0
  68. package/dist/catalog-foreign.js +114 -0
  69. package/dist/catalog-tables.d.ts +9 -0
  70. package/dist/catalog-tables.d.ts.map +1 -0
  71. package/dist/catalog-tables.js +114 -0
  72. package/dist/catalog.d.ts +8 -0
  73. package/dist/catalog.d.ts.map +1 -0
  74. package/dist/catalog.js +351 -0
  75. package/dist/check-hazards.d.ts +7 -0
  76. package/dist/check-hazards.d.ts.map +1 -0
  77. package/dist/check-hazards.js +83 -0
  78. package/dist/check-reporters.d.ts +8 -0
  79. package/dist/check-reporters.d.ts.map +1 -0
  80. package/dist/check-reporters.js +76 -0
  81. package/dist/check.d.ts +3 -0
  82. package/dist/check.d.ts.map +1 -0
  83. package/dist/check.js +229 -0
  84. package/dist/cli-defaults.d.ts +24 -0
  85. package/dist/cli-defaults.d.ts.map +1 -0
  86. package/dist/cli-defaults.js +65 -0
  87. package/dist/cli-diff.d.ts +13 -0
  88. package/dist/cli-diff.d.ts.map +1 -0
  89. package/dist/cli-diff.js +348 -0
  90. package/dist/cli-reports.d.ts +9 -0
  91. package/dist/cli-reports.d.ts.map +1 -0
  92. package/dist/cli-reports.js +90 -0
  93. package/dist/cli-tools.d.ts +17 -0
  94. package/dist/cli-tools.d.ts.map +1 -0
  95. package/dist/cli-tools.js +136 -0
  96. package/dist/cli.d.ts +2 -0
  97. package/dist/cli.d.ts.map +1 -0
  98. package/dist/cli.js +239 -0
  99. package/dist/config-schema-gen.d.ts +2 -0
  100. package/dist/config-schema-gen.d.ts.map +1 -0
  101. package/dist/config-schema-gen.js +11 -0
  102. package/dist/config.d.ts +58 -0
  103. package/dist/config.d.ts.map +1 -0
  104. package/dist/config.js +132 -0
  105. package/dist/core.d.ts +115 -0
  106. package/dist/core.d.ts.map +1 -0
  107. package/dist/core.js +1 -0
  108. package/dist/corpus.d.ts +26 -0
  109. package/dist/corpus.d.ts.map +1 -0
  110. package/dist/corpus.js +112 -0
  111. package/dist/database-url.d.ts +8 -0
  112. package/dist/database-url.d.ts.map +1 -0
  113. package/dist/database-url.js +74 -0
  114. package/dist/db-admin.d.ts +23 -0
  115. package/dist/db-admin.d.ts.map +1 -0
  116. package/dist/db-admin.js +147 -0
  117. package/dist/diagnostics.d.ts +16 -0
  118. package/dist/diagnostics.d.ts.map +1 -0
  119. package/dist/diagnostics.js +155 -0
  120. package/dist/diff-score.d.ts +12 -0
  121. package/dist/diff-score.d.ts.map +1 -0
  122. package/dist/diff-score.js +339 -0
  123. package/dist/doctor.d.ts +17 -0
  124. package/dist/doctor.d.ts.map +1 -0
  125. package/dist/doctor.js +110 -0
  126. package/dist/hash.d.ts +7 -0
  127. package/dist/hash.d.ts.map +1 -0
  128. package/dist/hash.js +34 -0
  129. package/dist/index.d.ts +24 -0
  130. package/dist/index.d.ts.map +1 -0
  131. package/dist/index.js +17 -0
  132. package/dist/lineage.d.ts +23 -0
  133. package/dist/lineage.d.ts.map +1 -0
  134. package/dist/lineage.js +61 -0
  135. package/dist/migrations-status.d.ts +35 -0
  136. package/dist/migrations-status.d.ts.map +1 -0
  137. package/dist/migrations-status.js +131 -0
  138. package/dist/plan-order.d.ts +4 -0
  139. package/dist/plan-order.d.ts.map +1 -0
  140. package/dist/plan-order.js +178 -0
  141. package/dist/planner-replace.d.ts +4 -0
  142. package/dist/planner-replace.d.ts.map +1 -0
  143. package/dist/planner-replace.js +76 -0
  144. package/dist/planner-table.d.ts +3 -0
  145. package/dist/planner-table.d.ts.map +1 -0
  146. package/dist/planner-table.js +165 -0
  147. package/dist/planner.d.ts +5 -0
  148. package/dist/planner.d.ts.map +1 -0
  149. package/dist/planner.js +385 -0
  150. package/dist/render-guards.d.ts +12 -0
  151. package/dist/render-guards.d.ts.map +1 -0
  152. package/dist/render-guards.js +159 -0
  153. package/dist/render.d.ts +7 -0
  154. package/dist/render.d.ts.map +1 -0
  155. package/dist/render.js +325 -0
  156. package/dist/selfcheck.d.ts +11 -0
  157. package/dist/selfcheck.d.ts.map +1 -0
  158. package/dist/selfcheck.js +43 -0
  159. package/dist/source-normalize.d.ts +14 -0
  160. package/dist/source-normalize.d.ts.map +1 -0
  161. package/dist/source-normalize.js +420 -0
  162. package/dist/source.d.ts +4 -0
  163. package/dist/source.d.ts.map +1 -0
  164. package/dist/source.js +233 -0
  165. package/dist/sql/ast.d.ts +42 -0
  166. package/dist/sql/ast.d.ts.map +1 -0
  167. package/dist/sql/ast.js +241 -0
  168. package/dist/sql/canonical-nodes.d.ts +5 -0
  169. package/dist/sql/canonical-nodes.d.ts.map +1 -0
  170. package/dist/sql/canonical-nodes.js +101 -0
  171. package/dist/sql/extract-helpers.d.ts +18 -0
  172. package/dist/sql/extract-helpers.d.ts.map +1 -0
  173. package/dist/sql/extract-helpers.js +127 -0
  174. package/dist/sql/extract.d.ts +13 -0
  175. package/dist/sql/extract.d.ts.map +1 -0
  176. package/dist/sql/extract.js +323 -0
  177. package/dist/sql/facts.d.ts +34 -0
  178. package/dist/sql/facts.d.ts.map +1 -0
  179. package/dist/sql/facts.js +392 -0
  180. package/dist/sql/identifiers.d.ts +13 -0
  181. package/dist/sql/identifiers.d.ts.map +1 -0
  182. package/dist/sql/identifiers.js +83 -0
  183. package/dist/sql/normalize-deparse.d.ts +25 -0
  184. package/dist/sql/normalize-deparse.d.ts.map +1 -0
  185. package/dist/sql/normalize-deparse.js +96 -0
  186. package/dist/sql/object-hash.d.ts +5 -0
  187. package/dist/sql/object-hash.d.ts.map +1 -0
  188. package/dist/sql/object-hash.js +24 -0
  189. package/dist/sql/parser.d.ts +8 -0
  190. package/dist/sql/parser.d.ts.map +1 -0
  191. package/dist/sql/parser.js +89 -0
  192. package/dist/sql/privileges.d.ts +33 -0
  193. package/dist/sql/privileges.d.ts.map +1 -0
  194. package/dist/sql/privileges.js +379 -0
  195. package/dist/sql/split.d.ts +3 -0
  196. package/dist/sql/split.d.ts.map +1 -0
  197. package/dist/sql/split.js +182 -0
  198. package/dist/sql/statements.d.ts +17 -0
  199. package/dist/sql/statements.d.ts.map +1 -0
  200. package/dist/sql/statements.js +284 -0
  201. package/dist/sql/table-constraints.d.ts +15 -0
  202. package/dist/sql/table-constraints.d.ts.map +1 -0
  203. package/dist/sql/table-constraints.js +304 -0
  204. package/dist/sql/table-shape.d.ts +38 -0
  205. package/dist/sql/table-shape.d.ts.map +1 -0
  206. package/dist/sql/table-shape.js +287 -0
  207. package/dist/sync.d.ts +27 -0
  208. package/dist/sync.d.ts.map +1 -0
  209. package/dist/sync.js +86 -0
  210. package/dist/typegen-model.d.ts +78 -0
  211. package/dist/typegen-model.d.ts.map +1 -0
  212. package/dist/typegen-model.js +338 -0
  213. package/dist/typegen-views.d.ts +7 -0
  214. package/dist/typegen-views.d.ts.map +1 -0
  215. package/dist/typegen-views.js +92 -0
  216. package/dist/typegen-zod.d.ts +3 -0
  217. package/dist/typegen-zod.d.ts.map +1 -0
  218. package/dist/typegen-zod.js +149 -0
  219. package/dist/typegen.d.ts +4 -0
  220. package/dist/typegen.d.ts.map +1 -0
  221. package/dist/typegen.js +184 -0
  222. package/dist/validators.d.ts +3 -0
  223. package/dist/validators.d.ts.map +1 -0
  224. package/dist/validators.js +104 -0
  225. package/dist/verify-environment.d.ts +5 -0
  226. package/dist/verify-environment.d.ts.map +1 -0
  227. package/dist/verify-environment.js +92 -0
  228. package/dist/verify.d.ts +3 -0
  229. package/dist/verify.d.ts.map +1 -0
  230. package/dist/verify.js +261 -0
  231. package/docs/benchmarks/additive-correctness.svg +86 -0
  232. package/docs/benchmarks/additive-latency.svg +60 -0
  233. package/docs/benchmarks/functions-policies-correctness.svg +86 -0
  234. package/docs/benchmarks/functions-policies-latency.svg +60 -0
  235. package/docs/benchmarks/realistic-correctness.svg +86 -0
  236. package/docs/benchmarks/realistic-latency.svg +60 -0
  237. package/docs/benchmarks/scaling-latency.svg +106 -0
  238. package/docs/benchmarks/workflow-latency.svg +98 -0
  239. package/docs/benchmarks/xl-correctness.svg +86 -0
  240. package/docs/benchmarks/xl-latency.svg +60 -0
  241. package/docs/benchmarks/xxl-correctness.svg +86 -0
  242. package/docs/benchmarks/xxl-latency.svg +66 -0
  243. package/docs/case-study-anilize.md +51 -0
  244. package/docs/ci-gate.md +44 -0
  245. package/docs/ci.md +68 -0
  246. package/docs/commands.md +35 -0
  247. package/docs/config.md +72 -0
  248. package/docs/corpus.md +33 -0
  249. package/docs/diagnostics.md +77 -0
  250. package/docs/hints.md +92 -0
  251. package/docs/release.md +19 -0
  252. package/docs/support-matrix.md +57 -0
  253. package/examples/postgres/schemas/001_app.sql +17 -0
  254. package/examples/supabase/schemas/001_app.sql +13 -0
  255. package/examples/supabase/schemas-next/001_app.sql +21 -0
  256. package/examples/supabase/supaschema.config.json +15 -0
  257. package/package.json +99 -0
@@ -0,0 +1,35 @@
1
+ # Commercial License
2
+
3
+ supaschema is dual-licensed: **AGPL-3.0-only** (`LICENSE`) for open-source use, or a **commercial license** for proprietary and hosted use.
4
+
5
+ ## Which license applies to you
6
+
7
+ You can use supaschema for free under the AGPL-3.0 — including inside your company — as long as you comply with the AGPL. The AGPL's copyleft reaches across a network: if you modify supaschema and let others interact with the modified version over a network, you must offer them the corresponding source under the AGPL.
8
+
9
+ **You do not need a commercial license to:**
10
+
11
+ - Use supaschema as a dev dependency to generate and check your own migrations and types.
12
+ - Run it in your own CI against your own repositories.
13
+ - Modify it for internal use, where the modified version is not offered to third parties over a network.
14
+
15
+ **You need a commercial license to:**
16
+
17
+ - Embed supaschema (modified or not) in a proprietary product, CLI, or SDK you distribute without releasing your source under the AGPL.
18
+ - Offer supaschema's functionality as part of a **hosted or SaaS service** to third parties without releasing the service's corresponding source under the AGPL.
19
+ - Otherwise use supaschema in a way the AGPL-3.0 would require you to release source you do not wish to release.
20
+
21
+ If you are unsure whether your use is covered, ask — the goal is for ordinary adoption to stay free and frictionless.
22
+
23
+ ## What the commercial license grants
24
+
25
+ A commercial license grants the right to use, modify, and distribute supaschema **without** the AGPL's source-disclosure obligations, under terms set in a signed agreement. Scope (per-product, per-seat, per-org, OEM/redistribution) and support level are defined per engagement.
26
+
27
+ ## How to obtain one
28
+
29
+ Open an issue titled "Commercial license inquiry" on the [project repository](https://github.com/jmclaughlin724/supaschema/issues), or contact the copyright holder listed in `package.json`. Include:
30
+
31
+ - Your company and the product or service supaschema would be part of.
32
+ - Whether it is distributed software, a hosted service, or both.
33
+ - Approximate scale (repositories, seats, or end customers).
34
+
35
+ Pricing is quoted per engagement. No rights beyond the AGPL-3.0 are granted without a signed commercial agreement.
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # supaschema
2
+
3
+ [![CI](https://github.com/jmclaughlin724/supaschema/actions/workflows/ci.yml/badge.svg)](https://github.com/jmclaughlin724/supaschema/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/supaschema)](https://www.npmjs.com/package/supaschema) [![npm downloads](https://img.shields.io/npm/dm/supaschema)](https://www.npmjs.com/package/supaschema) [![node](https://img.shields.io/node/v/supaschema)](https://github.com/jmclaughlin724/supaschema/blob/main/package.json) [![license](https://img.shields.io/npm/l/supaschema)](https://github.com/jmclaughlin724/supaschema/blob/main/LICENSE) [![codecov](https://codecov.io/gh/jmclaughlin724/supaschema/branch/main/graph/badge.svg)](https://codecov.io/gh/jmclaughlin724/supaschema) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/jmclaughlin724/supaschema/badge)](https://scorecard.dev/viewer/?uri=github.com/jmclaughlin724/supaschema) [![install size](https://packagephobia.com/badge?p=supaschema)](https://packagephobia.com/result?p=supaschema)
4
+
5
+ **Declarative Postgres and Supabase migrations in milliseconds — no Docker, no shadow database, no ORM.** supaschema reads your SQL with PostgreSQL's own parser, shipped as WASM inside the package, so it diffs your schema, writes a replay-safe migration, and regenerates your TypeScript + Zod types in a single command — without standing up a database to do it.
6
+
7
+ - **Fast at any scale.** It parses instead of replaying: a full diff of an 8,300-object production schema runs in under two seconds, where the Supabase CLI's shadow-database engines take minutes. The gap widens from ~20× to ~70× as your schema grows.
8
+ - **Catches the tenant-isolation regression other tools ship.** An RLS policy's `USING` predicate _is_ the tenant boundary. Every Supabase CLI engine diffs policies by name and silently drops a tightened predicate; supaschema compares policy bodies structurally and catches it before it merges.
9
+ - **Replay-safe by construction.** Every statement is guarded (`IF NOT EXISTS`, catalog-checked `DO` blocks), so a crashed or retried deploy just re-runs the file — where the CLI's unguarded `CREATE TABLE` / `CREATE INDEX` fail on the second apply.
10
+
11
+ ```bash
12
+ npx supaschema diff # writes the migration AND refreshes database.types.ts + database.zod.ts
13
+ ```
14
+
15
+ ![Diff latency vs schema size — supaschema stays in seconds where shadow-database engines climb into minutes](docs/benchmarks/scaling-latency.svg)
16
+
17
+ The promise of declarative schema management sounds great: keep your schema in SQL files, edit them in your editor, diff against your database to produce a type-safe, idempotent migration, and get regenerated types back in your repo.
18
+
19
+ In practice, the existing tooling needs a running database at every step. Diff engines replay your schema into a throwaway Docker database just to read it, and type generators introspect only what your database has already applied — so an unapplied change sitting in your editor can't produce a migration or its types until the database catches up. That's a backlog, a time sink, and a constant drain on CPU.
20
+
21
+ supaschema knows every table, column, type, and enum without a database because it's built on PostgreSQL's own parser. The same system that would normally interpret your SQL inside a Docker container ships inside the package, embedded in your repo.
22
+
23
+ Migrations diff without an ORM, without Docker, without a shadow database, and without introspection — reading your live catalog directly and read-only, or your schema files and git refs with no database at all. Zod-validated types generate with no database whatsoever. Nothing is applied to your local or remote database. All within milliseconds.
24
+
25
+ Measured head-to-head against the Supabase CLI's diff engines on identical fixtures ([benchmarks](#benchmarks)):
26
+
27
+ | | supaschema | Supabase CLI engines (all five) |
28
+ | --- | --- | --- |
29
+ | Diff latency, 1 → 2,500 tables (~17,500 objects) | 0.2s → 3.2s | 3.6–4.6s → ~3.5 minutes |
30
+ | Generated SQL survives a second apply | every fixture | fails on column/index changes |
31
+ | Diff content vs intended change (F1) | 1.000 on every scored fixture | 0.982–0.999 (every engine missed the same policy change) |
32
+ | Infrastructure per diff | none — pure TypeScript + WASM | Docker shadow database |
33
+ | Ambiguous change (rename, drop, type change) | blocked until explicitly hinted | inferred or dropped silently |
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ npm install --save-dev supaschema
39
+ ```
40
+
41
+ Requires Node 22+ and PostgreSQL 15+. Run `npx supaschema init` to scaffold `supaschema.config.json`. In a Supabase project the database URL is discovered from `supabase/config.toml` automatically; anywhere else, set `SUPASCHEMA_DATABASE_URL` or define named `environments` in the config. If setup misbehaves, `npx supaschema doctor` prints a one-page diagnosis.
42
+
43
+ ## Quick Start
44
+
45
+ Edit your schema files (`supabase/schemas/` by default), then generate the migration:
46
+
47
+ ```bash
48
+ npx supaschema diff
49
+ ```
50
+
51
+ supaschema compares the schema you declared with the schema your database has, and writes the difference to `supabase/migrations/<timestamp>_<name>.sql`:
52
+
53
+ ```sql
54
+ -- Generated by supaschema 0.1.0.
55
+ -- Operations: 1 create constraint, 1 create table, 1 drop function, ...
56
+ -- supaschema: lineage from=a145201bcaa397f6... to=4db806bc9b786ea2...
57
+ SET lock_timeout = '5s';
58
+
59
+ DROP FUNCTION IF EXISTS "app"."legacy_ping"();
60
+
61
+ CREATE TABLE IF NOT EXISTS app.audit_events (
62
+ id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
63
+ account_id bigint NOT NULL
64
+ );
65
+
66
+ DO $supaschema$
67
+ BEGIN
68
+ IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_constraint c ...) THEN
69
+ ALTER TABLE app.audit_events ADD CONSTRAINT audit_events_pkey PRIMARY KEY (id);
70
+ END IF;
71
+ END
72
+ $supaschema$;
73
+ ```
74
+
75
+ Prove the migration, then apply it with your normal runner:
76
+
77
+ ```bash
78
+ npx supaschema check # static replay-safety and lock-hazard gate
79
+ npx supaschema verify # applies it twice in throwaway databases, compares catalogs
80
+ supabase db push # your runner applies it — supaschema never touches your real databases
81
+ ```
82
+
83
+ Types come from the same tree, in the same step. Run `npx supaschema types` once to create `database.types.ts` and `database.zod.ts` (runtime Zod validators); from then on every `diff` refreshes them with the migration. You never wait for a deploy to get correct types.
84
+
85
+ No flags are needed day to day: sources, output paths, and names all have sensible defaults, every applied default is printed, and every flag overrides ([commands](#commands)).
86
+
87
+ ## Features
88
+
89
+ - **Replay-safe by construction.** Every statement is guarded (`IF NOT EXISTS`, `DROP ... IF EXISTS`, catalog-checked `DO` blocks). A crashed deploy can simply run the file again.
90
+ - **Fails closed on ambiguity.** Drops, renames, and type changes stay blocked until you approve the exact object in `hints`. Nothing is inferred from name similarity.
91
+ - **Normalized output.** Migrations are written in one canonical SQL style, so formatting never shows up as a change. An object the deparser can't faithfully reproduce keeps your original spelling and says so with a warning.
92
+ - **Types and validators without a database.** `supaschema types` writes Supabase-compatible TypeScript types plus runtime Zod validators straight from your schema files, and `diff` refreshes both automatically with every migration. No introspection, no running database, no applying migrations first — the parser already knows your schema, so you never stop mid-workflow to deploy before types can regenerate.
93
+ - **No git ceremony.** Schema files are diffed straight from disk — no staging or committing before you can generate.
94
+ - **Everything stays in sync.** Your editor validates the config via JSON Schema and `--watch` re-diffs on save; named `environments` point commands at local, staging, or production; `migrations` reconciles files against each database's applied history; `--fail-on-diff` gates drift in CI.
95
+ - **Accuracy is measured, not assumed.** Diff output is scored against ground-truth change manifests ([benchmarks](#accuracy)).
96
+ - **AI agents are governed.** A bundled Claude Code + Codex rule, skill, and hook set blocks hand-edits to generated migrations ([AI agents](#ai-agents)).
97
+ - **Usable as a library.** Every CLI capability is an exported, typed function ([library](#library)).
98
+
99
+ ## Benchmarks
100
+
101
+ All numbers are reproducible from this repo (`npm run benchmark`; harness in [benchmarks/README.md](benchmarks/README.md)) and verified, not just timed: every generated migration is applied in one transaction, applied a second time, and the resulting catalog is compared against the target. Reference run 2026-06-12: Apple Silicon, Node 24, PostgreSQL 17.6, Supabase CLI 2.106.0.
102
+
103
+ ### Speed
104
+
105
+ **The diff alone** (charted at the top): supaschema's cost is parsing and planning, so it stays in seconds at every scale — **0.2s** at one table, **1.4s** at 1,000 tables, **3.2s** at 2,500 tables (~17,500 objects). Every shadow-database engine pays for a full schema replay into a fresh Docker database on every diff: **~4 seconds at a single table**, **~39 seconds** at 1,000 tables, and **~3.5 minutes** at 2,500 — a gap that widens from ~20x to ~70x as your schema grows.
106
+
107
+ **The full real-world step — migration plus regenerated types:**
108
+
109
+ ![Full workflow: migration + regenerated types](docs/benchmarks/workflow-latency.svg)
110
+
111
+ In the real workflow you don't stop at the diff: you need the migration _and_ your regenerated types. For supaschema that's still one command — `diff` writes the migration and refreshes `database.types.ts` and `database.zod.ts` in the same invocation — so the full step costs **0.23s** at one table and **5.2s** at 2,500. The engines can't regenerate types from unapplied SQL, so their full step is `db diff`, then apply the migration, then `supabase gen types`: **~6 seconds** at a single table, **~45 seconds** at 1,000 tables, **~3.7 minutes** at 2,500 — and what comes back is TypeScript only, with no runtime validators.
112
+
113
+ | Median, full workflow | 1 table | 50 tables | 1,000 tables | 2,500 tables |
114
+ | --- | --- | --- | --- | --- |
115
+ | supaschema (migration + TS types + Zod) | 0.24s | 0.26s | 2.25s | 5.22s |
116
+ | supabase engines (diff + apply + gen types) | 5.7–8.1s | 6.4–7.6s | 44.8–50.4s | 212.6–229.7s |
117
+
118
+ ![Realistic fixture latency](docs/benchmarks/realistic-latency.svg)
119
+
120
+ | Median diff time | 1 table | 50 tables | 1,000 tables | 2,500 tables |
121
+ | -------------------- | ------- | --------- | ------------ | ------------ |
122
+ | supaschema (files) | 0.19s | 0.19s | 1.38s | 3.23s |
123
+ | supaschema (live db) | 0.21s | 0.23s | 1.25s | 2.78s |
124
+ | supabase default | 3.86s | 4.11s | 39.12s | 208.48s |
125
+ | supabase migra | 3.81s | 4.43s | 38.74s | 204.54s |
126
+ | supabase pg-delta | 3.58s | 4.15s | 38.52s | 207.47s |
127
+ | supabase pg-schema | 4.61s | 3.98s | 38.57s | 205.59s |
128
+ | supabase pgadmin | 3.73s | 4.01s | 38.74s | 209.98s |
129
+
130
+ Per-fixture latency charts: [additive](docs/benchmarks/additive-latency.svg) · [functions-policies](docs/benchmarks/functions-policies-latency.svg) · [xl](docs/benchmarks/xl-latency.svg) · [xxl](docs/benchmarks/xxl-latency.svg).
131
+
132
+ ### Accuracy
133
+
134
+ Two independent measures back every run. **F1** scores the emitted statements against a ground-truth change manifest by object identity — recall is "did you touch every object that changed", precision penalizes operations beyond the intent and destructive drop+create of data-bearing objects. The **catalog fingerprint** is the objective oracle that needs no manifest: the generated migration is applied to a throwaway database and the resulting catalog is compared to the target. F1 says you named the right objects; the fingerprint says the SQL is actually correct.
135
+
136
+ Every fixture is scored — the generated `realistic`/`xl`/`xxl` set and the hand-authored `additive`/`functions-policies` fixtures all carry manifests. supaschema scores **F1 1.000** on each, in both file and live-database modes. Every Supabase engine scores **0.982–0.999, and the gap is always the same miss: each silently dropped an RLS policy change** — and on that fixture each engine's migration also fails the fingerprint check, so the miss is confirmed objectively, not just by the rubric.
137
+
138
+ That miss matters more than speed. A slow diff costs seconds and an unreplayable diff costs a deploy, but a silently dropped policy change ships a tenant-isolation hole that review, CI, and the migration runner all wave through. The same comparison run against a real ~8,300-object production Supabase tree is written up in the [anilize case study](docs/case-study-anilize.md).
139
+
140
+ ### Replay safety
141
+
142
+ ![XL fixture correctness](docs/benchmarks/xl-correctness.svg)
143
+
144
+ Every engine's migration applies once and reaches the target catalog. Only supaschema's also applies **twice**: the others emit unguarded `ADD COLUMN` and `CREATE INDEX`, which fail on re-run for every fixture containing column or index changes. Per-fixture charts: [additive](docs/benchmarks/additive-correctness.svg) · [functions-policies](docs/benchmarks/functions-policies-correctness.svg) · [realistic](docs/benchmarks/realistic-correctness.svg) · [xxl](docs/benchmarks/xxl-correctness.svg).
145
+
146
+ ## How It Works
147
+
148
+ supaschema reads SQL with PostgreSQL's own parser, shipped inside the package. The part of Postgres that understands SQL runs in the library itself — that's why no Docker and no shadow database are needed, and why the engine never touches SQL with regex.
149
+
150
+ 1. **Parse** — every statement, from your files or a live database, becomes a parse tree.
151
+ 2. **Compare** — two definitions are equal when their parse trees are equal. Formatting, keyword case, and type spellings can never show up as fake changes.
152
+ 3. **Plan** — changes are ordered by dependency. Anything destructive waits for your explicit approval in `hints`.
153
+ 4. **Render** — the migration is written as guarded SQL that is safe to run twice, stamped with a lineage marker that chains it to the next one.
154
+ 5. **Gate** — existing migrations are never overwritten, and a migration that doesn't continue the chain is refused.
155
+ 6. **Verify** — the migration runs twice in throwaway databases, applied exactly the way `supabase db push` applies it, and the result must match your schema files.
156
+
157
+ ## Commands
158
+
159
+ | Command | Purpose |
160
+ | --- | --- |
161
+ | `diff` | Generate the migration (`--fail-on-diff` CI drift gate, `--watch` editor loop, `--out stdout`, `--write-hints`) |
162
+ | `check` | Static replay-safety gate for the migrations directory or named files — including other tools' migrations (`--reporter github\|sarif\|json`) |
163
+ | `verify` | Apply-twice proof for the newest pending migration in disposable databases |
164
+ | `types` | Generate Supabase-compatible TypeScript types and Zod validators from the schema tree — no database needed (`--out stdout` to print) |
165
+ | `plan` / `inspect` / `fingerprint` | JSON diff plan · extracted schema model · one-line schema-equality hash |
166
+ | `migrations` | Files on disk vs a database's applied history: applied, pending, ghosts, out-of-order |
167
+ | `sync` | Gate pending migrations, then optionally drive the Supabase CLI (`--local` / `--remote`) |
168
+ | `audit` | Coverage report for adopting an existing schema |
169
+ | `corpus` / `selfcheck` | The engine's own correctness oracles ([docs/corpus.md](docs/corpus.md)) |
170
+ | `doctor` / `init` / `completion` / `explain` | Setup diagnosis · config scaffold · shell completions · offline diagnostic decoder |
171
+
172
+ Defaults: `--from` is the database (falling back to `git:HEAD`), `--to` is the schema tree, and output lands in the migrations directory — paths set by `schemaPaths` and `migrationsDir` in the config. Full flags: [docs/commands.md](docs/commands.md). Exit codes: `0` ok · `1` runtime error · `2` diagnostic errors · `3` drift found.
173
+
174
+ ## Sources
175
+
176
+ Either side of a diff can be any of these — generating a diff never creates a database or runs Docker:
177
+
178
+ | Source | Reads |
179
+ | --- | --- |
180
+ | `dir:supabase/schemas` | SQL files exactly as they sit on disk |
181
+ | `git:HEAD` (any ref) | the schema tree at a committed ref |
182
+ | `database:$DATABASE_URL` | a live catalog, read-only |
183
+ | `dump:path.sql` / `dump:-` | one SQL file, or stdin |
184
+ | `catalog:path.json` | a saved `inspect` snapshot, for air-gapped or point-in-time comparison |
185
+
186
+ ## Safety
187
+
188
+ - Destructive changes require the exact object key in `hints.destructive`; column changes then render as per-column `ALTER`s instead of dropping and recreating the table, and type changes that would rewrite the whole table are flagged (`SUPA_CHECK_ALTER_COLUMN_TYPE_REWRITE`).
189
+ - Renames come only from `hints.renames`. `CASCADE` is never emitted. Idempotency is required, not optional.
190
+ - Data statements (`INSERT`/`UPDATE`/`DELETE`/`DO`) are never treated as schema changes, and unsupported DDL produces a blocking diagnostic instead of passing through.
191
+ - Diagnostics redact credentials (URL passwords, JWTs, secrets).
192
+ - Recovery steps for blocked plans: [docs/hints.md](docs/hints.md) · config reference: [docs/config.md](docs/config.md).
193
+
194
+ ## AI Agents
195
+
196
+ The package ships a governance bundle so coding agents generate migrations through the diff instead of hand-writing SQL:
197
+
198
+ - `AGENTS.md` — operator brief: invariants, commands, recovery codes.
199
+ - `.claude/rules/` and `.codex/rules/` — the migration policy for Claude Code and Codex.
200
+ - `.claude/skills/supaschema/` — the step-by-step workflow skill, with recovery steps for every blocking `SUPA_*` code.
201
+ - `.claude/hooks/` and `.codex/hooks/` — wired PreToolUse hooks that **block any edit to a generated migration** (identified by its lineage marker) in both runtimes.
202
+
203
+ Copy the surfaces into your repo root to get the write-time enforcement — the hooks only run where `.claude/settings.json` and `.codex/hooks.json` are wired. Pointing agents at `node_modules/supaschema/` gives them the guidance without the hooks. `supaschema explain <SUPA_CODE>` decodes every diagnostic offline.
204
+
205
+ ## Library
206
+
207
+ Every CLI capability is an exported, typed function:
208
+
209
+ ```ts
210
+ import {
211
+ extractSourceModel, // any source prefix -> SchemaModel
212
+ extractCatalogModel, // live pg_catalog -> SchemaModel
213
+ planSchemaDiff, // two models -> MigrationPlan
214
+ renderMigrationSplit, // plan -> { sql, concurrentSql? }
215
+ checkMigrationSql, // SQL -> replay-safety diagnostics
216
+ verifyMigration, // apply-twice + reconvergence proof
217
+ migrationsStatus, // disk files vs applied history
218
+ syncMigrations, // gate + orchestrate the migration runner
219
+ runCorpus, // the corpus oracle
220
+ resolveDatabaseUrl,
221
+ parseLineage,
222
+ loadConfig,
223
+ } from "supaschema";
224
+ ```
225
+
226
+ ## Scope and Stability
227
+
228
+ Modeled: schemas, extensions, types/enums/domains, tables, constraints, indexes, sequences, functions/procedures, views/materialized views, triggers, RLS, policies, grants/default privileges, foreign data wrappers, comments. Deliberate non-goals (partitioning, publications, event triggers, collations) fail closed with diagnostics ([support matrix](docs/support-matrix.md)). Pre-1.0: pin an exact version in CI. Worked Supabase and plain-PostgreSQL setups live in `examples/`.
229
+
230
+ ## Documentation
231
+
232
+ [commands](docs/commands.md) · [config](docs/config.md) · [hints & recovery](docs/hints.md) · [CI recipes](docs/ci.md) · [CI gate & paid tier](docs/ci-gate.md) · [diagnostics](docs/diagnostics.md) · [support matrix](docs/support-matrix.md) · [corpus oracle](docs/corpus.md) · [case study](docs/case-study-anilize.md) · [benchmark harness](benchmarks/README.md)
233
+
234
+ ## Development
235
+
236
+ ```bash
237
+ npm run check # lint + typecheck + tests + build
238
+ npm run fixture:verify # render a fixture migration, apply twice, compare catalogs
239
+ npm run corpus:check # replay a dirty-real corpus and require reconvergence to zero
240
+ npm run benchmark # threshold-enforced benchmarks
241
+ ```
242
+
243
+ ## Contributing and License
244
+
245
+ Bug reports and feature requests: [GitHub issues](https://github.com/jmclaughlin724/supaschema/issues). Run `npm run check` before opening a pull request.
246
+
247
+ supaschema is an independent open-source project and is not affiliated with or endorsed by Supabase.
248
+
249
+ Dual-licensed: **AGPL-3.0-only** ([LICENSE](LICENSE)) for open-source use, **commercial** ([LICENSE-COMMERCIAL.md](LICENSE-COMMERCIAL.md)) for embedding in proprietary products or hosted services.
@@ -0,0 +1,104 @@
1
+ # Comparison Benchmarks
2
+
3
+ This harness compares `supaschema` against Supabase CLI schema diff engines. It writes machine-readable JSON and SVG charts.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ SUPASCHEMA_COMPARE_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres \
9
+ npm run bench:compare
10
+
11
+ npm run bench:plot
12
+ ```
13
+
14
+ `bench:plot` accepts any number of result JSON files plus an optional output directory and emits one artifact set per fixture (sample size), so each scale is saved and read separately:
15
+
16
+ ```bash
17
+ node benchmarks/plot.js benchmarks/results/comparison.json benchmarks/results/comparison-xl.json
18
+ ```
19
+
20
+ Outputs:
21
+
22
+ - `benchmarks/results/comparison.json` — raw rows from `bench:compare` (use `SUPASCHEMA_COMPARE_OUT` to direct separate runs to separate files, e.g. `comparison-xl.json`)
23
+ - per fixture: `<fixture>-latency.svg`, `<fixture>-correctness.svg`, and `<fixture>-results.json` (that fixture's rows plus source-run metadata)
24
+ - `summary.md` — per-fixture result tables plus a cross-fixture scaling table (each tool's median and its ratio vs its own smallest fixture)
25
+
26
+ ## Tool Selection
27
+
28
+ By default the harness attempts `supaschema` plus every Supabase `db diff` engine available in the installed CLI. Limit the run with comma-separated filters:
29
+
30
+ ```bash
31
+ SUPASCHEMA_COMPARE_TOOLS=supaschema-file,supaschema-db,supabase-pg-delta \
32
+ SUPASCHEMA_COMPARE_FIXTURES=additive \
33
+ SUPASCHEMA_COMPARE_ITERATIONS=10 \
34
+ SUPASCHEMA_COMPARE_TIMEOUT_MS=30000 \
35
+ SUPASCHEMA_COMPARE_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres \
36
+ npm run bench:compare
37
+ ```
38
+
39
+ The harness refuses non-local database hosts by default because it creates and drops temporary databases. Set `SUPASCHEMA_COMPARE_ALLOW_REMOTE=1` only for disposable remote benchmark clusters. Use `SUPASCHEMA_COMPARE_PORT_BASE` to move the per-run Supabase temp project ports away from local conflicts.
40
+
41
+ Real-project fixtures: `node benchmarks/tools/build-project-fixture.mjs --tree <schemas dir> --out <fixture dir>` turns an actual declarative tree into a fixture (fixpoint statement ordering, Supabase auth stubs, support-surface filtering with a `dropped.log`, and a `fixture.json` carrying the schema list + supaschema adapter). Point the harness at it with `SUPASCHEMA_COMPARE_FIXTURE_DIRS=<fixture dir>` so private schemas never enter the package tree.
42
+
43
+ When the compare admin URL is `supabase_admin` (required to install most extensions on a local Supabase stack), also set `SUPASCHEMA_COMPARE_SEED_ROLE=postgres`: after seeding, the harness transfers ownership of every user-schema object to that role. This works around two Supabase CLI behaviors found while benchmarking real schemas (CLI 2.105, verified by direct bisection 2026-06-11):
44
+
45
+ 1. **Silent empty diffs for supabase_admin-owned objects.** `supabase db diff --from <db1> --to <db2>` returns an empty diff with exit 0 — no warning — when the differing objects are owned by `supabase_admin`, regardless of the connecting user. The same databases diff correctly after `ALTER ... OWNER TO postgres`. A diff tool reporting "no changes" against databases that differ by a whole table is the failure mode supaschema's `SUPA_PLAN_EMPTY_WITH_DRIFT` invariant exists to make impossible.
46
+ 2. **`&sslmode=` URL corruption.** The CLI appends `&sslmode=...` to a bare `--to` URL with no query string, corrupting the database name (`FATAL: database "...&sslmode=" does not exist`) — visible only under `--debug`. Work around it by always passing URLs with an existing query string (e.g. `?sslmode=disable`).
47
+
48
+ The default is 10 measured iterations plus 1 warmup per adapter/fixture pair. A full default run across all adapters and fixtures takes tens of minutes because Supabase engines and realistic-fixture seeding dominate; use the filters above for quick local passes.
49
+
50
+ Fixtures: the on-disk pairs under `benchmarks/fixtures/` (`additive`, `functions-policies`) plus a generated `realistic` fixture — a deterministic 50-table Supabase-shaped schema (FKs, RLS policies, triggers, views, materialized views, functions, grants, comments) with a mixed change set, materialized at run time from the shared `dist/benchmark-fixtures.js` generator. Set `SUPASCHEMA_COMPARE_XL_TABLES` (for example `1000` for ~7000 objects) to add an `xl` fixture at that table count; it is opt-in because shadow-database engines take minutes per run at that scale — raise `SUPASCHEMA_COMPARE_TIMEOUT_MS` accordingly.
51
+
52
+ Adapters:
53
+
54
+ - `supaschema-file` — `diff` between two SQL dumps (diff only)
55
+ - `supaschema-db` — `diff` between two live catalogs (diff only)
56
+ - `supabase-default` / `supabase-migra` / `supabase-pg-delta` / `supabase-pg-schema` / `supabase-pgadmin` — `supabase db diff` per engine (diff only)
57
+ - `supaschema-workflow` — the full real-world step measured as one command: `diff` writes the migration and refreshes seeded `database.types.ts` + `database.zod.ts` (TypeScript types **and** runtime Zod validators) in the same invocation
58
+ - `supabase-*-workflow` — the same real-world step for each Supabase engine: `db diff`, apply the generated migration to the database (types cannot regenerate from unapplied SQL), then `supabase gen types --lang=typescript --db-url` (TypeScript only; no validators)
59
+
60
+ Both workflow lanes are spawned through the same `tools/run-workflow.mjs` wrapper, so per-process overhead is identical on both sides. Workflow rows are charted separately (`workflow-latency.svg`) and excluded from the diff-only charts; never average the two lanes together. The captured output of a workflow run is still the generated migration, so verification and accuracy scoring apply to workflow rows unchanged.
61
+
62
+ ## Reading Results
63
+
64
+ Use latency charts for speed and correctness charts for whether generated output:
65
+
66
+ - applied once,
67
+ - applied twice,
68
+ - matched the target catalog fingerprint after the first and second apply.
69
+
70
+ `matchesTargetAfterFirstApply` and `matchesTargetAfterSecondApply` are recorded separately in the JSON. `matchesTargetFingerprint` is true only when first apply matches, second apply succeeds, and second apply still matches. Command failures, skips, and unsupported adapters remain visible in the correctness chart.
71
+
72
+ Timing fields: `elapsedMs` is the final command attempt only and is what the latency chart plots; `totalElapsedMs` additionally includes retry sleeps and failed environmental attempts (for example Supabase shadow-port conflicts), with `attempts` recording how many runs happened.
73
+
74
+ Accuracy fields (generated fixtures only, which carry a ground-truth change manifest): `outputRecall`, `outputPrecision`, and `outputF1` score the generated SQL's content against the manifest, with `outputMissedSample`/`outputExcessSample` naming up to 8 missed or spurious object keys. Every emitted statement is classified through the PostgreSQL parser (supaschema guard `DO` blocks are unwrapped and classified too). Precision penalizes operations beyond the manifest and destructive drop+create of data-bearing objects (tables, materialized views, sequences, schemas, types/domains); drop+create of recreateable metadata (policies, triggers, views, indexes, functions) is the standard PostgreSQL change lane and is not penalized. `summary.md` reports the per-tool mean as the Output F1 column.
75
+
76
+ Verification applies the fixture `from` state per statement, then applies the generated migration in one transaction per apply — mirroring runners like `supabase db push` — so transactional failures are not masked by autocommit.
77
+
78
+ Do not average all modes together. Source-file diff, live-catalog diff, and replay verification measure different work.
79
+
80
+ ## Measured Results (as of 2026-06-12)
81
+
82
+ Single sequential reference run, 2026-06-12 (`BENCH_ALL_SEQUENTIAL=1 bash benchmarks/tools/bench-all.sh`), with diff-output accuracy scoring active: Apple Silicon (darwin arm64), Node 24, PostgreSQL 17.6 (Supabase local), Supabase CLI 2.106.0, supaschema 0.1.0 with deparse normalization and type generation in the default diff path. Fixture scale: `additive`/`functions-policies` ≈ 1 table; `realistic` = 50 tables (~350 objects); `xl` = 1,000 tables (~7,000 objects); `xxl` = 2,500 tables (~17,500 objects). 3 iterations per cell, single-iteration at `xxl`. Regenerate with `benchmarks/tools/bench-all.sh` (parallel by default; `BENCH_ALL_SEQUENTIAL=1` for publication-grade latency medians free of cross-fixture CPU contention); per-fixture rows live in `<fixture>-results.json` and the generated `summary.md`. All durations are seconds.
83
+
84
+ | Fixture | supaschema (file / live-db) | Supabase engines | Replay-safe (applies twice) | Output F1 (supaschema / engines) |
85
+ | --- | --- | --- | --- | --- |
86
+ | `additive` | 0.19s / 0.21s | 3.6–4.6s | supaschema only | — (no manifest) |
87
+ | `functions-policies` | 0.18s / 0.21s | 3.3–3.7s | all tools | — (no manifest) |
88
+ | `realistic` | 0.19s / 0.23s | 4.0–4.4s | supaschema only | 1.000 / 0.982 |
89
+ | `xl` | 1.38s / 1.25s | 38.5–39.1s | supaschema only | 1.000 / 0.999 |
90
+ | `xxl` | 3.2s / 2.8s | 204.5–210.0s | supaschema only | 1.000 / 0.999 |
91
+
92
+ The full-workflow lanes (same run) measure the real-world step of producing the migration **and** regenerated types:
93
+
94
+ | Fixture | `supaschema-workflow` (migration + TS types + Zod) | `supabase-*-workflow` (db diff + apply + gen types) |
95
+ | --- | --- | --- |
96
+ | `additive` | 0.24s | 5.8–8.1s |
97
+ | `functions-policies` | 0.23s | 5.7–6.2s |
98
+ | `realistic` | 0.26s | 6.4–7.6s |
99
+ | `xl` | 2.25s | 44.8–50.4s |
100
+ | `xxl` | 5.22s | 212.6–229.7s |
101
+
102
+ Regression with schema size: supaschema stays near three seconds through 2,500 tables because its cost is parse/plan/catalog-read bound. All five Supabase shadow-database engines cluster near ~3.5–4.5s even at one table and grow with schema replay cost to ~39s at 1,000 tables and ~3.4–3.5 minutes at 2,500 — the gap grows from ~20× to ~70×. Verification outcomes are scale-independent: Supabase engines emit unguarded DDL, so apply-twice fails on every fixture whose diff contains `ADD COLUMN`/`CREATE INDEX` statements (`additive`, `realistic`, `xl`, `xxl`); `functions-policies` passes everywhere because its diff is entirely `CREATE OR REPLACE`. Accuracy outcomes: supaschema scores F1 1.000 in both modes on every manifest-scored fixture; every Supabase engine misses the policy-predicate change (recall loss → 0.982 at 50 tables, 0.999 at 1,000/2,500 where the larger manifest dilutes the same miss).
103
+
104
+ The internal threshold suite (`npm run benchmark`) enforces these as regression gates in CI on PostgreSQL 15/16/17 — every lane fails the build past its `SUPASCHEMA_*_MS` threshold. Reference warm-max values from the same machine: `realisticTreeDiff` 15ms, `noDriftDiff` 15ms, `largeInMemoryDiff` 74ms (250 tables), `liveCatalogDiff` 39ms, `liveCatalogDiffXl` 426ms, `endToEndMigration` 11ms, `endToEndMigrationLarge` 185ms, `endToEndMigrationXl` 692ms, `shadowRoundTripDiff` 1019ms, `replayVerification` 132ms.