UUIDNext icon indicating copy to clipboard operation
UUIDNext copied to clipboard

Add a way to create an UUIDv7 for a specific timestamp

Open mareek opened this issue 1 year ago • 1 comments

Things to keep in mind :

  • It's not just making UuidV7Generator.New(DateTime) public
  • Most of the time, calls to UuidAdvanced.NewV7FromADateTime (name not final) shouldn't interfere with calls to Uuid.NewSequential and Uuid.NewDatabaseFriendly (see exception below)
  • Developers that use this feature by mistake by calling UuidAdvanced.NewV7FromADateTime(DateTime.Now) or something similar should except the same behavior as calling Uuid.NewSequential
  • Multiple calls to UuidAdvanced.NewV7FromADateTime with the same timestamp during a reasonably short period of time should produce ordered Uuids

See .NET 9's Guid.CreateVersion7(DateTimeOffset) and #14 for inspiration

mareek avatar Aug 23 '24 03:08 mareek

One thing that would be very nice is, besides support for the time component, to also be able to determine the random component. This is pretty important for database queries.

Suppose I have a UUIDv7 Id field. It's indexed/partitioned/sharded based on the Id field. I want to query the database for data for the last day, without creating additional indexes. Then being able to construct a query like this:

SELECT * FROM Id >= lowerBoundry

would be nice, where the random component for the lower boundary is initialised to zero. To support these types of queries initializing the random component to the maximum or minimum value needs to be supported in the API.

timstokman avatar Aug 30 '24 11:08 timstokman

I added a new static class UUIDNext.Tools.UuidToolkit with advanced methods to create custom UUID v7 :

  • CreateUuidV7FromSpecificDate(DateTimeOffset date) that address this issue
  • CreateUuidV7(long timestamp, Span<byte> followingBytes) that address @timstokman's comment

I will publish a beta version with this new class in the coming weeks and write some documentation about theses methods

mareek avatar Sep 28 '24 23:09 mareek

One thing that would be very nice is, besides support for the time component, to also be able to determine the random component. This is pretty important for database queries. ... would be nice, where the random component for the lower boundary is initialised to zero. To support these types of queries initializing the random component to the maximum or minimum value needs to be supported in the API.

Hi @timstokman, I've released a new beta version that allow you to create a UUID v7 based on a timestamp and a span of bytes by using the UUIDNext.Tools.UuidToolkit.CreateUuidV7(long, Span<byte>) method.

Can you try it and tell me if it meet your needs ?

Thanks in advance

mareek avatar Oct 07 '24 17:10 mareek

The API will work for me.

I can give you a bit of context, might help you out. My application right now has two custom helper methods for uuidv7 where this would help:

public static Guid GenerateMinRangeId(DateTimeOffset dateTime)
{
    Span<byte> bytes = stackalloc byte[16];
    Span<byte> timestampInMillisecondsBytes = stackalloc byte[8];
    BinaryPrimitives.TryWriteInt64BigEndian(timestampInMillisecondsBytes, dateTime.ToUnixTimeMilliseconds());
    timestampInMillisecondsBytes.Slice(2, 6).CopyTo(bytes);
    return new Guid(bytes, true);
}

public static Guid GenerateMaxRangeId(DateTimeOffset dateTime)
{
    Span<byte> bytes = stackalloc byte[16];
    Span<byte> timestampInMillisecondsBytes = stackalloc byte[8];
    BinaryPrimitives.TryWriteInt64BigEndian(timestampInMillisecondsBytes, dateTime.ToUnixTimeMilliseconds());
    timestampInMillisecondsBytes.Slice(2, 6).CopyTo(bytes);
    bytes.Slice(6, 10).Fill(byte.MaxValue);
    return new Guid(bytes, true);
}

I use these methods for a couple of things.

Querying things based on a time-range:

Guid minRangeId = GenerateMinRangeId(minTimestamp);
Guid maxRangeId = GenerateMaxRangeId(maxTimestamp); 
myQueryable.Where(entity => entity.Id >= minRangeId && entity.Id <= maxRangeId)

Creating citus postgres time-based partitions for UUIDv7 for one day:

Guid minGuid = GenerateMinRangeId(referenceDate);
Guid maxGuid = GenerateMinRangeId(referenceDate.AddDays(1)); // Postgres partition ranges are inclusive-exclusive, so this works
ExecuteSql($@"CREATE TABLE IF NOT EXISTS public.""{table}_{referenceDate:yyyyMMdd}"" PARTITION OF public.""{table}"" FOR VALUES FROM ('{minGuid}') TO ('{maxGuid}')");

It'd be nice if these methods don't need internal UUIDv7 methods, which your API achieves. An extra overload that takes a DateTimeOffset instead of a long might be nice though.

timstokman avatar Oct 07 '24 20:10 timstokman

Thank you for your feedback, I'm glad that the new method fit your use case.

I plan to keep the method signature with a timestamp and a span because I think it conveys the message that this is a method for advanced users who understand UUIDv7 layout and have very specific use case in mind, and I don't want "normal" users that only wants "better UUID" to use it inadvertently.

mareek avatar Oct 08 '24 22:10 mareek

Is there a way to do this for the Sql Server V8 variant?

Aaron-P avatar Jul 24 '25 13:07 Aaron-P

@Aaron-P no, there is currently no way to do this for the SQL Server V8 variant

mareek avatar Jul 25 '25 15:07 mareek

Is there a way to do this for the Sql Server V8 variant?

@Aaron-P an issue has been created for this : #32

mareek avatar Jul 31 '25 16:07 mareek