departure 1.0.0

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