departure-76c9880 6.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +8 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +60 -0
- data/.travis.yml +30 -0
- data/CHANGELOG.md +192 -0
- data/CODE_OF_CONDUCT.md +50 -0
- data/Dockerfile +32 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +227 -0
- data/RELEASING.md +17 -0
- data/Rakefile +17 -0
- data/bin/console +14 -0
- data/bin/rspec +16 -0
- data/bin/setup +7 -0
- data/config.yml.erb +4 -0
- data/configuration.rb +16 -0
- data/departure.gemspec +35 -0
- data/docker-compose.yml +23 -0
- data/lib/active_record/connection_adapters/for_alter.rb +91 -0
- data/lib/active_record/connection_adapters/percona_adapter.rb +168 -0
- data/lib/departure.rb +43 -0
- data/lib/departure/alter_argument.rb +49 -0
- data/lib/departure/cli_generator.rb +84 -0
- data/lib/departure/command.rb +96 -0
- data/lib/departure/configuration.rb +20 -0
- data/lib/departure/connection_base.rb +9 -0
- data/lib/departure/connection_details.rb +96 -0
- data/lib/departure/dsn.rb +24 -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 +96 -0
- data/lib/departure/null_logger.rb +15 -0
- data/lib/departure/option.rb +75 -0
- data/lib/departure/railtie.rb +21 -0
- data/lib/departure/runner.rb +62 -0
- data/lib/departure/user_options.rb +44 -0
- data/lib/departure/version.rb +3 -0
- data/lib/lhm.rb +23 -0
- data/lib/lhm/adapter.rb +107 -0
- data/lib/lhm/column_with_sql.rb +96 -0
- data/lib/lhm/column_with_type.rb +29 -0
- data/test_database.rb +80 -0
- metadata +245 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
module Departure
|
2
|
+
class InvalidAlterStatement < StandardError; end
|
3
|
+
|
4
|
+
# Represents the '--alter' argument of Percona's pt-online-schema-change
|
5
|
+
# See https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html
|
6
|
+
class AlterArgument
|
7
|
+
ALTER_TABLE_REGEX = /\AALTER TABLE [^\s]*[\n]* /
|
8
|
+
|
9
|
+
attr_reader :table_name
|
10
|
+
|
11
|
+
# Constructor
|
12
|
+
#
|
13
|
+
# @param statement [String]
|
14
|
+
# @raise [InvalidAlterStatement] if the statement is not an ALTER TABLE
|
15
|
+
def initialize(statement)
|
16
|
+
@statement = statement
|
17
|
+
|
18
|
+
match = statement.match(ALTER_TABLE_REGEX)
|
19
|
+
raise InvalidAlterStatement unless match
|
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('`')
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the '--alter' pt-online-schema-change argument as a string. See
|
29
|
+
# https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html
|
30
|
+
def to_s
|
31
|
+
"--alter \"#{parsed_statement}\""
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :statement
|
37
|
+
|
38
|
+
# Removes the 'ALTER TABLE' portion of the SQL statement
|
39
|
+
#
|
40
|
+
# @return [String]
|
41
|
+
def parsed_statement
|
42
|
+
@parsed_statement ||= statement
|
43
|
+
.gsub(ALTER_TABLE_REGEX, '')
|
44
|
+
.gsub('`', '\\\`')
|
45
|
+
.gsub(/\\n/, '')
|
46
|
+
.gsub('"', '\\\"')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'departure/dsn'
|
2
|
+
require 'departure/option'
|
3
|
+
require 'departure/alter_argument'
|
4
|
+
require 'departure/connection_details'
|
5
|
+
require 'departure/user_options'
|
6
|
+
|
7
|
+
module Departure
|
8
|
+
# Generates the equivalent Percona's pt-online-schema-change command to the
|
9
|
+
# given SQL statement
|
10
|
+
#
|
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
|
13
|
+
#
|
14
|
+
class CliGenerator
|
15
|
+
COMMAND_NAME = 'pt-online-schema-change'.freeze
|
16
|
+
DEFAULT_OPTIONS = Set.new(
|
17
|
+
[
|
18
|
+
Option.new('execute'),
|
19
|
+
Option.new('statistics'),
|
20
|
+
Option.new('alter-foreign-keys-method', 'auto'),
|
21
|
+
Option.new('no-check-alter')
|
22
|
+
]
|
23
|
+
).freeze
|
24
|
+
|
25
|
+
# TODO: Better doc.
|
26
|
+
#
|
27
|
+
# Constructor. Specify any arguments to pass to pt-online-schema-change
|
28
|
+
# passing the PERCONA_ARGS env var when executing the migration
|
29
|
+
#
|
30
|
+
# @param connection_data [Hash]
|
31
|
+
def initialize(connection_details)
|
32
|
+
@connection_details = connection_details
|
33
|
+
end
|
34
|
+
|
35
|
+
# Generates the percona command. Fills all the connection credentials from
|
36
|
+
# the current AR connection, but that can be amended via ENV-vars:
|
37
|
+
# PERCONA_DB_HOST, PERCONA_DB_USER, PERCONA_DB_PASSWORD, PERCONA_DB_NAME
|
38
|
+
# Table name can't not be amended, it populates automatically from the
|
39
|
+
# migration data
|
40
|
+
#
|
41
|
+
# @param table_name [String]
|
42
|
+
# @param statement [String] MySQL statement
|
43
|
+
# @return [String]
|
44
|
+
def generate(table_name, statement)
|
45
|
+
alter_argument = AlterArgument.new(statement)
|
46
|
+
dsn = DSN.new(connection_details.database, table_name)
|
47
|
+
|
48
|
+
"#{command} #{all_options} #{dsn} #{alter_argument}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generates the percona command for a raw MySQL statement. Fills all the
|
52
|
+
# connection credentials from the current AR connection, but that can
|
53
|
+
# amended via ENV-vars: PERCONA_DB_HOST, PERCONA_DB_USER,
|
54
|
+
# PERCONA_DB_PASSWORD, PERCONA_DB_NAME Table name can't not be amended, it
|
55
|
+
# populates automatically from the migration data
|
56
|
+
#
|
57
|
+
# @param statement [String] MySQL statement
|
58
|
+
# @return [String]
|
59
|
+
def parse_statement(statement)
|
60
|
+
alter_argument = AlterArgument.new(statement)
|
61
|
+
dsn = DSN.new(connection_details.database, alter_argument.table_name)
|
62
|
+
|
63
|
+
"#{command} #{all_options} #{dsn} #{alter_argument}"
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_reader :connection_details
|
69
|
+
|
70
|
+
def command
|
71
|
+
"#{COMMAND_NAME} #{connection_details}"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns all the arguments to execute pt-online-schema-change with
|
75
|
+
#
|
76
|
+
# @return [String]
|
77
|
+
def all_options
|
78
|
+
env_variable_options = UserOptions.new
|
79
|
+
global_configuration_options = UserOptions.new(Departure.configuration.global_percona_args)
|
80
|
+
options = env_variable_options.merge(global_configuration_options).merge(DEFAULT_OPTIONS)
|
81
|
+
options.to_a.join(' ')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Departure
|
2
|
+
# Executes the given command returning it's status and errors
|
3
|
+
class Command
|
4
|
+
COMMAND_NOT_FOUND = 127
|
5
|
+
|
6
|
+
# Constructor
|
7
|
+
#
|
8
|
+
# @param command_line [String]
|
9
|
+
# @param error_log_path [String]
|
10
|
+
# @param logger [#write_no_newline]
|
11
|
+
def initialize(command_line, error_log_path, logger)
|
12
|
+
@command_line = command_line
|
13
|
+
@error_log_path = error_log_path
|
14
|
+
@logger = logger
|
15
|
+
end
|
16
|
+
|
17
|
+
# Executes the command returning its status. It also prints its stdout to
|
18
|
+
# the logger and its stderr to the file specified in error_log_path.
|
19
|
+
#
|
20
|
+
# @raise [NoStatusError] if the spawned process' status can't be retrieved
|
21
|
+
# @raise [SignalError] if the spawned process received a signal
|
22
|
+
# @raise [CommandNotFoundError] if pt-online-schema-change can't be found
|
23
|
+
#
|
24
|
+
# @return [Process::Status]
|
25
|
+
def run
|
26
|
+
log_started
|
27
|
+
|
28
|
+
run_in_process
|
29
|
+
|
30
|
+
log_finished
|
31
|
+
|
32
|
+
validate_status!
|
33
|
+
status
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :command_line, :error_log_path, :logger, :status
|
39
|
+
|
40
|
+
# Runs the command in a separate process, capturing its stdout and
|
41
|
+
# execution status
|
42
|
+
def run_in_process
|
43
|
+
Open3.popen3(full_command) do |_stdin, stdout, _stderr, waith_thr|
|
44
|
+
begin
|
45
|
+
loop do
|
46
|
+
IO.select([stdout])
|
47
|
+
data = stdout.read_nonblock(8192)
|
48
|
+
logger.write_no_newline(data)
|
49
|
+
end
|
50
|
+
rescue EOFError # rubocop:disable Lint/HandleExceptions
|
51
|
+
# noop
|
52
|
+
ensure
|
53
|
+
@status = waith_thr.value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Builds the actual command including stderr redirection to the specified
|
59
|
+
# log file
|
60
|
+
#
|
61
|
+
# @return [String]
|
62
|
+
def full_command
|
63
|
+
"#{command_line} 2> #{error_log_path}"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Validates the status of the execution
|
67
|
+
#
|
68
|
+
# @raise [NoStatusError] if the spawned process' status can't be retrieved
|
69
|
+
# @raise [SignalError] if the spawned process received a signal
|
70
|
+
# @raise [CommandNotFoundError] if pt-online-schema-change can't be found
|
71
|
+
def validate_status!
|
72
|
+
raise SignalError.new(status) if status.signaled? # rubocop:disable Style/RaiseArgs
|
73
|
+
raise CommandNotFoundError if status.exitstatus == COMMAND_NOT_FOUND
|
74
|
+
raise Error, error_message unless status.success?
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the error message that appeared in the process' stderr
|
78
|
+
#
|
79
|
+
# @return [String]
|
80
|
+
def error_message
|
81
|
+
File.read(error_log_path)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Logs when the execution started
|
85
|
+
def log_started
|
86
|
+
logger.write("\n")
|
87
|
+
logger.say("Running #{command_line}\n\n", true)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Prints a line break to keep the logs separate from the execution time
|
91
|
+
# print by the migration
|
92
|
+
def log_finished
|
93
|
+
logger.write("\n")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Departure
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :tmp_path, :global_percona_args, :enabled_by_default
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@tmp_path = '.'.freeze
|
7
|
+
@error_log_filename = 'departure_error.log'.freeze
|
8
|
+
@global_percona_args = nil
|
9
|
+
@enabled_by_default = true
|
10
|
+
end
|
11
|
+
|
12
|
+
def error_log_path
|
13
|
+
File.join(tmp_path, error_log_filename)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :error_log_filename
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,96 @@
|
|
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 [Hash] connection parametes 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 ||= "#{host_argument} -P #{port} -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
|
+
# Returns the host fragment of the details string, adds ssl options if needed
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
def host_argument
|
47
|
+
host_string = host
|
48
|
+
if ssl_ca.present?
|
49
|
+
host_string += ";mysql_ssl=1;mysql_ssl_client_ca=#{ssl_ca}"
|
50
|
+
end
|
51
|
+
"-h \"#{host_string}\""
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
attr_reader :connection_data
|
57
|
+
|
58
|
+
# Returns the database host name, defaulting to localhost. If PERCONA_DB_HOST
|
59
|
+
# is passed its value will be used instead
|
60
|
+
#
|
61
|
+
# @return [String]
|
62
|
+
def host
|
63
|
+
ENV.fetch('PERCONA_DB_HOST', connection_data[:host]) || 'localhost'
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the database user. If PERCONA_DB_USER is passed its value will be
|
67
|
+
# used instead
|
68
|
+
#
|
69
|
+
# @return [String]
|
70
|
+
def user
|
71
|
+
ENV.fetch('PERCONA_DB_USER', connection_data[:username])
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the database user's password. If PERCONA_DB_PASSWORD is passed its
|
75
|
+
# value will be used instead
|
76
|
+
#
|
77
|
+
# @return [String]
|
78
|
+
def password
|
79
|
+
ENV.fetch('PERCONA_DB_PASSWORD', connection_data[:password])
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the database's port.
|
83
|
+
#
|
84
|
+
# @return [String]
|
85
|
+
def port
|
86
|
+
connection_data.fetch(:port, DEFAULT_PORT)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the database' SSL CA certificate.
|
90
|
+
#
|
91
|
+
# @return [String]
|
92
|
+
def ssl_ca
|
93
|
+
connection_data.fetch(:sslca, nil)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,24 @@
|
|
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
|
+
end
|
13
|
+
|
14
|
+
# Returns the pt-online-schema-change DSN string. See
|
15
|
+
# https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
|
16
|
+
def to_s
|
17
|
+
"D=#{database},t=#{table_name}"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :table_name, :database
|
23
|
+
end
|
24
|
+
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
|