TypedUUID

A Typed UUID is an UUID with an enum embeded within the UUID.

UUIDs are 128bit or 16bytes. The hex format is represented below where x is a hex representation of 4 bits.

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

  • M is 4 bits and is the Version
  • N is 3 bits and is the Variant of the Version followed a bit

We modify this and use the following structure where the 15th & 16th bytes in the UUID are the enum XORed with the result of XORing bytes 5 & 6 with bytes 13 & 14.

xxxxxxxx-YYYY-xxxx-xxxx-xxxxZZZZTTTT

Where:

  • TTTT is the Type ENUM & Version 0bEEEE_EEEE_EEEE_EVVV; XORed with (YYYY xor ZZZZ)
    • The Es are the bits of the 13 bit ENUM supporting 8,192 enums/types (0 - 8,191)
    • The Vs are the bits in the 3 bit version supporting 8 versions (0 - 7)
  • YYYY bytes XORed with ZZZZ and the Type ENUM to produce the identifying bytes
  • ZZZZ bytes XORed with YYYY and the Type ENUM to produce the identifying bytes

XORing bytes 5 & 6 with 13 & 14 and XORing again with bytes 15 & 16 of the Typed UUID will give us back the ENUM and Version of the Type using soley the UUID.

Versions

As with regular UUID Typed UUIDs come in multiple version. The current versions are:

  • Version 1: A timebased UUID where the first 56 bits are an unsigned integer representing the microseconds since epoch. Followed by 56 random bits or a sequence counter. Followed by 16 bits which are the UUID type.

  • Version 3: A name-based UUID where the first 112 bits are based off the MD5 digest of the namespace and name. The following 16 bits are the UUID type.

  • Version 4: A random UUID where the first 112 bits are random. The following 16 bits are the UUID type.

  • Version 5: A name-based UUID where the first 112 bits are based off the SHA1 digest of the namespace and name. The following 16 bits are the UUID type.

Install

Add this to your Gemfile:

gem 'typed_uuid'

Once bundled you can add an initializer to Rails to register your types as shown below. This maps the Model Classes to an integer between 0 and 65,535.

# config/initializers/uuid_types.rb

ActiveRecord::Base.register_uuid_types({
  Listing:                  0,
  Address:                  {enum: 5},
  Building:                 {enum: 512, version: 1},
  'Building::SkyScrpaer' => 8_191
})

# Or:

ActiveRecord::Base.register_uuid_types({
  0         => :Listing,
  512       => :Building,
  8_191     => 'Building::SkyScrpaer'
})

Usage

In your migrations simply replace id: :uuid with id: :typed_uuid when creating a table.

class CreateProperties < ActiveRecord::Migration[5.2]
  def change
    create_table :properties, id: :typed_uuid do |t|
      t.string "name", limit: 255
    end
  end
end

To add a typed UUID to an existing table:

class UpdateProperties < ActiveRecord::Migration[6.1]
  def change
    klass_enum = ::ActiveRecord::Base.uuid_type_from_table_name(:properties)

    # Add the column
    add_column :properties, :typed_uuid, :uuid, default: -> { "typed_uuid('\\x#{klass_enum.to_s(16).rjust(4, '0')}')" }

    # Update existing properties with a new typed UUID
    execute "UPDATE properties SET id = typed_uuid('\\x#{klass_enum.to_s(16).rjust(4, '0')}');"

    # Add null constraint since we'll swap these out for the primary key
    change_column_null :properties, :typed_uuid, false

    # TODO: Here you will want to update any reference to the old primary key
    # with the new typed_uuid that will be the new primary key.

    # Replace the old primary key with the typed_uuid
    execute "ALTER TABLE properties DROP CONSTRAINT properties_pkey;"
    rename_column :properties, :typed_uuid, :id
    execute "ALTER TABLE properties ADD PRIMARY KEY (id);"
  end

When you need to lookup they class of a TypedUUID:

ActiveRecord::Base.class_from_uuid(my_uuid) #=> MyClass

STI Models

When using STI Model Rails will generate the UUID to be inserted. This UUID will be calculated of the STI Model class and not the base class.

In the migration you can still used id: :typed_uuid, this will use the base class to calculated the default type for the UUID. You could also set the id to :uuid and the default to false so when no ID is given it will error.