Skip to content

Commit 4b6a58e

Browse files
committed
hashes, but fix the codegen to work better for expire etc scenarios (optional NX|XX|...)
1 parent 7858604 commit 4b6a58e

File tree

8 files changed

+150
-70
lines changed

8 files changed

+150
-70
lines changed

StackExchange.Redis.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PONG/@EntryIndexedValue">PONG</s:String>
44
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RES/@EntryIndexedValue">RES</s:String>
55
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SE/@EntryIndexedValue">SE</s:String>
6+
<s:Boolean x:Key="/Default/UserDictionary/Words/=maxlen/@EntryIndexedValue">True</s:Boolean>
67
<s:Boolean x:Key="/Default/UserDictionary/Words/=pite/@EntryIndexedValue">True</s:Boolean>
78
<s:Boolean x:Key="/Default/UserDictionary/Words/=RESPite/@EntryIndexedValue">True</s:Boolean>
89
<s:Boolean x:Key="/Default/UserDictionary/Words/=pubsub/@EntryIndexedValue">True</s:Boolean>

eng/StackExchange.Redis.Build/RespCommandGenerator.cs

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ void AddLiteral(string token, LiteralFlags literalFlags)
460460
{
461461
if (IsRESPite(attrib.AttributeClass, RESPite.RespPrefixAttribute))
462462
{
463-
if (attrib.ConstructorArguments[0].Value?.ToString() is { Length: > 0 } val)
463+
if (attrib.ConstructorArguments[0].Value?.ToString() is { } val)
464464
{
465465
AddNotes(ref debugNote, $"prefix {val}");
466466
AddLiteral(val, LiteralFlags.None);
@@ -479,19 +479,38 @@ void AddLiteral(string token, LiteralFlags literalFlags)
479479
if (IsRESPite(attrib.AttributeClass, RESPite.RespIgnoreAttribute))
480480
{
481481
var val = attrib.ConstructorArguments[0].Value;
482-
if (val is string s)
482+
var expr = val switch
483483
{
484-
ignoreExpression = CodeLiteral(s);
485-
}
486-
else if (val is bool b)
484+
string s => CodeLiteral(s),
485+
bool b => b ? "true" : "false",
486+
long l when attrib.ConstructorArguments[0].Type is INamedTypeSymbol { EnumUnderlyingType: not null } enumType
487+
=> GetEnumExpression(enumType, l),
488+
long l => l.ToString(CultureInfo.InvariantCulture),
489+
int i when attrib.ConstructorArguments[0].Type is INamedTypeSymbol { EnumUnderlyingType: not null } enumType
490+
=> GetEnumExpression(enumType, i),
491+
int i => i.ToString(CultureInfo.InvariantCulture),
492+
_ => null,
493+
};
494+
495+
if (expr is not null)
487496
{
488-
ignoreExpression = b ? "true" : "false";
497+
flags |= ParameterFlags.IgnoreExpression;
498+
ignoreExpression = expr;
489499
}
490-
else if (val is long l)
500+
501+
static string GetEnumExpression(INamedTypeSymbol enumType, object value)
491502
{
492-
ignoreExpression = l.ToString(CultureInfo.InvariantCulture);
503+
foreach (var member in enumType.GetMembers())
504+
{
505+
if (member is IFieldSymbol { IsStatic: true, IsConst: true } field
506+
&& Equals(field.ConstantValue, value))
507+
{
508+
return $"{GetFullName(enumType)}.{field.Name}";
509+
}
510+
}
511+
512+
return $"({GetFullName(enumType)}){value}";
493513
}
494-
if (ignoreExpression is not null) flags |= ParameterFlags.IgnoreExpression;
495514
}
496515
}
497516
}
@@ -1071,11 +1090,11 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null)
10711090
}
10721091
else
10731092
{
1074-
sb.Append("(");
1093+
if (literalCount != 0) sb.Append("(");
10751094
WriteParameterName(parameter);
10761095
if (parameter.IsNullable) sb.Append("!");
10771096
sb.Append((parameter.Flags & ParameterFlags.CollectionWithCount) == 0 ? ".Length" : ".Count");
1078-
sb.Append(" + ").Append(1 + literalCount).Append(")");
1097+
if (literalCount != 0) sb.Append(" + ").Append(literalCount).Append(")");
10791098
}
10801099

10811100
sb.Append(" : 0)");
@@ -1084,6 +1103,17 @@ void WriteParameterName(in ParameterTuple p, StringBuilder? target = null)
10841103
// help identify what this is (not needed for collections, since foo.Count etc)
10851104
sb.Append(" // ");
10861105
WriteParameterName(parameter);
1106+
if (argCount != 1) sb.Append(" (").Append(parameter.Name).Append(")"); // give an example
1107+
}
1108+
1109+
if (literalCount != 0)
1110+
{
1111+
if (parameter.IsCollection) sb.Append(" //");
1112+
sb.Append(" with");
1113+
foreach (var literal in parameter.Literals.Span)
1114+
{
1115+
sb.Append(" ").Append(string.IsNullOrEmpty(literal.Token) ? "(count)" : literal.Token);
1116+
}
10871117
}
10881118
}
10891119
index++;
@@ -1129,9 +1159,11 @@ void WriteLiteral(in ParameterTuple p, bool suffix)
11291159
{
11301160
if (p.IsCollection)
11311161
{
1162+
sb = NewLine().Append("writer.WriteBulkString(");
11321163
WriteParameterName(p);
11331164
if (p.IsNullable) sb.Append("!");
1134-
sb.Append((p.Flags & ParameterFlags.CollectionWithCount) == 0 ? ".Length" : ".Count");
1165+
sb.Append((p.Flags & ParameterFlags.CollectionWithCount) == 0 ? ".Length" : ".Count")
1166+
.Append(");");
11351167
}
11361168
else
11371169
{

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,28 @@ internal static partial class HashCommandsExtensions
2929
[RespCommand]
3030
public static partial RespOperation<bool> HExists(this in HashCommands context, RedisKey key, RedisValue hashField);
3131

32+
[RespCommand]
33+
private static partial RespOperation<RedisValue[]> HExpire(
34+
this in HashCommands context,
35+
RedisKey key,
36+
long seconds,
37+
[RespIgnore(ExpireWhen.Always)] ExpireWhen when,
38+
[RespPrefix("FIELDS"), RespPrefix] RedisValue[] hashFields);
39+
40+
[RespCommand]
41+
private static partial RespOperation<RedisValue[]> HPExpire(
42+
this in HashCommands context,
43+
RedisKey key,
44+
long milliseconds,
45+
[RespIgnore(ExpireWhen.Always)] ExpireWhen when,
46+
[RespPrefix("FIELDS"), RespPrefix] RedisValue[] hashFields);
47+
3248
[RespCommand]
3349
public static partial RespOperation<RedisValue> HGet(this in HashCommands context, RedisKey key, RedisValue hashField);
3450

51+
[RespCommand("hget")]
52+
public static partial RespOperation<Lease<byte>?> HGetLease(this in HashCommands context, RedisKey key, RedisValue hashField);
53+
3554
[RespCommand]
3655
public static partial RespOperation<HashEntry[]> HGetAll(this in HashCommands context, RedisKey key);
3756

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

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using RESPite.Messages;
33
using StackExchange.Redis;
44

5+
// ReSharper disable MemberCanBePrivate.Global
6+
// ReSharper disable InconsistentNaming
57
namespace RESPite.StackExchange.Redis;
68

79
internal static partial class RedisCommands
@@ -41,43 +43,63 @@ public static partial RespOperation<bool> Copy(
4143
[RespCommand]
4244
public static partial RespOperation<long> Exists(this in KeyCommands context, [RespKey] RedisKey[] keys);
4345

44-
public static RespOperation<bool> Expire(this in KeyCommands context, RedisKey key, TimeSpan? expiry, ExpireWhen when = ExpireWhen.Always)
46+
public static RespOperation<bool> Expire(
47+
this in KeyCommands context,
48+
RedisKey key,
49+
TimeSpan? expiry,
50+
ExpireWhen when = ExpireWhen.Always)
4551
{
4652
if (expiry is null || expiry == TimeSpan.MaxValue)
4753
{
4854
if (when != ExpireWhen.Always) Throw(when);
4955
return Persist(context, key);
5056
static void Throw(ExpireWhen when) => throw new ArgumentException($"PERSIST cannot be used with {when}.");
5157
}
58+
5259
var millis = (long)expiry.GetValueOrDefault().TotalMilliseconds;
5360
if (millis % 1000 == 0) // use seconds
5461
{
5562
return Expire(context, key, millis / 1000, when);
5663
}
64+
5765
return PExpire(context, key, millis, when);
5866
}
5967

60-
[RespCommand(Formatter = "ExpireFormatter.Instance")]
61-
public static partial RespOperation<bool> Expire(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always);
68+
[RespCommand]
69+
public static partial RespOperation<bool> Expire(
70+
this in KeyCommands context,
71+
RedisKey key,
72+
long seconds,
73+
[RespIgnore(ExpireWhen.Always)] ExpireWhen when = ExpireWhen.Always);
6274

63-
public static RespOperation<bool> ExpireAt(this in KeyCommands context, RedisKey key, DateTime? expiry, ExpireWhen when = ExpireWhen.Always)
75+
public static RespOperation<bool> ExpireAt(
76+
this in KeyCommands context,
77+
RedisKey key,
78+
DateTime? expiry,
79+
ExpireWhen when = ExpireWhen.Always)
6480
{
6581
if (expiry is null || expiry == DateTime.MaxValue)
6682
{
6783
if (when != ExpireWhen.Always) Throw(when);
6884
return Persist(context, key);
6985
static void Throw(ExpireWhen when) => throw new ArgumentException($"PERSIST cannot be used with {when}.");
7086
}
87+
7188
var millis = RedisDatabase.GetUnixTimeMilliseconds(expiry.GetValueOrDefault());
7289
if (millis % 1000 == 0) // use seconds
7390
{
7491
return ExpireAt(context, key, millis / 1000, when);
7592
}
93+
7694
return PExpireAt(context, key, millis, when);
7795
}
7896

79-
[RespCommand(Formatter = "ExpireFormatter.Instance")]
80-
public static partial RespOperation<bool> ExpireAt(this in KeyCommands context, RedisKey key, long seconds, ExpireWhen when = ExpireWhen.Always);
97+
[RespCommand]
98+
public static partial RespOperation<bool> ExpireAt(
99+
this in KeyCommands context,
100+
RedisKey key,
101+
long seconds,
102+
[RespIgnore(ExpireWhen.Always)] ExpireWhen when = ExpireWhen.Always);
81103

82104
[RespCommand(Parser = "RespParsers.DateTimeFromSeconds")]
83105
public static partial RespOperation<DateTime?> ExpireTime(this in KeyCommands context, RedisKey key);
@@ -86,22 +108,38 @@ public static RespOperation<bool> ExpireAt(this in KeyCommands context, RedisKey
86108
public static partial RespOperation<bool> Move(this in KeyCommands context, RedisKey key, int db);
87109

88110
[RespCommand("object")]
89-
public static partial RespOperation<string?> ObjectEncoding(this in KeyCommands context, [RespPrefix("ENCODING")] RedisKey key);
111+
public static partial RespOperation<string?> ObjectEncoding(
112+
this in KeyCommands context,
113+
[RespPrefix("ENCODING")] RedisKey key);
90114

91115
[RespCommand("object")]
92-
public static partial RespOperation<long?> ObjectFreq(this in KeyCommands context, [RespPrefix("FREQ")] RedisKey key);
116+
public static partial RespOperation<long?> ObjectFreq(
117+
this in KeyCommands context,
118+
[RespPrefix("FREQ")] RedisKey key);
93119

94120
[RespCommand("object", Parser = "RespParsers.TimeSpanFromSeconds")]
95-
public static partial RespOperation<TimeSpan?> ObjectIdleTime(this in KeyCommands context, [RespPrefix("IDLETIME")] RedisKey key);
121+
public static partial RespOperation<TimeSpan?> ObjectIdleTime(
122+
this in KeyCommands context,
123+
[RespPrefix("IDLETIME")] RedisKey key);
96124

97125
[RespCommand("object")]
98-
public static partial RespOperation<long?> ObjectRefCount(this in KeyCommands context, [RespPrefix("REFCOUNT")] RedisKey key);
126+
public static partial RespOperation<long?> ObjectRefCount(
127+
this in KeyCommands context,
128+
[RespPrefix("REFCOUNT")] RedisKey key);
99129

100-
[RespCommand(Formatter = "ExpireFormatter.Instance")]
101-
public static partial RespOperation<bool> PExpire(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always);
130+
[RespCommand]
131+
public static partial RespOperation<bool> PExpire(
132+
this in KeyCommands context,
133+
RedisKey key,
134+
long milliseconds,
135+
[RespIgnore(ExpireWhen.Always)] ExpireWhen when = ExpireWhen.Always);
102136

103-
[RespCommand(Formatter = "ExpireFormatter.Instance")]
104-
public static partial RespOperation<bool> PExpireAt(this in KeyCommands context, RedisKey key, long milliseconds, ExpireWhen when = ExpireWhen.Always);
137+
[RespCommand]
138+
public static partial RespOperation<bool> PExpireAt(
139+
this in KeyCommands context,
140+
RedisKey key,
141+
long milliseconds,
142+
[RespIgnore(ExpireWhen.Always)] ExpireWhen when = ExpireWhen.Always);
105143

106144
[RespCommand(Parser = "RespParsers.DateTimeFromMilliseconds")]
107145
public static partial RespOperation<DateTime?> PExpireTime(this in KeyCommands context, RedisKey key);
@@ -137,7 +175,11 @@ public static RespOperation<bool> Rename(this in KeyCommands context, RedisKey k
137175
public static partial RespOperation<bool> RenameNx(this in KeyCommands context, RedisKey key, RedisKey newKey);
138176

139177
[RespCommand(Formatter = "RestoreFormatter.Instance")]
140-
public static partial RespOperation Restore(this in KeyCommands context, RedisKey key, TimeSpan? ttl, byte[] serializedValue);
178+
public static partial RespOperation Restore(
179+
this in KeyCommands context,
180+
RedisKey key,
181+
TimeSpan? ttl,
182+
byte[] serializedValue);
141183

142184
[RespCommand]
143185
public static partial RespOperation<bool> Touch(this in KeyCommands context, RedisKey key);
@@ -191,43 +233,6 @@ public void Format(
191233
}
192234
}
193235

194-
private sealed class ExpireFormatter : IRespFormatter<(RedisKey Key, long Value, ExpireWhen When)>
195-
{
196-
public static readonly ExpireFormatter Instance = new();
197-
private ExpireFormatter() { }
198-
199-
public void Format(
200-
scoped ReadOnlySpan<byte> command,
201-
ref RespWriter writer,
202-
in (RedisKey Key, long Value, ExpireWhen When) request)
203-
{
204-
writer.WriteCommand(command, request.When == ExpireWhen.Always ? 2 : 3);
205-
writer.Write(request.Key);
206-
writer.Write(request.Value);
207-
switch (request.When)
208-
{
209-
case ExpireWhen.Always:
210-
break;
211-
case ExpireWhen.HasExpiry:
212-
writer.WriteRaw("$2\r\nXX\r\n"u8);
213-
break;
214-
case ExpireWhen.HasNoExpiry:
215-
writer.WriteRaw("$2\r\nNX\r\n"u8);
216-
break;
217-
case ExpireWhen.GreaterThanCurrentExpiry:
218-
writer.WriteRaw("$2\r\nGT\r\n"u8);
219-
break;
220-
case ExpireWhen.LessThanCurrentExpiry:
221-
writer.WriteRaw("$2\r\nLT\r\n"u8);
222-
break;
223-
default:
224-
Throw();
225-
static void Throw() => throw new ArgumentOutOfRangeException(nameof(request.When));
226-
break;
227-
}
228-
}
229-
}
230-
231236
private sealed class RestoreFormatter : IRespFormatter<(RedisKey Key, TimeSpan? Ttl, byte[] SerializedValue)>
232237
{
233238
public static readonly RestoreFormatter Instance = new();
@@ -248,6 +253,7 @@ public void Format(
248253
{
249254
writer.WriteRaw("$1\r\n0\r\n"u8);
250255
}
256+
251257
writer.WriteBulkString(request.SerializedValue);
252258
}
253259
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using RESPite.Messages;
33
using StackExchange.Redis;
44

5+
// ReSharper disable MemberCanBePrivate.Global
56
// ReSharper disable InconsistentNaming
67
namespace RESPite.StackExchange.Redis;
78

src/RESPite.StackExchange.Redis/RespContextDatabase.Hash.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,10 @@ public Task<HashEntry[]> HashGetAllAsync(RedisKey key, CommandFlags flags = Comm
181181
=> Context(flags).Hashes().HGetAll(key).AsTask();
182182

183183
public Lease<byte>? HashGetLease(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None)
184-
=> throw new NotImplementedException();
184+
=> Context(flags).Hashes().HGetLease(key, hashField).Wait(SyncTimeout);
185185

186186
public Task<Lease<byte>?> HashGetLeaseAsync(RedisKey key, RedisValue hashField, CommandFlags flags = CommandFlags.None)
187-
=> throw new NotImplementedException();
187+
=> Context(flags).Hashes().HGetLease(key, hashField).AsTask();
188188

189189
public long HashIncrement(
190190
RedisKey key,

src/RESPite.StackExchange.Redis/RespFormatters.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ public static void Write(this ref RespWriter writer, in RedisKey key)
5757
}
5858
}
5959

60+
internal static void WriteBulkString(this ref RespWriter writer, ExpireWhen when)
61+
{
62+
switch (when)
63+
{
64+
case ExpireWhen.HasExpiry:
65+
writer.WriteRaw("$2\r\nXX\r\n"u8);
66+
break;
67+
case ExpireWhen.HasNoExpiry:
68+
writer.WriteRaw("$2\r\nNX\r\n"u8);
69+
break;
70+
case ExpireWhen.GreaterThanCurrentExpiry:
71+
writer.WriteRaw("$2\r\nGT\r\n"u8);
72+
break;
73+
case ExpireWhen.LessThanCurrentExpiry:
74+
writer.WriteRaw("$2\r\nLT\r\n"u8);
75+
break;
76+
default:
77+
Throw();
78+
static void Throw() => throw new ArgumentOutOfRangeException(nameof(when));
79+
break;
80+
}
81+
}
82+
6083
internal static void WriteBulkString(this ref RespWriter writer, ListSide side)
6184
{
6285
switch (side)
@@ -69,9 +92,9 @@ internal static void WriteBulkString(this ref RespWriter writer, ListSide side)
6992
break;
7093
default:
7194
Throw();
95+
static void Throw() => throw new ArgumentOutOfRangeException(nameof(side));
7296
break;
7397
}
74-
static void Throw() => throw new ArgumentOutOfRangeException(nameof(side));
7598
}
7699

77100
// ReSharper disable once MemberCanBePrivate.Global

src/RESPite/RespIgnoreAttribute.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,5 @@ public sealed class RespIgnoreAttribute : Attribute
99
{
1010
private readonly object _value;
1111
public object Value => _value;
12-
public RespIgnoreAttribute(string value) => _value = value;
13-
public RespIgnoreAttribute(long value) => _value = value;
14-
public RespIgnoreAttribute(bool value) => _value = value;
12+
public RespIgnoreAttribute(object value) => _value = value;
1513
}

0 commit comments

Comments
 (0)