From 29b2b5cea10d787952ff60479254298dbc2c34ff Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 12 Feb 2026 12:08:55 -0500 Subject: [PATCH 1/5] checkpoint api feature support to align with tdb v8.3.2+ --- package-lock.json | 4 +-- package.json | 2 +- src/ffi.ts | 1 + src/tidesdb.test.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++ src/tidesdb.ts | 13 +++++++++ 5 files changed, 81 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25c783d..2fdb13d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tidesdb", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tidesdb", - "version": "0.4.0", + "version": "0.5.1", "license": "MPL-2.0", "dependencies": { "koffi": "^2.9.0" diff --git a/package.json b/package.json index e8b72a7..082aeea 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ffi.ts b/src/ffi.ts index 24b9708..35e62cb 100644 --- a/src/ffi.ts +++ b/src/ffi.ts @@ -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)'); diff --git a/src/tidesdb.test.ts b/src/tidesdb.test.ts index eda32e5..cf93d44 100644 --- a/src/tidesdb.test.ts +++ b/src/tidesdb.test.ts @@ -474,6 +474,70 @@ describe('TidesDB', () => { }); }); + describe('Checkpoint', () => { + test('create checkpoint of database', () => { + db.createColumnFamily('test_cf'); + const cf = db.getColumnFamily('test_cf'); + + // Insert data + const txn = db.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 + const checkpointDir = path.join(tempDir, 'checkpoint'); + db.checkpoint(checkpointDir); + + // Verify checkpoint directory was created + expect(fs.existsSync(checkpointDir)).toBe(true); + }); + + test('checkpoint to existing non-empty directory throws', () => { + db.createColumnFamily('test_cf'); + + // Create a non-empty directory + const checkpointDir = path.join(tempDir, 'checkpoint_exists'); + fs.mkdirSync(checkpointDir, { recursive: true }); + fs.writeFileSync(path.join(checkpointDir, 'dummy'), 'data'); + + expect(() => db.checkpoint(checkpointDir)).toThrow(); + }); + + test('checkpoint can be opened as a database', () => { + db.createColumnFamily('test_cf'); + const cf = db.getColumnFamily('test_cf'); + + // Insert data + const txn = db.beginTransaction(); + txn.put(cf, Buffer.from('cp_key'), Buffer.from('cp_value'), -1); + txn.commit(); + txn.free(); + + // Create checkpoint + const checkpointDir = path.join(tempDir, 'checkpoint_open'); + db.checkpoint(checkpointDir); + + // Open the checkpoint as a separate database + const cpDb = TidesDB.open({ + dbPath: checkpointDir, + numFlushThreads: 1, + numCompactionThreads: 1, + }); + + try { + const cpCf = cpDb.getColumnFamily('test_cf'); + const readTxn = cpDb.beginTransaction(); + const value = readTxn.get(cpCf, Buffer.from('cp_key')); + expect(value.toString()).toBe('cp_value'); + readTxn.free(); + } finally { + cpDb.close(); + } + }); + }); + describe('Statistics', () => { test('get column family stats', () => { db.createColumnFamily('test_cf'); diff --git a/src/tidesdb.ts b/src/tidesdb.ts index 9c68d31..1fecb0c 100644 --- a/src/tidesdb.ts +++ b/src/tidesdb.ts @@ -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'; @@ -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. */ From b06d0b5d18970a391b0dc69c560fdd3ffa8a1f9a Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 12 Feb 2026 12:16:58 -0500 Subject: [PATCH 2/5] correct flaky checkpoint test --- src/tidesdb.test.ts | 65 +++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/tidesdb.test.ts b/src/tidesdb.test.ts index cf93d44..c1a2229 100644 --- a/src/tidesdb.test.ts +++ b/src/tidesdb.test.ts @@ -486,23 +486,31 @@ describe('TidesDB', () => { txn.commit(); txn.free(); - // Create checkpoint - const checkpointDir = path.join(tempDir, 'checkpoint'); - db.checkpoint(checkpointDir); + // Create checkpoint (must be outside the database directory) + const checkpointDir = createTempDir(); + fs.rmSync(checkpointDir, { recursive: true, force: true }); + try { + db.checkpoint(checkpointDir); - // Verify checkpoint directory was created - expect(fs.existsSync(checkpointDir)).toBe(true); + // Verify checkpoint directory was created + expect(fs.existsSync(checkpointDir)).toBe(true); + } finally { + removeTempDir(checkpointDir); + } }); test('checkpoint to existing non-empty directory throws', () => { db.createColumnFamily('test_cf'); - // Create a non-empty directory - const checkpointDir = path.join(tempDir, 'checkpoint_exists'); - fs.mkdirSync(checkpointDir, { recursive: true }); + // Create a non-empty directory outside the database path + const checkpointDir = createTempDir(); fs.writeFileSync(path.join(checkpointDir, 'dummy'), 'data'); - expect(() => db.checkpoint(checkpointDir)).toThrow(); + try { + expect(() => db.checkpoint(checkpointDir)).toThrow(); + } finally { + removeTempDir(checkpointDir); + } }); test('checkpoint can be opened as a database', () => { @@ -515,25 +523,30 @@ describe('TidesDB', () => { txn.commit(); txn.free(); - // Create checkpoint - const checkpointDir = path.join(tempDir, 'checkpoint_open'); - db.checkpoint(checkpointDir); - - // Open the checkpoint as a separate database - const cpDb = TidesDB.open({ - dbPath: checkpointDir, - numFlushThreads: 1, - numCompactionThreads: 1, - }); - + // Create checkpoint (must be outside the database directory) + const checkpointDir = createTempDir(); + fs.rmSync(checkpointDir, { recursive: true, force: true }); try { - const cpCf = cpDb.getColumnFamily('test_cf'); - const readTxn = cpDb.beginTransaction(); - const value = readTxn.get(cpCf, Buffer.from('cp_key')); - expect(value.toString()).toBe('cp_value'); - readTxn.free(); + db.checkpoint(checkpointDir); + + // Open the checkpoint as a separate database + const cpDb = TidesDB.open({ + dbPath: checkpointDir, + numFlushThreads: 1, + numCompactionThreads: 1, + }); + + try { + const cpCf = cpDb.getColumnFamily('test_cf'); + const readTxn = cpDb.beginTransaction(); + const value = readTxn.get(cpCf, Buffer.from('cp_key')); + expect(value.toString()).toBe('cp_value'); + readTxn.free(); + } finally { + cpDb.close(); + } } finally { - cpDb.close(); + removeTempDir(checkpointDir); } }); }); From b3d1ce9c7e23853920a8247d98523f08dba7a625 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 12 Feb 2026 12:34:25 -0500 Subject: [PATCH 3/5] pass empty dir for checkpoint test --- src/tidesdb.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/tidesdb.test.ts b/src/tidesdb.test.ts index c1a2229..8e5a6be 100644 --- a/src/tidesdb.test.ts +++ b/src/tidesdb.test.ts @@ -486,14 +486,14 @@ describe('TidesDB', () => { txn.commit(); txn.free(); - // Create checkpoint (must be outside the database directory) + // Create checkpoint (must be outside the database directory, empty dir) const checkpointDir = createTempDir(); - fs.rmSync(checkpointDir, { recursive: true, force: true }); try { db.checkpoint(checkpointDir); - // Verify checkpoint directory was created + // Verify checkpoint directory exists with contents expect(fs.existsSync(checkpointDir)).toBe(true); + expect(fs.readdirSync(checkpointDir).length).toBeGreaterThan(0); } finally { removeTempDir(checkpointDir); } @@ -523,9 +523,8 @@ describe('TidesDB', () => { txn.commit(); txn.free(); - // Create checkpoint (must be outside the database directory) + // Create checkpoint (must be outside the database directory, empty dir) const checkpointDir = createTempDir(); - fs.rmSync(checkpointDir, { recursive: true, force: true }); try { db.checkpoint(checkpointDir); From 6159579eefdb34934e0a90ab06dbab7d9178e50d Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 12 Feb 2026 12:45:26 -0500 Subject: [PATCH 4/5] pass non-existent directory to checkpoint on Windows. the C library's tidesdb_checkpoint expects to create the checkpoint directory itself. Using fs.mkdtempSync pre-created the directory, which worked on Linux/macOS but caused subdirectory creation to fail on Windows. Match the Go binding's approach by ensuring the path does not exist before calling checkpoint --- src/tidesdb.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/tidesdb.test.ts b/src/tidesdb.test.ts index 8e5a6be..f4fdc78 100644 --- a/src/tidesdb.test.ts +++ b/src/tidesdb.test.ts @@ -486,8 +486,9 @@ describe('TidesDB', () => { txn.commit(); txn.free(); - // Create checkpoint (must be outside the database directory, empty dir) - const checkpointDir = createTempDir(); + // Create checkpoint (directory must NOT exist; the C library creates it) + const checkpointDir = path.join(os.tmpdir(), `tidesdb-checkpoint-${Date.now()}`); + fs.rmSync(checkpointDir, { recursive: true, force: true }); try { db.checkpoint(checkpointDir); @@ -503,7 +504,8 @@ describe('TidesDB', () => { db.createColumnFamily('test_cf'); // Create a non-empty directory outside the database path - const checkpointDir = createTempDir(); + const checkpointDir = path.join(os.tmpdir(), `tidesdb-checkpoint-nonempty-${Date.now()}`); + fs.mkdirSync(checkpointDir, { recursive: true }); fs.writeFileSync(path.join(checkpointDir, 'dummy'), 'data'); try { @@ -523,8 +525,9 @@ describe('TidesDB', () => { txn.commit(); txn.free(); - // Create checkpoint (must be outside the database directory, empty dir) - const checkpointDir = createTempDir(); + // Create checkpoint (directory must NOT exist; the C library creates it) + const checkpointDir = path.join(os.tmpdir(), `tidesdb-checkpoint-open-${Date.now()}`); + fs.rmSync(checkpointDir, { recursive: true, force: true }); try { db.checkpoint(checkpointDir); From 1a02a289b1df5d7f597f9e77770461e0e605ed33 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Thu, 12 Feb 2026 13:15:30 -0500 Subject: [PATCH 5/5] fix tests use relative paths in checkpoint tests to fix windows ci failure. the c library ensure_parent_dir function fails on absolute windows paths because it tries to stat the bare drive letter component without a trailing backslash --- src/tidesdb.test.ts | 64 ++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/tidesdb.test.ts b/src/tidesdb.test.ts index f4fdc78..1e78533 100644 --- a/src/tidesdb.test.ts +++ b/src/tidesdb.test.ts @@ -475,80 +475,102 @@ 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', () => { - db.createColumnFamily('test_cf'); - const cf = db.getColumnFamily('test_cf'); + cpDb.createColumnFamily('test_cf'); + const cf = cpDb.getColumnFamily('test_cf'); // Insert data - const txn = db.beginTransaction(); + 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 = path.join(os.tmpdir(), `tidesdb-checkpoint-${Date.now()}`); + const checkpointDir = `tidesdb-checkpoint-${Date.now()}`; fs.rmSync(checkpointDir, { recursive: true, force: true }); try { - db.checkpoint(checkpointDir); + cpDb.checkpoint(checkpointDir); // Verify checkpoint directory exists with contents expect(fs.existsSync(checkpointDir)).toBe(true); expect(fs.readdirSync(checkpointDir).length).toBeGreaterThan(0); } finally { - removeTempDir(checkpointDir); + fs.rmSync(checkpointDir, { recursive: true, force: true }); } }); test('checkpoint to existing non-empty directory throws', () => { - db.createColumnFamily('test_cf'); + cpDb.createColumnFamily('test_cf'); - // Create a non-empty directory outside the database path - const checkpointDir = path.join(os.tmpdir(), `tidesdb-checkpoint-nonempty-${Date.now()}`); + // 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(() => db.checkpoint(checkpointDir)).toThrow(); + expect(() => cpDb.checkpoint(checkpointDir)).toThrow(); } finally { - removeTempDir(checkpointDir); + fs.rmSync(checkpointDir, { recursive: true, force: true }); } }); test('checkpoint can be opened as a database', () => { - db.createColumnFamily('test_cf'); - const cf = db.getColumnFamily('test_cf'); + cpDb.createColumnFamily('test_cf'); + const cf = cpDb.getColumnFamily('test_cf'); // Insert data - const txn = db.beginTransaction(); + 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 = path.join(os.tmpdir(), `tidesdb-checkpoint-open-${Date.now()}`); + const checkpointDir = `tidesdb-checkpoint-open-${Date.now()}`; fs.rmSync(checkpointDir, { recursive: true, force: true }); try { - db.checkpoint(checkpointDir); + cpDb.checkpoint(checkpointDir); // Open the checkpoint as a separate database - const cpDb = TidesDB.open({ + const cpOpenDb = TidesDB.open({ dbPath: checkpointDir, numFlushThreads: 1, numCompactionThreads: 1, }); try { - const cpCf = cpDb.getColumnFamily('test_cf'); - const readTxn = cpDb.beginTransaction(); + 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 { - cpDb.close(); + cpOpenDb.close(); } } finally { - removeTempDir(checkpointDir); + fs.rmSync(checkpointDir, { recursive: true, force: true }); } }); });