relq 1.0.25 → 1.0.27

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.
Files changed (52) hide show
  1. package/dist/cjs/cli/commands/commit.cjs +80 -0
  2. package/dist/cjs/cli/commands/import.cjs +1 -0
  3. package/dist/cjs/cli/commands/pull.cjs +108 -34
  4. package/dist/cjs/cli/commands/push.cjs +48 -8
  5. package/dist/cjs/cli/commands/rollback.cjs +205 -84
  6. package/dist/cjs/cli/commands/schema-ast.cjs +219 -0
  7. package/dist/cjs/cli/index.cjs +6 -0
  8. package/dist/cjs/cli/utils/ast-codegen.cjs +95 -3
  9. package/dist/cjs/cli/utils/ast-transformer.cjs +12 -0
  10. package/dist/cjs/cli/utils/change-tracker.cjs +135 -0
  11. package/dist/cjs/cli/utils/commit-manager.cjs +54 -0
  12. package/dist/cjs/cli/utils/migration-generator.cjs +319 -0
  13. package/dist/cjs/cli/utils/repo-manager.cjs +99 -3
  14. package/dist/cjs/cli/utils/schema-diff.cjs +390 -0
  15. package/dist/cjs/cli/utils/schema-hash.cjs +4 -0
  16. package/dist/cjs/cli/utils/schema-to-ast.cjs +477 -0
  17. package/dist/cjs/schema-definition/column-types.cjs +50 -4
  18. package/dist/cjs/schema-definition/pg-enum.cjs +10 -0
  19. package/dist/cjs/schema-definition/pg-function.cjs +19 -0
  20. package/dist/cjs/schema-definition/pg-sequence.cjs +22 -1
  21. package/dist/cjs/schema-definition/pg-trigger.cjs +39 -0
  22. package/dist/cjs/schema-definition/pg-view.cjs +17 -0
  23. package/dist/cjs/schema-definition/sql-expressions.cjs +3 -0
  24. package/dist/cjs/schema-definition/table-definition.cjs +4 -0
  25. package/dist/config.d.ts +98 -0
  26. package/dist/esm/cli/commands/commit.js +83 -3
  27. package/dist/esm/cli/commands/import.js +1 -0
  28. package/dist/esm/cli/commands/pull.js +109 -35
  29. package/dist/esm/cli/commands/push.js +49 -9
  30. package/dist/esm/cli/commands/rollback.js +206 -85
  31. package/dist/esm/cli/commands/schema-ast.js +183 -0
  32. package/dist/esm/cli/index.js +6 -0
  33. package/dist/esm/cli/utils/ast-codegen.js +93 -3
  34. package/dist/esm/cli/utils/ast-transformer.js +12 -0
  35. package/dist/esm/cli/utils/change-tracker.js +134 -0
  36. package/dist/esm/cli/utils/commit-manager.js +51 -0
  37. package/dist/esm/cli/utils/migration-generator.js +318 -0
  38. package/dist/esm/cli/utils/repo-manager.js +96 -3
  39. package/dist/esm/cli/utils/schema-diff.js +389 -0
  40. package/dist/esm/cli/utils/schema-hash.js +4 -0
  41. package/dist/esm/cli/utils/schema-to-ast.js +447 -0
  42. package/dist/esm/schema-definition/column-types.js +50 -4
  43. package/dist/esm/schema-definition/pg-enum.js +10 -0
  44. package/dist/esm/schema-definition/pg-function.js +19 -0
  45. package/dist/esm/schema-definition/pg-sequence.js +22 -1
  46. package/dist/esm/schema-definition/pg-trigger.js +39 -0
  47. package/dist/esm/schema-definition/pg-view.js +17 -0
  48. package/dist/esm/schema-definition/sql-expressions.js +3 -0
  49. package/dist/esm/schema-definition/table-definition.js +4 -0
  50. package/dist/index.d.ts +98 -0
  51. package/dist/schema-builder.d.ts +223 -24
  52. package/package.json +1 -1
@@ -476,6 +476,7 @@ export async function parseSQL(sql) {
476
476
  const schema = {
477
477
  enums: [],
478
478
  domains: [],
479
+ compositeTypes: [],
479
480
  sequences: [],
480
481
  tables: [],
481
482
  views: [],
@@ -550,6 +551,7 @@ export async function introspectedToParsedSchema(schema) {
550
551
  tables: [],
551
552
  enums: [],
552
553
  domains: [],
554
+ compositeTypes: [],
553
555
  sequences: [],
554
556
  views: [],
555
557
  functions: [],
@@ -571,6 +573,15 @@ export async function introspectedToParsedSchema(schema) {
571
573
  checkExpression: d.checkExpression,
572
574
  });
573
575
  }
576
+ for (const ct of schema.compositeTypes || []) {
577
+ parsed.compositeTypes.push({
578
+ name: ct.name,
579
+ attributes: ct.attributes.map(attr => ({
580
+ name: attr.name,
581
+ type: attr.type,
582
+ })),
583
+ });
584
+ }
574
585
  for (const t of schema.tables || []) {
575
586
  const columns = [];
576
587
  for (const c of t.columns) {
@@ -805,6 +816,7 @@ export function normalizedToParsedSchema(schema) {
805
816
  checkExpression: d.check,
806
817
  checkName: d.checkName,
807
818
  })),
819
+ compositeTypes: [],
808
820
  sequences: (schema.sequences || []).map(s => ({
809
821
  name: s.name,
810
822
  startValue: s.start,
@@ -556,3 +556,137 @@ export function generateCombinedSQL(changes) {
556
556
  }
557
557
  return lines.join('\n');
558
558
  }
559
+ export function generateDownSQL(changes) {
560
+ const reversed = [...changes].reverse();
561
+ const lines = [
562
+ '--',
563
+ '-- DOWN Migration (Rollback)',
564
+ `-- Generated at: ${new Date().toISOString()}`,
565
+ '--',
566
+ '',
567
+ ];
568
+ for (const change of reversed) {
569
+ const downSQL = generateDownSQLForChange(change);
570
+ if (downSQL) {
571
+ lines.push(downSQL);
572
+ lines.push('');
573
+ }
574
+ }
575
+ return lines.join('\n');
576
+ }
577
+ function generateDownSQLForChange(change) {
578
+ const { type, objectType, objectName, parentName, before, after } = change;
579
+ switch (type) {
580
+ case 'CREATE':
581
+ return generateDropSQL(objectType, objectName, parentName);
582
+ case 'DROP':
583
+ if (before) {
584
+ return generateCreateSQL(objectType, objectName, before, parentName);
585
+ }
586
+ return `-- Cannot reverse DROP ${objectType} ${objectName} (no 'before' state saved)`;
587
+ case 'ALTER':
588
+ if (before && after) {
589
+ return generateAlterReverseSQL(objectType, objectName, after, before, parentName);
590
+ }
591
+ return `-- Cannot reverse ALTER ${objectType} ${objectName} (no state saved)`;
592
+ case 'RENAME':
593
+ if (before && after) {
594
+ const oldName = before.name || objectName;
595
+ const newName = after.name || objectName;
596
+ return generateRenameSQL(objectType, newName, oldName, parentName);
597
+ }
598
+ return `-- Cannot reverse RENAME ${objectType} ${objectName} (no state saved)`;
599
+ default:
600
+ return null;
601
+ }
602
+ }
603
+ function generateDropSQL(objectType, objectName, parentName) {
604
+ switch (objectType) {
605
+ case 'TABLE':
606
+ return `DROP TABLE IF EXISTS "${objectName}" CASCADE;`;
607
+ case 'COLUMN':
608
+ return `ALTER TABLE "${parentName}" DROP COLUMN IF EXISTS "${objectName}";`;
609
+ case 'INDEX':
610
+ return `DROP INDEX IF EXISTS "${objectName}";`;
611
+ case 'ENUM':
612
+ return `DROP TYPE IF EXISTS "${objectName}";`;
613
+ case 'SEQUENCE':
614
+ return `DROP SEQUENCE IF EXISTS "${objectName}";`;
615
+ case 'FUNCTION':
616
+ return `DROP FUNCTION IF EXISTS "${objectName}" CASCADE;`;
617
+ case 'TRIGGER':
618
+ return `DROP TRIGGER IF EXISTS "${objectName}" ON "${parentName}";`;
619
+ case 'VIEW':
620
+ return `DROP VIEW IF EXISTS "${objectName}";`;
621
+ case 'CONSTRAINT':
622
+ case 'CHECK':
623
+ case 'FOREIGN_KEY':
624
+ case 'PRIMARY_KEY':
625
+ return `ALTER TABLE "${parentName}" DROP CONSTRAINT IF EXISTS "${objectName}";`;
626
+ default:
627
+ return `-- Cannot generate DROP for ${objectType} ${objectName}`;
628
+ }
629
+ }
630
+ function generateCreateSQL(objectType, objectName, state, parentName) {
631
+ switch (objectType) {
632
+ case 'COLUMN':
633
+ if (state.dataType) {
634
+ const nullable = state.isNullable === false ? ' NOT NULL' : '';
635
+ const defaultVal = state.defaultValue ? ` DEFAULT ${state.defaultValue}` : '';
636
+ return `ALTER TABLE "${parentName}" ADD COLUMN "${objectName}" ${state.dataType}${nullable}${defaultVal};`;
637
+ }
638
+ break;
639
+ case 'INDEX':
640
+ if (state.columns) {
641
+ const unique = state.isUnique ? 'UNIQUE ' : '';
642
+ const cols = Array.isArray(state.columns) ? state.columns.join(', ') : state.columns;
643
+ return `CREATE ${unique}INDEX "${objectName}" ON "${state.tableName || parentName}" (${cols});`;
644
+ }
645
+ break;
646
+ }
647
+ return `-- Cannot reconstruct CREATE ${objectType} ${objectName} from saved state`;
648
+ }
649
+ function generateAlterReverseSQL(objectType, objectName, from, to, parentName) {
650
+ switch (objectType) {
651
+ case 'COLUMN':
652
+ const stmts = [];
653
+ if (from.dataType !== to.dataType && to.dataType) {
654
+ stmts.push(`ALTER TABLE "${parentName}" ALTER COLUMN "${objectName}" TYPE ${to.dataType};`);
655
+ }
656
+ if (from.isNullable !== to.isNullable) {
657
+ if (to.isNullable === false) {
658
+ stmts.push(`ALTER TABLE "${parentName}" ALTER COLUMN "${objectName}" SET NOT NULL;`);
659
+ }
660
+ else {
661
+ stmts.push(`ALTER TABLE "${parentName}" ALTER COLUMN "${objectName}" DROP NOT NULL;`);
662
+ }
663
+ }
664
+ if (from.defaultValue !== to.defaultValue) {
665
+ if (to.defaultValue) {
666
+ stmts.push(`ALTER TABLE "${parentName}" ALTER COLUMN "${objectName}" SET DEFAULT ${to.defaultValue};`);
667
+ }
668
+ else {
669
+ stmts.push(`ALTER TABLE "${parentName}" ALTER COLUMN "${objectName}" DROP DEFAULT;`);
670
+ }
671
+ }
672
+ return stmts.length > 0 ? stmts.join('\n') : `-- No reverse ALTER needed for ${objectName}`;
673
+ default:
674
+ return `-- Cannot reverse ALTER ${objectType} ${objectName}`;
675
+ }
676
+ }
677
+ function generateRenameSQL(objectType, fromName, toName, parentName) {
678
+ switch (objectType) {
679
+ case 'TABLE':
680
+ return `ALTER TABLE "${fromName}" RENAME TO "${toName}";`;
681
+ case 'COLUMN':
682
+ return `ALTER TABLE "${parentName}" RENAME COLUMN "${fromName}" TO "${toName}";`;
683
+ case 'INDEX':
684
+ return `ALTER INDEX "${fromName}" RENAME TO "${toName}";`;
685
+ case 'ENUM':
686
+ return `ALTER TYPE "${fromName}" RENAME TO "${toName}";`;
687
+ case 'SEQUENCE':
688
+ return `ALTER SEQUENCE "${fromName}" RENAME TO "${toName}";`;
689
+ default:
690
+ return `-- Cannot rename ${objectType} ${fromName} to ${toName}`;
691
+ }
692
+ }
@@ -1,6 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import * as crypto from 'crypto';
3
4
  import { normalizeSchema, generateSchemaHash } from "./schema-hash.js";
5
+ import { schemaToAST } from "./schema-to-ast.js";
4
6
  const RELQ_DIR = '.relq';
5
7
  const COMMITS_FILE = 'commits.json';
6
8
  const HEAD_FILE = 'HEAD';
@@ -189,3 +191,52 @@ export async function checkSyncStatus(connection, baseDir = process.cwd()) {
189
191
  const remoteAhead = [...remoteHashes].filter(h => !localHashes.has(h)).length;
190
192
  return { inSync, localHead, remoteHead, localAhead, remoteAhead };
191
193
  }
194
+ export function generateASTHash(ast) {
195
+ const normalized = {
196
+ enums: [...ast.enums].sort((a, b) => a.name.localeCompare(b.name)),
197
+ domains: [...ast.domains].sort((a, b) => a.name.localeCompare(b.name)),
198
+ compositeTypes: [...ast.compositeTypes].sort((a, b) => a.name.localeCompare(b.name)),
199
+ sequences: [...ast.sequences].sort((a, b) => a.name.localeCompare(b.name)),
200
+ tables: [...ast.tables].sort((a, b) => a.name.localeCompare(b.name)),
201
+ views: [...ast.views].sort((a, b) => a.name.localeCompare(b.name)),
202
+ functions: [...ast.functions].sort((a, b) => a.name.localeCompare(b.name)),
203
+ triggers: [...ast.triggers].sort((a, b) => a.name.localeCompare(b.name)),
204
+ extensions: [...ast.extensions].sort(),
205
+ };
206
+ const json = JSON.stringify(normalized, null, 0);
207
+ return crypto
208
+ .createHash('sha1')
209
+ .update(json, 'utf8')
210
+ .digest('hex');
211
+ }
212
+ export function createCommitFromSchema(schema, author, message, commitLimit = 1000, baseDir = process.cwd()) {
213
+ const parsedSchema = schemaToAST(schema);
214
+ const hash = generateASTHash(parsedSchema);
215
+ const parentHash = getLocalHead(baseDir);
216
+ const commit = {
217
+ hash,
218
+ parentHash,
219
+ schemaSnapshot: parsedSchema,
220
+ author,
221
+ message,
222
+ createdAt: new Date(),
223
+ };
224
+ addLocalCommit(commit, commitLimit, baseDir);
225
+ return commit;
226
+ }
227
+ export async function createCommitFromSchemaWithRemote(schema, connection, author, message, commitLimit = 1000, baseDir = process.cwd()) {
228
+ const parsedSchema = schemaToAST(schema);
229
+ const hash = generateASTHash(parsedSchema);
230
+ const parentHash = getLocalHead(baseDir);
231
+ const commit = {
232
+ hash,
233
+ parentHash,
234
+ schemaSnapshot: parsedSchema,
235
+ author,
236
+ message,
237
+ createdAt: new Date(),
238
+ };
239
+ addLocalCommit(commit, commitLimit, baseDir);
240
+ await addRemoteCommit(connection, commit, commitLimit);
241
+ return commit;
242
+ }
@@ -23,6 +23,324 @@ export function generateMigration(diff, options = {}) {
23
23
  }
24
24
  return { up, down };
25
25
  }
26
+ export function generateMigrationFromComparison(comparison, options = {}) {
27
+ const { includeDown = true } = options;
28
+ const up = [];
29
+ const down = [];
30
+ for (const rename of comparison.renamed.enums) {
31
+ up.push(`ALTER TYPE "${rename.from}" RENAME TO "${rename.to}";`);
32
+ if (includeDown) {
33
+ down.unshift(`ALTER TYPE "${rename.to}" RENAME TO "${rename.from}";`);
34
+ }
35
+ }
36
+ for (const rename of comparison.renamed.sequences) {
37
+ up.push(`ALTER SEQUENCE "${rename.from}" RENAME TO "${rename.to}";`);
38
+ if (includeDown) {
39
+ down.unshift(`ALTER SEQUENCE "${rename.to}" RENAME TO "${rename.from}";`);
40
+ }
41
+ }
42
+ for (const rename of comparison.renamed.tables) {
43
+ up.push(`ALTER TABLE "${rename.from}" RENAME TO "${rename.to}";`);
44
+ if (includeDown) {
45
+ down.unshift(`ALTER TABLE "${rename.to}" RENAME TO "${rename.from}";`);
46
+ }
47
+ }
48
+ for (const rename of comparison.renamed.columns) {
49
+ up.push(`ALTER TABLE "${rename.table}" RENAME COLUMN "${rename.from}" TO "${rename.to}";`);
50
+ if (includeDown) {
51
+ down.unshift(`ALTER TABLE "${rename.table}" RENAME COLUMN "${rename.to}" TO "${rename.from}";`);
52
+ }
53
+ }
54
+ for (const rename of comparison.renamed.indexes) {
55
+ up.push(`ALTER INDEX "${rename.from}" RENAME TO "${rename.to}";`);
56
+ if (includeDown) {
57
+ down.unshift(`ALTER INDEX "${rename.to}" RENAME TO "${rename.from}";`);
58
+ }
59
+ }
60
+ for (const rename of comparison.renamed.functions) {
61
+ up.push(`ALTER FUNCTION "${rename.from}" RENAME TO "${rename.to}";`);
62
+ if (includeDown) {
63
+ down.unshift(`ALTER FUNCTION "${rename.to}" RENAME TO "${rename.from}";`);
64
+ }
65
+ }
66
+ for (const enumMod of comparison.modified.enums) {
67
+ for (const value of enumMod.changes.added) {
68
+ up.push(`ALTER TYPE "${enumMod.name}" ADD VALUE IF NOT EXISTS '${value}';`);
69
+ }
70
+ if (enumMod.changes.removed.length > 0 && includeDown) {
71
+ down.unshift(`-- Warning: Cannot remove enum values in PostgreSQL: ${enumMod.changes.removed.join(', ')}`);
72
+ }
73
+ }
74
+ for (const colMod of comparison.modified.columns) {
75
+ const { upSQL, downSQL } = generateColumnModification(colMod.table, colMod.column, colMod.changes);
76
+ up.push(...upSQL);
77
+ if (includeDown)
78
+ down.unshift(...downSQL);
79
+ }
80
+ for (const ext of comparison.added.extensions) {
81
+ up.push(`CREATE EXTENSION IF NOT EXISTS "${ext}";`);
82
+ if (includeDown) {
83
+ down.unshift(`DROP EXTENSION IF EXISTS "${ext}";`);
84
+ }
85
+ }
86
+ for (const enumDef of comparison.added.enums) {
87
+ const values = enumDef.values.map(v => `'${v}'`).join(', ');
88
+ up.push(`CREATE TYPE "${enumDef.name}" AS ENUM (${values});`);
89
+ if (includeDown) {
90
+ down.unshift(`DROP TYPE IF EXISTS "${enumDef.name}";`);
91
+ }
92
+ }
93
+ for (const domain of comparison.added.domains) {
94
+ let domainSQL = `CREATE DOMAIN "${domain.name}" AS ${domain.baseType}`;
95
+ if (domain.notNull)
96
+ domainSQL += ' NOT NULL';
97
+ if (domain.defaultValue)
98
+ domainSQL += ` DEFAULT ${domain.defaultValue}`;
99
+ if (domain.checkExpression) {
100
+ const checkName = domain.checkName ? `CONSTRAINT "${domain.checkName}" ` : '';
101
+ domainSQL += ` ${checkName}CHECK (${domain.checkExpression})`;
102
+ }
103
+ up.push(domainSQL + ';');
104
+ if (includeDown) {
105
+ down.unshift(`DROP DOMAIN IF EXISTS "${domain.name}";`);
106
+ }
107
+ }
108
+ for (const seq of comparison.added.sequences) {
109
+ const parts = [`CREATE SEQUENCE "${seq.name}"`];
110
+ if (seq.startValue !== undefined)
111
+ parts.push(`START WITH ${seq.startValue}`);
112
+ if (seq.increment !== undefined)
113
+ parts.push(`INCREMENT BY ${seq.increment}`);
114
+ if (seq.minValue !== undefined)
115
+ parts.push(`MINVALUE ${seq.minValue}`);
116
+ if (seq.maxValue !== undefined)
117
+ parts.push(`MAXVALUE ${seq.maxValue}`);
118
+ if (seq.cache !== undefined)
119
+ parts.push(`CACHE ${seq.cache}`);
120
+ if (seq.cycle)
121
+ parts.push('CYCLE');
122
+ up.push(parts.join(' ') + ';');
123
+ if (includeDown) {
124
+ down.unshift(`DROP SEQUENCE IF EXISTS "${seq.name}";`);
125
+ }
126
+ }
127
+ for (const table of comparison.added.tables) {
128
+ up.push(...generateCreateTableFromParsed(table));
129
+ if (includeDown) {
130
+ down.unshift(`DROP TABLE IF EXISTS "${table.name}" CASCADE;`);
131
+ }
132
+ }
133
+ for (const { table, column } of comparison.added.columns) {
134
+ up.push(`ALTER TABLE "${table}" ADD COLUMN ${generateParsedColumnDef(column)};`);
135
+ if (includeDown) {
136
+ down.unshift(`ALTER TABLE "${table}" DROP COLUMN IF EXISTS "${column.name}";`);
137
+ }
138
+ }
139
+ for (const { table, index } of comparison.added.indexes) {
140
+ up.push(generateCreateIndexFromParsed(table, index));
141
+ if (includeDown) {
142
+ down.unshift(`DROP INDEX IF EXISTS "${index.name}";`);
143
+ }
144
+ }
145
+ for (const view of comparison.added.views) {
146
+ const materialized = view.isMaterialized ? 'MATERIALIZED ' : '';
147
+ up.push(`CREATE ${materialized}VIEW "${view.name}" AS ${view.definition};`);
148
+ if (includeDown) {
149
+ down.unshift(`DROP ${materialized}VIEW IF EXISTS "${view.name}";`);
150
+ }
151
+ }
152
+ for (const func of comparison.added.functions) {
153
+ const argsStr = func.args.map(a => `${a.name || ''} ${a.type}`.trim()).join(', ');
154
+ const volatility = func.volatility || 'VOLATILE';
155
+ up.push(`CREATE OR REPLACE FUNCTION "${func.name}"(${argsStr}) RETURNS ${func.returnType} LANGUAGE ${func.language} ${volatility} AS $$ ${func.body} $$;`);
156
+ if (includeDown) {
157
+ down.unshift(`DROP FUNCTION IF EXISTS "${func.name}"(${func.args.map(a => a.type).join(', ')});`);
158
+ }
159
+ }
160
+ for (const trigger of comparison.added.triggers) {
161
+ const events = trigger.events.join(' OR ');
162
+ const forEach = trigger.forEach || 'ROW';
163
+ up.push(`CREATE TRIGGER "${trigger.name}" ${trigger.timing} ${events} ON "${trigger.table}" FOR EACH ${forEach} EXECUTE FUNCTION ${trigger.functionName}();`);
164
+ if (includeDown) {
165
+ down.unshift(`DROP TRIGGER IF EXISTS "${trigger.name}" ON "${trigger.table}";`);
166
+ }
167
+ }
168
+ for (const trigger of comparison.removed.triggers) {
169
+ up.push(`DROP TRIGGER IF EXISTS "${trigger.name}" ON "${trigger.table}";`);
170
+ if (includeDown) {
171
+ const events = trigger.events.join(' OR ');
172
+ const forEach = trigger.forEach || 'ROW';
173
+ down.unshift(`CREATE TRIGGER "${trigger.name}" ${trigger.timing} ${events} ON "${trigger.table}" FOR EACH ${forEach} EXECUTE FUNCTION ${trigger.functionName}();`);
174
+ }
175
+ }
176
+ for (const func of comparison.removed.functions) {
177
+ const argTypes = func.args.map(a => a.type).join(', ');
178
+ up.push(`DROP FUNCTION IF EXISTS "${func.name}"(${argTypes});`);
179
+ if (includeDown) {
180
+ const argsStr = func.args.map(a => `${a.name || ''} ${a.type}`.trim()).join(', ');
181
+ const volatility = func.volatility || 'VOLATILE';
182
+ down.unshift(`CREATE OR REPLACE FUNCTION "${func.name}"(${argsStr}) RETURNS ${func.returnType} LANGUAGE ${func.language} ${volatility} AS $$ ${func.body} $$;`);
183
+ }
184
+ }
185
+ for (const view of comparison.removed.views) {
186
+ const materialized = view.isMaterialized ? 'MATERIALIZED ' : '';
187
+ up.push(`DROP ${materialized}VIEW IF EXISTS "${view.name}";`);
188
+ if (includeDown) {
189
+ down.unshift(`CREATE ${materialized}VIEW "${view.name}" AS ${view.definition};`);
190
+ }
191
+ }
192
+ for (const { index } of comparison.removed.indexes) {
193
+ up.push(`DROP INDEX IF EXISTS "${index.name}";`);
194
+ }
195
+ for (const { table, column } of comparison.removed.columns) {
196
+ up.push(`ALTER TABLE "${table}" DROP COLUMN IF EXISTS "${column.name}";`);
197
+ if (includeDown) {
198
+ down.unshift(`ALTER TABLE "${table}" ADD COLUMN ${generateParsedColumnDef(column)};`);
199
+ }
200
+ }
201
+ for (const table of comparison.removed.tables) {
202
+ up.push(`DROP TABLE IF EXISTS "${table.name}" CASCADE;`);
203
+ if (includeDown) {
204
+ down.unshift(...generateCreateTableFromParsed(table));
205
+ }
206
+ }
207
+ for (const seq of comparison.removed.sequences) {
208
+ up.push(`DROP SEQUENCE IF EXISTS "${seq.name}";`);
209
+ if (includeDown) {
210
+ const parts = [`CREATE SEQUENCE "${seq.name}"`];
211
+ if (seq.startValue !== undefined)
212
+ parts.push(`START WITH ${seq.startValue}`);
213
+ if (seq.increment !== undefined)
214
+ parts.push(`INCREMENT BY ${seq.increment}`);
215
+ if (seq.minValue !== undefined)
216
+ parts.push(`MINVALUE ${seq.minValue}`);
217
+ if (seq.maxValue !== undefined)
218
+ parts.push(`MAXVALUE ${seq.maxValue}`);
219
+ if (seq.cache !== undefined)
220
+ parts.push(`CACHE ${seq.cache}`);
221
+ if (seq.cycle)
222
+ parts.push('CYCLE');
223
+ down.unshift(parts.join(' ') + ';');
224
+ }
225
+ }
226
+ for (const domain of comparison.removed.domains) {
227
+ up.push(`DROP DOMAIN IF EXISTS "${domain.name}";`);
228
+ if (includeDown) {
229
+ let domainSQL = `CREATE DOMAIN "${domain.name}" AS ${domain.baseType}`;
230
+ if (domain.notNull)
231
+ domainSQL += ' NOT NULL';
232
+ if (domain.defaultValue)
233
+ domainSQL += ` DEFAULT ${domain.defaultValue}`;
234
+ if (domain.checkExpression) {
235
+ const checkName = domain.checkName ? `CONSTRAINT "${domain.checkName}" ` : '';
236
+ domainSQL += ` ${checkName}CHECK (${domain.checkExpression})`;
237
+ }
238
+ down.unshift(domainSQL + ';');
239
+ }
240
+ }
241
+ for (const enumDef of comparison.removed.enums) {
242
+ up.push(`DROP TYPE IF EXISTS "${enumDef.name}";`);
243
+ if (includeDown) {
244
+ const values = enumDef.values.map(v => `'${v}'`).join(', ');
245
+ down.unshift(`CREATE TYPE "${enumDef.name}" AS ENUM (${values});`);
246
+ }
247
+ }
248
+ for (const ext of comparison.removed.extensions) {
249
+ up.push(`DROP EXTENSION IF EXISTS "${ext}";`);
250
+ if (includeDown) {
251
+ down.unshift(`CREATE EXTENSION IF NOT EXISTS "${ext}";`);
252
+ }
253
+ }
254
+ return { up, down };
255
+ }
256
+ function generateColumnModification(tableName, columnName, changes) {
257
+ const upSQL = [];
258
+ const downSQL = [];
259
+ for (const change of changes) {
260
+ switch (change.field) {
261
+ case 'type':
262
+ upSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${change.to} USING "${columnName}"::${change.to};`);
263
+ downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" TYPE ${change.from} USING "${columnName}"::${change.from};`);
264
+ break;
265
+ case 'nullable':
266
+ if (change.to) {
267
+ upSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP NOT NULL;`);
268
+ downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET NOT NULL;`);
269
+ }
270
+ else {
271
+ upSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET NOT NULL;`);
272
+ downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP NOT NULL;`);
273
+ }
274
+ break;
275
+ case 'default':
276
+ if (change.to !== undefined && change.to !== null) {
277
+ upSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET DEFAULT ${change.to};`);
278
+ }
279
+ else {
280
+ upSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT;`);
281
+ }
282
+ if (change.from !== undefined && change.from !== null) {
283
+ downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" SET DEFAULT ${change.from};`);
284
+ }
285
+ else {
286
+ downSQL.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT;`);
287
+ }
288
+ break;
289
+ }
290
+ }
291
+ return { upSQL, downSQL };
292
+ }
293
+ function generateCreateTableFromParsed(table) {
294
+ const sql = [];
295
+ const columnDefs = [];
296
+ for (const col of table.columns) {
297
+ columnDefs.push(` ${generateParsedColumnDef(col)}`);
298
+ }
299
+ let createSQL = `CREATE TABLE "${table.name}" (\n${columnDefs.join(',\n')}\n)`;
300
+ if (table.isPartitioned && table.partitionType && table.partitionKey?.length) {
301
+ createSQL += ` PARTITION BY ${table.partitionType} (${table.partitionKey.join(', ')})`;
302
+ }
303
+ sql.push(createSQL + ';');
304
+ for (const idx of table.indexes) {
305
+ const isPKIndex = table.columns.some(c => c.isPrimaryKey && idx.columns.includes(c.name) && idx.columns.length === 1);
306
+ if (!isPKIndex) {
307
+ sql.push(generateCreateIndexFromParsed(table.name, idx));
308
+ }
309
+ }
310
+ return sql;
311
+ }
312
+ function generateParsedColumnDef(col) {
313
+ const parts = [`"${col.name}"`, col.type];
314
+ if (col.isPrimaryKey) {
315
+ parts.push('PRIMARY KEY');
316
+ }
317
+ if (!col.isNullable && !col.isPrimaryKey) {
318
+ parts.push('NOT NULL');
319
+ }
320
+ if (col.isUnique && !col.isPrimaryKey) {
321
+ parts.push('UNIQUE');
322
+ }
323
+ if (col.defaultValue !== undefined) {
324
+ parts.push(`DEFAULT ${col.defaultValue}`);
325
+ }
326
+ if (col.references) {
327
+ parts.push(`REFERENCES "${col.references.table}"("${col.references.column}")`);
328
+ if (col.references.onDelete) {
329
+ parts.push(`ON DELETE ${col.references.onDelete}`);
330
+ }
331
+ if (col.references.onUpdate) {
332
+ parts.push(`ON UPDATE ${col.references.onUpdate}`);
333
+ }
334
+ }
335
+ return parts.join(' ');
336
+ }
337
+ function generateCreateIndexFromParsed(tableName, idx) {
338
+ const unique = idx.isUnique ? 'UNIQUE ' : '';
339
+ const colList = idx.columns.map(c => `"${c}"`).join(', ');
340
+ const using = idx.method && idx.method !== 'btree' ? ` USING ${idx.method}` : '';
341
+ const where = idx.whereClause ? ` WHERE ${idx.whereClause}` : '';
342
+ return `CREATE ${unique}INDEX IF NOT EXISTS "${idx.name}" ON "${tableName}"${using} (${colList})${where};`;
343
+ }
26
344
  export function generateMigrationFile(diff, name, options = {}) {
27
345
  const { message, includeComments = true } = options;
28
346
  const { up, down } = generateMigration(diff, options);
@@ -183,7 +183,42 @@ export function getCommitHistory(limit = 20, projectRoot = process.cwd()) {
183
183
  }
184
184
  return history;
185
185
  }
186
- export function createCommit(schema, author, message, projectRoot = process.cwd()) {
186
+ export function buildTrackingMap(ast) {
187
+ const map = {
188
+ tables: {},
189
+ columns: {},
190
+ indexes: {},
191
+ enums: {},
192
+ functions: {},
193
+ };
194
+ for (const table of ast.tables) {
195
+ if (table.trackingId) {
196
+ map.tables[table.trackingId] = table.name;
197
+ }
198
+ for (const col of table.columns) {
199
+ if (col.trackingId) {
200
+ map.columns[col.trackingId] = `${table.name}.${col.name}`;
201
+ }
202
+ }
203
+ for (const idx of table.indexes) {
204
+ if (idx.trackingId) {
205
+ map.indexes[idx.trackingId] = idx.name;
206
+ }
207
+ }
208
+ }
209
+ for (const e of ast.enums) {
210
+ if (e.trackingId) {
211
+ map.enums[e.trackingId] = e.name;
212
+ }
213
+ }
214
+ for (const f of ast.functions) {
215
+ if (f.trackingId) {
216
+ map.functions[f.trackingId] = f.name;
217
+ }
218
+ }
219
+ return map;
220
+ }
221
+ export function createCommit(schema, author, message, projectRoot = process.cwd(), options) {
187
222
  const hash = generateHash(schema);
188
223
  const parentHash = getHead(projectRoot);
189
224
  const commit = {
@@ -205,9 +240,18 @@ export function createCommit(schema, author, message, projectRoot = process.cwd(
205
240
  triggers: schema.triggers?.length || 0,
206
241
  },
207
242
  };
243
+ if (options?.schemaAST) {
244
+ commit.schemaAST = options.schemaAST;
245
+ commit.trackingMap = buildTrackingMap(options.schemaAST);
246
+ }
247
+ if (options?.changes) {
248
+ commit.changes = options.changes;
249
+ }
208
250
  saveCommit(commit, projectRoot);
209
251
  setHead(hash, projectRoot);
210
- clearStaged(projectRoot);
252
+ if (!options?.skipClearStaged) {
253
+ clearStaged(projectRoot);
254
+ }
211
255
  return commit;
212
256
  }
213
257
  export function getStaged(projectRoot = process.cwd()) {
@@ -430,12 +474,61 @@ export async function ensureRemoteTable(connection) {
430
474
  message TEXT,
431
475
  schema_snapshot JSONB NOT NULL,
432
476
  stats JSONB,
433
- created_at TIMESTAMPTZ DEFAULT NOW()
477
+ created_at TIMESTAMPTZ DEFAULT NOW(),
478
+ applied_at TIMESTAMPTZ,
479
+ rolled_back_at TIMESTAMPTZ
434
480
  );
435
481
 
436
482
  CREATE INDEX IF NOT EXISTS idx_relq_commits_hash ON _relq_commits(hash);
437
483
  CREATE INDEX IF NOT EXISTS idx_relq_commits_created ON _relq_commits(created_at DESC);
438
484
  `);
485
+ await pool.query(`
486
+ DO $$
487
+ BEGIN
488
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = '_relq_commits' AND column_name = 'applied_at') THEN
489
+ ALTER TABLE _relq_commits ADD COLUMN applied_at TIMESTAMPTZ;
490
+ END IF;
491
+ IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = '_relq_commits' AND column_name = 'rolled_back_at') THEN
492
+ ALTER TABLE _relq_commits ADD COLUMN rolled_back_at TIMESTAMPTZ;
493
+ END IF;
494
+ END $$;
495
+ `);
496
+ }
497
+ finally {
498
+ await pool.end();
499
+ }
500
+ }
501
+ export async function markCommitAsApplied(connection, commitHash) {
502
+ const { Pool } = await import("../../addon/pg/index.js");
503
+ const pool = new Pool({
504
+ host: connection.host,
505
+ port: connection.port || 5432,
506
+ database: connection.database,
507
+ user: connection.user,
508
+ password: connection.password,
509
+ connectionString: connection.url,
510
+ ssl: connection.ssl,
511
+ });
512
+ try {
513
+ await pool.query(`UPDATE _relq_commits SET applied_at = NOW(), rolled_back_at = NULL WHERE hash = $1`, [commitHash]);
514
+ }
515
+ finally {
516
+ await pool.end();
517
+ }
518
+ }
519
+ export async function markCommitAsRolledBack(connection, commitHash) {
520
+ const { Pool } = await import("../../addon/pg/index.js");
521
+ const pool = new Pool({
522
+ host: connection.host,
523
+ port: connection.port || 5432,
524
+ database: connection.database,
525
+ user: connection.user,
526
+ password: connection.password,
527
+ connectionString: connection.url,
528
+ ssl: connection.ssl,
529
+ });
530
+ try {
531
+ await pool.query(`UPDATE _relq_commits SET rolled_back_at = NOW() WHERE hash = $1`, [commitHash]);
439
532
  }
440
533
  finally {
441
534
  await pool.end();