Skip to content

feat: add support for UUID keys#2

Open
chiperific wants to merge 3 commits intorubymonolith:mainfrom
chiperific:feat-add-support-for-uuid-keys
Open

feat: add support for UUID keys#2
chiperific wants to merge 3 commits intorubymonolith:mainfrom
chiperific:feat-add-support-for-uuid-keys

Conversation

@chiperific
Copy link

@chiperific chiperific commented Feb 13, 2026

Fixes #1

Add UUID primary key support

Summary

FixtureBot generates deterministic integer IDs using CRC32, but tables with UUID primary keys (common in PostgreSQL) aren't supported. This PR adds deterministic UUID generation using UUID v5 (RFC 4122), which hashes a namespace + name with SHA-1 to produce a stable UUID. Same inputs always produce the same UUID — matching the existing "stable IDs" philosophy.

It does rely on column.type, assumed to be determined by Rails' ActiveRecord. I'm not sure if we need to provide support for those not using ActiveRecord.

What changed

  • Key.generate_uuid — New method that produces deterministic UUIDs via UUID v5. Uses Digest::SHA1 from stdlib, no new dependencies.
  • Schema::Table — Added primary_key_type field (defaults to :integer). Validates only :integer and :uuid are accepted.
  • Row::Builder — Routes ID generation through a generate_key_for helper that checks the table's primary_key_type. Foreign keys and join table keys look up the referenced table's type, so an integer table referencing a UUID table gets the right key format.
  • FixtureSet — Passes schema.tables to the builder so it can resolve cross-table key types.
  • Rails::SchemaLoader — Auto-detects UUID primary keys via column.type == :uuid. Works for PostgreSQL and MariaDB 10.7+. MySQL and SQLite store UUIDs as char(36)/varchar(36) which the adapter reports as :string, so auto-detection falls back to :integer for those databases.

Design decisions

Decision Rationale
UUID v5 v5 is deterministic from inputs
RFC 4122 URL namespace Well-known constant from the spec; open to using a custom FixtureBot namespace instead if you prefer
primary_key_type defaults to :integer Full backward compatibility — existing schemas work unchanged
column.type == :uuid for detection Delegates to the adapter's type mapping rather than pattern-matching on sql_type strings

Test coverage

  • Key.generate_uuid: determinism, v5 format validation, uniqueness across records and tables
  • Full integration: UUID-only tables with belongs_to and join tables
  • Mixed integer/UUID: integer table referencing a UUID table
  • primary_key_type validation: rejects unsupported types
  • Per-adapter SchemaLoader specs: PostgreSQL (detects :uuid), MySQL (falls back), SQLite (falls back, plus no-PK edge case)

Backward compatibility

All existing tests pass unchanged. primary_key_type defaults to :integer, the tables: parameter defaults to {}, and generate_key_for falls back to Key.generate for anything that isn't :uuid.

module FixtureBot
module Key
# RFC 4122 URL namespace UUID, used as the base namespace for UUID v5 generation.
URL_NAMESPACE = "6ba7b811-9dad-11d1-80b4-00c04fd430c8"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the pre-defined namespace ID for URL was just arbitrary, our use case doesn't fit the classes given.

We append "fixturebot:" to ensure we don't have collisions should another gem use the same

We could create our own "starting point" using UUID v4 to generate a random one to serve as our "seed", but either way we should be collision proof.

namespace_bytes = [namespace_uuid.tr("-", "")].pack("H32")

# SHA-1 hash of namespace bytes + name (RFC 4122 Section 4.3)
hash = Digest::SHA1.digest(namespace_bytes + name.to_s)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think not having extra dependencies here is a bonus.

column = @connection.columns(table_name).find { |c| c.name == pk }
return :integer unless column

column.type == :uuid ? :uuid : :integer
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started by looking at the sql_type, but it's possible for a Rails app to use UUIDs even when the database that doesn't have UUIDs natively (e.g. using char(36)), so it seemed better to get the type from Rails then from the db itself.

defaults: definition.defaults[row.table],
join_tables: schema.join_tables
join_tables: schema.join_tables,
tables: schema.tables
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now pass the full hash of tables along in a few places, mostly for the sake of join tables.

For example:

table :tenants, singular: :tenant, columns: [:name], primary_key_type: :uuid
table :posts,   singular: :post,   columns: [:title, :tenant_id] do
  belongs_to :tenant, table: :tenants
end

When building a posts row, the builder needs to generate tenant_id. That foreign key must match the tenants table's key type (UUID), not the posts table's type (integer). So it needs to look up @tables[:tenants].primary_key_type to decide.

I don't love it, but I couldn't come up with a cleaner way.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to ensure we covered the Rails-supported databases (at least in theory since we're just using sqlite3 and stubbing out the rest), so I added a new directory and put adapter specific specs in it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also moved this spec in to the rails dir since that's where it lives in code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for UUIDs as keys

1 participant