Skip to content

Commit 8fb0bcf

Browse files
mamcxgefjonJasonAtClockworkjdetter
authored
Add UUID built-in convenience type to SpacetimeDB (#3538)
# Description of Changes Closes [#3290](#3290). Adds a new "special" type to SATS, `UUID`, which is represented as the product `{ __uuid__: u128 }`. Adds versions of this type to all of our various languages' module bindings libraries and client SDKs, and updates codegen to recognize it and output references to those named library types. Adds methods for creating new UUIDs according to the V4 (all random) and V7 (timestamp, monotonic counter and random) specifications. # API and ABI breaking changes We add a new type # Expected complexity level and risk 2 it impacts all over the code # Testing - [x] Extends the Rust and Unreal SDK tests, and the associated `module-test` modules in Rust, C# and TypeScript, with uses of UUIDs. - [x] Extends the C# SDK regression tests with uses of UUIDs. - [x] Extends the TypeScript test suite with tests with uses of UUIDs. --------- Signed-off-by: Mario Montoya <mamcx@elmalabarista.com> Co-authored-by: Phoebe Goldman <phoebe@clockworklabs.io> Co-authored-by: Jason Larabie <jason@clockworklabs.io> Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
1 parent 0386222 commit 8fb0bcf

136 files changed

Lines changed: 7756 additions & 199 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ unicode-ident = "1.0.12"
307307
unicode-normalization = "0.1.23"
308308
url = "2.3.1"
309309
urlencoding = "2.1.2"
310-
uuid = { version = "1.18.1", features = ["v4"] }
310+
uuid = { version = "1.18.1", default-features = false }
311311
v8 = "140.2"
312312
walkdir = "2.2.5"
313313
wasmbin = "0.6"

crates/bindings-csharp/BSATN.Runtime.Tests/Tests.cs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace SpacetimeDB;
22

33
using System.Diagnostics.CodeAnalysis;
4+
using System.Security.Cryptography;
45
using CsCheck;
56
using SpacetimeDB.BSATN;
67
using Xunit;
@@ -848,4 +849,126 @@ public static void GeneratedEnumEqualsWorks()
848849
iter: 10_000
849850
);
850851
}
852+
853+
[Fact]
854+
public static void UUidRoundTrip()
855+
{
856+
var u1 = Uuid.NIL;
857+
var s = u1.ToString();
858+
var u2 = Uuid.Parse(s);
859+
Assert.Equal(u1, u2);
860+
Assert.Equal(u1.ToGuid(), u2.ToGuid());
861+
Assert.Equal(s, u2.ToString());
862+
}
863+
864+
[Fact]
865+
public static void UuidToString()
866+
{
867+
foreach (
868+
var uuid in new[]
869+
{
870+
Uuid.NIL,
871+
new Uuid(new U128(0x0102030405060708UL, 0x090A0B0C0D0E0F10UL)),
872+
Uuid.MAX,
873+
}
874+
)
875+
{
876+
var s = uuid.ToString();
877+
var uuid2 = Uuid.Parse(s);
878+
Assert.Equal(uuid, uuid2);
879+
var g = new Guid(s);
880+
Assert.Equal(s, g.ToString()); // same canonical form
881+
}
882+
}
883+
884+
[Fact]
885+
public static void WrapAround()
886+
{
887+
// Check wraparound behavior
888+
var counter = int.MaxValue;
889+
var ts = Timestamp.UNIX_EPOCH;
890+
891+
_ = Uuid.FromCounterV7(ref counter, ts, new byte[4]);
892+
893+
Assert.Equal(0, counter);
894+
}
895+
896+
[Fact]
897+
public static void NegativeTimestampThrows()
898+
{
899+
var counter = 0;
900+
var ts = new Timestamp(-1);
901+
902+
var ex = Assert.Throws<ArgumentException>(
903+
() => Uuid.FromCounterV7(ref counter, ts, new byte[4])
904+
);
905+
906+
Assert.Contains("timestamp", ex.Message, StringComparison.OrdinalIgnoreCase);
907+
}
908+
909+
[Fact]
910+
public static void UuidOrdered()
911+
{
912+
var u1 = new Uuid(new U128(1, 0));
913+
var u2 = new Uuid(new U128(2, 0));
914+
915+
Assert.True(u1 < u2);
916+
Assert.True(u2 > u1);
917+
Assert.Equal(u1, u1);
918+
Assert.NotEqual(u1, u2);
919+
920+
// Check we start from zero
921+
var counter = 0;
922+
var ts = Timestamp.UNIX_EPOCH;
923+
924+
var uStart = Uuid.FromCounterV7(ref counter, ts, new byte[4]);
925+
Assert.Equal(0, uStart.GetCounter());
926+
927+
// Check ordering across many UUIDs
928+
const int total = 10_000_000;
929+
counter = int.MaxValue - total;
930+
931+
var uuids = Enumerable
932+
.Range(0, 1000)
933+
.Select(_ =>
934+
{
935+
var bytes = new byte[4];
936+
RandomNumberGenerator.Fill(bytes);
937+
return Uuid.FromCounterV7(ref counter, ts, bytes);
938+
})
939+
.ToList();
940+
941+
for (var i = 0; i < uuids.Count - 1; i++)
942+
{
943+
var a = uuids[i];
944+
var b = uuids[i + 1];
945+
946+
Assert.Equal(Uuid.UuidVersion.V7, a.GetVersion());
947+
948+
Assert.True(a < b, $"UUIDs are not ordered at {i}: {a} !< {b}");
949+
Assert.True(
950+
a.GetCounter() < b.GetCounter(),
951+
$"UUID counters are not ordered at {i}: {a.GetCounter()} !< {b.GetCounter()}"
952+
);
953+
}
954+
}
955+
956+
[Fact]
957+
public static void UuidVersion()
958+
{
959+
var u = Uuid.NIL;
960+
Assert.Equal(Uuid.UuidVersion.Nil, u.GetVersion());
961+
962+
u = Uuid.MAX;
963+
Assert.Equal(Uuid.UuidVersion.Max, u.GetVersion());
964+
965+
var randomBytes = new byte[16];
966+
RandomNumberGenerator.Fill(randomBytes);
967+
u = Uuid.FromRandomBytesV4(randomBytes);
968+
Assert.Equal(Uuid.UuidVersion.V4, u.GetVersion());
969+
970+
var counter = 0;
971+
u = Uuid.FromCounterV7(ref counter, Timestamp.UNIX_EPOCH, randomBytes.AsSpan()[..4]);
972+
Assert.Equal(Uuid.UuidVersion.V7, u.GetVersion());
973+
}
851974
}

crates/bindings-csharp/BSATN.Runtime/BSATN/U128.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
namespace SpacetimeDB;
55

6+
using System.Buffers.Binary;
67
using System.Numerics;
78
using System.Runtime.InteropServices;
89

@@ -33,6 +34,33 @@ public U128(ulong upper, ulong lower)
3334

3435
internal ulong Upper => _upper;
3536

37+
/// Returns a <see cref="U128" /> from a big-endian byte array.
38+
public static U128 FromBytesBigEndian(ReadOnlySpan<byte> bytes)
39+
{
40+
if (bytes.Length != Size)
41+
{
42+
throw new ArgumentException(
43+
$"Byte array must be exactly {Size} bytes long.",
44+
nameof(bytes)
45+
);
46+
}
47+
48+
var upper = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(0, 8));
49+
var lower = BinaryPrimitives.ReadUInt64BigEndian(bytes.Slice(8, 8));
50+
51+
return new U128(upper, lower);
52+
}
53+
54+
public byte[] ToBytesBigEndian()
55+
{
56+
var bytes = new byte[Size];
57+
58+
BinaryPrimitives.WriteUInt64BigEndian(bytes.AsSpan(0, 8), _upper);
59+
BinaryPrimitives.WriteUInt64BigEndian(bytes.AsSpan(8, 8), _lower);
60+
61+
return bytes;
62+
}
63+
3664
/// <inheritdoc cref="IComparable.CompareTo(object)" />
3765
public int CompareTo(object? value)
3866
{
@@ -90,4 +118,11 @@ private BigInteger AsBigInt() =>
90118

91119
/// <inheritdoc cref="object.ToString()" />
92120
public override string ToString() => AsBigInt().ToString();
121+
122+
public static U128 FromGuid(Guid guid)
123+
{
124+
Span<byte> bytes = stackalloc byte[16];
125+
guid.TryWriteBytes(bytes);
126+
return FromBytesBigEndian(bytes);
127+
}
93128
}

0 commit comments

Comments
 (0)