How to override/modify rails built-in migration generator?

First posted here: How to override/modify rails built-in migration generator? · Issue #53356 · rails/rails · GitHub

background

In our Rails app, we have a folder called plugins where we can have Rails-like apps related to our main Rails app.

Those include migration, jobs, models, etc.

One of the pieces that are not very well glued together is the migration generation story–Currently, we need to generate via rails g migration_name a migration file and then move it to the plugins/<plugin-name>/db/migrate folder.

I’ve read the rails generator guide and ended up creating this .rb generator:

/lib/generators/rails/plugin_migration_generator.rb

# frozen_string_literal: true

require "rails/generators/active_record/migration/migration_generator"

class Rails::PluginMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
  class_option :plugin_name,
               type: :string,
               banner: "plugin_name",
               desc: "The plugin name to generate the migration into.",
               required: true

  source_root "#{Gem.loaded_specs["activerecord"].full_gem_path}/lib/rails/generators/active_record/migration/templates"

  private

  def db_migrate_path
    "plugins/#{options["plugin_name"]}/db/migrate"
  end
end

And it works fine for the most part.

Working pictures

--help command:

running rails g plugin_migration some_migration2 --plugin-name discourse-chat-integration:

Then I read the overriding rails generators part. The docs mention that you can override/configure the built-in rails generators.

This leads to my question: How to override/modify rails built-in migration generator?

A similar question was asked before in the forum. What I’m looking for is more to extend the options of the generator rather than override completely

Steps to reproduce

in Linux/MacOs

rails new app 
cd app
mkdir -p lib/generators/rails  
touch lib/generators/rails/migration_generator.rb && curl https://gist.githubusercontent.com/Grubba27/1f764790df4ed7a039c47061890ab063/raw/1dbfff909cd4a621d48d04b4e77b1ddf8c352e4f/migration_generator.rb >> lib/generators/rails/migration_generator.rb
touch lib/generators/rails/plugin_migration_generator.rb && curl https://gist.githubusercontent.com/Grubba27/1f764790df4ed7a039c47061890ab063/raw/1dbfff909cd4a621d48d04b4e77b1ddf8c352e4f/plugin_migration_generator.rb >> lib/generators/rails/plugin_migration_generator.rb

These are the two files from the gists:

plugin_migration_generator.rb

# frozen_string_literal: true

require "rails/generators/active_record/migration/migration_generator"

class Rails::PluginMigrationGenerator < ActiveRecord::Generators::MigrationGenerator
  class_option :plugin_name,
               type: :string,
               banner: "plugin_name",
               desc: "The plugin name to generate the migration into.",
               required: true

  source_root "#{Gem.loaded_specs["activerecord"].full_gem_path}/lib/rails/generators/active_record/migration/templates"

  private

  def db_migrate_path
    if options["plugin_name"]
      "plugins/#{options["plugin_name"]}/db/migrate"
    else
      "db/migrate"
    end
  end
end

migration_generator.rb

# frozen_string_literal: true

require "rails/generators/active_record/migration/migration_generator"

class Rails::MigrationGenerator < ActiveRecord::Generators::MigrationGenerator
  class_option :plugin_name,
               type: :string,
               banner: "plugin_name",
               desc: "The plugin name to generate the migration into."

  source_root "#{Gem.loaded_specs["activerecord"].full_gem_path}/lib/rails/generators/active_record/migration/templates"

  private

  def db_migrate_path
    if options["plugin_name"]
      "plugins/#{options["plugin_name"]}/db/migrate"
    else
      "db/migrate"
    end
  end
end

Expected behavior

running the following command

rails g migration foo2 --plugin-name testing
# creates plugins/testing/db/migrate/20241017210353_foo.rb

Actual behavior

rails g migration foo --plugin-name testing
# creates db/migrate/20241017210413_foo.rb

System configuration

Rails version:

Rails 7.1.4.1

Ruby version:

ruby 3.2.2 

In rails you can configure per database migration the migrations paths using migrations_paths. This can be set either in database.yml or using ActiveRecord::Migrator.migrations_paths = [ Rails.root.join("plugins/testing/db/migrate").to_s ]

1 Like

Would it handle the cases where I want to create a migration using the default /db/migrate?

Not exactly as you wanted but now I understand better what you’re trying to achieve.

So Rails had adapters for generators also. For example the migration generator looks like this:

module Rails
  module Generators
    class MigrationGenerator < NamedBase # :nodoc:
      argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
      hook_for :orm, required: true, desc: "ORM to be invoked"

      def self.exit_on_failure? # :nodoc:
        true
      end
    end
  end
end

The generator by itself just defines the parameters and invokes the :orm to do the actual work. The ORM is chosen by either the parameter given in the CLI, the configuration config.generators.orm or defaulting to :active_record. So in a default rails application configuration rails will call the generator ActiveRecord::Generators::MigrationGenerator. From this generator you’re interested in these lines:

module ActiveRecord
  module Generators
    class MigrationGenerator < Base
      def create_migration_file
        migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb")
      end

So to achieve your purpose you’d need to override the method db_migrate_path. One way I’m thinking you can achieve that is:

  • configure your own orm generator (e.g. :my_active_record)
  • define MyActiveRecord::Generators::MigrationGenerator which inherits from ActiveRecord::Generators::MigrationGenerator
  • add an option --plugin to your generator
  • in your generator leverage the plugin option given to determine the migrations path
  • set your generator the default one in your app
  • add a fallback to :active_record from your orm generator so you don’t have to reimplement everything

Code-wise it will be something like the below (not tested):

# add to application.rb configuration for your generator
config.generators do |g|
  g.org :my_active_record
  g.fallbacks[:my_active_record] = :active_record
end
# define your generator
require "rails/generators/active_record/migration/migration_generator"

class MyActiveRecord::Generators::MigrationGenerator < ActiveRecord::Generators::MigrationGenerator
  class_option :plugin, type: :string, default: ""

  def db_migrate_path
    options[:plugin].present? ? "plugins/#{options[:plugin]}/db/migrate" : "db/migrate"
  end
end

Now you should be able to rails g migration --plugin testing some_migration

1 Like