/** * Migrate Dry Run Command * * Preview migrations that would be applied without executing them. * Shows the SQL that would be run and affected tables. * * @module cli/commands/migrate-dry-run */ import { resolve } from 'node:path' import { existsSync } from 'node:fs' import type { MigrateCLIOptions } from '../types.js' import { formatSuccess, formatError, formatInfo, formatWarning, formatDim, } from '../formatting.js' import { loadMigrationsFromDirectory, createQueryExecutor } from '../utils.js' /** * Migrate dry-run command */ export async function runMigrateDryRun(options: MigrateCLIOptions): Promise { const url = options.url || process.env['DATABASE_URL'] if (!url) { console.error(formatError('Database URL is required')) console.error(formatInfo('Provide --url or set DATABASE_URL environment variable')) process.exit(1) } // Load migrations from directory const migrationsDir = resolve(process.cwd(), options.migrationsDir || './migrations') if (!existsSync(migrationsDir)) { console.error(formatError(`Migrations directory not found: ${migrationsDir}`)) console.error(formatInfo('Create a migrations directory or specify one with --migrations-dir')) process.exit(1) } console.log(formatInfo(`Loading migrations from: ${migrationsDir}`)) try { const migrations = loadMigrationsFromDirectory(migrationsDir) if (migrations.length === 0) { console.log(formatInfo('No migrations found')) return } console.log(formatInfo(`Found ${migrations.length} migration(s)`)) // Import migration system dynamically const { createMigrationRegistry, createMigrationRunner } = await import('@dotdo/postgres/migrations') const { createMigrationValidator } = await import('@dotdo/postgres/migrations/validator') // Create registry - cast MigrationDefinition to Migration (structurally compatible) const registry = createMigrationRegistry({ migrations: migrations as import('@dotdo/postgres/migrations').Migration[], validateOnCreate: false, allowVersionGaps: true, }) // Connect to database console.log(formatInfo('Connecting to database...')) const executor = await createQueryExecutor(url) // Create runner - use conditional spreading for optional properties const runner = createMigrationRunner(executor, registry, { ...(options.verbose ? { debug: options.verbose } : {}), }) // Get pending migrations const schemaVersion = await runner.getSchemaVersion() const comparison = await runner.compareVersion() // Output results if (options.json) { const pendingMigrations = migrations.filter((m) => comparison.missingVersions.includes(m.version) ) console.log(JSON.stringify({ currentVersion: schemaVersion.currentVersion, targetVersion: registry.getLatestVersion(), pendingCount: comparison.pendingCount, needsMigration: comparison.needsMigration, pendingMigrations: pendingMigrations.map((m) => ({ id: m.id, name: m.name, version: m.version, hasDown: !!m.down, upSql: m.up, downSql: m.down, })), }, null, 2)) } else { console.log('') console.log('Migration Dry Run') console.log('=================') console.log('') console.log(`Current version: ${schemaVersion.currentVersion}`) console.log(`Target version: ${registry.getLatestVersion()}`) console.log('') if (!comparison.needsMigration) { console.log(formatSuccess('Database is up to date - no migrations needed')) } else { console.log(formatWarning(`${comparison.pendingCount} migration(s) would be applied:`)) console.log('') // Get pending migrations const pendingMigrations = migrations.filter((m) => comparison.missingVersions.includes(m.version) ).sort((a, b) => a.version - b.version) // Create validator for operation detection const validator = createMigrationValidator() for (const migration of pendingMigrations) { console.log(` ${formatInfo(`v${migration.version}: ${migration.name}`)}`) console.log(` ${formatDim(`ID: ${migration.id}`)}`) // Detect operations const operations = validator.detectOperations(migration.up) const tables = validator.extractTableNames(migration.up) if (tables.length > 0) { console.log(` ${formatDim(`Tables: ${tables.join(', ')}`)}`) } if (operations.length > 0) { const opTypes = [...new Set(operations.map((o: { type: string }) => o.type))] console.log(` ${formatDim(`Operations: ${opTypes.join(', ')}`)}`) // Warn about destructive operations const destructive = operations.filter((o: { isDestructive: boolean }) => o.isDestructive) if (destructive.length > 0) { console.log(` ${formatWarning('Destructive operations detected!')}`) } } if (!migration.down) { console.log(` ${formatWarning('No rollback SQL defined')}`) } // Show SQL if verbose if (options.verbose) { console.log('') console.log(' SQL to execute:') const lines = migration.up.trim().split('\n') for (const line of lines) { console.log(` ${formatDim(line)}`) } } console.log('') } // Summary console.log(formatInfo('Run `npx postgres.do migrate` to apply these migrations')) } } // Cleanup if ('end' in executor && typeof executor.end === 'function') { await executor.end() } } catch (error) { console.error(formatError('Dry run error:'), error instanceof Error ? error.message : error) if (options.verbose && error instanceof Error && error.stack) { console.error(error.stack) } process.exit(1) } }