percona_migrator 0.1.0.rc.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 54dcba8208db6d98ee85f3124a8b4d725617e80d
4
+ data.tar.gz: 86b55bab745a0952886b09aa32323f78db9e8d9f
5
+ SHA512:
6
+ metadata.gz: 695c5ed29fa887f1f8df33b66ab86a8d63c6fde601e17ff91440f237ec05d62c6e4645128fa144a29dd412b1dbcfa9ef56356e5caa04833d2a88a2ff8ce20200
7
+ data.tar.gz: 36d59e8106ceb679b867e90f2d0b1598f17fa74a4eb79d33f73b0c13311ac82a7dd20c990d9a8a711969cec61e1b1de0bbe393205a6a89860569d3248f0515e7
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .byebug_history
11
+ tags
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ before_install:
5
+ - gpg --keyserver hkp://keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2A && gpg -a --export CD2EFD2A | sudo apt-key add -
6
+ - echo "deb http://repo.percona.com/apt `lsb_release -cs` main" | sudo tee -a /etc/apt/sources.list
7
+ - sudo apt-get update -qq
8
+ - sudo apt-get install percona-toolkit
9
+ - gem update bundler
10
+ - bin/setup
11
+
12
+ branches:
13
+ only:
14
+ - master
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ Please follow the format in [Keep a Changelog](http://keepachangelog.com/)
6
+
7
+ ## [Unreleased]
8
+
9
+ - Initial gem version
@@ -0,0 +1,50 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This Code of Conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at accounts@redbooth.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+
45
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
46
+ version 1.3.0, available at
47
+ [http://contributor-covenant.org/version/1/3/0/][version]
48
+
49
+ [homepage]: http://contributor-covenant.org
50
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Pau Pérez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # PerconaMigrator [![Build Status](https://travis-ci.org/redbooth/percona_migrator.svg?branch=master)](https://travis-ci.org/redbooth/percona_migrator)
2
+
3
+ Percona Migrator is a tool for running online and non-blocking
4
+ DDL `ActiveRecord::Migrations` using `pt-online-schema-change` command-line tool of
5
+ [Percona
6
+ Toolkit](https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html)
7
+ which supports foreign key constraints.
8
+
9
+ It adds a `db:percona_migrate:up` runs your migration using the
10
+ `pt-online-schema-change` command. It will apply exactly the same changes as
11
+ if you run it with `db:migrate:up` avoiding deadlocks and without the need to
12
+ change how you write regular rails migrations.
13
+
14
+ It also disables `rake db:migrate:up` for the ddl migrations on envs with
15
+ PERCONA_TOOLKIT var set to ensure all these migrations use Percona in production.
16
+
17
+ ## Installation
18
+
19
+ Percona Migrator relies on `pt-online-schema-change` from [Percona
20
+ Toolkit](https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html)
21
+
22
+ For mac, you can install it with homebrew typing `brew install percona-toolkit`. For
23
+ linux machines check out the [Percona Toolkit download
24
+ page](https://www.percona.com/downloads/percona-toolkit/) to find the package
25
+ that fits your distribution.
26
+
27
+ You can also get it from [Percona's apt repository](https://www.percona.com/doc/percona-xtradb-cluster/5.5/installation/apt_repo.html)
28
+
29
+ Once installed, add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ gem 'percona_migrator'
33
+ ```
34
+
35
+ And then execute:
36
+
37
+ $ bundle
38
+
39
+ Or install it yourself as:
40
+
41
+ $ gem install percona_migrator
42
+
43
+ ## Usage
44
+
45
+ Percona Migrator is meant to be used only on production or production-like
46
+ environments. To that end, it will only run if the `PERCONA_TOOLKIT`
47
+ environment variable is present.
48
+
49
+ From that same environment where you added the variable, execute the following:
50
+
51
+ 1. `bundle exec rake db:migrate:status` to find out your migration's version
52
+ number
53
+ 2. `rake db:percona_migrate:up VERSION=<version>`. This will run the migration
54
+ and mark it as up. Otherwise, if the migration fails, it will still be listed as down
55
+
56
+ You can also mark the migration as run manually, by executing `bundle exec rake
57
+ db:migrate:mark_as_up VERSION=<version>`. Likewise, there's a `bundle exec rake
58
+ db:migrate:mark_as_down VERSION=<version>` that may be of help.
59
+
60
+ ## Development
61
+
62
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
63
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
64
+ prompt that will allow you to experiment.
65
+
66
+ To install this gem onto your local machine, run `bundle exec rake install`. To
67
+ release a new version, update the version number in `version.rb`, and then run
68
+ `bundle exec rake release`, which will create a git tag for the version, push
69
+ git commits and tags, and push the `.gem` file to
70
+ [rubygems.org](https://rubygems.org).
71
+
72
+ ## Contributing
73
+
74
+ Bug reports and pull requests are welcome on GitHub at
75
+ https://github.com/redbooth/percona_migrator.
76
+
77
+ Please note that this project is released with a Contributor Code of Conduct. By
78
+ participating in this project you agree to abide by its terms.
79
+
80
+ Check the code of conduct [here](CODE_OF_CONDUCT.md)
81
+
82
+ ## Changelog
83
+
84
+ You can consult the changelog [here](CHANGELOG.md)
85
+
86
+ ## License
87
+
88
+ The gem is available as open source under the terms of the [MIT
89
+ License](http://opensource.org/licenses/MIT).
90
+
data/Rakefile ADDED
@@ -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).create_test_database
16
+ end
17
+ end
data/bin/console ADDED
@@ -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
data/bin/setup ADDED
@@ -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
data/config.yml ADDED
@@ -0,0 +1,3 @@
1
+ username: 'root'
2
+ password: ''
3
+ database: 'percona_migrator_test'
data/configuration.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'yaml'
2
+
3
+ class Configuration
4
+ CONFIG_PATH = 'config.yml'
5
+
6
+ attr_reader :config
7
+
8
+ def initialize
9
+ @config = YAML.load_file(CONFIG_PATH)
10
+ end
11
+
12
+ def [](key)
13
+ config[key]
14
+ end
15
+ end
@@ -0,0 +1,153 @@
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 'percona_migrator'
5
+ require 'forwardable'
6
+
7
+ module ActiveRecord
8
+ class Base
9
+ # Establishes a connection to the database that's used by all Active
10
+ # Record objects.
11
+ def self.percona_connection(config)
12
+ connection = mysql2_connection(config)
13
+ client = connection.raw_connection
14
+
15
+ # TODO: use AR's logger. It must pass a logger instance around, at least
16
+ # the one the migration uses
17
+ logger = config[:logger] || $stdout
18
+
19
+ config.merge!(
20
+ logger: logger,
21
+ runner: PerconaMigrator::Runner.new(logger),
22
+ cli_generator: PerconaMigrator::CliGenerator.new(config)
23
+ )
24
+
25
+ connection_options = { mysql_adapter: connection }
26
+
27
+ ConnectionAdapters::PerconaMigratorAdapter.new(
28
+ client,
29
+ logger,
30
+ connection_options,
31
+ config
32
+ )
33
+ end
34
+ end
35
+
36
+ module ConnectionAdapters
37
+ # It doesn't implement #create_table as this statement is harmless and
38
+ # pretty fast. No need to do it with Percona
39
+ class PerconaMigratorAdapter < AbstractMysqlAdapter
40
+
41
+ class Column < AbstractMysqlAdapter::Column
42
+ def adapter
43
+ PerconaMigratorAdapter
44
+ end
45
+ end
46
+
47
+ extend Forwardable
48
+
49
+ ADAPTER_NAME = 'Percona'.freeze
50
+
51
+ def_delegators :mysql_adapter, :tables, :select_values, :exec_delete,
52
+ :exec_insert, :exec_query, :last_inserted_id, :select
53
+
54
+ def initialize(connection, logger, connection_options, config)
55
+ super
56
+ @mysql_adapter = connection_options[:mysql_adapter]
57
+ @logger = logger
58
+ @runner = config[:runner]
59
+ @cli_generator = config[:cli_generator]
60
+ end
61
+
62
+ # Returns true, as this adapter supports migrations
63
+ def supports_migrations?
64
+ true
65
+ end
66
+
67
+ # Delegates #each_hash to the mysql adapter
68
+ #
69
+ # @param result [Mysql2::Result]
70
+ def each_hash(result)
71
+ if block_given?
72
+ mysql_adapter.each_hash(result, &Proc.new)
73
+ else
74
+ mysql_adapter.each_hash(result)
75
+ end
76
+ end
77
+
78
+ def new_column(field, default, type, null, collation)
79
+ Column.new(field, default, type, null, collation)
80
+ end
81
+
82
+ # Adds a new column to the named table
83
+ #
84
+ # @param table_name [String, Symbol]
85
+ # @param column_name [String, Symbol]
86
+ # @param type [Symbol]
87
+ # @param options [Hash] optional
88
+ def add_column(table_name, column_name, type, options = {})
89
+ super
90
+ command = cli_generator.generate(table_name, @sql)
91
+ runner.execute(command)
92
+ end
93
+
94
+ # Removes the column(s) from the table definition
95
+ #
96
+ # @param table_name [String, Symbol]
97
+ # @param column_names [String, Symbol, Array<String>, Array<Symbol>]
98
+ def remove_column(table_name, *column_names)
99
+ super
100
+ command = cli_generator.generate(table_name, @sql)
101
+ runner.execute(command)
102
+ end
103
+
104
+ # Adds a new index to the table
105
+ #
106
+ # @param table_name [String, Symbol]
107
+ # @param column_name [String, Symbol]
108
+ # @param options [Hash] optional
109
+ def add_index(table_name, column_name, options = {})
110
+ index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
111
+ execute "ADD #{index_type} INDEX #{quote_column_name(index_name)} (#{index_columns})#{index_options}"
112
+
113
+ command = cli_generator.generate(table_name, @sql)
114
+ runner.execute(command)
115
+ end
116
+
117
+ # Remove the given index from the table.
118
+ #
119
+ # @param table_name [String, Symbol]
120
+ # @param options [Hash] optional
121
+ def remove_index(table_name, options = {})
122
+ index_name = index_name_for_remove(table_name, options)
123
+ execute "DROP INDEX #{quote_column_name(index_name)}"
124
+
125
+ command = cli_generator.generate(table_name, @sql)
126
+ runner.execute(command)
127
+ end
128
+
129
+ # Records the SQL statement to be executed. This is used to then delegate
130
+ # the execution to Percona's pt-online-schema-change.
131
+ #
132
+ # @param sql [String]
133
+ # @param _name [String] optional
134
+ def execute(sql, _name = nil)
135
+ @sql = sql
136
+ true
137
+ end
138
+
139
+ # This abstract method leaves up to the connection adapter freeing the
140
+ # result, if it needs to. Check out: https://github.com/rails/rails/blob/330c6af05c8b188eb072afa56c07d5fe15767c3c/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L247
141
+ #
142
+ # @param sql [String]
143
+ # @param name [String] optional
144
+ def execute_and_free(sql, name = nil)
145
+ yield mysql_adapter.execute(sql, name)
146
+ end
147
+
148
+ private
149
+
150
+ attr_reader :mysql_adapter, :logger, :runner, :cli_generator
151
+ end
152
+ end
153
+ end
@@ -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::PerconaMigratorAdapter::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
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
+
@@ -0,0 +1,33 @@
1
+ module PerconaMigrator
2
+
3
+ # Represents the '--alter' argument of Percona's pt-online-schema-change
4
+ # See https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html
5
+ class AlterArgument
6
+
7
+ # Constructor
8
+ #
9
+ # @param statement [String]
10
+ def initialize(statement)
11
+ @statement = statement
12
+ end
13
+
14
+ # Returns the '--alter' pt-online-schema-change argumment as a string. See
15
+ # https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html
16
+ def to_s
17
+ "--alter \"#{parsed_statement}\""
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :statement
23
+
24
+ # Removes the 'ALTER TABLE' portion of the SQL statement
25
+ #
26
+ # @return [String]
27
+ def parsed_statement
28
+ @parsed_statement ||= statement
29
+ .gsub(/ALTER TABLE `(\w+)` /, '')
30
+ .gsub('`','\\\`')
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,117 @@
1
+ require 'percona_migrator/alter_argument'
2
+
3
+ module PerconaMigrator
4
+
5
+ # Represents the 'DSN' argument of Percona's pt-online-schema-change
6
+ # See https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
7
+ class DSN
8
+
9
+ # Constructor
10
+ #
11
+ # @param database [String, Symbol]
12
+ # @param table_name [String, Symbol]
13
+ def initialize(database, table_name)
14
+ @database = database
15
+ @table_name = table_name
16
+ end
17
+
18
+ # Returns the pt-online-schema-change DSN string. See
19
+ # https://www.percona.com/doc/percona-toolkit/2.0/pt-online-schema-change.html#dsn-options
20
+ def to_s
21
+ "D=#{database},t=#{table_name}"
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :table_name, :database
27
+ end
28
+
29
+ # Generates the equivalent Percona's pt-online-schema-change command to the
30
+ # given SQL statement
31
+ class CliGenerator # Command
32
+ BASE_COMMAND = 'pt-online-schema-change'
33
+ BASE_OPTIONS = %w(
34
+ --execute
35
+ --statistics
36
+ --recursion-method=none
37
+ --alter-foreign-keys-method=auto
38
+ )
39
+
40
+ # Constructor
41
+ #
42
+ # @param connection_data [Hash]
43
+ def initialize(connection_data)
44
+ @connection_data = connection_data
45
+ init_base_command
46
+ add_connection_details
47
+ end
48
+
49
+ # Generates the percona command. Fills all the connection credentials from
50
+ # the current AR connection, but that can amended via ENV-vars:
51
+ # PERCONA_DB_HOST, PERCONA_DB_USER, PERCONA_DB_PASSWORD, PERCONA_DB_NAME
52
+ # Table name can't not be amended, it populates automatically from the
53
+ # migration data
54
+ #
55
+ # @param table_name [String]
56
+ # @param statement [String] MySQL statement
57
+ # @return [String]
58
+ def generate(table_name, statement)
59
+ dsn = DSN.new(database, table_name)
60
+ alter_argument = AlterArgument.new(statement)
61
+
62
+ "#{to_s} #{dsn} #{alter_argument}"
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :connection_data
68
+
69
+ # Sets up the command with its options
70
+ def init_base_command
71
+ @command = [BASE_COMMAND, BASE_OPTIONS.join(' ')]
72
+ end
73
+
74
+ # Adds the host, user and password, if present, to the command
75
+ def add_connection_details
76
+ @command.push("-h #{host}")
77
+ @command.push("-u #{user}")
78
+ @command.push("-p #{password}") if password.present?
79
+ end
80
+
81
+ # Returns the command as a string that can be executed in a shell
82
+ def to_s
83
+ @command.join(' ')
84
+ end
85
+
86
+ # Returns the database host name, defaulting to localhost. If PERCONA_DB_HOST
87
+ # is passed its value will be used instead
88
+ #
89
+ # @return [String]
90
+ def host
91
+ ENV['PERCONA_DB_HOST'] || connection_data[:host] || 'localhost'
92
+ end
93
+
94
+ # Returns the database user. If PERCONA_DB_USER is passed its value will be
95
+ # used instead
96
+ #
97
+ # @return [String]
98
+ def user
99
+ ENV['PERCONA_DB_USER'] || connection_data[:username]
100
+ end
101
+
102
+ # Returns the database user's password. If PERCONA_DB_PASSWORD is passed its
103
+ # value will be used instead
104
+ #
105
+ # @return [String]
106
+ def password
107
+ ENV['PERCONA_DB_PASSWORD'] || connection_data[:password]
108
+ end
109
+
110
+ # TODO: Doesn't the abstract adapter already handle this somehow?
111
+ # Returns the database name. If PERCONA_DB_NAME is passed its value will be
112
+ # used instead
113
+ def database
114
+ ENV['PERCONA_DB_NAME'] || connection_data[:database]
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,44 @@
1
+ require 'percona_migrator'
2
+ require 'lhm' # It's our own Lhm adapter, not the gem
3
+ require 'rails'
4
+
5
+ module PerconaMigrator
6
+ class Railtie < Rails::Railtie
7
+ railtie_name :percona_migrator
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 'percona_migrator.configure_rails_initialization' do
17
+ ActiveSupport.on_load(:active_record) do
18
+
19
+ ActiveRecord::Migration.class_eval do
20
+ alias_method :original_migrate, :migrate
21
+
22
+ # Replaces the current connection adapter with the PerconaAdapter and
23
+ # patches LHM, then it continues with the regular migration process.
24
+ #
25
+ # @param direction [Symbol] :up or :down
26
+ def migrate(direction)
27
+ reconnect_with_percona
28
+ ::Lhm.migration = self
29
+ original_migrate(direction)
30
+ end
31
+
32
+ # Make all connections in the connection pool to use PerconaAdapter
33
+ # instead of the current adapter.
34
+ def reconnect_with_percona
35
+ connection_config = ActiveRecord::Base.connection_config
36
+ ActiveRecord::Base.establish_connection(
37
+ connection_config.merge(adapter: 'percona')
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,88 @@
1
+ require 'open3'
2
+
3
+ module PerconaMigrator
4
+
5
+ # It executes pt-online-schema-change commands in a new process and gets its
6
+ # output and status
7
+ class Runner
8
+
9
+ NONE = "\e[0m"
10
+ CYAN = "\e[38;5;86m"
11
+ GREEN = "\e[32m"
12
+ RED = "\e[31m"
13
+
14
+ # Executes the given command printing the output to the logger
15
+ #
16
+ # @param command [String]
17
+ # @param logger [IO]
18
+ def self.execute(command, logger)
19
+ new(command, logger).execute
20
+ end
21
+
22
+ # Constructor
23
+ #
24
+ # @param logger [IO]
25
+ def initialize(logger)
26
+ @logger = logger
27
+ @status = nil
28
+ end
29
+
30
+ # Runs and logs the given command
31
+ #
32
+ # @param command [String]
33
+ # @return [Boolean]
34
+ def execute(command)
35
+ @command = command
36
+
37
+ log_started
38
+ run_command
39
+ log_finished
40
+
41
+ status
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :command, :logger, :status
47
+
48
+ # TODO: log as a migration logger subitem
49
+ #
50
+ # Logs when the execution started
51
+ def log_started
52
+ logger.puts "\n#{CYAN}-- #{command}#{NONE}\n\n"
53
+ end
54
+
55
+ # Executes the command outputing any errors
56
+ #
57
+ # @raise [Errno::ENOENT] if pt-online-schema-change can't be found
58
+ def run_command
59
+ Open3.popen2(command) do |_stdin, stdout, process|
60
+ @status = process.value
61
+ logger.puts stdout.read
62
+ end
63
+
64
+ if status.nil?
65
+ Kernel.warn("Error running '#{command}': status could not be retrieved")
66
+ end
67
+
68
+ if status && status.signaled?
69
+ Kernel.warn("Error running '#{command}': #{status.to_s}")
70
+ end
71
+
72
+ rescue Errno::ENOENT
73
+ raise Errno::ENOENT, "Please install pt-online-schema-change. Check: https://www.percona.com/doc/percona-toolkit"
74
+ end
75
+
76
+ # Logs the status of the execution once it's finished
77
+ def log_finished
78
+ return unless status
79
+
80
+ value = status.exitstatus
81
+ return unless value
82
+
83
+ message = value.zero? ? "#{GREEN}Done!#{NONE}" : "#{RED}Failed!#{NONE}"
84
+
85
+ logger.puts("\n#{message}")
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,3 @@
1
+ module PerconaMigrator
2
+ VERSION = '0.1.0.rc.1'.freeze
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_record'
2
+ require 'active_support/all'
3
+
4
+ require 'percona_migrator/version'
5
+ require 'percona_migrator/runner'
6
+ require 'percona_migrator/cli_generator'
7
+
8
+ require 'percona_migrator/railtie' if defined?(Rails)
9
+
10
+ module PerconaMigrator
11
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'percona_migrator/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'percona_migrator'
9
+ spec.version = PerconaMigrator::VERSION
10
+ spec.authors = ['Ilya Zayats', 'Pau Pérez', 'Fran Casas']
11
+ spec.email = ['ilya.zayats@redbooth.com', 'pau.perez@redbooth.com', 'fran.casas@redbooth.com']
12
+
13
+ spec.summary = %q{pt-online-schema-change runner for ActiveRecord migrations}
14
+ spec.description = %q{Execute your ActiveRecord migrations with Percona's pt-online-schema-change}
15
+ spec.homepage = 'http://github.com/redbooth/percona_migrator'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.require_paths = ['lib']
20
+
21
+ # TODO: Relax me
22
+ spec.add_runtime_dependency 'rails', '~>3.2.22'
23
+ spec.add_runtime_dependency 'mysql2', '0.3.20'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.10'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.4', '>= 3.4.0'
28
+ spec.add_development_dependency 'rspec-its', '~> 1.2'
29
+ spec.add_development_dependency 'byebug', '~> 8.2', '>= 8.2.1'
30
+ end
data/test_database.rb ADDED
@@ -0,0 +1,25 @@
1
+ class TestDatabase
2
+ def initialize(config)
3
+ @config = config
4
+ end
5
+
6
+ # Creates the percona_migrator_test database and comments table in it
7
+ def create_test_database
8
+ %x(#{mysql_command} "DROP DATABASE IF EXISTS percona_migrator_test; CREATE DATABASE percona_migrator_test DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci")
9
+ %x(#{mysql_command} "USE percona_migrator_test; DROP TABLE IF EXISTS comments; CREATE TABLE comments (id int(12) NOT NULL AUTO_INCREMENT, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;")
10
+ end
11
+
12
+ # Creates the ActiveRecord's schema_migrations table required for
13
+ # migrations to work
14
+ def create_schema_migrations_table
15
+ %x(#{mysql_command} "USE percona_migrator_test; DROP TABLE IF EXISTS schema_migrations; CREATE TABLE schema_migrations ( version varchar(255) COLLATE utf8_unicode_ci NOT NULL, UNIQUE KEY unique_schema_migrations (version)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci")
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :config
21
+
22
+ def mysql_command
23
+ "mysql --user=#{config['username']} --password=#{config['password']} -e"
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: percona_migrator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.rc.1
5
+ platform: ruby
6
+ authors:
7
+ - Ilya Zayats
8
+ - Pau Pérez
9
+ - Fran Casas
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2016-03-01 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rails
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.22
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: 3.2.22
29
+ - !ruby/object:Gem::Dependency
30
+ name: mysql2
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - '='
34
+ - !ruby/object:Gem::Version
35
+ version: 0.3.20
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - '='
41
+ - !ruby/object:Gem::Version
42
+ version: 0.3.20
43
+ - !ruby/object:Gem::Dependency
44
+ name: bundler
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.10'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '1.10'
57
+ - !ruby/object:Gem::Dependency
58
+ name: rake
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '10.0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '10.0'
71
+ - !ruby/object:Gem::Dependency
72
+ name: rspec
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '3.4'
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 3.4.0
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '3.4'
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 3.4.0
91
+ - !ruby/object:Gem::Dependency
92
+ name: rspec-its
93
+ requirement: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.2'
98
+ type: :development
99
+ prerelease: false
100
+ version_requirements: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '1.2'
105
+ - !ruby/object:Gem::Dependency
106
+ name: byebug
107
+ requirement: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '8.2'
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 8.2.1
115
+ type: :development
116
+ prerelease: false
117
+ version_requirements: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '8.2'
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 8.2.1
125
+ description: Execute your ActiveRecord migrations with Percona's pt-online-schema-change
126
+ email:
127
+ - ilya.zayats@redbooth.com
128
+ - pau.perez@redbooth.com
129
+ - fran.casas@redbooth.com
130
+ executables: []
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - ".gitignore"
135
+ - ".rspec"
136
+ - ".travis.yml"
137
+ - CHANGELOG.md
138
+ - CODE_OF_CONDUCT.md
139
+ - Gemfile
140
+ - LICENSE.txt
141
+ - README.md
142
+ - Rakefile
143
+ - bin/console
144
+ - bin/setup
145
+ - config.yml
146
+ - configuration.rb
147
+ - lib/active_record/connection_adapters/percona_adapter.rb
148
+ - lib/lhm.rb
149
+ - lib/lhm/adapter.rb
150
+ - lib/lhm/column_with_sql.rb
151
+ - lib/lhm/column_with_type.rb
152
+ - lib/percona_migrator.rb
153
+ - lib/percona_migrator/alter_argument.rb
154
+ - lib/percona_migrator/cli_generator.rb
155
+ - lib/percona_migrator/railtie.rb
156
+ - lib/percona_migrator/runner.rb
157
+ - lib/percona_migrator/version.rb
158
+ - percona_migrator.gemspec
159
+ - test_database.rb
160
+ homepage: http://github.com/redbooth/percona_migrator
161
+ licenses:
162
+ - MIT
163
+ metadata: {}
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ required_rubygems_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">"
176
+ - !ruby/object:Gem::Version
177
+ version: 1.3.1
178
+ requirements: []
179
+ rubyforge_project:
180
+ rubygems_version: 2.2.2
181
+ signing_key:
182
+ specification_version: 4
183
+ summary: pt-online-schema-change runner for ActiveRecord migrations
184
+ test_files: []
185
+ has_rdoc: