Skip to content

Commit 015bd92

Browse files
authored
Merge pull request #38 from tidesdb/v0-9-0
extend api with column family clone method and transaction reset capa…
2 parents 4a6cf2b + c9b3fe7 commit 015bd92

3 files changed

Lines changed: 197 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.8.0"
7+
version = "0.9.0"
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/tidesdb.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,12 @@ class _CCacheStats(Structure):
377377
_lib.tidesdb_rename_column_family.argtypes = [c_void_p, c_char_p, c_char_p]
378378
_lib.tidesdb_rename_column_family.restype = c_int
379379

380+
_lib.tidesdb_clone_column_family.argtypes = [c_void_p, c_char_p, c_char_p]
381+
_lib.tidesdb_clone_column_family.restype = c_int
382+
383+
_lib.tidesdb_txn_reset.argtypes = [c_void_p, c_int]
384+
_lib.tidesdb_txn_reset.restype = c_int
385+
380386
_lib.tidesdb_cf_update_runtime_config.argtypes = [c_void_p, POINTER(_CColumnFamilyConfig), c_int]
381387
_lib.tidesdb_cf_update_runtime_config.restype = c_int
382388

@@ -915,6 +921,28 @@ def rollback(self) -> None:
915921
if result != TDB_SUCCESS:
916922
raise TidesDBError.from_code(result, "failed to rollback transaction")
917923

924+
def reset(self, isolation: IsolationLevel = IsolationLevel.READ_COMMITTED) -> None:
925+
"""
926+
Reset a committed or aborted transaction for reuse with a new isolation level.
927+
928+
This avoids the overhead of freeing and reallocating transaction resources
929+
in hot loops. The transaction must be committed or rolled back before reset.
930+
931+
Args:
932+
isolation: New isolation level for the reset transaction
933+
934+
Raises:
935+
TidesDBError: If transaction is still active (not committed/aborted)
936+
"""
937+
if self._closed:
938+
raise TidesDBError("Transaction is closed")
939+
940+
result = _lib.tidesdb_txn_reset(self._txn, int(isolation))
941+
if result != TDB_SUCCESS:
942+
raise TidesDBError.from_code(result, "failed to reset transaction")
943+
944+
self._committed = False
945+
918946
def savepoint(self, name: str) -> None:
919947
"""Create a savepoint within the transaction."""
920948
if self._closed:
@@ -1295,6 +1323,30 @@ def rename_column_family(self, old_name: str, new_name: str) -> None:
12951323
if result != TDB_SUCCESS:
12961324
raise TidesDBError.from_code(result, "failed to rename column family")
12971325

1326+
def clone_column_family(self, source_name: str, dest_name: str) -> None:
1327+
"""
1328+
Create a complete copy of an existing column family with a new name.
1329+
1330+
The clone contains all the data from the source at the time of cloning.
1331+
The clone is completely independent - modifications to one do not affect
1332+
the other.
1333+
1334+
Args:
1335+
source_name: Name of the source column family to clone
1336+
dest_name: Name for the new cloned column family
1337+
1338+
Raises:
1339+
TidesDBError: If clone fails (e.g., source not found, dest exists)
1340+
"""
1341+
if self._closed:
1342+
raise TidesDBError("Database is closed")
1343+
1344+
result = _lib.tidesdb_clone_column_family(
1345+
self._db, source_name.encode("utf-8"), dest_name.encode("utf-8")
1346+
)
1347+
if result != TDB_SUCCESS:
1348+
raise TidesDBError.from_code(result, "failed to clone column family")
1349+
12981350
def register_comparator(
12991351
self,
13001352
name: str,

tests/test_tidesdb.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,150 @@ def test_no_ttl(self, db, cf):
320320
assert value == b"value"
321321

322322

323+
class TestCloneColumnFamily:
324+
"""Tests for column family clone operations."""
325+
326+
def test_clone_column_family(self, db, cf):
327+
"""Test cloning a column family with data."""
328+
with db.begin_txn() as txn:
329+
txn.put(cf, b"key1", b"value1")
330+
txn.put(cf, b"key2", b"value2")
331+
txn.commit()
332+
333+
db.clone_column_family("test_cf", "cloned_cf")
334+
335+
cloned = db.get_column_family("cloned_cf")
336+
assert cloned is not None
337+
assert cloned.name == "cloned_cf"
338+
339+
with db.begin_txn() as txn:
340+
assert txn.get(cloned, b"key1") == b"value1"
341+
assert txn.get(cloned, b"key2") == b"value2"
342+
343+
db.drop_column_family("cloned_cf")
344+
345+
def test_clone_independence(self, db, cf):
346+
"""Test that clone is independent from source."""
347+
with db.begin_txn() as txn:
348+
txn.put(cf, b"key1", b"original")
349+
txn.commit()
350+
351+
db.clone_column_family("test_cf", "cloned_cf")
352+
cloned = db.get_column_family("cloned_cf")
353+
354+
with db.begin_txn() as txn:
355+
txn.put(cf, b"key1", b"modified")
356+
txn.commit()
357+
358+
with db.begin_txn() as txn:
359+
assert txn.get(cloned, b"key1") == b"original"
360+
assert txn.get(cf, b"key1") == b"modified"
361+
362+
db.drop_column_family("cloned_cf")
363+
364+
def test_clone_nonexistent_source(self, db):
365+
"""Test cloning a non-existent column family raises error."""
366+
with pytest.raises(tidesdb.TidesDBError):
367+
db.clone_column_family("nonexistent", "cloned_cf")
368+
369+
def test_clone_to_existing_name(self, db, cf):
370+
"""Test cloning to an already existing name raises error."""
371+
db.create_column_family("existing_cf")
372+
with pytest.raises(tidesdb.TidesDBError):
373+
db.clone_column_family("test_cf", "existing_cf")
374+
db.drop_column_family("existing_cf")
375+
376+
def test_clone_listed(self, db, cf):
377+
"""Test that cloned column family appears in list."""
378+
db.clone_column_family("test_cf", "cloned_cf")
379+
380+
names = db.list_column_families()
381+
assert "test_cf" in names
382+
assert "cloned_cf" in names
383+
384+
db.drop_column_family("cloned_cf")
385+
386+
387+
class TestTransactionReset:
388+
"""Tests for transaction reset operations."""
389+
390+
def test_reset_after_commit(self, db, cf):
391+
"""Test resetting a transaction after commit."""
392+
txn = db.begin_txn()
393+
txn.put(cf, b"key1", b"value1")
394+
txn.commit()
395+
396+
txn.reset(tidesdb.IsolationLevel.READ_COMMITTED)
397+
398+
txn.put(cf, b"key2", b"value2")
399+
txn.commit()
400+
txn.close()
401+
402+
with db.begin_txn() as txn:
403+
assert txn.get(cf, b"key1") == b"value1"
404+
assert txn.get(cf, b"key2") == b"value2"
405+
406+
def test_reset_after_rollback(self, db, cf):
407+
"""Test resetting a transaction after rollback."""
408+
txn = db.begin_txn()
409+
txn.put(cf, b"key1", b"value1")
410+
txn.rollback()
411+
412+
txn.reset(tidesdb.IsolationLevel.READ_COMMITTED)
413+
414+
txn.put(cf, b"key2", b"value2")
415+
txn.commit()
416+
txn.close()
417+
418+
with db.begin_txn() as txn:
419+
with pytest.raises(tidesdb.TidesDBError):
420+
txn.get(cf, b"key1")
421+
assert txn.get(cf, b"key2") == b"value2"
422+
423+
def test_reset_with_different_isolation(self, db, cf):
424+
"""Test resetting with a different isolation level."""
425+
txn = db.begin_txn()
426+
txn.put(cf, b"key1", b"value1")
427+
txn.commit()
428+
429+
txn.reset(tidesdb.IsolationLevel.SERIALIZABLE)
430+
431+
txn.put(cf, b"key2", b"value2")
432+
txn.commit()
433+
txn.close()
434+
435+
with db.begin_txn() as txn:
436+
assert txn.get(cf, b"key1") == b"value1"
437+
assert txn.get(cf, b"key2") == b"value2"
438+
439+
def test_reset_reuse_loop(self, db, cf):
440+
"""Test resetting in a loop for batch processing."""
441+
txn = db.begin_txn()
442+
443+
for i in range(5):
444+
txn.put(cf, f"batch_key_{i}".encode(), f"batch_value_{i}".encode())
445+
txn.commit()
446+
if i < 4:
447+
txn.reset(tidesdb.IsolationLevel.READ_COMMITTED)
448+
449+
txn.close()
450+
451+
with db.begin_txn() as txn:
452+
for i in range(5):
453+
value = txn.get(cf, f"batch_key_{i}".encode())
454+
assert value == f"batch_value_{i}".encode()
455+
456+
def test_reset_closed_transaction_raises(self, db, cf):
457+
"""Test that resetting a closed transaction raises error."""
458+
txn = db.begin_txn()
459+
txn.put(cf, b"key1", b"value1")
460+
txn.commit()
461+
txn.close()
462+
463+
with pytest.raises(tidesdb.TidesDBError):
464+
txn.reset(tidesdb.IsolationLevel.READ_COMMITTED)
465+
466+
323467
class TestStats:
324468
"""Tests for statistics operations."""
325469

0 commit comments

Comments
 (0)