#!/usr/bin/env node /** * 数据库迁移运行器 * 按顺序执行 migrations/ 目录下的所有 .sql 文件 * 记录已执行的迁移到 schema_migrations 表(幂等性保障) * * 用法: * node migrations/_runner.js # 执行所有待执行迁移 * node migrations/_runner.js --status # 查看迁移状态 * node migrations/_runner.js --reset # 重置数据库(慎用) */ import { DatabaseSync } from 'node:sqlite' import { readFileSync, readdirSync, mkdirSync, unlinkSync, existsSync } from 'node:fs' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) const root = join(__dirname, '..') const dataDir = join(root, 'data') mkdirSync(dataDir, { recursive: true }) const dbPath = join(dataDir, 'reporter-station.db') /** 初始化数据库连接(确保 schema_migrations 表存在) */ function initDb() { const db = new DatabaseSync(dbPath) db.exec(` PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS schema_migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, applied_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ); `) return db } const args = process.argv.slice(2) if (args.includes('--status')) { const db = initDb() const applied = db.prepare('SELECT name, applied_at FROM schema_migrations ORDER BY id').all() const allFiles = readdirSync(__dirname) .filter(f => f.endsWith('.sql') && !f.startsWith('_')) .sort() console.log('\n迁移状态:') for (const f of allFiles) { const name = f.replace('.sql', '') const row = applied.find(a => a.name === name) if (row) { console.log(` [x] ${name} (applied at ${row.applied_at})`) } else { console.log(` [ ] ${name}`) } } db.close() process.exit(0) } if (args.includes('--seed')) { const db = initDb() const seedFile = '002_seed_data.sql' const sql = readFileSync(join(__dirname, seedFile), 'utf-8') try { db.exec(sql) console.log(`已重新执行 seed 数据(${seedFile})`) } catch (err) { console.error(`seed 失败: ${err.message}`) process.exit(1) } db.close() process.exit(0) } if (args.includes('--reset')) { for (const ext of ['', '-wal', '-shm']) { const f = dbPath + ext if (existsSync(f)) { unlinkSync(f); console.log(`已删除: ${f}`) } } console.log('数据库已重置,重新启动服务将自动执行迁移。') process.exit(0) } // 默认:执行待执行的迁移 const db = initDb() const applied = new Set(db.prepare('SELECT name FROM schema_migrations').all().map(r => r.name)) const allFiles = readdirSync(__dirname) .filter(f => f.endsWith('.sql') && !f.startsWith('_')) .sort() let appliedCount = 0 for (const file of allFiles) { const name = file.replace('.sql', '') if (applied.has(name)) { console.log(` 跳过 (已执行): ${name}`) continue } const sql = readFileSync(join(__dirname, file), 'utf-8') try { db.exec('BEGIN') db.exec(sql) db.prepare('INSERT INTO schema_migrations (name) VALUES (?)').run(name) db.exec('COMMIT') console.log(` [x] ${name}`) appliedCount++ } catch (err) { db.exec('ROLLBACK') console.error(` [!] ${name} 执行失败: ${err.message}`) process.exit(1) } } db.close() if (appliedCount === 0) { console.log('\n没有待执行的迁移,数据库已是最新。') } else { console.log(`\n成功执行 ${appliedCount} 个迁移。`) }