departure-76c9880 6.2.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +8 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +60 -0
  6. data/.travis.yml +30 -0
  7. data/CHANGELOG.md +192 -0
  8. data/CODE_OF_CONDUCT.md +50 -0
  9. data/Dockerfile +32 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +227 -0
  13. data/RELEASING.md +17 -0
  14. data/Rakefile +17 -0
  15. data/bin/console +14 -0
  16. data/bin/rspec +16 -0
  17. data/bin/setup +7 -0
  18. data/config.yml.erb +4 -0
  19. data/configuration.rb +16 -0
  20. data/departure.gemspec +35 -0
  21. data/docker-compose.yml +23 -0
  22. data/lib/active_record/connection_adapters/for_alter.rb +91 -0
  23. data/lib/active_record/connection_adapters/percona_adapter.rb +168 -0
  24. data/lib/departure.rb +43 -0
  25. data/lib/departure/alter_argument.rb +49 -0
  26. data/lib/departure/cli_generator.rb +84 -0
  27. data/lib/departure/command.rb +96 -0
  28. data/lib/departure/configuration.rb +20 -0
  29. data/lib/departure/connection_base.rb +9 -0
  30. data/lib/departure/connection_details.rb +96 -0
  31. data/lib/departure/dsn.rb +24 -0
  32. data/lib/departure/errors.rb +39 -0
  33. data/lib/departure/log_sanitizers/password_sanitizer.rb +22 -0
  34. data/lib/departure/logger.rb +42 -0
  35. data/lib/departure/logger_factory.rb +16 -0
  36. data/lib/departure/migration.rb +96 -0
  37. data/lib/departure/null_logger.rb +15 -0
  38. data/lib/departure/option.rb +75 -0
  39. data/lib/departure/railtie.rb +21 -0
  40. data/lib/departure/runner.rb +62 -0
  41. data/lib/departure/user_options.rb +44 -0
  42. data/lib/departure/version.rb +3 -0
  43. data/lib/lhm.rb +23 -0
  44. data/lib/lhm/adapter.rb +107 -0
  45. data/lib/lhm/column_with_sql.rb +96 -0
  46. data/lib/lhm/column_with_type.rb +29 -0
  47. data/test_database.rb +80 -0
  48. metadata +245 -0
@@ -0,0 +1,17 @@
1
+ # Releasing Departure
2
+
3
+ All releases come from the master branch. All other branches won't be maintained
4
+ and will receive bug fix releases only.
5
+
6
+ In order to give support to a new major Rails version, we'll branch off of
7
+ master, name it following the Rails repo convention, such as `v4.2`, and
8
+ we'll keep it open for bug fixes.
9
+
10
+ 1. Update `lib/departure/version.rb` accordingly
11
+ 2. Review the `CHANGELOG.md` and add a new section following the format
12
+ `[version] - YYYY-MM-DD`. We conform to the guidelines of
13
+ http://keepachangelog.com/
14
+ 3. Commit the changes with the message `Prepare release VERSION`
15
+ 4. Execute the release rake task as `bundle exec rake release`. It creates the
16
+ tag, builds and pushes the gem to Rubygems.
17
+ 5. Announce it! :tada:
@@ -0,0 +1,17 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ require './configuration'
5
+ require './test_database'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: :spec
10
+
11
+ namespace :db do
12
+ desc 'Create the test database'
13
+ task :create do
14
+ config = Configuration.new
15
+ TestDatabase.new(config).setup_test_database
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'percona_migrator'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
@@ -0,0 +1,16 @@
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', Pathname.new(__FILE__).realpath)
12
+
13
+ require 'rubygems'
14
+ require 'bundler/setup'
15
+
16
+ load Gem.bin_path('rspec-core', 'rspec')
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ bundle exec rake db:create
@@ -0,0 +1,4 @@
1
+ username: <%= ENV['PERCONA_DB_USER'] || 'root' %>
2
+ password: <%= ENV['PERCONA_DB_PASSWORD'] || '' %>
3
+ database: <%= ENV['PERCONA_DB_NAME'] || 'departure_test' %>
4
+ hostname: <%= ENV['PERCONA_DB_HOST'] || 'localhost' %>
@@ -0,0 +1,16 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+
4
+ class Configuration
5
+ CONFIG_PATH = 'config.yml.erb'.freeze
6
+
7
+ attr_reader :config
8
+
9
+ def initialize
10
+ @config = YAML.load(ERB.new(File.read(CONFIG_PATH)).result).freeze
11
+ end
12
+
13
+ def [](key)
14
+ config[key]
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'departure/version'
7
+
8
+ # This environment variable is set on CI to facilitate testing with multiple
9
+ # versions of Rails.
10
+ RAILS_DEPENDENCY_VERSION = ENV.fetch('RAILS_VERSION', ['>= 5.2.0', '< 6.1'])
11
+
12
+ Gem::Specification.new do |spec|
13
+ spec.name = 'departure-76c9880'
14
+ spec.version = Departure::VERSION
15
+ spec.authors = ['Ilya Zayats', 'Pau Pérez', 'Fran Casas', 'Jorge Morante', 'Enrico Stano', 'Adrian Serafin', 'Kirk Haines', 'Guillermo Iguaran']
16
+ spec.email = ['ilya.zayats@redbooth.com', 'pau.perez@redbooth.com', 'nflamel@gmail.com', 'jorge.morante@redbooth.com', 'adrian@softmad.pl', 'wyhaines@gmail.com', 'guilleiguaran@gmail.com']
17
+
18
+ spec.summary = %q(pt-online-schema-change runner for ActiveRecord migrations)
19
+ spec.description = %q(Execute your ActiveRecord migrations with Percona's pt-online-schema-change. Formerly known as Percona Migrator.)
20
+ spec.homepage = 'https://github.com/departurerb/departure'
21
+ spec.license = 'MIT'
22
+
23
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_runtime_dependency 'railties', *Array(RAILS_DEPENDENCY_VERSION)
27
+ spec.add_runtime_dependency 'activerecord', *Array(RAILS_DEPENDENCY_VERSION)
28
+ spec.add_runtime_dependency 'mysql2', '>= 0.4.0', '<= 0.5.3'
29
+
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'rspec', '~> 3.4', '>= 3.4.0'
32
+ spec.add_development_dependency 'rspec-its', '~> 1.2'
33
+ spec.add_development_dependency 'byebug', '~> 8.2', '>= 8.2.1'
34
+ spec.add_development_dependency 'climate_control', '~> 0.0.3'
35
+ end
@@ -0,0 +1,23 @@
1
+ version: '2'
2
+ services:
3
+ db:
4
+ image: mysql/mysql-server
5
+ ports:
6
+ - "3306:3306"
7
+ environment:
8
+ MYSQL_DATABASE: departure_test
9
+ MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
10
+ MYSQL_ROOT_HOST: '%'
11
+ rails:
12
+ build: .
13
+ links:
14
+ - db
15
+ depends_on:
16
+ - db
17
+ tty: true
18
+ stdin_open: true
19
+ environment:
20
+ PERCONA_DB_USER: root
21
+ PERCONA_DB_HOST: db
22
+ PERCONA_DB_PASSWORD:
23
+ PERCONA_DB_NAME: departure_test
@@ -0,0 +1,91 @@
1
+ require 'active_record/connection_adapters/mysql/schema_statements'
2
+
3
+ module ForAlterStatements
4
+ class << self
5
+ def included(_)
6
+ STDERR.puts 'Including for_alter statements'
7
+ end
8
+ end
9
+
10
+ def bulk_change_table(table_name, operations) #:nodoc:
11
+ sqls = operations.flat_map do |command, args|
12
+ table = args.shift
13
+ arguments = args
14
+
15
+ method = :"#{command}_for_alter"
16
+
17
+ raise "Unknown method called : #{method}(#{arguments.inspect})" unless respond_to?(method, true)
18
+ public_send(method, table, *arguments)
19
+ end.join(', ')
20
+
21
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
22
+ end
23
+
24
+ def change_column_for_alter(table_name, column_name, type, options = {})
25
+ column = column_for(table_name, column_name)
26
+ type ||= column.sql_type
27
+
28
+ options = {
29
+ default: column.default,
30
+ null: column.null,
31
+ comment: column.comment
32
+ }.merge(options)
33
+
34
+ td = create_table_definition(table_name)
35
+ cd = td.new_column_definition(column.name, type, options)
36
+ schema_creation.accept(ActiveRecord::ConnectionAdapters::ChangeColumnDefinition.new(cd, column.name))
37
+ end
38
+
39
+ def rename_column_for_alter(table_name, column_name, new_column_name)
40
+ column = column_for(table_name, column_name)
41
+ options = {
42
+ default: column.default,
43
+ null: column.null,
44
+ auto_increment: column.auto_increment?
45
+ }
46
+
47
+ columns_sql = "SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE #{quote(column_name)}"
48
+ current_type = exec_query(columns_sql, 'SCHEMA').first['Type']
49
+ td = create_table_definition(table_name)
50
+ cd = td.new_column_definition(new_column_name, current_type, options)
51
+ schema_creation.accept(ActiveRecord::ConnectionAdapters::ChangeColumnDefinition.new(cd, column.name))
52
+ end
53
+
54
+ def add_index_for_alter(table_name, column_name, options = {})
55
+ index_name, index_type, index_columns, _,
56
+ index_algorithm, index_using = add_index_options(table_name, column_name, options)
57
+
58
+ index_algorithm[0, 0] = ', ' if index_algorithm.present?
59
+ "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
60
+ end
61
+
62
+ def remove_index_for_alter(table_name, options = {})
63
+ index_name = index_name_for_remove(table_name, options)
64
+ "DROP INDEX #{quote_column_name(index_name)}"
65
+ end
66
+
67
+ def add_timestamps_for_alter(table_name, options = {})
68
+ [
69
+ add_column_for_alter(table_name, :created_at, :datetime, options),
70
+ add_column_for_alter(table_name, :updated_at, :datetime, options)
71
+ ]
72
+ end
73
+
74
+ def remove_timestamps_for_alter(table_name, _options = {})
75
+ [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
76
+ end
77
+
78
+ def add_column_for_alter(table_name, column_name, type, options = {})
79
+ td = create_table_definition(table_name)
80
+ cd = td.new_column_definition(column_name, type, options)
81
+ schema_creation.accept(ActiveRecord::ConnectionAdapters::AddColumnDefinition.new(cd))
82
+ end
83
+
84
+ def remove_column_for_alter(_table_name, column_name, _type = nil, _options = {})
85
+ "DROP COLUMN #{quote_column_name(column_name)}"
86
+ end
87
+
88
+ def remove_columns_for_alter(table_name, *column_names)
89
+ column_names.map { |column_name| remove_column_for_alter(table_name, column_name) }
90
+ end
91
+ end
@@ -0,0 +1,168 @@
1
+ require 'active_record/connection_adapters/abstract_mysql_adapter'
2
+ require 'active_record/connection_adapters/statement_pool'
3
+ require 'active_record/connection_adapters/mysql2_adapter'
4
+ require 'departure'
5
+ require 'forwardable'
6
+
7
+ module ActiveRecord
8
+ module ConnectionHandling
9
+ # Establishes a connection to the database that's used by all Active
10
+ # Record objects.
11
+ def percona_connection(config)
12
+ config[:username] = 'root' if config[:username].nil?
13
+ mysql2_connection = mysql2_connection(config)
14
+
15
+ connection_details = Departure::ConnectionDetails.new(config)
16
+ verbose = ActiveRecord::Migration.verbose
17
+ sanitizers = [
18
+ Departure::LogSanitizers::PasswordSanitizer.new(connection_details)
19
+ ]
20
+ percona_logger = Departure::LoggerFactory.build(sanitizers: sanitizers, verbose: verbose)
21
+ cli_generator = Departure::CliGenerator.new(connection_details)
22
+
23
+ runner = Departure::Runner.new(
24
+ percona_logger,
25
+ cli_generator,
26
+ mysql2_connection
27
+ )
28
+
29
+ connection_options = { mysql_adapter: mysql2_connection }
30
+
31
+ ConnectionAdapters::DepartureAdapter.new(
32
+ runner,
33
+ logger,
34
+ connection_options,
35
+ config
36
+ )
37
+ end
38
+ end
39
+
40
+ module ConnectionAdapters
41
+ class DepartureAdapter < AbstractMysqlAdapter
42
+ class Column < ActiveRecord::ConnectionAdapters::MySQL::Column
43
+ def adapter
44
+ DepartureAdapter
45
+ end
46
+ end
47
+
48
+ class SchemaCreation < ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation
49
+ def visit_DropForeignKey(name) # rubocop:disable Naming/MethodName
50
+ fk_name =
51
+ if name =~ /^__(.+)/
52
+ Regexp.last_match(1)
53
+ else
54
+ "_#{name}"
55
+ end
56
+
57
+ "DROP FOREIGN KEY #{fk_name}"
58
+ end
59
+ end
60
+
61
+ extend Forwardable
62
+
63
+ unless method_defined?(:change_column_for_alter)
64
+ include ForAlterStatements
65
+ end
66
+
67
+ ADAPTER_NAME = 'Percona'.freeze
68
+
69
+ def_delegators :mysql_adapter, :last_inserted_id, :each_hash, :set_field_encoding
70
+
71
+ def initialize(connection, _logger, connection_options, _config)
72
+ @mysql_adapter = connection_options[:mysql_adapter]
73
+ super
74
+ @prepared_statements = false
75
+ end
76
+
77
+ def exec_delete(sql, name, binds)
78
+ execute(to_sql(sql, binds), name)
79
+ @connection.affected_rows
80
+ end
81
+ alias exec_update exec_delete
82
+
83
+ def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) # rubocop:disable Lint/UnusedMethodArgument, Metrics/LineLength
84
+ execute(to_sql(sql, binds), name)
85
+ end
86
+
87
+ def exec_query(sql, name = 'SQL', _binds = [])
88
+ result = execute(sql, name)
89
+ ActiveRecord::Result.new(result.fields, result.to_a)
90
+ end
91
+
92
+ # Executes a SELECT query and returns an array of rows. Each row is an
93
+ # array of field values.
94
+
95
+ def select_rows(arel, name = nil, binds = [])
96
+ select_all(arel, name, binds).rows
97
+ end
98
+
99
+ # Executes a SELECT query and returns an array of record hashes with the
100
+ # column names as keys and column values as values.
101
+ def select(sql, name = nil, binds = [])
102
+ exec_query(sql, name, binds)
103
+ end
104
+
105
+ # Returns true, as this adapter supports migrations
106
+ def supports_migrations?
107
+ true
108
+ end
109
+
110
+ # rubocop:disable Metrics/ParameterLists
111
+ def new_column(field, default, type_metadata, null, table_name, default_function, collation, comment)
112
+ Column.new(field, default, type_metadata, null, table_name, default_function, collation, comment)
113
+ end
114
+ # rubocop:enable Metrics/ParameterLists
115
+
116
+ # Adds a new index to the table
117
+ #
118
+ # @param table_name [String, Symbol]
119
+ # @param column_name [String, Symbol]
120
+ # @param options [Hash] optional
121
+ def add_index(table_name, column_name, options = {})
122
+ index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
123
+ execute "ALTER TABLE #{quote_table_name(table_name)} ADD #{index_type} INDEX #{quote_column_name(index_name)} (#{index_columns})#{index_options}" # rubocop:disable Metrics/LineLength
124
+ end
125
+
126
+ # Remove the given index from the table.
127
+ #
128
+ # @param table_name [String, Symbol]
129
+ # @param options [Hash] optional
130
+ def remove_index(table_name, options = {})
131
+ index_name = index_name_for_remove(table_name, options)
132
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP INDEX #{quote_column_name(index_name)}"
133
+ end
134
+
135
+ def schema_creation
136
+ SchemaCreation.new(self)
137
+ end
138
+
139
+ def change_table(table_name, _options = {})
140
+ recorder = ActiveRecord::Migration::CommandRecorder.new(self)
141
+ yield update_table_definition(table_name, recorder)
142
+ bulk_change_table(table_name, recorder.commands)
143
+ end
144
+
145
+ # Returns the MySQL error number from the exception. The
146
+ # AbstractMysqlAdapter requires it to be implemented
147
+ def error_number(_exception); end
148
+
149
+ def full_version
150
+ if ActiveRecord::VERSION::MAJOR < 6
151
+ get_full_version
152
+ else
153
+ schema_cache.database_version.full_version_string
154
+ end
155
+ end
156
+
157
+ # This is a method defined in Rails 6.0, and we have no control over the
158
+ # naming of this method.
159
+ def get_full_version # rubocop:disable Naming/AccessorMethodName
160
+ mysql_adapter.raw_connection.server_info[:version]
161
+ end
162
+
163
+ private
164
+
165
+ attr_reader :mysql_adapter
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,43 @@
1
+ require 'active_record'
2
+ require 'active_support/all'
3
+
4
+ require 'active_record/connection_adapters/for_alter'
5
+
6
+ require 'departure/version'
7
+ require 'departure/log_sanitizers/password_sanitizer'
8
+ require 'departure/runner'
9
+ require 'departure/cli_generator'
10
+ require 'departure/logger'
11
+ require 'departure/null_logger'
12
+ require 'departure/logger_factory'
13
+ require 'departure/configuration'
14
+ require 'departure/errors'
15
+ require 'departure/command'
16
+ require 'departure/connection_base'
17
+ require 'departure/migration'
18
+
19
+ require 'departure/railtie' if defined?(Rails)
20
+
21
+ # We need the OS not to buffer the IO to see pt-osc's output while migrating
22
+ $stdout.sync = true
23
+
24
+ ActiveSupport.on_load(:active_record) do
25
+ ActiveRecord::Migration.class_eval do
26
+ include Departure::Migration
27
+ end
28
+ end
29
+
30
+ module Departure
31
+ class << self
32
+ attr_accessor :configuration
33
+ end
34
+
35
+ def self.configure
36
+ self.configuration ||= Configuration.new
37
+ yield(configuration)
38
+ end
39
+
40
+ def self.load
41
+ # No-op left for compatibility
42
+ end
43
+ end