From 0bac378b43f79f210e500649601bead9e77fee5a Mon Sep 17 00:00:00 2001 From: Luca Guidi Date: Wed, 11 Mar 2009 11:45:46 +0100 Subject: [PATCH] Rake tasks for plugin migrations --- .../abstract/schema_statements.rb | 3 +- activerecord/lib/active_record/migration.rb | 57 +++++++------ activerecord/test/cases/migration_test.rb | 68 +++++++++++++++ railties/lib/commands/plugin.rb | 37 ++++++++- railties/lib/initializer.rb | 4 + railties/lib/rails/plugin/loader.rb | 1 + railties/lib/tasks/databases.rake | 90 ++++++++++++++++++-- railties/lib/tasks/framework.rake | 17 +++- railties/test/initializer_test.rb | 14 +++- 9 files changed, 253 insertions(+), 38 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index c29c156..1e7f646 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -319,8 +319,9 @@ module ActiveRecord unless tables.detect { |t| t == sm_table } create_table(sm_table, :id => false) do |schema_migrations_table| schema_migrations_table.column :version, :string, :null => false + schema_migrations_table.column :plugin, :string end - add_index sm_table, :version, :unique => true, + add_index sm_table, [ :version, :plugin ], :unique => true, :name => 'unique_schema_migrations' # Backwards-compatibility: if we find schema_info, assume we've diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 657acd6..353fbde 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -359,9 +359,9 @@ module ActiveRecord # until they are needed class MigrationProxy - attr_accessor :name, :version, :filename + attr_accessor :name, :version, :filename, :plugin - delegate :migrate, :announce, :write, :to=>:migration + delegate :migrate, :announce, :write, :to => :migration private @@ -378,48 +378,48 @@ module ActiveRecord class Migrator#:nodoc: class << self - def migrate(migrations_path, target_version = nil) + def migrate(migrations_path, target_version = nil, plugin_name = nil) case - when target_version.nil? then up(migrations_path, target_version) - when current_version > target_version then down(migrations_path, target_version) - else up(migrations_path, target_version) + when target_version.nil? then up(migrations_path, target_version, plugin_name) + when current_version(plugin_name) > target_version then down(migrations_path, target_version, plugin_name) + else up(migrations_path, target_version, plugin_name) end end - def rollback(migrations_path, steps=1) - migrator = self.new(:down, migrations_path) + def rollback(migrations_path, steps=1, plugin_name = nil) + migrator = self.new(:down, migrations_path, nil, plugin_name) start_index = migrator.migrations.index(migrator.current_migration) - + return unless start_index finish = migrator.migrations[start_index + steps] - down(migrations_path, finish ? finish.version : 0) + down(migrations_path, finish ? finish.version : 0, plugin_name) end - def up(migrations_path, target_version = nil) - self.new(:up, migrations_path, target_version).migrate + def up(migrations_path, target_version = nil, plugin_name = nil) + self.new(:up, migrations_path, target_version, plugin_name).migrate end - def down(migrations_path, target_version = nil) - self.new(:down, migrations_path, target_version).migrate + def down(migrations_path, target_version = nil, plugin_name = nil) + self.new(:down, migrations_path, target_version, plugin_name).migrate end - - def run(direction, migrations_path, target_version) - self.new(direction, migrations_path, target_version).run + + def run(direction, migrations_path, target_version, plugin_name = nil) + self.new(direction, migrations_path, target_version, plugin_name).run end def schema_migrations_table_name Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix end - def get_all_versions - Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort + def get_all_versions(plugin_name = nil) + Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name} WHERE plugin #{plugin_name ? "= '#{plugin_name}'" : "IS NULL"}").map(&:to_i).sort end - def current_version + def current_version(plugin_name = nil) sm_table = schema_migrations_table_name if Base.connection.table_exists?(sm_table) - get_all_versions.max || 0 + get_all_versions(plugin_name).max || 0 else 0 end @@ -431,10 +431,10 @@ module ActiveRecord end end - def initialize(direction, migrations_path, target_version = nil) + def initialize(direction, migrations_path, target_version = nil, plugin_name = nil) raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? Base.connection.initialize_schema_migrations_table - @direction, @migrations_path, @target_version = direction, migrations_path, target_version + @direction, @migrations_path, @target_version, @plugin_name = direction, migrations_path, target_version, plugin_name end def current_version @@ -515,6 +515,7 @@ module ActiveRecord migration.name = name.camelize migration.version = version migration.filename = file + migration.plugin = @plugin_name end end @@ -529,7 +530,7 @@ module ActiveRecord end def migrated - @migrated_versions ||= self.class.get_all_versions + @migrated_versions ||= self.class.get_all_versions(@plugin_name) end private @@ -539,10 +540,14 @@ module ActiveRecord @migrated_versions ||= [] if down? @migrated_versions.delete(version.to_i) - Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'") + Base.connection.update(@plugin_name ? + "DELETE FROM #{sm_table} WHERE version = '#{version}' AND plugin = '#{@plugin_name}'" : + "DELETE FROM #{sm_table} WHERE version = '#{version}'") else @migrated_versions.push(version.to_i).sort! - Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')") + Base.connection.insert(@plugin_name ? + "INSERT INTO #{sm_table} (version, plugin) VALUES ('#{version}', '#{@plugin_name}')" : + "INSERT INTO #{sm_table} (version) VALUES ('#{version}')") end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 50d039e..2b1cb01 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -961,6 +961,14 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal migrations[0].name, 'InnocentJointable' end + def test_finds_plugin_pending_migrations + ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2", 1, 'foo') + migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/interleaved/pass_2", nil, 'foo').pending_migrations + assert_equal 1, migrations.size + migrations[0].version == '3' + migrations[0].name == 'innocent_jointable' + end + def test_only_loads_pending_migrations # migrate up to 1 ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) @@ -996,6 +1004,38 @@ if ActiveRecord::Base.connection.supports_migrations? end end + def test_record_version_state_after_migrating + assert_equal(0, ActiveRecord::Migrator.current_version) + sm_table = ActiveRecord::Migrator.schema_migrations_table_name + version = ActiveRecord::Migrator.current_version + 1 + plugin_name = 'foo' + + assert_sql("INSERT INTO #{sm_table} (version) VALUES ('#{version}')") do + ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1) + end + + assert_sql("DELETE FROM #{sm_table} WHERE version = '#{version}'") do + ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0) + end + + assert_sql("INSERT INTO #{sm_table} (version, plugin) VALUES ('#{version}', '#{plugin_name}')") do + ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1, plugin_name) + end + + assert_sql("DELETE FROM #{sm_table} WHERE version = '#{version}' AND plugin = '#{plugin_name}'") do + ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", 0, plugin_name) + end + end + + def test_get_all_versions + assert_equal(0, ActiveRecord::Migrator.current_version) + ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 1) + ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/valid", 2, 'foo') + + assert_equal [ 1 ], ActiveRecord::Migrator.get_all_versions + assert_equal [ 2 ], ActiveRecord::Migrator.get_all_versions('foo') + end + def test_migrator_db_has_no_schema_migrations_table ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations;") assert_nothing_raised do @@ -1053,6 +1093,23 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal(0, ActiveRecord::Migrator.current_version) end + def test_migrator_plugin_rollback + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", nil, 'foo') + assert_equal(3, ActiveRecord::Migrator.current_version('foo')) + + ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid", 1, 'foo') + assert_equal(2, ActiveRecord::Migrator.current_version('foo')) + + ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid", 1, 'foo') + assert_equal(1, ActiveRecord::Migrator.current_version('foo')) + + ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid", 1, 'foo') + assert_equal(0, ActiveRecord::Migrator.current_version('foo')) + + ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid", 1, 'foo') + assert_equal(0, ActiveRecord::Migrator.current_version('foo')) + end + def test_schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" @@ -1067,6 +1124,11 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Base.table_name_suffix = "" end + def test_schema_migrations_table_structure + Person.connection.initialize_schema_migrations_table + assert_equal %w(version plugin), Person.connection.columns(ActiveRecord::Migrator.schema_migrations_table_name).map(&:name) + end + def test_proper_table_name assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) @@ -1148,6 +1210,12 @@ if ActiveRecord::Base.connection.supports_migrations? end end + def test_migrator_with_duplicates_from_plugin + assert_raises(ActiveRecord::DuplicateMigrationVersionError) do + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate", nil, 'foo') + end + end + def test_migrator_with_missing_version_numbers assert_raise(ActiveRecord::UnknownMigrationVersionError) do ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/missing", 500) diff --git a/railties/lib/commands/plugin.rb b/railties/lib/commands/plugin.rb index 8589b16..0d519d0 100644 --- a/railties/lib/commands/plugin.rb +++ b/railties/lib/commands/plugin.rb @@ -480,6 +480,7 @@ module Commands o.separator " source Add a plugin source repository." o.separator " unsource Remove a plugin repository." o.separator " sources List currently configured plugin repositories." + o.separator " rename Rename a plugin in the migrations table." o.separator "" o.separator "EXAMPLES" @@ -504,7 +505,9 @@ module Commands o.separator " Remove a repository from the source list:" o.separator " #{@script_name} unsource http://dev.rubyonrails.com/svn/rails/plugins/\n" o.separator " Show currently configured repositories:" - o.separator " #{@script_name} sources\n" + o.separator " #{@script_name} sources\n" + o.separator " Rename a plugin:" + o.separator " #{@script_name} rename continuous-builder continuous_builder\n" end end @@ -513,7 +516,7 @@ module Commands options.parse!(general) command = general.shift - if command =~ /^(list|discover|install|source|unsource|sources|remove|update|info)$/ + if command =~ /^(list|discover|install|source|unsource|sources|remove|update|rename|info)$/ command = Commands.const_get(command.capitalize).new(self) command.parse!(sub) else @@ -895,6 +898,36 @@ module Commands end end end + + class Rename + def initialize(base_command) + require File.expand_path(File.join(RAILS_ROOT, 'config', 'environment')) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} rename OLD_NAME NEW_NAME" + o.define_head "Rename a plugin in the migrations table." + end + end + + def parse!(args) + options.parse!(args) + old_name, new_name = args + + ActiveRecord::Base.transaction do + FileUtils.mv(File.join(RAILS_ROOT, 'vendor', 'plugins', old_name), + File.join(RAILS_ROOT, 'vendor', 'plugins', new_name)) + + sanitized_sql = ActiveRecord::Base.send(:sanitize_sql, "UPDATE #{ActiveRecord::Migrator.schema_migrations_table_name} set plugin = '#{new_name}' WHERE plugin = '#{old_name}'") + ActiveRecord::Base.connection.update(sanitized_sql) + end + + puts "Renamed #{old_name} in #{new_name}" + end + end end class RecursiveHTTPFetcher diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index edea4e5..32121f8 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -71,6 +71,10 @@ module Rails def public_path=(path) @@public_path = path end + + def plugins + @@plugins ||= ActiveSupport::OrderedHash.new + end end # The Initializer is responsible for processing the Rails configuration, such diff --git a/railties/lib/rails/plugin/loader.rb b/railties/lib/rails/plugin/loader.rb index 66e01d7..11bf5cd 100644 --- a/railties/lib/rails/plugin/loader.rb +++ b/railties/lib/rails/plugin/loader.rb @@ -106,6 +106,7 @@ module Rails def register_plugin_as_loaded(plugin) initializer.loaded_plugins << plugin + Rails.plugins[plugin.name] = plugin end def configuration diff --git a/railties/lib/tasks/databases.rake b/railties/lib/tasks/databases.rake index 9588fab..41ea908 100644 --- a/railties/lib/tasks/databases.rake +++ b/railties/lib/tasks/databases.rake @@ -111,13 +111,35 @@ namespace :db do desc "Migrate the database through scripts in db/migrate and update db/schema.rb by invoking db:schema:dump. Target specific version with VERSION=x. Turn off output with VERBOSE=false." - task :migrate => :environment do - ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true - ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil) - Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby - end + task :migrate => ['db:migrate:plugins', 'db:migrate:application'] namespace :migrate do + desc "Run migrations from application" + task :application => :environment do + ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + + desc "Run migrations from plugins" + task :plugins => :environment do + raise "PLUGIN is require when specify PLUGIN_VERSION" if ENV['PLUGIN_VERSION'] && !ENV['PLUGIN'] + + plugins = if ENV["PLUGIN"] + raise "Unknown plugin: #{ENV["PLUGIN"]}" unless Rails.plugins.key?(ENV["PLUGIN"]) + Hash[ENV["PLUGIN"], Rails.plugins[ENV["PLUGIN"]]] + else + Rails.plugins + end + + plugins.each do |name, plugin| + plugin_migrations_path = "#{plugin.directory}/db/migrate" + ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + ActiveRecord::Migrator.migrate(plugin_migrations_path, ENV["PLUGIN_VERSION"] ? ENV["PLUGIN_VERSION"].to_i : nil, name) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + end + desc 'Rollbacks the database one migration and re migrate up. If you want to rollback more than one step, define STEP=x. Target specific version with VERSION=x.' task :redo => :environment do if ENV["VERSION"] @@ -147,6 +169,26 @@ namespace :db do ActiveRecord::Migrator.run(:down, "db/migrate/", version) Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby end + + namespace :plugins do + desc 'Runs the "up" for a given PLUGIN and a given migration PLUGIN_VERSION' + task :up => :environment do + raise "Both PLUGIN and PLUGIN_VERSION are required" unless ENV["PLUGIN_VERSION"] && ENV["PLUGIN"] + raise "Unknown plugin: #{ENV["PLUGIN"]}" unless Rails.plugins.key?(ENV["PLUGIN"]) + plugin_migrations_path = "#{Rails.plugins[ENV["PLUGIN"]].directory}/db/migrate" + ActiveRecord::Migrator.run(:up, plugin_migrations_path, ENV["PLUGIN_VERSION"].to_i, ENV["PLUGIN"]) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + + desc 'Runs the "down" for a given PLUGIN and a given migration PLUGIN_VERSION' + task :down => :environment do + raise "Both PLUGIN and PLUGIN_VERSION are required" unless ENV["PLUGIN_VERSION"] && ENV["PLUGIN"] + raise "Unknown plugin: #{ENV["PLUGIN"]}" unless Rails.plugins.key?(ENV["PLUGIN"]) + plugin_migrations_path = "#{Rails.plugins[ENV["PLUGIN"]].directory}/db/migrate" + ActiveRecord::Migrator.run(:down, plugin_migrations_path, ENV["PLUGIN_VERSION"].to_i, ENV["PLUGIN"]) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + end end desc 'Rolls the schema back to the previous version. Specify the number of steps with STEP=n' @@ -195,17 +237,53 @@ namespace :db do task :abort_if_pending_migrations => :environment do if defined? ActiveRecord pending_migrations = ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations + pending_migrations << Rails.plugins.map do |name, plugin| + ActiveRecord::Migrator.new(:up, "#{plugin.directory}/db/migrate", nil, name).pending_migrations + end + pending_migrations.flatten! if pending_migrations.any? puts "You have #{pending_migrations.size} pending migrations:" pending_migrations.each do |pending_migration| - puts ' %4d %s' % [pending_migration.version, pending_migration.name] + pending_migration_string = ' %4d %s' % [pending_migration.version, pending_migration.name] + pending_migration_string << " (#{pending_migration.plugin})" if pending_migration.plugin + puts pending_migration_string end abort %{Run "rake db:migrate" to update your database then try again.} end end end + namespace :plugins do + desc 'Rolls the schema back to the previous version for a given PLUGIN. Specify the number of steps with STEP=n' + task :rollback => :environment do + raise "PLUGIN is required" unless ENV["PLUGIN"] + raise "Unknown plugin: #{ENV["PLUGIN"]}" unless Rails.plugins.key?(ENV["PLUGIN"]) + step = ENV['STEP'] ? ENV['STEP'].to_i : 1 + plugin_migrations_path = "#{Rails.plugins[ENV["PLUGIN"]].directory}/db/migrate" + ActiveRecord::Migrator.rollback(plugin_migrations_path, step, ENV["PLUGIN"]) + Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby + end + + desc 'Rollbacks the database one migration and re migrate up for a given PLUGIN. If you want to rollback more than one step, define STEP=x. Target specific version with PLUGIN_VERSION=x.' + task :redo => :environment do + if ENV["PLUGIN_VERSION"] + Rake::Task["db:migrate:plugins:down"].invoke + Rake::Task["db:migrate:plugins:up"].invoke + else + Rake::Task["db:plugins:rollback"].invoke + Rake::Task["db:migrate:plugins"].invoke + end + end + + desc "Retrieves the current schema version number for a given PLUGIN" + task :version => :environment do + raise "PLUGIN is required" unless ENV["PLUGIN"] + raise "Unknown plugin: #{ENV["PLUGIN"]}" unless Rails.plugins.key?(ENV["PLUGIN"]) + puts "Current version of #{ENV["PLUGIN"]}: #{ActiveRecord::Migrator.current_version(ENV["PLUGIN"])}" + end + end + namespace :fixtures do desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :load => :environment do diff --git a/railties/lib/tasks/framework.rake b/railties/lib/tasks/framework.rake index 191c936..d484cdf 100644 --- a/railties/lib/tasks/framework.rake +++ b/railties/lib/tasks/framework.rake @@ -78,7 +78,7 @@ namespace :rails do end desc "Update both configs, scripts and public/javascripts from Rails" - task :update => [ "update:scripts", "update:javascripts", "update:configs", "update:application_controller" ] + task :update => [ "update:scripts", "update:javascripts", "update:configs", "update:application_controller", "update:schema_migrations" ] desc "Applies the template supplied by LOCATION=/path/to/template" task :template do @@ -130,7 +130,20 @@ namespace :rails do puts "#{old_style} has been renamed to #{new_style}, update your SCM as necessary" end end - + + desc "Update schema_migrations table structure" + task :schema_migrations => :environment do + sm_table = ActiveRecord::Migrator.schema_migrations_table_name + connection = ActiveRecord::Base.connection + + unless connection.columns(sm_table).map(&:name).include?('plugin') + connection.add_column sm_table, :plugin, :string + connection.remove_index sm_table, :version rescue nil + connection.add_index [ :version, :plugin ], :unique => true, + :name => 'unique_schema_migrations' + end + end + desc "Generate dispatcher files in RAILS_ROOT/public" task :generate_dispatchers do require 'railties_path' diff --git a/railties/test/initializer_test.rb b/railties/test/initializer_test.rb index 561f7b8..7ccf795 100644 --- a/railties/test/initializer_test.rb +++ b/railties/test/initializer_test.rb @@ -190,6 +190,7 @@ class InitializerPluginLoadingTests < Test::Unit::TestCase @initializer = Rails::Initializer.new(@configuration) @valid_plugin_path = plugin_fixture_path('default/stubby') @empty_plugin_path = plugin_fixture_path('default/empty') + Rails.plugins.clear end def test_no_plugins_are_loaded_if_the_configuration_has_an_empty_plugin_list @@ -271,8 +272,19 @@ class InitializerPluginLoadingTests < Test::Unit::TestCase assert $LOAD_PATH.include?(File.join(plugin_fixture_path('default/acts/acts_as_chunky_bacon'), 'lib')) end - private + def test_should_store_loaded_plugins_in_rails_module + load_plugins! + assert_plugins [:a, :acts_as_chunky_bacon, :engine, :gemlike, :plugin_with_no_lib_dir, :stubby], Rails.plugins.values + end + def test_should_store_only_specified_plugins_in_rails_module + plugin_names = [:plugin_with_no_lib_dir, :acts_as_chunky_bacon] + only_load_the_following_plugins! plugin_names + load_plugins! + assert_plugins plugin_names, Rails.plugins.values + end + + private def load_plugins! @initializer.add_plugin_load_paths @initializer.load_plugins -- 1.5.4.5