/** * 查看本地迁移文件与数据库 schema_migrations 的对齐状态。 * * 用法: * npm run db:migrate:status */ import fs from 'node:fs' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import { closeDb, query } from '../src/db/client.js' const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url)) const MIGRATIONS_DIR = path.join(CURRENT_DIR, '../src/db/migrations') const MIGRATION_FILE_PATTERN = /^(\d{3})_.+\.sql$/i function listLocalMigrations(): string[] { if (!fs.existsSync(MIGRATIONS_DIR)) { return [] } return fs .readdirSync(MIGRATIONS_DIR) .filter((name) => MIGRATION_FILE_PATTERN.test(name)) .sort((a, b) => a.localeCompare(b, 'en')) } function normalizeMigrationName(value: unknown): string { const raw = String(value || '').trim() if (!raw) { return '' } return raw.replace(/\.sql$/i, '') } async function main() { const localFiles = listLocalMigrations() const localNames = localFiles.map((file) => file.replace(/\.sql$/i, '')) let appliedNames = new Set() try { const result = await query<{ name: string | null; filename: string | null; run_on: string | null }>( ` SELECT name, filename, run_on FROM schema_migrations ORDER BY COALESCE(filename, name), id `, ) appliedNames = new Set( result.rows .map((row) => normalizeMigrationName(row.filename || row.name)) .filter(Boolean), ) } catch (error) { const message = error instanceof Error ? error.message : String(error) if (/schema_migrations/i.test(message) && /does not exist|不存在/i.test(message)) { console.warn('[db:migrate:status] schema_migrations 尚不存在,视为尚未应用任何迁移') } else { throw error } } const pending = localNames.filter((name) => !appliedNames.has(name)) const orphaned = [...appliedNames].filter((name) => !localNames.includes(name)) console.info('[db:migrate:status] 本地迁移文件') if (localFiles.length === 0) { console.info(' (无)') } else { for (const file of localFiles) { const name = file.replace(/\.sql$/i, '') const mark = appliedNames.has(name) ? 'applied' : 'pending' console.info(` [${mark}] ${file}`) } } console.info(`[db:migrate:status] 已应用 ${appliedNames.size} 个,待应用 ${pending.length} 个`) if (pending.length > 0) { console.info('[db:migrate:status] 待应用:') for (const name of pending) { console.info(` - ${name}.sql`) } } if (orphaned.length > 0) { console.warn('[db:migrate:status] 数据库中存在本地没有的迁移记录(可能是旧环境残留):') for (const name of orphaned) { console.warn(` - ${name}`) } } } main() .catch((error) => { console.error(error) process.exitCode = 1 }) .finally(async () => { await closeDb() })