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}"); } }