Skip to content

Commit cdbb3e3

Browse files
authored
Merge pull request #43 from tidesdb/updates99
addition of range cost, updated pyproject
2 parents a7db1f3 + a82b60e commit cdbb3e3

4 files changed

Lines changed: 211 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "tidesdb"
7-
version = "0.9.2"
7+
version = "0.9.3"
88
description = "Official Python bindings for TidesDB - A high-performance embedded key-value storage engine"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/tidesdb/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
default_config,
2525
default_column_family_config,
2626
save_config_to_ini,
27+
load_config_from_ini,
2728
COMPARATOR_FUNC,
2829
)
2930

@@ -45,5 +46,6 @@
4546
"default_config",
4647
"default_column_family_config",
4748
"save_config_to_ini",
49+
"load_config_from_ini",
4850
"COMPARATOR_FUNC",
4951
]

src/tidesdb/tidesdb.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,19 @@ class _CCacheStats(Structure):
400400
_lib.tidesdb_is_compacting.argtypes = [c_void_p]
401401
_lib.tidesdb_is_compacting.restype = c_int
402402

403+
_lib.tidesdb_range_cost.argtypes = [
404+
c_void_p,
405+
POINTER(c_uint8),
406+
c_size_t,
407+
POINTER(c_uint8),
408+
c_size_t,
409+
POINTER(c_double),
410+
]
411+
_lib.tidesdb_range_cost.restype = c_int
412+
413+
_lib.tidesdb_cf_config_load_from_ini.argtypes = [c_char_p, c_char_p, POINTER(_CColumnFamilyConfig)]
414+
_lib.tidesdb_cf_config_load_from_ini.restype = c_int
415+
403416
# Comparator function type: int (*)(const uint8_t*, size_t, const uint8_t*, size_t, void*)
404417
COMPARATOR_FUNC = ctypes.CFUNCTYPE(c_int, POINTER(c_uint8), c_size_t, POINTER(c_uint8), c_size_t, c_void_p)
405418
DESTROY_FUNC = ctypes.CFUNCTYPE(None, c_void_p)
@@ -570,6 +583,49 @@ def save_config_to_ini(file_path: str, cf_name: str, config: ColumnFamilyConfig)
570583
raise TidesDBError.from_code(result, "failed to save config to INI file")
571584

572585

586+
def load_config_from_ini(file_path: str, cf_name: str) -> ColumnFamilyConfig:
587+
"""
588+
Load column family configuration from an INI file.
589+
590+
Args:
591+
file_path: Path to the INI file to read
592+
cf_name: Name of the section to read (column family name)
593+
594+
Returns:
595+
ColumnFamilyConfig populated from the INI file
596+
"""
597+
c_config = _CColumnFamilyConfig()
598+
result = _lib.tidesdb_cf_config_load_from_ini(
599+
file_path.encode("utf-8"), cf_name.encode("utf-8"), ctypes.byref(c_config)
600+
)
601+
if result != TDB_SUCCESS:
602+
raise TidesDBError.from_code(result, "failed to load config from INI file")
603+
604+
return ColumnFamilyConfig(
605+
write_buffer_size=c_config.write_buffer_size,
606+
level_size_ratio=c_config.level_size_ratio,
607+
min_levels=c_config.min_levels,
608+
dividing_level_offset=c_config.dividing_level_offset,
609+
klog_value_threshold=c_config.klog_value_threshold,
610+
compression_algorithm=CompressionAlgorithm(c_config.compression_algorithm),
611+
enable_bloom_filter=bool(c_config.enable_bloom_filter),
612+
bloom_fpr=c_config.bloom_fpr,
613+
enable_block_indexes=bool(c_config.enable_block_indexes),
614+
index_sample_ratio=c_config.index_sample_ratio,
615+
block_index_prefix_len=c_config.block_index_prefix_len,
616+
sync_mode=SyncMode(c_config.sync_mode),
617+
sync_interval_us=c_config.sync_interval_us,
618+
comparator_name=c_config.comparator_name.decode("utf-8").rstrip("\x00"),
619+
skip_list_max_level=c_config.skip_list_max_level,
620+
skip_list_probability=c_config.skip_list_probability,
621+
default_isolation_level=IsolationLevel(c_config.default_isolation_level),
622+
min_disk_space=c_config.min_disk_space,
623+
l1_file_count_trigger=c_config.l1_file_count_trigger,
624+
l0_queue_stall_threshold=c_config.l0_queue_stall_threshold,
625+
use_btree=bool(c_config.use_btree),
626+
)
627+
628+
573629
class Iterator:
574630
"""Iterator for traversing key-value pairs in a column family."""
575631

@@ -742,6 +798,36 @@ def update_runtime_config(self, config: ColumnFamilyConfig, persist_to_disk: boo
742798
if result != TDB_SUCCESS:
743799
raise TidesDBError.from_code(result, "failed to update runtime config")
744800

801+
def range_cost(self, key_a: bytes, key_b: bytes) -> float:
802+
"""
803+
Estimate the computational cost of iterating between two keys.
804+
805+
The returned value is an opaque double meaningful only for comparison
806+
with other values from the same function. It uses only in-memory metadata
807+
and performs no disk I/O. Key order does not matter.
808+
809+
Args:
810+
key_a: First key (bound of range)
811+
key_b: Second key (bound of range)
812+
813+
Returns:
814+
Estimated traversal cost (higher = more expensive)
815+
816+
Raises:
817+
TidesDBError: If arguments are invalid (NULL pointers, zero-length keys)
818+
"""
819+
key_a_buf = (c_uint8 * len(key_a)).from_buffer_copy(key_a) if key_a else None
820+
key_b_buf = (c_uint8 * len(key_b)).from_buffer_copy(key_b) if key_b else None
821+
cost = c_double()
822+
823+
result = _lib.tidesdb_range_cost(
824+
self._cf, key_a_buf, len(key_a), key_b_buf, len(key_b), ctypes.byref(cost)
825+
)
826+
if result != TDB_SUCCESS:
827+
raise TidesDBError.from_code(result, "failed to estimate range cost")
828+
829+
return cost.value
830+
745831
def get_stats(self) -> Stats:
746832
"""Get statistics for this column family."""
747833
stats_ptr = POINTER(_CStats)()

tests/test_tidesdb.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,5 +589,127 @@ def test_checkpoint_independence(self, db, cf, temp_db_path):
589589
shutil.rmtree(checkpoint_dir, ignore_errors=True)
590590

591591

592+
class TestRangeCost:
593+
"""Tests for range cost estimation."""
594+
595+
def test_range_cost_returns_float(self, db, cf):
596+
"""Test that range_cost returns a float value."""
597+
with db.begin_txn() as txn:
598+
txn.put(cf, b"key_a", b"value_a")
599+
txn.put(cf, b"key_z", b"value_z")
600+
txn.commit()
601+
602+
cost = cf.range_cost(b"key_a", b"key_z")
603+
assert isinstance(cost, float)
604+
assert cost >= 0.0
605+
606+
def test_range_cost_empty_cf(self, db, cf):
607+
"""Test range_cost on an empty column family."""
608+
cost = cf.range_cost(b"a", b"z")
609+
assert isinstance(cost, float)
610+
assert cost >= 0.0
611+
612+
def test_range_cost_key_order_irrelevant(self, db, cf):
613+
"""Test that key order does not matter."""
614+
with db.begin_txn() as txn:
615+
txn.put(cf, b"aaa", b"1")
616+
txn.put(cf, b"zzz", b"2")
617+
txn.commit()
618+
619+
cost_ab = cf.range_cost(b"aaa", b"zzz")
620+
cost_ba = cf.range_cost(b"zzz", b"aaa")
621+
assert cost_ab == cost_ba
622+
623+
def test_range_cost_narrow_vs_wide(self, db, cf):
624+
"""Test that a wider range costs at least as much as a narrow one."""
625+
with db.begin_txn() as txn:
626+
for i in range(50):
627+
txn.put(cf, f"key:{i:04d}".encode(), f"val:{i}".encode())
628+
txn.commit()
629+
630+
narrow = cf.range_cost(b"key:0010", b"key:0015")
631+
wide = cf.range_cost(b"key:0000", b"key:0049")
632+
# Wide range should generally cost >= narrow range
633+
assert wide >= narrow
634+
635+
def test_range_cost_comparison(self, db, cf):
636+
"""Test comparing costs of different ranges."""
637+
with db.begin_txn() as txn:
638+
for i in range(100):
639+
txn.put(cf, f"user:{i:04d}".encode(), f"data:{i}".encode())
640+
txn.commit()
641+
642+
cost_a = cf.range_cost(b"user:0000", b"user:0009")
643+
cost_b = cf.range_cost(b"user:0000", b"user:0099")
644+
# Both should be valid floats
645+
assert isinstance(cost_a, float)
646+
assert isinstance(cost_b, float)
647+
648+
649+
class TestLoadConfigFromIni:
650+
"""Tests for loading column family config from INI files."""
651+
652+
def test_save_and_load_roundtrip(self, temp_db_path):
653+
"""Test that saving and loading config produces equivalent results."""
654+
original = tidesdb.default_column_family_config()
655+
original.write_buffer_size = 32 * 1024 * 1024
656+
original.compression_algorithm = tidesdb.CompressionAlgorithm.ZSTD_COMPRESSION
657+
original.enable_bloom_filter = True
658+
original.bloom_fpr = 0.001
659+
original.sync_mode = tidesdb.SyncMode.SYNC_FULL
660+
original.min_levels = 7
661+
original.use_btree = True
662+
663+
ini_path = os.path.join(temp_db_path, "test_config.ini")
664+
tidesdb.save_config_to_ini(ini_path, "my_cf", original)
665+
666+
loaded = tidesdb.load_config_from_ini(ini_path, "my_cf")
667+
668+
assert loaded.write_buffer_size == original.write_buffer_size
669+
assert loaded.compression_algorithm == original.compression_algorithm
670+
assert loaded.enable_bloom_filter == original.enable_bloom_filter
671+
assert abs(loaded.bloom_fpr - original.bloom_fpr) < 1e-9
672+
assert loaded.sync_mode == original.sync_mode
673+
assert loaded.min_levels == original.min_levels
674+
assert loaded.use_btree == original.use_btree
675+
676+
def test_load_nonexistent_file_raises(self, temp_db_path):
677+
"""Test that loading from a non-existent file raises error."""
678+
ini_path = os.path.join(temp_db_path, "nonexistent.ini")
679+
with pytest.raises(tidesdb.TidesDBError):
680+
tidesdb.load_config_from_ini(ini_path, "my_cf")
681+
682+
def test_load_preserves_all_fields(self, temp_db_path):
683+
"""Test that all configuration fields survive a save/load roundtrip."""
684+
original = tidesdb.default_column_family_config()
685+
original.level_size_ratio = 8
686+
original.dividing_level_offset = 3
687+
original.klog_value_threshold = 1024
688+
original.index_sample_ratio = 2
689+
original.block_index_prefix_len = 32
690+
original.sync_interval_us = 500000
691+
original.skip_list_max_level = 16
692+
original.skip_list_probability = 0.5
693+
original.min_disk_space = 200 * 1024 * 1024
694+
original.l1_file_count_trigger = 8
695+
original.l0_queue_stall_threshold = 30
696+
697+
ini_path = os.path.join(temp_db_path, "full_config.ini")
698+
tidesdb.save_config_to_ini(ini_path, "full_cf", original)
699+
700+
loaded = tidesdb.load_config_from_ini(ini_path, "full_cf")
701+
702+
assert loaded.level_size_ratio == original.level_size_ratio
703+
assert loaded.dividing_level_offset == original.dividing_level_offset
704+
assert loaded.klog_value_threshold == original.klog_value_threshold
705+
assert loaded.index_sample_ratio == original.index_sample_ratio
706+
assert loaded.block_index_prefix_len == original.block_index_prefix_len
707+
assert loaded.sync_interval_us == original.sync_interval_us
708+
assert loaded.skip_list_max_level == original.skip_list_max_level
709+
assert loaded.min_disk_space == original.min_disk_space
710+
assert loaded.l1_file_count_trigger == original.l1_file_count_trigger
711+
assert loaded.l0_queue_stall_threshold == original.l0_queue_stall_threshold
712+
713+
592714
if __name__ == "__main__":
593715
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)