122 lines
3.5 KiB
JavaScript
122 lines
3.5 KiB
JavaScript
#!/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} 个迁移。`)
|
||
} |