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.
- package/.agents/skills/supaschema/SKILL.md +61 -0
- package/.claude/hooks/block-generated-migration-edits.mjs +32 -0
- package/.claude/rules/supaschema.md +22 -0
- package/.claude/settings.json +16 -0
- package/.claude/skills/supaschema/SKILL.md +61 -0
- package/.codex/hooks/supaschema-tool-gate.mjs +73 -0
- package/.codex/hooks.json +16 -0
- package/.codex/rules/supaschema.rules +22 -0
- package/AGENTS.md +40 -0
- package/LICENSE +661 -0
- package/LICENSE-COMMERCIAL.md +35 -0
- package/README.md +249 -0
- package/benchmarks/README.md +104 -0
- package/benchmarks/compare.js +489 -0
- package/benchmarks/fixtures/additive/from.sql +8 -0
- package/benchmarks/fixtures/additive/manifest.json +1 -0
- package/benchmarks/fixtures/additive/to.sql +9 -0
- package/benchmarks/fixtures/functions-policies/from.sql +24 -0
- package/benchmarks/fixtures/functions-policies/manifest.json +5 -0
- package/benchmarks/fixtures/functions-policies/to.sql +24 -0
- package/benchmarks/plot-lib.js +234 -0
- package/benchmarks/plot-svg.js +339 -0
- package/benchmarks/plot.js +154 -0
- package/benchmarks/tools/bench-all.sh +49 -0
- package/benchmarks/tools/build-project-fixture.mjs +245 -0
- package/benchmarks/tools/compare-db.mjs +101 -0
- package/benchmarks/tools/compare-fixtures.mjs +84 -0
- package/benchmarks/tools/compare-report.mjs +90 -0
- package/benchmarks/tools/compare-supabase.mjs +67 -0
- package/benchmarks/tools/registry.js +266 -0
- package/benchmarks/tools/run-workflow.mjs +77 -0
- package/bin/postinstall.mjs +26 -0
- package/bin/supaschema +2 -0
- package/config-schema.json +208 -0
- package/corpus/supabase-style/corpus.json +6 -0
- package/corpus/supabase-style/migrations/20260101000000_init.sql +28 -0
- package/corpus/supabase-style/migrations/20260102000000_noise.sql +13 -0
- package/corpus/supabase-style/migrations/20260103000000_churn.sql +4 -0
- package/corpus/supabase-style/migrations/20260104000000_triggers.sql +17 -0
- package/corpus/supabase-style/roles.sql +13 -0
- package/corpus/supabase-style/tree/functions.sql +26 -0
- package/corpus/supabase-style/tree/policies.sql +4 -0
- package/corpus/supabase-style/tree/schema.sql +4 -0
- package/corpus/supabase-style/tree/tables.sql +20 -0
- package/corpus/supabase-style/tree/triggers.sql +3 -0
- package/corpus/supabase-style/tree/types.sql +2 -0
- package/corpus/supabase-style/tree/views.sql +6 -0
- package/dist/audit.d.ts +20 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +68 -0
- package/dist/benchmark-db.d.ts +5 -0
- package/dist/benchmark-db.d.ts.map +1 -0
- package/dist/benchmark-db.js +71 -0
- package/dist/benchmark-fixtures.d.ts +10 -0
- package/dist/benchmark-fixtures.d.ts.map +1 -0
- package/dist/benchmark-fixtures.js +201 -0
- package/dist/benchmark.d.ts +2 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +308 -0
- package/dist/catalog-comments.d.ts +9 -0
- package/dist/catalog-comments.d.ts.map +1 -0
- package/dist/catalog-comments.js +194 -0
- package/dist/catalog-extras.d.ts +12 -0
- package/dist/catalog-extras.d.ts.map +1 -0
- package/dist/catalog-extras.js +408 -0
- package/dist/catalog-foreign.d.ts +15 -0
- package/dist/catalog-foreign.d.ts.map +1 -0
- package/dist/catalog-foreign.js +114 -0
- package/dist/catalog-tables.d.ts +9 -0
- package/dist/catalog-tables.d.ts.map +1 -0
- package/dist/catalog-tables.js +114 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +351 -0
- package/dist/check-hazards.d.ts +7 -0
- package/dist/check-hazards.d.ts.map +1 -0
- package/dist/check-hazards.js +83 -0
- package/dist/check-reporters.d.ts +8 -0
- package/dist/check-reporters.d.ts.map +1 -0
- package/dist/check-reporters.js +76 -0
- package/dist/check.d.ts +3 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +229 -0
- package/dist/cli-defaults.d.ts +24 -0
- package/dist/cli-defaults.d.ts.map +1 -0
- package/dist/cli-defaults.js +65 -0
- package/dist/cli-diff.d.ts +13 -0
- package/dist/cli-diff.d.ts.map +1 -0
- package/dist/cli-diff.js +348 -0
- package/dist/cli-reports.d.ts +9 -0
- package/dist/cli-reports.d.ts.map +1 -0
- package/dist/cli-reports.js +90 -0
- package/dist/cli-tools.d.ts +17 -0
- package/dist/cli-tools.d.ts.map +1 -0
- package/dist/cli-tools.js +136 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +239 -0
- package/dist/config-schema-gen.d.ts +2 -0
- package/dist/config-schema-gen.d.ts.map +1 -0
- package/dist/config-schema-gen.js +11 -0
- package/dist/config.d.ts +58 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +132 -0
- package/dist/core.d.ts +115 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1 -0
- package/dist/corpus.d.ts +26 -0
- package/dist/corpus.d.ts.map +1 -0
- package/dist/corpus.js +112 -0
- package/dist/database-url.d.ts +8 -0
- package/dist/database-url.d.ts.map +1 -0
- package/dist/database-url.js +74 -0
- package/dist/db-admin.d.ts +23 -0
- package/dist/db-admin.d.ts.map +1 -0
- package/dist/db-admin.js +147 -0
- package/dist/diagnostics.d.ts +16 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +155 -0
- package/dist/diff-score.d.ts +12 -0
- package/dist/diff-score.d.ts.map +1 -0
- package/dist/diff-score.js +339 -0
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +110 -0
- package/dist/hash.d.ts +7 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +34 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/lineage.d.ts +23 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +61 -0
- package/dist/migrations-status.d.ts +35 -0
- package/dist/migrations-status.d.ts.map +1 -0
- package/dist/migrations-status.js +131 -0
- package/dist/plan-order.d.ts +4 -0
- package/dist/plan-order.d.ts.map +1 -0
- package/dist/plan-order.js +178 -0
- package/dist/planner-replace.d.ts +4 -0
- package/dist/planner-replace.d.ts.map +1 -0
- package/dist/planner-replace.js +76 -0
- package/dist/planner-table.d.ts +3 -0
- package/dist/planner-table.d.ts.map +1 -0
- package/dist/planner-table.js +165 -0
- package/dist/planner.d.ts +5 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +385 -0
- package/dist/render-guards.d.ts +12 -0
- package/dist/render-guards.d.ts.map +1 -0
- package/dist/render-guards.js +159 -0
- package/dist/render.d.ts +7 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +325 -0
- package/dist/selfcheck.d.ts +11 -0
- package/dist/selfcheck.d.ts.map +1 -0
- package/dist/selfcheck.js +43 -0
- package/dist/source-normalize.d.ts +14 -0
- package/dist/source-normalize.d.ts.map +1 -0
- package/dist/source-normalize.js +420 -0
- package/dist/source.d.ts +4 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +233 -0
- package/dist/sql/ast.d.ts +42 -0
- package/dist/sql/ast.d.ts.map +1 -0
- package/dist/sql/ast.js +241 -0
- package/dist/sql/canonical-nodes.d.ts +5 -0
- package/dist/sql/canonical-nodes.d.ts.map +1 -0
- package/dist/sql/canonical-nodes.js +101 -0
- package/dist/sql/extract-helpers.d.ts +18 -0
- package/dist/sql/extract-helpers.d.ts.map +1 -0
- package/dist/sql/extract-helpers.js +127 -0
- package/dist/sql/extract.d.ts +13 -0
- package/dist/sql/extract.d.ts.map +1 -0
- package/dist/sql/extract.js +323 -0
- package/dist/sql/facts.d.ts +34 -0
- package/dist/sql/facts.d.ts.map +1 -0
- package/dist/sql/facts.js +392 -0
- package/dist/sql/identifiers.d.ts +13 -0
- package/dist/sql/identifiers.d.ts.map +1 -0
- package/dist/sql/identifiers.js +83 -0
- package/dist/sql/normalize-deparse.d.ts +25 -0
- package/dist/sql/normalize-deparse.d.ts.map +1 -0
- package/dist/sql/normalize-deparse.js +96 -0
- package/dist/sql/object-hash.d.ts +5 -0
- package/dist/sql/object-hash.d.ts.map +1 -0
- package/dist/sql/object-hash.js +24 -0
- package/dist/sql/parser.d.ts +8 -0
- package/dist/sql/parser.d.ts.map +1 -0
- package/dist/sql/parser.js +89 -0
- package/dist/sql/privileges.d.ts +33 -0
- package/dist/sql/privileges.d.ts.map +1 -0
- package/dist/sql/privileges.js +379 -0
- package/dist/sql/split.d.ts +3 -0
- package/dist/sql/split.d.ts.map +1 -0
- package/dist/sql/split.js +182 -0
- package/dist/sql/statements.d.ts +17 -0
- package/dist/sql/statements.d.ts.map +1 -0
- package/dist/sql/statements.js +284 -0
- package/dist/sql/table-constraints.d.ts +15 -0
- package/dist/sql/table-constraints.d.ts.map +1 -0
- package/dist/sql/table-constraints.js +304 -0
- package/dist/sql/table-shape.d.ts +38 -0
- package/dist/sql/table-shape.d.ts.map +1 -0
- package/dist/sql/table-shape.js +287 -0
- package/dist/sync.d.ts +27 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +86 -0
- package/dist/typegen-model.d.ts +78 -0
- package/dist/typegen-model.d.ts.map +1 -0
- package/dist/typegen-model.js +338 -0
- package/dist/typegen-views.d.ts +7 -0
- package/dist/typegen-views.d.ts.map +1 -0
- package/dist/typegen-views.js +92 -0
- package/dist/typegen-zod.d.ts +3 -0
- package/dist/typegen-zod.d.ts.map +1 -0
- package/dist/typegen-zod.js +149 -0
- package/dist/typegen.d.ts +4 -0
- package/dist/typegen.d.ts.map +1 -0
- package/dist/typegen.js +184 -0
- package/dist/validators.d.ts +3 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +104 -0
- package/dist/verify-environment.d.ts +5 -0
- package/dist/verify-environment.d.ts.map +1 -0
- package/dist/verify-environment.js +92 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +261 -0
- package/docs/benchmarks/additive-correctness.svg +86 -0
- package/docs/benchmarks/additive-latency.svg +60 -0
- package/docs/benchmarks/functions-policies-correctness.svg +86 -0
- package/docs/benchmarks/functions-policies-latency.svg +60 -0
- package/docs/benchmarks/realistic-correctness.svg +86 -0
- package/docs/benchmarks/realistic-latency.svg +60 -0
- package/docs/benchmarks/scaling-latency.svg +106 -0
- package/docs/benchmarks/workflow-latency.svg +98 -0
- package/docs/benchmarks/xl-correctness.svg +86 -0
- package/docs/benchmarks/xl-latency.svg +60 -0
- package/docs/benchmarks/xxl-correctness.svg +86 -0
- package/docs/benchmarks/xxl-latency.svg +66 -0
- package/docs/case-study-anilize.md +51 -0
- package/docs/ci-gate.md +44 -0
- package/docs/ci.md +68 -0
- package/docs/commands.md +35 -0
- package/docs/config.md +72 -0
- package/docs/corpus.md +33 -0
- package/docs/diagnostics.md +77 -0
- package/docs/hints.md +92 -0
- package/docs/release.md +19 -0
- package/docs/support-matrix.md +57 -0
- package/examples/postgres/schemas/001_app.sql +17 -0
- package/examples/supabase/schemas/001_app.sql +13 -0
- package/examples/supabase/schemas-next/001_app.sql +21 -0
- package/examples/supabase/supaschema.config.json +15 -0
- package/package.json +99 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="454" viewBox="0 0 1200 454" role="img">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="supaGradient" x1="0" y1="0" x2="1" y2="0">
|
|
4
|
+
<stop offset="0%" stop-color="#059669" />
|
|
5
|
+
<stop offset="100%" stop-color="#34d399" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="slateGradient" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
+
<stop offset="0%" stop-color="#3b4757" />
|
|
9
|
+
<stop offset="100%" stop-color="#526079" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect width="100%" height="100%" rx="14" fill="#0b1220" />
|
|
13
|
+
<text x="36.0" y="46.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="21" font-weight="700">Verification & accuracy — xl fixture (1,000 tables)</text>
|
|
14
|
+
<text x="36.0" y="70.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12.5" font-weight="400">Each generated migration is applied in one transaction, applied again, and the catalog is fingerprinted against the target.</text>
|
|
15
|
+
<text x="36.0" y="88.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12.5" font-weight="400">Output F1 scores generated SQL content against the fixture's ground-truth change manifest (1.000 = exact).</text>
|
|
16
|
+
<text x="490.0" y="116.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">applies once</text>
|
|
17
|
+
<text x="670.0" y="116.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">applies twice</text>
|
|
18
|
+
<text x="850.0" y="116.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">matches target</text>
|
|
19
|
+
<text x="1045.0" y="116.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">output F1</text>
|
|
20
|
+
<circle cx="41" cy="143" r="4" fill="#34d399" />
|
|
21
|
+
<text x="54.0" y="147.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="650">supaschema-db</text>
|
|
22
|
+
<rect x="430" y="132" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
23
|
+
<text x="490.0" y="147.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
24
|
+
<rect x="610" y="132" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
25
|
+
<text x="670.0" y="147.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
26
|
+
<rect x="790" y="132" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
27
|
+
<text x="850.0" y="147.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
28
|
+
<rect x="985" y="132" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
29
|
+
<text x="1045.0" y="147.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">1.000</text>
|
|
30
|
+
<circle cx="41" cy="181" r="4" fill="#34d399" />
|
|
31
|
+
<text x="54.0" y="185.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="650">supaschema-file</text>
|
|
32
|
+
<rect x="430" y="170" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
33
|
+
<text x="490.0" y="185.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
34
|
+
<rect x="610" y="170" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
35
|
+
<text x="670.0" y="185.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
36
|
+
<rect x="790" y="170" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
37
|
+
<text x="850.0" y="185.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
38
|
+
<rect x="985" y="170" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
39
|
+
<text x="1045.0" y="185.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">1.000</text>
|
|
40
|
+
<text x="54.0" y="223.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-default</text>
|
|
41
|
+
<rect x="430" y="208" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
42
|
+
<text x="490.0" y="223.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
43
|
+
<rect x="610" y="208" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
44
|
+
<text x="670.0" y="223.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/3</text>
|
|
45
|
+
<rect x="790" y="208" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
46
|
+
<text x="850.0" y="223.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
47
|
+
<rect x="985" y="208" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
48
|
+
<text x="1045.0" y="223.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
49
|
+
<text x="54.0" y="261.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-migra</text>
|
|
50
|
+
<rect x="430" y="246" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
51
|
+
<text x="490.0" y="261.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
52
|
+
<rect x="610" y="246" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
53
|
+
<text x="670.0" y="261.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/3</text>
|
|
54
|
+
<rect x="790" y="246" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
55
|
+
<text x="850.0" y="261.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
56
|
+
<rect x="985" y="246" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
57
|
+
<text x="1045.0" y="261.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
58
|
+
<text x="54.0" y="299.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pg-delta</text>
|
|
59
|
+
<rect x="430" y="284" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
60
|
+
<text x="490.0" y="299.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
61
|
+
<rect x="610" y="284" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
62
|
+
<text x="670.0" y="299.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/3</text>
|
|
63
|
+
<rect x="790" y="284" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
64
|
+
<text x="850.0" y="299.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
65
|
+
<rect x="985" y="284" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
66
|
+
<text x="1045.0" y="299.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
67
|
+
<text x="54.0" y="337.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pg-schema</text>
|
|
68
|
+
<rect x="430" y="322" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
69
|
+
<text x="490.0" y="337.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
70
|
+
<rect x="610" y="322" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
71
|
+
<text x="670.0" y="337.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/3</text>
|
|
72
|
+
<rect x="790" y="322" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
73
|
+
<text x="850.0" y="337.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
74
|
+
<rect x="985" y="322" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
75
|
+
<text x="1045.0" y="337.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
76
|
+
<text x="54.0" y="375.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pgadmin</text>
|
|
77
|
+
<rect x="430" y="360" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
78
|
+
<text x="490.0" y="375.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
79
|
+
<rect x="610" y="360" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
80
|
+
<text x="670.0" y="375.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/3</text>
|
|
81
|
+
<rect x="790" y="360" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
82
|
+
<text x="850.0" y="375.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 3/3</text>
|
|
83
|
+
<rect x="985" y="360" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
84
|
+
<text x="1045.0" y="375.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
85
|
+
<text x="36.0" y="432.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400">supaschema 0.1.0 · Supabase CLI 2.106.0 · Node v24.14.0 · Apple Silicon · 3 iterations</text>
|
|
86
|
+
</svg>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="410" viewBox="0 0 1200 410" role="img">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="supaGradient" x1="0" y1="0" x2="1" y2="0">
|
|
4
|
+
<stop offset="0%" stop-color="#059669" />
|
|
5
|
+
<stop offset="100%" stop-color="#34d399" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="slateGradient" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
+
<stop offset="0%" stop-color="#3b4757" />
|
|
9
|
+
<stop offset="100%" stop-color="#526079" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect width="100%" height="100%" rx="14" fill="#0b1220" />
|
|
13
|
+
<text x="36.0" y="46.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="21" font-weight="700">Diff latency — xl fixture (1,000 tables)</text>
|
|
14
|
+
<text x="36.0" y="70.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12.5" font-weight="400">Median seconds per diff, log scale · whisker marks p95 · lower is better</text>
|
|
15
|
+
<line x1="300.0" y1="94" x2="300.0" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
16
|
+
<text x="300.0" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">1.00s</text>
|
|
17
|
+
<line x1="399.3" y1="94" x2="399.3" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
18
|
+
<text x="399.3" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">2.00s</text>
|
|
19
|
+
<line x1="530.7" y1="94" x2="530.7" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
20
|
+
<text x="530.7" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">5.00s</text>
|
|
21
|
+
<line x1="630.0" y1="94" x2="630.0" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
22
|
+
<text x="630.0" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">10.0s</text>
|
|
23
|
+
<line x1="729.3" y1="94" x2="729.3" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
24
|
+
<text x="729.3" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">20.0s</text>
|
|
25
|
+
<line x1="860.7" y1="94" x2="860.7" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
26
|
+
<text x="860.7" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">50.0s</text>
|
|
27
|
+
<line x1="960.0" y1="94" x2="960.0" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
28
|
+
<text x="960.0" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">100s</text>
|
|
29
|
+
<circle cx="41" cy="117" r="4" fill="#34d399" />
|
|
30
|
+
<text x="54.0" y="121.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="650">supaschema-file</text>
|
|
31
|
+
<rect x="300" y="108" width="48.7" height="18" rx="4" fill="url(#supaGradient)" />
|
|
32
|
+
<line x1="354.8" y1="105" x2="354.8" y2="129" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
33
|
+
<text x="1164.0" y="121.0" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="650" text-anchor="end">1.41s · p95 1.47s</text>
|
|
34
|
+
<circle cx="41" cy="151" r="4" fill="#34d399" />
|
|
35
|
+
<text x="54.0" y="155.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="650">supaschema-db</text>
|
|
36
|
+
<rect x="300" y="142" width="73.0" height="18" rx="4" fill="url(#supaGradient)" />
|
|
37
|
+
<line x1="436.1" y1="139" x2="436.1" y2="163" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
38
|
+
<text x="1164.0" y="155.0" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="650" text-anchor="end">1.66s · p95 2.58s</text>
|
|
39
|
+
<text x="54.0" y="189.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-migra</text>
|
|
40
|
+
<rect x="300" y="176" width="525.7" height="18" rx="4" fill="url(#slateGradient)" />
|
|
41
|
+
<line x1="830.6" y1="173" x2="830.6" y2="197" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
42
|
+
<text x="1164.0" y="189.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">39.2s · p95 40.5s</text>
|
|
43
|
+
<text x="54.0" y="223.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pg-schema</text>
|
|
44
|
+
<rect x="300" y="210" width="531.9" height="18" rx="4" fill="url(#slateGradient)" />
|
|
45
|
+
<line x1="838.4" y1="207" x2="838.4" y2="231" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
46
|
+
<text x="1164.0" y="223.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">40.9s · p95 42.8s</text>
|
|
47
|
+
<text x="54.0" y="257.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pg-delta</text>
|
|
48
|
+
<rect x="300" y="244" width="532.1" height="18" rx="4" fill="url(#slateGradient)" />
|
|
49
|
+
<line x1="832.6" y1="241" x2="832.6" y2="265" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
50
|
+
<text x="1164.0" y="257.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">41.0s · p95 41.1s</text>
|
|
51
|
+
<text x="54.0" y="291.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pgadmin</text>
|
|
52
|
+
<rect x="300" y="278" width="535.3" height="18" rx="4" fill="url(#slateGradient)" />
|
|
53
|
+
<line x1="839.5" y1="275" x2="839.5" y2="299" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
54
|
+
<text x="1164.0" y="291.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">41.9s · p95 43.1s</text>
|
|
55
|
+
<text x="54.0" y="325.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-default</text>
|
|
56
|
+
<rect x="300" y="312" width="539.2" height="18" rx="4" fill="url(#slateGradient)" />
|
|
57
|
+
<line x1="856.6" y1="309" x2="856.6" y2="333" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
58
|
+
<text x="1164.0" y="325.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">43.1s · p95 48.6s</text>
|
|
59
|
+
<text x="36.0" y="388.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400">supaschema 0.1.0 · Supabase CLI 2.106.0 · Node v24.14.0 · Apple Silicon · 3 iterations</text>
|
|
60
|
+
</svg>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="454" viewBox="0 0 1200 454" role="img">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="supaGradient" x1="0" y1="0" x2="1" y2="0">
|
|
4
|
+
<stop offset="0%" stop-color="#059669" />
|
|
5
|
+
<stop offset="100%" stop-color="#34d399" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="slateGradient" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
+
<stop offset="0%" stop-color="#3b4757" />
|
|
9
|
+
<stop offset="100%" stop-color="#526079" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect width="100%" height="100%" rx="14" fill="#0b1220" />
|
|
13
|
+
<text x="36.0" y="46.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="21" font-weight="700">Verification & accuracy — xxl fixture (2,500 tables)</text>
|
|
14
|
+
<text x="36.0" y="70.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12.5" font-weight="400">Each generated migration is applied in one transaction, applied again, and the catalog is fingerprinted against the target.</text>
|
|
15
|
+
<text x="36.0" y="88.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12.5" font-weight="400">Output F1 scores generated SQL content against the fixture's ground-truth change manifest (1.000 = exact).</text>
|
|
16
|
+
<text x="490.0" y="116.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">applies once</text>
|
|
17
|
+
<text x="670.0" y="116.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">applies twice</text>
|
|
18
|
+
<text x="850.0" y="116.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">matches target</text>
|
|
19
|
+
<text x="1045.0" y="116.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">output F1</text>
|
|
20
|
+
<circle cx="41" cy="143" r="4" fill="#34d399" />
|
|
21
|
+
<text x="54.0" y="147.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="650">supaschema-db</text>
|
|
22
|
+
<rect x="430" y="132" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
23
|
+
<text x="490.0" y="147.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
24
|
+
<rect x="610" y="132" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
25
|
+
<text x="670.0" y="147.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
26
|
+
<rect x="790" y="132" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
27
|
+
<text x="850.0" y="147.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
28
|
+
<rect x="985" y="132" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
29
|
+
<text x="1045.0" y="147.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">1.000</text>
|
|
30
|
+
<circle cx="41" cy="181" r="4" fill="#34d399" />
|
|
31
|
+
<text x="54.0" y="185.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="650">supaschema-file</text>
|
|
32
|
+
<rect x="430" y="170" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
33
|
+
<text x="490.0" y="185.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
34
|
+
<rect x="610" y="170" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
35
|
+
<text x="670.0" y="185.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
36
|
+
<rect x="790" y="170" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
37
|
+
<text x="850.0" y="185.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
38
|
+
<rect x="985" y="170" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
39
|
+
<text x="1045.0" y="185.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">1.000</text>
|
|
40
|
+
<text x="54.0" y="223.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-default</text>
|
|
41
|
+
<rect x="430" y="208" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
42
|
+
<text x="490.0" y="223.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
43
|
+
<rect x="610" y="208" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
44
|
+
<text x="670.0" y="223.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/1</text>
|
|
45
|
+
<rect x="790" y="208" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
46
|
+
<text x="850.0" y="223.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
47
|
+
<rect x="985" y="208" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
48
|
+
<text x="1045.0" y="223.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
49
|
+
<text x="54.0" y="261.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-migra</text>
|
|
50
|
+
<rect x="430" y="246" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
51
|
+
<text x="490.0" y="261.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
52
|
+
<rect x="610" y="246" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
53
|
+
<text x="670.0" y="261.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/1</text>
|
|
54
|
+
<rect x="790" y="246" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
55
|
+
<text x="850.0" y="261.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
56
|
+
<rect x="985" y="246" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
57
|
+
<text x="1045.0" y="261.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
58
|
+
<text x="54.0" y="299.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pg-delta</text>
|
|
59
|
+
<rect x="430" y="284" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
60
|
+
<text x="490.0" y="299.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
61
|
+
<rect x="610" y="284" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
62
|
+
<text x="670.0" y="299.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/1</text>
|
|
63
|
+
<rect x="790" y="284" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
64
|
+
<text x="850.0" y="299.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
65
|
+
<rect x="985" y="284" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
66
|
+
<text x="1045.0" y="299.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
67
|
+
<text x="54.0" y="337.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pg-schema</text>
|
|
68
|
+
<rect x="430" y="322" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
69
|
+
<text x="490.0" y="337.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
70
|
+
<rect x="610" y="322" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
71
|
+
<text x="670.0" y="337.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/1</text>
|
|
72
|
+
<rect x="790" y="322" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
73
|
+
<text x="850.0" y="337.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
74
|
+
<rect x="985" y="322" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
75
|
+
<text x="1045.0" y="337.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
76
|
+
<text x="54.0" y="375.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pgadmin</text>
|
|
77
|
+
<rect x="430" y="360" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
78
|
+
<text x="490.0" y="375.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
79
|
+
<rect x="610" y="360" width="120" height="23" rx="11.5" fill="rgba(239,68,68,0.12)" stroke="#ef4444" stroke-opacity="0.55" />
|
|
80
|
+
<text x="670.0" y="375.5" fill="#f87171" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✗ 0/1</text>
|
|
81
|
+
<rect x="790" y="360" width="120" height="23" rx="11.5" fill="rgba(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
|
|
82
|
+
<text x="850.0" y="375.5" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">✓ 1/1</text>
|
|
83
|
+
<rect x="985" y="360" width="120" height="23" rx="11.5" fill="rgba(251,191,36,0.12)" stroke="#fbbf24" stroke-opacity="0.55" />
|
|
84
|
+
<text x="1045.0" y="375.5" fill="#fbbf24" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">0.999</text>
|
|
85
|
+
<text x="36.0" y="432.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400">supaschema 0.1.0 · Supabase CLI 2.106.0 · Node v24.14.0 · Apple Silicon · 1 iteration</text>
|
|
86
|
+
</svg>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="410" viewBox="0 0 1200 410" role="img">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="supaGradient" x1="0" y1="0" x2="1" y2="0">
|
|
4
|
+
<stop offset="0%" stop-color="#059669" />
|
|
5
|
+
<stop offset="100%" stop-color="#34d399" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="slateGradient" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
+
<stop offset="0%" stop-color="#3b4757" />
|
|
9
|
+
<stop offset="100%" stop-color="#526079" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect width="100%" height="100%" rx="14" fill="#0b1220" />
|
|
13
|
+
<text x="36.0" y="46.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="21" font-weight="700">Diff latency — xxl fixture (2,500 tables)</text>
|
|
14
|
+
<text x="36.0" y="70.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12.5" font-weight="400">Median seconds per diff, log scale · whisker marks p95 · lower is better</text>
|
|
15
|
+
<line x1="300.0" y1="94" x2="300.0" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
16
|
+
<text x="300.0" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">1.00s</text>
|
|
17
|
+
<line x1="366.2" y1="94" x2="366.2" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
18
|
+
<text x="366.2" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">2.00s</text>
|
|
19
|
+
<line x1="453.8" y1="94" x2="453.8" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
20
|
+
<text x="453.8" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">5.00s</text>
|
|
21
|
+
<line x1="520.0" y1="94" x2="520.0" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
22
|
+
<text x="520.0" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">10.0s</text>
|
|
23
|
+
<line x1="586.2" y1="94" x2="586.2" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
24
|
+
<text x="586.2" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">20.0s</text>
|
|
25
|
+
<line x1="673.8" y1="94" x2="673.8" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
26
|
+
<text x="673.8" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">50.0s</text>
|
|
27
|
+
<line x1="740.0" y1="94" x2="740.0" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
28
|
+
<text x="740.0" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">100s</text>
|
|
29
|
+
<line x1="806.2" y1="94" x2="806.2" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
30
|
+
<text x="806.2" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">200s</text>
|
|
31
|
+
<line x1="893.8" y1="94" x2="893.8" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
32
|
+
<text x="893.8" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">500s</text>
|
|
33
|
+
<line x1="960.0" y1="94" x2="960.0" y2="336" stroke="#1e293b" stroke-width="1" />
|
|
34
|
+
<text x="960.0" y="358.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400" text-anchor="middle">1000s</text>
|
|
35
|
+
<circle cx="41" cy="117" r="4" fill="#34d399" />
|
|
36
|
+
<text x="54.0" y="121.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="650">supaschema-db</text>
|
|
37
|
+
<rect x="300" y="108" width="92.9" height="18" rx="4" fill="url(#supaGradient)" />
|
|
38
|
+
<line x1="392.9" y1="105" x2="392.9" y2="129" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
39
|
+
<text x="1164.0" y="121.0" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="650" text-anchor="end">2.64s · p95 2.64s</text>
|
|
40
|
+
<circle cx="41" cy="151" r="4" fill="#34d399" />
|
|
41
|
+
<text x="54.0" y="155.0" fill="#f8fafc" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="650">supaschema-file</text>
|
|
42
|
+
<rect x="300" y="142" width="110.2" height="18" rx="4" fill="url(#supaGradient)" />
|
|
43
|
+
<line x1="410.2" y1="139" x2="410.2" y2="163" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
44
|
+
<text x="1164.0" y="155.0" fill="#34d399" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="650" text-anchor="end">3.17s · p95 3.17s</text>
|
|
45
|
+
<text x="54.0" y="189.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pg-delta</text>
|
|
46
|
+
<rect x="300" y="176" width="509.4" height="18" rx="4" fill="url(#slateGradient)" />
|
|
47
|
+
<line x1="809.4" y1="173" x2="809.4" y2="197" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
48
|
+
<text x="1164.0" y="189.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">207s · p95 207s</text>
|
|
49
|
+
<text x="54.0" y="223.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-migra</text>
|
|
50
|
+
<rect x="300" y="210" width="509.5" height="18" rx="4" fill="url(#slateGradient)" />
|
|
51
|
+
<line x1="809.5" y1="207" x2="809.5" y2="231" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
52
|
+
<text x="1164.0" y="223.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">207s · p95 207s</text>
|
|
53
|
+
<text x="54.0" y="257.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pgadmin</text>
|
|
54
|
+
<rect x="300" y="244" width="511.5" height="18" rx="4" fill="url(#slateGradient)" />
|
|
55
|
+
<line x1="811.5" y1="241" x2="811.5" y2="265" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
56
|
+
<text x="1164.0" y="257.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">211s · p95 211s</text>
|
|
57
|
+
<text x="54.0" y="291.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-default</text>
|
|
58
|
+
<rect x="300" y="278" width="513.7" height="18" rx="4" fill="url(#slateGradient)" />
|
|
59
|
+
<line x1="813.7" y1="275" x2="813.7" y2="299" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
60
|
+
<text x="1164.0" y="291.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">216s · p95 216s</text>
|
|
61
|
+
<text x="54.0" y="325.0" fill="#e2e8f0" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="13" font-weight="450">supabase-pg-schema</text>
|
|
62
|
+
<rect x="300" y="312" width="513.9" height="18" rx="4" fill="url(#slateGradient)" />
|
|
63
|
+
<line x1="813.9" y1="309" x2="813.9" y2="333" stroke="#f8fafc" stroke-opacity="0.55" stroke-width="2" />
|
|
64
|
+
<text x="1164.0" y="325.0" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="12" font-weight="450" text-anchor="end">217s · p95 217s</text>
|
|
65
|
+
<text x="36.0" y="388.0" fill="#64748b" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11" font-weight="400">supaschema 0.1.0 · Supabase CLI 2.106.0 · Node v24.14.0 · Apple Silicon · 1 iteration</text>
|
|
66
|
+
</svg>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Case study: a production multi-tenant Supabase platform
|
|
2
|
+
|
|
3
|
+
supaschema was built while developing a production multi-tenant SaaS on Supabase: roughly **30 schemas, ~8,300 schema objects, and hundreds of RLS policies** (tenant isolation is enforced in the database, so almost every table carries policies). The pain that motivated the project was concrete: every schema edit meant waiting on the Supabase CLI's shadow-database diff before a migration — and its generated types — could catch up. That wait is a tax on every change, and it grows with the schema.
|
|
4
|
+
|
|
5
|
+
All numbers below are reproducible. Object names are kept generic; the measurements are from the real tree.
|
|
6
|
+
|
|
7
|
+
## Speed: the whole tree, no database, in under two seconds
|
|
8
|
+
|
|
9
|
+
Extracting and planning the entire declarative tree (8,271 modeled objects) runs on the parser alone — no Docker, no shadow database, no introspection:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
extract-from 1012ms · extract-to 876ms · plan 5ms
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A full diff is two extractions plus a plan — **~1.9 seconds over the entire production tree.** The equivalent Supabase CLI diff replays all 8,300 objects into a fresh Docker shadow database on every run, which is minutes at this scale (see the [scaling benchmark](../README.md#speed)).
|
|
16
|
+
|
|
17
|
+
The 8,271 figure is what supaschema models; the tree also produced 91 expected fail-closed diagnostics, concentrated in the bootstrap layer (managed-schema declarations for `extensions`/`vault`/roles, which a real adoption excludes via `schemas.exclude`) plus normalize-fidelity warnings. None are engine errors.
|
|
18
|
+
|
|
19
|
+
## Head-to-head on real schema (bounded slice)
|
|
20
|
+
|
|
21
|
+
A 282-object slice of the real tree (three schemas: identity, calculators, messaging — 73 RLS policies among them) was diffed against itself plus a small additive change, supaschema versus the default Supabase CLI engine, one iteration, both applied to a throwaway Postgres and re-applied:
|
|
22
|
+
|
|
23
|
+
| | supaschema | Supabase CLI (default) |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| Diff latency | **361 ms** | 35,293 ms (~**98× slower**) |
|
|
26
|
+
| Migration applies once → target catalog | yes | yes |
|
|
27
|
+
| Migration applies **twice** (crash-safe re-run) | **yes** | **no** |
|
|
28
|
+
| Reaches the target catalog after re-run | **yes** | **no** |
|
|
29
|
+
|
|
30
|
+
The Supabase engine emitted an unguarded `CREATE TABLE public.…(…)` (no `IF NOT EXISTS`) and an unguarded `CREATE INDEX`, so the second apply fails with `relation "…" already exists`. supaschema's output is guarded by construction (`IF NOT EXISTS`, catalog-checked `DO` blocks), so a crashed or retried deploy simply runs the file again.
|
|
31
|
+
|
|
32
|
+
Reproduce against any declarative tree:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
node benchmarks/tools/build-project-fixture.mjs --tree <your supabase/schemas> --out benchmarks/fixtures/project
|
|
36
|
+
SUPASCHEMA_COMPARE_FIXTURES=project \
|
|
37
|
+
SUPASCHEMA_COMPARE_TOOLS=supaschema-file,supabase-default \
|
|
38
|
+
SUPASCHEMA_COMPARE_ITERATIONS=1 \
|
|
39
|
+
SUPASCHEMA_COMPARE_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres \
|
|
40
|
+
node benchmarks/compare.js
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Security: the miss that speed hides
|
|
44
|
+
|
|
45
|
+
Speed is the felt pain; the more dangerous gap is correctness on RLS. On a multi-tenant platform, a policy's `USING` predicate **is** the tenant boundary. Tightening `USING (true)` to `USING (tenant_id = current_tenant())` is a one-line change that closes an isolation hole — and every Supabase CLI diff engine measured here silently drops that policy change (it diffs policies by name, not by body). See the [functions-policies](../README.md#accuracy) and realistic fixtures, where each engine scores F1 0.982–0.999 on exactly that miss while supaschema scores 1.000, and the missed-policy migration provably fails to reach the target catalog.
|
|
46
|
+
|
|
47
|
+
A slow diff costs seconds. An unreplayable diff costs a deploy. A silently dropped policy change ships a tenant-isolation hole that review, CI, and the migration runner all wave through — which on this platform's hundreds of policies is the failure mode that actually matters.
|
|
48
|
+
|
|
49
|
+
## Why both speed and security
|
|
50
|
+
|
|
51
|
+
The two are the same wedge from different ends. The parser-based, no-database design is what makes supaschema fast; that same AST-based identity is what lets it compare policy bodies structurally instead of by name, which is what catches the isolation regression. You do not trade one for the other.
|
package/docs/ci-gate.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# CI gate: drift, replay-safety, and the policy-isolation check
|
|
2
|
+
|
|
3
|
+
supaschema already ships the pieces of a CI gate. This document describes the free layer that exists today and the paid layer it is designed to become.
|
|
4
|
+
|
|
5
|
+
## Free: the GitHub Action and reporters
|
|
6
|
+
|
|
7
|
+
The committed composite action runs supaschema against a declarative tree on every push or PR:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- uses: jmclaughlin724/supaschema@<tag>
|
|
11
|
+
with:
|
|
12
|
+
args: diff --from database:$DATABASE_URL --to dir:supabase/schemas --fail-on-diff --quiet
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Three gates compose from existing commands, no service required:
|
|
16
|
+
|
|
17
|
+
- **Drift** — `diff --fail-on-diff` exits 3 when the declarative tree and the database disagree, failing the build on un-migrated schema.
|
|
18
|
+
- **Replay-safety** — `check --reporter github` annotates the PR diff, or `check --reporter sarif` uploads to the GitHub **code-scanning / Security** tab, flagging unguarded DDL, `CASCADE`, session `search_path`, and lock hazards.
|
|
19
|
+
- **Policy isolation** — because the diff plan classifies an RLS policy-body change as its own operation, a tightened `USING` predicate surfaces as a reviewable change instead of being dropped. This is the differentiator: the engines a team would otherwise rely on miss it.
|
|
20
|
+
|
|
21
|
+
This layer is AGPL and free. It is the funnel.
|
|
22
|
+
|
|
23
|
+
## Paid: the hosted policy-isolation gate
|
|
24
|
+
|
|
25
|
+
The free Action runs in the customer's CI and reports per-run. The paid layer is a hosted GitHub App that turns the same engine into an org-level security control:
|
|
26
|
+
|
|
27
|
+
- **PR + Security-tab surface** — SARIF findings posted to every PR and aggregated in the org's code-scanning dashboard, with a dedicated **tenant-isolation policy change** finding class (a policy `USING`/`WITH CHECK` predicate changed, was removed, or weakened) that requires explicit review acknowledgement before merge.
|
|
28
|
+
- **History and drift over time** — per-repo migration lineage, drift trend, and a record of every policy change shipped, so an isolation regression has an audit trail.
|
|
29
|
+
- **Org policy** — required-check enforcement, allowed-hint review (no `hints.destructive: "*"` reaching `main`), and per-environment gates (`staging`/`production`) wired to named `environments`.
|
|
30
|
+
- **No-CI-config onboarding** — install the App, point it at `supabase/schemas`, done; no workflow YAML, no `DATABASE_URL` plumbing in the customer's Actions.
|
|
31
|
+
|
|
32
|
+
The engine is unchanged; the paid surface is hosting, the Security-tab integration, the policy-change finding class, and org administration. That is a per-repo or per-seat SaaS, not a CLI license — far higher ARPU, and the security framing (a tenant-isolation gate) carries the willingness to pay.
|
|
33
|
+
|
|
34
|
+
## Sequencing
|
|
35
|
+
|
|
36
|
+
1. Publish the package so the Action resolves (`npx supaschema@<tag>`).
|
|
37
|
+
2. Ship the free Action + SARIF reporter as the funnel; instrument adoption.
|
|
38
|
+
3. Build the hosted App once there is adoption signal and at least one reference customer (the case study is the first).
|
|
39
|
+
|
|
40
|
+
## Open pricing decisions (founder)
|
|
41
|
+
|
|
42
|
+
- Per-repo vs per-seat vs per-org-tier.
|
|
43
|
+
- Whether the tenant-isolation finding class is the free hook or a paid-only gate.
|
|
44
|
+
- Self-serve vs sales-assisted for the commercial license (see `LICENSE-COMMERCIAL.md`).
|
package/docs/ci.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# CI Recipe
|
|
2
|
+
|
|
3
|
+
A minimal GitHub Actions job that diffs a pull request's declarative tree against the base branch, checks the rendered migration, and proves it against a disposable PostgreSQL service:
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
name: schema-diff
|
|
7
|
+
on: pull_request
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
supaschema:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
services:
|
|
13
|
+
postgres:
|
|
14
|
+
image: postgres:17
|
|
15
|
+
env:
|
|
16
|
+
POSTGRES_PASSWORD: postgres
|
|
17
|
+
ports:
|
|
18
|
+
- 5432:5432
|
|
19
|
+
options: >-
|
|
20
|
+
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
with:
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
- uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: 22
|
|
30
|
+
- run: npm install --no-save supaschema
|
|
31
|
+
- name: Render migration from the PR diff
|
|
32
|
+
run: |
|
|
33
|
+
npx supaschema diff \
|
|
34
|
+
--from "git:origin/${{ github.base_ref }}" \
|
|
35
|
+
--to dir:supabase/schemas \
|
|
36
|
+
--out /tmp/migration.sql
|
|
37
|
+
- name: Check replay safety
|
|
38
|
+
run: npx supaschema check /tmp/migration.sql
|
|
39
|
+
- name: Verify apply-twice and catalog parity
|
|
40
|
+
run: |
|
|
41
|
+
npx supaschema verify \
|
|
42
|
+
--from "git:origin/${{ github.base_ref }}" \
|
|
43
|
+
--to dir:supabase/schemas \
|
|
44
|
+
--migration /tmp/migration.sql \
|
|
45
|
+
--database-url postgresql://postgres:postgres@localhost:5432/postgres
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Notes:
|
|
49
|
+
|
|
50
|
+
- `fetch-depth: 0` is required so `git:` sources can read the base ref.
|
|
51
|
+
- `verify` creates and drops temporary databases on the service; the role must have `CREATEDB`. Add `--ensure-roles` when migrations grant to roles (e.g. `authenticated`) that a bare PostgreSQL service does not have.
|
|
52
|
+
- `verify` refuses non-local database hosts by default (it creates and force-drops databases). GitHub Actions service containers are `localhost`, so CI recipes work unchanged; set `SUPASCHEMA_VERIFY_ALLOW_REMOTE=1` only when verifying against intentionally disposable remote infrastructure.
|
|
53
|
+
- For a standalone drift gate, `supaschema diff --fail-on-diff --quiet` exits 3 when the live database and the declarative tree have diverged (`--from` defaults to the resolved database and `--to` to the config schema tree; spell them explicitly when the job's defaults are ambiguous).
|
|
54
|
+
- `SUPASCHEMA_DATABASE_URL` can replace per-command `--database-url` flags; without either, supaschema discovers the local Supabase stack from `supabase/config.toml`.
|
|
55
|
+
- Set `SUPASCHEMA_BENCHMARK_DATABASE_URL` (or rely on `SUPASCHEMA_DATABASE_URL`) when running `npm run benchmark` in package CI so `database:`, shadow-style round trip, replay verification, and end-to-end migration timings are enforced instead of skipped.
|
|
56
|
+
- Exit code `2` means diagnostics contained errors — fail the job and read the stderr output.
|
|
57
|
+
- Pin a PostgreSQL service major that matches production; run a matrix over 15/16/17 for libraries.
|
|
58
|
+
- `supaschema check <dir>/*.sql` accepts multiple files, so one step can gate every new migration in a PR for replay safety — including migrations generated by other tools (shadow-database engines routinely emit unguarded `CREATE FUNCTION` / bare `DROP`). Add `--reporter github` for inline PR annotations on each finding, or `--reporter sarif` to upload into GitHub code scanning (`github/codeql-action/upload-sarif`).
|
|
59
|
+
- The committed composite action wraps any invocation: `uses: jmclaughlin724/supaschema@v0.1.0` with `args: "diff --fail-on-diff --quiet"` and `SUPASCHEMA_DATABASE_URL` in the step env (always pin a tag; the action runs `npx supaschema@<version>` with `version` defaulting to `latest`).
|
|
60
|
+
- Husky pre-commit recipe — gate staged migrations locally before CI sees them:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
# .husky/pre-commit
|
|
64
|
+
staged=$(git diff --cached --name-only --diff-filter=ACM -- 'supabase/migrations/*.sql')
|
|
65
|
+
[ -z "$staged" ] || npx supaschema check $staged
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `npm run corpus:check` runs the corpus oracle (replay a dirty-real migrations corpus, diff against its tree, apply twice, require reconvergence to zero) against the same service container; it prints a skip notice and exits 0 when no database URL resolves. Background and the regression-corpus discipline live in `docs/corpus.md`.
|
package/docs/commands.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Command reference
|
|
2
|
+
|
|
3
|
+
Every command, with full flags. Global options on every command: `--config <path>` (explicit `.json`/`.mjs`/`.js` config file), `--env <name>` (database URL from a named `environments` entry in the config), `--quiet` (suppress stderr diagnostics).
|
|
4
|
+
|
|
5
|
+
Zero-flag defaults, applied wherever a flag is omitted and printed to stderr when used: `--from` resolves to `database:<resolved URL>` (the applied state) and falls back to `git:HEAD`; `--to` resolves to `dir:<config.schemaPaths[0]>` (`supabase/schemas`); migrations write to `config.migrationsDir` (`supabase/migrations`) as `<UTC timestamp>_<name>.sql` with the name derived from the planned operations; `check` with no arguments gates every `.sql` in the migrations directory; `verify --migration` defaults to the newest pending file there.
|
|
6
|
+
|
|
7
|
+
Exit codes: `0` success · `1` runtime error · `2` diagnostics contained at least one error · `3` `--fail-on-diff` found operations.
|
|
8
|
+
|
|
9
|
+
## Generation and planning
|
|
10
|
+
|
|
11
|
+
- `supaschema diff [--from <source>] [--to <target>] [--out <file|stdout>] [--name <snake_case>] [--migrations-dir <dir>] [--schema <names>] [--dry-run] [--json] [--fail-on-diff] [--no-check-chain] [--summary] [--write-hints <file>] [--timing] [--watch]` renders the migration into the migrations directory (zero-flag: applied state → schema tree → `<migrations-dir>/<UTC timestamp>_<derived name>.sql`; an empty plan writes nothing and prints `no schema changes`). `--out stdout` prints instead of writing; `--json` prints a machine-readable payload; `--fail-on-diff` exits 3 when the plan contains operations (the CI drift gate); `--summary` prints a drift report (operation and diagnostic counts grouped by kind and schema) even when the plan is blocked; `--write-hints` writes the gated destructive object keys as a `hints.destructive` skeleton for review; `--timing` prints extract/plan/render phase timings to stderr; `--watch` re-prints the drift summary whenever a `dir:` source changes (editor loop — forces dry-run, never writes files).
|
|
12
|
+
- `supaschema types [--from <source>] [--out <file|stdout>]` generates Supabase-compatible TypeScript types (`Database`, table `Row`/`Insert`/`Update`, views, enums, relationships, `Constants`, and helper generics) plus runtime Zod validators straight from the schema tree — no database, no introspection, no applied migrations. Defaults: `--from` the config schema tree, TypeScript to `config.typesFile` (`database.types.ts`), Zod validators to `config.zodFile` (`database.zod.ts`; requires `zod` in the consuming project); `--out stdout` prints the TypeScript artifact only. Once a generated file exists, every `diff` that writes a migration refreshes it automatically, so types never wait on a deploy. View columns are typed by resolving direct column references (and `SELECT *`) through their source table; expressions fall back to `unknown`.
|
|
13
|
+
- `supaschema plan [--from <source>] [--to <target>] [--schema <names>] [--timing]` prints the object-level diff plan as JSON; use `diff` to render SQL.
|
|
14
|
+
- `supaschema inspect [--from <source>] [--schema <names>]` prints the extracted schema model as JSON (default source: the config schema tree; save it for `catalog:` sources).
|
|
15
|
+
- `supaschema fingerprint --from <source>` prints only the model fingerprint — two sources with equal fingerprints have identical schemas.
|
|
16
|
+
|
|
17
|
+
## Validation and proof
|
|
18
|
+
|
|
19
|
+
- `supaschema check [migrations...] [--reporter text|github|sarif|json]` validates replay-safety, lock hazards, and parser diagnostics. Zero arguments gates every `.sql` in `config.migrationsDir`; named files and shell globs gate exactly those, including migrations generated by other tools; `-` reads stdin (`other-tool | supaschema check -`); `--reporter github` emits inline PR annotations and `--reporter sarif` feeds GitHub code scanning.
|
|
20
|
+
- `supaschema verify [--from <source>] [--to <target>] [--migration <file>] [--migrations-dir <dir>] [--database-url <url>] [--ensure-roles] [--ensure-environment] [--keep-databases]` (zero-flag: applied state → schema tree, newest pending migration) applies the migration twice in disposable databases, compares catalog fingerprints, then cross-lane diffs the migrated catalog against the target model and requires zero residual operations (`SUPA_VERIFY_RECONVERGENCE`). The URL auto-resolves when omitted; non-local hosts are refused without `SUPASCHEMA_VERIFY_ALLOW_REMOTE=1`. A capability preflight fails fast when the role lacks `CREATEDB`. `--ensure-roles` pre-creates referenced NOLOGIN roles; `--ensure-environment` (default under adapter `supabase-auto`) stubs Supabase-provisioned surfaces (`auth.uid()`-family helpers, `auth.users`, the cron schema) so real-world trees verify against bare PostgreSQL; `--keep-databases` preserves the temporary databases for inspection after a failed run.
|
|
21
|
+
- `supaschema selfcheck [--database-url <url>]` re-extracts the live catalog's rendered SQL through the source pipeline and reports any object whose identity diverges — the regression net for cross-lane false changes.
|
|
22
|
+
- `supaschema corpus [--corpus-dir <dir>] [--database-url <url>] [--json]` runs the corpus oracle: replays the committed dirty-real migrations corpus into a disposable database, cross-lane diffs it against the corpus tree, applies the rendered reconciliation twice, and requires the re-diff to converge to zero (`SUPA_CORPUS_RECONVERGENCE`). See [corpus.md](corpus.md).
|
|
23
|
+
|
|
24
|
+
## Migration-state operations
|
|
25
|
+
|
|
26
|
+
- `supaschema migrations [--migrations-dir <dir>] [--database-url <url>] [--history-table <schema.table>] [--json]` reconciles migration files on disk against a target's applied history (`supabase_migrations.schema_migrations` by default): applied, pending (supaschema-generated files flagged via their lineage marker), ghost versions (applied with no file on disk), and out-of-order pending files. Ghosts and out-of-order are errors (exit 2). Run once per target to compare worktree, local, and remote.
|
|
27
|
+
- `supaschema sync [--migrations-dir <dir>] [--database-url <url>] [--local] [--remote]` is the opt-in auto-sync orchestrator: reconciles disk vs applied history, refuses on ghost/out-of-order history, runs the replay-safety check on every pending migration, then — only with explicit flags — executes the Supabase CLI (`--local` → `supabase migration up`, `--remote` → `supabase db push`, runner prompts intact). Without flags it is a dry run; the runner keeps sole ownership of the history table.
|
|
28
|
+
|
|
29
|
+
## Setup and tooling
|
|
30
|
+
|
|
31
|
+
- `supaschema init` writes `supaschema.config.json` with the defaults (including the `$schema` pointer for editor autocomplete).
|
|
32
|
+
- `supaschema audit --from <source> [--json]` reports support-matrix coverage: objects modeled by kind/schema plus every statement outside the contract grouped by diagnostic code (exit 2 when any exist) — the adoption question answered from the same extraction the diff engine uses.
|
|
33
|
+
- `supaschema doctor [--database-url <url>] [--json]` diagnoses the environment in one paste-able report: Node version, parser load, config, which URL-resolution lane fired, database reachability, `CREATEDB` capability, migrations-history health, and the declarative tree.
|
|
34
|
+
- `supaschema completion <bash|zsh|fish>` prints a shell completion script.
|
|
35
|
+
- `supaschema explain <code>` explains a diagnostic code offline.
|