Code Monkey home page Code Monkey logo

good-migrations's Introduction

good_migrations

This gem prevents Rails from auto-loading app code while it's running migrations, preventing the common mistake of referencing ActiveRecord models from migration code.

Usage

Add good_migrations to your Gemfile:

gem 'good_migrations'

And you're done! That's it.

Prerequisites

This gem requires that your app uses either of these autoloader strategies:

  • The classic ActiveSupport::Dependencies autoloader (e.g. config.autoloader = :classic), which is going away with Rails 7
  • Version 2.5 or higher of the Zeitwerk autoloader (e.g. config.autoloader = :zeitwerk) If your app uses an earlier version of zeitwerk, you'll see a warning every time db:migrate is run

Background

Over the life of your Ruby on Rails application, your app's models will change dramatically, but according to the Rails guides, your migrations shouldn't:

In general, editing existing migrations is not a good idea. You will be creating extra work for yourself and your co-workers and cause major headaches if the existing version of the migration has already been run on production machines. Instead, you should write a new migration that performs the changes you require.

That means that if your migrations reference the ActiveRecord model objects you've defined in app/models, your old migrations are likely to break. That's not good.

By adding this gem to your project's Gemfile, autoloading paths inside 'app/' while running any of the db:migrate Rake tasks will raise an error, explaining the dangers inherent.

Some will reply, "who cares if old migrations are broken? I can still run rake db:setup because I have a db/schema.rb file". The problem with this approach is that, so long as some migrations aren't runnable, the db/schema.rb can't be regenerated from scratch and its veracity can no longer be trusted. In practice, we've seen numerous projects accumulate cruft in db/schema.rb as the result of erroneous commits to work-in-progress migrations, leading to the development and test databases falling out of sync with production. That's not good!

For more background, see the last section of this blog post on healthy migration habits

Adding to an existing app

If you add good_migrations to an existing application and any of those migrations relied on auto-loading code from app/, then you'll see errors raised whenever those migrations are run.

You have several options if this happens:

  • If you're confident that every long-lasting environment has run the latest migrations, you could consider squashing your existing migrations into a single migration file that reflects the current state of your schema. This is a tricky procedure to pull off in complex apps, and can require extra coordination in cases where a high number of contributors are working on the application simultaneously. The squasher gem may be able to help.
  • You can rewrite those past migrations to inline any application code inside the migration's namespace. One way to do this is to run migrations until they fail, check out the git ref of the failing migration so the codebase is rewound to where it was at the time the migration was written, and finally inline the necessary app code to get the migration passing before checking out your primary branch. Rewriting any migration introduces risk of the resulting schema diverging from production, so this requires significant care and attention
  • If neither of the above options are feasible, you can configure the good_migrations gem to ignore migrations prior to a specified date with the permit_autoloading_before option, which will effectively disable the gem's auto-loading prevention for all migrations prior to a specified time

Configuration

To configure the gem, call GoodMigrations.config at some point as Rails is loading (a good idea would be an initializer like config/initializers/good_migrations.rb)

GoodMigrations.config do |config|
  # Setting `permit_autoloading_before` will DISABLE good_migrations for
  # any migrations before the given time. Don't set this unless you need to!
  #
  # Accepts parseable time strings as well as `Date` & `Time` objects
  # config.permit_autoloading_before = "20140728132502"
end

Working around good_migrations

The gem only prevents auto-loading, so you can always can explicitly require the app code that you need in your migration.

If needed, it is possible to run a command with good_migrations disabled by running the command with the env var GOOD_MIGRATIONS=skip.

Acknowledgements

Credit for figuring out where to hook into the ActiveSupport autoloader goes to @tenderlove for this gist. And thanks to @fxn for implementing the hook necessary for zeitwerk support to be possible.

Caveats

Because this gem works by augmenting the auto-loader, it will not work if your Rails environment (development, by default) is configured to eager load your application's classes (see: config.eager_load).

good-migrations's People

Contributors

jasonkarns avatar kbaribeau avatar maxlap avatar reiz avatar rosston avatar rousisk avatar searls avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

good-migrations's Issues

Polymorphic Associations

Hey, I'm having trouble with good_migrations in a migration where one of the classes has a polymorphic association. Just wondering if anyone has hit this issue before, or has any tips?

Here's a rough example of what it looks like:

class MigrateWidgets < ActiveRecord::Migration[5.2]
  class Widget < ActiveRecord::Base
    belongs_to :poly, polymorphic: true
  end
  class Foo < ActiveRecord::Base; end
  class Bar < ActiveRecord::Base; end

  def up
    # Example 1; GoodMigrations::LoadError (attempting to load app/models/foo.rb)
    Widget.where(poly_type: 'Foo').includes(:poly).find_each do |widget|
      # ...
    end

    # Example 2; GoodMigrations::LoadError (attempting to load app/models/bar.rb)
    Widget.where(poly_type: 'Bar').last.poly
  end
end

Integrating into an existing app

Hello, I'm looking to introduce this gem to a project I have at work. That project has been ongoing for 10 years, you can imagine how many older migrations we have that can be using code in a way that this gem blocks.

My issue is I don't have any desire to edit the many hundreds existing migrations. So what I think would be ideal for my need, is if I could setup a date, and migrations before that date can load code, while those after it cannot.

A way I thought would be to look at the caller when the loading happens and if a migration file is in that stack, then allow the loading.

I would be open to work on a PR for this, but since you currently have no configuration system, I'm not sure where I could do such configuration.

A configuration system would be useful for other things. For example, I think it would be great if I could write the error message myself, so that I can provide information specific for my application, such as helpers that people can use for some simple actions.

Documentation for "Working around good_migrations" is incomplete/inaccurate.

Per the README:

The gem only prevents auto-loading, so you can always can explicitly require the app code that you need in your migration.

But when attempting to require application code in a complex data migration, I was still blocked by the gem from running it. It was as if the require statements were not there.

Does this actually work? And if so, is there more precise documentation that could point users toward how to do it?

I ended up inlining > 100 lines of duplicated application code because I couldn't require a well-tested backfill service due to this issue.

Thanks for any help!

Compatibility with Zeitwerk

I just tried adding this to a new Rails 6 project, and it doesn't seem to be raising an error for a migration that should be getting flagged.

My first guess is that the Zeitwerk loader doesn't use the code path that good_migrations hooks in to (ActiveSupport::Dependencies.load_file). I've added puts statements in the right places and they're not getting hit, while adding config.autoloader = :classic to application.rb gets the good_migrations error I'd expect.

I've not dug further to see if there's an equivalent place in Zeitwerk to hook in to, but thought I'd raise the issue for others that may run in to it. (Or for others to say it works for them and I must be doing something wrong...)

Migration not raising error

I'm trying to implement good migrations in an existing rails app, but I can't figure out why isn't the gem raising errors.

I have created the following migration which should raise an error:

class BadMigration < ActiveRecord::Migration[6.1]
  def change
    User.all
  end
end

But when the migration is run, I get the following output, without any error or warning:

> DISABLE_SPRING=1 bundle exec rake db:migrate
== 20221201132109 BadMigration: migrating =====================================
== 20221201132109 BadMigration: migrated (0.0002s) ============================

I've setted config.autoloader = :zeitwerk in application.rb and config.eager_load = false in development.rb.
Currently using rails 6.1.4 and zeitwerk 2.6.6, any Ideia what could be going wrong?

Error not raised during migration that references AR model

Hi. I just installed this gem, but it doesn't seem to be raising an error when I believe it should be. I am running a migration in my development environment, where I have config.eager_load = false. I added a new migration that references an AR model as such:

class PopulateEncryptedOtpFields < ActiveRecord::Migration
  def up
    User.reset_column_information

    User.find_each do |user|
      user.otp_secret_key = user.read_attribute('otp_secret_key')
      user.save!
    end
  end

  def down
    User.reset_column_information

    User.find_each do |user|
      user.otp_secret_key = ROTP::Base32.random_base32
      user.save!
    end
  end
end

When I run bin/rake db:migrate, the migration completes successfully. Shouldn't it fail?

New release?

Could you release a new patch version with the fixes for Rails 5? That would be great, would love to put this into the Gemfile ...

Complain about any `config.autoload_paths` code used in migration?

Thanks for your work in creating and maintaining this gem.

Currently, it only warns for app:

if path.starts_with? File.join(Rails.application.root, "app")

We'd love if this was configurable, or perhaps respected config.autoload_paths. The reasoning is: occasionally in our data migrations, we'll use code in /lib. Some production instances of our app may run the migrations months later, when the underlying code may have changed, and any calls to /lib are therefore potentially unsafe.

Happy to submit a PR here if this sounds appropriate!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.