Files
smartmongo/test/test.tsmdb.wal.ts

412 lines
14 KiB
TypeScript

import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartmongo from '../ts/index.js';
import * as path from 'path';
import * as fs from 'fs/promises';
const { WAL, ObjectId } = smartmongo.tsmdb;
let wal: InstanceType<typeof WAL>;
const TEST_WAL_PATH = '/tmp/tsmdb-test-wal/test.wal';
// Helper to clean up test files
async function cleanupTestFiles() {
try {
await fs.rm('/tmp/tsmdb-test-wal', { recursive: true, force: true });
} catch {
// Ignore if doesn't exist
}
}
// ============================================================================
// Setup
// ============================================================================
tap.test('wal: cleanup before tests', async () => {
await cleanupTestFiles();
});
tap.test('wal: should create WAL instance', async () => {
wal = new WAL(TEST_WAL_PATH, { checkpointInterval: 100 });
expect(wal).toBeTruthy();
});
tap.test('wal: should initialize WAL', async () => {
const result = await wal.initialize();
expect(result).toBeTruthy();
expect(result.recoveredEntries).toBeArray();
expect(result.recoveredEntries.length).toEqual(0);
});
// ============================================================================
// LSN Tests
// ============================================================================
tap.test('wal: getCurrentLsn should return 0 initially', async () => {
const lsn = wal.getCurrentLsn();
expect(lsn).toEqual(0);
});
tap.test('wal: LSN should increment after logging', async () => {
const doc = { _id: new ObjectId(), name: 'Test' };
const lsn = await wal.logInsert('testdb', 'testcoll', doc as any);
expect(lsn).toEqual(1);
expect(wal.getCurrentLsn()).toEqual(1);
});
// ============================================================================
// Insert Logging Tests
// ============================================================================
tap.test('wal: logInsert should create entry with correct structure', async () => {
const doc = { _id: new ObjectId(), name: 'InsertTest', value: 42 };
const lsn = await wal.logInsert('testdb', 'insertcoll', doc as any);
expect(lsn).toBeGreaterThan(0);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
expect(entry).toBeTruthy();
expect(entry!.operation).toEqual('insert');
expect(entry!.dbName).toEqual('testdb');
expect(entry!.collName).toEqual('insertcoll');
expect(entry!.documentId).toEqual(doc._id.toHexString());
expect(entry!.data).toBeTruthy();
expect(entry!.timestamp).toBeGreaterThan(0);
expect(entry!.checksum).toBeGreaterThan(0);
});
tap.test('wal: logInsert with transaction ID', async () => {
const doc = { _id: new ObjectId(), name: 'TxnInsertTest' };
const lsn = await wal.logInsert('testdb', 'insertcoll', doc as any, 'txn-123');
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
expect(entry!.txnId).toEqual('txn-123');
});
// ============================================================================
// Update Logging Tests
// ============================================================================
tap.test('wal: logUpdate should store old and new document', async () => {
const oldDoc = { _id: new ObjectId(), name: 'OldName', value: 1 };
const newDoc = { ...oldDoc, name: 'NewName', value: 2 };
const lsn = await wal.logUpdate('testdb', 'updatecoll', oldDoc as any, newDoc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
expect(entry).toBeTruthy();
expect(entry!.operation).toEqual('update');
expect(entry!.data).toBeTruthy();
expect(entry!.previousData).toBeTruthy();
expect(entry!.documentId).toEqual(oldDoc._id.toHexString());
});
// ============================================================================
// Delete Logging Tests
// ============================================================================
tap.test('wal: logDelete should record deleted document', async () => {
const doc = { _id: new ObjectId(), name: 'ToDelete' };
const lsn = await wal.logDelete('testdb', 'deletecoll', doc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
expect(entry).toBeTruthy();
expect(entry!.operation).toEqual('delete');
expect(entry!.previousData).toBeTruthy();
expect(entry!.data).toBeUndefined();
});
// ============================================================================
// Transaction Logging Tests
// ============================================================================
tap.test('wal: logBeginTransaction should create begin entry', async () => {
const lsn = await wal.logBeginTransaction('txn-begin-test');
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
expect(entry).toBeTruthy();
expect(entry!.operation).toEqual('begin');
expect(entry!.txnId).toEqual('txn-begin-test');
});
tap.test('wal: logCommitTransaction should create commit entry', async () => {
const lsn = await wal.logCommitTransaction('txn-commit-test');
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
expect(entry).toBeTruthy();
expect(entry!.operation).toEqual('commit');
expect(entry!.txnId).toEqual('txn-commit-test');
});
tap.test('wal: logAbortTransaction should create abort entry', async () => {
const lsn = await wal.logAbortTransaction('txn-abort-test');
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
expect(entry).toBeTruthy();
expect(entry!.operation).toEqual('abort');
expect(entry!.txnId).toEqual('txn-abort-test');
});
// ============================================================================
// getTransactionEntries Tests
// ============================================================================
tap.test('wal: getTransactionEntries should return entries for transaction', async () => {
// Log a complete transaction
const txnId = 'txn-entries-test';
await wal.logBeginTransaction(txnId);
const doc1 = { _id: new ObjectId(), name: 'TxnDoc1' };
await wal.logInsert('testdb', 'txncoll', doc1 as any, txnId);
const doc2 = { _id: new ObjectId(), name: 'TxnDoc2' };
await wal.logInsert('testdb', 'txncoll', doc2 as any, txnId);
await wal.logCommitTransaction(txnId);
const entries = wal.getTransactionEntries(txnId);
expect(entries.length).toEqual(4); // begin + 2 inserts + commit
expect(entries.every(e => e.txnId === txnId)).toBeTrue();
});
tap.test('wal: getTransactionEntries should return empty for unknown transaction', async () => {
const entries = wal.getTransactionEntries('unknown-txn-id');
expect(entries.length).toEqual(0);
});
// ============================================================================
// getEntriesAfter Tests
// ============================================================================
tap.test('wal: getEntriesAfter should filter by LSN', async () => {
const currentLsn = wal.getCurrentLsn();
// Add more entries
const doc = { _id: new ObjectId(), name: 'AfterTest' };
await wal.logInsert('testdb', 'aftercoll', doc as any);
const entries = wal.getEntriesAfter(currentLsn);
expect(entries.length).toEqual(1);
expect(entries[0].lsn).toBeGreaterThan(currentLsn);
});
tap.test('wal: getEntriesAfter with LSN 0 should return all entries', async () => {
const entries = wal.getEntriesAfter(0);
expect(entries.length).toBeGreaterThan(0);
expect(entries.length).toEqual(wal.getCurrentLsn());
});
// ============================================================================
// Checkpoint Tests
// ============================================================================
tap.test('wal: checkpoint should create checkpoint entry', async () => {
const lsn = await wal.checkpoint();
expect(lsn).toBeGreaterThan(0);
// After checkpoint, getEntriesAfter(checkpoint) should be limited
const entries = wal.getEntriesAfter(0);
expect(entries.some(e => e.operation === 'checkpoint')).toBeTrue();
});
// ============================================================================
// Document Recovery Tests
// ============================================================================
tap.test('wal: recoverDocument should deserialize document from entry', async () => {
const originalDoc = { _id: new ObjectId(), name: 'RecoverTest', nested: { a: 1, b: 2 } };
const lsn = await wal.logInsert('testdb', 'recovercoll', originalDoc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
const recovered = wal.recoverDocument(entry!);
expect(recovered).toBeTruthy();
expect(recovered!.name).toEqual('RecoverTest');
expect(recovered!.nested.a).toEqual(1);
expect(recovered!.nested.b).toEqual(2);
});
tap.test('wal: recoverDocument should return null for entry without data', async () => {
const lsn = await wal.logBeginTransaction('recover-no-data');
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
const recovered = wal.recoverDocument(entry!);
expect(recovered).toBeNull();
});
tap.test('wal: recoverPreviousDocument should deserialize previous state', async () => {
const oldDoc = { _id: new ObjectId(), name: 'Old', value: 100 };
const newDoc = { ...oldDoc, name: 'New', value: 200 };
const lsn = await wal.logUpdate('testdb', 'recovercoll', oldDoc as any, newDoc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
const previous = wal.recoverPreviousDocument(entry!);
expect(previous).toBeTruthy();
expect(previous!.name).toEqual('Old');
expect(previous!.value).toEqual(100);
});
tap.test('wal: recoverPreviousDocument should return null for insert entry', async () => {
const doc = { _id: new ObjectId(), name: 'NoPrevious' };
const lsn = await wal.logInsert('testdb', 'recovercoll', doc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
const previous = wal.recoverPreviousDocument(entry!);
expect(previous).toBeNull();
});
// ============================================================================
// WAL Persistence and Recovery Tests
// ============================================================================
tap.test('wal: should persist and recover entries', async () => {
// Close current WAL
await wal.close();
// Create new WAL instance and initialize (should recover)
const wal2 = new WAL(TEST_WAL_PATH, { checkpointInterval: 100 });
const result = await wal2.initialize();
// Should have recovered entries
expect(result.recoveredEntries).toBeArray();
// After checkpoint, there might not be many recoverable entries
// but getCurrentLsn should be preserved or reset
await wal2.close();
});
// ============================================================================
// Entry Checksum Tests
// ============================================================================
tap.test('wal: entries should have valid checksums', async () => {
wal = new WAL(TEST_WAL_PATH + '.checksum', { checkpointInterval: 100 });
await wal.initialize();
const doc = { _id: new ObjectId(), name: 'ChecksumTest' };
const lsn = await wal.logInsert('testdb', 'checksumcoll', doc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
expect(entry!.checksum).toBeGreaterThan(0);
expect(typeof entry!.checksum).toEqual('number');
});
// ============================================================================
// Edge Cases
// ============================================================================
tap.test('wal: should handle special characters in document', async () => {
const doc = {
_id: new ObjectId(),
name: 'Test\nWith\tSpecial\r\nChars',
emoji: '🎉',
unicode: '日本語',
};
const lsn = await wal.logInsert('testdb', 'specialcoll', doc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
const recovered = wal.recoverDocument(entry!);
expect(recovered!.name).toEqual('Test\nWith\tSpecial\r\nChars');
expect(recovered!.emoji).toEqual('🎉');
expect(recovered!.unicode).toEqual('日本語');
});
tap.test('wal: should handle binary data in documents', async () => {
const doc = {
_id: new ObjectId(),
binaryField: Buffer.from([0x00, 0xFF, 0x7F, 0x80]),
};
const lsn = await wal.logInsert('testdb', 'binarycoll', doc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
const recovered = wal.recoverDocument(entry!);
expect(recovered).toBeTruthy();
});
tap.test('wal: should handle nested documents', async () => {
const doc = {
_id: new ObjectId(),
level1: {
level2: {
level3: {
value: 'deep',
},
},
},
};
const lsn = await wal.logInsert('testdb', 'nestedcoll', doc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
const recovered = wal.recoverDocument(entry!);
expect(recovered!.level1.level2.level3.value).toEqual('deep');
});
tap.test('wal: should handle arrays in documents', async () => {
const doc = {
_id: new ObjectId(),
tags: ['a', 'b', 'c'],
numbers: [1, 2, 3],
mixed: [1, 'two', { three: 3 }],
};
const lsn = await wal.logInsert('testdb', 'arraycoll', doc as any);
const entries = wal.getEntriesAfter(lsn - 1);
const entry = entries.find(e => e.lsn === lsn);
const recovered = wal.recoverDocument(entry!);
expect(recovered!.tags).toEqual(['a', 'b', 'c']);
expect(recovered!.numbers).toEqual([1, 2, 3]);
expect(recovered!.mixed[2].three).toEqual(3);
});
// ============================================================================
// Cleanup
// ============================================================================
tap.test('wal: cleanup', async () => {
await wal.close();
await cleanupTestFiles();
expect(true).toBeTrue();
});
export default tap.start();