105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
/**
|
|
* 查看本地迁移文件与数据库 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<string>()
|
|
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()
|
|
})
|