relq 1.0.24 → 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/cli/utils/schema-validator.cjs +21 -1
- package/dist/cjs/schema-definition/column-types.cjs +63 -10
- 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/cli/utils/schema-validator.js +21 -1
- package/dist/esm/schema-definition/column-types.js +63 -10
- 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
|
@@ -40,8 +40,11 @@ const repo_manager_1 = require("../utils/repo-manager.cjs");
|
|
|
40
40
|
const config_1 = require("../../config/config.cjs");
|
|
41
41
|
const config_loader_1 = require("../utils/config-loader.cjs");
|
|
42
42
|
const change_tracker_1 = require("../utils/change-tracker.cjs");
|
|
43
|
+
const commit_manager_1 = require("../utils/commit-manager.cjs");
|
|
44
|
+
const schema_to_ast_1 = require("../utils/schema-to-ast.cjs");
|
|
43
45
|
const fs = __importStar(require("fs"));
|
|
44
46
|
const path = __importStar(require("path"));
|
|
47
|
+
const jiti_1 = require("jiti");
|
|
45
48
|
async function commitCommand(context) {
|
|
46
49
|
const { config, flags, args, projectRoot } = context;
|
|
47
50
|
const author = config?.author || 'Developer <dev@example.com>';
|
|
@@ -52,6 +55,9 @@ async function commitCommand(context) {
|
|
|
52
55
|
const schemaPath = path.resolve(projectRoot, (0, config_loader_1.getSchemaPath)(config ?? undefined));
|
|
53
56
|
const { requireValidSchema } = await Promise.resolve().then(() => __importStar(require("../utils/config-loader.cjs")));
|
|
54
57
|
await requireValidSchema(schemaPath, flags);
|
|
58
|
+
if (flags['from-schema']) {
|
|
59
|
+
return commitFromSchema(context, schemaPath, author);
|
|
60
|
+
}
|
|
55
61
|
const staged = (0, repo_manager_1.getStagedChanges)(projectRoot);
|
|
56
62
|
if (staged.length === 0) {
|
|
57
63
|
console.log('nothing to commit, working tree clean');
|
|
@@ -76,6 +82,8 @@ async function commitCommand(context) {
|
|
|
76
82
|
}
|
|
77
83
|
const sortedChanges = (0, change_tracker_1.sortChangesByDependency)(staged);
|
|
78
84
|
const sql = (0, change_tracker_1.generateCombinedSQL)(sortedChanges);
|
|
85
|
+
const downSQL = (0, change_tracker_1.generateDownSQL)(sortedChanges);
|
|
86
|
+
const snapshot = (0, repo_manager_1.loadSnapshot)(projectRoot);
|
|
79
87
|
const creates = staged.filter(c => c.type === 'CREATE').length;
|
|
80
88
|
const alters = staged.filter(c => c.type === 'ALTER').length;
|
|
81
89
|
const drops = staged.filter(c => c.type === 'DROP').length;
|
|
@@ -96,6 +104,8 @@ async function commitCommand(context) {
|
|
|
96
104
|
timestamp: new Date().toISOString(),
|
|
97
105
|
changes: sortedChanges,
|
|
98
106
|
sql,
|
|
107
|
+
downSQL,
|
|
108
|
+
schema: snapshot,
|
|
99
109
|
snapshotHash: hash.substring(0, 12),
|
|
100
110
|
stats: {
|
|
101
111
|
creates,
|
|
@@ -149,4 +159,74 @@ async function commitCommand(context) {
|
|
|
149
159
|
(0, cli_utils_1.hint)("run 'relq export' to export as SQL file");
|
|
150
160
|
console.log('');
|
|
151
161
|
}
|
|
162
|
+
async function commitFromSchema(context, schemaPath, author) {
|
|
163
|
+
const { config, flags, args, projectRoot } = context;
|
|
164
|
+
const spinner = (0, cli_utils_1.createSpinner)();
|
|
165
|
+
let message = flags['m'] || flags['message'];
|
|
166
|
+
if (!message) {
|
|
167
|
+
if (args.length > 0) {
|
|
168
|
+
message = args.join(' ');
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
(0, cli_utils_1.fatal)('commit message required', "usage: relq commit --from-schema -m '<message>'");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
spinner.start('Loading schema file');
|
|
175
|
+
let schemaModule;
|
|
176
|
+
try {
|
|
177
|
+
const jiti = (0, jiti_1.createJiti)(path.dirname(schemaPath), { interopDefault: true });
|
|
178
|
+
const module = await jiti.import(schemaPath);
|
|
179
|
+
if (module && module.default && typeof module.default === 'object') {
|
|
180
|
+
schemaModule = module.default;
|
|
181
|
+
}
|
|
182
|
+
else if (module && typeof module === 'object') {
|
|
183
|
+
schemaModule = module;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
throw new Error('Schema file must export an object with table/enum definitions');
|
|
187
|
+
}
|
|
188
|
+
spinner.succeed('Loaded schema file');
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
spinner.fail('Failed to load schema');
|
|
192
|
+
(0, cli_utils_1.fatal)(`Could not load schema: ${err instanceof Error ? err.message : String(err)}`);
|
|
193
|
+
}
|
|
194
|
+
spinner.start('Converting schema to AST');
|
|
195
|
+
const ast = (0, schema_to_ast_1.schemaToAST)(schemaModule);
|
|
196
|
+
spinner.succeed('Converted schema to AST');
|
|
197
|
+
const schemaHash = (0, commit_manager_1.generateASTHash)(ast);
|
|
198
|
+
spinner.start('Creating commit');
|
|
199
|
+
try {
|
|
200
|
+
const commit = (0, commit_manager_1.createCommitFromSchema)(schemaModule, author, message, config?.commitLimit ?? 1000, projectRoot);
|
|
201
|
+
spinner.succeed('Created commit');
|
|
202
|
+
const tableCount = ast.tables.length;
|
|
203
|
+
const enumCount = ast.enums.length;
|
|
204
|
+
const functionCount = ast.functions.length;
|
|
205
|
+
const viewCount = ast.views.length;
|
|
206
|
+
const triggerCount = ast.triggers.length;
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(`[${(0, repo_manager_1.shortHash)(commit.hash)}] ${message}`);
|
|
209
|
+
const statsParts = [];
|
|
210
|
+
if (tableCount > 0)
|
|
211
|
+
statsParts.push(`${tableCount} table(s)`);
|
|
212
|
+
if (enumCount > 0)
|
|
213
|
+
statsParts.push(`${enumCount} enum(s)`);
|
|
214
|
+
if (functionCount > 0)
|
|
215
|
+
statsParts.push(`${functionCount} function(s)`);
|
|
216
|
+
if (viewCount > 0)
|
|
217
|
+
statsParts.push(`${viewCount} view(s)`);
|
|
218
|
+
if (triggerCount > 0)
|
|
219
|
+
statsParts.push(`${triggerCount} trigger(s)`);
|
|
220
|
+
console.log(` ${statsParts.length > 0 ? statsParts.join(', ') : 'empty schema'}`);
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log(cli_utils_1.colors.muted(`Schema hash: ${schemaHash.substring(0, 12)}`));
|
|
223
|
+
console.log('');
|
|
224
|
+
(0, cli_utils_1.hint)("run 'relq push' to apply changes to database");
|
|
225
|
+
(0, cli_utils_1.hint)("run 'relq log' to view commit history");
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
spinner.fail('Failed to create commit');
|
|
229
|
+
(0, cli_utils_1.fatal)(`Could not create commit: ${err instanceof Error ? err.message : String(err)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
152
232
|
exports.default = commitCommand;
|
|
@@ -211,6 +211,7 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
|
|
|
211
211
|
tables: filteredTables,
|
|
212
212
|
enums: filteredEnums,
|
|
213
213
|
domains: filteredDomains,
|
|
214
|
+
compositeTypes: parsedSchema.compositeTypes || [],
|
|
214
215
|
sequences: filteredSequences,
|
|
215
216
|
views: parsedSchema.views,
|
|
216
217
|
functions: parsedSchema.functions,
|
|
@@ -413,13 +413,18 @@ async function pullCommand(context) {
|
|
|
413
413
|
console.log(` ${cli_utils_1.colors.red(`-${removed.length}`)} tables removed`);
|
|
414
414
|
}
|
|
415
415
|
console.log('');
|
|
416
|
-
|
|
416
|
+
const noAutoMerge = flags['no-auto-merge'] === true;
|
|
417
|
+
if (!dryRun && noAutoMerge) {
|
|
417
418
|
const proceed = await (0, cli_utils_1.confirm)(`${cli_utils_1.colors.bold('Pull these changes?')}`, true);
|
|
418
419
|
if (!proceed) {
|
|
419
420
|
(0, cli_utils_1.fatal)('Operation cancelled by user');
|
|
420
421
|
}
|
|
421
422
|
console.log('');
|
|
422
423
|
}
|
|
424
|
+
else if (!dryRun) {
|
|
425
|
+
console.log(`${cli_utils_1.colors.green('Auto-merging')} (no conflicts detected)`);
|
|
426
|
+
console.log('');
|
|
427
|
+
}
|
|
423
428
|
}
|
|
424
429
|
else if (schemaExists && !force) {
|
|
425
430
|
(0, cli_utils_1.warning)('Local schema exists but not tracked');
|
|
@@ -481,6 +486,7 @@ async function pullCommand(context) {
|
|
|
481
486
|
}
|
|
482
487
|
spinner.start('Generating TypeScript schema...');
|
|
483
488
|
const parsedSchema = await (0, ast_transformer_1.introspectedToParsedSchema)(dbSchema);
|
|
489
|
+
(0, ast_codegen_1.assignTrackingIds)(parsedSchema);
|
|
484
490
|
const typescript = (0, ast_codegen_1.generateTypeScriptFromAST)(parsedSchema, {
|
|
485
491
|
camelCase: config.generate?.camelCase ?? true,
|
|
486
492
|
importPath: 'relq/schema-builder',
|
|
@@ -586,7 +592,7 @@ async function pullCommand(context) {
|
|
|
586
592
|
triggers: filteredTriggers || [],
|
|
587
593
|
};
|
|
588
594
|
const schemaChanges = (0, schema_comparator_1.compareSchemas)(beforeSchema, afterSchema);
|
|
589
|
-
|
|
595
|
+
(0, ast_codegen_1.copyTrackingIdsToNormalized)(parsedSchema, currentSchema);
|
|
590
596
|
(0, repo_manager_1.saveSnapshot)(currentSchema, projectRoot);
|
|
591
597
|
const duration = Date.now() - startTime;
|
|
592
598
|
if (noCommit) {
|
|
@@ -692,26 +698,3 @@ function detectObjectConflicts(local, remote) {
|
|
|
692
698
|
}
|
|
693
699
|
return conflicts;
|
|
694
700
|
}
|
|
695
|
-
function applyTrackingIdsToSnapshot(typescript, snapshot) {
|
|
696
|
-
for (const table of snapshot.tables) {
|
|
697
|
-
const tablePattern = new RegExp(`defineTable\\s*\\(\\s*['"]${table.name}['"]\\s*,\\s*\\{[^}]+\\}\\s*,\\s*\\{[^}]*\\$trackingId:\\s*['"]([^'"]+)['"]`, 's');
|
|
698
|
-
const tableMatch = typescript.match(tablePattern);
|
|
699
|
-
if (tableMatch) {
|
|
700
|
-
table.trackingId = tableMatch[1];
|
|
701
|
-
}
|
|
702
|
-
for (const col of table.columns) {
|
|
703
|
-
const colPattern = new RegExp(`(?:${col.tsName}|${col.name}):\\s*\\w+\\([^)]*\\)[^\\n]*\\.\\\$id\\(['"]([^'"]+)['"]\\)`);
|
|
704
|
-
const colMatch = typescript.match(colPattern);
|
|
705
|
-
if (colMatch) {
|
|
706
|
-
col.trackingId = colMatch[1];
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
for (const idx of table.indexes) {
|
|
710
|
-
const idxPattern = new RegExp(`index\\s*\\(\\s*['"]${idx.name}['"]\\s*\\)[^\\n]*\\.\\\$id\\(['"]([^'"]+)['"]\\)`);
|
|
711
|
-
const idxMatch = typescript.match(idxPattern);
|
|
712
|
-
if (idxMatch) {
|
|
713
|
-
idx.trackingId = idxMatch[1];
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
@@ -52,7 +52,7 @@ async function pushCommand(context) {
|
|
|
52
52
|
const { projectRoot } = context;
|
|
53
53
|
const force = flags['force'] === true;
|
|
54
54
|
const dryRun = flags['dry-run'] === true;
|
|
55
|
-
const
|
|
55
|
+
const metadataOnly = flags['metadata-only'] === true;
|
|
56
56
|
const noVerify = flags['no-verify'] === true;
|
|
57
57
|
const skipPrompt = flags['yes'] === true || flags['y'] === true;
|
|
58
58
|
const includeFunctions = config.includeFunctions ?? false;
|
|
@@ -165,9 +165,46 @@ async function pushCommand(context) {
|
|
|
165
165
|
console.log('');
|
|
166
166
|
}
|
|
167
167
|
if (dryRun) {
|
|
168
|
-
console.log(`${cli_utils_1.colors.yellow('Dry run')} -
|
|
168
|
+
console.log(`${cli_utils_1.colors.yellow('Dry run')} - showing changes that would be applied`);
|
|
169
169
|
console.log('');
|
|
170
|
-
|
|
170
|
+
if (hasObjectsToDrop && force) {
|
|
171
|
+
console.log(`${cli_utils_1.colors.red('DROP statements:')}`);
|
|
172
|
+
for (const obj of analysis.objectsToDrop.slice(0, 5)) {
|
|
173
|
+
console.log(` ${generateDropSQL(obj)}`);
|
|
174
|
+
}
|
|
175
|
+
if (analysis.objectsToDrop.length > 5) {
|
|
176
|
+
console.log(` ${cli_utils_1.colors.muted(`... and ${analysis.objectsToDrop.length - 5} more`)}`);
|
|
177
|
+
}
|
|
178
|
+
console.log('');
|
|
179
|
+
}
|
|
180
|
+
const commitsToProcess = [...toPush].reverse();
|
|
181
|
+
let totalStatements = 0;
|
|
182
|
+
for (const commit of commitsToProcess) {
|
|
183
|
+
const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
|
|
184
|
+
if (fs.existsSync(commitPath)) {
|
|
185
|
+
const enhancedCommit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
186
|
+
if (enhancedCommit.sql && enhancedCommit.sql.trim()) {
|
|
187
|
+
const statements = enhancedCommit.sql.split(';').filter(s => s.trim());
|
|
188
|
+
totalStatements += statements.length;
|
|
189
|
+
console.log(`${cli_utils_1.colors.cyan(`Commit ${(0, repo_manager_1.shortHash)(commit.hash)}:`)} ${commit.message}`);
|
|
190
|
+
for (const stmt of statements.slice(0, 3)) {
|
|
191
|
+
console.log(` ${stmt.trim().substring(0, 80)}${stmt.trim().length > 80 ? '...' : ''};`);
|
|
192
|
+
}
|
|
193
|
+
if (statements.length > 3) {
|
|
194
|
+
console.log(` ${cli_utils_1.colors.muted(`... and ${statements.length - 3} more statements`)}`);
|
|
195
|
+
}
|
|
196
|
+
console.log('');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (totalStatements === 0 && !hasObjectsToDrop) {
|
|
201
|
+
console.log(`${cli_utils_1.colors.muted('No SQL changes to apply')}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(`${cli_utils_1.colors.muted('Total:')} ${totalStatements + (hasObjectsToDrop ? analysis.objectsToDrop.length : 0)} statements`);
|
|
205
|
+
}
|
|
206
|
+
console.log('');
|
|
207
|
+
console.log(`${cli_utils_1.colors.muted('Remove')} ${cli_utils_1.colors.cyan('--dry-run')} ${cli_utils_1.colors.muted('to execute these changes.')}`);
|
|
171
208
|
console.log('');
|
|
172
209
|
return;
|
|
173
210
|
}
|
|
@@ -180,8 +217,8 @@ async function pushCommand(context) {
|
|
|
180
217
|
}
|
|
181
218
|
spinner.succeed(`Pushed ${toPush.length} commit(s) to ${(0, repo_manager_1.getConnectionLabel)(connection)}`);
|
|
182
219
|
}
|
|
183
|
-
if (
|
|
184
|
-
spinner.start('Applying
|
|
220
|
+
if (!metadataOnly && !dryRun) {
|
|
221
|
+
spinner.start('Applying schema changes...');
|
|
185
222
|
const pg = await Promise.resolve().then(() => __importStar(require("../../addon/pg/index.cjs")));
|
|
186
223
|
const client = new pg.Client({
|
|
187
224
|
host: connection.host,
|
|
@@ -218,6 +255,9 @@ async function pushCommand(context) {
|
|
|
218
255
|
}
|
|
219
256
|
await client.query('COMMIT');
|
|
220
257
|
spinner.succeed(`Applied ${statementsRun} statement(s) from ${sqlExecuted} commit(s)`);
|
|
258
|
+
for (const commit of commitsToProcess) {
|
|
259
|
+
await (0, repo_manager_1.markCommitAsApplied)(connection, commit.hash);
|
|
260
|
+
}
|
|
221
261
|
let hasRenameOperations = false;
|
|
222
262
|
for (const commit of commitsToProcess) {
|
|
223
263
|
const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
|
|
@@ -262,13 +302,13 @@ async function pushCommand(context) {
|
|
|
262
302
|
const oldHash = remoteHead ? (0, repo_manager_1.shortHash)(remoteHead) : '(none)';
|
|
263
303
|
const newHash = (0, repo_manager_1.shortHash)(localHead);
|
|
264
304
|
console.log(` ${oldHash}..${newHash} ${cli_utils_1.colors.muted('main -> main')}`);
|
|
265
|
-
if (hasObjectsToDrop && force &&
|
|
305
|
+
if (hasObjectsToDrop && force && !metadataOnly && !dryRun) {
|
|
266
306
|
console.log('');
|
|
267
307
|
(0, cli_utils_1.warning)(`Dropped ${analysis.objectsToDrop.length} object(s) from remote`);
|
|
268
308
|
}
|
|
269
|
-
if (
|
|
309
|
+
if (metadataOnly) {
|
|
270
310
|
console.log('');
|
|
271
|
-
console.log(`${cli_utils_1.colors.muted('
|
|
311
|
+
console.log(`${cli_utils_1.colors.muted('Metadata only - SQL not executed. Remove')} ${cli_utils_1.colors.cyan('--metadata-only')} ${cli_utils_1.colors.muted('to apply changes.')}`);
|
|
272
312
|
}
|
|
273
313
|
console.log('');
|
|
274
314
|
}
|
|
@@ -39,109 +39,230 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const config_loader_1 = require("../utils/config-loader.cjs");
|
|
40
40
|
const env_loader_1 = require("../utils/env-loader.cjs");
|
|
41
41
|
const cli_utils_1 = require("../utils/cli-utils.cjs");
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
up: upMatch?.[1]?.trim() || '',
|
|
47
|
-
down: downMatch?.[1]?.trim() || '',
|
|
48
|
-
};
|
|
49
|
-
}
|
|
42
|
+
const repo_manager_1 = require("../utils/repo-manager.cjs");
|
|
43
|
+
const schema_diff_1 = require("../utils/schema-diff.cjs");
|
|
44
|
+
const migration_generator_1 = require("../utils/migration-generator.cjs");
|
|
50
45
|
async function rollbackCommand(context) {
|
|
51
46
|
const { config, args, flags } = context;
|
|
52
47
|
if (!config) {
|
|
53
|
-
(0, cli_utils_1.fatal)('No configuration found', `
|
|
54
|
-
return;
|
|
48
|
+
(0, cli_utils_1.fatal)('No configuration found', `Run ${cli_utils_1.colors.cyan('relq init')} to create one.`);
|
|
55
49
|
}
|
|
56
50
|
await (0, config_loader_1.requireValidConfig)(config, { calledFrom: 'rollback' });
|
|
57
51
|
const connection = config.connection;
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const dryRun = flags['dry-run'] === true;
|
|
62
|
-
const force = flags['force'] === true;
|
|
63
|
-
console.log(`Rolling back ${count} migration(s)...`);
|
|
64
|
-
console.log(` Connection: ${(0, env_loader_1.getConnectionDescription)(connection)}`);
|
|
52
|
+
const { projectRoot } = context;
|
|
53
|
+
const preview = flags['preview'] === true || flags['dry-run'] === true;
|
|
54
|
+
const skipPrompt = flags['yes'] === true || flags['y'] === true;
|
|
65
55
|
console.log('');
|
|
56
|
+
if (!(0, repo_manager_1.isInitialized)(projectRoot)) {
|
|
57
|
+
(0, cli_utils_1.fatal)('not a relq repository (or any parent directories): .relq', `Run ${cli_utils_1.colors.cyan('relq init')} to initialize.`);
|
|
58
|
+
}
|
|
59
|
+
const localHead = (0, repo_manager_1.getHead)(projectRoot);
|
|
60
|
+
if (!localHead) {
|
|
61
|
+
(0, cli_utils_1.fatal)('no commits to rollback', 'Repository has no commits.');
|
|
62
|
+
}
|
|
63
|
+
const targetRef = args[0] || 'HEAD~1';
|
|
64
|
+
const target = resolveTarget(projectRoot, targetRef, localHead);
|
|
65
|
+
if (!target) {
|
|
66
|
+
(0, cli_utils_1.fatal)(`invalid rollback target: ${targetRef}`, 'Use a commit hash or HEAD~N notation.');
|
|
67
|
+
}
|
|
68
|
+
if (target.commitsToRollback.length === 0) {
|
|
69
|
+
console.log('Nothing to rollback - already at target commit.');
|
|
70
|
+
console.log('');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const spinner = (0, cli_utils_1.createSpinner)();
|
|
66
74
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
75
|
+
console.log(`Rolling back to ${cli_utils_1.colors.yellow((0, repo_manager_1.shortHash)(target.hash))}`);
|
|
76
|
+
console.log(`${cli_utils_1.colors.muted(target.message)}`);
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(`${cli_utils_1.colors.red('Commits to rollback:')} ${target.commitsToRollback.length}`);
|
|
79
|
+
for (const commit of target.commitsToRollback.slice(0, 5)) {
|
|
80
|
+
console.log(` ${cli_utils_1.colors.red('↩')} ${(0, repo_manager_1.shortHash)(commit.hash)} ${commit.message}`);
|
|
81
|
+
}
|
|
82
|
+
if (target.commitsToRollback.length > 5) {
|
|
83
|
+
console.log(` ${cli_utils_1.colors.muted(`... and ${target.commitsToRollback.length - 5} more`)}`);
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
const currentCommitPath = path.join(projectRoot, '.relq', 'commits', `${localHead}.json`);
|
|
87
|
+
const targetCommitPath = path.join(projectRoot, '.relq', 'commits', `${target.hash}.json`);
|
|
88
|
+
if (!fs.existsSync(currentCommitPath)) {
|
|
89
|
+
(0, cli_utils_1.fatal)(`current commit not found: ${localHead}`);
|
|
90
|
+
}
|
|
91
|
+
if (!fs.existsSync(targetCommitPath)) {
|
|
92
|
+
(0, cli_utils_1.fatal)(`target commit not found: ${target.hash}`);
|
|
93
|
+
}
|
|
94
|
+
const currentCommit = JSON.parse(fs.readFileSync(currentCommitPath, 'utf-8'));
|
|
95
|
+
const targetCommit = JSON.parse(fs.readFileSync(targetCommitPath, 'utf-8'));
|
|
96
|
+
let rollbackSQL = [];
|
|
97
|
+
if (currentCommit.schemaAST && targetCommit.schemaAST) {
|
|
98
|
+
const comparison = (0, schema_diff_1.compareSchemas)(currentCommit.schemaAST, targetCommit.schemaAST);
|
|
99
|
+
const migration = (0, migration_generator_1.generateMigrationFromComparison)(comparison);
|
|
100
|
+
rollbackSQL = migration.down;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
(0, cli_utils_1.warning)('No schema AST found - using commit SQL reversal (may be incomplete)');
|
|
91
104
|
console.log('');
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
for (const commit of target.commitsToRollback) {
|
|
106
|
+
const commitPath = path.join(projectRoot, '.relq', 'commits', `${commit.hash}.json`);
|
|
107
|
+
if (fs.existsSync(commitPath)) {
|
|
108
|
+
const enhancedCommit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
109
|
+
if (enhancedCommit.downSQL) {
|
|
110
|
+
rollbackSQL.push(enhancedCommit.downSQL);
|
|
111
|
+
}
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
}
|
|
115
|
+
if (rollbackSQL.length === 0) {
|
|
116
|
+
(0, cli_utils_1.warning)('No rollback SQL generated - manual intervention may be required');
|
|
117
|
+
console.log('');
|
|
118
|
+
}
|
|
119
|
+
if (preview) {
|
|
120
|
+
console.log(`${cli_utils_1.colors.yellow('Preview')} - showing rollback SQL`);
|
|
121
|
+
console.log('');
|
|
122
|
+
if (rollbackSQL.length > 0) {
|
|
123
|
+
for (const sql of rollbackSQL.slice(0, 10)) {
|
|
124
|
+
console.log(` ${sql.substring(0, 100)}${sql.length > 100 ? '...' : ''}`);
|
|
110
125
|
}
|
|
111
|
-
if (
|
|
112
|
-
console.log(`
|
|
113
|
-
console.log(down);
|
|
114
|
-
console.log('');
|
|
126
|
+
if (rollbackSQL.length > 10) {
|
|
127
|
+
console.log(` ${cli_utils_1.colors.muted(`... and ${rollbackSQL.length - 10} more statements`)}`);
|
|
115
128
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(` ${cli_utils_1.colors.muted('(no SQL statements)')}`);
|
|
132
|
+
}
|
|
133
|
+
console.log('');
|
|
134
|
+
console.log(`${cli_utils_1.colors.muted('Remove')} ${cli_utils_1.colors.cyan('--preview')} ${cli_utils_1.colors.muted('to execute rollback.')}`);
|
|
135
|
+
console.log('');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!skipPrompt) {
|
|
139
|
+
(0, cli_utils_1.warning)('This will modify your database!');
|
|
140
|
+
console.log('');
|
|
141
|
+
const confirmed = await (0, cli_utils_1.confirm)(`Rollback ${target.commitsToRollback.length} commit(s)?`, false);
|
|
142
|
+
if (!confirmed) {
|
|
143
|
+
(0, cli_utils_1.fatal)('Rollback cancelled by user');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
spinner.start('Connecting to remote...');
|
|
147
|
+
await (0, repo_manager_1.ensureRemoteTable)(connection);
|
|
148
|
+
spinner.succeed(`Connected to ${cli_utils_1.colors.cyan((0, env_loader_1.getConnectionDescription)(connection))}`);
|
|
149
|
+
if (rollbackSQL.length > 0) {
|
|
150
|
+
spinner.start('Executing rollback...');
|
|
151
|
+
const pg = await Promise.resolve().then(() => __importStar(require("../../addon/pg/index.cjs")));
|
|
152
|
+
const client = new pg.Client({
|
|
153
|
+
host: connection.host,
|
|
154
|
+
port: connection.port,
|
|
155
|
+
database: connection.database,
|
|
156
|
+
user: connection.user,
|
|
157
|
+
password: connection.password,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
await client.connect();
|
|
161
|
+
await client.query('BEGIN');
|
|
162
|
+
let statementsRun = 0;
|
|
163
|
+
for (const sql of rollbackSQL) {
|
|
164
|
+
if (sql.trim()) {
|
|
165
|
+
await client.query(sql);
|
|
166
|
+
statementsRun++;
|
|
132
167
|
}
|
|
133
168
|
}
|
|
169
|
+
await client.query('COMMIT');
|
|
170
|
+
spinner.succeed(`Executed ${statementsRun} rollback statement(s)`);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
try {
|
|
174
|
+
await client.query('ROLLBACK');
|
|
175
|
+
spinner.fail('Rollback failed - transaction rolled back');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
spinner.fail('Rollback failed');
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
134
181
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
console.log(`Rolled back ${toRollback.length} migration(s).`);
|
|
182
|
+
finally {
|
|
183
|
+
await client.end();
|
|
138
184
|
}
|
|
139
185
|
}
|
|
140
|
-
|
|
141
|
-
|
|
186
|
+
spinner.start('Updating commit status...');
|
|
187
|
+
for (const commit of target.commitsToRollback) {
|
|
188
|
+
await (0, repo_manager_1.markCommitAsRolledBack)(connection, commit.hash);
|
|
189
|
+
}
|
|
190
|
+
spinner.succeed('Marked commits as rolled back');
|
|
191
|
+
const headPath = path.join(projectRoot, '.relq', 'HEAD');
|
|
192
|
+
fs.writeFileSync(headPath, target.hash);
|
|
193
|
+
if (targetCommit.schema) {
|
|
194
|
+
const snapshotPath = path.join(projectRoot, '.relq', 'snapshot.json');
|
|
195
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(targetCommit.schema, null, 2));
|
|
142
196
|
}
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(`${cli_utils_1.colors.green('Rollback complete')}`);
|
|
199
|
+
console.log(` ${(0, repo_manager_1.shortHash)(localHead)} → ${(0, repo_manager_1.shortHash)(target.hash)}`);
|
|
200
|
+
console.log('');
|
|
143
201
|
}
|
|
144
|
-
catch (
|
|
145
|
-
|
|
202
|
+
catch (err) {
|
|
203
|
+
spinner.fail('Rollback failed');
|
|
204
|
+
(0, cli_utils_1.fatal)(err instanceof Error ? err.message : String(err));
|
|
146
205
|
}
|
|
147
206
|
}
|
|
207
|
+
function resolveTarget(projectRoot, ref, currentHead) {
|
|
208
|
+
const commits = (0, repo_manager_1.getAllCommits)(projectRoot);
|
|
209
|
+
const commitsByHash = new Map(commits.map(c => [c.hash, c]));
|
|
210
|
+
if (ref.startsWith('HEAD~')) {
|
|
211
|
+
const n = parseInt(ref.slice(5), 10);
|
|
212
|
+
if (isNaN(n) || n < 1)
|
|
213
|
+
return null;
|
|
214
|
+
let current = currentHead;
|
|
215
|
+
const commitsToRollback = [];
|
|
216
|
+
for (let i = 0; i < n; i++) {
|
|
217
|
+
const commitPath = path.join(projectRoot, '.relq', 'commits', `${current}.json`);
|
|
218
|
+
if (!fs.existsSync(commitPath))
|
|
219
|
+
return null;
|
|
220
|
+
const commit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
221
|
+
commitsToRollback.push(commit);
|
|
222
|
+
if (!commit.parentHash) {
|
|
223
|
+
if (i < n - 1) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
hash: commit.hash,
|
|
228
|
+
message: '(initial state)',
|
|
229
|
+
commitsToRollback: commitsToRollback.slice(0, -1),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
current = commit.parentHash;
|
|
233
|
+
}
|
|
234
|
+
const targetPath = path.join(projectRoot, '.relq', 'commits', `${current}.json`);
|
|
235
|
+
if (!fs.existsSync(targetPath))
|
|
236
|
+
return null;
|
|
237
|
+
const targetCommit = JSON.parse(fs.readFileSync(targetPath, 'utf-8'));
|
|
238
|
+
return {
|
|
239
|
+
hash: current,
|
|
240
|
+
message: targetCommit.message,
|
|
241
|
+
commitsToRollback,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const normalizedRef = ref.length < 40 ? commits.find(c => c.hash.startsWith(ref))?.hash : ref;
|
|
245
|
+
if (!normalizedRef || !commitsByHash.has(normalizedRef)) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const commitsToRollback = [];
|
|
249
|
+
let current = currentHead;
|
|
250
|
+
while (current && current !== normalizedRef) {
|
|
251
|
+
const commitPath = path.join(projectRoot, '.relq', 'commits', `${current}.json`);
|
|
252
|
+
if (!fs.existsSync(commitPath))
|
|
253
|
+
break;
|
|
254
|
+
const commit = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
|
|
255
|
+
commitsToRollback.push(commit);
|
|
256
|
+
current = commit.parentHash || '';
|
|
257
|
+
}
|
|
258
|
+
if (current !== normalizedRef) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const targetCommit = commitsByHash.get(normalizedRef);
|
|
262
|
+
return {
|
|
263
|
+
hash: normalizedRef,
|
|
264
|
+
message: targetCommit?.message || '',
|
|
265
|
+
commitsToRollback,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
exports.default = rollbackCommand;
|