import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartdb from '../ts/index.js'; import { MongoClient, Db } from 'mongodb'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- let tmpDir: string; function makeTmpDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'smartdb-migration-test-')); } function cleanTmpDir(dir: string): void { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true, force: true }); } } /** * Create a v0 (legacy JSON) storage layout: * {base}/{db}/{coll}.json * {base}/{db}/{coll}.indexes.json */ function createV0Layout(basePath: string, dbName: string, collName: string, docs: any[]): void { const dbDir = path.join(basePath, dbName); fs.mkdirSync(dbDir, { recursive: true }); // Convert docs to the extended JSON format that the old Rust engine wrote: // ObjectId is stored as { "$oid": "hex" } const jsonDocs = docs.map(doc => { const clone = { ...doc }; if (!clone._id) { // Generate a fake ObjectId-like hex string const hex = [...Array(24)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); clone._id = { '$oid': hex }; } return clone; }); const collPath = path.join(dbDir, `${collName}.json`); fs.writeFileSync(collPath, JSON.stringify(jsonDocs, null, 2)); const indexPath = path.join(dbDir, `${collName}.indexes.json`); fs.writeFileSync(indexPath, JSON.stringify([ { name: '_id_', key: { _id: 1 } }, ], null, 2)); } // ============================================================================ // Migration: v0 → v1 basic // ============================================================================ tap.test('migration: detects v0 format and migrates on startup', async () => { tmpDir = makeTmpDir(); // Create v0 layout with test data createV0Layout(tmpDir, 'mydb', 'users', [ { name: 'Alice', age: 30, email: 'alice@test.com' }, { name: 'Bob', age: 25, email: 'bob@test.com' }, { name: 'Charlie', age: 35, email: 'charlie@test.com' }, ]); createV0Layout(tmpDir, 'mydb', 'products', [ { sku: 'W001', name: 'Widget', price: 9.99 }, { sku: 'G001', name: 'Gadget', price: 19.99 }, ]); // Verify v0 files exist expect(fs.existsSync(path.join(tmpDir, 'mydb', 'users.json'))).toBeTrue(); expect(fs.existsSync(path.join(tmpDir, 'mydb', 'products.json'))).toBeTrue(); // Start server — migration should run automatically const server = new smartdb.SmartdbServer({ socketPath: path.join(os.tmpdir(), `smartdb-mig-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`), storage: 'file', storagePath: tmpDir, }); await server.start(); // v1 directories should now exist expect(fs.existsSync(path.join(tmpDir, 'mydb', 'users', 'data.rdb'))).toBeTrue(); expect(fs.existsSync(path.join(tmpDir, 'mydb', 'products', 'data.rdb'))).toBeTrue(); // v0 files should still exist (not deleted) expect(fs.existsSync(path.join(tmpDir, 'mydb', 'users.json'))).toBeTrue(); expect(fs.existsSync(path.join(tmpDir, 'mydb', 'products.json'))).toBeTrue(); // Connect and verify data is accessible const client = new MongoClient(server.getConnectionUri(), { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); const db = client.db('mydb'); // Users collection const users = await db.collection('users').find({}).toArray(); expect(users.length).toEqual(3); const alice = users.find(u => u.name === 'Alice'); expect(alice).toBeTruthy(); expect(alice!.age).toEqual(30); expect(alice!.email).toEqual('alice@test.com'); // Products collection const products = await db.collection('products').find({}).toArray(); expect(products.length).toEqual(2); const widget = products.find(p => p.sku === 'W001'); expect(widget).toBeTruthy(); expect(widget!.price).toEqual(9.99); await client.close(); await server.stop(); }); // ============================================================================ // Migration: migrated data survives another restart // ============================================================================ tap.test('migration: migrated data persists across restart', async () => { const server = new smartdb.SmartdbServer({ socketPath: path.join(os.tmpdir(), `smartdb-mig-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`), storage: 'file', storagePath: tmpDir, }); await server.start(); const client = new MongoClient(server.getConnectionUri(), { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); const db = client.db('mydb'); const users = await db.collection('users').find({}).toArray(); expect(users.length).toEqual(3); const products = await db.collection('products').find({}).toArray(); expect(products.length).toEqual(2); await client.close(); await server.stop(); }); // ============================================================================ // Migration: can write new data after migration // ============================================================================ tap.test('migration: new writes work after migration', async () => { const server = new smartdb.SmartdbServer({ socketPath: path.join(os.tmpdir(), `smartdb-mig-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`), storage: 'file', storagePath: tmpDir, }); await server.start(); const client = new MongoClient(server.getConnectionUri(), { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); const db = client.db('mydb'); // Insert new documents await db.collection('users').insertOne({ name: 'Diana', age: 28 }); const count = await db.collection('users').countDocuments(); expect(count).toEqual(4); // Update existing migrated document await db.collection('users').updateOne( { name: 'Alice' }, { $set: { age: 31 } } ); const alice = await db.collection('users').findOne({ name: 'Alice' }); expect(alice!.age).toEqual(31); // Delete a migrated document await db.collection('products').deleteOne({ sku: 'G001' }); const prodCount = await db.collection('products').countDocuments(); expect(prodCount).toEqual(1); await client.close(); await server.stop(); cleanTmpDir(tmpDir); }); // ============================================================================ // Migration: skips already-migrated data // ============================================================================ tap.test('migration: no-op for v1 format', async () => { tmpDir = makeTmpDir(); // Start fresh to create v1 layout const server = new smartdb.SmartdbServer({ socketPath: path.join(os.tmpdir(), `smartdb-mig-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`), storage: 'file', storagePath: tmpDir, }); await server.start(); const client = new MongoClient(server.getConnectionUri(), { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); const db = client.db('v1test'); await db.collection('items').insertOne({ x: 1 }); await client.close(); await server.stop(); // Restart — migration should detect v1 and skip const server2 = new smartdb.SmartdbServer({ socketPath: path.join(os.tmpdir(), `smartdb-mig-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`), storage: 'file', storagePath: tmpDir, }); await server2.start(); const client2 = new MongoClient(server2.getConnectionUri(), { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client2.connect(); const db2 = client2.db('v1test'); const doc = await db2.collection('items').findOne({ x: 1 }); expect(doc).toBeTruthy(); await client2.close(); await server2.stop(); cleanTmpDir(tmpDir); }); // ============================================================================ // Migration: empty storage is handled gracefully // ============================================================================ tap.test('migration: empty storage directory works', async () => { tmpDir = makeTmpDir(); const server = new smartdb.SmartdbServer({ socketPath: path.join(os.tmpdir(), `smartdb-mig-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`), storage: 'file', storagePath: tmpDir, }); await server.start(); const client = new MongoClient(server.getConnectionUri(), { directConnection: true, serverSelectionTimeoutMS: 5000, }); await client.connect(); // Should work fine with empty storage const db = client.db('emptytest'); await db.collection('first').insertOne({ hello: 'world' }); const doc = await db.collection('first').findOne({ hello: 'world' }); expect(doc).toBeTruthy(); await client.close(); await server.stop(); cleanTmpDir(tmpDir); }); export default tap.start();