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

Most upvoted comments

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

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.

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.

class AddSiteToGetaways < ActiveRecord::Migration[5.0]
  def change
    add_reference :getaways, :site, type: :uuid, foreign_key: true
  end
end

I was also surprised by this behavior. I’m using this snippet to have all generated migrations use uuid as the primary key type:

Rails.application.config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
end

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:

Rails.application.config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid

  # Because I'm using :uuid for PKs, let me specify them as the default for FKs too.
  g.orm :active_record, foreign_key_type: :uuid
end

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 a foreign_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

g.orm :active_record, primary_key_type: :uuid

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

g.orm :active_record, force_foreign_key_type: :uuid

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 the type: 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 the type option and problem solved”.

begin
  require 'bundler/inline'
rescue LoadError => e
  $stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
  raise e
end

gemfile true do
  source 'https://rubygems.org'
  gem 'rails', github: 'rails/rails'
  gem 'pg'
end

require 'active_record'
require 'logger'

ActiveRecord::Base.logger = Logger.new STDOUT
ActiveRecord::Base.establish_connection adapter: 'postgresql',
                                        url: "postgres://#{ `whoami` }@localhost/rails-issue-23422"

ActiveRecord::Schema.define do
  enable_extension 'uuid-ossp'

  create_table 'dogs', default: -> { 'uuid_generate_v4()' },
                       force:   :cascade,
                       id:      :uuid
end

class CreateToys < ActiveRecord::Migration[5.0]
  def change
    create_table :toy, id: :uuid do |t|
      t.belongs_to :dog, foreign_key: true
    end
  end
end

CreateToys.migrate :up

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

  1. Configure Rails to use UUIDs:
config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
end
  1. Generate a migration with a foreign key, one way or another:
rails g migration add_location_to_users location:references
  1. Open /db/migrate/xxxxx_add_location_to_users.rb

Expected behavior

The generated migration should have specified type: :uuid in add_column.

Actual behavior

The generator did not specify the type in add_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

config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
  hacker = Module.new do
    def options_for_migration
      ar = Rails.application.config.generators.active_record
      return super unless %i[belongs_to references].include?(type) && \
                          ar[:primary_key_type] == :uuid

      { type: :uuid }.merge(super)
    end
  end
  require 'rails/generators/generated_attribute'
  Rails::Generators::GeneratedAttribute.prepend hacker
end

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 in t.references :user, type: :uuid, null: false, .... same for add_reference i.e add_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:

t.string :status
t.float :percent_complete
t.references :other_table

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-surprising t.<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 add type: :uuid to new references/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.

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 …

The type: :uuid is necessary here because ActiveRecord::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:

  1. If no type is specified and the referenced table exists, find the primary key for the referenced table and use that type.
  2. If no type is specified, but the referenced table does not exist, default to the primary_key_type specified for the generator (in the assumption that will be correct most of the time - except for legacy tables).
  3. If a type is specified, use that type, don’t bother sniffing.

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 uuids 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.