Unverified Commit 55a4e090 authored by Marc Gravell's avatar Marc Gravell Committed by GitHub

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 +/.
parent bb981525
# Release Notes
## (unreleased)
- add `ulong` support to `RedisValue` and `RedisResult`
- fix: remove odd equality: `"-" != 0` (we do, however, still allow `"-0"`, as that is at least semantically valid, and is logically `== 0`)
## 2.0.593
- performance: unify spin-wait usage on sync/async paths to one competitor
......
using System;
using System.Buffers;
using System.Buffers.Text;
using System.Globalization;
using System.Net;
using System.Runtime.InteropServices;
......@@ -52,6 +53,8 @@ internal static EndPoint TryParseEndPoint(string host, string port)
internal static string ToString(long value) => value.ToString(NumberFormatInfo.InvariantInfo);
internal static string ToString(ulong value) => value.ToString(NumberFormatInfo.InvariantInfo);
internal static string ToString(double value)
{
if (double.IsInfinity(value))
......@@ -149,6 +152,41 @@ internal static bool TryParseDouble(string s, out double value)
return double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out value);
}
internal static bool TryParseUInt64(string s, out ulong value)
=> ulong.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value);
internal static bool TryParseUInt64(ReadOnlySpan<byte> s, out ulong value)
=> Utf8Parser.TryParse(s, out value, out int bytes, standardFormat: 'D') & bytes == s.Length;
internal static bool TryParseInt64(ReadOnlySpan<byte> s, out long value)
=> Utf8Parser.TryParse(s, out value, out int bytes, standardFormat: 'D') & bytes == s.Length;
internal static bool CouldBeInteger(string s)
{
if (string.IsNullOrEmpty(s) || s.Length > PhysicalConnection.MaxInt64TextLen) return false;
bool isSigned = s[0] == '-';
for (int i = isSigned ? 1 : 0; i < s.Length; i++)
{
char c = s[i];
if (c < '0' | c > '9') return false;
}
return true;
}
internal static bool CouldBeInteger(ReadOnlySpan<byte> s)
{
if (s.IsEmpty | s.Length > PhysicalConnection.MaxInt64TextLen) return false;
bool isSigned = s[0] == '-';
for (int i = isSigned ? 1 : 0; i < s.Length; i++)
{
byte c = s[i];
if (c < (byte)'0' | c > (byte)'9') return false;
}
return true;
}
internal static bool TryParseInt64(string s, out long value)
=> long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out value);
internal static bool TryParseDouble(ReadOnlySpan<byte> s, out double value)
{
if (s.IsEmpty)
......@@ -172,21 +210,13 @@ internal static bool TryParseDouble(ReadOnlySpan<byte> s, out double value)
value = double.NegativeInfinity;
return true;
}
var ss = DecodeUtf8(s);
return double.TryParse(ss, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out value);
}
internal static unsafe string DecodeUtf8(ReadOnlySpan<byte> span)
{
if (span.IsEmpty) return "";
fixed(byte* ptr = &MemoryMarshal.GetReference(span))
{
return Encoding.UTF8.GetString(ptr, span.Length);
}
return Utf8Parser.TryParse(s, out value, out int bytes) & bytes == s.Length;
}
private static bool CaseInsensitiveASCIIEqual(string xLowerCase, ReadOnlySpan<byte> y)
{
if (y.Length != xLowerCase.Length) return false;
for(int i = 0; i < y.Length; i++)
for (int i = 0; i < y.Length; i++)
{
if (char.ToLower((char)y[i]) != xLowerCase[i]) return false;
}
......@@ -275,7 +305,8 @@ internal static string GetString(ReadOnlySequence<byte> buffer)
}
internal static unsafe string GetString(ReadOnlySpan<byte> span)
{
fixed (byte* ptr = &MemoryMarshal.GetReference(span))
if (span.IsEmpty) return "";
fixed (byte* ptr = span)
{
return Encoding.UTF8.GetString(ptr, span.Length);
}
......
......@@ -667,7 +667,10 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter output)
WriteUnifiedBlob(output, (byte[])null);
break;
case RedisValue.StorageType.Int64:
WriteUnifiedInt64(output, (long)value);
WriteUnifiedInt64(output, value.OverlappedValueInt64);
break;
case RedisValue.StorageType.UInt64:
WriteUnifiedUInt64(output, value.OverlappedValueUInt64);
break;
case RedisValue.StorageType.Double: // use string
case RedisValue.StorageType.String:
......@@ -752,6 +755,7 @@ internal static void WriteCrlf(PipeWriter writer)
writer.Advance(2);
}
internal static int WriteRaw(Span<byte> span, long value, bool withLengthPrefix = false, int offset = 0)
{
if (value >= 0 && value <= 9)
......@@ -1108,7 +1112,7 @@ unsafe static internal void WriteRaw(PipeWriter writer, string value, int expect
{
// encode directly in one hit
var span = writer.GetSpan(expectedLength);
fixed (byte* bPtr = &MemoryMarshal.GetReference(span))
fixed (byte* bPtr = span)
{
totalBytes = Encoding.UTF8.GetBytes(cPtr, value.Length, bPtr, expectedLength);
}
......@@ -1128,7 +1132,7 @@ unsafe static internal void WriteRaw(PipeWriter writer, string value, int expect
int charsUsed, bytesUsed;
bool completed;
fixed (byte* bPtr = &MemoryMarshal.GetReference(span))
fixed (byte* bPtr = span)
{
encoder.Convert(cPtr + charOffset, charsRemaining, bPtr, span.Length, final, out charsUsed, out bytesUsed, out completed);
}
......@@ -1188,6 +1192,26 @@ private static void WriteUnifiedInt64(PipeWriter writer, long value)
var bytes = WriteRaw(span, value, withLengthPrefix: true, offset: 1);
writer.Advance(bytes);
}
private static void WriteUnifiedUInt64(PipeWriter writer, ulong value)
{
// note from specification: A client sends to the Redis server a RESP Array consisting of just Bulk Strings.
// (i.e. we can't just send ":123\r\n", we need to send "$3\r\n123\r\n"
// ${asc-len}\r\n = 3 + MaxInt32TextLen
// {asc}\r\n = MaxInt64TextLen + 2
var span = writer.GetSpan(5 + MaxInt32TextLen + MaxInt64TextLen);
Span<byte> valueSpan = stackalloc byte[MaxInt64TextLen];
if (!Utf8Formatter.TryFormat(value, valueSpan, out var len))
throw new InvalidOperationException("TryFormat failed");
span[0] = (byte)'$';
int offset = WriteRaw(span, len, withLengthPrefix: false, offset: 1);
valueSpan.Slice(0, len).CopyTo(span.Slice(offset));
offset += len;
offset = WriteCrlf(span, offset);
writer.Advance(offset);
}
internal static void WriteInteger(PipeWriter writer, long value)
{
//note: client should never write integer; only server does this
......
......@@ -319,11 +319,7 @@ internal unsafe string GetString()
if (Payload.IsSingleSegment)
{
var span = Payload.First.Span;
fixed (byte* ptr = &MemoryMarshal.GetReference(span))
{
return Encoding.UTF8.GetString(ptr, span.Length);
}
return Format.GetString(Payload.First.Span);
}
var decoder = Encoding.UTF8.GetDecoder();
int charCount = 0;
......@@ -332,7 +328,7 @@ internal unsafe string GetString()
var span = segment.Span;
if (span.IsEmpty) continue;
fixed(byte* bPtr = &MemoryMarshal.GetReference(span))
fixed(byte* bPtr = span)
{
charCount += decoder.GetCharCount(bPtr, span.Length, false);
}
......@@ -349,7 +345,7 @@ internal unsafe string GetString()
var span = segment.Span;
if (span.IsEmpty) continue;
fixed (byte* bPtr = &MemoryMarshal.GetReference(span))
fixed (byte* bPtr = span)
{
var written = decoder.GetChars(bPtr, span.Length, cPtr, charCount, false);
cPtr += written;
......@@ -383,11 +379,11 @@ internal bool TryGetInt64(out long value)
return false;
}
if (Payload.IsSingleSegment) return RedisValue.TryParseInt64(Payload.First.Span, out value);
if (Payload.IsSingleSegment) return Format.TryParseInt64(Payload.First.Span, out value);
Span<byte> span = stackalloc byte[(int)Payload.Length]; // we already checked the length was <= MaxInt64TextLen
Payload.CopyTo(span);
return RedisValue.TryParseInt64(span, out value);
return Format.TryParseInt64(span, out value);
}
}
}
......
......@@ -3541,8 +3541,8 @@ protected override void WriteImpl(PhysicalConnection physical)
}
else
{ // recognises well-known types
var val = RedisValue.TryParse(arg);
if (val.IsNull && arg != null) throw new InvalidCastException($"Unable to parse value: '{arg}'");
var val = RedisValue.TryParse(arg, out var valid);
if (!valid) throw new InvalidCastException($"Unable to parse value: '{arg}'");
physical.WriteBulkString(val);
}
}
......
......@@ -112,6 +112,12 @@ internal static RedisResult TryCreate(PhysicalConnection connection, in RawResul
/// <param name="result">The result to convert to a <see cref="long"/>.</param>
public static explicit operator long(RedisResult result) => result.AsInt64();
/// <summary>
/// Interprets the result as an <see cref="ulong"/>.
/// </summary>
/// <param name="result">The result to convert to a <see cref="ulong"/>.</param>
[CLSCompliant(false)]
public static explicit operator ulong(RedisResult result) => result.AsUInt64();
/// <summary>
/// Interprets the result as an <see cref="int"/>.
/// </summary>
/// <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
/// <param name="result">The result to convert to a <see cref="T:Nullable{long}"/>.</param>
public static explicit operator long? (RedisResult result) => result.AsNullableInt64();
/// <summary>
/// Interprets the result as a <see cref="T:Nullable{ulong}"/>.
/// </summary>
/// <param name="result">The result to convert to a <see cref="T:Nullable{ulong}"/>.</param>
[CLSCompliant(false)]
public static explicit operator ulong? (RedisResult result) => result.AsNullableUInt64();
/// <summary>
/// Interprets the result as a <see cref="T:Nullable{int}"/>.
/// </summary>
/// <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
/// <param name="result">The result to convert to a <see cref="T:long[]"/>.</param>
public static explicit operator long[] (RedisResult result) => result.AsInt64Array();
/// <summary>
/// Interprets the result as a <see cref="T:ulong[]"/>.
/// </summary>
/// <param name="result">The result to convert to a <see cref="T:ulong[]"/>.</param>
[CLSCompliant(false)]
public static explicit operator ulong[] (RedisResult result) => result.AsUInt64Array();
/// <summary>
/// Interprets the result as a <see cref="T:int[]"/>.
/// </summary>
/// <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
internal abstract int AsInt32();
internal abstract int[] AsInt32Array();
internal abstract long AsInt64();
internal abstract ulong AsUInt64();
internal abstract long[] AsInt64Array();
internal abstract ulong[] AsUInt64Array();
internal abstract bool? AsNullableBoolean();
internal abstract double? AsNullableDouble();
internal abstract int? AsNullableInt32();
internal abstract long? AsNullableInt64();
internal abstract ulong? AsNullableUInt64();
internal abstract RedisKey AsRedisKey();
internal abstract RedisKey[] AsRedisKeyArray();
internal abstract RedisResult[] AsRedisResultArray();
......@@ -279,12 +300,22 @@ internal override long AsInt64()
if (IsSingleton) return _value[0].AsInt64();
throw new InvalidCastException();
}
internal override ulong AsUInt64()
{
if (IsSingleton) return _value[0].AsUInt64();
throw new InvalidCastException();
}
internal override long[] AsInt64Array()
=> IsNull ? null
: IsEmpty ? Array.Empty<long>()
: Array.ConvertAll(_value, x => x.AsInt64());
internal override ulong[] AsUInt64Array()
=> IsNull ? null
: IsEmpty ? Array.Empty<ulong>()
: Array.ConvertAll(_value, x => x.AsUInt64());
internal override bool? AsNullableBoolean()
{
if (IsSingleton) return _value[0].AsNullableBoolean();
......@@ -308,6 +339,11 @@ internal override long[] AsInt64Array()
if (IsSingleton) return _value[0].AsNullableInt64();
throw new InvalidCastException();
}
internal override ulong? AsNullableUInt64()
{
if (IsSingleton) return _value[0].AsNullableUInt64();
throw new InvalidCastException();
}
internal override RedisKey AsRedisKey()
{
......@@ -378,11 +414,14 @@ public ErrorRedisResult(string value)
internal override int AsInt32() => throw new RedisServerException(value);
internal override int[] AsInt32Array() => throw new RedisServerException(value);
internal override long AsInt64() => throw new RedisServerException(value);
internal override ulong AsUInt64() => throw new RedisServerException(value);
internal override long[] AsInt64Array() => throw new RedisServerException(value);
internal override ulong[] AsUInt64Array() => throw new RedisServerException(value);
internal override bool? AsNullableBoolean() => throw new RedisServerException(value);
internal override double? AsNullableDouble() => throw new RedisServerException(value);
internal override int? AsNullableInt32() => throw new RedisServerException(value);
internal override long? AsNullableInt64() => throw new RedisServerException(value);
internal override ulong? AsNullableUInt64() => throw new RedisServerException(value);
internal override RedisKey AsRedisKey() => throw new RedisServerException(value);
internal override RedisKey[] AsRedisKeyArray() => throw new RedisServerException(value);
internal override RedisResult[] AsRedisResultArray() => throw new RedisServerException(value);
......@@ -415,11 +454,14 @@ public SingleRedisResult(RedisValue value, ResultType? resultType)
internal override int AsInt32() => (int)_value;
internal override int[] AsInt32Array() => new[] { AsInt32() };
internal override long AsInt64() => (long)_value;
internal override ulong AsUInt64() => (ulong)_value;
internal override long[] AsInt64Array() => new[] { AsInt64() };
internal override ulong[] AsUInt64Array() => new[] { AsUInt64() };
internal override bool? AsNullableBoolean() => (bool?)_value;
internal override double? AsNullableDouble() => (double?)_value;
internal override int? AsNullableInt32() => (int?)_value;
internal override long? AsNullableInt64() => (long?)_value;
internal override ulong? AsNullableUInt64() => (ulong?)_value;
internal override RedisKey AsRedisKey() => (byte[])_value;
internal override RedisKey[] AsRedisKeyArray() => new[] { AsRedisKey() };
internal override RedisResult[] AsRedisResultArray() => throw new InvalidCastException();
......
This diff is collapsed.
using System.Globalization;
using Xunit;
using Xunit.Abstractions;
using static StackExchange.Redis.RedisValue;
namespace StackExchange.Redis.Tests.Issues
{
public class Issue1103 : TestBase
{
public Issue1103(ITestOutputHelper output) : base(output) { }
[Theory]
[InlineData(142205255210238005UL, (int)StorageType.Int64)]
[InlineData(ulong.MaxValue, (int)StorageType.UInt64)]
[InlineData(ulong.MinValue, (int)StorageType.Int64)]
[InlineData(0x8000000000000000UL, (int)StorageType.UInt64)]
[InlineData(0x8000000000000001UL, (int)StorageType.UInt64)]
[InlineData(0x7FFFFFFFFFFFFFFFUL, (int)StorageType.Int64)]
public void LargeUInt64StoredCorrectly(ulong value, int storageType)
{
RedisKey key = Me();
using (var muxer = Create())
{
var db = muxer.GetDatabase();
RedisValue typed = value;
// only need UInt64 for 64-bits
Assert.Equal((StorageType)storageType, typed.Type);
db.StringSet(key, typed);
var fromRedis = db.StringGet(key);
Log($"{fromRedis.Type}: {fromRedis}");
Assert.Equal(StorageType.Raw, fromRedis.Type);
Assert.Equal(value, (ulong)fromRedis);
Assert.Equal(value.ToString(CultureInfo.InvariantCulture), fromRedis.ToString());
var simplified = fromRedis.Simplify();
Log($"{simplified.Type}: {simplified}");
Assert.Equal((StorageType)storageType, typed.Type);
Assert.Equal(value, (ulong)simplified);
Assert.Equal(value.ToString(CultureInfo.InvariantCulture), fromRedis.ToString());
}
}
[Fact]
public void UnusualRedisValueOddities() // things we found while doing this
{
RedisValue x = 0, y = "0";
Assert.Equal(x, y);
Assert.Equal(y, x);
y = "-0";
Assert.Equal(x, y);
Assert.Equal(y, x);
y = "-"; // this is the oddness; this used to return true
Assert.NotEqual(x, y);
Assert.NotEqual(y, x);
y = "+";
Assert.NotEqual(x, y);
Assert.NotEqual(y, x);
y = ".";
Assert.NotEqual(x, y);
Assert.NotEqual(y, x);
}
}
}
......@@ -1063,10 +1063,9 @@ public void StreamPendingNoMessagesOrConsumers()
public void StreamPositionDefaultValueIsBeginning()
{
RedisValue position = StreamPosition.Beginning;
Assert.Equal(StreamConstants.ReadMinValue, StreamPosition.Resolve(position, RedisCommand.XREAD));
Assert.Equal(StreamConstants.ReadMinValue, StreamPosition.Resolve(position, RedisCommand.XREADGROUP));
Assert.Equal(StreamConstants.ReadMinValue, StreamPosition.Resolve(position, RedisCommand.XGROUP));
Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREAD));
Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREADGROUP));
Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XGROUP));
}
[Fact]
......@@ -1074,7 +1073,7 @@ public void StreamPositionValidateBeginning()
{
var position = StreamPosition.Beginning;
Assert.Equal(StreamConstants.ReadMinValue, StreamPosition.Resolve(position, RedisCommand.XREAD));
Assert.Equal(StreamConstants.AllMessages, StreamPosition.Resolve(position, RedisCommand.XREAD));
}
[Fact]
......
......@@ -88,7 +88,7 @@ public ReadOnlySpan<TypedRedisValue> Span
if (Type != ResultType.MultiBulk) return default;
var arr = (TypedRedisValue[])_value.DirectObject;
if (arr == null) return default;
var length = (int)_value.DirectInt64;
var length = (int)_value.DirectOverlappedBits64;
return new ReadOnlySpan<TypedRedisValue>(arr, 0, length);
}
}
......@@ -99,7 +99,7 @@ public ArraySegment<TypedRedisValue> Segment
if (Type != ResultType.MultiBulk) return default;
var arr = (TypedRedisValue[])_value.DirectObject;
if (arr == null) return default;
var length = (int)_value.DirectInt64;
var length = (int)_value.DirectOverlappedBits64;
return new ArraySegment<TypedRedisValue>(arr, 0, length);
}
}
......@@ -163,7 +163,7 @@ internal void Recycle(int limit = -1)
{
if (_value.DirectObject is TypedRedisValue[] arr)
{
if (limit < 0) limit = (int)_value.DirectInt64;
if (limit < 0) limit = (int)_value.DirectOverlappedBits64;
for (int i = 0; i < limit; i++)
{
arr[i].Recycle();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment