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.
- checksums.yaml +5 -5
- data/.codeclimate.yml +8 -0
- data/.gitignore +1 -0
- data/.pryrc +11 -0
- data/.rubocop.yml +60 -0
- data/.travis.yml +19 -2
- data/CHANGELOG.md +56 -0
- data/Dockerfile +32 -0
- data/Gemfile +2 -1
- data/LICENSE.txt +2 -1
- data/README.md +56 -15
- data/RELEASING.md +1 -1
- data/bin/console +3 -3
- data/bin/rspec +6 -7
- data/config.yml.erb +4 -0
- data/configuration.rb +3 -2
- data/departure.gemspec +14 -9
- 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 +75 -17
- data/lib/departure.rb +11 -49
- data/lib/departure/alter_argument.rb +10 -4
- data/lib/departure/cli_generator.rb +2 -3
- data/lib/departure/command.rb +3 -3
- data/lib/departure/configuration.rb +2 -1
- data/lib/departure/connection_base.rb +9 -0
- data/lib/departure/connection_details.rb +29 -3
- data/lib/departure/dsn.rb +0 -2
- data/lib/departure/log_sanitizers/password_sanitizer.rb +2 -1
- data/lib/departure/logger.rb +1 -2
- data/lib/departure/logger_factory.rb +0 -1
- data/lib/departure/migration.rb +104 -0
- data/lib/departure/option.rb +4 -4
- data/lib/departure/railtie.rb +6 -13
- data/lib/departure/runner.rb +0 -2
- data/lib/departure/version.rb +1 -1
- data/lib/lhm.rb +1 -3
- data/lib/lhm/adapter.rb +1 -3
- data/lib/lhm/column_with_sql.rb +8 -4
- data/lib/lhm/column_with_type.rb +0 -2
- data/test_database.rb +37 -9
- metadata +55 -33
- data/config.yml +0 -3
data/lib/departure/command.rb
CHANGED
@@ -44,10 +44,10 @@ module Departure
|
|
44
44
|
begin
|
45
45
|
loop do
|
46
46
|
IO.select([stdout])
|
47
|
-
data = stdout.read_nonblock(
|
47
|
+
data = stdout.read_nonblock(8192)
|
48
48
|
logger.write_no_newline(data)
|
49
49
|
end
|
50
|
-
rescue EOFError
|
50
|
+
rescue EOFError # rubocop:disable Lint/HandleExceptions
|
51
51
|
# noop
|
52
52
|
ensure
|
53
53
|
@status = waith_thr.value
|
@@ -69,7 +69,7 @@ module Departure
|
|
69
69
|
# @raise [SignalError] if the spawned process received a signal
|
70
70
|
# @raise [CommandNotFoundError] if pt-online-schema-change can't be found
|
71
71
|
def validate_status!
|
72
|
-
raise SignalError.new(status) if status.signaled?
|
72
|
+
raise SignalError.new(status) if status.signaled? # rubocop:disable Style/RaiseArgs
|
73
73
|
raise CommandNotFoundError if status.exitstatus == COMMAND_NOT_FOUND
|
74
74
|
raise Error, error_message unless status.success?
|
75
75
|
end
|
@@ -1,11 +1,12 @@
|
|
1
1
|
module Departure
|
2
2
|
class Configuration
|
3
|
-
attr_accessor :tmp_path, :global_percona_args
|
3
|
+
attr_accessor :tmp_path, :global_percona_args, :enabled_by_default
|
4
4
|
|
5
5
|
def initialize
|
6
6
|
@tmp_path = '.'.freeze
|
7
7
|
@error_log_filename = 'departure_error.log'.freeze
|
8
8
|
@global_percona_args = nil
|
9
|
+
@enabled_by_default = true
|
9
10
|
end
|
10
11
|
|
11
12
|
def error_log_path
|
@@ -1,7 +1,8 @@
|
|
1
|
+
require 'shellwords'
|
1
2
|
module Departure
|
2
3
|
# Holds the parameters of the DB connection and formats them to string
|
3
4
|
class ConnectionDetails
|
4
|
-
|
5
|
+
DEFAULT_PORT = 3306
|
5
6
|
# Constructor
|
6
7
|
#
|
7
8
|
# @param [Hash] connection parametes as used in #establish_conneciton
|
@@ -14,7 +15,7 @@ module Departure
|
|
14
15
|
#
|
15
16
|
# @return [String]
|
16
17
|
def to_s
|
17
|
-
@to_s ||= "-
|
18
|
+
@to_s ||= "#{host_argument} -P #{port} -u #{user} #{password_argument}"
|
18
19
|
end
|
19
20
|
|
20
21
|
# TODO: Doesn't the abstract adapter already handle this somehow?
|
@@ -33,12 +34,23 @@ module Departure
|
|
33
34
|
# @return [String]
|
34
35
|
def password_argument
|
35
36
|
if password.present?
|
36
|
-
|
37
|
+
%(--password #{Shellwords.escape(password)} )
|
37
38
|
else
|
38
39
|
''
|
39
40
|
end
|
40
41
|
end
|
41
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
|
+
|
42
54
|
private
|
43
55
|
|
44
56
|
attr_reader :connection_data
|
@@ -66,5 +78,19 @@ module Departure
|
|
66
78
|
def password
|
67
79
|
ENV.fetch('PERCONA_DB_PASSWORD', connection_data[:password])
|
68
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
|
69
95
|
end
|
70
96
|
end
|
data/lib/departure/dsn.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Departure
|
2
2
|
module LogSanitizers
|
3
3
|
class PasswordSanitizer
|
4
|
-
PASSWORD_REPLACEMENT = '[filtered_password]'
|
4
|
+
PASSWORD_REPLACEMENT = '[filtered_password]'.freeze
|
5
5
|
|
6
6
|
delegate :password_argument, to: :connection_details
|
7
7
|
|
@@ -15,6 +15,7 @@ module Departure
|
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
18
|
+
|
18
19
|
attr_accessor :connection_details
|
19
20
|
end
|
20
21
|
end
|
data/lib/departure/logger.rb
CHANGED
@@ -4,7 +4,6 @@ module Departure
|
|
4
4
|
# the from ActiveRecord::Migration because the migration's instance can't be
|
5
5
|
# seen from the connection adapter.
|
6
6
|
class Logger
|
7
|
-
|
8
7
|
def initialize(sanitizers)
|
9
8
|
@sanitizers = sanitizers
|
10
9
|
end
|
@@ -15,7 +14,7 @@ module Departure
|
|
15
14
|
# @param message [String]
|
16
15
|
# @param subitem [Boolean] whether to show message as a nested log item
|
17
16
|
def say(message, subitem = false)
|
18
|
-
write "#{subitem ?
|
17
|
+
write "#{subitem ? ' ->' : '--'} #{message}"
|
19
18
|
end
|
20
19
|
|
21
20
|
# Outputs the text through the stdout adding a new line at the 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::ConnectionBase.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
|
data/lib/departure/option.rb
CHANGED
@@ -24,10 +24,10 @@ module Departure
|
|
24
24
|
#
|
25
25
|
# @param [Option]
|
26
26
|
# @return [Boolean]
|
27
|
-
def ==(
|
28
|
-
name ==
|
27
|
+
def ==(other)
|
28
|
+
name == other.name
|
29
29
|
end
|
30
|
-
alias
|
30
|
+
alias eql? ==
|
31
31
|
|
32
32
|
# Returns the option's hash
|
33
33
|
#
|
@@ -65,7 +65,7 @@ module Departure
|
|
65
65
|
def value_as_string
|
66
66
|
if value.nil?
|
67
67
|
''
|
68
|
-
elsif value.include?(
|
68
|
+
elsif value.include?('=')
|
69
69
|
" #{value}"
|
70
70
|
else
|
71
71
|
"=#{value}"
|
data/lib/departure/railtie.rb
CHANGED
@@ -6,23 +6,16 @@ module Departure
|
|
6
6
|
class Railtie < Rails::Railtie
|
7
7
|
railtie_name :departure
|
8
8
|
|
9
|
-
# It drops all previous database connections and reconnects using this
|
10
|
-
# PerconaAdapter. By doing this, all later ActiveRecord methods called in
|
11
|
-
# the migration will use this adapter instead of Mysql2Adapter.
|
12
|
-
#
|
13
|
-
# It also patches ActiveRecord's #migrate method so that it patches LHM
|
14
|
-
# first. This will make migrations written with LHM to go through the
|
15
|
-
# regular Rails Migration DSL.
|
16
|
-
initializer 'departure.configure_rails_initialization' do
|
17
|
-
ActiveSupport.on_load(:active_record) do
|
18
|
-
Departure.load
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
9
|
initializer 'departure.configure' do |app|
|
23
10
|
Departure.configure do |config|
|
24
11
|
config.tmp_path = app.paths['tmp'].first
|
25
12
|
end
|
26
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
|
27
20
|
end
|
28
21
|
end
|
data/lib/departure/runner.rb
CHANGED
data/lib/departure/version.rb
CHANGED
data/lib/lhm.rb
CHANGED
@@ -4,14 +4,13 @@ require 'lhm/adapter'
|
|
4
4
|
# while providing a different behaviour. We delegate all LHM's methods to
|
5
5
|
# ActiveRecord so that you don't need to modify your old LHM migrations
|
6
6
|
module Lhm
|
7
|
-
|
8
7
|
# Yields an adapter instance so that Lhm migration Dsl methods get
|
9
8
|
# delegated to ActiveRecord::Migration ones instead
|
10
9
|
#
|
11
10
|
# @param table_name [String]
|
12
11
|
# @param _options [Hash]
|
13
12
|
# @param block [Block]
|
14
|
-
def self.change_table(table_name, _options = {}, &block)
|
13
|
+
def self.change_table(table_name, _options = {}, &block) # rubocop:disable Lint/UnusedMethodArgument
|
15
14
|
yield Adapter.new(@migration, table_name)
|
16
15
|
end
|
17
16
|
|
@@ -22,4 +21,3 @@ module Lhm
|
|
22
21
|
@migration = migration
|
23
22
|
end
|
24
23
|
end
|
25
|
-
|
data/lib/lhm/adapter.rb
CHANGED
@@ -2,12 +2,10 @@ require 'lhm/column_with_sql'
|
|
2
2
|
require 'lhm/column_with_type'
|
3
3
|
|
4
4
|
module Lhm
|
5
|
-
|
6
5
|
# Translates Lhm DSL to ActiveRecord's one, so Lhm migrations will now go
|
7
6
|
# through Percona as well, without any modification on the migration's
|
8
7
|
# code
|
9
8
|
class Adapter
|
10
|
-
|
11
9
|
# Constructor
|
12
10
|
#
|
13
11
|
# @param migration [ActiveRecord::Migtration]
|
@@ -79,7 +77,7 @@ module Lhm
|
|
79
77
|
# @param index_name [String]
|
80
78
|
def add_unique_index(columns, index_name = nil)
|
81
79
|
options = { unique: true }
|
82
|
-
options.merge!(name: index_name) if index_name
|
80
|
+
options.merge!(name: index_name) if index_name # rubocop:disable Performance/RedundantMerge
|
83
81
|
|
84
82
|
migration.add_index(table_name, columns, options)
|
85
83
|
end
|
data/lib/lhm/column_with_sql.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
|
3
3
|
module Lhm
|
4
|
-
|
5
4
|
# Abstracts the details of a table column definition when specified with a MySQL
|
6
5
|
# column definition string
|
7
6
|
class ColumnWithSql
|
@@ -53,12 +52,17 @@ module Lhm
|
|
53
52
|
#
|
54
53
|
# @return [column_factory]
|
55
54
|
def column
|
56
|
-
cast_type = ActiveRecord::Base.connection.lookup_cast_type
|
55
|
+
cast_type = ActiveRecord::Base.connection.send(:lookup_cast_type, definition)
|
56
|
+
metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
|
57
|
+
type: cast_type.type,
|
58
|
+
sql_type: definition,
|
59
|
+
limit: cast_type.limit
|
60
|
+
)
|
61
|
+
mysql_metadata = ActiveRecord::ConnectionAdapters::MySQL::TypeMetadata.new(metadata)
|
57
62
|
@column ||= self.class.column_factory.new(
|
58
63
|
name,
|
59
64
|
default_value,
|
60
|
-
|
61
|
-
definition,
|
65
|
+
mysql_metadata,
|
62
66
|
null_value
|
63
67
|
)
|
64
68
|
end
|
data/lib/lhm/column_with_type.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
1
|
module Lhm
|
2
|
-
|
3
2
|
# Abstracts the details of a table column definition when specified with a type
|
4
3
|
# as a symbol. This is the regular ActiveRecord's #add_column syntax:
|
5
4
|
#
|
6
5
|
# add_column :tablenames, :field, :string
|
7
6
|
#
|
8
7
|
class ColumnWithType
|
9
|
-
|
10
8
|
# Constructor
|
11
9
|
#
|
12
10
|
# @param name [String, Symbol]
|
data/test_database.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
3
|
+
|
1
4
|
# Setups the test database with the schema_migrations table that ActiveRecord
|
2
5
|
# requires for the migrations, plus a table for the Comment model used throught
|
3
6
|
# the tests.
|
4
7
|
#
|
5
8
|
class TestDatabase
|
6
|
-
|
7
9
|
# Constructor
|
8
10
|
#
|
9
11
|
# @param config [Hash]
|
@@ -19,7 +21,7 @@ class TestDatabase
|
|
19
21
|
drop_and_create_schema_migrations_table
|
20
22
|
end
|
21
23
|
|
22
|
-
# Creates the #{database} database and the comments table in it.
|
24
|
+
# Creates the #{@database} database and the comments table in it.
|
23
25
|
# Before, it drops both if they already exist
|
24
26
|
def setup_test_database
|
25
27
|
drop_and_create_test_database
|
@@ -29,7 +31,13 @@ class TestDatabase
|
|
29
31
|
# Creates the ActiveRecord's schema_migrations table required for
|
30
32
|
# migrations to work. Before, it drops the table if it already exists
|
31
33
|
def drop_and_create_schema_migrations_table
|
32
|
-
|
34
|
+
sql = [
|
35
|
+
"USE #{@database}",
|
36
|
+
'DROP TABLE IF EXISTS schema_migrations',
|
37
|
+
'CREATE TABLE schema_migrations ( version varchar(255) COLLATE utf8_unicode_ci NOT NULL, UNIQUE KEY unique_schema_migrations (version)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'
|
38
|
+
]
|
39
|
+
|
40
|
+
run_commands(sql)
|
33
41
|
end
|
34
42
|
|
35
43
|
private
|
@@ -37,16 +45,36 @@ class TestDatabase
|
|
37
45
|
attr_reader :config, :database
|
38
46
|
|
39
47
|
def drop_and_create_test_database
|
40
|
-
|
48
|
+
sql = [
|
49
|
+
"DROP DATABASE IF EXISTS #{@database}",
|
50
|
+
"CREATE DATABASE #{@database} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci"
|
51
|
+
]
|
52
|
+
|
53
|
+
run_commands(sql)
|
41
54
|
end
|
42
55
|
|
43
56
|
def drop_and_create_comments_table
|
44
|
-
|
57
|
+
sql = [
|
58
|
+
"USE #{@database}",
|
59
|
+
'DROP TABLE IF EXISTS comments',
|
60
|
+
'CREATE TABLE comments ( id bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci'
|
61
|
+
]
|
62
|
+
|
63
|
+
run_commands(sql)
|
64
|
+
end
|
65
|
+
|
66
|
+
def run_commands(sql)
|
67
|
+
conn.execute('START TRANSACTION')
|
68
|
+
sql.each { |str| conn.execute(str) }
|
69
|
+
conn.execute('COMMIT')
|
45
70
|
end
|
46
71
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
72
|
+
def conn
|
73
|
+
@conn ||= ActiveRecord::Base.mysql2_connection(
|
74
|
+
host: @config['hostname'],
|
75
|
+
username: @config['username'],
|
76
|
+
password: @config['password'],
|
77
|
+
reconnect: true
|
78
|
+
)
|
51
79
|
end
|
52
80
|
end
|