@xubylele/schema-forge 1.12.1 → 1.13.0
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/README.md +58 -5
- package/dist/api.d.ts +13 -1
- package/dist/api.js +1071 -166
- package/dist/cli.js +1083 -170
- package/package.json +12 -12
package/dist/cli.js
CHANGED
|
@@ -30,11 +30,190 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
mod
|
|
31
31
|
));
|
|
32
32
|
|
|
33
|
+
// node_modules/@xubylele/schema-forge-core/dist/core/normalize.js
|
|
34
|
+
function normalizeIdent(input) {
|
|
35
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
36
|
+
}
|
|
37
|
+
function pkName(table) {
|
|
38
|
+
return `pk_${normalizeIdent(table)}`;
|
|
39
|
+
}
|
|
40
|
+
function uqName(table, column) {
|
|
41
|
+
return `uq_${normalizeIdent(table)}_${normalizeIdent(column)}`;
|
|
42
|
+
}
|
|
43
|
+
function legacyPkName(table) {
|
|
44
|
+
return `${normalizeIdent(table)}_pkey`;
|
|
45
|
+
}
|
|
46
|
+
function legacyUqName(table, column) {
|
|
47
|
+
return `${normalizeIdent(table)}_${normalizeIdent(column)}_key`;
|
|
48
|
+
}
|
|
49
|
+
function simpleHash(input) {
|
|
50
|
+
let hash = 2166136261;
|
|
51
|
+
for (let index = 0; index < input.length; index++) {
|
|
52
|
+
hash ^= input.charCodeAt(index);
|
|
53
|
+
hash = Math.imul(hash, 16777619);
|
|
54
|
+
}
|
|
55
|
+
return (hash >>> 0).toString(36);
|
|
56
|
+
}
|
|
57
|
+
function hashSqlContent(value) {
|
|
58
|
+
return simpleHash(value);
|
|
59
|
+
}
|
|
60
|
+
function normalizeSqlExpression(value) {
|
|
61
|
+
if (!value) {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
return normalizeSpacesOutsideQuotes(value);
|
|
65
|
+
}
|
|
66
|
+
function deterministicIndexName(params) {
|
|
67
|
+
const tableName = normalizeIdent(params.table) || "table";
|
|
68
|
+
const columnList = params.columns ?? [];
|
|
69
|
+
if (columnList.length > 0) {
|
|
70
|
+
const columnToken = columnList.map((column) => normalizeIdent(column) || "col").join("_").slice(0, 40);
|
|
71
|
+
return `idx_${tableName}_${columnToken}`;
|
|
72
|
+
}
|
|
73
|
+
const normalizedExpression = normalizeSqlExpression(params.expression);
|
|
74
|
+
const identExpr = normalizeIdent(normalizedExpression).slice(0, 24) || "expr";
|
|
75
|
+
const hash = simpleHash(normalizedExpression || params.expression || "").slice(0, 8);
|
|
76
|
+
return `idx_${tableName}_${identExpr}_${hash}`;
|
|
77
|
+
}
|
|
78
|
+
function normalizeSpacesOutsideQuotes(value) {
|
|
79
|
+
let result = "";
|
|
80
|
+
let inSingleQuote = false;
|
|
81
|
+
let inDoubleQuote = false;
|
|
82
|
+
let pendingSpace = false;
|
|
83
|
+
for (const char of value) {
|
|
84
|
+
if (char === "'" && !inDoubleQuote) {
|
|
85
|
+
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
86
|
+
result += " ";
|
|
87
|
+
}
|
|
88
|
+
pendingSpace = false;
|
|
89
|
+
inSingleQuote = !inSingleQuote;
|
|
90
|
+
result += char;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === '"' && !inSingleQuote) {
|
|
94
|
+
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
95
|
+
result += " ";
|
|
96
|
+
}
|
|
97
|
+
pendingSpace = false;
|
|
98
|
+
inDoubleQuote = !inDoubleQuote;
|
|
99
|
+
result += char;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!inSingleQuote && !inDoubleQuote && /\s/.test(char)) {
|
|
103
|
+
pendingSpace = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
107
|
+
result += " ";
|
|
108
|
+
}
|
|
109
|
+
pendingSpace = false;
|
|
110
|
+
result += char;
|
|
111
|
+
}
|
|
112
|
+
return result.trim();
|
|
113
|
+
}
|
|
114
|
+
function normalizeKnownFunctionsOutsideQuotes(value) {
|
|
115
|
+
let result = "";
|
|
116
|
+
let inSingleQuote = false;
|
|
117
|
+
let inDoubleQuote = false;
|
|
118
|
+
let buffer = "";
|
|
119
|
+
function flushBuffer() {
|
|
120
|
+
if (!buffer) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
result += buffer.replace(/\bnow\s*\(\s*\)/gi, "now()").replace(/\bgen_random_uuid\s*\(\s*\)/gi, "gen_random_uuid()");
|
|
124
|
+
buffer = "";
|
|
125
|
+
}
|
|
126
|
+
for (const char of value) {
|
|
127
|
+
if (char === "'" && !inDoubleQuote) {
|
|
128
|
+
flushBuffer();
|
|
129
|
+
inSingleQuote = !inSingleQuote;
|
|
130
|
+
result += char;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (char === '"' && !inSingleQuote) {
|
|
134
|
+
flushBuffer();
|
|
135
|
+
inDoubleQuote = !inDoubleQuote;
|
|
136
|
+
result += char;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
140
|
+
result += char;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
buffer += char;
|
|
144
|
+
}
|
|
145
|
+
flushBuffer();
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
function normalizePunctuationOutsideQuotes(value) {
|
|
149
|
+
let result = "";
|
|
150
|
+
let inSingleQuote = false;
|
|
151
|
+
let inDoubleQuote = false;
|
|
152
|
+
for (let index = 0; index < value.length; index++) {
|
|
153
|
+
const char = value[index];
|
|
154
|
+
if (char === "'" && !inDoubleQuote) {
|
|
155
|
+
inSingleQuote = !inSingleQuote;
|
|
156
|
+
result += char;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (char === '"' && !inSingleQuote) {
|
|
160
|
+
inDoubleQuote = !inDoubleQuote;
|
|
161
|
+
result += char;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!inSingleQuote && !inDoubleQuote && (char === "(" || char === ")")) {
|
|
165
|
+
while (result.endsWith(" ")) {
|
|
166
|
+
result = result.slice(0, -1);
|
|
167
|
+
}
|
|
168
|
+
result += char;
|
|
169
|
+
let lookahead = index + 1;
|
|
170
|
+
while (lookahead < value.length && value[lookahead] === " ") {
|
|
171
|
+
lookahead++;
|
|
172
|
+
}
|
|
173
|
+
index = lookahead - 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!inSingleQuote && !inDoubleQuote && char === ",") {
|
|
177
|
+
while (result.endsWith(" ")) {
|
|
178
|
+
result = result.slice(0, -1);
|
|
179
|
+
}
|
|
180
|
+
result += ", ";
|
|
181
|
+
let lookahead = index + 1;
|
|
182
|
+
while (lookahead < value.length && value[lookahead] === " ") {
|
|
183
|
+
lookahead++;
|
|
184
|
+
}
|
|
185
|
+
index = lookahead - 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
result += char;
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function normalizeDefault(expr) {
|
|
193
|
+
if (expr === void 0 || expr === null) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const trimmed = expr.trim();
|
|
197
|
+
if (trimmed.length === 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const normalizedSpacing = normalizeSpacesOutsideQuotes(trimmed);
|
|
201
|
+
const normalizedPunctuation = normalizePunctuationOutsideQuotes(normalizedSpacing);
|
|
202
|
+
return normalizeKnownFunctionsOutsideQuotes(normalizedPunctuation);
|
|
203
|
+
}
|
|
204
|
+
var init_normalize = __esm({
|
|
205
|
+
"node_modules/@xubylele/schema-forge-core/dist/core/normalize.js"() {
|
|
206
|
+
"use strict";
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
33
210
|
// node_modules/@xubylele/schema-forge-core/dist/core/parser.js
|
|
34
211
|
function parseSchema(source) {
|
|
35
212
|
const lines = source.split("\n");
|
|
36
213
|
const tables = {};
|
|
37
214
|
const policyList = [];
|
|
215
|
+
const indexList = [];
|
|
216
|
+
const viewList = [];
|
|
38
217
|
let currentLine = 0;
|
|
39
218
|
const validBaseColumnTypes = /* @__PURE__ */ new Set([
|
|
40
219
|
"uuid",
|
|
@@ -338,6 +517,48 @@ function parseSchema(source) {
|
|
|
338
517
|
};
|
|
339
518
|
return { policy, nextLineIndex: lineIdx };
|
|
340
519
|
}
|
|
520
|
+
const policyDeclRegex = /^policy\s+"([^"]*)"\s+on\s+\w+/;
|
|
521
|
+
function parseViewDeclaration(startLine, options) {
|
|
522
|
+
const firstLine = cleanLine(lines[startLine]);
|
|
523
|
+
const declMatch = firstLine.match(/^view\s+(\w+)\s+as(?:\s+([\s\S]+))?$/);
|
|
524
|
+
if (!declMatch) {
|
|
525
|
+
throw new Error(`Line ${startLine + 1}: Invalid view declaration. Expected: view <name> as <sql>`);
|
|
526
|
+
}
|
|
527
|
+
const viewName = declMatch[1];
|
|
528
|
+
validateIdentifier(viewName, startLine + 1, "view");
|
|
529
|
+
const queryLines = [];
|
|
530
|
+
const inlineQuery = declMatch[2]?.trim();
|
|
531
|
+
if (inlineQuery) {
|
|
532
|
+
queryLines.push(inlineQuery);
|
|
533
|
+
}
|
|
534
|
+
let lineIdx = startLine + 1;
|
|
535
|
+
while (lineIdx < lines.length) {
|
|
536
|
+
const rawLine = lines[lineIdx];
|
|
537
|
+
const cleaned = cleanLine(rawLine);
|
|
538
|
+
if (!cleaned) {
|
|
539
|
+
if (queryLines.length > 0) {
|
|
540
|
+
queryLines.push("");
|
|
541
|
+
}
|
|
542
|
+
lineIdx++;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (cleaned.startsWith("table ") || policyDeclRegex.test(cleaned) || cleaned.startsWith("index ") || cleaned.startsWith("view ") || options?.stopAtTableClose === true && cleaned === "}") {
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
queryLines.push(rawLine.trim());
|
|
549
|
+
lineIdx++;
|
|
550
|
+
}
|
|
551
|
+
const query = queryLines.join("\n").trim();
|
|
552
|
+
if (!query) {
|
|
553
|
+
throw new Error(`Line ${startLine + 1}: View '${viewName}' is missing SQL query body`);
|
|
554
|
+
}
|
|
555
|
+
const view = {
|
|
556
|
+
name: viewName,
|
|
557
|
+
query,
|
|
558
|
+
hash: hashSqlContent(query)
|
|
559
|
+
};
|
|
560
|
+
return { view, nextLineIndex: lineIdx };
|
|
561
|
+
}
|
|
341
562
|
function parseTableBlock(startLine) {
|
|
342
563
|
const firstLine = cleanLine(lines[startLine]);
|
|
343
564
|
const match = firstLine.match(/^table\s+(\w+)\s*\{\s*$/);
|
|
@@ -362,6 +583,12 @@ function parseSchema(source) {
|
|
|
362
583
|
foundClosingBrace = true;
|
|
363
584
|
break;
|
|
364
585
|
}
|
|
586
|
+
if (cleaned.startsWith("view ")) {
|
|
587
|
+
const { view, nextLineIndex } = parseViewDeclaration(lineIdx, { stopAtTableClose: true });
|
|
588
|
+
viewList.push({ view, startLine: lineIdx + 1 });
|
|
589
|
+
lineIdx = nextLineIndex;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
365
592
|
try {
|
|
366
593
|
const column = parseColumn(cleaned, lineIdx + 1);
|
|
367
594
|
columns.push(column);
|
|
@@ -381,7 +608,129 @@ function parseSchema(source) {
|
|
|
381
608
|
};
|
|
382
609
|
return lineIdx;
|
|
383
610
|
}
|
|
384
|
-
|
|
611
|
+
function parseIndexColumns(columnsRaw, lineNum) {
|
|
612
|
+
const parsed = columnsRaw.split(",").map((column) => column.trim()).filter(Boolean);
|
|
613
|
+
if (parsed.length === 0) {
|
|
614
|
+
throw new Error(`Line ${lineNum}: Index columns clause requires at least one column`);
|
|
615
|
+
}
|
|
616
|
+
for (const column of parsed) {
|
|
617
|
+
validateIdentifier(column, lineNum, "column");
|
|
618
|
+
}
|
|
619
|
+
return parsed;
|
|
620
|
+
}
|
|
621
|
+
function parseIndexDeclaration(startLine) {
|
|
622
|
+
const firstLine = cleanLine(lines[startLine]);
|
|
623
|
+
const inlineExprMatch = firstLine.match(/^index(?:\s+(\w+))?\s+on\s+(\w+)\s*\((.+)\)\s*$/);
|
|
624
|
+
if (inlineExprMatch) {
|
|
625
|
+
const name = inlineExprMatch[1];
|
|
626
|
+
const table2 = inlineExprMatch[2];
|
|
627
|
+
const expressionRaw = inlineExprMatch[3].trim();
|
|
628
|
+
if (name) {
|
|
629
|
+
validateIdentifier(name, startLine + 1, "column");
|
|
630
|
+
}
|
|
631
|
+
validateIdentifier(table2, startLine + 1, "table");
|
|
632
|
+
if (!expressionRaw) {
|
|
633
|
+
throw new Error(`Line ${startLine + 1}: Expression index requires a non-empty expression`);
|
|
634
|
+
}
|
|
635
|
+
const normalizedExpression = normalizeSqlExpression(expressionRaw);
|
|
636
|
+
const resolvedName2 = name ?? deterministicIndexName({ table: table2, expression: normalizedExpression });
|
|
637
|
+
return {
|
|
638
|
+
index: {
|
|
639
|
+
name: resolvedName2,
|
|
640
|
+
table: table2,
|
|
641
|
+
columns: [],
|
|
642
|
+
unique: false,
|
|
643
|
+
expression: normalizedExpression
|
|
644
|
+
},
|
|
645
|
+
nextLineIndex: startLine + 1
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const blockMatch = firstLine.match(/^index(?:\s+(\w+))?\s+on\s+(\w+)\s*$/);
|
|
649
|
+
if (!blockMatch) {
|
|
650
|
+
throw new Error(`Line ${startLine + 1}: Invalid index declaration. Expected: index [name] on <table> or index [name] on <table>(<expression>)`);
|
|
651
|
+
}
|
|
652
|
+
const declaredName = blockMatch[1];
|
|
653
|
+
const table = blockMatch[2];
|
|
654
|
+
if (declaredName) {
|
|
655
|
+
validateIdentifier(declaredName, startLine + 1, "column");
|
|
656
|
+
}
|
|
657
|
+
validateIdentifier(table, startLine + 1, "table");
|
|
658
|
+
let lineIdx = startLine + 1;
|
|
659
|
+
let columns;
|
|
660
|
+
let unique = false;
|
|
661
|
+
let where;
|
|
662
|
+
let expression;
|
|
663
|
+
const seenClauses = /* @__PURE__ */ new Set();
|
|
664
|
+
while (lineIdx < lines.length) {
|
|
665
|
+
const cleaned = cleanLine(lines[lineIdx]);
|
|
666
|
+
if (!cleaned) {
|
|
667
|
+
lineIdx++;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (cleaned.startsWith("table ") || cleaned.startsWith("policy ") || cleaned.startsWith("index ")) {
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
if (cleaned === "unique") {
|
|
674
|
+
if (seenClauses.has("unique")) {
|
|
675
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'unique' in index declaration`);
|
|
676
|
+
}
|
|
677
|
+
seenClauses.add("unique");
|
|
678
|
+
unique = true;
|
|
679
|
+
lineIdx++;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (cleaned.startsWith("columns ")) {
|
|
683
|
+
if (seenClauses.has("columns")) {
|
|
684
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'columns' in index declaration`);
|
|
685
|
+
}
|
|
686
|
+
seenClauses.add("columns");
|
|
687
|
+
columns = parseIndexColumns(cleaned.slice("columns ".length), lineIdx + 1);
|
|
688
|
+
lineIdx++;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (cleaned.startsWith("where ")) {
|
|
692
|
+
if (seenClauses.has("where")) {
|
|
693
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'where' in index declaration`);
|
|
694
|
+
}
|
|
695
|
+
seenClauses.add("where");
|
|
696
|
+
where = normalizeSqlExpression(cleaned.slice("where ".length));
|
|
697
|
+
lineIdx++;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (cleaned.startsWith("expression ")) {
|
|
701
|
+
if (seenClauses.has("expression")) {
|
|
702
|
+
throw new Error(`Line ${lineIdx + 1}: Duplicate 'expression' in index declaration`);
|
|
703
|
+
}
|
|
704
|
+
seenClauses.add("expression");
|
|
705
|
+
expression = normalizeSqlExpression(cleaned.slice("expression ".length));
|
|
706
|
+
lineIdx++;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
throw new Error(`Line ${lineIdx + 1}: Unexpected index clause '${cleaned}'`);
|
|
710
|
+
}
|
|
711
|
+
if (columns && expression || !columns && !expression) {
|
|
712
|
+
throw new Error(`Line ${startLine + 1}: Index must define exactly one target kind: either 'columns' or 'expression'`);
|
|
713
|
+
}
|
|
714
|
+
if (expression !== void 0 && expression.trim().length === 0) {
|
|
715
|
+
throw new Error(`Line ${startLine + 1}: Expression index requires a non-empty expression`);
|
|
716
|
+
}
|
|
717
|
+
const resolvedName = declaredName ?? deterministicIndexName({
|
|
718
|
+
table,
|
|
719
|
+
columns,
|
|
720
|
+
expression
|
|
721
|
+
});
|
|
722
|
+
return {
|
|
723
|
+
index: {
|
|
724
|
+
name: resolvedName,
|
|
725
|
+
table,
|
|
726
|
+
columns: columns ?? [],
|
|
727
|
+
unique,
|
|
728
|
+
...where !== void 0 && { where },
|
|
729
|
+
...expression !== void 0 && { expression }
|
|
730
|
+
},
|
|
731
|
+
nextLineIndex: lineIdx
|
|
732
|
+
};
|
|
733
|
+
}
|
|
385
734
|
while (currentLine < lines.length) {
|
|
386
735
|
const cleaned = cleanLine(lines[currentLine]);
|
|
387
736
|
if (!cleaned) {
|
|
@@ -395,10 +744,27 @@ function parseSchema(source) {
|
|
|
395
744
|
const { policy, nextLineIndex } = parsePolicyBlock(currentLine);
|
|
396
745
|
policyList.push({ policy, startLine: currentLine + 1 });
|
|
397
746
|
currentLine = nextLineIndex;
|
|
747
|
+
} else if (cleaned.startsWith("index ")) {
|
|
748
|
+
const { index, nextLineIndex } = parseIndexDeclaration(currentLine);
|
|
749
|
+
indexList.push({ index, startLine: currentLine + 1 });
|
|
750
|
+
currentLine = nextLineIndex;
|
|
751
|
+
} else if (cleaned.startsWith("view ")) {
|
|
752
|
+
const { view, nextLineIndex } = parseViewDeclaration(currentLine);
|
|
753
|
+
viewList.push({ view, startLine: currentLine + 1 });
|
|
754
|
+
currentLine = nextLineIndex;
|
|
398
755
|
} else {
|
|
399
|
-
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table or policy definition.`);
|
|
756
|
+
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table, index, view, or policy definition.`);
|
|
400
757
|
}
|
|
401
758
|
}
|
|
759
|
+
for (const { index, startLine } of indexList) {
|
|
760
|
+
if (!tables[index.table]) {
|
|
761
|
+
throw new Error(`Line ${startLine}: Index "${index.name}" references undefined table "${index.table}"`);
|
|
762
|
+
}
|
|
763
|
+
if (!tables[index.table].indexes) {
|
|
764
|
+
tables[index.table].indexes = [];
|
|
765
|
+
}
|
|
766
|
+
tables[index.table].indexes.push(index);
|
|
767
|
+
}
|
|
402
768
|
for (const { policy, startLine } of policyList) {
|
|
403
769
|
if (!tables[policy.table]) {
|
|
404
770
|
throw new Error(`Line ${startLine}: Policy "${policy.name}" references undefined table "${policy.table}"`);
|
|
@@ -408,164 +774,27 @@ function parseSchema(source) {
|
|
|
408
774
|
}
|
|
409
775
|
tables[policy.table].policies.push(policy);
|
|
410
776
|
}
|
|
411
|
-
|
|
777
|
+
const views = {};
|
|
778
|
+
for (const { view, startLine } of viewList) {
|
|
779
|
+
if (views[view.name]) {
|
|
780
|
+
throw new Error(`Line ${startLine}: Duplicate view definition '${view.name}'`);
|
|
781
|
+
}
|
|
782
|
+
views[view.name] = view;
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
tables,
|
|
786
|
+
...Object.keys(views).length > 0 && { views }
|
|
787
|
+
};
|
|
412
788
|
}
|
|
413
789
|
var POLICY_COMMANDS;
|
|
414
790
|
var init_parser = __esm({
|
|
415
791
|
"node_modules/@xubylele/schema-forge-core/dist/core/parser.js"() {
|
|
416
792
|
"use strict";
|
|
793
|
+
init_normalize();
|
|
417
794
|
POLICY_COMMANDS = ["select", "insert", "update", "delete", "all"];
|
|
418
795
|
}
|
|
419
796
|
});
|
|
420
797
|
|
|
421
|
-
// node_modules/@xubylele/schema-forge-core/dist/core/normalize.js
|
|
422
|
-
function normalizeIdent(input) {
|
|
423
|
-
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
424
|
-
}
|
|
425
|
-
function pkName(table) {
|
|
426
|
-
return `pk_${normalizeIdent(table)}`;
|
|
427
|
-
}
|
|
428
|
-
function uqName(table, column) {
|
|
429
|
-
return `uq_${normalizeIdent(table)}_${normalizeIdent(column)}`;
|
|
430
|
-
}
|
|
431
|
-
function legacyPkName(table) {
|
|
432
|
-
return `${normalizeIdent(table)}_pkey`;
|
|
433
|
-
}
|
|
434
|
-
function legacyUqName(table, column) {
|
|
435
|
-
return `${normalizeIdent(table)}_${normalizeIdent(column)}_key`;
|
|
436
|
-
}
|
|
437
|
-
function normalizeSpacesOutsideQuotes(value) {
|
|
438
|
-
let result = "";
|
|
439
|
-
let inSingleQuote = false;
|
|
440
|
-
let inDoubleQuote = false;
|
|
441
|
-
let pendingSpace = false;
|
|
442
|
-
for (const char of value) {
|
|
443
|
-
if (char === "'" && !inDoubleQuote) {
|
|
444
|
-
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
445
|
-
result += " ";
|
|
446
|
-
}
|
|
447
|
-
pendingSpace = false;
|
|
448
|
-
inSingleQuote = !inSingleQuote;
|
|
449
|
-
result += char;
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
if (char === '"' && !inSingleQuote) {
|
|
453
|
-
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
454
|
-
result += " ";
|
|
455
|
-
}
|
|
456
|
-
pendingSpace = false;
|
|
457
|
-
inDoubleQuote = !inDoubleQuote;
|
|
458
|
-
result += char;
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
if (!inSingleQuote && !inDoubleQuote && /\s/.test(char)) {
|
|
462
|
-
pendingSpace = true;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
if (pendingSpace && result.length > 0 && result[result.length - 1] !== " ") {
|
|
466
|
-
result += " ";
|
|
467
|
-
}
|
|
468
|
-
pendingSpace = false;
|
|
469
|
-
result += char;
|
|
470
|
-
}
|
|
471
|
-
return result.trim();
|
|
472
|
-
}
|
|
473
|
-
function normalizeKnownFunctionsOutsideQuotes(value) {
|
|
474
|
-
let result = "";
|
|
475
|
-
let inSingleQuote = false;
|
|
476
|
-
let inDoubleQuote = false;
|
|
477
|
-
let buffer = "";
|
|
478
|
-
function flushBuffer() {
|
|
479
|
-
if (!buffer) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
result += buffer.replace(/\bnow\s*\(\s*\)/gi, "now()").replace(/\bgen_random_uuid\s*\(\s*\)/gi, "gen_random_uuid()");
|
|
483
|
-
buffer = "";
|
|
484
|
-
}
|
|
485
|
-
for (const char of value) {
|
|
486
|
-
if (char === "'" && !inDoubleQuote) {
|
|
487
|
-
flushBuffer();
|
|
488
|
-
inSingleQuote = !inSingleQuote;
|
|
489
|
-
result += char;
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
if (char === '"' && !inSingleQuote) {
|
|
493
|
-
flushBuffer();
|
|
494
|
-
inDoubleQuote = !inDoubleQuote;
|
|
495
|
-
result += char;
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (inSingleQuote || inDoubleQuote) {
|
|
499
|
-
result += char;
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
buffer += char;
|
|
503
|
-
}
|
|
504
|
-
flushBuffer();
|
|
505
|
-
return result;
|
|
506
|
-
}
|
|
507
|
-
function normalizePunctuationOutsideQuotes(value) {
|
|
508
|
-
let result = "";
|
|
509
|
-
let inSingleQuote = false;
|
|
510
|
-
let inDoubleQuote = false;
|
|
511
|
-
for (let index = 0; index < value.length; index++) {
|
|
512
|
-
const char = value[index];
|
|
513
|
-
if (char === "'" && !inDoubleQuote) {
|
|
514
|
-
inSingleQuote = !inSingleQuote;
|
|
515
|
-
result += char;
|
|
516
|
-
continue;
|
|
517
|
-
}
|
|
518
|
-
if (char === '"' && !inSingleQuote) {
|
|
519
|
-
inDoubleQuote = !inDoubleQuote;
|
|
520
|
-
result += char;
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
if (!inSingleQuote && !inDoubleQuote && (char === "(" || char === ")")) {
|
|
524
|
-
while (result.endsWith(" ")) {
|
|
525
|
-
result = result.slice(0, -1);
|
|
526
|
-
}
|
|
527
|
-
result += char;
|
|
528
|
-
let lookahead = index + 1;
|
|
529
|
-
while (lookahead < value.length && value[lookahead] === " ") {
|
|
530
|
-
lookahead++;
|
|
531
|
-
}
|
|
532
|
-
index = lookahead - 1;
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
if (!inSingleQuote && !inDoubleQuote && char === ",") {
|
|
536
|
-
while (result.endsWith(" ")) {
|
|
537
|
-
result = result.slice(0, -1);
|
|
538
|
-
}
|
|
539
|
-
result += ", ";
|
|
540
|
-
let lookahead = index + 1;
|
|
541
|
-
while (lookahead < value.length && value[lookahead] === " ") {
|
|
542
|
-
lookahead++;
|
|
543
|
-
}
|
|
544
|
-
index = lookahead - 1;
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
result += char;
|
|
548
|
-
}
|
|
549
|
-
return result;
|
|
550
|
-
}
|
|
551
|
-
function normalizeDefault(expr) {
|
|
552
|
-
if (expr === void 0 || expr === null) {
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
const trimmed = expr.trim();
|
|
556
|
-
if (trimmed.length === 0) {
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
|
-
const normalizedSpacing = normalizeSpacesOutsideQuotes(trimmed);
|
|
560
|
-
const normalizedPunctuation = normalizePunctuationOutsideQuotes(normalizedSpacing);
|
|
561
|
-
return normalizeKnownFunctionsOutsideQuotes(normalizedPunctuation);
|
|
562
|
-
}
|
|
563
|
-
var init_normalize = __esm({
|
|
564
|
-
"node_modules/@xubylele/schema-forge-core/dist/core/normalize.js"() {
|
|
565
|
-
"use strict";
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
|
|
569
798
|
// node_modules/@xubylele/schema-forge-core/dist/core/diff.js
|
|
570
799
|
function getTableNamesFromState(state) {
|
|
571
800
|
return new Set(Object.keys(state.tables));
|
|
@@ -610,6 +839,62 @@ function policyEquals(oldP, newP) {
|
|
|
610
839
|
return false;
|
|
611
840
|
return true;
|
|
612
841
|
}
|
|
842
|
+
function resolveSchemaIndexName(index) {
|
|
843
|
+
if (index.name && index.name.trim().length > 0) {
|
|
844
|
+
return index.name.trim();
|
|
845
|
+
}
|
|
846
|
+
return deterministicIndexName({
|
|
847
|
+
table: index.table,
|
|
848
|
+
columns: index.columns,
|
|
849
|
+
expression: index.expression
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
function resolveStateIndexName(index) {
|
|
853
|
+
if (index.name && index.name.trim().length > 0) {
|
|
854
|
+
return index.name.trim();
|
|
855
|
+
}
|
|
856
|
+
return deterministicIndexName({
|
|
857
|
+
table: index.table,
|
|
858
|
+
columns: index.columns,
|
|
859
|
+
expression: index.expression
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
function normalizeIndexWhere(value) {
|
|
863
|
+
return normalizeSqlExpression(value);
|
|
864
|
+
}
|
|
865
|
+
function normalizeIndexExpression(value) {
|
|
866
|
+
return normalizeSqlExpression(value);
|
|
867
|
+
}
|
|
868
|
+
function indexesEqual(previous, current) {
|
|
869
|
+
if (previous.table !== current.table)
|
|
870
|
+
return false;
|
|
871
|
+
if ((previous.unique ?? false) !== (current.unique ?? false))
|
|
872
|
+
return false;
|
|
873
|
+
if (previous.columns.length !== current.columns.length)
|
|
874
|
+
return false;
|
|
875
|
+
for (let index = 0; index < previous.columns.length; index++) {
|
|
876
|
+
if (previous.columns[index] !== current.columns[index]) {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (normalizeIndexWhere(previous.where) !== normalizeIndexWhere(current.where))
|
|
881
|
+
return false;
|
|
882
|
+
if (normalizeIndexExpression(previous.expression) !== normalizeIndexExpression(current.expression))
|
|
883
|
+
return false;
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
function resolveStateViewHash(view) {
|
|
887
|
+
if (view.hash && view.hash.trim().length > 0) {
|
|
888
|
+
return view.hash;
|
|
889
|
+
}
|
|
890
|
+
return hashSqlContent(view.query);
|
|
891
|
+
}
|
|
892
|
+
function resolveSchemaViewHash(view) {
|
|
893
|
+
if (view.hash && view.hash.trim().length > 0) {
|
|
894
|
+
return view.hash;
|
|
895
|
+
}
|
|
896
|
+
return hashSqlContent(view.query);
|
|
897
|
+
}
|
|
613
898
|
function diffSchemas(oldState, newSchema) {
|
|
614
899
|
const operations = [];
|
|
615
900
|
const oldTableNames = getTableNamesFromState(oldState);
|
|
@@ -622,6 +907,15 @@ function diffSchemas(oldState, newSchema) {
|
|
|
622
907
|
kind: "create_table",
|
|
623
908
|
table: newSchema.tables[tableName]
|
|
624
909
|
});
|
|
910
|
+
const createdTable = newSchema.tables[tableName];
|
|
911
|
+
const createdIndexes = (createdTable.indexes ?? []).map((index) => ({ ...index, name: resolveSchemaIndexName(index) })).sort((left, right) => left.name.localeCompare(right.name));
|
|
912
|
+
for (const index of createdIndexes) {
|
|
913
|
+
operations.push({
|
|
914
|
+
kind: "create_index",
|
|
915
|
+
tableName,
|
|
916
|
+
index
|
|
917
|
+
});
|
|
918
|
+
}
|
|
625
919
|
}
|
|
626
920
|
}
|
|
627
921
|
const commonTableNames = sortedNewTableNames.filter((tableName) => oldTableNames.has(tableName));
|
|
@@ -765,7 +1059,66 @@ function diffSchemas(oldState, newSchema) {
|
|
|
765
1059
|
operations.push({
|
|
766
1060
|
kind: "add_primary_key_constraint",
|
|
767
1061
|
tableName,
|
|
768
|
-
columnName: currentPrimaryKey
|
|
1062
|
+
columnName: currentPrimaryKey
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
for (const tableName of commonTableNames) {
|
|
1067
|
+
const newTable = newSchema.tables[tableName];
|
|
1068
|
+
const oldTable = oldState.tables[tableName];
|
|
1069
|
+
if (!newTable || !oldTable) {
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const oldIndexes = oldTable.indexes ?? {};
|
|
1073
|
+
const newIndexesByName = /* @__PURE__ */ new Map();
|
|
1074
|
+
for (const index of newTable.indexes ?? []) {
|
|
1075
|
+
const resolved = { ...index, name: resolveSchemaIndexName(index) };
|
|
1076
|
+
newIndexesByName.set(resolved.name, resolved);
|
|
1077
|
+
}
|
|
1078
|
+
const oldIndexesByName = /* @__PURE__ */ new Map();
|
|
1079
|
+
for (const oldIndex of Object.values(oldIndexes)) {
|
|
1080
|
+
oldIndexesByName.set(resolveStateIndexName(oldIndex), oldIndex);
|
|
1081
|
+
}
|
|
1082
|
+
const oldIndexNames = Array.from(oldIndexesByName.keys()).sort((a, b) => a.localeCompare(b));
|
|
1083
|
+
const newIndexNames = Array.from(newIndexesByName.keys()).sort((a, b) => a.localeCompare(b));
|
|
1084
|
+
const indexDrops = [];
|
|
1085
|
+
const indexCreates = [];
|
|
1086
|
+
for (const oldIndexName of oldIndexNames) {
|
|
1087
|
+
const oldIndex = oldIndexesByName.get(oldIndexName);
|
|
1088
|
+
if (!oldIndex) {
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
const nextIndex = newIndexesByName.get(oldIndexName);
|
|
1092
|
+
if (!nextIndex) {
|
|
1093
|
+
indexDrops.push({ ...oldIndex, name: oldIndexName });
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (!indexesEqual(oldIndex, nextIndex)) {
|
|
1097
|
+
indexDrops.push({ ...oldIndex, name: oldIndexName });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
for (const newIndexName of newIndexNames) {
|
|
1101
|
+
const newIndex = newIndexesByName.get(newIndexName);
|
|
1102
|
+
if (!newIndex) {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
const oldIndex = oldIndexesByName.get(newIndexName);
|
|
1106
|
+
if (!oldIndex || !indexesEqual(oldIndex, newIndex)) {
|
|
1107
|
+
indexCreates.push(newIndex);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
for (const index of indexDrops) {
|
|
1111
|
+
operations.push({
|
|
1112
|
+
kind: "drop_index",
|
|
1113
|
+
tableName,
|
|
1114
|
+
index
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
for (const index of indexCreates) {
|
|
1118
|
+
operations.push({
|
|
1119
|
+
kind: "create_index",
|
|
1120
|
+
tableName,
|
|
1121
|
+
index
|
|
769
1122
|
});
|
|
770
1123
|
}
|
|
771
1124
|
}
|
|
@@ -817,6 +1170,36 @@ function diffSchemas(oldState, newSchema) {
|
|
|
817
1170
|
}
|
|
818
1171
|
}
|
|
819
1172
|
}
|
|
1173
|
+
const oldViews = oldState.views ?? {};
|
|
1174
|
+
const newViews = newSchema.views ?? {};
|
|
1175
|
+
const oldViewNames = Object.keys(oldViews).sort((a, b) => a.localeCompare(b));
|
|
1176
|
+
const newViewNames = Object.keys(newViews).sort((a, b) => a.localeCompare(b));
|
|
1177
|
+
const oldViewNameSet = new Set(oldViewNames);
|
|
1178
|
+
const newViewNameSet = new Set(newViewNames);
|
|
1179
|
+
for (const viewName of newViewNames) {
|
|
1180
|
+
const nextView = newViews[viewName];
|
|
1181
|
+
if (!nextView) {
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (!oldViewNameSet.has(viewName)) {
|
|
1185
|
+
operations.push({ kind: "create_view", view: nextView });
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
const prevView = oldViews[viewName];
|
|
1189
|
+
if (!prevView) {
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
const oldHash = resolveStateViewHash(prevView);
|
|
1193
|
+
const newHash = resolveSchemaViewHash(nextView);
|
|
1194
|
+
if (oldHash !== newHash) {
|
|
1195
|
+
operations.push({ kind: "replace_view", view: nextView });
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
for (const viewName of oldViewNames) {
|
|
1199
|
+
if (!newViewNameSet.has(viewName)) {
|
|
1200
|
+
operations.push({ kind: "drop_view", viewName });
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
820
1203
|
for (const tableName of sortedOldTableNames) {
|
|
821
1204
|
if (!newTableNames.has(tableName)) {
|
|
822
1205
|
operations.push({
|
|
@@ -916,9 +1299,52 @@ function validateSchema(schema) {
|
|
|
916
1299
|
for (const tableName in schema.tables) {
|
|
917
1300
|
const table = schema.tables[tableName];
|
|
918
1301
|
validateTableColumns(tableName, table, schema.tables);
|
|
1302
|
+
validateTableIndexes(tableName, table, schema.tables);
|
|
919
1303
|
}
|
|
920
1304
|
validatePolicies(schema);
|
|
921
1305
|
}
|
|
1306
|
+
function validateTableIndexes(tableName, table, allTables) {
|
|
1307
|
+
if (!table.indexes?.length) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const normalizedNames = /* @__PURE__ */ new Set();
|
|
1311
|
+
for (const index of table.indexes) {
|
|
1312
|
+
validateSingleIndex(tableName, table, index, allTables);
|
|
1313
|
+
const effectiveName = index.name?.trim() || deterministicIndexName({
|
|
1314
|
+
table: index.table,
|
|
1315
|
+
columns: index.columns,
|
|
1316
|
+
expression: index.expression
|
|
1317
|
+
});
|
|
1318
|
+
if (normalizedNames.has(effectiveName)) {
|
|
1319
|
+
throw new Error(`Table '${tableName}': duplicate index name '${effectiveName}'`);
|
|
1320
|
+
}
|
|
1321
|
+
normalizedNames.add(effectiveName);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function validateSingleIndex(tableName, table, index, allTables) {
|
|
1325
|
+
if (!allTables[index.table]) {
|
|
1326
|
+
throw new Error(`Table '${tableName}': index '${index.name}' references table '${index.table}' which does not exist`);
|
|
1327
|
+
}
|
|
1328
|
+
if (index.table !== tableName) {
|
|
1329
|
+
throw new Error(`Table '${tableName}': index '${index.name}' must target table '${tableName}'`);
|
|
1330
|
+
}
|
|
1331
|
+
const hasColumns = index.columns.length > 0;
|
|
1332
|
+
const hasExpression = index.expression !== void 0;
|
|
1333
|
+
if (hasColumns && hasExpression || !hasColumns && !hasExpression) {
|
|
1334
|
+
throw new Error(`Table '${tableName}': index '${index.name}' must define exactly one of columns or expression`);
|
|
1335
|
+
}
|
|
1336
|
+
if (hasExpression && index.expression.trim().length === 0) {
|
|
1337
|
+
throw new Error(`Table '${tableName}': index '${index.name}' expression cannot be empty`);
|
|
1338
|
+
}
|
|
1339
|
+
if (hasColumns) {
|
|
1340
|
+
const tableColumns = new Set(table.columns.map((column) => column.name));
|
|
1341
|
+
for (const columnName of index.columns) {
|
|
1342
|
+
if (!tableColumns.has(columnName)) {
|
|
1343
|
+
throw new Error(`Table '${tableName}': index '${index.name}' references unknown column '${columnName}'`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
922
1348
|
function validateDuplicateTables(schema) {
|
|
923
1349
|
const tableNames = Object.keys(schema.tables);
|
|
924
1350
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -997,6 +1423,7 @@ var VALID_POLICY_COMMANDS, VALID_BASE_COLUMN_TYPES;
|
|
|
997
1423
|
var init_validator = __esm({
|
|
998
1424
|
"node_modules/@xubylele/schema-forge-core/dist/core/validator.js"() {
|
|
999
1425
|
"use strict";
|
|
1426
|
+
init_normalize();
|
|
1000
1427
|
VALID_POLICY_COMMANDS = [
|
|
1001
1428
|
"select",
|
|
1002
1429
|
"insert",
|
|
@@ -1097,12 +1524,22 @@ function classifyOperation(operation) {
|
|
|
1097
1524
|
return "DESTRUCTIVE";
|
|
1098
1525
|
case "add_primary_key_constraint":
|
|
1099
1526
|
return "SAFE";
|
|
1527
|
+
case "create_index":
|
|
1528
|
+
return "SAFE";
|
|
1529
|
+
case "drop_index":
|
|
1530
|
+
return "WARNING";
|
|
1100
1531
|
case "create_policy":
|
|
1101
1532
|
return "SAFE";
|
|
1102
1533
|
case "drop_policy":
|
|
1103
1534
|
return "DESTRUCTIVE";
|
|
1104
1535
|
case "modify_policy":
|
|
1105
1536
|
return "WARNING";
|
|
1537
|
+
case "create_view":
|
|
1538
|
+
return "SAFE";
|
|
1539
|
+
case "drop_view":
|
|
1540
|
+
return "DESTRUCTIVE";
|
|
1541
|
+
case "replace_view":
|
|
1542
|
+
return "WARNING";
|
|
1106
1543
|
default:
|
|
1107
1544
|
const _exhaustive = operation;
|
|
1108
1545
|
return _exhaustive;
|
|
@@ -1225,6 +1662,14 @@ function checkOperationSafety(operation) {
|
|
|
1225
1662
|
message: "Primary key constraint removed",
|
|
1226
1663
|
operationKind: operation.kind
|
|
1227
1664
|
};
|
|
1665
|
+
case "drop_index":
|
|
1666
|
+
return {
|
|
1667
|
+
safetyLevel,
|
|
1668
|
+
code: "DROP_INDEX",
|
|
1669
|
+
table: operation.tableName,
|
|
1670
|
+
message: `Index '${operation.index.name}' removed`,
|
|
1671
|
+
operationKind: operation.kind
|
|
1672
|
+
};
|
|
1228
1673
|
case "drop_policy":
|
|
1229
1674
|
return {
|
|
1230
1675
|
safetyLevel,
|
|
@@ -1241,6 +1686,22 @@ function checkOperationSafety(operation) {
|
|
|
1241
1686
|
message: "Policy expression changed",
|
|
1242
1687
|
operationKind: operation.kind
|
|
1243
1688
|
};
|
|
1689
|
+
case "drop_view":
|
|
1690
|
+
return {
|
|
1691
|
+
safetyLevel,
|
|
1692
|
+
code: "DROP_VIEW",
|
|
1693
|
+
table: operation.viewName,
|
|
1694
|
+
message: "View removed",
|
|
1695
|
+
operationKind: operation.kind
|
|
1696
|
+
};
|
|
1697
|
+
case "replace_view":
|
|
1698
|
+
return {
|
|
1699
|
+
safetyLevel,
|
|
1700
|
+
code: "REPLACE_VIEW",
|
|
1701
|
+
table: operation.view.name,
|
|
1702
|
+
message: "View definition changed",
|
|
1703
|
+
operationKind: operation.kind
|
|
1704
|
+
};
|
|
1244
1705
|
default:
|
|
1245
1706
|
return null;
|
|
1246
1707
|
}
|
|
@@ -1402,6 +1863,7 @@ var init_fs = __esm({
|
|
|
1402
1863
|
// node_modules/@xubylele/schema-forge-core/dist/core/state-transform.js
|
|
1403
1864
|
async function schemaToState(schema) {
|
|
1404
1865
|
const tables = {};
|
|
1866
|
+
let views;
|
|
1405
1867
|
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
1406
1868
|
const columns = {};
|
|
1407
1869
|
const primaryKeyColumn = table.primaryKey ?? table.columns.find((column) => column.primaryKey)?.name ?? null;
|
|
@@ -1426,15 +1888,40 @@ async function schemaToState(schema) {
|
|
|
1426
1888
|
};
|
|
1427
1889
|
}
|
|
1428
1890
|
}
|
|
1891
|
+
let indexes;
|
|
1892
|
+
if (table.indexes?.length) {
|
|
1893
|
+
indexes = {};
|
|
1894
|
+
for (const index of table.indexes) {
|
|
1895
|
+
indexes[index.name] = {
|
|
1896
|
+
name: index.name,
|
|
1897
|
+
table: index.table,
|
|
1898
|
+
columns: [...index.columns],
|
|
1899
|
+
unique: index.unique,
|
|
1900
|
+
...index.where !== void 0 && { where: index.where },
|
|
1901
|
+
...index.expression !== void 0 && { expression: index.expression }
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1429
1905
|
tables[tableName] = {
|
|
1430
1906
|
columns,
|
|
1431
1907
|
...primaryKeyColumn !== null && { primaryKey: primaryKeyColumn },
|
|
1908
|
+
...indexes !== void 0 && { indexes },
|
|
1432
1909
|
...policies !== void 0 && { policies }
|
|
1433
1910
|
};
|
|
1434
1911
|
}
|
|
1912
|
+
if (schema.views && Object.keys(schema.views).length > 0) {
|
|
1913
|
+
views = {};
|
|
1914
|
+
for (const [viewName, view] of Object.entries(schema.views)) {
|
|
1915
|
+
views[viewName] = {
|
|
1916
|
+
query: view.query,
|
|
1917
|
+
hash: view.hash
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1435
1921
|
return {
|
|
1436
1922
|
version: 1,
|
|
1437
|
-
tables
|
|
1923
|
+
tables,
|
|
1924
|
+
...views !== void 0 && { views }
|
|
1438
1925
|
};
|
|
1439
1926
|
}
|
|
1440
1927
|
var init_state_transform = __esm({
|
|
@@ -1463,6 +1950,242 @@ var init_state_manager = __esm({
|
|
|
1463
1950
|
}
|
|
1464
1951
|
});
|
|
1465
1952
|
|
|
1953
|
+
// node_modules/@xubylele/schema-forge-core/dist/core/plan-builder.js
|
|
1954
|
+
function toAction(operation) {
|
|
1955
|
+
switch (operation.kind) {
|
|
1956
|
+
case "create_table":
|
|
1957
|
+
case "add_column":
|
|
1958
|
+
case "add_primary_key_constraint":
|
|
1959
|
+
case "create_index":
|
|
1960
|
+
case "create_policy":
|
|
1961
|
+
case "create_view":
|
|
1962
|
+
return "create";
|
|
1963
|
+
case "drop_table":
|
|
1964
|
+
case "drop_column":
|
|
1965
|
+
case "drop_primary_key_constraint":
|
|
1966
|
+
case "drop_index":
|
|
1967
|
+
case "drop_policy":
|
|
1968
|
+
case "drop_view":
|
|
1969
|
+
return "delete";
|
|
1970
|
+
case "column_type_changed":
|
|
1971
|
+
case "column_nullability_changed":
|
|
1972
|
+
case "column_default_changed":
|
|
1973
|
+
case "column_unique_changed":
|
|
1974
|
+
case "modify_policy":
|
|
1975
|
+
case "replace_view":
|
|
1976
|
+
return "modify";
|
|
1977
|
+
default: {
|
|
1978
|
+
const exhaustiveCheck = operation;
|
|
1979
|
+
return exhaustiveCheck;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function actionToSymbol(action) {
|
|
1984
|
+
if (action === "create") {
|
|
1985
|
+
return "+";
|
|
1986
|
+
}
|
|
1987
|
+
if (action === "modify") {
|
|
1988
|
+
return "~";
|
|
1989
|
+
}
|
|
1990
|
+
return "-";
|
|
1991
|
+
}
|
|
1992
|
+
function formatNullable(value) {
|
|
1993
|
+
return value ? "nullable" : "not null";
|
|
1994
|
+
}
|
|
1995
|
+
function formatUnique(value) {
|
|
1996
|
+
return value ? "unique" : "not unique";
|
|
1997
|
+
}
|
|
1998
|
+
function formatDefault(value) {
|
|
1999
|
+
if (value === null) {
|
|
2000
|
+
return "null";
|
|
2001
|
+
}
|
|
2002
|
+
return value;
|
|
2003
|
+
}
|
|
2004
|
+
function toEntry(operation) {
|
|
2005
|
+
const action = toAction(operation);
|
|
2006
|
+
const symbol = actionToSymbol(action);
|
|
2007
|
+
switch (operation.kind) {
|
|
2008
|
+
case "create_table":
|
|
2009
|
+
return {
|
|
2010
|
+
action,
|
|
2011
|
+
symbol,
|
|
2012
|
+
operationKind: operation.kind,
|
|
2013
|
+
summary: `create table ${operation.table.name}`,
|
|
2014
|
+
tableName: operation.table.name
|
|
2015
|
+
};
|
|
2016
|
+
case "drop_table":
|
|
2017
|
+
return {
|
|
2018
|
+
action,
|
|
2019
|
+
symbol,
|
|
2020
|
+
operationKind: operation.kind,
|
|
2021
|
+
summary: `drop table ${operation.tableName}`,
|
|
2022
|
+
tableName: operation.tableName
|
|
2023
|
+
};
|
|
2024
|
+
case "add_column":
|
|
2025
|
+
return {
|
|
2026
|
+
action,
|
|
2027
|
+
symbol,
|
|
2028
|
+
operationKind: operation.kind,
|
|
2029
|
+
summary: `add column ${operation.column.name} to ${operation.tableName}`,
|
|
2030
|
+
tableName: operation.tableName,
|
|
2031
|
+
columnName: operation.column.name
|
|
2032
|
+
};
|
|
2033
|
+
case "drop_column":
|
|
2034
|
+
return {
|
|
2035
|
+
action,
|
|
2036
|
+
symbol,
|
|
2037
|
+
operationKind: operation.kind,
|
|
2038
|
+
summary: `drop column ${operation.columnName} from ${operation.tableName}`,
|
|
2039
|
+
tableName: operation.tableName,
|
|
2040
|
+
columnName: operation.columnName
|
|
2041
|
+
};
|
|
2042
|
+
case "column_type_changed":
|
|
2043
|
+
return {
|
|
2044
|
+
action,
|
|
2045
|
+
symbol,
|
|
2046
|
+
operationKind: operation.kind,
|
|
2047
|
+
summary: `modify column ${operation.columnName} type on ${operation.tableName} (${operation.fromType} -> ${operation.toType})`,
|
|
2048
|
+
tableName: operation.tableName,
|
|
2049
|
+
columnName: operation.columnName,
|
|
2050
|
+
from: operation.fromType,
|
|
2051
|
+
to: operation.toType
|
|
2052
|
+
};
|
|
2053
|
+
case "column_nullability_changed":
|
|
2054
|
+
return {
|
|
2055
|
+
action,
|
|
2056
|
+
symbol,
|
|
2057
|
+
operationKind: operation.kind,
|
|
2058
|
+
summary: `modify column ${operation.columnName} nullability on ${operation.tableName} (${formatNullable(operation.from)} -> ${formatNullable(operation.to)})`,
|
|
2059
|
+
tableName: operation.tableName,
|
|
2060
|
+
columnName: operation.columnName,
|
|
2061
|
+
from: operation.from,
|
|
2062
|
+
to: operation.to
|
|
2063
|
+
};
|
|
2064
|
+
case "column_default_changed":
|
|
2065
|
+
return {
|
|
2066
|
+
action,
|
|
2067
|
+
symbol,
|
|
2068
|
+
operationKind: operation.kind,
|
|
2069
|
+
summary: `modify column ${operation.columnName} default on ${operation.tableName} (${formatDefault(operation.fromDefault)} -> ${formatDefault(operation.toDefault)})`,
|
|
2070
|
+
tableName: operation.tableName,
|
|
2071
|
+
columnName: operation.columnName,
|
|
2072
|
+
from: operation.fromDefault,
|
|
2073
|
+
to: operation.toDefault
|
|
2074
|
+
};
|
|
2075
|
+
case "column_unique_changed":
|
|
2076
|
+
return {
|
|
2077
|
+
action,
|
|
2078
|
+
symbol,
|
|
2079
|
+
operationKind: operation.kind,
|
|
2080
|
+
summary: `modify column ${operation.columnName} unique constraint on ${operation.tableName} (${formatUnique(operation.from)} -> ${formatUnique(operation.to)})`,
|
|
2081
|
+
tableName: operation.tableName,
|
|
2082
|
+
columnName: operation.columnName,
|
|
2083
|
+
from: operation.from,
|
|
2084
|
+
to: operation.to
|
|
2085
|
+
};
|
|
2086
|
+
case "drop_primary_key_constraint":
|
|
2087
|
+
return {
|
|
2088
|
+
action,
|
|
2089
|
+
symbol,
|
|
2090
|
+
operationKind: operation.kind,
|
|
2091
|
+
summary: `drop primary key constraint on ${operation.tableName}`,
|
|
2092
|
+
tableName: operation.tableName
|
|
2093
|
+
};
|
|
2094
|
+
case "add_primary_key_constraint":
|
|
2095
|
+
return {
|
|
2096
|
+
action,
|
|
2097
|
+
symbol,
|
|
2098
|
+
operationKind: operation.kind,
|
|
2099
|
+
summary: `add primary key constraint on ${operation.tableName} (${operation.columnName})`,
|
|
2100
|
+
tableName: operation.tableName,
|
|
2101
|
+
columnName: operation.columnName
|
|
2102
|
+
};
|
|
2103
|
+
case "create_index":
|
|
2104
|
+
return {
|
|
2105
|
+
action,
|
|
2106
|
+
symbol,
|
|
2107
|
+
operationKind: operation.kind,
|
|
2108
|
+
summary: `create index ${operation.index.name} on ${operation.tableName}`,
|
|
2109
|
+
tableName: operation.tableName
|
|
2110
|
+
};
|
|
2111
|
+
case "drop_index":
|
|
2112
|
+
return {
|
|
2113
|
+
action,
|
|
2114
|
+
symbol,
|
|
2115
|
+
operationKind: operation.kind,
|
|
2116
|
+
summary: `drop index ${operation.index.name} on ${operation.tableName}`,
|
|
2117
|
+
tableName: operation.tableName
|
|
2118
|
+
};
|
|
2119
|
+
case "create_policy":
|
|
2120
|
+
return {
|
|
2121
|
+
action,
|
|
2122
|
+
symbol,
|
|
2123
|
+
operationKind: operation.kind,
|
|
2124
|
+
summary: `create policy ${operation.policy.name} on ${operation.tableName}`,
|
|
2125
|
+
tableName: operation.tableName
|
|
2126
|
+
};
|
|
2127
|
+
case "drop_policy":
|
|
2128
|
+
return {
|
|
2129
|
+
action,
|
|
2130
|
+
symbol,
|
|
2131
|
+
operationKind: operation.kind,
|
|
2132
|
+
summary: `drop policy ${operation.policyName} on ${operation.tableName}`,
|
|
2133
|
+
tableName: operation.tableName
|
|
2134
|
+
};
|
|
2135
|
+
case "modify_policy":
|
|
2136
|
+
return {
|
|
2137
|
+
action,
|
|
2138
|
+
symbol,
|
|
2139
|
+
operationKind: operation.kind,
|
|
2140
|
+
summary: `modify policy ${operation.policyName} on ${operation.tableName}`,
|
|
2141
|
+
tableName: operation.tableName
|
|
2142
|
+
};
|
|
2143
|
+
case "create_view":
|
|
2144
|
+
return {
|
|
2145
|
+
action,
|
|
2146
|
+
symbol,
|
|
2147
|
+
operationKind: operation.kind,
|
|
2148
|
+
summary: `create view ${operation.view.name}`
|
|
2149
|
+
};
|
|
2150
|
+
case "drop_view":
|
|
2151
|
+
return {
|
|
2152
|
+
action,
|
|
2153
|
+
symbol,
|
|
2154
|
+
operationKind: operation.kind,
|
|
2155
|
+
summary: `drop view ${operation.viewName}`
|
|
2156
|
+
};
|
|
2157
|
+
case "replace_view":
|
|
2158
|
+
return {
|
|
2159
|
+
action,
|
|
2160
|
+
symbol,
|
|
2161
|
+
operationKind: operation.kind,
|
|
2162
|
+
summary: `replace view ${operation.view.name}`
|
|
2163
|
+
};
|
|
2164
|
+
default: {
|
|
2165
|
+
const exhaustiveCheck = operation;
|
|
2166
|
+
return exhaustiveCheck;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
function formatMigrationPlanLine(entry) {
|
|
2171
|
+
return `${entry.symbol} ${entry.summary}`;
|
|
2172
|
+
}
|
|
2173
|
+
function formatMigrationPlanLines(entries) {
|
|
2174
|
+
return entries.map(formatMigrationPlanLine);
|
|
2175
|
+
}
|
|
2176
|
+
function buildMigrationPlan(diff) {
|
|
2177
|
+
const entries = diff.operations.map(toEntry);
|
|
2178
|
+
return {
|
|
2179
|
+
entries,
|
|
2180
|
+
lines: formatMigrationPlanLines(entries)
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
var init_plan_builder = __esm({
|
|
2184
|
+
"node_modules/@xubylele/schema-forge-core/dist/core/plan-builder.js"() {
|
|
2185
|
+
"use strict";
|
|
2186
|
+
}
|
|
2187
|
+
});
|
|
2188
|
+
|
|
1466
2189
|
// node_modules/@xubylele/schema-forge-core/dist/generator/sql-generator.js
|
|
1467
2190
|
function generateEnableRls(tableName) {
|
|
1468
2191
|
return `ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;`;
|
|
@@ -1507,14 +2230,46 @@ function generateOperation(operation, provider, sqlConfig) {
|
|
|
1507
2230
|
return generateDropPrimaryKeyConstraint(operation.tableName);
|
|
1508
2231
|
case "add_primary_key_constraint":
|
|
1509
2232
|
return generateAddPrimaryKeyConstraint(operation.tableName, operation.columnName);
|
|
2233
|
+
case "create_index":
|
|
2234
|
+
return generateCreateIndex(operation.tableName, operation.index);
|
|
2235
|
+
case "drop_index":
|
|
2236
|
+
return generateDropIndex(operation.tableName, operation.index);
|
|
1510
2237
|
case "create_policy":
|
|
1511
2238
|
return generateCreatePolicy(operation.tableName, operation.policy);
|
|
1512
2239
|
case "drop_policy":
|
|
1513
2240
|
return generateDropPolicy(operation.tableName, operation.policyName);
|
|
2241
|
+
case "create_view":
|
|
2242
|
+
return generateCreateOrReplaceView(operation.view);
|
|
2243
|
+
case "drop_view":
|
|
2244
|
+
return generateDropView(operation.viewName);
|
|
2245
|
+
case "replace_view":
|
|
2246
|
+
return generateCreateOrReplaceView(operation.view);
|
|
1514
2247
|
case "modify_policy":
|
|
1515
2248
|
return generateModifyPolicy(operation.tableName, operation.policyName, operation.policy);
|
|
1516
2249
|
}
|
|
1517
2250
|
}
|
|
2251
|
+
function generateCreateOrReplaceView(view) {
|
|
2252
|
+
return `CREATE OR REPLACE VIEW ${view.name} AS
|
|
2253
|
+
${view.query};`;
|
|
2254
|
+
}
|
|
2255
|
+
function generateDropView(viewName) {
|
|
2256
|
+
return `DROP VIEW IF EXISTS ${viewName};`;
|
|
2257
|
+
}
|
|
2258
|
+
function generateCreateIndex(tableName, index) {
|
|
2259
|
+
const uniqueToken = index.unique ? "UNIQUE " : "";
|
|
2260
|
+
const payload = index.expression ? `(${index.expression})` : `(${index.columns.join(", ")})`;
|
|
2261
|
+
const whereClause = index.where ? ` WHERE ${index.where}` : "";
|
|
2262
|
+
return `CREATE ${uniqueToken}INDEX ${index.name} ON ${tableName} ${payload}${whereClause};`;
|
|
2263
|
+
}
|
|
2264
|
+
function generateDropIndex(tableName, index) {
|
|
2265
|
+
const fallbackName = deterministicIndexName({
|
|
2266
|
+
table: tableName,
|
|
2267
|
+
columns: index.columns,
|
|
2268
|
+
expression: index.expression
|
|
2269
|
+
});
|
|
2270
|
+
const indexNames = Array.from(/* @__PURE__ */ new Set([index.name, fallbackName]));
|
|
2271
|
+
return indexNames.map((indexName) => `DROP INDEX IF EXISTS ${indexName};`).join("\n");
|
|
2272
|
+
}
|
|
1518
2273
|
function generateCreateTable(table, provider, sqlConfig) {
|
|
1519
2274
|
const columnDefs = table.columns.map((col) => generateColumnDefinition(col, provider, sqlConfig));
|
|
1520
2275
|
const lines = ["CREATE TABLE " + table.name + " ("];
|
|
@@ -2558,6 +3313,26 @@ function renderPolicy(policy) {
|
|
|
2558
3313
|
}
|
|
2559
3314
|
return lines.join("\n");
|
|
2560
3315
|
}
|
|
3316
|
+
function renderIndex(index) {
|
|
3317
|
+
const resolvedName = index.name || deterministicIndexName({
|
|
3318
|
+
table: index.table,
|
|
3319
|
+
columns: index.columns,
|
|
3320
|
+
expression: index.expression
|
|
3321
|
+
});
|
|
3322
|
+
const lines = [`index ${resolvedName} on ${index.table}`];
|
|
3323
|
+
if (index.expression) {
|
|
3324
|
+
lines.push(`expression ${index.expression}`);
|
|
3325
|
+
} else {
|
|
3326
|
+
lines.push(`columns ${index.columns.join(", ")}`);
|
|
3327
|
+
}
|
|
3328
|
+
if (index.unique) {
|
|
3329
|
+
lines.push("unique");
|
|
3330
|
+
}
|
|
3331
|
+
if (index.where) {
|
|
3332
|
+
lines.push(`where ${index.where}`);
|
|
3333
|
+
}
|
|
3334
|
+
return lines.join("\n");
|
|
3335
|
+
}
|
|
2561
3336
|
function schemaToDsl(schema) {
|
|
2562
3337
|
const tableNames = Object.keys(schema.tables).sort((left, right) => left.localeCompare(right));
|
|
2563
3338
|
const blocks = [];
|
|
@@ -2569,6 +3344,17 @@ function schemaToDsl(schema) {
|
|
|
2569
3344
|
}
|
|
2570
3345
|
tableLines.push("}");
|
|
2571
3346
|
blocks.push(tableLines.join("\n"));
|
|
3347
|
+
const sortedIndexes = (table.indexes ?? []).map((index) => ({
|
|
3348
|
+
...index,
|
|
3349
|
+
name: index.name || deterministicIndexName({
|
|
3350
|
+
table: index.table,
|
|
3351
|
+
columns: index.columns,
|
|
3352
|
+
expression: index.expression
|
|
3353
|
+
})
|
|
3354
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
3355
|
+
for (const index of sortedIndexes) {
|
|
3356
|
+
blocks.push(renderIndex(index));
|
|
3357
|
+
}
|
|
2572
3358
|
if (table.policies?.length) {
|
|
2573
3359
|
for (const policy of table.policies) {
|
|
2574
3360
|
blocks.push(renderPolicy(policy));
|
|
@@ -2586,6 +3372,7 @@ ${blocks.join("\n\n")}
|
|
|
2586
3372
|
var init_schema_to_dsl = __esm({
|
|
2587
3373
|
"node_modules/@xubylele/schema-forge-core/dist/core/sql/schema-to-dsl.js"() {
|
|
2588
3374
|
"use strict";
|
|
3375
|
+
init_normalize();
|
|
2589
3376
|
}
|
|
2590
3377
|
});
|
|
2591
3378
|
|
|
@@ -3039,13 +3826,17 @@ __export(dist_exports, {
|
|
|
3039
3826
|
SchemaValidationError: () => SchemaValidationError,
|
|
3040
3827
|
analyzeSchemaDrift: () => analyzeSchemaDrift,
|
|
3041
3828
|
applySqlOps: () => applySqlOps,
|
|
3829
|
+
buildMigrationPlan: () => buildMigrationPlan,
|
|
3042
3830
|
checkOperationSafety: () => checkOperationSafety,
|
|
3043
3831
|
checkSchemaSafety: () => checkSchemaSafety,
|
|
3044
3832
|
classifyOperation: () => classifyOperation,
|
|
3833
|
+
deterministicIndexName: () => deterministicIndexName,
|
|
3045
3834
|
diffSchemas: () => diffSchemas,
|
|
3046
3835
|
ensureDir: () => ensureDir2,
|
|
3047
3836
|
fileExists: () => fileExists2,
|
|
3048
3837
|
findFiles: () => findFiles,
|
|
3838
|
+
formatMigrationPlanLine: () => formatMigrationPlanLine,
|
|
3839
|
+
formatMigrationPlanLines: () => formatMigrationPlanLines,
|
|
3049
3840
|
generateSql: () => generateSql,
|
|
3050
3841
|
getColumnNamesFromSchema: () => getColumnNamesFromSchema,
|
|
3051
3842
|
getColumnNamesFromState: () => getColumnNamesFromState,
|
|
@@ -3056,6 +3847,7 @@ __export(dist_exports, {
|
|
|
3056
3847
|
getStatePath: () => getStatePath2,
|
|
3057
3848
|
getTableNamesFromSchema: () => getTableNamesFromSchema,
|
|
3058
3849
|
getTableNamesFromState: () => getTableNamesFromState,
|
|
3850
|
+
hashSqlContent: () => hashSqlContent,
|
|
3059
3851
|
introspectPostgresSchema: () => introspectPostgresSchema,
|
|
3060
3852
|
legacyPkName: () => legacyPkName,
|
|
3061
3853
|
legacyUqName: () => legacyUqName,
|
|
@@ -3063,6 +3855,7 @@ __export(dist_exports, {
|
|
|
3063
3855
|
loadState: () => loadState,
|
|
3064
3856
|
normalizeDefault: () => normalizeDefault,
|
|
3065
3857
|
normalizeIdent: () => normalizeIdent,
|
|
3858
|
+
normalizeSqlExpression: () => normalizeSqlExpression,
|
|
3066
3859
|
nowTimestamp: () => nowTimestamp,
|
|
3067
3860
|
parseAddDropConstraint: () => parseAddDropConstraint,
|
|
3068
3861
|
parseAlterColumnType: () => parseAlterColumnType,
|
|
@@ -3101,6 +3894,7 @@ var init_dist = __esm({
|
|
|
3101
3894
|
init_validate();
|
|
3102
3895
|
init_safety();
|
|
3103
3896
|
init_state_manager();
|
|
3897
|
+
init_plan_builder();
|
|
3104
3898
|
init_sql_generator();
|
|
3105
3899
|
init_parse_migration();
|
|
3106
3900
|
init_apply_ops();
|
|
@@ -3117,12 +3911,12 @@ var init_dist = __esm({
|
|
|
3117
3911
|
});
|
|
3118
3912
|
|
|
3119
3913
|
// src/cli.ts
|
|
3120
|
-
var
|
|
3914
|
+
var import_commander10 = require("commander");
|
|
3121
3915
|
|
|
3122
3916
|
// package.json
|
|
3123
3917
|
var package_default = {
|
|
3124
3918
|
name: "@xubylele/schema-forge",
|
|
3125
|
-
version: "1.
|
|
3919
|
+
version: "1.13.0",
|
|
3126
3920
|
description: "Universal migration generator from schema DSL",
|
|
3127
3921
|
main: "dist/cli.js",
|
|
3128
3922
|
type: "commonjs",
|
|
@@ -3131,12 +3925,12 @@ var package_default = {
|
|
|
3131
3925
|
},
|
|
3132
3926
|
exports: {
|
|
3133
3927
|
".": {
|
|
3134
|
-
require: "dist/cli.js",
|
|
3135
|
-
types: "dist/cli.d.ts"
|
|
3928
|
+
require: "./dist/cli.js",
|
|
3929
|
+
types: "./dist/cli.d.ts"
|
|
3136
3930
|
},
|
|
3137
3931
|
"./api": {
|
|
3138
|
-
require: "dist/api.js",
|
|
3139
|
-
types: "dist/api.d.ts"
|
|
3932
|
+
require: "./dist/api.js",
|
|
3933
|
+
types: "./dist/api.d.ts"
|
|
3140
3934
|
}
|
|
3141
3935
|
},
|
|
3142
3936
|
scripts: {
|
|
@@ -3176,19 +3970,19 @@ var package_default = {
|
|
|
3176
3970
|
boxen: "^8.0.1",
|
|
3177
3971
|
chalk: "^5.6.2",
|
|
3178
3972
|
commander: "^14.0.3",
|
|
3179
|
-
pg: "^8.
|
|
3973
|
+
pg: "^8.20.0",
|
|
3180
3974
|
"update-notifier": "^7.3.1"
|
|
3181
3975
|
},
|
|
3182
3976
|
devDependencies: {
|
|
3183
3977
|
"@changesets/cli": "^2.30.0",
|
|
3184
|
-
"@types/node": "^25.2
|
|
3185
|
-
"@types/pg": "^8.
|
|
3186
|
-
"@xubylele/schema-forge-core": "^1.
|
|
3187
|
-
testcontainers: "^11.
|
|
3978
|
+
"@types/node": "^25.5.2",
|
|
3979
|
+
"@types/pg": "^8.20.0",
|
|
3980
|
+
"@xubylele/schema-forge-core": "^1.6.0",
|
|
3981
|
+
testcontainers: "^11.13.0",
|
|
3188
3982
|
"ts-node": "^10.9.2",
|
|
3189
3983
|
tsup: "^8.5.1",
|
|
3190
|
-
typescript: "^
|
|
3191
|
-
vitest: "^4.
|
|
3984
|
+
typescript: "^6.0.2",
|
|
3985
|
+
vitest: "^4.1.2"
|
|
3192
3986
|
}
|
|
3193
3987
|
};
|
|
3194
3988
|
|
|
@@ -3388,6 +4182,10 @@ async function generateSql2(diff, provider, config) {
|
|
|
3388
4182
|
const core = await loadCore();
|
|
3389
4183
|
return core.generateSql(diff, provider, config);
|
|
3390
4184
|
}
|
|
4185
|
+
async function buildMigrationPlan2(diff) {
|
|
4186
|
+
const core = await loadCore();
|
|
4187
|
+
return core.buildMigrationPlan(diff);
|
|
4188
|
+
}
|
|
3391
4189
|
async function schemaToState2(schema) {
|
|
3392
4190
|
const core = await loadCore();
|
|
3393
4191
|
return core.schemaToState(schema);
|
|
@@ -4009,12 +4807,109 @@ async function runIntrospect(options = {}) {
|
|
|
4009
4807
|
}
|
|
4010
4808
|
}
|
|
4011
4809
|
|
|
4012
|
-
// src/commands/
|
|
4810
|
+
// src/commands/plan.ts
|
|
4013
4811
|
var import_commander7 = require("commander");
|
|
4014
4812
|
var import_path13 = __toESM(require("path"));
|
|
4015
4813
|
function resolveConfigPath5(root, targetPath) {
|
|
4016
4814
|
return import_path13.default.isAbsolute(targetPath) ? targetPath : import_path13.default.join(root, targetPath);
|
|
4017
4815
|
}
|
|
4816
|
+
async function runPlan(options = {}) {
|
|
4817
|
+
if (options.safe && options.force) {
|
|
4818
|
+
throw new Error("Cannot use --safe and --force flags together. Choose one:\n --safe: Block destructive operations\n --force: Bypass safety checks");
|
|
4819
|
+
}
|
|
4820
|
+
const root = getProjectRoot();
|
|
4821
|
+
const configPath = getConfigPath(root);
|
|
4822
|
+
if (!await fileExists(configPath)) {
|
|
4823
|
+
throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
|
|
4824
|
+
}
|
|
4825
|
+
const config = await readJsonFile(configPath, {});
|
|
4826
|
+
const useLiveDatabase = Boolean(options.url || process.env.DATABASE_URL);
|
|
4827
|
+
const requiredFields = useLiveDatabase ? ["schemaFile"] : ["schemaFile", "stateFile"];
|
|
4828
|
+
for (const field of requiredFields) {
|
|
4829
|
+
const value = config[field];
|
|
4830
|
+
if (!value || typeof value !== "string") {
|
|
4831
|
+
throw new Error(`Invalid config: '${field}' is required`);
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
const schemaPath = resolveConfigPath5(root, config.schemaFile);
|
|
4835
|
+
const statePath = config.stateFile ? resolveConfigPath5(root, config.stateFile) : null;
|
|
4836
|
+
const schemaSource = await readTextFile(schemaPath);
|
|
4837
|
+
const schema = await parseSchema2(schemaSource);
|
|
4838
|
+
try {
|
|
4839
|
+
await validateSchema2(schema);
|
|
4840
|
+
} catch (error2) {
|
|
4841
|
+
if (error2 instanceof Error) {
|
|
4842
|
+
throw await createSchemaValidationError(error2.message);
|
|
4843
|
+
}
|
|
4844
|
+
throw error2;
|
|
4845
|
+
}
|
|
4846
|
+
const previousState = useLiveDatabase ? await withPostgresQueryExecutor(
|
|
4847
|
+
resolvePostgresConnectionString({ url: options.url }),
|
|
4848
|
+
async (query) => {
|
|
4849
|
+
const schemaFilters = parseSchemaList(options.schema);
|
|
4850
|
+
const liveSchema = await introspectPostgresSchema2({
|
|
4851
|
+
query,
|
|
4852
|
+
...schemaFilters ? { schemas: schemaFilters } : {}
|
|
4853
|
+
});
|
|
4854
|
+
return schemaToState2(liveSchema);
|
|
4855
|
+
}
|
|
4856
|
+
) : await loadState2(statePath ?? "");
|
|
4857
|
+
const diff = await diffSchemas2(previousState, schema);
|
|
4858
|
+
if (options.force) {
|
|
4859
|
+
forceWarning("Are you sure to use --force? This option will bypass safety checks for destructive operations.");
|
|
4860
|
+
}
|
|
4861
|
+
if (options.safe && !options.force && diff.operations.length > 0) {
|
|
4862
|
+
const findings = await validateSchemaChanges2(previousState, schema);
|
|
4863
|
+
const destructiveFindings = findings.filter((f) => f.severity === "error");
|
|
4864
|
+
if (destructiveFindings.length > 0) {
|
|
4865
|
+
const errorMessages = destructiveFindings.map((f) => {
|
|
4866
|
+
const target = f.column ? `${f.table}.${f.column}` : f.table;
|
|
4867
|
+
const typeRange = f.from && f.to ? ` (${f.from} -> ${f.to})` : "";
|
|
4868
|
+
return ` - ${f.code}: ${target}${typeRange}`;
|
|
4869
|
+
}).join("\n");
|
|
4870
|
+
throw await createSchemaValidationError(
|
|
4871
|
+
`Cannot proceed with --safe flag: Found ${destructiveFindings.length} destructive operation(s):
|
|
4872
|
+
${errorMessages}
|
|
4873
|
+
|
|
4874
|
+
Remove --safe flag or modify schema to avoid destructive changes.`
|
|
4875
|
+
);
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
if (!options.safe && !options.force && diff.operations.length > 0) {
|
|
4879
|
+
const findings = await validateSchemaChanges2(previousState, schema);
|
|
4880
|
+
const riskyFindings = findings.filter((f) => f.severity === "error" || f.severity === "warning");
|
|
4881
|
+
if (riskyFindings.length > 0) {
|
|
4882
|
+
const confirmed = await confirmDestructiveOps(findings);
|
|
4883
|
+
if (!confirmed) {
|
|
4884
|
+
if (process.exitCode !== EXIT_CODES.CI_DESTRUCTIVE) {
|
|
4885
|
+
process.exitCode = EXIT_CODES.VALIDATION_ERROR;
|
|
4886
|
+
}
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
if (diff.operations.length === 0) {
|
|
4892
|
+
success("No changes detected");
|
|
4893
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
const plan = await buildMigrationPlan2(diff);
|
|
4897
|
+
console.log(plan.lines.join("\n"));
|
|
4898
|
+
process.exitCode = EXIT_CODES.SUCCESS;
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
// src/commands/preview.ts
|
|
4902
|
+
var import_commander8 = require("commander");
|
|
4903
|
+
async function runPreview(options = {}) {
|
|
4904
|
+
await runPlan(options);
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
// src/commands/validate.ts
|
|
4908
|
+
var import_commander9 = require("commander");
|
|
4909
|
+
var import_path14 = __toESM(require("path"));
|
|
4910
|
+
function resolveConfigPath6(root, targetPath) {
|
|
4911
|
+
return import_path14.default.isAbsolute(targetPath) ? targetPath : import_path14.default.join(root, targetPath);
|
|
4912
|
+
}
|
|
4018
4913
|
async function runValidate(options = {}) {
|
|
4019
4914
|
const root = getProjectRoot();
|
|
4020
4915
|
const configPath = getConfigPath(root);
|
|
@@ -4030,11 +4925,11 @@ async function runValidate(options = {}) {
|
|
|
4030
4925
|
throw new Error(`Invalid config: '${field}' is required`);
|
|
4031
4926
|
}
|
|
4032
4927
|
}
|
|
4033
|
-
const schemaPath =
|
|
4928
|
+
const schemaPath = resolveConfigPath6(root, config.schemaFile);
|
|
4034
4929
|
if (!config.stateFile) {
|
|
4035
4930
|
throw new Error("Invalid config: 'stateFile' is required");
|
|
4036
4931
|
}
|
|
4037
|
-
const statePath =
|
|
4932
|
+
const statePath = resolveConfigPath6(root, config.stateFile);
|
|
4038
4933
|
const schemaSource = await readTextFile(schemaPath);
|
|
4039
4934
|
const schema = await parseSchema2(schemaSource);
|
|
4040
4935
|
try {
|
|
@@ -4194,7 +5089,7 @@ async function seedLastSeenVersion(version) {
|
|
|
4194
5089
|
}
|
|
4195
5090
|
|
|
4196
5091
|
// src/cli.ts
|
|
4197
|
-
var program = new
|
|
5092
|
+
var program = new import_commander10.Command();
|
|
4198
5093
|
program.name("schema-forge").description("CLI tool for schema management and SQL generation").version(package_default.version).option("--safe", "Prevent execution of destructive operations").option("--force", "Force execution by bypassing safety checks and CI detection");
|
|
4199
5094
|
function validateFlagExclusivity(options) {
|
|
4200
5095
|
if (options.safe && options.force) {
|
|
@@ -4242,6 +5137,24 @@ program.command("diff").description("Compare two schema versions and generate mi
|
|
|
4242
5137
|
await handleError(error2);
|
|
4243
5138
|
}
|
|
4244
5139
|
});
|
|
5140
|
+
program.command("plan").description("Preview migration operations as a human-readable plan. In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--url <string>", "PostgreSQL connection URL for live plan (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names to introspect (default: public)").action(async (options) => {
|
|
5141
|
+
try {
|
|
5142
|
+
const globalOptions = program.opts();
|
|
5143
|
+
validateFlagExclusivity(globalOptions);
|
|
5144
|
+
await runPlan({ ...options, ...globalOptions });
|
|
5145
|
+
} catch (error2) {
|
|
5146
|
+
await handleError(error2);
|
|
5147
|
+
}
|
|
5148
|
+
});
|
|
5149
|
+
program.command("preview").description("Preview migration operations (alias of plan). In CI environments (CI=true), exits with code 3 if destructive operations are detected unless --force is used.").option("--url <string>", "PostgreSQL connection URL for live preview (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names to introspect (default: public)").action(async (options) => {
|
|
5150
|
+
try {
|
|
5151
|
+
const globalOptions = program.opts();
|
|
5152
|
+
validateFlagExclusivity(globalOptions);
|
|
5153
|
+
await runPreview({ ...options, ...globalOptions });
|
|
5154
|
+
} catch (error2) {
|
|
5155
|
+
await handleError(error2);
|
|
5156
|
+
}
|
|
5157
|
+
});
|
|
4245
5158
|
program.command("doctor").description("Check live database drift against state. Exits with code 2 when drift is detected.").option("--json", "Output structured JSON").option("--url <string>", "PostgreSQL connection URL (defaults to DATABASE_URL)").option("--schema <list>", "Comma-separated schema names to introspect (default: public)").action(async (options) => {
|
|
4246
5159
|
try {
|
|
4247
5160
|
await runDoctor(options);
|