Skip to content

Commit 35e5084

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 35e5084

2 files changed

Lines changed: 235 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: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,223 @@ 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+
449+
t.Run("BITPOS rejects BIT unit without end offset", func(t *testing.T) {
450+
require.NoError(t, rdb.Set(ctx, "mykey", "\x80", 0).Err())
451+
err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, "BIT").Err()
452+
require.ErrorContains(t, err, "not started as an integer")
453+
})
454+
455+
t.Run("BITPOS rejects BYTE unit without end offset", func(t *testing.T) {
456+
require.NoError(t, rdb.Set(ctx, "mykey", "\x80", 0).Err())
457+
err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, "BYTE").Err()
458+
require.ErrorContains(t, err, "not started as an integer")
459+
})
460+
461+
t.Run("BITPOS rejects non-integer bit argument", func(t *testing.T) {
462+
require.NoError(t, rdb.Set(ctx, "mykey", "\x80", 0).Err())
463+
err := rdb.Do(ctx, "BITPOS", "mykey", "x").Err()
464+
require.ErrorContains(t, err, "The bit argument must be 1 or 0")
465+
})
466+
467+
t.Run("BITPOS rejects non-integer bit argument with BIT unit", func(t *testing.T) {
468+
require.NoError(t, rdb.Set(ctx, "mykey", "\x80", 0).Err())
469+
err := rdb.Do(ctx, "BITPOS", "mykey", "x", 0, 0, "BIT").Err()
470+
require.ErrorContains(t, err, "The bit argument must be 1 or 0")
471+
})
472+
473+
t.Run("BITPOS rejects bit argument of 2", func(t *testing.T) {
474+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff", 0).Err())
475+
err := rdb.Do(ctx, "BITPOS", "mykey", 2).Err()
476+
require.ErrorContains(t, err, "The bit argument must be 1 or 0")
477+
})
478+
479+
t.Run("BITPOS rejects bit argument of -1", func(t *testing.T) {
480+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff", 0).Err())
481+
err := rdb.Do(ctx, "BITPOS", "mykey", -1).Err()
482+
require.ErrorContains(t, err, "The bit argument must be 1 or 0")
483+
})
484+
485+
t.Run("BITPOS rejects non-integer start offset", func(t *testing.T) {
486+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff", 0).Err())
487+
err := rdb.Do(ctx, "BITPOS", "mykey", 1, "abc").Err()
488+
require.ErrorContains(t, err, "not started as an integer")
489+
})
490+
491+
t.Run("BITPOS rejects non-integer end offset", func(t *testing.T) {
492+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff", 0).Err())
493+
err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, "abc").Err()
494+
require.ErrorContains(t, err, "not started as an integer")
495+
})
496+
497+
t.Run("BITPOS bit=1 with nonexistent key returns -1", func(t *testing.T) {
498+
require.NoError(t, rdb.Del(ctx, "nosuchkey").Err())
499+
val, err := rdb.Do(ctx, "BITPOS", "nosuchkey", 1).Int64()
500+
require.NoError(t, err)
501+
require.EqualValues(t, -1, val)
502+
})
503+
504+
t.Run("BITPOS bit=0 with nonexistent key returns 0", func(t *testing.T) {
505+
require.NoError(t, rdb.Del(ctx, "nosuchkey").Err())
506+
val, err := rdb.Do(ctx, "BITPOS", "nosuchkey", 0).Int64()
507+
require.NoError(t, err)
508+
require.EqualValues(t, 0, val)
509+
})
510+
511+
t.Run("BITPOS BYTE with negative start", func(t *testing.T) {
512+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\x00\xff", 0).Err())
513+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 0, -2, -1, "BYTE").Int64()
514+
require.NoError(t, err)
515+
require.EqualValues(t, 8, val)
516+
})
517+
518+
t.Run("BITPOS BIT with negative start and end", func(t *testing.T) {
519+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\x00\xff", 0).Err())
520+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 0, -16, -9, "BIT").Int64()
521+
require.NoError(t, err)
522+
require.EqualValues(t, 8, val)
523+
})
524+
525+
t.Run("BITPOS returns -1 when start > end after normalization", func(t *testing.T) {
526+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\x00\xff", 0).Err())
527+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 2, 1, "BYTE").Int64()
528+
require.NoError(t, err)
529+
require.EqualValues(t, -1, val)
530+
})
531+
532+
t.Run("BITPOS BIT returns -1 when start > end after normalization", func(t *testing.T) {
533+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\x00\xff", 0).Err())
534+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 16, 8, "BIT").Int64()
535+
require.NoError(t, err)
536+
require.EqualValues(t, -1, val)
537+
})
538+
539+
t.Run("BITPOS BYTE bit=0 with all-ones string and explicit end returns -1", func(t *testing.T) {
540+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\xff\xff", 0).Err())
541+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 0, 0, 2, "BYTE").Int64()
542+
require.NoError(t, err)
543+
require.EqualValues(t, -1, val)
544+
})
545+
546+
t.Run("BITPOS BIT bit=0 with all-ones string and explicit end returns -1", func(t *testing.T) {
547+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\xff\xff", 0).Err())
548+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 0, 0, 23, "BIT").Int64()
549+
require.NoError(t, err)
550+
require.EqualValues(t, -1, val)
551+
})
552+
553+
t.Run("BITPOS BYTE bit=0 without end extends past string (finds trailing zero)", func(t *testing.T) {
554+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\xff\xff", 0).Err())
555+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 0).Int64()
556+
require.NoError(t, err)
557+
require.EqualValues(t, 24, val)
558+
})
559+
560+
t.Run("BITPOS BYTE with end beyond string length clamps correctly", func(t *testing.T) {
561+
require.NoError(t, rdb.Set(ctx, "mykey", "\x00\xff\x00", 0).Err())
562+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, 100, "BYTE").Int64()
563+
require.NoError(t, err)
564+
require.EqualValues(t, 8, val)
565+
})
566+
567+
t.Run("BITPOS BIT with end beyond string length clamps correctly", func(t *testing.T) {
568+
require.NoError(t, rdb.Set(ctx, "mykey", "\x00\xff\x00", 0).Err())
569+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 0, 999, "BIT").Int64()
570+
require.NoError(t, err)
571+
require.EqualValues(t, 8, val)
572+
})
573+
574+
t.Run("BITPOS BYTE with only start argument", func(t *testing.T) {
575+
require.NoError(t, rdb.Set(ctx, "mykey", "\x00\x00\xff", 0).Err())
576+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 2).Int64()
577+
require.NoError(t, err)
578+
require.EqualValues(t, 16, val)
579+
})
580+
581+
t.Run("BITPOS BYTE with start past string returns -1 for bit=1", func(t *testing.T) {
582+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff", 0).Err())
583+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 5).Int64()
584+
require.NoError(t, err)
585+
require.EqualValues(t, -1, val)
586+
})
587+
588+
t.Run("BITPOS on wrong type returns WRONGTYPE error", func(t *testing.T) {
589+
require.NoError(t, rdb.Del(ctx, "mylist").Err())
590+
require.NoError(t, rdb.LPush(ctx, "mylist", "a").Err())
591+
err := rdb.Do(ctx, "BITPOS", "mylist", 1).Err()
592+
require.ErrorContains(t, err, "WRONGTYPE")
593+
})
594+
595+
t.Run("BITPOS BYTE bit=0 finds first zero in middle byte", func(t *testing.T) {
596+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\x0f\xff", 0).Err())
597+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 0, 0, 2, "BYTE").Int64()
598+
require.NoError(t, err)
599+
require.EqualValues(t, 8, val)
600+
})
601+
602+
t.Run("BITPOS BIT bit=0 finds first zero within bit range", func(t *testing.T) {
603+
require.NoError(t, rdb.Set(ctx, "mykey", "\xff\x0f\xff", 0).Err())
604+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 0, 8, 15, "BIT").Int64()
605+
require.NoError(t, err)
606+
require.EqualValues(t, 8, val)
607+
})
608+
609+
t.Run("BITPOS BIT single bit precision check", func(t *testing.T) {
610+
require.NoError(t, rdb.Set(ctx, "mykey", "\x00\x80", 0).Err())
611+
val, err := rdb.Do(ctx, "BITPOS", "mykey", 1, 8, 8, "BIT").Int64()
612+
require.NoError(t, err)
613+
require.EqualValues(t, 8, val)
614+
615+
val, err = rdb.Do(ctx, "BITPOS", "mykey", 1, 9, 15, "BIT").Int64()
616+
require.NoError(t, err)
617+
require.EqualValues(t, -1, val)
618+
})
619+
403620
/* Test cases adapted from redis test cases : https://github.com/redis/redis/blob/unstable/tests/unit/bitops.tcl
404621
*/
405622
t.Run("BITPOS bit=0 with empty key returns 0", func(t *testing.T) {

0 commit comments

Comments
 (0)