Rolling back a Rails migrations with execute - ruby-on-rails

I would like to know what Rails does when rolling back a migration file, if it has an execute with some raw SQL statement.
Here is a contrived example:
def change
add_column :my_table, :new_column
execute "update my_table set new_column = some_value where some_condition"
end
When rolling back this migration, does Rails silently ignore the execute portion of the migration?

If you look in command_recorder.rb (in the active record gem) then you'll see that some migration methods (eg add_column) have a corresponding invert_foo method that reverses the change. If there is no corresponding invert method then rails will raise an error. This is the case for it has no way of knowing how to reverse an arbitrary statement.
If you would like your migration to be reversible you need to tell rails how to reverse it. The general form for this is
reversible do |direction|
direction.up { ... }
direction.down {...}
end
The up and down methods can be called as many times as you need and will only yield to the block if the direction matches whether the migration is running or being rolled back.
In your case this would just be
reversible do |direction|
direction.up { execute "..." }
end
Since you don't need to undo changes to the column if the column is about to be dropped.

I would assume as much. You should split this into an up and a down migration.
Down should look something like this:
def down
raise ActiveRecord::IrreversibleMigration
end

Related

RSpec proper way to check created column

I've got POST endpoint which creates a new JourneyProgress record in my db.
post :enroll do
JourneyProgress.create!(user: current_user, journey: journey, percent_progress: 0.0, started_at: DateTime.now)
status :no_content
end
I want to check if percent_progress and started_at were set with below example:
let(:current_date) { 'Thu, 16 Jul 2020 17:08:02 +0200'.to_date }
before do
allow(DateTime).to receive(:now) { current_date }
end
it 'set starting progress' do
call
expect(JourneyProgress.last.started_at).to eq(current_date)
expect(JourneyProgress.last.percent_progress).to eq(0.0)
end
The specs will pass but I'm not sure if JourneyProgress.last.(some record name) is in line with convention. Is there a better way to check this?
If I change it to:
it 'set starting progress' do
expect(call.started_at).to eq(current_date)
...
end
I'm getting an error:
NoMethodError:
undefined method `started_at' for 204:Integer
If you really want to test the value of the started_at column, something like this would work.
it 'set starting progress' do
call
expect(JourneyProgress.last.started_at).to eq(current_date)
end
but I'd suggest that you think twice about what is worth testing in this scenario, it would make a lot more sense to check that a JourneyProgress record is being inserted into your DB and that the endpoint actually returns the correct HTTP Status code.
it 'persists the record in database' do
expect { call }.to change { JourneyProgress.count }.by(1)
end
it 'responds with no content status' do
call
expect(response).to have_http_status(:no_content)
end
As other comments state I'd also use 201 (created) instead of no content in this context.
It looks like you are trying to write an integration test that verifies that your software wrote a record to the database with the appropriate values for percent_progress and started_at.
You are correct to be concerned about using .last in your tests like you have. If you were to run your tests in parallel on a build server, there's a good chance that two different tests would both be adding records to the database at the same time (or in an indeterminate order), causing the test to be flaky. You could resolve this potential flakiness by returning the id of the newly created record and then looking up that record in the test after the call event. But, there's a better solution...
If you were to modify your migration for the JourneyProgress model to look like this:
create_table :journey_progress do |t|
t.user_id :integer
t.journey_id :integer
t.percent_progress :float, default: 0.0
t.timestamps
end
Then, you would be guaranteed that the percent_progress field would always default to 0.0. And, you could use the ActiveRecord managed created_at timestamp in lieu of the custom started_at timestamp that you would have to manage.
So, you won't have to test that either thing got set correctly, because you can trust ActiveRecord and your database to do the right thing because they've already been thoroughly tested by their authors.
Now, your code would look more like this:
post :enroll do
journey_progress = JourneyProgress.create!(
user: current_user,
journey: journey
)
status :created
end
And, your tests would look more like what Sebastian Delgado mentioned:
it 'persists the record in database' do
expect { call }.to change { JourneyProgress.count }.by(1)
end

How to stop a migration if a validation is false?

Requirements
I need to create a migration, for a live production database that does the following:
For each company verify if a set of names are instances of an utterance class associated with it.
If said validation doesn't exist have the validation fail and return an error.
If not continue migrating
Current behaviour
I tried this:
class AddFeature < ActiveRecord::Migration[5.1]
def change
run_migration = true
Company.all.each do |organization|
Company.product_types_names.each { |type| run_migration &= Utterance.exists?("utter_#{type.to_s}", organization.id) }
end
if run_migration
# my code
end
end
end
Although the changes to the database, don't occur I need the migration to stop with an error. Currently, the migration isn't stopped by any form of error when I an utterance doesn't exist.
Expected behavior
I would like to know how to simply return an error and stop the migration when one any of the instances don't exist.
Something like this:
class AddFeature < ActiveRecord::Migration[5.1]
def change
Company.all.each do |organization|
Company.product_types_names.each { |type| run_migration &= Utterance.exists?("utter_#{type.to_s}", organization.id) }
# return_errors_and stop the app if validation false
end
# my code
end
end
Generally speaking, it is not recommended to write your custom code in Rails migrations. Migrations are for manipulations on database schema. You manipulate on data.
Answering your question: you can simply stop your migration by raising an exception, such as:
raise StandardError, "Invalid data"
In this case the migration will be stopped and not marked as completed (migration version won't be saved to schema_migrations table in your database). On next call of rake db:migrate it will try to run that migration again.

saving multiple tables at once in rails

I have an action that update three tables at once like this:
def action_save
#user.update(param_param_list1)
#application.update(param_list2)
#college.update(param_list3)
end
but to make the program better, I want to either save all three together at once or not at all
Use an ActiveRecord::Transaction:
def action_save
#college.transaction do
#user.update!(param_param_list1)
#application.update!(param_list2)
#college.update!(param_list3)
end
end
A transaction ensures that all the database action within that block are performed. Or if there is an error, then the whole transaction is rolled back.

Stop rails from escaping hstore array in views

I'm trying to use a postgreSQL column of type hstore array and everything seems to work just fine. However, my views escapes the array and turns it to bad formatted string. My code looks like that:
Migration:
class AddVariantsToItem < ActiveRecord::Migration
def change
execute 'CREATE EXTENSION hstore'
add_column :items, :variants, :hstore, array: true, default: []
end
end
And now, if i will, for instance, use Item.last.variants in rails console, it gives me a proper array
> Item.last.variants
#=> [{"name"=>"Small", "price"=>"12.00"}, {"name"=>"Medium", "price"=>"20.00"}]
However, using the exact same code in slim views gives me a escaped string:
div
= debug Item.last.variants
/ Gives me:
/ '{"\"name\"=>\"Small\", \"price\"=>\"12.00\"","\"name\"=>\"Medium\", \"price\"=>\"20.00\""}'
Using raw, == or .html_save does not changes anything. Can anyone tell me if i can do anything about it?
Item.last.variants is an Array. When putting something into view, its being stringified (I'm not sure, but I think its to_s method or inspect).
My advice is that you shouldn't put whole objects. In this particular example, I think you should iterate over it and show data manually.

Rails Migrations - Modify rows based on condition

I need to update a table data in database using RAILS migrations.
Sample:
Table: Table_A(Col_A(number), Col_B(varchar),...)
Query: UPDATE Table_A SET Col_B = "XXX" where Col_B = "YYY"
What would be the best way to do this using RAILS Migrations. I am not even sure if RAILS migrations is the way to go for updating data in database. Any explanation would be helpful.
It's usually better to do these sorts of big data updates in a rake task. I usually write them so they have two versions: rake change_lots_of_data:report and rake change_lots_of_data:update. The 'report' version just executes the where clause and spits out a list of what would be changed. The 'update' version uses the very same where clause but makes the changes.
Some advantages of doing it this way are:
Migrations are saved for changing the database structure
You can run the 'report' version as often as you want to make sure the right records are going to be updated.
It's easier to unit test the class called by the rake task.
If you ever need to apply the same criteria to make the change again, you can just run the rake task again. It's possible but trickier to do that with migrations.
I prefer to do any database data changes in a rake task so that's it's
Obvious
Repeatable
Won't later be executed via rake db:migrate
The code:
namespace :update do
desc "Update table A to set Col_B to YYY"
task :table_a => :environment do
TableA.where(Col_B: "YYY").update_all(Col_B: "XXX")
end
end
end
Then you can rake update:table_a to execute the update.
This should be done in a rake task...
namespace :onetime do
task :update_my_data => :environment do
TableA.where(Col_B: "YYY").update_all(Col_B: "XXX")
end
end
Then after you deploy:
rake onetime:update_my_data
At my company we delete the contents of the onetime namespace rake task after it's been run in production. Just a convention for us I guess.
More details about the update_all method: http://apidock.com/rails/ActiveRecord/Relation/update_all
You can do like this:
class YourMigration < ActiveRecord::Migration
def up
execute('UPDATE Table_A SET Col_B = "XXX" where Col_B = "YYY"')
end
def down
end
end
Or:
class YourMigration < ActiveRecord::Migration
def up
update('UPDATE Table_A SET Col_B = "XXX" where Col_B = "YYY"')
end
def down
end
end
ActiveRecord::Base.connection.execute("update Table_A set Col_B = 'XXX' where Col_B = 'YYY')

Resources