This is an automated email from the ASF dual-hosted git repository.
git-hulk 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 fbe066f65 fix(bitfield): support Redis positional '#N' offset syntax
(#3470)
fbe066f65 is described below
commit fbe066f65b603b9da4bd5b3fefe3810cb59dd193
Author: Nikhil K Tyagi <[email protected]>
AuthorDate: Wed May 6 07:26:36 2026 +0530
fix(bitfield): support Redis positional '#N' offset syntax (#3470)
Linked issue #3469
## Problem
`BITFIELD` commands with positional offset `#N` syntax failed in
kvrocks. Redis supports `#N` as shorthand for `N * bit_width`, so
`INCRBY u16 #0 1` means bit
offset `0 * 16 = 0`, `#1` means `16`, etc.
Example that worked in Redis but not kvrocks:
BITFIELD mykey OVERFLOW SAT INCRBY u16 #0 1
## Root Cause
`GetBitOffsetFromArgument` called `ParseInt<uint32_t>` directly on
offset string. `"#0"` fails integer parsing — `#` prefix was never
handled.
## Fix
In `CommandBitfield::Parse()`, after encoding is parsed (bit width
known), detect `#` prefix and expand: `offset = N * encoding.Bits()`.
Plain integer offsets
unchanged.
## Test
Added integration tests in
`tests/gocase/unit/type/bitmap/bitmap_test.go` covering:
- `SET`/`GET` with `#N` across multiple positions
- `INCRBY` with `#N`
- `OVERFLOW SAT INCRBY` with `#N` (original failing case)
- `BITFIELD_RO GET` with `#N`
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Vikram Alagh <[email protected]>
Co-authored-by: 纪华裕 <[email protected]>
Co-authored-by: Aleks Lozovyuk <[email protected]>
---
src/commands/cmd_bit.cc | 16 ++-
tests/gocase/unit/type/bitmap/bitmap_test.go | 141 +++++++++++++++++++++++++++
2 files changed, 155 insertions(+), 2 deletions(-)
diff --git a/src/commands/cmd_bit.cc b/src/commands/cmd_bit.cc
index 13be25f31..eb829d31c 100644
--- a/src/commands/cmd_bit.cc
+++ b/src/commands/cmd_bit.cc
@@ -18,6 +18,8 @@
*
*/
+#include <limits>
+
#include "commander.h"
#include "commands/command_parser.h"
#include "error_constants.h"
@@ -302,8 +304,18 @@ class CommandBitfield : public Commander {
}
cmd.encoding = encoding.GetValue();
- // parse offset
- if (!GetBitOffsetFromArgument(group[2], &cmd.offset).IsOK()) {
+ // parse offset — support Redis '#N' positional syntax: #N means N *
bit_width
+ if (!group[2].empty() && group[2][0] == '#') {
+ auto pos_parse = ParseInt<uint64_t>(group[2].substr(1), 10);
+ if (!pos_parse) {
+ return {Status::RedisParseErr, "bit offset is not an integer or out
of range"};
+ }
+ uint64_t offset64 = *pos_parse *
static_cast<uint64_t>(cmd.encoding.Bits());
+ if (offset64 > std::numeric_limits<uint32_t>::max()) {
+ return {Status::RedisParseErr, "bit offset is not an integer or out
of range"};
+ }
+ cmd.offset = static_cast<uint32_t>(offset64);
+ } else if (!GetBitOffsetFromArgument(group[2], &cmd.offset).IsOK()) {
return {Status::RedisParseErr, "bit offset is not an integer or out of
range"};
}
diff --git a/tests/gocase/unit/type/bitmap/bitmap_test.go
b/tests/gocase/unit/type/bitmap/bitmap_test.go
index 6211ae3c8..ede1a7d43 100644
--- a/tests/gocase/unit/type/bitmap/bitmap_test.go
+++ b/tests/gocase/unit/type/bitmap/bitmap_test.go
@@ -379,6 +379,147 @@ func TestBitmap(t *testing.T) {
require.ErrorContains(t, rdb.Do(ctx, "BITFIELD_RO", "str",
"INCRBY", "u8", "32", 2).Err(), "BITFIELD_RO only supports the GET subcommand")
})
+ t.Run("BITFIELD positional offset #N syntax", func(t *testing.T) {
+ require.NoError(t, rdb.Del(ctx, "bf_pos").Err())
+
+ // #0 with u16 = offset 0, #1 = offset 16, #2 = offset 32
+ res := rdb.Do(ctx, "BITFIELD", "bf_pos", "SET", "u16", "#0",
100)
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(0)}, res.Val())
+
+ res = rdb.Do(ctx, "BITFIELD", "bf_pos", "SET", "u16", "#1", 200)
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(0)}, res.Val())
+
+ res = rdb.Do(ctx, "BITFIELD", "bf_pos", "GET", "u16", "#0",
"GET", "u16", "#1")
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(100), int64(200)},
res.Val())
+
+ // INCRBY with #N
+ res = rdb.Do(ctx, "BITFIELD", "bf_pos", "INCRBY", "u16", "#0",
1)
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(101)}, res.Val())
+
+ // OVERFLOW SAT with #N
+ res = rdb.Do(ctx, "BITFIELD", "bf_pos", "OVERFLOW", "SAT",
"INCRBY", "u16", "#1", 65535)
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(65535)}, res.Val())
+
+ // BITFIELD_RO GET with #N
+ for _, command := range []string{"BITFIELD", "BITFIELD_RO"} {
+ res = rdb.Do(ctx, command, "bf_pos", "GET", "u16", "#0")
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(101)},
res.Val())
+ }
+ })
+
+ t.Run("BITFIELD positional offset #N invalid and boundary cases",
func(t *testing.T) {
+ require.NoError(t, rdb.Del(ctx, "bf_pos").Err())
+
+ // bare '#' with no number
+ util.ErrorRegexp(t, rdb.Do(ctx, "BITFIELD", "bf_pos", "SET",
"u16", "#", 1).Err(), ".*out of range.*")
+
+ // non-numeric after '#'
+ util.ErrorRegexp(t, rdb.Do(ctx, "BITFIELD", "bf_pos", "SET",
"u16", "#abc", 1).Err(), ".*out of range.*")
+
+ // negative index
+ util.ErrorRegexp(t, rdb.Do(ctx, "BITFIELD", "bf_pos", "SET",
"u16", "#-1", 1).Err(), ".*out of range.*")
+
+ // overflow: #268435456 * 16 bits = 4294967296 > UINT32_MAX
+ util.ErrorRegexp(t, rdb.Do(ctx, "BITFIELD", "bf_pos", "SET",
"u16", "#268435456", 1).Err(), ".*out of range.*")
+
+ // overflow with u8: #536870912 * 8 = 4294967296 > UINT32_MAX
+ util.ErrorRegexp(t, rdb.Do(ctx, "BITFIELD", "bf_pos", "SET",
"u8", "#536870912", 1).Err(), ".*out of range.*")
+
+ // overflow with u32: #134217728 * 32 = 4294967296 > UINT32_MAX
+ util.ErrorRegexp(t, rdb.Do(ctx, "BITFIELD", "bf_pos", "SET",
"u32", "#134217728", 1).Err(), ".*out of range.*")
+
+ // overflow with i64: #67108864 * 64 = 4294967296 > UINT32_MAX
+ util.ErrorRegexp(t, rdb.Do(ctx, "BITFIELD", "bf_pos", "SET",
"i64", "#67108864", 1).Err(), ".*out of range.*")
+
+ // just below overflow with u8: #536870911 * 8 = 4294967288 <=
UINT32_MAX — must not error
+ res := rdb.Do(ctx, "BITFIELD", "bf_pos", "SET", "u8", "#0", 255)
+ require.NoError(t, res.Err())
+ })
+
+ t.Run("BITFIELD positional offset #N with signed types", func(t
*testing.T) {
+ // i8: #0 = offset 0, #1 = offset 8
+ require.NoError(t, rdb.Del(ctx, "bf_i8").Err())
+ res := rdb.Do(ctx, "BITFIELD", "bf_i8", "SET", "i8", "#0", -10)
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(0)}, res.Val())
+
+ res = rdb.Do(ctx, "BITFIELD", "bf_i8", "SET", "i8", "#1", 42)
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(0)}, res.Val())
+
+ res = rdb.Do(ctx, "BITFIELD", "bf_i8", "GET", "i8", "#0",
"GET", "i8", "#1")
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(-10), int64(42)},
res.Val())
+
+ // INCRBY with signed i8 and #N
+ res = rdb.Do(ctx, "BITFIELD", "bf_i8", "INCRBY", "i8", "#1", -2)
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(40)}, res.Val())
+
+ // i32: #0 = offset 0, #1 = offset 32
+ require.NoError(t, rdb.Del(ctx, "bf_i32").Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_i32", "SET", "i32", "#0",
-100000)
+ require.NoError(t, res.Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_i32", "SET", "i32", "#1",
999999)
+ require.NoError(t, res.Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_i32", "GET", "i32", "#0",
"GET", "i32", "#1")
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(-100000),
int64(999999)}, res.Val())
+ })
+
+ t.Run("BITFIELD positional offset #N with various unsigned widths",
func(t *testing.T) {
+ require.NoError(t, rdb.Del(ctx, "bf_widths").Err())
+
+ // u8: #N * 8
+ res := rdb.Do(ctx, "BITFIELD", "bf_widths", "SET", "u8", "#0",
255)
+ require.NoError(t, res.Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_widths", "SET", "u8", "#1",
128)
+ require.NoError(t, res.Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_widths", "GET", "u8", "#0",
"GET", "u8", "#1")
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(255), int64(128)},
res.Val())
+
+ // u32: #N * 32
+ require.NoError(t, rdb.Del(ctx, "bf_widths").Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_widths", "SET", "u32", "#0",
1000000)
+ require.NoError(t, res.Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_widths", "SET", "u32", "#1",
2000000)
+ require.NoError(t, res.Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_widths", "GET", "u32", "#0",
"GET", "u32", "#1")
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(1000000),
int64(2000000)}, res.Val())
+
+ // u1: #N * 1 — single bit fields
+ require.NoError(t, rdb.Del(ctx, "bf_widths").Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_widths", "SET", "u1", "#0", 1)
+ require.NoError(t, res.Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_widths", "SET", "u1", "#1", 1)
+ require.NoError(t, res.Err())
+ res = rdb.Do(ctx, "BITFIELD", "bf_widths", "GET", "u1", "#0",
"GET", "u1", "#1")
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(1), int64(1)},
res.Val())
+ })
+
+ t.Run("BITFIELD positional offset #N mixed with absolute offset",
func(t *testing.T) {
+ require.NoError(t, rdb.Del(ctx, "bf_mix").Err())
+
+ // mix #N positional and absolute offset in same command
+ res := rdb.Do(ctx, "BITFIELD", "bf_mix", "SET", "u8", "#0", 10,
"SET", "u8", "8", 20)
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(0), int64(0)},
res.Val())
+
+ // #0 = offset 0, absolute 8 = offset 8 — should be same as #1
+ res = rdb.Do(ctx, "BITFIELD", "bf_mix", "GET", "u8", "#0",
"GET", "u8", "#1")
+ require.NoError(t, res.Err())
+ require.EqualValues(t, []interface{}{int64(10), int64(20)},
res.Val())
+ })
+
t.Run("BITPOS BIT option check", func(t *testing.T) {
require.NoError(t, rdb.Set(ctx, "mykey", "\x00\xff\xf0",
0).Err())
cmd := rdb.BitPosSpan(ctx, "mykey", 1, 7, 15, "bit")