diff --git a/FoundationDB.Client/Core/IFdbTransactionHandler.cs b/FoundationDB.Client/Core/IFdbTransactionHandler.cs
index 4c59c4bc0..32255efe3 100644
--- a/FoundationDB.Client/Core/IFdbTransactionHandler.cs
+++ b/FoundationDB.Client/Core/IFdbTransactionHandler.cs
@@ -61,6 +61,9 @@ public interface IFdbTransactionHandler : IDisposable
///
long GetCommittedVersion();
+ /// Returns the which was used by versionstamps operations in this transaction.
+ Task GetVersionStampAsync(CancellationToken ct);
+
/// Sets the snapshot read version used by a transaction. This is not needed in simple cases.
/// Read version to use in this transaction
///
diff --git a/FoundationDB.Client/FdbMutationType.cs b/FoundationDB.Client/FdbMutationType.cs
index 4899b5a61..e4a4492f2 100644
--- a/FoundationDB.Client/FdbMutationType.cs
+++ b/FoundationDB.Client/FdbMutationType.cs
@@ -91,7 +91,13 @@ public enum FdbMutationType
/// If ``param`` is shorter than the existing value in the database, the existing value is truncated to match the length of ``param``.
/// The smaller of the two values is then stored in the database.
///
- Min = 13
+ Min = 13,
+
+ //TODO: XML Comments!
+ VersionStampedKey = 14,
+
+ //TODO: XML Comments!
+ VersionStampedValue = 15,
}
diff --git a/FoundationDB.Client/FdbTransaction.cs b/FoundationDB.Client/FdbTransaction.cs
index 25fc38b93..2d4ea3970 100644
--- a/FoundationDB.Client/FdbTransaction.cs
+++ b/FoundationDB.Client/FdbTransaction.cs
@@ -90,6 +90,9 @@ public sealed partial class FdbTransaction : IFdbTransaction
/// CancellationToken that should be used for all async operations executing inside this transaction
private CancellationToken m_cancellation;
+ /// Random token (but constant per transaction retry) used to generate incomplete VersionStamps
+ private ulong m_versionStampToken;
+
#endregion
#region Constructors...
@@ -284,6 +287,81 @@ public void SetReadVersion(long version)
m_handler.SetReadVersion(version);
}
+ /// Returns the which was used by versionstamps operations in this transaction.
+ ///
+ /// The Task will be ready only after the successful completion of a call to on this transaction.
+ /// Read-only transactions do not modify the database when committed and will result in the Task completing with an error.
+ /// Keep in mind that a transaction which reads keys and then sets them to their current values may be optimized to a read-only transaction.
+ ///
+ public Task GetVersionStampAsync()
+ {
+ EnsureNotFailedOrDisposed();
+
+ return m_handler.GetVersionStampAsync(m_cancellation);
+ }
+
+ private ulong GenerateNewVersionStampToken()
+ {
+ // We need to generate a 80-bits stamp, and also need to mark it as 'incomplete' by forcing the highest bit to 1.
+ // Since this is supposed to be a version number with a ~1M tickrate per seconds, we will play it safe, and force the 8 highest bits to 1,
+ // meaning that we only reduce the database potential lifetime but 1/256th, before getting into trouble.
+ //
+ // By doing some empirical testing, it also seems that the last 16 bits are a transction batch order which is usually a low number.
+ // Again, we will force the 4 highest bit to 1 to reduce the change of collision with a complete version stamp.
+ //
+ // So the final token will look like: 'FF xx xx xx xx xx xx xx Fy yy', were 'x' is the random token, and 'y' will lowest 12 bits of the transaction retry count
+
+ var rnd = new Random(); //TODO: singleton? (need locking!!)
+ ulong x;
+ unsafe
+ {
+ double r = rnd.NextDouble();
+ x = *(ulong*) &r;
+ }
+ x |= 0xFF00000000000000UL;
+
+ lock (this)
+ {
+ ulong token = m_versionStampToken;
+ if (token == 0)
+ {
+ token = x;
+ m_versionStampToken = x;
+ }
+ return token;
+ }
+ }
+
+ /// Return a place-holder 80-bit VersionStamp, whose value is not yet known, but will be filled by the database at commit time.
+ /// This value can used to generate temporary keys or value, for use with the or mutations
+ ///
+ /// The generate placeholder will use a random value that is unique per transaction (and changes at reach retry).
+ /// If the key contains the exact 80-bit byte signature of this token, the corresponding location will be tagged and replaced with the actual VersionStamp at commit time.
+ /// If another part of the key contains (by random chance) the same exact byte sequence, then an error will be triggered, and hopefully the transaction will retry with another byte sequence.
+ ///
+ [Pure]
+ public VersionStamp CreateVersionStamp()
+ {
+ var token = m_versionStampToken;
+ if (token == 0) token = GenerateNewVersionStampToken();
+ return VersionStamp.Custom(token, (ushort) (m_context.Retries | 0xF000), incomplete: true);
+ }
+
+ /// Return a place-holder 96-bit VersionStamp with an attached user version, whose value is not yet known, but will be filled by the database at commit time.
+ /// This value can used to generate temporary keys or value, for use with the or mutations
+ ///
+ /// The generate placeholder will use a random value that is unique per transaction (and changes at reach retry).
+ /// If the key contains the exact 80-bit byte signature of this token, the corresponding location will be tagged and replaced with the actual VersionStamp at commit time.
+ /// If another part of the key contains (by random chance) the same exact byte sequence, then an error will be triggered, and hopefully the transaction will retry with another byte sequence.
+ ///
+ public VersionStamp CreateVersionStamp(int userVersion)
+ {
+ var token = m_versionStampToken;
+ if (token == 0) token = GenerateNewVersionStampToken();
+
+ return VersionStamp.Custom(token, (ushort) (m_context.Retries | 0xF000), userVersion, incomplete: true);
+ }
+
#endregion
#region Get...
@@ -513,6 +591,23 @@ private static void EnsureMutationTypeIsSupported(FdbMutationType mutation, int
return;
}
+ if (mutation == FdbMutationType.VersionStampedKey || mutation == FdbMutationType.VersionStampedValue)
+ {
+ if (selectedApiVersion < 400)
+ {
+ if (Fdb.GetMaxApiVersion() >= 400)
+ {
+ throw new FdbException(FdbError.InvalidMutationType, "Atomic mutations for VersionStamps are only supported starting from API level 400. You need to select API level 400 or more at the start of your process.");
+ }
+ else
+ {
+ throw new FdbException(FdbError.InvalidMutationType, "Atomic mutations Max and Min are only supported starting from client version 4.x. You need to update the version of the client, and select API level 400 or more at the start of your process..");
+ }
+ }
+ // ok!
+ return;
+ }
+
// this could be a new mutation type, or an invalid value.
throw new FdbException(FdbError.InvalidMutationType, "An invalid mutation type was issued. If you are attempting to call a new mutation type, you will need to update the version of this assembly, and select the latest API level.");
}
@@ -755,6 +850,11 @@ private void RestoreDefaultSettings()
{
this.Timeout = m_database.DefaultTimeout;
}
+
+ // if we have used a random token for versionstamps, we need to clear it (and generate a new one)
+ // => this ensure that if the error was due to a collision between the token and another part of the key,
+ // a transaction retry will hopefully use a different token that does not collide.
+ m_versionStampToken = 0;
}
/// Reset the transaction to its initial state.
diff --git a/FoundationDB.Client/FdbTransactionExtensions.cs b/FoundationDB.Client/FdbTransactionExtensions.cs
index 16d4d362c..e59d39fc2 100644
--- a/FoundationDB.Client/FdbTransactionExtensions.cs
+++ b/FoundationDB.Client/FdbTransactionExtensions.cs
@@ -36,6 +36,7 @@ namespace FoundationDB.Client
using System.Threading.Tasks;
using Doxense.Diagnostics.Contracts;
using Doxense.Linq;
+ using Doxense.Memory;
using Doxense.Serialization.Encoders;
using JetBrains.Annotations;
@@ -425,6 +426,89 @@ public static void AtomicMin([NotNull] this IFdbTransaction trans, Slice key, Sl
trans.Atomic(key, value, FdbMutationType.Min);
}
+ private static int GetVersionStampOffset(Slice buffer, Slice token, string argName)
+ {
+ // the buffer MUST contain one incomplete stamp, either the random token of the current transsaction or the default token (all-FF)
+
+ int p = token.HasValue ? buffer.IndexOf(token) : -1;
+ if (p >= 0)
+ { // found a candidate spot, we have to make sure that it is only present once in the key!
+
+ if (buffer.IndexOf(token, p + token.Count) >= 0)
+ {
+ if (argName == "key")
+ throw new ArgumentException("The key should only contain one occurrence of a VersionStamp.", argName);
+ else
+ throw new ArgumentException("The value should only contain one occurrence of a VersionStamp.", argName);
+ }
+ }
+ else
+ { // not found, maybe it is using the default incomplete stamp (all FF) ?
+ p = buffer.IndexOf(VersionStamp.IncompleteToken);
+ if (p < 0)
+ {
+ if (argName == "key")
+ throw new ArgumentException("The key should contain at least one VersionStamp.", argName);
+ else
+ throw new ArgumentException("The value should contain at least one VersionStamp.", argName);
+ }
+ }
+ Contract.Assert(p >= 0 && p + token.Count <= buffer.Count);
+
+ return p;
+ }
+
+ /// Set the of the in the database, with the replaced by the resolved version at commit time.
+ /// Transaction to use for the operation
+ /// Name of the key whose value is to be mutated. This key must contain a single , whose position will be automatically detected.
+ /// New value for this key.
+ public static void SetVersionStampedKey([NotNull] this IFdbTransaction trans, Slice key, Slice value)
+ {
+ Contract.NotNull(trans, nameof(trans));
+
+ //TODO: PERF: optimize this to not have to allocate!
+ var token = trans.CreateVersionStamp().ToSlice();
+ var offset = GetVersionStampOffset(key, token, nameof(key));
+
+ var writer = new SliceWriter(key.Count + 2);
+ writer.WriteBytes(key);
+ writer.WriteFixed16(checked((ushort) offset)); //note: currently stored as 16-bits in Little Endian
+
+ trans.Atomic(writer.ToSlice(), value, FdbMutationType.VersionStampedKey);
+ }
+
+ /// Set the of the in the database, with the replaced by the resolved version at commit time.
+ /// Transaction to use for the operation
+ /// Name of the key whose value is to be mutated. This key must contain a single , whose start is defined by .
+ /// Offset within of the start of the 80-bit VersionStamp.
+ /// New value for this key.
+ public static void SetVersionStampedKey([NotNull] this IFdbTransaction trans, Slice key, int stampOffset, Slice value)
+ {
+ Contract.NotNull(trans, nameof(trans));
+
+ if (stampOffset > key.Count - 10) throw new ArgumentException("The VersionStamp overflows past the end of the key.", nameof(stampOffset));
+ if (stampOffset > 0xFFFF) throw new ArgumentException("The offset is too large to fit within 16-bits.");
+
+ var writer = new SliceWriter(key.Count + 2);
+ writer.WriteBytes(key);
+ writer.WriteFixed16(checked((ushort) stampOffset)); //note: currently stored as 16-bits in Little Endian
+
+ trans.Atomic(writer.ToSlice(), value, FdbMutationType.VersionStampedKey);
+ }
+
+ /// Set the of the in the database, with the first 10 bytes overwritten with the transaction's .
+ /// Transaction to use for the operation
+ /// Name of the key whose value is to be mutated.
+ /// Value whose first 10 bytes will be overwritten by the database with the resolved VersionStamp at commit time. The rest of the value will be untouched.
+ public static void SetVersionStampedValue([NotNull] this IFdbTransaction trans, Slice key, Slice value)
+ {
+ Contract.NotNull(trans, nameof(trans));
+
+ if (value.Count < 10) throw new ArgumentException("The value must be at least 10 bytes long.", nameof(value));
+
+ trans.Atomic(key, value, FdbMutationType.VersionStampedValue);
+ }
+
#endregion
#region GetRange...
diff --git a/FoundationDB.Client/Filters/FdbTransactionFilter.cs b/FoundationDB.Client/Filters/FdbTransactionFilter.cs
index 688f3e6e9..bfcf54712 100644
--- a/FoundationDB.Client/Filters/FdbTransactionFilter.cs
+++ b/FoundationDB.Client/Filters/FdbTransactionFilter.cs
@@ -251,6 +251,24 @@ public virtual long GetCommittedVersion()
return m_transaction.GetCommittedVersion();
}
+ public virtual Task GetVersionStampAsync()
+ {
+ ThrowIfDisposed();
+ return m_transaction.GetVersionStampAsync();
+ }
+
+ public virtual VersionStamp CreateVersionStamp()
+ {
+ ThrowIfDisposed();
+ return m_transaction.CreateVersionStamp();
+ }
+
+ public virtual VersionStamp CreateVersionStamp(int userVersion)
+ {
+ ThrowIfDisposed();
+ return m_transaction.CreateVersionStamp(userVersion);
+ }
+
public virtual void SetReadVersion(long version)
{
ThrowIfDisposed();
diff --git a/FoundationDB.Client/Filters/Logging/FdbLoggedTransaction.cs b/FoundationDB.Client/Filters/Logging/FdbLoggedTransaction.cs
index 58de2beab..23b12cbce 100644
--- a/FoundationDB.Client/Filters/Logging/FdbLoggedTransaction.cs
+++ b/FoundationDB.Client/Filters/Logging/FdbLoggedTransaction.cs
@@ -312,6 +312,14 @@ public override Task OnErrorAsync(FdbError code)
);
}
+ public override Task GetVersionStampAsync()
+ {
+ return ExecuteAsync(
+ new FdbTransactionLog.GetVersionStampCommand(),
+ (tr, cmd) => tr.GetVersionStampAsync()
+ );
+ }
+
public override void Set(Slice key, Slice value)
{
Execute(
diff --git a/FoundationDB.Client/Filters/Logging/FdbTransactionLog.Commands.cs b/FoundationDB.Client/Filters/Logging/FdbTransactionLog.Commands.cs
index 6439cb75e..8cb6093e8 100644
--- a/FoundationDB.Client/Filters/Logging/FdbTransactionLog.Commands.cs
+++ b/FoundationDB.Client/Filters/Logging/FdbTransactionLog.Commands.cs
@@ -832,6 +832,12 @@ public override string GetResult(KeyResolver resolver)
}
+ public sealed class GetVersionStampCommand : Command
+ {
+ public override Operation Op { get { return Operation.GetVersionStamp; } }
+
+ }
+
public sealed class GetReadVersionCommand : Command
{
public override Operation Op { get { return Operation.GetReadVersion; } }
diff --git a/FoundationDB.Client/Filters/Logging/FdbTransactionLog.cs b/FoundationDB.Client/Filters/Logging/FdbTransactionLog.cs
index c6c166072..541187e22 100644
--- a/FoundationDB.Client/Filters/Logging/FdbTransactionLog.cs
+++ b/FoundationDB.Client/Filters/Logging/FdbTransactionLog.cs
@@ -552,6 +552,7 @@ public enum Operation
Reset,
OnError,
SetOption,
+ GetVersionStamp,
Log,
}
diff --git a/FoundationDB.Client/IFdbTransaction.cs b/FoundationDB.Client/IFdbTransaction.cs
index 3d6c4ccda..98f684dd9 100644
--- a/FoundationDB.Client/IFdbTransaction.cs
+++ b/FoundationDB.Client/IFdbTransaction.cs
@@ -112,6 +112,32 @@ public interface IFdbTransaction : IFdbReadOnlyTransaction
///
long GetCommittedVersion();
+ /// Returns the which was used by versionstamps operations in this transaction.
+ ///
+ /// The Task will be ready only after the successful completion of a call to on this transaction.
+ /// Read-only transactions do not modify the database when committed and will result in the Task completing with an error.
+ /// Keep in mind that a transaction which reads keys and then sets them to their current values may be optimized to a read-only transaction.
+ ///
+ Task GetVersionStampAsync();
+
+ /// Return a place-holder 80-bit VersionStamp, whose value is not yet known, but will be filled by the database at commit time.
+ /// This value can used to generate temporary keys or value, for use with the or mutations
+ ///
+ /// The generate placeholder will use a random value that is unique per transaction (and changes at reach retry).
+ /// If the key contains the exact 80-bit byte signature of this token, the corresponding location will be tagged and replaced with the actual VersionStamp at commit time.
+ /// If another part of the key contains (by random chance) the same exact byte sequence, then an error will be triggered, and hopefully the transaction will retry with another byte sequence.
+ ///
+ VersionStamp CreateVersionStamp();
+
+ /// Return a place-holder 96-bit VersionStamp with an attached user version, whose value is not yet known, but will be filled by the database at commit time.
+ /// This value can used to generate temporary keys or value, for use with the or mutations
+ ///
+ /// The generate placeholder will use a random value that is unique per transaction (and changes at reach retry).
+ /// If the key contains the exact 80-bit byte signature of this token, the corresponding location will be tagged and replaced with the actual VersionStamp at commit time.
+ /// If another part of the key contains (by random chance) the same exact byte sequence, then an error will be triggered, and hopefully the transaction will retry with another byte sequence.
+ ///
+ VersionStamp CreateVersionStamp(int userVersion);
+
///
/// Watch a key for any change in the database.
///
diff --git a/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs b/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs
index 7fa2a4d49..68f28989c 100644
--- a/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs
+++ b/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs
@@ -38,6 +38,7 @@ namespace Doxense.Collections.Tuples.Encoding
using Doxense.Collections.Tuples;
using Doxense.Diagnostics.Contracts;
using Doxense.Runtime.Converters;
+ using FoundationDB.Client;
using JetBrains.Annotations;
/// Helper methods used during serialization of values to the tuple binary format
@@ -547,6 +548,11 @@ public static void SerializeTo(ref TupleWriter writer, Uuid64 value)
TupleParser.WriteUuid64(ref writer, value);
}
+ public static void SerializeTo(ref TupleWriter writer, VersionStamp value)
+ {
+ TupleParser.WriteVersionStamp(ref writer, value);
+ }
+
/// Writes an IPaddress as a 32-bit (IPv4) or 128-bit (IPv6) byte array
public static void SerializeTo(ref TupleWriter writer, System.Net.IPAddress value)
{
@@ -670,6 +676,7 @@ private static Dictionary InitializeDefaultUnpackers()
[typeof(TimeSpan)] = new Func(TuplePackers.DeserializeTimeSpan),
[typeof(DateTime)] = new Func(TuplePackers.DeserializeDateTime),
[typeof(System.Net.IPAddress)] = new Func(TuplePackers.DeserializeIPAddress),
+ [typeof(VersionStamp)] = new Func(TuplePackers.DeserializeVersionStamp),
[typeof(ITuple)] = new Func(TuplePackers.DeserializeTuple),
};
@@ -839,6 +846,8 @@ public static object DeserializeBoxed(Slice slice)
case TupleTypes.Decimal: return TupleParser.ParseDecimal(slice);
case TupleTypes.Uuid128: return TupleParser.ParseGuid(slice);
case TupleTypes.Uuid64: return TupleParser.ParseUuid64(slice);
+ case TupleTypes.VersionStamp80: return TupleParser.ParseVersionStamp(slice);
+ case TupleTypes.VersionStamp96: return TupleParser.ParseVersionStamp(slice);
}
}
@@ -1674,6 +1683,24 @@ public static Uuid64 DeserializeUuid64(Slice slice)
throw new FormatException($"Cannot convert tuple segment of type 0x{type:X} into an Uuid64");
}
+ public static VersionStamp DeserializeVersionStamp(Slice slice)
+ {
+ if (slice.IsNullOrEmpty) return default(VersionStamp);
+
+ int type = slice[0];
+
+ if (type == TupleTypes.VersionStamp80 || type == TupleTypes.VersionStamp96)
+ {
+ if (VersionStamp.TryParse(slice.Substring(1), out var stamp))
+ {
+ return stamp;
+ }
+ throw new FormatException("Cannot convert malformed tuple segment into a VersionStamp");
+ }
+
+ throw new FormatException($"Cannot convert tuple segment of type 0x{type:X} into a VersionStamp");
+ }
+
/// Deserialize a tuple segment into Guid
/// Slice that contains a single packed element
[CanBeNull]
diff --git a/FoundationDB.Client/Layers/Tuples/Encoding/TupleParser.cs b/FoundationDB.Client/Layers/Tuples/Encoding/TupleParser.cs
index 503dd12da..a2e24b5f6 100644
--- a/FoundationDB.Client/Layers/Tuples/Encoding/TupleParser.cs
+++ b/FoundationDB.Client/Layers/Tuples/Encoding/TupleParser.cs
@@ -34,6 +34,7 @@ namespace Doxense.Collections.Tuples.Encoding
using Doxense.Collections.Tuples;
using Doxense.Diagnostics.Contracts;
using Doxense.Memory;
+ using FoundationDB.Client;
using JetBrains.Annotations;
/// Helper class that contains low-level encoders for the tuple binary format
@@ -758,6 +759,28 @@ public static void WriteUuid64(ref TupleWriter writer, Uuid64? value)
if (!value.HasValue) WriteNil(ref writer); else WriteUuid64(ref writer, value.Value);
}
+ public static void WriteVersionStamp(ref TupleWriter writer, VersionStamp value)
+ {
+ if (value.HasUserVersion)
+ { // 96-bits Versionstamp
+ writer.Output.EnsureBytes(13);
+ writer.Output.UnsafeWriteByte(TupleTypes.VersionStamp96);
+ value.WriteTo(writer.Output.Allocate(12));
+ }
+ else
+ { // 80-bits Versionstamp
+ writer.Output.EnsureBytes(11);
+ writer.Output.UnsafeWriteByte(TupleTypes.VersionStamp80);
+ value.WriteTo(writer.Output.Allocate(10));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void WriteVersionStamp(ref TupleWriter writer, VersionStamp? value)
+ {
+ if (!value.HasValue) WriteNil(ref writer); else WriteVersionStamp(ref writer, value.Value);
+ }
+
/// Mark the start of a new embedded tuple
public static void BeginTuple(ref TupleWriter writer)
{
@@ -854,6 +877,7 @@ internal static Slice UnescapeByteStringSlow([NotNull] byte[] buffer, int offset
}
/// Parse a tuple segment containing a byte array
+ [Pure]
public static Slice ParseBytes(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Bytes && slice[-1] == 0);
@@ -865,6 +889,7 @@ public static Slice ParseBytes(Slice slice)
}
/// Parse a tuple segment containing an ASCII string stored as a byte array
+ [Pure]
public static string ParseAscii(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Bytes && slice[-1] == 0);
@@ -877,6 +902,7 @@ public static string ParseAscii(Slice slice)
}
/// Parse a tuple segment containing a unicode string
+ [Pure]
public static string ParseUnicode(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Utf8 && slice[-1] == 0);
@@ -888,6 +914,7 @@ public static string ParseUnicode(Slice slice)
}
/// Parse a tuple segment containing an embedded tuple
+ [Pure]
public static ITuple ParseTuple(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.TupleStart && slice[-1] == 0);
@@ -897,6 +924,7 @@ public static ITuple ParseTuple(Slice slice)
}
/// Parse a tuple segment containing a single precision number (float32)
+ [Pure]
public static float ParseSingle(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Single);
@@ -927,6 +955,7 @@ public static float ParseSingle(Slice slice)
}
/// Parse a tuple segment containing a double precision number (float64)
+ [Pure]
public static double ParseDouble(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Double);
@@ -958,6 +987,7 @@ public static double ParseDouble(Slice slice)
}
/// Parse a tuple segment containing a quadruple precision number (float128)
+ [Pure]
public static decimal ParseDecimal(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Decimal);
@@ -971,6 +1001,7 @@ public static decimal ParseDecimal(Slice slice)
}
/// Parse a tuple segment containing a 128-bit GUID
+ [Pure]
public static Guid ParseGuid(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Uuid128);
@@ -985,6 +1016,7 @@ public static Guid ParseGuid(Slice slice)
}
/// Parse a tuple segment containing a 128-bit UUID
+ [Pure]
public static Uuid128 ParseUuid128(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Uuid128);
@@ -998,6 +1030,7 @@ public static Uuid128 ParseUuid128(Slice slice)
}
/// Parse a tuple segment containing a 64-bit UUID
+ [Pure]
public static Uuid64 ParseUuid64(Slice slice)
{
Contract.Requires(slice.HasValue && slice[0] == TupleTypes.Uuid64);
@@ -1010,6 +1043,20 @@ public static Uuid64 ParseUuid64(Slice slice)
return Uuid64.Read(slice.Substring(1, 8));
}
+ /// Parse a tuple segment containing an 80-bit or 96-bit VersionStamp
+ [Pure]
+ public static VersionStamp ParseVersionStamp(Slice slice)
+ {
+ Contract.Requires(slice.HasValue && (slice[0] == TupleTypes.VersionStamp80 || slice[0] == TupleTypes.VersionStamp96));
+
+ if (slice.Count != 11 && slice.Count != 13)
+ {
+ throw new FormatException("Slice has invalid size for a VersionStamp");
+ }
+
+ return VersionStamp.Parse(slice.Substring(1));
+ }
+
#endregion
#region Parsing...
@@ -1096,6 +1143,16 @@ public static Slice ParseNext(ref TupleReader reader)
return reader.Input.ReadBytes(9);
}
+ case TupleTypes.VersionStamp80:
+ { // <32>(10 bytes)
+ return reader.Input.ReadBytes(11);
+ }
+
+ case TupleTypes.VersionStamp96:
+ { // <33>(12 bytes)
+ return reader.Input.ReadBytes(13);
+ }
+
case TupleTypes.AliasDirectory:
case TupleTypes.AliasSystem:
{ // or
diff --git a/FoundationDB.Client/Layers/Tuples/Encoding/TupleTypes.cs b/FoundationDB.Client/Layers/Tuples/Encoding/TupleTypes.cs
index 9389f8b5f..99c736761 100644
--- a/FoundationDB.Client/Layers/Tuples/Encoding/TupleTypes.cs
+++ b/FoundationDB.Client/Layers/Tuples/Encoding/TupleTypes.cs
@@ -86,6 +86,11 @@ internal static class TupleTypes
/// UUID (64 bits) [DRAFT]
internal const byte Uuid64 = 49; //TODO: this is not official yet! may change!
+ //TODO: xmldoc
+ internal const byte VersionStamp80 = 0x32;
+ //TODO: xmldoc
+ internal const byte VersionStamp96 = 0x33;
+
/// Standard prefix of the Directory Layer
/// This is not a part of the tuple encoding itself, but helps the tuple decoder pretty-print tuples that would otherwise be unparsable.
internal const byte AliasDirectory = 254;
@@ -112,6 +117,8 @@ public static TupleSegmentType DecodeSegmentType(Slice segment)
case Decimal: return TupleSegmentType.Decimal;
case Uuid128: return TupleSegmentType.Uuid128;
case Uuid64: return TupleSegmentType.Uuid64;
+ case VersionStamp80: return TupleSegmentType.VersionStamp80;
+ case VersionStamp96: return TupleSegmentType.VersionStamp96;
}
if (type <= IntPos8 && type >= IntNeg8)
@@ -138,6 +145,8 @@ public enum TupleSegmentType
Decimal = 35,
Uuid128 = 48,
Uuid64 = 49,
+ VersionStamp80 = 0x32,
+ VersionStamp96 = 0x33,
}
}
diff --git a/FoundationDB.Client/Native/FdbNative.cs b/FoundationDB.Client/Native/FdbNative.cs
index 5560a6e74..1f865f658 100644
--- a/FoundationDB.Client/Native/FdbNative.cs
+++ b/FoundationDB.Client/Native/FdbNative.cs
@@ -51,7 +51,6 @@ internal static unsafe class FdbNative
private const string FDB_C_DLL = "fdb_c.dll";
#endif
-
/// Handle on the native FDB C API library
private static readonly UnmanagedLibrary FdbCLib;
@@ -169,6 +168,9 @@ public static extern void fdb_transaction_clear_range(
[DllImport(FDB_C_DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern FdbError fdb_transaction_get_committed_version(TransactionHandle transaction, out long version);
+ [DllImport(FDB_C_DLL, CallingConvention = CallingConvention.Cdecl)]
+ public static extern FutureHandle fdb_transaction_get_versionstamp(TransactionHandle transaction);
+
[DllImport(FDB_C_DLL, CallingConvention = CallingConvention.Cdecl)]
public static extern FutureHandle fdb_transaction_watch(TransactionHandle transaction, byte* keyName, int keyNameLength);
@@ -531,6 +533,16 @@ public static FutureHandle TransactionCommit(TransactionHandle transaction)
return future;
}
+ public static FutureHandle TransactionGetVersionStamp(TransactionHandle transaction)
+ {
+ var future = NativeMethods.fdb_transaction_get_versionstamp(transaction);
+ Contract.Assert(future != null);
+#if DEBUG_NATIVE_CALLS
+ Debug.WriteLine("fdb_transaction_get_versionstamp(0x" + transaction.Handle.ToString("x") + ") => 0x" + future.Handle.ToString("x"));
+#endif
+ return future;
+ }
+
public static FutureHandle TransactionWatch(TransactionHandle transaction, Slice key)
{
if (key.IsNullOrEmpty) throw new ArgumentException("Key cannot be null or empty", "key");
@@ -822,6 +834,25 @@ public static FdbError FutureGetStringArray(FutureHandle future, out string[] re
return err;
}
+ public static FdbError FutureGetVersionStamp(FutureHandle future, out VersionStamp stamp)
+ {
+ byte* ptr;
+ int keyLength;
+ var err = NativeMethods.fdb_future_get_key(future, out ptr, out keyLength);
+#if DEBUG_NATIVE_CALLS
+ Debug.WriteLine("fdb_future_get_key(0x" + future.Handle.ToString("x") + ") => err=" + err + ", keyLength=" + keyLength);
+#endif
+
+ if (keyLength != 10 || ptr == null)
+ {
+ stamp = default;
+ return err;
+ }
+
+ VersionStamp.ReadUnsafe(ptr, 10, out stamp);
+ return err;
+ }
+
public static void TransactionSet(TransactionHandle transaction, Slice key, Slice value)
{
fixed (byte* pKey = key.Array)
diff --git a/FoundationDB.Client/Native/FdbNativeTransaction.cs b/FoundationDB.Client/Native/FdbNativeTransaction.cs
index dabb06068..426348100 100644
--- a/FoundationDB.Client/Native/FdbNativeTransaction.cs
+++ b/FoundationDB.Client/Native/FdbNativeTransaction.cs
@@ -393,6 +393,25 @@ public long GetCommittedVersion()
return version;
}
+ public Task GetVersionStampAsync(CancellationToken ct)
+ {
+ var future = FdbNative.TransactionGetVersionStamp(m_handle);
+ return FdbFuture.CreateTaskFromHandle(future, GetVersionStampResult, ct);
+ }
+
+ private static VersionStamp GetVersionStampResult(FutureHandle h)
+ {
+ Contract.Requires(h != null);
+ var err = FdbNative.FutureGetVersionStamp(h, out VersionStamp stamp);
+#if DEBUG_TRANSACTIONS
+ Debug.WriteLine("FdbTransaction[" + m_id + "].FutureGetVersionStamp() => err=" + err + ", vs=" + stamp + ")");
+#endif
+ Fdb.DieOnError(err);
+
+ return stamp;
+ }
+
+
///
/// Attempts to commit the sets and clears previously applied to the database snapshot represented by this transaction to the actual database.
/// The commit may or may not succeed – in particular, if a conflicting transaction previously committed, then the commit must fail in order to preserve transactional isolation.
diff --git a/FoundationDB.Client/VersionStamp.cs b/FoundationDB.Client/VersionStamp.cs
new file mode 100644
index 000000000..beec01694
--- /dev/null
+++ b/FoundationDB.Client/VersionStamp.cs
@@ -0,0 +1,406 @@
+#region BSD Licence
+/* Copyright (c) 2013-2018, Doxense SAS
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Doxense nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#endregion
+
+namespace FoundationDB.Client
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Runtime.CompilerServices;
+ using Doxense;
+ using Doxense.Diagnostics.Contracts;
+ using Doxense.Memory;
+ using JetBrains.Annotations;
+
+ /// VersionStamp
+ /// A versionstamp is unique, monotonically (but not sequentially) increasing value for each committed transaction.
+ /// Its size can either be 10 bytes (80-bits) or 12-bytes (96-bits).
+ /// The first 8 bytes are the committed version of the database. The next 2 bytes are monotonic in the serialization order for transactions.
+ /// The optional last 2 bytes can contain a user-provider version number used to allow multiple stamps inside the same transaction.
+ ///
+ [DebuggerDisplay("{ToString(),nq}")]
+ public readonly struct VersionStamp : IEquatable, IComparable
+ {
+ //REVIEW: they are called "Versionstamp" in the doc, but "VersionStamp" seems more .NETy (like 'TimeSpan').
+ // => Should we keep the uppercase 'S' or not ?
+
+ private const ulong PLACEHOLDER_VERSION = ulong.MaxValue;
+ private const ushort PLACEHOLDER_ORDER = ushort.MaxValue;
+ private const ushort NO_USER_VERSION = 0;
+ private const ulong HSB_VERSION = 0x8000000000000000UL;
+
+ private const ushort FLAGS_NONE = 0x0;
+ private const ushort FLAGS_HAS_VERSION = 0x1; // unset: 80-bits, set: 96-bits
+ private const ushort FLAGS_IS_INCOMPLETE = 0x2; // unset: complete, set: incomplete
+
+ /// Serialized bytes of the default incomplete stamp (composed of only 0xFF)
+ internal static readonly Slice IncompleteToken = Slice.Repeat(0xFF, 10);
+
+ /// Commit version of the transaction
+ /// This value is determined by the database at commit time.
+
+ public readonly ulong TransactionVersion; // Bytes 0..7
+
+ /// Transaction Batch Order
+ /// This value is determined by the database at commit time.
+ public readonly ushort TransactionOrder; // Bytes 8..9
+
+ /// User-provided version (between 0 and 65535)
+ /// For 80-bits VersionStamps, this value will be 0 and will not be part of the serialized key. You can use to distinguish between both types of stamps.
+ public readonly ushort UserVersion; // Bytes 10..11 (if 'FLAGS_HAS_VERSION' is set)
+
+ /// Internal flags (FLAGS_xxx constants)
+ private readonly ushort Flags;
+ //note: this flag is only present in memory, and is not serialized
+
+ private VersionStamp(ulong version, ushort order, ushort user, ushort flags)
+ {
+ this.TransactionVersion = version;
+ this.TransactionOrder = order;
+ this.UserVersion = user;
+ this.Flags = flags;
+ }
+
+ /// Creates an incomplete 80-bit with no user version.
+ /// Placeholder that will be serialized as FF FF FF FF FF FF FF FF FF FF (10 bytes).
+ ///
+ /// This stamp contains a temporary marker that will be later filled by the database with the actual VersioStamp by the database at transaction commit time.
+ /// If you need to create multiple distinct stamps within the same transaction, please use instead.
+ ///
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static VersionStamp Incomplete()
+ {
+ return new VersionStamp(PLACEHOLDER_VERSION, PLACEHOLDER_ORDER, NO_USER_VERSION, FLAGS_IS_INCOMPLETE);
+ }
+
+ /// Creates an incomplete 96-bit with the given user version.
+ /// Value between 0 and 65535 that will be appended at the end of the Versionstamp, making it unique within the transaction.
+ /// Placeholder that will be serialized as FF FF FF FF FF FF FF FF FF FF vv vv (12 bytes) where 'vv vv' is the user version encoded in little-endian.
+ /// If is less than 0, or greater than 65534 (0xFFFE).
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static VersionStamp Incomplete(int userVersion)
+ {
+ Contract.Between(userVersion, 0, 0xFFFF, nameof(userVersion), "Local version must fit in 16-bits.");
+ return new VersionStamp(PLACEHOLDER_VERSION, PLACEHOLDER_ORDER, (ushort) userVersion, FLAGS_IS_INCOMPLETE | FLAGS_HAS_VERSION);
+ }
+
+ /// Creates an incomplete 96-bit with the given user version.
+ /// Value between 0 and 65535 that will be appended at the end of the Versionstamp, making it unique within the transaction.
+ /// Placeholder that will be serialized as FF FF FF FF FF FF FF FF FF FF vv vv (12 bytes) where 'vv vv' is the user version encoded in little-endian.
+ /// If is less than 0, or greater than 65534 (0xFFFE).
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static VersionStamp Incomplete(ushort userVersion)
+ {
+ return new VersionStamp(PLACEHOLDER_VERSION, PLACEHOLDER_ORDER, userVersion, FLAGS_IS_INCOMPLETE | FLAGS_HAS_VERSION);
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static VersionStamp Custom(ulong version, ushort order, bool incomplete)
+ {
+ return new VersionStamp(version, order, NO_USER_VERSION, incomplete ? FLAGS_IS_INCOMPLETE : FLAGS_NONE);
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static VersionStamp Custom(ulong version, ushort order, int userVersion, bool incomplete)
+ {
+ Contract.Between(userVersion, 0, 0xFFFF, nameof(userVersion), "Local version must fit in 16-bits.");
+ return new VersionStamp(version, order, (ushort) userVersion, incomplete ? (ushort) (FLAGS_IS_INCOMPLETE | FLAGS_HAS_VERSION) : FLAGS_HAS_VERSION);
+ }
+
+ /// Creates a 80-bit , obtained from the database.
+ /// Complete stamp, without user version.
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static VersionStamp Complete(ulong version, ushort order)
+ {
+ return new VersionStamp(version, order, NO_USER_VERSION, FLAGS_NONE);
+ }
+
+ /// Creates a 96-bit , obtained from the database.
+ /// Complete stamp, with a user version.
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static VersionStamp Complete(ulong version, ushort order, int userVersion)
+ {
+ Contract.Between(userVersion, 0, 0xFFFF, nameof(userVersion), "Local version must fit in 16-bits, and cannot be 0xFFFF.");
+ return new VersionStamp(version, order, (ushort) userVersion, FLAGS_HAS_VERSION);
+ }
+
+ /// Creates a 96-bit , obtained from the database.
+ /// Complete stamp, with a user version.
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static VersionStamp Complete(ulong version, ushort order, ushort userVersion)
+ {
+ return new VersionStamp(version, order, userVersion, FLAGS_HAS_VERSION);
+ }
+
+ /// Test if the stamp has a user version (96-bits) or not (80-bits)
+ public bool HasUserVersion
+ {
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => (this.Flags & FLAGS_HAS_VERSION) != 0;
+ }
+
+ /// Test if the stamp is marked as incomplete (true), or has already been resolved by the database (false)
+ public bool IsIncomplete
+ {
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => (this.Flags & FLAGS_IS_INCOMPLETE) != 0;
+ }
+
+ /// Return the length (in bytes) of the versionstamp when serialized in binary format
+ /// Returns 12 bytes for stamps with a user version, and 10 bytes without.
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int GetLength() => 10 + 2 * (this.Flags & FLAGS_HAS_VERSION);
+
+ public override string ToString()
+ {
+ if (this.HasUserVersion)
+ {
+ return this.IsIncomplete
+ ? $"@?#{this.UserVersion}"
+ : $"@{this.TransactionVersion}-{this.TransactionOrder}#{this.UserVersion}";
+ }
+ else
+ {
+ return this.IsIncomplete
+ ? "@?"
+ : $"@{this.TransactionVersion}-{this.TransactionOrder}";
+ }
+ }
+
+ public Slice ToSlice()
+ {
+ int len = GetLength(); // 10 or 12
+ var tmp = Slice.Create(len);
+ unsafe
+ {
+ fixed (byte* ptr = &tmp.DangerousGetPinnableReference())
+ {
+ WriteUnsafe(ptr, len, in this);
+ }
+ }
+ return tmp;
+ }
+
+ public void WriteTo(in Slice buffer)
+ {
+ int len = GetLength(); // 10 or 12
+ if (buffer.Count < len) throw new ArgumentException($"The target buffer must be at least {len} bytes long.");
+ unsafe
+ {
+ fixed (byte* ptr = &buffer.DangerousGetPinnableReference())
+ {
+ WriteUnsafe(ptr, len, in this);
+ }
+ }
+ }
+
+ public void WriteTo(ref SliceWriter writer)
+ {
+ var tmp = writer.Allocate(GetLength());
+ unsafe
+ {
+ fixed (byte* ptr = &tmp.DangerousGetPinnableReference())
+ {
+ WriteUnsafe(ptr, tmp.Count, in this);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static unsafe void WriteUnsafe(byte* ptr, int len, in VersionStamp vs)
+ {
+ Contract.Debug.Assert(len == 10 || len == 12);
+ UnsafeHelpers.StoreUInt64BE(ptr, vs.TransactionVersion);
+ UnsafeHelpers.StoreUInt16BE(ptr + 8, vs.TransactionOrder);
+ if (len == 12)
+ {
+ UnsafeHelpers.StoreUInt16BE(ptr + 10, vs.UserVersion);
+ }
+ }
+
+ /// Parse a VersionStamp from a sequence of 10 bytes
+ /// If the buffer length is not exactly 12 bytes
+ [Pure]
+ public static VersionStamp Parse(Slice data)
+ {
+ return TryParse(data, out var vs) ? vs : throw new FormatException("A VersionStamp is either 10 or 12 bytes.");
+ }
+
+ /// Try parsing a VersionStamp from a sequence of bytes
+ public static bool TryParse(Slice data, out VersionStamp vs)
+ {
+ if (data.Count != 10 && data.Count != 12)
+ {
+ vs = default;
+ return false;
+ }
+ unsafe
+ {
+ fixed (byte* ptr = &data.DangerousGetPinnableReference())
+ {
+ ReadUnsafe(ptr, data.Count, out vs);
+ return true;
+ }
+ }
+ }
+
+ internal static unsafe void ReadUnsafe(byte* ptr, int len, out VersionStamp vs)
+ {
+ Contract.Debug.Assert(len == 10 || len == 12);
+ // reads a complete 12 bytes Versionstamp
+ ulong ver = UnsafeHelpers.LoadUInt64BE(ptr);
+ ushort order = UnsafeHelpers.LoadUInt16BE(ptr + 8);
+ ushort idx = len == 10 ? NO_USER_VERSION : UnsafeHelpers.LoadUInt16BE(ptr + 10);
+ ushort flags = FLAGS_NONE;
+ if (len == 12) flags |= FLAGS_HAS_VERSION;
+ if ((ver & HSB_VERSION) != 0) flags |= FLAGS_IS_INCOMPLETE;
+ vs = new VersionStamp(ver, order, idx, flags);
+ }
+
+ #region Equality, Comparision, ...
+
+ public override bool Equals(object obj)
+ {
+ return obj is VersionStamp vs && Equals(vs);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCodes.Combine(this.TransactionVersion.GetHashCode(), this.TransactionOrder, this.UserVersion, this.Flags);
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(VersionStamp other)
+ {
+ //PERF: could we use Unsafe and compare the next sizeof(VersionStamp) bytes at once?
+ return (this.TransactionVersion == other.TransactionVersion)
+ & (this.TransactionOrder == other.TransactionOrder)
+ & (this.UserVersion == other.UserVersion)
+ & (this.Flags == other.Flags);
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(VersionStamp left, VersionStamp right)
+ {
+ return left.Equals(right);
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(VersionStamp left, VersionStamp right)
+ {
+ return !left.Equals(right);
+ }
+
+ [Pure]
+ public int CompareTo(VersionStamp other)
+ {
+ //ordering rules:
+ // - incomplete stamps are stored AFTER resolved stamps (since if they commit they would have a value higher than any other stamp already in the database)
+ // - ordered by transaction number then transaction batch order
+ // - stamps with no user version are sorted before stamps with user version if they have the same first 10 bytes, so (XXXX) is before (XXXX, 0)
+
+ if (this.IsIncomplete)
+ { // we ignore the transaction version/order!
+ if (!other.IsIncomplete) return +1; // we are after
+ }
+ else
+ {
+ if (other.IsIncomplete) return -1; // we are before
+ int cmp = this.TransactionVersion.CompareTo(other.TransactionVersion);
+ if (cmp != 0) return cmp;
+ }
+
+ // both have same version+order, or both are incomplete
+ // => we need to decide on the (optional) user version
+ return this.HasUserVersion
+ ? (other.HasUserVersion ? this.UserVersion.CompareTo(other.UserVersion) : +1)
+ : (other.HasUserVersion ? -1 : 0);
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator <(VersionStamp left, VersionStamp right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator <=(VersionStamp left, VersionStamp right)
+ {
+ return left.CompareTo(right) <= 0;
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator >(VersionStamp left, VersionStamp right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator >=(VersionStamp left, VersionStamp right)
+ {
+ return left.CompareTo(right) >= 0;
+ }
+
+ //REVIEW: does these make sense or not?
+ // VersionStamp - VersionStamp == ???
+ // VersionStamp + 123 == ???
+ // VersionStamp * 2 == ???
+
+ public sealed class Comparer : IEqualityComparer, IComparer
+ {
+ /// Default comparer for s
+ public static Comparer Default { get; } = new Comparer();
+
+ private Comparer()
+ { }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(VersionStamp x, VersionStamp y)
+ {
+ return x.Equals(y);
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int GetHashCode(VersionStamp obj)
+ {
+ return obj.GetHashCode();
+ }
+
+ [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Compare(VersionStamp x, VersionStamp y)
+ {
+ return x.CompareTo(y);
+ }
+
+ }
+
+ #endregion
+
+ }
+
+}
diff --git a/FoundationDB.Tests/FoundationDB.Tests.csproj b/FoundationDB.Tests/FoundationDB.Tests.csproj
index 0c78ba023..ae776350b 100644
--- a/FoundationDB.Tests/FoundationDB.Tests.csproj
+++ b/FoundationDB.Tests/FoundationDB.Tests.csproj
@@ -30,6 +30,7 @@
105,108,109,114,472,660,661,628,1066
AnyCPU
true
+ latest
pdbonly
@@ -42,6 +43,7 @@
105,108,109,114,472,660,661,628,1066
AnyCPU
true
+ latest
true
@@ -93,6 +95,7 @@
+
@@ -122,6 +125,7 @@
+
diff --git a/FoundationDB.Tests/FoundationDB.Tests.csproj.DotSettings b/FoundationDB.Tests/FoundationDB.Tests.csproj.DotSettings
new file mode 100644
index 000000000..96331d1ce
--- /dev/null
+++ b/FoundationDB.Tests/FoundationDB.Tests.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ CSharp72
\ No newline at end of file
diff --git a/FoundationDB.Tests/TransactionFacts.cs b/FoundationDB.Tests/TransactionFacts.cs
index 50dbf1839..a4c1f5f15 100644
--- a/FoundationDB.Tests/TransactionFacts.cs
+++ b/FoundationDB.Tests/TransactionFacts.cs
@@ -1989,6 +1989,193 @@ public async Task Test_Simple_Read_Transaction()
}
}
+ [Test]
+ public async Task Test_VersionStamps_Share_The_Same_Token_Per_Transaction_Attempt()
+ {
+ // Veryify that we can set versionstamped keys inside a transaction
+
+ using (var db = await OpenTestDatabaseAsync())
+ {
+ using (var tr = db.BeginTransaction(this.Cancellation))
+ {
+ // should return a 80-bit incomplete stamp, using a random token
+ var x = tr.CreateVersionStamp();
+ Log($"> x : {x} with token '{x.ToSlice():X}'");
+ Assert.That(x.IsIncomplete, Is.True, "Placeholder token should be incomplete");
+ Assert.That(x.HasUserVersion, Is.False);
+ Assert.That(x.UserVersion, Is.Zero);
+ Assert.That(x.TransactionVersion >> 56, Is.EqualTo(0xFF), "Highest 8 bit of Transaction Version should be set to 1");
+ Assert.That(x.TransactionOrder >> 12, Is.EqualTo(0xF), "Hight 4 bits of Transaction Order should be set to 1");
+
+ // should return a 96-bit incomplete stamp, using a the same random token and user version 0
+ var x0 = tr.CreateVersionStamp(0);
+ Log($"> x0 : {x0.ToSlice():X} => {x0}");
+ Assert.That(x0.IsIncomplete, Is.True, "Placeholder token should be incomplete");
+ Assert.That(x0.TransactionVersion, Is.EqualTo(x.TransactionVersion), "All generated stamps by one transaction should share the random token value ");
+ Assert.That(x0.TransactionOrder, Is.EqualTo(x.TransactionOrder), "All generated stamps by one transaction should share the random token value ");
+ Assert.That(x0.HasUserVersion, Is.True);
+ Assert.That(x0.UserVersion, Is.EqualTo(0));
+
+ // should return a 96-bit incomplete stamp, using a the same random token and user version 1
+ var x1 = tr.CreateVersionStamp(1);
+ Log($"> x1 : {x1.ToSlice():X} => {x1}");
+ Assert.That(x1.IsIncomplete, Is.True, "Placeholder token should be incomplete");
+ Assert.That(x1.TransactionVersion, Is.EqualTo(x.TransactionVersion), "All generated stamps by one transaction should share the random token value ");
+ Assert.That(x1.TransactionOrder, Is.EqualTo(x.TransactionOrder), "All generated stamps by one transaction should share the random token value ");
+ Assert.That(x1.HasUserVersion, Is.True);
+ Assert.That(x1.UserVersion, Is.EqualTo(1));
+
+ // should return a 96-bit incomplete stamp, using a the same random token and user version 42
+ var x42 = tr.CreateVersionStamp(42);
+ Log($"> x42: {x42.ToSlice():X} => {x42}");
+ Assert.That(x42.IsIncomplete, Is.True, "Placeholder token should be incomplete");
+ Assert.That(x42.TransactionVersion, Is.EqualTo(x.TransactionVersion), "All generated stamps by one transaction should share the random token value ");
+ Assert.That(x42.TransactionOrder, Is.EqualTo(x.TransactionOrder), "All generated stamps by one transaction should share the random token value ");
+ Assert.That(x42.HasUserVersion, Is.True);
+ Assert.That(x42.UserVersion, Is.EqualTo(42));
+
+ // Reset the transaction
+ // => stamps should use a new value
+ Log("Reset!");
+ tr.Reset();
+
+ var y = tr.CreateVersionStamp();
+ Log($"> y : {y.ToSlice():X} => {y}'");
+ Assert.That(y, Is.Not.EqualTo(x), "VersionStamps should change when a transaction is reset");
+
+ Assert.That(y.IsIncomplete, Is.True, "Placeholder token should be incomplete");
+ Assert.That(y.HasUserVersion, Is.False);
+ Assert.That(y.UserVersion, Is.Zero);
+ Assert.That(y.TransactionVersion >> 56, Is.EqualTo(0xFF), "Highest 8 bit of Transaction Version should be set to 1");
+ Assert.That(y.TransactionOrder >> 12, Is.EqualTo(0xF), "Hight 4 bits of Transaction Order should be set to 1");
+
+ var y42 = tr.CreateVersionStamp(42);
+ Log($"> y42: {y42.ToSlice():X} => {y42}");
+ Assert.That(y42.IsIncomplete, Is.True, "Placeholder token should be incomplete");
+ Assert.That(y42.TransactionVersion, Is.EqualTo(y.TransactionVersion), "All generated stamps by one transaction should share the random token value ");
+ Assert.That(y42.TransactionOrder, Is.EqualTo(y.TransactionOrder), "All generated stamps by one transaction should share the random token value ");
+ Assert.That(y42.HasUserVersion, Is.True);
+ Assert.That(y42.UserVersion, Is.EqualTo(42));
+ }
+ }
+ }
+
+ [Test]
+ public async Task Test_VersionStamp_Operations()
+ {
+ // Veryify that we can set versionstamped keys inside a transaction
+
+ using (var db = await OpenTestDatabaseAsync())
+ {
+ var location = db.Partition.ByKey("versionstamps");
+
+ await db.ClearRangeAsync(location, this.Cancellation);
+
+ VersionStamp vsActual; // will contain the actual version stamp used by the database
+
+ Log("Inserting keys with version stamps:");
+ using (var tr = db.BeginTransaction(this.Cancellation))
+ {
+
+ // should return a 80-bit incomplete stamp, using a random token
+ var vs = tr.CreateVersionStamp();
+ Log($"> placeholder stamp: {vs} with token '{vs.ToSlice():X}'");
+
+ // a single key using the 80-bit stamp
+ tr.SetVersionStampedKey(location.Keys.Encode("foo", vs, 123), Slice.FromString("Hello, World!"));
+
+ // simulate a batch of 3 keys, using 96-bits stamps
+ tr.SetVersionStampedKey(location.Keys.Encode("bar", tr.CreateVersionStamp(0)), Slice.FromString("Zero"));
+ tr.SetVersionStampedKey(location.Keys.Encode("bar", tr.CreateVersionStamp(1)), Slice.FromString("One"));
+ tr.SetVersionStampedKey(location.Keys.Encode("bar", tr.CreateVersionStamp(42)), Slice.FromString("FortyTwo"));
+
+ // value that contain the stamp
+ var val = Slice.FromString("$$$$$$$$$$Hello World!"); // '$' will be replaced by the stamp
+ Log($"> {val:X}");
+ tr.SetVersionStampedValue(location.Keys.Encode("baz"), val);
+
+ // need to be request BEFORE the commit
+ var vsTask = tr.GetVersionStampAsync();
+
+ await tr.CommitAsync();
+ Log(tr.GetCommittedVersion());
+
+ // need to be resolved AFTER the commit
+ vsActual = await vsTask;
+ Log($"> actual stamp: {vsActual} with token '{vsActual.ToSlice():X}'");
+ }
+
+ await DumpSubspace(db, location);
+
+ Log("Checking database content:");
+ using (var tr = db.BeginReadOnlyTransaction(this.Cancellation))
+ {
+ {
+ var foo = await tr.GetRange(location.Keys.ToKeyRange("foo")).SingleAsync();
+ Log("> Found 1 result under (foo,)");
+ Log($"- {location.ExtractKey(foo.Key):K} = {foo.Value:V}");
+ Assert.That(foo.Value.ToString(), Is.EqualTo("Hello, World!"));
+
+ var t = location.Keys.Unpack(foo.Key);
+ Assert.That(t.Get(0), Is.EqualTo("foo"));
+ Assert.That(t.Get(2), Is.EqualTo(123));
+
+ var vs = t.Get(1);
+ Assert.That(vs.IsIncomplete, Is.False);
+ Assert.That(vs.HasUserVersion, Is.False);
+ Assert.That(vs.UserVersion, Is.Zero);
+ Assert.That(vs.TransactionVersion, Is.EqualTo(vsActual.TransactionVersion));
+ Assert.That(vs.TransactionOrder, Is.EqualTo(vsActual.TransactionOrder));
+ }
+
+ {
+ var items = await tr.GetRange(location.Keys.ToKeyRange("bar")).ToListAsync();
+ Log($"> Found {items.Count} results under (bar,)");
+ foreach (var item in items)
+ {
+ Log($"- {location.ExtractKey(item.Key):K} = {item.Value:V}");
+ }
+
+ Assert.That(items.Count, Is.EqualTo(3), "Should have found 3 keys under 'foo'");
+
+ Assert.That(items[0].Value.ToString(), Is.EqualTo("Zero"));
+ var vs0 = location.Keys.DecodeLast(items[0].Key);
+ Assert.That(vs0.IsIncomplete, Is.False);
+ Assert.That(vs0.HasUserVersion, Is.True);
+ Assert.That(vs0.UserVersion, Is.EqualTo(0));
+ Assert.That(vs0.TransactionVersion, Is.EqualTo(vsActual.TransactionVersion));
+ Assert.That(vs0.TransactionOrder, Is.EqualTo(vsActual.TransactionOrder));
+
+ Assert.That(items[1].Value.ToString(), Is.EqualTo("One"));
+ var vs1 = location.Keys.DecodeLast(items[1].Key);
+ Assert.That(vs1.IsIncomplete, Is.False);
+ Assert.That(vs1.HasUserVersion, Is.True);
+ Assert.That(vs1.UserVersion, Is.EqualTo(1));
+ Assert.That(vs1.TransactionVersion, Is.EqualTo(vsActual.TransactionVersion));
+ Assert.That(vs1.TransactionOrder, Is.EqualTo(vsActual.TransactionOrder));
+
+ Assert.That(items[2].Value.ToString(), Is.EqualTo("FortyTwo"));
+ var vs42 = location.Keys.DecodeLast(items[2].Key);
+ Assert.That(vs42.IsIncomplete, Is.False);
+ Assert.That(vs42.HasUserVersion, Is.True);
+ Assert.That(vs42.UserVersion, Is.EqualTo(42));
+ Assert.That(vs42.TransactionVersion, Is.EqualTo(vsActual.TransactionVersion));
+ Assert.That(vs42.TransactionOrder, Is.EqualTo(vsActual.TransactionOrder));
+ }
+
+ {
+ var baz = await tr.GetAsync(location.Keys.Encode("baz"));
+ Log($"> {baz:X}");
+ // ensure that the first 10 bytes have been overwritten with the stamp
+ Assert.That(baz.Count, Is.GreaterThan(0), "Key should be present in the database");
+ Assert.That(baz.StartsWith(vsActual.ToSlice()), Is.True, "The first 10 bytes should match the resolved stamp");
+ Assert.That(baz.Substring(10), Is.EqualTo(Slice.FromString("Hello World!")), "The rest of the slice should be untouched");
+ }
+ }
+
+ }
+ }
+
[Test, Category("LongRunning")]
public async Task Test_BadPractice_Future_Fuzzer()
{
diff --git a/FoundationDB.Tests/Utils/TuPackFacts.cs b/FoundationDB.Tests/Utils/TuPackFacts.cs
new file mode 100644
index 000000000..664b40b47
--- /dev/null
+++ b/FoundationDB.Tests/Utils/TuPackFacts.cs
@@ -0,0 +1,2210 @@
+#region BSD Licence
+/* Copyright (c) 2013-2018, Doxense SAS
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Doxense nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#endregion
+
+//#define ENABLE_VALUETUPLE
+
+// ReSharper disable AccessToModifiedClosure
+namespace Doxense.Collections.Tuples.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Net;
+ using Doxense.Collections.Tuples.Encoding;
+ using FoundationDB.Client;
+ using FoundationDB.Client.Tests;
+ using NUnit.Framework;
+
+ [TestFixture]
+ public class TuPackFacts : FdbTest
+ {
+
+ #region Serialization...
+
+ [Test]
+ public void Test_TuplePack_Serialize_Bytes()
+ {
+ // Byte arrays are stored with prefix '01' followed by the bytes, and terminated by '00'. All occurences of '00' in the byte array are escaped with '00 FF'
+ // - Best case: packed_size = 2 + array_len
+ // - Worst case: packed_size = 2 + array_len * 2
+
+ Slice packed;
+
+ packed = TuPack.EncodeKey(new byte[] {0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0});
+ Assert.That(packed.ToString(), Is.EqualTo("<01><12>4Vx<9A><00>"));
+ packed = TuPack.EncodeKey(new byte[] {0x00, 0x42});
+ Assert.That(packed.ToString(), Is.EqualTo("<01><00>B<00>"));
+ packed = TuPack.EncodeKey(new byte[] {0x42, 0x00});
+ Assert.That(packed.ToString(), Is.EqualTo("<01>B<00><00>"));
+ packed = TuPack.EncodeKey(new byte[] {0x42, 0x00, 0x42});
+ Assert.That(packed.ToString(), Is.EqualTo("<01>B<00>B<00>"));
+ packed = TuPack.EncodeKey(new byte[] {0x42, 0x00, 0x00, 0x42});
+ Assert.That(packed.ToString(), Is.EqualTo("<01>B<00><00>B<00>"));
+ }
+
+ [Test]
+ public void Test_TuplePack_Deserialize_Bytes()
+ {
+ ITuple t;
+
+ t = TuPack.Unpack(Slice.Unescape("<01><01><23><45><67><89><00>"));
+ Assert.That(t.Get(0), Is.EqualTo(new byte[] {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}));
+ Assert.That(t.Get(0).ToHexaString(' '), Is.EqualTo("01 23 45 67 89 AB CD EF"));
+
+ t = TuPack.Unpack(Slice.Unescape("<01><42><00><00>"));
+ Assert.That(t.Get(0), Is.EqualTo(new byte[] {0x42, 0x00}));
+ Assert.That(t.Get(0).ToHexaString(' '), Is.EqualTo("42 00"));
+
+ t = TuPack.Unpack(Slice.Unescape("<01><00><42><00>"));
+ Assert.That(t.Get(0), Is.EqualTo(new byte[] {0x00, 0x42}));
+ Assert.That(t.Get(0).ToHexaString(' '), Is.EqualTo("00 42"));
+
+ t = TuPack.Unpack(Slice.Unescape("<01><42><00><42><00>"));
+ Assert.That(t.Get(0), Is.EqualTo(new byte[] {0x42, 0x00, 0x42}));
+ Assert.That(t.Get(0).ToHexaString(' '), Is.EqualTo("42 00 42"));
+
+ t = TuPack.Unpack(Slice.Unescape("<01><42><00><00><42><00>"));
+ Assert.That(t.Get(0), Is.EqualTo(new byte[] {0x42, 0x00, 0x00, 0x42}));
+ Assert.That(t.Get(0).ToHexaString(' '), Is.EqualTo("42 00 00 42"));
+ }
+
+ [Test]
+ public void Test_TuplePack_Serialize_Unicode_Strings()
+ {
+ // Unicode strings are stored with prefix '02' followed by the utf8 bytes, and terminated by '00'. All occurences of '00' in the UTF8 bytes are escaped with '00 FF'
+
+ Slice packed;
+
+ // simple string
+ packed = TuPack.EncodeKey("hello world");
+ Assert.That(packed.ToString(), Is.EqualTo("<02>hello world<00>"));
+
+ // empty
+ packed = TuPack.EncodeKey(String.Empty);
+ Assert.That(packed.ToString(), Is.EqualTo("<02><00>"));
+
+ // null
+ packed = TuPack.EncodeKey(default(string));
+ Assert.That(packed.ToString(), Is.EqualTo("<00>"));
+
+ // unicode
+ packed = TuPack.EncodeKey("こんにちは世界");
+ // note: Encoding.UTF8.GetBytes("こんにちは世界") => { e3 81 93 e3 82 93 e3 81 ab e3 81 a1 e3 81 af e4 b8 96 e7 95 8c }
+ Assert.That(packed.ToString(), Is.EqualTo("<02><81><93><82><93><81><81><81><96><95><8C><00>"));
+ }
+
+ [Test]
+ public void Test_TuplePack_Deserialize_Unicode_Strings()
+ {
+ ITuple t;
+
+ // simple string
+ t = TuPack.Unpack(Slice.Unescape("<02>hello world<00>"));
+ Assert.That(t.Get(0), Is.EqualTo("hello world"));
+ Assert.That(t[0], Is.EqualTo("hello world"));
+
+ // empty
+ t = TuPack.Unpack(Slice.Unescape("<02><00>"));
+ Assert.That(t.Get(0), Is.EqualTo(String.Empty));
+ Assert.That(t[0], Is.EqualTo(String.Empty));
+
+ // null
+ t = TuPack.Unpack(Slice.Unescape("<00>"));
+ Assert.That(t.Get(0), Is.EqualTo(default(string)));
+ Assert.That(t[0], Is.Null);
+
+ // unicode
+ t = TuPack.Unpack(Slice.Unescape("<02><81><93><82><93><81><81><81><96><95><8C><00>"));
+ // note: Encoding.UTF8.GetString({ e3 81 93 e3 82 93 e3 81 ab e3 81 a1 e3 81 af e4 b8 96 e7 95 8c }) => "こんにちは世界"
+ Assert.That(t.Get(0), Is.EqualTo("こんにちは世界"));
+ Assert.That(t[0], Is.EqualTo("こんにちは世界"));
+ }
+
+ [Test]
+ public void Test_TuplePack_Serialize_Guids()
+ {
+ // 128-bit Guids are stored with prefix '30' followed by 16 bytes formatted according to RFC 4122
+
+ // System.Guid are stored in Little-Endian, but RFC 4122's UUIDs are stored in Big Endian, so per convention we will swap them
+
+ Slice packed;
+
+ // note: new Guid(bytes from 0 to 15) => "03020100-0504-0706-0809-0a0b0c0d0e0f";
+ packed = TuPack.EncodeKey(Guid.Parse("00010203-0405-0607-0809-0a0b0c0d0e0f"));
+ Assert.That(packed.ToString(), Is.EqualTo("0<00><01><02><03><04><05><06><07><08><09><0A><0B><0C><0D><0E><0F>"));
+
+ packed = TuPack.EncodeKey(Guid.Empty);
+ Assert.That(packed.ToString(), Is.EqualTo("0<00><00><00><00><00><00><00><00><00><00><00><00><00><00><00><00>"));
+
+ }
+
+ [Test]
+ public void Test_TuplePack_Deserialize_Guids()
+ {
+ // 128-bit Guids are stored with prefix '30' followed by 16 bytes
+ // we also accept byte arrays (prefix '01') if they are of length 16
+
+ ITuple packed;
+
+ packed = TuPack.Unpack(Slice.Unescape("<30><00><01><02><03><04><05><06><07><08><09><0A><0B><0C><0D><0E><0F>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Guid.Parse("00010203-0405-0607-0809-0a0b0c0d0e0f")));
+ Assert.That(packed[0], Is.EqualTo(Guid.Parse("00010203-0405-0607-0809-0a0b0c0d0e0f")));
+
+ packed = TuPack.Unpack(Slice.Unescape("<30><00><00><00><00><00><00><00><00><00><00><00><00><00><00><00><00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Guid.Empty));
+ Assert.That(packed[0], Is.EqualTo(Guid.Empty));
+
+ // unicode string
+ packed = TuPack.Unpack(Slice.Unescape("<02>03020100-0504-0706-0809-0a0b0c0d0e0f<00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Guid.Parse("03020100-0504-0706-0809-0a0b0c0d0e0f")));
+ //note: t[0] returns a string, not a GUID
+
+ // null maps to Guid.Empty
+ packed = TuPack.Unpack(Slice.Unescape("<00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Guid.Empty));
+ //note: t[0] returns null, not a GUID
+
+ }
+
+ [Test]
+ public void Test_TuplePack_Serialize_Uuid128s()
+ {
+ // UUID128s are stored with prefix '30' followed by 16 bytes formatted according to RFC 4122
+
+ Slice packed;
+
+ // note: new Uuid(bytes from 0 to 15) => "03020100-0504-0706-0809-0a0b0c0d0e0f";
+ packed = TuPack.EncodeKey(Uuid128.Parse("00010203-0405-0607-0809-0a0b0c0d0e0f"));
+ Assert.That(packed.ToString(), Is.EqualTo("0<00><01><02><03><04><05><06><07><08><09><0A><0B><0C><0D><0E><0F>"));
+
+ packed = TuPack.EncodeKey(Uuid128.Empty);
+ Assert.That(packed.ToString(), Is.EqualTo("0<00><00><00><00><00><00><00><00><00><00><00><00><00><00><00><00>"));
+ }
+
+ [Test]
+ public void Test_TuplePack_Deserialize_Uuid128s()
+ {
+ // UUID128s are stored with prefix '30' followed by 16 bytes (the result of uuid.ToByteArray())
+ // we also accept byte arrays (prefix '01') if they are of length 16
+
+ ITuple packed;
+
+ // note: new Uuid(bytes from 0 to 15) => "00010203-0405-0607-0809-0a0b0c0d0e0f";
+ packed = TuPack.Unpack(Slice.Unescape("<30><00><01><02><03><04><05><06><07><08><09><0A><0B><0C><0D><0E><0F>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid128.Parse("00010203-0405-0607-0809-0a0b0c0d0e0f")));
+ Assert.That(packed[0], Is.EqualTo(Uuid128.Parse("00010203-0405-0607-0809-0a0b0c0d0e0f")));
+
+ packed = TuPack.Unpack(Slice.Unescape("<30><00><00><00><00><00><00><00><00><00><00><00><00><00><00><00><00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid128.Empty));
+ Assert.That(packed[0], Is.EqualTo(Uuid128.Empty));
+
+ // unicode string
+ packed = TuPack.Unpack(Slice.Unescape("<02>00010203-0405-0607-0809-0a0b0c0d0e0f<00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid128.Parse("00010203-0405-0607-0809-0a0b0c0d0e0f")));
+ //note: t[0] returns a string, not a UUID
+
+ // null maps to Uuid.Empty
+ packed = TuPack.Unpack(Slice.Unescape("<00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid128.Empty));
+ //note: t[0] returns null, not a UUID
+
+ }
+
+ [Test]
+ public void Test_TuplePack_Serialize_Uuid64s()
+ {
+ // UUID64s are stored with prefix '31' followed by 8 bytes formatted according to RFC 4122
+
+ Slice packed;
+
+ // note: new Uuid(bytes from 0 to 7) => "00010203-04050607";
+ packed = TuPack.EncodeKey(Uuid64.Parse("00010203-04050607"));
+ Assert.That(packed.ToString(), Is.EqualTo("1<00><01><02><03><04><05><06><07>"));
+
+ packed = TuPack.EncodeKey(Uuid64.Parse("01234567-89ABCDEF"));
+ Assert.That(packed.ToString(), Is.EqualTo("1<01>#Eg<89>"));
+
+ packed = TuPack.EncodeKey(Uuid64.Empty);
+ Assert.That(packed.ToString(), Is.EqualTo("1<00><00><00><00><00><00><00><00>"));
+
+ packed = TuPack.EncodeKey(new Uuid64(0xBADC0FFEE0DDF00DUL));
+ Assert.That(packed.ToString(), Is.EqualTo("1<0F><0D>"));
+
+ packed = TuPack.EncodeKey(new Uuid64(0xDEADBEEFL));
+ Assert.That(packed.ToString(), Is.EqualTo("1<00><00><00><00>"));
+ }
+
+ [Test]
+ public void Test_TuplePack_Deserialize_Uuid64s()
+ {
+ // UUID64s are stored with prefix '31' followed by 8 bytes (the result of uuid.ToByteArray())
+ // we also accept byte arrays (prefix '01') if they are of length 8, and unicode strings (prefix '02')
+
+ ITuple packed;
+
+ // note: new Uuid(bytes from 0 to 15) => "00010203-0405-0607-0809-0a0b0c0d0e0f";
+ packed = TuPack.Unpack(Slice.Unescape("<31><01><23><45><67><89>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid64.Parse("01234567-89abcdef")));
+ Assert.That(packed[0], Is.EqualTo(Uuid64.Parse("01234567-89abcdef")));
+
+ packed = TuPack.Unpack(Slice.Unescape("<31><00><00><00><00><00><00><00><00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid64.Empty));
+ Assert.That(packed[0], Is.EqualTo(Uuid64.Empty));
+
+ // 8 bytes
+ packed = TuPack.Unpack(Slice.Unescape("<01><01><23><45><67><89><00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid64.Parse("01234567-89abcdef")));
+ //note: t[0] returns a string, not a UUID
+
+ // unicode string
+ packed = TuPack.Unpack(Slice.Unescape("<02>01234567-89abcdef<00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid64.Parse("01234567-89abcdef")));
+ //note: t[0] returns a string, not a UUID
+
+ // null maps to Uuid.Empty
+ packed = TuPack.Unpack(Slice.Unescape("<00>"));
+ Assert.That(packed.Get(0), Is.EqualTo(Uuid64.Empty));
+ //note: t[0] returns null, not a UUID
+
+ }
+
+ [Test]
+ public void Test_TuplePack_Serialize_Integers()
+ {
+ // Positive integers are stored with a variable-length encoding.
+ // - The prefix is 0x14 + the minimum number of bytes to encode the integer, from 0 to 8, so valid prefixes range from 0x14 to 0x1C
+ // - The bytes are stored in High-Endian (ie: the upper bits first)
+ // Examples:
+ // - 0 => <14>
+ // - 1..255 => <15><##>
+ // - 256..65535 .. => <16>
+ // - ulong.MaxValue => <1C>
+
+ Assert.That(
+ TuPack.EncodeKey(0).ToString(),
+ Is.EqualTo("<14>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(1).ToString(),
+ Is.EqualTo("<15><01>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(255).ToString(),
+ Is.EqualTo("<15>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(256).ToString(),
+ Is.EqualTo("<16><01><00>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(65535).ToString(),
+ Is.EqualTo("<16>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(65536).ToString(),
+ Is.EqualTo("<17><01><00><00>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(int.MaxValue).ToString(),
+ Is.EqualTo("<18><7F>")
+ );
+
+ // signed max
+ Assert.That(
+ TuPack.EncodeKey(long.MaxValue).ToString(),
+ Is.EqualTo("<1C><7F>")
+ );
+
+ // unsigned max
+ Assert.That(
+ TuPack.EncodeKey(ulong.MaxValue).ToString(),
+ Is.EqualTo("<1C>")
+ );
+ }
+
+ [Test]
+ public void Test_TuplePack_Deserialize_Integers()
+ {
+
+ Action verify = (encoded, value) =>
+ {
+ var slice = Slice.Unescape(encoded);
+ Assert.That(TuplePackers.DeserializeBoxed(slice), Is.EqualTo(value), "DeserializeBoxed({0})", encoded);
+
+ // int64
+ Assert.That(TuplePackers.DeserializeInt64(slice), Is.EqualTo(value), "DeserializeInt64({0})", encoded);
+ Assert.That(TuplePacker.Deserialize(slice), Is.EqualTo(value), "Deserialize({0})", encoded);
+
+ // uint64
+ if (value >= 0)
+ {
+ Assert.That(TuplePackers.DeserializeUInt64(slice), Is.EqualTo((ulong) value), "DeserializeUInt64({0})", encoded);
+ Assert.That(TuplePacker.Deserialize(slice), Is.EqualTo((ulong) value), "Deserialize({0})", encoded);
+ }
+ else
+ {
+ Assert.That(() => TuplePackers.DeserializeUInt64(slice), Throws.InstanceOf(), "DeserializeUInt64({0})", encoded);
+ }
+
+ // int32
+ if (value <= int.MaxValue && value >= int.MinValue)
+ {
+ Assert.That(TuplePackers.DeserializeInt32(slice), Is.EqualTo((int) value), "DeserializeInt32({0})", encoded);
+ Assert.That(TuplePacker.Deserialize(slice), Is.EqualTo((int) value), "Deserialize({0})", encoded);
+ }
+ else
+ {
+ Assert.That(() => TuplePackers.DeserializeInt32(slice), Throws.InstanceOf(), "DeserializeInt32({0})", encoded);
+ }
+
+ // uint32
+ if (value <= uint.MaxValue && value >= 0)
+ {
+ Assert.That(TuplePackers.DeserializeUInt32(slice), Is.EqualTo((uint) value), "DeserializeUInt32({0})", encoded);
+ Assert.That(TuplePacker.Deserialize(slice), Is.EqualTo((uint) value), "Deserialize({0})", encoded);
+ }
+ else
+ {
+ Assert.That(() => TuplePackers.DeserializeUInt32(slice), Throws.InstanceOf(), "DeserializeUInt32({0})", encoded);
+ }
+
+ // int16
+ if (value <= short.MaxValue && value >= short.MinValue)
+ {
+ Assert.That(TuplePackers.DeserializeInt16(slice), Is.EqualTo((short) value), "DeserializeInt16({0})", encoded);
+ Assert.That(TuplePacker.Deserialize(slice), Is.EqualTo((short) value), "Deserialize({0})", encoded);
+ }
+ else
+ {
+ Assert.That(() => TuplePackers.DeserializeInt16(slice), Throws.InstanceOf(), "DeserializeInt16({0})", encoded);
+ }
+
+ // uint16
+ if (value <= ushort.MaxValue && value >= 0)
+ {
+ Assert.That(TuplePackers.DeserializeUInt16(slice), Is.EqualTo((ushort) value), "DeserializeUInt16({0})", encoded);
+ Assert.That(TuplePacker.Deserialize(slice), Is.EqualTo((ushort) value), "Deserialize({0})", encoded);
+ }
+ else
+ {
+ Assert.That(() => TuplePackers.DeserializeUInt16(slice), Throws.InstanceOf(), "DeserializeUInt16({0})", encoded);
+ }
+
+ // sbyte
+ if (value <= sbyte.MaxValue && value >= sbyte.MinValue)
+ {
+ Assert.That(TuplePackers.DeserializeSByte(slice), Is.EqualTo((sbyte) value), "DeserializeSByte({0})", encoded);
+ Assert.That(TuplePacker.Deserialize(slice), Is.EqualTo((sbyte) value), "Deserialize({0})", encoded);
+ }
+ else
+ {
+ Assert.That(() => TuplePackers.DeserializeSByte(slice), Throws.InstanceOf(), "DeserializeSByte({0})", encoded);
+ }
+
+ // byte
+ if (value <= 255 && value >= 0)
+ {
+ Assert.That(TuplePackers.DeserializeByte(slice), Is.EqualTo((byte) value), "DeserializeByte({0})", encoded);
+ Assert.That(TuplePacker.Deserialize(slice), Is.EqualTo((byte) value), "Deserialize({0})", encoded);
+ }
+ else
+ {
+ Assert.That(() => TuplePackers.DeserializeByte(slice), Throws.InstanceOf(), "DeserializeByte({0})", encoded);
+ }
+
+ };
+ verify("<14>", 0);
+ verify("<15>{", 123);
+ verify("<15><80>", 128);
+ verify("<15>", 255);
+ verify("<16><01><00>", 256);
+ verify("<16><04>", 1234);
+ verify("<16><80><00>", 32768);
+ verify("<16>", 65535);
+ verify("<17><01><00><00>", 65536);
+ verify("<13>", -1);
+ verify("<13><00>", -255);
+ verify("<12>", -256);
+ verify("<12><00><00>", -65535);
+ verify("<11>", -65536);
+ verify("<18><7F>", int.MaxValue);
+ verify("<10><7F>", int.MinValue);
+ verify("<1C><7F>", long.MaxValue);
+ verify("<0C><7F>", long.MinValue);
+ }
+
+ [Test]
+ public void Test_TuplePack_Serialize_Negative_Integers()
+ {
+ // Negative integers are stored with a variable-length encoding.
+ // - The prefix is 0x14 - the minimum number of bytes to encode the integer, from 0 to 8, so valid prefixes range from 0x0C to 0x13
+ // - The value is encoded as the one's complement, and stored in High-Endian (ie: the upper bits first)
+ // - There is no way to encode '-0', it will be encoded as '0' (<14>)
+ // Examples:
+ // - -255..-1 => <13><00> .. <13>
+ // - -65535..-256 => <12><00>00> .. <12>
+ // - long.MinValue => <0C><7F>
+
+ Assert.That(
+ TuPack.EncodeKey(-1).ToString(),
+ Is.EqualTo("<13>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(-255).ToString(),
+ Is.EqualTo("<13><00>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(-256).ToString(),
+ Is.EqualTo("<12>")
+ );
+ Assert.That(
+ TuPack.EncodeKey(-257).ToString(),
+ Is.EqualTo("<12>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(-65535).ToString(),
+ Is.EqualTo("<12><00><00>")
+ );
+ Assert.That(
+ TuPack.EncodeKey(-65536).ToString(),
+ Is.EqualTo("<11>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(int.MinValue).ToString(),
+ Is.EqualTo("<10><7F>")
+ );
+
+ Assert.That(
+ TuPack.EncodeKey(long.MinValue).ToString(),
+ Is.EqualTo("<0C><7F>")
+ );
+ }
+
+ [Test]
+ public void Test_TuplePack_Serialize_Singles()
+ {
+ // 32-bit floats are stored in 5 bytes, using the prefix 0x20 followed by the High-Endian representation of their normalized form
+
+ Assert.That(TuPack.EncodeKey(0f).ToHexaString(' '), Is.EqualTo("20 80 00 00 00"));
+ Assert.That(TuPack.EncodeKey(42f).ToHexaString(' '), Is.EqualTo("20 C2 28 00 00"));
+ Assert.That(TuPack.EncodeKey(-42f).ToHexaString(' '), Is.EqualTo("20 3D D7 FF FF"));
+
+ Assert.That(TuPack.EncodeKey((float) Math.Sqrt(2)).ToHexaString(' '), Is.EqualTo("20 BF B5 04 F3"));
+
+ Assert.That(TuPack.EncodeKey(float.MinValue).ToHexaString(' '), Is.EqualTo("20 00 80 00 00"), "float.MinValue");
+ Assert.That(TuPack.EncodeKey(float.MaxValue).ToHexaString(' '), Is.EqualTo("20 FF 7F FF FF"), "float.MaxValue");
+ Assert.That(TuPack.EncodeKey(-0f).ToHexaString(' '), Is.EqualTo("20 7F FF FF FF"), "-0f");
+ Assert.That(TuPack.EncodeKey(float.NegativeInfinity).ToHexaString(' '), Is.EqualTo("20 00 7F FF FF"), "float.NegativeInfinity");
+ Assert.That(TuPack.EncodeKey(float.PositiveInfinity).ToHexaString(' '), Is.EqualTo("20 FF 80 00 00"), "float.PositiveInfinity");
+ Assert.That(TuPack.EncodeKey(float.Epsilon).ToHexaString(' '), Is.EqualTo("20 80 00 00 01"), "+float.Epsilon");
+ Assert.That(TuPack.EncodeKey(-float.Epsilon).ToHexaString(' '), Is.EqualTo("20 7F FF FF FE"), "-float.Epsilon");
+
+ // all possible variants of NaN should all be equal
+ Assert.That(TuPack.EncodeKey(float.NaN).ToHexaString(' '), Is.EqualTo("20 00 3F FF FF"), "float.NaN");
+
+ // cook up a non standard NaN (with some bits set in the fraction)
+ float f = float.NaN; // defined as 1f / 0f
+ uint nan;
+ unsafe { nan = *((uint*) &f); }
+ nan += 123;
+ unsafe { f = *((float*) &nan); }
+ Assert.That(float.IsNaN(f), Is.True);
+ Assert.That(
+ TuPack.EncodeKey(f).ToHexaString(' '),
+ Is.EqualTo("20 00 3F FF FF"),
+ "All variants of NaN must be normalized"
+ //note: if we have 20 00 3F FF 84, that means that the NaN was not normalized
+ );
+
+ }
+
+ [Test]
+ public void Test_TuplePack_Deserialize_Singles()
+ {
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 80 00 00 00")), Is.EqualTo(0f), "0f");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 C2 28 00 00")), Is.EqualTo(42f), "42f");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 3D D7 FF FF")), Is.EqualTo(-42f), "-42f");
+
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 BF B5 04 F3")), Is.EqualTo((float) Math.Sqrt(2)), "Sqrt(2)");
+
+ // well known values
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 00 80 00 00")), Is.EqualTo(float.MinValue), "float.MinValue");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 FF 7F FF FF")), Is.EqualTo(float.MaxValue), "float.MaxValue");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 7F FF FF FF")), Is.EqualTo(-0f), "-0f");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 00 7F FF FF")), Is.EqualTo(float.NegativeInfinity), "float.NegativeInfinity");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 FF 80 00 00")), Is.EqualTo(float.PositiveInfinity), "float.PositiveInfinity");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 00 80 00 00")), Is.EqualTo(float.MinValue), "float.Epsilon");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 80 00 00 01")), Is.EqualTo(float.Epsilon), "+float.Epsilon");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 7F FF FF FE")), Is.EqualTo(-float.Epsilon), "-float.Epsilon");
+
+ // all possible variants of NaN should end up equal and normalized to float.NaN
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 00 3F FF FF")), Is.EqualTo(float.NaN), "float.NaN");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("20 00 3F FF FF")), Is.EqualTo(float.NaN), "float.NaN");
+ }
+
+ [Test]
+ public void Test_TuplePack_Serialize_Doubles()
+ {
+ // 64-bit floats are stored in 9 bytes, using the prefix 0x21 followed by the High-Endian representation of their normalized form
+
+ Assert.That(TuPack.EncodeKey(0d).ToHexaString(' '), Is.EqualTo("21 80 00 00 00 00 00 00 00"));
+ Assert.That(TuPack.EncodeKey(42d).ToHexaString(' '), Is.EqualTo("21 C0 45 00 00 00 00 00 00"));
+ Assert.That(TuPack.EncodeKey(-42d).ToHexaString(' '), Is.EqualTo("21 3F BA FF FF FF FF FF FF"));
+
+ Assert.That(TuPack.EncodeKey(Math.PI).ToHexaString(' '), Is.EqualTo("21 C0 09 21 FB 54 44 2D 18"));
+ Assert.That(TuPack.EncodeKey(Math.E).ToHexaString(' '), Is.EqualTo("21 C0 05 BF 0A 8B 14 57 69"));
+
+ Assert.That(TuPack.EncodeKey(double.MinValue).ToHexaString(' '), Is.EqualTo("21 00 10 00 00 00 00 00 00"), "double.MinValue");
+ Assert.That(TuPack.EncodeKey(double.MaxValue).ToHexaString(' '), Is.EqualTo("21 FF EF FF FF FF FF FF FF"), "double.MaxValue");
+ Assert.That(TuPack.EncodeKey(-0d).ToHexaString(' '), Is.EqualTo("21 7F FF FF FF FF FF FF FF"), "-0d");
+ Assert.That(TuPack.EncodeKey(double.NegativeInfinity).ToHexaString(' '), Is.EqualTo("21 00 0F FF FF FF FF FF FF"), "double.NegativeInfinity");
+ Assert.That(TuPack.EncodeKey(double.PositiveInfinity).ToHexaString(' '), Is.EqualTo("21 FF F0 00 00 00 00 00 00"), "double.PositiveInfinity");
+ Assert.That(TuPack.EncodeKey(double.Epsilon).ToHexaString(' '), Is.EqualTo("21 80 00 00 00 00 00 00 01"), "+double.Epsilon");
+ Assert.That(TuPack.EncodeKey(-double.Epsilon).ToHexaString(' '), Is.EqualTo("21 7F FF FF FF FF FF FF FE"), "-double.Epsilon");
+
+ // all possible variants of NaN should all be equal
+
+ Assert.That(TuPack.EncodeKey(double.NaN).ToHexaString(' '), Is.EqualTo("21 00 07 FF FF FF FF FF FF"), "double.NaN");
+
+ // cook up a non standard NaN (with some bits set in the fraction)
+ double d = double.NaN; // defined as 1d / 0d
+ ulong nan;
+ unsafe { nan = *((ulong*) &d); }
+ nan += 123;
+ unsafe { d = *((double*) &nan); }
+ Assert.That(double.IsNaN(d), Is.True);
+ Assert.That(
+ TuPack.EncodeKey(d).ToHexaString(' '),
+ Is.EqualTo("21 00 07 FF FF FF FF FF FF")
+ //note: if we have 21 00 07 FF FF FF FF FF 84, that means that the NaN was not normalized
+ );
+
+ // roundtripping vectors of doubles
+ var tuple = STuple.Create(Math.PI, Math.E, Math.Log(1), Math.Log(2));
+ Assert.That(TuPack.Unpack(TuPack.EncodeKey(Math.PI, Math.E, Math.Log(1), Math.Log(2))), Is.EqualTo(tuple));
+ Assert.That(TuPack.Unpack(TuPack.Pack(STuple.Create(Math.PI, Math.E, Math.Log(1), Math.Log(2)))), Is.EqualTo(tuple));
+ Assert.That(TuPack.Unpack(TuPack.Pack(STuple.Empty.Append(Math.PI).Append(Math.E).Append(Math.Log(1)).Append(Math.Log(2)))), Is.EqualTo(tuple));
+ }
+
+ [Test]
+ public void Test_TuplePack_Deserialize_Doubles()
+ {
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 80 00 00 00 00 00 00 00")), Is.EqualTo(0d), "0d");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 C0 45 00 00 00 00 00 00")), Is.EqualTo(42d), "42d");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 3F BA FF FF FF FF FF FF")), Is.EqualTo(-42d), "-42d");
+
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 C0 09 21 FB 54 44 2D 18")), Is.EqualTo(Math.PI), "Math.PI");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 C0 05 BF 0A 8B 14 57 69")), Is.EqualTo(Math.E), "Math.E");
+
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 00 10 00 00 00 00 00 00")), Is.EqualTo(double.MinValue), "double.MinValue");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 FF EF FF FF FF FF FF FF")), Is.EqualTo(double.MaxValue), "double.MaxValue");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 7F FF FF FF FF FF FF FF")), Is.EqualTo(-0d), "-0d");
+ Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 00 0F FF FF FF FF FF FF")), Is.EqualTo(double.NegativeInfinity), "double.NegativeInfinity");
+ Assert.That(TuPack.DecodeKey