departure-76c9880 6.2.0

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