Dual booting when there is a breaking database change - ruby-on-rails

I am upgrading an app from 6.0 to 6.1, using the next_rails dual boot gem. As part of the upgrade I needed to execute rails active_storage:update which adds a service_name to the ActiveStorage::Blob table. When I run my tests with next rspec spec this works fine, but rspec spec fails with tests that use active_storage with messages like
ActiveRecord::NotNullViolation:
PG::NotNullViolation: ERROR: null value in column "service_name" violates not-null constraint
Is there a way of adding some conditional code (if Rails::VERSION::MINOR) that detects the version is 6.0 and somehow removes the null constraint for the service_name column.

There are two approaches.
Migrate down to the version before the update:
if Rails.version == "6.0.0"
ActiveRecord::Base.connection.migration_context.migrate(20220816031332)
else
ActiveRecord::Base.connection.migration_context.migrate
end
# or maybe invoke the db:migrate tasks instead
You might need to turn off pending migration check: config.active_record.migration_error = false
This approach is general and should work with any migration(s).
The second approach is to call migration helpers manually. This is specific to the problem posed in this question and just changes the columns without updating migration version in the database:
if Rails.version == "6.0.0"
ActiveRecord::Migration.change_column_null(:active_storage_blobs, :service_name, true)
else
ActiveRecord::Migration.change_column_null(:active_storage_blobs, :service_name, false)
end
The code can be placed in config/environment.rb after Rails.application.initialize!. If you wish to place it earlier, you would need to establish the connection to the database manually.
Note that both methods cause persistent changes to the database, e.g. will remain after the tests have run. The changes only affect the database of which ever environment the code is run in e.g. test or development.

Related

ActiveRecord::UnknownAttributeError when adding passive column

I'm trying to run this migration, and the goal is to not have any server downtime while adding new columns. Since these columns have no relationships to other tables, exist in the code, or are required, I assumed this would be possible. But, after running the migration I observed UnknownAttributeError's being thrown when instantiating a new instance of the model.
The error:
Controller/existing_table/some_method::ActiveRecord::UnknownAttributeError
/code/app/models/existing_table.rb:40:in `new’
The line from the error:
e = ExistingTable.new(existing_table.attributes.except("id"))
The migration:
class AddTestFieldToExistingTable < ActiveRecord::Migration
def change
add_column :existing_table, :test_field, :integer
end
end
When I go to recreate the issue by opening up a console and executing lines similar as above, I don't get the error. My guess is that there is some attributes cached and we do need to have downtime while running these "passive" migrations.
I wonder if the 'existing_table' is still in memory, doesn't have the 'test_field' attribute, and when creating a new instance of ExistingTable it doesn't know how to assign the field. Since I cannot recreate it without starting a new instance, I would be guessing on an alternative solution of re-writing the ExistingTable constructor.
Is there a way to avoid restarting the rails instance or re-writing this code? Am I on the right track?
I tried to replicate your doubt, and I did not get the error, besides, everything worked as expected.
I am using Rails 7.0.1
First, I started puma:
rails s -b 0.0.0.0
Then create the migration:
rails g migration AddPassiveToGiro passive:boolean
The migration looks like this:
class AddPassiveToGiro < ActiveRecord::Migration[7.0]
def change
add_column :giros, :passive, :boolean
end
end
On rails console I created a new record:
Giro.create giro: 'delete me', passive: true
Then I modify the view where I list those records (using haml)
.list-item-description
= giro.passive.blank? ? 'not passive' : 'is passive'
And everything worked fine, no errors.
Rails load the tables' columns only once when the Rails server is started.
Depending on your setup and how you run your migration at deployment it might be possible that the server or at least one server instance started before the migration was done. Then that server instance will not see the newly added column until the next server restart.
To be safe you need to make sure that the migration ran successfully before you deploy the code changes using that change. Depending on your server setup that might mean that you need two different deployments: One with the migration and then later another with the code changes.

ActiveRecord how to ignore pending migrations

The problem is the following:
I have db/seed.rb full of initial data.
One of migrations depends on data this seed provides.
I'm trying to deploy my app from empty db.
Result is:
RAILS_ENV=production rake db:migrate - fails due to lack of initial data
RAILS_ENV=production rake db:seed - fails due to pending migrations
I wanted to somehow tell rake to ignore pending migrations, but unable to do it so far.
UPDATE (due to additional experience)
Sometimes migrations and model code goes out of sync, so migrations are not being run.
To avoid this problem recently used redefining of model in migrations:
# reset all callbacks, hooks, etc for this model
class MyAwesomeModel < ActiveRecord::Base
end
class DoSomethingCool < ActiveRecord::Migration
def change
...
end
end
I am not very sure if this will help you. But I was looking for something and found this question. So it looks like this might help:
In RAILS_ROOT/config/environments/development.rb
Set the following setting to false:
# NOTE: original version is `:page_load`
config.active_record.migration_error = false
In my situation it now does not show the pending migration error anymore. Should work for rake tasks and console for the same environment as well.
Source in rails/rails
Rename the migration dependent on the data from:
20140730091353_migration_name.rb
to
.20140730091353_migration_name.rb
(add a dot at the start of the filename)
Then run rake db:seed (it will no longer complain on the pending migrations) and then rename back the migration.
If you have more migrations following after, you have to rename all of them or just move it temporary away.
Rails stores migration information in a table called schema_migrations.
You can add the version from your migration into that table to skip a specific migration.
The version is the number string which comes before the description in the file name.
[version]_Create_Awesome.rb
I had a similar issue. I commented out the add_column lines and ran the rake db:migrate commands and then removed the comment when I will need it for the testing or production environment.
There is no way unless you monkey patch the Rails code. I strongly advise you to fix your migrations instead.
A migration should not depend on the existence of some data in the database. It can depend on a previous migration, but of course absolutely not on the data on the db.
If you came across the "pending migrations" issue when trying to seed your data from within a running Rails application, you can simply call this directly which avoids the abort_if_pending_migrations check:
ActiveRecord::Tasks::DatabaseTasks.load_seed
See where seeds are actually called from within ActiveRecord:
https://github.com/rails/rails/blob/v6.0.3.2/activerecord/lib/active_record/railties/databases.rake#L331
and see the DatabaseTasks docs:
https://apidock.com/rails/v6.0.0/ActiveRecord/Tasks/DatabaseTasks
https://apidock.com/rails/v6.0.0/ActiveRecord/Tasks/DatabaseTasks/load_seed

Migration critique

Working on a 3.0 rails project for a client and I have a sensitive migration that I need to run off a live production server. Its essentially suppose to down case all the State abbreviations in the DB, FL -> fl, PA -> pa etc...
I can't test locally due to restrictions:
does calling the wording of the migration effect anything? I know it does with add and create etc but not sure when updating info like this.
rails g migration UpdateStateAbbreviation
def self.up
say_with_time "Updating states abbreviation..." do
State.find(:all).each do |s|
tmp = s.abbreviation.downcase
s.update_attribute :abbreviation, tmp
end
end end
Rake db:migrate
One very important rule with migrations is to never reference models in your migrations. This might seem like an academic concern, but at some point in the future you may not have a State model at all, and when you remove app/models/state.rb then this migration will not work.
A properly constructed migration will execute properly regardless of changes in the future. Whatever it does may be later un-done, there's nothing wrong with that, but setting it up for failure is never a good idea.
You can do this downcasing operation in your database using a string function and something like:
execute "UPDATE states SET abbreviation=LOWER(abbreviation)"
Using models in the migration causes all sorts of problems. This goes for using your model to pre-populate certain key records as well. Use seeds.rb if you must, or even better, a rake task to do it for you.
As a note, if you can't test locally you have a very flawed development process. You should always run and test your migrations, both up and down where applicable, to ensure that they work correctly. Where you can't get actual production data for reasons of security or privacy, work with your DBA to get a scrubbed, non-sensitive version for testing purposes. State names should not be confidential, for instance.

How can I force ActiveRecord to recognize a schema change in a migration?

I'm working on a Rails project that has some migrations written in pure SQL for performance reasons. They are working fine until we try to run change_column later on. If we're trying to migrate in one pass, the change_column migration will fail with the following error message:
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DEFAULT NULL' at line 1: ALTER TABLE `articles` CHANGE `state` `web_state` DEFAULT NULL
Presumably Rails has no idea we migrated via SQL in a previous migration. However, if we run rake db:migrate again after the failure everything works perfectly.
How can I force Rails to recognize changes make through raw SQL?
I think you are looking for reset_column_information
http://apidock.com/rails/v3.2.3/ActiveRecord/ModelSchema/ClassMethods/reset_column_information
But based on the error message i dont think this is the right fix

Ruby on Rails Migration - Create New Database Schema

I have a migration that runs an SQL script to create a new Postgres schema. When creating a new database in Postgres by default it creates a schema called 'public', which is the main schema we use. The migration to create the new database schema seems to be working fine, however the problem occurs after the migration has run, when rails tries to update the 'schema_info' table that it relies on it says that it does not exist, as if it is looking for it in the new database schema and not the default 'public' schema where the table actually is.
Does anybody know how I can tell rails to look at the 'public' schema for this table?
Example of SQL being executed: ~
CREATE SCHEMA new_schema;
COMMENT ON SCHEMA new_schema IS 'this is the new Postgres database schema to sit along side the "public" schema';
-- various tables, triggers and functions created in new_schema
Error being thrown: ~
RuntimeError: ERROR C42P01 Mrelation "schema_info" does not exist
L221 RRangeVarGetRelid: UPDATE schema_info SET version = ??
Thanks for your help
Chris Knight
Well that depends what your migration looks like, what your database.yml looks like and what exactly you are trying to attempt. Anyway more information is needed change the names if you have to and post an example database.yml and the migration. does the migration change the search_path for the adapter for example ?
But know that in general rails and postgresql schemas don't work well together (yet?).
There are a few places which have problems. Try and build and app that uses only one pg database with 2 non-default schemas one for dev and one for test and tell me about it. (from thefollowing I can already tell you that you will get burned)
Maybe it was fixed since the last time I played with it but when I see http://rails.lighthouseapp.com/projects/8994/tickets/390-postgres-adapter-quotes-table-name-breaks-when-non-default-schema-is-used or this http://rails.lighthouseapp.com/projects/8994/tickets/918-postgresql-tables-not-generating-correct-schema-list or this in postgresql_adapter.rb
# Drops a PostgreSQL database
#
# Example:
# drop_database 'matt_development'
def drop_database(name) #:nodoc:
execute "DROP DATABASE IF EXISTS #{name}"
end
(yes this is wrong if you use the same database with different schemas for both dev and test, this would drop both databases each time you run the unit tests !)
I actually started writing patches. the first one was for the indexes methods in the adapter which didn't care about the search_path ending up with duplicated indexes in some conditions, then I started getting hurt by the rest and ended up abandonning the idea of using schemas: I wanted to get my app done and I didn't have the extra time needed to fix the problems I had using schemas.
I'm not sure I understand what you're asking exactly, but, rake will be expecting to update the version of the Rails schema into the schema_info table. Check your database.yml config file, this is where rake will be looking to find the table to update.
Is it a possibility that you are migrating to a new Postgres schema and rake is still pointing to the old one? I'm not sure then that a standard Rails migration is what you need. It might be best to create your own rake task instead.
Edit: If you're referencing two different databases or Postgres schemas, Rails doesn't support this in standard migrations. Rails assumes one database, so migrations from one database to another is usually not possible. When you run "rake db:migrate" it actually looks at the RAILS_ENV environment variable to find the correct entry in database.yml. If rake starts the migration looking at the "development" environment and database config from database.yml, it will expect to update to this environment at the end of the migration.
So, you'll probably need to do this from outside the Rails stack as you can't reference two databases at the same time within Rails. There are attempts at plugins to allow this, but they're majorly hacky and don't work properly.
You can use pg_power. It provides additional DSL for migration to create PostgreSQL schemas and not only.

Resources