rails: Rails 5 - Incorrect foreign key type when specifying references in model generator
While playing with Rails 5 (5.0.0.beta2), I’ve run into a problem with foreign keys and generated models.
I have two models:
class Gateway < ApplicationRecord
belongs_to :site
end
class Site < ApplicationRecord
has_many :gateways
end
I wanted to use Postgres and UUID primary keys for this, so I followed the examples at http://blog.mccartie.com/2015/10/20/default-uuid’s-in-rails.html, and enabled UUID primary keys in my config/application.rb
, as follows:
config.generators do |g|
g.orm :active_record, primary_key_type: :uuid
end
The generated migrations looked like this:
class EnableUuidExtension < ActiveRecord::Migration[5.0]
def change
enable_extension "uuid-ossp"
end
end
class CreateSites < ActiveRecord::Migration[5.0]
def change
create_table :sites, id: :uuid do |t|
t.string :name
t.timestamps
end
end
end
class CreateGateways < ActiveRecord::Migration[5.0]
def change
create_table :gateways, id: :uuid do |t|
t.string :name
t.string :ip_address
t.references :site, foreign_key: true
t.timestamps
end
end
end
When running the final migration, to create the gateways
table, I get this error message:
$ rails db:migrate
== 20160202103820 CreateGateways: migrating ===================================
-- create_table(:gateways, {:id=>:uuid})
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::DatatypeMismatch: ERROR: foreign key constraint "fk_rails_a0c8241d05" cannot be implemented
DETAIL: Key columns "site_id" and "id" are of incompatible types: integer and uuid.
: CREATE TABLE "gateways" ("id" uuid DEFAULT uuid_generate_v4() PRIMARY KEY, "name" character varying, "ip_address" character varying, "site_id" integer, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL, CONSTRAINT "fk_rails_a0c8241d05"
FOREIGN KEY ("site_id")
REFERENCES "sites" ("id")
) /Users/steve/heroku/guru-api/db/migrate/20160202103820_create_gateways.rb:3:in `change'
/Users/steve/heroku/guru-api/bin/rails:9:in `require'
/Users/steve/heroku/guru-api/bin/rails:9:in `<top (required)>'
/Users/steve/heroku/guru-api/bin/spring:13:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
ActiveRecord::StatementInvalid: PG::DatatypeMismatch: ERROR: foreign key constraint "fk_rails_a0c8241d05" cannot be implemented
DETAIL: Key columns "site_id" and "id" are of incompatible types: integer and uuid.
: CREATE TABLE "gateways" ("id" uuid DEFAULT uuid_generate_v4() PRIMARY KEY, "name" character varying, "ip_address" character varying, "site_id" integer, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL, CONSTRAINT "fk_rails_a0c8241d05"
FOREIGN KEY ("site_id")
REFERENCES "sites" ("id")
)
/Users/steve/heroku/guru-api/db/migrate/20160202103820_create_gateways.rb:3:in `change'
/Users/steve/heroku/guru-api/bin/rails:9:in `require'
/Users/steve/heroku/guru-api/bin/rails:9:in `<top (required)>'
/Users/steve/heroku/guru-api/bin/spring:13:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
PG::DatatypeMismatch: ERROR: foreign key constraint "fk_rails_a0c8241d05" cannot be implemented
DETAIL: Key columns "site_id" and "id" are of incompatible types: integer and uuid.
/Users/steve/heroku/guru-api/db/migrate/20160202103820_create_gateways.rb:3:in `change'
/Users/steve/heroku/guru-api/bin/rails:9:in `require'
/Users/steve/heroku/guru-api/bin/rails:9:in `<top (required)>'
/Users/steve/heroku/guru-api/bin/spring:13:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace)
It looks to me like the model generator is not checking to see what type of primary key is set on the referenced model - so it’s assuming (incorrectly, in this case) that it will be an integer type.
I’ve dumped my basic app into a public GitHub repo here: https://github.com/stephenorr/rails-api
If you need me to do anything else to help diagnose this, let me know.
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 30
- Comments: 43 (20 by maintainers)
Commits related to this issue
- Fix UUID foreign key Of course, this is expected behavior, silly me. https://github.com/rails/rails/issues/23422 — committed to CashCacheTracker/cashcache-api by sporkexec 8 years ago
- Prefer UUIDs for primary keys As we intend to be a federated system, we'll want to start with UUIDs for primary keys so we don't have to worry about identity collisions later on. This configures act... — committed to wecohere/nourish.party by zspencer 7 years ago
- Prefer UUIDs for primary keys (#44) As we intend to be a federated system, we'll want to start with UUIDs for primary keys so we don't have to worry about identity collisions later on. This conf... — committed to wecohere/nourish.party by zspencer 7 years ago
to solve this add uuid type as it says here: http://edgeguides.rubyonrails.org/active_record_postgresql.html
create_table :comments, id: :uuid, default: ‘gen_random_uuid()’ do |t|
t.belongs_to :post, type: :uuid
t.references :post, type: :uuid end
That’s expected. Migrations need to work the same everywhere, so they can’t vary their behaviour by inspecting the current state of the database.
I was also surprised by this behavior. I’m using this snippet to have all generated migrations use
uuid
as the primary key type:Perhaps one could opt-in to using
:uuid
as the default type for foreign keys in generated migrations (similar to what @jpwynn suggested). I like this approach because it very closely mirrors how one opts-in to:uuid
for primary keys:I regularly run into this issue because the behaviour is so counterintuitive. Having primary keys configured for UUIDs via
primary_key_type: :uuid
should make foreign keys in generated migrations UUIDs too. Or at least there should be aforeign_key_type: :uuid
setting.Such behaviour would be independent from the current state of the database and consistent with the existing generator behaviour for primary keys.
Why is this issue closed?
Rails clearly has a broken behavior here because
belongs_to/references
is not respecting a system-wide configuration. Would a PR fixing this be accepted?Ran into this in Rails 5.0.1… if Rails “knows” to globally use uuid for the reference via
it is counterintuitive in the extreme that when creating a foreign key reference to ANY new model that it assume an integer.
Seems to me when foreign keys are added in a migration, this should be the same default.
On one hand, an app COULD have some legacy tables that have integer primary key (in which case fixing the rails model generator so it relies upon the primary_key_type: :uuid would give a mismatch).
But on the other hand, right now, it currently ALWAYS fails on references to new tables if primary_key_type: :uuid is simply ignored as it is now.
So… it seems to me the better choice is to break ‘sometimes’ in the case of legacy integer-key tables (and require a manual edit to those migrations), as opposed to continue breaking ‘always’ on all new tables (and requiring a manual edit to every migration involving a foreign key).
If reluctant to change the default to use primary_key_type when creating foreign keys, a solution might be to introduce a similar setting for foreign keys that lets developers ‘opt in’ to using uuid for both primary and foreign keys
Just ran into this issue myself and was pretty confusing… Is it really not possible/desirable for the migration to introspect on the current state of the database?
Can confirm this issue still exists in the stable 6.0 release.
Same problem here, rails 5 master. I have explicitly configured the primary key type and migration to add a child table 💥.
Contrary to what @matthewd says above, no database inspection should be needed to follow the application wide explicit configuration.
As @rafaecheve pointed out, it is possible to manually set the foreign key type. However, the fact that rails doesn’t use the provided config is surprising. IMO the least-surprising behavior would be for the migration generator to automatically add the
type:
option to references.Less desirable (because it introduces yet another configuration and could allow for situations in which someone sets the two options to incompatible values) would be to add a
foreign_key_type
option to the generators so that developers can explicitly declare the intent for foreign keys to be a particular type. Then, as in the other–clearly superior in every way–behavior, the migration generator could automatically add thetype:
option to references.Regardless, while the current behavior violates the principle of least surprise and requires manual developer intervention, at least the error message was relatively clear.
@maclover7 here’s the executable test script you asked for. I was hesitant to include it because I think the script (and @stephenorr’s example app) only demonstrate the
PG::DatatypeMismatch
error. That error just a symptom of the bug in the migration generator. Without running the generator yourself, it’s probably pretty easy to say “just add thetype
option and problem solved”.Of course! ☺️
My argument is predicated on the principle of least surprise. I think it is surprising that Rails doesn’t assume every primary key is a UUID when the configuration is set as such. I’ll write this as if it’s a bug report to show what I think is a less surprising behavior!
Steps to reproduce
/db/migrate/xxxxx_add_location_to_users.rb
Expected behavior
The generated migration should have specified
type: :uuid
inadd_column
.Actual behavior
The generator did not specify the
type
inadd_column
.Note that this suggestion is just for Rails to make better assumptions. Right now, its assumption is that foreign keys always reference integers. My argument is that it should assume that foreign keys reference UUIDs if primary keys are set to be UUIDs.
This suggestion doesn’t require any knowledge of the schema, it just requires changing the generator to look at the
primary_key_type
config.P.S., I’m not sure what you mean when you say “migrations need to leave the database in a consistent state when run in different orders”. How does that work with foreign keys? The foreign table has to exist first, yeah?
I have also ran into this issue; running the latest production release of rails 4.
It was very surprising that that when we used the
references
method to tie to a table with a uuid id it created integer foreign key fields.I write a hot fix for this. Add below code to
config/application.rb
In your migration you can do the following and it will all work:
t.references :xxxxxxxx, type: :uuid, foreign_key: true
, sometimes automagical things rails does isn’t optimal …This problem still exists in Rails 6 and I am revisiting rails development after some yrs. Anyone facing the same problem should add
type: :uuid
int.references :user, type: :uuid, null: false, ....
same foradd_reference
i.eadd_reference :products, :category, type: :uuid, null: false, foreign_key: true
@stevenpetryk is right I think in the sense that rails shouldn’t be assuming integers as primary keys if it was configured to generate them as UUIDs
I’ll jump on the bandwagon and say this should definitely not be a closed issue - it’s not very often Rails behaves in surprising and unintuitive ways. It should definitely respect the primary key generator or have a foreign key generator setting.
@stevehill1981’s solution has my vote, but I also agree that just using the primary key setting (without sniffing) is a reasonable improvement. Otherwise, the
t.references
method feels like a misfeature and possibly should be deprecated and removed. Other people have said it’s surprising, but to illustrate the general principle:The method
.references
inhabiting the space where the datatype goes implies that the datatype will be inferred, either directly or with some reasonably-accurate heuristic. But if no inference is happening, then why was this syntax chosen instead of the less-surprisingt.<datatype>, references: :other_table
?I don’t actually care if the sniffing happens, I’d be perfectly happy with the generators respecting the primary key setting in the application config so that – the same way they know to add
id: :uuid
to new table definitions they addtype: :uuid
to newreferences/belongs_to
columns.I encountered this issue today while setting up a new database and having to manually edit all of the migrations I write to account for this is consistently tripping me up in what should be a relatively smooth process.
The
type: :uuid
is necessary here becauseActiveRecord::ConnectionAdapters::ReferenceDefinition
defaults to:bigint
, and doesn’t attempt to look up the type of the referenced column.I think the proper behaviour should be:
primary_key_type
specified for the generator (in the assumption that will be correct most of the time - except for legacy tables).I’m generally of the opinion that the sniffing should be done even if we’re not creating a foreign key - because it’s nonsensical to create a column of a different type to the one being referenced.
I’ve got some code that sort of works, but I’ve managed to break the tests in the process and I’m not confident in my ability to fix them 😢
Late to the party, but was also struck by how inelegantly ActiveRecord handles this, especially when you’ve explicitly defined the primary key type.
If the argument against implementing introspection is that the migration should operate consistently, then there should be no project wide generator option for uuid either. (I’m certainly not advocating it should be removed - but having a separate option for FK definitely makes sense).
This problem is plaguing a project I’m currently working on
Just ran into this myself! Could a member clarify whether a PR for this would be welcome or if this is going to be treated as intended behavior? I think most people trying UUIDs for the first time will be surprised by this.
Will give it a whirl later today if I get chance.
Why can’t this be reopened so that a generator option can be added and tracked?
Seems I’m a bit late to the party however I just came across the same issue and was quite surprised how rails handled the foreign key constraint. In my honest opinion it makes sense to have rails making the foreign keys
uuid
s if the primary key has been specified to be uuid. Worse case scenario we should be able to configure it ourselves by setting the foreign key type. This issue isn’t resolved so it’s quite weird that this is a closed issue already.