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
package/dist/verify.js ADDED
@@ -0,0 +1,261 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { Client } from "pg";
3
+ import { extractCatalogModel } from "./catalog.js";
4
+ import { checkMigrationSql } from "./check.js";
5
+ import { resolveConfig } from "./config.js";
6
+ import { applyMigrationSql, applySql as applyPerStatementSql, assertLocalDatabaseUrl, createDatabaseWithRetry, databaseUrlWithDatabase, tempDatabaseName, } from "./db-admin.js";
7
+ import { diagnostic, hasErrors } from "./diagnostics.js";
8
+ import { planSchemaDiff } from "./planner.js";
9
+ import { extractSourceModel } from "./source.js";
10
+ import { asRecord, astStatements, roleSpecName } from "./sql/ast.js";
11
+ import { extractObjectsFromSql } from "./sql/extract.js";
12
+ import { quoteIdent } from "./sql/identifiers.js";
13
+ import { parseSqlAst } from "./sql/parser.js";
14
+ import { preflightCapability, supabaseEnvironmentStubSql } from "./verify-environment.js";
15
+ export async function verifyMigration(options) {
16
+ const config = resolveConfig(options.config);
17
+ const diagnostics = [];
18
+ try {
19
+ assertLocalDatabaseUrl(options.databaseUrl, "SUPASCHEMA_VERIFY_ALLOW_REMOTE");
20
+ }
21
+ catch (error) {
22
+ return [
23
+ diagnostic("SUPA_VERIFY_FAILED", "error", errorMessage(error), {
24
+ hint: "verify creates and drops databases; non-local hosts require SUPASCHEMA_VERIFY_ALLOW_REMOTE=1.",
25
+ }),
26
+ ];
27
+ }
28
+ const migrationSql = await readFile(options.migrationPath, "utf8");
29
+ const extractOptions = {};
30
+ if (options.config !== undefined) {
31
+ extractOptions.config = options.config;
32
+ }
33
+ if (options.cwd !== undefined) {
34
+ extractOptions.cwd = options.cwd;
35
+ }
36
+ diagnostics.push(...(await checkMigrationSql(migrationSql, extractOptions)));
37
+ if (hasErrors(diagnostics)) {
38
+ return diagnostics;
39
+ }
40
+ const from = await extractSourceModel(options.from, extractOptions);
41
+ const to = await extractSourceModel(options.to, extractOptions);
42
+ diagnostics.push(...from.diagnostics, ...to.diagnostics);
43
+ if (hasErrors(diagnostics)) {
44
+ return diagnostics;
45
+ }
46
+ const admin = new Client({ connectionString: options.databaseUrl });
47
+ const migrationDb = tempDatabaseName("migration");
48
+ const targetDb = tempDatabaseName("target");
49
+ const created = [];
50
+ const environmentEnsured = options.ensureEnvironment ?? config.adapter === "supabase-auto";
51
+ try {
52
+ await admin.connect();
53
+ const capability = await preflightCapability(admin);
54
+ if (capability) {
55
+ diagnostics.push(capability);
56
+ await admin.end().catch(() => undefined);
57
+ return diagnostics;
58
+ }
59
+ for (const databaseName of [migrationDb, targetDb]) {
60
+ await createDatabaseWithRetry(admin, `CREATE DATABASE ${quoteIdent(databaseName)}`);
61
+ created.push(databaseName);
62
+ }
63
+ const migrationUrl = databaseUrlWithDatabase(options.databaseUrl, migrationDb);
64
+ const targetUrl = databaseUrlWithDatabase(options.databaseUrl, targetDb);
65
+ if (options.ensureRoles === true) {
66
+ for (const role of await collectReferencedRoles([from, to])) {
67
+ await admin.query(`DO $supaschema$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = ${quoteRoleLiteral(role)}) THEN\n CREATE ROLE ${quoteIdent(role)} NOLOGIN;\n END IF;\nEND\n$supaschema$`);
68
+ }
69
+ }
70
+ if (environmentEnsured) {
71
+ // Stubs land in both databases, so catalog parity is unaffected.
72
+ await applySql(migrationUrl, supabaseEnvironmentStubSql, "per-statement");
73
+ await applySql(targetUrl, supabaseEnvironmentStubSql, "per-statement");
74
+ }
75
+ await applyModel(migrationUrl, from);
76
+ await applySql(migrationUrl, migrationSql, config.transactionMode);
77
+ await applySql(migrationUrl, migrationSql, config.transactionMode);
78
+ await applyModel(targetUrl, to);
79
+ const afterMigration = await extractCatalogModel({
80
+ databaseUrl: migrationUrl,
81
+ source: "verify:migration",
82
+ });
83
+ const expectedTarget = await extractCatalogModel({
84
+ databaseUrl: targetUrl,
85
+ source: "verify:target",
86
+ });
87
+ diagnostics.push(...afterMigration.diagnostics, ...expectedTarget.diagnostics);
88
+ if (afterMigration.fingerprint !== expectedTarget.fingerprint) {
89
+ diagnostics.push(diagnostic("SUPA_VERIFY_FINGERPRINT_MISMATCH", "error", "migration result catalog fingerprint does not match target catalog fingerprint", {
90
+ hint: fingerprintMismatchHint(afterMigration, expectedTarget),
91
+ }));
92
+ }
93
+ // Reconvergence: the fingerprint check compares two databases built from
94
+ // the same models, so a modeling error symmetric in the models passes it.
95
+ // Cross-lane diffing the migrated catalog against the target *model*
96
+ // requires the diff itself to converge to zero — the detector for false
97
+ // drift (catalog-invisible no-ops, built-in objects, spelling drift).
98
+ const reconvergence = planSchemaDiff(afterMigration, to, {
99
+ config: { ...config, hints: { destructive: ["*"], renames: [] } },
100
+ });
101
+ // Environment-pack stubs exist only in the temp databases; subtract their
102
+ // object keys so they do not read as residual drift.
103
+ const stubKeys = new Set();
104
+ if (environmentEnsured) {
105
+ const stubs = await extractObjectsFromSql(supabaseEnvironmentStubSql, { config });
106
+ for (const object of stubs.objects) {
107
+ stubKeys.add(object.key);
108
+ }
109
+ }
110
+ const residualOperations = reconvergence.operations.filter((operation) => !stubKeys.has(operation.key));
111
+ if (residualOperations.length > 0) {
112
+ const residual = residualOperations
113
+ .slice(0, 6)
114
+ .map((operation) => `${operation.kind} ${operation.key}`)
115
+ .join(", ");
116
+ diagnostics.push(diagnostic("SUPA_VERIFY_RECONVERGENCE", "error", `${residualOperations.length} operation(s) remain between the migrated catalog and the target model; the diff would never converge to empty`, {
117
+ hint: `residual: ${residual}. The target model declares state the catalog cannot reproduce (or vice versa); fix the model or the engine's lane parity.`,
118
+ }));
119
+ }
120
+ }
121
+ catch (error) {
122
+ const message = errorMessage(error);
123
+ diagnostics.push(diagnostic("SUPA_VERIFY_FAILED", "error", message, {
124
+ hint: "Use a disposable PostgreSQL database URL whose role can CREATE DATABASE and DROP DATABASE.",
125
+ }));
126
+ const stubbedSchema = environmentEnsured
127
+ ? managedSchemaReferenced(message, config.managedSchemas)
128
+ : undefined;
129
+ if (stubbedSchema !== undefined) {
130
+ diagnostics.push(diagnostic("SUPA_VERIFY_STUB_REFERENCE", "warning", `verify failed while referencing the "${stubbedSchema}" managed schema, which --ensure-environment provisions only as a minimal stub`, {
131
+ hint: `--ensure-environment stubs auth.users (the GoTrue column set), the auth.uid/role/jwt/email helpers, and the cron tables; other ${stubbedSchema} objects are absent. This may be a stub limitation rather than a real migration defect — re-run verify against a real Supabase database (without --ensure-environment) to confirm.`,
132
+ }));
133
+ }
134
+ }
135
+ finally {
136
+ if (options.keepDatabases === true && created.length > 0) {
137
+ diagnostics.push(diagnostic("SUPA_VERIFY_CLEANUP_FAILED", "warning", `kept temporary databases for inspection: ${created.join(", ")}`, { hint: "Drop them manually when done (--keep-databases was set)." }));
138
+ }
139
+ else {
140
+ for (const databaseName of created.reverse()) {
141
+ try {
142
+ await admin.query(`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, [databaseName]);
143
+ await admin.query(`DROP DATABASE IF EXISTS ${quoteIdent(databaseName)} WITH (FORCE)`);
144
+ }
145
+ catch (cleanupError) {
146
+ diagnostics.push(diagnostic("SUPA_VERIFY_CLEANUP_FAILED", "warning", errorMessage(cleanupError), {
147
+ hint: `Temporary database ${databaseName} may need manual removal.`,
148
+ }));
149
+ }
150
+ }
151
+ }
152
+ await admin.end().catch(() => undefined);
153
+ }
154
+ return diagnostics;
155
+ }
156
+ /**
157
+ * Roles referenced by grants, default privileges, and policies. Roles are
158
+ * cluster-level so models cannot create them; verify pre-creates missing
159
+ * NOLOGIN roles when ensureRoles is set.
160
+ */
161
+ async function collectReferencedRoles(models) {
162
+ const roles = new Set();
163
+ const add = (value) => {
164
+ if (typeof value === "string" && value.length > 0 && value !== "PUBLIC") {
165
+ roles.add(value);
166
+ }
167
+ };
168
+ for (const model of models) {
169
+ for (const object of model.objects) {
170
+ if (object.ref.kind === "grant" || object.ref.kind === "default-privilege") {
171
+ add(object.metadata.grantee);
172
+ add(object.metadata.forRole);
173
+ continue;
174
+ }
175
+ if (object.ref.kind !== "policy") {
176
+ continue;
177
+ }
178
+ const parsed = await parseSqlAst(object.sql);
179
+ if (parsed.ast === undefined) {
180
+ continue;
181
+ }
182
+ for (const statement of astStatements(parsed.ast, object.sql)) {
183
+ const policy = asRecord(statement.node.CreatePolicyStmt);
184
+ for (const role of Array.isArray(policy?.roles) ? policy.roles : []) {
185
+ add(roleSpecName(role));
186
+ }
187
+ }
188
+ }
189
+ }
190
+ return [...roles].sort((left, right) => left.localeCompare(right));
191
+ }
192
+ function quoteRoleLiteral(role) {
193
+ return `'${role.replaceAll("'", "''")}'`;
194
+ }
195
+ const mismatchSampleLimit = 12;
196
+ function fingerprintMismatchHint(migration, target) {
197
+ const appliedHashes = new Map(migration.objects.map((object) => [object.key, object.hash]));
198
+ const targetHashes = new Map(target.objects.map((object) => [object.key, object.hash]));
199
+ const missing = [];
200
+ const changed = [];
201
+ for (const [key, hash] of targetHashes) {
202
+ const applied = appliedHashes.get(key);
203
+ if (applied === undefined) {
204
+ missing.push(key);
205
+ }
206
+ else if (applied !== hash) {
207
+ changed.push(key);
208
+ }
209
+ }
210
+ const unexpected = [...appliedHashes.keys()].filter((key) => !targetHashes.has(key));
211
+ const parts = [];
212
+ for (const [label, keys] of [
213
+ ["missing from migration result", missing],
214
+ ["not present in target", unexpected],
215
+ ["definition differs", changed],
216
+ ]) {
217
+ if (keys.length === 0) {
218
+ continue;
219
+ }
220
+ const sorted = [...keys].sort((left, right) => left.localeCompare(right));
221
+ const sample = sorted.slice(0, mismatchSampleLimit).join(", ");
222
+ const suffix = sorted.length > mismatchSampleLimit ? ` (+${sorted.length - mismatchSampleLimit} more)` : "";
223
+ parts.push(`${label}: ${sample}${suffix}`);
224
+ }
225
+ return parts.join("; ") || `migration=${migration.fingerprint} target=${target.fingerprint}`;
226
+ }
227
+ async function applyModel(databaseUrl, model) {
228
+ const sql = model.objects
229
+ .sort((left, right) => left.ordinal - right.ordinal)
230
+ .map((object) => object.sql)
231
+ .join(";\n");
232
+ await applySql(databaseUrl, sql, "per-statement");
233
+ }
234
+ /**
235
+ * "per-migration" wraps the whole file in one transaction to mirror runners
236
+ * like `supabase db push`; statement-by-statement autocommit would mask
237
+ * transactional failures such as using an enum value added in the same file
238
+ * or CREATE INDEX CONCURRENTLY inside a transaction block.
239
+ */
240
+ async function applySql(databaseUrl, sql, mode) {
241
+ if (mode === "per-migration") {
242
+ await applyMigrationSql(databaseUrl, sql);
243
+ return;
244
+ }
245
+ await applyPerStatementSql(databaseUrl, sql);
246
+ }
247
+ function errorMessage(error) {
248
+ if (error instanceof Error) {
249
+ return error.message;
250
+ }
251
+ return String(error);
252
+ }
253
+ function managedSchemaReferenced(message, managedSchemas) {
254
+ const lower = message.toLowerCase();
255
+ for (const schema of managedSchemas) {
256
+ if (lower.includes(`${schema.toLowerCase()}.`)) {
257
+ return schema;
258
+ }
259
+ }
260
+ return undefined;
261
+ }
@@ -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 &amp; accuracy — additive fixture (1 table)</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
29
+ <text x="1045.0" y="147.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
39
+ <text x="1045.0" y="185.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
48
+ <text x="1045.0" y="223.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
57
+ <text x="1045.0" y="261.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
66
+ <text x="1045.0" y="299.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
75
+ <text x="1045.0" y="337.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
84
+ <text x="1045.0" y="375.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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 — additive fixture (1 table)</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">0.10s</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">0.20s</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">0.50s</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">1.00s</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">2.00s</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">5.00s</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">10.0s</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="104.3" height="18" rx="4" fill="url(#supaGradient)" />
32
+ <line x1="425.5" y1="105" x2="425.5" 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">0.21s · p95 0.24s</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="131.9" height="18" rx="4" fill="url(#supaGradient)" />
37
+ <line x1="462.1" y1="139" x2="462.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">0.25s · p95 0.31s</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="529.9" height="18" rx="4" fill="url(#slateGradient)" />
41
+ <line x1="861.1" y1="173" x2="861.1" 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">4.04s · p95 5.01s</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-pgadmin</text>
44
+ <rect x="300" y="210" width="531.5" height="18" rx="4" fill="url(#slateGradient)" />
45
+ <line x1="853.1" y1="207" x2="853.1" 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">4.08s · p95 4.74s</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-default</text>
48
+ <rect x="300" y="244" width="532.2" height="18" rx="4" fill="url(#slateGradient)" />
49
+ <line x1="941.4" y1="241" x2="941.4" 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">4.10s · p95 8.78s</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-pg-delta</text>
52
+ <rect x="300" y="278" width="538.8" height="18" rx="4" fill="url(#slateGradient)" />
53
+ <line x1="842.0" y1="275" x2="842.0" 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">4.29s · p95 4.39s</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-pg-schema</text>
56
+ <rect x="300" y="312" width="546.6" height="18" rx="4" fill="url(#slateGradient)" />
57
+ <line x1="851.9" y1="309" x2="851.9" 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">4.53s · p95 4.70s</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 &amp; accuracy — functions-policies fixture (1 table)</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
29
+ <text x="1045.0" y="147.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
39
+ <text x="1045.0" y="185.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
44
+ <text x="670.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>
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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
48
+ <text x="1045.0" y="223.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
53
+ <text x="670.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>
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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
57
+ <text x="1045.0" y="261.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
62
+ <text x="670.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>
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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
66
+ <text x="1045.0" y="299.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
71
+ <text x="670.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>
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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
75
+ <text x="1045.0" y="337.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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(16,185,129,0.14)" stroke="#10b981" stroke-opacity="0.55" />
80
+ <text x="670.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>
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(100,116,139,0.12)" stroke="#64748b" stroke-opacity="0.55" />
84
+ <text x="1045.0" y="375.5" fill="#94a3b8" font-family="ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif" font-size="11.5" font-weight="600" text-anchor="middle">—</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>