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.
@@ -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,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,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
@@ -0,0 +1,3 @@
1
+ module Departure
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -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
+
@@ -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