I need to add a new integer column to an existing table in my Rails app. The column can only have values 1, 2, 3, so I'd like to add a check constraint to the table/column. How do I specify this constraint within a Rails migration?
Rails migration does not provide any way to add Constraints, but you can still do it via migration but by passing actual SQL to execute()
Create Migration file:
ruby script/generate Migration AddConstraint
Now, in the migration file:
class AddConstraint < ActiveRecord::Migration
def self.up
execute "ALTER TABLE table_name ADD CONSTRAINT check_constraint_name CHECK (check_column_name IN (1, 2, 3) )"
end
def self.down
execute "ALTER TABLE table_name DROP CONSTRAINT check_constraint_name"
end
end
Rails 6.1+ Check Constraints
Rails 6.1 added basic support for check constraints to database migrations.
So now, a migration for adding a check constraint which restricts integer column values only to 1, 2, and 3 can be written as follows:
class AddConstraint < ActiveRecord::Migration
def up
add_check_constraint :table_name, 'check_column_name IN (1, 2, 3)', name: 'check_constraint_name'
end
def down
remove_check_constraint :table_name, name: 'check_constraint_name'
end
end
Here is a link to the relative PR where you can find more details about add_check_constraint and remove_check_constraint.
You can do it with Migration Validators gem. See details here: https://github.com/vprokopchuk256/mv-core
With that gem you'll be able to define inclusion validation on db level:
def change
change_table :table_name do |t|
t.integer :column_name, inclusion: [1, 2, 3]
end
end
moreover you is able to define how that validation should be defined and even error message that should be shown:
def change
change_table :posts do |t|
t.integer :priority,
inclusion: { in: [1, 2, 3],
as: :trigger,
message: "can't be anything else than 1, 2, or 3" }
end
end
you can even level up that validation from migration right to your model:
class Post < ActiveRecord::Base
enforce_migration_validations
end
and then validation defines in migration will be also defined as ActiveModel validation in your model:
Post.new(priority: 3).valid?
=> true
Post.new(priority: 4).valid?
=> false
Post.new(priority: 4).errors.full_messages
=> ["Priority can't be anything else than 1, 2, or 3"]
This answer is obsolete as of May 2021
I just published a gem for this: active_record-postgres-constraints. As the README there describes, you can use it with a db/schema.rb file, and it adds support for the following methods in migrations:
create_table TABLE_NAME do |t|
# Add columns
t.check_constraint conditions
# conditions can be a String, Array or Hash
end
add_check_constraint TABLE_NAME, conditions
remove_check_constraint TABLE_NAME, CONSTRAINT_NAME
Note that at this time, only postgres is supported.
I have just worked through getting a PostgreSQL CHECK constraint to work.
Nilesh's solution is not quite complete; the db/schema.rb file won't include the constraint, so tests and any deployments that use db:setup won't get the constraint. As per http://guides.rubyonrails.org/migrations.html#types-of-schema-dumps
While in a migration you can execute custom SQL statements, the
schema dumper cannot reconstitute those statements from the database.
If you are using features like this, then you should set the schema
format to :sql.
I.e., in config/application.rb set
config.active_record.schema_format = :sql
Unfortunately, if you're using PostgreSQL you may get an error when loading the resultant dump, see discussion at ERROR: must be owner of language plpgsql. I didn't want to go down the PostgreSQL configuration path in that discussion; plus in any case i'm fond of having a readable db/schema.rb file. So that ruled out custom SQL in the migration file for me.
The https://github.com/vprokopchuk256/mv-core gem suggested by Valera seems promising, but it only supports a limited set of constraints (and I got an error when I tried to use it, though that may be due to incompatibilities with other gems I'm including).
The solution (hack) I went with is to have the model code insert the constraint. Since it's kindof like a validation, that's where I put it:
class MyModel < ActiveRecord::Base
validates :my_constraint
def my_constraint
unless MyModel.connection.execute("SELECT * FROM information_schema.check_constraints WHERE constraint_name = 'my_constraint'").any?
MyModel.connection.execute("ALTER TABLE my_models ADD CONSTRAINT my_constraint CHECK ( ...the SQL expression goes here ... )")
end
end
Of course this does an extra select before each validation; if that's a problem a solution would be to put it in an "after connect" monkey patch such as discussed in How to run specific script after connected to oracle using rails? (You can't simply cache the result of the select, because the validation/constraint addition happens within a transaction that might get rolled back, so you need to check each time.)
You can use Sequel gem https://github.com/jeremyevans/sequel
Sequel.migration do
change do
create_table(:artists) do
primary_key :id
String :name
constraint(:name_min_length){char_length(name) > 2}
end
end
end
Related
I wrote a migration with the following (create new table named sources):
class CreateSources < ActiveRecord::Migration
def change
create_table :sources do |t|
t.string :name, null: false, default: ""
t.timestamps null: false
end
end
end
And then I modified my existing model :
class Property < ActiveRecord::Base
validates :source, allow_blank: true, inclusion: { in:
Source.all.map{ |source| source.name } }
I want to add validation to the property's source to only allow source from sources table.
And then when I run the migration, I got the following error:
ActiveRecord::StatementInvalid: Mysql2::Error: Table 'sources' doesn't exist: SELECT `sources`.* FROM `sources`
The problem is query of source table is occured when it hasn't been initialized yet.
Any tips on how I can get the migration to run?
This is run on production level. so I might can't drop all the migration and rearrange it.
Rails version 4.2.5
SQL version 5.7
Keep in mind that your Source.all.map{ |source| source.name } is going to be executed when the Property class is being loaded. The Source class might not be properly initialized at that point and there might not be a proper database connection set up. Also, you'll only access Source.all once so you'd have to restart your app if you added a new Source.
Instead, validate by hand:
class Property < ActiveRecord::Base
validate :valid_source
private
def valid_source
return if(source.blank?)
return if(Source.where(name: source).exists?)
errors.add(:source, 'Unknown source') # Or whatever you want to say
end
end
That way you're checking the sources table at the right time.
Also, I wouldn't expect the error you're seeing to occur in a migration. Perhaps you're using a model inside a migration, that is to be avoided.
As an aside, is there particular reason that you don't have belongs_to :source instead? Copying the name around like that is very error prone, using a reference (hopefully backed by a foreign key in the database) would be much safer.
Have you defined Source model? I hope so.
Here the problem looks like the loading of Property class takes priority before
migration is run and hence the issue.
I'm a RoR developer and I'm used to create my own databases using scaffold and such. However I was told to create a rails app to an existing populated database. I Searched and i found this:
1. write config/database.yml to reference your database.
2. Run "rake db:schema:dump" to generate db/schema.rb. Here's the
documentation:
$ rake -T db:schema:dump
...
rake db:schema:dump # Create a db/schema.rb file that can be
portably used against any DB supported by AR
3. Convert schema.rb into db/migrate/001_create_database.rb:
Class CreateMigration < ActiveRecord::Migration
def self.up
# insert schema.rb here
end
def self.down
# drop all the tables if you really need
# to support migration back to version 0
end
end
However I saw some comments saying that they lost their data and some saying that it worked. I can't take chances to lose the data from the database. Can someone please give me some more solid explanation? Or a better solution
I had the same problem but I can't use the solution above.
My database was created before than the ruby on rails app and it didn't have the schema_migrations table created, so when it ran this:
if ActiveRecord::Migrator.current_version > 2
# force the next migration 002_create_database.rb to be skipped
ActiveRecord::SchemaMigration.create(version: '2')
# the version '2' above is the version of the file which is (002 becomes 2)
end
it returned false in the if statement thus, it never skip the create_database migration in my case.
I was looking for some way where I can run the migrations and avoid to create tables again and, at the same time, new coders can run it and create the tables.
After some search I found the table_exists? function, so:
I created the schema migration using the rake db:schema:dump
Then I created my first migration: 001_create_database.rb and pasted the schema from the db/schema.rb file
Finally, I edited the migration adding and if statement before each create table:
if !table_exists?("CarsTable")
create_table "CarsTable" do |t|
t.bigint "Owner", null: false
t.string "Color", limit: 20, null: false
t.string "Make", limit: 40, null: false
t.string "Model", limit: 20, null: false
t.string "Plate", limit: 10, null: false
end
end
I tested this in 2 ways, with a database previously created and populated (for production case and my own case) and without any database (for new coders) and it worked.
Rails check all migration files in db/migrate/ and checks if they all are part of the "schema_migrations" table. Your code above will only work for fresh copy of your code (i.e. if I clone your Rails project), because migrations are run in the order of the filename. Since it is 00001_create_d... then it will be the first to be ran on my end, followed by other migration files you have.
You are right that you will then possibly lose data if you migrate, because the schema code you have will be ran after all of your migration files have already been migrated.
Now since it is only you and other developers working on the project, who cannot simply just do rake db:migrate, and that new cloners of your project will have no problems with your code above, then I could do the following below to force that 001_create_database.rb to be already part of your schema_migrations table, thereby skipping it, but not for new cloners (like myself).
IMPORTANT: backup your database first before doing below code
db/migrate/001_safeguard_create_database.rb
class SafeguardCreateDatabase < ActiveRecord::Migration
def up
# if current migration version already has a created database
if ActiveRecord::Migrator.current_version > 2
# force the next migration 002_create_database.rb to be skipped
ActiveRecord::SchemaMigration.create(version: '2')
# the version '2' above is the version of the file which is (002 becomes 2)
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
db/migrate/002_create_database.rb
class CreateDatabase < ActiveRecord::Migration
def up
# your schema.rb here
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
After creating these two files, try rake db:migrate. It should only process 001_safeguard_create_database, and skip 002_create_database because it is assumed that your current DB is already set up with it. The 002_create_database then will only be ran for new project users who do not have a DB yet; and for these users, these first two migration files be be ran first, followed by all of your other migrations.
I found a problem in one of legacy applications (outdated rails-3.0.20).
This application has lots of components and nested models. Problem existed only on one of production servers (same environments like other productions and mine dev).
There was model with name space which looks like
module Great
class Item
end
end
Table name was named great_items.
When i debug/open it on server with fault i've found that calculated table name was items istead of great_items.
$ Great::Item.all
#=> ActiveRecord::StatementInvalid: No attribute named `name` exists for table `items`
So i thought mby there is simmilar class with same namespace, I've checked it and it wasn't. My 2nd thought was to set table name explicit i tried
self.table_name = 'great_items'
# &
set_table_name 'great_items'
After this changes i run rails c and table name was setted fine:
$ Great::Item.table_name
#=> 'great_items'
But when i tried to obtain some items there was a freak error, which i could not understand till now!
$ Great::Item.all
#=> ActiveRecord::StatementInvalid: Mysql2::Error: Table 'db.items' doesn't exist: SELECT `great_items`.* FROM `items` WHERE `great_items`.`some_default_scope` = 0
As you can see in above example table has correct name for select values and in where statement, but in from value is incorrect.
I was curious so I've checked ActiveRecord::Base mysql adapter and there was some kind of catching table name so i tried to reset_table_name. Reset helped to setup expected name('great_items') but above errors didn't missed.
Problem dissapeared when i turn of class catching in production enviroment - but it wasn't solution.
Finally I kinda 'solved' this using reset_column_information after set_table_name, but i think it isn't good solution either.
My question is do you know what really could cause this issue and how to solve it without reloading class cache?
Assumed table names dont take into account the modules, as you noticed.
But as you already know, you can set it yourself, using:
self.table_name = 'great_items'
According to this doc, because you use 3.0.x, you have to use:
set_table_name "great_items"
This must be put on top of your class definition
Try this
class RenameOldTableToNewTable< ActiveRecord::Migration
def self.up
rename_table :old_table_name, :new_table_name
end
def self.down
rename_table :new_table_name, :old_table_name
end
end
For the reason, I've used mysql cmd insert into table_name (....) update custom_reports ...and hence I miss out on Model validations
validates_uniqueness_of :name
validates_presence_of :name, :description
How to validate now in rails way? Or, use the mysql way to validate(needs help in this way too)?
Rails validation and other ActiveRecord and ActiveModel magic don't work if you only execute custom SQL command. None of your model classes is even instantized then.
For Mysql (or any sql like DB), you can modify the column attribute to:
Unique (this would validate uniqueness)
Not null (this would validate presence)
I know doing the above with OCI8 and oracle would result in exceptions which I am guessing should be same with ActiveRecord and Mysql, so you should be handling your exceptions correctly
But as #Marek as said you should be relying on Active record and be doing things like
Model.create()
OR
model_instance.save()
If you want to find (and perhaps handle) the entries in your db that are not valid, try the following in the rails console:
ModelName.find_each do |item|
unless item.valid?
puts "Item ##{item.id} is invalid"
# code to fix the problem...
end
end
valid? runs the Validations again, but does not alter the data.
I'm trying to run rake test:units and I keep getting this:
Mysql::Error: Duplicate entry '2147483647' for key 1: INSERT INTO `ts_schema_migrations` (version) VALUES ('20081008010000')
The "ts_" is there because I have ActiveRecord::Base.table_name_prefix set. I'm confused because there is no value '20081008010000' already in the table, and there is no migration with the value '2147483647' (though the value does appear in the table).
In Rails' schema_statments.rb, there is the following:
def initialize_schema_migrations_table
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
unless tables.detect { |t| t == sm_table }
create_table(sm_table, :id => false) do |schema_migrations_table|
schema_migrations_table.column :version, :string, :null => false
end
...
In my development database, ts_schema_migrations.version is a VARCHAR. In test, though it's an INTEGER. I've dropped the tables and re-run the migrations (and/or a rake db:schema:load RAILS_ENV=test) several times. No changes.
Is something wrong with my MySQL adapter?
It looks as though your test schema is Rails 1.x somehow, whereas development is Rails 2. Perhaps you could set RAILS_ENV to test and run rake db:reset
It looks like you skipped some steps when upgrading from Rails 1.x to 2.0.
Go through and read the upgrade notes:
http://www.slashdotdash.net/2007/12/03/rails-2-upgrade-notes/
And the release notes:
http://weblog.rubyonrails.org/2007/12/7/rails-2-0-it-s-done
They will tell you all the steps you need to follow. Particularly regenerating all the scripts and migrating your database to the new system of database migrations by timestamp instead of incrementing migration id.