#!/usr/bin/env node import fs from 'node:fs'; import { buildControlSchemaSql, createSqliteSnapshot, inspectSqliteDatabase, migrateSqliteSnapshot, provisionUserSpace, } from '../mindspace-userdata-postgres.mjs'; function usage() { return ` Usage: node scripts/migrate-mindspace-sqlite-to-postgres.mjs --source --user-id [--apply] [--replace-shadow] Default is read-only dry-run. --apply requires MINDSPACE_USERDATA_PG_URL. The command creates a consistent SQLite snapshot and never modifies the source SQLite. `.trim(); } export function parseArgs(argv = []) { const options = { apply: false, replaceShadow: false, source: '', userId: '', help: false }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--apply') options.apply = true; else if (arg === '--replace-shadow') options.replaceShadow = true; else if (arg === '--source') options.source = argv[++index] ?? ''; else if (arg === '--user-id') options.userId = argv[++index] ?? ''; else if (arg === '--help' || arg === '-h') options.help = true; else throw new Error(`未知参数:${arg}`); } if (!options.help && (!options.source || !options.userId)) throw new Error('--source 和 --user-id 必填'); return options; } export async function run(argv = process.argv.slice(2), env = process.env) { const options = parseArgs(argv); if (options.help) { console.log(usage()); return { mode: 'help' }; } const inspected = inspectSqliteDatabase(options.source); const sourceRows = inspected.tables.reduce((total, table) => total + table.rows.length, 0); if (!options.apply) { console.log(JSON.stringify({ mode: 'dry-run', userId: options.userId, source: inspected.path, sizeBytes: inspected.sizeBytes, tables: inspected.tables.map((table) => ({ name: table.name, rows: table.rows.length })), sourceRows, note: '未修改 SQLite 或 PostgreSQL;使用 --apply 才会创建影子空间。', }, null, 2)); return { mode: 'dry-run', tableCount: inspected.tables.length, sourceRows }; } if (!env.MINDSPACE_USERDATA_PG_URL) throw new Error('--apply requires MINDSPACE_USERDATA_PG_URL'); const snapshotPath = createSqliteSnapshot(options.source); const snapshot = inspectSqliteDatabase(snapshotPath); const { default: pg } = await import('pg'); const client = new pg.Client({ connectionString: env.MINDSPACE_USERDATA_PG_URL }); try { await client.connect(); await client.query(buildControlSchemaSql()); await provisionUserSpace(client, options.userId, { sourceSqlitePath: inspected.path, }); const result = await migrateSqliteSnapshot(client, snapshot, options.userId, { replaceShadow: options.replaceShadow, sourcePath: inspected.path, }); if (result.sourceRows !== result.targetRows) throw new Error('迁移行数校验失败'); console.log(JSON.stringify({ mode: 'shadow', ...result }, null, 2)); return { mode: 'shadow', ...result }; } finally { await client.end().catch(() => {}); fs.rmSync(snapshotPath, { force: true }); } } if (import.meta.url === `file://${process.argv[1]}`) { run().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; }); }