diff --git a/docs/Arrays.md b/docs/Arrays.md index 84e8bedb4..c0bfc703d 100644 --- a/docs/Arrays.md +++ b/docs/Arrays.md @@ -218,6 +218,7 @@ Use `ArrayInfoAsync` for metadata: ```csharp ArrayInfo info = await db.ArrayInfoAsync(key); +ArrayInfo fullInfo = await db.ArrayInfoAsync(key, full: true); Console.WriteLine($"Count: {info.Count}"); Console.WriteLine($"Length: {info.Length}"); diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index c7d16f61d..aae40a9c6 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -8,7 +8,7 @@ Current package versions: ## Unreleased -- (none) +- Fix logic inversion with `ARGREP NOCASE`, add `IsReversed` to simplify ordering, and support `ARINFO FULL`. ([#3087 by @mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/3087)) ## 2.13.10 diff --git a/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs index 4fb411454..ce23d6e65 100644 --- a/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs +++ b/eng/StackExchange.Redis.Build/AsciiHashGenerator.cs @@ -32,6 +32,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Where(pair => pair.Name is { Length: > 0 }) .Collect(); + // looking for [AsciiHash] partial static bool TryFormat(enum input, out string/ReadOnlySpan output) { } + var formatMethods = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is MethodDeclarationSyntax decl && IsStaticPartial(decl.Modifiers) && + HasAsciiHash(decl.AttributeLists), + TransformFormatMethods) + .Where(pair => pair.Name is { Length: > 0 }) + .Collect(); + // looking for [AsciiHash("some type")] enum Foo { } var enums = context.SyntaxProvider .CreateSyntaxProvider( @@ -41,9 +50,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Collect(); context.RegisterSourceOutput( - types.Combine(methods).Combine(enums), + types.Combine(methods).Combine(formatMethods).Combine(enums), (ctx, content) => - Generate(ctx, content.Left.Left, content.Left.Right, content.Right)); + Generate(ctx, content.Left.Left.Left, content.Left.Left.Right, content.Left.Right, content.Right)); static bool IsStaticPartial(SyntaxTokenList tokens) => tokens.Any(SyntaxKind.StaticKeyword) && tokens.Any(SyntaxKind.PartialKeyword); @@ -309,6 +318,80 @@ static bool IsBytes(ITypeSymbol type) return (ns, parentType, method.DeclaredAccessibility, method.Name, from, to, caseSensitive, builder.Build(), defaultValue); } + private (string Namespace, string ParentType, Accessibility Accessibility, string Name, + (string Type, string Name, RefKind RefKind) From, (string Type, string Name, RefKind RefKind, bool IsBytes) To, + BasicArray<(string EnumMember, string FormatText)> Members) TransformFormatMethods( + GeneratorSyntaxContext ctx, + CancellationToken cancellationToken) + { + if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not IMethodSymbol + { + IsStatic: true, + IsPartialDefinition: true, + PartialImplementationPart: null, + Arity: 0, + ReturnType.SpecialType: SpecialType.System_Boolean, + Parameters: + { + IsDefaultOrEmpty: false, + Length: 2, + }, + } method) return default; + + if (TryGetAsciiHashAttribute(method.GetAttributes()) is not { }) return default; + + if (method.ContainingType is not { } containingType) return default; + var parentType = GetName(containingType); + var ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + + var arg = method.Parameters[0]; + if (arg is not + { + IsOptional: false, + RefKind: RefKind.None or RefKind.In or RefKind.Ref or RefKind.RefReadOnlyParameter, + Type: INamedTypeSymbol { TypeKind: TypeKind.Enum }, + }) return default; + var from = (arg.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), arg.Name, arg.RefKind); + + var enumMembers = arg.Type.GetMembers(); + var builder = new BasicArray<(string EnumMember, string FormatText)>.Builder(enumMembers.Length); + HashSet values = new(); + foreach (var member in enumMembers) + { + if (member is IFieldSymbol { IsStatic: true, IsConst: true } field) + { + var rawValue = GetRawValue(field.Name, TryGetAsciiHashAttribute(member.GetAttributes())); + if (string.IsNullOrWhiteSpace(rawValue)) continue; + if (field.ConstantValue is { } constValue && !values.Add(constValue)) continue; + builder.Add((field.Name, rawValue)); + } + } + + arg = method.Parameters[1]; + if (arg is not + { + IsOptional: false, + RefKind: RefKind.Out, + }) return default; + bool toBytes = IsReadOnlySpanOfByte(arg.Type); + if (arg.Type.SpecialType != SpecialType.System_String && !toBytes) return default; + var to = (arg.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), arg.Name, arg.RefKind, toBytes); + + return (ns, parentType, method.DeclaredAccessibility, method.Name, from, to, builder.Build()); + + static bool IsReadOnlySpanOfByte(ITypeSymbol type) + { + return type is INamedTypeSymbol + { + TypeKind: TypeKind.Struct, + Arity: 1, + Name: "ReadOnlySpan", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true }, + TypeArguments: { Length: 1 } typeArguments, + } && typeArguments[0].SpecialType == SpecialType.System_Byte; + } + } + private bool IsCaseSensitive(AttributeData attrib) { foreach (var member in attrib.NamedArguments) @@ -343,9 +426,13 @@ private void Generate( (string Type, string Name, bool IsBytes, RefKind RefKind) From, (string Type, string Name, RefKind RefKind) To, (string Name, bool Value, RefKind RefKind) CaseSensitive, BasicArray<(string EnumMember, string ParseText)> Members, int DefaultValue)> parseMethods, + ImmutableArray<(string Namespace, string ParentType, Accessibility Accessibility, string Name, + (string Type, string Name, RefKind RefKind) From, + (string Type, string Name, RefKind RefKind, bool IsBytes) To, + BasicArray<(string EnumMember, string FormatText)> Members)> formatMethods, ImmutableArray<(string Namespace, string ParentType, string Name, int Count, int MaxChars, int MaxBytes)> enums) { - if (types.IsDefaultOrEmpty & parseMethods.IsDefaultOrEmpty & enums.IsDefaultOrEmpty) return; // nothing to do + if (types.IsDefaultOrEmpty & parseMethods.IsDefaultOrEmpty & formatMethods.IsDefaultOrEmpty & enums.IsDefaultOrEmpty) return; // nothing to do var sb = new StringBuilder("// ") .AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine(); @@ -356,6 +443,7 @@ private void Generate( BuildTypeImplementations(sb, types); BuildEnumParsers(sb, parseMethods); + BuildEnumFormatters(sb, formatMethods); BuildEnumLengths(sb, enums); ctx.AddSource(nameof(AsciiHash) + ".generated.cs", sb.ToString()); } @@ -644,6 +732,102 @@ void Write(bool caseSensitive) } } + private void BuildEnumFormatters( + StringBuilder sb, + in ImmutableArray<(string Namespace, string ParentType, Accessibility Accessibility, string Name, + (string Type, string Name, RefKind RefKind) From, + (string Type, string Name, RefKind RefKind, bool IsBytes) To, + BasicArray<(string EnumMember, string FormatText)> Members)> enums) + { + if (enums.IsDefaultOrEmpty) return; // nope + + int indent = 0; + StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4); + + foreach (var grp in enums.GroupBy(l => (l.Namespace, l.ParentType))) + { + NewLine(); + int braces = 0; + if (!string.IsNullOrWhiteSpace(grp.Key.Namespace)) + { + NewLine().Append("namespace ").Append(grp.Key.Namespace); + NewLine().Append("{"); + indent++; + braces++; + } + + if (!string.IsNullOrWhiteSpace(grp.Key.ParentType)) + { + if (grp.Key.ParentType.Contains('.')) // nested types + { + foreach (var part in grp.Key.ParentType.Split('.')) + { + NewLine().Append("partial class ").Append(part); + NewLine().Append("{"); + indent++; + braces++; + } + } + else + { + NewLine().Append("partial class ").Append(grp.Key.ParentType); + NewLine().Append("{"); + indent++; + braces++; + } + } + + foreach (var method in grp) + { + NewLine().Append(Format(method.Accessibility)).Append(" static partial bool ") + .Append(method.Name).Append("(") + .Append(Format(method.From.RefKind)) + .Append(method.From.Type).Append(" ").Append(method.From.Name).Append(", ") + .Append(Format(method.To.RefKind)) + .Append(method.To.Type).Append(" ").Append(method.To.Name) + .Append(")"); + + NewLine().Append("{"); + indent++; + NewLine().Append("// ").Append(method.From.Type).Append(" has ").Append(method.Members.Length).Append(" formatted members"); + NewLine().Append("switch (").Append(method.From.Name).Append(")"); + NewLine().Append("{"); + indent++; + + foreach (var member in method.Members) + { + var formatted = SyntaxFactory + .LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(member.FormatText)) + .ToFullString(); + if (method.To.IsBytes) formatted += "u8"; + + NewLine().Append("case ").Append(method.From.Type).Append(".").Append(member.EnumMember).Append(":"); + indent++; + NewLine().Append(method.To.Name).Append(" = ").Append(formatted).Append(";"); + NewLine().Append("return true;"); + indent--; + } + + NewLine().Append("default:"); + indent++; + NewLine().Append(method.To.Name).Append(" = ").Append(method.To.IsBytes ? "default" : "default!").Append(";"); + NewLine().Append("return false;"); + indent--; + indent--; + NewLine().Append("}"); + indent--; + NewLine().Append("}"); + } + + // handle any closing braces + while (braces-- > 0) + { + indent--; + NewLine().Append("}"); + } + } + } + private static bool HasCaseSensitiveCharacters(string value) { foreach (char c in value ?? "") diff --git a/src/StackExchange.Redis/ArrayGrepRequest.cs b/src/StackExchange.Redis/ArrayGrepRequest.cs index f3cbc0109..642f6a060 100644 --- a/src/StackExchange.Redis/ArrayGrepRequest.cs +++ b/src/StackExchange.Redis/ArrayGrepRequest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using RESPite; @@ -16,12 +17,14 @@ private enum LocalFlags : byte { None = 0, IsFrozen = 1 << 0, - CaseSensitive = 1 << 1, + CaseInsensitive = 1 << 1, IsIntersection = 1 << 2, StartSpecified = 1 << 3, EndSpecified = 1 << 4, LimitSpecified = 1 << 5, IncludeValues = 1 << 6, + Reversed = 1 << 7, + // warning: next flag needs : ushort } private void Freeze() => _flags |= LocalFlags.IsFrozen; @@ -131,13 +134,37 @@ public long? Limit private long _limit; /// - /// Indicates whether matches are performed in a case-insensitive manner. + /// Indicates whether matches are performed in a case-sensitive manner. /// /// Corresponds to the NOCASE parameter. + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Prefer " + nameof(IsCaseInsensitive))] public bool IsCaseSensitive { - get => GetFlag(LocalFlags.CaseSensitive); - set => SetFlag(LocalFlags.CaseSensitive, value); + get => !IsCaseInsensitive; + set => IsCaseInsensitive = !value; + } + + /// + /// Indicates whether matches are performed in a case-insensitive manner. + /// + /// Corresponds to the NOCASE parameter. + public bool IsCaseInsensitive + { + get => GetFlag(LocalFlags.CaseInsensitive); + set => SetFlag(LocalFlags.CaseInsensitive, value); + } + + /// + /// Indicates whether the query order should be reversed; this is equivalent to + /// reversing the order of and . + /// + /// Corresponds to the NOCASE parameter. + public bool IsReversed + { + get => GetFlag(LocalFlags.Reversed); + set => SetFlag(LocalFlags.Reversed, value); } /// @@ -317,7 +344,7 @@ public override int ArgCount } if (request.IsIntersection) count++; - if (request.IsCaseSensitive) count++; + if (request.IsCaseInsensitive) count++; if (request.IncludeValues) count++; var limit = request.Limit; if (limit.HasValue) count += 2; @@ -325,27 +352,30 @@ public override int ArgCount } } - protected override void WriteImpl(PhysicalConnection physical) + private static void AddIndex(PhysicalConnection physical, RedisArrayIndex? index, ReadOnlySpan fallback) { - 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); + physical.WriteRaw(fallback); } - index = request.End; - if (index.HasValue) + } + protected override void WriteImpl(PhysicalConnection physical) + { + physical.WriteHeader(Command, ArgCount); + physical.WriteBulkString(key); + if (request.IsReversed) { - physical.WriteBulkString(index.GetValueOrDefault().Value); + AddIndex(physical, request.End, "$1\r\n+\r\n"u8); + AddIndex(physical, request.Start, "$1\r\n-\r\n"u8); } else { - physical.WriteRaw("$1\r\n+\r\n"u8); + AddIndex(physical, request.Start, "$1\r\n-\r\n"u8); + AddIndex(physical, request.End, "$1\r\n+\r\n"u8); } var pCount = request.Count; for (int i = 0; i < pCount; i++) @@ -354,7 +384,7 @@ protected override void WriteImpl(PhysicalConnection 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.IsCaseInsensitive) 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) diff --git a/src/StackExchange.Redis/ArrayInfo.cs b/src/StackExchange.Redis/ArrayInfo.cs index 19506acaa..84e9c058e 100644 --- a/src/StackExchange.Redis/ArrayInfo.cs +++ b/src/StackExchange.Redis/ArrayInfo.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using RESPite; @@ -7,47 +9,164 @@ 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) +public readonly struct ArrayInfo { + private readonly Dictionary? otherValues; + + internal int OtherValueCount => otherValues?.Count ?? -1; + + /// + /// Create a new instance with the specified values. + /// + public ArrayInfo(scoped ReadOnlySpan> values) + { + foreach (ref readonly var pair in values) + { + if (string.IsNullOrEmpty(pair.Key)) continue; + if (ArrayInfoFieldMetadata.TryParse(pair.Key, out var field)) + { + switch (field) + { + case ArrayInfoField.Count when TryRead(pair, out var index): + Count = index; + continue; + case ArrayInfoField.Length when TryRead(pair, out var index): + Length = index; + continue; + case ArrayInfoField.NextInsertIndex when TryRead(pair, out var index): + NextInsertIndex = index; + continue; + case ArrayInfoField.Slices when TryRead(pair, out var index): + Slices = index; + continue; + case ArrayInfoField.DirectorySize when TryRead(pair, out var index): + DirectorySize = index; + continue; + case ArrayInfoField.SuperDirEntries when TryRead(pair, out var index): + SuperDirEntries = index; + continue; + case ArrayInfoField.SliceSize when TryRead(pair, out var index): + SliceSize = index; + continue; + } + } + // unknown field, or unable to handle directly + otherValues ??= new(); + otherValues[pair.Key] = pair.Value; + } + } + + private static bool TryRead(in KeyValuePair pair, out RedisArrayIndex value) + { + var val = pair.Value.Simplify(); + if (val.IsInteger) + { + try + { + value = new((ulong)val); + return true; + } + catch (OverflowException) { } + } + + value = default; + return false; + } + + /// + /// Get values from the instance by key. + /// + public RedisValue this[string key] + { + get + { + if (!string.IsNullOrEmpty(key)) + { + if (otherValues is { } && otherValues.TryGetValue(key, out var value)) + { + return value; + } + + // spoof fields that are handled directly + if (ArrayInfoFieldMetadata.TryParse(key, out var field)) + { + switch (field) + { + case ArrayInfoField.Count: return Count.ToRedisValue(); + case ArrayInfoField.Length: return Length.ToRedisValue(); + case ArrayInfoField.NextInsertIndex: return NextInsertIndex.ToRedisValue(); + case ArrayInfoField.Slices: return Slices.ToRedisValue(); + case ArrayInfoField.DirectorySize: return DirectorySize.ToRedisValue(); + case ArrayInfoField.SuperDirEntries: return SuperDirEntries.ToRedisValue(); + case ArrayInfoField.SliceSize: return SliceSize.ToRedisValue(); + } + } + } + return RedisValue.Null; + } + } + + /// + /// Gets all array metadata values as a dictionary. + /// + public Dictionary ToDictionary() + { + var result = otherValues is { } + ? new Dictionary(otherValues, otherValues.Comparer) + : new Dictionary(); + + // for the *known* fields: they'll only be held in "otherValues" if we can't + // parse the value naturally, so: the dictionary takes precedence, with the fields as fallback. + AddIfMissing(result, ArrayInfoField.Count, Count); + AddIfMissing(result, ArrayInfoField.Length, Length); + AddIfMissing(result, ArrayInfoField.NextInsertIndex, NextInsertIndex); + AddIfMissing(result, ArrayInfoField.Slices, Slices); + AddIfMissing(result, ArrayInfoField.DirectorySize, DirectorySize); + AddIfMissing(result, ArrayInfoField.SuperDirEntries, SuperDirEntries); + AddIfMissing(result, ArrayInfoField.SliceSize, SliceSize); + return result; + + static void AddIfMissing(Dictionary values, ArrayInfoField field, RedisArrayIndex value) + { + if (ArrayInfoFieldMetadata.TryFormat(field, out var key) && !values.ContainsKey(key)) + { + values.Add(key, value.ToRedisValue()); + } + } + } + /// /// The number of array cells that have values. /// - public RedisArrayIndex Count { get; } = count; + public RedisArrayIndex Count { get; } /// /// The notional length of the array. /// - public RedisArrayIndex Length { get; } = length; + public RedisArrayIndex Length { get; } /// /// The current array write-head. /// - public RedisArrayIndex NextInsertIndex { get; } = nextInsertIndex; + public RedisArrayIndex NextInsertIndex { get; } /// /// The number of slices used by the array. /// - public RedisArrayIndex Slices { get; } = slices; + public RedisArrayIndex Slices { get; } /// /// The size of the array directory. /// - public RedisArrayIndex DirectorySize { get; } = directorySize; + public RedisArrayIndex DirectorySize { get; } /// /// The number of super-directory entries. /// - public RedisArrayIndex SuperDirEntries { get; } = superDirEntries; + public RedisArrayIndex SuperDirEntries { get; } /// /// The configured slice size. /// - public RedisArrayIndex SliceSize { get; } = sliceSize; + public RedisArrayIndex SliceSize { get; } } diff --git a/src/StackExchange.Redis/ArrayInfoField.cs b/src/StackExchange.Redis/ArrayInfoField.cs index aa523258e..903d4799e 100644 --- a/src/StackExchange.Redis/ArrayInfoField.cs +++ b/src/StackExchange.Redis/ArrayInfoField.cs @@ -64,4 +64,10 @@ internal static partial class ArrayInfoFieldMetadata { [AsciiHash] internal static partial bool TryParse(ReadOnlySpan value, out ArrayInfoField field); + + [AsciiHash] + internal static partial bool TryParse(ReadOnlySpan value, out ArrayInfoField field); + + [AsciiHash] + public static partial bool TryFormat(ArrayInfoField value, out string key); } diff --git a/src/StackExchange.Redis/Interfaces/IDatabase.Arrays.cs b/src/StackExchange.Redis/Interfaces/IDatabase.Arrays.cs index 9dbb7b755..4e8151414 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabase.Arrays.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabase.Arrays.cs @@ -147,7 +147,7 @@ public partial interface IDatabase /// Gets array metadata. /// [Experimental(Experiments.Server_8_8, UrlFormat = Experiments.UrlFormat)] - ArrayInfo ArrayInfo(RedisKey key, CommandFlags flags = CommandFlags.None); + ArrayInfo ArrayInfo(RedisKey key, bool full = false, 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 index 1c5fdd2de..6890388a7 100644 --- a/src/StackExchange.Redis/Interfaces/IDatabaseAsync.Arrays.cs +++ b/src/StackExchange.Redis/Interfaces/IDatabaseAsync.Arrays.cs @@ -96,9 +96,9 @@ public partial interface IDatabaseAsync [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); + Task ArrayInfoAsync(RedisKey key, bool full = false, CommandFlags flags = CommandFlags.None); } #pragma warning restore RS0026 diff --git a/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs b/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs index 06dd08f06..1b31b0f2d 100644 --- a/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs +++ b/src/StackExchange.Redis/KeyNotificationTypeMetadata.cs @@ -11,70 +11,17 @@ internal static partial class KeyNotificationTypeMetadata [AsciiHash] internal static partial bool TryParse(ReadOnlySpan value, out KeyNotificationType keyNotificationType); + [AsciiHash] + private static partial bool TryFormat(KeyNotificationType type, out ReadOnlySpan value); + public static KeyNotificationType Parse(ReadOnlySpan value) { return TryParse(value, out var result) ? result : KeyNotificationType.Unknown; } - internal static ReadOnlySpan GetRawBytes(KeyNotificationType type) => type switch + internal static ReadOnlySpan GetRawBytes(KeyNotificationType type) { - KeyNotificationType.Append => "append"u8, - KeyNotificationType.Copy => "copy"u8, - KeyNotificationType.Del => "del"u8, - KeyNotificationType.Expire => "expire"u8, - KeyNotificationType.HDel => "hdel"u8, - KeyNotificationType.HExpired => "hexpired"u8, - KeyNotificationType.HIncrByFloat => "hincrbyfloat"u8, - KeyNotificationType.HIncrBy => "hincrby"u8, - KeyNotificationType.HPersist => "hpersist"u8, - KeyNotificationType.HSet => "hset"u8, - KeyNotificationType.IncrByFloat => "incrbyfloat"u8, - KeyNotificationType.IncrBy => "incrby"u8, - KeyNotificationType.LInsert => "linsert"u8, - KeyNotificationType.LPop => "lpop"u8, - KeyNotificationType.LPush => "lpush"u8, - KeyNotificationType.LRem => "lrem"u8, - KeyNotificationType.LSet => "lset"u8, - KeyNotificationType.LTrim => "ltrim"u8, - KeyNotificationType.MoveFrom => "move_from"u8, - KeyNotificationType.MoveTo => "move_to"u8, - KeyNotificationType.Persist => "persist"u8, - KeyNotificationType.RenameFrom => "rename_from"u8, - KeyNotificationType.RenameTo => "rename_to"u8, - KeyNotificationType.Restore => "restore"u8, - KeyNotificationType.RPop => "rpop"u8, - KeyNotificationType.RPush => "rpush"u8, - KeyNotificationType.SAdd => "sadd"u8, - KeyNotificationType.Set => "set"u8, - KeyNotificationType.SetRange => "setrange"u8, - KeyNotificationType.SortStore => "sortstore"u8, - KeyNotificationType.SRem => "srem"u8, - KeyNotificationType.SPop => "spop"u8, - KeyNotificationType.XAdd => "xadd"u8, - KeyNotificationType.XDel => "xdel"u8, - KeyNotificationType.XGroupCreateConsumer => "xgroup-createconsumer"u8, - KeyNotificationType.XGroupCreate => "xgroup-create"u8, - KeyNotificationType.XGroupDelConsumer => "xgroup-delconsumer"u8, - KeyNotificationType.XGroupDestroy => "xgroup-destroy"u8, - KeyNotificationType.XGroupSetId => "xgroup-setid"u8, - KeyNotificationType.XSetId => "xsetid"u8, - KeyNotificationType.XTrim => "xtrim"u8, - KeyNotificationType.ZAdd => "zadd"u8, - KeyNotificationType.ZDiffStore => "zdiffstore"u8, - KeyNotificationType.ZInterStore => "zinterstore"u8, - KeyNotificationType.ZUnionStore => "zunionstore"u8, - KeyNotificationType.ZIncr => "zincr"u8, - KeyNotificationType.ZRemByRank => "zrembyrank"u8, - 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, - KeyNotificationType.Overwritten => "overwritten"u8, - KeyNotificationType.TypeChanged => "type_changed"u8, - _ => throw new ArgumentOutOfRangeException(nameof(type)), - }; + if (TryFormat(type, out var value)) return value; + throw new ArgumentOutOfRangeException(nameof(type)); + } } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.Arrays.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.Arrays.cs index 5cf08fb63..2ad2d6dc8 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.Arrays.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.Arrays.cs @@ -71,6 +71,6 @@ public Task ArraySeekAsync(RedisKey key, RedisArrayIndex index, CommandFla 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); + public Task ArrayInfoAsync(RedisKey key, bool full = false, CommandFlags flags = CommandFlags.None) => + Inner.ArrayInfoAsync(ToInner(key), full, flags); } diff --git a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.Arrays.cs b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.Arrays.cs index 6fb17fa8a..3d9f0f03e 100644 --- a/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.Arrays.cs +++ b/src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.Arrays.cs @@ -69,6 +69,6 @@ public bool ArraySeek(RedisKey key, RedisArrayIndex index, CommandFlags 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); + public ArrayInfo ArrayInfo(RedisKey key, bool full = false, CommandFlags flags = CommandFlags.None) => + Inner.ArrayInfo(ToInner(key), full, flags); } diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt index 2ec7fc333..f5222e907 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt @@ -2375,7 +2375,6 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Protocol.get -> [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 @@ -2402,7 +2401,7 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Protocol.get -> [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.ArrayInfo(StackExchange.Redis.RedisKey key, bool full = false, 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[]! @@ -2425,7 +2424,7 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Protocol.get -> [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.ArrayInfoAsync(StackExchange.Redis.RedisKey key, bool full = false, 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! diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt index ab058de62..2af96c4be 100644 --- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1,8 @@ #nullable enable +[SER006]StackExchange.Redis.ArrayGrepRequest.IsCaseInsensitive.get -> bool +[SER006]StackExchange.Redis.ArrayGrepRequest.IsCaseInsensitive.set -> void +[SER006]StackExchange.Redis.ArrayGrepRequest.IsReversed.get -> bool +[SER006]StackExchange.Redis.ArrayGrepRequest.IsReversed.set -> void +[SER006]StackExchange.Redis.ArrayInfo.ArrayInfo(scoped System.ReadOnlySpan> values) -> void +[SER006]StackExchange.Redis.ArrayInfo.ToDictionary() -> System.Collections.Generic.Dictionary! +[SER006]StackExchange.Redis.ArrayInfo.this[string! key].get -> StackExchange.Redis.RedisValue diff --git a/src/StackExchange.Redis/RedisDatabase.Arrays.cs b/src/StackExchange.Redis/RedisDatabase.Arrays.cs index 3b35e8d3f..1fab7db0a 100644 --- a/src/StackExchange.Redis/RedisDatabase.Arrays.cs +++ b/src/StackExchange.Redis/RedisDatabase.Arrays.cs @@ -140,9 +140,9 @@ public RedisValue[] ArrayLastItems(RedisKey key, int count, bool reverse = false return msg is null ? Array.Empty() : ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } - public ArrayInfo ArrayInfo(RedisKey key, CommandFlags flags = CommandFlags.None) + public ArrayInfo ArrayInfo(RedisKey key, bool full = false, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.ARINFO, key); + var msg = GetArrayInfoMessage(key, full, flags); return ExecuteSync(msg, ResultProcessor.ArrayInfo); } @@ -284,12 +284,19 @@ public Task ArrayLastItemsAsync(RedisKey key, int count, bool reve : ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty()); } - public Task ArrayInfoAsync(RedisKey key, CommandFlags flags = CommandFlags.None) + public Task ArrayInfoAsync(RedisKey key, bool full = false, CommandFlags flags = CommandFlags.None) { - var msg = Message.Create(Database, flags, RedisCommand.ARINFO, key); + var msg = GetArrayInfoMessage(key, full, flags); return ExecuteAsync(msg, ResultProcessor.ArrayInfo); } + private Message GetArrayInfoMessage(RedisKey key, bool full, CommandFlags flags) + { + return full + ? Message.Create(Database, flags, RedisCommand.ARINFO, key, RedisLiterals.FULL) + : Message.Create(Database, flags, RedisCommand.ARINFO, key); + } + private Message? GetArraySetMessage(RedisKey key, RedisArrayIndex index, RedisValue[] values, CommandFlags flags) { if (values == null) throw new ArgumentNullException(nameof(values)); diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs index 7f147b358..ec9bd3ad7 100644 --- a/src/StackExchange.Redis/RedisLiterals.cs +++ b/src/StackExchange.Redis/RedisLiterals.cs @@ -77,6 +77,7 @@ public static readonly RedisValue FLUSH = "FLUSH", FNX = "FNX", FREQ = "FREQ", + FULL = "FULL", FXX = "FXX", GET = "GET", GETKEYS = "GETKEYS", diff --git a/src/StackExchange.Redis/ResultProcessor.Arrays.cs b/src/StackExchange.Redis/ResultProcessor.Arrays.cs index 6df279e85..865bbb1ed 100644 --- a/src/StackExchange.Redis/ResultProcessor.Arrays.cs +++ b/src/StackExchange.Redis/ResultProcessor.Arrays.cs @@ -1,4 +1,7 @@ using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; // ReSharper disable once CheckNamespace namespace StackExchange.Redis; @@ -108,13 +111,16 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes return false; } - RedisArrayIndex count = default, length = default, nextInsertIndex = default, slices = default, directorySize = default, superDirEntries = default, sliceSize = default; + var lease = ArrayPool>.Shared.Rent(result.ItemsCount / 2); + int count = 0; var iter = result.GetItems().GetEnumerator(); while (iter.MoveNext()) { - if (!iter.Current.TryParse(ArrayInfoFieldMetadata.TryParse, out ArrayInfoField field)) + // try to parse the field as a known enum, and get the known string for it, otherwise: alloc + if (!(iter.Current.TryParse(ArrayInfoFieldMetadata.TryParse, out ArrayInfoField field) + && ArrayInfoFieldMetadata.TryFormat(field, out var key))) { - field = ArrayInfoField.Unknown; + key = iter.Current.GetString() ?? ""; } if (!iter.MoveNext()) @@ -122,39 +128,19 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes break; } - ref readonly RawResult value = ref iter.Current; - if (!TryParseArrayIndex(value, out RedisArrayIndex index)) + try { - continue; + lease[count++] = new(key, iter.Current.AsRedisValue()); } - - switch (field) + catch (Exception ex) { - 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; + // quietly ignore non-scalar results + Debug.WriteLine(ex.Message); } } - SetResult(message, new ArrayInfo(count, length, nextInsertIndex, slices, directorySize, superDirEntries, sliceSize)); + SetResult(message, new ArrayInfo(new(lease, 0, count))); + ArrayPool>.Shared.Return(lease); return true; } } diff --git a/tests/StackExchange.Redis.Tests/ArrayTests.cs b/tests/StackExchange.Redis.Tests/ArrayTests.cs index 8efd71009..8ef29b987 100644 --- a/tests/StackExchange.Redis.Tests/ArrayTests.cs +++ b/tests/StackExchange.Redis.Tests/ArrayTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using Xunit.Sdk; @@ -240,7 +241,7 @@ public async Task GrepBasics() 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; + andNoCase.IsCaseInsensitive = true; AssertIndexEntries(await db.ArrayGrepAsync(key, andNoCase), 0); await db.KeyDeleteAsync(key); @@ -264,7 +265,7 @@ public async Task GrepRegexAndErrors() 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; + noCase.IsCaseInsensitive = true; AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 3); await db.KeyDeleteAsync(key); @@ -277,29 +278,35 @@ public async Task GrepRegexAndErrors() 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; + noCase.IsCaseInsensitive = true; AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1, 3, 4, 5, 6); + // and same again, with reversed start/end + noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("foo|bar")); + noCase.IsCaseInsensitive = true; + noCase.IsReversed = true; + AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 6, 5, 4, 3, 1, 0); + noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("^(foo|bar)$")); - noCase.IsCaseSensitive = true; + noCase.IsCaseInsensitive = true; AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1, 4); noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("^(foo|bar)")); - noCase.IsCaseSensitive = true; + noCase.IsCaseInsensitive = true; AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1, 3, 4); noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("(foo|bar)$")); - noCase.IsCaseSensitive = true; + noCase.IsCaseInsensitive = true; AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1, 3, 4, 5, 6); noCase = CreateGrep(ArrayGrepRequest.Predicate.Regex("alpha|alps")); - noCase.IsCaseSensitive = true; + noCase.IsCaseInsensitive = 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; + noCase.IsCaseInsensitive = true; AssertIndexEntries(await db.ArrayGrepAsync(key, noCase), 0, 1); await db.KeyDeleteAsync(key); @@ -461,6 +468,42 @@ public async Task InfoTypeEncodingAndWrongType() await AssertServerErrorAsync("WRONGTYPE", async () => _ = await db.ArrayCountAsync(wrongType)); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task InfoToDictionary(bool full) + { + 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(100, "c")])); + + var info = await db.ArrayInfoAsync(key, full); + var dictionary = info.ToDictionary(); + LogDictionary(dictionary, $"ArrayInfo full={full}"); + + AssertArrayInfoDictionaryKnownFields(dictionary); + AssertIndex(info.Count, 3); + AssertIndex(info.Length, 101); + Assert.Equal(3, (long)dictionary["count"]); + Assert.Equal(101, (long)dictionary["len"]); + + if (full) + { + Assert.Contains("sparse-slices", dictionary.Keys); + + var basicDictionary = (await db.ArrayInfoAsync(key)).ToDictionary(); + Assert.DoesNotContain("sparse-slices", basicDictionary.Keys); + LogFullOnlyFields(basicDictionary, dictionary); + } + else + { + Assert.DoesNotContain("sparse-slices", dictionary.Keys); + } + } + private static RedisArrayEntry Entry(long index, RedisValue value) => new RedisArrayEntry(index, value); private static RedisKey WithSuffix(RedisKey key, string suffix) => (RedisKey)(key.ToString() + suffix); @@ -507,6 +550,54 @@ private static void AssertIndex(RedisArrayIndex? actual, ulong expected) Assert.Equal(expected, actual.GetValueOrDefault().Value); } + private static void AssertArrayInfoDictionaryKnownFields(Dictionary dictionary) + { + Assert.Contains("count", dictionary.Keys); + Assert.Contains("len", dictionary.Keys); + Assert.Contains("next-insert-index", dictionary.Keys); + Assert.Contains("slices", dictionary.Keys); + Assert.Contains("directory-size", dictionary.Keys); + Assert.Contains("super-dir-entries", dictionary.Keys); + Assert.Contains("slice-size", dictionary.Keys); + } + + private void LogDictionary(Dictionary dictionary, string caption) + { + Log($"{caption}: {dictionary.Count} field(s)"); + var keys = new List(dictionary.Keys); + keys.Sort(StringComparer.Ordinal); + foreach (var key in keys) + { + Log($" {key}: {dictionary[key]}"); + } + } + + private void LogFullOnlyFields(Dictionary basicDictionary, Dictionary fullDictionary) + { + var keys = new List(); + foreach (var key in fullDictionary.Keys) + { + if (!basicDictionary.ContainsKey(key)) + { + keys.Add(key); + } + } + + keys.Sort(StringComparer.Ordinal); + if (keys.Count == 0) + { + Log("ArrayInfo full-only fields: (none)"); + } + else + { + Log($"ArrayInfo full-only fields: {keys.Count}"); + foreach (var key in keys) + { + Log($" {key}: {fullDictionary[key]}"); + } + } + } + private static void AssertIndexEntries(RedisArrayEntry[] actual, params ulong[] expected) { Assert.Equal(expected.Length, actual.Length);