departure 4.0.1 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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