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,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
applyMigrationSql,
|
|
9
|
+
applySql,
|
|
10
|
+
assertLocalDatabaseUrl,
|
|
11
|
+
catalogFingerprint,
|
|
12
|
+
createTemporaryDatabases,
|
|
13
|
+
dropTemporaryDatabases,
|
|
14
|
+
transferOwnership,
|
|
15
|
+
} from "./tools/compare-db.mjs";
|
|
16
|
+
import { discoverFixtures, materializeGeneratedFixtures } from "./tools/compare-fixtures.mjs";
|
|
17
|
+
import {
|
|
18
|
+
combineExecutions,
|
|
19
|
+
errorMessage,
|
|
20
|
+
failedResult,
|
|
21
|
+
preview,
|
|
22
|
+
redactSecrets,
|
|
23
|
+
skippedResult,
|
|
24
|
+
summary,
|
|
25
|
+
unsupportedResult,
|
|
26
|
+
} from "./tools/compare-report.mjs";
|
|
27
|
+
import { prepareSupabaseWorkdir } from "./tools/compare-supabase.mjs";
|
|
28
|
+
import { adapterAvailability, adapters } from "./tools/registry.js";
|
|
29
|
+
|
|
30
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const packageRoot = resolve(here, "..");
|
|
32
|
+
const { scoreDiffOutput } = await import(join(packageRoot, "dist/diff-score.js"));
|
|
33
|
+
const fixtureRoot = resolve(here, "fixtures");
|
|
34
|
+
const outputPath = resolve(
|
|
35
|
+
packageRoot,
|
|
36
|
+
process.env.SUPASCHEMA_COMPARE_OUT ?? "benchmarks/results/comparison.json",
|
|
37
|
+
);
|
|
38
|
+
const selectedTools = csvSet(process.env.SUPASCHEMA_COMPARE_TOOLS);
|
|
39
|
+
const selectedFixtures = csvSet(process.env.SUPASCHEMA_COMPARE_FIXTURES);
|
|
40
|
+
const iterations = numberEnv("SUPASCHEMA_COMPARE_ITERATIONS", 10);
|
|
41
|
+
const warmups = numberEnv("SUPASCHEMA_COMPARE_WARMUPS", 1);
|
|
42
|
+
const commandTimeoutMs = numberEnv("SUPASCHEMA_COMPARE_TIMEOUT_MS", 30_000);
|
|
43
|
+
const databaseUrl = process.env.SUPASCHEMA_COMPARE_DATABASE_URL;
|
|
44
|
+
|
|
45
|
+
if (databaseUrl) {
|
|
46
|
+
assertLocalDatabaseUrl(databaseUrl);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const startedAt = new Date().toISOString();
|
|
50
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "supaschema-compare-"));
|
|
51
|
+
const fixtures = [
|
|
52
|
+
...(await discoverFixtures(fixtureRoot)),
|
|
53
|
+
...(await materializeGeneratedFixtures(
|
|
54
|
+
tempRoot,
|
|
55
|
+
numberEnv("SUPASCHEMA_COMPARE_XL_TABLES", 0),
|
|
56
|
+
numberEnv("SUPASCHEMA_COMPARE_XXL_TABLES", 0),
|
|
57
|
+
)),
|
|
58
|
+
].sort((left, right) => left.name.localeCompare(right.name));
|
|
59
|
+
const results = [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
for (const fixture of fixtures) {
|
|
63
|
+
if (selectedFixtures && !selectedFixtures.has(fixture.name)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const contextBase = await prepareFixtureContext(fixture);
|
|
67
|
+
try {
|
|
68
|
+
await runFixtureAdapters(contextBase, fixture);
|
|
69
|
+
} finally {
|
|
70
|
+
await cleanupFixtureContext(contextBase);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
if (!process.env.SUPASCHEMA_COMPARE_KEEP_TEMP) {
|
|
75
|
+
await rm(tempRoot, { force: true, recursive: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runFixtureAdapters(contextBase, fixture) {
|
|
80
|
+
for (const adapter of adapters) {
|
|
81
|
+
if (selectedTools && !selectedTools.has(adapter.id)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const reason = await adapterSkipReason(adapter, contextBase);
|
|
85
|
+
if (reason) {
|
|
86
|
+
results.push(skippedResult(adapter, fixture, reason));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const unsupportedReason = adapter.unsupported?.(contextBase);
|
|
90
|
+
if (unsupportedReason) {
|
|
91
|
+
results.push(unsupportedResult(adapter, fixture, unsupportedReason));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
for (let index = -warmups; index < iterations; index += 1) {
|
|
95
|
+
const warmup = index < 0;
|
|
96
|
+
let context;
|
|
97
|
+
try {
|
|
98
|
+
context = await prepareRunContext(adapter, contextBase, index);
|
|
99
|
+
const result = await runAdapter(adapter, fixture, context, warmup, index);
|
|
100
|
+
if (!warmup) {
|
|
101
|
+
results.push(result);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (!warmup) {
|
|
105
|
+
results.push(failedResult(adapter, fixture, warmup, index, error));
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
if (context) {
|
|
109
|
+
await cleanupRunContext(context);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const payload = {
|
|
117
|
+
completedAt: new Date().toISOString(),
|
|
118
|
+
environment: {
|
|
119
|
+
arch: process.arch,
|
|
120
|
+
databaseEnabled: Boolean(databaseUrl),
|
|
121
|
+
iterations,
|
|
122
|
+
node: process.version,
|
|
123
|
+
platform: process.platform,
|
|
124
|
+
commandTimeoutMs,
|
|
125
|
+
toolVersions: await collectToolVersions(),
|
|
126
|
+
warmups,
|
|
127
|
+
},
|
|
128
|
+
generatedBy: "supaschema bench:compare",
|
|
129
|
+
results,
|
|
130
|
+
startedAt,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
134
|
+
await writeFile(outputPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
135
|
+
process.stdout.write(`${outputPath}\n`);
|
|
136
|
+
process.stdout.write(`${summary(payload)}\n`);
|
|
137
|
+
|
|
138
|
+
async function prepareFixtureContext(fixture) {
|
|
139
|
+
const fromSql = await readFile(fixture.fromSqlPath, "utf8");
|
|
140
|
+
const toSql = await readFile(fixture.toSqlPath, "utf8");
|
|
141
|
+
const context = {
|
|
142
|
+
env: process.env,
|
|
143
|
+
fixture,
|
|
144
|
+
fromSql,
|
|
145
|
+
fromSqlPath: fixture.fromSqlPath,
|
|
146
|
+
packageRoot,
|
|
147
|
+
supaschemaAdapter: fixture.supaschemaAdapter,
|
|
148
|
+
schemas: fixture.schemas,
|
|
149
|
+
tempRoot,
|
|
150
|
+
toDirectory: fixture.toDirectory,
|
|
151
|
+
toSql,
|
|
152
|
+
toSqlPath: fixture.toSqlPath,
|
|
153
|
+
};
|
|
154
|
+
// Seed each fixture state once into a template database; per-run databases
|
|
155
|
+
// clone the template (CREATE DATABASE ... TEMPLATE), which turns large-
|
|
156
|
+
// schema seeding from minutes of statement replay into a file copy.
|
|
157
|
+
if (databaseUrl) {
|
|
158
|
+
const urls = await createTemporaryDatabases(databaseUrl, 2);
|
|
159
|
+
context.templateDatabaseUrls = urls;
|
|
160
|
+
context.templateFromName = basename(new URL(urls[0]).pathname);
|
|
161
|
+
context.templateToName = basename(new URL(urls[1]).pathname);
|
|
162
|
+
// Objects must not be owned by supabase_admin: the Supabase CLI's diff
|
|
163
|
+
// engines silently omit supabase_admin-owned objects (empty diff, exit 0).
|
|
164
|
+
const seedRole = process.env.SUPASCHEMA_COMPARE_SEED_ROLE;
|
|
165
|
+
await applySql(urls[0], fromSql);
|
|
166
|
+
await applySql(urls[1], toSql);
|
|
167
|
+
if (seedRole) {
|
|
168
|
+
await transferOwnership(urls[0], seedRole);
|
|
169
|
+
await transferOwnership(urls[1], seedRole);
|
|
170
|
+
}
|
|
171
|
+
context.targetFingerprint = await catalogFingerprint(urls[1]);
|
|
172
|
+
}
|
|
173
|
+
return context;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function cleanupFixtureContext(context) {
|
|
177
|
+
await dropTemporaryDatabases(databaseUrl, context.templateDatabaseUrls ?? []);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function prepareRunContext(adapter, base, iteration) {
|
|
181
|
+
const runRoot = await mkdtemp(join(tempRoot, `${base.fixture.name}-${adapter.id}-${iteration}-`));
|
|
182
|
+
const context = {
|
|
183
|
+
...base,
|
|
184
|
+
fromDatabaseUrl: undefined,
|
|
185
|
+
outputPath: join(runRoot, `${adapter.id}.${adapter.output === "xml" ? "xml" : "sql"}`),
|
|
186
|
+
runRoot,
|
|
187
|
+
toDatabaseUrl: undefined,
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
await prepareSupabaseWorkdir(context, adapter, iteration);
|
|
191
|
+
if (base.supaschemaAdapter && adapter.id.startsWith("supaschema")) {
|
|
192
|
+
context.supaschemaConfigPath = join(runRoot, "supaschema.config.json");
|
|
193
|
+
await writeFile(
|
|
194
|
+
context.supaschemaConfigPath,
|
|
195
|
+
`${JSON.stringify({ adapter: base.supaschemaAdapter })}\n`,
|
|
196
|
+
"utf8",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (adapter.requiresDatabase) {
|
|
200
|
+
const urls = [
|
|
201
|
+
...(await createTemporaryDatabases(databaseUrl, 1, base.templateFromName)),
|
|
202
|
+
...(await createTemporaryDatabases(databaseUrl, 1, base.templateToName)),
|
|
203
|
+
];
|
|
204
|
+
context.fromDatabaseUrl = urls[0];
|
|
205
|
+
context.toDatabaseUrl = urls[1];
|
|
206
|
+
context.createdDatabaseUrls = urls;
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
await cleanupRunContext(context).catch(() => undefined);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
return context;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function cleanupRunContext(context) {
|
|
216
|
+
if (!process.env.SUPASCHEMA_COMPARE_KEEP_TEMP) {
|
|
217
|
+
await dropTemporaryDatabases(databaseUrl, context.createdDatabaseUrls ?? []);
|
|
218
|
+
await rm(context.runRoot, { force: true, recursive: true });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function runAdapter(adapter, fixture, context, warmup, iteration) {
|
|
223
|
+
const commandSpec = await adapter.command(context);
|
|
224
|
+
const started = performance.now();
|
|
225
|
+
let execution;
|
|
226
|
+
let attempts = 0;
|
|
227
|
+
let elapsedMs = 0;
|
|
228
|
+
const executions = [];
|
|
229
|
+
const maxAttempts = adapter.maxAttempts ?? 2;
|
|
230
|
+
while (attempts < maxAttempts) {
|
|
231
|
+
if (attempts > 0 && adapter.retryDelayMs) {
|
|
232
|
+
await sleep(adapter.retryDelayMs);
|
|
233
|
+
}
|
|
234
|
+
await rm(context.outputPath, { force: true });
|
|
235
|
+
const attemptStarted = performance.now();
|
|
236
|
+
execution = await exec(commandSpec);
|
|
237
|
+
// Latency claims use the final attempt only; retry sleeps and failed
|
|
238
|
+
// environmental attempts (port conflicts) land in totalElapsedMs.
|
|
239
|
+
elapsedMs = performance.now() - attemptStarted;
|
|
240
|
+
attempts += 1;
|
|
241
|
+
executions.push(execution);
|
|
242
|
+
if (!adapter.retryOnFailure?.(execution)) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
execution = combineExecutions(executions);
|
|
247
|
+
const totalElapsedMs = performance.now() - started;
|
|
248
|
+
const commandFailed = adapterCommandFailed(adapter, execution);
|
|
249
|
+
const generatedOutput = commandFailed ? "" : await commandOutput(context, execution);
|
|
250
|
+
const generatedSql = adapter.output === "sql" ? sqlOutput(generatedOutput) : "";
|
|
251
|
+
const verification =
|
|
252
|
+
adapter.output === "sql" && generatedSql.trim()
|
|
253
|
+
? await verifyGeneratedSql(context, generatedSql)
|
|
254
|
+
: {
|
|
255
|
+
appliesOnce: false,
|
|
256
|
+
appliesTwice: false,
|
|
257
|
+
matchesTargetAfterFirstApply: false,
|
|
258
|
+
matchesTargetAfterSecondApply: false,
|
|
259
|
+
matchesTargetFingerprint: false,
|
|
260
|
+
reason: adapter.output === "sql" ? "no SQL output" : `output format ${adapter.output}`,
|
|
261
|
+
};
|
|
262
|
+
const outputScore =
|
|
263
|
+
fixture.manifest && generatedSql.trim()
|
|
264
|
+
? await scoreDiffOutput(generatedSql, fixture.manifest)
|
|
265
|
+
: undefined;
|
|
266
|
+
return {
|
|
267
|
+
adapter: adapter.id,
|
|
268
|
+
...(outputScore && {
|
|
269
|
+
outputExcessSample: outputScore.excess.slice(0, 8),
|
|
270
|
+
outputF1: Number(outputScore.f1.toFixed(3)),
|
|
271
|
+
outputMissedSample: outputScore.missed.slice(0, 8),
|
|
272
|
+
outputPrecision: Number(outputScore.precision.toFixed(3)),
|
|
273
|
+
outputRecall: Number(outputScore.recall.toFixed(3)),
|
|
274
|
+
}),
|
|
275
|
+
appliesOnce: verification.appliesOnce,
|
|
276
|
+
appliesTwice: verification.appliesTwice,
|
|
277
|
+
attempts,
|
|
278
|
+
command: redactSecrets([commandSpec.command, ...commandSpec.args].join(" ")),
|
|
279
|
+
commandFailed,
|
|
280
|
+
elapsedMs: Math.round(elapsedMs),
|
|
281
|
+
exitCode: execution.exitCode,
|
|
282
|
+
fixture: fixture.name,
|
|
283
|
+
matchesTargetAfterFirstApply: verification.matchesTargetAfterFirstApply,
|
|
284
|
+
matchesTargetAfterSecondApply: verification.matchesTargetAfterSecondApply,
|
|
285
|
+
matchesTargetFingerprint: verification.matchesTargetFingerprint,
|
|
286
|
+
mode: adapter.mode,
|
|
287
|
+
outputBytes: Buffer.byteLength(generatedOutput),
|
|
288
|
+
outputFormat: adapter.output,
|
|
289
|
+
skipped: false,
|
|
290
|
+
stderrBytes: Buffer.byteLength(execution.stderr),
|
|
291
|
+
stderrPreview: preview(redactSecrets(execution.stderr)),
|
|
292
|
+
stdoutPreview: preview(redactSecrets(generatedOutput)),
|
|
293
|
+
timedOut: execution.timedOut,
|
|
294
|
+
totalElapsedMs: Math.round(totalElapsedMs),
|
|
295
|
+
verificationReason: verification.reason,
|
|
296
|
+
warmup,
|
|
297
|
+
iteration,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function verifyGeneratedSql(context, generatedSql) {
|
|
302
|
+
if (!databaseUrl) {
|
|
303
|
+
return {
|
|
304
|
+
appliesOnce: false,
|
|
305
|
+
appliesTwice: false,
|
|
306
|
+
matchesTargetAfterFirstApply: false,
|
|
307
|
+
matchesTargetAfterSecondApply: false,
|
|
308
|
+
matchesTargetFingerprint: false,
|
|
309
|
+
reason: "set SUPASCHEMA_COMPARE_DATABASE_URL to verify generated SQL",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const [migrationUrl] = await createTemporaryDatabases(databaseUrl, 1, context.templateFromName);
|
|
313
|
+
try {
|
|
314
|
+
if (!context.templateFromName) {
|
|
315
|
+
await applySql(migrationUrl, context.fromSql);
|
|
316
|
+
}
|
|
317
|
+
await applyMigrationSql(migrationUrl, generatedSql);
|
|
318
|
+
const appliesOnce = true;
|
|
319
|
+
const firstFingerprint = await catalogFingerprint(migrationUrl);
|
|
320
|
+
const matchesTargetAfterFirstApply = context.targetFingerprint
|
|
321
|
+
? firstFingerprint === context.targetFingerprint
|
|
322
|
+
: false;
|
|
323
|
+
let appliesTwice = false;
|
|
324
|
+
let matchesTargetAfterSecondApply = false;
|
|
325
|
+
let secondApplyReason;
|
|
326
|
+
try {
|
|
327
|
+
await applyMigrationSql(migrationUrl, generatedSql);
|
|
328
|
+
appliesTwice = true;
|
|
329
|
+
const secondFingerprint = await catalogFingerprint(migrationUrl);
|
|
330
|
+
matchesTargetAfterSecondApply = context.targetFingerprint
|
|
331
|
+
? secondFingerprint === context.targetFingerprint
|
|
332
|
+
: false;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
appliesTwice = false;
|
|
335
|
+
secondApplyReason = `second apply failed: ${errorMessage(error)}`;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
appliesOnce,
|
|
339
|
+
appliesTwice,
|
|
340
|
+
matchesTargetAfterFirstApply,
|
|
341
|
+
matchesTargetAfterSecondApply,
|
|
342
|
+
matchesTargetFingerprint:
|
|
343
|
+
matchesTargetAfterFirstApply && appliesTwice && matchesTargetAfterSecondApply,
|
|
344
|
+
reason: secondApplyReason,
|
|
345
|
+
};
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return {
|
|
348
|
+
appliesOnce: false,
|
|
349
|
+
appliesTwice: false,
|
|
350
|
+
matchesTargetAfterFirstApply: false,
|
|
351
|
+
matchesTargetAfterSecondApply: false,
|
|
352
|
+
matchesTargetFingerprint: false,
|
|
353
|
+
reason: errorMessage(error),
|
|
354
|
+
};
|
|
355
|
+
} finally {
|
|
356
|
+
await dropTemporaryDatabases(databaseUrl, [migrationUrl]);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function adapterSkipReason(adapter, context) {
|
|
361
|
+
if (adapter.requiresDatabase && !databaseUrl) {
|
|
362
|
+
return "set SUPASCHEMA_COMPARE_DATABASE_URL to enable database-backed comparisons";
|
|
363
|
+
}
|
|
364
|
+
const availability = await adapterAvailability(adapter);
|
|
365
|
+
if (!availability.available) {
|
|
366
|
+
return availability.reason;
|
|
367
|
+
}
|
|
368
|
+
return adapter.skip?.(context);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function adapterCommandFailed(adapter, execution) {
|
|
372
|
+
if (execution.timedOut || execution.exitCode === 124) {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
if (execution.exitCode === 0) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
return !adapter.acceptsExitCode?.(execution);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function exec({ args, command, stdin }) {
|
|
382
|
+
return new Promise((resolve) => {
|
|
383
|
+
const child = spawn(command, args, {
|
|
384
|
+
cwd: packageRoot,
|
|
385
|
+
env: process.env,
|
|
386
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
387
|
+
});
|
|
388
|
+
let stdout = "";
|
|
389
|
+
let stderr = "";
|
|
390
|
+
let resolved = false;
|
|
391
|
+
let timedOut = false;
|
|
392
|
+
const timeout = setTimeout(() => {
|
|
393
|
+
timedOut = true;
|
|
394
|
+
child.kill("SIGTERM");
|
|
395
|
+
setTimeout(() => child.kill("SIGKILL"), 2_000).unref();
|
|
396
|
+
}, commandTimeoutMs);
|
|
397
|
+
timeout.unref();
|
|
398
|
+
function finish(result) {
|
|
399
|
+
if (resolved) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
resolved = true;
|
|
403
|
+
clearTimeout(timeout);
|
|
404
|
+
resolve(result);
|
|
405
|
+
}
|
|
406
|
+
child.stdout.setEncoding("utf8");
|
|
407
|
+
child.stderr.setEncoding("utf8");
|
|
408
|
+
child.stdout.on("data", (chunk) => {
|
|
409
|
+
stdout += chunk;
|
|
410
|
+
});
|
|
411
|
+
child.stderr.on("data", (chunk) => {
|
|
412
|
+
stderr += chunk;
|
|
413
|
+
});
|
|
414
|
+
child.on("error", (error) => {
|
|
415
|
+
finish({ exitCode: 127, stderr: errorMessage(error), stdout: "", timedOut });
|
|
416
|
+
});
|
|
417
|
+
child.on("close", (exitCode, signal) => {
|
|
418
|
+
const resolvedExitCode = exitCode ?? (timedOut ? 124 : 1);
|
|
419
|
+
const timeoutMessage = timedOut ? `\nTimed out after ${commandTimeoutMs}ms (${signal})` : "";
|
|
420
|
+
finish({
|
|
421
|
+
exitCode: resolvedExitCode,
|
|
422
|
+
stderr: `${stderr}${timeoutMessage}`,
|
|
423
|
+
stdout,
|
|
424
|
+
timedOut,
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
if (stdin) {
|
|
428
|
+
child.stdin.end(stdin);
|
|
429
|
+
} else {
|
|
430
|
+
child.stdin.end();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function sqlOutput(stdout) {
|
|
436
|
+
return stdout
|
|
437
|
+
.split("\n")
|
|
438
|
+
.filter((line) => !line.startsWith("Connecting to"))
|
|
439
|
+
.join("\n")
|
|
440
|
+
.trim();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function commandOutput(context, execution) {
|
|
444
|
+
if (execution.stdout.trim()) {
|
|
445
|
+
return execution.stdout;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
return await readFile(context.outputPath, "utf8");
|
|
449
|
+
} catch {
|
|
450
|
+
return execution.stdout;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function csvSet(value) {
|
|
455
|
+
if (!value) {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
return new Set(
|
|
459
|
+
value
|
|
460
|
+
.split(",")
|
|
461
|
+
.map((item) => item.trim())
|
|
462
|
+
.filter(Boolean),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function numberEnv(name, fallback) {
|
|
467
|
+
const raw = process.env[name];
|
|
468
|
+
return raw === undefined ? fallback : Number(raw);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function sleep(ms) {
|
|
472
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function collectToolVersions() {
|
|
476
|
+
const packageJson = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
|
|
477
|
+
return {
|
|
478
|
+
supaschema: packageJson.version,
|
|
479
|
+
supabase: await commandVersion("supabase", ["--version"]),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function commandVersion(command, args) {
|
|
484
|
+
const result = await exec({ args, command });
|
|
485
|
+
if (result.exitCode !== 0) {
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
return result.stdout.trim() || result.stderr.trim() || undefined;
|
|
489
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[{ "change": "change", "key": "table:app.accounts" }]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
CREATE SCHEMA app;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE app.accounts (
|
|
4
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
5
|
+
name text NOT NULL
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
ALTER TABLE app.accounts ENABLE ROW LEVEL SECURITY;
|
|
9
|
+
|
|
10
|
+
CREATE POLICY accounts_select ON app.accounts
|
|
11
|
+
FOR SELECT
|
|
12
|
+
TO public
|
|
13
|
+
USING (true);
|
|
14
|
+
|
|
15
|
+
CREATE OR REPLACE FUNCTION app.greeting()
|
|
16
|
+
RETURNS text
|
|
17
|
+
LANGUAGE sql
|
|
18
|
+
STABLE
|
|
19
|
+
AS $$
|
|
20
|
+
SELECT 'hello'::text
|
|
21
|
+
$$;
|
|
22
|
+
|
|
23
|
+
CREATE VIEW app.account_names AS
|
|
24
|
+
SELECT name FROM app.accounts;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
CREATE SCHEMA app;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE app.accounts (
|
|
4
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
5
|
+
name text NOT NULL
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
ALTER TABLE app.accounts ENABLE ROW LEVEL SECURITY;
|
|
9
|
+
|
|
10
|
+
CREATE POLICY accounts_select ON app.accounts
|
|
11
|
+
FOR SELECT
|
|
12
|
+
TO public
|
|
13
|
+
USING (name IS NOT NULL);
|
|
14
|
+
|
|
15
|
+
CREATE OR REPLACE FUNCTION app.greeting()
|
|
16
|
+
RETURNS text
|
|
17
|
+
LANGUAGE sql
|
|
18
|
+
STABLE
|
|
19
|
+
AS $$
|
|
20
|
+
SELECT 'hi'::text
|
|
21
|
+
$$;
|
|
22
|
+
|
|
23
|
+
CREATE VIEW app.account_names AS
|
|
24
|
+
SELECT upper(name) AS name FROM app.accounts;
|