migrateCodebase
uses node:path
Move a codebase to this tcm's table layout and hash format, safely: the database as it was is kept beside it
(codebase.v<n>.sqlite; codebase.layout-<layout>.sqlite when only its tables move), a copy is migrated
(migrateFile), and the copy is used only if this tcm then reads every live definition back to its hash (verify);
otherwise the codebase is left as it was. Undefined if it's in this layout and format already.
import { Codebase } from "#codebase/tcmts/cli/codebase"; import { formatsOf, layoutOf, migrateFile } from "#codebase/tcmts/cli/migrate"; import type { Migrated } from "#codebase/tcmts/cli/migrate"; import { IR_VERSION } from "#codebase/tcmts/core/hash"; import { LAYOUT } from "#codebase/tcmts/store"; import fs from "node:fs"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; export function migrateCodebase( root: string, ): (Migrated & { backup: string; verified: number }) | undefined { const db = path.join(root, ".tcm", "codebase.sqlite"); // everything written so far in the database file itself, not its log const wal = new DatabaseSync(db); wal.exec("PRAGMA wal_checkpoint(TRUNCATE)"); wal.close(); const from = formatsOf(db); const layout = layoutOf(db); const formatted = from.every((v) => v === IR_VERSION); if (formatted && (!layout || layout === LAYOUT)) return undefined; if (from.some((v) => v > IR_VERSION)) throw new Error( `this codebase is in hash format v${Math.max(...from)}, newer than this tcm's (v${IR_VERSION}): use a newer tcm`, ); let backup = path.join( root, ".tcm", formatted ? `codebase.layout-${layout}.sqlite` : `codebase.v${Math.max(...from)}.sqlite`, ); if (fs.existsSync(backup)) backup = backup.replace(/\.sqlite$/, `.${Date.now()}.sqlite`); fs.copyFileSync(db, backup); const work = `${db}.migrating`; fs.copyFileSync(db, work); let report: Migrated; try { report = migrateFile(work); } catch (e) { fs.rmSync(work, { force: true }); throw new Error(`the migration failed, and the codebase is as it was: ${(e as Error).message}`); } for (const f of [`${db}-wal`, `${db}-shm`]) fs.rmSync(f, { force: true }); fs.renameSync(work, db); try { const cb = Codebase.find(root); const v = cb.verify(true); if (v.mismatches.length || v.problems.length) { const said = [ ...v.mismatches.map( (m) => `${m.name} hashes to ${m.rehashed.slice(0, 8)}, not ${m.stored.slice(0, 8)}`, ), ...v.problems, ].slice(0, 5); cb.store.close(); throw new Error(said.join("\n")); } cb.refreshShim(true); cb.store.close(); return { ...report, backup, verified: v.checked }; } catch (e) { for (const f of [`${db}-wal`, `${db}-shm`]) fs.rmSync(f, { force: true }); fs.copyFileSync(backup, db); throw new Error( `the migrated codebase didn't read back as itself, so it's as it was:\n${(e as Error).message}`, ); } }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.migrate.Migrated
- tcmts.cli.migrate.formatsOf
- tcmts.cli.migrate.layoutOf
- tcmts.cli.migrate.migrateFile
- tcmts.core.hash.IR_VERSION
- tcmts.store.LAYOUT