@@ -365,7 +365,7 @@ public readonly TimeDuration TimeDurationSince(Timestamp earlier) =>
365365 public static Timestamp operator - ( Timestamp point , TimeDuration interval ) =>
366366 new Timestamp ( checked ( point . MicrosecondsSinceUnixEpoch - interval . Microseconds ) ) ;
367367
368- public int CompareTo ( Timestamp that )
368+ public readonly int CompareTo ( Timestamp that )
369369 {
370370 return this . MicrosecondsSinceUnixEpoch . CompareTo ( that . MicrosecondsSinceUnixEpoch ) ;
371371 }
@@ -605,3 +605,226 @@ public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) =>
605605 // --- / customized ---
606606 }
607607}
608+
609+ /// <summary>
610+ /// A generator for monotonically increasing <see cref="Timestamp"/> s by millisecond increments.
611+ /// </summary>
612+ public sealed class ClockGenerator ( Timestamp start )
613+ {
614+ private long _microsSinceUnixEpoch = start . MicrosecondsSinceUnixEpoch ;
615+
616+ /// <summary>
617+ /// Returns the next <see cref="Timestamp"/> in the sequence, guaranteed to be
618+ /// greater than the previous one returned by this method.
619+ ///
620+ /// UUIDv7 requires monotonic millisecond timestamps, so each tick
621+ /// increases the timestamp by at least 1 millisecond (1_000 microseconds).
622+ ///
623+ /// # Exceptions
624+ ///
625+ /// If the internal timestamp overflows i64 microseconds.
626+ /// </summary>
627+ public Timestamp Tick ( )
628+ {
629+ checked
630+ {
631+ _microsSinceUnixEpoch += 1000 ;
632+ }
633+ return new Timestamp ( _microsSinceUnixEpoch ) ;
634+ }
635+
636+ public static implicit operator ClockGenerator ( Timestamp t ) => new ( t ) ;
637+ }
638+
639+ /// <summary>
640+ /// A universally unique identifier (UUID).
641+ ///
642+ /// Wraps the native <see cref="Guid"/> type and provides methods
643+ /// to generate nil, random (v4), and time-ordered (v7) UUIDs.
644+ /// </summary>
645+ [ StructLayout ( LayoutKind . Sequential ) ]
646+ public readonly record struct Uuid : IEquatable < Uuid > , IComparable , IComparable < Uuid >
647+ {
648+ private readonly U128 value ;
649+ internal Uuid ( U128 val ) => value = val ;
650+ public static readonly Uuid NIL = new ( FromGuid ( Guid . Empty ) ) ;
651+
652+ public static Uuid Nil ( ) => NIL ;
653+
654+ public static U128 FromGuid ( Guid guid )
655+ {
656+ Span < byte > bytes = stackalloc byte [ 16 ] ;
657+ guid . TryWriteBytes ( bytes ) ;
658+ if ( BitConverter . IsLittleEndian )
659+ {
660+ var lower = BitConverter . ToUInt64 ( bytes ) ;
661+ var upper = BitConverter . ToUInt64 ( bytes [ 8 ..] ) ;
662+ return new U128 ( upper , lower ) ;
663+ }
664+ else
665+ {
666+ var upper = BitConverter . ToUInt64 ( bytes ) ;
667+ var lower = BitConverter . ToUInt64 ( bytes [ 8 ..] ) ;
668+ return new U128 ( upper , lower ) ;
669+ }
670+ }
671+
672+ private static Guid GuidV4 ( ReadOnlySpan < byte > randomBytes )
673+ {
674+ if ( randomBytes . Length != 16 )
675+ {
676+ throw new ArgumentException ( "Must be 16 bytes" , nameof ( randomBytes ) ) ;
677+ }
678+
679+ Span < byte > bytes = stackalloc byte [ 16 ] ;
680+ randomBytes . CopyTo ( bytes ) ;
681+ bytes [ 6 ] = ( byte ) ( ( bytes [ 6 ] & 0x0F ) | 0x40 ) ; // version 4
682+ bytes [ 8 ] = ( byte ) ( ( bytes [ 8 ] & 0x3F ) | 0x80 ) ; // variant RFC 4122
683+
684+ return new Guid ( randomBytes ) ;
685+ }
686+
687+ /// <summary>
688+ /// Create a UUIDv4 from explicit random bytes.
689+ /// </summary>
690+ /// <remarks>
691+ /// This method assumes the provided bytes are already sufficiently random;
692+ /// it will only set the appropriate bits for the UUID version and variant.
693+ /// </remarks>
694+ /// <example>
695+ /// <code>
696+ /// var randomBytes = new byte[16];
697+ /// var uuid = Uuid.FromRandomBytesV4(randomBytes);
698+ /// Console.WriteLine(uuid);
699+ /// // Output: 00000000-0000-4000-8000-000000000000
700+ /// </code>
701+ /// </example>
702+ public static Uuid FromRandomBytesV4 ( ReadOnlySpan < byte > randomBytes )
703+ {
704+ return new ( FromGuid ( GuidV4 ( randomBytes ) ) ) ;
705+ }
706+
707+ /// <summary>
708+ /// Create a UUIDv7 from a UNIX timestamp (milliseconds) and 10 random bytes.
709+ /// </summary>
710+ /// <remarks>
711+ /// This method sets the variant field within the counter bytes without
712+ /// shifting data around it. Callers using the counter as a monotonic
713+ /// value should avoid storing significant data in the two least significant
714+ /// bits of the third byte.
715+ /// </remarks>
716+ /// <example>
717+ /// <code>
718+ /// ulong millis = 1686000000000UL;
719+ /// var randomBytes = new byte[10];
720+ /// var uuid = Uuid.FromUnixMillisV7(millis, randomBytes);
721+ /// Console.WriteLine(uuid);
722+ /// // Output: 01888d6e-5c00-7000-8000-000000000000
723+ /// </code>
724+ /// </example>
725+ public static Uuid FromUnixMillisV7 ( long millisSinceUnixEpoch , ReadOnlySpan < byte > randomBytes )
726+ {
727+ // TODO: Convert to ` CreateVersion7` from .NET 9 when we can.
728+ if ( millisSinceUnixEpoch < 0 )
729+ {
730+ throw new ArgumentOutOfRangeException ( nameof ( millisSinceUnixEpoch ) , "Timestamp precedes Unix epoch" ) ;
731+ }
732+
733+ // Generate random 16 bytes
734+ var bytes = GuidV4 ( randomBytes ) . ToByteArray ( ) ;
735+
736+ // Insert 48-bit timestamp (big endian)
737+ bytes [ 0 ] = ( byte ) ( ( millisSinceUnixEpoch >> 40 ) & 0xFF ) ;
738+ bytes [ 1 ] = ( byte ) ( ( millisSinceUnixEpoch >> 32 ) & 0xFF ) ;
739+ bytes [ 2 ] = ( byte ) ( ( millisSinceUnixEpoch >> 24 ) & 0xFF ) ;
740+ bytes [ 3 ] = ( byte ) ( ( millisSinceUnixEpoch >> 16 ) & 0xFF ) ;
741+ bytes [ 4 ] = ( byte ) ( ( millisSinceUnixEpoch >> 8 ) & 0xFF ) ;
742+ bytes [ 5 ] = ( byte ) ( millisSinceUnixEpoch & 0xFF ) ;
743+
744+ // Set version (0111) and variant (10xx)
745+ bytes [ 6 ] = ( byte ) ( ( bytes [ 6 ] & 0x0F ) | 0x70 ) ;
746+ bytes [ 8 ] = ( byte ) ( ( bytes [ 8 ] & 0x3F ) | 0x80 ) ;
747+
748+ return new Uuid ( FromGuid ( new Guid ( bytes ) ) ) ;
749+ }
750+
751+ /// <summary>
752+ /// Generate a UUIDv7 using a monotonic <see cref="ClockGenerator"/>.
753+ /// </summary>
754+ /// <remarks>
755+ /// This method sets the variant field within the counter bytes without
756+ /// shifting data around it. Callers using the counter as a monotonic
757+ /// value should avoid storing significant data in the two least significant
758+ /// bits of the third byte.
759+ /// </remarks>
760+ /// <example>
761+ /// <code>
762+ /// var clock = new ClockGenerator(1686000000000UL);
763+ /// var randomBytes = new byte[10];
764+ /// var uuid = Uuid.FromClockV7(clock, randomBytes);
765+ /// Console.WriteLine(uuid);
766+ /// // Output: 0000647e-5181-7000-8000-000000000000
767+ /// </code>
768+ /// </example>
769+ public static Uuid FromClockV7 ( ClockGenerator clock , ReadOnlySpan < byte > randomBytes )
770+
771+ {
772+ var millis = clock . Tick ( ) . MicrosecondsSinceUnixEpoch / 1000 ;
773+ return FromUnixMillisV7 ( millis , randomBytes ) ;
774+ }
775+
776+ /// <summary>
777+ /// Parses a UUID from its string representation.
778+ /// </summary>
779+ /// <example>
780+ /// <code>
781+ /// var s = "01888d6e-5c00-7000-8000-000000000000";
782+ /// var uuid = Uuid.Parse(s);
783+ /// Console.WriteLine(uuid.ToString() == s); // True
784+ /// </code>
785+ /// </example>
786+ public static Uuid Parse ( string s ) => new ( FromGuid ( Guid . Parse ( s ) ) ) ;
787+
788+ /// <summary>
789+ /// Converts this instance to a <see cref="Guid"/>.
790+ /// </summary>
791+ public Guid ToGuid ( ) => value . ToGuid ( ) ;
792+
793+ public override readonly string ToString ( ) => ToGuid ( ) . ToString ( ) ;
794+
795+ public readonly int CompareTo ( Uuid other ) => ToGuid ( ) . CompareTo ( other . ToGuid ( ) ) ;
796+ /// <inheritdoc cref="IComparable.CompareTo(object)" />
797+ public int CompareTo ( object ? value )
798+ {
799+ if ( value is Uuid other )
800+ {
801+ return CompareTo ( other ) ;
802+ }
803+ else if ( value is null )
804+ {
805+ return 1 ;
806+ }
807+ else
808+ {
809+ throw new ArgumentException ( "Argument must be a Uuid" , nameof ( value ) ) ;
810+ }
811+ }
812+ public static bool operator < ( Uuid l , Uuid r ) => l . CompareTo ( r ) < 0 ;
813+ public static bool operator > ( Uuid l , Uuid r ) => l . CompareTo ( r ) > 0 ;
814+
815+
816+ public readonly partial struct BSATN : IReadWrite < Uuid >
817+ {
818+ public Uuid Read ( BinaryReader reader ) => new ( new SpacetimeDB . BSATN . U128Stdb ( ) . Read ( reader ) ) ;
819+ public void Write ( BinaryWriter writer , Uuid value ) => new SpacetimeDB . BSATN . U128Stdb ( ) . Write ( writer , value . value ) ;
820+ // --- / auto-generated ---
821+
822+ // --- customized ---
823+ public AlgebraicType GetAlgebraicType ( ITypeRegistrar registrar ) =>
824+ // Return a Product directly, not a Ref, because this is a special type.
825+ new AlgebraicType . Product ( [
826+ // Using this specific name here is important.
827+ new ( "__uuid__" , new AlgebraicType . U128 ( default ) )
828+ ] ) ;
829+ }
830+ }
0 commit comments