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,35 @@
|
|
|
1
|
+
import type { Diagnostic } from "./core.js";
|
|
2
|
+
import type { MigrationLineage } from "./lineage.js";
|
|
3
|
+
export interface MigrationsStatusOptions {
|
|
4
|
+
databaseUrl?: string;
|
|
5
|
+
directory: string;
|
|
6
|
+
historyTable?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MigrationsStatusReport {
|
|
9
|
+
/** Versions present both on disk and in the target's history table. */
|
|
10
|
+
applied: string[];
|
|
11
|
+
/** Disk files whose version the target has not applied yet. */
|
|
12
|
+
pending: string[];
|
|
13
|
+
/** Versions in the target's history with no file on disk. */
|
|
14
|
+
ghosts: string[];
|
|
15
|
+
/** Pending files older than the target's newest applied version. */
|
|
16
|
+
outOfOrder: string[];
|
|
17
|
+
/** Pending files generated by supaschema, with their lineage markers. */
|
|
18
|
+
pendingLineage: MigrationLineage[];
|
|
19
|
+
files: string[];
|
|
20
|
+
historyTable: string;
|
|
21
|
+
target?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Three states can diverge independently: migration files in the worktree,
|
|
25
|
+
* the history table on one database (local), and the history table on
|
|
26
|
+
* another (remote/linked). This report reconciles the worktree against one
|
|
27
|
+
* target; run it once per target to see all three. Ghost versions and
|
|
28
|
+
* out-of-order pending files are integrity defects, not normal pending work.
|
|
29
|
+
*/
|
|
30
|
+
export declare function migrationsStatus(options: MigrationsStatusOptions): Promise<{
|
|
31
|
+
diagnostics: Diagnostic[];
|
|
32
|
+
report: MigrationsStatusReport;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function renderMigrationsStatus(report: MigrationsStatusReport): string;
|
|
35
|
+
//# sourceMappingURL=migrations-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations-status.d.ts","sourceRoot":"","sources":["../src/migrations-status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIrD,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,uEAAuE;IACvE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,oEAAoE;IACpE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,yEAAyE;IACzE,cAAc,EAAE,gBAAgB,EAAE,CAAC;IACnC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC;IAAE,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAA;CAAE,CAAC,CAsExE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAmB7E"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Client } from "pg";
|
|
4
|
+
import { diagnostic } from "./diagnostics.js";
|
|
5
|
+
import { parseLineage } from "./lineage.js";
|
|
6
|
+
import { quoteIdent } from "./sql/identifiers.js";
|
|
7
|
+
const defaultHistoryTable = "supabase_migrations.schema_migrations";
|
|
8
|
+
const versionPattern = /^(\d{8,})/u;
|
|
9
|
+
const lineageScanBytes = 4096;
|
|
10
|
+
/**
|
|
11
|
+
* Three states can diverge independently: migration files in the worktree,
|
|
12
|
+
* the history table on one database (local), and the history table on
|
|
13
|
+
* another (remote/linked). This report reconciles the worktree against one
|
|
14
|
+
* target; run it once per target to see all three. Ghost versions and
|
|
15
|
+
* out-of-order pending files are integrity defects, not normal pending work.
|
|
16
|
+
*/
|
|
17
|
+
export async function migrationsStatus(options) {
|
|
18
|
+
const diagnostics = [];
|
|
19
|
+
const historyTable = options.historyTable ?? defaultHistoryTable;
|
|
20
|
+
const files = (await readdir(options.directory))
|
|
21
|
+
.filter((name) => name.endsWith(".sql") && versionPattern.test(name))
|
|
22
|
+
.sort((left, right) => left.localeCompare(right));
|
|
23
|
+
const versionsByFile = new Map(files.map((name) => [name, versionPattern.exec(name)?.[1] ?? ""]));
|
|
24
|
+
const report = {
|
|
25
|
+
applied: [],
|
|
26
|
+
files,
|
|
27
|
+
ghosts: [],
|
|
28
|
+
historyTable,
|
|
29
|
+
outOfOrder: [],
|
|
30
|
+
pending: [],
|
|
31
|
+
pendingLineage: [],
|
|
32
|
+
};
|
|
33
|
+
if (!options.databaseUrl) {
|
|
34
|
+
report.pending = files;
|
|
35
|
+
diagnostics.push(diagnostic("SUPA_MIGRATIONS_NO_TARGET", "warning", "no database URL resolved; reporting disk files only", { hint: "Pass --database-url or set SUPASCHEMA_DATABASE_URL to compare against a target." }));
|
|
36
|
+
await annotateLineage(options.directory, report);
|
|
37
|
+
return { diagnostics, report };
|
|
38
|
+
}
|
|
39
|
+
report.target = redactUrl(options.databaseUrl);
|
|
40
|
+
const appliedVersions = await readHistory(options.databaseUrl, historyTable, diagnostics);
|
|
41
|
+
if (appliedVersions === undefined) {
|
|
42
|
+
return { diagnostics, report };
|
|
43
|
+
}
|
|
44
|
+
const diskVersions = new Set(versionsByFile.values());
|
|
45
|
+
const newestApplied = [...appliedVersions].sort().at(-1) ?? "";
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const version = versionsByFile.get(file) ?? "";
|
|
48
|
+
if (appliedVersions.has(version)) {
|
|
49
|
+
report.applied.push(file);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
report.pending.push(file);
|
|
53
|
+
if (version < newestApplied) {
|
|
54
|
+
report.outOfOrder.push(file);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
report.ghosts = [...appliedVersions].filter((version) => !diskVersions.has(version)).sort();
|
|
58
|
+
await annotateLineage(options.directory, report);
|
|
59
|
+
if (report.ghosts.length > 0) {
|
|
60
|
+
diagnostics.push(diagnostic("SUPA_MIGRATIONS_GHOST_VERSIONS", "error", `${report.ghosts.length} applied version(s) have no migration file on disk`, { hint: `ghost versions: ${report.ghosts.slice(0, 8).join(", ")}` }));
|
|
61
|
+
}
|
|
62
|
+
if (report.outOfOrder.length > 0) {
|
|
63
|
+
diagnostics.push(diagnostic("SUPA_MIGRATIONS_OUT_OF_ORDER", "error", `${report.outOfOrder.length} pending file(s) are older than the target's newest applied version`, { hint: `out-of-order: ${report.outOfOrder.slice(0, 8).join(", ")}` }));
|
|
64
|
+
}
|
|
65
|
+
return { diagnostics, report };
|
|
66
|
+
}
|
|
67
|
+
export function renderMigrationsStatus(report) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
lines.push(`migrations: ${report.files.length} file(s) on disk vs ${report.target ?? "no target"} (${report.historyTable})`);
|
|
70
|
+
lines.push(` applied: ${report.applied.length} pending: ${report.pending.length} ghosts: ${report.ghosts.length} out-of-order: ${report.outOfOrder.length}`);
|
|
71
|
+
for (const file of report.pending) {
|
|
72
|
+
const lineage = report.pendingLineage.find((item) => item.file === file);
|
|
73
|
+
lines.push(` pending: ${file}${lineage ? " (supaschema lineage)" : ""}`);
|
|
74
|
+
}
|
|
75
|
+
for (const version of report.ghosts) {
|
|
76
|
+
lines.push(` ghost: ${version} (applied on target, no file on disk)`);
|
|
77
|
+
}
|
|
78
|
+
for (const file of report.outOfOrder) {
|
|
79
|
+
lines.push(` out-of-order: ${file}`);
|
|
80
|
+
}
|
|
81
|
+
return `${lines.join("\n")}\n`;
|
|
82
|
+
}
|
|
83
|
+
async function readHistory(databaseUrl, historyTable, diagnostics) {
|
|
84
|
+
const parts = historyTable.split(".");
|
|
85
|
+
if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
|
|
86
|
+
diagnostics.push(diagnostic("SUPA_MIGRATIONS_HISTORY_TABLE", "error", `history table "${historyTable}" must be schema-qualified`));
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const client = new Client({ connectionString: databaseUrl });
|
|
90
|
+
try {
|
|
91
|
+
await client.connect();
|
|
92
|
+
const exists = await client.query("select exists (select 1 from pg_catalog.pg_class c join pg_catalog.pg_namespace n on n.oid = c.relnamespace where n.nspname = $1 and c.relname = $2) as found", [parts[0], parts[1]]);
|
|
93
|
+
if (exists.rows[0]?.found !== true) {
|
|
94
|
+
diagnostics.push(diagnostic("SUPA_MIGRATIONS_HISTORY_TABLE", "error", `history table ${historyTable} does not exist on the target`, { hint: "Pass --history-table for runners that record history elsewhere." }));
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const result = await client.query(`select version::text as version from ${quoteIdent(parts[0] ?? "")}.${quoteIdent(parts[1] ?? "")}`);
|
|
98
|
+
return new Set(result.rows.map((row) => row.version));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
diagnostics.push(diagnostic("SUPA_MIGRATIONS_TARGET_UNAVAILABLE", "error", error instanceof Error ? error.message : String(error), { hint: "Confirm the database URL is reachable." }));
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await client.end().catch(() => undefined);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function annotateLineage(directory, report) {
|
|
109
|
+
for (const file of report.pending) {
|
|
110
|
+
const handle = await readFile(join(directory, file), "utf8").catch(() => undefined);
|
|
111
|
+
if (handle === undefined) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const lineage = parseLineage(handle.slice(0, lineageScanBytes));
|
|
115
|
+
if (lineage) {
|
|
116
|
+
report.pendingLineage.push({ file, ...lineage });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function redactUrl(value) {
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(value);
|
|
123
|
+
if (url.password) {
|
|
124
|
+
url.password = "***";
|
|
125
|
+
}
|
|
126
|
+
return url.toString();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return "<database>";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Diagnostic, MigrationOperation } from "./core.js";
|
|
2
|
+
export declare function sortOperations(operations: MigrationOperation[], diagnostics: Diagnostic[]): MigrationOperation[];
|
|
3
|
+
export declare function compareOperations(left: MigrationOperation, right: MigrationOperation): number;
|
|
4
|
+
//# sourceMappingURL=plan-order.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-order.d.ts","sourceRoot":"","sources":["../src/plan-order.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAyB,MAAM,WAAW,CAAC;AAgCvF,wBAAgB,cAAc,CAC5B,UAAU,EAAE,kBAAkB,EAAE,EAChC,WAAW,EAAE,UAAU,EAAE,GACxB,kBAAkB,EAAE,CAyDtB;AAoFD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAU7F"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { diagnostic } from "./diagnostics.js";
|
|
2
|
+
const createOrder = new Map([
|
|
3
|
+
["schema", 10],
|
|
4
|
+
["extension", 20],
|
|
5
|
+
["enum", 30],
|
|
6
|
+
["type", 40],
|
|
7
|
+
["domain", 50],
|
|
8
|
+
["sequence", 60],
|
|
9
|
+
["foreign-data-wrapper", 70],
|
|
10
|
+
["foreign-server", 80],
|
|
11
|
+
["table", 90],
|
|
12
|
+
["foreign-table", 100],
|
|
13
|
+
["constraint", 110],
|
|
14
|
+
["index", 120],
|
|
15
|
+
["function", 130],
|
|
16
|
+
["procedure", 140],
|
|
17
|
+
["view", 150],
|
|
18
|
+
["materialized-view", 160],
|
|
19
|
+
["trigger", 170],
|
|
20
|
+
["rls", 180],
|
|
21
|
+
["policy", 190],
|
|
22
|
+
["default-privilege", 200],
|
|
23
|
+
["grant", 210],
|
|
24
|
+
["comment", 220],
|
|
25
|
+
]);
|
|
26
|
+
const dropOrder = new Map([...createOrder.entries()].map(([kind, rank]) => [kind, 1000 - rank]));
|
|
27
|
+
export function sortOperations(operations, diagnostics) {
|
|
28
|
+
const base = [...operations].sort(compareOperations);
|
|
29
|
+
const operationByKey = new Map(base.map((operation) => [operation.key, operation]));
|
|
30
|
+
const operationKeyByIdentity = identityIndex(base);
|
|
31
|
+
const outgoing = new Map();
|
|
32
|
+
const incomingCount = new Map();
|
|
33
|
+
for (const operation of base) {
|
|
34
|
+
outgoing.set(operation.key, new Set());
|
|
35
|
+
incomingCount.set(operation.key, 0);
|
|
36
|
+
}
|
|
37
|
+
for (const operation of base) {
|
|
38
|
+
for (const dependencyKey of operationDependencyKeys(operation, operationKeyByIdentity)) {
|
|
39
|
+
if (dependencyKey === operation.key || !operationByKey.has(dependencyKey)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (operation.kind === "drop") {
|
|
43
|
+
addEdge(operation.key, dependencyKey, outgoing, incomingCount);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
addEdge(dependencyKey, operation.key, outgoing, incomingCount);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const remainingIncoming = new Map(incomingCount);
|
|
51
|
+
const ready = base.filter((operation) => remainingIncoming.get(operation.key) === 0);
|
|
52
|
+
const sorted = [];
|
|
53
|
+
while (ready.length > 0) {
|
|
54
|
+
ready.sort(compareOperations);
|
|
55
|
+
const operation = ready.shift();
|
|
56
|
+
if (!operation) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
sorted.push(operation);
|
|
60
|
+
for (const target of outgoing.get(operation.key) ?? []) {
|
|
61
|
+
const nextCount = (remainingIncoming.get(target) ?? 0) - 1;
|
|
62
|
+
remainingIncoming.set(target, nextCount);
|
|
63
|
+
if (nextCount === 0) {
|
|
64
|
+
const targetOperation = operationByKey.get(target);
|
|
65
|
+
if (targetOperation) {
|
|
66
|
+
ready.push(targetOperation);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (sorted.length !== base.length) {
|
|
72
|
+
const leftover = new Set(base
|
|
73
|
+
.filter((operation) => (remainingIncoming.get(operation.key) ?? 0) > 0)
|
|
74
|
+
.map((operation) => operation.key));
|
|
75
|
+
diagnostics.push(diagnostic("SUPA_PLAN_DEPENDENCY_CYCLE", "error", "dependency ordering has a cycle", {
|
|
76
|
+
hint: traceCycle(leftover, outgoing) ?? [...leftover].join(", "),
|
|
77
|
+
}));
|
|
78
|
+
return base;
|
|
79
|
+
}
|
|
80
|
+
return sorted;
|
|
81
|
+
}
|
|
82
|
+
function identityIndex(operations) {
|
|
83
|
+
const index = new Map();
|
|
84
|
+
for (const operation of operations) {
|
|
85
|
+
for (const object of [operation.before, operation.after]) {
|
|
86
|
+
if (!object) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
index.set(refIdentity(object.ref), operation.key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return index;
|
|
93
|
+
}
|
|
94
|
+
function refIdentity(ref) {
|
|
95
|
+
if (ref.kind === "schema") {
|
|
96
|
+
return ref.name;
|
|
97
|
+
}
|
|
98
|
+
return `${ref.schema ?? "public"}.${ref.name}`;
|
|
99
|
+
}
|
|
100
|
+
function operationDependencyKeys(operation, operationKeyByIdentity) {
|
|
101
|
+
const source = operation.kind === "drop" ? operation.before : (operation.after ?? operation.before);
|
|
102
|
+
if (!source) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const keys = new Set();
|
|
106
|
+
for (const reference of source.dependencies) {
|
|
107
|
+
const operationKey = operationKeyByIdentity.get(reference);
|
|
108
|
+
if (operationKey) {
|
|
109
|
+
keys.add(operationKey);
|
|
110
|
+
}
|
|
111
|
+
const schemaOperationKey = operationKeyByIdentity.get(reference.split(".")[0] ?? "");
|
|
112
|
+
if (schemaOperationKey) {
|
|
113
|
+
keys.add(schemaOperationKey);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const ownSchema = source.ref.schema;
|
|
117
|
+
if (ownSchema) {
|
|
118
|
+
const schemaOperationKey = operationKeyByIdentity.get(ownSchema);
|
|
119
|
+
if (schemaOperationKey) {
|
|
120
|
+
keys.add(schemaOperationKey);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...keys];
|
|
124
|
+
}
|
|
125
|
+
function addEdge(from, to, outgoing, incomingCount) {
|
|
126
|
+
const targets = outgoing.get(from);
|
|
127
|
+
if (!targets || targets.has(to)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
targets.add(to);
|
|
131
|
+
incomingCount.set(to, (incomingCount.get(to) ?? 0) + 1);
|
|
132
|
+
}
|
|
133
|
+
function traceCycle(leftover, outgoing) {
|
|
134
|
+
for (const start of leftover) {
|
|
135
|
+
const path = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
let current = start;
|
|
138
|
+
while (current !== undefined && !seen.has(current)) {
|
|
139
|
+
seen.add(current);
|
|
140
|
+
path.push(current);
|
|
141
|
+
current = [...(outgoing.get(current) ?? [])].find((target) => leftover.has(target));
|
|
142
|
+
}
|
|
143
|
+
if (current !== undefined) {
|
|
144
|
+
const cycleStart = path.indexOf(current);
|
|
145
|
+
return [...path.slice(cycleStart), current].join(" -> ");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
export function compareOperations(left, right) {
|
|
151
|
+
const rank = operationRank(left) - operationRank(right);
|
|
152
|
+
if (rank !== 0) {
|
|
153
|
+
return rank;
|
|
154
|
+
}
|
|
155
|
+
const ordinal = operationOrdinal(left) - operationOrdinal(right);
|
|
156
|
+
if (ordinal !== 0) {
|
|
157
|
+
return ordinal;
|
|
158
|
+
}
|
|
159
|
+
return left.key.localeCompare(right.key);
|
|
160
|
+
}
|
|
161
|
+
function operationRank(operation) {
|
|
162
|
+
if (operation.kind === "drop") {
|
|
163
|
+
return dropOrder.get(operation.ref.kind) ?? 500;
|
|
164
|
+
}
|
|
165
|
+
if (operation.kind === "rename") {
|
|
166
|
+
return 900 + (createOrder.get(operation.ref.kind) ?? 500);
|
|
167
|
+
}
|
|
168
|
+
if (operation.kind === "alter") {
|
|
169
|
+
return 1050 + (createOrder.get(operation.ref.kind) ?? 500);
|
|
170
|
+
}
|
|
171
|
+
return 1000 + (createOrder.get(operation.ref.kind) ?? 500);
|
|
172
|
+
}
|
|
173
|
+
function operationOrdinal(operation) {
|
|
174
|
+
if (operation.kind === "drop") {
|
|
175
|
+
return -(operation.before?.ordinal ?? 0);
|
|
176
|
+
}
|
|
177
|
+
return operation.after?.ordinal ?? operation.before?.ordinal ?? 0;
|
|
178
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MigrationOperation, SupaschemaConfig } from "./core.js";
|
|
2
|
+
export declare function isDestructiveAllowed(key: string, config: SupaschemaConfig): boolean;
|
|
3
|
+
export declare function refineReplaceOperation(operation: MigrationOperation, config: SupaschemaConfig): MigrationOperation;
|
|
4
|
+
//# sourceMappingURL=planner-replace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner-replace.d.ts","sourceRoot":"","sources":["../src/planner-replace.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAgB,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAIpF,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CASnF;AAED,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,kBAAkB,EAC7B,MAAM,EAAE,gBAAgB,GACvB,kBAAkB,CAQpB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { diagnostic } from "./diagnostics.js";
|
|
2
|
+
import { stableJson } from "./hash.js";
|
|
3
|
+
export function isDestructiveAllowed(key, config) {
|
|
4
|
+
if (config.destructiveChanges === "allow") {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
if (config.destructiveChanges === "block") {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const hints = config.hints.destructive ?? [];
|
|
11
|
+
return hints.includes("*") || hints.includes(key);
|
|
12
|
+
}
|
|
13
|
+
export function refineReplaceOperation(operation, config) {
|
|
14
|
+
if (operation.ref.kind === "view") {
|
|
15
|
+
return refineViewReplace(operation, config);
|
|
16
|
+
}
|
|
17
|
+
if (operation.ref.kind === "function" || operation.ref.kind === "procedure") {
|
|
18
|
+
return refineRoutineReplace(operation, config);
|
|
19
|
+
}
|
|
20
|
+
return operation;
|
|
21
|
+
}
|
|
22
|
+
function refineViewReplace(operation, config) {
|
|
23
|
+
const before = viewColumns(operation.before);
|
|
24
|
+
const after = viewColumns(operation.after);
|
|
25
|
+
if (!before || !after) {
|
|
26
|
+
return operation;
|
|
27
|
+
}
|
|
28
|
+
operation.diagnostics = operation.diagnostics.filter((item) => item.code !== "SUPA_PLAN_VIEW_REPLACE_VERIFY_REQUIRED");
|
|
29
|
+
const prefixCompatible = after.length >= before.length && before.every((column, index) => after[index] === column);
|
|
30
|
+
if (prefixCompatible) {
|
|
31
|
+
return operation;
|
|
32
|
+
}
|
|
33
|
+
return markDropRequired(operation, config, "viewDropRequired", {
|
|
34
|
+
code: "SUPA_PLAN_VIEW_REPLACE_INCOMPATIBLE",
|
|
35
|
+
hint: `Add "${operation.key}" to hints.destructive to render a guarded DROP VIEW + CREATE after review.`,
|
|
36
|
+
message: "view replacement drops, renames, or reorders output columns; CREATE OR REPLACE VIEW cannot apply it",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function refineRoutineReplace(operation, config) {
|
|
40
|
+
const before = routineShape(operation.before);
|
|
41
|
+
const after = routineShape(operation.after);
|
|
42
|
+
if (before === after) {
|
|
43
|
+
return operation;
|
|
44
|
+
}
|
|
45
|
+
return markDropRequired(operation, config, "routineDropRequired", {
|
|
46
|
+
code: "SUPA_PLAN_ROUTINE_RETURN_TYPE_CHANGED",
|
|
47
|
+
hint: `Add "${operation.key}" to hints.destructive to render a guarded DROP + CREATE after review.`,
|
|
48
|
+
message: "routine replacement changes the return type or OUT parameters; CREATE OR REPLACE cannot apply it",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function markDropRequired(operation, config, metadataFlag, failure) {
|
|
52
|
+
operation.metadata[metadataFlag] = true;
|
|
53
|
+
operation.destructive = true;
|
|
54
|
+
if (!isDestructiveAllowed(operation.key, config)) {
|
|
55
|
+
operation.blocked = true;
|
|
56
|
+
operation.diagnostics.push(diagnostic(failure.code, "error", failure.message, {
|
|
57
|
+
hint: failure.hint,
|
|
58
|
+
ref: operation.ref,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
return operation;
|
|
62
|
+
}
|
|
63
|
+
function viewColumns(object) {
|
|
64
|
+
const columns = object?.metadata.viewColumns;
|
|
65
|
+
if (!Array.isArray(columns)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const names = columns.filter((column) => typeof column === "string");
|
|
69
|
+
return names.length === columns.length ? names : undefined;
|
|
70
|
+
}
|
|
71
|
+
function routineShape(object) {
|
|
72
|
+
return stableJson({
|
|
73
|
+
outParams: object?.metadata.outParams ?? null,
|
|
74
|
+
returns: object?.metadata.returns ?? null,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MigrationOperation, SchemaObject, SupaschemaConfig } from "./core.js";
|
|
2
|
+
export declare function makeTableAlterOperation(before: SchemaObject, after: SchemaObject, config: SupaschemaConfig): MigrationOperation | undefined;
|
|
3
|
+
//# sourceMappingURL=planner-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner-table.d.ts","sourceRoot":"","sources":["../src/planner-table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAEjB,MAAM,WAAW,CAAC;AAgBnB,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,gBAAgB,GACvB,kBAAkB,GAAG,SAAS,CAmFhC"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { diagnostic } from "./diagnostics.js";
|
|
2
|
+
import { stableJson } from "./hash.js";
|
|
3
|
+
import { isDestructiveAllowed } from "./planner-replace.js";
|
|
4
|
+
export function makeTableAlterOperation(before, after, config) {
|
|
5
|
+
if (before.ref.kind !== "table" || after.ref.kind !== "table") {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const beforeShape = canonicalShape(before);
|
|
9
|
+
const afterShape = canonicalShape(after);
|
|
10
|
+
if (!beforeShape || !afterShape) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
if (stableJson({ ...beforeShape, columns: undefined }) !==
|
|
14
|
+
stableJson({ ...afterShape, columns: undefined })) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const beforeColumns = canonicalColumns(beforeShape);
|
|
18
|
+
const afterColumns = canonicalColumns(afterShape);
|
|
19
|
+
if (!beforeColumns || !afterColumns) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const beforeByName = new Map(beforeColumns.map((column) => [column.name, column]));
|
|
23
|
+
const afterByName = new Map(afterColumns.map((column) => [column.name, column]));
|
|
24
|
+
const dropColumns = beforeColumns
|
|
25
|
+
.filter((column) => !afterByName.has(column.name))
|
|
26
|
+
.map((column) => column.name);
|
|
27
|
+
const addColumns = tableColumns(after).filter((column) => !beforeByName.has(column.name));
|
|
28
|
+
const alterColumns = [];
|
|
29
|
+
for (const column of afterColumns) {
|
|
30
|
+
const previous = beforeByName.get(column.name);
|
|
31
|
+
if (!previous || stableJson(previous) === stableJson(column)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const alteration = columnAlteration(previous, column, tableColumns(after));
|
|
35
|
+
if (!alteration) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
alterColumns.push(alteration);
|
|
39
|
+
}
|
|
40
|
+
if (dropColumns.length === 0 && addColumns.length === 0 && alterColumns.length === 0) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const diagnostics = [];
|
|
44
|
+
let blocked = false;
|
|
45
|
+
for (const column of addColumns) {
|
|
46
|
+
const unsafeReason = unsafeAddColumnReason(column);
|
|
47
|
+
if (!unsafeReason) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
blocked = true;
|
|
51
|
+
diagnostics.push(diagnostic("SUPA_PLAN_ADD_COLUMN_UNSAFE", "error", unsafeReason, {
|
|
52
|
+
hint: "Use an explicit reviewed migration for column rewrites, backfills, constraints, or table scans.",
|
|
53
|
+
ref: after.ref,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
const destructive = dropColumns.length > 0 || alterColumns.some((alteration) => alteration.type !== undefined);
|
|
57
|
+
if (destructive && !isDestructiveAllowed(after.key, config)) {
|
|
58
|
+
blocked = true;
|
|
59
|
+
diagnostics.push(diagnostic("SUPA_PLAN_COLUMN_ALTER_HINT_REQUIRED", "error", "column drops and type changes require an explicit destructive-change hint", {
|
|
60
|
+
hint: `Add "${after.key}" to hints.destructive after reviewing the rendered column ALTERs.`,
|
|
61
|
+
ref: after.ref,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
after,
|
|
66
|
+
before,
|
|
67
|
+
blocked,
|
|
68
|
+
destructive,
|
|
69
|
+
diagnostics,
|
|
70
|
+
key: after.key,
|
|
71
|
+
kind: "alter",
|
|
72
|
+
metadata: { addColumns, alterColumns, dropColumns },
|
|
73
|
+
ref: after.ref,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function columnAlteration(before, after, afterColumns) {
|
|
77
|
+
if (stableJson(before.identity ?? null) !== stableJson(after.identity ?? null) ||
|
|
78
|
+
stableJson(before.generated ?? null) !== stableJson(after.generated ?? null)) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const alteration = { name: after.name };
|
|
82
|
+
let facetsExplained = 0;
|
|
83
|
+
let facetsChanged = 0;
|
|
84
|
+
if (before.type !== after.type) {
|
|
85
|
+
facetsChanged += 1;
|
|
86
|
+
if (typeof after.type === "string") {
|
|
87
|
+
alteration.type = after.type;
|
|
88
|
+
facetsExplained += 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (before.notNull !== after.notNull) {
|
|
92
|
+
facetsChanged += 1;
|
|
93
|
+
facetsExplained += 1;
|
|
94
|
+
if (after.notNull === true) {
|
|
95
|
+
alteration.setNotNull = true;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
alteration.dropNotNull = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (stableJson(before.default ?? null) !== stableJson(after.default ?? null)) {
|
|
102
|
+
facetsChanged += 1;
|
|
103
|
+
if (after.default === undefined) {
|
|
104
|
+
alteration.dropDefault = true;
|
|
105
|
+
facetsExplained += 1;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const expression = afterColumns.find((column) => column.name === after.name)?.defaultExpression;
|
|
109
|
+
if (expression !== undefined) {
|
|
110
|
+
alteration.setDefault = expression;
|
|
111
|
+
facetsExplained += 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const residual = (entry) => {
|
|
116
|
+
const { default: _default, notNull: _notNull, type: _type, ...rest } = entry;
|
|
117
|
+
return stableJson(rest);
|
|
118
|
+
};
|
|
119
|
+
if (residual(before) !== residual(after)) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
if (facetsChanged === 0 || facetsExplained !== facetsChanged) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return alteration;
|
|
126
|
+
}
|
|
127
|
+
function canonicalShape(object) {
|
|
128
|
+
const shape = object.metadata.canonicalShape;
|
|
129
|
+
return shape && typeof shape === "object" ? shape : undefined;
|
|
130
|
+
}
|
|
131
|
+
function canonicalColumns(shape) {
|
|
132
|
+
const columns = shape.columns;
|
|
133
|
+
if (!Array.isArray(columns)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const entries = [];
|
|
137
|
+
for (const column of columns) {
|
|
138
|
+
if (!column ||
|
|
139
|
+
typeof column !== "object" ||
|
|
140
|
+
typeof column.name !== "string") {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
entries.push(column);
|
|
144
|
+
}
|
|
145
|
+
return entries;
|
|
146
|
+
}
|
|
147
|
+
function tableColumns(object) {
|
|
148
|
+
const columns = object.metadata.columns;
|
|
149
|
+
if (!Array.isArray(columns)) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
return columns.filter((column) => Boolean(column) &&
|
|
153
|
+
typeof column === "object" &&
|
|
154
|
+
typeof column.name === "string" &&
|
|
155
|
+
typeof column.definition === "string");
|
|
156
|
+
}
|
|
157
|
+
function unsafeAddColumnReason(column) {
|
|
158
|
+
if (column.identity || column.generated || column.hasInlineConstraint) {
|
|
159
|
+
return `column "${column.name}" adds inline constraints or generated behavior that requires explicit review`;
|
|
160
|
+
}
|
|
161
|
+
if (column.notNull === true && column.hasDefault !== true) {
|
|
162
|
+
return `column "${column.name}" is NOT NULL without a default and can fail on populated tables`;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MigrationPlan, RenderOptions, SchemaModel } from "./core.js";
|
|
2
|
+
type DiffOptions = Pick<RenderOptions, "config">;
|
|
3
|
+
export declare function planSchemaDiff(from: SchemaModel, to: SchemaModel, options?: DiffOptions): MigrationPlan;
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=planner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../src/planner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,aAAa,EAGb,aAAa,EACb,WAAW,EAGZ,MAAM,WAAW,CAAC;AAOnB,KAAK,WAAW,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AAEjD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,WAAW,EACjB,EAAE,EAAE,WAAW,EACf,OAAO,GAAE,WAAgB,GACxB,aAAa,CAkFf"}
|