diff --git a/.github/workflows/build_and_test_tidesdb.yml b/.github/workflows/build_and_test_tidesdb.yml index 8831dc6..e580f05 100644 --- a/.github/workflows/build_and_test_tidesdb.yml +++ b/.github/workflows/build_and_test_tidesdb.yml @@ -9,40 +9,73 @@ on: jobs: build: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} steps: - - name: Checkout tidesdb-cpp repo - uses: actions/checkout@v4 - with: - repository: tidesdb/tidesdb-cpp - path: tidesdb-cpp - - - name: Checkout tidesdb repo - uses: actions/checkout@v4 - with: - repository: tidesdb/tidesdb - path: tidesdb - - - name: Install libzstd-dev,liblz4-dev and libsnappy-dev - run: | - sudo apt update - sudo apt install -y libzstd-dev liblz4-dev libsnappy-dev - - - name: configure cmake build for tidesdb - run: | - cd tidesdb - cmake -DTIDESDB_WITH_SANITIZER=OFF --debug-output -S . -B build && make -C build/ - sudo cmake --install build - - - name: configure cmake build - run: | - cd tidesdb-cpp - cmake --debug-output -S . -B build && make -C build/ - - - name: run tests - run: | - cd tidesdb-cpp/build - ctest --output-on-failure + - name: Checkout tidesdb-cpp repo + uses: actions/checkout@v4 + with: + repository: tidesdb/tidesdb-cpp + path: tidesdb-cpp + - name: Checkout tidesdb repo + uses: actions/checkout@v4 + with: + repository: tidesdb/tidesdb + path: tidesdb + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt update + sudo apt install -y libzstd-dev liblz4-dev libsnappy-dev + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install zstd lz4 snappy + + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + run: | + choco install pkgconfiglite + vcpkg install zstd lz4 snappy + + - name: Configure cmake build for tidesdb (Unix) + if: runner.os != 'Windows' + run: | + cd tidesdb + cmake -DTIDESDB_WITH_SANITIZER=OFF --debug-output -S . -B build + cmake --build build + sudo cmake --install build + + - name: Configure cmake build for tidesdb (Windows) + if: runner.os == 'Windows' + run: | + cd tidesdb + cmake -DTIDESDB_WITH_SANITIZER=OFF --debug-output -S . -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake + cmake --build build --config Release + cmake --install build --config Release + + - name: Configure cmake build (Unix) + if: runner.os != 'Windows' + run: | + cd tidesdb-cpp + cmake --debug-output -S . -B build + cmake --build build + + - name: Configure cmake build (Windows) + if: runner.os == 'Windows' + run: | + cd tidesdb-cpp + cmake --debug-output -S . -B build -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake + cmake --build build --config Release + + - name: Run tests + run: | + cd tidesdb-cpp/build + ctest --output-on-failure \ No newline at end of file diff --git a/.gitignore b/.gitignore index a41c6bc..9ac0615 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,19 @@ -# Prerequisites *.d - -# Compiled Object files *.slo *.lo *.o *.obj - -# Precompiled Headers *.gch *.pch - -# Compiled Dynamic libraries *.so *.dylib *.dll - -# Fortran module files *.mod *.smod - -# Compiled Static libraries *.lai *.la *.a *.lib - -# Executables *.exe *.out *.app diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fef486..8827b03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,48 +1,39 @@ -# build instructions-- -# cmake -S . -B build -# cmake --build build -# cmake --install build cmake_minimum_required(VERSION 3.25) project(tidesdb_cpp CXX) -set(CMAKE_CXX_STANDARD 11) -set(PROJECT_VERSION 0.1.0) +set(CMAKE_CXX_STANDARD 17) +set(PROJECT_VERSION 1.0.0) add_compile_options(-Wextra -Wall -Werror) -find_library(LIBRARY_TIDEDB NAMES tidesdb REQUIRED) # require tidesdb library +find_library(LIBRARY_TIDEDB NAMES tidesdb REQUIRED) -add_library(tidesdb_cpp SHARED tidesdb.cpp) - -install(FILES tidesdb.hpp - DESTINATION include) +# INTERFACE library for header-only +add_library(tidesdb_cpp INTERFACE) if(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64") - target_include_directories(tidesdb_cpp PUBLIC /opt/homebrew/include) - target_link_directories(tidesdb_cpp PUBLIC /opt/homebrew/lib) + target_include_directories(tidesdb_cpp INTERFACE /opt/homebrew/include) + target_link_directories(tidesdb_cpp INTERFACE /opt/homebrew/lib) elseif(APPLE) - target_include_directories(tidesdb_cpp PUBLIC /usr/local/include) - target_link_directories(tidesdb_cpp PUBLIC /usr/local/lib) + target_include_directories(tidesdb_cpp INTERFACE /usr/local/include) + target_link_directories(tidesdb_cpp INTERFACE /usr/local/lib) endif() -target_link_libraries(tidesdb_cpp PUBLIC ${LIBRARY_TIDEDB}) +target_link_libraries(tidesdb_cpp INTERFACE ${LIBRARY_TIDEDB}) -install(TARGETS tidesdb_cpp - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib) +install(FILES tidesdb.hpp DESTINATION include) +install(TARGETS tidesdb_cpp EXPORT tidesdb_cpp-targets) include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip ) - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(googletest) enable_testing() - add_executable(tests test/tests.cpp) -target_link_libraries(tests GTest::gtest_main tidesdb_cpp ${LIBRARY_TIDEDB}) +target_link_libraries(tests GTest::gtest_main tidesdb_cpp) include(GoogleTest) gtest_discover_tests(tests) \ No newline at end of file diff --git a/test/tests.cpp b/test/tests.cpp index 7cc6e8e..0339613 100644 --- a/test/tests.cpp +++ b/test/tests.cpp @@ -1,8 +1,7 @@ -/* +/** * * Copyright (C) TidesDB - * - * Original Author: Evgeny Kornev + * Original Author: Alex Gaetano Padula * * Licensed under the Mozilla Public License, v. 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,99 +17,565 @@ */ #include +#include +#include +#include +#include + #include "../tidesdb.hpp" -std::string column_name = "my_db"; -const int flush_threshold = (1024 * 1024) * 128; -const tidesdb_compression_algo_t compression_algo = TDB_COMPRESS_SNAPPY; -const float probability = TDB_DEFAULT_SKIP_LIST_PROBABILITY; -const int max_level = TDB_DEFAULT_SKIP_LIST_MAX_LEVEL; -const bool bloom_filter = true; -const bool compressed = true; +namespace fs = std::filesystem; + +// Test fixture for database tests +class TidesDBTest : public ::testing::Test +{ + protected: + std::string db_path; + + void SetUp() override + { + db_path = "./test_db_" + + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()); + } + + void TearDown() override + { + cleanup_db(db_path); + } + + void cleanup_db(const std::string& path) + { + if (fs::exists(path)) + { + fs::remove_all(path); + } + } +}; + +// Open and close database +TEST_F(TidesDBTest, OpenClose) +{ + { + tidesdb::DB db(db_path); + // Database automatically closed on destruction + } +} + +// Create and drop column family +TEST_F(TidesDBTest, CreateDropColumnFamily) +{ + tidesdb::DB db(db_path); + + // Create column family + db.create_column_family("test_cf"); + + // Verify it exists + auto cf_list = db.list_column_families(); + EXPECT_NE(std::find(cf_list.begin(), cf_list.end(), "test_cf"), cf_list.end()); + + // Drop column family + db.drop_column_family("test_cf"); -TEST(TidesDB, Open_and_Close) + // Verify it's gone + cf_list = db.list_column_families(); + EXPECT_EQ(std::find(cf_list.begin(), cf_list.end(), "test_cf"), cf_list.end()); +} + +// Create column family with custom config +TEST_F(TidesDBTest, CreateColumnFamilyWithConfig) { - TidesDB::DB db; - EXPECT_EQ(db.Open("tmp"), 0); - EXPECT_EQ(db.Close(), 0); - _tidesdb_remove_directory("tmp"); + tidesdb::DB db(db_path); + + tidesdb::ColumnFamilyConfig config; + config.memtable_flush_size = 128 * 1024 * 1024; // 128MB + config.max_sstables_before_compaction = 512; + config.compaction_threads = 4; + config.compressed = true; + config.compress_algo = tidesdb::CompressionAlgo::LZ4; + config.bloom_filter_fp_rate = 0.01; + config.enable_background_compaction = true; + config.sync_mode = tidesdb::SyncMode::BACKGROUND; + config.sync_interval = 1000; + + db.create_column_family("custom_cf", config); + + // Verify configuration + auto stats = db.get_column_family_stats("custom_cf"); + EXPECT_EQ(stats.config.memtable_flush_size, 128 * 1024 * 1024); + EXPECT_EQ(stats.config.compress_algo, tidesdb::CompressionAlgo::LZ4); + EXPECT_TRUE(stats.config.compressed); + + db.drop_column_family("custom_cf"); } -TEST(TidesDB, Create_and_Drop_Column_Family) + +// List column families +TEST_F(TidesDBTest, ListColumnFamilies) { - TidesDB::DB db; - EXPECT_EQ(db.Open("tmp"), 0); - EXPECT_EQ(db.CreateColumnFamily(column_name, flush_threshold, max_level, probability, - compressed, compression_algo, bloom_filter), - 0); - EXPECT_EQ(db.DropColumnFamily(column_name), 0); - EXPECT_EQ(db.Close(), 0); - _tidesdb_remove_directory("tmp"); + tidesdb::DB db(db_path); + + // Create multiple column families + std::vector cf_names = {"cf1", "cf2", "cf3"}; + for (const auto& name : cf_names) + { + db.create_column_family(name); + } + + // List them + auto cf_list = db.list_column_families(); + EXPECT_EQ(cf_list.size(), cf_names.size()); + + for (const auto& name : cf_names) + { + EXPECT_NE(std::find(cf_list.begin(), cf_list.end(), name), cf_list.end()); + } + + // Clean up + for (const auto& name : cf_names) + { + db.drop_column_family(name); + } +} + +// Transaction put, get, delete +TEST_F(TidesDBTest, TransactionPutGetDelete) +{ + tidesdb::DB db(db_path); + db.create_column_family("test_cf"); + + // Put data + { + auto txn = db.begin_transaction(); + txn->put("test_cf", "key1", "value1"); + txn->put("test_cf", "key2", "value2"); + txn->commit(); + } + + // Get data + { + auto txn = db.begin_read_transaction(); + auto value1 = txn->get("test_cf", "key1"); + EXPECT_EQ(value1, "value1"); + + auto value2 = txn->get("test_cf", "key2"); + EXPECT_EQ(value2, "value2"); + } + + // Delete data + { + auto txn = db.begin_transaction(); + txn->remove("test_cf", "key1"); + txn->commit(); + } + + // Verify deletion + { + auto txn = db.begin_read_transaction(); + EXPECT_THROW({ txn->get("test_cf", "key1"); }, tidesdb::Exception); + } + + db.drop_column_family("test_cf"); } -TEST(TidesDB, Create_and_Column_Family_and_Put) +// Transaction with TTL +TEST_F(TidesDBTest, TransactionWithTTL) { - TidesDB::DB db; - EXPECT_EQ(db.Open("tmp"), 0); - EXPECT_EQ(db.CreateColumnFamily(column_name, flush_threshold, max_level, probability, - compressed, compression_algo, bloom_filter), - 0); - const std::vector key = {'k', 'e', 'y'}; - const std::vector value = {'v', 'a', 'l', 'u', 'e'}; - EXPECT_EQ(db.Put(column_name, &key, &value, std::chrono::seconds(-1)), 0); - EXPECT_EQ(db.DropColumnFamily(column_name), 0); - EXPECT_EQ(db.Close(), 0); - _tidesdb_remove_directory("tmp"); + tidesdb::DB db(db_path); + db.create_column_family("test_cf"); + + // Put with TTL (2 seconds from now) + auto ttl = std::time(nullptr) + 2; + { + auto txn = db.begin_transaction(); + txn->put("test_cf", "temp_key", "temp_value", ttl); + txn->commit(); + } + + // Verify it exists + { + auto txn = db.begin_read_transaction(); + auto value = txn->get("test_cf", "temp_key"); + EXPECT_EQ(value, "temp_value"); + } + + // Wait for expiration + std::this_thread::sleep_for(std::chrono::seconds(3)); + + // Verify it's expired + { + auto txn = db.begin_read_transaction(); + EXPECT_THROW({ txn->get("test_cf", "temp_key"); }, tidesdb::Exception); + } + + db.drop_column_family("test_cf"); } -TEST(TidesDB, Put_and_Get) +// Multi-operation transaction +TEST_F(TidesDBTest, MultiOperationTransaction) { - TidesDB::DB db; - std::vector got_value; - EXPECT_EQ(db.Open("tmp"), 0); - EXPECT_EQ(db.CreateColumnFamily(column_name, flush_threshold, max_level, probability, - compressed, compression_algo, bloom_filter), - 0); + tidesdb::DB db(db_path); + db.create_column_family("test_cf"); + + // Multiple operations + { + auto txn = db.begin_transaction(); + for (int i = 1; i <= 10; ++i) + { + std::string key = "key" + std::to_string(i); + std::string value = "value" + std::to_string(i); + txn->put("test_cf", key, value); + } + txn->commit(); + } + + // Verify all were written + { + auto txn = db.begin_read_transaction(); + for (int i = 1; i <= 10; ++i) + { + std::string key = "key" + std::to_string(i); + std::string expected = "value" + std::to_string(i); + auto value = txn->get("test_cf", key); + EXPECT_EQ(value, expected); + } + } - const std::vector key = {'k', 'e', 'y'}; - const std::vector value = {'v', 'a', 'l', 'u', 'e'}; - EXPECT_EQ(db.Put(column_name, &key, &value, std::chrono::seconds(-1)), 0); - EXPECT_EQ(db.Get(column_name, &key, &got_value), 0); - EXPECT_EQ(std::string(got_value.begin(), got_value.end()), - std::string(value.begin(), value.end())); - EXPECT_EQ(db.DropColumnFamily(column_name), 0); - EXPECT_EQ(db.Close(), 0); - _tidesdb_remove_directory("tmp"); + db.drop_column_family("test_cf"); } -TEST(TidesDB, Put_and_Delete) +// Transaction rollback +TEST_F(TidesDBTest, TransactionRollback) { - TidesDB::DB db; - std::vector got_value; + tidesdb::DB db(db_path); + db.create_column_family("test_cf"); - EXPECT_EQ(db.Open("tmp"), 0); - EXPECT_EQ(db.CreateColumnFamily(column_name, flush_threshold, max_level, probability, - compressed, compression_algo, true), - 0); - const std::vector key = {'k', 'e', 'y'}; - const std::vector value = {'v', 'a', 'l', 'u', 'e'}; - EXPECT_EQ(db.Put(column_name, &key, &value, std::chrono::seconds(-1)), 0); - EXPECT_EQ(db.Delete(column_name, &key), 0); + // Put and rollback + { + auto txn = db.begin_transaction(); + txn->put("test_cf", "rollback_key", "rollback_value"); + txn->rollback(); + } - try + // Verify data wasn't written { - db.Get(column_name, &key, &got_value); - FAIL() << "Expected std::runtime_error"; + auto txn = db.begin_read_transaction(); + EXPECT_THROW({ txn->get("test_cf", "rollback_key"); }, tidesdb::Exception); } - catch (const std::runtime_error& e) + + db.drop_column_family("test_cf"); +} + +// Forward iteration +TEST_F(TidesDBTest, ForwardIteration) +{ + tidesdb::DB db(db_path); + db.create_column_family("test_cf"); + + // Insert test data + std::map test_data = {{"key1", "value1"}, + {"key2", "value2"}, + {"key3", "value3"}, + {"key4", "value4"}, + {"key5", "value5"}}; + { - EXPECT_STREQ(e.what(), "Error 22: Key not found.\n"); + auto txn = db.begin_transaction(); + for (const auto& [key, value] : test_data) + { + txn->put("test_cf", key, value); + } + txn->commit(); } - catch (...) + + // Iterate forward { - FAIL() << "Expected std::runtime_error"; + auto txn = db.begin_read_transaction(); + auto iter = txn->new_iterator("test_cf"); + iter->seek_to_first(); + + int count = 0; + while (iter->valid()) + { + auto key = iter->key_string(); + auto value = iter->value_string(); + + EXPECT_NE(test_data.find(key), test_data.end()); + EXPECT_EQ(test_data[key], value); + + count++; + iter->next(); + } + + EXPECT_EQ(count, static_cast(test_data.size())); } - EXPECT_EQ(db.DropColumnFamily(column_name), 0); - EXPECT_EQ(db.Close(), 0); - _tidesdb_remove_directory("tmp"); + db.drop_column_family("test_cf"); } + +// Backward iteration +TEST_F(TidesDBTest, BackwardIteration) +{ + tidesdb::DB db(db_path); + db.create_column_family("test_cf"); + + // Insert test data + std::map test_data = { + {"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}}; + + { + auto txn = db.begin_transaction(); + for (const auto& [key, value] : test_data) + { + txn->put("test_cf", key, value); + } + txn->commit(); + } + + // Iterate backward + { + auto txn = db.begin_read_transaction(); + auto iter = txn->new_iterator("test_cf"); + iter->seek_to_last(); + + int count = 0; + while (iter->valid()) + { + auto key = iter->key_string(); + auto value = iter->value_string(); + + EXPECT_NE(test_data.find(key), test_data.end()); + EXPECT_EQ(test_data[key], value); + + count++; + iter->prev(); + } + + EXPECT_EQ(count, static_cast(test_data.size())); + } + + db.drop_column_family("test_cf"); +} + +// Get column family stats +TEST_F(TidesDBTest, GetColumnFamilyStats) +{ + tidesdb::DB db(db_path); + + tidesdb::ColumnFamilyConfig config; + config.memtable_flush_size = 2 * 1024 * 1024; // 2MB + config.max_level = 12; + config.compressed = true; + config.compress_algo = tidesdb::CompressionAlgo::SNAPPY; + config.bloom_filter_fp_rate = 0.01; + + db.create_column_family("test_cf", config); + + // Add some data + { + auto txn = db.begin_transaction(); + for (int i = 0; i < 10; ++i) + { + std::string key = "key" + std::to_string(i); + std::string value = "value" + std::to_string(i); + txn->put("test_cf", key, value); + } + txn->commit(); + } + + // Get statistics + auto stats = db.get_column_family_stats("test_cf"); + + EXPECT_EQ(stats.name, "test_cf"); + EXPECT_EQ(stats.config.memtable_flush_size, 2 * 1024 * 1024); + EXPECT_EQ(stats.config.max_level, 12); + EXPECT_TRUE(stats.config.compressed); + EXPECT_EQ(stats.config.compress_algo, tidesdb::CompressionAlgo::SNAPPY); + EXPECT_GT(stats.memtable_entries, 0); + + db.drop_column_family("test_cf"); +} + +// Compaction +TEST_F(TidesDBTest, Compaction) +{ + tidesdb::DB db(db_path); + + // Create CF with small flush threshold + tidesdb::ColumnFamilyConfig config; + config.memtable_flush_size = 1024; // 1KB + config.enable_background_compaction = false; + config.compaction_threads = 2; + + db.create_column_family("test_cf", config); + + // Add data to create SSTables + for (int batch = 0; batch < 5; ++batch) + { + auto txn = db.begin_transaction(); + for (int i = 0; i < 20; ++i) + { + std::string key = "key" + std::to_string(batch) + "_" + std::to_string(i); + std::string value(512, 'x'); // 512 bytes + txn->put("test_cf", key, value); + } + txn->commit(); + } + + // Get column family + auto cf = db.get_column_family("test_cf"); + + // Get stats before compaction + auto stats_before = db.get_column_family_stats("test_cf"); + + // Perform compaction if we have enough SSTables + if (stats_before.num_sstables >= 2) + { + cf.compact(); + + auto stats_after = db.get_column_family_stats("test_cf"); + // Stats should be available after compaction + EXPECT_EQ(stats_after.name, "test_cf"); + } + + db.drop_column_family("test_cf"); +} + +// Sync modes +TEST_F(TidesDBTest, SyncModes) +{ + tidesdb::DB db(db_path); + + struct SyncModeTest + { + tidesdb::SyncMode mode; + std::string name; + }; + + std::vector sync_modes = {{tidesdb::SyncMode::NONE, "none"}, + {tidesdb::SyncMode::BACKGROUND, "background"}, + {tidesdb::SyncMode::FULL, "full"}}; + + for (const auto& sm : sync_modes) + { + std::string cf_name = "cf_" + sm.name; + + tidesdb::ColumnFamilyConfig config; + config.sync_mode = sm.mode; + config.sync_interval = (sm.mode == tidesdb::SyncMode::BACKGROUND) ? 1000 : 0; + + db.create_column_family(cf_name, config); + + // Verify sync mode + auto stats = db.get_column_family_stats(cf_name); + EXPECT_EQ(stats.config.sync_mode, sm.mode); + + db.drop_column_family(cf_name); + } +} + +// Compression algorithms +TEST_F(TidesDBTest, CompressionAlgorithms) +{ + tidesdb::DB db(db_path); + + struct AlgoTest + { + tidesdb::CompressionAlgo algo; + std::string name; + bool compressed; + }; + + std::vector algorithms = {{tidesdb::CompressionAlgo::SNAPPY, "snappy", true}, + {tidesdb::CompressionAlgo::LZ4, "lz4", true}, + {tidesdb::CompressionAlgo::ZSTD, "zstd", true}}; + + // Test with compression enabled + for (const auto& alg : algorithms) + { + std::string cf_name = "cf_" + alg.name; + + tidesdb::ColumnFamilyConfig config; + config.compressed = alg.compressed; + config.compress_algo = alg.algo; + + db.create_column_family(cf_name, config); + + // Verify compression + auto stats = db.get_column_family_stats(cf_name); + EXPECT_TRUE(stats.config.compressed); + EXPECT_EQ(stats.config.compress_algo, alg.algo); + + db.drop_column_family(cf_name); + } + + // Test with compression disabled + { + tidesdb::ColumnFamilyConfig config; + config.compressed = false; + db.create_column_family("cf_no_compression", config); + + auto stats = db.get_column_family_stats("cf_no_compression"); + EXPECT_FALSE(stats.config.compressed); + + db.drop_column_family("cf_no_compression"); + } +} + +// Error handling +TEST_F(TidesDBTest, ErrorHandling) +{ + tidesdb::DB db(db_path); + + // Try to get stats for non-existent CF + EXPECT_THROW({ db.get_column_family_stats("nonexistent_cf"); }, tidesdb::Exception); + + // Try to drop non-existent CF + EXPECT_THROW({ db.drop_column_family("nonexistent_cf"); }, tidesdb::Exception); +} + +// Binary data +TEST_F(TidesDBTest, BinaryData) +{ + tidesdb::DB db(db_path); + db.create_column_family("test_cf"); + + // Store binary data + std::vector binary_key = {0x00, 0x01, 0x02, 0xFF}; + std::vector binary_value = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF}; + + { + auto txn = db.begin_transaction(); + txn->put("test_cf", binary_key, binary_value); + txn->commit(); + } + + // Retrieve binary data + { + auto txn = db.begin_read_transaction(); + auto value = txn->get("test_cf", binary_key); + EXPECT_EQ(value, binary_value); + } + + db.drop_column_family("test_cf"); +} + +// Move semantics +TEST_F(TidesDBTest, MoveSemantics) +{ + // Test DB move + tidesdb::DB db1(db_path); + db1.create_column_family("test_cf"); + + tidesdb::DB db2 = std::move(db1); + + // db2 should work, db1 should be moved-from + auto cf_list = db2.list_column_families(); + EXPECT_FALSE(cf_list.empty()); + + db2.drop_column_family("test_cf"); +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/tidesdb.cpp b/tidesdb.cpp deleted file mode 100644 index bd315fa..0000000 --- a/tidesdb.cpp +++ /dev/null @@ -1,379 +0,0 @@ -/* - * - * Copyright (C) TidesDB - * - * Original Author: Evgeny Kornev - * - * Licensed under the Mozilla Public License, v. 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.mozilla.org/en-US/MPL/2.0/ - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include "tidesdb.hpp" - -namespace TidesDB -{ - -DB::DB() -{ - tdb = nullptr; -} - -static void inline err_handler(tidesdb_err_t *err) -{ - if (err) - { - std::string error_message = err->message; - const int error_code = err->code; - tidesdb_err_free(err); - throw std::runtime_error("Error " + std::to_string(error_code) + ": " + error_message); - } -} - -int DB::Open(const std::string &dir_name) -{ - tidesdb_err_t *err = tidesdb_open(dir_name.c_str(), &this->tdb); - err_handler(err); - return 0; -} - -int DB::Close() const -{ - tidesdb_err_t *err = tidesdb_close(this->tdb); - err_handler(err); - return 0; -} - -int DB::CreateColumnFamily(const std::string &column_family_name, int flush_threshold, - int max_level, float probability, bool compressed, - tidesdb_compression_algo_t compress_algo, bool bloom_filter) const -{ - tidesdb_err_t *err = tidesdb_create_column_family(this->tdb, column_family_name.c_str(), - flush_threshold, max_level, probability, - compressed, compress_algo, bloom_filter); - err_handler(err); - return 0; -} - -int DB::DropColumnFamily(const std::string &column_family_name) const -{ - tidesdb_err_t *err = tidesdb_drop_column_family(this->tdb, column_family_name.c_str()); - err_handler(err); - return 0; -} - -int DB::Put(const std::string &column_family_name, const std::vector *key, - const std::vector *value, std::chrono::seconds ttl) const -{ - size_t key_size = key->size(); - size_t value_size = value->size(); - time_t ttl_time = ttl.count(); - tidesdb_err_t *err = tidesdb_put(this->tdb, column_family_name.c_str(), key->data(), key_size, - value->data(), value_size, ttl_time); - err_handler(err); - return 0; -} - -int DB::Get(const std::string &column_family_name, const std::vector *key, - std::vector *value) const -{ - size_t key_size = key->size(); - unsigned char *value_data = nullptr; - unsigned long value_size = 0; - tidesdb_err_t *err = tidesdb_get(this->tdb, column_family_name.c_str(), key->data(), key_size, - &value_data, &value_size); - if (err == nullptr) - { - value->assign(value_data, value_data + value_size); - free(value_data); - } - err_handler(err); - return 0; -} - -int DB::Delete(const std::string &column_family_name, const std::vector *key) const -{ - size_t key_size = key->size(); - tidesdb_err_t *err = - tidesdb_delete(this->tdb, column_family_name.c_str(), key->data(), key_size); - err_handler(err); - return 0; -} - -int DB::CompactSSTables(const std::string &column_family_name, int max_threads) const -{ - tidesdb_err_t *err = - tidesdb_compact_sstables(this->tdb, column_family_name.c_str(), max_threads); - err_handler(err); - return 0; -} - -int DB::StartIncrementalMerges(const std::string &column_family_name, std::chrono::seconds seconds, - int min_sstables) const -{ - auto duration = seconds.count(); - if (duration > std::numeric_limits::max() || duration < std::numeric_limits::min()) - { - return -1; - } - - tidesdb_err_t *err = tidesdb_start_incremental_merge(this->tdb, column_family_name.c_str(), - static_cast(duration), min_sstables); - err_handler(err); - return 0; -} - -int DB::Range(const std::string &column_family_name, const std::vector *start_key, - const std::vector *end_key, std::vector, - std::vector>> *result) const -{ - size_t start_key_size = start_key->size(); - size_t end_key_size = end_key->size(); - tidesdb_key_value_pair_t **c_result = nullptr; - size_t result_size = 0; - - tidesdb_err_t *err = tidesdb_range(this->tdb, column_family_name.c_str(), - start_key->data(), start_key_size, - end_key->data(), end_key_size, - &c_result, &result_size); - - if (err == nullptr && c_result != nullptr) - { - result->clear(); - result->reserve(result_size); - - for (size_t i = 0; i < result_size; i++) - { - std::vector key(c_result[i]->key, c_result[i]->key + c_result[i]->key_size); - std::vector value(c_result[i]->value, c_result[i]->value + c_result[i]->value_size); - result->emplace_back(key, value); - - (void)_tidesdb_free_key_value_pair(c_result[i]); - } - - free(c_result); - } - - err_handler(err); - return 0; -} - -int DB::ListColumnFamilies(std::vector *families) const -{ - char *c_list = nullptr; - - tidesdb_err_t *err = tidesdb_list_column_families(this->tdb, &c_list); - - if (err == nullptr && c_list != nullptr) - { - families->clear(); - - /* we parse the comma-separated list */ - char *token = strtok(c_list, ","); - while (token != nullptr) - { - families->emplace_back(token); - token = strtok(nullptr, ","); - } - - free(c_list); - } - - err_handler(err); - return 0; -} - -int DB::DeleteByRange(const std::string &column_family_name, const std::vector *start_key, - const std::vector *end_key) const -{ - size_t start_key_size = start_key->size(); - size_t end_key_size = end_key->size(); - - tidesdb_err_t *err = tidesdb_delete_by_range(this->tdb, column_family_name.c_str(), - start_key->data(), start_key_size, - end_key->data(), end_key_size); - - err_handler(err); - return 0; -} - -int DB::GetColumnFamilyStat(const std::string &column_family_name, - ColumnFamilyStat *stat) const -{ - tidesdb_column_family_stat_t *c_stat = nullptr; - - tidesdb_err_t *err = tidesdb_get_column_family_stat(this->tdb, column_family_name.c_str(), - &c_stat); - - if (err == nullptr && c_stat != nullptr) - { - /* we convert C stat to C++ stat */ - stat->name = std::string(c_stat->cf_name); - stat->num_sstables = c_stat->num_sstables; - stat->memtable_size = c_stat->memtable_size; - stat->memtable_entries_count = c_stat->memtable_entries_count; - stat->incremental_merging = c_stat->incremental_merging; - - /* we copy configuration */ - stat->config.name = std::string(c_stat->config.name); - stat->config.flush_threshold = c_stat->config.flush_threshold; - stat->config.max_level = c_stat->config.max_level; - stat->config.probability = c_stat->config.probability; - stat->config.compressed = c_stat->config.compressed; - stat->config.compress_algo = c_stat->config.compress_algo; - stat->config.bloom_filter = c_stat->config.bloom_filter; - - /* we copy sstable stats */ - stat->sstable_stats.clear(); - for (int i = 0; i < c_stat->num_sstables; i++) - { - SSTableStat sstable_stat; - sstable_stat.path = std::string(c_stat->sstable_stats[i]->sstable_path); - sstable_stat.size = c_stat->sstable_stats[i]->size; - sstable_stat.num_blocks = c_stat->sstable_stats[i]->num_blocks; - stat->sstable_stats.push_back(sstable_stat); - } - - /* we free the memory allocated by the C API */ - (void)tidesdb_free_column_family_stat(c_stat); - } - - err_handler(err); - return 0; -} - -Txn::Txn(const DB *db) -{ - this->tdb = db->GetTidesDB(); - this->txn = nullptr; -} - -Txn::~Txn() -{ - if (this->txn) - { - (void)tidesdb_txn_free(this->txn); - } -} - -int Txn::Begin() -{ - tidesdb_err_t *err = tidesdb_txn_begin(this->tdb, &this->txn, nullptr); - err_handler(err); - return 0; -} - -int Txn::Get(const std::vector *key, std::vector *value) const -{ - size_t key_size = key->size(); - unsigned char *value_data = nullptr; - unsigned long value_size = 0; - tidesdb_err_t *err = - tidesdb_txn_get(this->txn, key->data(), key_size, &value_data, &value_size); - if (err == nullptr) - { - value->assign(value_data, value_data + value_size); - free(value_data); - } - err_handler(err); - return 0; -} - -int Txn::Put(const std::vector *key, const std::vector *value, - std::chrono::seconds ttl) const -{ - auto ttl_time = std::chrono::duration_cast(ttl).count(); - tidesdb_err_t *err = tidesdb_txn_put(this->txn, key->data(), size_t(key->size()), value->data(), - size_t(value->size()), ttl_time); - err_handler(err); - return 0; -} - -int Txn::Delete(const std::vector *key) const -{ - tidesdb_err_t *err = tidesdb_txn_delete(this->txn, key->data(), size_t(key->size())); - err_handler(err); - return 0; -} - -int Txn::Commit() const -{ - tidesdb_err_t *err = tidesdb_txn_commit(this->txn); - err_handler(err); - return 0; -} - -int Txn::Rollback() const -{ - tidesdb_err_t *err = tidesdb_txn_rollback(this->txn); - if (err) - { - std::string error_message = err->message; - int error_code = err->code; - (void)tidesdb_err_free(err); - throw std::runtime_error("Error " + std::to_string(error_code) + ": " + error_message); - } - return 0; -} - -Cursor::Cursor(const DB *db, std::string column_family_name) -{ - this->tdb = db->GetTidesDB(); - this->cursor = nullptr; - this->column_family_name = std::move(column_family_name); -} - -int Cursor::Init() -{ - tidesdb_err_t *err = tidesdb_cursor_init(tdb, this->column_family_name.c_str(), &this->cursor); - err_handler(err); - return 0; -} - -Cursor::~Cursor() -{ - if (this->cursor) - { - (void)tidesdb_cursor_free(this->cursor); - } -} - -int Cursor::Next() const -{ - tidesdb_err_t *err = tidesdb_cursor_next(this->cursor); - err_handler(err); - return 0; -} - -int Cursor::Prev() const -{ - tidesdb_err_t *err = tidesdb_cursor_prev(this->cursor); - err_handler(err); - return 0; -} - -int Cursor::Get(std::vector &key, std::vector &value) const -{ - size_t key_size = key.size(); - size_t value_size = value.size(); - uint8_t *key_data = key.data(); - uint8_t *value_data = value.data(); - tidesdb_err_t *err = - tidesdb_cursor_get(this->cursor, &key_data, &key_size, &value_data, &value_size); - err_handler(err); - return 0; -} - -tidesdb_t *DB::GetTidesDB() const -{ - return this->tdb; -} - -} /* namespace TidesDB */ \ No newline at end of file diff --git a/tidesdb.hpp b/tidesdb.hpp index 2c66c9a..d389e59 100644 --- a/tidesdb.hpp +++ b/tidesdb.hpp @@ -1,8 +1,7 @@ -/* +/** * * Copyright (C) TidesDB - * - * Original Author: Evgeny Kornev + * Original Author: Alex Gaetano Padula * * Licensed under the Mozilla Public License, v. 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,261 +15,875 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#ifndef TIDESDB_HPP +#define TIDESDB_HPP +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#pragma once +using atomic_size_t = std::atomic; +using atomic_uint64_t = std::atomic; +using atomic_uint_fast64_t = std::atomic; + +#define _Atomic(T) std::atomic +#define _TIDESDB_ATOMIC_TYPES_DEFINED +extern "C" +{ #include +} -#include -#include -#include -#include +#undef _Atomic +#undef _TIDESDB_ATOMIC_TYPES_DEFINED + +namespace tidesdb +{ + +// Forward declarations +class Transaction; +class Iterator; +class ColumnFamily; -/* - * TidesDB Namespace - * contains database classes +/** + * @brief TidesDB exception class */ -namespace TidesDB +class Exception : public std::runtime_error { + public: + explicit Exception(const std::string& message, int error_code = TDB_ERROR) + : std::runtime_error(message), error_code_(error_code) + { + } + + int error_code() const noexcept + { + return error_code_; + } + + static Exception from_code(int code, const std::string& context = "") + { + std::string message; + + switch (code) + { + case TDB_ERR_MEMORY: + message = "memory allocation failed"; + break; + case TDB_ERR_INVALID_ARGS: + message = "invalid arguments"; + break; + case TDB_ERR_IO: + message = "I/O error"; + break; + case TDB_ERR_NOT_FOUND: + message = "not found"; + break; + case TDB_ERR_EXISTS: + message = "already exists"; + break; + case TDB_ERR_CORRUPT: + message = "data corruption"; + break; + case TDB_ERR_LOCK: + message = "lock acquisition failed"; + break; + case TDB_ERR_TXN_COMMITTED: + message = "transaction already committed"; + break; + case TDB_ERR_TXN_ABORTED: + message = "transaction aborted"; + break; + case TDB_ERR_READONLY: + message = "read-only transaction"; + break; + case TDB_ERR_FULL: + message = "database full"; + break; + case TDB_ERR_INVALID_NAME: + message = "invalid name"; + break; + case TDB_ERR_COMPARATOR_NOT_FOUND: + message = "comparator not found"; + break; + case TDB_ERR_MAX_COMPARATORS: + message = "max comparators reached"; + break; + case TDB_ERR_INVALID_CF: + message = "invalid column family"; + break; + case TDB_ERR_THREAD: + message = "thread operation failed"; + break; + case TDB_ERR_CHECKSUM: + message = "checksum verification failed"; + break; + case TDB_ERR_KEY_DELETED: + message = "key deleted"; + break; + case TDB_ERR_KEY_EXPIRED: + message = "key expired"; + break; + default: + message = "unknown error"; + break; + } + + if (!context.empty()) + { + message = context + ": " + message + " (code: " + std::to_string(code) + ")"; + } + else + { + message = message + " (code: " + std::to_string(code) + ")"; + } + + return Exception(message, code); + } + + private: + int error_code_; +}; + +/** + * @brief Compression algorithm enumeration + */ +enum class CompressionAlgo +{ + SNAPPY = COMPRESS_SNAPPY, + LZ4 = COMPRESS_LZ4, + ZSTD = COMPRESS_ZSTD +}; -/* - * SSTableStat Struct - * represents statistics about an SSTable. +/** + * @brief Sync mode enumeration */ -struct SSTableStat +enum class SyncMode { - std::string path; - size_t size; - size_t num_blocks; + NONE = TDB_SYNC_NONE, + BACKGROUND = TDB_SYNC_BACKGROUND, + FULL = TDB_SYNC_FULL }; -/* - * ColumnFamilyConfig Struct - * represents configuration for a column family. +/** + * @brief Column family configuration */ struct ColumnFamilyConfig { - std::string name; - int32_t flush_threshold; - int32_t max_level; - float probability; - bool compressed; - tidesdb_compression_algo_t compress_algo; - bool bloom_filter; + size_t memtable_flush_size = 67108864; // 64MB + int max_sstables_before_compaction = 512; + int compaction_threads = 4; + int max_level = 12; + float probability = 0.25f; + bool compressed = true; + CompressionAlgo compress_algo = CompressionAlgo::SNAPPY; + double bloom_filter_fp_rate = 0.01; + bool enable_background_compaction = true; + int background_compaction_interval = 1000000; // 1 second in microseconds + bool use_sbha = true; + SyncMode sync_mode = SyncMode::BACKGROUND; + int sync_interval = 1000; + std::optional comparator_name; + + /** + * @brief Get default column family configuration + */ + static ColumnFamilyConfig default_config() + { + auto c_config = tidesdb_default_column_family_config(); + ColumnFamilyConfig config; + config.memtable_flush_size = c_config.memtable_flush_size; + config.max_sstables_before_compaction = c_config.max_sstables_before_compaction; + config.compaction_threads = c_config.compaction_threads; + config.max_level = c_config.max_level; + config.probability = c_config.probability; + config.compressed = c_config.compressed != 0; + config.compress_algo = static_cast(c_config.compress_algo); + config.bloom_filter_fp_rate = c_config.bloom_filter_fp_rate; + config.enable_background_compaction = c_config.enable_background_compaction != 0; + config.background_compaction_interval = c_config.background_compaction_interval; + config.use_sbha = c_config.use_sbha != 0; + config.sync_mode = static_cast(c_config.sync_mode); + config.sync_interval = c_config.sync_interval; + return config; + } + + /** + * @brief Convert to C structure + */ + tidesdb_column_family_config_t to_c_struct() const + { + tidesdb_column_family_config_t c_config; + c_config.memtable_flush_size = memtable_flush_size; + c_config.max_sstables_before_compaction = max_sstables_before_compaction; + c_config.compaction_threads = compaction_threads; + c_config.max_level = max_level; + c_config.probability = probability; + c_config.compressed = compressed ? 1 : 0; + c_config.compress_algo = static_cast(compress_algo); + c_config.bloom_filter_fp_rate = bloom_filter_fp_rate; + c_config.enable_background_compaction = enable_background_compaction ? 1 : 0; + c_config.background_compaction_interval = background_compaction_interval; + c_config.use_sbha = use_sbha ? 1 : 0; + c_config.sync_mode = static_cast(sync_mode); + c_config.sync_interval = sync_interval; + c_config.comparator_name = comparator_name ? comparator_name->c_str() : nullptr; + return c_config; + } }; -/* - * ColumnFamilyStat Class - * represents statistics about a column family. +/** + * @brief Column family statistics */ -class ColumnFamilyStat +struct ColumnFamilyStats { - public: std::string name; + std::string comparator_name; int num_sstables; + size_t total_sstable_size; size_t memtable_size; - size_t memtable_entries_count; - bool incremental_merging; + int memtable_entries; ColumnFamilyConfig config; - std::vector sstable_stats; }; -/* - * DB Class - * represents TidesDB database. +/** + * @brief Iterator for traversing key-value pairs */ -class DB +class Iterator { - tidesdb_t *tdb; - public: - /* - * Open - * Opens an existing database or creates a new one. + explicit Iterator(tidesdb_iter_t* iter) : iter_(iter) + { + } + + ~Iterator() + { + if (iter_) + { + tidesdb_iter_free(iter_); + } + } + + // Non-copyable + Iterator(const Iterator&) = delete; + Iterator& operator=(const Iterator&) = delete; + + // Movable + Iterator(Iterator&& other) noexcept : iter_(other.iter_) + { + other.iter_ = nullptr; + } + + Iterator& operator=(Iterator&& other) noexcept + { + if (this != &other) + { + if (iter_) + { + tidesdb_iter_free(iter_); + } + iter_ = other.iter_; + other.iter_ = nullptr; + } + return *this; + } + + /** + * @brief Seek to first entry */ - int Open(const std::string &dir_name); - - /* - * Close - * Closes the database. - */ - [[nodiscard]] int Close() const; - - /* - * CreateColumnFamily - * Creates a new column family. + void seek_to_first() + { + check_valid(); + int result = tidesdb_iter_seek_to_first(iter_); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to seek to first"); + } + } + + /** + * @brief Seek to last entry */ - [[nodiscard]] int CreateColumnFamily(const std::string &name, int flush_threshold, - int max_level, float probability, bool compressed, - tidesdb_compression_algo_t compress_algo, - bool bloom_filter) const; - - /* - * DropColumnFamily - * Drops an existing column family. + void seek_to_last() + { + check_valid(); + int result = tidesdb_iter_seek_to_last(iter_); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to seek to last"); + } + } + + /** + * @brief Check if iterator is at a valid position */ - [[nodiscard]] int DropColumnFamily(const std::string &name) const; - - /* - * Put - * Puts a key-value pair into a column family. + bool valid() const + { + return iter_ && tidesdb_iter_valid(iter_); + } + + /** + * @brief Move to next entry + * @note Returns normally even if iterator becomes invalid (end of iteration) */ - int Put(const std::string &column_family_name, const std::vector *key, - const std::vector *value, std::chrono::seconds ttl) const; - /* - * Get - * Gets a value by key from a column family. + void next() + { + check_valid(); + tidesdb_iter_next(iter_); + } + + /** + * @brief Move to previous entry + * @note Returns normally even if iterator becomes invalid (beginning of iteration) */ - int Get(const std::string &column_family_name, const std::vector *key, - std::vector *value) const; - - /* - * Range - * Gets a range of key-value pairs from a column family. + void prev() + { + check_valid(); + tidesdb_iter_prev(iter_); + } + + /** + * @brief Get current key + * @note The returned vector is a copy. The iterator retains ownership of the internal key data. */ - int Range(const std::string &column_family_name, const std::vector *start_key, - const std::vector *end_key, - std::vector, std::vector>> *result) const; - - /* - * ListColumnFamilies - * Lists the column families in the database. + std::vector key() const + { + check_valid(); + + uint8_t* key_ptr = nullptr; + size_t key_size = 0; + + int result = tidesdb_iter_key(iter_, &key_ptr, &key_size); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to get key"); + } + + // Note: key_ptr points to internal iterator memory, do NOT free it + return std::vector(key_ptr, key_ptr + key_size); + } + + /** + * @brief Get current value + * @note The returned vector is a copy. The iterator retains ownership of the internal value + * data. */ - int ListColumnFamilies(std::vector *families) const; + std::vector value() const + { + check_valid(); - /* - * DeleteByRange - * Deletes a range of key-value pairs from a column family. - */ - int DeleteByRange(const std::string &column_family_name, const std::vector *start_key, - const std::vector *end_key) const; + uint8_t* value_ptr = nullptr; + size_t value_size = 0; - /* - * GetColumnFamilyStat - * Gets statistics about a column family. - */ - int GetColumnFamilyStat(const std::string &column_family_name, ColumnFamilyStat *stat) const; + int result = tidesdb_iter_value(iter_, &value_ptr, &value_size); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to get value"); + } - /* - * Delete - * Deletes a key-value pair from a column family. - */ - int Delete(const std::string &column_family_name, const std::vector *key) const; + // Note: value_ptr points to internal iterator memory, do NOT free it + return std::vector(value_ptr, value_ptr + value_size); + } - /* - * CompactSSTables - * compacts column family sstables by pairing and merging. + /** + * @brief Get current key as string */ - [[nodiscard]] int CompactSSTables(const std::string &column_family_name, int max_threads) const; - - /* - * StartIncrementalMerges - * starts background incremental merges for a column family. + std::string key_string() const + { + auto key_data = key(); + return std::string(key_data.begin(), key_data.end()); + } + + /** + * @brief Get current value as string */ - [[nodiscard]] int StartIncrementalMerges(const std::string &column_family_name, - std::chrono::seconds seconds, int min_sstables) const; - - [[nodiscard]] tidesdb_t *GetTidesDB() const; - - /* constructor */ - DB(); + std::string value_string() const + { + auto value_data = value(); + return std::string(value_data.begin(), value_data.end()); + } + + private: + tidesdb_iter_t* iter_; + + void check_valid() const + { + if (!iter_) + { + throw Exception("Iterator is closed"); + } + } }; -/* - * Txn Class - * represents TidesDB column family transaction. +/** + * @brief Transaction for operations */ -class Txn +class Transaction { - tidesdb_txn_t *txn; - tidesdb_t *tdb; - public: - /* - * Txn - * creates a new transaction for a database. + explicit Transaction(tidesdb_txn_t* txn) : txn_(txn), committed_(false) + { + } + + ~Transaction() + { + if (txn_) + { + tidesdb_txn_free(txn_); + } + } + + // Non-copyable + Transaction(const Transaction&) = delete; + Transaction& operator=(const Transaction&) = delete; + + // Movable + Transaction(Transaction&& other) noexcept : txn_(other.txn_), committed_(other.committed_) + { + other.txn_ = nullptr; + } + + Transaction& operator=(Transaction&& other) noexcept + { + if (this != &other) + { + if (txn_) + { + tidesdb_txn_free(txn_); + } + txn_ = other.txn_; + committed_ = other.committed_; + other.txn_ = nullptr; + } + return *this; + } + + /** + * @brief Put a key-value pair */ - explicit Txn(const DB *db); - ~Txn(); - - /* - * Begin - * begins a transaction. + void put(const std::string& column_family, const std::vector& key, + const std::vector& value, time_t ttl = -1) + { + check_valid(); + + int result = tidesdb_txn_put(txn_, column_family.c_str(), key.data(), key.size(), + value.data(), value.size(), ttl); + + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to put key-value pair"); + } + } + + /** + * @brief Put a key-value pair (string overload) */ - [[nodiscard]] int Begin(); - - /* - * Put - * puts a key-value pair into a column family. + void put(const std::string& column_family, const std::string& key, const std::string& value, + time_t ttl = -1) + { + std::vector key_data(key.begin(), key.end()); + std::vector value_data(value.begin(), value.end()); + put(column_family, key_data, value_data, ttl); + } + + /** + * @brief Get a value */ - int Put(const std::vector *key, const std::vector *value, - std::chrono::seconds ttl) const; + std::vector get(const std::string& column_family, + const std::vector& key) const + { + check_valid(); - /* - * Get - * gets a value by key from a column family. - */ - int Get(const std::vector *key, std::vector *value) const; + uint8_t* value_ptr = nullptr; + size_t value_size = 0; - /* - * Delete - * deletes a key-value pair from a column family. - */ - [[nodiscard]] int Delete(const std::vector *key) const; + int result = tidesdb_txn_get(txn_, column_family.c_str(), key.data(), key.size(), + &value_ptr, &value_size); + + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to get value"); + } - /* - * Commit - * commits the transaction. + std::vector value_data(value_ptr, value_ptr + value_size); + free(value_ptr); + return value_data; + } + + /** + * @brief Get a value (string overload) + */ + std::string get(const std::string& column_family, const std::string& key) const + { + std::vector key_data(key.begin(), key.end()); + auto value_data = get(column_family, key_data); + return std::string(value_data.begin(), value_data.end()); + } + + /** + * @brief Delete a key-value pair */ - [[nodiscard]] int Commit() const; + void remove(const std::string& column_family, const std::vector& key) + { + check_valid(); - /* - * Rollback - * rolls back the transaction. + int result = tidesdb_txn_delete(txn_, column_family.c_str(), key.data(), key.size()); + + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to delete key"); + } + } + + /** + * @brief Delete a key-value pair (string overload) + */ + void remove(const std::string& column_family, const std::string& key) + { + std::vector key_data(key.begin(), key.end()); + remove(column_family, key_data); + } + + /** + * @brief Commit the transaction + */ + void commit() + { + check_valid(); + + if (committed_) + { + throw Exception("Transaction already committed"); + } + + int result = tidesdb_txn_commit(txn_); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to commit transaction"); + } + + committed_ = true; + } + + /** + * @brief Rollback the transaction */ - [[nodiscard]] int Rollback() const; + void rollback() + { + check_valid(); + + if (committed_) + { + throw Exception("Transaction already committed"); + } + + int result = tidesdb_txn_rollback(txn_); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to rollback transaction"); + } + } + + /** + * @brief Create a new iterator + */ + std::unique_ptr new_iterator(const std::string& column_family) + { + check_valid(); + + tidesdb_iter_t* iter = nullptr; + int result = tidesdb_iter_new(txn_, column_family.c_str(), &iter); + + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to create iterator"); + } + + return std::make_unique(iter); + } + + private: + tidesdb_txn_t* txn_; + bool committed_; + + void check_valid() const + { + if (!txn_) + { + throw Exception("Transaction is closed"); + } + } }; -/* - * Cursor Class - * represents TidesDB column family cursor. +/** + * @brief Column family handle */ -class Cursor +class ColumnFamily { - tidesdb_cursor_t *cursor; - tidesdb_t *tdb; - std::string column_family_name; - public: - /* - * Cursor - * creates a new cursor for a column family. + explicit ColumnFamily(tidesdb_column_family_t* cf, const std::string& name) + : cf_(cf), name_(name) + { + } + + /** + * @brief Get column family name */ - Cursor(const DB *db, std::string column_family_name); - ~Cursor(); + const std::string& name() const + { + return name_; + } - /* - * Init - * initializes the cursor. + /** + * @brief Manually trigger compaction */ - [[nodiscard]] int Init(); + void compact() + { + if (!cf_) + { + throw Exception("Column family is invalid"); + } + + int result = tidesdb_compact(cf_); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to compact column family"); + } + } + + private: + tidesdb_column_family_t* cf_; + std::string name_; +}; - /* - * Next - * goes to the next key-value pair in the column family. +/** + * @brief Main TidesDB database class + */ +class DB +{ + public: + /** + * @brief Open a database */ - [[nodiscard]] int Next() const; + explicit DB(const std::string& db_path, bool enable_debug_logging = false, + int max_open_file_handles = TDB_DEFAULT_MAX_OPEN_FILE_HANDLES) + { + tidesdb_config_t config; + // db_path is a char array, need to copy string into it + std::strncpy(config.db_path, db_path.c_str(), sizeof(config.db_path) - 1); + config.db_path[sizeof(config.db_path) - 1] = '\0'; // Ensure null termination + config.enable_debug_logging = enable_debug_logging ? 1 : 0; + config.max_open_file_handles = max_open_file_handles; + + int result = tidesdb_open(&config, &db_); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to open database"); + } + } + + ~DB() + { + if (db_) + { + tidesdb_close(db_); + } + } + + // Non-copyable + DB(const DB&) = delete; + DB& operator=(const DB&) = delete; + + // Movable + DB(DB&& other) noexcept : db_(other.db_) + { + other.db_ = nullptr; + } + + DB& operator=(DB&& other) noexcept + { + if (this != &other) + { + if (db_) + { + tidesdb_close(db_); + } + db_ = other.db_; + other.db_ = nullptr; + } + return *this; + } + + /** + * @brief Create a column family + */ + void create_column_family(const std::string& name, const ColumnFamilyConfig& config = + ColumnFamilyConfig::default_config()) + { + check_valid(); + + auto c_config = config.to_c_struct(); + int result = tidesdb_create_column_family(db_, name.c_str(), &c_config); + + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to create column family"); + } + } + + /** + * @brief Drop a column family + */ + void drop_column_family(const std::string& name) + { + check_valid(); + + int result = tidesdb_drop_column_family(db_, name.c_str()); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to drop column family"); + } + } + + /** + * @brief Get a column family handle + */ + ColumnFamily get_column_family(const std::string& name) + { + check_valid(); + + tidesdb_column_family_t* cf = tidesdb_get_column_family(db_, name.c_str()); + if (!cf) + { + throw Exception("Column family not found: " + name, TDB_ERR_NOT_FOUND); + } - /* - * Prev - * goes to the previous key-value pair in the column family. + return ColumnFamily(cf, name); + } + + /** + * @brief List all column families + */ + std::vector list_column_families() + { + check_valid(); + + char** names = nullptr; + int count = 0; + + int result = tidesdb_list_column_families(db_, &names, &count); + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to list column families"); + } + + std::vector cf_names; + for (int i = 0; i < count; ++i) + { + cf_names.emplace_back(names[i]); + free(names[i]); + } + free(names); + + return cf_names; + } + + /** + * @brief Get column family statistics */ - [[nodiscard]] int Prev() const; + ColumnFamilyStats get_column_family_stats(const std::string& name) + { + check_valid(); + + tidesdb_column_family_stat_t* c_stats = nullptr; + int result = tidesdb_get_column_family_stats(db_, name.c_str(), &c_stats); + + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to get column family stats"); + } + + ColumnFamilyStats stats; + stats.name = c_stats->name; + stats.comparator_name = c_stats->comparator_name; + stats.num_sstables = c_stats->num_sstables; + stats.total_sstable_size = c_stats->total_sstable_size; + stats.memtable_size = c_stats->memtable_size; + stats.memtable_entries = c_stats->memtable_entries; + + stats.config.memtable_flush_size = c_stats->config.memtable_flush_size; + stats.config.max_sstables_before_compaction = + c_stats->config.max_sstables_before_compaction; + stats.config.compaction_threads = c_stats->config.compaction_threads; + stats.config.max_level = c_stats->config.max_level; + stats.config.probability = c_stats->config.probability; + stats.config.compressed = c_stats->config.compressed != 0; + stats.config.compress_algo = static_cast(c_stats->config.compress_algo); + stats.config.bloom_filter_fp_rate = c_stats->config.bloom_filter_fp_rate; + stats.config.enable_background_compaction = + c_stats->config.enable_background_compaction != 0; + stats.config.use_sbha = c_stats->config.use_sbha != 0; + stats.config.sync_mode = static_cast(c_stats->config.sync_mode); + stats.config.sync_interval = c_stats->config.sync_interval; + + free(c_stats); + return stats; + } + + /** + * @brief Begin a write transaction + */ + std::unique_ptr begin_transaction() + { + check_valid(); + + tidesdb_txn_t* txn = nullptr; + int result = tidesdb_txn_begin(db_, &txn); - /* - * Get - * gets the current key-value pair in the column family cursor. + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to begin transaction"); + } + + return std::make_unique(txn); + } + + /** + * @brief Begin a read-only transaction */ - [[nodiscard]] int Get(std::vector &key, std::vector &value) const; + std::unique_ptr begin_read_transaction() + { + check_valid(); + + tidesdb_txn_t* txn = nullptr; + int result = tidesdb_txn_begin_read(db_, &txn); + + if (result != TDB_SUCCESS) + { + throw Exception::from_code(result, "failed to begin read transaction"); + } + + return std::make_unique(txn); + } + + private: + tidesdb_t* db_ = nullptr; + + void check_valid() const + { + if (!db_) + { + throw Exception("Database is closed"); + } + } }; -}; /* namespace TidesDB */ +} // namespace tidesdb + +#endif // TIDESDB_HPP \ No newline at end of file