From e7700ca16020afa9b57b639b07c0f10a174f48aa Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Fri, 20 Feb 2026 22:18:54 -0500 Subject: [PATCH 1/2] column family commit hook implementation; bump version in cmakelist --- CMakeLists.txt | 2 +- include/tidesdb/tidesdb.hpp | 14 +++ src/tidesdb.cpp | 18 ++++ tests/tidesdb_test.cpp | 198 ++++++++++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a90c16..3eba538 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(tidesdb_cpp VERSION 2.3.2 LANGUAGES CXX) +project(tidesdb_cpp VERSION 2.3.3 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/include/tidesdb/tidesdb.hpp b/include/tidesdb/tidesdb.hpp index e40aabf..d59c049 100644 --- a/include/tidesdb/tidesdb.hpp +++ b/include/tidesdb/tidesdb.hpp @@ -189,6 +189,8 @@ struct ColumnFamilyConfig int l1FileCountTrigger = 4; int l0QueueStallThreshold = 20; bool useBtree = false; ///< Use B+tree format for klog (default: false = block-based) + tidesdb_commit_hook_fn commitHookFn = nullptr; ///< Optional commit hook callback (runtime-only) + void* commitHookCtx = nullptr; ///< Optional user context for commit hook (runtime-only) /** * @brief Get default column family configuration from TidesDB @@ -322,6 +324,18 @@ class ColumnFamily [[nodiscard]] double rangeCost(const std::vector& keyA, const std::vector& keyB) const; + /** + * @brief Set or replace the commit hook for this column family + * @param fn Commit hook callback + * @param ctx User-provided context passed to the callback + */ + void setCommitHook(tidesdb_commit_hook_fn fn, void* ctx); + + /** + * @brief Clear (disable) the commit hook for this column family + */ + void clearCommitHook(); + /** * @brief Update runtime-safe configuration settings * @param config New configuration (only runtime-safe fields are applied) diff --git a/src/tidesdb.cpp b/src/tidesdb.cpp index 7233fe4..94fa4a9 100644 --- a/src/tidesdb.cpp +++ b/src/tidesdb.cpp @@ -143,6 +143,8 @@ void ColumnFamilyConfig::saveToIni(const std::string& iniFile, const std::string std::memset(cConfig.comparator_ctx_str, 0, TDB_MAX_COMPARATOR_CTX); cConfig.comparator_fn_cached = nullptr; cConfig.comparator_ctx_cached = nullptr; + cConfig.commit_hook_fn = nullptr; + cConfig.commit_hook_ctx = nullptr; int result = tidesdb_cf_config_save_to_ini(iniFile.c_str(), sectionName.c_str(), &cConfig); checkResult(result, "failed to save config to INI"); @@ -292,6 +294,18 @@ double ColumnFamily::rangeCost(const std::vector& keyA, return cost; } +void ColumnFamily::setCommitHook(tidesdb_commit_hook_fn fn, void* ctx) +{ + int result = tidesdb_cf_set_commit_hook(cf_, fn, ctx); + checkResult(result, "failed to set commit hook"); +} + +void ColumnFamily::clearCommitHook() +{ + int result = tidesdb_cf_set_commit_hook(cf_, nullptr, nullptr); + checkResult(result, "failed to clear commit hook"); +} + void ColumnFamily::updateRuntimeConfig(const ColumnFamilyConfig& config, bool persistToDisk) { tidesdb_column_family_config_t cConfig; @@ -327,6 +341,8 @@ void ColumnFamily::updateRuntimeConfig(const ColumnFamilyConfig& config, bool pe std::memset(cConfig.comparator_ctx_str, 0, TDB_MAX_COMPARATOR_CTX); cConfig.comparator_fn_cached = nullptr; cConfig.comparator_ctx_cached = nullptr; + cConfig.commit_hook_fn = nullptr; + cConfig.commit_hook_ctx = nullptr; int result = tidesdb_cf_update_runtime_config(cf_, &cConfig, persistToDisk ? 1 : 0); checkResult(result, "failed to update runtime config"); @@ -654,6 +670,8 @@ void TidesDB::createColumnFamily(const std::string& name, const ColumnFamilyConf std::memset(cConfig.comparator_ctx_str, 0, TDB_MAX_COMPARATOR_CTX); cConfig.comparator_fn_cached = nullptr; cConfig.comparator_ctx_cached = nullptr; + cConfig.commit_hook_fn = config.commitHookFn; + cConfig.commit_hook_ctx = config.commitHookCtx; int result = tidesdb_create_column_family(db_, name.c_str(), &cConfig); checkResult(result, "failed to create column family"); diff --git a/tests/tidesdb_test.cpp b/tests/tidesdb_test.cpp index ca9a9d7..d4eea53 100644 --- a/tests/tidesdb_test.cpp +++ b/tests/tidesdb_test.cpp @@ -19,9 +19,11 @@ #include +#include #include #include #include +#include #include #include #include @@ -1139,6 +1141,202 @@ TEST_F(TidesDBTest, TransactionResetAfterRollback) } } +// Commit hook test helpers +struct HookTestCtx +{ + std::atomic callCount{0}; + std::atomic totalOps{0}; + std::atomic lastCommitSeq{0}; + std::mutex mu; + std::vector capturedKeys; +}; + +static int testCommitHook(const tidesdb_commit_op_t* ops, int num_ops, uint64_t commit_seq, + void* ctx) +{ + auto* hctx = static_cast(ctx); + hctx->callCount.fetch_add(1); + hctx->totalOps.fetch_add(num_ops); + hctx->lastCommitSeq.store(commit_seq); + + std::lock_guard lock(hctx->mu); + for (int i = 0; i < num_ops; ++i) + { + hctx->capturedKeys.emplace_back(reinterpret_cast(ops[i].key), ops[i].key_size); + } + return 0; +} + +TEST_F(TidesDBTest, CommitHookBasic) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + HookTestCtx hookCtx; + cf.setCommitHook(testCommitHook, &hookCtx); + + // Commit a transaction -- hook should fire + { + auto txn = db.beginTransaction(); + txn.put(cf, "key1", "value1", -1); + txn.put(cf, "key2", "value2", -1); + txn.commit(); + } + + ASSERT_GE(hookCtx.callCount.load(), 1); + ASSERT_GE(hookCtx.totalOps.load(), 2); + ASSERT_GT(hookCtx.lastCommitSeq.load(), 0u); + + // Verify captured keys + { + std::lock_guard lock(hookCtx.mu); + bool foundKey1 = false, foundKey2 = false; + for (const auto& k : hookCtx.capturedKeys) + { + if (k == "key1") foundKey1 = true; + if (k == "key2") foundKey2 = true; + } + ASSERT_TRUE(foundKey1); + ASSERT_TRUE(foundKey2); + } +} + +TEST_F(TidesDBTest, CommitHookClear) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + HookTestCtx hookCtx; + cf.setCommitHook(testCommitHook, &hookCtx); + + // First commit -- hook fires + { + auto txn = db.beginTransaction(); + txn.put(cf, "key1", "value1", -1); + txn.commit(); + } + + int countAfterFirst = hookCtx.callCount.load(); + ASSERT_GE(countAfterFirst, 1); + + // Clear the hook + cf.clearCommitHook(); + + // Second commit -- hook should NOT fire + { + auto txn = db.beginTransaction(); + txn.put(cf, "key2", "value2", -1); + txn.commit(); + } + + ASSERT_EQ(hookCtx.callCount.load(), countAfterFirst); +} + +TEST_F(TidesDBTest, CommitHookDeleteOps) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + // Insert data first + { + auto txn = db.beginTransaction(); + txn.put(cf, "del_key", "del_value", -1); + txn.commit(); + } + + HookTestCtx hookCtx; + cf.setCommitHook(testCommitHook, &hookCtx); + + // Delete the key -- hook should capture the delete + { + auto txn = db.beginTransaction(); + txn.del(cf, "del_key"); + txn.commit(); + } + + ASSERT_GE(hookCtx.callCount.load(), 1); + ASSERT_GE(hookCtx.totalOps.load(), 1); +} + +TEST_F(TidesDBTest, CommitHookMonotonicSeq) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + // Custom hook that records all commit sequences + struct SeqCtx + { + std::mutex mu; + std::vector seqs; + } seqCtx; + + auto seqHook = [](const tidesdb_commit_op_t*, int, uint64_t commit_seq, void* ctx) -> int + { + auto* sctx = static_cast(ctx); + std::lock_guard lock(sctx->mu); + sctx->seqs.push_back(commit_seq); + return 0; + }; + + cf.setCommitHook(seqHook, &seqCtx); + + // Multiple commits + for (int i = 0; i < 5; ++i) + { + auto txn = db.beginTransaction(); + txn.put(cf, "key" + std::to_string(i), "value" + std::to_string(i), -1); + txn.commit(); + } + + // Verify monotonically increasing sequence numbers + std::lock_guard lock(seqCtx.mu); + ASSERT_GE(seqCtx.seqs.size(), 5u); + for (size_t i = 1; i < seqCtx.seqs.size(); ++i) + { + ASSERT_GT(seqCtx.seqs[i], seqCtx.seqs[i - 1]); + } +} + +TEST_F(TidesDBTest, CommitHookViaConfig) +{ + tidesdb::TidesDB db(getConfig()); + + HookTestCtx hookCtx; + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + cfConfig.commitHookFn = testCommitHook; + cfConfig.commitHookCtx = &hookCtx; + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + // Commit -- hook should already be active from config + { + auto txn = db.beginTransaction(); + txn.put(cf, "key1", "value1", -1); + txn.commit(); + } + + ASSERT_GE(hookCtx.callCount.load(), 1); + ASSERT_GE(hookCtx.totalOps.load(), 1); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); From 72faa59bee6f97562cd00327020ae9116f53f76f Mon Sep 17 00:00:00 2001 From: Alex Gaetano Padula Date: Fri, 20 Feb 2026 22:19:06 -0500 Subject: [PATCH 2/2] run code formatter --- include/tidesdb/tidesdb.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/tidesdb/tidesdb.hpp b/include/tidesdb/tidesdb.hpp index d59c049..a916286 100644 --- a/include/tidesdb/tidesdb.hpp +++ b/include/tidesdb/tidesdb.hpp @@ -189,8 +189,9 @@ struct ColumnFamilyConfig int l1FileCountTrigger = 4; int l0QueueStallThreshold = 20; bool useBtree = false; ///< Use B+tree format for klog (default: false = block-based) - tidesdb_commit_hook_fn commitHookFn = nullptr; ///< Optional commit hook callback (runtime-only) - void* commitHookCtx = nullptr; ///< Optional user context for commit hook (runtime-only) + tidesdb_commit_hook_fn commitHookFn = + nullptr; ///< Optional commit hook callback (runtime-only) + void* commitHookCtx = nullptr; ///< Optional user context for commit hook (runtime-only) /** * @brief Get default column family configuration from TidesDB