From 6ace3dd0595ba0bbceaf292a4e38c9123e017965 Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Wed, 11 Feb 2026 10:52:04 -0500 Subject: [PATCH] extend api with column family clone method and transaction reset capabilities, bump minor, extend tests --- CMakeLists.txt | 2 +- code_formatter.sh | 12 ++- include/tidesdb/tidesdb.hpp | 13 +++ src/tidesdb.cpp | 12 +++ tests/tidesdb_test.cpp | 183 ++++++++++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9410171..3116055 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(tidesdb_cpp VERSION 2.2.0 LANGUAGES CXX) +project(tidesdb_cpp VERSION 2.3.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/code_formatter.sh b/code_formatter.sh index 09e52ab..e0ae2b6 100755 --- a/code_formatter.sh +++ b/code_formatter.sh @@ -1,7 +1,13 @@ #!/bin/bash +set -euo pipefail # Before submitting a PR, run this script to format the source code. -EXCLUDE_DIRS="external\|cmake-build-debug\|.idea|build|cmake" - -find . -type f -name "*.cpp" -o -name "*.hpp" | grep -v "$EXCLUDE_DIRS" | xargs clang-format -i \ No newline at end of file +find . \ + \( -path "./external" \ + -o -path "./cmake-build-debug" \ + -o -path "./.idea" \ + -o -path "./build" \ + -o -path "./cmake" \) -prune \ + -o -type f \( -name "*.cpp" -o -name "*.hpp" \) -print0 \ +| xargs -0 clang-format -i \ No newline at end of file diff --git a/include/tidesdb/tidesdb.hpp b/include/tidesdb/tidesdb.hpp index 15e0380..a72f08d 100644 --- a/include/tidesdb/tidesdb.hpp +++ b/include/tidesdb/tidesdb.hpp @@ -474,6 +474,12 @@ class Transaction */ [[nodiscard]] Iterator newIterator(ColumnFamily& cf); + /** + * @brief Reset a committed or aborted transaction for reuse + * @param isolation New isolation level for the reset transaction + */ + void reset(IsolationLevel isolation); + private: friend class TidesDB; explicit Transaction(tidesdb_txn_t* txn) : txn_(txn) @@ -579,6 +585,13 @@ class TidesDB */ void renameColumnFamily(const std::string& oldName, const std::string& newName); + /** + * @brief Clone a column family + * @param srcName Source column family name + * @param dstName Destination column family name + */ + void cloneColumnFamily(const std::string& srcName, const std::string& dstName); + /** * @brief Create a backup of the database * @param dir Backup directory (must be empty or non-existent) diff --git a/src/tidesdb.cpp b/src/tidesdb.cpp index aa5c9b2..52a83b4 100644 --- a/src/tidesdb.cpp +++ b/src/tidesdb.cpp @@ -546,6 +546,12 @@ Iterator Transaction::newIterator(ColumnFamily& cf) return Iterator(iter); } +void Transaction::reset(IsolationLevel isolation) +{ + int result = tidesdb_txn_reset(txn_, static_cast(isolation)); + checkResult(result, "failed to reset transaction"); +} + //----------------------------------------------------------------------------- // TidesDB //----------------------------------------------------------------------------- @@ -726,6 +732,12 @@ void TidesDB::renameColumnFamily(const std::string& oldName, const std::string& checkResult(result, "failed to rename column family"); } +void TidesDB::cloneColumnFamily(const std::string& srcName, const std::string& dstName) +{ + int result = tidesdb_clone_column_family(db_, srcName.c_str(), dstName.c_str()); + checkResult(result, "failed to clone column family"); +} + void TidesDB::backup(const std::string& dir) { int result = tidesdb_backup(db_, const_cast(dir.c_str())); diff --git a/tests/tidesdb_test.cpp b/tests/tidesdb_test.cpp index 664a11f..4b5f5fc 100644 --- a/tests/tidesdb_test.cpp +++ b/tests/tidesdb_test.cpp @@ -819,6 +819,189 @@ TEST_F(TidesDBTest, BtreeStats) ASSERT_GE(stats.btreeAvgHeight, 0.0); } +TEST_F(TidesDBTest, CloneColumnFamily) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("source_cf", cfConfig); + + auto cf = db.getColumnFamily("source_cf"); + + // Write data to source + { + auto txn = db.beginTransaction(); + txn.put(cf, "key1", "value1", -1); + txn.put(cf, "key2", "value2", -1); + txn.put(cf, "key3", "value3", -1); + txn.commit(); + } + + // Clone the column family + db.cloneColumnFamily("source_cf", "cloned_cf"); + + // Verify cloned CF exists and has the same data + auto clonedCf = db.getColumnFamily("cloned_cf"); + { + auto txn = db.beginTransaction(); + for (int i = 1; i <= 3; ++i) + { + std::string key = "key" + std::to_string(i); + std::string expectedValue = "value" + std::to_string(i); + + auto value = txn.get(clonedCf, key); + std::string valueStr(value.begin(), value.end()); + ASSERT_EQ(valueStr, expectedValue); + } + } + + // Verify source CF still exists and is independent + auto sourceCf = db.getColumnFamily("source_cf"); + { + auto txn = db.beginTransaction(); + auto value = txn.get(sourceCf, "key1"); + std::string valueStr(value.begin(), value.end()); + ASSERT_EQ(valueStr, "value1"); + } + + // Verify independence: write to clone doesn't affect source + { + auto txn = db.beginTransaction(); + txn.put(clonedCf, "clone_only_key", "clone_only_value", -1); + txn.commit(); + } + + { + auto txn = db.beginTransaction(); + EXPECT_THROW(txn.get(sourceCf, "clone_only_key"), tidesdb::Exception); + } +} + +TEST_F(TidesDBTest, CloneColumnFamilyErrors) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("existing_cf", cfConfig); + + // Clone non-existent source + EXPECT_THROW(db.cloneColumnFamily("nonexistent_cf", "new_cf"), tidesdb::Exception); + + // Clone to existing destination + EXPECT_THROW(db.cloneColumnFamily("existing_cf", "existing_cf"), tidesdb::Exception); +} + +TEST_F(TidesDBTest, TransactionReset) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + // Begin, use, commit, then reset and reuse + auto txn = db.beginTransaction(); + + // First batch of work + txn.put(cf, "key1", "value1", -1); + txn.commit(); + + // Reset instead of free + begin + txn.reset(tidesdb::IsolationLevel::ReadCommitted); + + // Second batch of work using the same transaction + txn.put(cf, "key2", "value2", -1); + txn.commit(); + + // Verify both batches were committed + { + auto readTxn = db.beginTransaction(); + auto value1 = readTxn.get(cf, "key1"); + std::string value1Str(value1.begin(), value1.end()); + ASSERT_EQ(value1Str, "value1"); + + auto value2 = readTxn.get(cf, "key2"); + std::string value2Str(value2.begin(), value2.end()); + ASSERT_EQ(value2Str, "value2"); + } +} + +TEST_F(TidesDBTest, TransactionResetWithDifferentIsolation) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + // Start with ReadCommitted + auto txn = db.beginTransaction(tidesdb::IsolationLevel::ReadCommitted); + txn.put(cf, "key1", "value1", -1); + txn.commit(); + + // Reset to RepeatableRead + txn.reset(tidesdb::IsolationLevel::RepeatableRead); + txn.put(cf, "key2", "value2", -1); + txn.commit(); + + // Reset to Snapshot + txn.reset(tidesdb::IsolationLevel::Snapshot); + txn.put(cf, "key3", "value3", -1); + txn.commit(); + + // Verify all writes succeeded + { + auto readTxn = db.beginTransaction(); + for (int i = 1; i <= 3; ++i) + { + std::string key = "key" + std::to_string(i); + std::string expectedValue = "value" + std::to_string(i); + + auto value = readTxn.get(cf, key); + std::string valueStr(value.begin(), value.end()); + ASSERT_EQ(valueStr, expectedValue); + } + } +} + +TEST_F(TidesDBTest, TransactionResetAfterRollback) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + auto txn = db.beginTransaction(); + + // Put and rollback + txn.put(cf, "rolled_back_key", "rolled_back_value", -1); + txn.rollback(); + + // Reset after rollback + txn.reset(tidesdb::IsolationLevel::ReadCommitted); + + // New work after reset + txn.put(cf, "new_key", "new_value", -1); + txn.commit(); + + // Verify rolled back key doesn't exist, new key does + { + auto readTxn = db.beginTransaction(); + EXPECT_THROW(readTxn.get(cf, "rolled_back_key"), tidesdb::Exception); + } + + { + auto readTxn = db.beginTransaction(); + auto value = readTxn.get(cf, "new_key"); + std::string valueStr(value.begin(), value.end()); + ASSERT_EQ(valueStr, "new_value"); + } +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv);