departure 4.0.1 → 6.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|