Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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

Change strategy to patch sql statements issued to the db
  • Loading branch information...
commit 5b3d5984fdfd2d3963933151fb22f3b9a0503333 2 parents f290b74 + 8d57728
@anthonyalberto authored
View
12 .travis.yml
@@ -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
2  Gemfile.lock
@@ -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
7 gemfiles/rails3.gemfile
@@ -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
7 gemfiles/rails4.gemfile
@@ -0,0 +1,7 @@
+source :rubygems
+gem "activerecord", "4.0.2"
+gem "activesupport", "4.0.2"
+gem "mysql2"
+gem "logger"
+gem "rspec"
+gem "pry"
View
27 lib/mysql_online_migrations.rb
@@ -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)
View
63 lib/mysql_online_migrations/columns.rb
@@ -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
View
21 lib/mysql_online_migrations/indexes.rb
@@ -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
View
32 lib/mysql_online_migrations/mysql2_adapter_without_lock.rb
@@ -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
View
138 spec/lib/migration/column_spec.rb
@@ -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
View
49 spec/lib/migration/index_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe ActiveRecord::Migration do
+ let(:comma_before_lock_none) { false }
+ let(:migration_arguments_with_lock) { [] }
+ context "#add_index" do
+ let(:method_name) { :add_index }
+ let(:migration_arguments) do
+ [
+ [:testing, :foo],
+ [:testing, :foo, { length: 10 }],
+ [:testing, [:foo, :bar, :baz], {}],
+ [:testing, [:foo, :bar, :baz], { unique: true }],
+ [:testing, [:foo, :bar, :baz], { unique: true, name: "best_index_of_the_world" }]
+ ]
+ end
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#remove_index" do
+ let(:method_name) { :remove_index }
+ let(:migration_arguments) do
+ [
+ [:testing, :baz],
+ [:testing, [:bar, :baz]],
+ [:testing, { column: [:bar, :baz] }],
+ [:testing, { name: "best_index_of_the_world2" }]
+ ]
+ end
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#rename_index" do
+ let(:method_name) { :rename_index }
+ let(:migration_arguments) do
+ [
+ [:testing, "best_index_of_the_world2", "renamed_best_index_of_the_world2"],
+ [:testing, "best_index_of_the_world3", "renamed_best_index_of_the_world3"]
+ ]
+ end
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+end
View
46 spec/lib/migration/table_spec.rb
@@ -0,0 +1,46 @@
+require "spec_helper"
+
+describe ActiveRecord::Migration do
+ let(:comma_before_lock_none) { true }
+ let(:migration_arguments_with_lock) { [] }
+
+ context "#create_table" do
+ let(:method_name) { :create_table }
+ let(:migration_arguments) do
+ [
+ [:test5]
+ ]
+ end
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#drop_table" do
+ let(:method_name) { :drop_table }
+ let(:migration_arguments) do
+ [
+ [:testing]
+ ]
+ end
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+
+ context "#rename_table" do
+ before(:each) do
+ @rescue_statement_when_stubbed = true
+ end
+
+ let(:method_name) { :rename_table }
+ let(:migration_arguments) do
+ [
+ [:testing, :testing20]
+ ]
+ end
+
+ it_behaves_like "a migration that adds LOCK=NONE when needed"
+ it_behaves_like "a migration that succeeds in MySQL"
+ end
+end
View
180 spec/lib/mysql_online_migrations/columns_spec.rb
@@ -1,180 +0,0 @@
-require "spec_helper"
-
-describe MysqlOnlineMigrations::Columns do
- let(:comma_before_lock_none) { true }
- let(:queries_with_lock) { {} }
-
- context "#add_column" do
- let(:queries) do
- {
- [:testing, :foo2, :string, {}] =>
- "ALTER TABLE `testing` ADD `foo2` varchar(255)",
- [:testing, :foo2, :string, { limit: 20, null: false, default: 'def' }] =>
- "ALTER TABLE `testing` ADD `foo2` varchar(20) DEFAULT 'def' NOT NULL",
- [:testing, :foo2, :decimal, { precision:3, scale: 2 }] =>
- "ALTER TABLE `testing` ADD `foo2` decimal(3,2)",
- }
- end
-
- let(:method_name) { :add_column }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- end
-
- context "#add_timestamps" do
- let(:queries) do
- {
- [:testing2, {}] =>
- [
- "ALTER TABLE `testing2` ADD `created_at` datetime",
- "ALTER TABLE `testing2` ADD `updated_at` datetime",
- ]
- }
- end
-
- let(:method_name) { :add_timestamps }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- end
-
- context "#remove_column" do
- let(:queries) do
- {
- [:testing, :foo, {}] =>
- "ALTER TABLE `testing` DROP `foo`",
- [:testing, [:foo, :bar], {}] =>
- [
- "ALTER TABLE `testing` DROP `foo`",
- "ALTER TABLE `testing` DROP `bar`"
- ],
- [:testing, :foo, :bar, {}] =>
- [
- "ALTER TABLE `testing` DROP `foo`",
- "ALTER TABLE `testing` DROP `bar`"
- ]
- }
- end
-
- let(:method_name) { :remove_column }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- end
-
- context "#remove_timestamps" do
- let(:queries) do
- {
- [:testing, {}] =>
- [
- "ALTER TABLE `testing` DROP `created_at`",
- "ALTER TABLE `testing` DROP `updated_at`",
- ]
- }
- end
-
- let(:method_name) { :remove_timestamps }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- end
-
- context "#change_column" do
- let(:queries) do
- # Unsupported with lock=none : change column type, change limit, set NOT NULL.
- {
- [:testing, :foo, :string, { default: 'def', limit: 100 }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(100) DEFAULT 'def'",
- [:testing, :foo, :string, { null: true, limit: 100 }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(100) DEFAULT NULL",
- }
- end
-
- let(:queries_with_lock) do
- {
- [:testing, :foo, :string, { limit: 200 }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(200) DEFAULT NULL",
- [:testing, :foo, :string, { default: 'def' }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(255) DEFAULT 'def'",
- [:testing, :foo, :string, { null: false }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(255) NOT NULL",
- [:testing, :foo, :string, { null: false, default: 'def', limit: 200 }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(200) DEFAULT 'def' NOT NULL",
- [:testing, :foo, :string, { null: true }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(255) DEFAULT NULL",
- [:testing, :foo, :integer, { null: true, limit: 6 }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` bigint DEFAULT NULL",
- [:testing, :foo, :integer, { null: true, limit: 1 }] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` tinyint DEFAULT NULL"
- }
- end
-
- let(:method_name) { :change_column }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- it_behaves_like "queries_with_lock that executes properly"
- end
-
- context "#change_column_default" do
- let(:queries) do
- {
- [:testing, :foo, 'def', {}] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(100) DEFAULT 'def'",
- [:testing, :foo, nil, {}] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo` varchar(100) DEFAULT NULL"
- }
- end
-
- let(:method_name) { :change_column_default }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- it_behaves_like "queries_with_lock that executes properly"
- end
-
- context "#change_column_null" do
- let(:queries) 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, {}] =>
- "ALTER TABLE `testing` CHANGE `bam` `bam` varchar(100) DEFAULT 'test'",
- [:testing, :bam, true, 'def', {}] =>
- "ALTER TABLE `testing` CHANGE `bam` `bam` varchar(100) DEFAULT 'test'"
- }
- end
-
- let(:queries_with_lock) do
- {
- [:testing, :bam, false, nil, {}] =>
- "ALTER TABLE `testing` CHANGE `bam` `bam` varchar(100) DEFAULT 'test' NOT NULL",
- [:testing, :bam, false, 'def', {}] =>
- [
- "UPDATE `testing` SET `bam`='def' WHERE `bam` IS NULL",
- "ALTER TABLE `testing` CHANGE `bam` `bam` varchar(100) DEFAULT 'test' NOT NULL"
- ]
- }
- end
-
- let(:method_name) { :change_column_null }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- it_behaves_like "queries_with_lock that executes properly"
- end
-
- context "#rename_column" do
- let(:queries) do
- {
- [:testing, :foo, :foo2, {}] =>
- "ALTER TABLE `testing` CHANGE `foo` `foo2` varchar(100) DEFAULT NULL"
- }
- end
-
- let(:method_name) { :rename_column }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- end
-end
View
68 spec/lib/mysql_online_migrations/indexes_spec.rb
@@ -1,68 +0,0 @@
-require "spec_helper"
-
-describe MysqlOnlineMigrations::Indexes do
- let(:comma_before_lock_none) { false }
- let(:queries_with_lock) { {} }
-
- context "#add_index" do
- let(:queries) do
- {
- [:testing, :foo, {}] =>
- "CREATE INDEX `index_testing_on_foo` ON `testing` (`foo`)",
- [:testing, :foo, { length: 10 }] =>
- "CREATE INDEX `index_testing_on_foo` ON `testing` (`foo`(10))",
- [:testing, [:foo, :bar, :baz], {}] =>
- "CREATE INDEX `index_testing_on_foo_and_bar_and_baz` ON `testing` (`foo`, `bar`, `baz`)",
- [:testing, [:foo, :bar, :baz], { unique: true }] =>
- "CREATE UNIQUE INDEX `index_testing_on_foo_and_bar_and_baz` ON `testing` (`foo`, `bar`, `baz`)",
- [:testing, [:foo, :bar, :baz], { unique: true, name: "best_index_of_the_world" }] =>
- "CREATE UNIQUE INDEX `best_index_of_the_world` ON `testing` (`foo`, `bar`, `baz`)",
- }
- end
-
- let(:method_name) { :add_index }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- end
-
- context "#remove_index" do
- let(:queries) do
- {
- [:testing, :baz, {}] =>
- "DROP INDEX `index_testing_on_baz` ON `testing`",
- [:testing, [:bar, :baz], {}] =>
- "DROP INDEX `index_testing_on_bar_and_baz` ON `testing`",
- [:testing, { column: [:bar, :baz] }, {}] =>
- "DROP INDEX `index_testing_on_bar_and_baz` ON `testing`",
- [:testing, { name: "best_index_of_the_world2" }, {}] =>
- "DROP INDEX `best_index_of_the_world2` ON `testing`"
- }
- end
- let(:method_name) { :remove_index }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- end
-
- context "#rename_index" do
- let(:queries) do
- {
- [:testing, "best_index_of_the_world2", "renamed_best_index_of_the_world2", {}] =>
- [
- "DROP INDEX `best_index_of_the_world2` ON `testing`",
- "CREATE INDEX `renamed_best_index_of_the_world2` ON `testing` (`extra`)",
- ],
- [:testing, "best_index_of_the_world3", "renamed_best_index_of_the_world3", {}] =>
- [
- "DROP INDEX `best_index_of_the_world3` ON `testing`",
- "CREATE UNIQUE INDEX `renamed_best_index_of_the_world3` ON `testing` (`baz`, `extra`)",
- ]
- }
- end
- let(:method_name) { :rename_index }
-
- it_behaves_like "a method that adds LOCK=NONE when needed"
- it_behaves_like "a request with LOCK=NONE that doesn't crash in MySQL"
- end
-end
View
137 spec/lib/mysql_online_migrations/mysql2_adapter_without_lock_spec.rb
@@ -0,0 +1,137 @@
+require "spec_helper"
+
+describe ActiveRecord::ConnectionAdapters::Mysql2AdapterWithoutLock do
+ context "#initialize" do
+ it "successfully instantiates a working adapter" do
+ ActiveRecord::ConnectionAdapters::Mysql2AdapterWithoutLock.new(@adapter).should be_active
+ end
+ end
+
+ context "#lock_none_statement" do
+ context "with mysql_online_migrations set to true" do
+ context "with alter" do
+ let(:query) { "alter " }
+ it "adds ', LOCK=NONE'" do
+ @adapter_without_lock.lock_none_statement("alter ").should == ", LOCK=NONE"
+ end
+ end
+ context "with drop index" do
+ let(:query) { "drop index " }
+ it "adds ' LOCK=NONE'" do
+ @adapter_without_lock.lock_none_statement("drop index ").should == " LOCK=NONE"
+ end
+ end
+ context "with create index" do
+ let(:query) { "create index " }
+ it "adds ' LOCK=NONE'" do
+ @adapter_without_lock.lock_none_statement("create index ").should == " LOCK=NONE"
+ end
+ end
+ context "with a query with LOCK=NONE already there" do
+ it "doesn't add anything" do
+ @adapter_without_lock.lock_none_statement("alter LOCK=NONE ").should == ""
+ end
+ end
+ end
+
+ context "with mysql_online_migrations set to false" do
+ before(:each) do
+ set_ar_setting(false)
+ end
+
+ after(:each) do
+ set_ar_setting(true)
+ end
+
+ it "doesn't add anything to the request" do
+ @adapter_without_lock.lock_none_statement("alter ").should == ""
+ end
+ end
+ end
+
+ context "#execute" do
+ shared_examples_for "#execute that changes the SQL" do
+ it "adds LOCK=NONE at the end of the query" do
+ comma = query =~ /alter /i ? "," : ""
+ expected_output = "#{query} #{comma} LOCK=NONE"
+ @adapter_without_lock.should_receive(:original_execute).with(expected_output, nil)
+ @adapter_without_lock.execute(query)
+ end
+ end
+
+ shared_examples_for "#execute that doesn't change the SQL" do
+ it "just passes the query to original_execute" do
+ @adapter_without_lock.should_receive(:original_execute).with(query, nil)
+ @adapter_without_lock.execute(query)
+ end
+ end
+
+ context "with an optimizable DDL statement" do
+ context "with alter" do
+ let(:query) { "alter " }
+ it_behaves_like "#execute that changes the SQL"
+ end
+ context "with drop index" do
+ let(:query) { "drop index " }
+ it_behaves_like "#execute that changes the SQL"
+ end
+ context "with create index" do
+ let(:query) { "create index " }
+ it_behaves_like "#execute that changes the SQL"
+ end
+ context "with create unique index" do
+ let(:query) { "create unique index " }
+ it_behaves_like "#execute that changes the SQL"
+ end
+ end
+
+ context "with other DDL statements" do
+ context "with create table" do
+ let(:query) { "create table " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+
+ context "with drop table" do
+ let(:query) { "drop table " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+ end
+
+ context "with a regular statement" do
+ context "with select" do
+ let(:query) { "select " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+
+ context "with set" do
+ let(:query) { "set " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+
+ context "with insert" do
+ let(:query) { "insert " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+
+ context "with update" do
+ let(:query) { "update " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+
+ context "with delete" do
+ let(:query) { "delete " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+
+ context "with show" do
+ let(:query) { "show " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+
+ context "with explain" do
+ let(:query) { "explain " }
+ it_behaves_like "#execute that doesn't change the SQL"
+ end
+ end
+ end
+end
View
30 spec/lib/mysql_online_migrations_spec.rb
@@ -0,0 +1,30 @@
+require "spec_helper"
+
+describe MysqlOnlineMigrations do
+ let(:migration) { migration = ActiveRecord::Migration.new }
+
+ context ".prepended" do
+ it "sets ActiveRecord::Base.mysql_online_migrations to true" do
+ ActiveRecord::Base.mysql_online_migrations.should be_true
+ end
+ end
+
+ context "#connection" do
+ it "memoizes an instance of Mysql2AdapterWithoutLock" do
+ ActiveRecord::ConnectionAdapters::Mysql2AdapterWithoutLock.should_receive(:new)
+ .with(an_instance_of(ActiveRecord::ConnectionAdapters::Mysql2Adapter)).once.and_call_original
+ 3.times { @connection = migration.connection }
+ @connection.should be_an_instance_of(ActiveRecord::ConnectionAdapters::Mysql2AdapterWithoutLock)
+ end
+ end
+
+ context "#with_lock" do
+ it "switches mysql_online_migrations flag to false and then back to original value after block execution" do
+ ActiveRecord::Base.mysql_online_migrations.should be_true
+ migration.with_lock do
+ ActiveRecord::Base.mysql_online_migrations.should be_false
+ end
+ ActiveRecord::Base.mysql_online_migrations.should be_true
+ end
+ end
+end
View
2  spec/spec_helper.rb
@@ -11,7 +11,7 @@
require 'logger'
require 'pry'
require 'support/helpers'
-require 'support/shared_examples/mysql_online_migrations'
+require 'support/shared_examples/migration'
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
View
57 spec/support/helpers.rb
@@ -1,21 +1,42 @@
module Helpers
- CATCH_STATEMENT_REGEX = /^(alter|create|drop|update) /i
- DDL_STATEMENT_REGEX = /^(alter|create|drop) /i
+ CATCH_STATEMENT_REGEX = /^(alter|create|drop|update|rename) /i
+ DDL_STATEMENT_REGEX = /^(alter|create (unique )? ?index|drop index) /i
- def execute(statement)
+ def build_migration(method_name, args, &block)
+ migration = ActiveRecord::Migration.new
+ migration.instance_variable_set(:@test_method_name, method_name)
+ migration.instance_variable_set(:@test_args, args)
+ migration.instance_variable_set(:@test_block, block)
+ migration.define_singleton_method(:change) do
+ public_send(@test_method_name, *@test_args, &@test_block)
+ end
+ migration
+ end
+
+ def regular_execute(statement)
+ @queries_received_by_regular_adapter << statement
+ end
+
+ def execute_without_lock(statement)
+ @queries_received_by_adapter_without_lock << statement
end
def unstub_execute
@adapter.unstub(:execute)
end
- def stub_execute
- original_execute = @adapter.method(:execute)
- @adapter.stub(:execute) do |statement|
- if statement =~ CATCH_STATEMENT_REGEX
- execute(statement.squeeze(' ').strip)
+ def stub_adapter_without_lock
+ ActiveRecord::ConnectionAdapters::Mysql2AdapterWithoutLock.stub(:new).and_return(@adapter_without_lock)
+ end
+
+ def stub_execute(adapter, original_method, method_to_call)
+ original_execute = adapter.method(original_method)
+
+ adapter.stub(original_method) do |sql|
+ if sql =~ CATCH_STATEMENT_REGEX
+ send(method_to_call, sql.squeeze(' ').strip)
else
- original_execute.call(statement)
+ original_execute.call(sql)
end
end
end
@@ -28,9 +49,16 @@ def add_lock_none(str, with_comma)
end
end
+ def drop_all_tables
+ @adapter.tables.each do |table|
+ @adapter.drop_table(table) rescue nil
+ end
+ end
+
def rebuild_table
@table_name = :testing
- @adapter.drop_table @table_name rescue nil
+ drop_all_tables
+
@adapter.create_table @table_name do |t|
t.column :foo, :string, :limit => 100
t.column :bar, :string, :limit => 100
@@ -41,7 +69,6 @@ def rebuild_table
end
@table_name = :testing2
- @adapter.drop_table @table_name rescue nil
@adapter.create_table @table_name do |t|
end
@@ -54,18 +81,16 @@ def rebuild_table
def setup
ActiveRecord::Base.establish_connection(
adapter: :mysql2,
- reconnect: false,
database: "mysql_online_migrations",
- username: "root",
- host: "localhost",
- encoding: "utf8",
- socket: "/tmp/mysql.sock"
+ username: "travis",
+ encoding: "utf8"
)
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger.level = Logger::INFO
@adapter = ActiveRecord::Base.connection
+ @adapter_without_lock = ActiveRecord::ConnectionAdapters::Mysql2AdapterWithoutLock.new(@adapter)
rebuild_table
end
View
71 spec/support/shared_examples/migration.rb
@@ -0,0 +1,71 @@
+def reset_queries_collectors
+ @queries_received_by_regular_adapter = []
+ @queries_received_by_adapter_without_lock = []
+end
+
+def staged_for_travis
+ set_ar_setting(false) if ENV["TRAVIS"] # Travis doesn't run MySQL 5.6. Run tests locally first.
+ yield
+ set_ar_setting(true) if ENV["TRAVIS"]
+end
+
+shared_examples_for "a migration that adds LOCK=NONE when needed" do
+ before(:each) do
+ stub_adapter_without_lock
+ stub_execute(@adapter, :execute, :regular_execute)
+ stub_execute(@adapter_without_lock, :original_execute, :execute_without_lock)
+ @migration_arguments = migration_arguments + migration_arguments_with_lock
+ end
+
+ it "executes the same query as the original adapter, with LOCK=NONE when required" do
+ @migration_arguments.each do |migration_argument|
+ reset_queries_collectors
+
+ begin
+ @adapter.public_send(method_name, *migration_argument)
+ rescue => e
+ raise e unless @rescue_statement_when_stubbed
+ end
+
+ begin
+ build_migration(method_name, migration_argument).migrate(:up)
+ rescue => e
+ raise e unless @rescue_statement_when_stubbed
+ end
+
+ @queries_received_by_regular_adapter.length.should > 0
+ @queries_received_by_regular_adapter.length.should == @queries_received_by_adapter_without_lock.length
+ @queries_received_by_regular_adapter.each_with_index do |query, index|
+ @queries_received_by_adapter_without_lock[index].should == add_lock_none(query, comma_before_lock_none)
+ end
+ end
+ end
+end
+
+shared_examples_for "a migration that succeeds in MySQL" do
+ it "succeeds without exception" do
+ staged_for_travis do
+ migration_arguments.each do |migration_argument|
+ migration = build_migration(method_name, migration_argument)
+ migration.migrate(:up)
+ rebuild_table
+ end
+ end
+ end
+end
+
+shared_examples_for "a migration with a non-lockable statement" do
+ it "raises a MySQL exception" do
+ staged_for_travis do
+ migration_arguments_with_lock.each do |migration_argument|
+ migration = build_migration(method_name, migration_argument)
+ begin
+ migration.migrate(:up)
+ rescue ActiveRecord::StatementInvalid => e
+ e.message.should =~ /LOCK=NONE is not supported/
+ end
+ rebuild_table
+ end
+ end
+ end
+end
View
56 spec/support/shared_examples/mysql_online_migrations.rb
@@ -1,56 +0,0 @@
-shared_examples_for "a method that adds LOCK=NONE when needed" do
- before(:each) do
- stub_execute
- @queries = queries.merge(queries_with_lock)
- end
-
- it "adds LOCK=NONE at the end of the query" do
- @queries.each do |arguments, output|
- Array.wrap(output).each { |out| should_receive(:execute).with(add_lock_none(out, comma_before_lock_none)) }
- @adapter.public_send(method_name, *arguments)
- end
- end
-
- context "with AR global setting set to false" do
- before(:each) { set_ar_setting(false) }
-
- it "doesn't add lock none" do
- @queries.each do |arguments, output|
- Array.wrap(output).each { |out| should_receive(:execute).with(out) }
- @adapter.public_send(method_name, *arguments)
- end
- end
- end
-
- context "with lock: true option" do
- it "doesn't add lock none" do
- @queries.each do |arguments, output|
- Array.wrap(output).each { |out| should_receive(:execute).with(out) }
- arguments[-1] = arguments.last.merge(lock: true)
- @adapter.public_send(method_name, *arguments)
- end
- end
- end
-end
-
-shared_examples_for "a request with LOCK=NONE that doesn't crash in MySQL" do
- it "succeeds without exception" do
- queries.each do |_, output|
- Array.wrap(output).each do |out|
- @adapter.execute(add_lock_none(out, comma_before_lock_none))
- rebuild_table
- end
- end
- end
-end
-
-shared_examples_for "queries_with_lock that executes properly" do
- it "succeeds without exception" do
- queries.each do |_, output|
- Array.wrap(output).each do |out|
- @adapter.execute(out)
- rebuild_table
- end
- end
- end
-end
Please sign in to comment.
Something went wrong with that request. Please try again.