This is an automated email from the ASF dual-hosted git repository.

jihuayu pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 081053769 feat(CuckooFilter): add cf.exists and cf.mexists command 
(#3467)
081053769 is described below

commit 081053769d7b70200e21d7f434ed02c579b8033a
Author: hurukawa <[email protected]>
AuthorDate: Mon Sep 28 10:39:59 2026 +0800

    feat(CuckooFilter): add cf.exists and cf.mexists command (#3467)
    
    task id: #3359
    
    This pr is This PR was written using codex and GPT-5.5
    
    ---------
    
    Co-authored-by: qiang_liu <[email protected]>
    Co-authored-by: 纪华裕 <[email protected]>
---
 src/commands/cmd_cuckoo_filter.cc                  |  64 +++++-
 src/storage/redis_metadata.h                       |   3 +-
 src/types/cuckoo_filter_sub_filter.cc              |  31 +++
 src/types/cuckoo_filter_sub_filter.h               |   1 +
 src/types/redis_cuckoo_chain.cc                    |  61 ++++-
 src/types/redis_cuckoo_chain.h                     |   9 +
 tests/cppunit/types/cuckoo_filter_test.cc          | 254 ++++++++++++++++++++-
 tests/gocase/unit/type/bloom/cuckoo_filter_test.go |  94 ++++++++
 8 files changed, 511 insertions(+), 6 deletions(-)

diff --git a/src/commands/cmd_cuckoo_filter.cc 
b/src/commands/cmd_cuckoo_filter.cc
index 2002912f9..3026662c1 100644
--- a/src/commands/cmd_cuckoo_filter.cc
+++ b/src/commands/cmd_cuckoo_filter.cc
@@ -131,8 +131,68 @@ class CommandCFAdd : public Commander {
   }
 };
 
-// Register the CF.RESERVE and CF.ADD commands
+class CommandCFExists : public Commander {
+ public:
+  Status Parse(const std::vector<std::string> &args) override {
+    // CF.EXISTS key item
+    if (args.size() != 3) {
+      return {Status::RedisParseErr, errWrongNumOfArguments};
+    }
+    return Commander::Parse(args);
+  }
+
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, 
std::string *output) override {
+    redis::CuckooChain cuckoo_db(srv->storage, conn->GetNamespace());
+    bool exists = false;
+    auto s = cuckoo_db.Exists(ctx, args_[1], args_[2], &exists);
+
+    if (!s.ok()) {
+      return {Status::RedisExecErr, s.ToString()};
+    }
+
+    *output = conn->Bool(exists);
+    return Status::OK();
+  }
+};
+
+class CommandCFMExists : public Commander {
+ public:
+  Status Parse(const std::vector<std::string> &args) override {
+    // CF.MEXISTS key item [item ...]
+    if (args.size() < 3) {
+      return {Status::RedisParseErr, errWrongNumOfArguments};
+    }
+    items_.reserve(args.size() - 2);
+    for (size_t i = 2; i < args.size(); ++i) {
+      items_.emplace_back(args[i]);
+    }
+    return Commander::Parse(args);
+  }
+
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, 
std::string *output) override {
+    redis::CuckooChain cuckoo_db(srv->storage, conn->GetNamespace());
+    std::vector<bool> exists(items_.size(), false);
+    auto s = cuckoo_db.MExists(ctx, args_[1], items_, &exists);
+
+    if (!s.ok()) {
+      return {Status::RedisExecErr, s.ToString()};
+    }
+
+    *output = redis::MultiLen(items_.size());
+    for (bool exist : exists) {
+      *output += conn->Bool(exist);
+    }
+    return Status::OK();
+  }
+
+ private:
+  std::vector<std::string> items_;
+};
+
+// Register the CF.RESERVE, CF.ADD, CF.EXISTS, and CF.MEXISTS commands
 REDIS_REGISTER_COMMANDS(CuckooFilter, 
MakeCmdAttr<CommandCFReserve>("cf.reserve", -3, "write", 1, 1, 1),
-                        MakeCmdAttr<CommandCFAdd>("cf.add", 3, "write", 1, 1, 
1))
+                        MakeCmdAttr<CommandCFAdd>("cf.add", 3, "write", 1, 1, 
1),
+                        MakeCmdAttr<CommandCFExists>("cf.exists", 3, 
"read-only", 1, 1, 1),
+                        MakeCmdAttr<CommandCFMExists>("cf.mexists", -3, 
"read-only", 1, 1, 1))
 
 }  // namespace redis
diff --git a/src/storage/redis_metadata.h b/src/storage/redis_metadata.h
index 3f9a793cb..66e847f44 100644
--- a/src/storage/redis_metadata.h
+++ b/src/storage/redis_metadata.h
@@ -347,7 +347,8 @@ class CuckooChainMetadata : public Metadata {
   /// When a filter is full, a new one is created with capacity = 
base_capacity * expansion^n
   uint16_t expansion;
 
-  /// The capacity of the first filter.
+  /// The capacity of the first filter. Auto-created filters use 
kCFDefaultCapacity; CF.RESERVE stores the requested
+  /// capacity.
   uint64_t base_capacity;
 
   /// Number of fingerprints per bucket
diff --git a/src/types/cuckoo_filter_sub_filter.cc 
b/src/types/cuckoo_filter_sub_filter.cc
index 0a0748cc3..0e631bf2c 100644
--- a/src/types/cuckoo_filter_sub_filter.cc
+++ b/src/types/cuckoo_filter_sub_filter.cc
@@ -45,6 +45,37 @@ rocksdb::Status CuckooSubFilter::TryInsert(uint64_t hash, 
uint8_t fingerprint, b
   return pages_.TryInsertInBucket(filter_index_, num_buckets_, bucket2_idx, 
fingerprint, inserted);
 }
 
+rocksdb::Status CuckooSubFilter::Contains(uint64_t hash, uint8_t fingerprint, 
bool *exists) {
+  *exists = false;
+  uint32_t bucket1_idx = getPrimaryBucketIndex(hash);
+  uint32_t bucket2_idx = getSecondaryBucketIndex(hash, fingerprint);
+  auto s = pages_.PrefetchBuckets(filter_index_, num_buckets_, bucket1_idx, 
bucket2_idx);
+  if (!s.ok()) return s;
+
+  uint8_t slot = 0;
+  for (size_t i = 0; i < bucket_size_; ++i) {
+    s = pages_.GetBucketSlot(filter_index_, num_buckets_, bucket1_idx, 
static_cast<uint32_t>(i), &slot);
+    if (!s.ok()) return s;
+    if (slot == fingerprint) {
+      *exists = true;
+      return rocksdb::Status::OK();
+    }
+  }
+
+  if (bucket1_idx == bucket2_idx) return rocksdb::Status::OK();
+
+  for (size_t i = 0; i < bucket_size_; ++i) {
+    s = pages_.GetBucketSlot(filter_index_, num_buckets_, bucket2_idx, 
static_cast<uint32_t>(i), &slot);
+    if (!s.ok()) return s;
+    if (slot == fingerprint) {
+      *exists = true;
+      return rocksdb::Status::OK();
+    }
+  }
+
+  return rocksdb::Status::OK();
+}
+
 rocksdb::Status CuckooSubFilter::TryKickOutInsert(uint64_t hash, uint8_t 
fingerprint, uint16_t max_iterations,
                                                   bool *inserted) {
   *inserted = false;
diff --git a/src/types/cuckoo_filter_sub_filter.h 
b/src/types/cuckoo_filter_sub_filter.h
index 2bd26df85..d3ca1f170 100644
--- a/src/types/cuckoo_filter_sub_filter.h
+++ b/src/types/cuckoo_filter_sub_filter.h
@@ -39,6 +39,7 @@ class CuckooSubFilter {
   uint32_t NumBuckets() const { return num_buckets_; }
 
   rocksdb::Status TryInsert(uint64_t hash, uint8_t fingerprint, bool 
*inserted);
+  rocksdb::Status Contains(uint64_t hash, uint8_t fingerprint, bool *exists);
   // Performs speculative kick-out mutations in the page cache. On success, 
dirty pages remain staged for
   // WriteToBatch(); on inserted=false or non-OK status, cached pages are 
discarded before returning.
   rocksdb::Status TryKickOutInsert(uint64_t hash, uint8_t fingerprint, 
uint16_t max_iterations, bool *inserted);
diff --git a/src/types/redis_cuckoo_chain.cc b/src/types/redis_cuckoo_chain.cc
index f7074ce94..ef74d17b0 100644
--- a/src/types/redis_cuckoo_chain.cc
+++ b/src/types/redis_cuckoo_chain.cc
@@ -60,7 +60,7 @@ rocksdb::Status CuckooChain::Reserve(engine::Context &ctx, 
const Slice &user_key
     return rocksdb::Status::InvalidArgument("capacity must be larger than 0");
   }
 
-  // RedisBloom requires minimum capacity to ensure at least one bucket can be 
created
+  // Require minimum capacity to ensure at least one bucket can be created
   // With load factor 0.955, capacity=1 and bucket_size=4 results in 0 buckets
   if (capacity < 2) {
     return rocksdb::Status::InvalidArgument("capacity must be at least 2");
@@ -291,4 +291,63 @@ rocksdb::Status 
CuckooChain::commitSubFilterAndMetadata(engine::Context &ctx, co
   return storage_->Write(ctx, storage_->DefaultWriteOptions(), 
batch->GetWriteBatch());
 }
 
+rocksdb::Status CuckooChain::Exists(engine::Context &ctx, const Slice 
&user_key, const Slice &item, bool *exists) {
+  std::vector<bool> result;
+  auto s = MExists(ctx, user_key, std::vector<std::string>{item.ToString()}, 
&result);
+  if (!s.ok()) return s;
+  *exists = result[0];
+  return rocksdb::Status::OK();
+}
+
+rocksdb::Status CuckooChain::MExists(engine::Context &ctx, const Slice 
&user_key, const std::vector<std::string> &items,
+                                     std::vector<bool> *exists) {
+  exists->assign(items.size(), false);
+  std::string ns_key = AppendNamespacePrefix(user_key);
+
+  CuckooChainMetadata metadata(false);
+  auto s = getCuckooChainMetadata(ctx, ns_key, &metadata);
+  if (s.IsNotFound()) return rocksdb::Status::OK();
+  if (!s.ok()) return s;
+
+  s = validateMetadata(metadata);
+  if (!s.ok()) return s;
+
+  std::vector<uint64_t> hashes(items.size());
+  std::vector<uint8_t> fingerprints(items.size());
+  for (size_t i = 0; i < items.size(); ++i) {
+    hashes[i] = CuckooFilterHelper::Hash(items[i].data(), items[i].size());
+    fingerprints[i] = CuckooFilterHelper::GenerateFingerprint(hashes[i]);
+    CHECK(fingerprints[i] != 0);
+  }
+
+  size_t found_count = 0;
+  for (int filter_idx = static_cast<int>(metadata.n_filters) - 1; filter_idx 
>= 0; --filter_idx) {
+    auto current_filter_idx = static_cast<uint16_t>(filter_idx);
+    uint32_t num_buckets = 0;
+    s = CuckooFilterHelper::GetFilterNumBuckets(metadata.base_capacity, 
metadata.expansion, metadata.bucket_size,
+                                                current_filter_idx, 
&num_buckets);
+    if (!s.ok()) return s;
+
+    CuckooSubFilter sub_filter(storage_, ctx, ns_key, 
storage_->IsSlotIdEncoded(), metadata.version,
+                               metadata.bucket_size, metadata.page_size, 
current_filter_idx, num_buckets);
+    for (size_t i = 0; i < items.size(); ++i) {
+      if ((*exists)[i]) continue;
+
+      bool item_exists = false;
+      s = sub_filter.Contains(hashes[i], fingerprints[i], &item_exists);
+      if (!s.ok()) return s;
+      if (item_exists) {
+        (*exists)[i] = true;
+        ++found_count;
+      }
+    }
+
+    if (found_count == items.size()) {
+      break;
+    }
+  }
+
+  return rocksdb::Status::OK();
+}
+
 }  // namespace redis
diff --git a/src/types/redis_cuckoo_chain.h b/src/types/redis_cuckoo_chain.h
index ae20b056c..d3a4381e5 100644
--- a/src/types/redis_cuckoo_chain.h
+++ b/src/types/redis_cuckoo_chain.h
@@ -20,6 +20,8 @@
 
 #pragma once
 
+#include <vector>
+
 #include "cuckoo_filter.h"
 #include "storage/redis_db.h"
 #include "storage/redis_metadata.h"
@@ -47,6 +49,13 @@ class CuckooChain : public Database {
   // Duplicate items are allowed, so added is true whenever insertion succeeds.
   rocksdb::Status Add(engine::Context &ctx, const Slice &user_key, const Slice 
&item, bool *added);
 
+  // Returns true if the item might exist, and false if it definitely does not.
+  rocksdb::Status Exists(engine::Context &ctx, const Slice &user_key, const 
Slice &item, bool *exists);
+
+  // Returns whether each item might exist in the cuckoo filter.
+  rocksdb::Status MExists(engine::Context &ctx, const Slice &user_key, const 
std::vector<std::string> &items,
+                          std::vector<bool> *exists);
+
  private:
   // Loads metadata for a cuckoo filter key.
   rocksdb::Status getCuckooChainMetadata(engine::Context &ctx, const Slice 
&ns_key, CuckooChainMetadata *metadata);
diff --git a/tests/cppunit/types/cuckoo_filter_test.cc 
b/tests/cppunit/types/cuckoo_filter_test.cc
index 66a24ad42..8f92263b3 100644
--- a/tests/cppunit/types/cuckoo_filter_test.cc
+++ b/tests/cppunit/types/cuckoo_filter_test.cc
@@ -23,6 +23,7 @@
 #include <algorithm>
 #include <limits>
 #include <memory>
+#include <string>
 #include <vector>
 
 #include "common/encoding.h"
@@ -289,7 +290,7 @@ TEST_F(RedisCuckooFilterTest, 
CalculateRequiredBucketsCalculation) {
 
 TEST_F(RedisCuckooFilterTest, FingerprintGeneration) {
   // Test fingerprint generation ensures non-zero values in range [1, 255]
-  // Following RedisBloom: fp = hash % 255 + 1
+  // Standard implementation: fp = hash % 255 + 1
   for (uint64_t hash = 0; hash < 1000; ++hash) {
     uint8_t fp = redis::CuckooFilterHelper::GenerateFingerprint(hash);
     ASSERT_GE(fp, 1) << "Fingerprint should be at least 1";
@@ -306,7 +307,7 @@ TEST_F(RedisCuckooFilterTest, FingerprintGeneration) {
 TEST_F(RedisCuckooFilterTest, AlternateBucketCalculation) {
   std::vector<uint32_t> num_buckets_cases = {1, 2, 128, 256, 512, 1024, 2048};
 
-  // Test GetAltHash symmetry at hash level (following RedisBloom design)
+  // Test GetAltHash symmetry property
   // h2 = GetAltHash(fp, h1)
   // h1 = GetAltHash(fp, h2)  <- this is the symmetry property
   for (auto num_buckets : num_buckets_cases) {
@@ -743,3 +744,252 @@ TEST_F(RedisCuckooFilterTest, 
ExpansionWritesNewFilterIndexPage) {
   ASSERT_TRUE(s.ok()) << s.ToString();
   EXPECT_EQ(page.size(), expected_page_size);
 }
+
+TEST_F(RedisCuckooFilterTest, ExistsBasic) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+  addAndVerify(key_, "test_item", 1000, 4, 500, 2, 1);
+
+  bool exists = false;
+  auto s = cuckoo_->Exists(*ctx_, key_, "test_item", &exists);
+  ASSERT_TRUE(s.ok()) << "Failed to check existence: " << s.ToString();
+  ASSERT_TRUE(exists) << "Item should exist";
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsNonExistentItem) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+  addAndVerify(key_, "item1", 1000, 4, 500, 2, 1);
+
+  bool exists = false;
+  auto s = cuckoo_->Exists(*ctx_, key_, "item2", &exists);
+  ASSERT_TRUE(s.ok());
+  ASSERT_FALSE(exists) << "Item should not exist";
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsNonExistentFilter) {
+  bool exists = true;
+  auto s = cuckoo_->Exists(*ctx_, "nonexistent_key", "item", &exists);
+  ASSERT_TRUE(s.ok()) << s.ToString();
+  ASSERT_FALSE(exists);
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsMultipleItems) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+
+  std::vector<std::string> items = {"apple", "banana", "cherry", "date", 
"elderberry"};
+  for (size_t i = 0; i < items.size(); ++i) {
+    addAndVerify(key_, items[i], 1000, 4, 500, 2, i + 1);
+  }
+
+  for (const auto &item : items) {
+    bool exists = false;
+    auto s = cuckoo_->Exists(*ctx_, key_, item, &exists);
+    ASSERT_TRUE(s.ok()) << "Failed to check: " << item;
+    ASSERT_TRUE(exists) << "Item should exist: " << item;
+  }
+
+  std::vector<std::string> non_items = {"fig", "grape", "honeydew"};
+  for (const auto &item : non_items) {
+    bool exists = false;
+    auto s = cuckoo_->Exists(*ctx_, key_, item, &exists);
+    ASSERT_TRUE(s.ok());
+    ASSERT_FALSE(exists) << "Item should not exist: " << item;
+  }
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsDuplicateItems) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+
+  std::string item = "duplicate_item";
+  for (int i = 0; i < 3; ++i) {
+    addAndVerify(key_, item, 1000, 4, 500, 2, i + 1);
+  }
+
+  bool exists = false;
+  auto s = cuckoo_->Exists(*ctx_, key_, item, &exists);
+  ASSERT_TRUE(s.ok());
+  ASSERT_TRUE(exists) << "Duplicate item should exist";
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsEmptyString) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+  addAndVerify(key_, "", 1000, 4, 500, 2, 1);
+
+  bool exists = false;
+  auto s = cuckoo_->Exists(*ctx_, key_, "", &exists);
+  ASSERT_TRUE(s.ok());
+  ASSERT_TRUE(exists) << "Empty string should exist";
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsBinaryData) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+  std::string binary_item = std::string("\x00\x01\x02\xFF\xFE", 5);
+  addAndVerify(key_, binary_item, 1000, 4, 500, 2, 1);
+
+  bool exists = false;
+  auto s = cuckoo_->Exists(*ctx_, key_, binary_item, &exists);
+  ASSERT_TRUE(s.ok());
+  ASSERT_TRUE(exists) << "Binary data should exist";
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsAfterExpansion) {
+  reserveAndVerify(key_, 10, 2, 500, 2);
+
+  std::vector<std::string> items;
+  items.reserve(30);
+  for (int i = 0; i < 30; ++i) {
+    items.push_back("item_" + std::to_string(i));
+  }
+
+  for (const auto &item : items) {
+    bool added = false;
+    auto s = cuckoo_->Add(*ctx_, key_, item, &added);
+    if (s.ok() && added) {
+      bool exists = false;
+      s = cuckoo_->Exists(*ctx_, key_, item, &exists);
+      ASSERT_TRUE(s.ok());
+      ASSERT_TRUE(exists) << "Item should exist: " << item;
+    }
+  }
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsWithDifferentBucketSizes) {
+  std::vector<uint8_t> bucket_sizes = {1, 2, 4, 8};
+
+  for (size_t i = 0; i < bucket_sizes.size(); ++i) {
+    std::string test_key = key_ + "_bucket_" + std::to_string(bucket_sizes[i]);
+    reserveAndVerify(test_key, 100, bucket_sizes[i], 500, 2);
+
+    std::vector<std::string> items = {"item1", "item2", "item3"};
+    for (size_t j = 0; j < items.size(); ++j) {
+      addAndVerify(test_key, items[j], 100, bucket_sizes[i], 500, 2, j + 1);
+    }
+
+    for (const auto &item : items) {
+      bool exists = false;
+      auto s = cuckoo_->Exists(*ctx_, test_key, item, &exists);
+      ASSERT_TRUE(s.ok()) << "bucket_size=" << 
static_cast<int>(bucket_sizes[i]);
+      ASSERT_TRUE(exists) << "Item should exist with bucket_size=" << 
static_cast<int>(bucket_sizes[i]);
+    }
+  }
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsLongString) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+  std::string long_item(10000, 'x');
+  addAndVerify(key_, long_item, 1000, 4, 500, 2, 1);
+
+  bool exists = false;
+  auto s = cuckoo_->Exists(*ctx_, key_, long_item, &exists);
+  ASSERT_TRUE(s.ok());
+  ASSERT_TRUE(exists) << "Long string should exist";
+
+  std::string different_long_item(10000, 'y');
+  exists = false;
+  s = cuckoo_->Exists(*ctx_, key_, different_long_item, &exists);
+  ASSERT_TRUE(s.ok());
+  ASSERT_FALSE(exists) << "Different long string should not exist";
+}
+
+TEST_F(RedisCuckooFilterTest, MExistsBasic) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+  addAndVerify(key_, "alpha", 1000, 4, 500, 2, 1);
+  addAndVerify(key_, "gamma", 1000, 4, 500, 2, 2);
+
+  std::vector<bool> exists;
+  auto s = cuckoo_->MExists(*ctx_, key_, {"alpha", "beta", "gamma"}, &exists);
+  ASSERT_TRUE(s.ok()) << s.ToString();
+  ASSERT_EQ(exists.size(), 3);
+  EXPECT_TRUE(exists[0]);
+  EXPECT_FALSE(exists[1]);
+  EXPECT_TRUE(exists[2]);
+}
+
+TEST_F(RedisCuckooFilterTest, MExistsNonExistentFilter) {
+  std::vector<bool> exists;
+  auto s = cuckoo_->MExists(*ctx_, "nonexistent_key", {"alpha", "beta"}, 
&exists);
+  ASSERT_TRUE(s.ok()) << s.ToString();
+  ASSERT_EQ(exists.size(), 2);
+  EXPECT_FALSE(exists[0]);
+  EXPECT_FALSE(exists[1]);
+}
+
+TEST_F(RedisCuckooFilterTest, MExistsAfterExpansion) {
+  reserveAndVerify(key_, 4, 1, 1, 2);
+
+  std::vector<std::string> inserted_items;
+  for (int i = 0; i < 20; ++i) {
+    std::string item = "expand_item_" + std::to_string(i);
+    bool added = false;
+    auto s = cuckoo_->Add(*ctx_, key_, item, &added);
+    ASSERT_TRUE(s.ok()) << s.ToString();
+    ASSERT_TRUE(added);
+    inserted_items.emplace_back(std::move(item));
+  }
+
+  auto metadata = getMetadata(key_);
+  ASSERT_GT(metadata.n_filters, 1);
+  ASSERT_EQ(metadata.size, 20);
+
+  std::vector<bool> exists;
+  auto s = cuckoo_->MExists(*ctx_, key_, {inserted_items.front(), 
"definitely_absent", inserted_items.back()}, &exists);
+  ASSERT_TRUE(s.ok()) << s.ToString();
+  ASSERT_EQ(exists.size(), 3);
+  EXPECT_TRUE(exists[0]);
+  EXPECT_FALSE(exists[1]);
+  EXPECT_TRUE(exists[2]);
+}
+
+TEST_F(RedisCuckooFilterTest, MExistsStopsAfterAllItemsFoundInNewestFilter) {
+  CuckooChainMetadata metadata;
+  metadata.size = 0;
+  metadata.base_capacity = 1000;
+  metadata.bucket_size = 4;
+  metadata.max_iterations = 500;
+  metadata.expansion = 2;
+  metadata.n_filters = 2;
+  metadata.num_deleted_items = 0;
+  metadata.page_size = kCuckooFilterDefaultPageSize;
+  writeMetadata(key_, metadata);
+
+  writePage(makePageKey(key_, metadata, 0, 0), "corrupt");
+
+  addAndVerify(key_, "alpha", metadata.base_capacity, metadata.bucket_size, 
metadata.max_iterations, metadata.expansion,
+               1, metadata.n_filters);
+  addAndVerify(key_, "beta", metadata.base_capacity, metadata.bucket_size, 
metadata.max_iterations, metadata.expansion,
+               2, metadata.n_filters);
+
+  std::vector<bool> exists;
+  auto s = cuckoo_->MExists(*ctx_, key_, {"alpha", "beta"}, &exists);
+  ASSERT_TRUE(s.ok()) << s.ToString();
+  ASSERT_EQ(exists.size(), 2);
+  EXPECT_TRUE(exists[0]);
+  EXPECT_TRUE(exists[1]);
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsBatchTest) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+
+  for (int x = 0; x < 100; ++x) {
+    addAndVerify(key_, std::to_string(x), 1000, 4, 500, 2, x + 1);
+  }
+
+  for (int x = 0; x < 100; ++x) {
+    bool exists = false;
+    auto s = cuckoo_->Exists(*ctx_, key_, std::to_string(x), &exists);
+    ASSERT_TRUE(s.ok()) << "Failed to check item " << x;
+    ASSERT_TRUE(exists) << "Item " << x << " should exist";
+  }
+}
+
+TEST_F(RedisCuckooFilterTest, ExistsWithRepeatedInsertions) {
+  reserveAndVerify(key_, 1000, 4, 500, 2);
+
+  std::string item = "k1";
+  addAndVerify(key_, item, 1000, 4, 500, 2, 1);
+  addAndVerify(key_, item, 1000, 4, 500, 2, 2);
+
+  bool exists = false;
+  auto s = cuckoo_->Exists(*ctx_, key_, item, &exists);
+  ASSERT_TRUE(s.ok());
+  ASSERT_TRUE(exists) << "Item should exist after duplicate insertions";
+}
diff --git a/tests/gocase/unit/type/bloom/cuckoo_filter_test.go 
b/tests/gocase/unit/type/bloom/cuckoo_filter_test.go
index 852dde75e..89048dd80 100644
--- a/tests/gocase/unit/type/bloom/cuckoo_filter_test.go
+++ b/tests/gocase/unit/type/bloom/cuckoo_filter_test.go
@@ -49,6 +49,48 @@ func TestCuckooFilter(t *testing.T) {
                require.ErrorContains(t, rdb.Do(ctx, "cf.add", key, 
"item").Err(), "WRONGTYPE")
        })
 
+       t.Run("CF.EXISTS missing key returns false", func(t *testing.T) {
+               key := "test_cuckoo_filter_exists_missing_key"
+               require.NoError(t, rdb.Del(ctx, key).Err())
+               require.Equal(t, false, rdb.Do(ctx, "cf.exists", key, 
"item").Val())
+       })
+
+       t.Run("CF.EXISTS existing and absent item", func(t *testing.T) {
+               key := "test_cuckoo_filter_exists_items"
+               require.NoError(t, rdb.Del(ctx, key).Err())
+               require.Equal(t, int64(1), rdb.Do(ctx, "cf.add", key, 
"present").Val())
+               require.Equal(t, true, rdb.Do(ctx, "cf.exists", key, 
"present").Val())
+               require.Equal(t, false, rdb.Do(ctx, "cf.exists", key, 
"absent").Val())
+       })
+
+       t.Run("CF.MEXISTS returns ordered booleans", func(t *testing.T) {
+               key := "test_cuckoo_filter_mexists_ordered"
+               require.NoError(t, rdb.Del(ctx, key).Err())
+               require.Equal(t, int64(1), rdb.Do(ctx, "cf.add", key, 
"alpha").Val())
+               require.Equal(t, int64(1), rdb.Do(ctx, "cf.add", key, 
"gamma").Val())
+               require.Equal(t, []interface{}{true, false, true}, rdb.Do(ctx, 
"cf.mexists", key, "alpha", "beta", "gamma").Val())
+       })
+
+       t.Run("CF.EXISTS wrong type returns WRONGTYPE", func(t *testing.T) {
+               key := "test_cuckoo_filter_exists_wrong_type"
+               require.NoError(t, rdb.Set(ctx, key, "value", 0).Err())
+               require.ErrorContains(t, rdb.Do(ctx, "cf.exists", key, 
"item").Err(), "WRONGTYPE")
+       })
+
+       t.Run("CF.MEXISTS wrong type returns WRONGTYPE", func(t *testing.T) {
+               key := "test_cuckoo_filter_mexists_wrong_type"
+               require.NoError(t, rdb.Set(ctx, key, "value", 0).Err())
+               require.ErrorContains(t, rdb.Do(ctx, "cf.mexists", key, 
"item1", "item2").Err(), "WRONGTYPE")
+       })
+
+       t.Run("CF.EXISTS and CF.MEXISTS wrong number of arguments", func(t 
*testing.T) {
+               require.Error(t, rdb.Do(ctx, "cf.exists").Err())
+               require.Error(t, rdb.Do(ctx, "cf.exists", "key_only").Err())
+               require.Error(t, rdb.Do(ctx, "cf.exists", "key", "item1", 
"item2").Err())
+               require.Error(t, rdb.Do(ctx, "cf.mexists").Err())
+               require.Error(t, rdb.Do(ctx, "cf.mexists", "key_only").Err())
+       })
+
        t.Run("Reserve expansion", func(t *testing.T) {
                require.NoError(t, rdb.Do(ctx, "cf.reserve", 
"test_cuckoo_filter_expansion_256", "1000", "EXPANSION", "256").Err())
                require.NoError(t, rdb.Do(ctx, "cf.reserve", 
"test_cuckoo_filter_expansion_max", "1000", "EXPANSION", "32768").Err())
@@ -142,6 +184,20 @@ func TestCuckooFilter(t *testing.T) {
                }
        })
 
+       t.Run("CF.EXISTS and CF.MEXISTS after expansion", func(t *testing.T) {
+               key := "test_cuckoo_filter_exists_after_expansion"
+               require.NoError(t, rdb.Del(ctx, key).Err())
+               require.NoError(t, rdb.Do(ctx, "cf.reserve", key, "4", 
"BUCKETSIZE", "1", "MAXITERATIONS", "1", "EXPANSION", "2").Err())
+               for i := 0; i < 20; i++ {
+                       result := rdb.Do(ctx, "cf.add", key, 
fmt.Sprintf("expand_item_%d", i))
+                       require.NoError(t, result.Err())
+                       require.Equal(t, int64(1), result.Val())
+               }
+               require.Equal(t, true, rdb.Do(ctx, "cf.exists", key, 
"expand_item_0").Val())
+               require.Equal(t, true, rdb.Do(ctx, "cf.exists", key, 
"expand_item_19").Val())
+               require.Equal(t, []interface{}{true, false, true}, rdb.Do(ctx, 
"cf.mexists", key, "expand_item_0", "not_inserted", "expand_item_19").Val())
+       })
+
        t.Run("Add to full non-scaling filter returns error", func(t 
*testing.T) {
                key := "test_cuckoo_filter_full_nonscaling"
                require.NoError(t, rdb.Del(ctx, key).Err())
@@ -188,3 +244,41 @@ func TestCuckooFilter(t *testing.T) {
                require.Equal(t, int64(1), result.Val())
        })
 }
+
+func TestCuckooFilterRESP3(t *testing.T) {
+       srv := util.StartServer(t, map[string]string{
+               "resp3-enabled": "yes",
+       })
+       defer srv.Close()
+
+       c := srv.NewTCPClient()
+       defer func() { require.NoError(t, c.Close()) }()
+
+       require.NoError(t, c.WriteArgs("HELLO", "3"))
+       for _, line := range []string{
+               "%6",
+               "$6", "server", "$5", "redis",
+               "$7", "version", "$5", "7.0.0",
+               "$5", "proto", ":3",
+               "$4", "mode", "$10", "standalone",
+               "$4", "role", "$6", "master",
+               "$7", "modules", "_",
+       } {
+               c.MustRead(t, line)
+       }
+
+       key := "test_cuckoo_filter_resp3"
+       require.NoError(t, c.WriteArgs("cf.add", key, "alpha"))
+       c.MustRead(t, ":1")
+
+       require.NoError(t, c.WriteArgs("cf.exists", key, "alpha"))
+       c.MustRead(t, "#t")
+
+       require.NoError(t, c.WriteArgs("cf.exists", key, "beta"))
+       c.MustRead(t, "#f")
+
+       require.NoError(t, c.WriteArgs("cf.mexists", key, "alpha", "beta"))
+       c.MustRead(t, "*2")
+       c.MustRead(t, "#t")
+       c.MustRead(t, "#f")
+}

Reply via email to