departure-next 6.7.1.pre.pre
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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +7 -0
- data/.github/workflows/test.yml +54 -0
- data/.gitignore +14 -0
- data/.pryrc +11 -0
- data/.rspec +2 -0
- data/.rubocop.yml +66 -0
- data/.rubocop_todo.yml +238 -0
- data/20250312235906_add_deleted_reason_to_newsfeed_activities.rb +5 -0
- data/Appraisals +15 -0
- data/CHANGELOG.md +224 -0
- data/CODE_OF_CONDUCT.md +50 -0
- data/Dockerfile +32 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +206 -0
- data/LICENSE.txt +22 -0
- data/README.md +246 -0
- data/RELEASING.md +17 -0
- data/Rakefile +25 -0
- data/bin/console +14 -0
- data/bin/rails +24 -0
- data/bin/rspec +16 -0
- data/bin/setup +7 -0
- data/config.yml.erb +5 -0
- data/configuration.rb +16 -0
- data/departure-next.gemspec +34 -0
- data/docker-compose.yml +23 -0
- data/gemfiles/rails_6_1.gemfile +10 -0
- data/gemfiles/rails_6_1.gemfile.lock +243 -0
- data/gemfiles/rails_7_0.gemfile +10 -0
- data/gemfiles/rails_7_0.gemfile.lock +242 -0
- data/gemfiles/rails_7_1.gemfile +10 -0
- data/gemfiles/rails_7_1.gemfile.lock +274 -0
- data/gemfiles/rails_7_2.gemfile +10 -0
- data/gemfiles/rails_7_2.gemfile.lock +274 -0
- data/lib/active_record/connection_adapters/for_alter.rb +103 -0
- data/lib/active_record/connection_adapters/patch_connection_handling.rb +18 -0
- data/lib/active_record/connection_adapters/percona_adapter.rb +187 -0
- data/lib/active_record/connection_adapters/rails_7_2_departure_adapter.rb +218 -0
- data/lib/departure/alter_argument.rb +49 -0
- data/lib/departure/cli_generator.rb +84 -0
- data/lib/departure/command.rb +105 -0
- data/lib/departure/configuration.rb +21 -0
- data/lib/departure/connection_base.rb +11 -0
- data/lib/departure/connection_details.rb +121 -0
- data/lib/departure/dsn.rb +25 -0
- data/lib/departure/errors.rb +39 -0
- data/lib/departure/log_sanitizers/password_sanitizer.rb +22 -0
- data/lib/departure/logger.rb +42 -0
- data/lib/departure/logger_factory.rb +16 -0
- data/lib/departure/migration.rb +104 -0
- data/lib/departure/null_logger.rb +15 -0
- data/lib/departure/option.rb +75 -0
- data/lib/departure/rails_adapter.rb +97 -0
- data/lib/departure/railtie.rb +21 -0
- data/lib/departure/runner.rb +75 -0
- data/lib/departure/user_options.rb +44 -0
- data/lib/departure/version.rb +3 -0
- data/lib/departure.rb +40 -0
- data/lib/lhm/adapter.rb +107 -0
- data/lib/lhm/column_with_sql.rb +89 -0
- data/lib/lhm/column_with_type.rb +29 -0
- data/lib/lhm.rb +23 -0
- data/test_database.rb +76 -0
- metadata +282 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
module Departure
|
3
|
+
# Holds the parameters of the DB connection and formats them to string
|
4
|
+
class ConnectionDetails
|
5
|
+
DEFAULT_PORT = 3306
|
6
|
+
# Constructor
|
7
|
+
#
|
8
|
+
# @param connection_data [Hash] connection parameters as used in #establish_conneciton
|
9
|
+
def initialize(connection_data)
|
10
|
+
@connection_data = connection_data
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the details formatted as an string to be used with
|
14
|
+
# pt-online-schema-change. It follows the mysql client's format.
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
def to_s
|
18
|
+
@to_s ||= "#{base_connection} -u #{user} #{password_argument}"
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO: Doesn't the abstract adapter already handle this somehow?
|
22
|
+
# Returns the database name. If PERCONA_DB_NAME is passed its value will be
|
23
|
+
# used instead
|
24
|
+
#
|
25
|
+
# Returns the database name
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
def database
|
29
|
+
ENV.fetch('PERCONA_DB_NAME', connection_data[:database])
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the password fragment of the details string if a password is passed
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
def password_argument
|
36
|
+
if password.present?
|
37
|
+
%(--password #{Shellwords.escape(password)} )
|
38
|
+
else
|
39
|
+
''
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :connection_data
|
46
|
+
|
47
|
+
# Returns conditionally host or socket configuration
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
def base_connection
|
51
|
+
return socket_argument if socket.present?
|
52
|
+
|
53
|
+
"#{host_argument} -P #{port}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the host fragment of the details string, adds ssl options if needed
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
def host_argument
|
60
|
+
host_string = host
|
61
|
+
if ssl_ca.present?
|
62
|
+
host_string += ";mysql_ssl=1;mysql_ssl_client_ca=#{ssl_ca}"
|
63
|
+
end
|
64
|
+
"-h \"#{host_string}\""
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns the socket fragment of the details string
|
68
|
+
# FIXME: SSL connection
|
69
|
+
#
|
70
|
+
# @return [String]
|
71
|
+
def socket_argument
|
72
|
+
"-S #{socket}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns the database host name, defaulting to localhost. If PERCONA_DB_HOST
|
76
|
+
# is passed its value will be used instead
|
77
|
+
#
|
78
|
+
# @return [String]
|
79
|
+
def host
|
80
|
+
ENV.fetch('PERCONA_DB_HOST', connection_data[:host]) || 'localhost'
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the database user. If PERCONA_DB_USER is passed its value will be
|
84
|
+
# used instead
|
85
|
+
#
|
86
|
+
# @return [String]
|
87
|
+
def user
|
88
|
+
ENV.fetch('PERCONA_DB_USER', connection_data[:username])
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the database user's password. If PERCONA_DB_PASSWORD is passed its
|
92
|
+
# value will be used instead
|
93
|
+
#
|
94
|
+
# @return [String]
|
95
|
+
def password
|
96
|
+
ENV.fetch('PERCONA_DB_PASSWORD', connection_data[:password])
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the database socket path. If PERCONA_DB_SOCKET is passed its value
|
100
|
+
# will be used instead
|
101
|
+
#
|
102
|
+
# @return [String]
|
103
|
+
def socket
|
104
|
+
ENV.fetch('PERCONA_DB_SOCKET', connection_data[:socket])
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns the database's port.
|
108
|
+
#
|
109
|
+
# @return [String]
|
110
|
+
def port
|
111
|
+
connection_data.fetch(:port, DEFAULT_PORT)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the database' SSL CA certificate.
|
115
|
+
#
|
116
|
+
# @return [String]
|
117
|
+
def ssl_ca
|
118
|
+
connection_data.fetch(:sslca, nil)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Departure
|
2
|
+
# Represents the 'DSN' argument of Percona's pt-online-schema-change
|
3
|
+
# See https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
|
4
|
+
class DSN
|
5
|
+
# Constructor
|
6
|
+
#
|
7
|
+
# @param database [String, Symbol]
|
8
|
+
# @param table_name [String, Symbol]
|
9
|
+
def initialize(database, table_name)
|
10
|
+
@database = database
|
11
|
+
@table_name = table_name
|
12
|
+
@suffix = ENV.fetch('PERCONA_DSN_SUFFIX', nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the pt-online-schema-change DSN string. See
|
16
|
+
# https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
|
17
|
+
def to_s
|
18
|
+
"D=#{database},t=#{table_name}#{suffix.nil? ? nil : ',' + suffix}"
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :table_name, :database, :suffix
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Departure
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
# Used when for whatever reason we couldn't get the spawned process'
|
5
|
+
# status.
|
6
|
+
class NoStatusError < Error
|
7
|
+
def message
|
8
|
+
'Status could not be retrieved'.freeze
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Used when the spawned process failed by receiving a signal.
|
13
|
+
# pt-online-schema-change returns "SIGSEGV (signal 11)" on failures.
|
14
|
+
class SignalError < Error
|
15
|
+
attr_reader :status
|
16
|
+
|
17
|
+
# Constructor
|
18
|
+
#
|
19
|
+
# @param status [Process::Status]
|
20
|
+
def initialize(status)
|
21
|
+
super
|
22
|
+
@status = status
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
status.to_s
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class CommandNotFoundError < Error
|
31
|
+
def message
|
32
|
+
'Please install pt-online-schema-change. Check: https://www.percona.com/doc/percona-toolkit for further details'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Used to prevent running the db:migrate rake task when providing arguments
|
37
|
+
# through PERCONA_ARGS env var
|
38
|
+
class ArgumentsNotSupported < Error; end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Departure
|
2
|
+
module LogSanitizers
|
3
|
+
class PasswordSanitizer
|
4
|
+
PASSWORD_REPLACEMENT = '[filtered_password]'.freeze
|
5
|
+
|
6
|
+
delegate :password_argument, to: :connection_details
|
7
|
+
|
8
|
+
def initialize(connection_details)
|
9
|
+
@connection_details = connection_details
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(log_statement)
|
13
|
+
return log_statement if password_argument.blank?
|
14
|
+
log_statement.gsub(password_argument, PASSWORD_REPLACEMENT)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_accessor :connection_details
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Departure
|
2
|
+
# Copies the ActiveRecord::Migration #say and #write plus a new
|
3
|
+
# #write_no_newline to log the migration's status. It's not possible to reuse
|
4
|
+
# the from ActiveRecord::Migration because the migration's instance can't be
|
5
|
+
# seen from the connection adapter.
|
6
|
+
class Logger
|
7
|
+
def initialize(sanitizers)
|
8
|
+
@sanitizers = sanitizers
|
9
|
+
end
|
10
|
+
|
11
|
+
# Outputs the message through the stdout, following the
|
12
|
+
# ActiveRecord::Migration log format
|
13
|
+
#
|
14
|
+
# @param message [String]
|
15
|
+
# @param subitem [Boolean] whether to show message as a nested log item
|
16
|
+
def say(message, subitem = false)
|
17
|
+
write "#{subitem ? ' ->' : '--'} #{message}"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Outputs the text through the stdout adding a new line at the end
|
21
|
+
#
|
22
|
+
# @param text [String]
|
23
|
+
def write(text = '')
|
24
|
+
puts(sanitize(text))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Outputs the text through the stdout without adding a new line at the end
|
28
|
+
#
|
29
|
+
# @param text [String]
|
30
|
+
def write_no_newline(text)
|
31
|
+
print(sanitize(text))
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_accessor :sanitizers
|
37
|
+
|
38
|
+
def sanitize(text)
|
39
|
+
sanitizers.inject(text) { |memo, sanitizer| sanitizer.execute(memo) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Departure
|
2
|
+
module LoggerFactory
|
3
|
+
# Returns the appropriate logger instance for the given configuration. Use
|
4
|
+
# :verbose option to log to the stdout
|
5
|
+
#
|
6
|
+
# @param verbose [Boolean]
|
7
|
+
# @return [#say, #write]
|
8
|
+
def self.build(sanitizers: [], verbose: true)
|
9
|
+
if verbose
|
10
|
+
Departure::Logger.new(sanitizers)
|
11
|
+
else
|
12
|
+
Departure::NullLogger.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Departure
|
2
|
+
# Hooks Departure into Rails migrations by replacing the configured database
|
3
|
+
# adapter.
|
4
|
+
#
|
5
|
+
# It also patches ActiveRecord's #migrate method so that it patches LHM
|
6
|
+
# first. This will make migrations written with LHM to go through the
|
7
|
+
# regular Rails Migration DSL.
|
8
|
+
module Migration
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
# Holds the name of the adapter that was configured by the app.
|
13
|
+
mattr_accessor :original_adapter
|
14
|
+
|
15
|
+
# Declare on a per-migration class basis whether or not to use Departure.
|
16
|
+
# The default for this attribute is set based on
|
17
|
+
# Departure.configuration.enabled_by_default (default true).
|
18
|
+
class_attribute :uses_departure
|
19
|
+
self.uses_departure = true
|
20
|
+
|
21
|
+
alias_method :active_record_migrate, :migrate
|
22
|
+
remove_method :migrate
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
# Declare `uses_departure!` in the class body of your migration to enable
|
27
|
+
# Departure for that migration only when
|
28
|
+
# Departure.configuration.enabled_by_default is false.
|
29
|
+
def uses_departure!
|
30
|
+
self.uses_departure = true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Declare `disable_departure!` in the class body of your migration to
|
34
|
+
# disable Departure for that migration only (when
|
35
|
+
# Departure.configuration.enabled_by_default is true, the default).
|
36
|
+
def disable_departure!
|
37
|
+
self.uses_departure = false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Replaces the current connection adapter with the PerconaAdapter and
|
42
|
+
# patches LHM, then it continues with the regular migration process.
|
43
|
+
#
|
44
|
+
# @param direction [Symbol] :up or :down
|
45
|
+
def departure_migrate(direction)
|
46
|
+
reconnect_with_percona
|
47
|
+
include_foreigner if defined?(Foreigner)
|
48
|
+
|
49
|
+
::Lhm.migration = self
|
50
|
+
active_record_migrate(direction)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Migrate with or without Departure based on uses_departure class
|
54
|
+
# attribute.
|
55
|
+
def migrate(direction)
|
56
|
+
if uses_departure?
|
57
|
+
departure_migrate(direction)
|
58
|
+
else
|
59
|
+
reconnect_without_percona
|
60
|
+
active_record_migrate(direction)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Includes the Foreigner's Mysql2Adapter implemention in
|
65
|
+
# DepartureAdapter to support foreign keys
|
66
|
+
def include_foreigner
|
67
|
+
Foreigner::Adapter.safe_include(
|
68
|
+
:DepartureAdapter,
|
69
|
+
Foreigner::ConnectionAdapters::Mysql2Adapter
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Make all connections in the connection pool to use PerconaAdapter
|
74
|
+
# instead of the current adapter.
|
75
|
+
def reconnect_with_percona
|
76
|
+
return if connection_config[:adapter] == 'percona'
|
77
|
+
Departure::ConnectionBase.establish_connection(connection_config.merge(adapter: 'percona'))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Reconnect without percona adapter when Departure is disabled but was
|
81
|
+
# enabled in a previous migration.
|
82
|
+
def reconnect_without_percona
|
83
|
+
return unless connection_config[:adapter] == 'percona'
|
84
|
+
Departure::OriginalAdapterConnection.establish_connection(connection_config.merge(adapter: original_adapter))
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# Capture the type of the adapter configured by the app if not already set.
|
90
|
+
def connection_config
|
91
|
+
configuration_hash.tap do |config|
|
92
|
+
self.class.original_adapter ||= config[:adapter]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private def configuration_hash
|
97
|
+
if ActiveRecord::VERSION::STRING >= '6.1'
|
98
|
+
ActiveRecord::Base.connection_db_config.configuration_hash
|
99
|
+
else
|
100
|
+
ActiveRecord::Base.connection_config
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Departure
|
2
|
+
class Option
|
3
|
+
attr_reader :name, :value
|
4
|
+
|
5
|
+
# Builds an instance by parsing its name and value out of the given string.
|
6
|
+
#
|
7
|
+
# @param string [String]
|
8
|
+
# @return [Option]
|
9
|
+
def self.from_string(string)
|
10
|
+
name, value = string.split(/\s|=/, 2)
|
11
|
+
new(name, value)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Constructor
|
15
|
+
#
|
16
|
+
# @param name [String]
|
17
|
+
# @param optional value [String]
|
18
|
+
def initialize(name, value = nil)
|
19
|
+
@name = normalize_option(name)
|
20
|
+
@value = value
|
21
|
+
end
|
22
|
+
|
23
|
+
# Compares two options
|
24
|
+
#
|
25
|
+
# @param [Option]
|
26
|
+
# @return [Boolean]
|
27
|
+
def ==(other)
|
28
|
+
name == other.name
|
29
|
+
end
|
30
|
+
alias eql? ==
|
31
|
+
|
32
|
+
# Returns the option's hash
|
33
|
+
#
|
34
|
+
# @return [Fixnum]
|
35
|
+
def hash
|
36
|
+
name.hash
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the option as string following the "--<name>=<value>" format or
|
40
|
+
# the short "-n=value" format
|
41
|
+
#
|
42
|
+
# @return [String]
|
43
|
+
def to_s
|
44
|
+
"#{name}#{value_as_string}"
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Returns the option name in "long" format, e.g., "--name"
|
50
|
+
#
|
51
|
+
# @return [String]
|
52
|
+
def normalize_option(name)
|
53
|
+
if name.start_with?('-')
|
54
|
+
name
|
55
|
+
elsif name.length == 1
|
56
|
+
"-#{name}"
|
57
|
+
else
|
58
|
+
"--#{name}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the value fragment of the option string if any value is specified
|
63
|
+
#
|
64
|
+
# @return [String]
|
65
|
+
def value_as_string
|
66
|
+
if value.nil?
|
67
|
+
''
|
68
|
+
elsif value.include?('=')
|
69
|
+
" #{value}"
|
70
|
+
else
|
71
|
+
"=#{value}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Departure
|
4
|
+
class RailsAdapter
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def current_version
|
9
|
+
ActiveRecord::VERSION
|
10
|
+
end
|
11
|
+
|
12
|
+
def for_current
|
13
|
+
self.for(current_version)
|
14
|
+
end
|
15
|
+
|
16
|
+
def for(ar_version)
|
17
|
+
if ar_version::MAJOR >= 7 && ar_version::MINOR >= 2
|
18
|
+
V7_2_Adapter
|
19
|
+
else
|
20
|
+
BaseAdapter
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class BaseAdapter
|
26
|
+
class << self
|
27
|
+
def register_integrations
|
28
|
+
require 'active_record/connection_adapters/percona_adapter'
|
29
|
+
|
30
|
+
ActiveSupport.on_load(:active_record) do
|
31
|
+
ActiveRecord::Migration.class_eval do
|
32
|
+
include Departure::Migration
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
38
|
+
def create_connection_adapter(**config)
|
39
|
+
mysql2_adapter = ActiveRecord::Base.mysql2_connection(config)
|
40
|
+
|
41
|
+
connection_details = Departure::ConnectionDetails.new(config)
|
42
|
+
verbose = ActiveRecord::Migration.verbose
|
43
|
+
sanitizers = [
|
44
|
+
Departure::LogSanitizers::PasswordSanitizer.new(connection_details)
|
45
|
+
]
|
46
|
+
percona_logger = Departure::LoggerFactory.build(sanitizers: sanitizers, verbose: verbose)
|
47
|
+
cli_generator = Departure::CliGenerator.new(connection_details)
|
48
|
+
|
49
|
+
runner = Departure::Runner.new(
|
50
|
+
percona_logger,
|
51
|
+
cli_generator,
|
52
|
+
mysql2_adapter
|
53
|
+
)
|
54
|
+
|
55
|
+
connection_options = { mysql_adapter: mysql2_adapter }
|
56
|
+
|
57
|
+
ActiveRecord::ConnectionAdapters::DepartureAdapter.new(
|
58
|
+
runner,
|
59
|
+
percona_logger,
|
60
|
+
connection_options,
|
61
|
+
config
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def sql_column
|
66
|
+
::ActiveRecord::ConnectionAdapters::DepartureAdapter::Column
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class V7_2_Adapter < BaseAdapter # rubocop:disable Naming/ClassAndModuleCamelCase
|
72
|
+
class << self
|
73
|
+
def register_integrations
|
74
|
+
require 'active_record/connection_adapters/rails_7_2_departure_adapter'
|
75
|
+
|
76
|
+
ActiveSupport.on_load(:active_record) do
|
77
|
+
ActiveRecord::Migration.class_eval do
|
78
|
+
include Departure::Migration
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
ActiveRecord::ConnectionAdapters.register 'percona',
|
83
|
+
'ActiveRecord::ConnectionAdapters::Rails72DepartureAdapter',
|
84
|
+
'active_record/connection_adapters/rails_7_2_departure_adapter'
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_connection_adapter(**config)
|
88
|
+
ActiveRecord::ConnectionAdapters::Rails72DepartureAdapter.new(config)
|
89
|
+
end
|
90
|
+
|
91
|
+
def sql_column
|
92
|
+
::ActiveRecord::ConnectionAdapters::Rails72DepartureAdapter::Column
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'departure'
|
2
|
+
require 'lhm' # It's our own Lhm adapter, not the gem
|
3
|
+
require 'rails'
|
4
|
+
|
5
|
+
module Departure
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
railtie_name :departure
|
8
|
+
|
9
|
+
initializer 'departure.configure' do |app|
|
10
|
+
Departure.configure do |config|
|
11
|
+
config.tmp_path = app.paths['tmp'].first
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
config.after_initialize do
|
16
|
+
Departure.configure do |dc|
|
17
|
+
ActiveRecord::Migration.uses_departure = dc.enabled_by_default
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Departure
|
4
|
+
# It executes pt-online-schema-change commands in a new process and gets its
|
5
|
+
# output and status
|
6
|
+
class Runner
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :raw_connection, :execute, :escape, :close, :affected_rows
|
10
|
+
|
11
|
+
# Constructor
|
12
|
+
#
|
13
|
+
# @param logger [#say, #write]
|
14
|
+
# @param cli_generator [CliGenerator]
|
15
|
+
# @param mysql_adapter [ActiveRecord::ConnectionAdapter] it must implement
|
16
|
+
# #execute and #raw_connection
|
17
|
+
def initialize(logger, cli_generator, mysql_adapter, config = Departure.configuration)
|
18
|
+
@logger = logger
|
19
|
+
@cli_generator = cli_generator
|
20
|
+
@mysql_adapter = mysql_adapter
|
21
|
+
@error_log_path = config&.error_log_path
|
22
|
+
@redirect_stderr = config&.redirect_stderr
|
23
|
+
end
|
24
|
+
|
25
|
+
def database_adapter
|
26
|
+
@mysql_adapter
|
27
|
+
end
|
28
|
+
|
29
|
+
def raw_connection
|
30
|
+
database_adapter.raw_connection
|
31
|
+
end
|
32
|
+
|
33
|
+
# Executes the passed sql statement using pt-online-schema-change for ALTER
|
34
|
+
# TABLE statements, or the specified mysql adapter otherwise.
|
35
|
+
#
|
36
|
+
# @param sql [String]
|
37
|
+
def query(sql)
|
38
|
+
if alter_statement?(sql)
|
39
|
+
command_line = cli_generator.parse_statement(sql)
|
40
|
+
execute(command_line)
|
41
|
+
else
|
42
|
+
database_adapter.execute(sql)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the number of rows affected by the last UPDATE, DELETE or INSERT
|
47
|
+
# statements
|
48
|
+
#
|
49
|
+
# @return [Integer]
|
50
|
+
def affected_rows
|
51
|
+
raw_connection.affected_rows
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO: rename it so we don't confuse it with AR's #execute
|
55
|
+
# Runs and logs the given command
|
56
|
+
#
|
57
|
+
# @param command_line [String]
|
58
|
+
# @return [Boolean]
|
59
|
+
def execute(command_line)
|
60
|
+
Command.new(command_line, error_log_path, logger, redirect_stderr).run
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
attr_reader :logger, :cli_generator, :mysql_adapter, :error_log_path, :redirect_stderr
|
66
|
+
|
67
|
+
# Checks whether the sql statement is an ALTER TABLE
|
68
|
+
#
|
69
|
+
# @param sql [String]
|
70
|
+
# @return [Boolean]
|
71
|
+
def alter_statement?(sql)
|
72
|
+
sql =~ /\Aalter table/i
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Departure
|
2
|
+
# Encapsulates the pt-online-schema-change options defined by the user
|
3
|
+
class UserOptions
|
4
|
+
delegate :each, :merge, to: :to_set
|
5
|
+
|
6
|
+
# Constructor
|
7
|
+
#
|
8
|
+
# @param arguments [String]
|
9
|
+
def initialize(arguments = ENV['PERCONA_ARGS'])
|
10
|
+
@arguments = arguments
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :arguments
|
16
|
+
|
17
|
+
# Returns the arguments the user defined but without duplicates
|
18
|
+
#
|
19
|
+
# @return [Set]
|
20
|
+
def to_set
|
21
|
+
Set.new(user_options)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns Option instances from the arguments the user specified, if any
|
25
|
+
#
|
26
|
+
# @return [Array]
|
27
|
+
def user_options
|
28
|
+
if arguments
|
29
|
+
build_options
|
30
|
+
else
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Builds Option instances from the user arguments
|
36
|
+
#
|
37
|
+
# @return [Array<Option>]
|
38
|
+
def build_options
|
39
|
+
arguments.split(/\s(?=-)/).map do |argument|
|
40
|
+
Option.from_string(argument)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|