departure 4.0.1 → 6.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -44,10 +44,10 @@ module Departure
44
44
  begin
45
45
  loop do
46
46
  IO.select([stdout])
47
- data = stdout.read_nonblock(8)
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
@@ -0,0 +1,9 @@
1
+ module Departure
2
+ class ConnectionBase < ActiveRecord::Base
3
+ def self.establish_connection(config = nil)
4
+ super.tap do
5
+ ActiveRecord::Base.connection_specification_name = connection_specification_name
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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 ||= "-h #{host} -u #{user} #{password_argument}"
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
- "-p #{password}"
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,9 +1,7 @@
1
1
  module Departure
2
-
3
2
  # Represents the 'DSN' argument of Percona's pt-online-schema-change
4
3
  # See https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
5
4
  class DSN
6
-
7
5
  # Constructor
8
6
  #
9
7
  # @param database [String, Symbol]
@@ -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
@@ -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 ? " ->" : "--"} #{message}"
17
+ write "#{subitem ? ' ->' : '--'} #{message}"
19
18
  end
20
19
 
21
20
  # Outputs the text through the stdout adding a new line at the end
@@ -1,6 +1,5 @@
1
1
  module Departure
2
2
  module LoggerFactory
3
-
4
3
  # Returns the appropriate logger instance for the given configuration. Use
5
4
  # :verbose option to log to the stdout
6
5
  #
@@ -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
@@ -24,10 +24,10 @@ module Departure
24
24
  #
25
25
  # @param [Option]
26
26
  # @return [Boolean]
27
- def ==(another_option)
28
- name == another_option.name
27
+ def ==(other)
28
+ name == other.name
29
29
  end
30
- alias :eql? :==
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}"
@@ -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
@@ -1,11 +1,9 @@
1
1
  require 'open3'
2
2
 
3
3
  module Departure
4
-
5
4
  # It executes pt-online-schema-change commands in a new process and gets its
6
5
  # output and status
7
6
  class Runner
8
-
9
7
  # Constructor
10
8
  #
11
9
  # @param logger [#say, #write]
@@ -1,3 +1,3 @@
1
1
  module Departure
2
- VERSION = '4.0.1'.freeze
2
+ VERSION = '6.3.0'.freeze
3
3
  end
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
@@ -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(definition)
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
- cast_type,
61
- definition,
65
+ mysql_metadata,
62
66
  null_value
63
67
  )
64
68
  end
@@ -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
- %x(#{mysql_command} "USE #{database}; DROP TABLE IF EXISTS schema_migrations; 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")
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
- %x(#{mysql_command} "DROP DATABASE IF EXISTS #{database}; CREATE DATABASE #{database} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci;")
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
- %x(#{mysql_command} "USE #{database}; DROP TABLE IF EXISTS comments; CREATE TABLE comments ( id int(12) NOT NULL AUTO_INCREMENT, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;")
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
- # Returns the command to run the mysql client. It uses the crendentials from
48
- # the provided config
49
- def mysql_command
50
- "mysql --user=#{config['username']} --password=#{config['password']} -e"
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