departure-76c9880 6.2.0

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