diff --git a/README.md b/README.md index 8fe45b2..4bdf456 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,3 @@ Multiple licenses apply: ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. - -## Support - -- [Discord](https://discord.gg/tWEmjR66cy) -- [GitHub Issues](https://github.com/tidesdb/tidesdb-python/issues) diff --git a/pyproject.toml b/pyproject.toml index c92407f..dbd88c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tidesdb" -version = "0.9.8" +version = "0.10.0" description = "Official Python bindings for TidesDB - A high-performance embedded key-value storage engine" readme = "README.md" requires-python = ">=3.10" diff --git a/src/tidesdb/__init__.py b/src/tidesdb/__init__.py index c63d3f6..35517ef 100644 --- a/src/tidesdb/__init__.py +++ b/src/tidesdb/__init__.py @@ -49,7 +49,7 @@ TDB_ERR_READONLY, ) -__version__ = "0.9.8" +__version__ = "0.10.0" __all__ = [ "TidesDB", "Transaction", diff --git a/src/tidesdb/tidesdb.py b/src/tidesdb/tidesdb.py index 1756eca..9df4211 100644 --- a/src/tidesdb/tidesdb.py +++ b/src/tidesdb/tidesdb.py @@ -212,7 +212,7 @@ class _CColumnFamilyConfig(Structure): ("use_btree", c_int), ("commit_hook_fn", c_void_p), ("commit_hook_ctx", c_void_p), - ("object_target_file_size", c_size_t), + ("_object_target_file_size_reserved", c_size_t), ("object_lazy_compaction", c_int), ("object_prefetch_compaction", c_int), ] @@ -411,6 +411,9 @@ class _CDbStats(Structure): _lib.tidesdb_txn_delete.argtypes = [c_void_p, c_void_p, POINTER(c_uint8), c_size_t] _lib.tidesdb_txn_delete.restype = c_int +_lib.tidesdb_txn_single_delete.argtypes = [c_void_p, c_void_p, POINTER(c_uint8), c_size_t] +_lib.tidesdb_txn_single_delete.restype = c_int + _lib.tidesdb_txn_commit.argtypes = [c_void_p] _lib.tidesdb_txn_commit.restype = c_int @@ -657,7 +660,6 @@ class ColumnFamilyConfig: l1_file_count_trigger: int = 4 l0_queue_stall_threshold: int = 20 use_btree: bool = False - object_target_file_size: int = 0 object_lazy_compaction: bool = False object_prefetch_compaction: bool = True @@ -690,7 +692,6 @@ def _to_c_struct(self, name: str = "") -> _CColumnFamilyConfig: c_config.l1_file_count_trigger = self.l1_file_count_trigger c_config.l0_queue_stall_threshold = self.l0_queue_stall_threshold c_config.use_btree = 1 if self.use_btree else 0 - c_config.object_target_file_size = self.object_target_file_size c_config.object_lazy_compaction = 1 if self.object_lazy_compaction else 0 c_config.object_prefetch_compaction = 1 if self.object_prefetch_compaction else 0 @@ -864,7 +865,6 @@ def default_column_family_config() -> ColumnFamilyConfig: l1_file_count_trigger=c_config.l1_file_count_trigger, l0_queue_stall_threshold=c_config.l0_queue_stall_threshold, use_btree=bool(c_config.use_btree), - object_target_file_size=c_config.object_target_file_size, object_lazy_compaction=bool(c_config.object_lazy_compaction), object_prefetch_compaction=bool(c_config.object_prefetch_compaction), ) @@ -927,7 +927,6 @@ def load_config_from_ini(file_path: str, cf_name: str) -> ColumnFamilyConfig: l1_file_count_trigger=c_config.l1_file_count_trigger, l0_queue_stall_threshold=c_config.l0_queue_stall_threshold, use_btree=bool(c_config.use_btree), - object_target_file_size=c_config.object_target_file_size, object_lazy_compaction=bool(c_config.object_lazy_compaction), object_prefetch_compaction=bool(c_config.object_prefetch_compaction), ) @@ -1304,7 +1303,6 @@ def get_stats(self) -> Stats: l1_file_count_trigger=c_cfg.l1_file_count_trigger, l0_queue_stall_threshold=c_cfg.l0_queue_stall_threshold, use_btree=bool(c_cfg.use_btree), - object_target_file_size=c_cfg.object_target_file_size, object_lazy_compaction=bool(c_cfg.object_lazy_compaction), object_prefetch_compaction=bool(c_cfg.object_prefetch_compaction), ) @@ -1424,6 +1422,36 @@ def delete(self, cf: ColumnFamily, key: bytes) -> None: if result != TDB_SUCCESS: raise TidesDBError.from_code(result, "failed to delete key") + def single_delete(self, cf: ColumnFamily, key: bytes) -> None: + """ + Write a tombstone carrying a caller-provided promise that the key has been + put at most once since its previous single-delete (or since the start of + history). This lets compaction drop the put and the tombstone together as + soon as both appear in the same merge input, rather than carrying the + tombstone forward to the largest active level. Read semantics match delete(). + + The engine does not verify the promise at runtime; violating it can leave + older puts visible and is a bug in the caller. Use only for insert-once- + then-delete patterns (classic insert benchmarks, secondary indexes on + never-updated columns, log tables with scheduled purges). Not safe for + repeated updates to the same key. When in doubt, prefer delete(). + + Args: + cf: Column family handle + key: Key as bytes + """ + if self._closed: + raise TidesDBError("Transaction is closed") + if self._committed: + raise TidesDBError("Transaction already committed") + + key_buf = (c_uint8 * len(key)).from_buffer_copy(key) if key else None + + result = _lib.tidesdb_txn_single_delete(self._txn, cf._cf, key_buf, len(key)) + + if result != TDB_SUCCESS: + raise TidesDBError.from_code(result, "failed to single-delete key") + def commit(self) -> None: """Commit the transaction.""" if self._closed: diff --git a/tests/test_tidesdb.py b/tests/test_tidesdb.py index 2e343a7..96323c3 100644 --- a/tests/test_tidesdb.py +++ b/tests/test_tidesdb.py @@ -198,6 +198,47 @@ def test_isolation_level(self, db, cf): txn.commit() txn.close() + def test_single_delete(self, db, cf): + """Test single_delete behaves like delete for read semantics.""" + with db.begin_txn() as txn: + txn.put(cf, b"sd_key", b"sd_value") + txn.commit() + + with db.begin_txn() as txn: + txn.single_delete(cf, b"sd_key") + txn.commit() + + with db.begin_txn() as txn: + with pytest.raises(tidesdb.TidesDBError): + txn.get(cf, b"sd_key") + + def test_single_delete_combined_with_put(self, db, cf): + """Test single_delete combined with other ops in the same transaction.""" + with db.begin_txn() as txn: + txn.put(cf, b"sd_a", b"value_a") + txn.put(cf, b"sd_b", b"value_b") + txn.commit() + + with db.begin_txn() as txn: + txn.put(cf, b"sd_c", b"value_c") + txn.single_delete(cf, b"sd_a") + txn.commit() + + with db.begin_txn() as txn: + assert txn.get(cf, b"sd_b") == b"value_b" + assert txn.get(cf, b"sd_c") == b"value_c" + with pytest.raises(tidesdb.TidesDBError): + txn.get(cf, b"sd_a") + + def test_single_delete_after_commit_raises(self, db, cf): + """Test that single_delete on a committed transaction raises.""" + txn = db.begin_txn() + txn.put(cf, b"sd_x", b"value_x") + txn.commit() + with pytest.raises(tidesdb.TidesDBError): + txn.single_delete(cf, b"sd_x") + txn.close() + class TestSavepoints: """Tests for savepoint operations.""" @@ -1201,14 +1242,12 @@ class TestObjectStoreConfigFields: def test_cf_config_defaults(self): """Test object store config defaults in ColumnFamilyConfig.""" config = tidesdb.ColumnFamilyConfig() - assert config.object_target_file_size == 0 assert config.object_lazy_compaction is False assert config.object_prefetch_compaction is True def test_cf_config_custom_values(self, db): """Test creating column family with object store config fields set.""" config = tidesdb.ColumnFamilyConfig( - object_target_file_size=128 * 1024 * 1024, object_lazy_compaction=True, object_prefetch_compaction=False, )