Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
52 changes: 52 additions & 0 deletions src/tidesdb/tidesdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
144 changes: 144 additions & 0 deletions tests/test_tidesdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
Loading