fix(tsmdb): add comprehensive unit tests for tsmdb components: checksum, query planner, index engine, session, and WAL
This commit is contained in:
417
test/test.tsmdb.indexengine.ts
Normal file
417
test/test.tsmdb.indexengine.ts
Normal file
@@ -0,0 +1,417 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartmongo from '../ts/index.js';
|
||||
|
||||
const { IndexEngine, MemoryStorageAdapter, ObjectId } = smartmongo.tsmdb;
|
||||
|
||||
let storage: InstanceType<typeof MemoryStorageAdapter>;
|
||||
let indexEngine: InstanceType<typeof IndexEngine>;
|
||||
|
||||
const TEST_DB = 'testdb';
|
||||
const TEST_COLL = 'indextest';
|
||||
|
||||
// ============================================================================
|
||||
// Setup
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: should create IndexEngine instance', async () => {
|
||||
storage = new MemoryStorageAdapter();
|
||||
await storage.initialize();
|
||||
await storage.createCollection(TEST_DB, TEST_COLL);
|
||||
|
||||
indexEngine = new IndexEngine(TEST_DB, TEST_COLL, storage);
|
||||
expect(indexEngine).toBeTruthy();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Index Creation Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: createIndex should create single-field index', async () => {
|
||||
const indexName = await indexEngine.createIndex({ name: 1 });
|
||||
|
||||
expect(indexName).toEqual('name_1');
|
||||
|
||||
const exists = await indexEngine.indexExists('name_1');
|
||||
expect(exists).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: createIndex should create compound index', async () => {
|
||||
const indexName = await indexEngine.createIndex({ city: 1, state: -1 });
|
||||
|
||||
expect(indexName).toEqual('city_1_state_-1');
|
||||
|
||||
const exists = await indexEngine.indexExists('city_1_state_-1');
|
||||
expect(exists).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: createIndex should use custom name if provided', async () => {
|
||||
const indexName = await indexEngine.createIndex({ email: 1 }, { name: 'custom_email_index' });
|
||||
|
||||
expect(indexName).toEqual('custom_email_index');
|
||||
|
||||
const exists = await indexEngine.indexExists('custom_email_index');
|
||||
expect(exists).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: createIndex should handle unique option', async () => {
|
||||
const indexName = await indexEngine.createIndex({ uniqueField: 1 }, { unique: true });
|
||||
|
||||
expect(indexName).toEqual('uniqueField_1');
|
||||
|
||||
const indexes = await indexEngine.listIndexes();
|
||||
const uniqueIndex = indexes.find(i => i.name === 'uniqueField_1');
|
||||
expect(uniqueIndex!.unique).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: createIndex should handle sparse option', async () => {
|
||||
const indexName = await indexEngine.createIndex({ sparseField: 1 }, { sparse: true });
|
||||
|
||||
expect(indexName).toEqual('sparseField_1');
|
||||
|
||||
const indexes = await indexEngine.listIndexes();
|
||||
const sparseIndex = indexes.find(i => i.name === 'sparseField_1');
|
||||
expect(sparseIndex!.sparse).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: createIndex should return existing index name if already exists', async () => {
|
||||
const indexName1 = await indexEngine.createIndex({ existingField: 1 }, { name: 'existing_idx' });
|
||||
const indexName2 = await indexEngine.createIndex({ existingField: 1 }, { name: 'existing_idx' });
|
||||
|
||||
expect(indexName1).toEqual(indexName2);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Index Listing and Existence Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: listIndexes should return all indexes', async () => {
|
||||
const indexes = await indexEngine.listIndexes();
|
||||
|
||||
expect(indexes.length).toBeGreaterThanOrEqual(5); // _id_ + created indexes
|
||||
expect(indexes.some(i => i.name === '_id_')).toBeTrue();
|
||||
expect(indexes.some(i => i.name === 'name_1')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: indexExists should return true for existing index', async () => {
|
||||
const exists = await indexEngine.indexExists('name_1');
|
||||
expect(exists).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: indexExists should return false for non-existent index', async () => {
|
||||
const exists = await indexEngine.indexExists('nonexistent_index');
|
||||
expect(exists).toBeFalse();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Document Operations and Index Updates
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: should insert documents for index testing', async () => {
|
||||
// Create a fresh index engine for document operations
|
||||
await storage.dropCollection(TEST_DB, TEST_COLL);
|
||||
await storage.createCollection(TEST_DB, TEST_COLL);
|
||||
|
||||
indexEngine = new IndexEngine(TEST_DB, TEST_COLL, storage);
|
||||
|
||||
// Create indexes first
|
||||
await indexEngine.createIndex({ age: 1 });
|
||||
await indexEngine.createIndex({ category: 1 });
|
||||
|
||||
// Insert test documents
|
||||
const docs = [
|
||||
{ _id: new ObjectId(), name: 'Alice', age: 25, category: 'A' },
|
||||
{ _id: new ObjectId(), name: 'Bob', age: 30, category: 'B' },
|
||||
{ _id: new ObjectId(), name: 'Charlie', age: 35, category: 'A' },
|
||||
{ _id: new ObjectId(), name: 'Diana', age: 28, category: 'C' },
|
||||
{ _id: new ObjectId(), name: 'Eve', age: 30, category: 'B' },
|
||||
];
|
||||
|
||||
for (const doc of docs) {
|
||||
const stored = await storage.insertOne(TEST_DB, TEST_COLL, doc);
|
||||
await indexEngine.onInsert(stored);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('indexengine: onInsert should update indexes', async () => {
|
||||
const newDoc = {
|
||||
_id: new ObjectId(),
|
||||
name: 'Frank',
|
||||
age: 40,
|
||||
category: 'D',
|
||||
};
|
||||
|
||||
const stored = await storage.insertOne(TEST_DB, TEST_COLL, newDoc);
|
||||
await indexEngine.onInsert(stored);
|
||||
|
||||
// Find by the indexed field
|
||||
const candidates = await indexEngine.findCandidateIds({ age: 40 });
|
||||
expect(candidates).toBeTruthy();
|
||||
expect(candidates!.size).toEqual(1);
|
||||
});
|
||||
|
||||
tap.test('indexengine: onUpdate should update indexes correctly', async () => {
|
||||
// Get an existing document
|
||||
const docs = await storage.findAll(TEST_DB, TEST_COLL);
|
||||
const oldDoc = docs.find(d => d.name === 'Alice')!;
|
||||
|
||||
// Update the document
|
||||
const newDoc = { ...oldDoc, age: 26 };
|
||||
await storage.updateById(TEST_DB, TEST_COLL, oldDoc._id, newDoc);
|
||||
await indexEngine.onUpdate(oldDoc, newDoc);
|
||||
|
||||
// Old value should not be in index
|
||||
const oldCandidates = await indexEngine.findCandidateIds({ age: 25 });
|
||||
expect(oldCandidates).toBeTruthy();
|
||||
expect(oldCandidates!.has(oldDoc._id.toHexString())).toBeFalse();
|
||||
|
||||
// New value should be in index
|
||||
const newCandidates = await indexEngine.findCandidateIds({ age: 26 });
|
||||
expect(newCandidates).toBeTruthy();
|
||||
expect(newCandidates!.has(oldDoc._id.toHexString())).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: onDelete should remove from indexes', async () => {
|
||||
const docs = await storage.findAll(TEST_DB, TEST_COLL);
|
||||
const docToDelete = docs.find(d => d.name === 'Frank')!;
|
||||
|
||||
await storage.deleteById(TEST_DB, TEST_COLL, docToDelete._id);
|
||||
await indexEngine.onDelete(docToDelete);
|
||||
|
||||
const candidates = await indexEngine.findCandidateIds({ age: 40 });
|
||||
expect(candidates).toBeTruthy();
|
||||
expect(candidates!.has(docToDelete._id.toHexString())).toBeFalse();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// findCandidateIds Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: findCandidateIds with equality filter', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({ age: 30 });
|
||||
|
||||
expect(candidates).toBeTruthy();
|
||||
expect(candidates!.size).toEqual(2); // Bob and Eve both have age 30
|
||||
});
|
||||
|
||||
tap.test('indexengine: findCandidateIds with $in filter', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({ age: { $in: [28, 30] } });
|
||||
|
||||
expect(candidates).toBeTruthy();
|
||||
expect(candidates!.size).toEqual(3); // Diana (28), Bob (30), Eve (30)
|
||||
});
|
||||
|
||||
tap.test('indexengine: findCandidateIds with no matching index', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({ nonIndexedField: 'value' });
|
||||
|
||||
// Should return null when no index can be used
|
||||
expect(candidates).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('indexengine: findCandidateIds with empty filter', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({});
|
||||
|
||||
// Empty filter = no index can be used
|
||||
expect(candidates).toBeNull();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Range Query Tests (B-Tree)
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: findCandidateIds with $gt', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({ age: { $gt: 30 } });
|
||||
|
||||
expect(candidates).toBeTruthy();
|
||||
// Charlie (35) is > 30
|
||||
expect(candidates!.size).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
tap.test('indexengine: findCandidateIds with $lt', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({ age: { $lt: 28 } });
|
||||
|
||||
expect(candidates).toBeTruthy();
|
||||
// Alice (26) is < 28
|
||||
expect(candidates!.size).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
tap.test('indexengine: findCandidateIds with $gte', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({ age: { $gte: 30 } });
|
||||
|
||||
expect(candidates).toBeTruthy();
|
||||
// Bob (30), Eve (30), Charlie (35)
|
||||
expect(candidates!.size).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
tap.test('indexengine: findCandidateIds with $lte', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({ age: { $lte: 28 } });
|
||||
|
||||
expect(candidates).toBeTruthy();
|
||||
// Alice (26), Diana (28)
|
||||
expect(candidates!.size).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
tap.test('indexengine: findCandidateIds with range $gt and $lt', async () => {
|
||||
const candidates = await indexEngine.findCandidateIds({ age: { $gt: 26, $lt: 35 } });
|
||||
|
||||
expect(candidates).toBeTruthy();
|
||||
// Diana (28), Bob (30), Eve (30) are between 26 and 35 exclusive
|
||||
expect(candidates!.size).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Index Selection Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: selectIndex should return best index for equality', async () => {
|
||||
const result = indexEngine.selectIndex({ age: 30 });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.name).toEqual('age_1');
|
||||
});
|
||||
|
||||
tap.test('indexengine: selectIndex should return best index for range query', async () => {
|
||||
const result = indexEngine.selectIndex({ age: { $gt: 25 } });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result!.name).toEqual('age_1');
|
||||
});
|
||||
|
||||
tap.test('indexengine: selectIndex should return null for no matching filter', async () => {
|
||||
const result = indexEngine.selectIndex({ nonIndexedField: 'value' });
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('indexengine: selectIndex should return null for empty filter', async () => {
|
||||
const result = indexEngine.selectIndex({});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
tap.test('indexengine: selectIndex should prefer more specific indexes', async () => {
|
||||
// Create a compound index
|
||||
await indexEngine.createIndex({ age: 1, category: 1 }, { name: 'age_category_compound' });
|
||||
|
||||
// Query that matches compound index
|
||||
const result = indexEngine.selectIndex({ age: 30, category: 'B' });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
// Should prefer the compound index since it covers more fields
|
||||
expect(result!.name).toEqual('age_category_compound');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Drop Index Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: dropIndex should remove the index', async () => {
|
||||
await indexEngine.createIndex({ dropTest: 1 }, { name: 'drop_test_idx' });
|
||||
expect(await indexEngine.indexExists('drop_test_idx')).toBeTrue();
|
||||
|
||||
await indexEngine.dropIndex('drop_test_idx');
|
||||
expect(await indexEngine.indexExists('drop_test_idx')).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('indexengine: dropIndex should throw for _id index', async () => {
|
||||
let threw = false;
|
||||
try {
|
||||
await indexEngine.dropIndex('_id_');
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: dropIndex should throw for non-existent index', async () => {
|
||||
let threw = false;
|
||||
try {
|
||||
await indexEngine.dropIndex('nonexistent_index');
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
}
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('indexengine: dropAllIndexes should remove all indexes except _id', async () => {
|
||||
// Create some indexes to drop
|
||||
await indexEngine.createIndex({ toDrop1: 1 });
|
||||
await indexEngine.createIndex({ toDrop2: 1 });
|
||||
|
||||
await indexEngine.dropAllIndexes();
|
||||
|
||||
const indexes = await indexEngine.listIndexes();
|
||||
expect(indexes.length).toEqual(1);
|
||||
expect(indexes[0].name).toEqual('_id_');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Unique Index Constraint Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: unique index should prevent duplicate inserts', async () => {
|
||||
// Create fresh collection
|
||||
await storage.dropCollection(TEST_DB, 'uniquetest');
|
||||
await storage.createCollection(TEST_DB, 'uniquetest');
|
||||
|
||||
const uniqueIndexEngine = new IndexEngine(TEST_DB, 'uniquetest', storage);
|
||||
await uniqueIndexEngine.createIndex({ email: 1 }, { unique: true });
|
||||
|
||||
// Insert first document
|
||||
const doc1 = { _id: new ObjectId(), email: 'test@example.com', name: 'Test' };
|
||||
const stored1 = await storage.insertOne(TEST_DB, 'uniquetest', doc1);
|
||||
await uniqueIndexEngine.onInsert(stored1);
|
||||
|
||||
// Try to insert duplicate
|
||||
const doc2 = { _id: new ObjectId(), email: 'test@example.com', name: 'Test2' };
|
||||
const stored2 = await storage.insertOne(TEST_DB, 'uniquetest', doc2);
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await uniqueIndexEngine.onInsert(stored2);
|
||||
} catch (e: any) {
|
||||
threw = true;
|
||||
expect(e.message).toContain('duplicate key');
|
||||
}
|
||||
|
||||
expect(threw).toBeTrue();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Sparse Index Tests
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: sparse index should not include documents without the field', async () => {
|
||||
// Create fresh collection
|
||||
await storage.dropCollection(TEST_DB, 'sparsetest');
|
||||
await storage.createCollection(TEST_DB, 'sparsetest');
|
||||
|
||||
const sparseIndexEngine = new IndexEngine(TEST_DB, 'sparsetest', storage);
|
||||
await sparseIndexEngine.createIndex({ optionalField: 1 }, { sparse: true });
|
||||
|
||||
// Insert doc with the field
|
||||
const doc1 = { _id: new ObjectId(), optionalField: 'hasValue', name: 'HasField' };
|
||||
const stored1 = await storage.insertOne(TEST_DB, 'sparsetest', doc1);
|
||||
await sparseIndexEngine.onInsert(stored1);
|
||||
|
||||
// Insert doc without the field
|
||||
const doc2 = { _id: new ObjectId(), name: 'NoField' };
|
||||
const stored2 = await storage.insertOne(TEST_DB, 'sparsetest', doc2);
|
||||
await sparseIndexEngine.onInsert(stored2);
|
||||
|
||||
// Search for documents with the field
|
||||
const candidates = await sparseIndexEngine.findCandidateIds({ optionalField: 'hasValue' });
|
||||
expect(candidates).toBeTruthy();
|
||||
expect(candidates!.size).toEqual(1);
|
||||
expect(candidates!.has(stored1._id.toHexString())).toBeTrue();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Cleanup
|
||||
// ============================================================================
|
||||
|
||||
tap.test('indexengine: cleanup', async () => {
|
||||
await storage.close();
|
||||
expect(true).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user