departure 7.0.0 → 8.0.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.
@@ -215,7 +215,11 @@ module ActiveRecord
215
215
 
216
216
  # THIS IS THE CORE CHANGES 1 related to size
217
217
  # We will sometimes have a process exit code instead of a result from executing
218
- @affected_rows_before_warnings = result.try(:size) || raw_connection.affected_rows
218
+ @affected_rows_before_warnings = if result.is_a? Process::Status
219
+ result.try(:size) || 0
220
+ else
221
+ result.try(:size) || raw_connection.affected_rows
222
+ end
219
223
  end
220
224
  elsif prepare
221
225
 
@@ -0,0 +1,83 @@
1
+ require 'active_record/connection_adapters/abstract_mysql_adapter'
2
+ require 'active_record/connection_adapters/mysql2_adapter'
3
+ require 'active_record/connection_adapters/patch_connection_handling'
4
+ require 'departure'
5
+ require 'forwardable'
6
+
7
+ module ActiveRecord
8
+ module ConnectionAdapters
9
+ class Rails81DepartureAdapter < ActiveRecord::ConnectionAdapters::Mysql2Adapter
10
+ TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) } if defined?(initialize_type_map)
11
+
12
+ class Column < ActiveRecord::ConnectionAdapters::MySQL::Column
13
+ def adapter
14
+ Rails81DepartureAdapter
15
+ end
16
+ end
17
+
18
+ # https://github.com/departurerb/departure/commit/f178ca26cd3befa1c68301d3b57810f8cdcff9eb
19
+ # For `DROP FOREIGN KEY constraint_name` with pt-online-schema-change requires specifying `_constraint_name`
20
+ # rather than the real constraint_name due to to a limitation in MySQL
21
+ # pt-online-schema-change adds a leading underscore to foreign key constraint names when creating the new table.
22
+ # https://www.percona.com/blog/2017/03/21/dropping-foreign-key-constraint-using-pt-online-schema-change-2/
23
+ class SchemaCreation < ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation
24
+ def visit_DropForeignKey(name) # rubocop:disable Naming/MethodName
25
+ fk_name =
26
+ if name =~ /^__(.+)/
27
+ Regexp.last_match(1)
28
+ else
29
+ "_#{name}"
30
+ end
31
+
32
+ "DROP FOREIGN KEY #{fk_name}"
33
+ end
34
+ end
35
+
36
+ extend Forwardable
37
+
38
+ include ForAlterStatements unless method_defined?(:change_column_for_alter)
39
+
40
+ ADAPTER_NAME = 'Percona'.freeze
41
+
42
+ def self.new_client(config)
43
+ original_client = super
44
+
45
+ Departure::DbClient.new(config, original_client)
46
+ end
47
+
48
+ # add_index is modified from the underlying mysql adapter implementation to ensure we add ALTER TABLE to it
49
+ def add_index(table_name, column_name, options = {})
50
+ index_definition, = add_index_options(table_name, column_name, **options)
51
+ execute <<-SQL.squish
52
+ ALTER TABLE #{quote_table_name(index_definition.table)}
53
+ ADD #{schema_creation.accept(index_definition)}
54
+ SQL
55
+ end
56
+
57
+ # remove_index is modified from the underlying mysql adapter implementation to ensure we add ALTER TABLE to it
58
+ def remove_index(table_name, column_name = nil, **options)
59
+ return if options[:if_exists] && !index_exists?(table_name, column_name, **options)
60
+
61
+ index_name = index_name_for_remove(table_name, column_name, options)
62
+
63
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP INDEX #{quote_column_name(index_name)}"
64
+ end
65
+
66
+ def schema_creation
67
+ SchemaCreation.new(self)
68
+ end
69
+
70
+ private
71
+
72
+ attr_reader :mysql_adapter
73
+
74
+ # rubocop:disable Metrics/ParameterLists
75
+ def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
76
+ return raw_connection.send_to_pt_online_schema_change(sql) if raw_connection.alter_statement?(sql)
77
+
78
+ super
79
+ end
80
+ # rubocop:enable Metrics/ParameterLists
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,52 @@
1
+ require 'open3'
2
+
3
+ require 'forwardable'
4
+
5
+ module Departure
6
+ # It executes pt-online-schema-change commands in a new process and gets its
7
+ # output and status
8
+ class DbClient
9
+ delegate_missing_to :database_client
10
+
11
+ attr_reader :database_client
12
+
13
+ def initialize(config, database_client)
14
+ @config = config
15
+ @database_client = database_client
16
+
17
+ connection_details = Departure::ConnectionDetails.new(config)
18
+ verbose = ActiveRecord::Migration.verbose
19
+ sanitizers = [Departure::LogSanitizers::PasswordSanitizer.new(connection_details)]
20
+ @logger = Departure::LoggerFactory.build(sanitizers: sanitizers, verbose: verbose)
21
+ @cli_generator = Departure::CliGenerator.new(connection_details)
22
+ end
23
+
24
+ # Intercepts raw query calls to pass ALTER TABLE statements to pt-online-schema-change
25
+ # otherwise sends to the database adapter
26
+ # eg: goes to pt-online-schema-change
27
+ # query("ALTER TABLE `comments` ADD INDEX `index_comments_on_some_id_field` (`some_id_field`))
28
+ # eg: sends to database adapter
29
+ # query("COMMIT") - query("SELECT * from 'comments'")
30
+ def query(raw_sql_string)
31
+ if alter_statement?(raw_sql_string)
32
+ send_to_pt_online_schema_change(raw_sql_string)
33
+ else
34
+ database_client.query(raw_sql_string)
35
+ end
36
+ end
37
+
38
+ # Runs raw_sql_string through pt-online-schema-change command line tool
39
+ def send_to_pt_online_schema_change(raw_sql_string)
40
+ command_line = @cli_generator.parse_statement(raw_sql_string)
41
+
42
+ Command.new(command_line,
43
+ Departure.configuration.error_log_path,
44
+ @logger,
45
+ Departure.configuration.redirect_stderr).run
46
+ end
47
+
48
+ def alter_statement?(raw_sql_string)
49
+ raw_sql_string =~ /\Aalter table/i
50
+ end
51
+ end
52
+ end
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'forwardable'
4
+
3
5
  module Departure
4
6
  class RailsAdapter
5
- extend Forwardable
7
+ extend ::Forwardable
8
+
9
+ class UnsupportedRailsVersionError < StandardError; end
10
+ class MustImplementError < StandardError; end
6
11
 
7
12
  class << self
8
13
  def version_matches?(version_string, compatibility_string = current_version::STRING)
9
- raise "Invalid Gem Version: '#{version_string}'" unless Gem::Version.correct?(version_string)
10
-
11
14
  requirement = Gem::Requirement.new(compatibility_string)
12
15
  requirement.satisfied_by?(Gem::Version.new(version_string))
13
16
  end
@@ -21,14 +24,14 @@ module Departure
21
24
  end
22
25
 
23
26
  def for(ar_version)
24
- if ar_version::MAJOR == 8
27
+ if ar_version::MAJOR == 8 && ar_version::MINOR.positive?
28
+ V8_1_Adapter
29
+ elsif ar_version::MAJOR == 8
25
30
  V8_0_Adapter
26
31
  elsif ar_version::MAJOR >= 7 && ar_version::MINOR >= 2
27
32
  V7_2_Adapter
28
- elsif ar_version::MAJOR >= 6
29
- BaseAdapter
30
33
  else
31
- raise "Unsupported Rails version: #{ar_version}"
34
+ raise UnsupportedRailsVersionError, "Unsupported Rails version: #{ar_version}"
32
35
  end
33
36
  end
34
37
  end
@@ -36,47 +39,22 @@ module Departure
36
39
  class BaseAdapter
37
40
  class << self
38
41
  def register_integrations
39
- require 'active_record/connection_adapters/percona_adapter'
40
-
41
- ActiveSupport.on_load(:active_record) do
42
- ActiveRecord::Migration.class_eval do
43
- include Departure::Migration
44
- end
45
-
46
- if ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR == 1
47
- require 'departure/rails_patches/active_record_migrator_with_advisory_lock_patch'
48
-
49
- ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
50
- end
51
- end
42
+ raise MustImplementError, 'adapter must implement register_integrations'
52
43
  end
53
44
 
54
45
  # ActiveRecord::ConnectionAdapters::Mysql2Adapter
55
- def create_connection_adapter(**config)
56
- mysql2_adapter = ActiveRecord::Base.mysql2_connection(config)
57
-
58
- connection_details = Departure::ConnectionDetails.new(config)
59
- verbose = ActiveRecord::Migration.verbose
60
- sanitizers = [
61
- Departure::LogSanitizers::PasswordSanitizer.new(connection_details)
62
- ]
63
- percona_logger = Departure::LoggerFactory.build(sanitizers: sanitizers, verbose: verbose)
64
- cli_generator = Departure::CliGenerator.new(connection_details)
65
-
66
- runner = Departure::Runner.new(
67
- percona_logger,
68
- cli_generator,
69
- mysql2_adapter
70
- )
71
-
72
- connection_options = { mysql_adapter: mysql2_adapter }
73
-
74
- ActiveRecord::ConnectionAdapters::DepartureAdapter.new(
75
- runner,
76
- percona_logger,
77
- connection_options,
78
- config
79
- )
46
+ def create_connection_adapter(**_config)
47
+ raise MustImplementError, 'adapter must implement create_connection_adapter'
48
+ end
49
+
50
+ # https://github.com/rails/rails/commit/9ad36e067222478090b36a985090475bb03e398c#diff-de807ece2205a84c0e3de66b0e5ab831325d567893b8b88ce0d6e9d498f923d1
51
+ # Rails Column arity changed to require cast_type in position 2 which required us introducing this indirection
52
+ def new_sql_column(name:,
53
+ default_value:,
54
+ mysql_metadata:,
55
+ null_value:,
56
+ **_kwargs)
57
+ sql_column.new(name, default_value, mysql_metadata, null_value)
80
58
  end
81
59
 
82
60
  def sql_column
@@ -142,5 +120,47 @@ module Departure
142
120
  end
143
121
  end
144
122
  end
123
+
124
+ class V8_1_Adapter < BaseAdapter # rubocop:disable Naming/ClassAndModuleCamelCase
125
+ class << self
126
+ def register_integrations
127
+ require 'active_record/connection_adapters/rails_8_1_departure_adapter'
128
+ require 'departure/rails_patches/active_record_migrator_with_advisory_lock_patch'
129
+
130
+ ActiveSupport.on_load(:active_record) do
131
+ ActiveRecord::Migration.class_eval do
132
+ include Departure::Migration
133
+ end
134
+
135
+ ActiveRecord::Migrator.prepend Departure::RailsPatches::ActiveRecordMigratorWithAdvisoryLockPatch
136
+ end
137
+
138
+ ActiveRecord::ConnectionAdapters.register 'percona',
139
+ 'ActiveRecord::ConnectionAdapters::Rails81DepartureAdapter',
140
+ 'active_record/connection_adapters/rails_8_1_departure_adapter'
141
+ end
142
+
143
+ def create_connection_adapter(**config)
144
+ ActiveRecord::ConnectionAdapters::Rails81DepartureAdapter.new(config)
145
+ end
146
+
147
+ # rubocop:disable Metrics/ParameterLists
148
+ # https://github.com/rails/rails/commit/9ad36e067222478090b36a985090475bb03e398c#diff-de807ece2205a84c0e3de66b0e5ab831325d567893b8b88ce0d6e9d498f923d1
149
+ # Rails Column arity changed to require cast_type in position 2 which required us introducing this indirection
150
+ def new_sql_column(name:,
151
+ cast_type:,
152
+ default_value:,
153
+ mysql_metadata:,
154
+ null_value:,
155
+ **_kwargs)
156
+ # rubocop:enable Metrics/ParameterLists
157
+ sql_column.new(name, cast_type, default_value, mysql_metadata, null_value)
158
+ end
159
+
160
+ def sql_column
161
+ ::ActiveRecord::ConnectionAdapters::MySQL::Column
162
+ end
163
+ end
164
+ end
145
165
  end
146
166
  end
@@ -1,10 +1,15 @@
1
1
  require 'open3'
2
+ require 'forwardable'
3
+
4
+ # Runner is deprecated and used in rails 8.0 and below
5
+ # We will slowly reduce the responsibility of departure to be delegating
6
+ # all calls to underlying adapters/connections
2
7
 
3
8
  module Departure
4
9
  # It executes pt-online-schema-change commands in a new process and gets its
5
10
  # output and status
6
11
  class Runner
7
- extend Forwardable
12
+ extend ::Forwardable
8
13
 
9
14
  def_delegators :raw_connection, :execute, :escape, :close, :affected_rows
10
15
 
@@ -1,3 +1,3 @@
1
1
  module Departure
2
- VERSION = '7.0.0'.freeze
2
+ VERSION = '8.0.0'.freeze
3
3
  end
data/lib/departure.rb CHANGED
@@ -5,6 +5,7 @@ require 'active_record/connection_adapters/for_alter'
5
5
 
6
6
  require 'departure/version'
7
7
  require 'departure/log_sanitizers/password_sanitizer'
8
+ require 'departure/db_client'
8
9
  require 'departure/runner'
9
10
  require 'departure/cli_generator'
10
11
  require 'departure/logger'
@@ -52,11 +52,12 @@ module Lhm
52
52
  limit: cast_type.limit
53
53
  )
54
54
  mysql_metadata = ActiveRecord::ConnectionAdapters::MySQL::TypeMetadata.new(metadata)
55
- @column ||= Departure::RailsAdapter.for_current.sql_column.new(
56
- name,
57
- default_value,
58
- mysql_metadata,
59
- null_value
55
+ @column ||= Departure::RailsAdapter.for_current.new_sql_column(
56
+ name:,
57
+ cast_type:,
58
+ default_value:,
59
+ mysql_metadata:,
60
+ null_value:
60
61
  )
61
62
  end
62
63
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: departure
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ilya Zayats
@@ -14,7 +14,7 @@ authors:
14
14
  - Guillermo Iguaran
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2025-08-27 00:00:00.000000000 Z
17
+ date: 1980-01-02 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: railties
@@ -22,28 +22,28 @@ dependencies:
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 7.0.1
25
+ version: 7.2.0
26
26
  type: :runtime
27
27
  prerelease: false
28
28
  version_requirements: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 7.0.1
32
+ version: 7.2.0
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: activerecord
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 7.0.1
39
+ version: 7.2.0
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 7.0.1
46
+ version: 7.2.0
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: mysql2
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -70,14 +70,14 @@ dependencies:
70
70
  requirements:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
- version: 2.4.1
73
+ version: 2.5.0
74
74
  type: :development
75
75
  prerelease: false
76
76
  version_requirements: !ruby/object:Gem::Requirement
77
77
  requirements:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
- version: 2.4.1
80
+ version: 2.5.0
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: rake
83
83
  requirement: !ruby/object:Gem::Requirement
@@ -193,19 +193,17 @@ files:
193
193
  - configuration.rb
194
194
  - departure.gemspec
195
195
  - docker-compose.yml
196
- - gemfiles/rails_7_0.gemfile
197
- - gemfiles/rails_7_0.gemfile.lock
198
- - gemfiles/rails_7_1.gemfile
199
- - gemfiles/rails_7_1.gemfile.lock
200
196
  - gemfiles/rails_7_2.gemfile
201
197
  - gemfiles/rails_7_2.gemfile.lock
202
198
  - gemfiles/rails_8_0.gemfile
203
199
  - gemfiles/rails_8_0.gemfile.lock
200
+ - gemfiles/rails_8_1.gemfile
201
+ - gemfiles/rails_8_1.gemfile.lock
204
202
  - lib/active_record/connection_adapters/for_alter.rb
205
203
  - lib/active_record/connection_adapters/patch_connection_handling.rb
206
- - lib/active_record/connection_adapters/percona_adapter.rb
207
204
  - lib/active_record/connection_adapters/rails_7_2_departure_adapter.rb
208
205
  - lib/active_record/connection_adapters/rails_8_0_departure_adapter.rb
206
+ - lib/active_record/connection_adapters/rails_8_1_departure_adapter.rb
209
207
  - lib/departure.rb
210
208
  - lib/departure/alter_argument.rb
211
209
  - lib/departure/cli_generator.rb
@@ -213,6 +211,7 @@ files:
213
211
  - lib/departure/configuration.rb
214
212
  - lib/departure/connection_base.rb
215
213
  - lib/departure/connection_details.rb
214
+ - lib/departure/db_client.rb
216
215
  - lib/departure/dsn.rb
217
216
  - lib/departure/errors.rb
218
217
  - lib/departure/log_sanitizers/password_sanitizer.rb
@@ -250,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
250
249
  - !ruby/object:Gem::Version
251
250
  version: '0'
252
251
  requirements: []
253
- rubygems_version: 3.6.2
252
+ rubygems_version: 3.7.2
254
253
  specification_version: 4
255
254
  summary: pt-online-schema-change runner for ActiveRecord migrations
256
255
  test_files: []
@@ -1,16 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source 'https://rubygems.org'
4
-
5
- gem 'base64'
6
- gem 'bigdecimal'
7
- gem 'codeclimate-test-reporter', '~> 1.0.3', group: :test, require: nil
8
- gem 'lhm'
9
- gem 'logger'
10
- gem 'mutex_m', require: false
11
- gem 'rails', '> 7.0.8'
12
- gem 'rubocop', '~> 1.74.0', require: false
13
- gem 'rubocop-performance', '~> 1.20.2', require: false
14
- gem 'zeitwerk', '< 2.7.0'
15
-
16
- gemspec path: '../'
@@ -1,15 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source 'https://rubygems.org'
4
-
5
- gem 'base64'
6
- gem 'codeclimate-test-reporter', '~> 1.0.3', group: :test, require: nil
7
- gem 'lhm'
8
- gem 'logger'
9
- gem 'mutex_m', require: false
10
- gem 'rails', '> 7.1.3'
11
- gem 'rubocop', '~> 1.74.0', require: false
12
- gem 'rubocop-performance', '~> 1.20.2', require: false
13
- gem 'zeitwerk', '< 2.7.0'
14
-
15
- gemspec path: '../'