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,339 @@
|
|
|
1
|
+
import { asRecord, astStatements, functionIdentity, qualifiedName, rangeVarName, readArray, readString, stringValue, } from "./sql/ast.js";
|
|
2
|
+
import { objectKey } from "./sql/identifiers.js";
|
|
3
|
+
import { parseSqlAst } from "./sql/parser.js";
|
|
4
|
+
const unclassified = { destructive: false, keys: [] };
|
|
5
|
+
export async function scoreDiffOutput(sql, manifest) {
|
|
6
|
+
const emittedKeys = new Set();
|
|
7
|
+
const destructiveKeys = new Set();
|
|
8
|
+
let classifiedStatements = 0;
|
|
9
|
+
const classify = async (text) => {
|
|
10
|
+
const parsed = await parseSqlAst(text, "diff-score");
|
|
11
|
+
if (parsed.ast === undefined) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const statement of astStatements(parsed.ast, text)) {
|
|
15
|
+
const classified = statementKeys(statement.tag, statement.node);
|
|
16
|
+
if (classified === "do-block") {
|
|
17
|
+
for (const segment of doBlockStatements(statement.node)) {
|
|
18
|
+
await classify(segment);
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (classified.keys.length === 0) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
classifiedStatements += 1;
|
|
26
|
+
for (const key of classified.keys) {
|
|
27
|
+
emittedKeys.add(key);
|
|
28
|
+
if (classified.destructive) {
|
|
29
|
+
destructiveKeys.add(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
await classify(sql);
|
|
35
|
+
const manifestKeys = new Set(manifest.map((entry) => entry.key));
|
|
36
|
+
const missed = [...manifestKeys].filter((key) => !emittedKeys.has(key)).sort();
|
|
37
|
+
const excess = [...emittedKeys]
|
|
38
|
+
.filter((key) => !manifestKeys.has(key) || destructiveKeys.has(key))
|
|
39
|
+
.sort();
|
|
40
|
+
const matched = manifestKeys.size - missed.length;
|
|
41
|
+
const recall = manifestKeys.size === 0 ? 1 : matched / manifestKeys.size;
|
|
42
|
+
const precision = emittedKeys.size === 0 ? 1 : (emittedKeys.size - excess.length) / emittedKeys.size;
|
|
43
|
+
const f1 = recall + precision === 0 ? 0 : (2 * recall * precision) / (recall + precision);
|
|
44
|
+
return { classifiedStatements, excess, f1, matched, missed, precision, recall };
|
|
45
|
+
}
|
|
46
|
+
function keysOf(keys, destructive = false) {
|
|
47
|
+
return { destructive, keys };
|
|
48
|
+
}
|
|
49
|
+
function statementKeys(tag, statementNode) {
|
|
50
|
+
const node = asRecord(statementNode[tag]) ?? {};
|
|
51
|
+
switch (tag) {
|
|
52
|
+
case "DoStmt":
|
|
53
|
+
return "do-block";
|
|
54
|
+
case "CreateStmt": {
|
|
55
|
+
const name = rangeVarName(node.relation);
|
|
56
|
+
return name ? keysOf([objectKey({ kind: "table", ...name })]) : unclassified;
|
|
57
|
+
}
|
|
58
|
+
case "CreateTableAsStmt": {
|
|
59
|
+
const name = rangeVarName(asRecord(node.into)?.rel);
|
|
60
|
+
const kind = readString(node.objtype) === "OBJECT_MATVIEW" ? "materialized-view" : "table";
|
|
61
|
+
return name ? keysOf([objectKey({ kind, ...name })]) : unclassified;
|
|
62
|
+
}
|
|
63
|
+
case "RefreshMatViewStmt": {
|
|
64
|
+
const name = rangeVarName(node.relation);
|
|
65
|
+
return name ? keysOf([objectKey({ kind: "materialized-view", ...name })]) : unclassified;
|
|
66
|
+
}
|
|
67
|
+
case "AlterTableStmt":
|
|
68
|
+
return keysOf(alterTableKeys(node));
|
|
69
|
+
case "IndexStmt": {
|
|
70
|
+
const table = rangeVarName(node.relation);
|
|
71
|
+
const index = readString(node.idxname);
|
|
72
|
+
return table && index
|
|
73
|
+
? keysOf([
|
|
74
|
+
objectKey({ kind: "index", name: index, schema: table.schema, table: table.name }),
|
|
75
|
+
])
|
|
76
|
+
: unclassified;
|
|
77
|
+
}
|
|
78
|
+
case "CreateFunctionStmt": {
|
|
79
|
+
const identity = functionIdentity(node.funcname, node.parameters);
|
|
80
|
+
return identity ? keysOf([objectKey({ kind: "function", ...identity })]) : unclassified;
|
|
81
|
+
}
|
|
82
|
+
case "ViewStmt": {
|
|
83
|
+
const name = rangeVarName(node.view);
|
|
84
|
+
return name ? keysOf([objectKey({ kind: "view", ...name })]) : unclassified;
|
|
85
|
+
}
|
|
86
|
+
case "CreatePolicyStmt": {
|
|
87
|
+
const table = rangeVarName(node.table);
|
|
88
|
+
const policy = readString(node.policy_name);
|
|
89
|
+
return table && policy
|
|
90
|
+
? keysOf([
|
|
91
|
+
objectKey({ kind: "policy", name: policy, schema: table.schema, table: table.name }),
|
|
92
|
+
])
|
|
93
|
+
: unclassified;
|
|
94
|
+
}
|
|
95
|
+
case "AlterEnumStmt": {
|
|
96
|
+
const name = qualifiedName(node.typeName);
|
|
97
|
+
return name ? keysOf([objectKey({ kind: "enum", ...name })]) : unclassified;
|
|
98
|
+
}
|
|
99
|
+
case "CreateEnumStmt": {
|
|
100
|
+
const name = qualifiedName(node.typeName);
|
|
101
|
+
return name ? keysOf([objectKey({ kind: "enum", ...name })]) : unclassified;
|
|
102
|
+
}
|
|
103
|
+
case "CreateTrigStmt": {
|
|
104
|
+
const table = rangeVarName(node.relation);
|
|
105
|
+
const trigger = readString(node.trigname);
|
|
106
|
+
return table && trigger
|
|
107
|
+
? keysOf([
|
|
108
|
+
objectKey({ kind: "trigger", name: trigger, schema: table.schema, table: table.name }),
|
|
109
|
+
])
|
|
110
|
+
: unclassified;
|
|
111
|
+
}
|
|
112
|
+
case "DropStmt":
|
|
113
|
+
return keysOf(dropKeys(node), dataBearingDropTypes.has(readString(node.removeType) ?? ""));
|
|
114
|
+
default:
|
|
115
|
+
return unclassified;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function alterTableKeys(node) {
|
|
119
|
+
const table = rangeVarName(node.relation);
|
|
120
|
+
if (!table) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const keys = [];
|
|
124
|
+
for (const item of readArray(node.cmds)) {
|
|
125
|
+
const command = asRecord(asRecord(item)?.AlterTableCmd);
|
|
126
|
+
const subtype = readString(command?.subtype);
|
|
127
|
+
if (subtype === "AT_AddConstraint") {
|
|
128
|
+
const name = readString(asRecord(asRecord(command?.def)?.Constraint)?.conname);
|
|
129
|
+
if (name) {
|
|
130
|
+
keys.push(objectKey({ kind: "constraint", name, schema: table.schema, table: table.name }));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (subtype === "AT_EnableRowSecurity" || subtype === "AT_DisableRowSecurity") {
|
|
135
|
+
keys.push(objectKey({ kind: "rls", name: table.name, schema: table.schema, table: table.name }));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
keys.push(objectKey({ kind: "table", ...table }));
|
|
139
|
+
}
|
|
140
|
+
return [...new Set(keys)];
|
|
141
|
+
}
|
|
142
|
+
const dataBearingDropTypes = new Set([
|
|
143
|
+
"OBJECT_DOMAIN",
|
|
144
|
+
"OBJECT_MATVIEW",
|
|
145
|
+
"OBJECT_SCHEMA",
|
|
146
|
+
"OBJECT_SEQUENCE",
|
|
147
|
+
"OBJECT_TABLE",
|
|
148
|
+
"OBJECT_TYPE",
|
|
149
|
+
]);
|
|
150
|
+
const plainDropKinds = new Map([
|
|
151
|
+
["OBJECT_INDEX", "index"],
|
|
152
|
+
["OBJECT_MATVIEW", "materialized-view"],
|
|
153
|
+
["OBJECT_SCHEMA", "schema"],
|
|
154
|
+
["OBJECT_SEQUENCE", "sequence"],
|
|
155
|
+
["OBJECT_TABLE", "table"],
|
|
156
|
+
["OBJECT_VIEW", "view"],
|
|
157
|
+
]);
|
|
158
|
+
const tableScopedDropKinds = new Map([
|
|
159
|
+
["OBJECT_POLICY", "policy"],
|
|
160
|
+
["OBJECT_TRIGGER", "trigger"],
|
|
161
|
+
]);
|
|
162
|
+
const typeDropKinds = new Map([
|
|
163
|
+
["OBJECT_DOMAIN", "domain"],
|
|
164
|
+
["OBJECT_TYPE", "enum"],
|
|
165
|
+
]);
|
|
166
|
+
function dropKeys(node) {
|
|
167
|
+
const removeType = readString(node.removeType) ?? "";
|
|
168
|
+
const keys = [];
|
|
169
|
+
for (const object of readArray(node.objects)) {
|
|
170
|
+
const plainKind = plainDropKinds.get(removeType);
|
|
171
|
+
if (plainKind) {
|
|
172
|
+
const parts = objectNameParts(object);
|
|
173
|
+
const name = parts.at(-1);
|
|
174
|
+
if (name && removeType === "OBJECT_SCHEMA") {
|
|
175
|
+
keys.push(objectKey({ kind: plainKind, name }));
|
|
176
|
+
}
|
|
177
|
+
else if (name) {
|
|
178
|
+
const schema = parts.length > 1 ? (parts.at(-2) ?? "public") : "public";
|
|
179
|
+
keys.push(objectKey({ kind: plainKind, name, schema }));
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const tableScopedKind = tableScopedDropKinds.get(removeType);
|
|
184
|
+
if (tableScopedKind) {
|
|
185
|
+
const parts = objectNameParts(object);
|
|
186
|
+
const name = parts.at(-1);
|
|
187
|
+
const table = parts.at(-2);
|
|
188
|
+
if (name && table) {
|
|
189
|
+
keys.push(objectKey({
|
|
190
|
+
kind: tableScopedKind,
|
|
191
|
+
name,
|
|
192
|
+
schema: parts.length > 2 ? (parts.at(-3) ?? "public") : "public",
|
|
193
|
+
table,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const typeKind = typeDropKinds.get(removeType);
|
|
199
|
+
if (typeKind) {
|
|
200
|
+
const name = qualifiedName(asRecord(asRecord(object)?.TypeName)?.names);
|
|
201
|
+
if (name) {
|
|
202
|
+
keys.push(objectKey({ kind: typeKind, ...name }));
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (removeType === "OBJECT_FUNCTION" || removeType === "OBJECT_PROCEDURE") {
|
|
207
|
+
const name = qualifiedName(asRecord(asRecord(object)?.ObjectWithArgs)?.objname);
|
|
208
|
+
if (name) {
|
|
209
|
+
keys.push(objectKey({ kind: "function", ...name }));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return keys;
|
|
214
|
+
}
|
|
215
|
+
function objectNameParts(object) {
|
|
216
|
+
return readArray(asRecord(object)?.List ? asRecord(asRecord(object)?.List)?.items : object)
|
|
217
|
+
.map((item) => stringValue(item))
|
|
218
|
+
.filter((item) => item !== undefined);
|
|
219
|
+
}
|
|
220
|
+
function doBlockStatements(statementNode) {
|
|
221
|
+
const node = asRecord(statementNode.DoStmt);
|
|
222
|
+
for (const item of readArray(node?.args)) {
|
|
223
|
+
const defElem = asRecord(asRecord(item)?.DefElem);
|
|
224
|
+
if (readString(defElem?.defname) !== "as") {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const body = stringValue(defElem?.arg);
|
|
228
|
+
return body ? guardedBodySegments(body) : [];
|
|
229
|
+
}
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
function guardedBodySegments(body) {
|
|
233
|
+
const thenOffsets = keywordOffsets(body, "then");
|
|
234
|
+
const endIfOffsets = keywordOffsets(body, "end").filter((offset) => nextKeywordIs(body, offset + "end".length, "if"));
|
|
235
|
+
const segments = [];
|
|
236
|
+
let cursor = -1;
|
|
237
|
+
for (const endIf of endIfOffsets) {
|
|
238
|
+
const start = thenOffsets.find((offset) => offset > cursor && offset < endIf);
|
|
239
|
+
if (start === undefined) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
segments.push(body.slice(start + "then".length, endIf));
|
|
243
|
+
cursor = endIf;
|
|
244
|
+
}
|
|
245
|
+
if (segments.length > 0) {
|
|
246
|
+
return segments;
|
|
247
|
+
}
|
|
248
|
+
const begin = keywordOffsets(body, "begin").at(0);
|
|
249
|
+
const lastEnd = keywordOffsets(body, "end").at(-1);
|
|
250
|
+
if (begin !== undefined && lastEnd !== undefined && lastEnd > begin) {
|
|
251
|
+
return [body.slice(begin + "begin".length, lastEnd)];
|
|
252
|
+
}
|
|
253
|
+
return [body];
|
|
254
|
+
}
|
|
255
|
+
const whitespaceChars = " \t\n\r";
|
|
256
|
+
function nextKeywordIs(text, from, word) {
|
|
257
|
+
let index = from;
|
|
258
|
+
while (index < text.length && whitespaceChars.includes(text[index] ?? "")) {
|
|
259
|
+
index += 1;
|
|
260
|
+
}
|
|
261
|
+
return isKeywordAt(text, index, word);
|
|
262
|
+
}
|
|
263
|
+
function isKeywordAt(text, offset, word) {
|
|
264
|
+
if (text.slice(offset, offset + word.length).toLowerCase() !== word) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
return !(isWordChar(text[offset - 1]) || isWordChar(text[offset + word.length]));
|
|
268
|
+
}
|
|
269
|
+
function isWordChar(char) {
|
|
270
|
+
if (char === undefined) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return ((char >= "a" && char <= "z") ||
|
|
274
|
+
(char >= "A" && char <= "Z") ||
|
|
275
|
+
(char >= "0" && char <= "9") ||
|
|
276
|
+
char === "_");
|
|
277
|
+
}
|
|
278
|
+
function keywordOffsets(text, word) {
|
|
279
|
+
const offsets = [];
|
|
280
|
+
let index = 0;
|
|
281
|
+
while (index < text.length) {
|
|
282
|
+
const char = text[index] ?? "";
|
|
283
|
+
const next = text[index + 1] ?? "";
|
|
284
|
+
if (char === "-" && next === "-") {
|
|
285
|
+
const lineEnd = text.indexOf("\n", index);
|
|
286
|
+
index = lineEnd === -1 ? text.length : lineEnd + 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (char === "/" && next === "*") {
|
|
290
|
+
const blockEnd = text.indexOf("*/", index + 2);
|
|
291
|
+
index = blockEnd === -1 ? text.length : blockEnd + 2;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (char === "'" || char === '"') {
|
|
295
|
+
index = skipQuoted(text, index, char);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (char === "$") {
|
|
299
|
+
const skipped = skipDollarQuoted(text, index);
|
|
300
|
+
if (skipped > index) {
|
|
301
|
+
index = skipped;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (isKeywordAt(text, index, word)) {
|
|
306
|
+
offsets.push(index);
|
|
307
|
+
index += word.length;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
index += 1;
|
|
311
|
+
}
|
|
312
|
+
return offsets;
|
|
313
|
+
}
|
|
314
|
+
function skipQuoted(text, from, quote) {
|
|
315
|
+
let index = from + 1;
|
|
316
|
+
while (index < text.length) {
|
|
317
|
+
if (text[index] === quote) {
|
|
318
|
+
if (text[index + 1] === quote) {
|
|
319
|
+
index += 2;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
return index + 1;
|
|
323
|
+
}
|
|
324
|
+
index += 1;
|
|
325
|
+
}
|
|
326
|
+
return text.length;
|
|
327
|
+
}
|
|
328
|
+
function skipDollarQuoted(text, from) {
|
|
329
|
+
let tagEnd = from + 1;
|
|
330
|
+
while (tagEnd < text.length && (isWordChar(text[tagEnd]) || text[tagEnd] === "$")) {
|
|
331
|
+
if (text[tagEnd] === "$") {
|
|
332
|
+
const tag = text.slice(from, tagEnd + 1);
|
|
333
|
+
const close = text.indexOf(tag, tagEnd + 1);
|
|
334
|
+
return close === -1 ? text.length : close + tag.length;
|
|
335
|
+
}
|
|
336
|
+
tagEnd += 1;
|
|
337
|
+
}
|
|
338
|
+
return from;
|
|
339
|
+
}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SupaschemaConfig } from "./config.js";
|
|
2
|
+
export interface DoctorCheck {
|
|
3
|
+
detail: string;
|
|
4
|
+
name: string;
|
|
5
|
+
status: "pass" | "fail" | "skip";
|
|
6
|
+
}
|
|
7
|
+
export interface DoctorReport {
|
|
8
|
+
checks: DoctorCheck[];
|
|
9
|
+
healthy: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runDoctor(config: SupaschemaConfig, options?: {
|
|
12
|
+
configPath?: string;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
databaseUrl?: string;
|
|
15
|
+
}): Promise<DoctorReport>;
|
|
16
|
+
export declare function renderDoctorReport(report: DoctorReport): string;
|
|
17
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKpD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;CAClB;AAID,wBAAsB,SAAS,CAC7B,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GACxE,OAAO,CAAC,YAAY,CAAC,CAkGvB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAO/D"}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Client } from "pg";
|
|
4
|
+
import { resolveDatabaseUrl, resolveSupabaseLocalDatabaseUrl } from "./database-url.js";
|
|
5
|
+
import { migrationsStatus } from "./migrations-status.js";
|
|
6
|
+
import { parseSqlAst } from "./sql/parser.js";
|
|
7
|
+
const minimumNodeMajor = 22;
|
|
8
|
+
export async function runDoctor(config, options = {}) {
|
|
9
|
+
const cwd = options.cwd ?? process.cwd();
|
|
10
|
+
const checks = [];
|
|
11
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
12
|
+
checks.push({
|
|
13
|
+
detail: `running ${process.versions.node}, requires >=${minimumNodeMajor}`,
|
|
14
|
+
name: "node version",
|
|
15
|
+
status: nodeMajor >= minimumNodeMajor ? "pass" : "fail",
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
const parsed = await parseSqlAst("SELECT 1");
|
|
19
|
+
checks.push({
|
|
20
|
+
detail: parsed.ast !== undefined ? "libpg-query WASM parser loaded" : "parser returned no AST",
|
|
21
|
+
name: "sql parser",
|
|
22
|
+
status: parsed.ast !== undefined ? "pass" : "fail",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
checks.push({ detail: errorMessage(error), name: "sql parser", status: "fail" });
|
|
27
|
+
}
|
|
28
|
+
checks.push({
|
|
29
|
+
detail: options.configPath ?? "defaults (no config file found is fine)",
|
|
30
|
+
name: "config",
|
|
31
|
+
status: "pass",
|
|
32
|
+
});
|
|
33
|
+
const explicit = options.databaseUrl;
|
|
34
|
+
const resolved = resolveDatabaseUrl(explicit);
|
|
35
|
+
const lane = explicit
|
|
36
|
+
? "explicit --database-url"
|
|
37
|
+
: process.env.SUPASCHEMA_DATABASE_URL
|
|
38
|
+
? "SUPASCHEMA_DATABASE_URL"
|
|
39
|
+
: resolveSupabaseLocalDatabaseUrl()
|
|
40
|
+
? "supabase/config.toml auto-discovery"
|
|
41
|
+
: "none";
|
|
42
|
+
checks.push({
|
|
43
|
+
detail: resolved ? `resolved via ${lane}` : "no URL resolves; database commands will skip",
|
|
44
|
+
name: "database url",
|
|
45
|
+
status: resolved ? "pass" : "skip",
|
|
46
|
+
});
|
|
47
|
+
if (resolved) {
|
|
48
|
+
const client = new Client({ connectionString: resolved });
|
|
49
|
+
try {
|
|
50
|
+
await client.connect();
|
|
51
|
+
const capability = await client.query("SELECT (rolcreatedb OR rolsuper) AS can_create FROM pg_catalog.pg_roles WHERE rolname = current_user");
|
|
52
|
+
checks.push({ detail: "SELECT 1 succeeded", name: "database reachable", status: "pass" });
|
|
53
|
+
const canCreate = capability.rows[0]?.can_create === true;
|
|
54
|
+
checks.push({
|
|
55
|
+
detail: canCreate
|
|
56
|
+
? "role can CREATE DATABASE (verify/corpus will work)"
|
|
57
|
+
: "role lacks CREATEDB; verify/corpus need a stronger role",
|
|
58
|
+
name: "createdb capability",
|
|
59
|
+
status: canCreate ? "pass" : "fail",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
checks.push({ detail: errorMessage(error), name: "database reachable", status: "fail" });
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await client.end().catch(() => undefined);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const migrationsDir = resolve(cwd, "supabase/migrations");
|
|
70
|
+
const hasMigrationsDir = await access(migrationsDir)
|
|
71
|
+
.then(() => true)
|
|
72
|
+
.catch(() => false);
|
|
73
|
+
if (hasMigrationsDir && resolved) {
|
|
74
|
+
const { report } = await migrationsStatus({ databaseUrl: resolved, directory: migrationsDir });
|
|
75
|
+
const broken = report.ghosts.length + report.outOfOrder.length;
|
|
76
|
+
checks.push({
|
|
77
|
+
detail: `${report.applied.length} applied, ${report.pending.length} pending, ${report.ghosts.length} ghosts, ${report.outOfOrder.length} out-of-order`,
|
|
78
|
+
name: "migrations history",
|
|
79
|
+
status: broken === 0 ? "pass" : "fail",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
checks.push({
|
|
84
|
+
detail: hasMigrationsDir ? "no database to compare against" : `${migrationsDir} not found`,
|
|
85
|
+
name: "migrations history",
|
|
86
|
+
status: "skip",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const tree = config.schemaPaths[0] ?? "supabase/schemas";
|
|
90
|
+
const hasTree = await access(resolve(cwd, tree))
|
|
91
|
+
.then(() => true)
|
|
92
|
+
.catch(() => false);
|
|
93
|
+
checks.push({
|
|
94
|
+
detail: hasTree ? `${tree} exists` : `${tree} not found (set schemaPaths in config)`,
|
|
95
|
+
name: "declarative tree",
|
|
96
|
+
status: hasTree ? "pass" : "skip",
|
|
97
|
+
});
|
|
98
|
+
return { checks, healthy: checks.every((item) => item.status !== "fail") };
|
|
99
|
+
}
|
|
100
|
+
export function renderDoctorReport(report) {
|
|
101
|
+
const lines = report.checks.map((item) => {
|
|
102
|
+
const badge = item.status === "pass" ? "PASS" : item.status === "fail" ? "FAIL" : "SKIP";
|
|
103
|
+
return `${badge} ${item.name}: ${item.detail}`;
|
|
104
|
+
});
|
|
105
|
+
lines.push(report.healthy ? "doctor: healthy" : "doctor: issues found");
|
|
106
|
+
return `${lines.join("\n")}\n`;
|
|
107
|
+
}
|
|
108
|
+
function errorMessage(error) {
|
|
109
|
+
return error instanceof Error ? error.message : String(error);
|
|
110
|
+
}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SchemaModel, SchemaObject } from "./core.js";
|
|
2
|
+
export declare const MODEL_FORMAT_VERSION = 2;
|
|
3
|
+
export declare function sha256(value: string): string;
|
|
4
|
+
export declare function stableJson(value: unknown): string;
|
|
5
|
+
export declare function fingerprintObjects(objects: SchemaObject[]): string;
|
|
6
|
+
export declare function fingerprintModel(model: SchemaModel): string;
|
|
7
|
+
//# sourceMappingURL=hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAI3D,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAKlE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAE3D"}
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const MODEL_FORMAT_VERSION = 2;
|
|
3
|
+
export function sha256(value) {
|
|
4
|
+
return createHash("sha256").update(value).digest("hex");
|
|
5
|
+
}
|
|
6
|
+
export function stableJson(value) {
|
|
7
|
+
return JSON.stringify(sortJson(value));
|
|
8
|
+
}
|
|
9
|
+
export function fingerprintObjects(objects) {
|
|
10
|
+
const payload = objects
|
|
11
|
+
.map((object) => ({ hash: object.hash, key: object.key }))
|
|
12
|
+
.sort((left, right) => left.key.localeCompare(right.key));
|
|
13
|
+
return sha256(stableJson(payload));
|
|
14
|
+
}
|
|
15
|
+
export function fingerprintModel(model) {
|
|
16
|
+
return fingerprintObjects(model.objects);
|
|
17
|
+
}
|
|
18
|
+
function sortJson(value) {
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value.map(sortJson);
|
|
21
|
+
}
|
|
22
|
+
if (value && typeof value === "object") {
|
|
23
|
+
return Object.fromEntries(Object.entries(value)
|
|
24
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
25
|
+
.map(([key, child]) => [key, sortJson(child)]));
|
|
26
|
+
}
|
|
27
|
+
if (value === null ||
|
|
28
|
+
typeof value === "boolean" ||
|
|
29
|
+
typeof value === "number" ||
|
|
30
|
+
typeof value === "string") {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type { AuditFinding, AuditReport } from "./audit.js";
|
|
2
|
+
export { auditModel, renderAuditReport } from "./audit.js";
|
|
3
|
+
export { extractCatalogModel } from "./catalog.js";
|
|
4
|
+
export { checkMigrationSql } from "./check.js";
|
|
5
|
+
export { defaultConfig, loadConfig, resolveConfig, supaschemaConfigSchema } from "./config.js";
|
|
6
|
+
export type { CheckOptions, Diagnostic, DiagnosticSeverity, ExtractOptions, MigrationOperation, MigrationOperationKind, MigrationPlan, ObjectKind, ObjectRef, RenameHint, RenderOptions, SchemaModel, SchemaObject, SupaschemaConfig, SupaschemaHints, TableColumn, VerifyMigrationOptions, } from "./core.js";
|
|
7
|
+
export type { CorpusOptions, CorpusReport } from "./corpus.js";
|
|
8
|
+
export { renderCorpusReport, runCorpus } from "./corpus.js";
|
|
9
|
+
export { resolveDatabaseUrl, resolveSupabaseLocalDatabaseUrl } from "./database-url.js";
|
|
10
|
+
export { applyMigrationSql, applySql, assertLocalDatabaseUrl, catalogFingerprint, createTemporaryDatabases, databaseUrlWithDatabase, dropTemporaryDatabases, tempDatabaseName, withTemporaryDatabases, } from "./db-admin.js";
|
|
11
|
+
export type { MigrationLineage } from "./lineage.js";
|
|
12
|
+
export { latestLineage, lineageLine, parseLineage } from "./lineage.js";
|
|
13
|
+
export type { MigrationsStatusOptions, MigrationsStatusReport } from "./migrations-status.js";
|
|
14
|
+
export { migrationsStatus, renderMigrationsStatus } from "./migrations-status.js";
|
|
15
|
+
export { planSchemaDiff } from "./planner.js";
|
|
16
|
+
export { renderMigration, renderMigrationSplit } from "./render.js";
|
|
17
|
+
export { extractSourceModel } from "./source.js";
|
|
18
|
+
export type { SyncOptions, SyncResult } from "./sync.js";
|
|
19
|
+
export { syncMigrations } from "./sync.js";
|
|
20
|
+
export { generateDatabaseTypes } from "./typegen.js";
|
|
21
|
+
export { generateZodSchemas } from "./typegen-zod.js";
|
|
22
|
+
export { runConfiguredValidators } from "./validators.js";
|
|
23
|
+
export { verifyMigration } from "./verify.js";
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAC/F,YAAY,EACV,YAAY,EACZ,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EACb,UAAU,EACV,SAAS,EACT,UAAU,EACV,aAAa,EACb,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,sBAAsB,GACvB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,+BAA+B,EAAE,MAAM,mBAAmB,CAAC;AACxF,OAAO,EACL,iBAAiB,EACjB,QAAQ,EACR,sBAAsB,EACtB,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,EACtB,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACxE,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAC9F,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { auditModel, renderAuditReport } from "./audit.js";
|
|
2
|
+
export { extractCatalogModel } from "./catalog.js";
|
|
3
|
+
export { checkMigrationSql } from "./check.js";
|
|
4
|
+
export { defaultConfig, loadConfig, resolveConfig, supaschemaConfigSchema } from "./config.js";
|
|
5
|
+
export { renderCorpusReport, runCorpus } from "./corpus.js";
|
|
6
|
+
export { resolveDatabaseUrl, resolveSupabaseLocalDatabaseUrl } from "./database-url.js";
|
|
7
|
+
export { applyMigrationSql, applySql, assertLocalDatabaseUrl, catalogFingerprint, createTemporaryDatabases, databaseUrlWithDatabase, dropTemporaryDatabases, tempDatabaseName, withTemporaryDatabases, } from "./db-admin.js";
|
|
8
|
+
export { latestLineage, lineageLine, parseLineage } from "./lineage.js";
|
|
9
|
+
export { migrationsStatus, renderMigrationsStatus } from "./migrations-status.js";
|
|
10
|
+
export { planSchemaDiff } from "./planner.js";
|
|
11
|
+
export { renderMigration, renderMigrationSplit } from "./render.js";
|
|
12
|
+
export { extractSourceModel } from "./source.js";
|
|
13
|
+
export { syncMigrations } from "./sync.js";
|
|
14
|
+
export { generateDatabaseTypes } from "./typegen.js";
|
|
15
|
+
export { generateZodSchemas } from "./typegen-zod.js";
|
|
16
|
+
export { runConfiguredValidators } from "./validators.js";
|
|
17
|
+
export { verifyMigration } from "./verify.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MigrationPlan } from "./core.js";
|
|
2
|
+
export interface MigrationLineage {
|
|
3
|
+
file: string;
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function lineageLine(plan: MigrationPlan): string;
|
|
8
|
+
/**
|
|
9
|
+
* Parses the machine-readable lineage marker supaschema embeds in every
|
|
10
|
+
* rendered migration header. Hand-authored migrations have no marker and are
|
|
11
|
+
* invisible to the chain gate by design.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseLineage(content: string): {
|
|
14
|
+
from: string;
|
|
15
|
+
to: string;
|
|
16
|
+
} | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Finds the newest supaschema-generated migration in a directory by filename
|
|
19
|
+
* order (timestamped names sort chronologically). Returns undefined when no
|
|
20
|
+
* lineage-bearing migration exists, which disables the chain gate.
|
|
21
|
+
*/
|
|
22
|
+
export declare function latestLineage(directory: string): Promise<MigrationLineage | undefined>;
|
|
23
|
+
//# sourceMappingURL=lineage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../src/lineage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAKD,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAmBtF;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAsB5F"}
|
package/dist/lineage.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const lineagePrefix = "-- supaschema: lineage ";
|
|
4
|
+
const headerByteLimit = 4096;
|
|
5
|
+
export function lineageLine(plan) {
|
|
6
|
+
return `${lineagePrefix}from=${plan.fromFingerprint} to=${plan.toFingerprint}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parses the machine-readable lineage marker supaschema embeds in every
|
|
10
|
+
* rendered migration header. Hand-authored migrations have no marker and are
|
|
11
|
+
* invisible to the chain gate by design.
|
|
12
|
+
*/
|
|
13
|
+
export function parseLineage(content) {
|
|
14
|
+
for (const line of content.split("\n")) {
|
|
15
|
+
if (!line.startsWith(lineagePrefix)) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const fields = new Map();
|
|
19
|
+
for (const token of line.slice(lineagePrefix.length).trim().split(/\s+/)) {
|
|
20
|
+
const separator = token.indexOf("=");
|
|
21
|
+
if (separator > 0) {
|
|
22
|
+
fields.set(token.slice(0, separator), token.slice(separator + 1));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const from = fields.get("from");
|
|
26
|
+
const to = fields.get("to");
|
|
27
|
+
if (from && to) {
|
|
28
|
+
return { from, to };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Finds the newest supaschema-generated migration in a directory by filename
|
|
35
|
+
* order (timestamped names sort chronologically). Returns undefined when no
|
|
36
|
+
* lineage-bearing migration exists, which disables the chain gate.
|
|
37
|
+
*/
|
|
38
|
+
export async function latestLineage(directory) {
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = (await readdir(directory, { withFileTypes: true }))
|
|
42
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".sql"))
|
|
43
|
+
.map((entry) => entry.name)
|
|
44
|
+
.sort((left, right) => right.localeCompare(left));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
for (const name of entries) {
|
|
53
|
+
const path = join(directory, name);
|
|
54
|
+
const content = await readFile(path, "utf8");
|
|
55
|
+
const lineage = parseLineage(content.slice(0, headerByteLimit));
|
|
56
|
+
if (lineage) {
|
|
57
|
+
return { file: path, from: lineage.from, to: lineage.to };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|