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,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { fixtureScale, formatSeconds, groupedStats, isWorkflow, percentile } from "./plot-lib.js";
|
|
5
|
+
import { renderCorrectnessSvg, renderLatencySvg, renderScalingSvg } from "./plot-svg.js";
|
|
6
|
+
|
|
7
|
+
const jsonInputs = process.argv.slice(2).filter((arg) => arg.endsWith(".json"));
|
|
8
|
+
const dirArgs = process.argv.slice(2).filter((arg) => !arg.endsWith(".json"));
|
|
9
|
+
const inputs = (jsonInputs.length > 0 ? jsonInputs : ["benchmarks/results/comparison.json"]).map(
|
|
10
|
+
(path) => resolve(path),
|
|
11
|
+
);
|
|
12
|
+
const outputDir = resolve(dirArgs[0] ?? "benchmarks/results");
|
|
13
|
+
const allResults = [];
|
|
14
|
+
const environments = [];
|
|
15
|
+
for (const input of inputs) {
|
|
16
|
+
const payload = JSON.parse(await readFile(input, "utf8"));
|
|
17
|
+
allResults.push(...payload.results);
|
|
18
|
+
environments.push({
|
|
19
|
+
arch: payload.environment.arch,
|
|
20
|
+
completedAt: payload.completedAt,
|
|
21
|
+
fixtures: [...new Set(payload.results.map((item) => item.fixture))].sort(),
|
|
22
|
+
iterations: payload.environment.iterations,
|
|
23
|
+
node: payload.environment.node,
|
|
24
|
+
platform: payload.environment.platform,
|
|
25
|
+
source: basename(input),
|
|
26
|
+
toolVersions: payload.environment.toolVersions,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const fixtures = [...new Set(allResults.map((item) => item.fixture))].sort(
|
|
30
|
+
(left, right) =>
|
|
31
|
+
(fixtureScale[left]?.order ?? 99) - (fixtureScale[right]?.order ?? 99) ||
|
|
32
|
+
left.localeCompare(right),
|
|
33
|
+
);
|
|
34
|
+
await mkdir(outputDir, { recursive: true });
|
|
35
|
+
const written = [];
|
|
36
|
+
const diffResults = allResults.filter((item) => !isWorkflow(item.adapter));
|
|
37
|
+
const workflowResults = allResults.filter((item) => isWorkflow(item.adapter));
|
|
38
|
+
for (const fixture of fixtures) {
|
|
39
|
+
const rows = allResults.filter((item) => item.fixture === fixture);
|
|
40
|
+
const diffRows = diffResults.filter((item) => item.fixture === fixture);
|
|
41
|
+
const measuredRows = diffRows.filter((item) => !item.skipped && !item.unsupported);
|
|
42
|
+
const latencyPath = join(outputDir, `${fixture}-latency.svg`);
|
|
43
|
+
const correctnessPath = join(outputDir, `${fixture}-correctness.svg`);
|
|
44
|
+
const resultsPath = join(outputDir, `${fixture}-results.json`);
|
|
45
|
+
if (measuredRows.length > 0) {
|
|
46
|
+
await writeFile(latencyPath, renderLatencySvg(measuredRows, fixture, environments), "utf8");
|
|
47
|
+
await writeFile(correctnessPath, renderCorrectnessSvg(diffRows, fixture, environments), "utf8");
|
|
48
|
+
written.push(latencyPath, correctnessPath);
|
|
49
|
+
}
|
|
50
|
+
await writeFile(
|
|
51
|
+
resultsPath,
|
|
52
|
+
`${JSON.stringify({ environments, fixture, results: rows }, null, 2)}\n`,
|
|
53
|
+
"utf8",
|
|
54
|
+
);
|
|
55
|
+
written.push(resultsPath);
|
|
56
|
+
}
|
|
57
|
+
const measuredDiffResults = diffResults.filter((item) => !item.skipped && !item.unsupported);
|
|
58
|
+
if (fixtures.length > 1 && measuredDiffResults.length > 0) {
|
|
59
|
+
const scalingPath = join(outputDir, "scaling-latency.svg");
|
|
60
|
+
await writeFile(scalingPath, renderScalingSvg(diffResults, fixtures, environments), "utf8");
|
|
61
|
+
written.push(scalingPath);
|
|
62
|
+
}
|
|
63
|
+
const measuredWorkflowResults = workflowResults.filter(
|
|
64
|
+
(item) => !item.skipped && !item.unsupported,
|
|
65
|
+
);
|
|
66
|
+
if (fixtures.length > 1 && measuredWorkflowResults.length > 0) {
|
|
67
|
+
const workflowPath = join(outputDir, "workflow-latency.svg");
|
|
68
|
+
await writeFile(
|
|
69
|
+
workflowPath,
|
|
70
|
+
renderScalingSvg(workflowResults, fixtures, environments, {
|
|
71
|
+
subtitle:
|
|
72
|
+
"supaschema: one diff = migration + TS types + Zod · Supabase engines: db diff + apply + gen types · log scale",
|
|
73
|
+
title: "Full workflow: migration + regenerated types",
|
|
74
|
+
}),
|
|
75
|
+
"utf8",
|
|
76
|
+
);
|
|
77
|
+
written.push(workflowPath);
|
|
78
|
+
}
|
|
79
|
+
const summaryPath = join(outputDir, "summary.md");
|
|
80
|
+
await writeFile(summaryPath, renderSummaryMarkdown(), "utf8");
|
|
81
|
+
written.push(summaryPath);
|
|
82
|
+
for (const path of written) {
|
|
83
|
+
process.stdout.write(`${path}\n`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderSummaryMarkdown() {
|
|
87
|
+
const lines = [
|
|
88
|
+
"# Comparison Summary",
|
|
89
|
+
"",
|
|
90
|
+
"Generated by `npm run bench:plot`. One section per fixture (sample size); scaling ratios compare each tool's median against its own smallest-fixture median, so growth with schema size is the per-tool regression curve. All durations are seconds.",
|
|
91
|
+
"",
|
|
92
|
+
"## Source Runs",
|
|
93
|
+
"",
|
|
94
|
+
];
|
|
95
|
+
for (const env of environments) {
|
|
96
|
+
lines.push(
|
|
97
|
+
`- \`${env.source}\`: fixtures ${env.fixtures.join(", ")}; ${env.iterations} iterations; supabase ${env.toolVersions?.supabase ?? "n/a"}; completed ${env.completedAt}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
for (const fixture of fixtures) {
|
|
101
|
+
const rows = allResults.filter(
|
|
102
|
+
(item) => item.fixture === fixture && !item.skipped && !item.unsupported,
|
|
103
|
+
);
|
|
104
|
+
lines.push("", `## Fixture: ${fixture}`, "");
|
|
105
|
+
lines.push(
|
|
106
|
+
"| Tool | Mode | Runs | Median | p95 | Applies once | Applies twice | Fully verified | Output F1 |",
|
|
107
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
108
|
+
);
|
|
109
|
+
for (const group of groupedStats(rows)) {
|
|
110
|
+
const groupRows = rows.filter((item) => item.adapter === group.label);
|
|
111
|
+
const mode = groupRows[0]?.mode ?? "";
|
|
112
|
+
const once = groupRows.filter((item) => item.appliesOnce).length;
|
|
113
|
+
const twice = groupRows.filter((item) => item.appliesTwice).length;
|
|
114
|
+
const verified = groupRows.filter((item) => item.matchesTargetFingerprint).length;
|
|
115
|
+
const scored = groupRows.filter((item) => typeof item.outputF1 === "number");
|
|
116
|
+
const f1 =
|
|
117
|
+
scored.length > 0
|
|
118
|
+
? (scored.reduce((sum, item) => sum + item.outputF1, 0) / scored.length).toFixed(3)
|
|
119
|
+
: "—";
|
|
120
|
+
lines.push(
|
|
121
|
+
`| ${group.label} | ${mode} | ${groupRows.length} | ${formatSeconds(group.median)} | ${formatSeconds(group.p95)} | ${once}/${groupRows.length} | ${twice}/${groupRows.length} | ${verified}/${groupRows.length} | ${f1} |`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
lines.push("", "## Scaling Across Fixtures (median seconds, ratio vs own smallest)", "");
|
|
126
|
+
const adapters = [...new Set(allResults.map((item) => item.adapter))].sort();
|
|
127
|
+
lines.push(`| Tool | ${fixtures.join(" | ")} |`, `| --- |${" --- |".repeat(fixtures.length)}`);
|
|
128
|
+
for (const adapter of adapters) {
|
|
129
|
+
const medians = fixtures.map((fixture) => {
|
|
130
|
+
const values = allResults
|
|
131
|
+
.filter(
|
|
132
|
+
(item) =>
|
|
133
|
+
item.adapter === adapter &&
|
|
134
|
+
item.fixture === fixture &&
|
|
135
|
+
!item.skipped &&
|
|
136
|
+
!item.unsupported,
|
|
137
|
+
)
|
|
138
|
+
.map((item) => item.elapsedMs);
|
|
139
|
+
return values.length > 0 ? percentile(values, 0.5) : undefined;
|
|
140
|
+
});
|
|
141
|
+
const base = medians.find((value) => value !== undefined && value > 0);
|
|
142
|
+
const cells = medians.map((value) => {
|
|
143
|
+
if (value === undefined) {
|
|
144
|
+
return "—";
|
|
145
|
+
}
|
|
146
|
+
return base
|
|
147
|
+
? `${formatSeconds(value)} (${(value / base).toFixed(1)}x)`
|
|
148
|
+
: formatSeconds(value);
|
|
149
|
+
});
|
|
150
|
+
lines.push(`| ${adapter} | ${cells.join(" | ")} |`);
|
|
151
|
+
}
|
|
152
|
+
lines.push("");
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
cd "$(dirname "$0")/../.."
|
|
4
|
+
export SUPASCHEMA_COMPARE_DATABASE_URL=${SUPASCHEMA_COMPARE_DATABASE_URL:-postgresql://postgres:postgres@127.0.0.1:54322/postgres}
|
|
5
|
+
export SUPASCHEMA_COMPARE_WARMUPS=1
|
|
6
|
+
export SUPASCHEMA_COMPARE_TIMEOUT_MS=480000
|
|
7
|
+
|
|
8
|
+
run_small() {
|
|
9
|
+
SUPASCHEMA_COMPARE_FIXTURES=additive,functions-policies SUPASCHEMA_COMPARE_ITERATIONS=3 \
|
|
10
|
+
SUPASCHEMA_COMPARE_PORT_BASE=55400 \
|
|
11
|
+
SUPASCHEMA_COMPARE_OUT=benchmarks/results/comparison.json node benchmarks/compare.js >/dev/null
|
|
12
|
+
echo "DONE small-fixtures"
|
|
13
|
+
}
|
|
14
|
+
run_realistic() {
|
|
15
|
+
SUPASCHEMA_COMPARE_FIXTURES=realistic SUPASCHEMA_COMPARE_ITERATIONS=3 \
|
|
16
|
+
SUPASCHEMA_COMPARE_PORT_BASE=56400 \
|
|
17
|
+
SUPASCHEMA_COMPARE_OUT=benchmarks/results/comparison-realistic.json node benchmarks/compare.js >/dev/null
|
|
18
|
+
echo "DONE realistic"
|
|
19
|
+
}
|
|
20
|
+
run_xl() {
|
|
21
|
+
SUPASCHEMA_COMPARE_XL_TABLES=1000 SUPASCHEMA_COMPARE_FIXTURES=xl SUPASCHEMA_COMPARE_ITERATIONS=3 \
|
|
22
|
+
SUPASCHEMA_COMPARE_PORT_BASE=57400 \
|
|
23
|
+
SUPASCHEMA_COMPARE_OUT=benchmarks/results/comparison-xl.json node benchmarks/compare.js >/dev/null
|
|
24
|
+
echo "DONE xl"
|
|
25
|
+
}
|
|
26
|
+
run_xxl() {
|
|
27
|
+
SUPASCHEMA_COMPARE_XXL_TABLES=2500 SUPASCHEMA_COMPARE_FIXTURES=xxl SUPASCHEMA_COMPARE_ITERATIONS=1 \
|
|
28
|
+
SUPASCHEMA_COMPARE_PORT_BASE=58400 \
|
|
29
|
+
SUPASCHEMA_COMPARE_OUT=benchmarks/results/comparison-xxl.json node benchmarks/compare.js >/dev/null
|
|
30
|
+
echo "DONE xxl"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if [ "${BENCH_ALL_SEQUENTIAL:-0}" = "1" ]; then
|
|
34
|
+
run_small
|
|
35
|
+
run_realistic
|
|
36
|
+
run_xl
|
|
37
|
+
run_xxl
|
|
38
|
+
else
|
|
39
|
+
run_small &
|
|
40
|
+
run_realistic &
|
|
41
|
+
run_xl &
|
|
42
|
+
run_xxl &
|
|
43
|
+
wait
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
node benchmarks/plot.js benchmarks/results/comparison.json benchmarks/results/comparison-realistic.json \
|
|
47
|
+
benchmarks/results/comparison-xl.json benchmarks/results/comparison-xxl.json >/dev/null
|
|
48
|
+
cp benchmarks/results/*-latency.svg benchmarks/results/*-correctness.svg docs/benchmarks/
|
|
49
|
+
echo "DONE plot-and-charts"
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Client } from "pg";
|
|
7
|
+
|
|
8
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const packageRoot = resolve(here, "../..");
|
|
10
|
+
const { resolveDatabaseUrl } = await import(join(packageRoot, "dist/database-url.js"));
|
|
11
|
+
const { extractObjectsFromSql } = await import(join(packageRoot, "dist/sql/extract.js"));
|
|
12
|
+
const { splitSqlStatements } = await import(join(packageRoot, "dist/sql/split.js"));
|
|
13
|
+
|
|
14
|
+
const usage =
|
|
15
|
+
"usage: node benchmarks/tools/build-project-fixture.mjs --tree <schemas dir> --out <fixture dir> [--database-url <admin url>]";
|
|
16
|
+
|
|
17
|
+
const args = parseArgs(process.argv.slice(2));
|
|
18
|
+
const treeRoot = resolve(args.tree ?? "supabase/schemas");
|
|
19
|
+
const outDir = resolve(args.out ?? "benchmarks/fixtures/project");
|
|
20
|
+
const databaseUrl = resolveDatabaseUrl(args["database-url"]);
|
|
21
|
+
if (!databaseUrl) {
|
|
22
|
+
fail(
|
|
23
|
+
`${usage}\nno database URL: pass --database-url, set SUPASCHEMA_DATABASE_URL, or run near supabase/config.toml`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const phases = [
|
|
28
|
+
"schema",
|
|
29
|
+
"types",
|
|
30
|
+
"tables",
|
|
31
|
+
"constraints",
|
|
32
|
+
"functions",
|
|
33
|
+
"views",
|
|
34
|
+
"triggers",
|
|
35
|
+
"policies",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const authStub = `
|
|
39
|
+
CREATE SCHEMA IF NOT EXISTS auth;
|
|
40
|
+
CREATE TABLE IF NOT EXISTS auth.users (
|
|
41
|
+
id uuid PRIMARY KEY,
|
|
42
|
+
email text,
|
|
43
|
+
raw_app_meta_data jsonb,
|
|
44
|
+
raw_user_meta_data jsonb,
|
|
45
|
+
created_at timestamptz DEFAULT now()
|
|
46
|
+
);
|
|
47
|
+
CREATE OR REPLACE FUNCTION auth.uid() RETURNS uuid LANGUAGE sql STABLE AS $$
|
|
48
|
+
SELECT nullif(current_setting('request.jwt.claim.sub', true), '')::uuid
|
|
49
|
+
$$;
|
|
50
|
+
CREATE OR REPLACE FUNCTION auth.role() RETURNS text LANGUAGE sql STABLE AS $$
|
|
51
|
+
SELECT coalesce(nullif(current_setting('request.jwt.claim.role', true), ''), 'anon')
|
|
52
|
+
$$;
|
|
53
|
+
CREATE OR REPLACE FUNCTION auth.jwt() RETURNS jsonb LANGUAGE sql STABLE AS $$
|
|
54
|
+
SELECT coalesce(nullif(current_setting('request.jwt.claims', true), ''), '{}')::jsonb
|
|
55
|
+
$$;
|
|
56
|
+
CREATE OR REPLACE FUNCTION auth.email() RETURNS text LANGUAGE sql STABLE AS $$
|
|
57
|
+
SELECT nullif(current_setting('request.jwt.claim.email', true), '')
|
|
58
|
+
$$;
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const changeSet = `
|
|
62
|
+
CREATE TABLE IF NOT EXISTS public.supaschema_benchmark_probe (
|
|
63
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
64
|
+
label text NOT NULL,
|
|
65
|
+
recorded_at timestamptz NOT NULL DEFAULT now()
|
|
66
|
+
);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS supaschema_benchmark_probe_label_idx
|
|
68
|
+
ON public.supaschema_benchmark_probe (label);
|
|
69
|
+
COMMENT ON TABLE public.supaschema_benchmark_probe IS 'supaschema benchmark change-set probe';
|
|
70
|
+
CREATE OR REPLACE VIEW public.supaschema_benchmark_probe_view AS
|
|
71
|
+
SELECT id, label FROM public.supaschema_benchmark_probe WHERE id > 0;
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const files = orderedTreeFiles(treeRoot);
|
|
75
|
+
process.stdout.write(`tree files: ${files.length}\n`);
|
|
76
|
+
const rawSql = files.map((file) => readFileSync(file, "utf8")).join("\n");
|
|
77
|
+
const statements = [...splitSqlStatements(authStub), ...splitSqlStatements(rawSql)];
|
|
78
|
+
process.stdout.write(`statements (with auth stub): ${statements.length}\n`);
|
|
79
|
+
|
|
80
|
+
const firstPass = await fixpointApply(statements);
|
|
81
|
+
const dropped = [...firstPass.dropped];
|
|
82
|
+
process.stdout.write(
|
|
83
|
+
`single-pass ordering: ${firstPass.ordered.length} statements; dropped: ${dropped.length}\n`,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Scope the fixture to supaschema's supported DDL surface so every engine in
|
|
87
|
+
// the comparison diffs the same schema. Statements supaschema fails closed on
|
|
88
|
+
// (FDW objects, DO blocks, unmodeled ALTER subtypes, duplicate object
|
|
89
|
+
// identities such as two partial REVOKEs for one grantee) are excluded and
|
|
90
|
+
// logged, then the fixpoint re-runs so dependents drop with them.
|
|
91
|
+
const kept = [];
|
|
92
|
+
const seenKeys = new Set();
|
|
93
|
+
let excluded = 0;
|
|
94
|
+
for (const statement of firstPass.ordered) {
|
|
95
|
+
const extraction = await extractObjectsFromSql(statement, { config: { adapter: "postgres" } });
|
|
96
|
+
if (extraction.diagnostics.some((item) => item.severity === "error")) {
|
|
97
|
+
dropped.push({ error: "supaschema support-matrix exclusion", sql: statement });
|
|
98
|
+
excluded += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const duplicate = extraction.objects.find((object) => seenKeys.has(object.key));
|
|
102
|
+
if (duplicate) {
|
|
103
|
+
dropped.push({ error: `duplicate object identity ${duplicate.key}`, sql: statement });
|
|
104
|
+
excluded += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
for (const object of extraction.objects) {
|
|
108
|
+
seenKeys.add(object.key);
|
|
109
|
+
}
|
|
110
|
+
kept.push(statement);
|
|
111
|
+
}
|
|
112
|
+
process.stdout.write(`support-surface filter: excluded ${excluded} statements\n`);
|
|
113
|
+
let ordered = kept;
|
|
114
|
+
if (excluded > 0) {
|
|
115
|
+
const secondPass = await fixpointApply(kept);
|
|
116
|
+
ordered = secondPass.ordered;
|
|
117
|
+
dropped.push(
|
|
118
|
+
...secondPass.dropped.map((entry) => ({
|
|
119
|
+
error: `orphaned by support-surface filter: ${entry.error}`,
|
|
120
|
+
sql: entry.sql,
|
|
121
|
+
})),
|
|
122
|
+
);
|
|
123
|
+
process.stdout.write(
|
|
124
|
+
`post-filter ordering: ${ordered.length} statements; orphaned dependents dropped: ${secondPass.dropped.length}\n`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function fixpointApply(inputStatements) {
|
|
129
|
+
const admin = new Client({ connectionString: databaseUrl });
|
|
130
|
+
await admin.connect();
|
|
131
|
+
const probeDb = `supa_fixture_build_${process.pid}`;
|
|
132
|
+
await admin.query(`DROP DATABASE IF EXISTS ${probeDb} WITH (FORCE)`);
|
|
133
|
+
await admin.query(`CREATE DATABASE ${probeDb}`);
|
|
134
|
+
const probeUrl = new URL(databaseUrl);
|
|
135
|
+
probeUrl.pathname = `/${probeDb}`;
|
|
136
|
+
const applied = [];
|
|
137
|
+
const failed = [];
|
|
138
|
+
try {
|
|
139
|
+
const client = new Client({ connectionString: probeUrl.toString() });
|
|
140
|
+
await client.connect();
|
|
141
|
+
try {
|
|
142
|
+
let pending = inputStatements.map((sql) => ({ error: "", sql }));
|
|
143
|
+
for (let pass = 0; pending.length > 0; pass += 1) {
|
|
144
|
+
const next = [];
|
|
145
|
+
for (const entry of pending) {
|
|
146
|
+
try {
|
|
147
|
+
await client.query(entry.sql);
|
|
148
|
+
applied.push(entry.sql);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
next.push({ error: error.message, sql: entry.sql });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (next.length === pending.length) {
|
|
154
|
+
failed.push(...next);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
pending = next;
|
|
158
|
+
process.stdout.write(
|
|
159
|
+
`pass ${pass + 1}: ${applied.length} applied, ${pending.length} pending\n`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
await client.end();
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
await admin.query(`DROP DATABASE IF EXISTS ${probeDb} WITH (FORCE)`);
|
|
167
|
+
await admin.end();
|
|
168
|
+
}
|
|
169
|
+
return { dropped: failed, ordered: applied };
|
|
170
|
+
}
|
|
171
|
+
const droppedClasses = new Map();
|
|
172
|
+
for (const entry of dropped) {
|
|
173
|
+
const key = entry.error.slice(0, 70);
|
|
174
|
+
droppedClasses.set(key, (droppedClasses.get(key) ?? 0) + 1);
|
|
175
|
+
}
|
|
176
|
+
for (const [message, count] of [...droppedClasses.entries()].sort((a, b) => b[1] - a[1])) {
|
|
177
|
+
process.stdout.write(` dropped ${String(count).padStart(4)} | ${message}\n`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fromSql = `${ordered.join(";\n")};\n`;
|
|
181
|
+
const toSql = `${fromSql}\n${changeSet.trim()}\n`;
|
|
182
|
+
mkdirSync(outDir, { recursive: true });
|
|
183
|
+
writeFileSync(join(outDir, "from.sql"), fromSql);
|
|
184
|
+
writeFileSync(join(outDir, "to.sql"), toSql);
|
|
185
|
+
writeFileSync(
|
|
186
|
+
join(outDir, "fixture.json"),
|
|
187
|
+
`${JSON.stringify({ supaschemaAdapter: "postgres", schemas: treeSchemas(fromSql) }, null, 2)}\n`,
|
|
188
|
+
);
|
|
189
|
+
writeFileSync(
|
|
190
|
+
join(outDir, "dropped.log"),
|
|
191
|
+
dropped.map((entry) => `-- ${entry.error}\n${entry.sql};\n`).join("\n"),
|
|
192
|
+
);
|
|
193
|
+
process.stdout.write(`fixture written to ${outDir}\n`);
|
|
194
|
+
|
|
195
|
+
function orderedTreeFiles(root) {
|
|
196
|
+
const list = (predicate) =>
|
|
197
|
+
execFileSync("find", [root, "-name", "*.sql"], { encoding: "utf8" })
|
|
198
|
+
.trim()
|
|
199
|
+
.split("\n")
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
.filter(predicate)
|
|
202
|
+
.sort();
|
|
203
|
+
const bootstrap = list((file) => file.includes("_bootstrap"));
|
|
204
|
+
const phased = phases.flatMap((phase) =>
|
|
205
|
+
list((file) => !file.includes("_bootstrap") && file.endsWith(`/${phase}.sql`)),
|
|
206
|
+
);
|
|
207
|
+
const phaseNames = new Set(phases.map((phase) => `${phase}.sql`));
|
|
208
|
+
const remainder = list(
|
|
209
|
+
(file) => !file.includes("_bootstrap") && !phaseNames.has(file.split("/").at(-1) ?? ""),
|
|
210
|
+
);
|
|
211
|
+
return [...bootstrap, ...phased, ...remainder];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function treeSchemas(sql) {
|
|
215
|
+
// `public` always exists and is never CREATE'd, but project objects (and
|
|
216
|
+
// the benchmark change set) live there.
|
|
217
|
+
const names = new Set(["public"]);
|
|
218
|
+
for (const statement of splitSqlStatements(sql)) {
|
|
219
|
+
const trimmed = statement.replace(/^CREATE SCHEMA (?:IF NOT EXISTS )?/i, "");
|
|
220
|
+
if (trimmed !== statement) {
|
|
221
|
+
const name = trimmed.split(/\s/)[0]?.replaceAll('"', "");
|
|
222
|
+
if (name && !["auth", "extensions", "vault", "graphql", "graphql_public"].includes(name)) {
|
|
223
|
+
names.add(name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return [...names].sort();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseArgs(argv) {
|
|
231
|
+
const parsed = {};
|
|
232
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
233
|
+
const key = argv[index];
|
|
234
|
+
if (key?.startsWith("--")) {
|
|
235
|
+
parsed[key.slice(2)] = argv[index + 1];
|
|
236
|
+
index += 1;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return parsed;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function fail(message) {
|
|
243
|
+
process.stderr.write(`${message}\n`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { Client } from "pg";
|
|
4
|
+
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const packageRoot = resolve(here, "../..");
|
|
7
|
+
const { quoteIdent } = await import(join(packageRoot, "dist/sql/identifiers.js"));
|
|
8
|
+
const dbAdmin = await import(join(packageRoot, "dist/db-admin.js"));
|
|
9
|
+
|
|
10
|
+
export const {
|
|
11
|
+
applyMigrationSql,
|
|
12
|
+
applySql,
|
|
13
|
+
assertLocalDatabaseUrl,
|
|
14
|
+
databaseUrlWithDatabase,
|
|
15
|
+
dropTemporaryDatabases,
|
|
16
|
+
} = dbAdmin;
|
|
17
|
+
|
|
18
|
+
export async function createTemporaryDatabases(adminUrl, count, templateName) {
|
|
19
|
+
return dbAdmin.createTemporaryDatabases(adminUrl, count, {
|
|
20
|
+
purpose: "compare",
|
|
21
|
+
...(templateName ? { templateName } : {}),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function catalogFingerprint(url) {
|
|
26
|
+
return dbAdmin.catalogFingerprint(url, "bench:catalog");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The Supabase CLI's diff engines silently omit objects owned by
|
|
30
|
+
// supabase_admin (empty diff, exit 0). After seeding as the admin (which
|
|
31
|
+
// extension creation requires), hand every user-schema object to `role` so
|
|
32
|
+
// the engines can see them.
|
|
33
|
+
export async function transferOwnership(url, role) {
|
|
34
|
+
const client = new Client({ connectionString: url });
|
|
35
|
+
const owner = quoteIdent(role);
|
|
36
|
+
try {
|
|
37
|
+
await client.connect();
|
|
38
|
+
const schemas = await client.query(
|
|
39
|
+
"select nspname from pg_namespace where nspname !~ '^pg_' and nspname not in ('information_schema', 'extensions', 'vault') order by nspname",
|
|
40
|
+
);
|
|
41
|
+
for (const { nspname } of schemas.rows) {
|
|
42
|
+
await client.query(`ALTER SCHEMA ${quoteIdent(nspname)} OWNER TO ${owner}`);
|
|
43
|
+
}
|
|
44
|
+
const relations = await client.query(`
|
|
45
|
+
select n.nspname, c.relname, c.relkind
|
|
46
|
+
from pg_class c
|
|
47
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
48
|
+
where n.nspname !~ '^pg_' and n.nspname not in ('information_schema', 'extensions', 'vault')
|
|
49
|
+
and c.relkind in ('r', 'p', 'S', 'v', 'm', 'f')
|
|
50
|
+
and not exists (select 1 from pg_depend d where d.objid = c.oid and d.deptype in ('e', 'i'))
|
|
51
|
+
order by n.nspname, c.relname
|
|
52
|
+
`);
|
|
53
|
+
const relationKinds = new Map([
|
|
54
|
+
["S", "SEQUENCE"],
|
|
55
|
+
["f", "FOREIGN TABLE"],
|
|
56
|
+
["m", "MATERIALIZED VIEW"],
|
|
57
|
+
["p", "TABLE"],
|
|
58
|
+
["r", "TABLE"],
|
|
59
|
+
["v", "VIEW"],
|
|
60
|
+
]);
|
|
61
|
+
for (const row of relations.rows) {
|
|
62
|
+
const kind = relationKinds.get(row.relkind);
|
|
63
|
+
await client.query(
|
|
64
|
+
`ALTER ${kind} ${quoteIdent(row.nspname)}.${quoteIdent(row.relname)} OWNER TO ${owner}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const routines = await client.query(`
|
|
68
|
+
select n.nspname, p.proname, p.prokind,
|
|
69
|
+
pg_get_function_identity_arguments(p.oid) as args
|
|
70
|
+
from pg_proc p
|
|
71
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
72
|
+
where n.nspname !~ '^pg_' and n.nspname not in ('information_schema', 'extensions', 'vault')
|
|
73
|
+
and p.prokind in ('f', 'p')
|
|
74
|
+
and not exists (select 1 from pg_depend d where d.objid = p.oid and d.deptype in ('e', 'i'))
|
|
75
|
+
order by n.nspname, p.proname
|
|
76
|
+
`);
|
|
77
|
+
for (const row of routines.rows) {
|
|
78
|
+
const kind = row.prokind === "p" ? "PROCEDURE" : "FUNCTION";
|
|
79
|
+
await client.query(
|
|
80
|
+
`ALTER ${kind} ${quoteIdent(row.nspname)}.${quoteIdent(row.proname)}(${row.args}) OWNER TO ${owner}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const types = await client.query(`
|
|
84
|
+
select n.nspname, t.typname, t.typtype
|
|
85
|
+
from pg_type t
|
|
86
|
+
join pg_namespace n on n.oid = t.typnamespace
|
|
87
|
+
where n.nspname !~ '^pg_' and n.nspname not in ('information_schema', 'extensions', 'vault')
|
|
88
|
+
and t.typtype in ('e', 'd')
|
|
89
|
+
and not exists (select 1 from pg_depend d where d.objid = t.oid and d.deptype in ('e', 'i'))
|
|
90
|
+
order by n.nspname, t.typname
|
|
91
|
+
`);
|
|
92
|
+
for (const row of types.rows) {
|
|
93
|
+
const kind = row.typtype === "d" ? "DOMAIN" : "TYPE";
|
|
94
|
+
await client.query(
|
|
95
|
+
`ALTER ${kind} ${quoteIdent(row.nspname)}.${quoteIdent(row.typname)} OWNER TO ${owner}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
await client.end().catch(() => undefined);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const packageRoot = resolve(here, "../..");
|
|
7
|
+
const { makeRealisticSqlFixture, realisticFixtureManifest } = await import(
|
|
8
|
+
join(packageRoot, "dist/benchmark-fixtures.js")
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export async function discoverFixtures(fixtureRoot) {
|
|
12
|
+
const entries = await readdir(fixtureRoot, { withFileTypes: true });
|
|
13
|
+
const fixtures = [];
|
|
14
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
15
|
+
if (!entry.isDirectory()) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
fixtures.push(await fixtureFromDirectory(join(fixtureRoot, entry.name), entry.name));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const extraDirs = (process.env.SUPASCHEMA_COMPARE_FIXTURE_DIRS ?? "")
|
|
22
|
+
.split(/[,:]/)
|
|
23
|
+
.map((value) => value.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
for (const directory of extraDirs) {
|
|
26
|
+
fixtures.push(await fixtureFromDirectory(resolve(directory), basename(resolve(directory))));
|
|
27
|
+
}
|
|
28
|
+
return fixtures;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fixtureFromDirectory(directory, name) {
|
|
32
|
+
const fixture = {
|
|
33
|
+
directory,
|
|
34
|
+
fromSqlPath: join(directory, "from.sql"),
|
|
35
|
+
name,
|
|
36
|
+
toDirectory: directory,
|
|
37
|
+
toSqlPath: join(directory, "to.sql"),
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
const config = JSON.parse(await readFile(join(directory, "fixture.json"), "utf8"));
|
|
41
|
+
if (Array.isArray(config.schemas) && config.schemas.length > 0) {
|
|
42
|
+
fixture.schemas = config.schemas.map(String);
|
|
43
|
+
}
|
|
44
|
+
if (typeof config.supaschemaAdapter === "string") {
|
|
45
|
+
fixture.supaschemaAdapter = config.supaschemaAdapter;
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
try {
|
|
49
|
+
const manifest = JSON.parse(await readFile(join(directory, "manifest.json"), "utf8"));
|
|
50
|
+
if (Array.isArray(manifest)) {
|
|
51
|
+
fixture.manifest = manifest;
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
return fixture;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function materializeGeneratedFixtures(tempRoot, xlTables, xxlTables = 0) {
|
|
58
|
+
const generated = [{ name: "realistic", tableCount: 50 }];
|
|
59
|
+
if (xlTables > 0) {
|
|
60
|
+
generated.push({ name: "xl", tableCount: xlTables });
|
|
61
|
+
}
|
|
62
|
+
if (xxlTables > 0) {
|
|
63
|
+
generated.push({ name: "xxl", tableCount: xxlTables });
|
|
64
|
+
}
|
|
65
|
+
const fixtures = [];
|
|
66
|
+
for (const { name, tableCount } of generated) {
|
|
67
|
+
const directory = join(tempRoot, `${name}-fixture`);
|
|
68
|
+
await mkdir(directory, { recursive: true });
|
|
69
|
+
const sql = makeRealisticSqlFixture(tableCount);
|
|
70
|
+
const fromSqlPath = join(directory, "from.sql");
|
|
71
|
+
const toSqlPath = join(directory, "to.sql");
|
|
72
|
+
await writeFile(fromSqlPath, sql.from, "utf8");
|
|
73
|
+
await writeFile(toSqlPath, sql.to, "utf8");
|
|
74
|
+
fixtures.push({
|
|
75
|
+
directory,
|
|
76
|
+
fromSqlPath,
|
|
77
|
+
manifest: realisticFixtureManifest(tableCount),
|
|
78
|
+
name,
|
|
79
|
+
toDirectory: directory,
|
|
80
|
+
toSqlPath,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return fixtures;
|
|
84
|
+
}
|