From bfd192220031bf973eb7d05ee2d2c542e7b2d583 Mon Sep 17 00:00:00 2001 From: cxljs Date: Thu, 21 May 2026 22:14:57 +0800 Subject: [PATCH] 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 * fix: move CmdTypeUint and CmdTypeUintSlice to end of iota sequence Co-Authored-By: Claude Opus 4.7 * 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 * 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 * mark ArrayCmdable API as experimental Co-Authored-By: Claude Opus 4.7 * Use `any` instead of `interface{}` and document index range constraint on ArrayCmdable interface. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 --- array_commands.go | 387 +++++++ array_commands_test.go | 2181 ++++++++++++++++++++++++++++++++++++++++ command.go | 207 ++++ commands.go | 1 + 4 files changed, 2776 insertions(+) create mode 100644 array_commands.go create mode 100644 array_commands_test.go diff --git a/array_commands.go b/array_commands.go new file mode 100644 index 00000000..71037dfd --- /dev/null +++ b/array_commands.go @@ -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 +} diff --git a/array_commands_test.go b/array_commands_test.go new file mode 100644 index 00000000..bc126aa9 --- /dev/null +++ b/array_commands_test.go @@ -0,0 +1,2181 @@ +package redis_test + +import ( + "context" + "fmt" + "math" + "strings" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" + + "github.com/redis/go-redis/v9" +) + +var _ = Describe("Array Commands", Label("array"), func() { + ctx := context.TODO() + var client *redis.Client + + BeforeEach(func() { + SkipBeforeRedisVersion(8.8, "Redis 8.8.0 introduces support for Array") + client = redis.NewClient(redisOptions()) + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + Describe("ARSet and ARGet", func() { + It("should ARSet and ARGet basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + n, err := client.ARSet(ctx, "myarray", 0, "hello").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("hello")) + + _, err = client.ARGet(ctx, "myarray", 1).Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should ARSet overwrite existing value", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + n, err := client.ARSet(ctx, "myarray", 0, "hello").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + n, err = client.ARSet(ctx, "myarray", 0, "world").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(0))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("world")) + }) + + It("should ARGet non-existing key", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + _, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should ARGet validate index on non-existing key", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + err := client.ARGet(ctx, "myarray", 0).Err() + // Non-existing key returns redis.Nil, not an index error + Expect(err).To(Equal(redis.Nil)) + }) + + It("should ARSet and ARGet with integer values", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 0, "12345").Err()).NotTo(HaveOccurred()) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("12345")) + }) + + It("should ARSet and ARGet with float values", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 0, "3.14159").Err()).NotTo(HaveOccurred()) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("3.14159")) + }) + + It("should ARSet and ARGet with small strings", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 0, "abc").Err()).NotTo(HaveOccurred()) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("abc")) + }) + + It("should ARSet and ARGet with large string", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + longstr := strings.Repeat("x", 100) + Expect(client.ARSet(ctx, "myarray", 0, longstr).Err()).NotTo(HaveOccurred()) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(longstr)) + }) + + It("should ARSet and ARGet with empty string", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 0, "").Err()).NotTo(HaveOccurred()) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("")) + }) + }) + + Describe("ARLen and ARCount", func() { + It("should ARLen and ARCount basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + length, err := client.ARLen(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(length).To(Equal(uint64(0))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(0))) + + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + length, err = client.ARLen(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(length).To(Equal(uint64(1))) + count, err = client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(1))) + + Expect(client.ARSet(ctx, "myarray", 5, "b").Err()).NotTo(HaveOccurred()) + length, err = client.ARLen(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(length).To(Equal(uint64(6))) + count, err = client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(2))) + + Expect(client.ARSet(ctx, "myarray", 100, "c").Err()).NotTo(HaveOccurred()) + length, err = client.ARLen(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(length).To(Equal(uint64(101))) + count, err = client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(3))) + }) + }) + + Describe("ARDel", func() { + It("should ARDel basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 1, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 2, "c").Err()).NotTo(HaveOccurred()) + + n, err := client.ARDel(ctx, "myarray", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + _, err = client.ARGet(ctx, "myarray", 1).Result() + Expect(err).To(Equal(redis.Nil)) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(2))) + + // Delete non-existing index returns 0 + n, err = client.ARDel(ctx, "myarray", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(0))) + }) + + It("should ARDel multiple indices", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 1, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 2, "c").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 3, "d").Err()).NotTo(HaveOccurred()) + + n, err := client.ARDel(ctx, "myarray", 0, 1, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(3))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(1))) + }) + + It("should ARDel last element delete key", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + + _, err := client.ARDel(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + + exists, err := client.Exists(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(Equal(int64(0))) + }) + }) + + Describe("ARDelRange", func() { + It("should ARDelRange basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + for i := 0; i < 10; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("%d", i*10)).Err()).NotTo(HaveOccurred()) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(10))) + + n, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 2, End: 6}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(5))) + + count, err = client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(5))) + }) + + It("should ARDelRange reverse order", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + for i := 0; i < 10; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("%d", i*10)).Err()).NotTo(HaveOccurred()) + } + + n, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 6, End: 2}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(5))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(5))) + }) + + It("should ARDelRange with multiple ranges in single call", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + for i := 0; i < 20; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("val%d", i)).Err()).NotTo(HaveOccurred()) + } + + n, err := client.ARDelRange(ctx, "myarray", + redis.ARRange{Start: 2, End: 4}, + redis.ARRange{Start: 10, End: 14}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(8))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(12))) + + // Verify correct elements deleted + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("val0")) + + _, err = client.ARGet(ctx, "myarray", 2).Result() + Expect(err).To(Equal(redis.Nil)) + + _, err = client.ARGet(ctx, "myarray", 4).Result() + Expect(err).To(Equal(redis.Nil)) + + val, err = client.ARGet(ctx, "myarray", 15).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("val15")) + }) + }) + + Describe("ARMSet and ARMGet", func() { + It("should ARMSet basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + n, err := client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "a"}, + redis.AREntry{Index: 1, Value: "b"}, + redis.AREntry{Index: 2, Value: "c"}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(3))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("a")) + + val, err = client.ARGet(ctx, "myarray", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("b")) + + val, err = client.ARGet(ctx, "myarray", 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("c")) + }) + + It("should ARMSet return only newly filled slots", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + + n, err := client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "aa"}, + redis.AREntry{Index: 1, Value: "b"}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("aa")) + + val, err = client.ARGet(ctx, "myarray", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("b")) + }) + + It("should ARMGet basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 1, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 5, "c").Err()).NotTo(HaveOccurred()) + + result, err := client.ARMGet(ctx, "myarray", 0, 1, 5, 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(4)) + Expect(result[0]).To(Equal("a")) + Expect(result[1]).To(Equal("b")) + Expect(result[2]).To(Equal("c")) + Expect(result[3]).To(BeNil()) + }) + }) + + Describe("ARGetRange and contiguous ARSet", func() { + It("should ARGetRange basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "a"}, + redis.AREntry{Index: 1, Value: "b"}, + redis.AREntry{Index: 2, Value: "c"}, + redis.AREntry{Index: 3, Value: "d"}, + redis.AREntry{Index: 4, Value: "e"}, + ).Err()).NotTo(HaveOccurred()) + + result, err := client.ARGetRange(ctx, "myarray", 1, 3).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"b", "c", "d"})) + }) + + It("should ARGetRange reverse", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "a"}, + redis.AREntry{Index: 1, Value: "b"}, + redis.AREntry{Index: 2, Value: "c"}, + redis.AREntry{Index: 3, Value: "d"}, + redis.AREntry{Index: 4, Value: "e"}, + ).Err()).NotTo(HaveOccurred()) + + result, err := client.ARGetRange(ctx, "myarray", 3, 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"d", "c", "b"})) + }) + + It("should ARGetRange error when range exceeds hard limit", func() { + err := client.ARGetRange(ctx, "myarray", 0, 1000000).Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("range exceeds maximum")) + }) + + It("should ARGetRange reverse error when range exceeds hard limit", func() { + err := client.ARGetRange(ctx, "myarray", 1000000, 0).Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("range exceeds maximum")) + }) + + It("should ARSet contiguous write basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + n, err := client.ARSet(ctx, "myarray", 0, "a", "b", "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(3))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("a")) + + val, err = client.ARGet(ctx, "myarray", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("b")) + + val, err = client.ARGet(ctx, "myarray", 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("c")) + }) + }) + + Describe("ARScan", func() { + It("should ARScan return only existing elements with indices", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 5, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 9, "c").Err()).NotTo(HaveOccurred()) + + result, err := client.ARScan(ctx, "myarray", 0, 10, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(3)) + Expect(result[0]).To(Equal(redis.AREntry{Index: 0, Value: "a"})) + Expect(result[1]).To(Equal(redis.AREntry{Index: 5, Value: "b"})) + Expect(result[2]).To(Equal(redis.AREntry{Index: 9, Value: "c"})) + }) + + It("should ARScan on empty range return empty array", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 500, "x").Err()).NotTo(HaveOccurred()) + + result, err := client.ARScan(ctx, "myarray", 0, 100, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEmpty()) + }) + + It("should ARScan reversed range", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 5, "b").Err()).NotTo(HaveOccurred()) + + result, err := client.ARScan(ctx, "myarray", 5, 0, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(2)) + Expect(result[0]).To(Equal(redis.AREntry{Index: 5, Value: "b"})) + Expect(result[1]).To(Equal(redis.AREntry{Index: 0, Value: "a"})) + }) + + It("should ARScan on non-existent key return empty array", func() { + Expect(client.Del(ctx, "nokey").Err()).NotTo(HaveOccurred()) + + result, err := client.ARScan(ctx, "nokey", 0, 100, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEmpty()) + }) + + It("should ARScan with mixed value types", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "string").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 1, "12345").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 2, "3.14").Err()).NotTo(HaveOccurred()) + + result, err := client.ARScan(ctx, "myarray", 0, 10, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(3)) + Expect(result[0]).To(Equal(redis.AREntry{Index: 0, Value: "string"})) + Expect(result[1]).To(Equal(redis.AREntry{Index: 1, Value: "12345"})) + Expect(result[2]).To(Equal(redis.AREntry{Index: 2, Value: "3.14"})) + }) + + It("should ARScan with LIMIT", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + for i := 0; i < 20; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Delete some in the middle + _, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 5, End: 14}).Result() + Expect(err).NotTo(HaveOccurred()) + + result, err := client.ARScan(ctx, "myarray", 0, 100, &redis.ARScanArgs{Limit: 3}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(3)) + Expect(result[0]).To(Equal(redis.AREntry{Index: 0, Value: "0"})) + Expect(result[1]).To(Equal(redis.AREntry{Index: 1, Value: "1"})) + Expect(result[2]).To(Equal(redis.AREntry{Index: 2, Value: "2"})) + }) + }) + + Describe("ARInsert", func() { + It("should ARInsert basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + idx, err := client.ARInsert(ctx, "myarray", "a").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(idx).To(Equal(uint64(0))) + + idx, err = client.ARInsert(ctx, "myarray", "b").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(idx).To(Equal(uint64(1))) + + idx, err = client.ARInsert(ctx, "myarray", "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(idx).To(Equal(uint64(2))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("a")) + + val, err = client.ARGet(ctx, "myarray", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("b")) + + val, err = client.ARGet(ctx, "myarray", 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("c")) + }) + + It("should ARInsert with multiple values", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + idx, err := client.ARInsert(ctx, "myarray", "x", "y", "z").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(idx).To(Equal(uint64(2))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("x")) + + val, err = client.ARGet(ctx, "myarray", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("y")) + + val, err = client.ARGet(ctx, "myarray", 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("z")) + }) + }) + + Describe("ARGrep", func() { + It("should ARGREP MATCH return matching indexes", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "alpha"}, + redis.AREntry{Index: 1, Value: "beta"}, + redis.AREntry{Index: 2, Value: "alphabet"}, + redis.AREntry{Index: 5, Value: "gamma"}, + ).Err()).NotTo(HaveOccurred()) + + result, err := client.ARGrep(ctx, "myarray", "-", "+", &redis.ARGrepArgs{ + Predicates: []redis.ARGrepPredicate{ + {Type: redis.ARGrepMatch, Value: "alpha"}, + }, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]uint64{0, 2})) + }) + + It("should ARGREP support WITHVALUES and reverse ranges", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "alpha"}, + redis.AREntry{Index: 1, Value: "beta"}, + redis.AREntry{Index: 2, Value: "alphabet"}, + redis.AREntry{Index: 3, Value: "delta"}, + ).Err()).NotTo(HaveOccurred()) + + result, err := client.ARGrepWithValues(ctx, "myarray", "3", "0", &redis.ARGrepArgs{ + Predicates: []redis.ARGrepPredicate{ + {Type: redis.ARGrepMatch, Value: "alpha"}, + }, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(2)) + Expect(result[0]).To(Equal(redis.AREntry{Index: 2, Value: "alphabet"})) + Expect(result[1]).To(Equal(redis.AREntry{Index: 0, Value: "alpha"})) + }) + + It("should ARGREP support AND, GLOB, and NOCASE", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "RedisArray"}, + redis.AREntry{Index: 1, Value: "redis-match"}, + redis.AREntry{Index: 2, Value: "array-only"}, + redis.AREntry{Index: 3, Value: "plain"}, + ).Err()).NotTo(HaveOccurred()) + + result, err := client.ARGrep(ctx, "myarray", "-", "+", &redis.ARGrepArgs{ + Predicates: []redis.ARGrepPredicate{ + {Type: redis.ARGrepMatch, Value: "redis"}, + {Type: redis.ARGrepGlob, Value: "*array*"}, + }, + CombineAnd: true, + NoCase: true, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]uint64{0})) + }) + + It("should ARGREP support RE predicates", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "foo123"}, + redis.AREntry{Index: 1, Value: "bar"}, + redis.AREntry{Index: 2, Value: "zoo999"}, + redis.AREntry{Index: 3, Value: "Foo777"}, + ).Err()).NotTo(HaveOccurred()) + + result, err := client.ARGrep(ctx, "myarray", "-", "+", &redis.ARGrepArgs{ + Predicates: []redis.ARGrepPredicate{ + {Type: redis.ARGrepRegex, Value: `^.*[0-9]{3}$`}, + }, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]uint64{0, 2, 3})) + + result, err = client.ARGrep(ctx, "myarray", "-", "+", &redis.ARGrepArgs{ + Predicates: []redis.ARGrepPredicate{ + {Type: redis.ARGrepRegex, Value: `^foo[0-9]+$`}, + }, + NoCase: true, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]uint64{0, 3})) + }) + + It("should ARGREP LIMIT stop after enough matches", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "hit-1"}, + redis.AREntry{Index: 1, Value: "hit-2"}, + redis.AREntry{Index: 2, Value: "miss"}, + redis.AREntry{Index: 3, Value: "hit-3"}, + ).Err()).NotTo(HaveOccurred()) + + result, err := client.ARGrep(ctx, "myarray", "-", "+", &redis.ARGrepArgs{ + Predicates: []redis.ARGrepPredicate{ + {Type: redis.ARGrepMatch, Value: "hit"}, + }, + Limit: 2, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]uint64{0, 1})) + }) + + It("should ARGREP handle missing keys", func() { + Expect(client.Del(ctx, "nokey").Err()).NotTo(HaveOccurred()) + + result, err := client.ARGrep(ctx, "nokey", "-", "+", &redis.ARGrepArgs{ + Predicates: []redis.ARGrepPredicate{ + {Type: redis.ARGrepMatch, Value: "foo"}, + }, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEmpty()) + }) + + It("should ARGREP not panic with nil args", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "alpha"}, + redis.AREntry{Index: 1, Value: "beta"}, + ).Err()).NotTo(HaveOccurred()) + + // nil args should not panic, just send bare ARGREP without predicates + Expect(func() { + _ = client.ARGrep(ctx, "myarray", "-", "+", nil) + }).NotTo(Panic()) + + Expect(func() { + _ = client.ARGrepWithValues(ctx, "myarray", "-", "+", nil) + }).NotTo(Panic()) + }) + }) + + Describe("ARRing", func() { + It("should ARRING create ring buffer", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + for i := 0; i < 10; i++ { + _, err := client.ARRing(ctx, "myarray", 5, fmt.Sprintf("%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + // After wrap, indices 0-4 should have values 5-9 + for i := 0; i < 5; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("%d", i+5))) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(5))) + }) + + It("should ARRING basic wraparound", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + for i := 0; i < 20; i++ { + idx, err := client.ARRing(ctx, "myarray", 10, fmt.Sprintf("val%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(idx).To(Equal(uint64(i % 10))) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(10))) + + // Values should be the last 10 inserted (val10-val19) + for i := 0; i < 10; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("val%d", i+10))) + } + }) + + It("should ARRING with size 1", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + for i := 0; i < 100; i++ { + _, err := client.ARRing(ctx, "myarray", 1, fmt.Sprintf("val%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(1))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("val99")) + }) + }) + + Describe("ARNext and ARSeek", func() { + It("should ARNext track insert position", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + next, err := client.ARNext(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(next).To(Equal(uint64(0))) + + _, err = client.ARInsert(ctx, "myarray", "a").Result() + Expect(err).NotTo(HaveOccurred()) + + next, err = client.ARNext(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(next).To(Equal(uint64(1))) + + _, err = client.ARInsert(ctx, "myarray", "b").Result() + Expect(err).NotTo(HaveOccurred()) + + next, err = client.ARNext(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(next).To(Equal(uint64(2))) + }) + + It("should ARSeek work", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + _, err := client.ARInsert(ctx, "myarray", "a").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.ARInsert(ctx, "myarray", "b").Result() + Expect(err).NotTo(HaveOccurred()) + + n, err := client.ARSeek(ctx, "myarray", 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + idx, err := client.ARInsert(ctx, "myarray", "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(idx).To(Equal(uint64(10))) + + next, err := client.ARNext(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(next).To(Equal(uint64(11))) + + val, err := client.ARGet(ctx, "myarray", 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("c")) + }) + + It("should ARNext return nil when insert cursor is exhausted", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + _, err := client.ARInsert(ctx, "myarray", "a").Result() + Expect(err).NotTo(HaveOccurred()) + + // Move to terminal cursor state + _, err = client.ARSeek(ctx, "myarray", 18446744073709551615).Result() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.ARNext(ctx, "myarray").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should ARSeek on non-existent key not create it", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + n, err := client.ARSeek(ctx, "myarray", 100).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(0))) + + exists, err := client.Exists(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(Equal(int64(0))) + + // Now create the array and verify ARSeek works + _, err = client.ARInsert(ctx, "myarray", "first").Result() + Expect(err).NotTo(HaveOccurred()) + + exists, err = client.Exists(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(Equal(int64(1))) + + n, err = client.ARSeek(ctx, "myarray", 50).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + _, err = client.ARInsert(ctx, "myarray", "second").Result() + Expect(err).NotTo(HaveOccurred()) + + next, err := client.ARNext(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(next).To(Equal(uint64(51))) + }) + }) + + Describe("ARLastItems", func() { + It("should ARLASTITEMS basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + for i := 0; i < 5; i++ { + _, err := client.ARInsert(ctx, "myarray", fmt.Sprintf("%d", i*10)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + result, err := client.ARLastItems(ctx, "myarray", 3, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"20", "30", "40"})) + + result, err = client.ARLastItems(ctx, "myarray", 3, true).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"40", "30", "20"})) + }) + + It("should ARLASTITEMS handle empty and partial cases", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Empty array + result, err := client.ARLastItems(ctx, "myarray", 5, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEmpty()) + + // Fewer items than requested + _, err = client.ARRing(ctx, "myarray", 10, "a").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.ARRing(ctx, "myarray", 10, "b").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.ARRing(ctx, "myarray", 10, "c").Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.ARLastItems(ctx, "myarray", 5, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"a", "b", "c"})) + }) + + It("should ARLASTITEMS with various counts and REV", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + for i := 0; i < 20; i++ { + _, err := client.ARRing(ctx, "myarray", 10, fmt.Sprintf("item%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + // Get exactly 1 item + result, err := client.ARLastItems(ctx, "myarray", 1, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"item19"})) + + result, err = client.ARLastItems(ctx, "myarray", 1, true).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"item19"})) + + // Get 3 items + result, err = client.ARLastItems(ctx, "myarray", 3, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"item17", "item18", "item19"})) + + result, err = client.ARLastItems(ctx, "myarray", 3, true).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"item19", "item18", "item17"})) + }) + + It("should ARLASTITEMS edge cases", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Empty array + result, err := client.ARLastItems(ctx, "myarray", 5, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEmpty()) + + result, err = client.ARLastItems(ctx, "myarray", 5, true).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEmpty()) + + // Single element + _, err = client.ARInsert(ctx, "myarray", "only").Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.ARLastItems(ctx, "myarray", 1, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"only"})) + + result, err = client.ARLastItems(ctx, "myarray", 10, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"only"})) + + // Two elements + _, err = client.ARInsert(ctx, "myarray", "second").Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.ARLastItems(ctx, "myarray", 5, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"only", "second"})) + + result, err = client.ARLastItems(ctx, "myarray", 5, true).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"second", "only"})) + }) + }) + + Describe("AROp", func() { + It("should AROpSum", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "10"}, + redis.AREntry{Index: 1, Value: "20"}, + redis.AREntry{Index: 2, Value: "30"}, + ).Err()).NotTo(HaveOccurred()) + + val, err := client.AROpSum(ctx, "myarray", 0, 2).Float64() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(float64(60))) + }) + + It("should AROpMin", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "30"}, + redis.AREntry{Index: 1, Value: "10"}, + redis.AREntry{Index: 2, Value: "20"}, + ).Err()).NotTo(HaveOccurred()) + + val, err := client.AROpMin(ctx, "myarray", 0, 2).Float64() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(float64(10))) + }) + + It("should AROpMax", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "30"}, + redis.AREntry{Index: 1, Value: "10"}, + redis.AREntry{Index: 2, Value: "20"}, + ).Err()).NotTo(HaveOccurred()) + + val, err := client.AROpMax(ctx, "myarray", 0, 2).Float64() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(float64(30))) + }) + + It("should AROpMatch", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "hello"}, + redis.AREntry{Index: 1, Value: "world"}, + redis.AREntry{Index: 2, Value: "hello"}, + redis.AREntry{Index: 3, Value: "foo"}, + ).Err()).NotTo(HaveOccurred()) + + val, err := client.AROpMatch(ctx, "myarray", 0, 3, "hello").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(2))) + + val, err = client.AROpMatch(ctx, "myarray", 0, 3, "world").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(1))) + + val, err = client.AROpMatch(ctx, "myarray", 0, 3, "bar").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(0))) + }) + + It("should AROpUsed", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "a"}, + redis.AREntry{Index: 2, Value: "b"}, + redis.AREntry{Index: 5, Value: "c"}, + ).Err()).NotTo(HaveOccurred()) + + val, err := client.AROpUsed(ctx, "myarray", 0, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(3))) + }) + + It("should AROpAnd/AROpOr/AROpXor", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "255"}, + redis.AREntry{Index: 1, Value: "15"}, + redis.AREntry{Index: 2, Value: "240"}, + ).Err()).NotTo(HaveOccurred()) + + // AND: 255 & 15 & 240 = 0 + val, err := client.AROpAnd(ctx, "myarray", 0, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(0))) + + // OR: 255 | 15 | 240 = 255 + val, err = client.AROpOr(ctx, "myarray", 0, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(255))) + + // XOR: 255 ^ 15 ^ 240 = 0 + val, err = client.AROpXor(ctx, "myarray", 0, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(0))) + }) + + It("should AROpAnd/AROpOr/AROpXor truncate floats toward zero", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "7.9"}, + redis.AREntry{Index: 1, Value: "3.2"}, + redis.AREntry{Index: 2, Value: "1.8"}, + ).Err()).NotTo(HaveOccurred()) + + // Truncated: 7, 3, 1 → AND = 1 + val, err := client.AROpAnd(ctx, "myarray", 0, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(1))) + + // OR = 7 + val, err = client.AROpOr(ctx, "myarray", 0, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(7))) + + // XOR: 7 ^ 3 ^ 1 = 5 + val, err = client.AROpXor(ctx, "myarray", 0, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(5))) + }) + + It("should AROpAnd/AROpOr/AROpXor return negative results", func() { + // AND: -1 & -5 = -5 (all bits of -5 are subset of -1) + cmd := client.Del(ctx, "myarray") + Expect(cmd.Err()).NotTo(HaveOccurred()) + cmd2 := client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "-1"}, + redis.AREntry{Index: 1, Value: "-5"}, + ) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + val, err := client.AROpAnd(ctx, "myarray", 0, 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(-5))) + + // OR: -4 | 3 = -1 + cmd = client.Del(ctx, "myarray") + Expect(cmd.Err()).NotTo(HaveOccurred()) + cmd2 = client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "-4"}, + redis.AREntry{Index: 1, Value: "3"}, + ) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + val, err = client.AROpOr(ctx, "myarray", 0, 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(-1))) + + // XOR: 0 ^ -1 = -1 + cmd = client.Del(ctx, "myarray") + Expect(cmd.Err()).NotTo(HaveOccurred()) + cmd2 = client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "0"}, + redis.AREntry{Index: 1, Value: "-1"}, + ) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + val, err = client.AROpXor(ctx, "myarray", 0, 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(-1))) + }) + + It("should AROpMatch with large strings", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + largestr := strings.Repeat("x", 300) + largestr2 := strings.Repeat("y", 300) + + Expect(client.ARSet(ctx, "myarray", 0, largestr).Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 1, "small").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 2, largestr).Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 3, largestr2).Err()).NotTo(HaveOccurred()) + + val, err := client.AROpMatch(ctx, "myarray", 0, 3, largestr).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(2))) + + val, err = client.AROpMatch(ctx, "myarray", 0, 3, largestr2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(1))) + + val, err = client.AROpMatch(ctx, "myarray", 0, 3, "small").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(1))) + + val, err = client.AROpMatch(ctx, "myarray", 0, 3, "notfound").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(int64(0))) + }) + }) + + Describe("ARInfo", func() { + It("should ARInfo basics", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "a"}, + redis.AREntry{Index: 1, Value: "b"}, + redis.AREntry{Index: 100, Value: "c"}, + ).Err()).NotTo(HaveOccurred()) + + info, err := client.ARInfo(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(info["count"]).To(Equal(int64(3))) + Expect(info["len"]).To(Equal(int64(101))) + }) + }) + + Describe("ARInfoFull", func() { + It("should ARInfoFull return all basic fields plus slice statistics", func() { + cmd := client.Del(ctx, "myarray") + Expect(cmd.Err()).NotTo(HaveOccurred()) + cmd2 := client.ARMSet(ctx, "myarray", + redis.AREntry{Index: 0, Value: "a"}, + redis.AREntry{Index: 1, Value: "b"}, + redis.AREntry{Index: 100, Value: "c"}, + ) + Expect(cmd2.Err()).NotTo(HaveOccurred()) + + info, err := client.ARInfoFull(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + + // Same basic fields as ARInfo + Expect(info["count"]).To(Equal(int64(3))) + Expect(info["len"]).To(Equal(int64(101))) + Expect(info).To(HaveKey("next-insert-index")) + Expect(info).To(HaveKey("slices")) + Expect(info).To(HaveKey("directory-size")) + Expect(info).To(HaveKey("super-dir-entries")) + Expect(info).To(HaveKey("slice-size")) + + // Full-only fields: per-encoding slice statistics + Expect(info).To(HaveKey("dense-slices")) + Expect(info).To(HaveKey("sparse-slices")) + Expect(info).To(HaveKey("avg-dense-size")) + Expect(info).To(HaveKey("avg-dense-fill")) + Expect(info).To(HaveKey("avg-sparse-size")) + + // Sum of dense and sparse slices should equal total slices + denseSlices := info["dense-slices"].(int64) + sparseSlices := info["sparse-slices"].(int64) + totalSlices := info["slices"].(int64) + Expect(denseSlices + sparseSlices).To(Equal(totalSlices)) + }) + }) + + Describe("Type checks", func() { + It("should array commands return WRONGTYPE on wrong type", func() { + Expect(client.Del(ctx, "mykey").Err()).NotTo(HaveOccurred()) + Expect(client.Set(ctx, "mykey", "value", 0).Err()).NotTo(HaveOccurred()) + + err := client.ARGet(ctx, "mykey", 0).Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("WRONGTYPE")) + + err = client.ARSet(ctx, "mykey", 0, "foo").Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("WRONGTYPE")) + + err = client.ARLen(ctx, "mykey").Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("WRONGTYPE")) + + err = client.ARCount(ctx, "mykey").Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("WRONGTYPE")) + }) + + It("should ARMGET return WRONGTYPE on wrong type", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.Set(ctx, "myarray", "string_value", 0).Err()).NotTo(HaveOccurred()) + + err := client.ARMGet(ctx, "myarray", 0, 1, 2).Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("WRONGTYPE")) + }) + + It("should ARGETRANGE return WRONGTYPE on wrong type", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.Set(ctx, "myarray", "string_value", 0).Err()).NotTo(HaveOccurred()) + + err := client.ARGetRange(ctx, "myarray", 0, 10).Err() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("WRONGTYPE")) + }) + }) + + Describe("TYPE and OBJECT ENCODING", func() { + It("should TYPE return array", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "hello").Err()).NotTo(HaveOccurred()) + + typ, err := client.Type(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(typ).To(Equal("array")) + }) + + It("should OBJECT ENCODING return sliced-array", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 0, "hello").Err()).NotTo(HaveOccurred()) + + encoding, err := client.ObjectEncoding(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(encoding).To(Equal("sliced-array")) + }) + }) + + Describe("Sparse arrays", func() { + It("should sparse array with large gaps work", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 0, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 10000, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 1000000, "c").Err()).NotTo(HaveOccurred()) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("a")) + + val, err = client.ARGet(ctx, "myarray", 10000).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("b")) + + val, err = client.ARGet(ctx, "myarray", 1000000).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("c")) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(3))) + + length, err := client.ARLen(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(length).To(Equal(uint64(1000001))) + }) + }) + + Describe("Directory resizing", func() { + It("should handle many slices", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + sliceSize := 4096 + for i := 0; i < 20; i++ { + idx := uint64(i * sliceSize) + Expect(client.ARSet(ctx, "myarray", idx, fmt.Sprintf("slice%d", i)).Err()).NotTo(HaveOccurred()) + } + + for i := 0; i < 20; i++ { + idx := uint64(i * sliceSize) + val, err := client.ARGet(ctx, "myarray", idx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("slice%d", i))) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(20))) + }) + + It("should handle very large index jump", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 0, "start").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 1000000, "middle").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 10000000, "end").Err()).NotTo(HaveOccurred()) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("start")) + + val, err = client.ARGet(ctx, "myarray", 1000000).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("middle")) + + val, err = client.ARGet(ctx, "myarray", 10000000).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("end")) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(3))) + }) + }) + + Describe("Dense window growth", func() { + It("should handle right expansion", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + for i := 0; i < 100; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("val%d", i)).Err()).NotTo(HaveOccurred()) + } + + for i := 0; i < 100; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("val%d", i))) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(100))) + }) + + It("should handle left expansion", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 500, "anchor").Err()).NotTo(HaveOccurred()) + for i := 499; i >= 400; i-- { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("val%d", i)).Err()).NotTo(HaveOccurred()) + } + + val, err := client.ARGet(ctx, "myarray", 500).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("anchor")) + + for i := 400; i < 500; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("val%d", i))) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(101))) + }) + + It("should handle bidirectional expansion", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 500, "center").Err()).NotTo(HaveOccurred()) + for i := 1; i <= 50; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(500-i), fmt.Sprintf("left%d", i)).Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", uint64(500+i), fmt.Sprintf("right%d", i)).Err()).NotTo(HaveOccurred()) + } + + val, err := client.ARGet(ctx, "myarray", 500).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("center")) + + for i := 1; i <= 50; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(500-i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("left%d", i))) + + val, err = client.ARGet(ctx, "myarray", uint64(500+i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("right%d", i))) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(101))) + }) + }) + + Describe("Sparse to dense promotion", func() { + It("should promote sparse to dense when exceeding kmax threshold", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Add 15 elements at scattered offsets within one slice + for i := 0; i < 15; i++ { + idx := uint64(i * 100) + Expect(client.ARSet(ctx, "myarray", idx, fmt.Sprintf("sparse%d", i)).Err()).NotTo(HaveOccurred()) + } + + for i := 0; i < 15; i++ { + idx := uint64(i * 100) + val, err := client.ARGet(ctx, "myarray", idx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("sparse%d", i))) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(15))) + }) + + It("should promote then continue adding", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Phase 1: sparse + for i := 0; i < 5; i++ { + idx := uint64(i * 200) + Expect(client.ARSet(ctx, "myarray", idx, fmt.Sprintf("phase1_%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Phase 2: more to trigger promotion + for i := 5; i < 20; i++ { + idx := uint64(i * 200) + Expect(client.ARSet(ctx, "myarray", idx, fmt.Sprintf("phase2_%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Verify all + for i := 0; i < 5; i++ { + idx := uint64(i * 200) + val, err := client.ARGet(ctx, "myarray", idx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("phase1_%d", i))) + } + for i := 5; i < 20; i++ { + idx := uint64(i * 200) + val, err := client.ARGet(ctx, "myarray", idx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("phase2_%d", i))) + } + }) + }) + + Describe("Dense to sparse demotion", func() { + It("should demote dense to sparse when deleting below kmin", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Create dense slice with 50 elements + for i := 0; i < 50; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("val%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Delete most elements, leaving only 3 + for i := 3; i < 50; i++ { + _, err := client.ARDel(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + // Verify remaining elements + for i := 0; i < 3; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("val%d", i))) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(3))) + }) + + It("should demote then add again", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Create dense + for i := 0; i < 30; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("initial%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Delete to demote + for i := 4; i < 30; i++ { + _, err := client.ARDel(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(4))) + + // Add new elements + for i := 100; i < 105; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("new%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Verify old and new + for i := 0; i < 4; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("initial%d", i))) + } + for i := 100; i < 105; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("new%d", i))) + } + }) + }) + + Describe("ARDelRange detailed", func() { + It("should ARDELRANGE trigger dense to sparse demotion", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Create dense slice with 50 elements + for i := 0; i < 50; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("val%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Delete most elements with ARDELRANGE + n, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 3, End: 49}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(47))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(3))) + + // Verify remaining elements + for i := 0; i < 3; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("val%d", i))) + } + }) + + It("should ARDELRANGE with overlapping ranges", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + for i := 0; i < 20; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("val%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Overlapping ranges: [5,12] and [8,15] + n, err := client.ARDelRange(ctx, "myarray", + redis.ARRange{Start: 5, End: 12}, + redis.ARRange{Start: 8, End: 15}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(11))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(9))) + + val, err := client.ARGet(ctx, "myarray", 4).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("val4")) + + _, err = client.ARGet(ctx, "myarray", 5).Result() + Expect(err).To(Equal(redis.Nil)) + + _, err = client.ARGet(ctx, "myarray", 15).Result() + Expect(err).To(Equal(redis.Nil)) + + val, err = client.ARGet(ctx, "myarray", 16).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("val16")) + }) + + It("should ARDELRANGE sparse slice middle-span deletion", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 10, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 20, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 30, "c").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 40, "d").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 50, "e").Err()).NotTo(HaveOccurred()) + + n, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 20, End: 40}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(3))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(2))) + + val, err := client.ARGet(ctx, "myarray", 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("a")) + + _, err = client.ARGet(ctx, "myarray", 20).Result() + Expect(err).To(Equal(redis.Nil)) + + val, err = client.ARGet(ctx, "myarray", 50).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("e")) + }) + + It("should ARDELRANGE sparse whole-slice deletion", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 10, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 20, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 30, "c").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 40, "d").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 50, "e").Err()).NotTo(HaveOccurred()) + + n, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 0, End: 100}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(5))) + + exists, err := client.Exists(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(exists).To(Equal(int64(0))) + }) + + It("should ARDELRANGE sparse no-hit range", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 10, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 20, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 30, "c").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 40, "d").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 50, "e").Err()).NotTo(HaveOccurred()) + + n, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 11, End: 19}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(0))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(5))) + }) + + It("should ARDELRANGE delete entire slice then verify iteration", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Two slices + for i := 0; i < 10; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("slice0_%d", i)).Err()).NotTo(HaveOccurred()) + } + for i := 4096; i < 4106; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("slice1_%d", i)).Err()).NotTo(HaveOccurred()) + } + + // Delete entire first slice + n, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 0, End: 4095}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(10))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(10))) + + // ARSCAN should only find second slice elements + result, err := client.ARScan(ctx, "myarray", 0, 5000, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(10)) + Expect(result[0].Index).To(Equal(uint64(4096))) + Expect(result[0].Value).To(Equal("slice1_4096")) + }) + }) + + Describe("Circular buffer", func() { + It("should ARRING wraparound with ARLASTITEMS", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Create ring with 8 items, MOD 5 + for i := 0; i < 8; i++ { + _, err := client.ARRing(ctx, "myarray", 5, fmt.Sprintf("%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + // Values: 0->3, 1->4, 2->5, 3->6, 4->7 + result, err := client.ARLastItems(ctx, "myarray", 3, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"5", "6", "7"})) + + result, err = client.ARLastItems(ctx, "myarray", 3, true).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"7", "6", "5"})) + + // Request more items than exist + result, err = client.ARLastItems(ctx, "myarray", 10, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(5)) + }) + + It("should ARLASTITEMS handle empty and partial cases", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Empty array + result, err := client.ARLastItems(ctx, "myarray", 5, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeEmpty()) + + // Fewer items than requested + _, err = client.ARRing(ctx, "myarray", 10, "a").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.ARRing(ctx, "myarray", 10, "b").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.ARRing(ctx, "myarray", 10, "c").Result() + Expect(err).NotTo(HaveOccurred()) + + result, err = client.ARLastItems(ctx, "myarray", 5, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"a", "b", "c"})) + }) + + It("should ARNEXT track correctly with ARRING", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + for i := 0; i < 7; i++ { + expectedIdx := uint64(i % 4) + idx, err := client.ARRing(ctx, "myarray", 4, fmt.Sprintf("%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(idx).To(Equal(expectedIdx)) + + next, err := client.ARNext(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + if i < 4 { + Expect(next).To(Equal(uint64(i + 1))) + } else { + Expect(next).To(Equal(expectedIdx + 1)) + } + } + }) + + It("should ARRING truncation when size decreases", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Create ring buffer with MOD 10 + for i := 0; i < 15; i++ { + _, err := client.ARRing(ctx, "myarray", 10, fmt.Sprintf("v%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(10))) + + // Use smaller MOD - truncates and inserts new value + _, err = client.ARRing(ctx, "myarray", 5, "truncated").Result() + Expect(err).NotTo(HaveOccurred()) + + count, err = client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(5))) + + // Verify values + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("truncated")) + + val, err = client.ARGet(ctx, "myarray", 1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("v11")) + + // Positions 5-9 should be empty + _, err = client.ARGet(ctx, "myarray", 5).Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should ARRING shrink stop at first hole", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + for i := 0; i < 5; i++ { + _, err := client.ARRing(ctx, "myarray", 5, fmt.Sprintf("v%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + _, err := client.ARDel(ctx, "myarray", 3).Result() + Expect(err).NotTo(HaveOccurred()) + + _, err = client.ARRing(ctx, "myarray", 3, "new").Result() + Expect(err).NotTo(HaveOccurred()) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(2))) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("v4")) + + _, err = client.ARGet(ctx, "myarray", 2).Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should ARSEEK followed by ARRING work", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + _, err := client.ARInsert(ctx, "myarray", "a").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.ARInsert(ctx, "myarray", "b").Result() + Expect(err).NotTo(HaveOccurred()) + _, err = client.ARInsert(ctx, "myarray", "c").Result() + Expect(err).NotTo(HaveOccurred()) + + // Seek to position 10 + _, err = client.ARSeek(ctx, "myarray", 10).Result() + Expect(err).NotTo(HaveOccurred()) + + next, err := client.ARNext(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(next).To(Equal(uint64(10))) + + // Now use MOD - should insert at index 0 (10 % 5 = 0) + _, err = client.ARRing(ctx, "myarray", 5, "x").Result() + Expect(err).NotTo(HaveOccurred()) + + val, err := client.ARGet(ctx, "myarray", 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("x")) + }) + + It("should ARLASTITEMS reverse order work", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Create ring with wraparound + for i := 0; i < 12; i++ { + _, err := client.ARRing(ctx, "myarray", 8, fmt.Sprintf("v%d", i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + result, err := client.ARLastItems(ctx, "myarray", 4, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"v8", "v9", "v10", "v11"})) + + result, err = client.ARLastItems(ctx, "myarray", 4, true).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"v11", "v10", "v9", "v8"})) + + // Request all items + result, err = client.ARLastItems(ctx, "myarray", 100, false).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(8)) + }) + }) + + Describe("Large uint64 index values", func() { + const int64MaxPlus1 = uint64(math.MaxInt64) + 1 + const uint64Max = uint64(math.MaxUint64) + + It("should ARSet and ARGet with index exceeding int64 max", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + n, err := client.ARSet(ctx, "myarray", int64MaxPlus1, "value").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + val, err := client.ARGet(ctx, "myarray", int64MaxPlus1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("value")) + + length, err := client.ARLen(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(length).To(Equal(uint64(int64MaxPlus1 + 1))) + }) + + It("should ARSet contiguous write across int64 boundary", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + base := int64MaxPlus1 - 1 + n, err := client.ARSet(ctx, "myarray", base, "a", "b", "c").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(3))) + + val, err := client.ARGet(ctx, "myarray", base).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("a")) + + val, err = client.ARGet(ctx, "myarray", base+1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("b")) + + val, err = client.ARGet(ctx, "myarray", base+2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("c")) + }) + + It("should ARGetRange with large uint64 values", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + base := int64MaxPlus1 + Expect(client.ARSet(ctx, "myarray", base, "x").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", base+1, "y").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", base+2, "z").Err()).NotTo(HaveOccurred()) + + result, err := client.ARGetRange(ctx, "myarray", base, base+2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"x", "y", "z"})) + }) + + It("should ARMSet and ARMGet with large uint64 indexes", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + n, err := client.ARMSet(ctx, "myarray", + redis.AREntry{Index: int64MaxPlus1, Value: "a"}, + redis.AREntry{Index: int64MaxPlus1 + 1, Value: "b"}, + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(2))) + + result, err := client.ARMGet(ctx, "myarray", int64MaxPlus1, int64MaxPlus1+1, 0).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(3)) + Expect(result[0]).To(Equal("a")) + Expect(result[1]).To(Equal("b")) + Expect(result[2]).To(BeNil()) + }) + + It("should ARDel with large uint64 index", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", int64MaxPlus1, "val").Err()).NotTo(HaveOccurred()) + + n, err := client.ARDel(ctx, "myarray", int64MaxPlus1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + _, err = client.ARGet(ctx, "myarray", int64MaxPlus1).Result() + Expect(err).To(Equal(redis.Nil)) + }) + + It("should ARDelRange with large uint64 values", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + base := int64MaxPlus1 + Expect(client.ARSet(ctx, "myarray", base, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", base+1, "b").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", base+2, "c").Err()).NotTo(HaveOccurred()) + + n, err := client.ARDelRange(ctx, "myarray", redis.ARRange{Start: base, End: base + 1}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(uint64(2))) + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(1))) + + val, err := client.ARGet(ctx, "myarray", base+2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("c")) + }) + + It("should ARScan with large uint64 values", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + base := int64MaxPlus1 + Expect(client.ARSet(ctx, "myarray", base, "a").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", base+5, "b").Err()).NotTo(HaveOccurred()) + + result, err := client.ARScan(ctx, "myarray", base, base+10, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(2)) + Expect(result[0]).To(Equal(redis.AREntry{Index: base, Value: "a"})) + Expect(result[1]).To(Equal(redis.AREntry{Index: base + 5, Value: "b"})) + }) + + It("should ARSeek and ARNext with large uint64 index", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + _, err := client.ARInsert(ctx, "myarray", "a").Result() + Expect(err).NotTo(HaveOccurred()) + + n, err := client.ARSeek(ctx, "myarray", int64MaxPlus1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + idx, err := client.ARInsert(ctx, "myarray", "b").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(idx).To(Equal(int64MaxPlus1)) + + next, err := client.ARNext(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(next).To(Equal(int64MaxPlus1 + 1)) + + val, err := client.ARGet(ctx, "myarray", int64MaxPlus1).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal("b")) + }) + + It("should ARSeek to uint64 max then ARNext return error", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + _, err := client.ARInsert(ctx, "myarray", "a").Result() + Expect(err).NotTo(HaveOccurred()) + + n, err := client.ARSeek(ctx, "myarray", uint64Max).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(n).To(Equal(int64(1))) + + _, err = client.ARNext(ctx, "myarray").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should AROp with large uint64 range values", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + base := int64MaxPlus1 + Expect(client.ARMSet(ctx, "myarray", + redis.AREntry{Index: base, Value: "10"}, + redis.AREntry{Index: base + 1, Value: "20"}, + redis.AREntry{Index: base + 2, Value: "30"}, + ).Err()).NotTo(HaveOccurred()) + + val, err := client.AROpSum(ctx, "myarray", base, base+2).Float64() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(float64(60))) + + intVal, err := client.AROpUsed(ctx, "myarray", base, base+2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(intVal).To(Equal(int64(3))) + }) + }) + + Describe("Regression tests", func() { + It("should AROP work on whole-number floats", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 0, "10.0").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 1, "20.0").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 2, "30.0").Err()).NotTo(HaveOccurred()) + + // SUM + val, err := client.AROpSum(ctx, "myarray", 0, 2).Float64() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(float64(60))) + + // MIN + val, err = client.AROpMin(ctx, "myarray", 0, 2).Float64() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(float64(10))) + + // MAX + val, err = client.AROpMax(ctx, "myarray", 0, 2).Float64() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(float64(30))) + + // MATCH + intVal, err := client.AROpMatch(ctx, "myarray", 0, 2, "10.0").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(intVal).To(Equal(int64(1))) + }) + + It("should ARDELRANGE with multiple ranges across slices", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Create elements across slice boundaries + for i := 0; i < 10; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("%d", i)).Err()).NotTo(HaveOccurred()) + } + for i := 4096; i < 4106; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("%d", i)).Err()).NotTo(HaveOccurred()) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(20))) + + // Delete first slice partially + _, err = client.ARDelRange(ctx, "myarray", redis.ARRange{Start: 5, End: 9}).Result() + Expect(err).NotTo(HaveOccurred()) + + // AROP SUM should work across slices + val, err := client.AROpSum(ctx, "myarray", 0, 5000).Float64() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(float64(41015))) + + // AROP USED + intVal, err := client.AROpUsed(ctx, "myarray", 0, 5000).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(intVal).To(Equal(int64(15))) + }) + + It("should ARSCAN work over superdir blocks", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Elements in different superdir blocks + Expect(client.ARSet(ctx, "myarray", 0, "first").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 8388608, "second").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 16777216, "third").Err()).NotTo(HaveOccurred()) + + // Scan entire range + result, err := client.ARScan(ctx, "myarray", 0, 20000000, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(3)) + Expect(result[0]).To(Equal(redis.AREntry{Index: 0, Value: "first"})) + Expect(result[1]).To(Equal(redis.AREntry{Index: 8388608, Value: "second"})) + Expect(result[2]).To(Equal(redis.AREntry{Index: 16777216, Value: "third"})) + + // Reverse scan + result, err = client.ARScan(ctx, "myarray", 20000000, 0, nil).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(3)) + Expect(result[0]).To(Equal(redis.AREntry{Index: 16777216, Value: "third"})) + Expect(result[1]).To(Equal(redis.AREntry{Index: 8388608, Value: "second"})) + Expect(result[2]).To(Equal(redis.AREntry{Index: 0, Value: "first"})) + }) + + It("should ARGETRANGE work across superdir slice boundary", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + Expect(client.ARSet(ctx, "myarray", 8388607, "left").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 8388608, "mid").Err()).NotTo(HaveOccurred()) + Expect(client.ARSet(ctx, "myarray", 8388609, "right").Err()).NotTo(HaveOccurred()) + + result, err := client.ARGetRange(ctx, "myarray", 8388607, 8388609).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"left", "mid", "right"})) + + result, err = client.ARGetRange(ctx, "myarray", 8388609, 8388607).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(Equal([]any{"right", "mid", "left"})) + }) + + It("should stress test mixed operations across multiple slices", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + sliceSize := 4096 + + // Create elements across 5 slices + for slice := 0; slice < 5; slice++ { + base := slice * sliceSize + for i := 0; i < 20; i++ { + idx := uint64(base + i*50) + Expect(client.ARSet(ctx, "myarray", idx, fmt.Sprintf("s%d_e%d", slice, i)).Err()).NotTo(HaveOccurred()) + } + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(100))) + + // Delete half from each slice + for slice := 0; slice < 5; slice++ { + base := slice * sliceSize + for i := 10; i < 20; i++ { + _, err := client.ARDel(ctx, "myarray", uint64(base+i*50)).Result() + Expect(err).NotTo(HaveOccurred()) + } + } + + count, err = client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(50))) + + // Verify remaining elements + for slice := 0; slice < 5; slice++ { + base := slice * sliceSize + for i := 0; i < 10; i++ { + idx := uint64(base + i*50) + val, err := client.ARGet(ctx, "myarray", idx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("s%d_e%d", slice, i))) + } + } + }) + + It("should stress test rapid insert/delete cycles", func() { + Expect(client.Del(ctx, "myarray").Err()).NotTo(HaveOccurred()) + + // Multiple cycles of growth and shrinkage + for cycle := 0; cycle < 3; cycle++ { + // Grow + for i := 0; i < 100; i++ { + Expect(client.ARSet(ctx, "myarray", uint64(i), fmt.Sprintf("cycle%d_%d", cycle, i)).Err()).NotTo(HaveOccurred()) + } + + count, err := client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(100))) + + // Shrink (but leave some) + for i := 10; i < 100; i++ { + _, err := client.ARDel(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + } + + count, err = client.ARCount(ctx, "myarray").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(count).To(Equal(uint64(10))) + } + + // Verify final state + for i := 0; i < 10; i++ { + val, err := client.ARGet(ctx, "myarray", uint64(i)).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(fmt.Sprintf("cycle2_%d", i))) + } + }) + }) +}) diff --git a/command.go b/command.go index d36520e0..6dec765a 100644 --- a/command.go +++ b/command.go @@ -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, + } +} diff --git a/commands.go b/commands.go index b3b6badc..d347ffeb 100644 --- a/commands.go +++ b/commands.go @@ -228,6 +228,7 @@ type Cmdable interface { ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd ACLCmdable + ArrayCmdable BitMapCmdable ClusterCmdable GenericCmdable