Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tidesdb",
"version": "0.5.0",
"version": "0.5.1",
"description": "TypeScript bindings for TidesDB - A high-performance embedded key-value storage engine",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export const tidesdb_flush_memtable = lib.func('int tidesdb_flush_memtable(tides
export const tidesdb_is_flushing = lib.func('int tidesdb_is_flushing(tidesdb_column_family_t *cf)');
export const tidesdb_is_compacting = lib.func('int tidesdb_is_compacting(tidesdb_column_family_t *cf)');
export const tidesdb_backup = lib.func('int tidesdb_backup(tidesdb_t *db, char *dir)');
export const tidesdb_checkpoint = lib.func('int tidesdb_checkpoint(tidesdb_t *db, const char *checkpoint_dir)');

// Comparator operations
export const tidesdb_register_comparator = lib.func('int tidesdb_register_comparator(tidesdb_t *db, const char *name, void *fn, const char *ctx_str, void *ctx)');
Expand Down
101 changes: 101 additions & 0 deletions src/tidesdb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,107 @@ describe('TidesDB', () => {
});
});

describe('Checkpoint', () => {
// Use relative paths for checkpoint tests to avoid a Windows bug in the
// C library's tidesdb_checkpoint_ensure_parent_dir: it fails to stat/mkdir
// the drive-letter component (e.g. "C:") of absolute paths. Other bindings
// (Go, Lua) also use relative paths. Both DB and checkpoint must be on the
// same volume for hard links, so the DB is also opened from a relative path.
let cpDb: TidesDB;
let cpDbDir: string;

beforeEach(() => {
cpDbDir = `tidesdb-cp-test-${Date.now()}`;
cpDb = TidesDB.open({
dbPath: cpDbDir,
numFlushThreads: 2,
numCompactionThreads: 2,
});
});

afterEach(() => {
try { cpDb.close(); } catch { /* ignore */ }
fs.rmSync(cpDbDir, { recursive: true, force: true });
});

test('create checkpoint of database', () => {
cpDb.createColumnFamily('test_cf');
const cf = cpDb.getColumnFamily('test_cf');

// Insert data
const txn = cpDb.beginTransaction();
txn.put(cf, Buffer.from('cp_key1'), Buffer.from('cp_value1'), -1);
txn.put(cf, Buffer.from('cp_key2'), Buffer.from('cp_value2'), -1);
txn.commit();
txn.free();

// Create checkpoint (directory must NOT exist; the C library creates it)
const checkpointDir = `tidesdb-checkpoint-${Date.now()}`;
fs.rmSync(checkpointDir, { recursive: true, force: true });
try {
cpDb.checkpoint(checkpointDir);

// Verify checkpoint directory exists with contents
expect(fs.existsSync(checkpointDir)).toBe(true);
expect(fs.readdirSync(checkpointDir).length).toBeGreaterThan(0);
} finally {
fs.rmSync(checkpointDir, { recursive: true, force: true });
}
});

test('checkpoint to existing non-empty directory throws', () => {
cpDb.createColumnFamily('test_cf');

// Create a non-empty directory
const checkpointDir = `tidesdb-checkpoint-nonempty-${Date.now()}`;
fs.mkdirSync(checkpointDir, { recursive: true });
fs.writeFileSync(path.join(checkpointDir, 'dummy'), 'data');

try {
expect(() => cpDb.checkpoint(checkpointDir)).toThrow();
} finally {
fs.rmSync(checkpointDir, { recursive: true, force: true });
}
});

test('checkpoint can be opened as a database', () => {
cpDb.createColumnFamily('test_cf');
const cf = cpDb.getColumnFamily('test_cf');

// Insert data
const txn = cpDb.beginTransaction();
txn.put(cf, Buffer.from('cp_key'), Buffer.from('cp_value'), -1);
txn.commit();
txn.free();

// Create checkpoint (directory must NOT exist; the C library creates it)
const checkpointDir = `tidesdb-checkpoint-open-${Date.now()}`;
fs.rmSync(checkpointDir, { recursive: true, force: true });
try {
cpDb.checkpoint(checkpointDir);

// Open the checkpoint as a separate database
const cpOpenDb = TidesDB.open({
dbPath: checkpointDir,
numFlushThreads: 1,
numCompactionThreads: 1,
});

try {
const cpCf = cpOpenDb.getColumnFamily('test_cf');
const readTxn = cpOpenDb.beginTransaction();
const value = readTxn.get(cpCf, Buffer.from('cp_key'));
expect(value.toString()).toBe('cp_value');
readTxn.free();
} finally {
cpOpenDb.close();
}
} finally {
fs.rmSync(checkpointDir, { recursive: true, force: true });
}
});
});

describe('Statistics', () => {
test('get column family stats', () => {
db.createColumnFamily('test_cf');
Expand Down
13 changes: 13 additions & 0 deletions src/tidesdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
tidesdb_register_comparator,
tidesdb_get_cache_stats,
tidesdb_backup,
tidesdb_checkpoint,
} from './ffi';
import { checkResult, TidesDBError } from './error';
import { ColumnFamily } from './column-family';
Expand Down Expand Up @@ -324,6 +325,18 @@ export class TidesDB {
checkResult(result, 'failed to create backup');
}

/**
* Create a lightweight, near-instant snapshot of the database using hard links.
* Much faster than backup() as it uses hard links instead of copying SSTable data.
* @param dir Checkpoint directory path (must be non-existent or empty, same filesystem).
*/
checkpoint(dir: string): void {
if (!this._db) throw new Error('Database has been closed');

const result = tidesdb_checkpoint(this._db, dir);
checkResult(result, 'failed to create checkpoint');
}

/**
* Get statistics about the block cache.
*/
Expand Down
Loading