relq 1.0.25 → 1.0.26
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/cjs/cli/commands/commit.cjs +80 -0
- package/dist/cjs/cli/commands/import.cjs +1 -0
- package/dist/cjs/cli/commands/pull.cjs +8 -25
- package/dist/cjs/cli/commands/push.cjs +48 -8
- package/dist/cjs/cli/commands/rollback.cjs +205 -84
- package/dist/cjs/cli/commands/schema-ast.cjs +219 -0
- package/dist/cjs/cli/index.cjs +6 -0
- package/dist/cjs/cli/utils/ast-codegen.cjs +95 -3
- package/dist/cjs/cli/utils/ast-transformer.cjs +12 -0
- package/dist/cjs/cli/utils/change-tracker.cjs +135 -0
- package/dist/cjs/cli/utils/commit-manager.cjs +54 -0
- package/dist/cjs/cli/utils/migration-generator.cjs +319 -0
- package/dist/cjs/cli/utils/repo-manager.cjs +99 -3
- package/dist/cjs/cli/utils/schema-diff.cjs +390 -0
- package/dist/cjs/cli/utils/schema-hash.cjs +4 -0
- package/dist/cjs/cli/utils/schema-to-ast.cjs +477 -0
- package/dist/cjs/schema-definition/column-types.cjs +50 -4
- package/dist/cjs/schema-definition/pg-enum.cjs +10 -0
- package/dist/cjs/schema-definition/pg-function.cjs +19 -0
- package/dist/cjs/schema-definition/pg-sequence.cjs +22 -1
- package/dist/cjs/schema-definition/pg-trigger.cjs +39 -0
- package/dist/cjs/schema-definition/pg-view.cjs +17 -0
- package/dist/cjs/schema-definition/sql-expressions.cjs +3 -0
- package/dist/cjs/schema-definition/table-definition.cjs +4 -0
- package/dist/config.d.ts +98 -0
- package/dist/esm/cli/commands/commit.js +83 -3
- package/dist/esm/cli/commands/import.js +1 -0
- package/dist/esm/cli/commands/pull.js +9 -26
- package/dist/esm/cli/commands/push.js +49 -9
- package/dist/esm/cli/commands/rollback.js +206 -85
- package/dist/esm/cli/commands/schema-ast.js +183 -0
- package/dist/esm/cli/index.js +6 -0
- package/dist/esm/cli/utils/ast-codegen.js +93 -3
- package/dist/esm/cli/utils/ast-transformer.js +12 -0
- package/dist/esm/cli/utils/change-tracker.js +134 -0
- package/dist/esm/cli/utils/commit-manager.js +51 -0
- package/dist/esm/cli/utils/migration-generator.js +318 -0
- package/dist/esm/cli/utils/repo-manager.js +96 -3
- package/dist/esm/cli/utils/schema-diff.js +389 -0
- package/dist/esm/cli/utils/schema-hash.js +4 -0
- package/dist/esm/cli/utils/schema-to-ast.js +447 -0
- package/dist/esm/schema-definition/column-types.js +50 -4
- package/dist/esm/schema-definition/pg-enum.js +10 -0
- package/dist/esm/schema-definition/pg-function.js +19 -0
- package/dist/esm/schema-definition/pg-sequence.js +22 -1
- package/dist/esm/schema-definition/pg-trigger.js +39 -0
- package/dist/esm/schema-definition/pg-view.js +17 -0
- package/dist/esm/schema-definition/sql-expressions.js +3 -0
- package/dist/esm/schema-definition/table-definition.js +4 -0
- package/dist/index.d.ts +98 -0
- package/dist/schema-builder.d.ts +223 -24
- 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
|
|
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
|
-
|
|
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();
|