import fs from 'node:fs'; import path from 'node:path'; import Database from 'better-sqlite3'; import { newDb } from 'pg-mem'; 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> = { users: new Set(['must_change_password']), projects: new Set(['customer_access_enabled']), }; const memory = newDb({ autoCreateForeignKeyIndices: true }); const adapter = memory.adapters.createPg(); const client = new adapter.Client(); const sqlite = new Database(path.resolve('data/app.db'), { readonly: true }); try { await client.connect(); const schemaSource = fs.readFileSync(path.resolve('db/postgres/schema.sql'), 'utf8'); if (!/-- SINGLE_SCHEME_REPAIR_START[\s\S]+?-- SINGLE_SCHEME_REPAIR_END/.test(schemaSource)) throw new Error('PostgreSQL schema 缺少旧多方案数据修复脚本'); const schema = schemaSource .replace(/^BEGIN;|COMMIT;$/gm, '') .replace(/CREATE UNIQUE INDEX IF NOT EXISTS one_active_storage_config[^;]+;/, '') .replace(/-- COLLECTION_STATUS_REPAIR_START[\s\S]+?-- COLLECTION_STATUS_REPAIR_END/, '') .replace(/-- REVIEW_ROUND_REPAIR_START[\s\S]+?-- REVIEW_ROUND_REPAIR_END/, '') .replace(/-- PROJECT_REVIEW_STATUS_REPAIR_START[\s\S]+?-- PROJECT_REVIEW_STATUS_REPAIR_END/, '') .replace(/-- TEXT_ANNOTATION_TARGET_REPAIR_START[\s\S]+?-- TEXT_ANNOTATION_TARGET_REPAIR_END/, '') .replace(/-- SINGLE_SCHEME_REPAIR_START[\s\S]+?-- SINGLE_SCHEME_REPAIR_END/, ''); await client.query(schema); 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[]; for (const row of rows) { const columns = Object.keys(row); const values = columns.map((column) => booleanColumns[table]?.has(column) ? Boolean(row[column]) : row[column]); await client.query(`INSERT INTO "${table}" (${columns.map((column) => `"${column}"`).join(',')}) VALUES (${columns.map((_, index) => `$${index + 1}`).join(',')})`, values); } const pgCount = Number((await client.query(`SELECT COUNT(*)::int AS count FROM "${table}"`)).rows[0].count); if (pgCount !== rows.length) throw new Error(`${table} 行数不一致:SQLite=${rows.length}, PostgreSQL=${pgCount}`); } const current = (await client.query(`SELECT n.version_number,n.review_status,i.storage_provider FROM notes n JOIN images i ON i.note_id=n.id AND i.version_number=n.version_number WHERE n.id=1 LIMIT 1`)).rows[0]; if (!current || Number(current.version_number) < 1) throw new Error('作品版本关系未正确迁移'); const invalidRoundLinks = Number((await client.query('SELECT COUNT(*)::int AS count FROM work_versions v LEFT JOIN review_rounds r ON r.id=v.review_round_id WHERE r.id IS NULL')).rows[0].count); const invalidActiveRounds = Number((await client.query('SELECT COUNT(*)::int AS count FROM notes n LEFT JOIN review_rounds r ON r.id=n.active_round_id WHERE r.id IS NULL OR r.note_id!=n.id')).rows[0].count); const invalidProjects = Number((await client.query('SELECT COUNT(*)::int AS count FROM notes n LEFT JOIN projects p ON p.id=n.project_id WHERE p.id IS NULL')).rows[0].count); if (invalidRoundLinks || invalidActiveRounds || invalidProjects) throw new Error(`迁移关联无效:rounds=${invalidRoundLinks}, notes=${invalidActiveRounds}, projects=${invalidProjects}`); const activeStorage = Number((await client.query("SELECT COUNT(*)::int AS count FROM storage_configs WHERE status='active'")).rows[0].count); if (activeStorage > 1) throw new Error('活动对象存储配置超过一个'); process.stdout.write(`PostgreSQL schema 与迁移映射验证通过:${tables.length} 张表,当前作品 V${current.version_number},存储=${current.storage_provider}\n`); } finally { sqlite.close(); await client.end(); }