[Pitch] UUID v7, other improvements

This is a way to erroneously obtain a huge number of identifiers with the same timestamp. Therefore, it is not supported by RFC 9562, and a more safe alternative - timestamp offset - is proposed.

I would recommend thoroughly studying RFC 9562 before making "let's do it just in case" suggestions.

2 Likes

@Karl ’s work here might be an interesting comparison: UniqueID - time-ordered UUIDs in Swift (He added support for random v4 UUIDS and ordered v6 UUIDs).

1 Like

Other commenters have already expressed preference for v7 over timeOrdered, but I just wanted to add that v7 is not just clearer and more ergonomic: it also avoids introducing ambiguity if we ever want to support a new version of UUID that’s also time-ordered.

6 Likes

There's this unorthodox way that allows using initialisers with different set of parameters per version:

extension UUID {
    struct V5 { private init() {}; static let v5 = V5() }
    struct V8 { private init() {}; static let v8 = V8() }
    
    init(_ v5: V5, name: String, namespace: String) { ... }
    init(_ v8: V8, customData: Data) { ... }
    ...
}

Usage example:

UUID(.v5, name: "name", namespace: "namespace")
UUID(.v8, customData: Data())
UUID(.v5, customData: Data()) // compilation error
3 Likes

Hi all,

I've updated the proposal with the following changes:

  • UUID.timeOrdered -> UUID.version7()
  • Added corresponding static func version4(), for completeness.
  • Renamed timeOrderedTimestamp to simply date.
  • Removed the entire Version struct. There isn't really much point to it, which became clearer when renaming all of its properties to versionX.
  • Changed the result type of version to Int.
  • Added an offset option to the version 7 generation API, alongside the ability to set a specific Date.
  • Added guarantee of monotonic increments, similar to the postgre approach (thanks @sergeyprokhorenko).

Updated proposal is here.

4 Likes

Question that (only just) occurred to me, but alluded to I think by some other commenters, in the realm of modern usage:

As Wikipedia tells me, "A UUID is a 128-bit number." Now that we have UInt128 in the standard library, are there type conversions or other APIs that become possible to express in modern Swift using that type which we should now add as part of this modernization?

3 Likes

It's a good question. I am hoping that the work on the "fully inhabited" protocols (see safe loading API for RawSpan) will provide a seamless connection between Span<UInt8> and Int128, when it's useful. That is, an Int128 is inherently Raw, and Raw is inherently UInt8. But I will leave the discussion of those points for the other thread, to avoid getting into the weeds on this one.

If if doesn't work out, we could revisit adding conveniences to this type too.

1 Like

Has it been considered making lower cased the default representation from .uuidString? As I understand it, the spec states UUIDs should be represented as lowercase.

I think the default should be as spec compliant as possible. One reason it would have tangible impact is that the default being uppercase currently allows implicit identification of iOS users vs other platforms if the UUID is logged without transforming to lowercase.

Making the default match the spec and other implementations would prevent that which seems like a good thing.

1 Like

A previous pitch to conform UUID to LosslessStringConvertible was accepted, but the pull requests for the proposal and implementation haven't been merged yet.

Instead of adding a lowercasedUUIDString property, you could add an initializer similar to the String.init(_:radix:uppercase:) used by binary integers.

extension String {
  public init(_ uuid: UUID, uppercase: Bool)
}

For example:

String(uuid)                    //-> "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
String(uuid, uppercase: true)   //-> "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
String(uuid, uppercase: false)  //-> "e621e1f8-c36c-495a-93fc-0c247a3e6e5f"
4 Likes

I like that but think the default should be lowercase :slight_smile:

Why offer a lowercasedUUIDString at all? String already has API to achieve this, we’re only saving 3 characters:

let foo: UUID
foo.uuidString.lowercased()
2 Likes

I think we could simply name the string accessors lowercased() and uppercased(), mirroring the String API.

There is a strong connection between a UUID value and its string representation, so in lowercasedUUIDString I feel like both the UUID and String parts are redundant.

12 Likes

We can't change uuidString without risking breaking existing apps.. However this we probably could:

  1. deprecate uuidString
  2. make a new property, say string and make that one lowercasing.
  3. do not provide uppercasing property/method (thus guiding users away from that). Those who want to uppercase will just call uuid.string.uppercased().

As for the name for nil - I don't think we need to religiously follow the RFC in that regards, the same way we don't want to call that constant Nil (as per the RFC).

5 Likes

I agree. It always struck me that .uuidString is a bit of a peculiar name. .string more accurately conveys what it is.

1 Like

The idiomatic spelling in Swift, if we're creating new APIs, would be as @benrimmington describes: that is, a converting initializer String(_: UUID, uppercase: Bool = false).

9 Likes

Won't be convenient working with optionals, compare the two:

struct SomeStruct {
    var field: String?
    ...
}

func foo(uuid: UUID?) {
    SomeStruct(
        field: uuid == nil ? nil : String(uuid!)
        ...
    )
    SomeStruct(
        field: uuid?.string
        ...
    )
}
2 Likes

Orthogonal problem, nothing unique to this converting initializer.
(Also, you can write optionalUUID.map(String.init) in such situations.)

2 Likes

Looks good to me + deprecating the uuidString property.

@tera - I think your concern could be addressed by also having an initialiser that accepts an optional UUID and returns an optional String. Eg:
String(_: UUID?, uppercase: Bool = false)

Whether that belongs in the standard library I’m not sure, but would be trivial to add as a convenience extension if needed.

We have precedents for having multiple ways of performing such conversions, for example, String(42) and 42.formatted(). That said, providing a single way in this case (such as String.init) is sufficient.

However, I would like to see the deprecation of uuidString being included in this pitch to better align with RFC 4122 and ITU-T X.667 in guiding users away from uppercased UUID. And to make this guidance consistent we should probably avoid introducing an option for uppercasing UUIDs in the new String.init (and thus not have its uppercase parameter).

5 Likes

Makes sense to me