From 84094254898ead4f0f7b06efc760652fecb14cbf Mon Sep 17 00:00:00 2001 From: xuzifu666 <1206332514@qq.com> Date: Mon, 9 Mar 2026 17:00:20 +0800 Subject: [PATCH] feat(bitop): Support DIFF, DIFF1, ANDOR, and ONE for command BITOP --- src/commands/cmd_bit.cc | 8 + src/types/redis_bitmap.cc | 175 ++++++++++++++-- src/types/redis_bitmap.h | 4 + tests/gocase/unit/type/bitmap/bitmap_test.go | 198 +++++++++++++++++++ 4 files changed, 372 insertions(+), 13 deletions(-) diff --git a/src/commands/cmd_bit.cc b/src/commands/cmd_bit.cc index 13be25f3134..fc33619c8c7 100644 --- a/src/commands/cmd_bit.cc +++ b/src/commands/cmd_bit.cc @@ -225,6 +225,14 @@ class CommandBitOp : public Commander { op_flag_ = kBitOpXor; else if (opname == "not") op_flag_ = kBitOpNot; + else if (opname == "diff") + op_flag_ = kBitOpDiff; + else if (opname == "diff1") + op_flag_ = kBitOpDiff1; + else if (opname == "andor") + op_flag_ = kBitOpAndor; + else if (opname == "one") + op_flag_ = kBitOpOne; else return {Status::RedisInvalidCmd, errInvalidSyntax}; if (op_flag_ == kBitOpNot && args.size() != 4) { diff --git a/src/types/redis_bitmap.cc b/src/types/redis_bitmap.cc index 00c8d1b3fde..cebcc869fe4 100644 --- a/src/types/redis_bitmap.cc +++ b/src/types/redis_bitmap.cc @@ -579,6 +579,89 @@ rocksdb::Status Bitmap::BitOp(engine::Context &ctx, BitOpFlags op_flag, const st apply_fast_path_op([](uint64_t &a, uint64_t b) { a |= b; }); } else if (op_flag == kBitOpXor) { apply_fast_path_op([](uint64_t &a, uint64_t b) { a ^= b; }); + } else if (op_flag == kBitOpDiff) { + // DIFF: dest = X & ~(Y1 | Y2 | ...) + while (frag_minlen >= sizeof(uint64_t) * 4) { + uint64_t y_or[4] = {0}; + for (uint64_t i = 1; i < frag_numkeys; i++) { + y_or[0] |= lp[i][0]; + y_or[1] |= lp[i][1]; + y_or[2] |= lp[i][2]; + y_or[3] |= lp[i][3]; + lp[i] += 4; + } + lres[0] &= ~y_or[0]; + lres[1] &= ~y_or[1]; + lres[2] &= ~y_or[2]; + lres[3] &= ~y_or[3]; + lres += 4; + j += sizeof(uint64_t) * 4; + frag_minlen -= sizeof(uint64_t) * 4; + } + } else if (op_flag == kBitOpDiff1) { + // DIFF1: dest = (Y1 | Y2 | ...) & ~X + while (frag_minlen >= sizeof(uint64_t) * 4) { + uint64_t y_or[4] = {0}; + for (uint64_t i = 1; i < frag_numkeys; i++) { + y_or[0] |= lp[i][0]; + y_or[1] |= lp[i][1]; + y_or[2] |= lp[i][2]; + y_or[3] |= lp[i][3]; + lp[i] += 4; + } + lres[0] = y_or[0] & ~lres[0]; + lres[1] = y_or[1] & ~lres[1]; + lres[2] = y_or[2] & ~lres[2]; + lres[3] = y_or[3] & ~lres[3]; + lres += 4; + j += sizeof(uint64_t) * 4; + frag_minlen -= sizeof(uint64_t) * 4; + } + } else if (op_flag == kBitOpAndor) { + // ANDOR: dest = X & (Y1 | Y2 | ...) + while (frag_minlen >= sizeof(uint64_t) * 4) { + uint64_t y_or[4] = {0}; + for (uint64_t i = 1; i < frag_numkeys; i++) { + y_or[0] |= lp[i][0]; + y_or[1] |= lp[i][1]; + y_or[2] |= lp[i][2]; + y_or[3] |= lp[i][3]; + lp[i] += 4; + } + lres[0] &= y_or[0]; + lres[1] &= y_or[1]; + lres[2] &= y_or[2]; + lres[3] &= y_or[3]; + lres += 4; + j += sizeof(uint64_t) * 4; + frag_minlen -= sizeof(uint64_t) * 4; + } + } else if (op_flag == kBitOpOne) { + // ONE: dest = 1 if exactly one input has bit set + while (frag_minlen >= sizeof(uint64_t) * 4) { + for (uint64_t k = 0; k < 4; k++) { + uint64_t count[64] = {0}; + for (uint64_t i = 0; i < frag_numkeys; i++) { + for (int bit = 0; bit < 64; bit++) { + if (lp[i][k] & (1ULL << bit)) { + count[bit]++; + } + } + } + lres[k] = 0; + for (int bit = 0; bit < 64; bit++) { + if (count[bit] == 1) { + lres[k] |= (1ULL << bit); + } + } + } + for (uint64_t i = 0; i < frag_numkeys; i++) { + lp[i] += 4; + } + lres += 4; + j += sizeof(uint64_t) * 4; + frag_minlen -= sizeof(uint64_t) * 4; + } } else if (op_flag == kBitOpNot) { while (frag_minlen >= sizeof(uint64_t) * 4) { lres[0] = ~lres[0]; @@ -595,23 +678,89 @@ rocksdb::Status Bitmap::BitOp(engine::Context &ctx, BitOpFlags op_flag, const st uint8_t output = 0, byte = 0; for (; j < frag_maxlen; j++) { - output = (fragments[0].size() <= j) ? 0 : fragments[0][j]; - if (op_flag == kBitOpNot) output = ~output; - for (uint64_t i = 1; i < frag_numkeys; i++) { - byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; - switch (op_flag) { - case kBitOpAnd: + switch (op_flag) { + case kBitOpAnd: + output = (fragments[0].size() <= j) ? 0 : fragments[0][j]; + for (uint64_t i = 1; i < frag_numkeys; i++) { + byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; output &= byte; - break; - case kBitOpOr: + } + break; + case kBitOpOr: + output = (fragments[0].size() <= j) ? 0 : fragments[0][j]; + for (uint64_t i = 1; i < frag_numkeys; i++) { + byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; output |= byte; - break; - case kBitOpXor: + } + break; + case kBitOpXor: + output = (fragments[0].size() <= j) ? 0 : fragments[0][j]; + for (uint64_t i = 1; i < frag_numkeys; i++) { + byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; output ^= byte; - break; - default: - break; + } + break; + case kBitOpNot: + output = (fragments[0].size() <= j) ? 0 : fragments[0][j]; + output = ~output; + break; + case kBitOpDiff: { + // DIFF: dest[i] = X[i] & ~(Y1[i] | Y2[i] | ...) + // BITOP DIFF destkey X [Y1 Y2 ...] + uint8_t x = (fragments[0].size() <= j) ? 0 : fragments[0][j]; + uint8_t y_or = 0; + for (uint64_t i = 1; i < frag_numkeys; i++) { + byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; + y_or |= byte; + } + output = x & ~y_or; + break; + } + case kBitOpDiff1: { + // DIFF1: dest[i] = (Y1[i] | Y2[i] | ...) & ~X[i] + // BITOP DIFF1 destkey X [Y1 Y2 ...] + uint8_t x = (fragments[0].size() <= j) ? 0 : fragments[0][j]; + uint8_t y_or = 0; + for (uint64_t i = 1; i < frag_numkeys; i++) { + byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; + y_or |= byte; + } + output = y_or & ~x; + break; + } + case kBitOpAndor: { + // ANDOR: dest[i] = X[i] & (Y1[i] | Y2[i] | ...) + // BITOP ANDOR destkey X [Y1 Y2 ...] + uint8_t x = (fragments[0].size() <= j) ? 0 : fragments[0][j]; + uint8_t y_or = 0; + for (uint64_t i = 1; i < frag_numkeys; i++) { + byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; + y_or |= byte; + } + output = x & y_or; + break; + } + case kBitOpOne: { + // ONE: dest[i] = 1 if exactly one input has bit set + // BITOP ONE destkey X1 [X2 X3 ...] + output = 0; + for (int bit = 0; bit < 8; bit++) { + int count = 0; + for (uint64_t i = 0; i < frag_numkeys; i++) { + byte = (fragments[i].size() <= j) ? 0 : fragments[i][j]; + if (byte & (1 << bit)) { + count++; + } + } + if (count == 1) { + output |= (1 << bit); + } + } + break; } + default: + output = 0; + break; } frag_res[j] = output; } diff --git a/src/types/redis_bitmap.h b/src/types/redis_bitmap.h index 32a53cc72ab..ba387604b96 100644 --- a/src/types/redis_bitmap.h +++ b/src/types/redis_bitmap.h @@ -34,6 +34,10 @@ enum BitOpFlags { kBitOpOr, kBitOpXor, kBitOpNot, + kBitOpDiff, + kBitOpDiff1, + kBitOpAndor, + kBitOpOne, }; namespace redis { diff --git a/tests/gocase/unit/type/bitmap/bitmap_test.go b/tests/gocase/unit/type/bitmap/bitmap_test.go index 6211ae3c836..7563ac792a1 100644 --- a/tests/gocase/unit/type/bitmap/bitmap_test.go +++ b/tests/gocase/unit/type/bitmap/bitmap_test.go @@ -568,4 +568,202 @@ func TestBitmap(t *testing.T) { } }) + t.Run("BITOP DIFF", func(t *testing.T) { + require.NoError(t, rdb.FlushDB(ctx).Err()) + // X = 1100, Y1 = 1010, Y2 = 0011 + // X & ~(Y1 | Y2) = 1100 & ~1011 = 1100 & 0100 = 0100 + Set2SetBit(t, rdb, ctx, "x", []byte("\x0c")) + Set2SetBit(t, rdb, ctx, "y1", []byte("\x0a")) + Set2SetBit(t, rdb, ctx, "y2", []byte("\x03")) + require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF", "dest", "x", "y1", "y2").Err()) + require.EqualValues(t, "\x04", rdb.Get(ctx, "dest").Val()) + }) + + t.Run("BITOP DIFF1", func(t *testing.T) { + require.NoError(t, rdb.FlushDB(ctx).Err()) + // X = 1100, Y1 = 1010, Y2 = 0011 + // (Y1 | Y2) & ~X = 1011 & ~1100 = 1011 & 0011 = 0011 + Set2SetBit(t, rdb, ctx, "x", []byte("\x0c")) + Set2SetBit(t, rdb, ctx, "y1", []byte("\x0a")) + Set2SetBit(t, rdb, ctx, "y2", []byte("\x03")) + require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF1", "dest", "x", "y1", "y2").Err()) + require.EqualValues(t, "\x03", rdb.Get(ctx, "dest").Val()) + }) + + t.Run("BITOP ANDOR", func(t *testing.T) { + require.NoError(t, rdb.FlushDB(ctx).Err()) + // X = 1100, Y1 = 1010, Y2 = 0011 + // X & (Y1 | Y2) = 1100 & 1011 = 1000 + Set2SetBit(t, rdb, ctx, "x", []byte("\x0c")) + Set2SetBit(t, rdb, ctx, "y1", []byte("\x0a")) + Set2SetBit(t, rdb, ctx, "y2", []byte("\x03")) + require.NoError(t, rdb.Do(ctx, "BITOP", "ANDOR", "dest", "x", "y1", "y2").Err()) + require.EqualValues(t, "\x08", rdb.Get(ctx, "dest").Val()) + }) + + t.Run("BITOP ONE", func(t *testing.T) { + require.NoError(t, rdb.FlushDB(ctx).Err()) + // Use simpler test case + // X1 = 01 (bit 0), X2 = 10 (bit 1), X3 = 00 (none) + // Exactly one bit set: only bit 0 is set in one input + require.NoError(t, rdb.SetBit(ctx, "x1", 0, 1).Err()) + require.NoError(t, rdb.SetBit(ctx, "x2", 1, 1).Err()) + // x3 has no bits set + require.NoError(t, rdb.Do(ctx, "BITOP", "ONE", "dest", "x1", "x2", "x3").Err()) + // Result should have both bits 0 and 1 set (each appears in exactly one input) + require.NoError(t, rdb.GetBit(ctx, "dest", 0).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "dest", 0).Val()) + require.NoError(t, rdb.GetBit(ctx, "dest", 1).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "dest", 1).Val()) + }) + + t.Run("BITOP operations with multiple keys", func(t *testing.T) { + require.NoError(t, rdb.FlushDB(ctx).Err()) + // Use simple test case with bit positions + // a: bits 0,8 set + // b: bits 1,9 set + // c: bits 2,10 set + require.NoError(t, rdb.SetBit(ctx, "a", 0, 1).Err()) + require.NoError(t, rdb.SetBit(ctx, "a", 8, 1).Err()) + require.NoError(t, rdb.SetBit(ctx, "b", 1, 1).Err()) + require.NoError(t, rdb.SetBit(ctx, "b", 9, 1).Err()) + require.NoError(t, rdb.SetBit(ctx, "c", 2, 1).Err()) + require.NoError(t, rdb.SetBit(ctx, "c", 10, 1).Err()) + + // DIFF: a & ~(b | c) + // a has bits 0,8; (b|c) has bits 1,2,9,10 + // Result: bits 0,8 should be set + require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF", "diff_res", "a", "b", "c").Err()) + require.NoError(t, rdb.GetBit(ctx, "diff_res", 0).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff_res", 0).Val()) + require.NoError(t, rdb.GetBit(ctx, "diff_res", 8).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff_res", 8).Val()) + + // DIFF1: (b | c) & ~a + // (b|c) has bits 1,2,9,10; a has bits 0,8 + // Result: bits 1,2,9,10 should be set + require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF1", "diff1_res", "a", "b", "c").Err()) + require.NoError(t, rdb.GetBit(ctx, "diff1_res", 1).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff1_res", 1).Val()) + require.NoError(t, rdb.GetBit(ctx, "diff1_res", 2).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff1_res", 2).Val()) + + // ANDOR: a & (b | c) + // a has bits 0,8; (b|c) has bits 1,2,9,10 + // Result: none (no common bits) + require.NoError(t, rdb.Do(ctx, "BITOP", "ANDOR", "andor_res", "a", "b", "c").Err()) + require.NoError(t, rdb.GetBit(ctx, "andor_res", 0).Err()) + require.EqualValues(t, 0, rdb.GetBit(ctx, "andor_res", 0).Val()) + + // ONE: exactly one bit set among a, b, c + // a has bits 0,8; b has bits 1,9; c has bits 2,10 + // All bits appear exactly once, so all should be set + require.NoError(t, rdb.Do(ctx, "BITOP", "ONE", "one_res", "a", "b", "c").Err()) + require.NoError(t, rdb.GetBit(ctx, "one_res", 0).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_res", 0).Val()) + require.NoError(t, rdb.GetBit(ctx, "one_res", 1).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_res", 1).Val()) + require.NoError(t, rdb.GetBit(ctx, "one_res", 2).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_res", 2).Val()) + }) + + t.Run("BITOP new operations with large bitmaps", func(t *testing.T) { + require.NoError(t, rdb.FlushDB(ctx).Err()) + // Create large bitmaps (256+ bytes) to trigger the fast path + // Set bits in a specific pattern + for i := 0; i < 32; i++ { + // x: bits at positions 0, 256, 512, ... + require.NoError(t, rdb.SetBit(ctx, "x", int64(i*256), 1).Err()) + // y1: bits at positions 1, 257, 513, ... + require.NoError(t, rdb.SetBit(ctx, "y1", int64(i*256+1), 1).Err()) + // y2: bits at positions 2, 258, 514, ... + require.NoError(t, rdb.SetBit(ctx, "y2", int64(i*256+2), 1).Err()) + } + + // DIFF: x & ~(y1 | y2) + require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF", "diff_large", "x", "y1", "y2").Err()) + // Check first few bits + require.NoError(t, rdb.GetBit(ctx, "diff_large", 0).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff_large", 0).Val()) + require.NoError(t, rdb.GetBit(ctx, "diff_large", 1).Err()) + require.EqualValues(t, 0, rdb.GetBit(ctx, "diff_large", 1).Val()) + require.NoError(t, rdb.GetBit(ctx, "diff_large", 2).Err()) + require.EqualValues(t, 0, rdb.GetBit(ctx, "diff_large", 2).Val()) + // Check a later bit + require.NoError(t, rdb.GetBit(ctx, "diff_large", 256).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff_large", 256).Val()) + + // DIFF1: (y1 | y2) & ~x + require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF1", "diff1_large", "x", "y1", "y2").Err()) + // Check first few bits + require.NoError(t, rdb.GetBit(ctx, "diff1_large", 0).Err()) + require.EqualValues(t, 0, rdb.GetBit(ctx, "diff1_large", 0).Val()) + require.NoError(t, rdb.GetBit(ctx, "diff1_large", 1).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff1_large", 1).Val()) + require.NoError(t, rdb.GetBit(ctx, "diff1_large", 2).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff1_large", 2).Val()) + + // ANDOR: x & (y1 | y2) + require.NoError(t, rdb.Do(ctx, "BITOP", "ANDOR", "andor_large", "x", "y1", "y2").Err()) + // All should be 0 (no common bits) + require.NoError(t, rdb.GetBit(ctx, "andor_large", 0).Err()) + require.EqualValues(t, 0, rdb.GetBit(ctx, "andor_large", 0).Val()) + }) + + t.Run("BITOP ONE with large bitmaps", func(t *testing.T) { + require.NoError(t, rdb.FlushDB(ctx).Err()) + // Create large bitmaps for ONE operation + // a1: bits at positions 0, 512, 1024 + // a2: bits at positions 1, 513, 1025 + // a3: bits at positions 2, 514, 1026 + for i := 0; i < 3; i++ { + require.NoError(t, rdb.SetBit(ctx, "a1", int64(i*512), 1).Err()) + require.NoError(t, rdb.SetBit(ctx, "a2", int64(i*512+1), 1).Err()) + require.NoError(t, rdb.SetBit(ctx, "a3", int64(i*512+2), 1).Err()) + } + + // ONE: exactly one bit set among a1, a2, a3 + require.NoError(t, rdb.Do(ctx, "BITOP", "ONE", "one_large", "a1", "a2", "a3").Err()) + // All bits should be set since each appears in exactly one input + require.NoError(t, rdb.GetBit(ctx, "one_large", 0).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_large", 0).Val()) + require.NoError(t, rdb.GetBit(ctx, "one_large", 1).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_large", 1).Val()) + require.NoError(t, rdb.GetBit(ctx, "one_large", 2).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_large", 2).Val()) + require.NoError(t, rdb.GetBit(ctx, "one_large", 512).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_large", 512).Val()) + require.NoError(t, rdb.GetBit(ctx, "one_large", 513).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_large", 513).Val()) + require.NoError(t, rdb.GetBit(ctx, "one_large", 514).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_large", 514).Val()) + }) + + t.Run("BITOP operations with missing keys", func(t *testing.T) { + require.NoError(t, rdb.FlushDB(ctx).Err()) + // x: bit 0 set + require.NoError(t, rdb.SetBit(ctx, "x", 0, 1).Err()) + // y1 and y2 are missing (treated as all zeros) + + // DIFF: x & ~(y1 | y2) = x & ~0 = x + require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF", "diff_missing", "x", "y1", "y2").Err()) + require.NoError(t, rdb.GetBit(ctx, "diff_missing", 0).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "diff_missing", 0).Val()) + + // DIFF1: (y1 | y2) & ~x = 0 & ~x = 0 + require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF1", "diff1_missing", "x", "y1", "y2").Err()) + require.NoError(t, rdb.GetBit(ctx, "diff1_missing", 0).Err()) + require.EqualValues(t, 0, rdb.GetBit(ctx, "diff1_missing", 0).Val()) + + // ANDOR: x & (y1 | y2) = x & 0 = 0 + require.NoError(t, rdb.Do(ctx, "BITOP", "ANDOR", "andor_missing", "x", "y1", "y2").Err()) + require.NoError(t, rdb.GetBit(ctx, "andor_missing", 0).Err()) + require.EqualValues(t, 0, rdb.GetBit(ctx, "andor_missing", 0).Val()) + + // ONE: exactly one bit set among x, y1, y2 + require.NoError(t, rdb.Do(ctx, "BITOP", "ONE", "one_missing", "x", "y1", "y2").Err()) + require.NoError(t, rdb.GetBit(ctx, "one_missing", 0).Err()) + require.EqualValues(t, 1, rdb.GetBit(ctx, "one_missing", 0).Val()) + }) + }