Skip to content

Commit 5524331

Browse files
gongna-ausisyphus-dev-ai
andcommitted
feat(bit): add BYTE/BIT option support for BITPOS command (Redis 7.0+)
Add explicit BYTE/BIT index option to BITPOS with strict syntax validation matching Redis 7.0 behavior. Reject invalid option values and extra arguments. Add Go integration tests for BYTE option, BIT vs BYTE comparison, case-insensitivity and error cases. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 2520bda commit 5524331

2 files changed

Lines changed: 64 additions & 26 deletions

File tree

src/commands/cmd_bit.cc

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -156,39 +156,31 @@ class CommandBitPos : public Commander {
156156
using Commander::Parse;
157157

158158
Status Parse(const std::vector<std::string> &args) override {
159-
if (args.size() >= 4) {
160-
auto parse_start = ParseInt<int64_t>(args[3], 10);
161-
if (!parse_start) {
162-
return {Status::RedisParseErr, errValueNotInteger};
163-
}
159+
auto parse_bit = ParseInt<int>(args[2], 10);
160+
if (!parse_bit || (*parse_bit != 0 && *parse_bit != 1)) {
161+
return {Status::RedisParseErr, "The bit argument must be 1 or 0."};
162+
}
163+
bit_ = (*parse_bit == 1);
164164

165-
start_ = *parse_start;
165+
if (args.size() >= 4) {
166+
start_ = GET_OR_RET(ParseInt<int64_t>(args[3], 10));
166167
}
167168

168169
if (args.size() >= 5) {
169-
auto parse_stop = ParseInt<int64_t>(args[4], 10);
170-
if (!parse_stop) {
171-
return {Status::RedisParseErr, errValueNotInteger};
172-
}
173-
170+
stop_ = GET_OR_RET(ParseInt<int64_t>(args[4], 10));
174171
stop_given_ = true;
175-
stop_ = *parse_stop;
176172
}
177173

178-
if (args.size() >= 6 && util::EqualICase(args[5], "BIT")) {
179-
is_bit_index_ = true;
180-
}
181-
182-
auto parse_arg = ParseInt<int64_t>(args[2], 10);
183-
if (!parse_arg) {
184-
return {Status::RedisParseErr, errValueNotInteger};
185-
}
186-
if (*parse_arg == 0) {
187-
bit_ = false;
188-
} else if (*parse_arg == 1) {
189-
bit_ = true;
190-
} else {
191-
return {Status::RedisParseErr, "The bit argument must be 1 or 0."};
174+
if (args.size() == 6) {
175+
if (util::EqualICase(args[5], "BIT")) {
176+
is_bit_index_ = true;
177+
} else if (util::EqualICase(args[5], "BYTE")) {
178+
is_bit_index_ = false;
179+
} else {
180+
return {Status::RedisParseErr, errInvalidSyntax};
181+
}
182+
} else if (args.size() > 6) {
183+
return {Status::RedisParseErr, errInvalidSyntax};
192184
}
193185

194186
return Commander::Parse(args);

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,52 @@ func TestBitmap(t *testing.T) {
400400
require.EqualValues(t, 2, cmd.Val())
401401
})
402402

403+
t.Run("BITPOS BYTE option produces same result as default byte-indexed mode", func(t *testing.T) {
404+
require.NoError(t, rdb.Set(ctx, "mykey", "\x00\xff\xf0", 0).Err())
405+
byteResult := rdb.BitPos(ctx, "mykey", 1, 1)
406+
require.NoError(t, byteResult.Err())
407+
explicitByte := rdb.BitPosSpan(ctx, "mykey", 1, 1, -1, "byte")
408+
require.NoError(t, explicitByte.Err())
409+
require.EqualValues(t, byteResult.Val(), explicitByte.Val())
410+
})
411+
412+
t.Run("BITPOS BYTE option is case-insensitive", func(t *testing.T) {
413+
require.NoError(t, rdb.Set(ctx, "mykey", "\x00\xff\xf0", 0).Err())
414+
lower, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, -1, "byte").Int64()
415+
require.NoError(t, err)
416+
upper, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, -1, "BYTE").Int64()
417+
require.NoError(t, err)
418+
require.EqualValues(t, lower, upper)
419+
})
420+
421+
t.Run("BITPOS BIT vs BYTE gives different results for same range", func(t *testing.T) {
422+
require.NoError(t, rdb.Set(ctx, "mykey", "\x00\xff\xf0", 0).Err())
423+
bitResult := rdb.BitPosSpan(ctx, "mykey", 1, 0, 15, "bit")
424+
require.NoError(t, bitResult.Err())
425+
require.EqualValues(t, 8, bitResult.Val())
426+
byteResult := rdb.BitPosSpan(ctx, "mykey", 1, 0, 15, "byte")
427+
require.NoError(t, byteResult.Err())
428+
require.EqualValues(t, 8, byteResult.Val())
429+
bitResult2 := rdb.BitPosSpan(ctx, "mykey", 1, 0, 7, "bit")
430+
require.NoError(t, bitResult2.Err())
431+
require.EqualValues(t, -1, bitResult2.Val())
432+
byteResult2 := rdb.BitPosSpan(ctx, "mykey", 1, 0, 7, "byte")
433+
require.NoError(t, byteResult2.Err())
434+
require.EqualValues(t, 8, byteResult2.Val())
435+
})
436+
437+
t.Run("BITPOS rejects invalid option", func(t *testing.T) {
438+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff", 0).Err())
439+
err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, -1, "INVALID").Err()
440+
require.Error(t, err)
441+
})
442+
443+
t.Run("BITPOS rejects extra arguments after BYTE/BIT", func(t *testing.T) {
444+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff", 0).Err())
445+
err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, -1, "BIT", "extra").Err()
446+
require.Error(t, err)
447+
})
448+
403449
/* Test cases adapted from redis test cases : https://github.com/redis/redis/blob/unstable/tests/unit/bitops.tcl
404450
*/
405451
t.Run("BITPOS bit=0 with empty key returns 0", func(t *testing.T) {

0 commit comments

Comments
 (0)