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.
Files changed (54) 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 +8 -25
  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/cli/utils/schema-validator.cjs +21 -1
  18. package/dist/cjs/schema-definition/column-types.cjs +63 -10
  19. package/dist/cjs/schema-definition/pg-enum.cjs +10 -0
  20. package/dist/cjs/schema-definition/pg-function.cjs +19 -0
  21. package/dist/cjs/schema-definition/pg-sequence.cjs +22 -1
  22. package/dist/cjs/schema-definition/pg-trigger.cjs +39 -0
  23. package/dist/cjs/schema-definition/pg-view.cjs +17 -0
  24. package/dist/cjs/schema-definition/sql-expressions.cjs +3 -0
  25. package/dist/cjs/schema-definition/table-definition.cjs +4 -0
  26. package/dist/config.d.ts +98 -0
  27. package/dist/esm/cli/commands/commit.js +83 -3
  28. package/dist/esm/cli/commands/import.js +1 -0
  29. package/dist/esm/cli/commands/pull.js +9 -26
  30. package/dist/esm/cli/commands/push.js +49 -9
  31. package/dist/esm/cli/commands/rollback.js +206 -85
  32. package/dist/esm/cli/commands/schema-ast.js +183 -0
  33. package/dist/esm/cli/index.js +6 -0
  34. package/dist/esm/cli/utils/ast-codegen.js +93 -3
  35. package/dist/esm/cli/utils/ast-transformer.js +12 -0
  36. package/dist/esm/cli/utils/change-tracker.js +134 -0
  37. package/dist/esm/cli/utils/commit-manager.js +51 -0
  38. package/dist/esm/cli/utils/migration-generator.js +318 -0
  39. package/dist/esm/cli/utils/repo-manager.js +96 -3
  40. package/dist/esm/cli/utils/schema-diff.js +389 -0
  41. package/dist/esm/cli/utils/schema-hash.js +4 -0
  42. package/dist/esm/cli/utils/schema-to-ast.js +447 -0
  43. package/dist/esm/cli/utils/schema-validator.js +21 -1
  44. package/dist/esm/schema-definition/column-types.js +63 -10
  45. package/dist/esm/schema-definition/pg-enum.js +10 -0
  46. package/dist/esm/schema-definition/pg-function.js +19 -0
  47. package/dist/esm/schema-definition/pg-sequence.js +22 -1
  48. package/dist/esm/schema-definition/pg-trigger.js +39 -0
  49. package/dist/esm/schema-definition/pg-view.js +17 -0
  50. package/dist/esm/schema-definition/sql-expressions.js +3 -0
  51. package/dist/esm/schema-definition/table-definition.js +4 -0
  52. package/dist/index.d.ts +98 -0
  53. package/dist/schema-builder.d.ts +223 -24
  54. 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
- if (!dryRun) {
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
- applyTrackingIdsToSnapshot(typescript, currentSchema);
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 applySQL = flags['apply'] === true;
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')} - no changes applied`);
168
+ console.log(`${cli_utils_1.colors.yellow('Dry run')} - showing changes that would be applied`);
169
169
  console.log('');
170
- console.log(`${cli_utils_1.colors.muted('Use')} ${cli_utils_1.colors.cyan('relq push --apply')} ${cli_utils_1.colors.muted('to execute.')}`);
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 (applySQL) {
184
- spinner.start('Applying SQL changes...');
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 && applySQL) {
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 (!applySQL) {
309
+ if (metadataOnly) {
270
310
  console.log('');
271
- console.log(`${cli_utils_1.colors.muted('Use')} ${cli_utils_1.colors.cyan('relq push --apply')} ${cli_utils_1.colors.muted('to execute SQL.')}`);
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
- function parseMigration(content) {
43
- const upMatch = content.match(/--\s*UP\s*\n([\s\S]*?)(?=--\s*DOWN|$)/i);
44
- const downMatch = content.match(/--\s*DOWN\s*\n([\s\S]*?)$/i);
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', `run ${cli_utils_1.colors.cyan('relq init')} to create a configuration file`);
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 migrationsDir = config.migrations?.directory || './migrations';
59
- const tableName = config.migrations?.tableName || '_relq_migrations';
60
- const count = parseInt(args[0]) || 1;
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
- const { Pool } = await Promise.resolve().then(() => __importStar(require("../../addon/pg/index.cjs")));
68
- const pool = new Pool({
69
- host: connection.host,
70
- port: connection.port || 5432,
71
- database: connection.database,
72
- user: connection.user,
73
- password: connection.password,
74
- connectionString: connection.url,
75
- });
76
- try {
77
- const result = await pool.query(`
78
- SELECT name FROM "${tableName}"
79
- ORDER BY id DESC
80
- LIMIT $1;
81
- `, [count]);
82
- if (result.rows.length === 0) {
83
- console.log('No migrations to rollback.');
84
- return;
85
- }
86
- const toRollback = result.rows.map(r => r.name);
87
- console.log('Migrations to rollback:');
88
- for (const name of toRollback) {
89
- console.log(` - ${name}`);
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
- if (!force && !dryRun) {
93
- const proceed = await (0, cli_utils_1.confirm)(`${cli_utils_1.colors.red('This will undo ' + toRollback.length + ' migration(s). Continue?')}`, false);
94
- if (!proceed) {
95
- console.log('Cancelled.');
96
- return;
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
- for (const name of toRollback) {
100
- const filePath = path.join(migrationsDir, name);
101
- if (!fs.existsSync(filePath)) {
102
- (0, cli_utils_1.warning)(`Migration file not found: ${name}`);
103
- continue;
104
- }
105
- const content = fs.readFileSync(filePath, 'utf-8');
106
- const { down } = parseMigration(content);
107
- if (!down) {
108
- (0, cli_utils_1.warning)(`No DOWN section in: ${name}`);
109
- continue;
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 (dryRun) {
112
- console.log(`[dry-run] Would rollback: ${name}`);
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
- else {
117
- console.log(`Rolling back: ${name}...`);
118
- const client = await pool.connect();
119
- try {
120
- await client.query('BEGIN');
121
- await client.query(down);
122
- await client.query(`DELETE FROM "${tableName}" WHERE name = $1`, [name]);
123
- await client.query('COMMIT');
124
- console.log(' Rolled back');
125
- }
126
- catch (error) {
127
- await client.query('ROLLBACK');
128
- throw error;
129
- }
130
- finally {
131
- client.release();
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
- if (!dryRun) {
136
- console.log('');
137
- console.log(`Rolled back ${toRollback.length} migration(s).`);
182
+ finally {
183
+ await client.end();
138
184
  }
139
185
  }
140
- finally {
141
- await pool.end();
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 (error) {
145
- (0, cli_utils_1.fatal)('Rollback failed', error instanceof Error ? error.message : String(error));
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;