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,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,96 @@
|
|
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
|
+
ActiveRecord::Base.connection_config.tap do |config|
|
92
|
+
self.class.original_adapter ||= config[:adapter]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
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,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,62 @@
|
|
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
|
+
# Constructor
|
8
|
+
#
|
9
|
+
# @param logger [#say, #write]
|
10
|
+
# @param cli_generator [CliGenerator]
|
11
|
+
# @param mysql_adapter [ActiveRecord::ConnectionAdapter] it must implement
|
12
|
+
# #execute and #raw_connection
|
13
|
+
def initialize(logger, cli_generator, mysql_adapter, config = Departure.configuration)
|
14
|
+
@logger = logger
|
15
|
+
@cli_generator = cli_generator
|
16
|
+
@mysql_adapter = mysql_adapter
|
17
|
+
@error_log_path = config.error_log_path
|
18
|
+
end
|
19
|
+
|
20
|
+
# Executes the passed sql statement using pt-online-schema-change for ALTER
|
21
|
+
# TABLE statements, or the specified mysql adapter otherwise.
|
22
|
+
#
|
23
|
+
# @param sql [String]
|
24
|
+
def query(sql)
|
25
|
+
if alter_statement?(sql)
|
26
|
+
command_line = cli_generator.parse_statement(sql)
|
27
|
+
execute(command_line)
|
28
|
+
else
|
29
|
+
mysql_adapter.execute(sql)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the number of rows affected by the last UPDATE, DELETE or INSERT
|
34
|
+
# statements
|
35
|
+
#
|
36
|
+
# @return [Integer]
|
37
|
+
def affected_rows
|
38
|
+
mysql_adapter.raw_connection.affected_rows
|
39
|
+
end
|
40
|
+
|
41
|
+
# TODO: rename it so we don't confuse it with AR's #execute
|
42
|
+
# Runs and logs the given command
|
43
|
+
#
|
44
|
+
# @param command_line [String]
|
45
|
+
# @return [Boolean]
|
46
|
+
def execute(command_line)
|
47
|
+
Command.new(command_line, error_log_path, logger).run
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :logger, :cli_generator, :mysql_adapter, :error_log_path
|
53
|
+
|
54
|
+
# Checks whether the sql statement is an ALTER TABLE
|
55
|
+
#
|
56
|
+
# @param sql [String]
|
57
|
+
# @return [Boolean]
|
58
|
+
def alter_statement?(sql)
|
59
|
+
sql =~ /\Aalter table/i
|
60
|
+
end
|
61
|
+
end
|
62
|
+
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
|
data/lib/lhm.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'lhm/adapter'
|
2
|
+
|
3
|
+
# Defines the same global namespace as LHM's gem does to mimic its API
|
4
|
+
# while providing a different behaviour. We delegate all LHM's methods to
|
5
|
+
# ActiveRecord so that you don't need to modify your old LHM migrations
|
6
|
+
module Lhm
|
7
|
+
# Yields an adapter instance so that Lhm migration Dsl methods get
|
8
|
+
# delegated to ActiveRecord::Migration ones instead
|
9
|
+
#
|
10
|
+
# @param table_name [String]
|
11
|
+
# @param _options [Hash]
|
12
|
+
# @param block [Block]
|
13
|
+
def self.change_table(table_name, _options = {}, &block) # rubocop:disable Lint/UnusedMethodArgument
|
14
|
+
yield Adapter.new(@migration, table_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Sets the migration to apply the adapter to
|
18
|
+
#
|
19
|
+
# @param migration [ActiveRecord::Migration]
|
20
|
+
def self.migration=(migration)
|
21
|
+
@migration = migration
|
22
|
+
end
|
23
|
+
end
|
data/lib/lhm/adapter.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'lhm/column_with_sql'
|
2
|
+
require 'lhm/column_with_type'
|
3
|
+
|
4
|
+
module Lhm
|
5
|
+
# Translates Lhm DSL to ActiveRecord's one, so Lhm migrations will now go
|
6
|
+
# through Percona as well, without any modification on the migration's
|
7
|
+
# code
|
8
|
+
class Adapter
|
9
|
+
# Constructor
|
10
|
+
#
|
11
|
+
# @param migration [ActiveRecord::Migtration]
|
12
|
+
# @param table_name [String, Symbol]
|
13
|
+
def initialize(migration, table_name)
|
14
|
+
@migration = migration
|
15
|
+
@table_name = table_name
|
16
|
+
end
|
17
|
+
|
18
|
+
# Adds the specified column through ActiveRecord
|
19
|
+
#
|
20
|
+
# @param column_name [String, Symbol]
|
21
|
+
# @param definition [String, Symbol]
|
22
|
+
def add_column(column_name, definition)
|
23
|
+
attributes = column_attributes(column_name, definition)
|
24
|
+
migration.add_column(*attributes)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Removes the specified column through ActiveRecord
|
28
|
+
#
|
29
|
+
# @param column_name [String, Symbol]
|
30
|
+
def remove_column(column_name)
|
31
|
+
migration.remove_column(table_name, column_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Adds an index in the specified columns through ActiveRecord. Note you
|
35
|
+
# can provide a name as well
|
36
|
+
#
|
37
|
+
# @param columns [Array<String>, Array<Symbol>, String, Symbol]
|
38
|
+
# @param index_name [String]
|
39
|
+
def add_index(columns, index_name = nil)
|
40
|
+
options = { name: index_name } if index_name
|
41
|
+
migration.add_index(table_name, columns, options || {})
|
42
|
+
end
|
43
|
+
|
44
|
+
# Removes the index in the given columns or by its name
|
45
|
+
#
|
46
|
+
# @param columns [Array<String>, Array<Symbol>, String, Symbol]
|
47
|
+
# @param index_name [String]
|
48
|
+
def remove_index(columns, index_name = nil)
|
49
|
+
options = if index_name
|
50
|
+
{ name: index_name }
|
51
|
+
else
|
52
|
+
{ column: columns }
|
53
|
+
end
|
54
|
+
migration.remove_index(table_name, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Change the column to use the provided definition, through ActiveRecord
|
58
|
+
#
|
59
|
+
# @param column_name [String, Symbol]
|
60
|
+
# @param definition [String, Symbol]
|
61
|
+
def change_column(column_name, definition)
|
62
|
+
attributes = column_attributes(column_name, definition)
|
63
|
+
migration.change_column(*attributes)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Renames the old_name column to new_name by using ActiveRecord
|
67
|
+
#
|
68
|
+
# @param old_name [String, Symbol]
|
69
|
+
# @param new_name [String, Symbol]
|
70
|
+
def rename_column(old_name, new_name)
|
71
|
+
migration.rename_column(table_name, old_name, new_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Adds a unique index on the given columns, with the provided name if passed
|
75
|
+
#
|
76
|
+
# @param columns [Array<String>, Array<Symbol>, String, Symbol]
|
77
|
+
# @param index_name [String]
|
78
|
+
def add_unique_index(columns, index_name = nil)
|
79
|
+
options = { unique: true }
|
80
|
+
options.merge!(name: index_name) if index_name # rubocop:disable Performance/RedundantMerge
|
81
|
+
|
82
|
+
migration.add_index(table_name, columns, options)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
attr_reader :migration, :table_name
|
88
|
+
|
89
|
+
# Returns the instance of ActiveRecord column with the given name and
|
90
|
+
# definition
|
91
|
+
#
|
92
|
+
# @param name [String, Symbol]
|
93
|
+
# @param definition [String]
|
94
|
+
def column(name, definition)
|
95
|
+
@column ||= if definition.is_a?(Symbol)
|
96
|
+
ColumnWithType.new(name, definition)
|
97
|
+
else
|
98
|
+
ColumnWithSql.new(name, definition)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def column_attributes(name, definition)
|
103
|
+
attributes = column(name, definition).attributes
|
104
|
+
[table_name, name].concat(attributes)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|