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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +7 -0
  3. data/.github/workflows/test.yml +54 -0
  4. data/.gitignore +14 -0
  5. data/.pryrc +11 -0
  6. data/.rspec +2 -0
  7. data/.rubocop.yml +66 -0
  8. data/.rubocop_todo.yml +238 -0
  9. data/20250312235906_add_deleted_reason_to_newsfeed_activities.rb +5 -0
  10. data/Appraisals +15 -0
  11. data/CHANGELOG.md +224 -0
  12. data/CODE_OF_CONDUCT.md +50 -0
  13. data/Dockerfile +32 -0
  14. data/Gemfile +11 -0
  15. data/Gemfile.lock +206 -0
  16. data/LICENSE.txt +22 -0
  17. data/README.md +246 -0
  18. data/RELEASING.md +17 -0
  19. data/Rakefile +25 -0
  20. data/bin/console +14 -0
  21. data/bin/rails +24 -0
  22. data/bin/rspec +16 -0
  23. data/bin/setup +7 -0
  24. data/config.yml.erb +5 -0
  25. data/configuration.rb +16 -0
  26. data/departure-next.gemspec +34 -0
  27. data/docker-compose.yml +23 -0
  28. data/gemfiles/rails_6_1.gemfile +10 -0
  29. data/gemfiles/rails_6_1.gemfile.lock +243 -0
  30. data/gemfiles/rails_7_0.gemfile +10 -0
  31. data/gemfiles/rails_7_0.gemfile.lock +242 -0
  32. data/gemfiles/rails_7_1.gemfile +10 -0
  33. data/gemfiles/rails_7_1.gemfile.lock +274 -0
  34. data/gemfiles/rails_7_2.gemfile +10 -0
  35. data/gemfiles/rails_7_2.gemfile.lock +274 -0
  36. data/lib/active_record/connection_adapters/for_alter.rb +103 -0
  37. data/lib/active_record/connection_adapters/patch_connection_handling.rb +18 -0
  38. data/lib/active_record/connection_adapters/percona_adapter.rb +187 -0
  39. data/lib/active_record/connection_adapters/rails_7_2_departure_adapter.rb +218 -0
  40. data/lib/departure/alter_argument.rb +49 -0
  41. data/lib/departure/cli_generator.rb +84 -0
  42. data/lib/departure/command.rb +105 -0
  43. data/lib/departure/configuration.rb +21 -0
  44. data/lib/departure/connection_base.rb +11 -0
  45. data/lib/departure/connection_details.rb +121 -0
  46. data/lib/departure/dsn.rb +25 -0
  47. data/lib/departure/errors.rb +39 -0
  48. data/lib/departure/log_sanitizers/password_sanitizer.rb +22 -0
  49. data/lib/departure/logger.rb +42 -0
  50. data/lib/departure/logger_factory.rb +16 -0
  51. data/lib/departure/migration.rb +104 -0
  52. data/lib/departure/null_logger.rb +15 -0
  53. data/lib/departure/option.rb +75 -0
  54. data/lib/departure/rails_adapter.rb +97 -0
  55. data/lib/departure/railtie.rb +21 -0
  56. data/lib/departure/runner.rb +75 -0
  57. data/lib/departure/user_options.rb +44 -0
  58. data/lib/departure/version.rb +3 -0
  59. data/lib/departure.rb +40 -0
  60. data/lib/lhm/adapter.rb +107 -0
  61. data/lib/lhm/column_with_sql.rb +89 -0
  62. data/lib/lhm/column_with_type.rb +29 -0
  63. data/lib/lhm.rb +23 -0
  64. data/test_database.rb +76 -0
  65. 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,15 @@
1
+ module Departure
2
+ class NullLogger
3
+ def say(_message, _subitem = false)
4
+ # noop
5
+ end
6
+
7
+ def write(_text)
8
+ # noop
9
+ end
10
+
11
+ def write_no_newline(_text)
12
+ # noop
13
+ end
14
+ end
15
+ 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