192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
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';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test: Deletes persist across restart (tombstone + hint staleness detection)
|
|
// Covers: append_tombstone to data.rdb, hint file data_file_size tracking,
|
|
// stale hint detection on restart
|
|
// ---------------------------------------------------------------------------
|
|
|
|
let tmpDir: string;
|
|
let localDb: smartdb.LocalSmartDb;
|
|
let client: MongoClient;
|
|
let db: Db;
|
|
|
|
function makeTmpDir(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'smartdb-delete-test-'));
|
|
}
|
|
|
|
function cleanTmpDir(dir: string): void {
|
|
if (fs.existsSync(dir)) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Setup
|
|
// ============================================================================
|
|
|
|
tap.test('setup: start local db and insert documents', async () => {
|
|
tmpDir = makeTmpDir();
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const info = await localDb.start();
|
|
client = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('deletetest');
|
|
|
|
const coll = db.collection('items');
|
|
await coll.insertMany([
|
|
{ name: 'keep-1', value: 100 },
|
|
{ name: 'keep-2', value: 200 },
|
|
{ name: 'delete-me', value: 999 },
|
|
{ name: 'keep-3', value: 300 },
|
|
]);
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(4);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Delete and verify
|
|
// ============================================================================
|
|
|
|
tap.test('delete-persistence: delete a document', async () => {
|
|
const coll = db.collection('items');
|
|
const result = await coll.deleteOne({ name: 'delete-me' });
|
|
expect(result.deletedCount).toEqual(1);
|
|
|
|
const remaining = await coll.countDocuments();
|
|
expect(remaining).toEqual(3);
|
|
|
|
const deleted = await coll.findOne({ name: 'delete-me' });
|
|
expect(deleted).toBeNull();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Graceful restart: delete survives
|
|
// ============================================================================
|
|
|
|
tap.test('delete-persistence: graceful stop and restart', async () => {
|
|
await client.close();
|
|
await localDb.stop(); // graceful — writes hint file
|
|
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const info = await localDb.start();
|
|
client = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('deletetest');
|
|
});
|
|
|
|
tap.test('delete-persistence: deleted doc stays deleted after graceful restart', async () => {
|
|
const coll = db.collection('items');
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(3);
|
|
|
|
const deleted = await coll.findOne({ name: 'delete-me' });
|
|
expect(deleted).toBeNull();
|
|
|
|
// The remaining docs are intact
|
|
const keep1 = await coll.findOne({ name: 'keep-1' });
|
|
expect(keep1).toBeTruthy();
|
|
expect(keep1!.value).toEqual(100);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Simulate ungraceful restart: delete after hint write, then restart
|
|
// The hint file data_file_size check should detect the stale hint
|
|
// ============================================================================
|
|
|
|
tap.test('delete-persistence: insert and delete more docs, then restart', async () => {
|
|
const coll = db.collection('items');
|
|
|
|
// Insert a new doc
|
|
await coll.insertOne({ name: 'temporary', value: 777 });
|
|
expect(await coll.countDocuments()).toEqual(4);
|
|
|
|
// Delete it
|
|
await coll.deleteOne({ name: 'temporary' });
|
|
expect(await coll.countDocuments()).toEqual(3);
|
|
|
|
const gone = await coll.findOne({ name: 'temporary' });
|
|
expect(gone).toBeNull();
|
|
});
|
|
|
|
tap.test('delete-persistence: stop and restart again', async () => {
|
|
await client.close();
|
|
await localDb.stop();
|
|
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const info = await localDb.start();
|
|
client = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('deletetest');
|
|
});
|
|
|
|
tap.test('delete-persistence: all deletes survived second restart', async () => {
|
|
const coll = db.collection('items');
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(3);
|
|
|
|
// Both deletes are permanent
|
|
expect(await coll.findOne({ name: 'delete-me' })).toBeNull();
|
|
expect(await coll.findOne({ name: 'temporary' })).toBeNull();
|
|
|
|
// Survivors intact
|
|
const names = (await coll.find({}).toArray()).map(d => d.name).sort();
|
|
expect(names).toEqual(['keep-1', 'keep-2', 'keep-3']);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Delete all docs and verify empty after restart
|
|
// ============================================================================
|
|
|
|
tap.test('delete-persistence: delete all remaining docs', async () => {
|
|
const coll = db.collection('items');
|
|
await coll.deleteMany({});
|
|
expect(await coll.countDocuments()).toEqual(0);
|
|
});
|
|
|
|
tap.test('delete-persistence: restart with empty collection', async () => {
|
|
await client.close();
|
|
await localDb.stop();
|
|
|
|
localDb = new smartdb.LocalSmartDb({ folderPath: tmpDir });
|
|
const info = await localDb.start();
|
|
client = new MongoClient(info.connectionUri, {
|
|
directConnection: true,
|
|
serverSelectionTimeoutMS: 5000,
|
|
});
|
|
await client.connect();
|
|
db = client.db('deletetest');
|
|
});
|
|
|
|
tap.test('delete-persistence: collection is empty after restart', async () => {
|
|
const coll = db.collection('items');
|
|
const count = await coll.countDocuments();
|
|
expect(count).toEqual(0);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Cleanup
|
|
// ============================================================================
|
|
|
|
tap.test('delete-persistence: cleanup', async () => {
|
|
await client.close();
|
|
await localDb.stop();
|
|
cleanTmpDir(tmpDir);
|
|
});
|
|
|
|
export default tap.start();
|