Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

205 lines
6.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* ═══════════════════════════════════════════════════════════
* 🗄️ AGE OS · 数据库迁移运行器
* ═══════════════════════════════════════════════════════════
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 按编号顺序执行 schema/*.sql 文件。
* 使用 schema_migrations 表跟踪已执行的迁移,
* 确保每个 SQL 文件只执行一次(幂等)。
*
* 用法:
* node scripts/db-migrate.js # 执行所有未执行的迁移
* node scripts/db-migrate.js --status # 查看迁移状态
* node scripts/db-migrate.js --force 003 # 强制重跑指定编号
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// ─── 数据库连接(复用 MCP db 模块的配置) ───
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.ZY_DB_HOST || '127.0.0.1',
port: parseInt(process.env.ZY_DB_PORT || '5432', 10),
user: process.env.ZY_DB_USER || 'zy_admin',
password: process.env.ZY_DB_PASS || '',
database: process.env.ZY_DB_NAME || 'age_os',
max: 3,
connectionTimeoutMillis: 10000
});
const SCHEMA_DIR = path.resolve(__dirname, '..', 'schema');
// ─── 确保迁移跟踪表存在 ───
async function ensureMigrationTable() {
await pool.query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
id SERIAL PRIMARY KEY,
filename VARCHAR(500) NOT NULL UNIQUE,
checksum VARCHAR(64),
executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
duration_ms INT,
success BOOLEAN NOT NULL DEFAULT true,
error_msg TEXT
);
`);
}
// ─── 获取已执行的迁移列表 ───
async function getExecutedMigrations() {
const result = await pool.query(
'SELECT filename, executed_at, success FROM schema_migrations WHERE success = true ORDER BY filename'
);
return new Set(result.rows.map(r => r.filename));
}
// ─── 获取所有 schema SQL 文件(按编号排序) ───
function getSchemaFiles() {
if (!fs.existsSync(SCHEMA_DIR)) {
console.error(`[Migrate] Schema目录不存在: ${SCHEMA_DIR}`);
return [];
}
return fs.readdirSync(SCHEMA_DIR)
.filter(f => f.endsWith('.sql'))
.sort() // 按文件名排序001, 002, 003...
.map(f => ({
filename: f,
filepath: path.join(SCHEMA_DIR, f)
}));
}
// ─── 计算简单校验和 ───
function computeChecksum(content) {
return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
}
// ─── 执行单个迁移 ───
async function executeMigration(file) {
const content = fs.readFileSync(file.filepath, 'utf8');
const checksum = computeChecksum(content);
const startTime = Date.now();
console.log(`[Migrate] ▶ 执行: ${file.filename}`);
try {
await pool.query(content);
const duration = Date.now() - startTime;
await pool.query(
`INSERT INTO schema_migrations (filename, checksum, duration_ms, success)
VALUES ($1, $2, $3, true)
ON CONFLICT (filename) DO UPDATE SET
checksum = $2, executed_at = NOW(), duration_ms = $3, success = true, error_msg = NULL`,
[file.filename, checksum, duration]
);
console.log(`[Migrate] ✅ ${file.filename} 完成 (${duration}ms)`);
return { success: true, duration };
} catch (err) {
const duration = Date.now() - startTime;
// 记录失败
await pool.query(
`INSERT INTO schema_migrations (filename, checksum, duration_ms, success, error_msg)
VALUES ($1, $2, $3, false, $4)
ON CONFLICT (filename) DO UPDATE SET
checksum = $2, executed_at = NOW(), duration_ms = $3, success = false, error_msg = $4`,
[file.filename, checksum, duration, err.message]
).catch(() => {});
console.error(`[Migrate] ❌ ${file.filename} 失败 (${duration}ms): ${err.message}`);
return { success: false, duration, error: err.message };
}
}
// ─── 主入口 ───
async function main() {
const args = process.argv.slice(2);
const isStatus = args.includes('--status');
const forceIndex = args.indexOf('--force');
const forcePrefix = forceIndex >= 0 ? args[forceIndex + 1] : null;
console.log('═══════════════════════════════════════════════');
console.log(' 🗄️ AGE OS · 数据库迁移运行器');
console.log(' 铸渊 · ICE-GL-ZY001');
console.log('═══════════════════════════════════════════════');
try {
// 测试连接
await pool.query('SELECT 1');
console.log('[Migrate] 数据库连接成功');
// 确保迁移表存在
await ensureMigrationTable();
const schemaFiles = getSchemaFiles();
const executed = await getExecutedMigrations();
if (isStatus) {
console.log('\n迁移状态:');
for (const file of schemaFiles) {
const status = executed.has(file.filename) ? '✅ 已执行' : '⏳ 待执行';
console.log(` ${status} ${file.filename}`);
}
console.log(`\n总计: ${schemaFiles.length} 个迁移文件, ${executed.size} 个已执行`);
return;
}
// 筛选待执行的迁移
let pending;
if (forcePrefix) {
pending = schemaFiles.filter(f => f.filename.startsWith(forcePrefix));
if (pending.length === 0) {
console.log(`[Migrate] 未找到前缀为 ${forcePrefix} 的迁移文件`);
return;
}
console.log(`[Migrate] 强制重跑 ${pending.length} 个迁移`);
} else {
pending = schemaFiles.filter(f => !executed.has(f.filename));
}
if (pending.length === 0) {
console.log('[Migrate] ✅ 所有迁移已是最新状态');
return;
}
console.log(`[Migrate] 发现 ${pending.length} 个待执行迁移\n`);
let successCount = 0;
let failCount = 0;
for (const file of pending) {
const result = await executeMigration(file);
if (result.success) {
successCount++;
} else {
failCount++;
// SQL 文件使用 IF NOT EXISTS继续执行后续迁移
console.warn(`[Migrate] ⚠️ ${file.filename} 失败,继续执行后续迁移...`);
}
}
console.log(`\n[Migrate] 完成: ${successCount} 成功, ${failCount} 失败`);
if (failCount > 0) {
process.exitCode = 1;
}
} catch (err) {
console.error('[Migrate] 严重错误:', err.message);
process.exitCode = 1;
} finally {
await pool.end();
}
}
main();