From 1316c4d1c0e4cb62f647ce88abb66b34d639b788 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sun, 6 Jul 2025 19:17:23 +0200 Subject: [PATCH] (feature): comb guid --- .../Comb/CombGuidGeneratorTests.cs | 70 ++++++++ src/MaksIT.Core/Comb/CombGuidGenerator.cs | 149 ++++++++++++++++++ src/MaksIT.Core/MaksIT.Core.csproj | 2 +- 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 src/MaksIT.Core.Tests/Comb/CombGuidGeneratorTests.cs create mode 100644 src/MaksIT.Core/Comb/CombGuidGenerator.cs diff --git a/src/MaksIT.Core.Tests/Comb/CombGuidGeneratorTests.cs b/src/MaksIT.Core.Tests/Comb/CombGuidGeneratorTests.cs new file mode 100644 index 0000000..e18fc8e --- /dev/null +++ b/src/MaksIT.Core.Tests/Comb/CombGuidGeneratorTests.cs @@ -0,0 +1,70 @@ +using MaksIT.Core.Comb; + + +namespace MaksIT.Core.Tests.Comb; + +public class CombGuidGeneratorTests { + [Theory] + [InlineData(CombGuidType.SqlServer)] + [InlineData(CombGuidType.PostgreSql)] + public void CreateCombGuid_WithBaseGuidAndTimestamp_EmbedsTimestampCorrectly(CombGuidType type) { + // Arrange + var baseGuid = Guid.NewGuid(); + var timestamp = DateTime.UtcNow; + + // Act + var combGuid = CombGuidGenerator.CreateCombGuid(baseGuid, timestamp, type); + var extractedTimestamp = CombGuidGenerator.ExtractTimestamp(combGuid, type); + + // Assert + Assert.Equal(timestamp, extractedTimestamp); + } + + [Theory] + [InlineData(CombGuidType.SqlServer)] + [InlineData(CombGuidType.PostgreSql)] + public void CreateCombGuid_WithTimestampOnly_GeneratesValidCombGuid(CombGuidType type) { + // Arrange + var timestamp = DateTime.UtcNow; + + // Act + var combGuid = CombGuidGenerator.CreateCombGuid(timestamp, type); + var extractedTimestamp = CombGuidGenerator.ExtractTimestamp(combGuid, type); + + // Assert + Assert.Equal(timestamp, extractedTimestamp); + } + + [Theory] + [InlineData(CombGuidType.SqlServer)] + [InlineData(CombGuidType.PostgreSql)] + public void CreateCombGuid_WithBaseGuidOnly_UsesCurrentUtcTimestamp(CombGuidType type) { + // Arrange + var baseGuid = Guid.NewGuid(); + var beforeCreation = DateTime.UtcNow; + + // Act + var combGuid = CombGuidGenerator.CreateCombGuid(baseGuid, type); + var extractedTimestamp = CombGuidGenerator.ExtractTimestamp(combGuid, type); + + // Assert + Assert.True(extractedTimestamp >= beforeCreation); + Assert.True(extractedTimestamp <= DateTime.UtcNow); + } + + [Theory] + [InlineData(CombGuidType.SqlServer)] + [InlineData(CombGuidType.PostgreSql)] + public void ExtractTimestamp_ReturnsCorrectTimestamp(CombGuidType type) { + // Arrange + var baseGuid = Guid.NewGuid(); + var timestamp = DateTime.UtcNow; + var combGuid = CombGuidGenerator.CreateCombGuid(baseGuid, timestamp, type); + + // Act + var extractedTimestamp = CombGuidGenerator.ExtractTimestamp(combGuid, type); + + // Assert + Assert.Equal(timestamp, extractedTimestamp); + } +} \ No newline at end of file diff --git a/src/MaksIT.Core/Comb/CombGuidGenerator.cs b/src/MaksIT.Core/Comb/CombGuidGenerator.cs new file mode 100644 index 0000000..5bacbee --- /dev/null +++ b/src/MaksIT.Core/Comb/CombGuidGenerator.cs @@ -0,0 +1,149 @@ +using System.Buffers.Binary; + + +namespace MaksIT.Core.Comb; + +/// +/// Specifies the layout strategy used to embed a timestamp in a COMB GUID. +/// +public enum CombGuidType { + /// + /// COMB GUID format compatible with SQL Server (timestamp in bytes 8–15). + /// + SqlServer, + + /// + /// COMB GUID format compatible with PostgreSQL (timestamp in bytes 0–7). + /// + PostgreSql +} + +/// +/// Provides methods to generate and extract COMB GUIDs with embedded timestamps. +/// COMB GUIDs improve index locality by combining randomness with a sortable timestamp. +/// +public static class CombGuidGenerator { + private const int TimestampByteLength = 8; + private static readonly DateTime UnixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Generates a COMB GUID using the specified base GUID, timestamp, and format type. + /// + /// The base GUID to embed the timestamp into. + /// The UTC timestamp to embed in the GUID. + /// The COMB GUID format to use. + /// The generated COMB GUID. + public static Guid CreateCombGuid(Guid baseGuid, DateTime timestamp, CombGuidType type) { + return type switch { + CombGuidType.SqlServer => CreateSqlServerCombGuid(baseGuid, timestamp), + CombGuidType.PostgreSql => CreatePostgreSqlCombGuid(baseGuid, timestamp), + _ => throw new ArgumentOutOfRangeException(nameof(type), "Unsupported COMB GUID type.") + }; + } + + /// + /// Generates a COMB GUID using a random GUID and a specified UTC timestamp. + /// + /// The UTC timestamp to embed in the GUID. + /// The COMB GUID format to use. + /// The generated COMB GUID. + public static Guid CreateCombGuid(DateTime timestamp, CombGuidType type) => + CreateCombGuid(Guid.NewGuid(), timestamp, type); + + /// + /// Generates a COMB GUID using a specified base GUID and the current UTC timestamp. + /// + /// The base GUID to embed the timestamp into. + /// The COMB GUID format to use. + /// The generated COMB GUID. + public static Guid CreateCombGuid(Guid baseGuid, CombGuidType type) => + CreateCombGuid(baseGuid, DateTime.UtcNow, type); + + /// + /// Extracts the embedded UTC timestamp from a COMB GUID using the specified format. + /// + /// The COMB GUID containing the timestamp. + /// The COMB GUID format used during creation. + /// The extracted UTC timestamp. + public static DateTime ExtractTimestamp(Guid combGuid, CombGuidType type) { + Span guidBytes = stackalloc byte[16]; + combGuid.TryWriteBytes(guidBytes); + + return type switch { + CombGuidType.SqlServer => ReadTimestampFromBytes(guidBytes.Slice(8, TimestampByteLength)), + CombGuidType.PostgreSql => ReadTimestampFromBytes(guidBytes.Slice(0, TimestampByteLength)), + _ => throw new ArgumentOutOfRangeException(nameof(type), "Unsupported COMB GUID type.") + }; + } + + /// + /// Creates a COMB GUID compatible with SQL Server by embedding the timestamp in bytes 8–15. + /// + /// The base GUID. + /// The UTC timestamp. + /// The resulting COMB GUID. + private static Guid CreateSqlServerCombGuid(Guid baseGuid, DateTime timestamp) { + Span guidBytes = stackalloc byte[16]; + baseGuid.TryWriteBytes(guidBytes); + WriteTimestampBytes(guidBytes.Slice(8, TimestampByteLength), timestamp); + return new Guid(guidBytes); + } + + /// + /// Creates a COMB GUID compatible with PostgreSQL by embedding the timestamp in bytes 0–7. + /// + /// The base GUID. + /// The UTC timestamp. + /// The resulting COMB GUID. + private static Guid CreatePostgreSqlCombGuid(Guid baseGuid, DateTime timestamp) { + Span baseBytes = stackalloc byte[16]; + baseGuid.TryWriteBytes(baseBytes); + + Span finalBytes = stackalloc byte[16]; + // first 8 bytes = timestamp + WriteTimestampBytes(finalBytes.Slice(0, TimestampByteLength), timestamp); + // remaining 8 bytes = random tail + baseBytes.Slice(TimestampByteLength, 16 - TimestampByteLength) + .CopyTo(finalBytes.Slice(TimestampByteLength, 16 - TimestampByteLength)); + + return new Guid(finalBytes); + } + + /// + /// Converts a DateTime into an 8-byte timestamp and writes it into the specified span. + /// + /// The span where the timestamp will be written. + /// The UTC timestamp to convert. + private static void WriteTimestampBytes(Span destination, DateTime timestamp) { + long ticks = timestamp.ToUniversalTime().Ticks; // full 64-bit precision + Span fullBytes = stackalloc byte[8]; + BinaryPrimitives.WriteInt64BigEndian(fullBytes, ticks); + fullBytes.CopyTo(destination); + } + + /// + /// Reads an 8-byte timestamp from the given span and converts it to a DateTime. + /// + /// The span containing the timestamp bytes. + /// The corresponding UTC DateTime. + private static DateTime ReadTimestampFromBytes(ReadOnlySpan source) { + long ticks = BinaryPrimitives.ReadInt64BigEndian(source); + return new DateTime(ticks, DateTimeKind.Utc); + } + + /// + /// Converts a DateTime to Unix time in milliseconds. + /// + /// The UTC DateTime to convert. + /// Unix time in milliseconds. + private static long ConvertToUnixTimeMilliseconds(DateTime timestamp) => + (long)(timestamp.ToUniversalTime() - UnixEpoch).TotalMilliseconds; + + /// + /// Converts Unix time in milliseconds to a UTC DateTime. + /// + /// Unix time in milliseconds. + /// The corresponding UTC DateTime. + private static DateTime ConvertFromUnixTimeMilliseconds(long milliseconds) => + UnixEpoch.AddMilliseconds(milliseconds); +} diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index 3282493..d652e65 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.4.1 + 1.4.2 Maksym Sadovnychyy MAKS-IT MaksIT.Core