percona_migrator 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 686c3a5b5ab858a587451fba1c6aa0b934f1cf9c
4
- data.tar.gz: d01ece64414481d9b43a143a16f748ce134afc7a
3
+ metadata.gz: df19cbdeb9b3e36074ff5e95ec745afafb5db5f6
4
+ data.tar.gz: df627f8393513e5699c63d37c84e46237596aea4
5
5
  SHA512:
6
- metadata.gz: 104039977e6a970cad93bcb93e7ff628a2a00c0a10715543daaf9915fca08a1bf6e1643dc2e2e34e947f494f43abc9662c62a0a3e53ae9c98a0500abf9a9576e
7
- data.tar.gz: f7e205f53c0cfd327dd293d0357b8e1e85b64ddf0a7a2cca14679a22607e3d5c8d1731757cb0ed1a25d83c38f31795fe23a74fc9cfd82af5cad1f42e67ce4e84
6
+ metadata.gz: 6a37c51ca128523a8ab16f9271a76e0612355ccba8e9406557634ccaf77d4e62db25fa29ccc9edf0fc5e7ebf952a6bb7074e870f7fa545c5714b5235212e6d51
7
+ data.tar.gz: 80a4e55d6136515ee6f742b9053a7dca9d1e11570bdc29d1f1b662e135f210bb88e54d6a09dceb9b3b88d4fee4056464e527dce22dadf6fd10d665b6d698586c
data/.travis.yml CHANGED
@@ -7,7 +7,7 @@ before_install:
7
7
  - sudo apt-get update -qq
8
8
  - sudo apt-get install percona-toolkit
9
9
  - gem update bundler
10
- - bin/setup
10
+ install: bin/setup
11
11
  after_success:
12
12
  - codeclimate-test-reporter
13
13
 
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ This project adheres to [Semantic Versioning](http://semver.org/).
5
5
  Please follow the format in [Keep a Changelog](http://keepachangelog.com/)
6
6
 
7
7
  ## [Unreleased]
8
+ ## [1.1.0] - 2017-04-07
9
+
10
+ ### Added
11
+
12
+ - Allow passing any `pt-online-schema-change`'s arguments through the
13
+ `PERCONA_ARGS` env var when executing a migration with `rake db:migrate:up`
14
+ or `db:migrate:down`.
15
+ - Allow setting global percona arguments via gem configuration
16
+ - Filter MySQL's password from logs
17
+
18
+ ### Changed
19
+
20
+ - Enable default pt-online-schema-change replicas discovering mechanism.
21
+ So far, this was purposely set to `none`. To keep this same behaviour
22
+ provide the `PERCONA_ARGS=--recursion-method=none` env var when running the
23
+ migration.
8
24
 
9
25
  ## [1.0.0] - 2016-11-30
10
26
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Percona Migrator [![Build Status](https://travis-ci.org/redbooth/percona_migrator.svg?branch=master)](https://travis-ci.org/redbooth/percona_migrator) [![Code Climate](https://codeclimate.com/github/redbooth/percona_migrator/badges/gpa.svg)](https://codeclimate.com/github/redbooth/percona_migrator)
1
+ # Departure [![Build Status](https://travis-ci.org/redbooth/departure.svg?branch=master)](https://travis-ci.org/redbooth/percona_migrator) [![Code Climate](https://codeclimate.com/github/redbooth/percona_migrator/badges/gpa.svg)](https://codeclimate.com/github/redbooth/percona_migrator)
2
2
 
3
- Percona Migrator is an **ActiveRecord connection adapter** that allows running
3
+ Departure is an **ActiveRecord connection adapter** that allows running
4
4
  **MySQL online and non-blocking DDL** through `ActiveRecord::Migration` without needing
5
5
  to use a different DSL other than Rails' migrations DSL.
6
6
 
@@ -9,15 +9,38 @@ It uses `pt-online-schema-change` command-line tool of
9
9
  Toolkit](https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html)
10
10
  which runs MySQL alter table statements without downtime.
11
11
 
12
+ ## Rename from "Percona Migrator"
13
+
14
+ This project was formerly known as "Percona Migrator", but this incurs in an
15
+ infringement of Percona's trade mark policy and thus has to be renamed. Said
16
+ name is likely to cause confusion as to the source of the wrapper.
17
+
18
+ The next major versions will use "Departure" as gem name.
19
+
12
20
  ## Installation
13
21
 
14
- Percona Migrator relies on `pt-online-schema-change` from [Percona
22
+ Departure relies on `pt-online-schema-change` from [Percona
15
23
  Toolkit](https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html)
16
24
 
17
25
  ### Mac
18
26
 
19
27
  `brew install percona-toolkit`
20
28
 
29
+ If when running a migration you see an error like:
30
+
31
+ ```
32
+ PerconaMigrator::Error: Cannot connect to MySQL: Cannot connect to MySQL because
33
+ the Perl DBI module is not installed or not found.
34
+ ```
35
+
36
+ You also need to install the DBI and DBD::MySQL modules from `cpan`.
37
+
38
+ ```
39
+ $ sudo cpan
40
+ cpan> install DBI
41
+ cpan> install DBD::mysql
42
+ ```
43
+
21
44
  ### Linux
22
45
 
23
46
  #### Ubuntu/Debian based
@@ -59,10 +82,52 @@ All the `ALTER TABLE` statements will be executed with
59
82
  `pt-online-schema-change`, which will provide additional output to the
60
83
  migration.
61
84
 
85
+ ### pt-online-schema-change arguments
86
+
87
+ #### with environment variable
88
+
89
+ You can specify any `pt-online-schema-change` arguments when running the
90
+ migration. All what you pass in the PERCONA_ARGS env var, will be bypassed to the
91
+ binary, overwriting any default values. Note the format is the same as in
92
+ `pt-online-schema-change`. Check the full list in [Percona Toolkit
93
+ documentation](https://www.percona.com/doc/percona-toolkit/2.2/pt-online-schema-change.html#options)
94
+
95
+ ```ruby
96
+ $ PERCONA_ARGS='--chunk-time=1' bundle exec rake db:migrate:up VERSION=xxx
97
+ ```
98
+
99
+ or even mulitple arguments
100
+
101
+ ```ruby
102
+ $ PERCONA_ARGS='--chunk-time=1 --critical-load=55' bundle exec rake db:migrate:up VERSION=xxx
103
+ ```
104
+
105
+ This however, only works for `db:migrate:up` or `db:migrate:down` rake tasks and
106
+ not with `db:migrate`. The settings you provide can't be generalized as these
107
+ vary depending on the database table and the kind of changes you apply.
108
+
109
+ #### with global configuration
110
+
111
+ You can specify any `pt-online-schema-change` arguments in global gem configuration
112
+ using `global_percona_args` option.
113
+
114
+ ```ruby
115
+ PerconaMigrator.configure do |config|
116
+ config.global_percona_args = '--chunk-time=1 --critical-load=55'
117
+ end
118
+ ```
119
+
120
+ Unlike using `PERCONA_ARGS`, options provided with global configuration will be applied
121
+ every time sql command is executed via `pt-online-schema-change`.
122
+
123
+ Arguments provided in global configuration can be overwritten with `PERCONA_ARGS` env variable.
124
+
125
+ We recommend using this option with caution and only when you understand the consequences.
126
+
62
127
  ### LHM support
63
128
 
64
129
  If you moved to Soundcloud's [Lhm](https://github.com/soundcloud/lhm) already,
65
- we got you covered. Percona Migrator overrides Lhm's DSL so that all the alter
130
+ we got you covered. Departure overrides Lhm's DSL so that all the alter
66
131
  statements also go through `pt-online-schema-change` as well.
67
132
 
68
133
  You can keep your Lhm migrations and start using Rails migration's DSL back
@@ -83,7 +148,7 @@ It's strongly recommended to name it after this gems name, such as
83
148
 
84
149
  ## How it works
85
150
 
86
- When booting your Rails app, Percona Migrator extends the
151
+ When booting your Rails app, Departure extends the
87
152
  `ActiveRecord::Migration#migrate` method to reset the connection and reestablish
88
153
  it using the `PerconaAdapter` instead of the one you defined in your
89
154
  `config/database.yml`.
data/bin/rspec ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rspec' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rspec-core", "rspec")
@@ -13,9 +13,13 @@ module ActiveRecord
13
13
 
14
14
  config[:username] = 'root' if config[:username].nil?
15
15
 
16
+ connection_details = PerconaMigrator::ConnectionDetails.new(config)
16
17
  verbose = ActiveRecord::Migration.verbose
17
- percona_logger = PerconaMigrator::LoggerFactory.build(verbose: verbose)
18
- cli_generator = PerconaMigrator::CliGenerator.new(config)
18
+ sanitizers = [
19
+ PerconaMigrator::LogSanitizers::PasswordSanitizer.new(connection_details)
20
+ ]
21
+ percona_logger = PerconaMigrator::LoggerFactory.build(sanitizers: sanitizers, verbose: verbose)
22
+ cli_generator = PerconaMigrator::CliGenerator.new(connection_details)
19
23
 
20
24
  runner = PerconaMigrator::Runner.new(
21
25
  percona_logger,
@@ -1,54 +1,36 @@
1
+ require 'percona_migrator/dsn'
2
+ require 'percona_migrator/option'
1
3
  require 'percona_migrator/alter_argument'
4
+ require 'percona_migrator/connection_details'
5
+ require 'percona_migrator/user_options'
2
6
 
3
7
  module PerconaMigrator
4
8
 
5
- # Represents the 'DSN' argument of Percona's pt-online-schema-change
6
- # See https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
7
- class DSN
8
-
9
- # Constructor
10
- #
11
- # @param database [String, Symbol]
12
- # @param table_name [String, Symbol]
13
- def initialize(database, table_name)
14
- @database = database
15
- @table_name = table_name
16
- end
17
-
18
- # Returns the pt-online-schema-change DSN string. See
19
- # https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
20
- def to_s
21
- "D=#{database},t=#{table_name}"
22
- end
23
-
24
- private
25
-
26
- attr_reader :table_name, :database
27
- end
28
-
29
9
  # Generates the equivalent Percona's pt-online-schema-change command to the
30
10
  # given SQL statement
31
11
  #
32
12
  # --no-check-alter is used to allow running CHANGE COLUMN statements. For
33
13
  # more details, check: www.percona.com/doc/percona-toolkit/2.2/pt-online-schema-change.html#cmdoption-pt-online-schema-change--[no]check-alter
34
14
  #
35
- class CliGenerator # Command
36
- BASE_COMMAND = 'pt-online-schema-change'
37
- BASE_OPTIONS = %w(
38
- --execute
39
- --statistics
40
- --recursion-method=none
41
- --alter-foreign-keys-method=auto
42
- --no-check-alter
43
- )
44
-
45
- # Constructor
15
+ class CliGenerator
16
+ COMMAND_NAME = 'pt-online-schema-change'.freeze
17
+ DEFAULT_OPTIONS = Set.new(
18
+ [
19
+ Option.new('execute'),
20
+ Option.new('statistics'),
21
+ Option.new('alter-foreign-keys-method', 'auto'),
22
+ Option.new('no-check-alter')
23
+ ]
24
+ ).freeze
25
+
26
+ # TODO: Better doc.
27
+ #
28
+ # Constructor. Specify any arguments to pass to pt-online-schema-change
29
+ # passing the PERCONA_ARGS env var when executing the migration
46
30
  #
47
31
  # @param connection_data [Hash]
48
- def initialize(connection_data)
49
- @connection_data = connection_data
50
- init_base_command
51
- add_connection_details
32
+ def initialize(connection_details)
33
+ @connection_details = connection_details
52
34
  end
53
35
 
54
36
  # Generates the percona command. Fills all the connection credentials from
@@ -62,9 +44,9 @@ module PerconaMigrator
62
44
  # @return [String]
63
45
  def generate(table_name, statement)
64
46
  alter_argument = AlterArgument.new(statement)
65
- dsn = DSN.new(database, table_name)
47
+ dsn = DSN.new(connection_details.database, table_name)
66
48
 
67
- "#{self} #{dsn} #{alter_argument}"
49
+ "#{command} #{all_options} #{dsn} #{alter_argument}"
68
50
  end
69
51
 
70
52
  # Generates the percona command for a raw MySQL statement. Fills all the
@@ -77,63 +59,27 @@ module PerconaMigrator
77
59
  # @return [String]
78
60
  def parse_statement(statement)
79
61
  alter_argument = AlterArgument.new(statement)
80
- dsn = DSN.new(database, alter_argument.table_name)
62
+ dsn = DSN.new(connection_details.database, alter_argument.table_name)
81
63
 
82
- "#{self} #{dsn} #{alter_argument}"
64
+ "#{command} #{all_options} #{dsn} #{alter_argument}"
83
65
  end
84
66
 
85
67
  private
86
68
 
87
- attr_reader :connection_data
88
-
89
- # Sets up the command with its options
90
- def init_base_command
91
- @command = [BASE_COMMAND, BASE_OPTIONS.join(' ')]
92
- end
93
-
94
- # Adds the host, user and password, if present, to the command
95
- def add_connection_details
96
- @command.push("-h #{host}")
97
- @command.push("-u #{user}")
98
- @command.push("-p #{password}") if password.present?
99
- end
100
-
101
- # Returns the command as a string that can be executed in a shell
102
- #
103
- # @return [String]
104
- def to_s
105
- @command.join(' ')
106
- end
107
-
108
- # Returns the database host name, defaulting to localhost. If PERCONA_DB_HOST
109
- # is passed its value will be used instead
110
- #
111
- # @return [String]
112
- def host
113
- ENV['PERCONA_DB_HOST'] || connection_data[:host] || 'localhost'
114
- end
69
+ attr_reader :connection_details
115
70
 
116
- # Returns the database user. If PERCONA_DB_USER is passed its value will be
117
- # used instead
118
- #
119
- # @return [String]
120
- def user
121
- ENV['PERCONA_DB_USER'] || connection_data[:username]
71
+ def command
72
+ "#{COMMAND_NAME} #{connection_details}"
122
73
  end
123
74
 
124
- # Returns the database user's password. If PERCONA_DB_PASSWORD is passed its
125
- # value will be used instead
75
+ # Returns all the arguments to execute pt-online-schema-change with
126
76
  #
127
77
  # @return [String]
128
- def password
129
- ENV['PERCONA_DB_PASSWORD'] || connection_data[:password]
130
- end
131
-
132
- # TODO: Doesn't the abstract adapter already handle this somehow?
133
- # Returns the database name. If PERCONA_DB_NAME is passed its value will be
134
- # used instead
135
- def database
136
- ENV['PERCONA_DB_NAME'] || connection_data[:database]
78
+ def all_options
79
+ env_variable_options = UserOptions.new
80
+ global_configuration_options = UserOptions.new(PerconaMigrator.configuration.global_percona_args)
81
+ options = env_variable_options.merge(global_configuration_options).merge(DEFAULT_OPTIONS)
82
+ options.to_a.join(' ')
137
83
  end
138
84
  end
139
85
  end
@@ -0,0 +1,102 @@
1
+ module PerconaMigrator
2
+ # Executes the given command returning it's status and errors
3
+ class Command
4
+ COMMAND_NOT_FOUND = 127
5
+
6
+ # Constructor
7
+ #
8
+ # @param command_line [String]
9
+ # @param error_log_path [String]
10
+ # @param logger [#write_no_newline]
11
+ def initialize(command_line, error_log_path, logger)
12
+ @command_line = command_line
13
+ @error_log_path = error_log_path
14
+ @logger = logger
15
+ end
16
+
17
+ # Executes the command returning its status. It also prints its stdout to
18
+ # the logger and its stderr to the file specified in error_log_path.
19
+ #
20
+ # @raise [NoStatusError] if the spawned process' status can't be retrieved
21
+ # @raise [SignalError] if the spawned process received a signal
22
+ # @raise [CommandNotFoundError] if pt-online-schema-change can't be found
23
+ #
24
+ # @return [Process::Status]
25
+ def run
26
+ log_deprecations
27
+ log_started
28
+
29
+ run_in_process
30
+
31
+ log_finished
32
+
33
+ validate_status!
34
+ status
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :command_line, :error_log_path, :logger, :status
40
+
41
+ # Runs the command in a separate process, capturing its stdout and
42
+ # execution status
43
+ def run_in_process
44
+ Open3.popen3(full_command) do |_stdin, stdout, _stderr, waith_thr|
45
+ begin
46
+ loop do
47
+ IO.select([stdout])
48
+ data = stdout.read_nonblock(8)
49
+ logger.write_no_newline(data)
50
+ end
51
+ rescue EOFError
52
+ # noop
53
+ ensure
54
+ @status = waith_thr.value
55
+ end
56
+ end
57
+ end
58
+
59
+ # Builds the actual command including stderr redirection to the specified
60
+ # log file
61
+ #
62
+ # @return [String]
63
+ def full_command
64
+ "#{command_line} 2> #{error_log_path}"
65
+ end
66
+
67
+ # Validates the status of the execution
68
+ #
69
+ # @raise [NoStatusError] if the spawned process' status can't be retrieved
70
+ # @raise [SignalError] if the spawned process received a signal
71
+ # @raise [CommandNotFoundError] if pt-online-schema-change can't be found
72
+ def validate_status!
73
+ raise SignalError.new(status) if status.signaled?
74
+ raise CommandNotFoundError if status.exitstatus == COMMAND_NOT_FOUND
75
+ raise Error, error_message unless status.success?
76
+ end
77
+
78
+ # Returns the error message that appeared in the process' stderr
79
+ #
80
+ # @return [String]
81
+ def error_message
82
+ File.read(error_log_path)
83
+ end
84
+
85
+ # Logs when the execution started
86
+ def log_started
87
+ logger.write("\n")
88
+ logger.say("Running #{command_line}\n\n", true)
89
+ end
90
+
91
+ # Prints a line break to keep the logs separate from the execution time
92
+ # print by the migration
93
+ def log_finished
94
+ logger.write("\n")
95
+ end
96
+
97
+ def log_deprecations
98
+ logger.write("\n")
99
+ logger.write("[DEPRECATION] This gem has been renamed to Departure and will no longer be supported. Please switch to Departure as soon as possible.")
100
+ end
101
+ end
102
+ end
@@ -1,10 +1,11 @@
1
1
  module PerconaMigrator
2
2
  class Configuration
3
- attr_accessor :tmp_path
3
+ attr_accessor :tmp_path, :global_percona_args
4
4
 
5
5
  def initialize
6
6
  @tmp_path = '.'.freeze
7
7
  @error_log_filename = 'percona_migrator_error.log'.freeze
8
+ @global_percona_args = nil
8
9
  end
9
10
 
10
11
  def error_log_path
@@ -0,0 +1,70 @@
1
+ module PerconaMigrator
2
+ # Holds the parameters of the DB connection and formats them to string
3
+ class ConnectionDetails
4
+
5
+ # Constructor
6
+ #
7
+ # @param [Hash] connection parametes as used in #establish_conneciton
8
+ def initialize(connection_data)
9
+ @connection_data = connection_data
10
+ end
11
+
12
+ # Returns the details formatted as an string to be used with
13
+ # pt-online-schema-change. It follows the mysql client's format.
14
+ #
15
+ # @return [String]
16
+ def to_s
17
+ @to_s ||= "-h #{host} -u #{user} #{password_argument}"
18
+ end
19
+
20
+ # TODO: Doesn't the abstract adapter already handle this somehow?
21
+ # Returns the database name. If PERCONA_DB_NAME is passed its value will be
22
+ # used instead
23
+ #
24
+ # Returns the database name
25
+ #
26
+ # @return [String]
27
+ def database
28
+ ENV.fetch('PERCONA_DB_NAME', connection_data[:database])
29
+ end
30
+
31
+ # Returns the password fragment of the details string if a password is passed
32
+ #
33
+ # @return [String]
34
+ def password_argument
35
+ if password.present?
36
+ "-p #{password}"
37
+ else
38
+ ''
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :connection_data
45
+
46
+ # Returns the database host name, defaulting to localhost. If PERCONA_DB_HOST
47
+ # is passed its value will be used instead
48
+ #
49
+ # @return [String]
50
+ def host
51
+ ENV.fetch('PERCONA_DB_HOST', connection_data[:host]) || 'localhost'
52
+ end
53
+
54
+ # Returns the database user. If PERCONA_DB_USER is passed its value will be
55
+ # used instead
56
+ #
57
+ # @return [String]
58
+ def user
59
+ ENV.fetch('PERCONA_DB_USER', connection_data[:username])
60
+ end
61
+
62
+ # Returns the database user's password. If PERCONA_DB_PASSWORD is passed its
63
+ # value will be used instead
64
+ #
65
+ # @return [String]
66
+ def password
67
+ ENV.fetch('PERCONA_DB_PASSWORD', connection_data[:password])
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,26 @@
1
+ module PerconaMigrator
2
+
3
+ # Represents the 'DSN' argument of Percona's pt-online-schema-change
4
+ # See https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
5
+ class DSN
6
+
7
+ # Constructor
8
+ #
9
+ # @param database [String, Symbol]
10
+ # @param table_name [String, Symbol]
11
+ def initialize(database, table_name)
12
+ @database = database
13
+ @table_name = table_name
14
+ end
15
+
16
+ # Returns the pt-online-schema-change DSN string. See
17
+ # https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
18
+ def to_s
19
+ "D=#{database},t=#{table_name}"
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :table_name, :database
25
+ end
26
+ end
@@ -32,4 +32,8 @@ module PerconaMigrator
32
32
  'Please install pt-online-schema-change. Check: https://www.percona.com/doc/percona-toolkit for further details'
33
33
  end
34
34
  end
35
+
36
+ # Used to prevent running the db:migrate rake task when providing arguments
37
+ # through PERCONA_ARGS env var
38
+ class ArgumentsNotSupported < Error; end
35
39
  end
@@ -0,0 +1,21 @@
1
+ module PerconaMigrator
2
+ module LogSanitizers
3
+ class PasswordSanitizer
4
+ PASSWORD_REPLACEMENT = '[filtered_password]'
5
+
6
+ delegate :password_argument, to: :connection_details
7
+
8
+ def initialize(connection_details)
9
+ @connection_details = connection_details
10
+ end
11
+
12
+ def execute(log_statement)
13
+ return log_statement if password_argument.blank?
14
+ log_statement.gsub(password_argument, PASSWORD_REPLACEMENT)
15
+ end
16
+
17
+ private
18
+ attr_accessor :connection_details
19
+ end
20
+ end
21
+ end
@@ -5,6 +5,10 @@ module PerconaMigrator
5
5
  # seen from the connection adapter.
6
6
  class Logger
7
7
 
8
+ def initialize(sanitizers)
9
+ @sanitizers = sanitizers
10
+ end
11
+
8
12
  # Outputs the message through the stdout, following the
9
13
  # ActiveRecord::Migration log format
10
14
  #
@@ -18,14 +22,22 @@ module PerconaMigrator
18
22
  #
19
23
  # @param text [String]
20
24
  def write(text = '')
21
- puts(text)
25
+ puts(sanitize(text))
22
26
  end
23
27
 
24
28
  # Outputs the text through the stdout without adding a new line at the end
25
29
  #
26
30
  # @param text [String]
27
31
  def write_no_newline(text)
28
- print(text)
32
+ print(sanitize(text))
33
+ end
34
+
35
+ private
36
+
37
+ attr_accessor :sanitizers
38
+
39
+ def sanitize(text)
40
+ sanitizers.inject(text) { |memo, sanitizer| sanitizer.execute(memo) }
29
41
  end
30
42
  end
31
43
  end
@@ -6,9 +6,9 @@ module PerconaMigrator
6
6
  #
7
7
  # @param verbose [Boolean]
8
8
  # @return [#say, #write]
9
- def self.build(verbose: true)
9
+ def self.build(sanitizers: [], verbose: true)
10
10
  if verbose
11
- PerconaMigrator::Logger.new
11
+ PerconaMigrator::Logger.new(sanitizers)
12
12
  else
13
13
  PerconaMigrator::NullLogger.new
14
14
  end
@@ -0,0 +1,63 @@
1
+ module PerconaMigrator
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
+ # Note the string must conform to "--<arg>=<value>" format.
7
+ #
8
+ # @param string [String]
9
+ # @return [Option]
10
+ def self.from_string(string)
11
+ pair = string.split('=')
12
+ name = pair[0][2..-1]
13
+ value = pair[1]
14
+
15
+ new(name, value)
16
+ end
17
+
18
+ # Constructor
19
+ #
20
+ # @param name [String]
21
+ # @param optional value [String]
22
+ def initialize(name, value = nil)
23
+ @name = name
24
+ @value = value
25
+ end
26
+
27
+ # Compares two options
28
+ #
29
+ # @param [Option]
30
+ # @return [Boolean]
31
+ def ==(another_option)
32
+ name == another_option.name
33
+ end
34
+ alias :eql? :==
35
+
36
+ # Returns the option's hash
37
+ #
38
+ # @return [Fixnum]
39
+ def hash
40
+ name.hash
41
+ end
42
+
43
+ # Returns the option as string following the "--<name>=<value>" format
44
+ #
45
+ # @return [String]
46
+ def to_s
47
+ "--#{name}#{value_as_string}"
48
+ end
49
+
50
+ private
51
+
52
+ # Returns the value fragment of the option string if any value is specified
53
+ #
54
+ # @return [String]
55
+ def value_as_string
56
+ if value.nil?
57
+ ''
58
+ else
59
+ "=#{value}"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -5,7 +5,6 @@ module PerconaMigrator
5
5
  # It executes pt-online-schema-change commands in a new process and gets its
6
6
  # output and status
7
7
  class Runner
8
- COMMAND_NOT_FOUND = 127
9
8
 
10
9
  # Constructor
11
10
  #
@@ -17,8 +16,7 @@ module PerconaMigrator
17
16
  @logger = logger
18
17
  @cli_generator = cli_generator
19
18
  @mysql_adapter = mysql_adapter
20
- @status = nil
21
- @config = config
19
+ @error_log_path = config.error_log_path
22
20
  end
23
21
 
24
22
  # Executes the passed sql statement using pt-online-schema-change for ALTER
@@ -27,8 +25,8 @@ module PerconaMigrator
27
25
  # @param sql [String]
28
26
  def query(sql)
29
27
  if alter_statement?(sql)
30
- command = cli_generator.parse_statement(sql)
31
- execute(command)
28
+ command_line = cli_generator.parse_statement(sql)
29
+ execute(command_line)
32
30
  else
33
31
  mysql_adapter.execute(sql)
34
32
  end
@@ -45,18 +43,15 @@ module PerconaMigrator
45
43
  # TODO: rename it so we don't confuse it with AR's #execute
46
44
  # Runs and logs the given command
47
45
  #
48
- # @param command [String]
46
+ # @param command_line [String]
49
47
  # @return [Boolean]
50
- def execute(command)
51
- @command = command
52
- logging { run_command }
53
- validate_status
54
- status
48
+ def execute(command_line)
49
+ Command.new(command_line, error_log_path, logger).run
55
50
  end
56
51
 
57
52
  private
58
53
 
59
- attr_reader :command, :logger, :status, :cli_generator, :mysql_adapter, :config
54
+ attr_reader :logger, :cli_generator, :mysql_adapter, :error_log_path
60
55
 
61
56
  # Checks whether the sql statement is an ALTER TABLE
62
57
  #
@@ -65,66 +60,5 @@ module PerconaMigrator
65
60
  def alter_statement?(sql)
66
61
  sql =~ /\Aalter table/i
67
62
  end
68
-
69
- # Logs the start and end of the execution
70
- #
71
- # @yield
72
- def logging
73
- log_started
74
- yield
75
- log_finished
76
- end
77
-
78
- # Logs when the execution started
79
- def log_started
80
- logger.write("\n")
81
- logger.say("Running #{command}\n\n", true)
82
- end
83
-
84
- # Executes the command and prints its output to the stdout
85
- def run_command
86
- Open3.popen3("#{command} 2> #{error_log_path}") do |_stdin, stdout, _stderr, waith_thr|
87
- begin
88
- loop do
89
- IO.select([stdout])
90
- data = stdout.read_nonblock(8)
91
- logger.write_no_newline(data)
92
- end
93
- rescue EOFError
94
- # noop
95
- ensure
96
- @status = waith_thr.value
97
- end
98
- end
99
- end
100
-
101
- # Validates the status of the execution
102
- #
103
- # @raise [NoStatusError] if the spawned process' status can't be retrieved
104
- # @raise [SignalError] if the spawned process received a signal
105
- # @raise [CommandNotFoundError] if pt-online-schema-change can't be found
106
- def validate_status
107
- raise SignalError.new(status) if status.signaled?
108
- raise CommandNotFoundError if status.exitstatus == COMMAND_NOT_FOUND
109
- raise Error, error_message unless status.success?
110
- end
111
-
112
- # Prints a line break to keep the logs separate from the execution time
113
- # print by the migration
114
- def log_finished
115
- logger.write("\n")
116
- end
117
-
118
- # The path where the percona toolkit stderr will be written
119
- #
120
- # @return [String]
121
- def error_log_path
122
- config.error_log_path
123
- end
124
-
125
- # @return [String]
126
- def error_message
127
- File.read(error_log_path)
128
- end
129
63
  end
130
64
  end
@@ -0,0 +1,44 @@
1
+ module PerconaMigrator
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(' ').map do |argument|
40
+ Option.from_string(argument)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,3 +1,3 @@
1
1
  module PerconaMigrator
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end
@@ -2,6 +2,7 @@ require 'active_record'
2
2
  require 'active_support/all'
3
3
 
4
4
  require 'percona_migrator/version'
5
+ require 'percona_migrator/log_sanitizers/password_sanitizer'
5
6
  require 'percona_migrator/runner'
6
7
  require 'percona_migrator/cli_generator'
7
8
  require 'percona_migrator/logger'
@@ -9,6 +10,7 @@ require 'percona_migrator/null_logger'
9
10
  require 'percona_migrator/logger_factory'
10
11
  require 'percona_migrator/configuration'
11
12
  require 'percona_migrator/errors'
13
+ require 'percona_migrator/command'
12
14
 
13
15
  require 'percona_migrator/railtie' if defined?(Rails)
14
16
 
@@ -28,6 +30,21 @@ module PerconaMigrator
28
30
  # Hooks Percona Migrator into Rails migrations by replacing the configured
29
31
  # database adapter
30
32
  def self.load
33
+ ActiveRecord::Migrator.instance_eval do
34
+ class << self
35
+ alias_method(:original_migrate, :migrate)
36
+ end
37
+
38
+ # Checks whether arguments are being passed through PERCONA_ARGS when running
39
+ # the db:migrate rake task
40
+ #
41
+ # @raise [ArgumentsNotSupported] if PERCONA_ARGS has any value
42
+ def migrate(migrations_paths, target_version = nil, &block)
43
+ raise ArgumentsNotSupported if ENV['PERCONA_ARGS'].present?
44
+ original_migrate(migrations_paths, target_version, &block)
45
+ end
46
+ end
47
+
31
48
  ActiveRecord::Migration.class_eval do
32
49
  alias_method :original_migrate, :migrate
33
50
 
@@ -55,10 +72,9 @@ module PerconaMigrator
55
72
  # Make all connections in the connection pool to use PerconaAdapter
56
73
  # instead of the current adapter.
57
74
  def reconnect_with_percona
58
- connection_config = ActiveRecord::Base.connection_config
59
- ActiveRecord::Base.establish_connection(
60
- connection_config.merge(adapter: 'percona')
61
- )
75
+ connection_config = ActiveRecord::Base
76
+ .connection_config.merge(adapter: 'percona')
77
+ ActiveRecord::Base.establish_connection(connection_config)
62
78
  end
63
79
  end
64
80
  end
@@ -7,14 +7,20 @@ require 'percona_migrator/version'
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'percona_migrator'
9
9
  spec.version = PerconaMigrator::VERSION
10
- spec.authors = ['Ilya Zayats', 'Pau Pérez', 'Fran Casas', 'Jorge Morante']
11
- spec.email = ['ilya.zayats@redbooth.com', 'pau.perez@redbooth.com', 'fran.casas@redbooth.com', 'jorge.morante@redbooth.com']
10
+ spec.authors = ['Ilya Zayats', 'Pau Pérez', 'Fran Casas', 'Jorge Morante', 'Adrian Serafin']
11
+ spec.email = ['ilya.zayats@redbooth.com', 'pau.perez@redbooth.com', 'fran.casas@redbooth.com', 'jorge.morante@redbooth.com', 'adrian@softmad.pl']
12
12
 
13
13
  spec.summary = %q{pt-online-schema-change runner for ActiveRecord migrations}
14
14
  spec.description = %q{Execute your ActiveRecord migrations with Percona's pt-online-schema-change}
15
15
  spec.homepage = 'http://github.com/redbooth/percona_migrator'
16
16
  spec.license = 'MIT'
17
17
 
18
+ spec.post_install_message = <<-MESSAGE
19
+ ! The Percona_migrator gem has been deprecated and has been replaced by Departure.
20
+ ! See: https://rubygems.org/gems/departure
21
+ ! And: https://github.com/redbooth/departure
22
+ MESSAGE
23
+
18
24
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
25
  spec.require_paths = ['lib']
20
26
 
@@ -26,4 +32,5 @@ Gem::Specification.new do |spec|
26
32
  spec.add_development_dependency 'rspec', '~> 3.4', '>= 3.4.0'
27
33
  spec.add_development_dependency 'rspec-its', '~> 1.2'
28
34
  spec.add_development_dependency 'byebug', '~> 8.2', '>= 8.2.1'
35
+ spec.add_development_dependency 'climate_control', '~> 0.0.3'
29
36
  end
metadata CHANGED
@@ -1,17 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: percona_migrator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ilya Zayats
8
8
  - Pau Pérez
9
9
  - Fran Casas
10
10
  - Jorge Morante
11
+ - Adrian Serafin
11
12
  autorequire:
12
13
  bindir: bin
13
14
  cert_chain: []
14
- date: 2016-11-30 00:00:00.000000000 Z
15
+ date: 2017-04-07 00:00:00.000000000 Z
15
16
  dependencies:
16
17
  - !ruby/object:Gem::Dependency
17
18
  name: rails
@@ -129,12 +130,27 @@ dependencies:
129
130
  - - ">="
130
131
  - !ruby/object:Gem::Version
131
132
  version: 8.2.1
133
+ - !ruby/object:Gem::Dependency
134
+ name: climate_control
135
+ requirement: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: 0.0.3
140
+ type: :development
141
+ prerelease: false
142
+ version_requirements: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: 0.0.3
132
147
  description: Execute your ActiveRecord migrations with Percona's pt-online-schema-change
133
148
  email:
134
149
  - ilya.zayats@redbooth.com
135
150
  - pau.perez@redbooth.com
136
151
  - fran.casas@redbooth.com
137
152
  - jorge.morante@redbooth.com
153
+ - adrian@softmad.pl
138
154
  executables: []
139
155
  extensions: []
140
156
  extra_rdoc_files: []
@@ -150,6 +166,7 @@ files:
150
166
  - RELEASING.md
151
167
  - Rakefile
152
168
  - bin/console
169
+ - bin/rspec
153
170
  - bin/setup
154
171
  - config.yml
155
172
  - configuration.rb
@@ -161,13 +178,19 @@ files:
161
178
  - lib/percona_migrator.rb
162
179
  - lib/percona_migrator/alter_argument.rb
163
180
  - lib/percona_migrator/cli_generator.rb
181
+ - lib/percona_migrator/command.rb
164
182
  - lib/percona_migrator/configuration.rb
183
+ - lib/percona_migrator/connection_details.rb
184
+ - lib/percona_migrator/dsn.rb
165
185
  - lib/percona_migrator/errors.rb
186
+ - lib/percona_migrator/log_sanitizers/password_sanitizer.rb
166
187
  - lib/percona_migrator/logger.rb
167
188
  - lib/percona_migrator/logger_factory.rb
168
189
  - lib/percona_migrator/null_logger.rb
190
+ - lib/percona_migrator/option.rb
169
191
  - lib/percona_migrator/railtie.rb
170
192
  - lib/percona_migrator/runner.rb
193
+ - lib/percona_migrator/user_options.rb
171
194
  - lib/percona_migrator/version.rb
172
195
  - percona_migrator.gemspec
173
196
  - test_database.rb
@@ -175,7 +198,10 @@ homepage: http://github.com/redbooth/percona_migrator
175
198
  licenses:
176
199
  - MIT
177
200
  metadata: {}
178
- post_install_message:
201
+ post_install_message: |2
202
+ ! The Percona_migrator gem has been deprecated and has been replaced by Departure.
203
+ ! See: https://rubygems.org/gems/departure
204
+ ! And: https://github.com/redbooth/departure
179
205
  rdoc_options: []
180
206
  require_paths:
181
207
  - lib
@@ -191,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
217
  version: '0'
192
218
  requirements: []
193
219
  rubyforge_project:
194
- rubygems_version: 2.4.5.1
220
+ rubygems_version: 2.6.10
195
221
  signing_key:
196
222
  specification_version: 4
197
223
  summary: pt-online-schema-change runner for ActiveRecord migrations