Skip to content

Commit 3603de9

Browse files
nkrokersixthkrumclaude
committed
feat(bitmap): support DIFF, DIFF1, ANDOR, ONE for BITOP command
Implements Redis 8.2+ bitwise operations: - DIFF: bits set in X but not in any Y - DIFF1: bits set in any Y but not in X - ANDOR: bits set in X and at least one Y - ONE: bits set in exactly one key Closes #3132 Co-Authored-By: Vikram Alagh <sixthkrum@gmail.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 04d4e7f commit 3603de9

4 files changed

Lines changed: 248 additions & 33 deletions

File tree

src/commands/cmd_bit.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ class CommandBitOp : public Commander {
225225
op_flag_ = kBitOpXor;
226226
else if (opname == "not")
227227
op_flag_ = kBitOpNot;
228+
else if (opname == "diff")
229+
op_flag_ = kBitOpDiff;
230+
else if (opname == "diff1")
231+
op_flag_ = kBitOpDiff1;
232+
else if (opname == "andor")
233+
op_flag_ = kBitOpAndOr;
234+
else if (opname == "one")
235+
op_flag_ = kBitOpOne;
228236
else
229237
return {Status::RedisInvalidCmd, errInvalidSyntax};
230238
if (op_flag_ == kBitOpNot && args.size() != 4) {

src/types/redis_bitmap.cc

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,8 @@ rocksdb::Status Bitmap::BitOp(engine::Context &ctx, BitOpFlags op_flag, const st
548548
* result in GCC compiling the code using multiple-words load/store
549549
* operations that are not supported even in ARM >= v6. */
550550
#ifndef USE_ALIGNED_ACCESS
551-
if (frag_minlen >= sizeof(uint64_t) * 4 && frag_numkeys <= 16) {
551+
if (frag_minlen >= sizeof(uint64_t) * 4 && frag_numkeys <= 16 &&
552+
op_flag != kBitOpDiff && op_flag != kBitOpDiff1 && op_flag != kBitOpAndOr && op_flag != kBitOpOne) {
552553
auto *lres = reinterpret_cast<uint64_t *>(frag_res.get());
553554
const uint64_t *lp[16];
554555
for (uint64_t i = 0; i < frag_numkeys; i++) {
@@ -595,22 +596,55 @@ rocksdb::Status Bitmap::BitOp(engine::Context &ctx, BitOpFlags op_flag, const st
595596

596597
uint8_t output = 0, byte = 0;
597598
for (; j < frag_maxlen; j++) {
598-
output = (fragments[0].size() <= j) ? 0 : fragments[0][j];
599-
if (op_flag == kBitOpNot) output = ~output;
600-
for (uint64_t i = 1; i < frag_numkeys; i++) {
601-
byte = (fragments[i].size() <= j) ? 0 : fragments[i][j];
602-
switch (op_flag) {
603-
case kBitOpAnd:
604-
output &= byte;
605-
break;
606-
case kBitOpOr:
607-
output |= byte;
608-
break;
609-
case kBitOpXor:
610-
output ^= byte;
611-
break;
612-
default:
613-
break;
599+
output = (fragments[0].size() <= j) ? 0 : static_cast<uint8_t>(fragments[0][j]);
600+
if (op_flag == kBitOpNot) {
601+
output = ~output;
602+
} else if (op_flag == kBitOpDiff1) {
603+
// DIFF1: bits set in any Y but not in X (X = fragments[0])
604+
uint8_t or_rest = 0;
605+
for (uint64_t i = 1; i < frag_numkeys; i++) {
606+
byte = (fragments[i].size() <= j) ? 0 : static_cast<uint8_t>(fragments[i][j]);
607+
or_rest |= byte;
608+
}
609+
output = or_rest & ~output;
610+
} else if (op_flag == kBitOpAndOr) {
611+
// ANDOR: bits set in X AND in at least one Y
612+
uint8_t or_rest = 0;
613+
for (uint64_t i = 1; i < frag_numkeys; i++) {
614+
byte = (fragments[i].size() <= j) ? 0 : static_cast<uint8_t>(fragments[i][j]);
615+
or_rest |= byte;
616+
}
617+
output = output & or_rest;
618+
} else if (op_flag == kBitOpOne) {
619+
// ONE: bits set in exactly one key across all inputs
620+
// xor_acc tracks odd parity, and_acc tracks bits set in 2+ keys
621+
uint8_t xor_acc = output, and_acc = 0;
622+
for (uint64_t i = 1; i < frag_numkeys; i++) {
623+
byte = (fragments[i].size() <= j) ? 0 : static_cast<uint8_t>(fragments[i][j]);
624+
and_acc |= (xor_acc & byte);
625+
xor_acc ^= byte;
626+
}
627+
output = xor_acc & ~and_acc;
628+
} else {
629+
for (uint64_t i = 1; i < frag_numkeys; i++) {
630+
byte = (fragments[i].size() <= j) ? 0 : static_cast<uint8_t>(fragments[i][j]);
631+
switch (op_flag) {
632+
case kBitOpAnd:
633+
output &= byte;
634+
break;
635+
case kBitOpOr:
636+
output |= byte;
637+
break;
638+
case kBitOpXor:
639+
output ^= byte;
640+
break;
641+
case kBitOpDiff:
642+
// DIFF: bits set in X but not in any Y
643+
output &= ~byte;
644+
break;
645+
default:
646+
break;
647+
}
614648
}
615649
}
616650
frag_res[j] = output;

src/types/redis_bitmap.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ enum BitOpFlags {
3434
kBitOpOr,
3535
kBitOpXor,
3636
kBitOpNot,
37+
kBitOpDiff,
38+
kBitOpDiff1,
39+
kBitOpAndOr,
40+
kBitOpOne,
3741
};
3842

3943
namespace redis {

tests/gocase/unit/type/bitmap/bitmap_test.go

Lines changed: 185 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ import (
3939
type BITOP int32
4040

4141
const (
42-
AND BITOP = 0
43-
OR BITOP = 1
44-
XOR BITOP = 2
45-
NOT BITOP = 3
42+
AND BITOP = 0
43+
OR BITOP = 1
44+
XOR BITOP = 2
45+
NOT BITOP = 3
46+
DIFF BITOP = 4
47+
DIFF1 BITOP = 5
48+
ANDOR BITOP = 6
49+
ONE BITOP = 7
4650
)
4751

4852
func Set2SetBit(t *testing.T, rdb *redis.Client, ctx context.Context, key string, bs []byte) {
@@ -88,22 +92,69 @@ func SimulateBitOp(op BITOP, values ...[]byte) string {
8892
} else {
8993
x = '0'
9094
}
91-
}
92-
for j := 1; j < len(binaryArray); j++ {
93-
left := int(x - '0')
94-
right := int(binaryArray[j][i] - '0')
95-
switch op {
96-
case AND:
97-
left = left & right
98-
case XOR:
99-
left = left ^ right
100-
case OR:
101-
left = left | right
95+
} else if op == DIFF {
96+
// bits in X but not in any Y
97+
for j := 1; j < len(binaryArray); j++ {
98+
if binaryArray[j][i] == '1' {
99+
x = '0'
100+
}
101+
}
102+
} else if op == DIFF1 {
103+
// bits in any Y but not in X
104+
orRest := byte('0')
105+
for j := 1; j < len(binaryArray); j++ {
106+
if binaryArray[j][i] == '1' {
107+
orRest = '1'
108+
}
102109
}
103-
if left == 0 {
110+
if orRest == '1' && x == '0' {
111+
x = '1'
112+
} else {
104113
x = '0'
114+
}
115+
} else if op == ANDOR {
116+
// bits in X AND at least one Y
117+
orRest := byte('0')
118+
for j := 1; j < len(binaryArray); j++ {
119+
if binaryArray[j][i] == '1' {
120+
orRest = '1'
121+
}
122+
}
123+
if x == '1' && orRest == '1' {
124+
x = '1'
105125
} else {
126+
x = '0'
127+
}
128+
} else if op == ONE {
129+
// bits set in exactly one key
130+
count := 0
131+
for j := 0; j < len(binaryArray); j++ {
132+
if binaryArray[j][i] == '1' {
133+
count++
134+
}
135+
}
136+
if count == 1 {
106137
x = '1'
138+
} else {
139+
x = '0'
140+
}
141+
} else {
142+
for j := 1; j < len(binaryArray); j++ {
143+
left := int(x - '0')
144+
right := int(binaryArray[j][i] - '0')
145+
switch op {
146+
case AND:
147+
left = left & right
148+
case XOR:
149+
left = left ^ right
150+
case OR:
151+
left = left | right
152+
}
153+
if left == 0 {
154+
x = '0'
155+
} else {
156+
x = '1'
157+
}
107158
}
108159
}
109160
binaryResult = append(binaryResult, x)
@@ -357,6 +408,124 @@ func TestBitmap(t *testing.T) {
357408
require.EqualValues(t, 32, rdb.BitOpOr(ctx, "x", "a", "b").Val())
358409
})
359410

411+
t.Run("BITOP DIFF basic", func(t *testing.T) {
412+
require.NoError(t, rdb.FlushDB(ctx).Err())
413+
// X=0xff, Y=0x0f -> DIFF = 0xf0 (bits in X not in Y)
414+
Set2SetBit(t, rdb, ctx, "x", []byte("\xff"))
415+
Set2SetBit(t, rdb, ctx, "y", []byte("\x0f"))
416+
require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF", "dest", "x", "y").Err())
417+
require.EqualValues(t, SimulateBitOp(DIFF, []byte("\xff"), []byte("\x0f")), rdb.Get(ctx, "dest").Val())
418+
})
419+
420+
t.Run("BITOP DIFF with multiple Y keys", func(t *testing.T) {
421+
require.NoError(t, rdb.FlushDB(ctx).Err())
422+
Set2SetBit(t, rdb, ctx, "x", []byte("\xff"))
423+
Set2SetBit(t, rdb, ctx, "y1", []byte("\x0f"))
424+
Set2SetBit(t, rdb, ctx, "y2", []byte("\xf0"))
425+
require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF", "dest", "x", "y1", "y2").Err())
426+
require.EqualValues(t, SimulateBitOp(DIFF, []byte("\xff"), []byte("\x0f"), []byte("\xf0")), rdb.Get(ctx, "dest").Val())
427+
})
428+
429+
t.Run("BITOP DIFF missing key treated as zero", func(t *testing.T) {
430+
require.NoError(t, rdb.FlushDB(ctx).Err())
431+
Set2SetBit(t, rdb, ctx, "x", []byte("\xaa"))
432+
require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF", "dest", "x", "no-such-key").Err())
433+
require.EqualValues(t, SimulateBitOp(DIFF, []byte("\xaa"), []byte("\x00")), rdb.Get(ctx, "dest").Val())
434+
})
435+
436+
t.Run("BITOP DIFF1 basic", func(t *testing.T) {
437+
require.NoError(t, rdb.FlushDB(ctx).Err())
438+
// X=0xff, Y=0x0f -> DIFF1 = 0x00 (bits in Y not in X, but X has all bits set)
439+
Set2SetBit(t, rdb, ctx, "x", []byte("\xff"))
440+
Set2SetBit(t, rdb, ctx, "y", []byte("\x0f"))
441+
require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF1", "dest", "x", "y").Err())
442+
require.EqualValues(t, SimulateBitOp(DIFF1, []byte("\xff"), []byte("\x0f")), rdb.Get(ctx, "dest").Val())
443+
})
444+
445+
t.Run("BITOP DIFF1 with partial overlap", func(t *testing.T) {
446+
require.NoError(t, rdb.FlushDB(ctx).Err())
447+
// X=0x0f, Y=0xff -> DIFF1 = 0xf0 (bits in Y not in X)
448+
Set2SetBit(t, rdb, ctx, "x", []byte("\x0f"))
449+
Set2SetBit(t, rdb, ctx, "y", []byte("\xff"))
450+
require.NoError(t, rdb.Do(ctx, "BITOP", "DIFF1", "dest", "x", "y").Err())
451+
require.EqualValues(t, SimulateBitOp(DIFF1, []byte("\x0f"), []byte("\xff")), rdb.Get(ctx, "dest").Val())
452+
})
453+
454+
t.Run("BITOP ANDOR basic", func(t *testing.T) {
455+
require.NoError(t, rdb.FlushDB(ctx).Err())
456+
// X=0xff, Y=0x0f -> ANDOR = 0x0f (bits in X AND at least one Y)
457+
Set2SetBit(t, rdb, ctx, "x", []byte("\xff"))
458+
Set2SetBit(t, rdb, ctx, "y", []byte("\x0f"))
459+
require.NoError(t, rdb.Do(ctx, "BITOP", "ANDOR", "dest", "x", "y").Err())
460+
require.EqualValues(t, SimulateBitOp(ANDOR, []byte("\xff"), []byte("\x0f")), rdb.Get(ctx, "dest").Val())
461+
})
462+
463+
t.Run("BITOP ANDOR with multiple Y keys", func(t *testing.T) {
464+
require.NoError(t, rdb.FlushDB(ctx).Err())
465+
Set2SetBit(t, rdb, ctx, "x", []byte("\xff"))
466+
Set2SetBit(t, rdb, ctx, "y1", []byte("\x0f"))
467+
Set2SetBit(t, rdb, ctx, "y2", []byte("\xf0"))
468+
require.NoError(t, rdb.Do(ctx, "BITOP", "ANDOR", "dest", "x", "y1", "y2").Err())
469+
require.EqualValues(t, SimulateBitOp(ANDOR, []byte("\xff"), []byte("\x0f"), []byte("\xf0")), rdb.Get(ctx, "dest").Val())
470+
})
471+
472+
t.Run("BITOP ONE basic", func(t *testing.T) {
473+
require.NoError(t, rdb.FlushDB(ctx).Err())
474+
// A=0xff, B=0x0f -> ONE = 0xf0 (bits set in exactly one key)
475+
Set2SetBit(t, rdb, ctx, "a", []byte("\xff"))
476+
Set2SetBit(t, rdb, ctx, "b", []byte("\x0f"))
477+
require.NoError(t, rdb.Do(ctx, "BITOP", "ONE", "dest", "a", "b").Err())
478+
require.EqualValues(t, SimulateBitOp(ONE, []byte("\xff"), []byte("\x0f")), rdb.Get(ctx, "dest").Val())
479+
})
480+
481+
t.Run("BITOP ONE with three keys", func(t *testing.T) {
482+
require.NoError(t, rdb.FlushDB(ctx).Err())
483+
Set2SetBit(t, rdb, ctx, "a", []byte("\xff"))
484+
Set2SetBit(t, rdb, ctx, "b", []byte("\x0f"))
485+
Set2SetBit(t, rdb, ctx, "c", []byte("\xf0"))
486+
require.NoError(t, rdb.Do(ctx, "BITOP", "ONE", "dest", "a", "b", "c").Err())
487+
require.EqualValues(t, SimulateBitOp(ONE, []byte("\xff"), []byte("\x0f"), []byte("\xf0")), rdb.Get(ctx, "dest").Val())
488+
})
489+
490+
t.Run("BITOP ONE single key returns same key", func(t *testing.T) {
491+
require.NoError(t, rdb.FlushDB(ctx).Err())
492+
Set2SetBit(t, rdb, ctx, "a", []byte("\xaa"))
493+
require.NoError(t, rdb.Do(ctx, "BITOP", "ONE", "dest", "a").Err())
494+
require.EqualValues(t, SimulateBitOp(ONE, []byte("\xaa")), rdb.Get(ctx, "dest").Val())
495+
})
496+
497+
t.Run("BITOP new ops fuzzing", func(t *testing.T) {
498+
require.NoError(t, rdb.FlushDB(ctx).Err())
499+
for i := 0; i < 10; i++ {
500+
numKeys := 2 + i%3
501+
vec := make([][]byte, numKeys)
502+
veckeys := make([]string, numKeys)
503+
for k := 0; k < numKeys; k++ {
504+
vec[k] = []byte(util.RandStringWithSeed(1, 10, util.Binary, int64(i*100+k)))
505+
veckeys[k] = fmt.Sprintf("fuzz-%d-%d", i, k)
506+
Set2SetBit(t, rdb, ctx, veckeys[k], vec[k])
507+
}
508+
doArgs := func(op string) []interface{} {
509+
args := []interface{}{"BITOP", op, "target"}
510+
for _, k := range veckeys {
511+
args = append(args, k)
512+
}
513+
return args
514+
}
515+
require.NoError(t, rdb.Do(ctx, doArgs("DIFF")...).Err())
516+
require.EqualValues(t, SimulateBitOp(DIFF, vec...), rdb.Get(ctx, "target").Val())
517+
518+
require.NoError(t, rdb.Do(ctx, doArgs("DIFF1")...).Err())
519+
require.EqualValues(t, SimulateBitOp(DIFF1, vec...), rdb.Get(ctx, "target").Val())
520+
521+
require.NoError(t, rdb.Do(ctx, doArgs("ANDOR")...).Err())
522+
require.EqualValues(t, SimulateBitOp(ANDOR, vec...), rdb.Get(ctx, "target").Val())
523+
524+
require.NoError(t, rdb.Do(ctx, doArgs("ONE")...).Err())
525+
require.EqualValues(t, SimulateBitOp(ONE, vec...), rdb.Get(ctx, "target").Val())
526+
}
527+
})
528+
360529
t.Run("BITFIELD and BITFIELD_RO on string type", func(t *testing.T) {
361530
str := "zhe ge ren hen lan, shen me dou mei you liu xia."
362531
require.NoError(t, rdb.Set(ctx, "str", str, 0).Err())

0 commit comments

Comments
 (0)