diff --git a/pom.xml b/pom.xml
index e4b3640..7aa58ba 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.tidesdb
tidesdb-java
- 0.6.0
+ 0.6.1
jar
TidesDB Java
diff --git a/src/main/c/com_tidesdb_TidesDB.c b/src/main/c/com_tidesdb_TidesDB.c
index 7072b56..cef7a40 100644
--- a/src/main/c/com_tidesdb_TidesDB.c
+++ b/src/main/c/com_tidesdb_TidesDB.c
@@ -366,6 +366,27 @@ JNIEXPORT void JNICALL Java_com_tidesdb_TidesDB_nativeBackup(JNIEnv *env, jclass
}
}
+JNIEXPORT void JNICALL Java_com_tidesdb_TidesDB_nativeCheckpoint(JNIEnv *env, jclass cls,
+ jlong handle, jstring dir)
+{
+ tidesdb_t *db = (tidesdb_t *)(uintptr_t)handle;
+ const char *checkpointDir = (*env)->GetStringUTFChars(env, dir, NULL);
+ if (checkpointDir == NULL)
+ {
+ throwTidesDBException(env, TDB_ERR_MEMORY, "Failed to get checkpoint directory");
+ return;
+ }
+
+ int result = tidesdb_checkpoint(db, checkpointDir);
+
+ (*env)->ReleaseStringUTFChars(env, dir, checkpointDir);
+
+ if (result != TDB_SUCCESS)
+ {
+ throwTidesDBException(env, result, getErrorMessage(result));
+ }
+}
+
JNIEXPORT void JNICALL Java_com_tidesdb_TidesDB_nativeRenameColumnFamily(JNIEnv *env, jclass cls,
jlong handle,
jstring oldName,
diff --git a/src/main/java/com/tidesdb/TidesDB.java b/src/main/java/com/tidesdb/TidesDB.java
index 4281140..5c0b7ed 100644
--- a/src/main/java/com/tidesdb/TidesDB.java
+++ b/src/main/java/com/tidesdb/TidesDB.java
@@ -228,6 +228,21 @@ public void backup(String dir) throws TidesDBException {
nativeBackup(nativeHandle, dir);
}
+ /**
+ * Creates a lightweight, near-instant snapshot of an open database using hard links
+ * instead of copying SSTable data.
+ *
+ * @param dir the checkpoint directory (must be non-existent or empty)
+ * @throws TidesDBException if the checkpoint fails
+ */
+ public void checkpoint(String dir) throws TidesDBException {
+ checkNotClosed();
+ if (dir == null || dir.isEmpty()) {
+ throw new IllegalArgumentException("Checkpoint directory cannot be null or empty");
+ }
+ nativeCheckpoint(nativeHandle, dir);
+ }
+
/**
* Atomically renames a column family and its underlying directory.
* The operation waits for any in-progress flush or compaction to complete before renaming.
@@ -307,6 +322,8 @@ private static native void nativeCreateColumnFamily(long handle, String name,
private static native void nativeBackup(long handle, String dir) throws TidesDBException;
+ private static native void nativeCheckpoint(long handle, String dir) throws TidesDBException;
+
private static native void nativeRenameColumnFamily(long handle, String oldName, String newName) throws TidesDBException;
private static native void nativeCloneColumnFamily(long handle, String sourceName, String destName) throws TidesDBException;
diff --git a/src/test/java/com/tidesdb/TidesDBTest.java b/src/test/java/com/tidesdb/TidesDBTest.java
index 5af56de..8606c62 100644
--- a/src/test/java/com/tidesdb/TidesDBTest.java
+++ b/src/test/java/com/tidesdb/TidesDBTest.java
@@ -614,6 +614,76 @@ void testCloneColumnFamily() throws TidesDBException {
@Test
@Order(16)
+ void testCheckpoint() throws TidesDBException {
+ Config config = Config.builder(tempDir.resolve("testdb16").toString())
+ .numFlushThreads(2)
+ .numCompactionThreads(2)
+ .logLevel(LogLevel.INFO)
+ .blockCacheSize(64 * 1024 * 1024)
+ .maxOpenSSTables(256)
+ .build();
+
+ try (TidesDB db = TidesDB.open(config)) {
+ ColumnFamilyConfig cfConfig = ColumnFamilyConfig.defaultConfig();
+ db.createColumnFamily("test_cf", cfConfig);
+
+ ColumnFamily cf = db.getColumnFamily("test_cf");
+
+ // Insert some data
+ try (Transaction txn = db.beginTransaction()) {
+ for (int i = 0; i < 10; i++) {
+ txn.put(cf, ("key" + i).getBytes(), ("value" + i).getBytes());
+ }
+ txn.commit();
+ }
+
+ // Create checkpoint
+ String checkpointDir = tempDir.resolve("testdb16_checkpoint").toString();
+ db.checkpoint(checkpointDir);
+
+ // Open the checkpoint as a separate database and verify data
+ Config checkpointConfig = Config.builder(checkpointDir)
+ .numFlushThreads(2)
+ .numCompactionThreads(2)
+ .logLevel(LogLevel.INFO)
+ .blockCacheSize(64 * 1024 * 1024)
+ .maxOpenSSTables(256)
+ .build();
+
+ try (TidesDB checkpointDb = TidesDB.open(checkpointConfig)) {
+ ColumnFamily checkpointCf = checkpointDb.getColumnFamily("test_cf");
+ assertNotNull(checkpointCf);
+
+ try (Transaction txn = checkpointDb.beginTransaction()) {
+ for (int i = 0; i < 10; i++) {
+ byte[] result = txn.get(checkpointCf, ("key" + i).getBytes());
+ assertNotNull(result);
+ assertArrayEquals(("value" + i).getBytes(), result);
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ @Order(17)
+ void testCheckpointNullDir() throws TidesDBException {
+ Config config = Config.builder(tempDir.resolve("testdb16b").toString())
+ .numFlushThreads(2)
+ .numCompactionThreads(2)
+ .logLevel(LogLevel.INFO)
+ .blockCacheSize(64 * 1024 * 1024)
+ .maxOpenSSTables(256)
+ .build();
+
+ try (TidesDB db = TidesDB.open(config)) {
+ assertThrows(IllegalArgumentException.class, () -> db.checkpoint(null));
+ assertThrows(IllegalArgumentException.class, () -> db.checkpoint(""));
+ }
+ }
+
+ @Test
+ @Order(18)
void testTransactionPutGetDeleteBadKey() throws TidesDBException {
Config config = Config.builder(tempDir.resolve("testdb3").toString())
.numFlushThreads(2)
@@ -656,7 +726,7 @@ void testTransactionPutGetDeleteBadKey() throws TidesDBException {
}
@Test
- @Order(17)
+ @Order(19)
void testTransactionReset() throws TidesDBException {
Config config = Config.builder(tempDir.resolve("testdb17").toString())
.numFlushThreads(2)
@@ -701,7 +771,7 @@ void testTransactionReset() throws TidesDBException {
}
@Test
- @Order(18)
+ @Order(20)
void testTransactionResetWithDifferentIsolation() throws TidesDBException {
Config config = Config.builder(tempDir.resolve("testdb18").toString())
.numFlushThreads(2)
@@ -746,7 +816,7 @@ void testTransactionResetWithDifferentIsolation() throws TidesDBException {
}
@Test
- @Order(19)
+ @Order(21)
void testTransactionResetNullIsolation() throws TidesDBException {
Config config = Config.builder(tempDir.resolve("testdb19").toString())
.numFlushThreads(2)