Skip to content

Commit 55a4e09

Browse files
authored
Fix #1103 - add explicit operator support for ulong on RedisValue (#1104)
* add failing test for #1103 * add explicit ulong handling into RedisValue * Fix (i.e. document and fix incorrect test assertions) the change re "-"/0; add ulong support to RedisResult * add extra tests for +/.
1 parent bb98152 commit 55a4e09

File tree

10 files changed

+489
-165
lines changed

10 files changed

+489
-165
lines changed

docs/ReleaseNotes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Release Notes
22

3+
## (unreleased)
4+
5+
- add `ulong` support to `RedisValue` and `RedisResult`
6+
- fix: remove odd equality: `"-" != 0` (we do, however, still allow `"-0"`, as that is at least semantically valid, and is logically `== 0`)
7+
38
## 2.0.593
49

510
- performance: unify spin-wait usage on sync/async paths to one competitor

src/StackExchange.Redis/Format.cs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Buffers;
3+
using System.Buffers.Text;
34
using System.Globalization;
45
using System.Net;
56
using System.Runtime.InteropServices;
@@ -52,6 +53,8 @@ internal static EndPoint TryParseEndPoint(string host, string port)
5253

5354
internal static string ToString(long value) => value.ToString(NumberFormatInfo.InvariantInfo);
5455

56+
internal static string ToString(ulong value) => value.ToString(NumberFormatInfo.InvariantInfo);
57+
5558
internal static string ToString(double value)
5659
{
5760
if (double.IsInfinity(value))
@@ -149,6 +152,41 @@ internal static bool TryParseDouble(string s, out double value)
149152
return double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out value);
150153
}
151154

155+
internal static bool TryParseUInt64(string s, out ulong value)
156+
=> ulong.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value);
157+
158+
internal static bool TryParseUInt64(ReadOnlySpan<byte> s, out ulong value)
159+
=> Utf8Parser.TryParse(s, out value, out int bytes, standardFormat: 'D') & bytes == s.Length;
160+
161+
internal static bool TryParseInt64(ReadOnlySpan<byte> s, out long value)
162+
=> Utf8Parser.TryParse(s, out value, out int bytes, standardFormat: 'D') & bytes == s.Length;
163+
164+
internal static bool CouldBeInteger(string s)
165+
{
166+
if (string.IsNullOrEmpty(s) || s.Length > PhysicalConnection.MaxInt64TextLen) return false;
167+
bool isSigned = s[0] == '-';
168+
for (int i = isSigned ? 1 : 0; i < s.Length; i++)
169+
{
170+
char c = s[i];
171+
if (c < '0' | c > '9') return false;
172+
}
173+
return true;
174+
}
175+
internal static bool CouldBeInteger(ReadOnlySpan<byte> s)
176+
{
177+
if (s.IsEmpty | s.Length > PhysicalConnection.MaxInt64TextLen) return false;
178+
bool isSigned = s[0] == '-';
179+
for (int i = isSigned ? 1 : 0; i < s.Length; i++)
180+
{
181+
byte c = s[i];
182+
if (c < (byte)'0' | c > (byte)'9') return false;
183+
}
184+
return true;
185+
}
186+
187+
internal static bool TryParseInt64(string s, out long value)
188+
=> long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value);
189+
152190
internal static bool TryParseDouble(ReadOnlySpan<byte> s, out double value)
153191
{
154192
if (s.IsEmpty)
@@ -172,21 +210,13 @@ internal static bool TryParseDouble(ReadOnlySpan<byte> s, out double value)
172210
value = double.NegativeInfinity;
173211
return true;
174212
}
175-
var ss = DecodeUtf8(s);
176-
return double.TryParse(ss, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out value);
177-
}
178-
internal static unsafe string DecodeUtf8(ReadOnlySpan<byte> span)
179-
{
180-
if (span.IsEmpty) return "";
181-
fixed(byte* ptr = &MemoryMarshal.GetReference(span))
182-
{
183-
return Encoding.UTF8.GetString(ptr, span.Length);
184-
}
213+
return Utf8Parser.TryParse(s, out value, out int bytes) & bytes == s.Length;
185214
}
215+
186216
private static bool CaseInsensitiveASCIIEqual(string xLowerCase, ReadOnlySpan<byte> y)
187217
{
188218
if (y.Length != xLowerCase.Length) return false;
189-
for(int i = 0; i < y.Length; i++)
219+
for (int i = 0; i < y.Length; i++)
190220
{
191221
if (char.ToLower((char)y[i]) != xLowerCase[i]) return false;
192222
}
@@ -275,7 +305,8 @@ internal static string GetString(ReadOnlySequence<byte> buffer)
275305
}
276306
internal static unsafe string GetString(ReadOnlySpan<byte> span)
277307
{
278-
fixed (byte* ptr = &MemoryMarshal.GetReference(span))
308+
if (span.IsEmpty) return "";
309+
fixed (byte* ptr = span)
279310
{
280311
return Encoding.UTF8.GetString(ptr, span.Length);
281312
}

src/StackExchange.Redis/PhysicalConnection.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,10 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter output)
667667
WriteUnifiedBlob(output, (byte[])null);
668668
break;
669669
case RedisValue.StorageType.Int64:
670-
WriteUnifiedInt64(output, (long)value);
670+
WriteUnifiedInt64(output, value.OverlappedValueInt64);
671+
break;
672+
case RedisValue.StorageType.UInt64:
673+
WriteUnifiedUInt64(output, value.OverlappedValueUInt64);
671674
break;
672675
case RedisValue.StorageType.Double: // use string
673676
case RedisValue.StorageType.String:
@@ -752,6 +755,7 @@ internal static void WriteCrlf(PipeWriter writer)
752755
writer.Advance(2);
753756
}
754757

758+
755759
internal static int WriteRaw(Span<byte> span, long value, bool withLengthPrefix = false, int offset = 0)
756760
{
757761
if (value >= 0 && value <= 9)
@@ -1108,7 +1112,7 @@ unsafe static internal void WriteRaw(PipeWriter writer, string value, int expect
11081112
{
11091113
// encode directly in one hit
11101114
var span = writer.GetSpan(expectedLength);
1111-
fixed (byte* bPtr = &MemoryMarshal.GetReference(span))
1115+
fixed (byte* bPtr = span)
11121116
{
11131117
totalBytes = Encoding.UTF8.GetBytes(cPtr, value.Length, bPtr, expectedLength);
11141118
}
@@ -1128,7 +1132,7 @@ unsafe static internal void WriteRaw(PipeWriter writer, string value, int expect
11281132

11291133
int charsUsed, bytesUsed;
11301134
bool completed;
1131-
fixed (byte* bPtr = &MemoryMarshal.GetReference(span))
1135+
fixed (byte* bPtr = span)
11321136
{
11331137
encoder.Convert(cPtr + charOffset, charsRemaining, bPtr, span.Length, final, out charsUsed, out bytesUsed, out completed);
11341138
}
@@ -1188,6 +1192,26 @@ private static void WriteUnifiedInt64(PipeWriter writer, long value)
11881192
var bytes = WriteRaw(span, value, withLengthPrefix: true, offset: 1);
11891193
writer.Advance(bytes);
11901194
}
1195+
1196+
private static void WriteUnifiedUInt64(PipeWriter writer, ulong value)
1197+
{
1198+
// note from specification: A client sends to the Redis server a RESP Array consisting of just Bulk Strings.
1199+
// (i.e. we can't just send ":123\r\n", we need to send "$3\r\n123\r\n"
1200+
1201+
// ${asc-len}\r\n = 3 + MaxInt32TextLen
1202+
// {asc}\r\n = MaxInt64TextLen + 2
1203+
var span = writer.GetSpan(5 + MaxInt32TextLen + MaxInt64TextLen);
1204+
1205+
Span<byte> valueSpan = stackalloc byte[MaxInt64TextLen];
1206+
if (!Utf8Formatter.TryFormat(value, valueSpan, out var len))
1207+
throw new InvalidOperationException("TryFormat failed");
1208+
span[0] = (byte)'$';
1209+
int offset = WriteRaw(span, len, withLengthPrefix: false, offset: 1);
1210+
valueSpan.Slice(0, len).CopyTo(span.Slice(offset));
1211+
offset += len;
1212+
offset = WriteCrlf(span, offset);
1213+
writer.Advance(offset);
1214+
}
11911215
internal static void WriteInteger(PipeWriter writer, long value)
11921216
{
11931217
//note: client should never write integer; only server does this

src/StackExchange.Redis/RawResult.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,7 @@ internal unsafe string GetString()
319319

320320
if (Payload.IsSingleSegment)
321321
{
322-
var span = Payload.First.Span;
323-
fixed (byte* ptr = &MemoryMarshal.GetReference(span))
324-
{
325-
return Encoding.UTF8.GetString(ptr, span.Length);
326-
}
322+
return Format.GetString(Payload.First.Span);
327323
}
328324
var decoder = Encoding.UTF8.GetDecoder();
329325
int charCount = 0;
@@ -332,7 +328,7 @@ internal unsafe string GetString()
332328
var span = segment.Span;
333329
if (span.IsEmpty) continue;
334330

335-
fixed(byte* bPtr = &MemoryMarshal.GetReference(span))
331+
fixed(byte* bPtr = span)
336332
{
337333
charCount += decoder.GetCharCount(bPtr, span.Length, false);
338334
}
@@ -349,7 +345,7 @@ internal unsafe string GetString()
349345
var span = segment.Span;
350346
if (span.IsEmpty) continue;
351347

352-
fixed (byte* bPtr = &MemoryMarshal.GetReference(span))
348+
fixed (byte* bPtr = span)
353349
{
354350
var written = decoder.GetChars(bPtr, span.Length, cPtr, charCount, false);
355351
cPtr += written;
@@ -383,11 +379,11 @@ internal bool TryGetInt64(out long value)
383379
return false;
384380
}
385381

386-
if (Payload.IsSingleSegment) return RedisValue.TryParseInt64(Payload.First.Span, out value);
382+
if (Payload.IsSingleSegment) return Format.TryParseInt64(Payload.First.Span, out value);
387383

388384
Span<byte> span = stackalloc byte[(int)Payload.Length]; // we already checked the length was <= MaxInt64TextLen
389385
Payload.CopyTo(span);
390-
return RedisValue.TryParseInt64(span, out value);
386+
return Format.TryParseInt64(span, out value);
391387
}
392388
}
393389
}

src/StackExchange.Redis/RedisDatabase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3541,8 +3541,8 @@ protected override void WriteImpl(PhysicalConnection physical)
35413541
}
35423542
else
35433543
{ // recognises well-known types
3544-
var val = RedisValue.TryParse(arg);
3545-
if (val.IsNull && arg != null) throw new InvalidCastException($"Unable to parse value: '{arg}'");
3544+
var val = RedisValue.TryParse(arg, out var valid);
3545+
if (!valid) throw new InvalidCastException($"Unable to parse value: '{arg}'");
35463546
physical.WriteBulkString(val);
35473547
}
35483548
}

src/StackExchange.Redis/RedisResult.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul
112112
/// <param name="result">The result to convert to a <see cref="long"/>.</param>
113113
public static explicit operator long(RedisResult result) => result.AsInt64();
114114
/// <summary>
115+
/// Interprets the result as an <see cref="ulong"/>.
116+
/// </summary>
117+
/// <param name="result">The result to convert to a <see cref="ulong"/>.</param>
118+
[CLSCompliant(false)]
119+
public static explicit operator ulong(RedisResult result) => result.AsUInt64();
120+
/// <summary>
115121
/// Interprets the result as an <see cref="int"/>.
116122
/// </summary>
117123
/// <param name="result">The result to convert to a <see cref="int"/>.</param>
@@ -142,6 +148,12 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul
142148
/// <param name="result">The result to convert to a <see cref="T:Nullable{long}"/>.</param>
143149
public static explicit operator long? (RedisResult result) => result.AsNullableInt64();
144150
/// <summary>
151+
/// Interprets the result as a <see cref="T:Nullable{ulong}"/>.
152+
/// </summary>
153+
/// <param name="result">The result to convert to a <see cref="T:Nullable{ulong}"/>.</param>
154+
[CLSCompliant(false)]
155+
public static explicit operator ulong? (RedisResult result) => result.AsNullableUInt64();
156+
/// <summary>
145157
/// Interprets the result as a <see cref="T:Nullable{int}"/>.
146158
/// </summary>
147159
/// <param name="result">The result to convert to a <see cref="T:Nullable{int}"/>.</param>
@@ -172,6 +184,12 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul
172184
/// <param name="result">The result to convert to a <see cref="T:long[]"/>.</param>
173185
public static explicit operator long[] (RedisResult result) => result.AsInt64Array();
174186
/// <summary>
187+
/// Interprets the result as a <see cref="T:ulong[]"/>.
188+
/// </summary>
189+
/// <param name="result">The result to convert to a <see cref="T:ulong[]"/>.</param>
190+
[CLSCompliant(false)]
191+
public static explicit operator ulong[] (RedisResult result) => result.AsUInt64Array();
192+
/// <summary>
175193
/// Interprets the result as a <see cref="T:int[]"/>.
176194
/// </summary>
177195
/// <param name="result">The result to convert to a <see cref="T:int[]"/>.</param>
@@ -206,11 +224,14 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul
206224
internal abstract int AsInt32();
207225
internal abstract int[] AsInt32Array();
208226
internal abstract long AsInt64();
227+
internal abstract ulong AsUInt64();
209228
internal abstract long[] AsInt64Array();
229+
internal abstract ulong[] AsUInt64Array();
210230
internal abstract bool? AsNullableBoolean();
211231
internal abstract double? AsNullableDouble();
212232
internal abstract int? AsNullableInt32();
213233
internal abstract long? AsNullableInt64();
234+
internal abstract ulong? AsNullableUInt64();
214235
internal abstract RedisKey AsRedisKey();
215236
internal abstract RedisKey[] AsRedisKeyArray();
216237
internal abstract RedisResult[] AsRedisResultArray();
@@ -279,12 +300,22 @@ internal override long AsInt64()
279300
if (IsSingleton) return _value[0].AsInt64();
280301
throw new InvalidCastException();
281302
}
303+
internal override ulong AsUInt64()
304+
{
305+
if (IsSingleton) return _value[0].AsUInt64();
306+
throw new InvalidCastException();
307+
}
282308

283309
internal override long[] AsInt64Array()
284310
=> IsNull ? null
285311
: IsEmpty ? Array.Empty<long>()
286312
: Array.ConvertAll(_value, x => x.AsInt64());
287313

314+
internal override ulong[] AsUInt64Array()
315+
=> IsNull ? null
316+
: IsEmpty ? Array.Empty<ulong>()
317+
: Array.ConvertAll(_value, x => x.AsUInt64());
318+
288319
internal override bool? AsNullableBoolean()
289320
{
290321
if (IsSingleton) return _value[0].AsNullableBoolean();
@@ -308,6 +339,11 @@ internal override long[] AsInt64Array()
308339
if (IsSingleton) return _value[0].AsNullableInt64();
309340
throw new InvalidCastException();
310341
}
342+
internal override ulong? AsNullableUInt64()
343+
{
344+
if (IsSingleton) return _value[0].AsNullableUInt64();
345+
throw new InvalidCastException();
346+
}
311347

312348
internal override RedisKey AsRedisKey()
313349
{
@@ -378,11 +414,14 @@ public ErrorRedisResult(string value)
378414
internal override int AsInt32() => throw new RedisServerException(value);
379415
internal override int[] AsInt32Array() => throw new RedisServerException(value);
380416
internal override long AsInt64() => throw new RedisServerException(value);
417+
internal override ulong AsUInt64() => throw new RedisServerException(value);
381418
internal override long[] AsInt64Array() => throw new RedisServerException(value);
419+
internal override ulong[] AsUInt64Array() => throw new RedisServerException(value);
382420
internal override bool? AsNullableBoolean() => throw new RedisServerException(value);
383421
internal override double? AsNullableDouble() => throw new RedisServerException(value);
384422
internal override int? AsNullableInt32() => throw new RedisServerException(value);
385423
internal override long? AsNullableInt64() => throw new RedisServerException(value);
424+
internal override ulong? AsNullableUInt64() => throw new RedisServerException(value);
386425
internal override RedisKey AsRedisKey() => throw new RedisServerException(value);
387426
internal override RedisKey[] AsRedisKeyArray() => throw new RedisServerException(value);
388427
internal override RedisResult[] AsRedisResultArray() => throw new RedisServerException(value);
@@ -415,11 +454,14 @@ public SingleRedisResult(RedisValue value, ResultType? resultType)
415454
internal override int AsInt32() => (int)_value;
416455
internal override int[] AsInt32Array() => new[] { AsInt32() };
417456
internal override long AsInt64() => (long)_value;
457+
internal override ulong AsUInt64() => (ulong)_value;
418458
internal override long[] AsInt64Array() => new[] { AsInt64() };
459+
internal override ulong[] AsUInt64Array() => new[] { AsUInt64() };
419460
internal override bool? AsNullableBoolean() => (bool?)_value;
420461
internal override double? AsNullableDouble() => (double?)_value;
421462
internal override int? AsNullableInt32() => (int?)_value;
422463
internal override long? AsNullableInt64() => (long?)_value;
464+
internal override ulong? AsNullableUInt64() => (ulong?)_value;
423465
internal override RedisKey AsRedisKey() => (byte[])_value;
424466
internal override RedisKey[] AsRedisKeyArray() => new[] { AsRedisKey() };
425467
internal override RedisResult[] AsRedisResultArray() => throw new InvalidCastException();

0 commit comments

Comments
 (0)