indinis
Version:
A storage library using LSM trees for storage and B-trees for indices with MVCC support
173 lines (143 loc) • 7.33 kB
text/typescript
// @tssrc/test/indinis.test.ts
import { Indinis, StorageValue } from '../index'; // Adjust path if needed
import * as fs from 'fs';
import * as path from 'path';
import { rimraf } from 'rimraf';
const TEST_DATA_DIR_BASE = path.resolve(__dirname, '..', '..', '.test-data', 'indinis-core');
describe('Indinis Core Functionality', () => {
let db: Indinis;
let testDataDir: string;
beforeEach(async () => {
const randomSuffix = `${Date.now()}-${Math.random().toString(36).substring(7)}`;
testDataDir = path.join(TEST_DATA_DIR_BASE, `test-${randomSuffix}`);
await fs.promises.mkdir(testDataDir, { recursive: true });
db = new Indinis(testDataDir);
});
afterEach(async () => {
// Ensure instance cleanup if needed (e.g., db.close())
// Relying on GC for now, which triggers C++ destructors.
// Use rimraf for reliable directory cleanup
if (fs.existsSync(testDataDir)) {
await rimraf(testDataDir);
}
// Add a small delay AFTER cleanup if needed to let C++ fully release files before next test
// await new Promise(res => setTimeout(res, 50));
});
afterAll(async () => {
if (fs.existsSync(TEST_DATA_DIR_BASE)) {
await rimraf(TEST_DATA_DIR_BASE);
}
});
it('should create an Indinis instance successfully', () => {
expect(db).toBeDefined();
expect(db instanceof Indinis).toBe(true);
expect(fs.existsSync(testDataDir)).toBe(true);
expect(fs.existsSync(path.join(testDataDir, 'data'))).toBe(true);
expect(fs.existsSync(path.join(testDataDir, 'indices'))).toBe(true);
});
it('should execute an empty transaction', async () => {
await expect(db.transaction(async tx => {})).resolves.toBeUndefined();
});
it('should allow basic put and get within a transaction', async () => {
const key = 'testKey';
const value = 'testValue';
await db.transaction(async tx => {
await tx.set(key, value);
const retrieved = await tx.get(key);
expect(retrieved).toBe(value);
});
await db.transaction(async tx => {
const retrieved = await tx.get(key);
expect(retrieved).toBe(value);
});
});
it('should abort transaction if callback throws error', async () => {
const key = 'abortKey';
const value = 'should not persist';
const errorMsg = 'Callback failed';
await expect(db.transaction(async tx => {
await tx.set(key, value);
expect(await tx.get(key)).toBe(value);
throw new Error(errorMsg);
})).rejects.toThrow(errorMsg);
await db.transaction(async tx => {
const retrieved = await tx.get(key);
expect(retrieved).toBeNull();
});
});
it('should create an index', async () => {
const result = await db.createIndex('my_index', 'some_field');
expect(result).toBe(true);
// REMOVED: Filesystem check - Index creation is logical, persistence happens later.
// expect(fs.existsSync(path.join(testDataDir, 'indices', 'my_index.idx'))).toBe(true);
});
it('should return false when creating an existing index', async () => {
const result1 = await db.createIndex('duplicate_index', 'field1');
expect(result1).toBe(true);
const result2 = await db.createIndex('duplicate_index', 'field2');
expect(result2).toBe(false); // Should fail as it already exists
});
it('should drop an existing index', async () => {
// 1. Create the index
await db.createIndex('index_to_drop', 'field_drop');
// REMOVED: Filesystem check before drop
// expect(fs.existsSync(path.join(testDataDir, 'indices', 'index_to_drop.idx'))).toBe(true);
// 2. Drop the index
const result = await db.dropIndex('index_to_drop');
expect(result).toBe(true); // Expect drop operation to succeed
// 3. Verify dropping again fails
const result2 = await db.dropIndex('index_to_drop');
expect(result2).toBe(false);
// Optional: Check filesystem *after* drop (if C++ dropIndex removes file immediately)
// If dropIndex only marks for deletion, this check might still fail.
// expect(fs.existsSync(path.join(testDataDir, 'indices', 'index_to_drop.idx'))).toBe(false);
});
it('should return false when dropping a non-existent index', async () => {
const result = await db.dropIndex('non_existent_index');
expect(result).toBe(false);
});
it('should persist data across restarts (basic check)', async () => {
const key = "persist-key";
const value = { message: "Hello Persistence" };
const valueStr = JSON.stringify(value);
// --- Phase 1: Write data using the first instance ---
const db1 = db; // Use the instance from beforeEach
await db1.transaction(async tx => {
await tx.set(key, valueStr);
});
console.log(`[Persist Test] Data written by db1 for key ${key}`);
// --- Crucial Step: Ensure data might be flushed ---
// Option A: Explicit close (if implemented)
// await db1.close();
// Option B: Small delay (Workaround for relying on GC/destructors)
// This gives background flush a chance. Adjust delay as needed.
console.log("[Persist Test] Waiting briefly for potential flush...");
await new Promise(resolve => setTimeout(resolve, 200)); // e.g., 200ms delay
console.log("[Persist Test] Wait finished.");
// --- Phase 2: Re-initialize Indinis instance ---
console.log("[Persist Test] Creating db2 instance...");
const db2 = new Indinis(testDataDir); // Point to the SAME directory
let retrievedValue: any = null;
let retrievedValueRaw: StorageValue | null = null;
console.log("[Persist Test] Starting read transaction with db2...");
try {
await db2.transaction(async tx => {
retrievedValueRaw = await tx.get(key);
console.log(`[Persist Test] tx.get('${key}') returned:`, retrievedValueRaw);
if (typeof retrievedValueRaw === 'string') {
retrievedValue = JSON.parse(retrievedValueRaw);
}
});
} catch (error) {
console.error("[Persist Test] Error during db2 transaction:", error);
// Fail the test explicitly if the read transaction errors
throw error;
}
console.log("[Persist Test] Read transaction finished. Raw value:", retrievedValueRaw);
// *** The Key Assertion ***
expect(retrievedValueRaw).not.toBeNull(); // <<< This failed before
expect(typeof retrievedValueRaw).toBe('string');
expect(retrievedValue).toEqual(value);
// await db2.close(); // if implemented
});
});