Permalink
Browse files

Merge pull request #5 from anthonyalberto/patch-AR-migrations

Change strategy to patch sql statements issued to the db
  • Loading branch information...
anthonyalberto committed Dec 5, 2013
2 parents f290b74 + 8d57728 commit 5b3d5984fdfd2d3963933151fb22f3b9a0503333
View
@@ -0,0 +1,12 @@
+language: ruby
+rvm:
+ - 2.0.0
+
+before_script:
+ - mysql -e 'create database mysql_online_migrations;'
+
+gemfile:
+ - gemfiles/rails3.gemfile
+ - gemfiles/rails4.gemfile
+
+script: bundle exec rspec spec
View
@@ -24,7 +24,7 @@ GEM
builder (3.0.4)
coderay (1.0.9)
diff-lcs (1.2.5)
- i18n (0.6.5)
+ i18n (0.6.9)
logger (1.2.8)
method_source (0.8.2)
multi_json (1.8.2)
View
@@ -0,0 +1,7 @@
+source :rubygems
+gem "activerecord", "3.2.16"
+gem "activesupport", "3.2.16"
+gem "mysql2"
+gem "logger"
+gem "rspec"
+gem "pry"
View
@@ -0,0 +1,7 @@
+source :rubygems
+gem "activerecord", "4.0.2"
+gem "activesupport", "4.0.2"
+gem "mysql2"
+gem "logger"
+gem "rspec"
+gem "pry"
@@ -1,33 +1,28 @@
require 'active_record'
+require "active_record/migration"
require "active_record/connection_adapters/mysql2_adapter"
+require "pry"
%w(*.rb).each do |path|
Dir["#{File.dirname(__FILE__)}/mysql_online_migrations/#{path}"].each { |f| require(f) }
end
module MysqlOnlineMigrations
- include Indexes
- include Columns
-
- def self.included(base)
+ def self.prepended(base)
ActiveRecord::Base.send(:class_attribute, :mysql_online_migrations, :instance_writer => false)
ActiveRecord::Base.send("mysql_online_migrations=", true)
end
- def lock_statement(lock, with_comma = false)
- return "" if lock == true
- return "" unless perform_migrations_online?
- puts "ONLINE MIGRATION"
- "#{with_comma ? ', ' : ''} LOCK=NONE"
- end
-
- def extract_lock_from_options(options)
- [options[:lock], options.except(:lock)]
+ def connection
+ @no_lock_adapter ||= ActiveRecord::ConnectionAdapters::Mysql2AdapterWithoutLock.new(super)
end
- def perform_migrations_online?
- ActiveRecord::Base.mysql_online_migrations == true
+ def with_lock
+ original_value = ActiveRecord::Base.mysql_online_migrations
+ ActiveRecord::Base.mysql_online_migrations = false
+ yield
+ ActiveRecord::Base.mysql_online_migrations = original_value
end
end
-ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:include, MysqlOnlineMigrations)
+ActiveRecord::Migration.send(:prepend, MysqlOnlineMigrations)
@@ -1,63 +0,0 @@
-module MysqlOnlineMigrations
- module Columns
- def add_column(table_name, column_name, type, options = {})
- lock, options = extract_lock_from_options(options)
- execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)} #{lock_statement(lock, true)}")
- end
-
- def add_timestamps(table_name, options = {})
- add_column table_name, :created_at, :datetime, options
- add_column table_name, :updated_at, :datetime, options
- end
-
- def change_column(table_name, column_name, type, options = {})
- lock, options = extract_lock_from_options(options)
- execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)} #{lock_statement(lock, true)}")
- end
-
- def change_column_default(table_name, column_name, default, options = {})
- column = column_for(table_name, column_name)
- change_column table_name, column_name, column.sql_type, options.merge(:default => default)
- end
-
- def change_column_null(table_name, column_name, null, default = nil, options = {})
- column = column_for(table_name, column_name)
-
- unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
- end
-
- change_column table_name, column_name, column.sql_type, options.merge(:null => null)
- end
-
- def rename_column(table_name, column_name, new_column_name, options = {})
- lock, options = extract_lock_from_options(options)
- execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)} #{lock_statement(lock, true)}")
- end
-
- def remove_column(table_name, *column_names)
- if column_names.flatten!
- message = 'Passing array to remove_columns is deprecated, please use ' +
- 'multiple arguments, like: `remove_columns(:posts, :foo, :bar)`'
- ActiveSupport::Deprecation.warn message, caller
- end
-
- lock, options = if column_names.last.is_a? Hash
- options = column_names.last
- column_names = column_names[0..-2]
- extract_lock_from_options(options)
- else
- [false, {}]
- end
-
- columns_for_remove(table_name, *column_names).each do |column_name|
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name} #{lock_statement(lock, true)}"
- end
- end
-
- def remove_timestamps(table_name, options = {})
- remove_column table_name, :updated_at, options
- remove_column table_name, :created_at, options
- end
- end
-end
@@ -1,21 +0,0 @@
-module MysqlOnlineMigrations
- module Indexes
- def add_index(table_name, column_name, options = {})
- lock, options = extract_lock_from_options(options)
- index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns}) #{lock_statement(lock)}"
- end
-
- def remove_index(table_name, options_index_name, options = {})
- lock, options = extract_lock_from_options(options)
- execute "DROP INDEX #{quote_column_name(index_name_for_remove(table_name, options_index_name))} ON #{quote_table_name(table_name)} #{lock_statement(lock)}"
- end
-
- def rename_index(table_name, old_name, new_name, options = {})
- old_index_def = indexes(table_name).detect { |i| i.name == old_name }
- return unless old_index_def
- remove_index(table_name, { :name => old_name }, options)
- add_index(table_name, old_index_def.columns, options.merge(name: new_name, unique: old_index_def.unique))
- end
- end
-end
@@ -0,0 +1,32 @@
+module ActiveRecord
+ module ConnectionAdapters
+ class Mysql2AdapterWithoutLock < Mysql2Adapter
+ OPTIMIZABLE_DDL_REGEX = /^(alter|create (unique )? ?index|drop index) /i
+ DDL_WITH_COMMA_REGEX = /^alter /i
+ DDL_WITH_LOCK_NONE_REGEX = / LOCK=NONE\s*$/i
+
+ def initialize(mysql2_adapter)
+ params = [:@connection, :@logger, :@connection_options, :@config].map do |sym|
+ mysql2_adapter.instance_variable_get(sym)
+ end
+ super(*params)
+ end
+
+ alias_method :original_execute, :execute
+ def execute(sql, name = nil)
+ if sql =~ OPTIMIZABLE_DDL_REGEX
+ sql = "#{sql} #{lock_none_statement(sql)}"
+ puts "EXECUTING #{sql}"
+ end
+ original_execute(sql, name)
+ end
+
+ def lock_none_statement(sql)
+ return "" unless ActiveRecord::Base.mysql_online_migrations
+ return "" if sql =~ DDL_WITH_LOCK_NONE_REGEX
+ comma_delimiter = (sql =~ DDL_WITH_COMMA_REGEX ? "," : "")
+ "#{comma_delimiter} LOCK=NONE"
+ end
+ end
+ end
+end
@@ -0,0 +1,138 @@
+require "spec_helper"
+
+describe ActiveRecord::Migration do
+ let(:comma_before_lock_none) { true }
+ let(:migration_arguments_with_lock) { [] }
+
+ context "#add_column" do
+ let(:method_name) { :add_column }
+ let(:migration_arguments) do
+ [
+ [:testing, :foo2, :string],
+ [:testing, :foo2, :string, { limit: 20, null: false, default: 'def' }],
+ [:testing, :foo2, :decimal, { precision:3, scale: 2 }]
+ ]
+ end
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#add_timestamps" do
+ let(:migration_arguments) do
+ [
+ [:testing2]
+ ]
+ end
+
+ let(:method_name) { :add_timestamps }
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#remove_column" do
+ let(:migration_arguments) do
+ [
+ [:testing, :foo],
+ [:testing, :foo, :bar]
+ ]
+ end
+
+ let(:method_name) { :remove_column }
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#remove_timestamps" do
+ let(:migration_arguments) do
+ [
+ [:testing]
+ ]
+ end
+
+ let(:method_name) { :remove_timestamps }
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#change_column" do
+ let(:migration_arguments) do
+ # Unsupported with lock=none : change column type, change limit, set NOT NULL.
+ [
+ [:testing, :foo, :string, { default: 'def', limit: 100 }],
+ [:testing, :foo, :string, { null: true, limit: 100 }]
+ ]
+ end
+
+ let(:migration_arguments_with_lock) do
+ [
+ [:testing, :foo, :string, { limit: 200 }],
+ [:testing, :foo, :string, { default: 'def' }],
+ [:testing, :foo, :string, { null: false }],
+ [:testing, :foo, :string, { null: false, default: 'def', limit: 200 }],
+ [:testing, :foo, :string, { null: true }],
+ [:testing, :foo, :integer, { null: true, limit: 6 }],
+ [:testing, :foo, :integer, { null: true, limit: 1 }]
+ ]
+ end
+
+ let(:method_name) { :change_column }
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ it_behaves_like "a migration with a non-lockable statement"
+ end
+
+ context "#change_column_default" do
+ let(:migration_arguments) do
+ [
+ [:testing, :foo, 'def'],
+ [:testing, :foo, nil]
+ ]
+ end
+
+ let(:method_name) { :change_column_default }
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#change_column_null" do
+ let(:migration_arguments) do
+ #change_column_null doesn't set DEFAULT in sql. It just issues an update statement before setting the NULL value if setting NULL to false
+ [
+ [:testing, :bam, true, nil],
+ [:testing, :bam, true, 'def']
+ ]
+ end
+
+ let(:migration_arguments_with_lock) do
+ [
+ [:testing, :bam, false, nil],
+ [:testing, :bam, false, 'def']
+ ]
+ end
+
+ let(:method_name) { :change_column_null }
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ it_behaves_like "a migration with a non-lockable statement"
+ end
+
+ context "#rename_column" do
+ let(:migration_arguments) do
+ [
+ [:testing, :foo, :foo2]
+ ]
+ end
+
+ let(:method_name) { :rename_column }
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+end
Oops, something went wrong.

0 comments on commit 5b3d598

Please sign in to comment.