Skip to content

Commit

Permalink
[Internal] JSON Binary Encoding: Adds support for writing unsigned 64…
Browse files Browse the repository at this point in the history
…-bit integer values (#4883)

## Description

This PR adds support for serializing unsigned Int64 values in Binary
encoding, ensuring that values exceeding the maximum signed Int64 limit
are preserved without conversion to floating-point doubles. This
enhancement enables full-fidelity roundtrips between Text and Binary
encoding formats. For values outside the signed Int64 range, retrieval
from either the JSON Reader or Navigator (across all encoding formats)
will return them as floating-point doubles.

## Type of change
- [x] New feature (non-breaking change which adds functionality)

## Closing issues
None
  • Loading branch information
sboshra authored Nov 18, 2024
1 parent 75cf02d commit 6c8ebe2
Show file tree
Hide file tree
Showing 42 changed files with 760 additions and 241 deletions.
2 changes: 1 addition & 1 deletion Microsoft.Azure.Cosmos/src/CosmosElements/CosmosElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public static CosmosElement Dispatch(
JsonNodeType.Null => CosmosNull.Create(),
JsonNodeType.False => CosmosBoolean.Create(false),
JsonNodeType.True => CosmosBoolean.Create(true),
JsonNodeType.Number64 => CosmosNumber64.Create(jsonNavigator, jsonNavigatorNode),
JsonNodeType.Number => CosmosNumber64.Create(jsonNavigator, jsonNavigatorNode),
JsonNodeType.FieldName => CosmosString.Create(jsonNavigator, jsonNavigatorNode),
JsonNodeType.String => CosmosString.Create(jsonNavigator, jsonNavigatorNode),
JsonNodeType.Array => CosmosArray.Create(jsonNavigator, jsonNavigatorNode),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ public LazyCosmosNumber64(
}

JsonNodeType type = jsonNavigator.GetNodeType(jsonNavigatorNode);
if (type != JsonNodeType.Number64)
if (type != JsonNodeType.Number)
{
throw new ArgumentOutOfRangeException($"{nameof(jsonNavigatorNode)} must be a {JsonNodeType.Number64} node. Got {type} instead.");
throw new ArgumentOutOfRangeException($"{nameof(jsonNavigatorNode)} must be a {JsonNodeType.Number} node. Got {type} instead.");
}

this.lazyNumber = new Lazy<Number64>(() => jsonNavigator.GetNumber64Value(jsonNavigatorNode));
this.lazyNumber = new Lazy<Number64>(() => jsonNavigator.GetNumberValue(jsonNavigatorNode));
}

public override Number64 GetValue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public int CompareTo(CosmosNumber64 cosmosNumber64)

public override void WriteTo(IJsonWriter jsonWriter)
{
jsonWriter.WriteNumber64Value(this.GetValue());
jsonWriter.WriteNumberValue(this.GetValue());
}

public static CosmosNumber64 Create(
Expand Down
10 changes: 5 additions & 5 deletions Microsoft.Azure.Cosmos/src/Json/IJsonNavigator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface IJsonNavigator
/// </summary>
/// <param name="numberNode">The node you want the number value from.</param>
/// <returns>A double that represents the number value in the node.</returns>
Number64 GetNumber64Value(IJsonNavigatorNode numberNode);
Number64 GetNumberValue(IJsonNavigatorNode numberNode);

/// <summary>
/// Tries to get the buffered string value from a node.
Expand Down Expand Up @@ -178,15 +178,15 @@ bool TryGetBufferedBinaryValue(
/// <summary>
/// Creates an <see cref="IJsonReader"/> that is able to read the supplied <see cref="IJsonNavigatorNode"/>.
/// </summary>
/// <param name="jsonNavigatorNode">The node to create a reader from..</param>
/// <param name="node">The node to create a reader from..</param>
/// <returns>The <see cref="IJsonReader"/> that is able to read the supplied <see cref="IJsonNavigatorNode"/>.</returns>
public IJsonReader CreateReader(IJsonNavigatorNode jsonNavigatorNode);
public IJsonReader CreateReader(IJsonNavigatorNode node);

/// <summary>
/// Writes a <see cref="IJsonNavigatorNode"/> to a <see cref="IJsonWriter"/>.
/// </summary>
/// <param name="jsonNavigatorNode">The <see cref="IJsonNavigatorNode"/> to write.</param>
/// <param name="node">The <see cref="IJsonNavigatorNode"/> to write.</param>
/// <param name="jsonWriter">The <see cref="IJsonWriter"/> to write to.</param>
void WriteNode(IJsonNavigatorNode jsonNavigatorNode, IJsonWriter jsonWriter);
void WriteNode(IJsonNavigatorNode node, IJsonWriter jsonWriter);
}
}
8 changes: 7 additions & 1 deletion Microsoft.Azure.Cosmos/src/Json/IJsonWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ interface IJsonWriter
/// Writes a number to the internal buffer.
/// </summary>
/// <param name="value">The value of the number to write.</param>
void WriteNumber64Value(Number64 value);
void WriteNumberValue(Number64 value);

/// <summary>
/// Writes an unsigned 64-bit integer to the internal buffer.
/// </summary>
/// <param name="value">The unsigned 64-bit integer value to write.</param>
void WriteNumberValue(ulong value);

/// <summary>
/// Writes a boolean to the internal buffer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ public override void WriteValue(uint value)
public override void WriteValue(long value)
{
base.WriteValue(value);
this.jsonWriter.WriteNumber64Value(value);
this.jsonWriter.WriteNumberValue(value);
}

/// <summary>
Expand Down Expand Up @@ -247,7 +247,7 @@ public override void WriteValue(float value)
public override void WriteValue(double value)
{
base.WriteValue(value);
this.jsonWriter.WriteNumber64Value(value);
this.jsonWriter.WriteNumberValue(value);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,12 @@ public override bool TryGetBufferedStringValue(out Utf8Memory bufferedUtf8String
bufferedUtf8StringValue = default;
return false;
}

/// <inheritdoc />
protected override bool TryGetUInt64NumberValue(out ulong value)
{
value = 0;
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public override void WriteNullValue()
this.writer.WriteNull();
}

public override void WriteNumber64Value(Number64 value)
public override void WriteNumberValue(Number64 value)
{
if (value.IsInteger)
{
Expand All @@ -114,6 +114,11 @@ public override void WriteNumber64Value(Number64 value)
}
}

public override void WriteNumberValue(ulong value)
{
this.writer.WriteValue(value);
}

public override void WriteObjectEnd()
{
this.writer.WriteEndObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static class NodeTypes
private const JsonNodeType Int64 = JsonNodeType.Int64;
private const JsonNodeType Int8 = JsonNodeType.Int8;
private const JsonNodeType Null = JsonNodeType.Null;
private const JsonNodeType Number = JsonNodeType.Number64;
private const JsonNodeType Number = JsonNodeType.Number;
private const JsonNodeType Object = JsonNodeType.Object;
private const JsonNodeType String = JsonNodeType.String;
private const JsonNodeType True = JsonNodeType.True;
Expand Down
24 changes: 24 additions & 0 deletions Microsoft.Azure.Cosmos/src/Json/JsonBinaryEncoding.Numbers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ public static bool TryGetNumberValue(ReadOnlySpan<byte> numberToken, UniformArra
bytesConsumed = 1 + 8;
break;

case JsonBinaryEncoding.TypeMarker.NumberUInt64:
if (numberToken.Length < (1 + 8))
{
return false;
}

number64 = MemoryMarshal.Read<ulong>(numberToken.Slice(1));
bytesConsumed = 1 + 8;
break;

case JsonBinaryEncoding.TypeMarker.NumberDouble:
if (numberToken.Length < (1 + 8))
{
Expand All @@ -187,6 +197,20 @@ public static bool TryGetNumberValue(ReadOnlySpan<byte> numberToken, UniformArra
return true;
}

public static bool TryGetUInt64Value(ReadOnlySpan<byte> numberToken, UniformArrayInfo uniformArrayInfo, out ulong value)
{
const int RequiredLength = 1 + sizeof(ulong);

if ((numberToken.Length >= RequiredLength) && (uniformArrayInfo == null) && (numberToken[0] == TypeMarker.NumberUInt64))
{
value = MemoryMarshal.Read<ulong>(numberToken.Slice(1));
return true;
}

value = 0;
return false;
}

public static sbyte GetInt8Value(ReadOnlySpan<byte> int8Token)
{
if (!JsonBinaryEncoding.TryGetInt8Value(int8Token, out sbyte int8Value))
Expand Down
14 changes: 7 additions & 7 deletions Microsoft.Azure.Cosmos/src/Json/JsonBinaryEncoding.Strings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ private static bool TryGetBufferedLengthPrefixedString(
{
switch (typeMarker)
{
case JsonBinaryEncoding.TypeMarker.String1ByteLength:
case JsonBinaryEncoding.TypeMarker.StrL1:
if (stringTokenSpan.Length < JsonBinaryEncoding.OneByteLength)
{
value = default;
Expand All @@ -444,7 +444,7 @@ private static bool TryGetBufferedLengthPrefixedString(
length = stringTokenSpan[0];
break;

case JsonBinaryEncoding.TypeMarker.String2ByteLength:
case JsonBinaryEncoding.TypeMarker.StrL2:
if (stringTokenSpan.Length < JsonBinaryEncoding.TwoByteLength)
{
value = default;
Expand All @@ -455,7 +455,7 @@ private static bool TryGetBufferedLengthPrefixedString(
length = MemoryMarshal.Read<ushort>(stringTokenSpan);
break;

case JsonBinaryEncoding.TypeMarker.String4ByteLength:
case JsonBinaryEncoding.TypeMarker.StrL4:
if (stringTokenSpan.Length < JsonBinaryEncoding.FourByteLength)
{
value = default;
Expand All @@ -466,7 +466,7 @@ private static bool TryGetBufferedLengthPrefixedString(
length = MemoryMarshal.Read<uint>(stringTokenSpan);
break;

case JsonBinaryEncoding.TypeMarker.ReferenceString1ByteOffset:
case JsonBinaryEncoding.TypeMarker.StrR1:
if (stringTokenSpan.Length < JsonBinaryEncoding.OneByteOffset)
{
value = default;
Expand All @@ -478,7 +478,7 @@ private static bool TryGetBufferedLengthPrefixedString(
buffer.Slice(start: stringTokenSpan[0]),
out value);

case JsonBinaryEncoding.TypeMarker.ReferenceString2ByteOffset:
case JsonBinaryEncoding.TypeMarker.StrR2:
if (stringTokenSpan.Length < JsonBinaryEncoding.TwoByteOffset)
{
value = default;
Expand All @@ -490,7 +490,7 @@ private static bool TryGetBufferedLengthPrefixedString(
buffer.Slice(start: MemoryMarshal.Read<ushort>(stringTokenSpan)),
out value);

case JsonBinaryEncoding.TypeMarker.ReferenceString3ByteOffset:
case JsonBinaryEncoding.TypeMarker.StrR3:
if (stringTokenSpan.Length < JsonBinaryEncoding.ThreeByteOffset)
{
value = default;
Expand All @@ -502,7 +502,7 @@ private static bool TryGetBufferedLengthPrefixedString(
buffer.Slice(start: MemoryMarshal.Read<UInt24>(stringTokenSpan)),
out value);

case JsonBinaryEncoding.TypeMarker.ReferenceString4ByteOffset:
case JsonBinaryEncoding.TypeMarker.StrR4:
if (stringTokenSpan.Length < JsonBinaryEncoding.FourByteOffset)
{
value = default;
Expand Down
20 changes: 10 additions & 10 deletions Microsoft.Azure.Cosmos/src/Json/JsonBinaryEncoding.TypeMarker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,37 +157,37 @@ public readonly struct TypeMarker
/// <summary>
/// Type marker for a String of 1-byte length
/// </summary>
public const byte String1ByteLength = 0xC0;
public const byte StrL1 = 0xC0;

/// <summary>
/// Type marker for a String of 2-byte length
/// </summary>
public const byte String2ByteLength = 0xC1;
public const byte StrL2 = 0xC1;

/// <summary>
/// Type marker for a String of 4-byte length
/// </summary>
public const byte String4ByteLength = 0xC2;
public const byte StrL4 = 0xC2;

/// <summary>
/// Reference string of 1-byte offset
/// </summary>
public const byte ReferenceString1ByteOffset = 0xC3;
public const byte StrR1 = 0xC3;

/// <summary>
/// Reference string of 2-byte offset
/// </summary>
public const byte ReferenceString2ByteOffset = 0xC4;
public const byte StrR2 = 0xC4;

/// <summary>
/// Reference string of 3-byte offset
/// </summary>
public const byte ReferenceString3ByteOffset = 0xC5;
public const byte StrR3 = 0xC5;

/// <summary>
/// Reference string of 4-byte offset
/// </summary>
public const byte ReferenceString4ByteOffset = 0xC6;
public const byte StrR4 = 0xC6;

/// <summary>
/// Type marker for a 8-byte unsigned integer
Expand Down Expand Up @@ -583,15 +583,15 @@ public static bool IsEncodedLengthString(byte typeMarker)
/// <param name="typeMarker">The input type marker.</param>
/// <returns>Whether the typeMarker is for a variable length string.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsVariableLengthString(byte typeMarker) => IsEncodedLengthString(typeMarker) || InRange(typeMarker, String1ByteLength, String4ByteLength + 1);
public static bool IsVariableLengthString(byte typeMarker) => IsEncodedLengthString(typeMarker) || InRange(typeMarker, StrL1, StrL4 + 1);

/// <summary>
/// Gets whether a typeMarker is for a reference string.
/// </summary>
/// <param name="typeMarker">The input type marker.</param>
/// <returns>Whether the typeMarker is for a reference string.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsReferenceString(byte typeMarker) => InRange(typeMarker, ReferenceString1ByteOffset, ReferenceString4ByteOffset + 1);
public static bool IsReferenceString(byte typeMarker) => InRange(typeMarker, StrR1, StrR4 + 1);

/// <summary>
/// Gets whether a typeMarker is for a GUID string.
Expand Down Expand Up @@ -624,7 +624,7 @@ public static bool IsEncodedLengthString(byte typeMarker)
/// <returns>Whether the typeMarker is for a string.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsString(byte typeMarker) => InRange(typeMarker, SystemString1ByteLengthMin, UserString2ByteLengthMax)
|| InRange(typeMarker, LowercaseGuidString, ReferenceString4ByteOffset + 1);
|| InRange(typeMarker, LowercaseGuidString, StrR4 + 1);

/// <summary>
/// Gets the length of a encoded string type marker.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ public override JsonNodeType GetNodeType(IJsonNavigatorNode node)
}

/// <inheritdoc />
public override Number64 GetNumber64Value(IJsonNavigatorNode numberNode)
public override Number64 GetNumberValue(IJsonNavigatorNode numberNode)
{
BinaryNavigatorNode binaryNavigatorNode = this.GetNodeOfType(JsonNodeType.Number64, numberNode);
BinaryNavigatorNode binaryNavigatorNode = this.GetNodeOfType(JsonNodeType.Number, numberNode);
return JsonBinaryEncoding.GetNumberValue(
this.GetBufferAt(binaryNavigatorNode.Offset),
binaryNavigatorNode.ExternalArrayInfo);
Expand Down Expand Up @@ -432,6 +432,16 @@ public override void WriteNode(IJsonNavigatorNode jsonNavigatorNode, IJsonWriter
}
#endregion

/// <inheritdoc />
protected override bool TryGetUInt64Value(IJsonNavigatorNode numberNode, out ulong value)
{
BinaryNavigatorNode binaryNavigatorNode = this.GetNodeOfType(JsonNodeType.Number, numberNode);
return JsonBinaryEncoding.TryGetUInt64Value(
this.GetBufferAt(binaryNavigatorNode.Offset),
binaryNavigatorNode.ExternalArrayInfo,
out value);
}

private IEnumerable<BinaryNavigatorNode> GetArrayItemsInternal(BinaryNavigatorNode arrayNode)
{
return Enumerator
Expand Down Expand Up @@ -474,13 +484,15 @@ private void WriteToInternal(BinaryNavigatorNode binaryNavigatorNode, IJsonWrite
jsonWriter.WriteBoolValue(true);
break;

case JsonNodeType.Number64:
case JsonNodeType.Number:
if (JsonBinaryEncoding.TryGetUInt64Value(buffer.Span, binaryNavigatorNode.ExternalArrayInfo, out ulong uint64Value))
{
Number64 value = JsonBinaryEncoding.GetNumberValue(
buffer.Span,
binaryNavigatorNode.ExternalArrayInfo);

jsonWriter.WriteNumber64Value(value);
jsonWriter.WriteNumberValue(uint64Value);
}
else
{
Number64 value = JsonBinaryEncoding.GetNumberValue(buffer.Span, binaryNavigatorNode.ExternalArrayInfo);
jsonWriter.WriteNumberValue(value);
}
break;

Expand Down Expand Up @@ -668,7 +680,7 @@ private JsonNodeType GetNodeType(int offset, UniformArrayInfo externalArrayInfo)
case TypeMarker.Float16:
case TypeMarker.Float32:
case TypeMarker.Float64:
nodeType = JsonNodeType.Number64;
nodeType = JsonNodeType.Number;
break;

case TypeMarker.ArrNumC1:
Expand Down
Loading

0 comments on commit 6c8ebe2

Please sign in to comment.