Conversation
| 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" |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
endWhen 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Also moved this spec in to the rails dir since that's where it lives in code.
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. UsesDigest::SHA1from stdlib, no new dependencies.Schema::Table— Addedprimary_key_typefield (defaults to:integer). Validates only:integerand:uuidare accepted.Row::Builder— Routes ID generation through agenerate_key_forhelper that checks the table'sprimary_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— Passesschema.tablesto the builder so it can resolve cross-table key types.Rails::SchemaLoader— Auto-detects UUID primary keys viacolumn.type == :uuid. Works for PostgreSQL and MariaDB 10.7+. MySQL and SQLite store UUIDs aschar(36)/varchar(36)which the adapter reports as:string, so auto-detection falls back to:integerfor those databases.Design decisions
primary_key_typedefaults to:integercolumn.type == :uuidfor detectionsql_typestringsTest coverage
Key.generate_uuid: determinism, v5 format validation, uniqueness across records and tablesprimary_key_typevalidation: rejects unsupported types:uuid), MySQL (falls back), SQLite (falls back, plus no-PK edge case)Backward compatibility
All existing tests pass unchanged.
primary_key_typedefaults to:integer, thetables:parameter defaults to{}, andgenerate_key_forfalls back toKey.generatefor anything that isn't:uuid.