A migration to add unique constraint to a combination of columns - ruby-on-rails

What I need is a migration to apply unique constraint to a combination of columns. i.e. for a people table, a combination of first_name, last_Name and Dob should be unique.

add_index :people, [:firstname, :lastname, :dob], unique: true

According to howmanyofme.com, "There are 46,427 people named John Smith" in the United States alone. That's about 127 years of days. As this is well over the average lifespan of a human being, this means that a DOB clash is mathematically certain.
All I'm saying is that that particular combination of unique fields could lead to extreme user/customer frustration in future.
Consider something that's actually unique, like a national identification number, if appropriate.
(I realise I'm very late to the party with this one, but it could help future readers.)

You may want to add a constraint without an index. This will depend on what database you're using. Below is sample migration code for Postgres. (tracking_number, carrier) is a list of the columns you want to use for the constraint.
class AddUniqeConstraintToShipments < ActiveRecord::Migration
def up
execute <<-SQL
alter table shipments
add constraint shipment_tracking_number unique (tracking_number, carrier);
SQL
end
def down
execute <<-SQL
alter table shipments
drop constraint if exists shipment_tracking_number;
SQL
end
end
There are different constraints you can add. Read the docs

For completeness sake, and to avoid confusion here are 3 ways of doing the same thing:
Adding a named unique constraint to a combination of columns in Rails 5.2+
Let's say we have Locations table that belongs to an advertiser and has column reference_code and you only want 1 reference code per advertiser. so you want to add a unique constraint to a combination of columns and name it.
Do:
rails g migration AddUniquenessConstraintToLocations
And make your migration look either something like this one liner:
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
add_index :locations, [:reference_code, :advertiser_id], unique: true, name: 'uniq_reference_code_per_advertiser'
end
end
OR this block version.
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
change_table :locations do |t|
t.index ['reference_code', 'advertiser_id'], name: 'uniq_reference_code_per_advertiser', unique: true
end
end
end
OR this raw SQL version
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
execute <<-SQL
ALTER TABLE locations
ADD CONSTRAINT uniq_reference_code_per_advertiser UNIQUE (reference_code, advertiser_id);
SQL
end
end
Any of these will have the same result, check your schema.rb

Hi You may add unique index in your migration to the columns for example
add_index(:accounts, [:branch_id, :party_id], :unique => true)
or separate unique indexes for each column

In the typical example of a join table between users and posts:
create_table :users
create_table :posts
create_table :ownerships do |t|
t.belongs_to :user, foreign_key: true, null: false
t.belongs_to :post, foreign_key: true, null: false
end
add_index :ownerships, [:user_id, :post_id], unique: true
Trying to create two similar records will throw a database error (Postgres in my case):
ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_ownerships_on_user_id_and_post_id"
DETAIL: Key (user_id, post_id)=(1, 1) already exists.
: INSERT INTO "ownerships" ("user_id", "post_id") VALUES ($1, $2) RETURNING "id"
e.g. doing that:
Ownership.create!(user_id: user_id, post_id: post_id)
Ownership.create!(user_id: user_id, post_id: post_id)
Fully runnable example: https://gist.github.com/Dorian/9d641ca78dad8eb64736173614d97ced
db/schema.rb generated: https://gist.github.com/Dorian/a8449287fa62b88463f48da986c1744a

If you are creating a new table just add unique: true
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :title, unique: true
t.text :body
t.references :user, foreign_key: true
t.timestamps
end
add_index :posts, :user_id, unique: true
end
end

Related

Ruby on Rails creating migration to reverse UUID primary keys

I'm currently in the process of writing code to convert our databases default ID columns to a UUID format.
One example of the code that I have for our migration is
def up
remove_reference :examples, :user, null: false, foreign_key: true
add_column :users, :uuid, :uuid, default: "gen_random_uuid()", null: false
change_table :users do |t|
t.remove :id, type: :id
t.rename :uuid, :id
end
execute "ALTER TABLE users ADD PRIMARY KEY (id);"
add_reference :examples, :user, null: false, foreign_key: true, type: :uuid
end
Essentially this allowed me to convert my ID column to a UUID format.
I created a down function so I would be able to rollback but it fails due to this error ERROR: column "user_id" of relation "examples" contains null values
I realize that there would be an issue because once there is data in the database it would be unable to rollback and create the correct references again. Does anyone have any ideas on how I should work on my down function?
def down
remove_reference :examples, :user, null: false, foreign_key: true, type: :uuid
execute 'ALTER TABLE users DROP CONSTRAINT users_pkey'
add_column :users, :new_id, :primary_key
change_table :users do |t|
t.remove :id, type: :uuid
t.rename :new_id, :id
end
add_reference :examples, :user, null: false, foreign_key: true
end
Does anyone have any suggestions on how I should proceed with this? The original migration was in one change function, but it would be unable to rollback due to the execute block.
Be careful doing this kind of database change in one-shot. I would suggest you to break into steps.
First step (new column uuid)
Create the new column for the uuid
def up
add_column :users, :uuid, :uuid, default: "gen_random_uuid()", null: false
add_column :examples, :user_uuid, :uuid
end
Adapt your code to populate the examples.user_uuid column with the recent created column; You can easily achieve this by creating a model callback, feeling the user_uuid automatically.
If your database has a GB of data, consider adding the uuid as nullable, and populate the column using queues or in batches. The new records will be already filled.
We have now two new columns, with new data comming and all synced
Second step (the renaming)
Once populated and working with new columns, is time to rename the columns and associate the new keys.
def up
change_table :examples do |t|
t.rename :user_id, type: :old_user_id
t.rename :user_uuid, :user_id
end
change_table :users do |t|
t.rename :id, type: :old_id
t.rename :uuid, :id
end
execute "ALTER TABLE users ADD PRIMARY KEY (id);"
add_reference :examples, :user, null: false, foreign_key: true, type: :uuid
end
def down
# ...
end
Rember to remove, or review the model code changed before, to support this new columns or ignore.
Now we have changed the columns, without loosing the old reference.
Be careful here, if your database is big, you may lock your operation. Perhaps you may need a maintenance window here.
Third step (removing the old columns)
Now we can remove the old columns and everything should work fine
note: Always be careful when making this kind of change into your database. It is very risk to perform something like this. If you want to go on, simulate several times the step of renaming. Make snapshot of your database before performing and inform your clients that might have a downtime in your service.
I don't know why you want to change your primary keys to be an uuid, this costs a lot to the database to query and join data. It's more complicated to compare an UUID than an integer. Consider just create a new indexed column uuid into your tables and let the database to join and based on this field.

Rails belongs_to migration file results in index is too long error [duplicate]

I am trying to add a unique index that gets created from the foreign keys of four associated tables:
add_index :studies,
["user_id", "university_id", "subject_name_id", "subject_type_id"],
:unique => true
The database’s limitation for the index name causes the migration to fail. Here’s the error message:
Index name 'index_studies_on_user_id_and_university_id_and_subject_name_id_and_subject_type_id' on table 'studies' is too long; the limit is 64 characters
How can I handle this? Can I specify a different index name?
Provide the :name option to add_index, e.g.:
add_index :studies,
["user_id", "university_id", "subject_name_id", "subject_type_id"],
unique: true,
name: 'my_index'
If using the :index option on references in a create_table block, it takes the same options hash as add_index as its value:
t.references :long_name, index: { name: :my_index }
You can change the index name in column definitions within a create_table block (such as you get from the migration generator).
create_table :studies do |t|
t.references :user, index: {:name => "index_my_shorter_name"}
end
In PostgreSQL, the default limit is 63 characters. Because index names must be unique it's nice to have a little convention. I use (I tweaked the example to explain more complex constructions):
def change
add_index :studies, [:professor_id, :user_id], name: :idx_study_professor_user
end
The normal index would have been:
:index_studies_on_professor_id_and_user_id
The logic would be:
index becomes idx
Singular table name
No joining words
No _id
Alphabetical order
Which usually does the job.
You can also do
t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
as in the Ruby on Rails API.
Similar to the previous answer: Just use the 'name' key with your regular add_index line:
def change
add_index :studies, :user_id, name: 'my_index'
end
I'm afraid none of these solutions worked for me. Perhaps because I was using belongs_to in my create_table migration for a polymorphic association.
I'll add my code below and a link to the solution that helped me in case anyone else stumbles upon when searching for 'Index name is too long' in connection with polymorphic associations.
The following code did NOT work for me:
def change
create_table :item_references do |t|
t.text :item_unique_id
t.belongs_to :referenceable, polymorphic: true
t.timestamps
end
add_index :item_references, [:referenceable_id, :referenceable_type], name: 'idx_item_refs'
end
This code DID work for me:
def change
create_table :item_references do |t|
t.text :item_unique_id
t.belongs_to :referenceable, polymorphic: true, index: { name: 'idx_item_refs' }
t.timestamps
end
end
This is the SO Q&A that helped me out: https://stackoverflow.com/a/30366460/3258059
I have a project that uses generators a lot and needed this to be automatic, so I copied the index_name function from the rails source to override it. I added this in config/initializers/generated_index_name.rb:
# make indexes shorter for postgres
require "active_record/connection_adapters/abstract/schema_statements"
module ActiveRecord
module ConnectionAdapters # :nodoc:
module SchemaStatements
def index_name(table_name, options) #:nodoc:
if Hash === options
if options[:column]
"ix_#{table_name}_on_#{Array(options[:column]) * '__'}".slice(0,63)
elsif options[:name]
options[:name]
else
raise ArgumentError, "You must specify the index name"
end
else
index_name(table_name, index_name_options(options))
end
end
end
end
end
It creates indexes like ix_assignments_on_case_id__project_id and just truncates it to 63 characters if it's still too long. That's still going to be non-unique if the table name is very long, but you can add complications like shortening the table name separately from the column names or actually checking for uniqueness.
Note, this is from a Rails 5.2 project; if you decide to do this, copy the source from your version.
I had this issue, but with the timestamps function. It was autogenerating an index on updated_at that exceeded the 63 character limit:
def change
create_table :toooooooooo_loooooooooooooooooooooooooooooong do |t|
t.timestamps
end
end
Index name 'index_toooooooooo_loooooooooooooooooooooooooooooong_on_updated_at' on table 'toooooooooo_loooooooooooooooooooooooooooooong' is too long; the limit is 63 characters
I tried to use timestamps to specify the index name:
def change
create_table :toooooooooo_loooooooooooooooooooooooooooooong do |t|
t.timestamps index: { name: 'too_loooooooooooooooooooooooooooooong_updated_at' }
end
end
However, this tries to apply the index name to both the updated_at and created_at fields:
Index name 'too_long_updated_at' on table 'toooooooooo_loooooooooooooooooooooooooooooong' already exists
Finally I gave up on timestamps and just created the timestamps the long way:
def change
create_table :toooooooooo_loooooooooooooooooooooooooooooong do |t|
t.datetime :updated_at, index: { name: 'too_long_on_updated_at' }
t.datetime :created_at, index: { name: 'too_long_on_created_at' }
end
end
This works but I'd love to hear if it's possible with the timestamps method!
create_table :you_table_name do |t|
t.references :studant, index: { name: 'name_for_studant_index' }
t.references :teacher, index: { name: 'name_for_teacher_index' }
end

Rails 5.1 & Postgres - adding unique constraint on two columns, when one is a foreign key

So I read this question, answer and the comments, but it doesn't answer my case, which is what to do when of the columns is a foreign key?
Here is my original migration to create the table in question:
class CreateTemplates < ActiveRecord::Migration[5.1]
def change
create_table :templates, id: :uuid do |t|
t.references :account, type: :uuid, foreign_key: true
t.string :name
t.text :info
t.string :title
t.timestamps
end
end
end
Since account_id is a foreign_key (and identifies the customer) it will appear in almost all (99%) of queries on this table.
Now it has been decided that name should be unique to account, so the model has been updated:
validates_uniqueness_of :name, scope: [:account]
So once I add the joint index:
add_index :templates, [:name, :account_id], unique: true
should I delete the index on account_id?
I ask because in SQLLite (see this), it seems the answer would be that I don't need the single index on account_id and to create my new index with account_id in the first position:
add_index :templates, [:account_id, :name], unique: true
I'm using postgres, so does the same idea apply?
You have to add extra index if it's not the first index.
So if you have this:
add_index :templates, [:name, :account_id], unique: true
then you should not delete the original :account_id foreign key index, since it is second index.
I recommend you to read about index implementations. It's pretty interesting and you can learn a lot from it.

How can I add a column to reference to another table on RoR?

Here is the Customer:
class CreateCustomer < ActiveRecord::Migration
def self.up
create_table :customers do |t|
t.column :email, :string, :null => false
end
end
def self.down
drop_table :customers
end
end
And this is the customer Info:
class CustomerInfo < ActiveRecord::Migration
def self.up
create_table :statuses do |t|
t.column :statuses, :string, :null => false
end
end
def self.down
drop_table :status
end
end
What I would like to do is the customer and customer Info have a one to one relationship. How can I do it in a new migration? thank you.
When you want a 1 to 1 in Rails, you have to decide which one of the models will store the foreign key. In your case, you probably want status to store the fk, so add an integer column called customer_id to the status table. Then you can add the has_one/belongs_to on Customer and Status. belongs_to always goes on the model with the foreign key.
Also I'm not sure if Rails will like you calling your table with the singular name, so you will probably have to do some extra work if you really want to call it 'status' instead of 'statuses'
You can try following thing in your next migration
add_column :customer_infos , :customer_id , :integer ,:references=>"customers" , :null=>:true
Then you can add the has_one/belongs_to on Customer and Cusomer_infos .
You can also execute an SQL statement.
statement = "ALTER TABLE users CHANGE id id SMALLINT( 5 ) UNSIGNED NOT NULL AUTO_INCREMENT" ActiveRecord::Base.connection.execute(statement)
you can entry manually in your migration
Note this is just an example. The final SQL statement syntax depends on the database.

Foreign Key Issues in Rails

Took me a while to track down this error but I finally found out why. I am modeling a card game using the Rails framework. Currently my database looks (mostly) like this:
cards cards_games games
----- ----------- -----
id id id
c_type card_id ...
value game_id other_stuff
And the Rails ActiveRecord card.rb and game.rb currently look like this
#card.rb
class Card < ActiveRecord::Base
has_and_belongs_to_many :player
has_and_belongs_to_many :game
has_and_belongs_to_many :cardsInPlay, :class_name => "Rule"
end
#game.rb
class Game < ActiveRecord::Base
has_and_belongs_to_many :cards
has_many :players
has_one :rules, :class_name => Rule
end
When I attempt to run a game and there are multiple games (more than 1), I get the error
ActiveRecord::StatementInvalid in GameController#start_game
# example
Mysql::Error: Duplicate entry '31' for key 1: INSERT INTO `cards_games` (`card_id`, `id`, `game_id`) VALUES (31, 31, 7)
Every time the action fails, cardid == id. This, I assume, has something with how Rails inserts the data into the database. Since there is no cardsgames object, I think it is just pulling card_id into id and inserting it into the database. This works fine until you have two games with the same card, which violates the primary key constraint on cardsgames. Being affluent with databases, my first solution to this problem was to try to force rails to follow a "real" definition of this relationship by dropping id and making cardid and gameid a primary key. It didn't work because the migration couldn't seem to handle having two primary keys (despite the Rails API saying that its okay to do it.. weird). Another solution for this is to omit the 'id' column in the INSERT INTO statement and let the database handle the auto increment. Unfortunately, I don't know how to do this either.
So, is there another work-around for this? Is there some nifty Rails trick that I just don't know? Or is this sort of structure not possible in Rails? This is really frustrating because I know what is wrong and I know several ways to fix it but due to the constraints of the Rail framework, I just cannot do it.
has_and_belongs_to_many implies a join table, which must not have an id primary key column. Change your migration to
create_table :cards_games, :id => false do ...
as pointed out by Matt. If you will only sleep better if you make a key from the two columns, create a unique index on them:
add_index :cards_games, [ :card_id, :game_id ], :unique => true
Additionally, your naming deviates from Rails convention and will make your code a little harder to read.
has_and_belongs_to_many defines a 1:M relationship when looking at an instance of a class. So in Card, you should be using:
has_and_belongs_to_many :players
has_and_belongs_to_many :games
Note plural "players" and "games". Similarly in Game:
has_one :rule
This will let you drop the unnecessary :class_name => Rule, too.
To drop the ID column, simply don't create it to begin with.
create_table :cards_rules, :id => false do ...
See Dr. Nics composite primary keys
http://compositekeys.rubyforge.org/
I found the solution after hacking my way through. I found out that you can use the "execute" function inside of a migration. This is infinitely useful and allowed me to put together an non-elegant solution to this problem. If anyone has a more elegant, more Rails-like solution, please let me know. Here's the solution in the form of a migration:
class Make < ActiveRecord::Migration
def self.up
drop_table :cards_games
create_table :cards_games do |t|
t.column :card_id, :integer, :null => false
t.column :game_id, :integer, :null => false
end
execute "ALTER TABLE cards_games DROP COLUMN id"
execute "ALTER TABLE cards_games ADD PRIMARY KEY (card_id, game_id)"
drop_table :cards_players
create_table :cards_players do |t|
t.column :card_id, :integer, :null => false
t.column :player_id, :integer, :null => false
end
execute "ALTER TABLE cards_players DROP COLUMN id"
execute "ALTER TABLE cards_players ADD PRIMARY KEY (card_id, player_id)"
drop_table :cards_rules
create_table :cards_rules do |t|
t.column :card_id, :integer, :null => false
t.column :rule_id, :integer, :null => false
end
execute "ALTER TABLE cards_rules DROP COLUMN id"
execute "ALTER TABLE cards_rules ADD PRIMARY KEY (card_id, rule_id)"
end
def self.down
drop_table :cards_games
create_table :cards_games do |t|
t.column :card_id, :integer
t.column :game_id, :integer
end
drop_table :cards_players
create_table :cards_players do |t|
t.column :card_id, :integer
t.column :player_id, :integer
end
drop_table :cards_rules
create_table :cards_rules do |t|
t.column :card_id, :integer
t.column :rule_id, :integer
end
end
end
You might want to check out this foreign_key_migrations plugin

Resources