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

twice 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 5940b730 Support BIT and BYTE options in the BITCOUNT command (#2087)
5940b730 is described below

commit 5940b730ba450cf83348766da2b7214f9a5ce1a9
Author: kay011 <[email protected]>
AuthorDate: Wed Feb 7 18:25:52 2024 +0800

    Support BIT and BYTE options in the BITCOUNT command (#2087)
    
    Co-authored-by: mwish <[email protected]>
    Co-authored-by: hulk <[email protected]>
    Co-authored-by: 纪华裕 <[email protected]>
---
 src/commands/cmd_bit.cc                      |   6 +-
 src/types/redis_bitmap.cc                    | 105 +++++++++++++++++----------
 src/types/redis_bitmap.h                     |   2 +-
 src/types/redis_bitmap_string.cc             |  44 +++++++++--
 src/types/redis_bitmap_string.h              |  18 ++++-
 tests/cppunit/types/bitmap_test.cc           |  61 +++++++++++++---
 tests/gocase/unit/type/bitmap/bitmap_test.go |  54 ++++++++++++++
 7 files changed, 230 insertions(+), 60 deletions(-)

diff --git a/src/commands/cmd_bit.cc b/src/commands/cmd_bit.cc
index 65e90d04..088e0add 100644
--- a/src/commands/cmd_bit.cc
+++ b/src/commands/cmd_bit.cc
@@ -120,8 +120,9 @@ class CommandBitCount : public Commander {
 
     if (args.size() == 5) {
       if (util::EqualICase(args[4], "BYTE")) {
+        is_bit_index_ = false;
       } else if (util::EqualICase(args[4], "BIT")) {
-        return {Status::RedisExecErr, errNotImplemented};
+        is_bit_index_ = true;
       } else {
         return {Status::RedisParseErr, errInvalidSyntax};
       }
@@ -133,7 +134,7 @@ class CommandBitCount : public Commander {
   Status Execute(Server *srv, Connection *conn, std::string *output) override {
     uint32_t cnt = 0;
     redis::Bitmap bitmap_db(srv->storage, conn->GetNamespace());
-    auto s = bitmap_db.BitCount(args_[1], start_, stop_, &cnt);
+    auto s = bitmap_db.BitCount(args_[1], start_, stop_, is_bit_index_, &cnt);
     if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
 
     *output = redis::Integer(cnt);
@@ -143,6 +144,7 @@ class CommandBitCount : public Commander {
  private:
   int64_t start_ = 0;
   int64_t stop_ = -1;
+  bool is_bit_index_ = false;
 };
 
 class CommandBitPos : public Commander {
diff --git a/src/types/redis_bitmap.cc b/src/types/redis_bitmap.cc
index 01360829..48b1125b 100644
--- a/src/types/redis_bitmap.cc
+++ b/src/types/redis_bitmap.cc
@@ -37,6 +37,31 @@ const char kErrBitmapStringOutOfRange[] =
     "The size of the bitmap string exceeds the "
     "configuration item max-bitmap-to-string-mb";
 
+/*
+ * If you setbit bit 0 1, the value is stored as 0x01 in Kvrocks but 0x80 in 
Redis.
+ * So we need to swap bits is to keep the same return value as Redis.
+ * This swap table is generated according to the following mapping definition.
+ * kBitSwapTable(x) =  ((x & 0x80) >> 7)| ((x & 0x40) >> 5)|\
+ *                  ((x & 0x20) >> 3)| ((x & 0x10) >> 1)|\
+ *                  ((x & 0x08) << 1)| ((x & 0x04) << 3)|\
+ *                  ((x & 0x02) << 5)| ((x & 0x01) << 7);
+ */
+static const uint8_t kBitSwapTable[256] = {
+    0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 
0x30, 0xB0, 0x70, 0xF0, 0x08, 0x88, 0x48,
+    0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 
0xF8, 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4,
+    0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, 0x0C, 0x8C, 
0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C,
+    0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, 0x02, 0x82, 0x42, 0xC2, 0x22, 
0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2,
+    0x32, 0xB2, 0x72, 0xF2, 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 
0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A,
+    0xFA, 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 
0xD6, 0x36, 0xB6, 0x76, 0xF6, 0x0E, 0x8E,
+    0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 
0x7E, 0xFE, 0x01, 0x81, 0x41, 0xC1, 0x21,
+    0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, 0x09, 
0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9,
+    0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, 0x05, 0x85, 0x45, 0xC5, 
0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55,
+    0xD5, 0x35, 0xB5, 0x75, 0xF5, 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 
0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD,
+    0x7D, 0xFD, 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 
0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, 0x0B,
+    0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 
0xBB, 0x7B, 0xFB, 0x07, 0x87, 0x47, 0xC7,
+    0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 
0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F,
+    0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF};
+
 // Resize the segment to makes its new length at least min_bytes, new bytes 
will be set to 0.
 // min_bytes can not more than kBitmapSegmentBytes
 void ExpandBitmapSegment(std::string *segment, size_t min_bytes) {
@@ -129,34 +154,9 @@ rocksdb::Status Bitmap::GetString(const Slice &user_key, 
const uint32_t max_btos
     uint32_t valid_size = std::min(
         {fragment.size(), static_cast<size_t>(kBitmapSegmentBytes), 
static_cast<size_t>(metadata.size - frag_index)});
 
-    /*
-     * If you setbit bit 0 1, the value is stored as 0x01 in Kvrocks but 0x80 
in Redis.
-     * So we need to swap bits is to keep the same return value as Redis.
-     * This swap table is generated according to the following mapping 
definition.
-     * swap_table(x) =  ((x & 0x80) >> 7)| ((x & 0x40) >> 5)|\
-     *                  ((x & 0x20) >> 3)| ((x & 0x10) >> 1)|\
-     *                  ((x & 0x08) << 1)| ((x & 0x04) << 3)|\
-     *                  ((x & 0x02) << 5)| ((x & 0x01) << 7);
-     */
-    static const uint8_t swap_table[256] = {
-        0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 
0xD0, 0x30, 0xB0, 0x70, 0xF0, 0x08, 0x88,
-        0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 
0xB8, 0x78, 0xF8, 0x04, 0x84, 0x44, 0xC4,
-        0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 
0xF4, 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC,
-        0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, 0x02, 
0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2,
-        0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, 0x0A, 0x8A, 0x4A, 
0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A,
-        0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, 0x06, 0x86, 0x46, 0xC6, 0x26, 
0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6,
-        0x36, 0xB6, 0x76, 0xF6, 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 
0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE,
-        0x7E, 0xFE, 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 
0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1,
-        0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 
0xD9, 0x39, 0xB9, 0x79, 0xF9, 0x05, 0x85,
-        0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 
0xB5, 0x75, 0xF5, 0x0D, 0x8D, 0x4D, 0xCD,
-        0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 
0xFD, 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3,
-        0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, 0x0B, 
0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB,
-        0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, 0x07, 0x87, 0x47, 
0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97,
-        0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 
0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF,
-        0x3F, 0xBF, 0x7F, 0xFF};
     for (uint32_t i = 0; i < valid_size; i++) {
       if (!fragment[i]) continue;
-      fragment[i] = 
static_cast<char>(swap_table[static_cast<uint8_t>(fragment[i])]);
+      fragment[i] = 
static_cast<char>(kBitSwapTable[static_cast<uint8_t>(fragment[i])]);
     }
     value->replace(frag_index, valid_size, fragment.data(), valid_size);
   }
@@ -210,7 +210,7 @@ rocksdb::Status Bitmap::SetBit(const Slice &user_key, 
uint32_t offset, bool new_
   return storage_->Write(storage_->DefaultWriteOptions(), 
batch->GetWriteBatch());
 }
 
-rocksdb::Status Bitmap::BitCount(const Slice &user_key, int64_t start, int64_t 
stop, uint32_t *cnt) {
+rocksdb::Status Bitmap::BitCount(const Slice &user_key, int64_t start, int64_t 
stop, bool is_bit_index, uint32_t *cnt) {
   *cnt = 0;
   std::string raw_value;
   std::string ns_key = AppendNamespacePrefix(user_key);
@@ -226,16 +226,24 @@ rocksdb::Status Bitmap::BitCount(const Slice &user_key, 
int64_t start, int64_t s
 
   if (metadata.Type() == kRedisString) {
     redis::BitmapString bitmap_string_db(storage_, namespace_);
-    return bitmap_string_db.BitCount(raw_value, start, stop, cnt);
+    return bitmap_string_db.BitCount(raw_value, start, stop, is_bit_index, 
cnt);
   }
 
+  auto totlen = static_cast<int64_t>(metadata.size);
+  if (is_bit_index) totlen <<= 3;
   // Counting bits in byte [start, stop].
-  std::tie(start, stop) = BitmapString::NormalizeRange(start, stop, 
static_cast<int64_t>(metadata.size));
+  std::tie(start, stop) = BitmapString::NormalizeRange(start, stop, totlen);
   // Always return 0 if start is greater than stop after normalization.
   if (start > stop) return rocksdb::Status::OK();
 
-  auto u_start = static_cast<uint32_t>(start);
-  auto u_stop = static_cast<uint32_t>(stop);
+  int64_t start_byte = start;
+  int64_t stop_byte = stop;
+  uint8_t first_byte_neg_mask = 0, last_byte_neg_mask = 0;
+  std::tie(start_byte, stop_byte) = 
BitmapString::NormalizeToByteRangeWithPaddingMask(
+      is_bit_index, start, stop, &first_byte_neg_mask, &last_byte_neg_mask);
+
+  auto u_start = static_cast<uint32_t>(start_byte);
+  auto u_stop = static_cast<uint32_t>(stop_byte);
 
   LatestSnapShot ss(storage_);
   rocksdb::ReadOptions read_options;
@@ -243,6 +251,7 @@ rocksdb::Status Bitmap::BitCount(const Slice &user_key, 
int64_t start, int64_t s
   uint32_t start_index = u_start / kBitmapSegmentBytes;
   uint32_t stop_index = u_stop / kBitmapSegmentBytes;
   // Don't use multi get to prevent large range query, and take too much memory
+  uint32_t mask_cnt = 0;
   for (uint32_t i = start_index; i <= stop_index; i++) {
     rocksdb::PinnableSlice pin_value;
     std::string sub_key =
@@ -252,16 +261,32 @@ rocksdb::Status Bitmap::BitCount(const Slice &user_key, 
int64_t start, int64_t s
     if (!s.ok() && !s.IsNotFound()) return s;
     // NotFound means all bits in this segment are 0.
     if (s.IsNotFound()) continue;
-    // Counting bits in [start_in_segment, start_in_segment + 
length_in_segment)
-    size_t start_in_segment = 0;
-    if (i == start_index) start_in_segment = u_start % kBitmapSegmentBytes;
-    // Though `ExpandBitmapSegment` might generate a segment with logical size 
less than pin_value.size(),
-    // the `RawPopcount` will always return 0 on these padding bytes, so we 
don't need to worry about it.
-    auto length_in_segment = static_cast<int64_t>(pin_value.size());
-    if (i == stop_index) length_in_segment = u_stop % kBitmapSegmentBytes + 1;
-    *cnt += BitmapString::RawPopcount(reinterpret_cast<const uint8_t 
*>(pin_value.data()) + start_in_segment,
-                                      length_in_segment);
+    // Counting bits in [start_in_segment, stop_in_segment]
+    int64_t start_in_segment = 0;                                              
  // start_index in 1024 bytes segment
+    auto readable_stop_in_segment = static_cast<int64_t>(pin_value.size() - 
1);  // stop_index  in 1024 bytes segment
+    auto stop_in_segment = readable_stop_in_segment;
+    if (i == start_index) {
+      start_in_segment = u_start % kBitmapSegmentBytes;
+      if (is_bit_index && start_in_segment <= readable_stop_in_segment && 
first_byte_neg_mask != 0) {
+        uint8_t first_mask_byte =
+            kBitSwapTable[static_cast<uint8_t>(pin_value[start_in_segment])] & 
first_byte_neg_mask;
+        mask_cnt += BitmapString::RawPopcount(&first_mask_byte, 1);
+      }
+    }
+    if (i == stop_index) {
+      stop_in_segment = u_stop % kBitmapSegmentBytes;
+      if (is_bit_index && stop_in_segment <= readable_stop_in_segment && 
last_byte_neg_mask != 0) {
+        uint8_t last_mask_byte = 
kBitSwapTable[static_cast<uint8_t>(pin_value[stop_in_segment])] & 
last_byte_neg_mask;
+        mask_cnt += BitmapString::RawPopcount(&last_mask_byte, 1);
+      }
+    }
+    if (stop_in_segment >= start_in_segment && readable_stop_in_segment >= 
start_in_segment) {
+      int64_t bytes = 0;
+      bytes = std::min(stop_in_segment, readable_stop_in_segment) - 
start_in_segment + 1;
+      *cnt += BitmapString::RawPopcount(reinterpret_cast<const uint8_t 
*>(pin_value.data()) + start_in_segment, bytes);
+    }
   }
+  *cnt -= mask_cnt;
   return rocksdb::Status::OK();
 }
 
diff --git a/src/types/redis_bitmap.h b/src/types/redis_bitmap.h
index a0ada45b..b6b0cc69 100644
--- a/src/types/redis_bitmap.h
+++ b/src/types/redis_bitmap.h
@@ -49,7 +49,7 @@ class Bitmap : public Database {
   rocksdb::Status GetBit(const Slice &user_key, uint32_t offset, bool *bit);
   rocksdb::Status GetString(const Slice &user_key, uint32_t max_btos_size, 
std::string *value);
   rocksdb::Status SetBit(const Slice &user_key, uint32_t offset, bool new_bit, 
bool *old_bit);
-  rocksdb::Status BitCount(const Slice &user_key, int64_t start, int64_t stop, 
uint32_t *cnt);
+  rocksdb::Status BitCount(const Slice &user_key, int64_t start, int64_t stop, 
bool is_bit_index, uint32_t *cnt);
   rocksdb::Status BitPos(const Slice &user_key, bool bit, int64_t start, 
int64_t stop, bool stop_given, int64_t *pos);
   rocksdb::Status BitOp(BitOpFlags op_flag, const std::string &op_name, const 
Slice &user_key,
                         const std::vector<Slice> &op_keys, int64_t *len);
diff --git a/src/types/redis_bitmap_string.cc b/src/types/redis_bitmap_string.cc
index 24befb54..3583ed42 100644
--- a/src/types/redis_bitmap_string.cc
+++ b/src/types/redis_bitmap_string.cc
@@ -68,18 +68,38 @@ rocksdb::Status BitmapString::SetBit(const Slice &ns_key, 
std::string *raw_value
   return storage_->Write(storage_->DefaultWriteOptions(), 
batch->GetWriteBatch());
 }
 
-rocksdb::Status BitmapString::BitCount(const std::string &raw_value, int64_t 
start, int64_t stop, uint32_t *cnt) {
+rocksdb::Status BitmapString::BitCount(const std::string &raw_value, int64_t 
start, int64_t stop, bool is_bit_index,
+                                       uint32_t *cnt) {
   *cnt = 0;
   std::string_view string_value = 
std::string_view{raw_value}.substr(Metadata::GetOffsetAfterExpire(raw_value[0]));
   auto strlen = static_cast<int64_t>(string_value.size());
-  std::tie(start, stop) = NormalizeRange(start, stop, strlen);
+  int64_t totlen = strlen;
+  if (is_bit_index) totlen <<= 3;
+  std::tie(start, stop) = NormalizeRange(start, stop, totlen);
+  // Always return 0 if start is greater than stop after normalization.
+  if (start > stop) return rocksdb::Status::OK();
+
+  /* By default:
+   * start means start byte in bitmap, stop means stop byte in bitmap.
+   * When is_bit_index is true, start and stop means start bit and stop bit.
+   * So it should be normalized bit range to byte range. */
+  int64_t start_byte = start;
+  int64_t stop_byte = stop;
+  uint8_t first_byte_neg_mask = 0, last_byte_neg_mask = 0;
+  std::tie(start_byte, stop_byte) =
+      NormalizeToByteRangeWithPaddingMask(is_bit_index, start, stop, 
&first_byte_neg_mask, &last_byte_neg_mask);
 
   /* Precondition: end >= 0 && end < strlen, so the only condition where
    * zero can be returned is: start > stop. */
-  if (start <= stop) {
-    int64_t bytes = stop - start + 1;
-    *cnt = RawPopcount(reinterpret_cast<const uint8_t *>(string_value.data()) 
+ start, bytes);
+  int64_t bytes = stop_byte - start_byte + 1;
+  *cnt = RawPopcount(reinterpret_cast<const uint8_t *>(string_value.data()) + 
start_byte, bytes);
+  if (first_byte_neg_mask != 0 || last_byte_neg_mask != 0) {
+    uint8_t firstlast[2] = {0, 0};
+    if (first_byte_neg_mask != 0) firstlast[0] = string_value[start_byte] & 
first_byte_neg_mask;
+    if (last_byte_neg_mask != 0) firstlast[1] = string_value[stop_byte] & 
last_byte_neg_mask;
+    *cnt -= RawPopcount(firstlast, 2);
   }
+
   return rocksdb::Status::OK();
 }
 
@@ -202,6 +222,20 @@ std::pair<int64_t, int64_t> 
BitmapString::NormalizeRange(int64_t origin_start, i
   return {origin_start, origin_end};
 }
 
+std::pair<int64_t, int64_t> 
BitmapString::NormalizeToByteRangeWithPaddingMask(bool is_bit, int64_t 
origin_start,
+                                                                              
int64_t origin_end,
+                                                                              
uint8_t *first_byte_neg_mask,
+                                                                              
uint8_t *last_byte_neg_mask) {
+  DCHECK(origin_start <= origin_end);
+  if (is_bit) {
+    *first_byte_neg_mask = ~((1 << (8 - (origin_start & 7))) - 1) & 0xFF;
+    *last_byte_neg_mask = (1 << (7 - (origin_end & 7))) - 1;
+    origin_start >>= 3;
+    origin_end >>= 3;
+  }
+  return {origin_start, origin_end};
+}
+
 rocksdb::Status BitmapString::Bitfield(const Slice &ns_key, std::string 
*raw_value,
                                        const std::vector<BitfieldOperation> 
&ops,
                                        
std::vector<std::optional<BitfieldValue>> *rets) {
diff --git a/src/types/redis_bitmap_string.h b/src/types/redis_bitmap_string.h
index 637a476d..ed4559c5 100644
--- a/src/types/redis_bitmap_string.h
+++ b/src/types/redis_bitmap_string.h
@@ -35,7 +35,8 @@ class BitmapString : public Database {
   BitmapString(engine::Storage *storage, const std::string &ns) : 
Database(storage, ns) {}
   static rocksdb::Status GetBit(const std::string &raw_value, uint32_t offset, 
bool *bit);
   rocksdb::Status SetBit(const Slice &ns_key, std::string *raw_value, uint32_t 
offset, bool new_bit, bool *old_bit);
-  static rocksdb::Status BitCount(const std::string &raw_value, int64_t start, 
int64_t stop, uint32_t *cnt);
+  static rocksdb::Status BitCount(const std::string &raw_value, int64_t start, 
int64_t stop, bool is_bit_index,
+                                  uint32_t *cnt);
   static rocksdb::Status BitPos(const std::string &raw_value, bool bit, 
int64_t start, int64_t stop, bool stop_given,
                                 int64_t *pos);
   rocksdb::Status Bitfield(const Slice &ns_key, std::string *raw_value, const 
std::vector<BitfieldOperation> &ops,
@@ -56,6 +57,21 @@ class BitmapString : public Database {
   // Return:
   //  The normalized [start, end] range.
   static std::pair<int64_t, int64_t> NormalizeRange(int64_t origin_start, 
int64_t origin_end, int64_t length);
+
+  // NormalizeToByteRangeWithPaddingMask converts input index range to a 
normalized byte index range.
+  // If the is_bit_index is false, it does nothing.
+  // If the index_it_bit is true, it convert the bit index range to a 
normalized byte index range, and
+  // pad the first byte negative mask and last byte negative mask.
+  // Such as, If the starting bit is the third bit of the first byte like 
'00010000', the first_byte_neg_mask will be
+  // padded to '11100000', if the end bit is in the fifth bit of the last byte 
like '00000100', the last_byte_neg_mask
+  // will be padded to '00000011'.
+  //
+  // Return:
+  //  The normalized [start_byte, stop_byte]
+  static std::pair<int64_t, int64_t> NormalizeToByteRangeWithPaddingMask(bool 
is_bit_index, int64_t origin_start,
+                                                                         
int64_t origin_end,
+                                                                         
uint8_t *first_byte_neg_mask,
+                                                                         
uint8_t *last_byte_neg_mask);
 };
 
 }  // namespace redis
diff --git a/tests/cppunit/types/bitmap_test.cc 
b/tests/cppunit/types/bitmap_test.cc
index 92a12e48..4795e476 100644
--- a/tests/cppunit/types/bitmap_test.cc
+++ b/tests/cppunit/types/bitmap_test.cc
@@ -72,9 +72,9 @@ TEST_P(RedisBitmapTest, BitCount) {
     bitmap_->SetBit(key_, offset, true, &bit);
   }
   uint32_t cnt = 0;
-  bitmap_->BitCount(key_, 0, 4 * 1024, &cnt);
+  bitmap_->BitCount(key_, 0, 4 * 1024, false, &cnt);
   EXPECT_EQ(cnt, 6);
-  bitmap_->BitCount(key_, 0, -1, &cnt);
+  bitmap_->BitCount(key_, 0, -1, false, &cnt);
   EXPECT_EQ(cnt, 6);
   auto s = bitmap_->Del(key_);
 }
@@ -86,17 +86,17 @@ TEST_P(RedisBitmapTest, BitCountNegative) {
     EXPECT_FALSE(bit);
   }
   uint32_t cnt = 0;
-  bitmap_->BitCount(key_, 0, 4 * 1024, &cnt);
+  bitmap_->BitCount(key_, 0, 4 * 1024, false, &cnt);
   EXPECT_EQ(cnt, 1);
-  bitmap_->BitCount(key_, 0, 0, &cnt);
+  bitmap_->BitCount(key_, 0, 0, false, &cnt);
   EXPECT_EQ(cnt, 1);
-  bitmap_->BitCount(key_, 0, -1, &cnt);
+  bitmap_->BitCount(key_, 0, -1, false, &cnt);
   EXPECT_EQ(cnt, 1);
-  bitmap_->BitCount(key_, -1, -1, &cnt);
+  bitmap_->BitCount(key_, -1, -1, false, &cnt);
   EXPECT_EQ(cnt, 1);
-  bitmap_->BitCount(key_, 1, 1, &cnt);
+  bitmap_->BitCount(key_, 1, 1, false, &cnt);
   EXPECT_EQ(cnt, 0);
-  bitmap_->BitCount(key_, -10000, -10000, &cnt);
+  bitmap_->BitCount(key_, -10000, -10000, false, &cnt);
   EXPECT_EQ(cnt, 1);
 
   {
@@ -104,7 +104,7 @@ TEST_P(RedisBitmapTest, BitCountNegative) {
     bitmap_->SetBit(key_, 5, true, &bit);
     EXPECT_FALSE(bit);
   }
-  bitmap_->BitCount(key_, -10000, -10000, &cnt);
+  bitmap_->BitCount(key_, -10000, -10000, false, &cnt);
   EXPECT_EQ(cnt, 2);
 
   {
@@ -115,15 +115,54 @@ TEST_P(RedisBitmapTest, BitCountNegative) {
     EXPECT_FALSE(bit);
   }
 
-  bitmap_->BitCount(key_, 0, 1024, &cnt);
+  bitmap_->BitCount(key_, 0, 1024, false, &cnt);
   EXPECT_EQ(cnt, 4);
 
-  bitmap_->BitCount(key_, 0, 1023, &cnt);
+  bitmap_->BitCount(key_, 0, 1023, false, &cnt);
   EXPECT_EQ(cnt, 3);
 
   auto s = bitmap_->Del(key_);
 }
 
+TEST_P(RedisBitmapTest, BitCountBITOption) {
+  std::set<uint32_t> offsets = {0, 100, 1024 * 8, 1024 * 8 + 1, 3 * 1024 * 8, 
3 * 1024 * 8 + 1};
+  for (const auto &offset : offsets) {
+    bool bit = false;
+    bitmap_->SetBit(key_, offset, true, &bit);
+  }
+
+  for (uint32_t bit_offset = 0; bit_offset <= 3 * 1024 * 8 + 10; ++bit_offset) 
{
+    uint32_t cnt = 0;
+    EXPECT_TRUE(bitmap_->BitCount(key_, bit_offset, bit_offset, true, 
&cnt).ok());
+    if (offsets.count(bit_offset) > 0) {
+      ASSERT_EQ(1, cnt) << "bit_offset: " << bit_offset;
+    } else {
+      ASSERT_EQ(0, cnt) << "bit_offset: " << bit_offset;
+    }
+  }
+
+  uint32_t cnt = 0;
+  bitmap_->BitCount(key_, 0, 4 * 1024 * 8, true, &cnt);
+  EXPECT_EQ(cnt, 6);
+  bitmap_->BitCount(key_, 0, -1, true, &cnt);
+  EXPECT_EQ(cnt, 6);
+  bitmap_->BitCount(key_, 0, 3 * 1024 * 8 + 1, true, &cnt);
+  EXPECT_EQ(cnt, 6);
+  bitmap_->BitCount(key_, 1, 3 * 1024 * 8 + 1, true, &cnt);
+  EXPECT_EQ(cnt, 5);
+  bitmap_->BitCount(key_, 0, 0, true, &cnt);
+  EXPECT_EQ(cnt, 1);
+  bitmap_->BitCount(key_, 0, 100, true, &cnt);
+  EXPECT_EQ(cnt, 2);
+  bitmap_->BitCount(key_, 100, 1024 * 8, true, &cnt);
+  EXPECT_EQ(cnt, 2);
+  bitmap_->BitCount(key_, 100, 3 * 1024 * 8, true, &cnt);
+  EXPECT_EQ(cnt, 4);
+  bitmap_->BitCount(key_, -1, -1, true, &cnt);
+  EXPECT_EQ(cnt, 0);  // NOTICE: the min storage unit is byte, the result is 
the same as Redis.
+  auto s = bitmap_->Del(key_);
+}
+
 TEST_P(RedisBitmapTest, BitPosClearBit) {
   int64_t pos = 0;
   bool old_bit = false;
diff --git a/tests/gocase/unit/type/bitmap/bitmap_test.go 
b/tests/gocase/unit/type/bitmap/bitmap_test.go
index a8ee50d0..508f52dd 100644
--- a/tests/gocase/unit/type/bitmap/bitmap_test.go
+++ b/tests/gocase/unit/type/bitmap/bitmap_test.go
@@ -205,6 +205,60 @@ func TestBitmap(t *testing.T) {
                require.EqualValues(t, maxOffset, cmd.Val())
        })
 
+       t.Run("BITCOUNT BIT/BYTE option check(type bitmap bitmap_string)", 
func(t *testing.T) {
+               require.NoError(t, rdb.Del(ctx, "foo").Err())
+               require.NoError(t, rdb.Do(ctx, "SET", "foo", "hello").Err())
+               cmd := rdb.Do(ctx, "BITCOUNT", "foo", 0, -1, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 21, cmd.Val())
+               require.NoError(t, rdb.Do(ctx, "SETBIT", "foo", 1024*8+2, 
1).Err())
+               require.NoError(t, rdb.Do(ctx, "SETBIT", "foo", 2*1024*8+1, 
1).Err())
+               cmd = rdb.Do(ctx, "BITCOUNT", "foo", 0, -1, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 23, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "foo", 0, 1024*8+2, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 22, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "foo", 40, 1024*8+2, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 1, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "foo", 0, 0, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 0, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "foo", 1024*8+2, 2*1024*8+1, 
"BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 2, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "foo", -1, -1, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 0, cmd.Val())
+               require.NoError(t, rdb.Del(ctx, "foo").Err())
+
+               require.NoError(t, rdb.Del(ctx, "bar").Err())
+               require.NoError(t, rdb.Do(ctx, "SETBIT", "bar", 0, 1).Err())
+               require.NoError(t, rdb.Do(ctx, "SETBIT", "bar", 100, 1).Err())
+               require.NoError(t, rdb.Do(ctx, "SETBIT", "bar", 1024*8+2, 
1).Err())
+               require.NoError(t, rdb.Do(ctx, "SETBIT", "bar", 2*1024*8+1, 
1).Err())
+               cmd = rdb.Do(ctx, "BITCOUNT", "bar", 0, 0, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 1, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "bar", 0, 100, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 2, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "bar", 100, 1024*8+2, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 2, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "bar", 1024*8+2, 2*1024*8+2, 
"BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 2, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "bar", 0, -1, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 4, cmd.Val())
+               cmd = rdb.Do(ctx, "BITCOUNT", "bar", -1, -1, "BIT")
+               require.NoError(t, cmd.Err())
+               require.EqualValues(t, 0, cmd.Val())
+               require.NoError(t, rdb.Del(ctx, "bar").Err())
+       })
+
        t.Run("BITOP NOT (known string)", func(t *testing.T) {
                Set2SetBit(t, rdb, ctx, "s", []byte("\xaa\x00\xff\x55"))
                require.NoError(t, rdb.BitOpNot(ctx, "dest", "s").Err())

Reply via email to