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 CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
15 changes: 15 additions & 0 deletions include/tidesdb/tidesdb.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +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)

/**
* @brief Get default column family configuration from TidesDB
Expand Down Expand Up @@ -322,6 +325,18 @@ class ColumnFamily
[[nodiscard]] double rangeCost(const std::vector<std::uint8_t>& keyA,
const std::vector<std::uint8_t>& 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)
Expand Down
18 changes: 18 additions & 0 deletions src/tidesdb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -292,6 +294,18 @@ double ColumnFamily::rangeCost(const std::vector<std::uint8_t>& 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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
198 changes: 198 additions & 0 deletions tests/tidesdb_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

#include <gtest/gtest.h>

#include <atomic>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
Expand Down Expand Up @@ -1139,6 +1141,202 @@ TEST_F(TidesDBTest, TransactionResetAfterRollback)
}
}

// Commit hook test helpers
struct HookTestCtx
{
std::atomic<int> callCount{0};
std::atomic<int> totalOps{0};
std::atomic<uint64_t> lastCommitSeq{0};
std::mutex mu;
std::vector<std::string> capturedKeys;
};

static int testCommitHook(const tidesdb_commit_op_t* ops, int num_ops, uint64_t commit_seq,
void* ctx)
{
auto* hctx = static_cast<HookTestCtx*>(ctx);
hctx->callCount.fetch_add(1);
hctx->totalOps.fetch_add(num_ops);
hctx->lastCommitSeq.store(commit_seq);

std::lock_guard<std::mutex> lock(hctx->mu);
for (int i = 0; i < num_ops; ++i)
{
hctx->capturedKeys.emplace_back(reinterpret_cast<const char*>(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<std::mutex> 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<uint64_t> seqs;
} seqCtx;

auto seqHook = [](const tidesdb_commit_op_t*, int, uint64_t commit_seq, void* ctx) -> int
{
auto* sctx = static_cast<SeqCtx*>(ctx);
std::lock_guard<std::mutex> 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<std::mutex> 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);
Expand Down
Loading