2026-07-21 15:28:55 +08:00
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' ,
2026-07-21 20:25:52 +08:00
'work_comments' , 'work_versions' , 'review_rounds' , 'review_events' , 'sessions' , 'customer_sessions' ,
2026-07-21 15:28:55 +08:00
'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 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 schema = fs . readFileSync ( path . resolve ( 'db/postgres/schema.sql' ) , 'utf8' )
. replace ( /^BEGIN;|COMMIT;$/gm , '' )
2026-07-21 19:23:58 +08:00
. replace ( /CREATE UNIQUE INDEX IF NOT EXISTS one_active_storage_config[^;]+;/ , '' )
2026-07-21 20:25:52 +08:00
. replace ( /-- COLLECTION_STATUS_REPAIR_START[\s\S]+?-- COLLECTION_STATUS_REPAIR_END/ , '' )
. replace ( /-- REVIEW_ROUND_REPAIR_START[\s\S]+?-- REVIEW_ROUND_REPAIR_END/ , '' ) ;
2026-07-21 15:28:55 +08:00
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 < 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 ] ) ;
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 ( '作品版本关系未正确迁移' ) ;
2026-07-21 20:25:52 +08:00
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 ) ;
if ( invalidRoundLinks || invalidActiveRounds ) throw new Error ( ` 验收轮次迁移关联无效: versions= ${ invalidRoundLinks } , notes= ${ invalidActiveRounds } ` ) ;
2026-07-21 15:28:55 +08:00
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 ( ) ;
}