From c9b3fe79bf7cc2a744e4c8bef742ee9f044bdcbf Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Wed, 11 Feb 2026 09:57:16 -0500 Subject: [PATCH] extend api with column family clone method and transaction reset capabilities, update toml to bump minor --- pyproject.toml | 2 +- src/tidesdb/tidesdb.py | 52 +++++++++++++++ tests/test_tidesdb.py | 144 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 386c00f..1897d97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tidesdb" -version = "0.8.0" +version = "0.9.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/tidesdb.py b/src/tidesdb/tidesdb.py index 3781837..7b0187f 100644 --- a/src/tidesdb/tidesdb.py +++ b/src/tidesdb/tidesdb.py @@ -377,6 +377,12 @@ class _CCacheStats(Structure): _lib.tidesdb_rename_column_family.argtypes = [c_void_p, c_char_p, c_char_p] _lib.tidesdb_rename_column_family.restype = c_int +_lib.tidesdb_clone_column_family.argtypes = [c_void_p, c_char_p, c_char_p] +_lib.tidesdb_clone_column_family.restype = c_int + +_lib.tidesdb_txn_reset.argtypes = [c_void_p, c_int] +_lib.tidesdb_txn_reset.restype = c_int + _lib.tidesdb_cf_update_runtime_config.argtypes = [c_void_p, POINTER(_CColumnFamilyConfig), c_int] _lib.tidesdb_cf_update_runtime_config.restype = c_int @@ -915,6 +921,28 @@ def rollback(self) -> None: if result != TDB_SUCCESS: raise TidesDBError.from_code(result, "failed to rollback transaction") + def reset(self, isolation: IsolationLevel = IsolationLevel.READ_COMMITTED) -> None: + """ + Reset a committed or aborted transaction for reuse with a new isolation level. + + This avoids the overhead of freeing and reallocating transaction resources + in hot loops. The transaction must be committed or rolled back before reset. + + Args: + isolation: New isolation level for the reset transaction + + Raises: + TidesDBError: If transaction is still active (not committed/aborted) + """ + if self._closed: + raise TidesDBError("Transaction is closed") + + result = _lib.tidesdb_txn_reset(self._txn, int(isolation)) + if result != TDB_SUCCESS: + raise TidesDBError.from_code(result, "failed to reset transaction") + + self._committed = False + def savepoint(self, name: str) -> None: """Create a savepoint within the transaction.""" if self._closed: @@ -1295,6 +1323,30 @@ def rename_column_family(self, old_name: str, new_name: str) -> None: if result != TDB_SUCCESS: raise TidesDBError.from_code(result, "failed to rename column family") + def clone_column_family(self, source_name: str, dest_name: str) -> None: + """ + Create a complete copy of an existing column family with a new name. + + The clone contains all the data from the source at the time of cloning. + The clone is completely independent - modifications to one do not affect + the other. + + Args: + source_name: Name of the source column family to clone + dest_name: Name for the new cloned column family + + Raises: + TidesDBError: If clone fails (e.g., source not found, dest exists) + """ + if self._closed: + raise TidesDBError("Database is closed") + + result = _lib.tidesdb_clone_column_family( + self._db, source_name.encode("utf-8"), dest_name.encode("utf-8") + ) + if result != TDB_SUCCESS: + raise TidesDBError.from_code(result, "failed to clone column family") + def register_comparator( self, name: str, diff --git a/tests/test_tidesdb.py b/tests/test_tidesdb.py index 9533226..030f5dc 100644 --- a/tests/test_tidesdb.py +++ b/tests/test_tidesdb.py @@ -320,6 +320,150 @@ def test_no_ttl(self, db, cf): assert value == b"value" +class TestCloneColumnFamily: + """Tests for column family clone operations.""" + + def test_clone_column_family(self, db, cf): + """Test cloning a column family with data.""" + with db.begin_txn() as txn: + txn.put(cf, b"key1", b"value1") + txn.put(cf, b"key2", b"value2") + txn.commit() + + db.clone_column_family("test_cf", "cloned_cf") + + cloned = db.get_column_family("cloned_cf") + assert cloned is not None + assert cloned.name == "cloned_cf" + + with db.begin_txn() as txn: + assert txn.get(cloned, b"key1") == b"value1" + assert txn.get(cloned, b"key2") == b"value2" + + db.drop_column_family("cloned_cf") + + def test_clone_independence(self, db, cf): + """Test that clone is independent from source.""" + with db.begin_txn() as txn: + txn.put(cf, b"key1", b"original") + txn.commit() + + db.clone_column_family("test_cf", "cloned_cf") + cloned = db.get_column_family("cloned_cf") + + with db.begin_txn() as txn: + txn.put(cf, b"key1", b"modified") + txn.commit() + + with db.begin_txn() as txn: + assert txn.get(cloned, b"key1") == b"original" + assert txn.get(cf, b"key1") == b"modified" + + db.drop_column_family("cloned_cf") + + def test_clone_nonexistent_source(self, db): + """Test cloning a non-existent column family raises error.""" + with pytest.raises(tidesdb.TidesDBError): + db.clone_column_family("nonexistent", "cloned_cf") + + def test_clone_to_existing_name(self, db, cf): + """Test cloning to an already existing name raises error.""" + db.create_column_family("existing_cf") + with pytest.raises(tidesdb.TidesDBError): + db.clone_column_family("test_cf", "existing_cf") + db.drop_column_family("existing_cf") + + def test_clone_listed(self, db, cf): + """Test that cloned column family appears in list.""" + db.clone_column_family("test_cf", "cloned_cf") + + names = db.list_column_families() + assert "test_cf" in names + assert "cloned_cf" in names + + db.drop_column_family("cloned_cf") + + +class TestTransactionReset: + """Tests for transaction reset operations.""" + + def test_reset_after_commit(self, db, cf): + """Test resetting a transaction after commit.""" + txn = db.begin_txn() + txn.put(cf, b"key1", b"value1") + txn.commit() + + txn.reset(tidesdb.IsolationLevel.READ_COMMITTED) + + txn.put(cf, b"key2", b"value2") + txn.commit() + txn.close() + + with db.begin_txn() as txn: + assert txn.get(cf, b"key1") == b"value1" + assert txn.get(cf, b"key2") == b"value2" + + def test_reset_after_rollback(self, db, cf): + """Test resetting a transaction after rollback.""" + txn = db.begin_txn() + txn.put(cf, b"key1", b"value1") + txn.rollback() + + txn.reset(tidesdb.IsolationLevel.READ_COMMITTED) + + txn.put(cf, b"key2", b"value2") + txn.commit() + txn.close() + + with db.begin_txn() as txn: + with pytest.raises(tidesdb.TidesDBError): + txn.get(cf, b"key1") + assert txn.get(cf, b"key2") == b"value2" + + def test_reset_with_different_isolation(self, db, cf): + """Test resetting with a different isolation level.""" + txn = db.begin_txn() + txn.put(cf, b"key1", b"value1") + txn.commit() + + txn.reset(tidesdb.IsolationLevel.SERIALIZABLE) + + txn.put(cf, b"key2", b"value2") + txn.commit() + txn.close() + + with db.begin_txn() as txn: + assert txn.get(cf, b"key1") == b"value1" + assert txn.get(cf, b"key2") == b"value2" + + def test_reset_reuse_loop(self, db, cf): + """Test resetting in a loop for batch processing.""" + txn = db.begin_txn() + + for i in range(5): + txn.put(cf, f"batch_key_{i}".encode(), f"batch_value_{i}".encode()) + txn.commit() + if i < 4: + txn.reset(tidesdb.IsolationLevel.READ_COMMITTED) + + txn.close() + + with db.begin_txn() as txn: + for i in range(5): + value = txn.get(cf, f"batch_key_{i}".encode()) + assert value == f"batch_value_{i}".encode() + + def test_reset_closed_transaction_raises(self, db, cf): + """Test that resetting a closed transaction raises error.""" + txn = db.begin_txn() + txn.put(cf, b"key1", b"value1") + txn.commit() + txn.close() + + with pytest.raises(tidesdb.TidesDBError): + txn.reset(tidesdb.IsolationLevel.READ_COMMITTED) + + class TestStats: """Tests for statistics operations."""