Rails - DB Level Validation for `:start_date` and `:end_date` - ruby-on-rails

I have a question related to the following code in Rails:
class CreateEmployments < ActiveRecord::Migration[6.0]
def change
create_table :employments do |t|
t.string :title, null: false
t.string :company_name
t.datetime :start_date
t.datetime :end_date
t.integer :user_id
t.timestamps
end
end
end
I'm trying to disallow the db to accept any :start_date value greater than :end_date . I want :end_date to be always greater than :start_date and want to do it at the db level. Is there a way to do it?
I know I can use model field validations, but I want to implement it at db level too. Any tips?
Thanks in advance!

Rules on DB level are called constraints.
class AddEmploymentsDateConstraint < ActiveRecord::Migration
def self.up
execute "ALTER TABLE employments ADD CONSTRAINT employments_date_check CHECK (end_date > start_date)"
end
def self.down
execute "ALTER TABLE employments DROP CONSTRAINT employments_date_check"
end
end
It is important to know what DB is used. For instance, if it is used SQLite, can not use ALTER TABLE syntax to add constraint - only on CREATE TABLE can add constraint. See answer.
Additionaly, rails 6.1+ supports constraints, see another answer.

Related

A migration to add unique constraint to a combination of columns

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

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.

How to set a default value for a datetime column to record creation time in a migration?

Consider the table creation script below:
create_table :foo do |t|
t.datetime :starts_at, :null => false
end
Is it's possible to set the default value as the current time?
I am trying to find a DB independent equivalent in rails for the SQL column definitions given below:
Oracle Syntax
start_at DATE DEFAULT SYSDATE()
MySQL Syntax
start_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
OR
start_at DATETIME DEFAULT NOW()
This is supported now in Rails 5.
Here is a sample migration:
class CreatePosts < ActiveRecord::Migration[5.0]
def change
create_table :posts do |t|
t.datetime :modified_at, default: -> { 'CURRENT_TIMESTAMP' }
t.timestamps
end
end
end
See discussion at https://github.com/rails/rails/issues/27077 and answer there by prathamesh-sonpatki
You can add a function in a model like this:
before_create :set_foo_to_now
def set_foo_to_now
self.foo = Time.now
end
So that the model will set the current time in the model.
You can also place some sql code in the migration for setting the default value at the database level, something like:
execute 'alter table foo alter column starts_at set default now()'
Setting something like this:
create_table :foo do |t|
t.datetime :starts_at, :null => false, :default => Time.now
end
causes executing the Time.now function during migrating so then the table in database is created like this:
create table foo ( starts_at timestamp not null default '2009-01-01 00:00:00');
but I think that it is not what you want.
If you need to change an existing DateTime column in Rails 5 (rather than creating a new table as specified in other answers) so that it can take advantage of the default date capability, you can create a migration like this:
class MakeStartsAtDefaultDateForFoo < ActiveRecord::Migration[5.0]
def change
change_column :foos, :starts_at, :datetime, default: -> { 'CURRENT_TIMESTAMP' }
end
end
Active Record automatically timestamps create and update operations if the table has fields named created_at/created_on or updated_at/updated_on. Source - api.rubyonrails.org
You don't need to do anything else except to have that column.
I was searching for a similar solutions but I ended using https://github.com/FooBarWidget/default_value_for.
The default_value_for plugin allows one to define default values for ActiveRecord models in a declarative manner. For example:
class User < ActiveRecord::Base
default_value_for :name, "(no name)"
default_value_for :last_seen do
Time.now
end
end
u = User.new
u.name # => "(no name)"
u.last_seen # => Mon Sep 22 17:28:38 +0200 2008
I usually do:
def change
execute("
ALTER TABLE your_table
ALTER COLUMN your_column
SET DEFAULT CURRENT_TIMESTAMP
")
end
So, your schema.rb is going to have something like:
create_table "your_table", force: :cascade do |t|
t.datetime "your_column", default: "now()"
end
Did you know that upserts fail unless you have a default updated_at/created_at????
there is no migration flag which automatically does this, you have to manually include an options object with a default key
create_table :table_foos do |t|
#...
# date with timestamp
t.datetime :last_something_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
# standard timestamps
t.timestamps({default: -> { "CURRENT_TIMESTAMP" }})
end
In the answer given by #szymon-lipiński (Szymon Lipiński), the execute method didn't work for me. It was throwing a MySQL syntax error.
The MySQL syntax which worked for me is this.
execute "ALTER TABLE mytable CHANGE `column_name` `column_name` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP"
So to set the default value for a datetime column in migration script can be done as follows:
def up
create_table :foo do |t|
t.datetime :starts_at, :null => false
end
execute "ALTER TABLE `foo` CHANGE `starts_at` `starts_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP"
end

id field without autoincrement option in migration

I have a DB migration like so:
class CreateParticipations < ActiveRecord::Migration
def self.up
create_table(:participations, :primary_key => 'Seat') do |t|
t.integer :Seat
t.string :Nickname
t.string :Clan
t.string :FirstName
t.string :LastName
t.string :Email
t.boolean :Payed
t.timestamps
end
end
def self.down
drop_table :participations
end
end
Now, seat is created with an Auto increment. However, I do not want that. I want it without an auto increment. I will define Seat myself in my Logic.
I have been looking around but I cannot find how to disable auto_increment.
How do I do this? Except for manually doing it in MySQL.
For the record, if you absolutely need to do this (it shouldn't happen often), here's the way to do a non-autoincrementing primary key with the Rails migration DSL:
create_table(:table_name, :id => false) do |t|
t.integer :id, :options => 'PRIMARY KEY'
end
That will work for MySQL anyway, if your DB uses different syntax to specify a primary key, replace the :options => 'PRIMARY KEY' with whatever that is.
This question is 3 years old, but incase anyone is wondering 3 years later, like I was, all you do is "change_column" in the event the table is already created:
change_column(:table_name, :id, :integer, :null => false)
This should work in Rails 2.x and 3.x.
O
Not saying its a good idea, but here's how I did it for SQLite3 - just replace that SQLiteAdapter with your DB's adapter - you might do this cleaner/short with a call to define_method
class AbstractAdapter
end
module ActiveRecord
module ConnectionAdapters
class SQLiteAdapter < AbstractAdapter
def supports_autoincrement?
false
end
end
end
end
<then you migration>
or
class SomeMigration < ActiveRecord::Migration
def change
create_table :table do |t|
ActiveRecord::ConnectionAdapters::SQLiteAdapter.send :define_method, :supports_autoincrement? do false end
t.integer etc
end
end
end
Of course just change the adapter for other db's
Is there a reason you can't use rails' id key and manually add an index called Seat?
I've seen some hacks just to get rails to -work- on non-increment-pk databases. I don't think it's an option. If I recall, that's how rails accesses all its per-row functionality.
Honestly, how -absolutely- do you need that slight boost in efficiency of ignoring rails' structure?
I think the real answer is "you can't." Activerecord has a few things it will not bend on.

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