diff --git a/pom.xml b/pom.xml
index 0f324d2..2577888 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.tidesdb
tidesdb-java
- 0.6.2
+ 0.6.3
jar
TidesDB Java
diff --git a/src/main/c/com_tidesdb_TidesDB.c b/src/main/c/com_tidesdb_TidesDB.c
index 0cc13e4..4edc165 100644
--- a/src/main/c/com_tidesdb_TidesDB.c
+++ b/src/main/c/com_tidesdb_TidesDB.c
@@ -912,3 +912,32 @@ JNIEXPORT void JNICALL Java_com_tidesdb_TidesDBIterator_nativeFree(JNIEnv *env,
tidesdb_iter_free(iter);
}
}
+
+JNIEXPORT jdouble JNICALL Java_com_tidesdb_ColumnFamily_nativeRangeCost(JNIEnv *env, jclass cls,
+ jlong handle,
+ jbyteArray keyA,
+ jbyteArray keyB)
+{
+ tidesdb_column_family_t *cf = (tidesdb_column_family_t *)(uintptr_t)handle;
+
+ jsize keyALen = (*env)->GetArrayLength(env, keyA);
+ jsize keyBLen = (*env)->GetArrayLength(env, keyB);
+
+ jbyte *keyABytes = (*env)->GetByteArrayElements(env, keyA, NULL);
+ jbyte *keyBBytes = (*env)->GetByteArrayElements(env, keyB, NULL);
+
+ double cost = 0.0;
+ int result = tidesdb_range_cost(cf, (uint8_t *)keyABytes, keyALen, (uint8_t *)keyBBytes,
+ keyBLen, &cost);
+
+ (*env)->ReleaseByteArrayElements(env, keyA, keyABytes, JNI_ABORT);
+ (*env)->ReleaseByteArrayElements(env, keyB, keyBBytes, JNI_ABORT);
+
+ if (result != TDB_SUCCESS)
+ {
+ throwTidesDBException(env, result, getErrorMessage(result));
+ return 0.0;
+ }
+
+ return (jdouble)cost;
+}
diff --git a/src/main/java/com/tidesdb/ColumnFamily.java b/src/main/java/com/tidesdb/ColumnFamily.java
index 57b91ad..944c0ed 100644
--- a/src/main/java/com/tidesdb/ColumnFamily.java
+++ b/src/main/java/com/tidesdb/ColumnFamily.java
@@ -125,6 +125,27 @@ public void updateRuntimeConfig(ColumnFamilyConfig config, boolean persistToDisk
persistToDisk);
}
+ /**
+ * Estimates the computational cost of iterating between two keys in this column family.
+ * The returned value is an opaque double — meaningful only for comparison with other
+ * values from the same method. Uses only in-memory metadata and performs no disk I/O.
+ * Key order does not matter — the method normalizes the range internally.
+ *
+ * @param keyA first key (bound of range)
+ * @param keyB second key (bound of range)
+ * @return estimated traversal cost (higher = more expensive), 0.0 if no overlapping data
+ * @throws TidesDBException if the estimation fails
+ */
+ public double rangeCost(byte[] keyA, byte[] keyB) throws TidesDBException {
+ if (keyA == null || keyA.length == 0) {
+ throw new IllegalArgumentException("keyA cannot be null or empty");
+ }
+ if (keyB == null || keyB.length == 0) {
+ throw new IllegalArgumentException("keyB cannot be null or empty");
+ }
+ return nativeRangeCost(nativeHandle, keyA, keyB);
+ }
+
long getNativeHandle() {
return nativeHandle;
}
@@ -137,4 +158,5 @@ long getNativeHandle() {
private static native void nativeUpdateRuntimeConfig(long handle, long writeBufferSize,
int skipListMaxLevel, float skipListProbability, double bloomFPR, int indexSampleRatio,
int syncMode, long syncIntervalUs, boolean persistToDisk) throws TidesDBException;
+ private static native double nativeRangeCost(long handle, byte[] keyA, byte[] keyB) throws TidesDBException;
}
diff --git a/src/test/java/com/tidesdb/TidesDBTest.java b/src/test/java/com/tidesdb/TidesDBTest.java
index 8606c62..9f6955d 100644
--- a/src/test/java/com/tidesdb/TidesDBTest.java
+++ b/src/test/java/com/tidesdb/TidesDBTest.java
@@ -815,6 +815,100 @@ void testTransactionResetWithDifferentIsolation() throws TidesDBException {
}
}
+ @Test
+ @Order(22)
+ void testRangeCost() throws TidesDBException {
+ Config config = Config.builder(tempDir.resolve("testdb20").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 data
+ try (Transaction txn = db.beginTransaction()) {
+ for (int i = 0; i < 100; i++) {
+ String key = String.format("key%04d", i);
+ txn.put(cf, key.getBytes(), ("value" + i).getBytes());
+ }
+ txn.commit();
+ }
+
+ // Estimate cost for a range
+ double cost = cf.rangeCost("key0000".getBytes(), "key0099".getBytes());
+ assertTrue(cost >= 0.0, "Range cost should be non-negative");
+ }
+ }
+
+ @Test
+ @Order(23)
+ void testRangeCostComparison() throws TidesDBException {
+ Config config = Config.builder(tempDir.resolve("testdb21").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 data
+ try (Transaction txn = db.beginTransaction()) {
+ for (int i = 0; i < 1000; i++) {
+ String key = String.format("key%04d", i);
+ txn.put(cf, key.getBytes(), ("value" + i).getBytes());
+ }
+ txn.commit();
+ }
+
+ // Both costs should be non-negative
+ double costSmall = cf.rangeCost("key0000".getBytes(), "key0010".getBytes());
+ double costLarge = cf.rangeCost("key0000".getBytes(), "key0999".getBytes());
+ assertTrue(costSmall >= 0.0, "Small range cost should be non-negative");
+ assertTrue(costLarge >= 0.0, "Large range cost should be non-negative");
+ }
+ }
+
+ @Test
+ @Order(24)
+ void testRangeCostNullKeys() throws TidesDBException {
+ Config config = Config.builder(tempDir.resolve("testdb22").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");
+
+ assertThrows(IllegalArgumentException.class,
+ () -> cf.rangeCost(null, "key".getBytes()));
+ assertThrows(IllegalArgumentException.class,
+ () -> cf.rangeCost("key".getBytes(), null));
+ assertThrows(IllegalArgumentException.class,
+ () -> cf.rangeCost(new byte[0], "key".getBytes()));
+ assertThrows(IllegalArgumentException.class,
+ () -> cf.rangeCost("key".getBytes(), new byte[0]));
+ }
+ }
+
@Test
@Order(21)
void testTransactionResetNullIsolation() throws TidesDBException {