From 2b6737c8a0b8306ea3ca6b684f5613943e9ed796 Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Wed, 25 Apr 2018 14:00:41 +0200 Subject: [PATCH 1/9] Add first prototype of VersionStamp struct - Handle both 80-bit and 96-bit sizes - Use internal flag to distinguish between both sizes, and incomplete/complete --- FoundationDB.Client/VersionStamp.cs | 413 ++++++++++++++++++ FoundationDB.Tests/FoundationDB.Tests.csproj | 3 + .../FoundationDB.Tests.csproj.DotSettings | 2 + FoundationDB.Tests/VersionStampFacts.cs | 201 +++++++++ 4 files changed, 619 insertions(+) create mode 100644 FoundationDB.Client/VersionStamp.cs create mode 100644 FoundationDB.Tests/FoundationDB.Tests.csproj.DotSettings create mode 100644 FoundationDB.Tests/VersionStampFacts.cs diff --git a/FoundationDB.Client/VersionStamp.cs b/FoundationDB.Client/VersionStamp.cs new file mode 100644 index 000000000..e9faf5bb1 --- /dev/null +++ b/FoundationDB.Client/VersionStamp.cs @@ -0,0 +1,413 @@ +#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 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 + + /// 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); + } + + /// 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, FLAGS_NONE, out vs); + return true; + } + } + } + + /// Parse a VersionStamp from a sequence of 10 bytes + /// If the buffer length is not exactly 12 bytes + [Pure] + public static VersionStamp ParseIncomplete(Slice data) + { + return TryParseIncomplete(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 TryParseIncomplete(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, FLAGS_IS_INCOMPLETE, out vs); + return true; + } + } + } + + internal static unsafe void ReadUnsafe(byte* ptr, int len, ushort flags, 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); + flags |= len == 12 ? FLAGS_HAS_VERSION : FLAGS_NONE; + 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..a67e669da 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 @@ -122,6 +124,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/VersionStampFacts.cs b/FoundationDB.Tests/VersionStampFacts.cs new file mode 100644 index 000000000..6075d688d --- /dev/null +++ b/FoundationDB.Tests/VersionStampFacts.cs @@ -0,0 +1,201 @@ +#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.Tests +{ + using System; + using Doxense.Memory; + using NUnit.Framework; + + [TestFixture] + public class VersionStampFacts : FdbTest + { + + [Test] + public void Test_Incomplete_VersionStamp() + { + { // 80-bits (no user version) + var vs = VersionStamp.Incomplete(); + Log(vs); + Assert.That(vs.TransactionVersion, Is.EqualTo(ulong.MaxValue)); + Assert.That(vs.TransactionOrder, Is.EqualTo(ushort.MaxValue)); + Assert.That(vs.IsIncomplete, Is.True); + Assert.That(vs.HasUserVersion, Is.False, "80-bits VersionStamps don't have a user version"); + Assert.That(vs.UserVersion, Is.Zero, "80-bits VersionStamps don't have a user version"); + + Assert.That(vs.GetLength(), Is.EqualTo(10)); + Assert.That(vs.ToSlice().ToHexaString(' '), Is.EqualTo("FF FF FF FF FF FF FF FF FF FF")); + Assert.That(vs.ToString(), Is.EqualTo("@?")); + } + + { // 96-bits, default user version + var vs = VersionStamp.Incomplete(0); + Log(vs); + Assert.That(vs.TransactionVersion, Is.EqualTo(ulong.MaxValue)); + Assert.That(vs.TransactionOrder, Is.EqualTo(ushort.MaxValue)); + Assert.That(vs.IsIncomplete, Is.True); + Assert.That(vs.HasUserVersion, Is.True, "96-bits VersionStamps have a user version"); + Assert.That(vs.UserVersion, Is.EqualTo(0)); + + Assert.That(vs.GetLength(), Is.EqualTo(12)); + Assert.That(vs.ToSlice().ToHexaString(' '), Is.EqualTo("FF FF FF FF FF FF FF FF FF FF 00 00")); + Assert.That(vs.ToString(), Is.EqualTo("@?#0")); + } + + { // 96 bits, custom user version + var vs = VersionStamp.Incomplete(123); + Log(vs); + Assert.That(vs.TransactionVersion, Is.EqualTo(ulong.MaxValue)); + Assert.That(vs.TransactionOrder, Is.EqualTo(ushort.MaxValue)); + Assert.That(vs.HasUserVersion, Is.True); + Assert.That(vs.UserVersion, Is.EqualTo(123)); + Assert.That(vs.IsIncomplete, Is.True); + Assert.That(vs.ToSlice().ToHexaString(' '), Is.EqualTo("FF FF FF FF FF FF FF FF FF FF 00 7B")); + Assert.That(vs.ToString(), Is.EqualTo("@?#123")); + } + + { // 96 bits, large user version + var vs = VersionStamp.Incomplete(12345); + Log(vs); + Assert.That(vs.TransactionVersion, Is.EqualTo(ulong.MaxValue)); + Assert.That(vs.TransactionOrder, Is.EqualTo(ushort.MaxValue)); + Assert.That(vs.HasUserVersion, Is.True); + Assert.That(vs.UserVersion, Is.EqualTo(12345)); + Assert.That(vs.IsIncomplete, Is.True); + Assert.That(vs.ToSlice().ToHexaString(' '), Is.EqualTo("FF FF FF FF FF FF FF FF FF FF 30 39")); + Assert.That(vs.ToString(), Is.EqualTo("@?#12345")); + } + + Assert.That(() => VersionStamp.Incomplete(-1), Throws.ArgumentException, "User version cannot be negative"); + Assert.That(() => VersionStamp.Incomplete(65536), Throws.ArgumentException, "User version cannot be larger than 0xFFFF"); + + { + var writer = default(SliceWriter); + writer.WriteFixed24BE(0xAAAAAA); + VersionStamp.Incomplete(123).WriteTo(ref writer); + writer.WriteFixed24BE(0xAAAAAA); + Assert.That(writer.ToSlice().ToHexaString(' '), Is.EqualTo("AA AA AA FF FF FF FF FF FF FF FF FF FF 00 7B AA AA AA")); + + var reader = new SliceReader(writer.ToSlice()); + reader.Skip(3); + var vs = VersionStamp.Parse(reader.ReadBytes(12)); + Assert.That(reader.Remaining, Is.EqualTo(3)); + + Assert.That(vs.TransactionVersion, Is.EqualTo(ulong.MaxValue)); + Assert.That(vs.TransactionOrder, Is.EqualTo(ushort.MaxValue)); + Assert.That(vs.UserVersion, Is.EqualTo(123)); + Assert.That(vs.IsIncomplete, Is.False, "NOTE: reading stamps is only supposed to happen for stamps already in the database!"); + } + + { + var buf = Slice.Repeat(0xAA, 18); + VersionStamp.Incomplete(123).WriteTo(buf.Substring(3, 12)); + Assert.That(buf.ToHexaString(' '), Is.EqualTo("AA AA AA FF FF FF FF FF FF FF FF FF FF 00 7B AA AA AA")); + } + } + + [Test] + public void Test_Complete_VersionStamp() + { + { // 80-bits, no user version + var vs = VersionStamp.Complete(0x0123456789ABCDEFUL, 123); + Log(vs); + Assert.That(vs.TransactionVersion, Is.EqualTo(0x0123456789ABCDEFUL)); + Assert.That(vs.TransactionOrder, Is.EqualTo(123)); + Assert.That(vs.HasUserVersion, Is.False); + Assert.That(vs.UserVersion, Is.Zero); + Assert.That(vs.IsIncomplete, Is.False); + Assert.That(vs.ToSlice().ToHexaString(' '), Is.EqualTo("01 23 45 67 89 AB CD EF 00 7B")); + Assert.That(vs.ToString(), Is.EqualTo("@81985529216486895-123")); + } + + { // 96 bits, default user version + var vs = VersionStamp.Complete(0x0123456789ABCDEFUL, 123, 0); + Log(vs); + Assert.That(vs.TransactionVersion, Is.EqualTo(0x0123456789ABCDEFUL)); + Assert.That(vs.TransactionOrder, Is.EqualTo(123)); + Assert.That(vs.HasUserVersion, Is.True); + Assert.That(vs.UserVersion, Is.Zero); + Assert.That(vs.IsIncomplete, Is.False); + Assert.That(vs.ToSlice().ToHexaString(' '), Is.EqualTo("01 23 45 67 89 AB CD EF 00 7B 00 00")); + Assert.That(vs.ToString(), Is.EqualTo("@81985529216486895-123#0")); + } + + { // custom user version + var vs = VersionStamp.Complete(0x0123456789ABCDEFUL, 123, 456); + Log(vs); + Assert.That(vs.TransactionVersion, Is.EqualTo(0x0123456789ABCDEFUL)); + Assert.That(vs.TransactionOrder, Is.EqualTo(123)); + Assert.That(vs.HasUserVersion, Is.True); + Assert.That(vs.UserVersion, Is.EqualTo(456)); + Assert.That(vs.IsIncomplete, Is.False); + Assert.That(vs.ToSlice().ToHexaString(' '), Is.EqualTo("01 23 45 67 89 AB CD EF 00 7B 01 C8")); + Assert.That(vs.ToString(), Is.EqualTo("@81985529216486895-123#456")); + } + + { // two bytes user version + var vs = VersionStamp.Complete(0x0123456789ABCDEFUL, 12345, 6789); + Log(vs); + Assert.That(vs.TransactionVersion, Is.EqualTo(0x0123456789ABCDEFUL)); + Assert.That(vs.TransactionOrder, Is.EqualTo(12345)); + Assert.That(vs.UserVersion, Is.EqualTo(6789)); + Assert.That(vs.IsIncomplete, Is.False); + Assert.That(vs.ToSlice().ToHexaString(' '), Is.EqualTo("01 23 45 67 89 AB CD EF 30 39 1A 85")); + Assert.That(vs.ToString(), Is.EqualTo("@81985529216486895-12345#6789")); + } + + Assert.That(() => VersionStamp.Complete(0x0123456789ABCDEFUL, 0, -1), Throws.ArgumentException, "User version cannot be negative"); + Assert.That(() => VersionStamp.Complete(0x0123456789ABCDEFUL, 0, 65536), Throws.ArgumentException, "User version cannot be larger than 0xFFFF"); + + { + var writer = default(SliceWriter); + writer.WriteFixed24BE(0xAAAAAA); + VersionStamp.Complete(0x0123456789ABCDEFUL, 123, 456).WriteTo(ref writer); + writer.WriteFixed24BE(0xAAAAAA); + Assert.That(writer.ToSlice().ToHexaString(' '), Is.EqualTo("AA AA AA 01 23 45 67 89 AB CD EF 00 7B 01 C8 AA AA AA")); + + var reader = new SliceReader(writer.ToSlice()); + reader.Skip(3); + var vs = VersionStamp.Parse(reader.ReadBytes(12)); + Assert.That(reader.Remaining, Is.EqualTo(3)); + + Assert.That(vs.TransactionVersion, Is.EqualTo(0x0123456789ABCDEFUL)); + Assert.That(vs.TransactionOrder, Is.EqualTo(123)); + Assert.That(vs.UserVersion, Is.EqualTo(456)); + Assert.That(vs.IsIncomplete, Is.False); + } + + { + var buf = Slice.Repeat(0xAA, 18); + VersionStamp.Complete(0x0123456789ABCDEFUL, 123, 456).WriteTo(buf.Substring(3, 12)); + Assert.That(buf.ToHexaString(' '), Is.EqualTo("AA AA AA 01 23 45 67 89 AB CD EF 00 7B 01 C8 AA AA AA")); + } + } + + } +} From 8192bb78362462ad730a66e29168962f3f9fef32 Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Wed, 25 Apr 2018 14:02:11 +0200 Subject: [PATCH 2/9] Add bascic support for VersionStamps to Tuple Encoder - Support both 80-bits and 96-bits variants - BUGBUG: cannot recognized complete/incomplete stamps yet when parsing. --- .../Layers/Tuples/Encoding/TuplePackers.cs | 25 ++++++++++++++ .../Layers/Tuples/Encoding/TupleParser.cs | 33 +++++++++++++++++++ .../Layers/Tuples/Encoding/TupleTypes.cs | 9 +++++ 3 files changed, 67 insertions(+) diff --git a/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs b/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs index a8e8cc9b6..2e3e17b42 100644 --- a/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs +++ b/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs @@ -40,6 +40,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 @@ -553,6 +554,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) { @@ -680,6 +686,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), }; @@ -1694,6 +1701,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..66957f676 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) { @@ -1096,6 +1119,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, } } From 2ccce084144af3a8fed9380b31b379c1f4748cd2 Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Wed, 25 Apr 2018 14:03:43 +0200 Subject: [PATCH 3/9] Add initial support for fdb_transaction_get_versionstamp and add SetStampedKey() / SetStampedValue() mutations --- .../Core/IFdbTransactionHandler.cs | 3 ++ FoundationDB.Client/FdbMutationType.cs | 8 +++- FoundationDB.Client/FdbTransaction.cs | 30 ++++++++++++++ .../FdbTransactionExtensions.cs | 16 ++++++++ .../Filters/FdbTransactionFilter.cs | 6 +++ .../Filters/Logging/FdbLoggedTransaction.cs | 8 ++++ .../Logging/FdbTransactionLog.Commands.cs | 6 +++ .../Filters/Logging/FdbTransactionLog.cs | 1 + FoundationDB.Client/IFdbTransaction.cs | 8 ++++ FoundationDB.Client/Native/FdbNative.cs | 35 +++++++++++++++- .../Native/FdbNativeTransaction.cs | 19 +++++++++ FoundationDB.Tests/TransactionFacts.cs | 41 +++++++++++++++++++ 12 files changed, 179 insertions(+), 2 deletions(-) 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..ac9e45120 100644 --- a/FoundationDB.Client/FdbTransaction.cs +++ b/FoundationDB.Client/FdbTransaction.cs @@ -284,6 +284,19 @@ 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); + } + #endregion #region Get... @@ -513,6 +526,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."); } diff --git a/FoundationDB.Client/FdbTransactionExtensions.cs b/FoundationDB.Client/FdbTransactionExtensions.cs index 16d4d362c..d4f2e8992 100644 --- a/FoundationDB.Client/FdbTransactionExtensions.cs +++ b/FoundationDB.Client/FdbTransactionExtensions.cs @@ -425,6 +425,22 @@ public static void AtomicMin([NotNull] this IFdbTransaction trans, Slice key, Sl trans.Atomic(key, value, FdbMutationType.Min); } + //TODO: XML Comments! + public static void SetVersionStampedKey([NotNull] this IFdbTransaction trans, Slice key, Slice value) + { + Contract.NotNull(trans, nameof(trans)); + + trans.Atomic(key, value, FdbMutationType.VersionStampedKey); + } + + //TODO: XML Comments! + public static void SetVersionStampedValue([NotNull] this IFdbTransaction trans, Slice key, Slice value) + { + Contract.NotNull(trans, nameof(trans)); + + 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..78d414e68 100644 --- a/FoundationDB.Client/Filters/FdbTransactionFilter.cs +++ b/FoundationDB.Client/Filters/FdbTransactionFilter.cs @@ -251,6 +251,12 @@ public virtual long GetCommittedVersion() return m_transaction.GetCommittedVersion(); } + public virtual Task GetVersionStampAsync() + { + ThrowIfDisposed(); + return m_transaction.GetVersionStampAsync(); + } + 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..39015be4c 100644 --- a/FoundationDB.Client/IFdbTransaction.cs +++ b/FoundationDB.Client/IFdbTransaction.cs @@ -112,6 +112,14 @@ 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(); + /// /// Watch a key for any change in the database. /// diff --git a/FoundationDB.Client/Native/FdbNative.cs b/FoundationDB.Client/Native/FdbNative.cs index 5560a6e74..8af1fa1d5 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,27 @@ 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; + } + //note: we assume that this is a complete stamp read from the database. + //BUGBUG: if the code serialize an incomplete stamp into a tuple, and unpacks it (logging?) it MAY be changed into a complete one! + // => we could check for the 'all FF' signature, but this only works for default incomplete tokens, not custom incomplete tokens ! + VersionStamp.ReadUnsafe(ptr, 10, /*FLAGS_NONE*/0, 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.Tests/TransactionFacts.cs b/FoundationDB.Tests/TransactionFacts.cs index 50dbf1839..2b8536ec2 100644 --- a/FoundationDB.Tests/TransactionFacts.cs +++ b/FoundationDB.Tests/TransactionFacts.cs @@ -36,6 +36,7 @@ namespace FoundationDB.Client.Tests using System.Text; using System.Threading; using System.Threading.Tasks; + using Doxense.Collections.Tuples; [TestFixture] public class TransactionFacts : FdbTest @@ -1989,6 +1990,46 @@ public async Task Test_Simple_Read_Transaction() } } + [Test] + public async Task Test_VersionStamp_Operations() + { + Fdb.Start(510); + using (var db = await OpenTestDatabaseAsync()) + { + Log("API Version: " + Fdb.ApiVersion); + + var location = db.Partition.ByKey("versionstamps"); + + await db.ClearRangeAsync(location, this.Cancellation); + + using (var tr = db.BeginTransaction(this.Cancellation)) + { + Slice HACKHACK_Packify(VersionStamp stamp) + { + var x = location.Keys.Encode(stamp); + x = x.Concat(Slice.FromFixed16((short) (location.GetPrefix().Count + 1))); + Log(x.ToHexaString(' ') + " | " + location.Keys.Dump(x)); + return x; + } + + tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete()), Slice.FromString("Hello, World!")); + tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(0)), Slice.FromString("Zero")); + tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(1)), Slice.FromString("One")); + tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(2)), Slice.FromString("Two")); + + var vsTask = tr.GetVersionStampAsync(); + + await tr.CommitAsync(); + Log(tr.GetCommittedVersion()); + + var vs = await vsTask; + Log(vs); + } + + await DumpSubspace(db, location); + } + } + [Test, Category("LongRunning")] public async Task Test_BadPractice_Future_Fuzzer() { From 1cb68193e5824023416655a1cc62a313c2088443 Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Wed, 25 Apr 2018 14:04:10 +0200 Subject: [PATCH 4/9] Re-add unit test on the Tuple Encoding that were dropped before + add tests for VersionStamps --- FoundationDB.Tests/FoundationDB.Tests.csproj | 1 + FoundationDB.Tests/Utils/TuPackFacts.cs | 2210 ++++++++++++++++++ 2 files changed, 2211 insertions(+) create mode 100644 FoundationDB.Tests/Utils/TuPackFacts.cs diff --git a/FoundationDB.Tests/FoundationDB.Tests.csproj b/FoundationDB.Tests/FoundationDB.Tests.csproj index a67e669da..ae776350b 100644 --- a/FoundationDB.Tests/FoundationDB.Tests.csproj +++ b/FoundationDB.Tests/FoundationDB.Tests.csproj @@ -95,6 +95,7 @@ + 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(Slice.FromHexa("21 FF F0 00 00 00 00 00 00")), Is.EqualTo(double.PositiveInfinity), "double.PositiveInfinity"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 80 00 00 00 00 00 00 01")), Is.EqualTo(double.Epsilon), "+double.Epsilon"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 7F FF FF FF FF FF FF FE")), Is.EqualTo(-double.Epsilon), "-double.Epsilon"); + + // all possible variants of NaN should end up equal and normalized to double.NaN + Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 00 07 FF FF FF FF FF FF")), Is.EqualTo(double.NaN), "double.NaN"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("21 00 07 FF FF FF FF FF 84")), Is.EqualTo(double.NaN), "double.NaN"); + } + + [Test] + public void Test_TuplePack_Serialize_Booleans() + { + // Booleans are stored as interger 0 (<14>) for false, and integer 1 (<15><01>) for true + + Slice packed; + + // bool + packed = TuPack.EncodeKey(false); + Assert.That(packed.ToString(), Is.EqualTo("<14>")); + packed = TuPack.EncodeKey(true); + Assert.That(packed.ToString(), Is.EqualTo("<15><01>")); + + // bool? + packed = TuPack.EncodeKey(default(bool?)); + Assert.That(packed.ToString(), Is.EqualTo("<00>")); + packed = TuPack.EncodeKey((bool?) false); + Assert.That(packed.ToString(), Is.EqualTo("<14>")); + packed = TuPack.EncodeKey((bool?) true); + Assert.That(packed.ToString(), Is.EqualTo("<15><01>")); + + // tuple containing bools + packed = TuPack.EncodeKey(true); + Assert.That(packed.ToString(), Is.EqualTo("<15><01>")); + packed = TuPack.EncodeKey(true, default(string), false); + Assert.That(packed.ToString(), Is.EqualTo("<15><01><00><14>")); + } + + [Test] + public void Test_TuplePack_Deserialize_Booleans() + { + // Null, 0, and empty byte[]/strings are equivalent to False. All others are equivalent to True + + // Falsy... + Assert.That(TuPack.DecodeKey(Slice.Unescape("<00>")), Is.False, "Null => False"); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<14>")), Is.False, "0 => False"); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<01><00>")), Is.False, "byte[0] => False"); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<02><00>")), Is.False, "String.Empty => False"); + + // Truthy + Assert.That(TuPack.DecodeKey(Slice.Unescape("<15><01>")), Is.True, "1 => True"); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<13>")), Is.True, "-1 => True"); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<01>Hello<00>")), Is.True, "'Hello' => True"); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<02>Hello<00>")), Is.True, "\"Hello\" => True"); + Assert.That(TuPack.DecodeKey(TuPack.EncodeKey(123456789)), Is.True, "random int => True"); + + Assert.That(TuPack.DecodeKey(Slice.Unescape("<02>True<00>")), Is.True, "\"True\" => True"); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<02>False<00>")), Is.True, "\"False\" => True ***"); + // note: even though it would be tempting to convert the string "false" to False, it is not a standard behavior accross all bindings + + // When decoded to object, though, they should return 0 and 1 + Assert.That(TuplePackers.DeserializeBoxed(TuPack.EncodeKey(false)), Is.EqualTo(0)); + Assert.That(TuplePackers.DeserializeBoxed(TuPack.EncodeKey(true)), Is.EqualTo(1)); + } + + [Test] + public void Test_TuplePack_Serialize_VersionStamps() + { + // incomplete, 80 bits + Assert.That( + TuPack.EncodeKey(VersionStamp.Incomplete()).ToHexaString(' '), + Is.EqualTo("33 FF FF FF FF FF FF FF FF FF FF") + ); + + // incomplete, 96 bits + Assert.That( + TuPack.EncodeKey(VersionStamp.Incomplete(0)).ToHexaString(' '), + Is.EqualTo("33 FF FF FF FF FF FF FF FF FF FF 00 00") + ); + Assert.That( + TuPack.EncodeKey(VersionStamp.Incomplete(42)).ToHexaString(' '), + Is.EqualTo("33 FF FF FF FF FF FF FF FF FF FF 00 2A") + ); + Assert.That( + TuPack.EncodeKey(VersionStamp.Incomplete(456)).ToHexaString(' '), + Is.EqualTo("33 FF FF FF FF FF FF FF FF FF FF 01 C8") + ); + Assert.That( + TuPack.EncodeKey(VersionStamp.Incomplete(65535)).ToHexaString(' '), + Is.EqualTo("33 FF FF FF FF FF FF FF FF FF FF FF FF") + ); + + // complete, 80 bits + Assert.That( + TuPack.EncodeKey(VersionStamp.Complete(0x0123456789ABCDEF, 1234)).ToHexaString(' '), + Is.EqualTo("33 01 23 45 67 89 AB CD EF 04 D2") + ); + + // complete, 96 bits + Assert.That( + TuPack.EncodeKey(VersionStamp.Complete(0x0123456789ABCDEF, 1234, 0)).ToHexaString(' '), + Is.EqualTo("33 01 23 45 67 89 AB CD EF 04 D2 00 00") + ); + Assert.That( + TuPack.EncodeKey(VersionStamp.Complete(0x0123456789ABCDEF, 1234, 42)).ToHexaString(' '), + Is.EqualTo("33 01 23 45 67 89 AB CD EF 04 D2 00 2A") + ); + Assert.That( + TuPack.EncodeKey(VersionStamp.Complete(0x0123456789ABCDEF, 65535, 42)).ToHexaString(' '), + Is.EqualTo("33 01 23 45 67 89 AB CD EF FF FF 00 2A") + ); + Assert.That( + TuPack.EncodeKey(VersionStamp.Complete(0x0123456789ABCDEF, 1234, 65535)).ToHexaString(' '), + Is.EqualTo("33 01 23 45 67 89 AB CD EF 04 D2 FF FF") + ); + } + + [Test] + public void Test_TuplePack_Deserailize_VersionStamps() + { + Assert.That(TuPack.DecodeKey(Slice.FromHexa("32 FF FF FF FF FF FF FF FF FF FF")), Is.EqualTo(VersionStamp.Incomplete()), "Incomplete()"); + + Assert.That(TuPack.DecodeKey(Slice.FromHexa("33 FF FF FF FF FF FF FF FF FF FF 00 00")), Is.EqualTo(VersionStamp.Incomplete()), "Incomplete(0)"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("33 FF FF FF FF FF FF FF FF FF FF 00 2A")), Is.EqualTo(VersionStamp.Incomplete(42)), "Incomplete(42)"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("33 FF FF FF FF FF FF FF FF FF FF 01 C8")), Is.EqualTo(VersionStamp.Incomplete(456)), "Incomplete(456)"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("33 FF FF FF FF FF FF FF FF FF FF FF FF")), Is.EqualTo(VersionStamp.Incomplete(65535)), "Incomplete(65535)"); + + Assert.That(TuPack.DecodeKey(Slice.FromHexa("32 01 23 45 67 89 AB CD EF 04 D2")), Is.EqualTo(VersionStamp.Complete(0x0123456789ABCDEF, 1234)), "Complete(..., 1234)"); + + Assert.That(TuPack.DecodeKey(Slice.FromHexa("33 01 23 45 67 89 AB CD EF 04 D2 00 00")), Is.EqualTo(VersionStamp.Complete(0x0123456789ABCDEF, 1234, 0)), "Complete(..., 1234, 0)"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("33 01 23 45 67 89 AB CD EF 04 D2 00 2A")), Is.EqualTo(VersionStamp.Complete(0x0123456789ABCDEF, 1234, 42)), "Complete(..., 1234, 42)"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("33 01 23 45 67 89 AB CD EF FF FF 00 2A")), Is.EqualTo(VersionStamp.Complete(0x0123456789ABCDEF, 65535, 42)), "Complete(..., 65535, 42)"); + Assert.That(TuPack.DecodeKey(Slice.FromHexa("33 01 23 45 67 89 AB CD EF 04 D2 FF FF")), Is.EqualTo(VersionStamp.Complete(0x0123456789ABCDEF, 1234, 65535)), "Complete(..., 1234, 65535)"); + } + + [Test] + public void Test_TuplePack_Serialize_IPAddress() + { + // IP Addresses are stored as a byte array (<01>..<00>), in network order (big-endian) + // They will take from 6 to 10 bytes, depending on the number of '.0' in them. + + Assert.That( + TuPack.EncodeKey(IPAddress.Loopback).ToHexaString(' '), + Is.EqualTo("01 7F 00 FF 00 FF 01 00") + ); + + Assert.That( + TuPack.EncodeKey(IPAddress.Any).ToHexaString(' '), + Is.EqualTo("01 00 FF 00 FF 00 FF 00 FF 00") + ); + + Assert.That( + TuPack.EncodeKey(IPAddress.Parse("1.2.3.4")).ToHexaString(' '), + Is.EqualTo("01 01 02 03 04 00") + ); + + } + + + [Test] + public void Test_TuplePack_Deserialize_IPAddress() + { + Assert.That(TuPack.DecodeKey(Slice.Unescape("<01><7F><00><00><01><00>")), Is.EqualTo(IPAddress.Parse("127.0.0.1"))); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<01><00><00><00><00><00>")), Is.EqualTo(IPAddress.Parse("0.0.0.0"))); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<01><01><02><03><04><00>")), Is.EqualTo(IPAddress.Parse("1.2.3.4"))); + + Assert.That(TuPack.DecodeKey(TuPack.EncodeKey("127.0.0.1")), Is.EqualTo(IPAddress.Loopback)); + + var ip = IPAddress.Parse("192.168.0.1"); + Assert.That(TuPack.DecodeKey(TuPack.EncodeKey(ip.ToString())), Is.EqualTo(ip)); + Assert.That(TuPack.DecodeKey(TuPack.EncodeKey(ip.GetAddressBytes())), Is.EqualTo(ip)); +#pragma warning disable 618 + Assert.That(TuPack.DecodeKey(TuPack.EncodeKey(ip.Address)), Is.EqualTo(ip)); +#pragma warning restore 618 + } + + [Test] + public void Test_TuplePack_NullableTypes() + { + // Nullable types will either be encoded as <14> for null, or their regular encoding if not null + + // serialize + + Assert.That(TuPack.EncodeKey(0), Is.EqualTo(Slice.Unescape("<14>"))); + Assert.That(TuPack.EncodeKey(123), Is.EqualTo(Slice.Unescape("<15>{"))); + Assert.That(TuPack.EncodeKey(null), Is.EqualTo(Slice.Unescape("<00>"))); + + Assert.That(TuPack.EncodeKey(0L), Is.EqualTo(Slice.Unescape("<14>"))); + Assert.That(TuPack.EncodeKey(123L), Is.EqualTo(Slice.Unescape("<15>{"))); + Assert.That(TuPack.EncodeKey(null), Is.EqualTo(Slice.Unescape("<00>"))); + + Assert.That(TuPack.EncodeKey(true), Is.EqualTo(Slice.Unescape("<15><01>"))); + Assert.That(TuPack.EncodeKey(false), Is.EqualTo(Slice.Unescape("<14>"))); + Assert.That(TuPack.EncodeKey(null), Is.EqualTo(Slice.Unescape("<00>")), "Maybe it was File Not Found?"); + + Assert.That(TuPack.EncodeKey(Guid.Empty), Is.EqualTo(Slice.Unescape("0<00><00><00><00><00><00><00><00><00><00><00><00><00><00><00><00>"))); + Assert.That(TuPack.EncodeKey(null), Is.EqualTo(Slice.Unescape("<00>"))); + + Assert.That(TuPack.EncodeKey(TimeSpan.Zero), Is.EqualTo(Slice.Unescape("!<80><00><00><00><00><00><00><00>"))); + Assert.That(TuPack.EncodeKey(null), Is.EqualTo(Slice.Unescape("<00>"))); + + // deserialize + + Assert.That(TuPack.DecodeKey(Slice.Unescape("<14>")), Is.EqualTo(0)); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<15>{")), Is.EqualTo(123)); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<00>")), Is.Null); + + Assert.That(TuPack.DecodeKey(Slice.Unescape("<14>")), Is.EqualTo(0L)); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<15>{")), Is.EqualTo(123L)); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<00>")), Is.Null); + + Assert.That(TuPack.DecodeKey(Slice.Unescape("<15><01>")), Is.True); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<14>")), Is.False); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<00>")), Is.Null); + + Assert.That(TuPack.DecodeKey(Slice.Unescape("0<00><00><00><00><00><00><00><00><00><00><00><00><00><00><00><00>")), Is.EqualTo(Guid.Empty)); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<00>")), Is.Null); + + Assert.That(TuPack.DecodeKey(Slice.Unescape("<14>")), Is.EqualTo(TimeSpan.Zero)); + Assert.That(TuPack.DecodeKey(Slice.Unescape("<00>")), Is.Null); + + } + + [Test] + public void Test_TuplePack_Serialize_Embedded_Tuples() + { + Action verify = (t, expected) => + { + var key = TuPack.Pack(t); + Assert.That(key.ToHexaString(' '), Is.EqualTo(expected)); + var t2 = TuPack.Unpack(key); + Assert.That(t2, Is.Not.Null); + Assert.That(t2.Count, Is.EqualTo(t.Count), "{0}", t2); + Assert.That(t2, Is.EqualTo(t)); + }; + + // Index composite key + ITuple value = STuple.Create(2014, 11, 6); // Indexing a date value (Y, M, D) + string docId = "Doc123"; + // key would be "(..., value, id)" + + verify( + STuple.Create(42, value, docId), + "15 2A 03 16 07 DE 15 0B 15 06 00 02 44 6F 63 31 32 33 00" + ); + verify( + STuple.Create(new object[] {42, value, docId}), + "15 2A 03 16 07 DE 15 0B 15 06 00 02 44 6F 63 31 32 33 00" + ); + verify( + STuple.Create(42).Append(value).Append(docId), + "15 2A 03 16 07 DE 15 0B 15 06 00 02 44 6F 63 31 32 33 00" + ); + verify( + STuple.Create(42).Append(value, docId), + "15 2A 03 16 07 DE 15 0B 15 06 00 02 44 6F 63 31 32 33 00" + ); + + // multiple depth + verify( + STuple.Create(1, STuple.Create(2, 3), STuple.Create(STuple.Create(4, 5, 6)), 7), + "15 01 03 15 02 15 03 00 03 03 15 04 15 05 15 06 00 00 15 07" + ); + + // corner cases + verify( + STuple.Create(STuple.Empty), + "03 00" // empty tumple should have header and footer + ); + verify( + STuple.Create(STuple.Empty, default(string)), + "03 00 00" // outer null should not be escaped + ); + verify( + STuple.Create(STuple.Create(default(string)), default(string)), + "03 00 FF 00 00" // inner null should be escaped, but not outer + ); + verify( + STuple.Create(STuple.Create(0x100, 0x10000, 0x1000000)), + "03 16 01 00 17 01 00 00 18 01 00 00 00 00" + ); + verify( + STuple.Create(default(string), STuple.Empty, default(string), STuple.Create(default(string)), default(string)), + "00 03 00 00 03 00 FF 00 00" + ); + + } + + [Test] + public void Test_TuplePack_Deserialize_Embedded_Tuples() + { + // ((42, (2014, 11, 6), "Hello", true), ) + var packed = TuPack.EncodeKey(STuple.Create(42, STuple.Create(2014, 11, 6), "Hello", true)); + Log($"t = {TuPack.Unpack(packed)}"); + Assert.That(packed[0], Is.EqualTo(TupleTypes.TupleStart), "Missing Embedded Tuple marker"); + { + var t = TuPack.DecodeKey(packed); + Assert.That(t, Is.Not.Null); + Assert.That(t.Count, Is.EqualTo(4)); + Assert.That(t.Get(0), Is.EqualTo(42)); + Assert.That(t.Get(1), Is.EqualTo(STuple.Create(2014, 11, 6))); + Assert.That(t.Get(2), Is.EqualTo("Hello")); + Assert.That(t.Get(3), Is.True); + } + { + var t = TuPack.DecodeKey>(packed); + Assert.That(t, Is.Not.Null); + Assert.That(t.Item1, Is.EqualTo(42)); + Assert.That(t.Item2, Is.EqualTo(STuple.Create(2014, 11, 6))); + Assert.That(t.Item3, Is.EqualTo("Hello")); + Assert.That(t.Item4, Is.True); + } + { + var t = TuPack.DecodeKey, string, bool>>(packed); + Assert.That(t, Is.Not.Null); + Assert.That(t.Item1, Is.EqualTo(42)); + Assert.That(t.Item2, Is.EqualTo(STuple.Create(2014, 11, 6))); + Assert.That(t.Item3, Is.EqualTo("Hello")); + Assert.That(t.Item4, Is.True); + } + + // (null,) + packed = TuPack.EncodeKey(default(string)); + Log($"t = {TuPack.Unpack(packed)}"); + { + var t = TuPack.DecodeKey(packed); + Assert.That(t, Is.Null); + } + { + var t = TuPack.DecodeKey, string, bool>>(packed); + Assert.That(t.Item1, Is.EqualTo(0)); + Assert.That(t.Item2, Is.EqualTo(default(STuple))); + Assert.That(t.Item3, Is.Null); + Assert.That(t.Item4, Is.False); + } + + //fallback if encoded as slice + packed = TuPack.EncodeKey(TuPack.EncodeKey(42, STuple.Create(2014, 11, 6), "Hello", true)); + Log($"t = {TuPack.Unpack(packed)}"); + Assert.That(packed[0], Is.EqualTo(TupleTypes.Bytes), "Missing Slice marker"); + { + var t = TuPack.DecodeKey, string, bool>>(packed); + Assert.That(t, Is.Not.Null); + Assert.That(t.Item1, Is.EqualTo(42)); + Assert.That(t.Item2, Is.EqualTo(STuple.Create(2014, 11, 6))); + Assert.That(t.Item3, Is.EqualTo("Hello")); + Assert.That(t.Item4, Is.True); + } + } + + [Test] + public void Test_TuplePack_SameBytes() + { + // two ways on packing the "same" tuple yield the same binary output + { + var expected = TuPack.EncodeKey("Hello World"); + Assert.That(TuPack.Pack(STuple.Create("Hello World")), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(((ITuple) STuple.Create("Hello World"))), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(STuple.Create(new object[] {"Hello World"})), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(STuple.Create("Hello World", 1234).Substring(0, 1)), Is.EqualTo(expected)); + } + { + var expected = TuPack.EncodeKey("Hello World", 1234); + Assert.That(TuPack.Pack(STuple.Create("Hello World", 1234)), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(((ITuple) STuple.Create("Hello World", 1234))), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(STuple.Create("Hello World").Append(1234)), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(((ITuple) STuple.Create("Hello World")).Append(1234)), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(STuple.Create(new object[] {"Hello World", 1234})), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(STuple.Create("Hello World", 1234, "Foo").Substring(0, 2)), Is.EqualTo(expected)); + } + { + var expected = TuPack.EncodeKey("Hello World", 1234, "Foo"); + Assert.That(TuPack.Pack(STuple.Create("Hello World", 1234, "Foo")), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(((ITuple) STuple.Create("Hello World", 1234, "Foo"))), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(STuple.Create("Hello World").Append(1234).Append("Foo")), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(((ITuple) STuple.Create("Hello World")).Append(1234).Append("Foo")), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(STuple.Create(new object[] {"Hello World", 1234, "Foo"})), Is.EqualTo(expected)); + Assert.That(TuPack.Pack(STuple.Create("Hello World", 1234, "Foo", "Bar").Substring(0, 3)), Is.EqualTo(expected)); + } + + // also, there should be no differences between int,long,uint,... if they have the same value + Assert.That(TuPack.Pack(STuple.Create("Hello", 123)), Is.EqualTo(TuPack.Pack(STuple.Create("Hello", 123L)))); + Assert.That(TuPack.Pack(STuple.Create("Hello", -123)), Is.EqualTo(TuPack.Pack(STuple.Create("Hello", -123L)))); + + // GUID / UUID128 should pack the same way + var g = Guid.NewGuid(); + Assert.That(TuPack.Pack(STuple.Create(g)), Is.EqualTo(TuPack.Pack(STuple.Create((Uuid128) g))), "GUID vs UUID128"); + } + + [Test] + public void Test_TuplePack_Numbers_Are_Sorted_Lexicographically() + { + // pick two numbers 'x' and 'y' at random, and check that the order of 'x' compared to 'y' is the same as 'pack(tuple(x))' compared to 'pack(tuple(y))' + + // ie: ensure that x.CompareTo(y) always has the same sign as Tuple(x).CompareTo(Tuple(y)) + + const int N = 1 * 1000 * 1000; + var rnd = new Random(); + var sw = Stopwatch.StartNew(); + + for (int i = 0; i < N; i++) + { + int x = rnd.Next() - 1073741824; + int y = x; + while (y == x) + { + y = rnd.Next() - 1073741824; + } + + var t1 = TuPack.EncodeKey(x); + var t2 = TuPack.EncodeKey(y); + + int dint = x.CompareTo(y); + int dtup = t1.CompareTo(t2); + + if (dtup == 0) Assert.Fail("Tuples for x={0} and y={1} should not have the same packed value", x, y); + + // compare signs + if (Math.Sign(dint) != Math.Sign(dtup)) + { + Assert.Fail("Tuples for x={0} and y={1} are not sorted properly ({2} / {3}): t(x)='{4}' and t(y)='{5}'", x, y, dint, dtup, t1.ToString(), t2.ToString()); + } + } + sw.Stop(); + Log("Checked {0:N0} tuples in {1:N1} ms", N, sw.ElapsedMilliseconds); + + } + + #endregion + + [Test] + public void Test_TuplePack_Pack() + { + Assert.That( + TuPack.Pack(STuple.Create()), + Is.EqualTo(Slice.Empty) + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world")).ToString(), + Is.EqualTo("<02>hello world<00>") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello", "world")).ToString(), + Is.EqualTo("<02>hello<00><02>world<00>") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world", 123)).ToString(), + Is.EqualTo("<02>hello world<00><15>{") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world", 1234, -1234)).ToString(), + Is.EqualTo("<02>hello world<00><16><04><12>-") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world", 123, false)).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14>") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world", 123, false, new byte[] {123, 1, 66, 0, 42})).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI)).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18>") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L)).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L, "こんにちは世界")).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>") + ); + Assert.That( + TuPack.Pack(STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L, "こんにちは世界", true)).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00><15><01>") + ); + Assert.That( + TuPack.Pack(STuple.Create(new object[] {"hello world", 123, false, new byte[] {123, 1, 66, 0, 42}})).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + Assert.That( + TuPack.Pack(STuple.FromArray(new object[] {"hello world", 123, false, new byte[] {123, 1, 66, 0, 42}}, 1, 2)).ToString(), + Is.EqualTo("<15>{<14>") + ); + Assert.That( + TuPack.Pack(STuple.FromEnumerable(new List {"hello world", 123, false, new byte[] {123, 1, 66, 0, 42}})).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + + } + + [Test] + public void Test_TuplePack_Pack_With_Prefix() + { + + Slice prefix = Slice.FromString("ABC"); + + Assert.That( + TuPack.Pack(prefix, STuple.Create()).ToString(), + Is.EqualTo("ABC") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world")).ToString(), + Is.EqualTo("ABC<02>hello world<00>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello", "world")).ToString(), + Is.EqualTo("ABC<02>hello<00><02>world<00>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world", 123)).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world", 1234, -1234)).ToString(), + Is.EqualTo("ABC<02>hello world<00><16><04><12>-") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world", 123, false)).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 })).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI)).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L)).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L, "こんにちは世界")).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L, "こんにちは世界", true)).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00><15><01>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.Create(new object[] { "hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 } })).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.FromArray(new object[] { "hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 } }, 1, 2)).ToString(), + Is.EqualTo("ABC<15>{<14>") + ); + Assert.That( + TuPack.Pack(prefix, STuple.FromEnumerable(new List { "hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 } })).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + + // Nil or Empty slice should be equivalent to no prefix + Assert.That( + TuPack.Pack(Slice.Nil, STuple.Create("hello world", 123)).ToString(), + Is.EqualTo("<02>hello world<00><15>{") + ); + Assert.That( + TuPack.Pack(Slice.Empty, STuple.Create("hello world", 123)).ToString(), + Is.EqualTo("<02>hello world<00><15>{") + ); + } + + [Test] + public void Test_TuplePack_PackTuples() + { + { + Slice[] slices; + var tuples = new ITuple[] + { + STuple.Create("hello"), + STuple.Create(123), + STuple.Create(false), + STuple.Create("world", 456, true) + }; + + // array version + slices = TuPack.PackTuples(tuples); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(tuples.Length)); + Assert.That(slices, Is.EqualTo(tuples.Select(t => TuPack.Pack(t)))); + + // IEnumerable version that is passed an array + slices = tuples.PackTuples(); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(tuples.Length)); + Assert.That(slices, Is.EqualTo(tuples.Select(t => TuPack.Pack(t)))); + + // IEnumerable version but with a "real" enumerable + slices = tuples.Select(t => t).PackTuples(); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(tuples.Length)); + Assert.That(slices, Is.EqualTo(tuples.Select(t => TuPack.Pack(t)))); + } + + //Optimized STuple<...> versions + + { + var packed = TuPack.PackTuples( + STuple.Create("Hello"), + STuple.Create(123, true), + STuple.Create(Math.PI, -1234L) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<02>Hello<00>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<15>{<15><01>")); + Assert.That(packed[2].ToString(), Is.EqualTo("!<09>!TD-<18><12>-")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + STuple.Create(123), + STuple.Create(456), + STuple.Create(789) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<15>{")); + Assert.That(packed[1].ToString(), Is.EqualTo("<16><01>")); + Assert.That(packed[2].ToString(), Is.EqualTo("<16><03><15>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + STuple.Create(123, true), + STuple.Create(456, false), + STuple.Create(789, false) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<15>{<15><01>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<16><01><14>")); + Assert.That(packed[2].ToString(), Is.EqualTo("<16><03><15><14>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + STuple.Create("foo", 123, true), + STuple.Create("bar", 456, false), + STuple.Create("baz", 789, false) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<02>foo<00><15>{<15><01>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<02>bar<00><16><01><14>")); + Assert.That(packed[2].ToString(), Is.EqualTo("<02>baz<00><16><03><15><14>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + STuple.Create("foo", 123, true, "yes"), + STuple.Create("bar", 456, false, "yes"), + STuple.Create("baz", 789, false, "no") + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<02>foo<00><15>{<15><01><02>yes<00>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<02>bar<00><16><01><14><02>yes<00>")); + Assert.That(packed[2].ToString(), Is.EqualTo("<02>baz<00><16><03><15><14><02>no<00>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + STuple.Create("foo", 123, true, "yes", 7), + STuple.Create("bar", 456, false, "yes", 42), + STuple.Create("baz", 789, false, "no", 9) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<02>foo<00><15>{<15><01><02>yes<00><15><07>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<02>bar<00><16><01><14><02>yes<00><15>*")); + Assert.That(packed[2].ToString(), Is.EqualTo("<02>baz<00><16><03><15><14><02>no<00><15><09>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + STuple.Create("foo", 123, true, "yes", 7, 1.5d), + STuple.Create("bar", 456, false, "yes", 42, 0.7d), + STuple.Create("baz", 789, false, "no", 9, 0.66d) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<02>foo<00><15>{<15><01><02>yes<00><15><07>!<00><00><00><00><00><00>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<02>bar<00><16><01><14><02>yes<00><15>*!ffffff")); + Assert.That(packed[2].ToString(), Is.EqualTo("<02>baz<00><16><03><15><14><02>no<00><15><09>!<1E>Q<85><1F>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + } + + [Test] + public void Test_TuplePack_PackTuples_With_Prefix() + { + Slice prefix = Slice.FromString("ABC"); + + { + Slice[] slices; + var tuples = new ITuple[] + { + STuple.Create("hello"), + STuple.Create(123), + STuple.Create(false), + STuple.Create("world", 456, true) + }; + + // array version + slices = TuPack.PackTuples(prefix, tuples); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(tuples.Length)); + Assert.That(slices, Is.EqualTo(tuples.Select(t => prefix + TuPack.Pack(t)))); + + // LINQ version + slices = TuPack.PackTuples(prefix, tuples.Select(x => x)); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(tuples.Length)); + Assert.That(slices, Is.EqualTo(tuples.Select(t => prefix + TuPack.Pack(t)))); + + } + + //Optimized STuple<...> versions + + { + var packed = TuPack.PackTuples( + prefix, + STuple.Create("Hello"), + STuple.Create(123, true), + STuple.Create(Math.PI, -1234L) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("ABC<02>Hello<00>")); + Assert.That(packed[1].ToString(), Is.EqualTo("ABC<15>{<15><01>")); + Assert.That(packed[2].ToString(), Is.EqualTo("ABC!<09>!TD-<18><12>-")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + prefix, + STuple.Create(123), + STuple.Create(456), + STuple.Create(789) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("ABC<15>{")); + Assert.That(packed[1].ToString(), Is.EqualTo("ABC<16><01>")); + Assert.That(packed[2].ToString(), Is.EqualTo("ABC<16><03><15>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + prefix, + STuple.Create(123, true), + STuple.Create(456, false), + STuple.Create(789, false) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("ABC<15>{<15><01>")); + Assert.That(packed[1].ToString(), Is.EqualTo("ABC<16><01><14>")); + Assert.That(packed[2].ToString(), Is.EqualTo("ABC<16><03><15><14>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.PackTuples( + prefix, + STuple.Create("foo", 123, true), + STuple.Create("bar", 456, false), + STuple.Create("baz", 789, false) + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("ABC<02>foo<00><15>{<15><01>")); + Assert.That(packed[1].ToString(), Is.EqualTo("ABC<02>bar<00><16><01><14>")); + Assert.That(packed[2].ToString(), Is.EqualTo("ABC<02>baz<00><16><03><15><14>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + } + + [Test] + public void Test_TuplePack_EncodeKey() + { + Assert.That( + TuPack.EncodeKey("hello world").ToString(), + Is.EqualTo("<02>hello world<00>") + ); + Assert.That( + TuPack.EncodeKey("hello", "world").ToString(), + Is.EqualTo("<02>hello<00><02>world<00>") + ); + Assert.That( + TuPack.EncodeKey("hello world", 123).ToString(), + Is.EqualTo("<02>hello world<00><15>{") + ); + Assert.That( + TuPack.EncodeKey("hello world", 1234, -1234).ToString(), + Is.EqualTo("<02>hello world<00><16><04><12>-") + ); + Assert.That( + TuPack.EncodeKey("hello world", 123, false).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14>") + ); + Assert.That( + TuPack.EncodeKey("hello world", 123, false, new byte[] {123, 1, 66, 0, 42}).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + Assert.That( + TuPack.EncodeKey("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18>") + ); + Assert.That( + TuPack.EncodeKey("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-") + ); + Assert.That( + TuPack.EncodeKey("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L, "こんにちは世界").ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>") + ); + Assert.That( + TuPack.EncodeKey("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L, "こんにちは世界", true).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00><15><01>") + ); + + } + + [Test] + public void Test_TuplePack_EncodeKey_With_Prefix() + { + Slice prefix = Slice.FromString("ABC"); + + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world").ToString(), + Is.EqualTo("ABC<02>hello world<00>") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello", "world").ToString(), + Is.EqualTo("ABC<02>hello<00><02>world<00>") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world", 123).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world", 1234, -1234).ToString(), + Is.EqualTo("ABC<02>hello world<00><16><04><12>-") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world", 123, false).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14>") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18>") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L, "こんにちは世界").ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>") + ); + Assert.That( + TuPack.EncodePrefixedKey(prefix, "hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L, "こんにちは世界", true).ToString(), + Is.EqualTo("ABC<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00><15><01>") + ); + + } + + [Test] + public void Test_TuplePack_EncodeKey_Boxed() + { + Slice slice; + + slice = TuPack.EncodeKey(default(object)); + Assert.That(slice.ToString(), Is.EqualTo("<00>")); + + slice = TuPack.EncodeKey(1); + Assert.That(slice.ToString(), Is.EqualTo("<15><01>")); + + slice = TuPack.EncodeKey(1L); + Assert.That(slice.ToString(), Is.EqualTo("<15><01>")); + + slice = TuPack.EncodeKey(1U); + Assert.That(slice.ToString(), Is.EqualTo("<15><01>")); + + slice = TuPack.EncodeKey(1UL); + Assert.That(slice.ToString(), Is.EqualTo("<15><01>")); + + slice = TuPack.EncodeKey(false); + Assert.That(slice.ToString(), Is.EqualTo("<14>")); + + slice = TuPack.EncodeKey(new byte[] {4, 5, 6}); + Assert.That(slice.ToString(), Is.EqualTo("<01><04><05><06><00>")); + + slice = TuPack.EncodeKey("hello"); + Assert.That(slice.ToString(), Is.EqualTo("<02>hello<00>")); + } + + [Test] + public void Test_TuplePack_EncodeKeys() + { + //Optimized STuple<...> versions + + { + var packed = TuPack.EncodeKeys( + "foo", + "bar", + "baz" + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<02>foo<00>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<02>bar<00>")); + Assert.That(packed[2].ToString(), Is.EqualTo("<02>baz<00>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.EncodeKeys( + 123, + 456, + 789 + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<15>{")); + Assert.That(packed[1].ToString(), Is.EqualTo("<16><01>")); + Assert.That(packed[2].ToString(), Is.EqualTo("<16><03><15>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + + { + var packed = TuPack.EncodeKeys(Enumerable.Range(0, 3)); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<14>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<15><01>")); + Assert.That(packed[2].ToString(), Is.EqualTo("<15><02>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.EncodeKeys(new[] {"Bonjour", "le", "Monde"}, (s) => s.Length); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("<15><07>")); + Assert.That(packed[1].ToString(), Is.EqualTo("<15><02>")); + Assert.That(packed[2].ToString(), Is.EqualTo("<15><05>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + } + + [Test] + public void Test_TuplePack_EncodeKeys_With_Prefix() + { + Slice prefix = Slice.FromString("ABC"); + + { + var packed = TuPack.EncodePrefixedKeys( + prefix, + "foo", + "bar", + "baz" + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("ABC<02>foo<00>")); + Assert.That(packed[1].ToString(), Is.EqualTo("ABC<02>bar<00>")); + Assert.That(packed[2].ToString(), Is.EqualTo("ABC<02>baz<00>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.EncodePrefixedKeys( + prefix, + 123, + 456, + 789 + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("ABC<15>{")); + Assert.That(packed[1].ToString(), Is.EqualTo("ABC<16><01>")); + Assert.That(packed[2].ToString(), Is.EqualTo("ABC<16><03><15>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + { + var packed = TuPack.EncodePrefixedKeys( + prefix, + new[] { "Bonjour", "le", "Monde" }, + (s) => s.Length + ); + Assert.That(packed, Is.Not.Null.And.Length.EqualTo(3)); + Assert.That(packed[0].ToString(), Is.EqualTo("ABC<15><07>")); + Assert.That(packed[1].ToString(), Is.EqualTo("ABC<15><02>")); + Assert.That(packed[2].ToString(), Is.EqualTo("ABC<15><05>")); + Assert.That(packed[1].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + Assert.That(packed[2].Array, Is.SameAs(packed[0].Array), "Should share same bufer"); + } + + } + + [Test] + public void Test_TuplePack_SerializersOfT() + { + Slice prefix = Slice.FromString("ABC"); + { + var serializer = TupleSerializer.Default; + var t = STuple.Create(123); + var tw = new TupleWriter(); + tw.Output.WriteBytes(prefix); + serializer.PackTo(ref tw, in t); + Assert.That(tw.ToSlice().ToString(), Is.EqualTo("ABC<15>{")); + } + { + var serializer = TupleSerializer.Default; + var t = STuple.Create("foo"); + var tw = new TupleWriter(); + tw.Output.WriteBytes(prefix); + serializer.PackTo(ref tw, in t); + Assert.That(tw.ToSlice().ToString(), Is.EqualTo("ABC<02>foo<00>")); + } + + { + var serializer = TupleSerializer.Default; + var t = STuple.Create("foo", 123); + var tw = new TupleWriter(); + tw.Output.WriteBytes(prefix); + serializer.PackTo(ref tw, in t); + Assert.That(tw.ToSlice().ToString(), Is.EqualTo("ABC<02>foo<00><15>{")); + } + + { + var serializer = TupleSerializer.Default; + var t = STuple.Create("foo", false, 123); + var tw = new TupleWriter(); + tw.Output.WriteBytes(prefix); + serializer.PackTo(ref tw, in t); + Assert.That(tw.ToSlice().ToString(), Is.EqualTo("ABC<02>foo<00><14><15>{")); + } + + { + var serializer = TupleSerializer.Default; + var t = STuple.Create("foo", false, 123, -1L); + var tw = new TupleWriter(); + tw.Output.WriteBytes(prefix); + serializer.PackTo(ref tw, in t); + Assert.That(tw.ToSlice().ToString(), Is.EqualTo("ABC<02>foo<00><14><15>{<13>")); + } + + { + var serializer = TupleSerializer.Default; + var t = STuple.Create("foo", false, 123, -1L, "narf"); + var tw = new TupleWriter(); + tw.Output.WriteBytes(prefix); + serializer.PackTo(ref tw, in t); + Assert.That(tw.ToSlice().ToString(), Is.EqualTo("ABC<02>foo<00><14><15>{<13><02>narf<00>")); + } + + { + var serializer = TupleSerializer.Default; + var t = STuple.Create("foo", false, 123, -1L, "narf", Math.PI); + var tw = new TupleWriter(); + tw.Output.WriteBytes(prefix); + serializer.PackTo(ref tw, in t); + Assert.That(tw.ToSlice().ToString(), Is.EqualTo("ABC<02>foo<00><14><15>{<13><02>narf<00>!<09>!TD-<18>")); + } + + } + [Test] + public void Test_TuplePack_Unpack() + { + + var packed = TuPack.EncodeKey("hello world"); + Log(packed); + + var tuple = TuPack.Unpack(packed); + Assert.That(tuple, Is.Not.Null); + Log(tuple); + Assert.That(tuple.Count, Is.EqualTo(1)); + Assert.That(tuple.Get(0), Is.EqualTo("hello world")); + + packed = TuPack.EncodeKey("hello world", 123); + Log(packed); + + tuple = TuPack.Unpack(packed); + Assert.That(tuple, Is.Not.Null); + Log(tuple); + Assert.That(tuple.Count, Is.EqualTo(2)); + Assert.That(tuple.Get(0), Is.EqualTo("hello world")); + Assert.That(tuple.Get(1), Is.EqualTo(123)); + + packed = TuPack.EncodeKey(1, 256, 257, 65536, int.MaxValue, long.MaxValue); + Log(packed); + + tuple = TuPack.Unpack(packed); + Assert.That(tuple, Is.Not.Null); + Assert.That(tuple.Count, Is.EqualTo(6)); + Assert.That(tuple.Get(0), Is.EqualTo(1)); + Assert.That(tuple.Get(1), Is.EqualTo(256)); + Assert.That(tuple.Get(2), Is.EqualTo(257), ((SlicedTuple) tuple).GetSlice(2).ToString()); + Assert.That(tuple.Get(3), Is.EqualTo(65536)); + Assert.That(tuple.Get(4), Is.EqualTo(int.MaxValue)); + Assert.That(tuple.Get(5), Is.EqualTo(long.MaxValue)); + + packed = TuPack.EncodeKey(-1, -256, -257, -65536, int.MinValue, long.MinValue); + Log(packed); + + tuple = TuPack.Unpack(packed); + Assert.That(tuple, Is.Not.Null); + Assert.That(tuple, Is.InstanceOf()); + Log(tuple); + Assert.That(tuple.Count, Is.EqualTo(6)); + Assert.That(tuple.Get(0), Is.EqualTo(-1)); + Assert.That(tuple.Get(1), Is.EqualTo(-256)); + Assert.That(tuple.Get(2), Is.EqualTo(-257), "Slice is " + ((SlicedTuple) tuple).GetSlice(2).ToString()); + Assert.That(tuple.Get(3), Is.EqualTo(-65536)); + Assert.That(tuple.Get(4), Is.EqualTo(int.MinValue)); + Assert.That(tuple.Get(5), Is.EqualTo(long.MinValue)); + } + + [Test] + public void Test_TuplePack_DecodeKey() + { + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<02>hello world<00>")), + Is.EqualTo("hello world") + ); + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<15>{")), + Is.EqualTo(123) + ); + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<02>hello<00><02>world<00>")), + Is.EqualTo(STuple.Create("hello", "world")) + ); + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<02>hello world<00><15>{")), + Is.EqualTo(STuple.Create("hello world", 123)) + ); + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<02>hello world<00><16><04><12>-")), + Is.EqualTo(STuple.Create("hello world", 1234, -1234L)) + ); + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<02>hello world<00><15>{<14>")), + Is.EqualTo(STuple.Create("hello world", 123, false)) + ); + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>")), + Is.EqualTo(STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }.AsSlice())) + ); + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18>")), + Is.EqualTo(STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }.AsSlice(), Math.PI)) + ); + Assert.That( + TuPack.DecodeKey(Slice.Unescape("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-")), + Is.EqualTo(STuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }.AsSlice(), Math.PI, -1234L)) + ); + //TODO: if/when we have tuples with 7 or 8 items... + //Assert.That( + // TuPack.DecodeKey(Slice.Unescape("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>")), + // Is.EqualTo(STuple.Create("hello world", 123, false, Slice.Create(new byte[] { 123, 1, 66, 0, 42 }), Math.PI, -1234L, "こんにちは世界")) + //); + //Assert.That( + // TuPack.DecodeKey(Slice.Unescape("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-<02><81><93><82><93><81><81><81><96><95><8C><00><15><01>")), + // Is.EqualTo(STuple.Create("hello world", 123, false, Slice.Create(new byte[] { 123, 1, 66, 0, 42 }), Math.PI, -1234L, "こんにちは世界", true)) + //); + } + + [Test] + public void Test_TuplePack_Serialize_ITupleFormattable() + { + // types that implement ITupleFormattable should be packed by calling ToTuple() and then packing the returned tuple + + Slice packed; + + packed = TuplePacker.Serialize(new Thing {Foo = 123, Bar = "hello"}); + Assert.That(packed.ToString(), Is.EqualTo("<03><15>{<02>hello<00><00>")); + + packed = TuplePacker.Serialize(new Thing()); + Assert.That(packed.ToString(), Is.EqualTo("<03><14><00><00>")); + + packed = TuplePacker.Serialize(default(Thing)); + Assert.That(packed.ToString(), Is.EqualTo("<00>")); + + } + + [Test] + public void Test_TuplePack_Deserialize_ITupleFormattable() + { + Slice slice; + Thing thing; + + slice = Slice.Unescape("<03><16><01><02>world<00><00>"); + thing = TuplePackers.DeserializeFormattable(slice); + Assert.That(thing, Is.Not.Null); + Assert.That(thing.Foo, Is.EqualTo(456)); + Assert.That(thing.Bar, Is.EqualTo("world")); + + slice = Slice.Unescape("<03><14><00><00>"); + thing = TuplePackers.DeserializeFormattable(slice); + Assert.That(thing, Is.Not.Null); + Assert.That(thing.Foo, Is.EqualTo(0)); + Assert.That(thing.Bar, Is.EqualTo(null)); + + slice = Slice.Unescape("<00>"); + thing = TuplePackers.DeserializeFormattable(slice); + Assert.That(thing, Is.Null); + } + + [Test] + public void Test_TuplePack_EncodeKeys_Of_T() + { + Slice[] slices; + + #region PackRange(Tuple, ...) + + var tuple = STuple.Create("hello"); + int[] items = new int[] {1, 2, 3, 123, -1, int.MaxValue}; + + // array version + slices = TuPack.EncodePrefixedKeys(tuple, items); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(items.Length)); + Assert.That(slices, Is.EqualTo(items.Select(x => TuPack.Pack(tuple.Append(x))))); + + // IEnumerable version that is passed an array + slices = TuPack.EncodePrefixedKeys(tuple, (IEnumerable) items); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(items.Length)); + Assert.That(slices, Is.EqualTo(items.Select(x => TuPack.Pack(tuple.Append(x))))); + + // IEnumerable version but with a "real" enumerable + slices = TuPack.EncodePrefixedKeys(tuple, items.Select(t => t)); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(items.Length)); + Assert.That(slices, Is.EqualTo(items.Select(x => TuPack.Pack(tuple.Append(x))))); + + #endregion + + #region PackRange(Slice, ...) + + string[] words = {"hello", "world", "très bien", "断トツ", "abc\0def", null, String.Empty}; + + var merged = TuPack.EncodePrefixedKeys(Slice.FromByte(42), words); + Assert.That(merged, Is.Not.Null); + Assert.That(merged.Length, Is.EqualTo(words.Length)); + + for (int i = 0; i < words.Length; i++) + { + var expected = Slice.FromByte(42) + TuPack.EncodeKey(words[i]); + Assert.That(merged[i], Is.EqualTo(expected)); + + Assert.That(merged[i].Array, Is.SameAs(merged[0].Array), "All slices should be stored in the same buffer"); + if (i > 0) Assert.That(merged[i].Offset, Is.EqualTo(merged[i - 1].Offset + merged[i - 1].Count), "All slices should be contiguous"); + } + + // corner cases + // ReSharper disable AssignNullToNotNullAttribute + Assert.That( + () => TuPack.EncodePrefixedKeys(Slice.Empty, default(int[])), + Throws.InstanceOf().With.Property("ParamName").EqualTo("keys")); + Assert.That( + () => TuPack.EncodePrefixedKeys(Slice.Empty, default(IEnumerable)), + Throws.InstanceOf().With.Property("ParamName").EqualTo("keys")); + // ReSharper restore AssignNullToNotNullAttribute + + #endregion + } + + [Test] + public void Test_TuplePack_EncodeKeys_Boxed() + { + Slice[] slices; + var tuple = STuple.Create("hello"); + object[] items = {"world", 123, false, Guid.NewGuid(), long.MinValue}; + + // array version + slices = TuPack.EncodePrefixedKeys(tuple, items); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(items.Length)); + Assert.That(slices, Is.EqualTo(items.Select(x => TuPack.Pack(tuple.Append(x))))); + + // IEnumerable version that is passed an array + slices = TuPack.EncodePrefixedKeys(tuple, (IEnumerable) items); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(items.Length)); + Assert.That(slices, Is.EqualTo(items.Select(x => TuPack.Pack(tuple.Append(x))))); + + // IEnumerable version but with a "real" enumerable + slices = TuPack.EncodePrefixedKeys(tuple, items.Select(t => t)); + Assert.That(slices, Is.Not.Null); + Assert.That(slices.Length, Is.EqualTo(items.Length)); + Assert.That(slices, Is.EqualTo(items.Select(x => TuPack.Pack(tuple.Append(x))))); + } + + [Test] + public void Test_TuplePack_Unpack_First_And_Last() + { + // should only work with tuples having at least one element + + Slice packed; + + packed = TuPack.EncodeKey(1); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo("1")); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo("1")); + + packed = TuPack.EncodeKey(1, 2); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo("1")); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo(2)); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo("2")); + + packed = TuPack.EncodeKey(1, 2, 3); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo("1")); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo(3)); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo("3")); + + packed = TuPack.EncodeKey(1, 2, 3, 4); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo("1")); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo(4)); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo("4")); + + packed = TuPack.EncodeKey(1, 2, 3, 4, 5); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo("1")); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo(5)); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo("5")); + + packed = TuPack.EncodeKey(1, 2, 3, 4, 5, 6); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo("1")); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo(6)); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo("6")); + + packed = TuPack.EncodeKey(1, 2, 3, 4, 5, 6, 7); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo("1")); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo(7)); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo("7")); + + packed = TuPack.EncodeKey(1, 2, 3, 4, 5, 6, 7, 8); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeFirst(packed), Is.EqualTo("1")); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo(8)); + Assert.That(TuPack.DecodeLast(packed), Is.EqualTo("8")); + + Assert.That(() => TuPack.DecodeFirst(Slice.Nil), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeFirst(Slice.Empty), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeLast(Slice.Nil), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeLast(Slice.Empty), Throws.InstanceOf()); + + } + + [Test] + public void Test_TuplePack_UnpackSingle() + { + // should only work with tuples having exactly one element + + Slice packed; + + packed = TuPack.EncodeKey(1); + Assert.That(TuPack.DecodeKey(packed), Is.EqualTo(1)); + Assert.That(TuPack.DecodeKey(packed), Is.EqualTo("1")); + + packed = TuPack.EncodeKey("Hello\0World"); + Assert.That(TuPack.DecodeKey(packed), Is.EqualTo("Hello\0World")); + + Assert.That(() => TuPack.DecodeKey(Slice.Nil), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeKey(Slice.Empty), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeKey(TuPack.EncodeKey(1, 2)), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeKey(TuPack.EncodeKey(1, 2, 3)), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeKey(TuPack.EncodeKey(1, 2, 3, 4)), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeKey(TuPack.EncodeKey(1, 2, 3, 4, 5)), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeKey(TuPack.EncodeKey(1, 2, 3, 4, 5, 6)), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeKey(TuPack.EncodeKey(1, 2, 3, 4, 5, 6, 7)), Throws.InstanceOf()); + Assert.That(() => TuPack.DecodeKey(TuPack.EncodeKey(1, 2, 3, 4, 5, 6, 7, 8)), Throws.InstanceOf()); + + } + + [Test] + public void Test_TuplePack_ToRange() + { + KeyRange range; + + // ToRange() should add 0x00 and 0xFF to the packed representations of the tuples + // note: we cannot increment the key to get the End key, because it conflicts with the Tuple Binary Encoding itself + + // Slice + range = TuPack.ToRange(Slice.FromString("ABC")); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<00>"), "Begin key should be suffixed by 0x00"); + Assert.That(range.End.ToString(), Is.EqualTo("ABC"), "End key should be suffixed by 0xFF"); + + // Tuples + + range = TuPack.ToRange(STuple.Create("Hello")); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00>")); + + range = TuPack.ToRange(STuple.Create("Hello", 123)); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{")); + + range = TuPack.ToRange(STuple.Create("Hello", 123, true)); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01>")); + + range = TuPack.ToRange(STuple.Create("Hello", 123, true, -1234L)); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-")); + + range = TuPack.ToRange(STuple.Create("Hello", 123, true, -1234L, "こんにちは世界")); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>")); + + range = TuPack.ToRange(STuple.Create("Hello", 123, true, -1234L, "こんにちは世界", Math.PI)); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18>")); + + range = TuPack.ToRange(STuple.Create("Hello", 123, true, -1234L, "こんにちは世界", Math.PI, false)); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><14><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><14>")); + + range = TuPack.ToRange(STuple.Create("Hello", 123, true, -1234L, "こんにちは世界", Math.PI, false, "TheEnd")); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><14><02>TheEnd<00><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><14><02>TheEnd<00>")); + } + + [Test] + public void Test_TuplePack_ToRange_With_Prefix() + { + Slice prefix = Slice.FromString("ABC"); + KeyRange range; + + range = TuPack.ToRange(prefix, STuple.Create("Hello")); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<02>Hello<00><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("ABC<02>Hello<00>")); + + range = TuPack.ToRange(prefix, STuple.Create("Hello", 123)); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<00>")); + Assert.That(range.End.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{")); + + range = TuPack.ToRange(prefix, STuple.Create("Hello", 123, true)); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01>")); + + range = TuPack.ToRange(prefix, STuple.Create("Hello", 123, true, -1234L)); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<00>")); + Assert.That(range.End.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-")); + + range = TuPack.ToRange(prefix, STuple.Create("Hello", 123, true, -1234L, "こんにちは世界")); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>")); + + range = TuPack.ToRange(prefix, STuple.Create("Hello", 123, true, -1234L, "こんにちは世界", Math.PI)); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18>")); + + range = TuPack.ToRange(prefix, STuple.Create("Hello", 123, true, -1234L, "こんにちは世界", Math.PI, false)); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><14><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><14>")); + + range = TuPack.ToRange(prefix, STuple.Create("Hello", 123, true, -1234L, "こんにちは世界", Math.PI, false, "TheEnd")); + Assert.That(range.Begin.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><14><02>TheEnd<00><00>")); + Assert.That(range.End.ToString(), Is.EqualTo("ABC<02>Hello<00><15>{<15><01><12>-<02><81><93><82><93><81><81><81><96><95><8C><00>!<09>!TD-<18><14><02>TheEnd<00>")); + + // Nil or Empty prefix should not add anything + + range = TuPack.ToRange(Slice.Nil, STuple.Create("Hello", 123)); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{")); + + range = TuPack.ToRange(Slice.Empty, STuple.Create("Hello", 123)); + Assert.That(range.Begin.ToString(), Is.EqualTo("<02>Hello<00><15>{<00>")); + Assert.That(range.End.ToString(), Is.EqualTo("<02>Hello<00><15>{")); + + } + + private class Thing : ITupleFormattable + { + public int Foo { get; set; } + public string Bar { get; set; } + + ITuple ITupleFormattable.ToTuple() + { + return STuple.Create(this.Foo, this.Bar); + } + + void ITupleFormattable.FromTuple(ITuple tuple) + { + this.Foo = tuple.Get(0); + this.Bar = tuple.Get(1); + } + } + +#if ENABLE_VALUETUPLES + + [Test] + public void Test_TuPack_ValueTuple_Pack() + { + Assert.That( + TuPack.Pack(ValueTuple.Create("hello world")).ToString(), + Is.EqualTo("<02>hello world<00>") + ); + Assert.That( + TuPack.Pack(ValueTuple.Create("hello world", 123)).ToString(), + Is.EqualTo("<02>hello world<00><15>{") + ); + Assert.That( + TuPack.Pack(ValueTuple.Create("hello world", 123, false)).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14>") + ); + Assert.That( + TuPack.Pack(ValueTuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 })).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>") + ); + Assert.That( + TuPack.Pack(ValueTuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI)).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18>") + ); + Assert.That( + TuPack.Pack(ValueTuple.Create("hello world", 123, false, new byte[] { 123, 1, 66, 0, 42 }, Math.PI, -1234L)).ToString(), + Is.EqualTo("<02>hello world<00><15>{<14><01>{<01>B<00>*<00>!<09>!TD-<18><12>-") + ); + + { // Embedded Tuples + var packed = TuPack.Pack(ValueTuple.Create("hello", ValueTuple.Create(123, false), "world")); + Assert.That( + packed.ToString(), + Is.EqualTo("<02>hello<00><03><15>{<14><00><02>world<00>") + ); + var t = TuPack.DecodeKey, string>(packed); + Assert.That(t.Item1, Is.EqualTo("hello")); + Assert.That(t.Item2.Item1, Is.EqualTo(123)); + Assert.That(t.Item2.Item2, Is.False); + Assert.That(t.Item3, Is.EqualTo("world")); + } + + } + +#endif + + } + + +} From 5051b06ed2374be197de292e7e125d5270b96fa2 Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Thu, 26 Apr 2018 20:31:01 +0200 Subject: [PATCH 5/9] Fix unpacking of VersionStamps in tuple (when using the boxed API) --- .../Layers/Tuples/Encoding/TuplePackers.cs | 2 ++ .../Layers/Tuples/Encoding/TupleParser.cs | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs b/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs index e3f5c5e8a..68f28989c 100644 --- a/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs +++ b/FoundationDB.Client/Layers/Tuples/Encoding/TuplePackers.cs @@ -846,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); } } diff --git a/FoundationDB.Client/Layers/Tuples/Encoding/TupleParser.cs b/FoundationDB.Client/Layers/Tuples/Encoding/TupleParser.cs index 66957f676..a2e24b5f6 100644 --- a/FoundationDB.Client/Layers/Tuples/Encoding/TupleParser.cs +++ b/FoundationDB.Client/Layers/Tuples/Encoding/TupleParser.cs @@ -877,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); @@ -888,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); @@ -900,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); @@ -911,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); @@ -920,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); @@ -950,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); @@ -981,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); @@ -994,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); @@ -1008,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); @@ -1021,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); @@ -1033,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... From 6ae022ac071ee6bc7dcd2ea7b1299fe337b533ce Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Thu, 26 Apr 2018 20:32:26 +0200 Subject: [PATCH 6/9] Add the logic on transactions to generate custom placeholder stamps (using a random token) - Each transaction generate a random token (and on each retry). - tr.CreateVersionStamp() can be used to get a stamp specific to this transaction --- FoundationDB.Client/FdbTransaction.cs | 70 +++++++++ .../Filters/FdbTransactionFilter.cs | 12 ++ FoundationDB.Client/IFdbTransaction.cs | 18 +++ FoundationDB.Client/VersionStamp.cs | 13 ++ FoundationDB.Tests/TransactionFacts.cs | 134 ++++++++++++++++-- 5 files changed, 232 insertions(+), 15 deletions(-) diff --git a/FoundationDB.Client/FdbTransaction.cs b/FoundationDB.Client/FdbTransaction.cs index ac9e45120..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... @@ -297,6 +300,68 @@ public Task GetVersionStampAsync() 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... @@ -785,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/Filters/FdbTransactionFilter.cs b/FoundationDB.Client/Filters/FdbTransactionFilter.cs index 78d414e68..bfcf54712 100644 --- a/FoundationDB.Client/Filters/FdbTransactionFilter.cs +++ b/FoundationDB.Client/Filters/FdbTransactionFilter.cs @@ -257,6 +257,18 @@ public virtual Task GetVersionStampAsync() 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/IFdbTransaction.cs b/FoundationDB.Client/IFdbTransaction.cs index 39015be4c..98f684dd9 100644 --- a/FoundationDB.Client/IFdbTransaction.cs +++ b/FoundationDB.Client/IFdbTransaction.cs @@ -120,6 +120,24 @@ public interface IFdbTransaction : IFdbReadOnlyTransaction /// 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/VersionStamp.cs b/FoundationDB.Client/VersionStamp.cs index e9faf5bb1..e16a4a554 100644 --- a/FoundationDB.Client/VersionStamp.cs +++ b/FoundationDB.Client/VersionStamp.cs @@ -115,6 +115,19 @@ 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)] diff --git a/FoundationDB.Tests/TransactionFacts.cs b/FoundationDB.Tests/TransactionFacts.cs index 2b8536ec2..f536d429b 100644 --- a/FoundationDB.Tests/TransactionFacts.cs +++ b/FoundationDB.Tests/TransactionFacts.cs @@ -37,6 +37,7 @@ namespace FoundationDB.Client.Tests using System.Threading; using System.Threading.Tasks; using Doxense.Collections.Tuples; + using Doxense.Memory; [TestFixture] public class TransactionFacts : FdbTest @@ -1993,37 +1994,140 @@ public async Task Test_Simple_Read_Transaction() [Test] public async Task Test_VersionStamp_Operations() { - Fdb.Start(510); + // Veryify that we can set versionstamped keys inside a transaction + using (var db = await OpenTestDatabaseAsync()) { - Log("API Version: " + Fdb.ApiVersion); - 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)) { - Slice HACKHACK_Packify(VersionStamp stamp) + //TODO: HACKACK: until we add support to he transaction itself, we have to 'patch' the versionstamps by hand! + Slice HACKHACK_Stampify(Slice key) { - var x = location.Keys.Encode(stamp); - x = x.Concat(Slice.FromFixed16((short) (location.GetPrefix().Count + 1))); - Log(x.ToHexaString(' ') + " | " + location.Keys.Dump(x)); - return x; + // find the stamp byte sequence in the key + var x = tr.CreateVersionStamp().ToSlice(); + int p = key.IndexOf(x); + Assert.That(p, Is.GreaterThan(0), "Stamp pattern was not found in the key!"); + + // append the offset at the end + var writer = new SliceWriter(key.Count + 2); + writer.WriteBytes(key); + writer.WriteFixed16((ushort) p); //note: the offset is Little Endian! + var y = writer.ToSlice(); + + //Log(y.ToHexaString(' ') + " | " + location.Keys.Dump(y)); + return y; } - tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete()), Slice.FromString("Hello, World!")); - tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(0)), Slice.FromString("Zero")); - tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(1)), Slice.FromString("One")); - tr.SetVersionStampedKey(HACKHACK_Packify(VersionStamp.Incomplete(2)), Slice.FromString("Two")); - + var vs = tr.CreateVersionStamp(); + Log($"> placeholder stamp: {vs} with token '{vs.ToSlice():X}'"); + Assert.That(vs.IsIncomplete, Is.True, "Placeholder token should be incomplete"); + Assert.That(vs.HasUserVersion, Is.False); + Assert.That(vs.UserVersion, Is.Zero); + Assert.That(vs.TransactionVersion >> 56, Is.EqualTo(0xFF), "Highest 8 bit of Transaction Version should be set to 1"); + Assert.That(vs.TransactionOrder >> 12, Is.EqualTo(0xF), "Hight 4 bits of Transaction Order should be set to 1"); + + var vs0 = tr.CreateVersionStamp(0); + Assert.That(vs0.IsIncomplete, Is.True, "Placeholder token should be incomplete"); + Assert.That(vs0.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value "); + Assert.That(vs0.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value "); + Assert.That(vs0.HasUserVersion, Is.True); + Assert.That(vs0.UserVersion, Is.EqualTo(0)); + + var vs1 = tr.CreateVersionStamp(1); + Assert.That(vs1.IsIncomplete, Is.True, "Placeholder token should be incomplete"); + Assert.That(vs1.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value "); + Assert.That(vs1.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value "); + Assert.That(vs1.HasUserVersion, Is.True); + Assert.That(vs1.UserVersion, Is.EqualTo(1)); + + var vs42 = tr.CreateVersionStamp(42); + Assert.That(vs42.IsIncomplete, Is.True, "Placeholder token should be incomplete"); + Assert.That(vs42.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value "); + Assert.That(vs42.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value "); + Assert.That(vs42.HasUserVersion, Is.True); + Assert.That(vs42.UserVersion, Is.EqualTo(42)); + + // a single key using the 80-bit stamp + tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("foo", vs, 123)), Slice.FromString("Hello, World!")); + + // simulate a batch of 3 keys, using 96-bits stamps + tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs0)), Slice.FromString("Zero")); + tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs1)), Slice.FromString("One")); + tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs42)), Slice.FromString("FortyTwo")); + + // need to be request BEFORE the commit var vsTask = tr.GetVersionStampAsync(); await tr.CommitAsync(); Log(tr.GetCommittedVersion()); - var vs = await vsTask; - Log(vs); + // need to be resolved AFTER the commit + vsActual = await vsTask; + Log($"> actual stamp: {vsActual} with token '{vsActual.ToSlice():X}'"); + } + + 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)); + } } await DumpSubspace(db, location); From cecf0e4930d2c8b90f71b577c2562006388d6ce0 Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Fri, 27 Apr 2018 14:31:44 +0200 Subject: [PATCH 7/9] SetVersionStampedKey will automatically detect the location of the versionstamp present in the key, and add the 16-bit offset suffix. --- .../FdbTransactionExtensions.cs | 50 +++++- FoundationDB.Client/VersionStamp.cs | 3 + FoundationDB.Tests/TransactionFacts.cs | 142 ++++++++++++------ 3 files changed, 143 insertions(+), 52 deletions(-) diff --git a/FoundationDB.Client/FdbTransactionExtensions.cs b/FoundationDB.Client/FdbTransactionExtensions.cs index d4f2e8992..b1e6cd2e8 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,19 +426,64 @@ 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; + } + //TODO: XML Comments! public static void SetVersionStampedKey([NotNull] this IFdbTransaction trans, Slice key, Slice value) { Contract.NotNull(trans, nameof(trans)); - trans.Atomic(key, value, FdbMutationType.VersionStampedKey); + //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); } - //TODO: XML Comments! + /// 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); } diff --git a/FoundationDB.Client/VersionStamp.cs b/FoundationDB.Client/VersionStamp.cs index e16a4a554..d1f3db3eb 100644 --- a/FoundationDB.Client/VersionStamp.cs +++ b/FoundationDB.Client/VersionStamp.cs @@ -57,6 +57,9 @@ namespace FoundationDB.Client 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. diff --git a/FoundationDB.Tests/TransactionFacts.cs b/FoundationDB.Tests/TransactionFacts.cs index f536d429b..a4c1f5f15 100644 --- a/FoundationDB.Tests/TransactionFacts.cs +++ b/FoundationDB.Tests/TransactionFacts.cs @@ -36,8 +36,6 @@ namespace FoundationDB.Client.Tests using System.Text; using System.Threading; using System.Threading.Tasks; - using Doxense.Collections.Tuples; - using Doxense.Memory; [TestFixture] public class TransactionFacts : FdbTest @@ -1991,6 +1989,77 @@ 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() { @@ -2007,60 +2076,23 @@ public async Task Test_VersionStamp_Operations() Log("Inserting keys with version stamps:"); using (var tr = db.BeginTransaction(this.Cancellation)) { - //TODO: HACKACK: until we add support to he transaction itself, we have to 'patch' the versionstamps by hand! - Slice HACKHACK_Stampify(Slice key) - { - // find the stamp byte sequence in the key - var x = tr.CreateVersionStamp().ToSlice(); - int p = key.IndexOf(x); - Assert.That(p, Is.GreaterThan(0), "Stamp pattern was not found in the key!"); - - // append the offset at the end - var writer = new SliceWriter(key.Count + 2); - writer.WriteBytes(key); - writer.WriteFixed16((ushort) p); //note: the offset is Little Endian! - var y = writer.ToSlice(); - - //Log(y.ToHexaString(' ') + " | " + location.Keys.Dump(y)); - return y; - } + // should return a 80-bit incomplete stamp, using a random token var vs = tr.CreateVersionStamp(); Log($"> placeholder stamp: {vs} with token '{vs.ToSlice():X}'"); - Assert.That(vs.IsIncomplete, Is.True, "Placeholder token should be incomplete"); - Assert.That(vs.HasUserVersion, Is.False); - Assert.That(vs.UserVersion, Is.Zero); - Assert.That(vs.TransactionVersion >> 56, Is.EqualTo(0xFF), "Highest 8 bit of Transaction Version should be set to 1"); - Assert.That(vs.TransactionOrder >> 12, Is.EqualTo(0xF), "Hight 4 bits of Transaction Order should be set to 1"); - - var vs0 = tr.CreateVersionStamp(0); - Assert.That(vs0.IsIncomplete, Is.True, "Placeholder token should be incomplete"); - Assert.That(vs0.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value "); - Assert.That(vs0.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value "); - Assert.That(vs0.HasUserVersion, Is.True); - Assert.That(vs0.UserVersion, Is.EqualTo(0)); - - var vs1 = tr.CreateVersionStamp(1); - Assert.That(vs1.IsIncomplete, Is.True, "Placeholder token should be incomplete"); - Assert.That(vs1.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value "); - Assert.That(vs1.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value "); - Assert.That(vs1.HasUserVersion, Is.True); - Assert.That(vs1.UserVersion, Is.EqualTo(1)); - - var vs42 = tr.CreateVersionStamp(42); - Assert.That(vs42.IsIncomplete, Is.True, "Placeholder token should be incomplete"); - Assert.That(vs42.TransactionVersion, Is.EqualTo(vs.TransactionVersion), "All generated stamps by one transaction should share the random token value "); - Assert.That(vs42.TransactionOrder, Is.EqualTo(vs.TransactionOrder), "All generated stamps by one transaction should share the random token value "); - Assert.That(vs42.HasUserVersion, Is.True); - Assert.That(vs42.UserVersion, Is.EqualTo(42)); // a single key using the 80-bit stamp - tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("foo", vs, 123)), Slice.FromString("Hello, World!")); + tr.SetVersionStampedKey(location.Keys.Encode("foo", vs, 123), Slice.FromString("Hello, World!")); // simulate a batch of 3 keys, using 96-bits stamps - tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs0)), Slice.FromString("Zero")); - tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs1)), Slice.FromString("One")); - tr.SetVersionStampedKey(HACKHACK_Stampify(location.Keys.Encode("bar", vs42)), Slice.FromString("FortyTwo")); + 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(); @@ -2073,6 +2105,8 @@ Slice HACKHACK_Stampify(Slice key) Log($"> actual stamp: {vsActual} with token '{vsActual.ToSlice():X}'"); } + await DumpSubspace(db, location); + Log("Checking database content:"); using (var tr = db.BeginReadOnlyTransaction(this.Cancellation)) { @@ -2128,9 +2162,17 @@ Slice HACKHACK_Stampify(Slice key) 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"); + } } - await DumpSubspace(db, location); } } From da593a6c07852ab009b2793b0ba51bdd08dd2e33 Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Fri, 27 Apr 2018 14:40:53 +0200 Subject: [PATCH 8/9] Add overload SetVersionStampedKey(...) where the caller can manually specifiy the offset of the versionstamp --- .../FdbTransactionExtensions.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/FoundationDB.Client/FdbTransactionExtensions.cs b/FoundationDB.Client/FdbTransactionExtensions.cs index b1e6cd2e8..e59d39fc2 100644 --- a/FoundationDB.Client/FdbTransactionExtensions.cs +++ b/FoundationDB.Client/FdbTransactionExtensions.cs @@ -458,7 +458,10 @@ private static int GetVersionStampOffset(Slice buffer, Slice token, string argNa return p; } - //TODO: XML Comments! + /// 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)); @@ -474,6 +477,25 @@ public static void SetVersionStampedKey([NotNull] this IFdbTransaction trans, Sl 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. From 79194993594a5d1ac92b917c8cbdcf3b38c7bd5b Mon Sep 17 00:00:00 2001 From: Christophe Chevalier Date: Fri, 27 Apr 2018 14:46:25 +0200 Subject: [PATCH 9/9] Fix parsing of incomplete VersionStamps, using the highest bit to distinguish between cases --- FoundationDB.Client/Native/FdbNative.cs | 6 ++--- FoundationDB.Client/VersionStamp.cs | 35 +++++-------------------- FoundationDB.Tests/VersionStampFacts.cs | 2 +- 3 files changed, 9 insertions(+), 34 deletions(-) diff --git a/FoundationDB.Client/Native/FdbNative.cs b/FoundationDB.Client/Native/FdbNative.cs index 8af1fa1d5..1f865f658 100644 --- a/FoundationDB.Client/Native/FdbNative.cs +++ b/FoundationDB.Client/Native/FdbNative.cs @@ -848,10 +848,8 @@ public static FdbError FutureGetVersionStamp(FutureHandle future, out VersionSta stamp = default; return err; } - //note: we assume that this is a complete stamp read from the database. - //BUGBUG: if the code serialize an incomplete stamp into a tuple, and unpacks it (logging?) it MAY be changed into a complete one! - // => we could check for the 'all FF' signature, but this only works for default incomplete tokens, not custom incomplete tokens ! - VersionStamp.ReadUnsafe(ptr, 10, /*FLAGS_NONE*/0, out stamp); + + VersionStamp.ReadUnsafe(ptr, 10, out stamp); return err; } diff --git a/FoundationDB.Client/VersionStamp.cs b/FoundationDB.Client/VersionStamp.cs index d1f3db3eb..beec01694 100644 --- a/FoundationDB.Client/VersionStamp.cs +++ b/FoundationDB.Client/VersionStamp.cs @@ -52,6 +52,7 @@ namespace FoundationDB.Client 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 @@ -262,46 +263,22 @@ public static bool TryParse(Slice data, out VersionStamp vs) { fixed (byte* ptr = &data.DangerousGetPinnableReference()) { - ReadUnsafe(ptr, data.Count, FLAGS_NONE, out vs); + ReadUnsafe(ptr, data.Count, out vs); return true; } } } - /// Parse a VersionStamp from a sequence of 10 bytes - /// If the buffer length is not exactly 12 bytes - [Pure] - public static VersionStamp ParseIncomplete(Slice data) - { - return TryParseIncomplete(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 TryParseIncomplete(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, FLAGS_IS_INCOMPLETE, out vs); - return true; - } - } - } - - internal static unsafe void ReadUnsafe(byte* ptr, int len, ushort flags, out VersionStamp vs) + 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); - flags |= len == 12 ? FLAGS_HAS_VERSION : FLAGS_NONE; + 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); } diff --git a/FoundationDB.Tests/VersionStampFacts.cs b/FoundationDB.Tests/VersionStampFacts.cs index 6075d688d..2e190ecf4 100644 --- a/FoundationDB.Tests/VersionStampFacts.cs +++ b/FoundationDB.Tests/VersionStampFacts.cs @@ -109,7 +109,7 @@ public void Test_Incomplete_VersionStamp() Assert.That(vs.TransactionVersion, Is.EqualTo(ulong.MaxValue)); Assert.That(vs.TransactionOrder, Is.EqualTo(ushort.MaxValue)); Assert.That(vs.UserVersion, Is.EqualTo(123)); - Assert.That(vs.IsIncomplete, Is.False, "NOTE: reading stamps is only supposed to happen for stamps already in the database!"); + Assert.That(vs.IsIncomplete, Is.True); } {