departure 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +95 -0
- data/CODE_OF_CONDUCT.md +50 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +147 -0
- data/Rakefile +17 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/config.yml +3 -0
- data/configuration.rb +15 -0
- data/departure.gemspec +35 -0
- data/lib/active_record/connection_adapters/percona_adapter.rb +121 -0
- data/lib/departure.rb +65 -0
- data/lib/departure/alter_argument.rb +43 -0
- data/lib/departure/cli_generator.rb +134 -0
- data/lib/departure/configuration.rb +18 -0
- data/lib/departure/errors.rb +35 -0
- data/lib/departure/logger.rb +31 -0
- data/lib/departure/logger_factory.rb +17 -0
- data/lib/departure/null_logger.rb +15 -0
- data/lib/departure/railtie.rb +28 -0
- data/lib/departure/runner.rb +136 -0
- data/lib/departure/version.rb +3 -0
- data/lib/lhm.rb +25 -0
- data/lib/lhm/adapter.rb +109 -0
- data/lib/lhm/column_with_sql.rb +90 -0
- data/lib/lhm/column_with_type.rb +31 -0
- data/test_database.rb +52 -0
- metadata +195 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module Departure
|
2
|
+
module LoggerFactory
|
3
|
+
|
4
|
+
# Returns the appropriate logger instance for the given configuration. Use
|
5
|
+
# :verbose option to log to the stdout
|
6
|
+
#
|
7
|
+
# @param verbose [Boolean]
|
8
|
+
# @return [#say, #write]
|
9
|
+
def self.build(verbose: true)
|
10
|
+
if verbose
|
11
|
+
Departure::Logger.new
|
12
|
+
else
|
13
|
+
Departure::NullLogger.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
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
|
+
# 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
|
+
initializer 'departure.configure' do |app|
|
23
|
+
Departure.configure do |config|
|
24
|
+
config.tmp_path = app.paths['tmp'].first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Departure
|
4
|
+
|
5
|
+
# It executes pt-online-schema-change commands in a new process and gets its
|
6
|
+
# output and status
|
7
|
+
class Runner
|
8
|
+
COMMAND_NOT_FOUND = 127
|
9
|
+
|
10
|
+
# Constructor
|
11
|
+
#
|
12
|
+
# @param logger [#say, #write]
|
13
|
+
# @param cli_generator [CliGenerator]
|
14
|
+
# @param mysql_adapter [ActiveRecord::ConnectionAdapter] it must implement
|
15
|
+
# #execute and #raw_connection
|
16
|
+
def initialize(logger, cli_generator, mysql_adapter, config = Departure.configuration)
|
17
|
+
@logger = logger
|
18
|
+
@cli_generator = cli_generator
|
19
|
+
@mysql_adapter = mysql_adapter
|
20
|
+
@status = nil
|
21
|
+
@config = config
|
22
|
+
end
|
23
|
+
|
24
|
+
# Executes the passed sql statement using pt-online-schema-change for ALTER
|
25
|
+
# TABLE statements, or the specified mysql adapter otherwise.
|
26
|
+
#
|
27
|
+
# @param sql [String]
|
28
|
+
def query(sql)
|
29
|
+
if alter_statement?(sql)
|
30
|
+
command = cli_generator.parse_statement(sql)
|
31
|
+
execute(command)
|
32
|
+
else
|
33
|
+
mysql_adapter.execute(sql)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the number of rows affected by the last UPDATE, DELETE or INSERT
|
38
|
+
# statements
|
39
|
+
#
|
40
|
+
# @return [Integer]
|
41
|
+
def affected_rows
|
42
|
+
mysql_adapter.raw_connection.affected_rows
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: rename it so we don't confuse it with AR's #execute
|
46
|
+
# Runs and logs the given command
|
47
|
+
#
|
48
|
+
# @param command [String]
|
49
|
+
# @return [Boolean]
|
50
|
+
def execute(command)
|
51
|
+
@command = command
|
52
|
+
logging { run_command }
|
53
|
+
validate_status
|
54
|
+
status
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :command, :logger, :status, :cli_generator, :mysql_adapter, :config
|
60
|
+
|
61
|
+
# Checks whether the sql statement is an ALTER TABLE
|
62
|
+
#
|
63
|
+
# @param sql [String]
|
64
|
+
# @return [Boolean]
|
65
|
+
def alter_statement?(sql)
|
66
|
+
sql =~ /\Aalter table/i
|
67
|
+
end
|
68
|
+
|
69
|
+
# Logs the start and end of the execution
|
70
|
+
#
|
71
|
+
# @yield
|
72
|
+
def logging
|
73
|
+
log_deprecations
|
74
|
+
log_started
|
75
|
+
yield
|
76
|
+
log_finished
|
77
|
+
end
|
78
|
+
|
79
|
+
def log_deprecations
|
80
|
+
logger.write("\n")
|
81
|
+
logger.write("[DEPRECATION] This gem has been renamed to Departure and will no longer be supported. Please switch to Departure as soon as possible.")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Logs when the execution started
|
85
|
+
def log_started
|
86
|
+
logger.write("\n")
|
87
|
+
logger.say("Running #{command}\n\n", true)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Executes the command and prints its output to the stdout
|
91
|
+
def run_command
|
92
|
+
Open3.popen3("#{command} 2> #{error_log_path}") do |_stdin, stdout, _stderr, waith_thr|
|
93
|
+
begin
|
94
|
+
loop do
|
95
|
+
IO.select([stdout])
|
96
|
+
data = stdout.read_nonblock(8)
|
97
|
+
logger.write_no_newline(data)
|
98
|
+
end
|
99
|
+
rescue EOFError
|
100
|
+
# noop
|
101
|
+
ensure
|
102
|
+
@status = waith_thr.value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Validates the status of the execution
|
108
|
+
#
|
109
|
+
# @raise [NoStatusError] if the spawned process' status can't be retrieved
|
110
|
+
# @raise [SignalError] if the spawned process received a signal
|
111
|
+
# @raise [CommandNotFoundError] if pt-online-schema-change can't be found
|
112
|
+
def validate_status
|
113
|
+
raise SignalError.new(status) if status.signaled?
|
114
|
+
raise CommandNotFoundError if status.exitstatus == COMMAND_NOT_FOUND
|
115
|
+
raise Error, error_message unless status.success?
|
116
|
+
end
|
117
|
+
|
118
|
+
# Prints a line break to keep the logs separate from the execution time
|
119
|
+
# print by the migration
|
120
|
+
def log_finished
|
121
|
+
logger.write("\n")
|
122
|
+
end
|
123
|
+
|
124
|
+
# The path where the percona toolkit stderr will be written
|
125
|
+
#
|
126
|
+
# @return [String]
|
127
|
+
def error_log_path
|
128
|
+
config.error_log_path
|
129
|
+
end
|
130
|
+
|
131
|
+
# @return [String]
|
132
|
+
def error_message
|
133
|
+
File.read(error_log_path)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/lhm.rb
ADDED
@@ -0,0 +1,25 @@
|
|
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
|
+
|
8
|
+
# Yields an adapter instance so that Lhm migration Dsl methods get
|
9
|
+
# delegated to ActiveRecord::Migration ones instead
|
10
|
+
#
|
11
|
+
# @param table_name [String]
|
12
|
+
# @param _options [Hash]
|
13
|
+
# @param block [Block]
|
14
|
+
def self.change_table(table_name, _options = {}, &block)
|
15
|
+
yield Adapter.new(@migration, table_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sets the migration to apply the adapter to
|
19
|
+
#
|
20
|
+
# @param migration [ActiveRecord::Migration]
|
21
|
+
def self.migration=(migration)
|
22
|
+
@migration = migration
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
data/lib/lhm/adapter.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'lhm/column_with_sql'
|
2
|
+
require 'lhm/column_with_type'
|
3
|
+
|
4
|
+
module Lhm
|
5
|
+
|
6
|
+
# Translates Lhm DSL to ActiveRecord's one, so Lhm migrations will now go
|
7
|
+
# through Percona as well, without any modification on the migration's
|
8
|
+
# code
|
9
|
+
class Adapter
|
10
|
+
|
11
|
+
# Constructor
|
12
|
+
#
|
13
|
+
# @param migration [ActiveRecord::Migtration]
|
14
|
+
# @param table_name [String, Symbol]
|
15
|
+
def initialize(migration, table_name)
|
16
|
+
@migration = migration
|
17
|
+
@table_name = table_name
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds the specified column through ActiveRecord
|
21
|
+
#
|
22
|
+
# @param column_name [String, Symbol]
|
23
|
+
# @param definition [String, Symbol]
|
24
|
+
def add_column(column_name, definition)
|
25
|
+
attributes = column_attributes(column_name, definition)
|
26
|
+
migration.add_column(*attributes)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Removes the specified column through ActiveRecord
|
30
|
+
#
|
31
|
+
# @param column_name [String, Symbol]
|
32
|
+
def remove_column(column_name)
|
33
|
+
migration.remove_column(table_name, column_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adds an index in the specified columns through ActiveRecord. Note you
|
37
|
+
# can provide a name as well
|
38
|
+
#
|
39
|
+
# @param columns [Array<String>, Array<Symbol>, String, Symbol]
|
40
|
+
# @param index_name [String]
|
41
|
+
def add_index(columns, index_name = nil)
|
42
|
+
options = { name: index_name } if index_name
|
43
|
+
migration.add_index(table_name, columns, options || {})
|
44
|
+
end
|
45
|
+
|
46
|
+
# Removes the index in the given columns or by its name
|
47
|
+
#
|
48
|
+
# @param columns [Array<String>, Array<Symbol>, String, Symbol]
|
49
|
+
# @param index_name [String]
|
50
|
+
def remove_index(columns, index_name = nil)
|
51
|
+
options = if index_name
|
52
|
+
{ name: index_name }
|
53
|
+
else
|
54
|
+
{ column: columns }
|
55
|
+
end
|
56
|
+
migration.remove_index(table_name, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Change the column to use the provided definition, through ActiveRecord
|
60
|
+
#
|
61
|
+
# @param column_name [String, Symbol]
|
62
|
+
# @param definition [String, Symbol]
|
63
|
+
def change_column(column_name, definition)
|
64
|
+
attributes = column_attributes(column_name, definition)
|
65
|
+
migration.change_column(*attributes)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Renames the old_name column to new_name by using ActiveRecord
|
69
|
+
#
|
70
|
+
# @param old_name [String, Symbol]
|
71
|
+
# @param new_name [String, Symbol]
|
72
|
+
def rename_column(old_name, new_name)
|
73
|
+
migration.rename_column(table_name, old_name, new_name)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Adds a unique index on the given columns, with the provided name if passed
|
77
|
+
#
|
78
|
+
# @param columns [Array<String>, Array<Symbol>, String, Symbol]
|
79
|
+
# @param index_name [String]
|
80
|
+
def add_unique_index(columns, index_name = nil)
|
81
|
+
options = { unique: true }
|
82
|
+
options.merge!(name: index_name) if index_name
|
83
|
+
|
84
|
+
migration.add_index(table_name, columns, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
attr_reader :migration, :table_name
|
90
|
+
|
91
|
+
# Returns the instance of ActiveRecord column with the given name and
|
92
|
+
# definition
|
93
|
+
#
|
94
|
+
# @param name [String, Symbol]
|
95
|
+
# @param definition [String]
|
96
|
+
def column(name, definition)
|
97
|
+
@column ||= if definition.is_a?(Symbol)
|
98
|
+
ColumnWithType.new(name, definition)
|
99
|
+
else
|
100
|
+
ColumnWithSql.new(name, definition)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def column_attributes(name, definition)
|
105
|
+
attributes = column(name, definition).attributes
|
106
|
+
[table_name, name].concat(attributes)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Lhm
|
4
|
+
|
5
|
+
# Abstracts the details of a table column definition when specified with a MySQL
|
6
|
+
# column definition string
|
7
|
+
class ColumnWithSql
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
# Returns the column's class to be used
|
11
|
+
#
|
12
|
+
# @return [Constant]
|
13
|
+
def self.column_factory
|
14
|
+
::ActiveRecord::ConnectionAdapters::DepartureAdapter::Column
|
15
|
+
end
|
16
|
+
|
17
|
+
# Constructor
|
18
|
+
#
|
19
|
+
# @param name [String, Symbol]
|
20
|
+
# @param definition [String]
|
21
|
+
def initialize(name, definition)
|
22
|
+
@name = name
|
23
|
+
@definition = definition
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the column data as an Array to be used with the splat operator.
|
27
|
+
# See Lhm::Adaper#add_column
|
28
|
+
#
|
29
|
+
# @return [Array]
|
30
|
+
def attributes
|
31
|
+
[type, column_options]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def_delegators :column, :limit, :type, :default, :null
|
37
|
+
|
38
|
+
attr_reader :name, :definition
|
39
|
+
|
40
|
+
# TODO: investigate
|
41
|
+
#
|
42
|
+
# Rails doesn't take into account lenght argument of INT in the
|
43
|
+
# definition, as an integer it will default it to 4 not an integer
|
44
|
+
#
|
45
|
+
# Returns the columns data as a Hash
|
46
|
+
#
|
47
|
+
# @return [Hash]
|
48
|
+
def column_options
|
49
|
+
{ limit: column.limit, default: column.default, null: column.null }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the column instance with the provided data
|
53
|
+
#
|
54
|
+
# @return [column_factory]
|
55
|
+
def column
|
56
|
+
@column ||= self.class.column_factory.new(
|
57
|
+
name,
|
58
|
+
default_value,
|
59
|
+
definition,
|
60
|
+
null_value
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Gets the DEFAULT value the column takes as specified in the
|
65
|
+
# definition, if any
|
66
|
+
#
|
67
|
+
# @return [String, NilClass]
|
68
|
+
def default_value
|
69
|
+
match = if definition =~ /timestamp|datetime/i
|
70
|
+
/default '?(.+[^'])'?/i.match(definition)
|
71
|
+
else
|
72
|
+
/default '?(\w+)'?/i.match(definition)
|
73
|
+
end
|
74
|
+
|
75
|
+
return unless match
|
76
|
+
|
77
|
+
match[1].downcase != 'null' ? match[1] : nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# Checks whether the column accepts NULL as specified in the definition
|
81
|
+
#
|
82
|
+
# @return [Boolean]
|
83
|
+
def null_value
|
84
|
+
match = /((\w*) NULL)/i.match(definition)
|
85
|
+
return true unless match
|
86
|
+
|
87
|
+
match[2].downcase == 'not' ? false : true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Lhm
|
2
|
+
|
3
|
+
# Abstracts the details of a table column definition when specified with a type
|
4
|
+
# as a symbol. This is the regular ActiveRecord's #add_column syntax:
|
5
|
+
#
|
6
|
+
# add_column :tablenames, :field, :string
|
7
|
+
#
|
8
|
+
class ColumnWithType
|
9
|
+
|
10
|
+
# Constructor
|
11
|
+
#
|
12
|
+
# @param name [String, Symbol]
|
13
|
+
# @param definition [Symbol]
|
14
|
+
def initialize(name, definition)
|
15
|
+
@name = name
|
16
|
+
@definition = definition
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the column data as an Array to be used with the splat operator.
|
20
|
+
# See Lhm::Adaper#add_column
|
21
|
+
#
|
22
|
+
# @return [Array]
|
23
|
+
def attributes
|
24
|
+
[definition]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :definition
|
30
|
+
end
|
31
|
+
end
|