@xubylele/schema-forge 1.12.2 → 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/dist/api.js CHANGED
@@ -30,11 +30,190 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
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
- const policyDeclRegex = /^policy\s+"([^"]*)"\s+on\s+\w+/;
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,9 +744,26 @@ 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.`);
757
+ }
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 = [];
400
765
  }
766
+ tables[index.table].indexes.push(index);
401
767
  }
402
768
  for (const { policy, startLine } of policyList) {
403
769
  if (!tables[policy.table]) {
@@ -408,164 +774,27 @@ function parseSchema(source) {
408
774
  }
409
775
  tables[policy.table].policies.push(policy);
410
776
  }
411
- return { tables };
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));
@@ -752,20 +1046,79 @@ function diffSchemas(oldState, newSchema) {
752
1046
  });
753
1047
  }
754
1048
  }
755
- }
756
- for (const tableName of commonTableNames) {
757
- const newTable = newSchema.tables[tableName];
758
- const oldTable = oldState.tables[tableName];
759
- if (!newTable || !oldTable) {
760
- continue;
1049
+ }
1050
+ for (const tableName of commonTableNames) {
1051
+ const newTable = newSchema.tables[tableName];
1052
+ const oldTable = oldState.tables[tableName];
1053
+ if (!newTable || !oldTable) {
1054
+ continue;
1055
+ }
1056
+ const previousPrimaryKey = resolveStatePrimaryKey(oldTable);
1057
+ const currentPrimaryKey = resolveSchemaPrimaryKey(newTable);
1058
+ if (currentPrimaryKey !== null && previousPrimaryKey !== currentPrimaryKey) {
1059
+ operations.push({
1060
+ kind: "add_primary_key_constraint",
1061
+ tableName,
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
+ });
761
1116
  }
762
- const previousPrimaryKey = resolveStatePrimaryKey(oldTable);
763
- const currentPrimaryKey = resolveSchemaPrimaryKey(newTable);
764
- if (currentPrimaryKey !== null && previousPrimaryKey !== currentPrimaryKey) {
1117
+ for (const index of indexCreates) {
765
1118
  operations.push({
766
- kind: "add_primary_key_constraint",
1119
+ kind: "create_index",
767
1120
  tableName,
768
- columnName: currentPrimaryKey
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(diff2) {
2177
+ const entries = diff2.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();
@@ -3126,6 +3920,8 @@ __export(api_exports, {
3126
3920
  importSchema: () => importSchema,
3127
3921
  init: () => init,
3128
3922
  introspect: () => introspect,
3923
+ plan: () => plan,
3924
+ preview: () => preview,
3129
3925
  validate: () => validate
3130
3926
  });
3131
3927
  module.exports = __toCommonJS(api_exports);
@@ -3245,6 +4041,10 @@ async function generateSql2(diff2, provider, config) {
3245
4041
  const core = await loadCore();
3246
4042
  return core.generateSql(diff2, provider, config);
3247
4043
  }
4044
+ async function buildMigrationPlan2(diff2) {
4045
+ const core = await loadCore();
4046
+ return core.buildMigrationPlan(diff2);
4047
+ }
3248
4048
  async function schemaToState2(schema) {
3249
4049
  const core = await loadCore();
3250
4050
  return core.schemaToState(schema);
@@ -3917,12 +4717,109 @@ async function runIntrospect(options = {}) {
3917
4717
  }
3918
4718
  }
3919
4719
 
3920
- // src/commands/validate.ts
4720
+ // src/commands/plan.ts
3921
4721
  var import_commander7 = require("commander");
3922
4722
  var import_path13 = __toESM(require("path"));
3923
4723
  function resolveConfigPath5(root, targetPath) {
3924
4724
  return import_path13.default.isAbsolute(targetPath) ? targetPath : import_path13.default.join(root, targetPath);
3925
4725
  }
4726
+ async function runPlan(options = {}) {
4727
+ if (options.safe && options.force) {
4728
+ throw new Error("Cannot use --safe and --force flags together. Choose one:\n --safe: Block destructive operations\n --force: Bypass safety checks");
4729
+ }
4730
+ const root = getProjectRoot();
4731
+ const configPath = getConfigPath(root);
4732
+ if (!await fileExists(configPath)) {
4733
+ throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
4734
+ }
4735
+ const config = await readJsonFile(configPath, {});
4736
+ const useLiveDatabase = Boolean(options.url || process.env.DATABASE_URL);
4737
+ const requiredFields = useLiveDatabase ? ["schemaFile"] : ["schemaFile", "stateFile"];
4738
+ for (const field of requiredFields) {
4739
+ const value = config[field];
4740
+ if (!value || typeof value !== "string") {
4741
+ throw new Error(`Invalid config: '${field}' is required`);
4742
+ }
4743
+ }
4744
+ const schemaPath = resolveConfigPath5(root, config.schemaFile);
4745
+ const statePath = config.stateFile ? resolveConfigPath5(root, config.stateFile) : null;
4746
+ const schemaSource = await readTextFile(schemaPath);
4747
+ const schema = await parseSchema2(schemaSource);
4748
+ try {
4749
+ await validateSchema2(schema);
4750
+ } catch (error2) {
4751
+ if (error2 instanceof Error) {
4752
+ throw await createSchemaValidationError(error2.message);
4753
+ }
4754
+ throw error2;
4755
+ }
4756
+ const previousState = useLiveDatabase ? await withPostgresQueryExecutor(
4757
+ resolvePostgresConnectionString({ url: options.url }),
4758
+ async (query) => {
4759
+ const schemaFilters = parseSchemaList(options.schema);
4760
+ const liveSchema = await introspectPostgresSchema2({
4761
+ query,
4762
+ ...schemaFilters ? { schemas: schemaFilters } : {}
4763
+ });
4764
+ return schemaToState2(liveSchema);
4765
+ }
4766
+ ) : await loadState2(statePath ?? "");
4767
+ const diff2 = await diffSchemas2(previousState, schema);
4768
+ if (options.force) {
4769
+ forceWarning("Are you sure to use --force? This option will bypass safety checks for destructive operations.");
4770
+ }
4771
+ if (options.safe && !options.force && diff2.operations.length > 0) {
4772
+ const findings = await validateSchemaChanges2(previousState, schema);
4773
+ const destructiveFindings = findings.filter((f) => f.severity === "error");
4774
+ if (destructiveFindings.length > 0) {
4775
+ const errorMessages = destructiveFindings.map((f) => {
4776
+ const target = f.column ? `${f.table}.${f.column}` : f.table;
4777
+ const typeRange = f.from && f.to ? ` (${f.from} -> ${f.to})` : "";
4778
+ return ` - ${f.code}: ${target}${typeRange}`;
4779
+ }).join("\n");
4780
+ throw await createSchemaValidationError(
4781
+ `Cannot proceed with --safe flag: Found ${destructiveFindings.length} destructive operation(s):
4782
+ ${errorMessages}
4783
+
4784
+ Remove --safe flag or modify schema to avoid destructive changes.`
4785
+ );
4786
+ }
4787
+ }
4788
+ if (!options.safe && !options.force && diff2.operations.length > 0) {
4789
+ const findings = await validateSchemaChanges2(previousState, schema);
4790
+ const riskyFindings = findings.filter((f) => f.severity === "error" || f.severity === "warning");
4791
+ if (riskyFindings.length > 0) {
4792
+ const confirmed = await confirmDestructiveOps(findings);
4793
+ if (!confirmed) {
4794
+ if (process.exitCode !== EXIT_CODES.CI_DESTRUCTIVE) {
4795
+ process.exitCode = EXIT_CODES.VALIDATION_ERROR;
4796
+ }
4797
+ return;
4798
+ }
4799
+ }
4800
+ }
4801
+ if (diff2.operations.length === 0) {
4802
+ success("No changes detected");
4803
+ process.exitCode = EXIT_CODES.SUCCESS;
4804
+ return;
4805
+ }
4806
+ const plan2 = await buildMigrationPlan2(diff2);
4807
+ console.log(plan2.lines.join("\n"));
4808
+ process.exitCode = EXIT_CODES.SUCCESS;
4809
+ }
4810
+
4811
+ // src/commands/preview.ts
4812
+ var import_commander8 = require("commander");
4813
+ async function runPreview(options = {}) {
4814
+ await runPlan(options);
4815
+ }
4816
+
4817
+ // src/commands/validate.ts
4818
+ var import_commander9 = require("commander");
4819
+ var import_path14 = __toESM(require("path"));
4820
+ function resolveConfigPath6(root, targetPath) {
4821
+ return import_path14.default.isAbsolute(targetPath) ? targetPath : import_path14.default.join(root, targetPath);
4822
+ }
3926
4823
  async function runValidate(options = {}) {
3927
4824
  const root = getProjectRoot();
3928
4825
  const configPath = getConfigPath(root);
@@ -3938,11 +4835,11 @@ async function runValidate(options = {}) {
3938
4835
  throw new Error(`Invalid config: '${field}' is required`);
3939
4836
  }
3940
4837
  }
3941
- const schemaPath = resolveConfigPath5(root, config.schemaFile);
4838
+ const schemaPath = resolveConfigPath6(root, config.schemaFile);
3942
4839
  if (!config.stateFile) {
3943
4840
  throw new Error("Invalid config: 'stateFile' is required");
3944
4841
  }
3945
- const statePath = resolveConfigPath5(root, config.stateFile);
4842
+ const statePath = resolveConfigPath6(root, config.stateFile);
3946
4843
  const schemaSource = await readTextFile(schemaPath);
3947
4844
  const schema = await parseSchema2(schemaSource);
3948
4845
  try {
@@ -4054,6 +4951,12 @@ async function doctor(options = {}) {
4054
4951
  async function validate(options = {}) {
4055
4952
  return runWithResult(() => runValidate(options));
4056
4953
  }
4954
+ async function plan(options = {}) {
4955
+ return runWithResult(() => runPlan(options));
4956
+ }
4957
+ async function preview(options = {}) {
4958
+ return runWithResult(() => runPreview(options));
4959
+ }
4057
4960
  async function introspect(options = {}) {
4058
4961
  return runWithResult(() => runIntrospect(options));
4059
4962
  }
@@ -4069,5 +4972,7 @@ async function importSchema(inputPath, options = {}) {
4069
4972
  importSchema,
4070
4973
  init,
4071
4974
  introspect,
4975
+ plan,
4976
+ preview,
4072
4977
  validate
4073
4978
  });