Files
delivery-desk/scripts/migrate-sqlite-to-postgres.ts

69 lines
3.8 KiB
TypeScript
Raw 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.
import fs from 'node:fs';
import path from 'node:path';
import Database from 'better-sqlite3';
import pg from 'pg';
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error('缺少 DATABASE_URL迁移未执行');
const replace = process.argv.includes('--replace');
const sqlitePath = path.resolve(process.env.SQLITE_PATH || 'data/app.db');
const schemaPath = path.resolve('db/postgres/schema.sql');
if (!fs.existsSync(sqlitePath)) throw new Error(`SQLite 数据库不存在:${sqlitePath}`);
const tables = [
'operation_groups', 'users', 'projects', 'collections', 'notes', 'images', 'annotations',
'work_comments', 'feedback_replies', 'work_versions', 'review_rounds', 'review_events', 'sessions', 'customer_sessions',
'audit_logs', 'api_keys', 'storage_configs',
] as const;
const booleanColumns: Record<string, Set<string>> = {
users: new Set(['must_change_password']),
projects: new Set(['customer_access_enabled']),
};
const sqlite = new Database(sqlitePath, { readonly: true });
const client = new pg.Client({ connectionString: databaseUrl, ssl: process.env.PGSSL === 'disable' ? false : undefined });
try {
await client.connect();
const schema = fs.readFileSync(schemaPath, 'utf8');
await client.query(schema);
const existing = Number((await client.query('SELECT COUNT(*)::int AS count FROM operation_groups')).rows[0].count);
if (existing && !replace) throw new Error('PostgreSQL 已有数据。确认覆盖时请显式添加 --replace');
await client.query('BEGIN');
await client.query('DROP INDEX IF EXISTS idx_work_versions_one_per_round');
if (replace) await client.query(`TRUNCATE ${[...tables].reverse().map((table) => `"${table}"`).join(', ')} RESTART IDENTITY CASCADE`);
for (const table of tables) {
const exists = sqlite.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
if (!exists) continue;
const rows = sqlite.prepare(`SELECT * FROM "${table}" ORDER BY id`).all() as Record<string, unknown>[];
for (const row of rows) {
const columns = Object.keys(row);
const values = columns.map((column) => booleanColumns[table]?.has(column) ? Boolean(row[column]) : row[column]);
const names = columns.map((column) => `"${column}"`).join(', ');
const placeholders = columns.map((_, index) => `$${index + 1}`).join(', ');
await client.query(`INSERT INTO "${table}" (${names}) VALUES (${placeholders})`, values);
}
if (rows.length) await client.query(`SELECT setval(pg_get_serial_sequence('${table}', 'id'), (SELECT MAX(id) FROM "${table}"), true)`);
}
const collectionStatusRepair = schema.match(/-- COLLECTION_STATUS_REPAIR_START([\s\S]+?)-- COLLECTION_STATUS_REPAIR_END/)?.[1];
const reviewRoundRepair = schema.match(/-- REVIEW_ROUND_REPAIR_START([\s\S]+?)-- REVIEW_ROUND_REPAIR_END/)?.[1];
const singleSchemeRepair = schema.match(/-- SINGLE_SCHEME_REPAIR_START([\s\S]+?)-- SINGLE_SCHEME_REPAIR_END/)?.[1];
if (!collectionStatusRepair) throw new Error('PostgreSQL schema 缺少作品交付集状态修复脚本');
if (!reviewRoundRepair) throw new Error('PostgreSQL schema 缺少验收轮次修复脚本');
if (!singleSchemeRepair) throw new Error('PostgreSQL schema 缺少单方案轮次修复脚本');
await client.query(reviewRoundRepair);
await client.query(singleSchemeRepair);
await client.query('CREATE UNIQUE INDEX idx_work_versions_one_per_round ON work_versions(review_round_id) WHERE review_round_id IS NOT NULL');
await client.query(collectionStatusRepair);
await client.query('COMMIT');
process.stdout.write(`迁移完成:${tables.length} 张表已从 ${sqlitePath} 导入 PostgreSQL\n`);
} catch (error) {
await client.query('ROLLBACK').catch(() => undefined);
throw error;
} finally {
sqlite.close();
await client.end().catch(() => undefined);
}