feat(command): Add AR* (array data structure) commands (#3813)

* feat: impl array data structure

* add integration tests for array commands

Co-Authored-By: Claude Opus 4.7 <[email protected]>

* fix: move CmdTypeUint and CmdTypeUintSlice to end of iota sequence

Co-Authored-By: Claude Opus 4.7 <[email protected]>

* test: add boundary tests for large uint64 index values

Covers ARSet, ARGet, ARGetRange, ARMSet, ARMGet, ARDel,
ARDelRange, ARScan, ARSeek, ARNext, and AROp.

Co-Authored-By: Claude Opus 4.7 <[email protected]>

* refactor: replace generic AROp with typed methods and improve test coverage

- Split AROp into specific methods: AROpSum, AROpMin, AROpMax, AROpAnd,
  AROpOr, AROpXor, AROpMatch, AROpUsed with proper return types
- Pre-allocate slices in ARScan, ARGrep, ARGrepWithValues, ARLastItems
- Add tests: ARInfoFull, negative AND/OR/XOR results, ARGrep nil args

Co-Authored-By: Claude Opus 4.7 <[email protected]>

* mark ArrayCmdable API as experimental

Co-Authored-By: Claude Opus 4.7 <[email protected]>

* Use `any` instead of `interface{}` and document index range constraint on ArrayCmdable interface.

Co-Authored-By: Claude Opus 4.7 <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 <[email protected]>
This commit is contained in:
cxljs
2026-05-21 17:14:57 +03:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 14a9a85363
commit bfd1922200
4 changed files with 2776 additions and 0 deletions
+387
View File
@@ -0,0 +1,387 @@
package redis
import (
"context"
)
// note: the APIs is experimental and may be subject to change.
//
// ArrayCmdable defines the interface for Redis Array data structure commands
// available in Redis 8.8.0+.
//
// Redis array supports index range [0, math.MaxUint64-1), so index parameters use uint64.
type ArrayCmdable interface {
ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd
ARGet(ctx context.Context, key string, index uint64) *StringCmd
ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd
ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd
ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd
ARInsert(ctx context.Context, key string, values ...string) *UintCmd
ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd
ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd
ARLen(ctx context.Context, key string) *UintCmd
ARCount(ctx context.Context, key string) *UintCmd
ARNext(ctx context.Context, key string) *UintCmd
ARSeek(ctx context.Context, key string, index uint64) *IntCmd
ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd
ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd
ARScan(ctx context.Context, key string, start, end uint64, args *ARScanArgs) *AREntrySliceCmd
AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd
AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd
AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd
AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd
AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd
AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd
AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd
AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd
ARGrep(ctx context.Context, key string, start, end string, args *ARGrepArgs) *UintSliceCmd
ARGrepWithValues(ctx context.Context, key string, start, end string, args *ARGrepArgs) *AREntrySliceCmd
ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd
ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd
}
// AREntry represents an index-value pair for ARMSET.
type AREntry struct {
Index uint64
Value string
}
// ARRange represents a start-end range for ARDELRANGE.
type ARRange struct {
Start uint64
End uint64
}
// ARScanArgs contains optional arguments for ARSCAN.
type ARScanArgs struct {
Limit uint64
}
// ARGrepPredicateType defines the type of predicate for ARGREP.
type ARGrepPredicateType string
const (
ARGrepExact ARGrepPredicateType = "EXACT"
ARGrepMatch ARGrepPredicateType = "MATCH"
ARGrepGlob ARGrepPredicateType = "GLOB"
ARGrepRegex ARGrepPredicateType = "RE"
)
// ARGrepPredicate represents a search predicate for ARGREP.
type ARGrepPredicate struct {
Type ARGrepPredicateType
Value string
}
// ARGrepArgs contains optional arguments for ARGREP.
// Redis ARGREP defaults to OR when multiple predicates are given.
// Set CombineAnd to true to combine predicates with AND instead.
type ARGrepArgs struct {
Predicates []ARGrepPredicate
CombineAnd bool
Limit uint64
NoCase bool
}
// ARSet sets one or more contiguous values starting at an index in an array.
// Returns the number of new slots that were set (previously empty).
func (c cmdable) ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd {
args := make([]any, 3, 3+len(values))
args[0] = "arset"
args[1] = key
args[2] = index
for _, v := range values {
args = append(args, v)
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARGet gets the value at an index in an array.
// Returns redis.Nil if the key or index does not exist.
func (c cmdable) ARGet(ctx context.Context, key string, index uint64) *StringCmd {
cmd := NewStringCmd(ctx, "arget", key, index)
_ = c(ctx, cmd)
return cmd
}
// ARGetRange gets values in a range of indexes.
// Returns values in the range, with nil for unset indexes.
func (c cmdable) ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd {
cmd := NewSliceCmd(ctx, "argetrange", key, start, end)
_ = c(ctx, cmd)
return cmd
}
// ARMGet gets values at multiple indexes in an array.
// Returns values at the specified indexes, with nil for unset indexes.
func (c cmdable) ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd {
args := make([]any, 2+len(indexes))
args[0] = "armget"
args[1] = key
for i, idx := range indexes {
args[2+i] = idx
}
cmd := NewSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARMSet sets multiple index-value pairs in an array.
// Returns the number of new slots that were set (previously empty).
func (c cmdable) ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd {
args := make([]any, 2, 2+2*len(members))
args[0] = "armset"
args[1] = key
for _, m := range members {
args = append(args, m.Index, m.Value)
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARInsert inserts one or more values at consecutive indexes.
// Returns the last index where a value was inserted.
func (c cmdable) ARInsert(ctx context.Context, key string, values ...string) *UintCmd {
args := make([]any, 2, 2+len(values))
args[0] = "arinsert"
args[1] = key
for _, v := range values {
args = append(args, v)
}
cmd := NewUintCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARDel deletes elements at the specified indexes in an array.
// Returns the number of elements deleted.
func (c cmdable) ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd {
args := make([]any, 2+len(indexes))
args[0] = "ardel"
args[1] = key
for i, idx := range indexes {
args[2+i] = idx
}
cmd := NewIntCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARDelRange deletes elements in one or more ranges.
// Returns the number of elements deleted.
func (c cmdable) ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd {
args := make([]any, 2, 2+2*len(ranges))
args[0] = "ardelrange"
args[1] = key
for _, r := range ranges {
args = append(args, r.Start, r.End)
}
cmd := NewUintCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARLen returns the length of an array (max index + 1).
// Returns 0 if the key does not exist.
func (c cmdable) ARLen(ctx context.Context, key string) *UintCmd {
cmd := NewUintCmd(ctx, "arlen", key)
_ = c(ctx, cmd)
return cmd
}
// ARCount returns the number of non-empty elements in an array.
// Returns 0 if the key does not exist.
func (c cmdable) ARCount(ctx context.Context, key string) *UintCmd {
cmd := NewUintCmd(ctx, "arcount", key)
_ = c(ctx, cmd)
return cmd
}
// ARNext returns the next index ARINSERT would use.
// Returns 0 for missing keys or when no insert happened yet.
// Returns nil when the insertion cursor is exhausted / would overflow.
func (c cmdable) ARNext(ctx context.Context, key string) *UintCmd {
cmd := NewUintCmd(ctx, "arnext", key)
_ = c(ctx, cmd)
return cmd
}
// ARSeek sets the ARINSERT / ARRING cursor to a specific index.
// Returns 1 if the cursor was set, 0 if the key does not exist.
func (c cmdable) ARSeek(ctx context.Context, key string, index uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arseek", key, index)
_ = c(ctx, cmd)
return cmd
}
// ARInfo returns metadata about an array.
func (c cmdable) ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd {
cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key)
_ = c(ctx, cmd)
return cmd
}
// ARInfoFull returns detailed metadata about an array including slice statistics.
func (c cmdable) ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd {
cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key, "full")
_ = c(ctx, cmd)
return cmd
}
// ARScan iterates existing elements in a range, returning index-value pairs.
func (c cmdable) ARScan(ctx context.Context, key string, start, end uint64, scanArgs *ARScanArgs) *AREntrySliceCmd {
args := make([]any, 4, 6)
args[0], args[1], args[2], args[3] = "arscan", key, start, end
if scanArgs != nil && scanArgs.Limit > 0 {
args = append(args, "limit", scanArgs.Limit)
}
cmd := NewAREntrySliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// AROpSum returns the sum of numeric elements in a range.
func (c cmdable) AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd {
cmd := NewStringCmd(ctx, "arop", key, start, end, "SUM")
_ = c(ctx, cmd)
return cmd
}
// AROpMin returns the minimum numeric element in a range.
func (c cmdable) AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd {
cmd := NewStringCmd(ctx, "arop", key, start, end, "MIN")
_ = c(ctx, cmd)
return cmd
}
// AROpMax returns the maximum numeric element in a range.
func (c cmdable) AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd {
cmd := NewStringCmd(ctx, "arop", key, start, end, "MAX")
_ = c(ctx, cmd)
return cmd
}
// AROpAnd returns the bitwise AND of integer elements in a range.
func (c cmdable) AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "AND")
_ = c(ctx, cmd)
return cmd
}
// AROpOr returns the bitwise OR of integer elements in a range.
func (c cmdable) AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "OR")
_ = c(ctx, cmd)
return cmd
}
// AROpXor returns the bitwise XOR of integer elements in a range.
func (c cmdable) AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "XOR")
_ = c(ctx, cmd)
return cmd
}
// AROpMatch returns the count of elements matching a target string in a range.
func (c cmdable) AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "MATCH", value)
_ = c(ctx, cmd)
return cmd
}
// AROpUsed returns the count of non-empty slots in a range.
func (c cmdable) AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd {
cmd := NewIntCmd(ctx, "arop", key, start, end, "USED")
_ = c(ctx, cmd)
return cmd
}
// ARGrep searches array elements in a range using textual predicates.
// Returns matching indexes only. Use ARGrepWithValues to also get the values.
func (c cmdable) ARGrep(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *UintSliceCmd {
args := make([]any, 4, 4+grepArgs.Len())
args[0], args[1], args[2], args[3] = "argrep", key, start, end
args = grepArgs.Append(args)
cmd := NewUintSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARGrepWithValues searches array elements in a range using textual predicates.
// Returns matching indexes and their values as index-value pairs.
func (c cmdable) ARGrepWithValues(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *AREntrySliceCmd {
args := make([]any, 4, 5+grepArgs.Len())
args[0], args[1], args[2], args[3] = "argrep", key, start, end
args = grepArgs.Append(args)
args = append(args, "withvalues")
cmd := NewAREntrySliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
func (args *ARGrepArgs) Len() int {
if args == nil {
return 0
}
n := 2 * len(args.Predicates)
if args.CombineAnd {
n++
}
if args.Limit > 0 {
n += 2
}
if args.NoCase {
n++
}
return n
}
func (args *ARGrepArgs) Append(a []any) []any {
if args == nil {
return a
}
for _, p := range args.Predicates {
a = append(a, string(p.Type), p.Value)
}
if args.CombineAnd {
a = append(a, "and")
}
if args.Limit > 0 {
a = append(a, "limit", args.Limit)
}
if args.NoCase {
a = append(a, "nocase")
}
return a
}
// ARRing inserts values into a ring buffer of specified size, wrapping and truncating as needed.
// Returns the last index where a value was inserted.
func (c cmdable) ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd {
args := make([]any, 3, 3+len(values))
args[0] = "arring"
args[1] = key
args[2] = size
for _, v := range values {
args = append(args, v)
}
cmd := NewUintCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
// ARLastItems returns the most recently inserted elements.
// When rev is true, returns items in reverse order.
func (c cmdable) ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd {
args := make([]any, 3, 4)
args[0], args[1], args[2] = "arlastitems", key, count
if rev {
args = append(args, "rev")
}
cmd := NewSliceCmd(ctx, args...)
_ = c(ctx, cmd)
return cmd
}
File diff suppressed because it is too large Load Diff
+207
View File
@@ -158,6 +158,9 @@ const (
CmdTypeHotKeys
CmdTypeIncrEXInt
CmdTypeIncrEXFloat
CmdTypeUint
CmdTypeUintSlice
CmdTypeAREntrySlice
)
type (
@@ -1046,6 +1049,52 @@ func (cmd *IntCmd) Clone() Cmder {
}
}
type UintCmd struct {
baseCmd
val uint64
}
var _ Cmder = (*UintCmd)(nil)
func NewUintCmd(ctx context.Context, args ...any) *UintCmd {
return &UintCmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
cmdType: CmdTypeUint,
},
}
}
func (cmd *UintCmd) SetVal(val uint64) {
cmd.val = val
}
func (cmd *UintCmd) Val() uint64 {
return cmd.val
}
func (cmd *UintCmd) Result() (uint64, error) {
return cmd.val, cmd.err
}
func (cmd *UintCmd) String() string {
return cmdString(cmd, cmd.val)
}
func (cmd *UintCmd) readReply(rd *proto.Reader) (err error) {
cmd.val, err = rd.ReadUint()
return err
}
func (cmd *UintCmd) Clone() Cmder {
return &UintCmd{
baseCmd: cmd.cloneBaseCmd(),
val: cmd.val,
}
}
//------------------------------------------------------------------------------
// DigestCmd is a command that returns a uint64 xxh3 hash digest.
@@ -1177,6 +1226,66 @@ func (cmd *IntSliceCmd) Clone() Cmder {
}
}
type UintSliceCmd struct {
baseCmd
val []uint64
}
var _ Cmder = (*UintSliceCmd)(nil)
func NewUintSliceCmd(ctx context.Context, args ...any) *UintSliceCmd {
return &UintSliceCmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
cmdType: CmdTypeUintSlice,
},
}
}
func (cmd *UintSliceCmd) SetVal(val []uint64) {
cmd.val = val
}
func (cmd *UintSliceCmd) Val() []uint64 {
return cmd.val
}
func (cmd *UintSliceCmd) Result() ([]uint64, error) {
return cmd.val, cmd.err
}
func (cmd *UintSliceCmd) String() string {
return cmdString(cmd, cmd.val)
}
func (cmd *UintSliceCmd) readReply(rd *proto.Reader) error {
n, err := rd.ReadArrayLen()
if err != nil {
return err
}
cmd.val = make([]uint64, n)
for i := range cmd.val {
if cmd.val[i], err = rd.ReadUint(); err != nil {
return err
}
}
return nil
}
func (cmd *UintSliceCmd) Clone() Cmder {
var val []uint64
if cmd.val != nil {
val = make([]uint64, len(cmd.val))
copy(val, cmd.val)
}
return &UintSliceCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
//------------------------------------------------------------------------------
type DurationCmd struct {
@@ -8070,6 +8179,13 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) {
}); ok {
return intCmd.Val(), intCmd.Err()
}
case CmdTypeUint:
if uintCmd, ok := cmd.(interface {
Val() uint64
Err() error
}); ok {
return uintCmd.Val(), uintCmd.Err()
}
case CmdTypeBool:
if boolCmd, ok := cmd.(interface {
Val() bool
@@ -8496,6 +8612,13 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) {
}); ok {
return intSliceCmd.Val(), intSliceCmd.Err()
}
case CmdTypeUintSlice:
if uintSliceCmd, ok := cmd.(interface {
Val() []uint64
Err() error
}); ok {
return uintSliceCmd.Val(), uintSliceCmd.Err()
}
case CmdTypeBoolSlice:
if boolSliceCmd, ok := cmd.(interface {
Val() []bool
@@ -8524,6 +8647,13 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) {
}); ok {
return keyValueSliceCmd.Val(), keyValueSliceCmd.Err()
}
case CmdTypeAREntrySlice:
if arEntrySliceCmd, ok := cmd.(interface {
Val() []AREntry
Err() error
}); ok {
return arEntrySliceCmd.Val(), arEntrySliceCmd.Err()
}
case CmdTypeMapStringString:
if mapCmd, ok := cmd.(interface {
Val() map[string]string
@@ -8688,3 +8818,80 @@ func (cmd *IncrEXFloatCmd) Clone() Cmder {
val: cmd.val,
}
}
//------------------------------------------------------------------------------
// AREntrySliceCmd is a command that returns index-value pairs from ARSCAN or ARGREP.
type AREntrySliceCmd struct {
baseCmd
val []AREntry
}
var _ Cmder = (*AREntrySliceCmd)(nil)
func NewAREntrySliceCmd(ctx context.Context, args ...any) *AREntrySliceCmd {
return &AREntrySliceCmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
cmdType: CmdTypeAREntrySlice,
},
}
}
func (cmd *AREntrySliceCmd) SetVal(val []AREntry) {
cmd.val = val
}
func (cmd *AREntrySliceCmd) Val() []AREntry {
return cmd.val
}
func (cmd *AREntrySliceCmd) Result() ([]AREntry, error) {
return cmd.val, cmd.err
}
func (cmd *AREntrySliceCmd) String() string {
return cmdString(cmd, cmd.val)
}
func (cmd *AREntrySliceCmd) readReply(rd *proto.Reader) error {
n, err := rd.ReadArrayLen()
if err != nil {
return err
}
if n == 0 {
cmd.val = make([]AREntry, 0)
return nil
}
cmd.val = make([]AREntry, n)
for i := range n {
if err = rd.ReadFixedArrayLen(2); err != nil {
return err
}
cmd.val[i].Index, err = rd.ReadUint()
if err != nil {
return err
}
cmd.val[i].Value, err = rd.ReadString()
if err != nil {
return err
}
}
return nil
}
func (cmd *AREntrySliceCmd) Clone() Cmder {
var val []AREntry
if cmd.val != nil {
val = make([]AREntry, len(cmd.val))
copy(val, cmd.val)
}
return &AREntrySliceCmd{
baseCmd: cmd.cloneBaseCmd(),
val: val,
}
}
+1
View File
@@ -228,6 +228,7 @@ type Cmdable interface {
ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd
ACLCmdable
ArrayCmdable
BitMapCmdable
ClusterCmdable
GenericCmdable