diff --git a/docs/Arrays.md b/docs/Arrays.md
new file mode 100644
index 000000000..84e8bedb4
--- /dev/null
+++ b/docs/Arrays.md
@@ -0,0 +1,225 @@
+# Redis Arrays
+
+Redis Arrays provide sparse arrays of arbitrary Redis values with unsigned array indexes and a notional write head. SE.Redis exposes the array API as experimental Redis 8.8 APIs; callers should expect details to change while the server feature is still in preview.
+
+## Prerequisites
+
+Arrays require Redis 8.8 or later. The APIs are marked with the `SER006` experimental warning.
+
+## Basic Usage
+
+Use `ArraySetAsync` and `ArrayGetAsync` to write and read individual cells:
+
+```csharp
+var db = conn.GetDatabase();
+RedisKey key = "events";
+
+bool inserted = await db.ArraySetAsync(key, 0, "created");
+RedisValue value = await db.ArrayGetAsync(key, 0);
+RedisValue missing = await db.ArrayGetAsync(key, 1);
+
+Console.WriteLine(inserted); // True when the cell did not previously have a value
+Console.WriteLine(value); // created
+Console.WriteLine(missing.IsNull); // True
+```
+
+Array indexes use `RedisArrayIndex`, with implicit conversions from `int`, `long`, and `ulong`. This allows normal small indexes to be used directly, while still allowing the full unsigned index range when needed.
+
+```csharp
+await db.ArraySetAsync(key, 42, "answer");
+await db.ArraySetAsync(key, new RedisArrayIndex(10_000_000UL), "large index");
+```
+
+## Sparse Arrays
+
+Arrays are sparse: unset cells do not have values. `ArrayLengthAsync` reports the notional length, which is the highest used index plus one. `ArrayCountAsync` reports only cells that currently have values.
+
+```csharp
+await db.KeyDeleteAsync(key);
+
+await db.ArraySetAsync(key, 0, "a");
+await db.ArraySetAsync(key, 10, "b");
+
+RedisArrayIndex length = await db.ArrayLengthAsync(key); // 11
+RedisArrayIndex count = await db.ArrayCountAsync(key); // 2
+```
+
+## Setting Multiple Values
+
+To write a contiguous range, pass the first index and the values:
+
+```csharp
+int inserted = await db.ArraySetAsync(key, 0, ["a", "b", "c"]);
+```
+
+To write multiple specific indexes, use `RedisArrayEntry` values:
+
+```csharp
+await db.ArraySetAsync(key,
+[
+ new RedisArrayEntry(0, "alpha"),
+ new RedisArrayEntry(5, "bravo"),
+ new RedisArrayEntry(100, "charlie"),
+]);
+```
+
+The returned `int` is the number of cells that were newly filled.
+
+## Reading Multiple Values
+
+Read selected indexes with `ArrayGetAsync`:
+
+```csharp
+RedisValue[] values = await db.ArrayGetAsync(key, [0, 5, 6, 100]);
+```
+
+Read a range with `ArrayGetRangeAsync`. Ranges can be read forward or backward:
+
+```csharp
+RedisValue[] forward = await db.ArrayGetRangeAsync(key, 0, 5);
+RedisValue[] reverse = await db.ArrayGetRangeAsync(key, 5, 0);
+```
+
+For sparse arrays, use `ArrayScanAsync` to return only populated cells in a range:
+
+```csharp
+RedisArrayEntry[] entries = await db.ArrayScanAsync(key, 0, 100, limit: 50);
+
+foreach (var entry in entries)
+{
+ Console.WriteLine($"{entry.Index}: {entry.Value}");
+}
+```
+
+## Deleting Values
+
+Delete a single cell with `ArrayDeleteAsync`:
+
+```csharp
+bool removed = await db.ArrayDeleteAsync(key, 5);
+```
+
+Delete multiple specific cells by index:
+
+```csharp
+int removedCount = await db.ArrayDeleteAsync(key, [0, 5, 100]);
+```
+
+Delete one or more ranges:
+
+```csharp
+await db.ArrayDeleteRangeAsync(key, 10, 20);
+
+await db.ArrayDeleteRangeAsync(key,
+[
+ new RedisArrayRange(100, 199),
+ new RedisArrayRange(500, 599),
+]);
+```
+
+## Searching
+
+Use `ArrayGrepRequest` with `ArrayGrepAsync` to search values. When `Start` or `End` is not specified, the server's open-ended lower or upper bound is used.
+
+```csharp
+var request = new ArrayGrepRequest
+{
+ Limit = 10,
+};
+request.AddPredicate(ArrayGrepRequest.Predicate.Match("error"));
+
+RedisArrayEntry[] matches = await db.ArrayGrepAsync(key, request);
+
+foreach (var match in matches)
+{
+ Console.WriteLine(match.Index);
+}
+```
+
+Set `IncludeValues` to return values along with the matching indexes:
+
+```csharp
+var request = new ArrayGrepRequest
+{
+ IncludeValues = true,
+};
+request.AddPredicate(ArrayGrepRequest.Predicate.Regex("^ERR[0-9]+"));
+
+RedisArrayEntry[] matches = await db.ArrayGrepAsync(key, request);
+
+foreach (var match in matches)
+{
+ Console.WriteLine($"{match.Index}: {match.Value}");
+}
+```
+
+Multiple predicates can be combined. By default, predicates are combined as `OR`; set `IsIntersection` to combine them as `AND`.
+
+```csharp
+var request = new ArrayGrepRequest
+{
+ IsIntersection = true,
+};
+request.AddPredicate(ArrayGrepRequest.Predicate.Match("redis"));
+request.AddPredicate(ArrayGrepRequest.Predicate.Glob("*array*"));
+
+RedisArrayEntry[] matches = await db.ArrayGrepAsync(key, request);
+```
+
+## Write Head
+
+Arrays have a write head used by insert operations. `ArrayInsertAsync` writes at the current write head and advances it.
+
+```csharp
+RedisArrayIndex first = await db.ArrayInsertAsync(key, "first");
+RedisArrayIndex second = await db.ArrayInsertAsync(key, "second");
+
+RedisArrayIndex? next = await db.ArrayNextAsync(key);
+```
+
+Move the write head with `ArraySeekAsync`:
+
+```csharp
+bool moved = await db.ArraySeekAsync(key, 1_000);
+RedisArrayIndex written = await db.ArrayInsertAsync(key, "later");
+```
+
+## Ring Buffers
+
+Use `ArrayRingAsync` to keep at most a fixed number of cells and wrap writes around that capacity:
+
+```csharp
+for (int i = 0; i < 10; i++)
+{
+ await db.ArrayRingAsync(key, maxLength: 5, value: i);
+}
+
+RedisArrayIndex count = await db.ArrayCountAsync(key); // 5
+```
+
+`ArrayLastItemsAsync` is intended for this capped ring-buffer model. It reads the last values in the ring-buffer sense, where "last" relates to the retained values after wrap-around and trimming:
+
+```csharp
+RedisValue[] last = await db.ArrayLastItemsAsync(key, count: 10);
+RedisValue[] lastReversed = await db.ArrayLastItemsAsync(key, count: 10, reverse: true);
+```
+
+## Operations and Info
+
+Use `ArrayOperationAsync` for simple server-side operations over a range:
+
+```csharp
+RedisValue sum = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Sum);
+RedisValue used = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Used);
+RedisValue matches = await db.ArrayOperationAsync(key, 0, 10, ArrayOperation.Match, "error");
+```
+
+Use `ArrayInfoAsync` for metadata:
+
+```csharp
+ArrayInfo info = await db.ArrayInfoAsync(key);
+
+Console.WriteLine($"Count: {info.Count}");
+Console.WriteLine($"Length: {info.Length}");
+Console.WriteLine($"Next insert index: {info.NextInsertIndex}");
+```
diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md
index 06a45430f..1a112807e 100644
--- a/docs/ReleaseNotes.md
+++ b/docs/ReleaseNotes.md
@@ -8,7 +8,8 @@ Current package versions:
## Unreleased
-- (none)
+- Add experimental Redis 8.8 array support, including array APIs on `IDatabase`/`IDatabaseAsync`,
+ array helper types, `RedisType.Array`, and array delete keyspace notification event types.
## 2.12.27
diff --git a/docs/exp/SER006.md b/docs/exp/SER006.md
index ff92a58a8..f1a09f05f 100644
--- a/docs/exp/SER006.md
+++ b/docs/exp/SER006.md
@@ -2,9 +2,10 @@ Redis 8.8 is currently in preview and may be subject to change.
New features in Redis 8.8:
-- `XNACK` for stream negative acknowledgements
+- Arrays (`ARGET`, `ARSET` etc)
+- Stream negative acknowledgements (`XNACK`)
- `Aggregate.Count` for sorted-set combination operations
-- Sub-key notifications
+- Sub-key (hash) keyspace/keyevent notifications
The corresponding library features must also be considered subject to change:
diff --git a/docs/index.md b/docs/index.md
index 0a2e6c721..69a4cc70f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -46,6 +46,7 @@ Documentation
- [Using RESP3](Resp3) - information on using RESP3
- [ServerMaintenanceEvent](ServerMaintenanceEvent) - how to listen and prepare for hosted server maintenance (e.g. Azure Cache for Redis)
- [Streams](Streams) - how to use the Stream data type
+- [Arrays](Arrays) - how to use Redis Arrays as sparse arrays of values
- [Vector Sets](VectorSets) - how to use Vector Sets for similarity search with embeddings
- [Where are `KEYS` / `SCAN` / `FLUSH*`?](KeysScan) - how to use server-based commands
- [Profiling](Profiling) - profiling interfaces, as well as how to profile in an `async` world
diff --git a/src/StackExchange.Redis/APITypes/RedisArrayEntry.cs b/src/StackExchange.Redis/APITypes/RedisArrayEntry.cs
new file mode 100644
index 000000000..8f07f69f6
--- /dev/null
+++ b/src/StackExchange.Redis/APITypes/RedisArrayEntry.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Describes an array entry at a specific index.
+///
+/// The array index.
+/// The value at this index.
+[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+public readonly struct RedisArrayEntry(RedisArrayIndex index, RedisValue value) : IEquatable
+{
+ private readonly RedisArrayIndex _index = index;
+ private readonly RedisValue _value = value;
+
+ internal RedisArrayEntry(RedisArrayIndex index)
+ : this(index, default)
+ {
+ }
+
+ ///
+ /// The array index.
+ ///
+ public RedisArrayIndex Index => _index;
+
+ ///
+ /// The value at this index.
+ ///
+ public RedisValue Value => _value;
+
+ ///
+ /// Converts to a key/value pair.
+ ///
+ /// The to create a from.
+ public static implicit operator KeyValuePair(RedisArrayEntry value) =>
+ new KeyValuePair(value._index, value._value);
+
+ ///
+ /// Converts from a key/value pair.
+ ///
+ /// The to get a from.
+ public static implicit operator RedisArrayEntry(KeyValuePair value) =>
+ new RedisArrayEntry(value.Key, value.Value);
+
+ ///
+ /// The "{index}: {value}" string representation.
+ ///
+ public override string ToString() => _index + ": " + _value;
+
+ ///
+ public override int GetHashCode() => _index.GetHashCode() ^ _value.GetHashCode();
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The to compare to.
+ public override bool Equals(object? obj) => obj is RedisArrayEntry entry && Equals(entry);
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The to compare to.
+ public bool Equals(RedisArrayEntry other) => _index == other._index && _value == other._value;
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ public static bool operator ==(RedisArrayEntry x, RedisArrayEntry y) => x._index == y._index && x._value == y._value;
+
+ ///
+ /// Compares two values for non-equality.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ public static bool operator !=(RedisArrayEntry x, RedisArrayEntry y) => x._index != y._index || x._value != y._value;
+}
diff --git a/src/StackExchange.Redis/APITypes/RedisArrayIndex.cs b/src/StackExchange.Redis/APITypes/RedisArrayIndex.cs
new file mode 100644
index 000000000..a024ff47f
--- /dev/null
+++ b/src/StackExchange.Redis/APITypes/RedisArrayIndex.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Represents an array index or length; conceptually this can be considered a ,
+/// but wrapped for convenience from languages that do not work well with unsigned values.
+///
+/// The array index.
+[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+[method: CLSCompliant(false)]
+public readonly struct RedisArrayIndex(ulong value) : IEquatable
+{
+ private readonly ulong value = value;
+
+ ///
+ /// The minimum array index value.
+ ///
+ public static RedisArrayIndex MinValue => new RedisArrayIndex(0);
+
+ ///
+ /// The maximum array index value.
+ ///
+ public static RedisArrayIndex MaxValue => new RedisArrayIndex(ulong.MaxValue);
+
+ ///
+ /// Initializes a value.
+ ///
+ /// The array index.
+ public RedisArrayIndex(int value)
+ : this(CheckedNonNegative(value))
+ {
+ }
+
+ ///
+ /// Initializes a value.
+ ///
+ /// The array index.
+ public RedisArrayIndex(long value)
+ : this(CheckedNonNegative(value))
+ {
+ }
+
+ ///
+ /// The numeric value of this index.
+ ///
+ [CLSCompliant(false)]
+ public ulong Value => value;
+
+ internal RedisValue ToRedisValue() => value;
+
+ ///
+ /// Converts from an .
+ ///
+ /// The array index.
+ public static implicit operator RedisArrayIndex(int value) => new RedisArrayIndex(value);
+
+ ///
+ /// Converts from a .
+ ///
+ /// The array index.
+ public static implicit operator RedisArrayIndex(long value) => new RedisArrayIndex(value);
+
+ ///
+ /// Converts from a .
+ ///
+ /// The array index.
+ [CLSCompliant(false)]
+ public static implicit operator RedisArrayIndex(ulong value) => new RedisArrayIndex(value);
+
+ ///
+ /// Converts to an .
+ ///
+ /// The array index.
+ public static explicit operator int(RedisArrayIndex value) => checked((int)value.value);
+
+ ///
+ /// Converts to a .
+ ///
+ /// The array index.
+ public static explicit operator long(RedisArrayIndex value) => checked((long)value.value);
+
+ ///
+ /// Converts to a .
+ ///
+ /// The array index.
+ [CLSCompliant(false)]
+ public static implicit operator ulong(RedisArrayIndex value) => value.value;
+
+ ///
+ /// The string representation of this array index.
+ ///
+ public override string ToString() => value.ToString();
+
+ ///
+ public override int GetHashCode() => value.GetHashCode();
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The to compare to.
+ public override bool Equals(object? obj) => obj is RedisArrayIndex index && Equals(index);
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The to compare to.
+ public bool Equals(RedisArrayIndex other) => value == other.value;
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ public static bool operator ==(RedisArrayIndex x, RedisArrayIndex y) => x.value == y.value;
+
+ ///
+ /// Compares two values for non-equality.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ public static bool operator !=(RedisArrayIndex x, RedisArrayIndex y) => x.value != y.value;
+
+ private static ulong CheckedNonNegative(long value)
+ {
+ if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "Array indices must be non-negative.");
+ return (ulong)value;
+ }
+}
diff --git a/src/StackExchange.Redis/APITypes/RedisArrayRange.cs b/src/StackExchange.Redis/APITypes/RedisArrayRange.cs
new file mode 100644
index 000000000..307b30c71
--- /dev/null
+++ b/src/StackExchange.Redis/APITypes/RedisArrayRange.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Describes a range of array indices.
+///
+/// The start index.
+/// The end index.
+[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+public readonly struct RedisArrayRange(RedisArrayIndex start, RedisArrayIndex end) : IEquatable
+{
+ private readonly RedisArrayIndex _start = start;
+ private readonly RedisArrayIndex _end = end;
+
+ ///
+ /// The start index.
+ ///
+ public RedisArrayIndex Start => _start;
+
+ ///
+ /// The end index.
+ ///
+ public RedisArrayIndex End => _end;
+
+ ///
+ /// The "{start}..{end}" string representation.
+ ///
+ public override string ToString() => _start + ".." + _end;
+
+ ///
+ public override int GetHashCode() => _start.GetHashCode() ^ _end.GetHashCode();
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The to compare to.
+ public override bool Equals(object? obj) => obj is RedisArrayRange range && Equals(range);
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The to compare to.
+ public bool Equals(RedisArrayRange other) => _start == other._start && _end == other._end;
+
+ ///
+ /// Compares two values for equality.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ public static bool operator ==(RedisArrayRange x, RedisArrayRange y) => x._start == y._start && x._end == y._end;
+
+ ///
+ /// Compares two values for non-equality.
+ ///
+ /// The first to compare.
+ /// The second to compare.
+ public static bool operator !=(RedisArrayRange x, RedisArrayRange y) => x._start != y._start || x._end != y._end;
+}
diff --git a/src/StackExchange.Redis/ArrayGrepRequest.cs b/src/StackExchange.Redis/ArrayGrepRequest.cs
new file mode 100644
index 000000000..f3cbc0109
--- /dev/null
+++ b/src/StackExchange.Redis/ArrayGrepRequest.cs
@@ -0,0 +1,367 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Describes an array grep operation.
+///
+[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+public class ArrayGrepRequest
+{
+ [Flags]
+ private enum LocalFlags : byte
+ {
+ None = 0,
+ IsFrozen = 1 << 0,
+ CaseSensitive = 1 << 1,
+ IsIntersection = 1 << 2,
+ StartSpecified = 1 << 3,
+ EndSpecified = 1 << 4,
+ LimitSpecified = 1 << 5,
+ IncludeValues = 1 << 6,
+ }
+
+ private void Freeze() => _flags |= LocalFlags.IsFrozen;
+
+ private void ThrowIfFrozen()
+ {
+ if (GetFlag(LocalFlags.IsFrozen)) Throw();
+ static void Throw() => throw new InvalidOperationException("Cannot modify a frozen request");
+ }
+
+ private LocalFlags _flags;
+ private bool GetFlag(LocalFlags flag) => (_flags & flag) != 0;
+
+ private void SetFlag(LocalFlags flag, bool value)
+ {
+ if (GetFlag(flag) == value) return;
+
+ ThrowIfFrozen();
+ if (value)
+ {
+ _flags |= flag;
+ }
+ else
+ {
+ _flags &= ~flag;
+ }
+ }
+
+ private RedisArrayIndex _start, _end;
+
+ ///
+ /// The start index for the search, or to use the server's open-ended lower bound.
+ ///
+ public RedisArrayIndex? Start
+ {
+ get => GetFlag(LocalFlags.StartSpecified) ? _start : null;
+ set
+ {
+ if (value.HasValue)
+ {
+ var newValue = value.GetValueOrDefault();
+ if (!GetFlag(LocalFlags.StartSpecified) || _start != newValue)
+ {
+ ThrowIfFrozen();
+ _start = newValue;
+ }
+ SetFlag(LocalFlags.StartSpecified, true);
+ }
+ else
+ {
+ SetFlag(LocalFlags.StartSpecified, false);
+ }
+ }
+ }
+
+ ///
+ /// The end index for the search, or to use the server's open-ended upper bound.
+ ///
+ public RedisArrayIndex? End
+ {
+ get => GetFlag(LocalFlags.EndSpecified) ? _end : null;
+ set
+ {
+ if (value.HasValue)
+ {
+ var newValue = value.GetValueOrDefault();
+ if (!GetFlag(LocalFlags.EndSpecified) || _end != newValue)
+ {
+ ThrowIfFrozen();
+ _end = newValue;
+ }
+ SetFlag(LocalFlags.EndSpecified, true);
+ }
+ else
+ {
+ SetFlag(LocalFlags.EndSpecified, false);
+ }
+ }
+ }
+
+ ///
+ /// When specified, provide an upper bound to the matches returned.
+ ///
+ /// Corresponds to the LIMIT parameter.
+ public long? Limit
+ {
+ get => GetFlag(LocalFlags.LimitSpecified) ? _limit : null;
+ set
+ {
+ if (value.HasValue)
+ {
+ var newValue = value.GetValueOrDefault();
+ if (!GetFlag(LocalFlags.LimitSpecified) || _limit != newValue)
+ {
+ ThrowIfFrozen();
+ _limit = newValue;
+ }
+ SetFlag(LocalFlags.LimitSpecified, true);
+ }
+ else
+ {
+ SetFlag(LocalFlags.LimitSpecified, false);
+ }
+ }
+ }
+
+ private long _limit;
+
+ ///
+ /// Indicates whether matches are performed in a case-insensitive manner.
+ ///
+ /// Corresponds to the NOCASE parameter.
+ public bool IsCaseSensitive
+ {
+ get => GetFlag(LocalFlags.CaseSensitive);
+ set => SetFlag(LocalFlags.CaseSensitive, value);
+ }
+
+ ///
+ /// When multiple predicates are provided, this indicates whether they should be combined with a logical AND (true) or OR (false).
+ ///
+ /// Corresponds to the AND/OR parameter.
+ public bool IsIntersection
+ {
+ get => GetFlag(LocalFlags.IsIntersection);
+ set => SetFlag(LocalFlags.IsIntersection, value);
+ }
+
+ ///
+ /// Indicates whether to fetch values as part of the query.
+ ///
+ /// Corresponds to the WITHVALUES parameter.
+ public bool IncludeValues
+ {
+ get => GetFlag(LocalFlags.IncludeValues);
+ set => SetFlag(LocalFlags.IncludeValues, value);
+ }
+
+ private object? _predicates;
+
+ ///
+ /// Gets the predicate at the specified index.
+ ///
+ /// The predicate index.
+ public Predicate this[int index]
+ {
+ get
+ {
+ return _predicates switch
+ {
+ Predicate p when index is 0 => p,
+ List list => list[index],
+ _ => Throw(),
+ };
+
+ static Predicate Throw() => throw new IndexOutOfRangeException();
+ }
+ }
+
+ ///
+ /// The number of predicates in this request.
+ ///
+ public int Count => _predicates switch
+ {
+ null => 0,
+ Predicate p => 1,
+ List list => list.Count,
+ _ => 0,
+ };
+
+ ///
+ /// Adds a predicate to this request.
+ ///
+ /// The predicate to add.
+ public void AddPredicate(Predicate predicate)
+ {
+ ThrowIfFrozen();
+ switch (_predicates)
+ {
+ case null:
+ _predicates = predicate;
+ break;
+ case Predicate existing:
+ _predicates = new List { existing, predicate };
+ break;
+ default:
+ ((List)_predicates).Add(predicate);
+ break;
+ }
+ }
+
+ internal Message CreateMessage(int db, RedisKey key, CommandFlags flags)
+ {
+ Freeze();
+ return new ArrayGrepMessage(db, key, this, flags);
+ }
+
+ ///
+ /// Describes a predicate used by an array grep operation.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ public abstract class Predicate
+ {
+ internal virtual int ArgCount => 2;
+ internal abstract void WriteTo(PhysicalConnection physical);
+ private protected Predicate() { }
+
+ ///
+ /// Creates an exact-value predicate.
+ ///
+ /// The value to match.
+ public static Predicate Exact(RedisValue value) => new ExactPredicate(value);
+
+ ///
+ /// Creates a pattern-match predicate.
+ ///
+ /// The pattern to match.
+ public static Predicate Match(string value) => new MatchPredicate(value);
+
+ ///
+ /// Creates a glob predicate.
+ ///
+ /// The glob pattern to match.
+ public static Predicate Glob(string value) => new GlobPredicate(value);
+
+ ///
+ /// Creates a regular expression predicate.
+ ///
+ /// The regular expression to match.
+ public static Predicate Regex(
+ #if NET7_0_OR_GREATER
+ [StringSyntax(StringSyntaxAttribute.Regex)]
+ #endif
+ string value) => new RegexPredicate(value);
+
+ private sealed class ExactPredicate(RedisValue value) : Predicate
+ {
+ public override string ToString() => $"EXACT '{value}'";
+
+ internal override void WriteTo(PhysicalConnection physical)
+ {
+ physical.WriteRaw("$5\r\nEXACT\r\n"u8);
+ physical.WriteBulkString(value);
+ }
+ }
+
+ private sealed class MatchPredicate(string pattern) : Predicate
+ {
+ public override string ToString() => $"MATCH '{pattern}'";
+
+ internal override void WriteTo(PhysicalConnection physical)
+ {
+ physical.WriteRaw("$5\r\nMATCH\r\n"u8);
+ physical.WriteBulkString(pattern);
+ }
+ }
+
+ private sealed class GlobPredicate(string pattern) : Predicate
+ {
+ public override string ToString() => $"GLOB '{pattern}'";
+
+ internal override void WriteTo(PhysicalConnection physical)
+ {
+ physical.WriteRaw("$4\r\nGLOB\r\n"u8);
+ physical.WriteBulkString(pattern);
+ }
+ }
+
+ private sealed class RegexPredicate(string re) : Predicate
+ {
+ public override string ToString() => $"RE '{re}'";
+
+ internal override void WriteTo(PhysicalConnection physical)
+ {
+ physical.WriteRaw("$2\r\nRE\r\n"u8);
+ physical.WriteBulkString(re);
+ }
+ }
+ }
+
+ private sealed class ArrayGrepMessage(int db, RedisKey key, ArrayGrepRequest request, CommandFlags flags)
+ : Message(db, flags, RedisCommand.ARGREP)
+ {
+ public override int ArgCount
+ {
+ get
+ {
+ var count = 3; // key, start, end
+ var pCount = request.Count;
+ for (int i = 0; i < pCount; i++)
+ {
+ count += request[i].ArgCount;
+ }
+
+ if (request.IsIntersection) count++;
+ if (request.IsCaseSensitive) count++;
+ if (request.IncludeValues) count++;
+ var limit = request.Limit;
+ if (limit.HasValue) count += 2;
+ return count;
+ }
+ }
+
+ protected override void WriteImpl(PhysicalConnection physical)
+ {
+ physical.WriteHeader(Command, ArgCount);
+ physical.WriteBulkString(key);
+ var index = request.Start;
+ if (index.HasValue)
+ {
+ physical.WriteBulkString(index.GetValueOrDefault().Value);
+ }
+ else
+ {
+ physical.WriteRaw("$1\r\n-\r\n"u8);
+ }
+ index = request.End;
+ if (index.HasValue)
+ {
+ physical.WriteBulkString(index.GetValueOrDefault().Value);
+ }
+ else
+ {
+ physical.WriteRaw("$1\r\n+\r\n"u8);
+ }
+ var pCount = request.Count;
+ for (int i = 0; i < pCount; i++)
+ {
+ request[i].WriteTo(physical);
+ }
+
+ if (request.IsIntersection) physical.WriteRaw("$3\r\nAND\r\n"u8);
+ if (request.IsCaseSensitive) physical.WriteRaw("$6\r\nNOCASE\r\n"u8);
+ if (request.IncludeValues) physical.WriteRaw("$10\r\nWITHVALUES\r\n"u8);
+ var limit = request.Limit;
+ if (limit.HasValue)
+ {
+ physical.WriteRaw("$5\r\nLIMIT\r\n"u8);
+ physical.WriteBulkString(limit.GetValueOrDefault());
+ }
+ }
+ }
+}
diff --git a/src/StackExchange.Redis/ArrayInfo.cs b/src/StackExchange.Redis/ArrayInfo.cs
new file mode 100644
index 000000000..19506acaa
--- /dev/null
+++ b/src/StackExchange.Redis/ArrayInfo.cs
@@ -0,0 +1,53 @@
+using System.Diagnostics.CodeAnalysis;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Contains metadata information about an array returned by the ARINFO command.
+///
+[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+public readonly struct ArrayInfo(
+ RedisArrayIndex count,
+ RedisArrayIndex length,
+ RedisArrayIndex nextInsertIndex,
+ RedisArrayIndex slices,
+ RedisArrayIndex directorySize,
+ RedisArrayIndex superDirEntries,
+ RedisArrayIndex sliceSize)
+{
+ ///
+ /// The number of array cells that have values.
+ ///
+ public RedisArrayIndex Count { get; } = count;
+
+ ///
+ /// The notional length of the array.
+ ///
+ public RedisArrayIndex Length { get; } = length;
+
+ ///
+ /// The current array write-head.
+ ///
+ public RedisArrayIndex NextInsertIndex { get; } = nextInsertIndex;
+
+ ///
+ /// The number of slices used by the array.
+ ///
+ public RedisArrayIndex Slices { get; } = slices;
+
+ ///
+ /// The size of the array directory.
+ ///
+ public RedisArrayIndex DirectorySize { get; } = directorySize;
+
+ ///
+ /// The number of super-directory entries.
+ ///
+ public RedisArrayIndex SuperDirEntries { get; } = superDirEntries;
+
+ ///
+ /// The configured slice size.
+ ///
+ public RedisArrayIndex SliceSize { get; } = sliceSize;
+}
diff --git a/src/StackExchange.Redis/ArrayInfoField.cs b/src/StackExchange.Redis/ArrayInfoField.cs
new file mode 100644
index 000000000..aa523258e
--- /dev/null
+++ b/src/StackExchange.Redis/ArrayInfoField.cs
@@ -0,0 +1,67 @@
+using System;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Represents fields in an ARINFO response.
+///
+internal enum ArrayInfoField
+{
+ ///
+ /// Unknown or unrecognized field.
+ ///
+ [AsciiHash("")]
+ Unknown = 0,
+
+ ///
+ /// The count field.
+ ///
+ [AsciiHash("count")]
+ Count,
+
+ ///
+ /// The len field.
+ ///
+ [AsciiHash("len")]
+ Length,
+
+ ///
+ /// The next-insert-index field.
+ ///
+ [AsciiHash("next-insert-index")]
+ NextInsertIndex,
+
+ ///
+ /// The slices field.
+ ///
+ [AsciiHash("slices")]
+ Slices,
+
+ ///
+ /// The directory-size field.
+ ///
+ [AsciiHash("directory-size")]
+ DirectorySize,
+
+ ///
+ /// The super-dir-entries field.
+ ///
+ [AsciiHash("super-dir-entries")]
+ SuperDirEntries,
+
+ ///
+ /// The slice-size field.
+ ///
+ [AsciiHash("slice-size")]
+ SliceSize,
+}
+
+///
+/// Metadata and parsing methods for ArrayInfoField.
+///
+internal static partial class ArrayInfoFieldMetadata
+{
+ [AsciiHash]
+ internal static partial bool TryParse(ReadOnlySpan value, out ArrayInfoField field);
+}
diff --git a/src/StackExchange.Redis/Enums/ArrayOperation.cs b/src/StackExchange.Redis/Enums/ArrayOperation.cs
new file mode 100644
index 000000000..63a561bc3
--- /dev/null
+++ b/src/StackExchange.Redis/Enums/ArrayOperation.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics.CodeAnalysis;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+///
+/// Describes an array aggregation operation.
+///
+[Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+public enum ArrayOperation
+{
+ ///
+ /// An unknown operation.
+ ///
+ Unknown = 0,
+
+ ///
+ /// Computes the sum of values in the range.
+ ///
+ Sum,
+
+ ///
+ /// Finds the minimum value in the range.
+ ///
+ Min,
+
+ ///
+ /// Finds the maximum value in the range.
+ ///
+ Max,
+
+ ///
+ /// Computes a bitwise AND over values in the range.
+ ///
+ And,
+
+ ///
+ /// Computes a bitwise OR over values in the range.
+ ///
+ Or,
+
+ ///
+ /// Computes a bitwise XOR over values in the range.
+ ///
+ Xor,
+
+ ///
+ /// Counts values in the range that match an operand.
+ ///
+ Match,
+
+ ///
+ /// Counts used cells in the range.
+ ///
+ Used,
+}
diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs
index 4c8f3be5b..d01c37a44 100644
--- a/src/StackExchange.Redis/Enums/RedisCommand.cs
+++ b/src/StackExchange.Redis/Enums/RedisCommand.cs
@@ -12,6 +12,25 @@ internal enum RedisCommand
ASKING,
AUTH,
+ ARCOUNT,
+ ARDEL,
+ ARDELRANGE,
+ ARGET,
+ ARGETRANGE,
+ ARGREP,
+ ARINFO,
+ ARINSERT,
+ ARLASTITEMS,
+ ARLEN,
+ ARMGET,
+ ARMSET,
+ ARNEXT,
+ AROP,
+ ARRING,
+ ARSCAN,
+ ARSEEK,
+ ARSET,
+
BGREWRITEAOF,
BGSAVE,
BITCOUNT,
@@ -311,6 +330,13 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
// for example spreading load via a .DemandReplica flag in the caller.
// Basically: would it fail on a read-only replica in 100% of cases? Then it goes in the list.
case RedisCommand.APPEND:
+ case RedisCommand.ARDEL:
+ case RedisCommand.ARDELRANGE:
+ case RedisCommand.ARINSERT:
+ case RedisCommand.ARMSET:
+ case RedisCommand.ARRING:
+ case RedisCommand.ARSEEK:
+ case RedisCommand.ARSET:
case RedisCommand.BITOP:
case RedisCommand.BLPOP:
case RedisCommand.BRPOP:
@@ -410,6 +436,17 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.NONE:
case RedisCommand.ASKING:
case RedisCommand.AUTH:
+ case RedisCommand.ARCOUNT:
+ case RedisCommand.ARGET:
+ case RedisCommand.ARGETRANGE:
+ case RedisCommand.ARGREP:
+ case RedisCommand.ARINFO:
+ case RedisCommand.ARLASTITEMS:
+ case RedisCommand.ARLEN:
+ case RedisCommand.ARMGET:
+ case RedisCommand.ARNEXT:
+ case RedisCommand.AROP:
+ case RedisCommand.ARSCAN:
case RedisCommand.BGREWRITEAOF:
case RedisCommand.BGSAVE:
case RedisCommand.BITCOUNT:
diff --git a/src/StackExchange.Redis/Enums/RedisType.cs b/src/StackExchange.Redis/Enums/RedisType.cs
index 90a41165b..7ab7b8ab1 100644
--- a/src/StackExchange.Redis/Enums/RedisType.cs
+++ b/src/StackExchange.Redis/Enums/RedisType.cs
@@ -1,4 +1,7 @@
-namespace StackExchange.Redis
+using System.Diagnostics.CodeAnalysis;
+using RESPite;
+
+namespace StackExchange.Redis
{
///
/// The intrinsic data-types supported by redis.
@@ -71,5 +74,11 @@ public enum RedisType
/// vector set elements have a string representation of a vector.
///
VectorSet,
+
+ ///
+ /// Redis Arrays are sparse arrays of arbitrary values with a notional write head.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Array,
}
}
diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.Arrays.cs b/src/StackExchange.Redis/Interfaces/IDatabase.Arrays.cs
new file mode 100644
index 000000000..9dbb7b755
--- /dev/null
+++ b/src/StackExchange.Redis/Interfaces/IDatabase.Arrays.cs
@@ -0,0 +1,153 @@
+#pragma warning disable RS0026 // similar overloads
+
+using System.Diagnostics.CodeAnalysis;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+public partial interface IDatabase
+{
+ ///
+ /// Sets the value at the specified array index.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ bool ArraySet(RedisKey key, RedisArrayIndex index, RedisValue value, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Sets a contiguous range of array values starting at the specified index.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ int ArraySet(RedisKey key, RedisArrayIndex index, RedisValue[] values, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Sets values at multiple array indices.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ int ArraySet(RedisKey key, RedisArrayEntry[] values, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the value at the specified array index.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisValue ArrayGet(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the values at the specified array indices.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisValue[] ArrayGet(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets values in the specified array index range.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisValue[] ArrayGetRange(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the notional length of the array.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex ArrayLength(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the number of array cells that have values.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex ArrayCount(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Deletes the value at the specified array index.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ bool ArrayDelete(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Deletes values at the specified array indices.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ int ArrayDelete(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Deletes values in the specified array index range.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex ArrayDeleteRange(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Deletes values in the specified array index ranges.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex ArrayDeleteRange(RedisKey key, RedisArrayRange[] ranges, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the non-empty values in the specified array index range.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayEntry[] ArrayScan(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, int limit = 0, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the array indices matching the specified grep request, optionally with values.
+ ///
+ ///
+ /// When is , returned entries contain only indices.
+ /// When is , returned entries contain indices and values.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayEntry[] ArrayGrep(RedisKey key, ArrayGrepRequest request, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Performs an operation over the specified array range.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisValue ArrayOperation(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, ArrayOperation operation, RedisValue operand = default, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Adds a value to a ring-buffer array.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex ArrayRing(RedisKey key, RedisArrayIndex maxLength, RedisValue value, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Adds values to a ring-buffer array.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex ArrayRing(RedisKey key, RedisArrayIndex maxLength, RedisValue[] values, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the current array write-head.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex? ArrayNext(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Inserts a value at the current array write-head.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex ArrayInsert(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Inserts values at the current array write-head.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisArrayIndex ArrayInsert(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Moves the array write-head to the specified index.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ bool ArraySeek(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the previous array items.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ RedisValue[] ArrayLastItems(RedisKey key, int count, bool reverse = false, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets array metadata.
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ ArrayInfo ArrayInfo(RedisKey key, CommandFlags flags = CommandFlags.None);
+}
+
+#pragma warning restore RS0026
diff --git a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.Arrays.cs b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.Arrays.cs
new file mode 100644
index 000000000..1c5fdd2de
--- /dev/null
+++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.Arrays.cs
@@ -0,0 +1,104 @@
+#pragma warning disable RS0026 // similar overloads
+
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using RESPite;
+
+namespace StackExchange.Redis;
+
+public partial interface IDatabaseAsync
+{
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArraySetAsync(RedisKey key, RedisArrayIndex index, RedisValue value, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArraySetAsync(RedisKey key, RedisArrayIndex index, RedisValue[] values, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArraySetAsync(RedisKey key, RedisArrayEntry[] values, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayGetAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayGetAsync(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayGetRangeAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayDeleteAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayDeleteAsync(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayDeleteRangeAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayDeleteRangeAsync(RedisKey key, RedisArrayRange[] ranges, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayScanAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, int limit = 0, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayGrepAsync(RedisKey key, ArrayGrepRequest request, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayOperationAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, ArrayOperation operation, RedisValue operand = default, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayRingAsync(RedisKey key, RedisArrayIndex maxLength, RedisValue value, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayRingAsync(RedisKey key, RedisArrayIndex maxLength, RedisValue[] values, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayNextAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayInsertAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayInsertAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArraySeekAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayLastItemsAsync(RedisKey key, int count, bool reverse = false, CommandFlags flags = CommandFlags.None);
+
+ ///
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ Task ArrayInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None);
+}
+
+#pragma warning restore RS0026
diff --git a/src/StackExchange.Redis/KeyNotificationType.cs b/src/StackExchange.Redis/KeyNotificationType.cs
index bf0db0991..237444a4e 100644
--- a/src/StackExchange.Redis/KeyNotificationType.cs
+++ b/src/StackExchange.Redis/KeyNotificationType.cs
@@ -114,6 +114,12 @@ public enum KeyNotificationType
ZRem = 49,
[AsciiHash("hexpire")]
HExpire = 50,
+ [AsciiHash("ardel")]
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ ArDel = 51,
+ [AsciiHash("ardelrange")]
+ [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)]
+ ArDelRange = 52,
// side-effect notifications
[AsciiHash("expired")]
diff --git a/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs b/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs
index ff11f4092..06dd08f06 100644
--- a/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs
+++ b/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs
@@ -68,6 +68,8 @@ public static KeyNotificationType Parse(ReadOnlySpan value)
KeyNotificationType.ZRemByScore => "zrembyscore"u8,
KeyNotificationType.ZRem => "zrem"u8,
KeyNotificationType.HExpire => "hexpire"u8,
+ KeyNotificationType.ArDel => "ardel"u8,
+ KeyNotificationType.ArDelRange => "ardelrange"u8,
KeyNotificationType.Expired => "expired"u8,
KeyNotificationType.Evicted => "evicted"u8,
KeyNotificationType.New => "new"u8,
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.Arrays.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.Arrays.cs
new file mode 100644
index 000000000..5cf08fb63
--- /dev/null
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.Arrays.cs
@@ -0,0 +1,76 @@
+using System.Threading.Tasks;
+
+// ReSharper disable once CheckNamespace
+namespace StackExchange.Redis.KeyspaceIsolation;
+
+internal partial class KeyPrefixed
+{
+ public Task ArraySetAsync(RedisKey key, RedisArrayIndex index, RedisValue value, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArraySetAsync(ToInner(key), index, value, flags);
+
+ public Task ArraySetAsync(RedisKey key, RedisArrayIndex index, RedisValue[] values, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArraySetAsync(ToInner(key), index, values, flags);
+
+ public Task ArraySetAsync(RedisKey key, RedisArrayEntry[] values, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArraySetAsync(ToInner(key), values, flags);
+
+ public Task ArrayGetAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayGetAsync(ToInner(key), index, flags);
+
+ public Task ArrayGetAsync(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayGetAsync(ToInner(key), indices, flags);
+
+ public Task ArrayGetRangeAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayGetRangeAsync(ToInner(key), start, end, flags);
+
+ public Task ArrayLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayLengthAsync(ToInner(key), flags);
+
+ public Task ArrayCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayCountAsync(ToInner(key), flags);
+
+ public Task ArrayDeleteAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayDeleteAsync(ToInner(key), index, flags);
+
+ public Task ArrayDeleteAsync(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayDeleteAsync(ToInner(key), indices, flags);
+
+ public Task ArrayDeleteRangeAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayDeleteRangeAsync(ToInner(key), start, end, flags);
+
+ public Task ArrayDeleteRangeAsync(RedisKey key, RedisArrayRange[] ranges, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayDeleteRangeAsync(ToInner(key), ranges, flags);
+
+ public Task ArrayScanAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, int limit = 0, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayScanAsync(ToInner(key), start, end, limit, flags);
+
+ public Task ArrayGrepAsync(RedisKey key, ArrayGrepRequest request, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayGrepAsync(ToInner(key), request, flags);
+
+ public Task ArrayOperationAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, ArrayOperation operation, RedisValue operand = default, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayOperationAsync(ToInner(key), start, end, operation, operand, flags);
+
+ public Task ArrayRingAsync(RedisKey key, RedisArrayIndex maxLength, RedisValue value, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayRingAsync(ToInner(key), maxLength, value, flags);
+
+ public Task ArrayRingAsync(RedisKey key, RedisArrayIndex maxLength, RedisValue[] values, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayRingAsync(ToInner(key), maxLength, values, flags);
+
+ public Task ArrayNextAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayNextAsync(ToInner(key), flags);
+
+ public Task ArrayInsertAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayInsertAsync(ToInner(key), value, flags);
+
+ public Task ArrayInsertAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayInsertAsync(ToInner(key), values, flags);
+
+ public Task ArraySeekAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArraySeekAsync(ToInner(key), index, flags);
+
+ public Task ArrayLastItemsAsync(RedisKey key, int count, bool reverse = false, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayLastItemsAsync(ToInner(key), count, reverse, flags);
+
+ public Task ArrayInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayInfoAsync(ToInner(key), flags);
+}
diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.Arrays.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.Arrays.cs
new file mode 100644
index 000000000..6fb17fa8a
--- /dev/null
+++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.Arrays.cs
@@ -0,0 +1,74 @@
+// ReSharper disable once CheckNamespace
+namespace StackExchange.Redis.KeyspaceIsolation;
+
+internal sealed partial class KeyPrefixedDatabase
+{
+ public bool ArraySet(RedisKey key, RedisArrayIndex index, RedisValue value, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArraySet(ToInner(key), index, value, flags);
+
+ public int ArraySet(RedisKey key, RedisArrayIndex index, RedisValue[] values, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArraySet(ToInner(key), index, values, flags);
+
+ public int ArraySet(RedisKey key, RedisArrayEntry[] values, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArraySet(ToInner(key), values, flags);
+
+ public RedisValue ArrayGet(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayGet(ToInner(key), index, flags);
+
+ public RedisValue[] ArrayGet(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayGet(ToInner(key), indices, flags);
+
+ public RedisValue[] ArrayGetRange(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayGetRange(ToInner(key), start, end, flags);
+
+ public RedisArrayIndex ArrayLength(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayLength(ToInner(key), flags);
+
+ public RedisArrayIndex ArrayCount(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayCount(ToInner(key), flags);
+
+ public bool ArrayDelete(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayDelete(ToInner(key), index, flags);
+
+ public int ArrayDelete(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayDelete(ToInner(key), indices, flags);
+
+ public RedisArrayIndex ArrayDeleteRange(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayDeleteRange(ToInner(key), start, end, flags);
+
+ public RedisArrayIndex ArrayDeleteRange(RedisKey key, RedisArrayRange[] ranges, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayDeleteRange(ToInner(key), ranges, flags);
+
+ public RedisArrayEntry[] ArrayScan(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, int limit = 0, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayScan(ToInner(key), start, end, limit, flags);
+
+ public RedisArrayEntry[] ArrayGrep(RedisKey key, ArrayGrepRequest request, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayGrep(ToInner(key), request, flags);
+
+ public RedisValue ArrayOperation(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, ArrayOperation operation, RedisValue operand = default, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayOperation(ToInner(key), start, end, operation, operand, flags);
+
+ public RedisArrayIndex ArrayRing(RedisKey key, RedisArrayIndex maxLength, RedisValue value, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayRing(ToInner(key), maxLength, value, flags);
+
+ public RedisArrayIndex ArrayRing(RedisKey key, RedisArrayIndex maxLength, RedisValue[] values, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayRing(ToInner(key), maxLength, values, flags);
+
+ public RedisArrayIndex? ArrayNext(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayNext(ToInner(key), flags);
+
+ public RedisArrayIndex ArrayInsert(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayInsert(ToInner(key), value, flags);
+
+ public RedisArrayIndex ArrayInsert(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayInsert(ToInner(key), values, flags);
+
+ public bool ArraySeek(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArraySeek(ToInner(key), index, flags);
+
+ public RedisValue[] ArrayLastItems(RedisKey key, int count, bool reverse = false, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayLastItems(ToInner(key), count, reverse, flags);
+
+ public ArrayInfo ArrayInfo(RedisKey key, CommandFlags flags = CommandFlags.None) =>
+ Inner.ArrayInfo(ToInner(key), flags);
+}
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
index ab058de62..7e2a4a4de 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
@@ -1 +1,136 @@
#nullable enable
+[SER006]StackExchange.Redis.ArrayGrepRequest
+[SER006]StackExchange.Redis.ArrayGrepRequest.AddPredicate(StackExchange.Redis.ArrayGrepRequest.Predicate! predicate) -> void
+[SER006]StackExchange.Redis.ArrayGrepRequest.ArrayGrepRequest() -> void
+[SER006]StackExchange.Redis.ArrayGrepRequest.Count.get -> int
+[SER006]StackExchange.Redis.ArrayGrepRequest.End.get -> StackExchange.Redis.RedisArrayIndex?
+[SER006]StackExchange.Redis.ArrayGrepRequest.End.set -> void
+[SER006]StackExchange.Redis.ArrayGrepRequest.IncludeValues.get -> bool
+[SER006]StackExchange.Redis.ArrayGrepRequest.IncludeValues.set -> void
+[SER006]StackExchange.Redis.ArrayGrepRequest.IsCaseSensitive.get -> bool
+[SER006]StackExchange.Redis.ArrayGrepRequest.IsCaseSensitive.set -> void
+[SER006]StackExchange.Redis.ArrayGrepRequest.IsIntersection.get -> bool
+[SER006]StackExchange.Redis.ArrayGrepRequest.IsIntersection.set -> void
+[SER006]StackExchange.Redis.ArrayGrepRequest.Limit.get -> long?
+[SER006]StackExchange.Redis.ArrayGrepRequest.Limit.set -> void
+[SER006]StackExchange.Redis.ArrayGrepRequest.Predicate
+[SER006]StackExchange.Redis.ArrayGrepRequest.Start.get -> StackExchange.Redis.RedisArrayIndex?
+[SER006]StackExchange.Redis.ArrayGrepRequest.Start.set -> void
+[SER006]StackExchange.Redis.ArrayGrepRequest.this[int index].get -> StackExchange.Redis.ArrayGrepRequest.Predicate!
+[SER006]StackExchange.Redis.ArrayInfo
+[SER006]StackExchange.Redis.ArrayInfo.ArrayInfo() -> void
+[SER006]StackExchange.Redis.ArrayInfo.ArrayInfo(StackExchange.Redis.RedisArrayIndex count, StackExchange.Redis.RedisArrayIndex length, StackExchange.Redis.RedisArrayIndex nextInsertIndex, StackExchange.Redis.RedisArrayIndex slices, StackExchange.Redis.RedisArrayIndex directorySize, StackExchange.Redis.RedisArrayIndex superDirEntries, StackExchange.Redis.RedisArrayIndex sliceSize) -> void
+[SER006]StackExchange.Redis.ArrayInfo.Count.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.ArrayInfo.DirectorySize.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.ArrayInfo.Length.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.ArrayInfo.NextInsertIndex.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.ArrayInfo.SliceSize.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.ArrayInfo.Slices.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.ArrayInfo.SuperDirEntries.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.And = 4 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.Match = 7 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.Max = 3 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.Min = 2 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.Or = 5 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.Sum = 1 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.Unknown = 0 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.Used = 8 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.ArrayOperation.Xor = 6 -> StackExchange.Redis.ArrayOperation
+[SER006]StackExchange.Redis.IDatabase.ArrayCount(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.IDatabase.ArrayDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
+[SER006]StackExchange.Redis.IDatabase.ArrayDelete(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex[]! indices, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int
+[SER006]StackExchange.Redis.IDatabase.ArrayDeleteRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.IDatabase.ArrayDeleteRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayRange[]! ranges, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.IDatabase.ArrayGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+[SER006]StackExchange.Redis.IDatabase.ArrayGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex[]! indices, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
+[SER006]StackExchange.Redis.IDatabase.ArrayGetRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
+[SER006]StackExchange.Redis.IDatabase.ArrayGrep(StackExchange.Redis.RedisKey key, StackExchange.Redis.ArrayGrepRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayEntry[]!
+[SER006]StackExchange.Redis.IDatabase.ArrayInfo(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ArrayInfo
+[SER006]StackExchange.Redis.IDatabase.ArrayInsert(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.IDatabase.ArrayInsert(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.IDatabase.ArrayLastItems(StackExchange.Redis.RedisKey key, int count, bool reverse = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
+[SER006]StackExchange.Redis.IDatabase.ArrayLength(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.IDatabase.ArrayNext(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex?
+[SER006]StackExchange.Redis.IDatabase.ArrayOperation(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end, StackExchange.Redis.ArrayOperation operation, StackExchange.Redis.RedisValue operand = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+[SER006]StackExchange.Redis.IDatabase.ArrayRing(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex maxLength, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.IDatabase.ArrayRing(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex maxLength, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.IDatabase.ArrayScan(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end, int limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisArrayEntry[]!
+[SER006]StackExchange.Redis.IDatabase.ArraySeek(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
+[SER006]StackExchange.Redis.IDatabase.ArraySet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayEntry[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int
+[SER006]StackExchange.Redis.IDatabase.ArraySet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
+[SER006]StackExchange.Redis.IDatabase.ArraySet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> int
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayCountAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayDeleteAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex[]! indices, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayDeleteRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayDeleteRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayRange[]! ranges, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayGetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex[]! indices, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayGetRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayGrepAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.ArrayGrepRequest! request, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayInfoAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayInsertAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayInsertAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayLastItemsAsync(StackExchange.Redis.RedisKey key, int count, bool reverse = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayLengthAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayNextAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayOperationAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end, StackExchange.Redis.ArrayOperation operation, StackExchange.Redis.RedisValue operand = default(StackExchange.Redis.RedisValue), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayRingAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex maxLength, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayRingAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex maxLength, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArrayScanAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end, int limit = 0, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArraySeekAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArraySetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayEntry[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArraySetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.IDatabaseAsync.ArraySetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+[SER006]StackExchange.Redis.KeyNotificationType.ArDel = 51 -> StackExchange.Redis.KeyNotificationType
+[SER006]StackExchange.Redis.KeyNotificationType.ArDelRange = 52 -> StackExchange.Redis.KeyNotificationType
+[SER006]StackExchange.Redis.RedisArrayEntry
+[SER006]StackExchange.Redis.RedisArrayEntry.Equals(StackExchange.Redis.RedisArrayEntry other) -> bool
+[SER006]StackExchange.Redis.RedisArrayEntry.Index.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.RedisArrayEntry.RedisArrayEntry() -> void
+[SER006]StackExchange.Redis.RedisArrayEntry.RedisArrayEntry(StackExchange.Redis.RedisArrayIndex index, StackExchange.Redis.RedisValue value) -> void
+[SER006]StackExchange.Redis.RedisArrayEntry.Value.get -> StackExchange.Redis.RedisValue
+[SER006]StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.RedisArrayIndex.Equals(StackExchange.Redis.RedisArrayIndex other) -> bool
+[SER006]StackExchange.Redis.RedisArrayIndex.RedisArrayIndex() -> void
+[SER006]StackExchange.Redis.RedisArrayIndex.RedisArrayIndex(int value) -> void
+[SER006]StackExchange.Redis.RedisArrayIndex.RedisArrayIndex(long value) -> void
+[SER006]StackExchange.Redis.RedisArrayIndex.RedisArrayIndex(ulong value) -> void
+[SER006]StackExchange.Redis.RedisArrayIndex.Value.get -> ulong
+[SER006]StackExchange.Redis.RedisArrayRange
+[SER006]StackExchange.Redis.RedisArrayRange.End.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.RedisArrayRange.Equals(StackExchange.Redis.RedisArrayRange other) -> bool
+[SER006]StackExchange.Redis.RedisArrayRange.RedisArrayRange() -> void
+[SER006]StackExchange.Redis.RedisArrayRange.RedisArrayRange(StackExchange.Redis.RedisArrayIndex start, StackExchange.Redis.RedisArrayIndex end) -> void
+[SER006]StackExchange.Redis.RedisArrayRange.Start.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]StackExchange.Redis.RedisType.Array = 9 -> StackExchange.Redis.RedisType
+[SER006]override StackExchange.Redis.RedisArrayEntry.Equals(object? obj) -> bool
+[SER006]override StackExchange.Redis.RedisArrayEntry.GetHashCode() -> int
+[SER006]override StackExchange.Redis.RedisArrayEntry.ToString() -> string!
+[SER006]override StackExchange.Redis.RedisArrayIndex.Equals(object? obj) -> bool
+[SER006]override StackExchange.Redis.RedisArrayIndex.GetHashCode() -> int
+[SER006]override StackExchange.Redis.RedisArrayIndex.ToString() -> string!
+[SER006]override StackExchange.Redis.RedisArrayRange.Equals(object? obj) -> bool
+[SER006]override StackExchange.Redis.RedisArrayRange.GetHashCode() -> int
+[SER006]override StackExchange.Redis.RedisArrayRange.ToString() -> string!
+[SER006]static StackExchange.Redis.ArrayGrepRequest.Predicate.Exact(StackExchange.Redis.RedisValue value) -> StackExchange.Redis.ArrayGrepRequest.Predicate!
+[SER006]static StackExchange.Redis.ArrayGrepRequest.Predicate.Glob(string! value) -> StackExchange.Redis.ArrayGrepRequest.Predicate!
+[SER006]static StackExchange.Redis.ArrayGrepRequest.Predicate.Match(string! value) -> StackExchange.Redis.ArrayGrepRequest.Predicate!
+[SER006]static StackExchange.Redis.ArrayGrepRequest.Predicate.Regex(string! value) -> StackExchange.Redis.ArrayGrepRequest.Predicate!
+[SER006]static StackExchange.Redis.RedisArrayEntry.implicit operator StackExchange.Redis.RedisArrayEntry(System.Collections.Generic.KeyValuePair value) -> StackExchange.Redis.RedisArrayEntry
+[SER006]static StackExchange.Redis.RedisArrayEntry.implicit operator System.Collections.Generic.KeyValuePair(StackExchange.Redis.RedisArrayEntry value) -> System.Collections.Generic.KeyValuePair
+[SER006]static StackExchange.Redis.RedisArrayEntry.operator !=(StackExchange.Redis.RedisArrayEntry x, StackExchange.Redis.RedisArrayEntry y) -> bool
+[SER006]static StackExchange.Redis.RedisArrayEntry.operator ==(StackExchange.Redis.RedisArrayEntry x, StackExchange.Redis.RedisArrayEntry y) -> bool
+[SER006]static StackExchange.Redis.RedisArrayIndex.MaxValue.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]static StackExchange.Redis.RedisArrayIndex.MinValue.get -> StackExchange.Redis.RedisArrayIndex
+[SER006]static StackExchange.Redis.RedisArrayIndex.implicit operator StackExchange.Redis.RedisArrayIndex(int value) -> StackExchange.Redis.RedisArrayIndex
+[SER006]static StackExchange.Redis.RedisArrayIndex.implicit operator StackExchange.Redis.RedisArrayIndex(long value) -> StackExchange.Redis.RedisArrayIndex
+[SER006]static StackExchange.Redis.RedisArrayIndex.implicit operator StackExchange.Redis.RedisArrayIndex(ulong value) -> StackExchange.Redis.RedisArrayIndex
+[SER006]static StackExchange.Redis.RedisArrayIndex.implicit operator ulong(StackExchange.Redis.RedisArrayIndex value) -> ulong
+[SER006]static StackExchange.Redis.RedisArrayIndex.explicit operator int(StackExchange.Redis.RedisArrayIndex value) -> int
+[SER006]static StackExchange.Redis.RedisArrayIndex.explicit operator long(StackExchange.Redis.RedisArrayIndex value) -> long
+[SER006]static StackExchange.Redis.RedisArrayIndex.operator !=(StackExchange.Redis.RedisArrayIndex x, StackExchange.Redis.RedisArrayIndex y) -> bool
+[SER006]static StackExchange.Redis.RedisArrayIndex.operator ==(StackExchange.Redis.RedisArrayIndex x, StackExchange.Redis.RedisArrayIndex y) -> bool
+[SER006]static StackExchange.Redis.RedisArrayRange.operator !=(StackExchange.Redis.RedisArrayRange x, StackExchange.Redis.RedisArrayRange y) -> bool
+[SER006]static StackExchange.Redis.RedisArrayRange.operator ==(StackExchange.Redis.RedisArrayRange x, StackExchange.Redis.RedisArrayRange y) -> bool
diff --git a/src/StackExchange.Redis/RedisDatabase.Arrays.cs b/src/StackExchange.Redis/RedisDatabase.Arrays.cs
new file mode 100644
index 000000000..3b35e8d3f
--- /dev/null
+++ b/src/StackExchange.Redis/RedisDatabase.Arrays.cs
@@ -0,0 +1,424 @@
+using System;
+using System.Threading.Tasks;
+
+// ReSharper disable once CheckNamespace
+namespace StackExchange.Redis;
+
+internal partial class RedisDatabase
+{
+ public bool ArraySet(RedisKey key, RedisArrayIndex index, RedisValue value, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARSET, key, index.ToRedisValue(), value);
+ return ExecuteSync(msg, ResultProcessor.Boolean);
+ }
+
+ public int ArraySet(RedisKey key, RedisArrayIndex index, RedisValue[] values, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArraySetMessage(key, index, values, flags);
+ return msg is null ? 0 : ExecuteSync(msg, ResultProcessor.Int32);
+ }
+
+ public int ArraySet(RedisKey key, RedisArrayEntry[] values, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArraySetMessage(key, values, flags);
+ return msg is null ? 0 : ExecuteSync(msg, ResultProcessor.Int32);
+ }
+
+ public RedisValue ArrayGet(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARGET, key, index.ToRedisValue());
+ return ExecuteSync(msg, ResultProcessor.RedisValue);
+ }
+
+ public RedisValue[] ArrayGet(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayIndicesMessage(RedisCommand.ARMGET, key, indices, flags);
+ return msg is null ? Array.Empty() : ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public RedisValue[] ArrayGetRange(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARGETRANGE, key, start.ToRedisValue(), end.ToRedisValue());
+ return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public RedisArrayIndex ArrayLength(RedisKey key, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARLEN, key);
+ return ExecuteSync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public RedisArrayIndex ArrayCount(RedisKey key, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARCOUNT, key);
+ return ExecuteSync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public bool ArrayDelete(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARDEL, key, index.ToRedisValue());
+ return ExecuteSync(msg, ResultProcessor.Boolean);
+ }
+
+ public int ArrayDelete(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayIndicesMessage(RedisCommand.ARDEL, key, indices, flags);
+ return msg is null ? 0 : ExecuteSync(msg, ResultProcessor.Int32);
+ }
+
+ public RedisArrayIndex ArrayDeleteRange(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARDELRANGE, key, start.ToRedisValue(), end.ToRedisValue());
+ return ExecuteSync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public RedisArrayIndex ArrayDeleteRange(RedisKey key, RedisArrayRange[] ranges, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayRangesMessage(key, ranges, flags);
+ return msg is null ? default : ExecuteSync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public RedisArrayEntry[] ArrayScan(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, int limit = 0, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayScanMessage(key, start, end, limit, flags);
+ return ExecuteSync(msg, ResultProcessor.RedisArrayEntryArray, defaultValue: Array.Empty());
+ }
+
+ public RedisArrayEntry[] ArrayGrep(RedisKey key, ArrayGrepRequest request, CommandFlags flags = CommandFlags.None)
+ {
+ if (request == null) throw new ArgumentNullException(nameof(request));
+ var msg = request.CreateMessage(Database, key, flags);
+ var processor = request.IncludeValues ? ResultProcessor.RedisArrayEntryArray : ResultProcessor.RedisArrayIndexEntryArray;
+ return ExecuteSync(msg, processor, defaultValue: Array.Empty());
+ }
+
+ public RedisValue ArrayOperation(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, ArrayOperation operation, RedisValue operand = default, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayOperationMessage(key, start, end, operation, operand, flags);
+ return ExecuteSync(msg, ResultProcessor.RedisValue);
+ }
+
+ public RedisArrayIndex ArrayRing(RedisKey key, RedisArrayIndex maxLength, RedisValue value, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARRING, key, maxLength.ToRedisValue(), value);
+ return ExecuteSync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public RedisArrayIndex ArrayRing(RedisKey key, RedisArrayIndex maxLength, RedisValue[] values, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayRingMessage(key, maxLength, values, flags);
+ return ExecuteSync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public RedisArrayIndex? ArrayNext(RedisKey key, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARNEXT, key);
+ return ExecuteSync(msg, ResultProcessor.NullableRedisArrayIndex);
+ }
+
+ public RedisArrayIndex ArrayInsert(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARINSERT, key, value);
+ return ExecuteSync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public RedisArrayIndex ArrayInsert(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayValuesMessage(RedisCommand.ARINSERT, key, values, flags);
+ return ExecuteSync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public bool ArraySeek(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARSEEK, key, index.ToRedisValue());
+ return ExecuteSync(msg, ResultProcessor.Boolean);
+ }
+
+ public RedisValue[] ArrayLastItems(RedisKey key, int count, bool reverse = false, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayLastItemsMessage(key, count, reverse, flags);
+ return msg is null ? Array.Empty() : ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public ArrayInfo ArrayInfo(RedisKey key, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARINFO, key);
+ return ExecuteSync(msg, ResultProcessor.ArrayInfo);
+ }
+
+ public Task ArraySetAsync(RedisKey key, RedisArrayIndex index, RedisValue value, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARSET, key, index.ToRedisValue(), value);
+ return ExecuteAsync(msg, ResultProcessor.Boolean);
+ }
+
+ public Task ArraySetAsync(RedisKey key, RedisArrayIndex index, RedisValue[] values, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArraySetMessage(key, index, values, flags);
+ return msg is null ? CompletedTask.FromDefault(0, asyncState) : ExecuteAsync(msg, ResultProcessor.Int32);
+ }
+
+ public Task ArraySetAsync(RedisKey key, RedisArrayEntry[] values, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArraySetMessage(key, values, flags);
+ return msg is null ? CompletedTask.FromDefault(0, asyncState) : ExecuteAsync(msg, ResultProcessor.Int32);
+ }
+
+ public Task ArrayGetAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARGET, key, index.ToRedisValue());
+ return ExecuteAsync(msg, ResultProcessor.RedisValue);
+ }
+
+ public Task ArrayGetAsync(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayIndicesMessage(RedisCommand.ARMGET, key, indices, flags);
+ return msg is null
+ ? CompletedTask.FromDefault(Array.Empty(), asyncState)
+ : ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public Task ArrayGetRangeAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARGETRANGE, key, start.ToRedisValue(), end.ToRedisValue());
+ return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public Task ArrayLengthAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARLEN, key);
+ return ExecuteAsync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public Task ArrayCountAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARCOUNT, key);
+ return ExecuteAsync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public Task ArrayDeleteAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARDEL, key, index.ToRedisValue());
+ return ExecuteAsync(msg, ResultProcessor.Boolean);
+ }
+
+ public Task ArrayDeleteAsync(RedisKey key, RedisArrayIndex[] indices, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayIndicesMessage(RedisCommand.ARDEL, key, indices, flags);
+ return msg is null ? CompletedTask.FromDefault(0, asyncState) : ExecuteAsync(msg, ResultProcessor.Int32);
+ }
+
+ public Task ArrayDeleteRangeAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARDELRANGE, key, start.ToRedisValue(), end.ToRedisValue());
+ return ExecuteAsync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public Task ArrayDeleteRangeAsync(RedisKey key, RedisArrayRange[] ranges, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayRangesMessage(key, ranges, flags);
+ return msg is null ? CompletedTask.FromDefault(default, asyncState) : ExecuteAsync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public Task ArrayScanAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, int limit = 0, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayScanMessage(key, start, end, limit, flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisArrayEntryArray, defaultValue: Array.Empty());
+ }
+
+ public Task ArrayGrepAsync(RedisKey key, ArrayGrepRequest request, CommandFlags flags = CommandFlags.None)
+ {
+ if (request == null) throw new ArgumentNullException(nameof(request));
+ var msg = request.CreateMessage(Database, key, flags);
+ var processor = request.IncludeValues ? ResultProcessor.RedisArrayEntryArray : ResultProcessor.RedisArrayIndexEntryArray;
+ return ExecuteAsync(msg, processor, defaultValue: Array.Empty());
+ }
+
+ public Task ArrayOperationAsync(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, ArrayOperation operation, RedisValue operand = default, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayOperationMessage(key, start, end, operation, operand, flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisValue);
+ }
+
+ public Task ArrayRingAsync(RedisKey key, RedisArrayIndex maxLength, RedisValue value, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARRING, key, maxLength.ToRedisValue(), value);
+ return ExecuteAsync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public Task ArrayRingAsync(RedisKey key, RedisArrayIndex maxLength, RedisValue[] values, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayRingMessage(key, maxLength, values, flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public Task ArrayNextAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARNEXT, key);
+ return ExecuteAsync(msg, ResultProcessor.NullableRedisArrayIndex);
+ }
+
+ public Task ArrayInsertAsync(RedisKey key, RedisValue value, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARINSERT, key, value);
+ return ExecuteAsync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public Task ArrayInsertAsync(RedisKey key, RedisValue[] values, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayValuesMessage(RedisCommand.ARINSERT, key, values, flags);
+ return ExecuteAsync(msg, ResultProcessor.RedisArrayIndex);
+ }
+
+ public Task ArraySeekAsync(RedisKey key, RedisArrayIndex index, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARSEEK, key, index.ToRedisValue());
+ return ExecuteAsync(msg, ResultProcessor.Boolean);
+ }
+
+ public Task ArrayLastItemsAsync(RedisKey key, int count, bool reverse = false, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = GetArrayLastItemsMessage(key, count, reverse, flags);
+ return msg is null
+ ? CompletedTask.FromDefault(Array.Empty(), asyncState)
+ : ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public Task ArrayInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(Database, flags, RedisCommand.ARINFO, key);
+ return ExecuteAsync(msg, ResultProcessor.ArrayInfo);
+ }
+
+ private Message? GetArraySetMessage(RedisKey key, RedisArrayIndex index, RedisValue[] values, CommandFlags flags)
+ {
+ if (values == null) throw new ArgumentNullException(nameof(values));
+ if (values.Length == 0) return null;
+
+ var args = new RedisValue[values.Length + 1];
+ args[0] = index.ToRedisValue();
+ Array.Copy(values, 0, args, 1, values.Length);
+ return Message.Create(Database, flags, RedisCommand.ARSET, key, args);
+ }
+
+ private Message? GetArraySetMessage(RedisKey key, RedisArrayEntry[] values, CommandFlags flags)
+ {
+ if (values == null) throw new ArgumentNullException(nameof(values));
+ if (values.Length == 0) return null;
+
+ var args = new RedisValue[values.Length * 2];
+ int offset = 0;
+ foreach (var value in values)
+ {
+ args[offset++] = value.Index.ToRedisValue();
+ args[offset++] = value.Value;
+ }
+ return Message.Create(Database, flags, RedisCommand.ARMSET, key, args);
+ }
+
+ private Message? GetArrayIndicesMessage(RedisCommand command, RedisKey key, RedisArrayIndex[] indices, CommandFlags flags)
+ {
+ if (indices == null) throw new ArgumentNullException(nameof(indices));
+ if (indices.Length == 0) return null;
+
+ var args = new RedisValue[indices.Length];
+ for (int i = 0; i < args.Length; i++)
+ {
+ args[i] = indices[i].ToRedisValue();
+ }
+ return Message.Create(Database, flags, command, key, args);
+ }
+
+ private Message? GetArrayRangesMessage(RedisKey key, RedisArrayRange[] ranges, CommandFlags flags)
+ {
+ if (ranges == null) throw new ArgumentNullException(nameof(ranges));
+ if (ranges.Length == 0) return null;
+
+ var args = new RedisValue[ranges.Length * 2];
+ int offset = 0;
+ foreach (var range in ranges)
+ {
+ args[offset++] = range.Start.ToRedisValue();
+ args[offset++] = range.End.ToRedisValue();
+ }
+ return Message.Create(Database, flags, RedisCommand.ARDELRANGE, key, args);
+ }
+
+ private static void CheckNonNegative(int value, string parameterName)
+ {
+ if (value < 0) throw new ArgumentOutOfRangeException(parameterName, "The value must be non-negative.");
+ }
+
+ private Message GetArrayScanMessage(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, int limit, CommandFlags flags)
+ {
+ CheckNonNegative(limit, nameof(limit));
+ return limit == 0
+ ? Message.Create(Database, flags, RedisCommand.ARSCAN, key, start.ToRedisValue(), end.ToRedisValue())
+ : Message.Create(Database, flags, RedisCommand.ARSCAN, key, start.ToRedisValue(), end.ToRedisValue(), RedisLiterals.LIMIT, limit);
+ }
+
+ private Message GetArrayOperationMessage(RedisKey key, RedisArrayIndex start, RedisArrayIndex end, ArrayOperation operation, RedisValue operand, CommandFlags flags)
+ {
+ bool hasOperand = !operand.IsNull;
+ if (operation == global::StackExchange.Redis.ArrayOperation.Match)
+ {
+ if (!hasOperand)
+ {
+ throw new ArgumentException("The Match operation requires a non-null operand.", nameof(operand));
+ }
+ }
+ else if (hasOperand)
+ {
+ throw new ArgumentException("An operand is only supported for the Match operation.", nameof(operand));
+ }
+
+ var literal = GetArrayOperationLiteral(operation);
+ return hasOperand
+ ? Message.Create(Database, flags, RedisCommand.AROP, key, start.ToRedisValue(), end.ToRedisValue(), literal, operand)
+ : Message.Create(Database, flags, RedisCommand.AROP, key, start.ToRedisValue(), end.ToRedisValue(), literal);
+ }
+
+ private static RedisValue GetArrayOperationLiteral(ArrayOperation operation) => operation switch
+ {
+ global::StackExchange.Redis.ArrayOperation.Sum => RedisLiterals.SUM,
+ global::StackExchange.Redis.ArrayOperation.Min => RedisLiterals.MIN,
+ global::StackExchange.Redis.ArrayOperation.Max => RedisLiterals.MAX,
+ global::StackExchange.Redis.ArrayOperation.And => RedisLiterals.AND,
+ global::StackExchange.Redis.ArrayOperation.Or => RedisLiterals.OR,
+ global::StackExchange.Redis.ArrayOperation.Xor => RedisLiterals.XOR,
+ global::StackExchange.Redis.ArrayOperation.Match => RedisLiterals.MATCH,
+ global::StackExchange.Redis.ArrayOperation.Used => RedisLiterals.USED,
+ _ => throw new ArgumentOutOfRangeException(nameof(operation)),
+ };
+
+ private Message GetArrayRingMessage(RedisKey key, RedisArrayIndex maxLength, RedisValue[] values, CommandFlags flags)
+ {
+ return GetArrayValuesMessage(RedisCommand.ARRING, key, values, flags, maxLength.ToRedisValue());
+ }
+
+ private Message GetArrayValuesMessage(RedisCommand command, RedisKey key, RedisValue[] values, CommandFlags flags, RedisValue? prefix = null)
+ {
+ if (values == null) throw new ArgumentNullException(nameof(values));
+ if (values.Length == 0) throw new ArgumentOutOfRangeException(nameof(values));
+
+ if (prefix.HasValue)
+ {
+ var args = new RedisValue[values.Length + 1];
+ args[0] = prefix.GetValueOrDefault();
+ Array.Copy(values, 0, args, 1, values.Length);
+ return Message.Create(Database, flags, command, key, args);
+ }
+
+ return Message.Create(Database, flags, command, key, values);
+ }
+
+ private Message? GetArrayLastItemsMessage(RedisKey key, int count, bool reverse, CommandFlags flags)
+ {
+ CheckNonNegative(count, nameof(count));
+ if (count == 0) return null;
+
+ return reverse
+ ? Message.Create(Database, flags, RedisCommand.ARLASTITEMS, key, count, RedisLiterals.REV)
+ : Message.Create(Database, flags, RedisCommand.ARLASTITEMS, key, count);
+ }
+}
diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs
index 9a8f54971..7f147b358 100644
--- a/src/StackExchange.Redis/RedisLiterals.cs
+++ b/src/StackExchange.Redis/RedisLiterals.cs
@@ -143,8 +143,10 @@ public static readonly RedisValue
STATS = "STATS",
STOP = "STOP",
STORE = "STORE",
+ SUM = "SUM",
TYPE = "TYPE",
USERNAME = "USERNAME",
+ USED = "USED",
WEIGHTS = "WEIGHTS",
WITHMATCHLEN = "WITHMATCHLEN",
WITHSCORES = "WITHSCORES",
diff --git a/src/StackExchange.Redis/ResultProcessor.Arrays.cs b/src/StackExchange.Redis/ResultProcessor.Arrays.cs
new file mode 100644
index 000000000..c5864021c
--- /dev/null
+++ b/src/StackExchange.Redis/ResultProcessor.Arrays.cs
@@ -0,0 +1,208 @@
+using System;
+
+// ReSharper disable once CheckNamespace
+namespace StackExchange.Redis;
+
+internal abstract partial class ResultProcessor
+{
+ public static readonly ResultProcessor
+ RedisArrayIndex = new RedisArrayIndexProcessor();
+
+ public static readonly ResultProcessor
+ NullableRedisArrayIndex = new NullableRedisArrayIndexProcessor();
+
+ public static readonly ResultProcessor
+ RedisArrayEntryArray = new RedisArrayEntryArrayProcessor();
+
+ public static readonly ResultProcessor
+ RedisArrayIndexEntryArray = new RedisArrayIndexEntryArrayProcessor();
+
+ public static readonly ResultProcessor
+ ArrayInfo = new ArrayInfoProcessor();
+
+ private static bool TryParseArrayIndex(in RawResult result, out RedisArrayIndex index)
+ {
+ switch (result.Resp2TypeBulkString)
+ {
+ case ResultType.Integer:
+ case ResultType.SimpleString:
+ case ResultType.BulkString:
+ if (!result.IsNull && result.TryParse(Format.TryParseUInt64, out ulong value))
+ {
+ index = new RedisArrayIndex(value);
+ return true;
+ }
+ break;
+ }
+
+ index = default;
+ return false;
+ }
+
+ private sealed class RedisArrayIndexProcessor : ResultProcessor
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ if (TryParseArrayIndex(result, out RedisArrayIndex index))
+ {
+ SetResult(message, index);
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ private sealed class NullableRedisArrayIndexProcessor : ResultProcessor
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ if (result.Resp2TypeBulkString == ResultType.BulkString && result.IsNull)
+ {
+ SetResult(message, null);
+ return true;
+ }
+
+ if (TryParseArrayIndex(result, out RedisArrayIndex index))
+ {
+ SetResult(message, index);
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ private sealed class RedisArrayEntryArrayProcessor : ResultProcessor
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ if (result.Resp2TypeArray != ResultType.Array)
+ {
+ return false;
+ }
+
+ if (result.IsNull)
+ {
+ SetResult(message, Array.Empty());
+ return true;
+ }
+
+ var items = result.GetItems();
+ if ((items.Length & 1) != 0)
+ {
+ return false;
+ }
+
+ var count = checked((int)items.Length) / 2;
+ var entries = new RedisArrayEntry[count];
+ var iter = items.GetEnumerator();
+ for (int i = 0; i < entries.Length; i++)
+ {
+ if (!iter.MoveNext() || !TryParseArrayIndex(iter.Current, out RedisArrayIndex index) || !iter.MoveNext())
+ {
+ return false;
+ }
+
+ entries[i] = new RedisArrayEntry(index, iter.Current.AsRedisValue());
+ }
+
+ SetResult(message, entries);
+ return true;
+ }
+ }
+
+ private sealed class RedisArrayIndexEntryArrayProcessor : ResultProcessor
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ if (result.Resp2TypeArray != ResultType.Array)
+ {
+ return false;
+ }
+
+ if (result.IsNull)
+ {
+ SetResult(message, Array.Empty());
+ return true;
+ }
+
+ var items = result.GetItems();
+ var count = checked((int)items.Length);
+ var entries = new RedisArrayEntry[count];
+ var iter = items.GetEnumerator();
+ for (int i = 0; i < entries.Length; i++)
+ {
+ if (!iter.MoveNext() || !TryParseArrayIndex(iter.Current, out RedisArrayIndex index))
+ {
+ return false;
+ }
+
+ entries[i] = new RedisArrayEntry(index);
+ }
+
+ SetResult(message, entries);
+ return true;
+ }
+ }
+
+ private sealed class ArrayInfoProcessor : ResultProcessor
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ if (result.Resp2TypeArray != ResultType.Array || result.IsNull)
+ {
+ return false;
+ }
+
+ RedisArrayIndex count = default, length = default, nextInsertIndex = default, slices = default, directorySize = default, superDirEntries = default, sliceSize = default;
+ var iter = result.GetItems().GetEnumerator();
+ while (iter.MoveNext())
+ {
+ if (!iter.Current.TryParse(ArrayInfoFieldMetadata.TryParse, out ArrayInfoField field))
+ {
+ field = ArrayInfoField.Unknown;
+ }
+
+ if (!iter.MoveNext())
+ {
+ break;
+ }
+
+ ref readonly RawResult value = ref iter.Current;
+ if (!TryParseArrayIndex(value, out RedisArrayIndex index))
+ {
+ continue;
+ }
+
+ switch (field)
+ {
+ case ArrayInfoField.Count:
+ count = index;
+ break;
+ case ArrayInfoField.Length:
+ length = index;
+ break;
+ case ArrayInfoField.NextInsertIndex:
+ nextInsertIndex = index;
+ break;
+ case ArrayInfoField.Slices:
+ slices = index;
+ break;
+ case ArrayInfoField.DirectorySize:
+ directorySize = index;
+ break;
+ case ArrayInfoField.SuperDirEntries:
+ superDirEntries = index;
+ break;
+ case ArrayInfoField.SliceSize:
+ sliceSize = index;
+ break;
+ }
+ }
+
+ SetResult(message, new ArrayInfo(count, length, nextInsertIndex, slices, directorySize, superDirEntries, sliceSize));
+ return true;
+ }
+ }
+}
diff --git a/tests/StackExchange.Redis.Tests/ArrayTests.cs b/tests/StackExchange.Redis.Tests/ArrayTests.cs
new file mode 100644
index 000000000..73ebc186f
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/ArrayTests.cs
@@ -0,0 +1,586 @@
+using System;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace StackExchange.Redis.Tests;
+
+// building on array.tcl from the redis tests
+[RunPerProtocol]
+public class ArrayTests(SharedConnectionFixture fixture, ITestOutputHelper log)
+ : TestBase(log, fixture)
+{
+ [Fact]
+ public async Task BasicSetGetTests()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ RedisKey missing = WithSuffix(key, ":missing");
+ await db.KeyDeleteAsync([key, missing]);
+
+ Assert.True(await db.ArraySetAsync(key, 0, "hello"));
+ Assert.Equal("hello", await db.ArrayGetAsync(key, 0));
+ Assert.Equal(RedisValue.Null, await db.ArrayGetAsync(key, 1));
+
+ Assert.False(await db.ArraySetAsync(key, 0, "world"));
+ Assert.Equal("world", await db.ArrayGetAsync(key, 0));
+
+ Assert.Equal(RedisValue.Null, await db.ArrayGetAsync(missing, 0));
+
+ Assert.True(await db.ArraySetAsync(key, 10, 12345));
+ Assert.Equal("12345", await db.ArrayGetAsync(key, 10));
+
+ Assert.True(await db.ArraySetAsync(key, 11, 3.14159));
+ var floatValue = await db.ArrayGetAsync(key, 11);
+ Assert.Equal(3.14159, (double)floatValue, precision: 5);
+
+ Assert.True(await db.ArraySetAsync(key, 12, "abc"));
+ Assert.Equal("abc", await db.ArrayGetAsync(key, 12));
+
+ var longString = new string('x', 100);
+ Assert.True(await db.ArraySetAsync(key, 13, longString));
+ Assert.Equal(longString, await db.ArrayGetAsync(key, 13));
+
+ Assert.True(await db.ArraySetAsync(key, 14, RedisValue.EmptyString));
+ Assert.Equal(RedisValue.EmptyString, await db.ArrayGetAsync(key, 14));
+ }
+
+ [Fact]
+ public async Task LengthCountAndSparseGaps()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ await db.KeyDeleteAsync(key);
+
+ AssertIndex(await db.ArrayLengthAsync(key), 0);
+ AssertIndex(await db.ArrayCountAsync(key), 0);
+
+ Assert.True(await db.ArraySetAsync(key, 0, "a"));
+ AssertIndex(await db.ArrayLengthAsync(key), 1);
+ AssertIndex(await db.ArrayCountAsync(key), 1);
+
+ Assert.True(await db.ArraySetAsync(key, 5, "b"));
+ AssertIndex(await db.ArrayLengthAsync(key), 6);
+ AssertIndex(await db.ArrayCountAsync(key), 2);
+
+ Assert.True(await db.ArraySetAsync(key, 100, "c"));
+ AssertIndex(await db.ArrayLengthAsync(key), 101);
+ AssertIndex(await db.ArrayCountAsync(key), 3);
+
+ await db.KeyDeleteAsync(key);
+ Assert.True(await db.ArraySetAsync(key, 0, "a"));
+ Assert.True(await db.ArraySetAsync(key, 10000, "b"));
+ Assert.True(await db.ArraySetAsync(key, 1000000, "c"));
+
+ Assert.Equal("a", await db.ArrayGetAsync(key, 0));
+ Assert.Equal("b", await db.ArrayGetAsync(key, 10000));
+ Assert.Equal("c", await db.ArrayGetAsync(key, 1000000));
+ AssertIndex(await db.ArrayCountAsync(key), 3);
+ AssertIndex(await db.ArrayLengthAsync(key), 1000001);
+ }
+
+ [Fact]
+ public async Task DeleteAndDeleteRange()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ await db.KeyDeleteAsync(key);
+
+ Assert.Equal(3, await db.ArraySetAsync(key, 0, ["a", "b", "c"]));
+ Assert.True(await db.ArrayDeleteAsync(key, 1));
+ Assert.Equal(RedisValue.Null, await db.ArrayGetAsync(key, 1));
+ AssertIndex(await db.ArrayCountAsync(key), 2);
+ Assert.False(await db.ArrayDeleteAsync(key, 1));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(4, await db.ArraySetAsync(key, 0, ["a", "b", "c", "d"]));
+ Assert.Equal(3, await db.ArrayDeleteAsync(key, [0, 1, 2]));
+ AssertIndex(await db.ArrayCountAsync(key), 1);
+
+ await db.KeyDeleteAsync(key);
+ Assert.True(await db.ArraySetAsync(key, 0, "a"));
+ Assert.True(await db.ArrayDeleteAsync(key, 0));
+ Assert.False(await db.KeyExistsAsync(key));
+
+ await db.KeyDeleteAsync(key);
+ await SetNumericValuesAsync(db, key, 10);
+ AssertIndex(await db.ArrayCountAsync(key), 10);
+ AssertIndex(await db.ArrayDeleteRangeAsync(key, 2, 6), 5);
+ AssertIndex(await db.ArrayCountAsync(key), 5);
+
+ await db.KeyDeleteAsync(key);
+ await SetNumericValuesAsync(db, key, 10);
+ AssertIndex(await db.ArrayDeleteRangeAsync(key, 6, 2), 5);
+ AssertIndex(await db.ArrayCountAsync(key), 5);
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(6, await db.ArraySetAsync(key, 0, ["a", "b", "c", "d", "e", "f"]));
+ AssertIndex(await db.ArrayDeleteRangeAsync(key, [new RedisArrayRange(0, 1), new RedisArrayRange(4, 5)]), 4);
+ AssertValues(await db.ArrayGetRangeAsync(key, 0, 5), RedisValue.Null, RedisValue.Null, "c", "d", RedisValue.Null, RedisValue.Null);
+ }
+
+ [Fact(Timeout = 10000)]
+ public async Task DeleteLastElementPublishesArrayDeleteBeforeKeyDeleteNotifications()
+ {
+ await using var conn = Create(allowAdmin: true, require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ await AssertArrayKeyspaceNotificationsEnabledAsync(conn);
+
+ RedisKey key = Me();
+ await db.KeyDeleteAsync(key);
+
+ var sub = conn.GetSubscriber();
+ var channel = RedisChannel.Pattern($"__key*@{db.Database}__:*");
+ var queue = await sub.SubscribeAsync(channel);
+ try
+ {
+ Assert.True(await db.ArraySetAsync(key, 0, "a"));
+ Assert.True(await db.ArrayDeleteAsync(key, 0));
+
+ AssertNotification(await ReadNotificationAsync(queue, key), KeyNotificationKind.KeySpace, KeyNotificationType.ArDel);
+ AssertNotification(await ReadNotificationAsync(queue, key), KeyNotificationKind.KeyEvent, KeyNotificationType.ArDel);
+ AssertNotification(await ReadNotificationAsync(queue, key), KeyNotificationKind.KeySpace, KeyNotificationType.Del);
+ AssertNotification(await ReadNotificationAsync(queue, key), KeyNotificationKind.KeyEvent, KeyNotificationType.Del);
+ }
+ finally
+ {
+ await queue.UnsubscribeAsync();
+ }
+ }
+
+ [Fact]
+ public async Task MultiSetMultiGetAndRanges()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ await db.KeyDeleteAsync(key);
+
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, "a"), Entry(1, "b"), Entry(2, "c")]));
+ Assert.Equal("a", await db.ArrayGetAsync(key, 0));
+ Assert.Equal("b", await db.ArrayGetAsync(key, 1));
+ Assert.Equal("c", await db.ArrayGetAsync(key, 2));
+
+ await db.KeyDeleteAsync(key);
+ Assert.True(await db.ArraySetAsync(key, 0, "a"));
+ Assert.Equal(1, await db.ArraySetAsync(key, [Entry(0, "aa"), Entry(1, "b")]));
+ Assert.Equal("aa", await db.ArrayGetAsync(key, 0));
+ Assert.Equal("b", await db.ArrayGetAsync(key, 1));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, "a"), Entry(1, "b"), Entry(5, "c")]));
+ AssertValues(await db.ArrayGetAsync(key, [0, 1, 5, 3]), "a", "b", "c", RedisValue.Null);
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(5, await db.ArraySetAsync(key, [Entry(0, "a"), Entry(1, "b"), Entry(2, "c"), Entry(3, "d"), Entry(4, "e")]));
+ AssertValues(await db.ArrayGetRangeAsync(key, 1, 3), "b", "c", "d");
+ AssertValues(await db.ArrayGetRangeAsync(key, 3, 1), "d", "c", "b");
+
+ await AssertServerErrorAsync("range exceeds maximum", async () => _ = await db.ArrayGetRangeAsync(key, 0, 1000000));
+ await AssertServerErrorAsync("range exceeds maximum", async () => _ = await db.ArrayGetRangeAsync(key, 1000000, 0));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(3, await db.ArraySetAsync(key, 0, ["a", "b", "c"]));
+ Assert.Equal("a", await db.ArrayGetAsync(key, 0));
+ Assert.Equal("b", await db.ArrayGetAsync(key, 1));
+ Assert.Equal("c", await db.ArrayGetAsync(key, 2));
+ }
+
+ [Fact]
+ public async Task Scan()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ RedisKey missing = WithSuffix(key, ":missing");
+ await db.KeyDeleteAsync([key, missing]);
+
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, "a"), Entry(5, "b"), Entry(9, "c")]));
+ AssertEntries(await db.ArrayScanAsync(key, 0, 10), Entry(0, "a"), Entry(5, "b"), Entry(9, "c"));
+
+ await db.KeyDeleteAsync(key);
+ Assert.True(await db.ArraySetAsync(key, 500, "x"));
+ Assert.Empty(await db.ArrayScanAsync(key, 0, 100));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(2, await db.ArraySetAsync(key, [Entry(0, "a"), Entry(5, "b")]));
+ AssertEntries(await db.ArrayScanAsync(key, 5, 0), Entry(5, "b"), Entry(0, "a"));
+
+ Assert.Empty(await db.ArrayScanAsync(missing, 0, 100));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, "string"), Entry(1, 12345), Entry(2, 3.14)]));
+ AssertEntries(await db.ArrayScanAsync(key, 0, 10), Entry(0, "string"), Entry(1, "12345"), Entry(2, "3.14"));
+ }
+
+ [Fact]
+ public async Task GrepBasics()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ RedisKey missing = WithSuffix(key, ":missing");
+ await db.KeyDeleteAsync([key, missing]);
+
+ Assert.Equal(4, await db.ArraySetAsync(key, [Entry(0, "alpha"), Entry(1, "beta"), Entry(2, "alphabet"), Entry(5, "gamma")]));
+ AssertIndexEntries(await db.ArrayGrepAsync(key, CreateGrep(ArrayGrepRequest.Predicate.Match("alpha"))), 0, 2);
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(4, await db.ArraySetAsync(key, [Entry(0, "alpha"), Entry(1, "beta"), Entry(2, "alphabet"), Entry(3, "delta")]));
+ var withValues = CreateGrep(ArrayGrepRequest.Predicate.Match("alpha"));
+ withValues.Start = 3;
+ withValues.End = 0;
+ withValues.IncludeValues = true;
+ AssertEntries(await db.ArrayGrepAsync(key, withValues), Entry(2, "alphabet"), Entry(0, "alpha"));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(4, await db.ArraySetAsync(key, [Entry(0, "RedisArray"), Entry(1, "redis-match"), Entry(2, "array-only"), Entry(3, "plain")]));
+ var andNoCase = CreateGrep(ArrayGrepRequest.Predicate.Match("redis"), ArrayGrepRequest.Predicate.Glob("*array*"));
+ andNoCase.IsIntersection = true;
+ andNoCase.IsCaseSensitive = true;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, andNoCase), 0);
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(4, await db.ArraySetAsync(key, [Entry(0, "hit-1"), Entry(1, "hit-2"), Entry(2, "miss"), Entry(3, "hit-3")]));
+ var limited = CreateGrep(ArrayGrepRequest.Predicate.Match("hit"));
+ limited.Limit = 2;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, limited), 0, 1);
+
+ Assert.Empty(await db.ArrayGrepAsync(missing, CreateGrep(ArrayGrepRequest.Predicate.Match("foo"))));
+ }
+
+ [Fact]
+ public async Task GrepRegexAndErrors()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ await db.KeyDeleteAsync(key);
+
+ Assert.Equal(4, await db.ArraySetAsync(key, [Entry(0, "foo123"), Entry(1, "bar"), Entry(2, "zoo999"), Entry(3, "Foo777")]));
+ AssertIndexEntries(await db.ArrayGrepAsync(key, CreateGrep(ArrayGrepRequest.Predicate.Regex("^.*[0-9]{3}$"))), 0, 2, 3);
+
+ var noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("^foo[0-9]+$"));
+ noCase.IsCaseSensitive = true;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 3);
+
+ await db.KeyDeleteAsync(key);
+ var values = new RedisArrayEntry[]
+ {
+ Entry(0, "foo"), Entry(1, "bar"), Entry(2, "baz"), Entry(3, "foobar"), Entry(4, "BAR"),
+ Entry(5, "quxfoo"), Entry(6, "zedbar"), Entry(7, "plain"), Entry(8, "ALPS"), Entry(9, "alphabet"),
+ };
+ Assert.Equal(10, await db.ArraySetAsync(key, values));
+
+ AssertIndexEntries(await db.ArrayGrepAsync(key, CreateGrep(ArrayGrepRequest.Predicate.Regex("foo|bar"))), 0, 1, 3, 5, 6);
+ noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("foo|bar"));
+ noCase.IsCaseSensitive = true;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1, 3, 4, 5, 6);
+
+ noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("^(foo|bar)$"));
+ noCase.IsCaseSensitive = true;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1, 4);
+
+ noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("^(foo|bar)"));
+ noCase.IsCaseSensitive = true;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1, 3, 4);
+
+ noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("(foo|bar)$"));
+ noCase.IsCaseSensitive = true;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1, 3, 4, 5, 6);
+
+ noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("alpha|alps"));
+ noCase.IsCaseSensitive = true;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 8, 9);
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(4, await db.ArraySetAsync(key, [Entry(0, "item-foo-123"), Entry(1, "ITEM-BAR-456"), Entry(2, "item-baz"), Entry(3, "plain")]));
+ noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("^item-(foo|bar)-[0-9]{3}$"));
+ noCase.IsCaseSensitive = true;
+ AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1);
+
+ await db.KeyDeleteAsync(key);
+ var re2048 = new string('a', 2048);
+ var re2049 = new string('a', 2049);
+ Assert.True(await db.ArraySetAsync(key, 0, re2048));
+ AssertIndexEntries(await db.ArrayGrepAsync(key, CreateGrep(ArrayGrepRequest.Predicate.Regex(re2048))), 0);
+ await AssertServerErrorAsync("maximum is 2048 bytes", async () => _ = await db.ArrayGrepAsync(key, CreateGrep(ArrayGrepRequest.Predicate.Regex(re2049))));
+ await AssertServerErrorAsync("backreferences are not supported", async () => _ = await db.ArrayGrepAsync(key, CreateGrep(ArrayGrepRequest.Predicate.Regex("(a)\\1"))));
+ await AssertServerErrorAsync("regular expression is empty", async () => _ = await db.ArrayGrepAsync(key, CreateGrep(ArrayGrepRequest.Predicate.Regex(""))));
+
+ await AssertServerErrorAsync("invalid regular expression", async () => _ = await db.ArrayGrepAsync(key, CreateGrep(ArrayGrepRequest.Predicate.Regex("\\x{1"))));
+
+ await db.KeyDeleteAsync(key);
+ Assert.True(await db.ArraySetAsync(key, 0, "foo"));
+ var request = new ArrayGrepRequest();
+ for (int i = 0; i < 250; i++)
+ {
+ request.AddPredicate(ArrayGrepRequest.Predicate.Match("foo"));
+ }
+ AssertIndexEntries(await db.ArrayGrepAsync(key, request), 0);
+
+ request = new ArrayGrepRequest();
+ for (int i = 0; i < 251; i++)
+ {
+ request.AddPredicate(ArrayGrepRequest.Predicate.Match("foo"));
+ }
+ await AssertServerErrorAsync("maximum is 250", async () => _ = await db.ArrayGrepAsync(key, request));
+ }
+
+ [Fact]
+ public async Task InsertRingNextSeekAndLastItems()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ RedisKey missing = WithSuffix(key, ":missing");
+ await db.KeyDeleteAsync([key, missing]);
+
+ AssertIndex(await db.ArrayInsertAsync(key, "a"), 0);
+ AssertIndex(await db.ArrayInsertAsync(key, "b"), 1);
+ AssertIndex(await db.ArrayInsertAsync(key, "c"), 2);
+ Assert.Equal("a", await db.ArrayGetAsync(key, 0));
+ Assert.Equal("b", await db.ArrayGetAsync(key, 1));
+ Assert.Equal("c", await db.ArrayGetAsync(key, 2));
+
+ await db.KeyDeleteAsync(key);
+ for (int i = 0; i < 10; i++)
+ {
+ _ = await db.ArrayRingAsync(key, 5, i);
+ }
+ Assert.Equal("5", await db.ArrayGetAsync(key, 0));
+ Assert.Equal("6", await db.ArrayGetAsync(key, 1));
+ Assert.Equal("7", await db.ArrayGetAsync(key, 2));
+ Assert.Equal("8", await db.ArrayGetAsync(key, 3));
+ Assert.Equal("9", await db.ArrayGetAsync(key, 4));
+ AssertIndex(await db.ArrayCountAsync(key), 5);
+
+ await db.KeyDeleteAsync(key);
+ AssertIndex(await db.ArrayNextAsync(key), 0);
+ AssertIndex(await db.ArrayInsertAsync(key, "a"), 0);
+ AssertIndex(await db.ArrayNextAsync(key), 1);
+ AssertIndex(await db.ArrayInsertAsync(key, "b"), 1);
+ AssertIndex(await db.ArrayNextAsync(key), 2);
+
+ Assert.False(await db.ArraySeekAsync(missing, 10));
+ Assert.True(await db.ArraySeekAsync(key, 10));
+ AssertIndex(await db.ArrayInsertAsync(key, "c"), 10);
+ AssertIndex(await db.ArrayNextAsync(key), 11);
+ Assert.Equal("c", await db.ArrayGetAsync(key, 10));
+
+ await db.KeyDeleteAsync(key);
+ AssertIndex(await db.ArrayInsertAsync(key, "a"), 0);
+ Assert.True(await db.ArraySeekAsync(key, RedisArrayIndex.MaxValue));
+ Assert.Null(await db.ArrayNextAsync(key));
+ await AssertServerErrorAsync("insert index overflow", async () => _ = await db.ArrayInsertAsync(key, "b"));
+
+ await db.KeyDeleteAsync(key);
+ for (int i = 0; i < 5; i++)
+ {
+ _ = await db.ArrayInsertAsync(key, i * 10);
+ }
+ AssertValues(await db.ArrayLastItemsAsync(key, 3), "20", "30", "40");
+ AssertValues(await db.ArrayLastItemsAsync(key, 3, reverse: true), "40", "30", "20");
+
+ Assert.True(await db.ArraySeekAsync(key, 0));
+ AssertValues(await db.ArrayLastItemsAsync(key, 3), "20", "30", "40");
+ AssertValues(await db.ArrayLastItemsAsync(key, 3, reverse: true), "40", "30", "20");
+ }
+
+ [Fact]
+ public async Task ArrayOperations()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ await db.KeyDeleteAsync(key);
+
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, 10), Entry(1, 20), Entry(2, 30)]));
+ Assert.Equal(60, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.Sum));
+ await Assert.ThrowsAsync(async () => _ = await db.ArrayOperationAsync(key, 0, 2, ArrayOperation.Match));
+ await Assert.ThrowsAsync(async () => _ = await db.ArrayOperationAsync(key, 0, 2, ArrayOperation.Sum, "value"));
+ await Assert.ThrowsAsync(async () => _ = await db.ArrayOperationAsync(key, 0, 2, ArrayOperation.Unknown));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, 30), Entry(1, 10), Entry(2, 20)]));
+ Assert.Equal(10, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.Min));
+ Assert.Equal(30, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.Max));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(4, await db.ArraySetAsync(key, [Entry(0, "hello"), Entry(1, "world"), Entry(2, "hello"), Entry(3, "foo")]));
+ Assert.Equal(2, await ArrayOperationInt64Async(db, key, 0, 3, ArrayOperation.Match, "hello"));
+ Assert.Equal(1, await ArrayOperationInt64Async(db, key, 0, 3, ArrayOperation.Match, "world"));
+ Assert.Equal(0, await ArrayOperationInt64Async(db, key, 0, 3, ArrayOperation.Match, "bar"));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, "a"), Entry(2, "b"), Entry(5, "c")]));
+ Assert.Equal(3, await ArrayOperationInt64Async(db, key, 0, 10, ArrayOperation.Used));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, 255), Entry(1, 15), Entry(2, 240)]));
+ Assert.Equal(0, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.And));
+ Assert.Equal(255, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.Or));
+ Assert.Equal(0, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.Xor));
+
+ await db.KeyDeleteAsync(key);
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, 7.9), Entry(1, 3.2), Entry(2, 1.8)]));
+ Assert.Equal(1, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.And));
+ Assert.Equal(7, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.Or));
+ Assert.Equal(5, await ArrayOperationInt64Async(db, key, 0, 2, ArrayOperation.Xor));
+ }
+
+ [Fact]
+ public async Task InfoTypeEncodingAndWrongType()
+ {
+ await using var conn = Create(require: RedisFeatures.v8_8_0);
+ var db = conn.GetDatabase();
+ RedisKey key = Me();
+ RedisKey wrongType = WithSuffix(key, ":wrong");
+ await db.KeyDeleteAsync([key, wrongType]);
+
+ Assert.Equal(3, await db.ArraySetAsync(key, [Entry(0, "a"), Entry(1, "b"), Entry(100, "c")]));
+ var info = await db.ArrayInfoAsync(key);
+ AssertIndex(info.Count, 3);
+ AssertIndex(info.Length, 101);
+ AssertIndex(info.NextInsertIndex, 0);
+ AssertIndex(info.Slices, 1);
+ AssertIndex(info.DirectorySize, 1);
+ AssertIndex(info.SuperDirEntries, 0);
+ AssertIndex(info.SliceSize, 4096);
+
+ Assert.Equal(RedisType.Array, await db.KeyTypeAsync(key));
+ Assert.Equal("sliced-array", await db.KeyEncodingAsync(key));
+
+ Assert.True(await db.StringSetAsync(wrongType, "value"));
+ await AssertServerErrorAsync("WRONGTYPE", async () => _ = await db.ArrayGetAsync(wrongType, 0));
+ await AssertServerErrorAsync("WRONGTYPE", async () => _ = await db.ArraySetAsync(wrongType, 0, "foo"));
+ await AssertServerErrorAsync("WRONGTYPE", async () => _ = await db.ArrayLengthAsync(wrongType));
+ await AssertServerErrorAsync("WRONGTYPE", async () => _ = await db.ArrayCountAsync(wrongType));
+ }
+
+ private static RedisArrayEntry Entry(long index, RedisValue value) => new RedisArrayEntry(index, value);
+
+ private static RedisKey WithSuffix(RedisKey key, string suffix) => (RedisKey)(key.ToString() + suffix);
+
+ private static ArrayGrepRequest CreateGrep(params ArrayGrepRequest.Predicate[] predicates)
+ {
+ var request = new ArrayGrepRequest();
+ foreach (var predicate in predicates)
+ {
+ request.AddPredicate(predicate);
+ }
+
+ return request;
+ }
+
+ private static async Task SetNumericValuesAsync(IDatabaseAsync db, RedisKey key, int count)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ Assert.True(await db.ArraySetAsync(key, i, i * 10));
+ }
+ }
+
+ private static async Task ArrayOperationInt64Async(
+ IDatabaseAsync db,
+ RedisKey key,
+ RedisArrayIndex start,
+ RedisArrayIndex end,
+ ArrayOperation operation,
+ RedisValue operand = default)
+ {
+ var result = await db.ArrayOperationAsync(key, start, end, operation, operand);
+ return (long)result;
+ }
+
+ private static void AssertIndex(RedisArrayIndex actual, ulong expected)
+ {
+ Assert.Equal(expected, actual.Value);
+ }
+
+ private static void AssertIndex(RedisArrayIndex? actual, ulong expected)
+ {
+ Assert.True(actual.HasValue);
+ Assert.Equal(expected, actual.GetValueOrDefault().Value);
+ }
+
+ private static void AssertIndexEntries(RedisArrayEntry[] actual, params ulong[] expected)
+ {
+ Assert.Equal(expected.Length, actual.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i], actual[i].Index.Value);
+ Assert.Equal(RedisValue.Null, actual[i].Value);
+ }
+ }
+
+ private static void AssertEntries(RedisArrayEntry[] actual, params RedisArrayEntry[] expected)
+ {
+ Assert.Equal(expected.Length, actual.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i].Index.Value, actual[i].Index.Value);
+ Assert.Equal(expected[i].Value, actual[i].Value);
+ }
+ }
+
+ private static void AssertValues(RedisValue[] actual, params RedisValue[] expected)
+ {
+ Assert.Equal(expected.Length, actual.Length);
+ for (int i = 0; i < expected.Length; i++)
+ {
+ Assert.Equal(expected[i], actual[i]);
+ }
+ }
+
+ private static async Task<(KeyNotificationKind Kind, KeyNotificationType Type)> ReadNotificationAsync(ChannelMessageQueue queue, RedisKey key)
+ {
+ for (int i = 0; i < 64; i++)
+ {
+ var message = await queue.ReadAsync(TestContext.Current.CancellationToken);
+ if (message.TryParseKeyNotification(out var notification)
+ && notification.GetKey() == key
+ && notification.Type is KeyNotificationType.ArDel or KeyNotificationType.Del)
+ {
+ return (notification.Kind, notification.Type);
+ }
+ }
+
+ Assert.Fail($"Timed out waiting for array keyspace notifications for '{key}'.");
+ return default;
+ }
+
+ private static void AssertNotification(
+ (KeyNotificationKind Kind, KeyNotificationType Type) actual,
+ KeyNotificationKind expectedKind,
+ KeyNotificationType expectedType)
+ {
+ Assert.Equal(expectedKind, actual.Kind);
+ Assert.Equal(expectedType, actual.Type);
+ }
+
+ private static async Task AssertArrayKeyspaceNotificationsEnabledAsync(IConnectionMultiplexer muxer)
+ {
+ foreach (var ep in muxer.GetEndPoints())
+ {
+ var server = muxer.GetServer(ep);
+ var config = await server.ConfigGetAsync("notify-keyspace-events");
+ var value = config.Length == 0 ? "" : config[0].Value.ToString() ?? "";
+
+ foreach (var token in "AKE")
+ {
+ Assert.SkipUnless(
+ value.IndexOf(token) >= 0,
+ $"Server {ep} notify-keyspace-events config '{value}' missing required token '{token}' for array keyspace notifications.");
+ }
+ }
+ }
+
+ private static async Task AssertServerErrorAsync(string expectedMessage, Func action)
+ {
+ var ex = await Assert.ThrowsAsync(action);
+ Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs b/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs
index 75ee4f9b4..e75e04670 100644
--- a/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs
+++ b/tests/StackExchange.Redis.Tests/PubSubKeyNotificationTests.cs
@@ -88,7 +88,7 @@ private async Task ConnectAsync(KeyNotificationK
// Check that the config contains all required tokens
foreach (var token in requiredTokens)
{
- Assert.SkipUnless(value.Contains(token), $"Server {ep} notify-keyspace-events config '{value}' missing required token '{token}' for {kind}");
+ Assert.SkipUnless(value.IndexOf(token) >= 0, $"Server {ep} notify-keyspace-events config '{value}' missing required token '{token}' for {kind}");
}
}