Skip to content

Commit ebdc301

Browse files
committed
zscan
1 parent c7ee978 commit ebdc301

File tree

7 files changed

+155
-22
lines changed

7 files changed

+155
-22
lines changed

eng/StackExchange.Redis.Build/RespCommandGenerator.cs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,17 @@ private static bool IsRESPite(ITypeSymbol? symbol, RESPite type)
134134
private enum SERedis
135135
{
136136
CommandFlags,
137+
RedisValue,
138+
RedisKey,
137139
}
138140

139141
private static bool IsSERedis(ITypeSymbol? symbol, SERedis type)
140142
{
141143
static string NameOf(SERedis type) => type switch
142144
{
143145
SERedis.CommandFlags => nameof(SERedis.CommandFlags),
146+
SERedis.RedisValue => nameof(SERedis.RedisValue),
147+
SERedis.RedisKey => nameof(SERedis.RedisKey),
144148
_ => type.ToString(),
145149
};
146150

@@ -481,14 +485,15 @@ void AddLiteral(string token, LiteralFlags literalFlags)
481485
var val = attrib.ConstructorArguments[0].Value;
482486
var expr = val switch
483487
{
484-
string s => CodeLiteral(s),
485-
bool b => b ? "true" : "false",
488+
null when IsSERedis(param.Type, SERedis.RedisValue) | IsSERedis(param.Type, SERedis.RedisKey) => ".IsNull is false",
489+
string s => " != " + CodeLiteral(s),
490+
bool b => b ? " is false" : " is true", // if we *ignore* true, then "incN = foo is false"
486491
long l when attrib.ConstructorArguments[0].Type is INamedTypeSymbol { EnumUnderlyingType: not null } enumType
487-
=> GetEnumExpression(enumType, l),
488-
long l => l.ToString(CultureInfo.InvariantCulture),
492+
=> " != " + GetEnumExpression(enumType, l),
493+
long l => " != " + l.ToString(CultureInfo.InvariantCulture),
489494
int i when attrib.ConstructorArguments[0].Type is INamedTypeSymbol { EnumUnderlyingType: not null } enumType
490-
=> GetEnumExpression(enumType, i),
491-
int i => i.ToString(CultureInfo.InvariantCulture),
495+
=> " != " + GetEnumExpression(enumType, i),
496+
int i => " != " + i.ToString(CultureInfo.InvariantCulture),
492497
_ => null,
493498
};
494499

@@ -1016,10 +1021,10 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null)
10161021
case ParameterFlags.Nullable | ParameterFlags.IgnoreExpression:
10171022
sb.Append(" is { } __val").Append(parameter.ArgIndex)
10181023
.Append(" && __val").Append(parameter.ArgIndex)
1019-
.Append(" != ").Append(parameter.IgnoreExpression);
1024+
.Append(parameter.IgnoreExpression);
10201025
break;
10211026
case ParameterFlags.IgnoreExpression:
1022-
sb.Append(" != ").Append(parameter.IgnoreExpression);
1027+
sb.Append(parameter.IgnoreExpression);
10231028
break;
10241029
case ParameterFlags.Collection:
10251030
// non-nullable collection; literals already handled
@@ -1103,7 +1108,7 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null)
11031108
// help identify what this is (not needed for collections, since foo.Count etc)
11041109
sb.Append(" // ");
11051110
WriteParameterName(parameter);
1106-
if (argCount != 1) sb.Append(" (").Append(parameter.Name).Append(")"); // give an example
1111+
if (tuple.Value.ShareCount != 1) sb.Append(" (").Append(parameter.Name).Append(")"); // give an example
11071112
}
11081113

11091114
if (literalCount != 0)

src/RESPite.StackExchange.Redis/RedisCommands.SortedSetCommands.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,14 @@ public static partial RespOperation<SortedSetEntry[]> ZRevRangeByScoreWithScores
758758
RedisKey key,
759759
RedisValue member);
760760

761+
[RespCommand(Parser = "RespParsers.ZScanSimple")]
762+
public static partial RespOperation<ScanResult<SortedSetEntry>> ZScan(
763+
this in SortedSetCommands context,
764+
RedisKey key,
765+
long cursor,
766+
[RespPrefix("MATCH"), RespIgnore] RedisValue pattern = default,
767+
[RespPrefix("COUNT"), RespIgnore(10)] long count = 10);
768+
761769
[RespCommand]
762770
public static partial RespOperation<double?> ZScore(
763771
this in SortedSetCommands context,
Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,41 @@
1-
namespace RESPite.StackExchange.Redis;
1+
using System.Buffers;
2+
using System.Runtime.CompilerServices;
3+
4+
namespace RESPite.StackExchange.Redis;
25

36
internal static partial class RedisCommands
47
{
58
public static ref readonly RespContext Self(this in RespContext context)
69
=> ref context; // this just proves that the above are well-defined in terms of escape analysis
710
}
11+
12+
public readonly struct ScanResult<T>
13+
{
14+
private const int MSB = 1 << 31;
15+
private readonly int _countAndIsPooled; // and use MSB for "ispooled"
16+
private readonly T[] values;
17+
18+
public ScanResult(long cursor, T[] values)
19+
{
20+
Cursor = cursor;
21+
this.values = values;
22+
_countAndIsPooled = values.Length;
23+
}
24+
internal ScanResult(long cursor, T[] values, int count)
25+
{
26+
this.Cursor = cursor;
27+
this.values = values;
28+
_countAndIsPooled = count | MSB;
29+
}
30+
31+
public long Cursor { get; }
32+
public ReadOnlySpan<T> Values => new(values, 0, _countAndIsPooled & ~MSB);
33+
34+
internal void UnsafeRecycle()
35+
{
36+
var arr = values;
37+
bool recycle = (_countAndIsPooled & MSB) != 0;
38+
Unsafe.AsRef(in this) = default; // best effort at salting the earth
39+
if (recycle && arr is not null) ArrayPool<T>.Shared.Return(arr);
40+
}
41+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using RESPite.Messages;
2+
using StackExchange.Redis;
3+
4+
namespace RESPite.StackExchange.Redis;
5+
6+
public static partial class RespParsers
7+
{
8+
internal static IRespParser<ScanResult<SortedSetEntry>> ZScanSimple = ScanResultParser.NonLeased;
9+
internal static IRespParser<ScanResult<SortedSetEntry>> ZScanLeased = ScanResultParser.Leased;
10+
11+
private sealed class ScanResultParser : IRespParser<ScanResult<SortedSetEntry>>
12+
{
13+
public static readonly ScanResultParser NonLeased = new(false);
14+
public static readonly ScanResultParser Leased = new(true);
15+
private readonly bool _leased;
16+
private ScanResultParser(bool leased) => _leased = leased;
17+
18+
ScanResult<SortedSetEntry> IRespParser<ScanResult<SortedSetEntry>>.Parse(ref RespReader reader)
19+
{
20+
reader.DemandAggregate();
21+
reader.MoveNextScalar();
22+
var cursor = reader.ReadInt64();
23+
reader.MoveNextAggregate();
24+
if (_leased)
25+
{
26+
var values = DefaultParser.ReadLeasedSortedSetEntryArray(ref reader, out int count);
27+
return new(cursor, values, count);
28+
}
29+
else
30+
{
31+
var values = DefaultParser.ReadSortedSetEntryArray(ref reader);
32+
return new(cursor, values);
33+
}
34+
}
35+
}
36+
}

src/RESPite.StackExchange.Redis/RespParsers.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace RESPite.StackExchange.Redis;
66

7-
public static class RespParsers
7+
public static partial class RespParsers
88
{
99
public static IRespParser<RedisValue> RedisValue => DefaultParser.Instance;
1010
public static IRespParser<RedisValue[]> RedisValueArray => DefaultParser.Instance;
@@ -114,13 +114,21 @@ ListPopResult IRespParser<ListPopResult>.Parse(ref RespReader reader)
114114
}
115115

116116
SortedSetEntry[] IRespParser<SortedSetEntry[]>.Parse(ref RespReader reader)
117-
{
118-
return reader.ReadPairArray(
119-
SharedReadRedisValue,
120-
static (ref RespReader reader) => reader.ReadDouble(),
121-
static (x, y) => new SortedSetEntry(x, y),
122-
scalar: true)!;
123-
}
117+
=> ReadSortedSetEntryArray(ref reader);
118+
119+
internal static SortedSetEntry[] ReadSortedSetEntryArray(ref RespReader reader) => reader.ReadPairArray(
120+
SharedReadRedisValue,
121+
static (ref RespReader reader) => reader.ReadDouble(),
122+
static (x, y) => new SortedSetEntry(x, y),
123+
scalar: true)!;
124+
125+
internal static SortedSetEntry[] ReadLeasedSortedSetEntryArray(ref RespReader reader, out int count)
126+
=> reader.ReadLeasedPairArray(
127+
SharedReadRedisValue,
128+
static (ref RespReader reader) => reader.ReadDouble(),
129+
static (x, y) => new SortedSetEntry(x, y),
130+
out count,
131+
scalar: true)!;
124132

125133
SortedSetEntry? IRespParser<SortedSetEntry?>.Parse(ref RespReader reader)
126134
{

src/RESPite/Messages/RespReader.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,4 +1727,46 @@ public readonly T ReadEnum<T>(T unknownValue = default) where T : struct, Enum
17271727
}
17281728
return result;
17291729
}
1730+
internal TResult[]? ReadLeasedPairArray<TFirst, TSecond, TResult>(
1731+
Projection<TFirst> first,
1732+
Projection<TSecond> second,
1733+
Func<TFirst, TSecond, TResult> combine,
1734+
out int count,
1735+
bool scalar = true)
1736+
{
1737+
DemandAggregate();
1738+
if (IsNull)
1739+
{
1740+
count = 0;
1741+
return null;
1742+
}
1743+
int sourceLength = AggregateLength();
1744+
count = sourceLength >> 1;
1745+
if (count is 0) return [];
1746+
1747+
var oversized = ArrayPool<TResult>.Shared.Rent(count);
1748+
var result = oversized.AsSpan(0, count);
1749+
if (scalar)
1750+
{
1751+
// if the data to be consumed is simple (scalar), we can use
1752+
// a simpler path that doesn't need to worry about RESP subtrees
1753+
for (int i = 0; i < result.Length; i++)
1754+
{
1755+
MoveNextScalar();
1756+
var x = first(ref this);
1757+
MoveNextScalar();
1758+
var y = second(ref this);
1759+
result[i] = combine(x, y);
1760+
}
1761+
// if we have an odd number of source elements, skip the last one
1762+
if ((sourceLength & 1) != 0) MoveNextScalar();
1763+
}
1764+
else
1765+
{
1766+
var agg = AggregateChildren();
1767+
agg.FillAll(result, first, second, combine);
1768+
agg.MovePast(out this);
1769+
}
1770+
return oversized;
1771+
}
17301772
}

src/RESPite/RespIgnoreAttribute.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ namespace RESPite;
55

66
[AttributeUsage(AttributeTargets.Parameter)]
77
[Conditional("DEBUG"), ImmutableObject(true)]
8-
public sealed class RespIgnoreAttribute : Attribute
8+
public sealed class RespIgnoreAttribute(object? value = null) : Attribute
99
{
10-
private readonly object _value;
11-
public object Value => _value;
12-
public RespIgnoreAttribute(object value) => _value = value;
10+
// note; nulls are always ignored (taking NRTs into account); the purpose
11+
// of an explicit null is for RedisValue - this prompts HasValue checks (i.e. non-trivial value).
12+
public object? Value => value;
1313
}

0 commit comments

Comments
 (0)