jkulubya · GitHub

@jkulubya

The GUID type byte layout in dotnet is

  • 4 byte int (typically little endian but can be big endian depending on cpu)
  • 2 byte short (typically little endian but can be big endian depending on cpu)
  • 2 byte short (typically little endian but can be big endian depending on cpu)
  • 8 bytes stored as-is

i.e. the raw bytes you provide in a constructor might be rearranged, and reading the raw bytes of a GUID out on the other side might not give you what you put in.

Secondly, even though the bytes might be rearranged internally, externally, the GUID behaves as if the bytes weren't rearranged i.e., Guid.ToString will print the bytes as you provided them, and not as they are laid out in memory.

What this means is that when treating a GUID as a dumb container for 16 bytes, you must rearrange the bytes going in and out yourself, otherwise the meaning of the GUID changes between the time it's written and when it's read. Guid.ToString and libraries like EF Core and Npgsql take care of this for you.

Take an example of this guid -> e6a48153-cb00-8fab-aa68-fc4387bf8f01.

Current/New behaviour GUID.ToString() (sent to e.g. Npgsql/EF Core) Guid in-memory layout Guid.ToUInt128() UInt128 in-memory layout (sent to TB)
New e6a48153-cb00-8fab-aa68-fc4387bf8f01 53-81-A4-E6-00-CB-AB-8F-AA-68-FC-43-87-BF-8F-01 2075611103632272357516034594534696166 E6-A4-81-53-CB-00-8F-AB-AA-68-FC-43-87-BF-8F-01
Current e6a48153-cb00-8fab-aa68-fc4387bf8f01 53-81-A4-E6-00-CB-AB-8F-AA-68-FC-43-87-BF-8F-01 2075611103632272355506525592271225171 53-81-A4-E6-00-CB-AB-8F-AA-68-FC-43-87-BF-8F-01

Notice how with the current behaviour the last column does not match the second, while it does with the new - which meant that queries via the tigerbeetle repl using uuids/guids that were saved in a separate database were failing with a NotFound error

…g endianness
Add E2E test that would have failed using old logic

batiati

Comment on lines +121 to +140

// "00000001-0001-4000-AA00-000000000000"
var idSourcedExternally = Guid.Parse("00000001-0001-4000-AA00-000000000000"); // e.g. guid from other database
var bytesStoredInTB = new byte[]
{0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x40, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

// Parse TB result (TB client reads by overlaying dotnet types on raw bytes)
UInt128 parsed = UInt128.Zero;
bytesStoredInTB.CopyTo(new Span<byte>(&parsed, 16));

// Round-trip via GUID
var roundTripped = parsed.ToGuid().ToUInt128();

// Send bytes to TB (TB client send raw bytes of dotnet types over the wire)
var roundTrippedBytes = new Span<byte>(&roundTripped, 16);

var externalIdUInt128 = idSourcedExternally.ToUInt128();
var externalIdBytes = new Span<byte>(&externalIdUInt128, 16);

Assert.IsTrue(roundTrippedBytes.SequenceEqual(bytesStoredInTB));
Assert.IsTrue(externalIdBytes.SequenceEqual(bytesStoredInTB));

Merged

Read the original on github.com ↗