Files
memind/scripts/migrate-mindspace-sqlite-to-postgres.mjs

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;
});
}