departure-76c9880 6.2.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +8 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +60 -0
  6. data/.travis.yml +30 -0
  7. data/CHANGELOG.md +192 -0
  8. data/CODE_OF_CONDUCT.md +50 -0
  9. data/Dockerfile +32 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +227 -0
  13. data/RELEASING.md +17 -0
  14. data/Rakefile +17 -0
  15. data/bin/console +14 -0
  16. data/bin/rspec +16 -0
  17. data/bin/setup +7 -0
  18. data/config.yml.erb +4 -0
  19. data/configuration.rb +16 -0
  20. data/departure.gemspec +35 -0
  21. data/docker-compose.yml +23 -0
  22. data/lib/active_record/connection_adapters/for_alter.rb +91 -0
  23. data/lib/active_record/connection_adapters/percona_adapter.rb +168 -0
  24. data/lib/departure.rb +43 -0
  25. data/lib/departure/alter_argument.rb +49 -0
  26. data/lib/departure/cli_generator.rb +84 -0
  27. data/lib/departure/command.rb +96 -0
  28. data/lib/departure/configuration.rb +20 -0
  29. data/lib/departure/connection_base.rb +9 -0
  30. data/lib/departure/connection_details.rb +96 -0
  31. data/lib/departure/dsn.rb +24 -0
  32. data/lib/departure/errors.rb +39 -0
  33. data/lib/departure/log_sanitizers/password_sanitizer.rb +22 -0
  34. data/lib/departure/logger.rb +42 -0
  35. data/lib/departure/logger_factory.rb +16 -0
  36. data/lib/departure/migration.rb +96 -0
  37. data/lib/departure/null_logger.rb +15 -0
  38. data/lib/departure/option.rb +75 -0
  39. data/lib/departure/railtie.rb +21 -0
  40. data/lib/departure/runner.rb +62 -0
  41. data/lib/departure/user_options.rb +44 -0
  42. data/lib/departure/version.rb +3 -0
  43. data/lib/lhm.rb +23 -0
  44. data/lib/lhm/adapter.rb +107 -0
  45. data/lib/lhm/column_with_sql.rb +96 -0
  46. data/lib/lhm/column_with_type.rb +29 -0
  47. data/test_database.rb +80 -0
  48. 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,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,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
@@ -0,0 +1,3 @@
1
+ module Departure
2
+ VERSION = '6.2.0'.freeze
3
+ end
@@ -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
@@ -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