86 lines
3.3 KiB
JavaScript
86 lines
3.3 KiB
JavaScript
#!/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 <sqlite> --user-id <uuid> [--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;
|
|
});
|
|
}
|