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 {