Skip to content

Commit 2800b3a

Browse files
authored
Fix ARGREP NOCASE logic inversion and support ARINFO FULL (#3087)
* - fix snafu with NOCASE (logic inverted) - add IsReversed to simplify reversal logic * PR ref * - support ARINFO FULL - refactors ArrayInfo initialization to be span based, and put the main logic inside ArrayInfo technically this is a break, but: `[Experimental]` * release notes
1 parent 45b819d commit 2800b3a

17 files changed

Lines changed: 524 additions & 146 deletions

docs/Arrays.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ Use `ArrayInfoAsync` for metadata:
218218

219219
```csharp
220220
ArrayInfo info = await db.ArrayInfoAsync(key);
221+
ArrayInfo fullInfo = await db.ArrayInfoAsync(key, full: true);
221222

222223
Console.WriteLine($"Count: {info.Count}");
223224
Console.WriteLine($"Length: {info.Length}");

docs/ReleaseNotes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Current package versions:
88

99
## Unreleased
1010

11-
- (none)
11+
- 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))
1212

1313
## 2.13.10
1414

eng/StackExchange.Redis.Build/AsciiHashGenerator.cs

Lines changed: 187 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3232
.Where(pair => pair.Name is { Length: > 0 })
3333
.Collect();
3434

35+
// looking for [AsciiHash] partial static bool TryFormat(enum input, out string/ReadOnlySpan<byte> output) { }
36+
var formatMethods = context.SyntaxProvider
37+
.CreateSyntaxProvider(
38+
static (node, _) => node is MethodDeclarationSyntax decl && IsStaticPartial(decl.Modifiers) &&
39+
HasAsciiHash(decl.AttributeLists),
40+
TransformFormatMethods)
41+
.Where(pair => pair.Name is { Length: > 0 })
42+
.Collect();
43+
3544
// looking for [AsciiHash("some type")] enum Foo { }
3645
var enums = context.SyntaxProvider
3746
.CreateSyntaxProvider(
@@ -41,9 +50,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4150
.Collect();
4251

4352
context.RegisterSourceOutput(
44-
types.Combine(methods).Combine(enums),
53+
types.Combine(methods).Combine(formatMethods).Combine(enums),
4554
(ctx, content) =>
46-
Generate(ctx, content.Left.Left, content.Left.Right, content.Right));
55+
Generate(ctx, content.Left.Left.Left, content.Left.Left.Right, content.Left.Right, content.Right));
4756

4857
static bool IsStaticPartial(SyntaxTokenList tokens)
4958
=> tokens.Any(SyntaxKind.StaticKeyword) && tokens.Any(SyntaxKind.PartialKeyword);
@@ -309,6 +318,80 @@ static bool IsBytes(ITypeSymbol type)
309318
return (ns, parentType, method.DeclaredAccessibility, method.Name, from, to, caseSensitive, builder.Build(), defaultValue);
310319
}
311320

321+
private (string Namespace, string ParentType, Accessibility Accessibility, string Name,
322+
(string Type, string Name, RefKind RefKind) From, (string Type, string Name, RefKind RefKind, bool IsBytes) To,
323+
BasicArray<(string EnumMember, string FormatText)> Members) TransformFormatMethods(
324+
GeneratorSyntaxContext ctx,
325+
CancellationToken cancellationToken)
326+
{
327+
if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) is not IMethodSymbol
328+
{
329+
IsStatic: true,
330+
IsPartialDefinition: true,
331+
PartialImplementationPart: null,
332+
Arity: 0,
333+
ReturnType.SpecialType: SpecialType.System_Boolean,
334+
Parameters:
335+
{
336+
IsDefaultOrEmpty: false,
337+
Length: 2,
338+
},
339+
} method) return default;
340+
341+
if (TryGetAsciiHashAttribute(method.GetAttributes()) is not { }) return default;
342+
343+
if (method.ContainingType is not { } containingType) return default;
344+
var parentType = GetName(containingType);
345+
var ns = containingType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
346+
347+
var arg = method.Parameters[0];
348+
if (arg is not
349+
{
350+
IsOptional: false,
351+
RefKind: RefKind.None or RefKind.In or RefKind.Ref or RefKind.RefReadOnlyParameter,
352+
Type: INamedTypeSymbol { TypeKind: TypeKind.Enum },
353+
}) return default;
354+
var from = (arg.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), arg.Name, arg.RefKind);
355+
356+
var enumMembers = arg.Type.GetMembers();
357+
var builder = new BasicArray<(string EnumMember, string FormatText)>.Builder(enumMembers.Length);
358+
HashSet<object> values = new();
359+
foreach (var member in enumMembers)
360+
{
361+
if (member is IFieldSymbol { IsStatic: true, IsConst: true } field)
362+
{
363+
var rawValue = GetRawValue(field.Name, TryGetAsciiHashAttribute(member.GetAttributes()));
364+
if (string.IsNullOrWhiteSpace(rawValue)) continue;
365+
if (field.ConstantValue is { } constValue && !values.Add(constValue)) continue;
366+
builder.Add((field.Name, rawValue));
367+
}
368+
}
369+
370+
arg = method.Parameters[1];
371+
if (arg is not
372+
{
373+
IsOptional: false,
374+
RefKind: RefKind.Out,
375+
}) return default;
376+
bool toBytes = IsReadOnlySpanOfByte(arg.Type);
377+
if (arg.Type.SpecialType != SpecialType.System_String && !toBytes) return default;
378+
var to = (arg.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), arg.Name, arg.RefKind, toBytes);
379+
380+
return (ns, parentType, method.DeclaredAccessibility, method.Name, from, to, builder.Build());
381+
382+
static bool IsReadOnlySpanOfByte(ITypeSymbol type)
383+
{
384+
return type is INamedTypeSymbol
385+
{
386+
TypeKind: TypeKind.Struct,
387+
Arity: 1,
388+
Name: "ReadOnlySpan",
389+
ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true },
390+
TypeArguments: { Length: 1 } typeArguments,
391+
} && typeArguments[0].SpecialType == SpecialType.System_Byte;
392+
}
393+
}
394+
312395
private bool IsCaseSensitive(AttributeData attrib)
313396
{
314397
foreach (var member in attrib.NamedArguments)
@@ -343,9 +426,13 @@ private void Generate(
343426
(string Type, string Name, bool IsBytes, RefKind RefKind) From, (string Type, string Name, RefKind RefKind) To,
344427
(string Name, bool Value, RefKind RefKind) CaseSensitive,
345428
BasicArray<(string EnumMember, string ParseText)> Members, int DefaultValue)> parseMethods,
429+
ImmutableArray<(string Namespace, string ParentType, Accessibility Accessibility, string Name,
430+
(string Type, string Name, RefKind RefKind) From,
431+
(string Type, string Name, RefKind RefKind, bool IsBytes) To,
432+
BasicArray<(string EnumMember, string FormatText)> Members)> formatMethods,
346433
ImmutableArray<(string Namespace, string ParentType, string Name, int Count, int MaxChars, int MaxBytes)> enums)
347434
{
348-
if (types.IsDefaultOrEmpty & parseMethods.IsDefaultOrEmpty & enums.IsDefaultOrEmpty) return; // nothing to do
435+
if (types.IsDefaultOrEmpty & parseMethods.IsDefaultOrEmpty & formatMethods.IsDefaultOrEmpty & enums.IsDefaultOrEmpty) return; // nothing to do
349436

350437
var sb = new StringBuilder("// <auto-generated />")
351438
.AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine();
@@ -356,6 +443,7 @@ private void Generate(
356443

357444
BuildTypeImplementations(sb, types);
358445
BuildEnumParsers(sb, parseMethods);
446+
BuildEnumFormatters(sb, formatMethods);
359447
BuildEnumLengths(sb, enums);
360448
ctx.AddSource(nameof(AsciiHash) + ".generated.cs", sb.ToString());
361449
}
@@ -644,6 +732,102 @@ void Write(bool caseSensitive)
644732
}
645733
}
646734

735+
private void BuildEnumFormatters(
736+
StringBuilder sb,
737+
in ImmutableArray<(string Namespace, string ParentType, Accessibility Accessibility, string Name,
738+
(string Type, string Name, RefKind RefKind) From,
739+
(string Type, string Name, RefKind RefKind, bool IsBytes) To,
740+
BasicArray<(string EnumMember, string FormatText)> Members)> enums)
741+
{
742+
if (enums.IsDefaultOrEmpty) return; // nope
743+
744+
int indent = 0;
745+
StringBuilder NewLine() => sb.AppendLine().Append(' ', indent * 4);
746+
747+
foreach (var grp in enums.GroupBy(l => (l.Namespace, l.ParentType)))
748+
{
749+
NewLine();
750+
int braces = 0;
751+
if (!string.IsNullOrWhiteSpace(grp.Key.Namespace))
752+
{
753+
NewLine().Append("namespace ").Append(grp.Key.Namespace);
754+
NewLine().Append("{");
755+
indent++;
756+
braces++;
757+
}
758+
759+
if (!string.IsNullOrWhiteSpace(grp.Key.ParentType))
760+
{
761+
if (grp.Key.ParentType.Contains('.')) // nested types
762+
{
763+
foreach (var part in grp.Key.ParentType.Split('.'))
764+
{
765+
NewLine().Append("partial class ").Append(part);
766+
NewLine().Append("{");
767+
indent++;
768+
braces++;
769+
}
770+
}
771+
else
772+
{
773+
NewLine().Append("partial class ").Append(grp.Key.ParentType);
774+
NewLine().Append("{");
775+
indent++;
776+
braces++;
777+
}
778+
}
779+
780+
foreach (var method in grp)
781+
{
782+
NewLine().Append(Format(method.Accessibility)).Append(" static partial bool ")
783+
.Append(method.Name).Append("(")
784+
.Append(Format(method.From.RefKind))
785+
.Append(method.From.Type).Append(" ").Append(method.From.Name).Append(", ")
786+
.Append(Format(method.To.RefKind))
787+
.Append(method.To.Type).Append(" ").Append(method.To.Name)
788+
.Append(")");
789+
790+
NewLine().Append("{");
791+
indent++;
792+
NewLine().Append("// ").Append(method.From.Type).Append(" has ").Append(method.Members.Length).Append(" formatted members");
793+
NewLine().Append("switch (").Append(method.From.Name).Append(")");
794+
NewLine().Append("{");
795+
indent++;
796+
797+
foreach (var member in method.Members)
798+
{
799+
var formatted = SyntaxFactory
800+
.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(member.FormatText))
801+
.ToFullString();
802+
if (method.To.IsBytes) formatted += "u8";
803+
804+
NewLine().Append("case ").Append(method.From.Type).Append(".").Append(member.EnumMember).Append(":");
805+
indent++;
806+
NewLine().Append(method.To.Name).Append(" = ").Append(formatted).Append(";");
807+
NewLine().Append("return true;");
808+
indent--;
809+
}
810+
811+
NewLine().Append("default:");
812+
indent++;
813+
NewLine().Append(method.To.Name).Append(" = ").Append(method.To.IsBytes ? "default" : "default!").Append(";");
814+
NewLine().Append("return false;");
815+
indent--;
816+
indent--;
817+
NewLine().Append("}");
818+
indent--;
819+
NewLine().Append("}");
820+
}
821+
822+
// handle any closing braces
823+
while (braces-- > 0)
824+
{
825+
indent--;
826+
NewLine().Append("}");
827+
}
828+
}
829+
}
830+
647831
private static bool HasCaseSensitiveCharacters(string value)
648832
{
649833
foreach (char c in value ?? "")

src/StackExchange.Redis/ArrayGrepRequest.cs

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel;
34
using System.Diagnostics.CodeAnalysis;
45
using RESPite;
56

@@ -16,12 +17,14 @@ private enum LocalFlags : byte
1617
{
1718
None = 0,
1819
IsFrozen = 1 << 0,
19-
CaseSensitive = 1 << 1,
20+
CaseInsensitive = 1 << 1,
2021
IsIntersection = 1 << 2,
2122
StartSpecified = 1 << 3,
2223
EndSpecified = 1 << 4,
2324
LimitSpecified = 1 << 5,
2425
IncludeValues = 1 << 6,
26+
Reversed = 1 << 7,
27+
// warning: next flag needs : ushort
2528
}
2629

2730
private void Freeze() => _flags |= LocalFlags.IsFrozen;
@@ -131,13 +134,37 @@ public long? Limit
131134
private long _limit;
132135

133136
/// <summary>
134-
/// Indicates whether matches are performed in a case-insensitive manner.
137+
/// Indicates whether matches are performed in a case-sensitive manner.
135138
/// </summary>
136139
/// <remarks>Corresponds to the <c>NOCASE</c> parameter.</remarks>
140+
[Browsable(false)]
141+
[EditorBrowsable(EditorBrowsableState.Never)]
142+
[Obsolete("Prefer " + nameof(IsCaseInsensitive))]
137143
public bool IsCaseSensitive
138144
{
139-
get => GetFlag(LocalFlags.CaseSensitive);
140-
set => SetFlag(LocalFlags.CaseSensitive, value);
145+
get => !IsCaseInsensitive;
146+
set => IsCaseInsensitive = !value;
147+
}
148+
149+
/// <summary>
150+
/// Indicates whether matches are performed in a case-insensitive manner.
151+
/// </summary>
152+
/// <remarks>Corresponds to the <c>NOCASE</c> parameter.</remarks>
153+
public bool IsCaseInsensitive
154+
{
155+
get => GetFlag(LocalFlags.CaseInsensitive);
156+
set => SetFlag(LocalFlags.CaseInsensitive, value);
157+
}
158+
159+
/// <summary>
160+
/// Indicates whether the query order should be reversed; this is equivalent to
161+
/// reversing the order of <see cref="Start"/> and <see cref="End"/>.
162+
/// </summary>
163+
/// <remarks>Corresponds to the <c>NOCASE</c> parameter.</remarks>
164+
public bool IsReversed
165+
{
166+
get => GetFlag(LocalFlags.Reversed);
167+
set => SetFlag(LocalFlags.Reversed, value);
141168
}
142169

143170
/// <summary>
@@ -317,35 +344,38 @@ public override int ArgCount
317344
}
318345

319346
if (request.IsIntersection) count++;
320-
if (request.IsCaseSensitive) count++;
347+
if (request.IsCaseInsensitive) count++;
321348
if (request.IncludeValues) count++;
322349
var limit = request.Limit;
323350
if (limit.HasValue) count += 2;
324351
return count;
325352
}
326353
}
327354

328-
protected override void WriteImpl(PhysicalConnection physical)
355+
private static void AddIndex(PhysicalConnection physical, RedisArrayIndex? index, ReadOnlySpan<byte> fallback)
329356
{
330-
physical.WriteHeader(Command, ArgCount);
331-
physical.WriteBulkString(key);
332-
var index = request.Start;
333357
if (index.HasValue)
334358
{
335359
physical.WriteBulkString(index.GetValueOrDefault().Value);
336360
}
337361
else
338362
{
339-
physical.WriteRaw("$1\r\n-\r\n"u8);
363+
physical.WriteRaw(fallback);
340364
}
341-
index = request.End;
342-
if (index.HasValue)
365+
}
366+
protected override void WriteImpl(PhysicalConnection physical)
367+
{
368+
physical.WriteHeader(Command, ArgCount);
369+
physical.WriteBulkString(key);
370+
if (request.IsReversed)
343371
{
344-
physical.WriteBulkString(index.GetValueOrDefault().Value);
372+
AddIndex(physical, request.End, "$1\r\n+\r\n"u8);
373+
AddIndex(physical, request.Start, "$1\r\n-\r\n"u8);
345374
}
346375
else
347376
{
348-
physical.WriteRaw("$1\r\n+\r\n"u8);
377+
AddIndex(physical, request.Start, "$1\r\n-\r\n"u8);
378+
AddIndex(physical, request.End, "$1\r\n+\r\n"u8);
349379
}
350380
var pCount = request.Count;
351381
for (int i = 0; i < pCount; i++)
@@ -354,7 +384,7 @@ protected override void WriteImpl(PhysicalConnection physical)
354384
}
355385

356386
if (request.IsIntersection) physical.WriteRaw("$3\r\nAND\r\n"u8);
357-
if (request.IsCaseSensitive) physical.WriteRaw("$6\r\nNOCASE\r\n"u8);
387+
if (request.IsCaseInsensitive) physical.WriteRaw("$6\r\nNOCASE\r\n"u8);
358388
if (request.IncludeValues) physical.WriteRaw("$10\r\nWITHVALUES\r\n"u8);
359389
var limit = request.Limit;
360390
if (limit.HasValue)

0 commit comments

Comments
 (0)