departure 4.0.1 → 6.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/departure.gemspec CHANGED
@@ -1,30 +1,35 @@
1
1
  # coding: utf-8
2
+
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
 
5
6
  require 'departure/version'
6
7
 
8
+ # This environment variable is set on CI to facilitate testing with multiple
9
+ # versions of Rails.
10
+ RAILS_DEPENDENCY_VERSION = ENV.fetch('RAILS_VERSION', ['>= 5.2.0', '<= 6.1'])
11
+
7
12
  Gem::Specification.new do |spec|
8
13
  spec.name = 'departure'
9
14
  spec.version = Departure::VERSION
10
- spec.authors = ['Ilya Zayats', 'Pau Pérez', 'Fran Casas', 'Jorge Morante', 'Enrico Stano', 'Adrian Serafin']
11
- spec.email = ['ilya.zayats@redbooth.com', 'pau.perez@redbooth.com', 'fran.casas@redbooth.com', 'jorge.morante@redbooth.com', 'adrian@softmad.pl']
15
+ spec.authors = ['Ilya Zayats', 'Pau Pérez', 'Fran Casas', 'Jorge Morante', 'Enrico Stano', 'Adrian Serafin', 'Kirk Haines', 'Guillermo Iguaran']
16
+ spec.email = ['ilya.zayats@redbooth.com', 'pau.perez@redbooth.com', 'nflamel@gmail.com', 'jorge.morante@redbooth.com', 'adrian@softmad.pl', 'wyhaines@gmail.com', 'guilleiguaran@gmail.com']
12
17
 
13
- spec.summary = %q{pt-online-schema-change runner for ActiveRecord migrations}
14
- spec.description = %q{Execute your ActiveRecord migrations with Percona's pt-online-schema-change. Formerly known as Percona Migrator.}
15
- spec.homepage = 'http://github.com/redbooth/departure'
18
+ spec.summary = %q(pt-online-schema-change runner for ActiveRecord migrations)
19
+ spec.description = %q(Execute your ActiveRecord migrations with Percona's pt-online-schema-change. Formerly known as Percona Migrator.)
20
+ spec.homepage = 'https://github.com/departurerb/departure'
16
21
  spec.license = 'MIT'
17
22
 
18
23
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
24
  spec.require_paths = ['lib']
20
25
 
21
- spec.add_runtime_dependency 'rails', '~> 4.2.0'
22
- spec.add_runtime_dependency 'mysql2', '~> 0.4.0'
26
+ spec.add_runtime_dependency 'railties', *Array(RAILS_DEPENDENCY_VERSION)
27
+ spec.add_runtime_dependency 'activerecord', *Array(RAILS_DEPENDENCY_VERSION)
28
+ spec.add_runtime_dependency 'mysql2', '>= 0.4.0', '<= 0.5.3'
23
29
 
24
- spec.add_development_dependency 'bundler', '~> 1.10'
25
30
  spec.add_development_dependency 'rake', '~> 10.0'
26
31
  spec.add_development_dependency 'rspec', '~> 3.4', '>= 3.4.0'
27
32
  spec.add_development_dependency 'rspec-its', '~> 1.2'
28
- spec.add_development_dependency 'byebug', '~> 8.2', '>= 8.2.1'
33
+ spec.add_development_dependency 'pry-byebug'
29
34
  spec.add_development_dependency 'climate_control', '~> 0.0.3'
30
35
  end
@@ -0,0 +1,23 @@
1
+ version: '2'
2
+ services:
3
+ db:
4
+ image: mysql/mysql-server
5
+ ports:
6
+ - "3306:3306"
7
+ environment:
8
+ MYSQL_DATABASE: departure_test
9
+ MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
10
+ MYSQL_ROOT_HOST: '%'
11
+ rails:
12
+ build: .
13
+ links:
14
+ - db
15
+ depends_on:
16
+ - db
17
+ tty: true
18
+ stdin_open: true
19
+ environment:
20
+ PERCONA_DB_USER: root
21
+ PERCONA_DB_HOST: db
22
+ PERCONA_DB_PASSWORD:
23
+ PERCONA_DB_NAME: departure_test
@@ -0,0 +1,91 @@
1
+ require 'active_record/connection_adapters/mysql/schema_statements'
2
+
3
+ module ForAlterStatements
4
+ class << self
5
+ def included(_)
6
+ STDERR.puts 'Including for_alter statements'
7
+ end
8
+ end
9
+
10
+ def bulk_change_table(table_name, operations) #:nodoc:
11
+ sqls = operations.flat_map do |command, args|
12
+ table = args.shift
13
+ arguments = args
14
+
15
+ method = :"#{command}_for_alter"
16
+
17
+ raise "Unknown method called : #{method}(#{arguments.inspect})" unless respond_to?(method, true)
18
+ public_send(method, table, *arguments)
19
+ end.join(', ')
20
+
21
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
22
+ end
23
+
24
+ def change_column_for_alter(table_name, column_name, type, options = {})
25
+ column = column_for(table_name, column_name)
26
+ type ||= column.sql_type
27
+
28
+ options = {
29
+ default: column.default,
30
+ null: column.null,
31
+ comment: column.comment
32
+ }.merge(options)
33
+
34
+ td = create_table_definition(table_name)
35
+ cd = td.new_column_definition(column.name, type, options)
36
+ schema_creation.accept(ActiveRecord::ConnectionAdapters::ChangeColumnDefinition.new(cd, column.name))
37
+ end
38
+
39
+ def rename_column_for_alter(table_name, column_name, new_column_name)
40
+ column = column_for(table_name, column_name)
41
+ options = {
42
+ default: column.default,
43
+ null: column.null,
44
+ auto_increment: column.auto_increment?
45
+ }
46
+
47
+ columns_sql = "SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE #{quote(column_name)}"
48
+ current_type = exec_query(columns_sql, 'SCHEMA').first['Type']
49
+ td = create_table_definition(table_name)
50
+ cd = td.new_column_definition(new_column_name, current_type, options)
51
+ schema_creation.accept(ActiveRecord::ConnectionAdapters::ChangeColumnDefinition.new(cd, column.name))
52
+ end
53
+
54
+ def add_index_for_alter(table_name, column_name, options = {})
55
+ index_name, index_type, index_columns, _,
56
+ index_algorithm, index_using = add_index_options(table_name, column_name, options)
57
+
58
+ index_algorithm[0, 0] = ', ' if index_algorithm.present?
59
+ "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
60
+ end
61
+
62
+ def remove_index_for_alter(table_name, options = {})
63
+ index_name = index_name_for_remove(table_name, options)
64
+ "DROP INDEX #{quote_column_name(index_name)}"
65
+ end
66
+
67
+ def add_timestamps_for_alter(table_name, options = {})
68
+ [
69
+ add_column_for_alter(table_name, :created_at, :datetime, options),
70
+ add_column_for_alter(table_name, :updated_at, :datetime, options)
71
+ ]
72
+ end
73
+
74
+ def remove_timestamps_for_alter(table_name, _options = {})
75
+ [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
76
+ end
77
+
78
+ def add_column_for_alter(table_name, column_name, type, options = {})
79
+ td = create_table_definition(table_name)
80
+ cd = td.new_column_definition(column_name, type, options)
81
+ schema_creation.accept(ActiveRecord::ConnectionAdapters::AddColumnDefinition.new(cd))
82
+ end
83
+
84
+ def remove_column_for_alter(_table_name, column_name, _type = nil, _options = {})
85
+ "DROP COLUMN #{quote_column_name(column_name)}"
86
+ end
87
+
88
+ def remove_columns_for_alter(table_name, *column_names)
89
+ column_names.map { |column_name| remove_column_for_alter(table_name, column_name) }
90
+ end
91
+ end
@@ -9,10 +9,12 @@ module ActiveRecord
9
9
  # Establishes a connection to the database that's used by all Active
10
10
  # Record objects.
11
11
  def percona_connection(config)
12
+ if config[:username].nil?
13
+ config = config.dup if config.frozen?
14
+ config[:username] = 'root'
15
+ end
12
16
  mysql2_connection = mysql2_connection(config)
13
17
 
14
- config[:username] = 'root' if config[:username].nil?
15
-
16
18
  connection_details = Departure::ConnectionDetails.new(config)
17
19
  verbose = ActiveRecord::Migration.verbose
18
20
  sanitizers = [
@@ -40,32 +42,54 @@ module ActiveRecord
40
42
 
41
43
  module ConnectionAdapters
42
44
  class DepartureAdapter < AbstractMysqlAdapter
43
-
44
- class Column < AbstractMysqlAdapter::Column
45
+ class Column < ActiveRecord::ConnectionAdapters::MySQL::Column
45
46
  def adapter
46
47
  DepartureAdapter
47
48
  end
48
49
  end
49
50
 
51
+ class SchemaCreation < ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation
52
+ def visit_DropForeignKey(name) # rubocop:disable Naming/MethodName
53
+ fk_name =
54
+ if name =~ /^__(.+)/
55
+ Regexp.last_match(1)
56
+ else
57
+ "_#{name}"
58
+ end
59
+
60
+ "DROP FOREIGN KEY #{fk_name}"
61
+ end
62
+ end
63
+
50
64
  extend Forwardable
51
65
 
66
+ unless method_defined?(:change_column_for_alter)
67
+ include ForAlterStatements
68
+ end
69
+
52
70
  ADAPTER_NAME = 'Percona'.freeze
53
71
 
54
72
  def_delegators :mysql_adapter, :last_inserted_id, :each_hash, :set_field_encoding
55
73
 
56
74
  def initialize(connection, _logger, connection_options, _config)
75
+ @mysql_adapter = connection_options[:mysql_adapter]
57
76
  super
58
77
  @prepared_statements = false
59
- @mysql_adapter = connection_options[:mysql_adapter]
78
+ end
79
+
80
+ def write_query?(sql) # :nodoc:
81
+ !ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
82
+ :desc, :describe, :set, :show, :use
83
+ ).match?(sql)
60
84
  end
61
85
 
62
86
  def exec_delete(sql, name, binds)
63
87
  execute(to_sql(sql, binds), name)
64
88
  @connection.affected_rows
65
89
  end
66
- alias :exec_update :exec_delete
90
+ alias exec_update exec_delete
67
91
 
68
- def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
92
+ def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) # rubocop:disable Lint/UnusedMethodArgument, Metrics/LineLength
69
93
  execute(to_sql(sql, binds), name)
70
94
  end
71
95
 
@@ -76,8 +100,9 @@ module ActiveRecord
76
100
 
77
101
  # Executes a SELECT query and returns an array of rows. Each row is an
78
102
  # array of field values.
79
- def select_rows(sql, name = nil)
80
- execute(sql, name).to_a
103
+
104
+ def select_rows(arel, name = nil, binds = [])
105
+ select_all(arel, name, binds).rows
81
106
  end
82
107
 
83
108
  # Executes a SELECT query and returns an array of record hashes with the
@@ -91,9 +116,11 @@ module ActiveRecord
91
116
  true
92
117
  end
93
118
 
94
- def new_column(field, default, cast_type, sql_type = nil, null = true, collation = '', extra = '')
95
- Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
119
+ # rubocop:disable Metrics/ParameterLists
120
+ def new_column(field, default, type_metadata, null, table_name, default_function, collation, comment)
121
+ Column.new(field, default, type_metadata, null, table_name, default_function, collation, comment)
96
122
  end
123
+ # rubocop:enable Metrics/ParameterLists
97
124
 
98
125
  # Adds a new index to the table
99
126
  #
@@ -101,25 +128,56 @@ module ActiveRecord
101
128
  # @param column_name [String, Symbol]
102
129
  # @param options [Hash] optional
103
130
  def add_index(table_name, column_name, options = {})
104
- index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
105
- execute "ALTER TABLE #{quote_table_name(table_name)} ADD #{index_type} INDEX #{quote_column_name(index_name)} (#{index_columns})#{index_options}"
131
+ if ActiveRecord::VERSION::STRING >= '6.1'
132
+ index, algorithm, if_not_exists = add_index_options(table_name, column_name, options)
133
+ create_index = CreateIndexDefinition.new(index, algorithm, if_not_exists)
134
+ execute schema_creation.accept(create_index)
135
+ else
136
+ index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
137
+ execute "ALTER TABLE #{quote_table_name(table_name)} ADD #{index_type} INDEX #{quote_column_name(index_name)} (#{index_columns})#{index_options}" # rubocop:disable Metrics/LineLength
138
+ end
106
139
  end
107
140
 
108
141
  # Remove the given index from the table.
109
142
  #
110
143
  # @param table_name [String, Symbol]
111
144
  # @param options [Hash] optional
112
- def remove_index(table_name, options = {})
113
- index_name = index_name_for_remove(table_name, options)
145
+ def remove_index(table_name, *args, **options)
146
+ column_name = args.first
147
+ if column_name
148
+ return if options[:if_exists] && !index_exists?(table_name, column_name, **options)
149
+ index_name = index_name_for_remove(table_name, column_name, **options)
150
+ else
151
+ index_name = index_name_for_remove(table_name, options)
152
+ end
114
153
  execute "ALTER TABLE #{quote_table_name(table_name)} DROP INDEX #{quote_column_name(index_name)}"
115
154
  end
116
155
 
156
+ def schema_creation
157
+ SchemaCreation.new(self)
158
+ end
159
+
160
+ def change_table(table_name, _options = {})
161
+ recorder = ActiveRecord::Migration::CommandRecorder.new(self)
162
+ yield update_table_definition(table_name, recorder)
163
+ bulk_change_table(table_name, recorder.commands)
164
+ end
165
+
117
166
  # Returns the MySQL error number from the exception. The
118
167
  # AbstractMysqlAdapter requires it to be implemented
119
- def error_number(_exception)
120
- end
168
+ def error_number(_exception); end
121
169
 
122
170
  def full_version
171
+ if ActiveRecord::VERSION::MAJOR < 6
172
+ get_full_version
173
+ else
174
+ schema_cache.database_version.full_version_string
175
+ end
176
+ end
177
+
178
+ # This is a method defined in Rails 6.0, and we have no control over the
179
+ # naming of this method.
180
+ def get_full_version # rubocop:disable Naming/AccessorMethodName
123
181
  mysql_adapter.raw_connection.server_info[:version]
124
182
  end
125
183
 
data/lib/departure.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'active_record'
2
2
  require 'active_support/all'
3
3
 
4
+ require 'active_record/connection_adapters/for_alter'
5
+
4
6
  require 'departure/version'
5
7
  require 'departure/log_sanitizers/password_sanitizer'
6
8
  require 'departure/runner'
@@ -11,12 +13,20 @@ require 'departure/logger_factory'
11
13
  require 'departure/configuration'
12
14
  require 'departure/errors'
13
15
  require 'departure/command'
16
+ require 'departure/connection_base'
17
+ require 'departure/migration'
14
18
 
15
19
  require 'departure/railtie' if defined?(Rails)
16
20
 
17
21
  # We need the OS not to buffer the IO to see pt-osc's output while migrating
18
22
  $stdout.sync = true
19
23
 
24
+ ActiveSupport.on_load(:active_record) do
25
+ ActiveRecord::Migration.class_eval do
26
+ include Departure::Migration
27
+ end
28
+ end
29
+
20
30
  module Departure
21
31
  class << self
22
32
  attr_accessor :configuration
@@ -27,55 +37,7 @@ module Departure
27
37
  yield(configuration)
28
38
  end
29
39
 
30
- # Hooks Percona Migrator into Rails migrations by replacing the configured
31
- # database adapter
32
40
  def self.load
33
- ActiveRecord::Migrator.instance_eval do
34
- class << self
35
- alias_method(:original_migrate, :migrate)
36
- end
37
-
38
- # Checks whether arguments are being passed through PERCONA_ARGS when running
39
- # the db:migrate rake task
40
- #
41
- # @raise [ArgumentsNotSupported] if PERCONA_ARGS has any value
42
- def migrate(migrations_paths, target_version = nil, &block)
43
- raise ArgumentsNotSupported if ENV['PERCONA_ARGS'].present?
44
- original_migrate(migrations_paths, target_version, &block)
45
- end
46
- end
47
-
48
- ActiveRecord::Migration.class_eval do
49
- alias_method :original_migrate, :migrate
50
-
51
- # Replaces the current connection adapter with the PerconaAdapter and
52
- # patches LHM, then it continues with the regular migration process.
53
- #
54
- # @param direction [Symbol] :up or :down
55
- def migrate(direction)
56
- reconnect_with_percona
57
- include_foreigner if defined?(Foreigner)
58
-
59
- ::Lhm.migration = self
60
- original_migrate(direction)
61
- end
62
-
63
- # Includes the Foreigner's Mysql2Adapter implemention in
64
- # DepartureAdapter to support foreign keys
65
- def include_foreigner
66
- Foreigner::Adapter.safe_include(
67
- :DepartureAdapter,
68
- Foreigner::ConnectionAdapters::Mysql2Adapter
69
- )
70
- end
71
-
72
- # Make all connections in the connection pool to use PerconaAdapter
73
- # instead of the current adapter.
74
- def reconnect_with_percona
75
- connection_config = ActiveRecord::Base
76
- .connection_config.merge(adapter: 'percona')
77
- ActiveRecord::Base.establish_connection(connection_config)
78
- end
79
- end
41
+ # No-op left for compatibility
80
42
  end
81
43
  end
@@ -4,7 +4,7 @@ module Departure
4
4
  # Represents the '--alter' argument of Percona's pt-online-schema-change
5
5
  # See https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html
6
6
  class AlterArgument
7
- ALTER_TABLE_REGEX = /\AALTER TABLE `(\w+)` /
7
+ ALTER_TABLE_REGEX = /\AALTER TABLE [^\s]*[\n]* /
8
8
 
9
9
  attr_reader :table_name
10
10
 
@@ -17,8 +17,12 @@ module Departure
17
17
 
18
18
  match = statement.match(ALTER_TABLE_REGEX)
19
19
  raise InvalidAlterStatement unless match
20
-
21
- @table_name = match.captures[0]
20
+ # Separates the ALTER TABLE from the table_name
21
+ #
22
+ # Removes the grave marks, if they are there, so we can get the table_name
23
+ @table_name = String(match)
24
+ .split(' ')[2]
25
+ .delete('`')
22
26
  end
23
27
 
24
28
  # Returns the '--alter' pt-online-schema-change argument as a string. See
@@ -37,7 +41,9 @@ module Departure
37
41
  def parsed_statement
38
42
  @parsed_statement ||= statement
39
43
  .gsub(ALTER_TABLE_REGEX, '')
40
- .gsub('`','\\\`')
44
+ .gsub('`', '\\\`')
45
+ .gsub(/\\n/, '')
46
+ .gsub('"', '\\\"')
41
47
  end
42
48
  end
43
49
  end
@@ -5,12 +5,11 @@ require 'departure/connection_details'
5
5
  require 'departure/user_options'
6
6
 
7
7
  module Departure
8
-
9
8
  # Generates the equivalent Percona's pt-online-schema-change command to the
10
9
  # given SQL statement
11
10
  #
12
- # --no-check-alter is used to allow running CHANGE COLUMN statements. For
13
- # more details, check: www.percona.com/doc/percona-toolkit/2.2/pt-online-schema-change.html#cmdoption-pt-online-schema-change--[no]check-alter
11
+ # --no-check-alter is used to allow running CHANGE COLUMN statements. For more details, check:
12
+ # www.percona.com/doc/percona-toolkit/2.2/pt-online-schema-change.html#cmdoption-pt-online-schema-change--[no]check-alter # rubocop:disable Metrics/LineLength
14
13
  #
15
14
  class CliGenerator
16
15
  COMMAND_NAME = 'pt-online-schema-change'.freeze