diff --git a/CMakeLists.txt b/CMakeLists.txt index 851d936..2b5f0fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(tidesdb_cpp VERSION 2.3.7 LANGUAGES CXX) +project(tidesdb_cpp VERSION 2.4.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/README.md b/README.md index 560984a..bb092c2 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,3 @@ Multiple licenses apply: ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. - -## Support - -- [Discord](https://discord.gg/tWEmjR66cy) -- [GitHub Issues](https://github.com/tidesdb/tidesdb-cpp/issues) diff --git a/include/tidesdb/tidesdb.hpp b/include/tidesdb/tidesdb.hpp index 264351b..35b3adf 100644 --- a/include/tidesdb/tidesdb.hpp +++ b/include/tidesdb/tidesdb.hpp @@ -194,11 +194,10 @@ struct ColumnFamilyConfig 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) - std::size_t objectTargetFileSize = 0; ///< Target SSTable size in object store mode (0=auto) - int objectLazyCompaction = 0; ///< 1 = compact less aggressively in object store mode - int objectPrefetchCompaction = 1; ///< 1 = download all inputs before merge + nullptr; ///< Optional commit hook callback (runtime-only) + void* commitHookCtx = nullptr; ///< Optional user context for commit hook (runtime-only) + int objectLazyCompaction = 0; ///< 1 = compact less aggressively in object store mode + int objectPrefetchCompaction = 1; ///< 1 = download all inputs before merge /** * @brief Get default column family configuration from TidesDB @@ -229,22 +228,25 @@ struct ColumnFamilyConfig */ struct ObjectStoreConfig { - std::string localCachePath; ///< Local directory for cached SSTable files (empty = use db_path) - std::size_t localCacheMaxBytes = 0; ///< Max local cache size in bytes (0 = unlimited) - bool cacheOnRead = true; ///< Cache downloaded files locally - bool cacheOnWrite = true; ///< Keep local copy after upload - int maxConcurrentUploads = 4; ///< Parallel upload threads - int maxConcurrentDownloads = 8; ///< Parallel download threads - std::size_t multipartThreshold = 64 * 1024 * 1024; ///< Use multipart upload above this size (64MB) - std::size_t multipartPartSize = 8 * 1024 * 1024; ///< Chunk size for multipart uploads (8MB) - bool syncManifestToObject = true; ///< Upload MANIFEST after each compaction - bool replicateWal = true; ///< Upload closed WAL segments for replication - bool walUploadSync = false; ///< false = background WAL upload, true = block flush until uploaded - std::size_t walSyncThresholdBytes = 1048576; ///< Sync active WAL when it grows by this many bytes (0 = off) - bool walSyncOnCommit = false; ///< Upload WAL after every txn commit for RPO=0 - bool replicaMode = false; ///< Enable read-only replica mode - std::uint64_t replicaSyncIntervalUs = 5000000; ///< MANIFEST poll interval in microseconds (5s) - bool replicaReplayWal = true; ///< Replay WAL from object store for near-real-time reads + std::string localCachePath; ///< Local directory for cached SSTable files (empty = use db_path) + std::size_t localCacheMaxBytes = 0; ///< Max local cache size in bytes (0 = unlimited) + bool cacheOnRead = true; ///< Cache downloaded files locally + bool cacheOnWrite = true; ///< Keep local copy after upload + int maxConcurrentUploads = 4; ///< Parallel upload threads + int maxConcurrentDownloads = 8; ///< Parallel download threads + std::size_t multipartThreshold = + 64 * 1024 * 1024; ///< Use multipart upload above this size (64MB) + std::size_t multipartPartSize = 8 * 1024 * 1024; ///< Chunk size for multipart uploads (8MB) + bool syncManifestToObject = true; ///< Upload MANIFEST after each compaction + bool replicateWal = true; ///< Upload closed WAL segments for replication + bool walUploadSync = + false; ///< false = background WAL upload, true = block flush until uploaded + std::size_t walSyncThresholdBytes = + 1048576; ///< Sync active WAL when it grows by this many bytes (0 = off) + bool walSyncOnCommit = false; ///< Upload WAL after every txn commit for RPO=0 + bool replicaMode = false; ///< Enable read-only replica mode + std::uint64_t replicaSyncIntervalUs = 5000000; ///< MANIFEST poll interval in microseconds (5s) + bool replicaReplayWal = true; ///< Replay WAL from object store for near-real-time reads /** * @brief Get default object store configuration from TidesDB @@ -273,8 +275,10 @@ struct Config float unifiedMemtableSkipListProbability = 0; ///< Skip list probability (0 = default 0.25) SyncMode unifiedMemtableSyncMode = SyncMode::None; ///< Sync mode for unified WAL std::uint64_t unifiedMemtableSyncIntervalUs = 0; ///< Sync interval for unified WAL - tidesdb_objstore_t* objectStore = nullptr; ///< Pluggable object store connector (nullptr = local only) - std::optional objectStoreConfig; ///< Object store behavior config (nullopt = defaults) + tidesdb_objstore_t* objectStore = + nullptr; ///< Pluggable object store connector (nullptr = local only) + std::optional + objectStoreConfig; ///< Object store behavior config (nullopt = defaults) }; /** @@ -582,6 +586,21 @@ class Transaction */ void del(ColumnFamily& cf, const std::vector& key); + /** + * @brief Single-delete a key + * + * Emits a single-delete tombstone. When a single-delete meets exactly one + * prior put for the same key during compaction, both records are dropped. + * Use only when the caller guarantees at most one put precedes the delete; + * otherwise prefer del(). See tidesdb_txn_single_delete (TDB v9.1.0). + */ + void singleDelete(ColumnFamily& cf, std::string_view key); + + /** + * @brief Single-delete a key (byte vector overload) + */ + void singleDelete(ColumnFamily& cf, const std::vector& key); + /** * @brief Commit the transaction */ diff --git a/src/tidesdb.cpp b/src/tidesdb.cpp index 2e96f05..b968bc4 100644 --- a/src/tidesdb.cpp +++ b/src/tidesdb.cpp @@ -69,7 +69,6 @@ ColumnFamilyConfig ColumnFamilyConfig::defaultConfig() config.l1FileCountTrigger = cConfig.l1_file_count_trigger; config.l0QueueStallThreshold = cConfig.l0_queue_stall_threshold; config.useBtree = cConfig.use_btree != 0; - config.objectTargetFileSize = cConfig.object_target_file_size; config.objectLazyCompaction = cConfig.object_lazy_compaction; config.objectPrefetchCompaction = cConfig.object_prefetch_compaction; @@ -106,7 +105,6 @@ ColumnFamilyConfig ColumnFamilyConfig::loadFromIni(const std::string& iniFile, config.l1FileCountTrigger = cConfig.l1_file_count_trigger; config.l0QueueStallThreshold = cConfig.l0_queue_stall_threshold; config.useBtree = cConfig.use_btree != 0; - config.objectTargetFileSize = cConfig.object_target_file_size; config.objectLazyCompaction = cConfig.object_lazy_compaction; config.objectPrefetchCompaction = cConfig.object_prefetch_compaction; @@ -139,7 +137,7 @@ void ColumnFamilyConfig::saveToIni(const std::string& iniFile, const std::string cConfig.l1_file_count_trigger = config.l1FileCountTrigger; cConfig.l0_queue_stall_threshold = config.l0QueueStallThreshold; cConfig.use_btree = config.useBtree ? 1 : 0; - cConfig.object_target_file_size = config.objectTargetFileSize; + cConfig.object_target_file_size = 0; /* retired, reserved in C for ABI compatibility */ cConfig.object_lazy_compaction = config.objectLazyCompaction; cConfig.object_prefetch_compaction = config.objectPrefetchCompaction; @@ -255,7 +253,6 @@ Stats ColumnFamily::getStats() const cfConfig.l1FileCountTrigger = cStats->config->l1_file_count_trigger; cfConfig.l0QueueStallThreshold = cStats->config->l0_queue_stall_threshold; cfConfig.useBtree = cStats->config->use_btree != 0; - cfConfig.objectTargetFileSize = cStats->config->object_target_file_size; cfConfig.objectLazyCompaction = cStats->config->object_lazy_compaction; cfConfig.objectPrefetchCompaction = cStats->config->object_prefetch_compaction; stats.config = cfConfig; @@ -355,7 +352,7 @@ void ColumnFamily::updateRuntimeConfig(const ColumnFamilyConfig& config, bool pe cConfig.l1_file_count_trigger = config.l1FileCountTrigger; cConfig.l0_queue_stall_threshold = config.l0QueueStallThreshold; cConfig.use_btree = config.useBtree ? 1 : 0; - cConfig.object_target_file_size = config.objectTargetFileSize; + cConfig.object_target_file_size = 0; /* retired, reserved in C for ABI compatibility */ cConfig.object_lazy_compaction = config.objectLazyCompaction; cConfig.object_prefetch_compaction = config.objectPrefetchCompaction; @@ -584,6 +581,19 @@ void Transaction::del(ColumnFamily& cf, const std::vector& key) checkResult(result, "failed to delete key"); } +void Transaction::singleDelete(ColumnFamily& cf, std::string_view key) +{ + int result = tidesdb_txn_single_delete( + txn_, cf.handle(), reinterpret_cast(key.data()), key.size()); + checkResult(result, "failed to single-delete key"); +} + +void Transaction::singleDelete(ColumnFamily& cf, const std::vector& key) +{ + int result = tidesdb_txn_single_delete(txn_, cf.handle(), key.data(), key.size()); + checkResult(result, "failed to single-delete key"); +} + void Transaction::commit() { int result = tidesdb_txn_commit(txn_); @@ -656,8 +666,7 @@ TidesDB::TidesDB(const Config& config) if (config.objectStoreConfig.has_value()) { const auto& os = config.objectStoreConfig.value(); - osCfg.local_cache_path = - os.localCachePath.empty() ? nullptr : os.localCachePath.c_str(); + osCfg.local_cache_path = os.localCachePath.empty() ? nullptr : os.localCachePath.c_str(); osCfg.local_cache_max_bytes = os.localCacheMaxBytes; osCfg.cache_on_read = os.cacheOnRead ? 1 : 0; osCfg.cache_on_write = os.cacheOnWrite ? 1 : 0; @@ -737,7 +746,7 @@ void TidesDB::createColumnFamily(const std::string& name, const ColumnFamilyConf cConfig.l1_file_count_trigger = config.l1FileCountTrigger; cConfig.l0_queue_stall_threshold = config.l0QueueStallThreshold; cConfig.use_btree = config.useBtree ? 1 : 0; - cConfig.object_target_file_size = config.objectTargetFileSize; + cConfig.object_target_file_size = 0; /* retired, reserved in C for ABI compatibility */ cConfig.object_lazy_compaction = config.objectLazyCompaction; cConfig.object_prefetch_compaction = config.objectPrefetchCompaction; diff --git a/tests/tidesdb_test.cpp b/tests/tidesdb_test.cpp index bdc9d31..b2476ad 100644 --- a/tests/tidesdb_test.cpp +++ b/tests/tidesdb_test.cpp @@ -139,6 +139,54 @@ TEST_F(TidesDBTest, TransactionPutGetDelete) } } +TEST_F(TidesDBTest, TransactionSingleDelete) +{ + tidesdb::TidesDB db(getConfig()); + + auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); + db.createColumnFamily("test_cf", cfConfig); + + auto cf = db.getColumnFamily("test_cf"); + + { + auto txn = db.beginTransaction(); + txn.put(cf, "sd_key", "sd_value", -1); + txn.commit(); + } + + { + auto txn = db.beginTransaction(); + txn.singleDelete(cf, "sd_key"); + txn.commit(); + } + + { + auto txn = db.beginTransaction(); + EXPECT_THROW(txn.get(cf, "sd_key"), tidesdb::Exception); + } + + // byte vector overload + std::vector key{'b', 'y', 't', 'e', 'k', 'e', 'y'}; + std::vector value{'v', 'a', 'l'}; + + { + auto txn = db.beginTransaction(); + txn.put(cf, key, value, -1); + txn.commit(); + } + + { + auto txn = db.beginTransaction(); + txn.singleDelete(cf, key); + txn.commit(); + } + + { + auto txn = db.beginTransaction(); + EXPECT_THROW(txn.get(cf, key), tidesdb::Exception); + } +} + TEST_F(TidesDBTest, TransactionWithTTL) { tidesdb::TidesDB db(getConfig()); @@ -1630,8 +1678,6 @@ TEST_F(TidesDBTest, ColumnFamilyConfigObjectStoreFields) { auto cfConfig = tidesdb::ColumnFamilyConfig::defaultConfig(); - // Verify object store fields are populated from C defaults - ASSERT_GT(cfConfig.objectTargetFileSize, 0u); // default 256MB ASSERT_GE(cfConfig.objectLazyCompaction, 0); ASSERT_GE(cfConfig.objectPrefetchCompaction, 0); }