Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/Arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
2 changes: 1 addition & 1 deletion docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
190 changes: 187 additions & 3 deletions eng/StackExchange.Redis.Build/AsciiHashGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> 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(
Expand All @@ -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);
Expand Down Expand Up @@ -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<object> 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)
Expand Down Expand Up @@ -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("// <auto-generated />")
.AppendLine().Append("// ").Append(GetType().Name).Append(" v").Append(GetVersion()).AppendLine();
Expand All @@ -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());
}
Expand Down Expand Up @@ -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 ?? "")
Expand Down
60 changes: 45 additions & 15 deletions src/StackExchange.Redis/ArrayGrepRequest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using RESPite;

Expand All @@ -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;
Expand Down Expand Up @@ -131,13 +134,37 @@ public long? Limit
private long _limit;

/// <summary>
/// Indicates whether matches are performed in a case-insensitive manner.
/// Indicates whether matches are performed in a case-sensitive manner.
/// </summary>
/// <remarks>Corresponds to the <c>NOCASE</c> parameter.</remarks>
[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;
}

/// <summary>
/// Indicates whether matches are performed in a case-insensitive manner.
/// </summary>
/// <remarks>Corresponds to the <c>NOCASE</c> parameter.</remarks>
public bool IsCaseInsensitive
{
get => GetFlag(LocalFlags.CaseInsensitive);
set => SetFlag(LocalFlags.CaseInsensitive, value);
}

/// <summary>
/// Indicates whether the query order should be reversed; this is equivalent to
/// reversing the order of <see cref="Start"/> and <see cref="End"/>.
/// </summary>
/// <remarks>Corresponds to the <c>NOCASE</c> parameter.</remarks>
public bool IsReversed
{
get => GetFlag(LocalFlags.Reversed);
set => SetFlag(LocalFlags.Reversed, value);
}

/// <summary>
Expand Down Expand Up @@ -317,35 +344,38 @@ 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;
return count;
}
}

protected override void WriteImpl(PhysicalConnection physical)
private static void AddIndex(PhysicalConnection physical, RedisArrayIndex? index, ReadOnlySpan<byte> 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++)
Expand All @@ -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)
Expand Down
Loading
Loading