diff --git a/README.md b/README.md index 65861bb..33c866f 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,48 @@ Run the task with custom paths: rake actual_db_schema:diff_schema_with_migrations[path/to/custom_schema.rb, path/to/custom_migrations] ``` +## Console Migrations + +Sometimes, it's necessary to modify the database without creating migration files. This can be useful for fixing a corrupted schema, conducting experiments (such as adding and removing indexes), or quickly adjusting the schema in development. This gem allows you to run the same commands used in migrations directly in the Rails console. + +By default, Console Migrations is disabled. You can enable it in two ways: + +### 1. Using Environment Variable + +Set the environment variable `ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED` to `true`: + +```sh +export ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED=true +``` + +### 2. Using Initializer + +Add the following line to your initializer file (`config/initializers/actual_db_schema.rb`): + +```ruby +config.console_migrations_enabled = true +``` + +### Usage + +Once enabled, you can run migration commands directly in the Rails console: + +```ruby +# Create a new table +create_table :posts do |t| + t.string :title +end + +# Add a column +add_column :users, :age, :integer + +# Remove an index +remove_index :users, :email + +# Rename a column +rename_column :users, :username, :handle +``` + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. diff --git a/lib/actual_db_schema.rb b/lib/actual_db_schema.rb index 0f4c2f9..4e85329 100644 --- a/lib/actual_db_schema.rb +++ b/lib/actual_db_schema.rb @@ -17,6 +17,7 @@ require_relative "actual_db_schema/patches/migration_context" require_relative "actual_db_schema/git_hooks" require_relative "actual_db_schema/multi_tenant" +require_relative "actual_db_schema/railtie" require_relative "actual_db_schema/schema_diff" require_relative "actual_db_schema/schema_parser" diff --git a/lib/actual_db_schema/configuration.rb b/lib/actual_db_schema/configuration.rb index 58c307c..539e1c8 100644 --- a/lib/actual_db_schema/configuration.rb +++ b/lib/actual_db_schema/configuration.rb @@ -3,7 +3,8 @@ module ActualDbSchema # Manages the configuration settings for the gem. class Configuration - attr_accessor :enabled, :auto_rollback_disabled, :ui_enabled, :git_hooks_enabled, :multi_tenant_schemas + attr_accessor :enabled, :auto_rollback_disabled, :ui_enabled, :git_hooks_enabled, :multi_tenant_schemas, + :console_migrations_enabled def initialize @enabled = Rails.env.development? @@ -11,6 +12,7 @@ def initialize @ui_enabled = Rails.env.development? || ENV["ACTUAL_DB_SCHEMA_UI_ENABLED"].present? @git_hooks_enabled = ENV["ACTUAL_DB_SCHEMA_GIT_HOOKS_ENABLED"].present? @multi_tenant_schemas = nil + @console_migrations_enabled = ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present? end def [](key) diff --git a/lib/actual_db_schema/console_migrations.rb b/lib/actual_db_schema/console_migrations.rb new file mode 100644 index 0000000..e9a4ca5 --- /dev/null +++ b/lib/actual_db_schema/console_migrations.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module ActualDbSchema + # Provides methods for executing schema modification commands directly in the Rails console. + module ConsoleMigrations + extend self + + SCHEMA_METHODS = %i[ + create_table + create_join_table + drop_table + change_table + add_column + remove_column + change_column + change_column_null + change_column_default + rename_column + add_index + remove_index + rename_index + add_timestamps + remove_timestamps + reversible + add_reference + remove_reference + add_foreign_key + remove_foreign_key + ].freeze + + SCHEMA_METHODS.each do |method_name| + define_method(method_name) do |*args, **kwargs, &block| + if kwargs.any? + migration_instance.public_send(method_name, *args, **kwargs, &block) + else + migration_instance.public_send(method_name, *args, &block) + end + end + end + + private + + def migration_instance + @migration_instance ||= Class.new(ActiveRecord::Migration[ActiveRecord::Migration.current_version]) {}.new + end + end +end diff --git a/lib/actual_db_schema/railtie.rb b/lib/actual_db_schema/railtie.rb new file mode 100644 index 0000000..7b3a2ee --- /dev/null +++ b/lib/actual_db_schema/railtie.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module ActualDbSchema + # Integrates the ConsoleMigrations module into the Rails console. + class Railtie < ::Rails::Railtie + console do + require_relative "console_migrations" + + if ActualDbSchema.config[:console_migrations_enabled] + TOPLEVEL_BINDING.receiver.extend(ActualDbSchema::ConsoleMigrations) + puts "[ActualDbSchema] ConsoleMigrations enabled. You can now use migration methods directly at the console." + end + end + end +end diff --git a/lib/generators/actual_db_schema/templates/actual_db_schema.rb b/lib/generators/actual_db_schema/templates/actual_db_schema.rb index dae3b9c..226de5e 100644 --- a/lib/generators/actual_db_schema/templates/actual_db_schema.rb +++ b/lib/generators/actual_db_schema/templates/actual_db_schema.rb @@ -20,4 +20,8 @@ # If your application leverages multiple schemas for multi-tenancy, define the active schemas. # config.multi_tenant_schemas = -> { ["public", "tenant1", "tenant2"] } + + # Enable console migrations + # config.console_migrations_enabled = true + config.console_migrations_enabled = ENV["ACTUAL_DB_SCHEMA_CONSOLE_MIGRATIONS_ENABLED"].present? end diff --git a/test/rake_task_console_migrations_test.rb b/test/rake_task_console_migrations_test.rb new file mode 100644 index 0000000..8041b11 --- /dev/null +++ b/test/rake_task_console_migrations_test.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "../lib/actual_db_schema/console_migrations" + +describe "console migrations" do + let(:utils) { TestUtils.new } + + before do + extend ActualDbSchema::ConsoleMigrations + + utils.reset_database_yml(TestingState.db_config["primary"]) + ActiveRecord::Base.configurations = { "test" => TestingState.db_config["primary"] } + ActiveRecord::Tasks::DatabaseTasks.database_configuration = { "test" => TestingState.db_config["primary"] } + ActiveRecord::Base.establish_connection(**TestingState.db_config["primary"]) + utils.cleanup + + utils.define_migration_file("20250124084321_create_users.rb", <<~RUBY) + class CreateUsers < ActiveRecord::Migration[6.0] + def change + create_table :users do |t| + t.string :name + t.string :middle_name + t.timestamps + end + + add_index :users, :name, name: "index_users_on_name", unique: true + end + end + RUBY + utils.run_migrations + end + + after do + utils.define_migration_file("20250124084323_drop_users.rb", <<~RUBY) + class DropUsers < ActiveRecord::Migration[6.0] + def change + drop_table :users + end + end + RUBY + utils.run_migrations + end + + it "adds a column to a table" do + add_column :users, :email, :string + assert ActiveRecord::Base.connection.column_exists?(:users, :email) + end + + it "removes a column from a table" do + remove_column :users, :middle_name + refute ActiveRecord::Base.connection.column_exists?(:users, :middle_name) + end + + it "creates and drops a table" do + refute ActiveRecord::Base.connection.table_exists?(:categories) + create_table :categories do |t| + t.string :title + t.timestamps + end + assert ActiveRecord::Base.connection.table_exists?(:categories) + + drop_table :categories + refute ActiveRecord::Base.connection.table_exists?(:categories) + end + + it "changes column type" do + change_column :users, :middle_name, :text + assert_equal :text, ActiveRecord::Base.connection.columns(:users).find { |c| c.name == "middle_name" }.type + end + + it "renames a column" do + rename_column :users, :name, :full_name + assert ActiveRecord::Base.connection.column_exists?(:users, :full_name) + refute ActiveRecord::Base.connection.column_exists?(:users, :name) + end + + it "adds and removes an index" do + add_index :users, :middle_name, name: "index_users_on_middle_name", unique: true + assert ActiveRecord::Base.connection.index_exists?(:users, :middle_name, name: "index_users_on_middle_name") + + remove_index :users, name: "index_users_on_middle_name" + refute ActiveRecord::Base.connection.index_exists?(:users, :middle_name, name: "index_users_on_middle_name") + end + + it "adds and removes timestamps" do + remove_timestamps :users + refute ActiveRecord::Base.connection.column_exists?(:users, :created_at) + refute ActiveRecord::Base.connection.column_exists?(:users, :updated_at) + + add_timestamps :users + assert ActiveRecord::Base.connection.column_exists?(:users, :created_at) + assert ActiveRecord::Base.connection.column_exists?(:users, :updated_at) + end +end