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,304 @@
|
|
|
1
|
+
import { asRecord, rangeVarName, readArray, readNumber, readString, stringList } from "./ast.js";
|
|
2
|
+
import { formatQualifiedName, quoteIdent } from "./identifiers.js";
|
|
3
|
+
import { elementText, findCharOutsideQuotes, findMatchingParen, fromByteString, tableElements, toByteString, } from "./statements.js";
|
|
4
|
+
export function tableConstraintSyntheses(createStmt, sql, byteOffset = 0) {
|
|
5
|
+
const relation = rangeVarName(createStmt.relation);
|
|
6
|
+
if (!relation) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const qualified = formatQualifiedName(relation.schema, relation.name);
|
|
10
|
+
const bytes = toByteString(sql);
|
|
11
|
+
const syntheses = [];
|
|
12
|
+
for (const element of tableElements(createStmt, bytes, byteOffset)) {
|
|
13
|
+
if (element.isColumn) {
|
|
14
|
+
syntheses.push(...inlineConstraintSyntheses(element, bytes, byteOffset, relation.name, qualified));
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const constraint = element.node;
|
|
18
|
+
const text = fromByteString(elementText(bytes, element));
|
|
19
|
+
const conname = readString(constraint.conname);
|
|
20
|
+
const name = conname ?? defaultConstraintName(relation.name, constraint, []);
|
|
21
|
+
if (!name) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const fragment = conname ? text : `CONSTRAINT ${quoteIdent(name)} ${text}`;
|
|
25
|
+
syntheses.push({ name, sql: `ALTER TABLE ONLY ${qualified} ADD ${fragment}` });
|
|
26
|
+
}
|
|
27
|
+
return syntheses;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Rebuilds the CREATE TABLE statement without its declared constraints so the
|
|
31
|
+
* table object's SQL matches the columns-only shape the catalog lane emits.
|
|
32
|
+
* Raw-apply consumers (verify, parity tests) then apply the table once and
|
|
33
|
+
* each constraint once instead of creating hoisted constraints twice. Returns
|
|
34
|
+
* undefined when the statement declares no hoistable constraints.
|
|
35
|
+
*/
|
|
36
|
+
export function stripDeclaredConstraints(createStmt, sql, byteOffset = 0) {
|
|
37
|
+
const relation = asRecord(createStmt.relation);
|
|
38
|
+
const bytes = toByteString(sql);
|
|
39
|
+
const elements = tableElements(createStmt, bytes, byteOffset);
|
|
40
|
+
if (elements.length === 0) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
let strippedAny = false;
|
|
44
|
+
const pieces = [];
|
|
45
|
+
const primaryColumns = primaryKeyColumns(elements);
|
|
46
|
+
for (const element of elements) {
|
|
47
|
+
if (!element.isColumn) {
|
|
48
|
+
strippedAny = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const { piece, stripped } = columnPieceWithoutHoisted(element, bytes, byteOffset);
|
|
52
|
+
if (stripped) {
|
|
53
|
+
strippedAny = true;
|
|
54
|
+
}
|
|
55
|
+
if (piece.length === 0) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// A stripped PRIMARY KEY implied NOT NULL on its columns; spell it
|
|
59
|
+
// explicitly so the rebuilt statement keeps the same column facts. The
|
|
60
|
+
// AST is authoritative: the piece retains a NOT NULL span exactly when a
|
|
61
|
+
// CONSTR_NOTNULL constraint exists, because NOT NULL is never hoisted.
|
|
62
|
+
const columnName = readString(element.node.colname);
|
|
63
|
+
if (columnName && primaryColumns.has(columnName) && !hasExplicitNotNull(element.node)) {
|
|
64
|
+
pieces.push(`${piece} NOT NULL`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
pieces.push(piece);
|
|
68
|
+
}
|
|
69
|
+
if (!strippedAny || pieces.length === 0) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const relationLocation = (readNumber(relation?.location) ?? 0) - byteOffset;
|
|
73
|
+
const open = findCharOutsideQuotes(bytes, "(", Math.max(relationLocation, 0));
|
|
74
|
+
if (open === -1) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const close = findMatchingParen(bytes, open);
|
|
78
|
+
if (close === -1) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const head = bytes.slice(0, open + 1);
|
|
82
|
+
const tail = bytes.slice(close);
|
|
83
|
+
return fromByteString(`${head}\n ${pieces.join(",\n ")}\n${tail}`);
|
|
84
|
+
}
|
|
85
|
+
function primaryKeyColumns(elements) {
|
|
86
|
+
const columns = new Set();
|
|
87
|
+
for (const element of elements) {
|
|
88
|
+
if (element.isColumn) {
|
|
89
|
+
const name = readString(element.node.colname);
|
|
90
|
+
const hasPrimary = readArray(element.node.constraints).some((item) => readString(asRecord(asRecord(item)?.Constraint)?.contype) === "CONSTR_PRIMARY");
|
|
91
|
+
if (name && hasPrimary) {
|
|
92
|
+
columns.add(name);
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (readString(element.node.contype) === "CONSTR_PRIMARY") {
|
|
97
|
+
for (const key of stringList(element.node.keys)) {
|
|
98
|
+
columns.add(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return columns;
|
|
103
|
+
}
|
|
104
|
+
function hasExplicitNotNull(columnDef) {
|
|
105
|
+
return readArray(columnDef.constraints).some((item) => readString(asRecord(asRecord(item)?.Constraint)?.contype) === "CONSTR_NOTNULL");
|
|
106
|
+
}
|
|
107
|
+
function columnPieceWithoutHoisted(element, bytes, byteOffset) {
|
|
108
|
+
const located = locatedInlineConstraints(element, byteOffset);
|
|
109
|
+
let piece = "";
|
|
110
|
+
let cursor = element.start;
|
|
111
|
+
let stripped = false;
|
|
112
|
+
for (const [index, item] of located.entries()) {
|
|
113
|
+
const end = located[index + 1]?.location ?? element.end;
|
|
114
|
+
const contype = readString(item.constraint.contype);
|
|
115
|
+
if (contype && inlineConstraintTypes.has(contype)) {
|
|
116
|
+
piece += bytes.slice(cursor, item.location);
|
|
117
|
+
cursor = end;
|
|
118
|
+
stripped = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
piece += bytes.slice(cursor, element.end);
|
|
122
|
+
piece = piece.trim();
|
|
123
|
+
if (piece.endsWith(",")) {
|
|
124
|
+
piece = piece.slice(0, -1).trimEnd();
|
|
125
|
+
}
|
|
126
|
+
return { piece: fromByteString(piece), stripped };
|
|
127
|
+
}
|
|
128
|
+
function inlineConstraintSyntheses(element, bytes, byteOffset, table, qualified) {
|
|
129
|
+
const column = readString(element.node.colname);
|
|
130
|
+
if (!column) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const located = locatedInlineConstraints(element, byteOffset);
|
|
134
|
+
const syntheses = [];
|
|
135
|
+
for (const [index, item] of located.entries()) {
|
|
136
|
+
const contype = readString(item.constraint.contype);
|
|
137
|
+
if (!contype || !inlineConstraintTypes.has(contype)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const end = located[index + 1]?.location ?? element.end;
|
|
141
|
+
let text = fromByteString(bytes.slice(item.location, end)).trim();
|
|
142
|
+
if (text.endsWith(",")) {
|
|
143
|
+
text = text.slice(0, -1).trimEnd();
|
|
144
|
+
}
|
|
145
|
+
const conname = readString(item.constraint.conname);
|
|
146
|
+
if (conname) {
|
|
147
|
+
// The AST already classified this as a named constraint; scanning only
|
|
148
|
+
// locates where the `CONSTRAINT <name>` prefix ends so the remainder
|
|
149
|
+
// fits the table-level template for its type.
|
|
150
|
+
text = skipConstraintNamePrefix(text);
|
|
151
|
+
}
|
|
152
|
+
const name = conname ?? defaultConstraintName(table, item.constraint, [column]);
|
|
153
|
+
if (!name) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const body = inlineConstraintBody(contype, column, text);
|
|
157
|
+
if (!body) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
syntheses.push({
|
|
161
|
+
name,
|
|
162
|
+
sql: `ALTER TABLE ONLY ${qualified} ADD CONSTRAINT ${quoteIdent(name)} ${body}`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return syntheses;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Skips a leading `CONSTRAINT <name>` token pair and returns the remainder.
|
|
169
|
+
* Character scanning only — whether the constraint is named comes from the
|
|
170
|
+
* AST (`conname`); this mirrors the render-guard keyword scanner in facts.ts.
|
|
171
|
+
*/
|
|
172
|
+
function skipConstraintNamePrefix(text) {
|
|
173
|
+
let index = skipSqlWhitespace(text, 0);
|
|
174
|
+
const keywordEnd = index + "CONSTRAINT".length;
|
|
175
|
+
if (text.slice(index, keywordEnd).toUpperCase() !== "CONSTRAINT") {
|
|
176
|
+
return text;
|
|
177
|
+
}
|
|
178
|
+
index = skipSqlWhitespace(text, keywordEnd);
|
|
179
|
+
if (text[index] === '"') {
|
|
180
|
+
index += 1;
|
|
181
|
+
while (index < text.length) {
|
|
182
|
+
if (text[index] === '"' && text[index + 1] === '"') {
|
|
183
|
+
index += 2;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (text[index] === '"') {
|
|
187
|
+
index += 1;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
index += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
while (index < text.length && isIdentifierChar(text[index] ?? "")) {
|
|
195
|
+
index += 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return text.slice(skipSqlWhitespace(text, index));
|
|
199
|
+
}
|
|
200
|
+
function isIdentifierChar(char) {
|
|
201
|
+
return ((char >= "a" && char <= "z") ||
|
|
202
|
+
(char >= "A" && char <= "Z") ||
|
|
203
|
+
(char >= "0" && char <= "9") ||
|
|
204
|
+
char === "_" ||
|
|
205
|
+
char === "$");
|
|
206
|
+
}
|
|
207
|
+
function skipSqlWhitespace(text, start) {
|
|
208
|
+
let index = start;
|
|
209
|
+
while (index < text.length) {
|
|
210
|
+
const char = text[index];
|
|
211
|
+
if (char === " " || char === "\t" || char === "\n" || char === "\r") {
|
|
212
|
+
index += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
return index;
|
|
218
|
+
}
|
|
219
|
+
function locatedInlineConstraints(element, byteOffset) {
|
|
220
|
+
return readArray(element.node.constraints)
|
|
221
|
+
.map((item) => asRecord(asRecord(item)?.Constraint))
|
|
222
|
+
.filter((item) => item !== undefined)
|
|
223
|
+
.map((constraint) => ({
|
|
224
|
+
constraint,
|
|
225
|
+
location: (readNumber(constraint.location) ?? -1) - byteOffset,
|
|
226
|
+
}))
|
|
227
|
+
.filter((item) => item.location >= 0)
|
|
228
|
+
.sort((left, right) => left.location - right.location);
|
|
229
|
+
}
|
|
230
|
+
const inlineConstraintTypes = new Set([
|
|
231
|
+
"CONSTR_CHECK",
|
|
232
|
+
"CONSTR_FOREIGN",
|
|
233
|
+
"CONSTR_PRIMARY",
|
|
234
|
+
"CONSTR_UNIQUE",
|
|
235
|
+
]);
|
|
236
|
+
function inlineConstraintBody(contype, column, text) {
|
|
237
|
+
switch (contype) {
|
|
238
|
+
case "CONSTR_PRIMARY":
|
|
239
|
+
return `PRIMARY KEY (${quoteIdent(column)})`;
|
|
240
|
+
case "CONSTR_UNIQUE":
|
|
241
|
+
return `UNIQUE (${quoteIdent(column)})`;
|
|
242
|
+
case "CONSTR_CHECK":
|
|
243
|
+
return text;
|
|
244
|
+
case "CONSTR_FOREIGN":
|
|
245
|
+
return `FOREIGN KEY (${quoteIdent(column)}) ${text}`;
|
|
246
|
+
default:
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function defaultConstraintName(table, constraint, impliedColumns) {
|
|
251
|
+
const contype = readString(constraint.contype);
|
|
252
|
+
const keys = stringList(constraint.keys);
|
|
253
|
+
const fkAttrs = stringList(constraint.fk_attrs);
|
|
254
|
+
const columns = fkAttrs.length > 0 ? fkAttrs : keys.length > 0 ? keys : impliedColumns;
|
|
255
|
+
const joined = columns.join("_");
|
|
256
|
+
switch (contype) {
|
|
257
|
+
case "CONSTR_PRIMARY":
|
|
258
|
+
return `${table}_pkey`;
|
|
259
|
+
case "CONSTR_UNIQUE":
|
|
260
|
+
return joined ? `${table}_${joined}_key` : undefined;
|
|
261
|
+
case "CONSTR_FOREIGN":
|
|
262
|
+
return joined ? `${table}_${joined}_fkey` : undefined;
|
|
263
|
+
case "CONSTR_CHECK": {
|
|
264
|
+
// PostgreSQL names a check after its column only when the expression
|
|
265
|
+
// references exactly one column; otherwise the bare `<table>_check`.
|
|
266
|
+
const referenced = columns.length > 0 ? columns : expressionColumns(constraint.raw_expr);
|
|
267
|
+
return referenced.length === 1 ? `${table}_${referenced[0]}_check` : `${table}_check`;
|
|
268
|
+
}
|
|
269
|
+
case "CONSTR_EXCLUSION":
|
|
270
|
+
return joined ? `${table}_${joined}_excl` : undefined;
|
|
271
|
+
default:
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function expressionColumns(expression) {
|
|
276
|
+
const columns = new Set();
|
|
277
|
+
const visit = (value) => {
|
|
278
|
+
if (Array.isArray(value)) {
|
|
279
|
+
for (const item of value) {
|
|
280
|
+
visit(item);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const node = asRecord(value);
|
|
285
|
+
if (!node) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const columnRef = asRecord(node.ColumnRef);
|
|
289
|
+
if (columnRef) {
|
|
290
|
+
const fields = stringList(columnRef.fields);
|
|
291
|
+
const name = fields.at(-1);
|
|
292
|
+
if (name) {
|
|
293
|
+
columns.add(name);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const child of Object.values(node)) {
|
|
297
|
+
if (child && typeof child === "object") {
|
|
298
|
+
visit(child);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
visit(expression);
|
|
303
|
+
return [...columns];
|
|
304
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AstNode } from "./ast.js";
|
|
2
|
+
export declare function canonicalColumnType(typeName: unknown): string;
|
|
3
|
+
/**
|
|
4
|
+
* Canonical table identity for hashing. The raw parse tree differs between a
|
|
5
|
+
* declarative source ("id bigint PRIMARY KEY") and a catalog reconstruction
|
|
6
|
+
* ('"id" bigint NOT NULL' plus a separate ADD CONSTRAINT), so the shape
|
|
7
|
+
* carries columns only: every table constraint — inline, in-CREATE, or
|
|
8
|
+
* ALTER-declared — is identity-owned by its own `constraint:` object, making
|
|
9
|
+
* table identity independent of where constraints are declared. Inline and
|
|
10
|
+
* in-CREATE constraints still apply primary-key-implied NOT NULL to columns
|
|
11
|
+
* before exclusion.
|
|
12
|
+
*/
|
|
13
|
+
export declare function canonicalTableShape(node: AstNode): Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* A regclass cast's string literal is an identifier reference, and
|
|
16
|
+
* pg_get_expr renders it unquoted (`ai.seq`) while declarative sources often
|
|
17
|
+
* quote it (`"ai"."seq"`). Both name the same object, so the canonical shape
|
|
18
|
+
* stores the unquoted spelling.
|
|
19
|
+
*/
|
|
20
|
+
export declare function canonicalizeRegclassLiterals(node: unknown): unknown;
|
|
21
|
+
/**
|
|
22
|
+
* Canonical sequence identity: option DefElems normalize to a keyed object
|
|
23
|
+
* with PostgreSQL's ascending defaults dropped, so a declarative
|
|
24
|
+
* `START 100 INCREMENT 5` and the catalog reconstruction hash identically
|
|
25
|
+
* regardless of option order or explicitly-spelled defaults.
|
|
26
|
+
*/
|
|
27
|
+
export declare function canonicalSequenceShape(node: AstNode): Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Canonical constraint identity shared by every declaration site: an
|
|
30
|
+
* ALTER TABLE ADD CONSTRAINT statement, an in-CREATE table-level constraint,
|
|
31
|
+
* an inline column constraint, and a catalog pg_constraint row must all hash
|
|
32
|
+
* to the same shape for the same table.
|
|
33
|
+
*/
|
|
34
|
+
export declare function canonicalConstraintShape(constraint: AstNode, table: {
|
|
35
|
+
name: string;
|
|
36
|
+
schema: string;
|
|
37
|
+
}, impliedColumns?: string[]): Record<string, unknown>;
|
|
38
|
+
//# sourceMappingURL=table-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-shape.d.ts","sourceRoot":"","sources":["../../src/sql/table-shape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAaxC,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAmB7D;AAQD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuC1E;AAwDD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAyBnE;AAkCD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkD7E;AAuBD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,OAAO,EACnB,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACvC,cAAc,GAAE,MAAM,EAAO,GAC5B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKzB"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { asRecord, readArray, readNumber, readString, stringList, typeNameToSql } from "./ast.js";
|
|
2
|
+
import { stripLocations } from "./object-hash.js";
|
|
3
|
+
export function canonicalColumnType(typeName) {
|
|
4
|
+
const base = typeNameToSql(typeName);
|
|
5
|
+
const node = asRecord(asRecord(typeName)?.TypeName) ?? asRecord(typeName);
|
|
6
|
+
const typmods = readArray(node?.typmods)
|
|
7
|
+
.map((item) => {
|
|
8
|
+
const constant = asRecord(asRecord(item)?.A_Const);
|
|
9
|
+
const integer = asRecord(constant?.ival);
|
|
10
|
+
const value = readNumber(integer?.ival);
|
|
11
|
+
return value === undefined ? undefined : String(value);
|
|
12
|
+
})
|
|
13
|
+
.filter((value) => value !== undefined);
|
|
14
|
+
if (typmods.length === 0) {
|
|
15
|
+
return base;
|
|
16
|
+
}
|
|
17
|
+
const arrayStart = base.indexOf("[]");
|
|
18
|
+
if (arrayStart === -1) {
|
|
19
|
+
return `${base}(${typmods.join(", ")})`;
|
|
20
|
+
}
|
|
21
|
+
return `${base.slice(0, arrayStart)}(${typmods.join(", ")})${base.slice(arrayStart)}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Canonical table identity for hashing. The raw parse tree differs between a
|
|
25
|
+
* declarative source ("id bigint PRIMARY KEY") and a catalog reconstruction
|
|
26
|
+
* ('"id" bigint NOT NULL' plus a separate ADD CONSTRAINT), so the shape
|
|
27
|
+
* carries columns only: every table constraint — inline, in-CREATE, or
|
|
28
|
+
* ALTER-declared — is identity-owned by its own `constraint:` object, making
|
|
29
|
+
* table identity independent of where constraints are declared. Inline and
|
|
30
|
+
* in-CREATE constraints still apply primary-key-implied NOT NULL to columns
|
|
31
|
+
* before exclusion.
|
|
32
|
+
*/
|
|
33
|
+
export function canonicalTableShape(node) {
|
|
34
|
+
const relation = asRecord(node.relation);
|
|
35
|
+
const columns = [];
|
|
36
|
+
const constraints = [];
|
|
37
|
+
for (const item of readArray(node.tableElts)) {
|
|
38
|
+
const columnDef = asRecord(asRecord(item)?.ColumnDef);
|
|
39
|
+
if (columnDef) {
|
|
40
|
+
columns.push(canonicalColumn(columnDef, constraints));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const constraint = asRecord(asRecord(item)?.Constraint);
|
|
44
|
+
if (constraint) {
|
|
45
|
+
constraints.push(canonicalConstraint(constraint, []));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const primaryColumns = new Set(constraints
|
|
49
|
+
.filter((constraint) => constraint.type === "CONSTR_PRIMARY")
|
|
50
|
+
.flatMap((constraint) => constraint.columns));
|
|
51
|
+
for (const column of columns) {
|
|
52
|
+
if (primaryColumns.has(column.name)) {
|
|
53
|
+
column.notNull = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const shape = {
|
|
57
|
+
columns,
|
|
58
|
+
relation: {
|
|
59
|
+
name: readString(relation?.relname) ?? "",
|
|
60
|
+
persistence: readString(relation?.relpersistence) ?? "p",
|
|
61
|
+
schema: readString(relation?.schemaname) ?? "public",
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
for (const semanticKey of ["inhRelations", "oncommit", "options", "partspec", "tablespacename"]) {
|
|
65
|
+
if (node[semanticKey] !== undefined && node[semanticKey] !== null) {
|
|
66
|
+
shape[semanticKey] = stripLocations(node[semanticKey]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return shape;
|
|
70
|
+
}
|
|
71
|
+
function canonicalColumn(columnDef, constraints) {
|
|
72
|
+
const name = readString(columnDef.colname) ?? "";
|
|
73
|
+
const column = {
|
|
74
|
+
name,
|
|
75
|
+
notNull: false,
|
|
76
|
+
type: canonicalColumnType(columnDef.typeName),
|
|
77
|
+
};
|
|
78
|
+
for (const item of readArray(columnDef.constraints)) {
|
|
79
|
+
const constraint = asRecord(asRecord(item)?.Constraint);
|
|
80
|
+
const contype = readString(constraint?.contype);
|
|
81
|
+
if (!constraint || !contype) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
switch (contype) {
|
|
85
|
+
case "CONSTR_NOTNULL":
|
|
86
|
+
column.notNull = true;
|
|
87
|
+
break;
|
|
88
|
+
case "CONSTR_NULL":
|
|
89
|
+
break;
|
|
90
|
+
case "CONSTR_DEFAULT":
|
|
91
|
+
column.default = canonicalizeRegclassLiterals(stripLocations(unwrapColumnTypeCast(constraint.raw_expr, columnDef.typeName)));
|
|
92
|
+
break;
|
|
93
|
+
case "CONSTR_IDENTITY":
|
|
94
|
+
column.identity = readString(constraint.generated_when) ?? "a";
|
|
95
|
+
break;
|
|
96
|
+
case "CONSTR_GENERATED":
|
|
97
|
+
column.generated = stripLocations(constraint.raw_expr);
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
constraints.push(canonicalConstraint(constraint, [name]));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return column;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* pg_get_expr renders a column default as `'x'::type` while declarative
|
|
108
|
+
* sources usually write the bare literal; a cast to the column's own base
|
|
109
|
+
* type is implied, so it is dropped from the canonical default.
|
|
110
|
+
*/
|
|
111
|
+
function unwrapColumnTypeCast(expression, columnTypeName) {
|
|
112
|
+
const typeCast = asRecord(asRecord(expression)?.TypeCast);
|
|
113
|
+
if (!typeCast) {
|
|
114
|
+
return expression;
|
|
115
|
+
}
|
|
116
|
+
if (typeNameToSql(typeCast.typeName) !== typeNameToSql(columnTypeName)) {
|
|
117
|
+
return expression;
|
|
118
|
+
}
|
|
119
|
+
return typeCast.arg;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A regclass cast's string literal is an identifier reference, and
|
|
123
|
+
* pg_get_expr renders it unquoted (`ai.seq`) while declarative sources often
|
|
124
|
+
* quote it (`"ai"."seq"`). Both name the same object, so the canonical shape
|
|
125
|
+
* stores the unquoted spelling.
|
|
126
|
+
*/
|
|
127
|
+
export function canonicalizeRegclassLiterals(node) {
|
|
128
|
+
if (Array.isArray(node)) {
|
|
129
|
+
return node.map((item) => canonicalizeRegclassLiterals(item));
|
|
130
|
+
}
|
|
131
|
+
if (typeof node !== "object" || node === null) {
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
const record = node;
|
|
135
|
+
const typeCast = asRecord(record.TypeCast);
|
|
136
|
+
if (typeCast && typeNameToSql(typeCast.typeName) === "regclass") {
|
|
137
|
+
const constant = asRecord(asRecord(typeCast.arg)?.A_Const);
|
|
138
|
+
const sval = asRecord(constant?.sval);
|
|
139
|
+
const literal = readString(sval?.sval);
|
|
140
|
+
if (literal !== undefined) {
|
|
141
|
+
// The cast itself is dropped: `'x'::regclass` and bare `'x'` name the
|
|
142
|
+
// same object inside a default expression, and trees commonly omit the
|
|
143
|
+
// cast that pg_get_expr always renders.
|
|
144
|
+
return { A_Const: { ...constant, sval: { sval: unquoteQualifiedName(literal) } } };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const result = {};
|
|
148
|
+
for (const [key, value] of Object.entries(record)) {
|
|
149
|
+
result[key] = canonicalizeRegclassLiterals(value);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
function unquoteQualifiedName(literal) {
|
|
154
|
+
const parts = [];
|
|
155
|
+
let current = "";
|
|
156
|
+
let quoted = false;
|
|
157
|
+
for (let index = 0; index < literal.length; index += 1) {
|
|
158
|
+
const char = literal[index];
|
|
159
|
+
if (char === '"') {
|
|
160
|
+
if (quoted && literal[index + 1] === '"') {
|
|
161
|
+
current += '"';
|
|
162
|
+
index += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
quoted = !quoted;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (char === "." && !quoted) {
|
|
169
|
+
parts.push(current);
|
|
170
|
+
current = "";
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
current += char;
|
|
174
|
+
}
|
|
175
|
+
parts.push(current);
|
|
176
|
+
return parts.join(".");
|
|
177
|
+
}
|
|
178
|
+
const sequenceTypeMax = new Map([
|
|
179
|
+
["bigint", "9223372036854775807"],
|
|
180
|
+
["integer", "2147483647"],
|
|
181
|
+
["smallint", "32767"],
|
|
182
|
+
]);
|
|
183
|
+
/**
|
|
184
|
+
* Canonical sequence identity: option DefElems normalize to a keyed object
|
|
185
|
+
* with PostgreSQL's ascending defaults dropped, so a declarative
|
|
186
|
+
* `START 100 INCREMENT 5` and the catalog reconstruction hash identically
|
|
187
|
+
* regardless of option order or explicitly-spelled defaults.
|
|
188
|
+
*/
|
|
189
|
+
export function canonicalSequenceShape(node) {
|
|
190
|
+
const sequence = asRecord(node.sequence);
|
|
191
|
+
const shape = {
|
|
192
|
+
relation: {
|
|
193
|
+
name: readString(sequence?.relname) ?? "",
|
|
194
|
+
persistence: readString(sequence?.relpersistence) ?? "p",
|
|
195
|
+
schema: readString(sequence?.schemaname) ?? "public",
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
let dataType = "bigint";
|
|
199
|
+
const options = new Map();
|
|
200
|
+
for (const item of readArray(node.options)) {
|
|
201
|
+
const defElem = asRecord(asRecord(item)?.DefElem);
|
|
202
|
+
const name = readString(defElem?.defname);
|
|
203
|
+
if (!name) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
options.set(name, defElem?.arg);
|
|
207
|
+
}
|
|
208
|
+
const asType = options.get("as");
|
|
209
|
+
if (asType !== undefined) {
|
|
210
|
+
dataType = typeNameToSql(asType);
|
|
211
|
+
}
|
|
212
|
+
if (dataType !== "bigint") {
|
|
213
|
+
shape.as = dataType;
|
|
214
|
+
}
|
|
215
|
+
const defaults = new Map([
|
|
216
|
+
["cache", "1"],
|
|
217
|
+
["increment", "1"],
|
|
218
|
+
["maxvalue", sequenceTypeMax.get(dataType) ?? ""],
|
|
219
|
+
["minvalue", "1"],
|
|
220
|
+
["start", "1"],
|
|
221
|
+
]);
|
|
222
|
+
for (const [name, fallback] of defaults) {
|
|
223
|
+
const value = sequenceOptionValue(options.get(name));
|
|
224
|
+
if (value !== undefined && value !== fallback) {
|
|
225
|
+
shape[name] = value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (sequenceOptionValue(options.get("cycle")) === "true") {
|
|
229
|
+
shape.cycle = true;
|
|
230
|
+
}
|
|
231
|
+
const ownedBy = options.get("owned_by");
|
|
232
|
+
if (ownedBy !== undefined) {
|
|
233
|
+
const path = stringList(ownedBy);
|
|
234
|
+
if (path.length > 0 && path.at(-1) !== "none") {
|
|
235
|
+
shape.ownedBy = path.join(".");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return shape;
|
|
239
|
+
}
|
|
240
|
+
function sequenceOptionValue(arg) {
|
|
241
|
+
if (arg === undefined || arg === null) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
const integer = asRecord(asRecord(arg)?.Integer);
|
|
245
|
+
if (integer) {
|
|
246
|
+
return String(readNumber(integer.ival) ?? 0);
|
|
247
|
+
}
|
|
248
|
+
const float = asRecord(asRecord(arg)?.Float);
|
|
249
|
+
if (float) {
|
|
250
|
+
return readString(float.fval);
|
|
251
|
+
}
|
|
252
|
+
const boolean = asRecord(asRecord(arg)?.Boolean);
|
|
253
|
+
if (boolean) {
|
|
254
|
+
return boolean.boolval === true ? "true" : "false";
|
|
255
|
+
}
|
|
256
|
+
return readString(asRecord(asRecord(arg)?.String)?.sval);
|
|
257
|
+
}
|
|
258
|
+
const constraintIdentityKeys = new Set(["conname", "contype", "fk_attrs", "keys", "location"]);
|
|
259
|
+
/**
|
|
260
|
+
* Canonical constraint identity shared by every declaration site: an
|
|
261
|
+
* ALTER TABLE ADD CONSTRAINT statement, an in-CREATE table-level constraint,
|
|
262
|
+
* an inline column constraint, and a catalog pg_constraint row must all hash
|
|
263
|
+
* to the same shape for the same table.
|
|
264
|
+
*/
|
|
265
|
+
export function canonicalConstraintShape(constraint, table, impliedColumns = []) {
|
|
266
|
+
return {
|
|
267
|
+
constraint: canonicalConstraint(constraint, impliedColumns),
|
|
268
|
+
table,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function canonicalConstraint(constraint, impliedColumns) {
|
|
272
|
+
const keys = stringList(constraint.keys);
|
|
273
|
+
const fkAttrs = stringList(constraint.fk_attrs);
|
|
274
|
+
const columns = fkAttrs.length > 0 ? fkAttrs : keys.length > 0 ? keys : impliedColumns;
|
|
275
|
+
const payload = {};
|
|
276
|
+
for (const [key, value] of Object.entries(constraint)) {
|
|
277
|
+
if (constraintIdentityKeys.has(key)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
payload[key] = stripLocations(value);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
columns: [...columns],
|
|
284
|
+
payload,
|
|
285
|
+
type: readString(constraint.contype) ?? "",
|
|
286
|
+
};
|
|
287
|
+
}
|
package/dist/sync.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Diagnostic, SupaschemaConfig } from "./core.js";
|
|
2
|
+
export interface SyncOptions {
|
|
3
|
+
config?: Partial<SupaschemaConfig>;
|
|
4
|
+
databaseUrl?: string;
|
|
5
|
+
directory: string;
|
|
6
|
+
/** Apply pending migrations to the target via `supabase migration up`. */
|
|
7
|
+
local?: boolean;
|
|
8
|
+
/** Push pending migrations to the linked project via `supabase db push`. */
|
|
9
|
+
remote?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SyncResult {
|
|
12
|
+
applied: boolean;
|
|
13
|
+
diagnostics: Diagnostic[];
|
|
14
|
+
pending: string[];
|
|
15
|
+
report: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Auto-sync orchestration: supaschema gates, the Supabase CLI applies. Every
|
|
19
|
+
* pending migration must pass the static replay-safety check before any
|
|
20
|
+
* runner executes; ghost or out-of-order history refuses outright; and
|
|
21
|
+
* nothing touches a database unless `local`/`remote` was explicitly chosen —
|
|
22
|
+
* the default is a dry run that prints exactly what would execute. History
|
|
23
|
+
* stays runner-owned: supaschema never writes
|
|
24
|
+
* supabase_migrations.schema_migrations itself.
|
|
25
|
+
*/
|
|
26
|
+
export declare function syncMigrations(options: SyncOptions): Promise<SyncResult>;
|
|
27
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAI9D,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAoE9E"}
|