import * as fs from 'fs'; import * as path from 'path'; import { migrateV0ToV1 } from './migrators/v0_to_v1.js'; /** * Detected storage format version. * - v0: Legacy JSON format ({db}/{coll}.json files) * - v1: Bitcask binary format ({db}/{coll}/data.rdb directories) */ type TStorageVersion = 0 | 1; /** * StorageMigrator — runs before the Rust engine starts. * * Detects the current storage format version and runs the appropriate * migration chain. The Rust engine only knows the current format (v1). * * Migration is safe: original files are never modified or deleted. * On success, a console hint is printed about which old files can be removed. */ export class StorageMigrator { private storagePath: string; constructor(storagePath: string) { this.storagePath = storagePath; } /** * Run any needed migrations. Safe to call even if storage is already current. */ async run(): Promise { if (!fs.existsSync(this.storagePath)) { return; // No data yet — nothing to migrate } const version = this.detectVersion(); if (version === 1) { return; // Already current } if (version === 0) { console.log(`[smartdb] Detected v0 (JSON) storage format at ${this.storagePath}`); console.log(`[smartdb] Running migration v0 → v1 (Bitcask binary format)...`); const deletableFiles = await migrateV0ToV1(this.storagePath); if (deletableFiles.length > 0) { console.log(`[smartdb] Migration v0 → v1 complete.`); console.log(`[smartdb] The following old files can be safely deleted:`); for (const f of deletableFiles) { console.log(`[smartdb] ${f}`); } } else { console.log(`[smartdb] Migration v0 → v1 complete. No old files to clean up.`); } } } /** * Detect the storage format version by inspecting the directory structure. * * v0: {db}/{coll}.json files exist * v1: {db}/{coll}/data.rdb directories exist */ private detectVersion(): TStorageVersion { const entries = fs.readdirSync(this.storagePath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const dbDir = path.join(this.storagePath, entry.name); const dbEntries = fs.readdirSync(dbDir, { withFileTypes: true }); for (const dbEntry of dbEntries) { // v1: subdirectory with data.rdb if (dbEntry.isDirectory()) { const dataRdb = path.join(dbDir, dbEntry.name, 'data.rdb'); if (fs.existsSync(dataRdb)) { return 1; } } // v0: .json file (not .indexes.json) if (dbEntry.isFile() && dbEntry.name.endsWith('.json') && !dbEntry.name.endsWith('.indexes.json')) { return 0; } } } // Empty or unrecognized — treat as v1 (fresh start) return 1; } }