alterity 0.9.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/alterity.rb +38 -11
- data/lib/alterity/configuration.rb +17 -30
- data/lib/alterity/default_configuration.rb +15 -0
- data/lib/alterity/mysql_client_additions.rb +2 -0
- data/lib/alterity/version.rb +1 -1
- data/spec/alterity_spec.rb +22 -2
- data/spec/bin/custom_config.rb +26 -0
- data/spec/bin/rails_app_migration_test.sh +67 -3
- data/spec/bin/test_custom_config_result.rb +20 -0
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f4e8e6c49bdb9df63ae315e6f010823a6f0f0eff52a6e7a8650a71217ed51b9
|
4
|
+
data.tar.gz: c20ae01f25ebc0b93abcbf4634e2a66e2b7978184409000677fdd77ad6750101
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f18b1dc05a88537b96525e04caa763b6442eacc1da1696a13f93bcd0f3d0bdac22ddee19d4aa0f11e45fbd7326a51508c1928f4641bb831b0da6645ad79b1522
|
7
|
+
data.tar.gz: a93f02e048fa0f1970123c504040219113497d214e78c806031c3df065e8e14bf628d45d2972b4c2fecd1930e1fd8b933e3845221b6ef45f74335b05ebbd1d59
|
data/lib/alterity.rb
CHANGED
@@ -8,14 +8,25 @@ require "alterity/railtie"
|
|
8
8
|
class Alterity
|
9
9
|
class << self
|
10
10
|
def process_sql_query(sql, &block)
|
11
|
-
case sql.strip
|
12
|
-
when /^alter
|
13
|
-
|
14
|
-
|
11
|
+
case sql.tr("\n", " ").strip
|
12
|
+
when /^alter\s+table\s+(?<table>.+?)\s+(?<updates>.+)/i
|
13
|
+
table = $~[:table]
|
14
|
+
updates = $~[:updates]
|
15
|
+
if updates.split(",").all? { |s| s =~ /^\s*drop\s+foreign\s+key/i } ||
|
16
|
+
updates.split(",").all? { |s| s =~ /^\s*add\s+constraint/i }
|
17
|
+
block.call
|
18
|
+
elsif updates =~ /drop\s+foreign\s+key/i || updates =~ /add\s+constraint/i
|
19
|
+
# ADD CONSTRAINT / DROP FOREIGN KEY have to go to the original table,
|
20
|
+
# other alterations need to got to the new table.
|
21
|
+
raise "[Alterity] Can't change a FK and do something else in the same query. Split it."
|
22
|
+
else
|
23
|
+
execute_alter(table, updates)
|
24
|
+
end
|
25
|
+
when /^create\s+index\s+(?<index>.+?)\s+on\s+(?<table>.+?)\s+(?<updates>.+)/i
|
15
26
|
execute_alter($~[:table], "ADD INDEX #{$~[:index]} #{$~[:updates]}")
|
16
|
-
when /^create
|
27
|
+
when /^create\s+unique\s+index\s+(?<index>.+?)\s+on\s+(?<table>.+?)\s+(?<updates>.+)/i
|
17
28
|
execute_alter($~[:table], "ADD UNIQUE INDEX #{$~[:index]} #{$~[:updates]}")
|
18
|
-
when /^drop
|
29
|
+
when /^drop\s+index\s+(?<index>.+?)\s+on\s+(?<table>.+)/i
|
19
30
|
execute_alter($~[:table], "DROP INDEX #{$~[:index]}")
|
20
31
|
else
|
21
32
|
block.call
|
@@ -24,6 +35,7 @@ class Alterity
|
|
24
35
|
|
25
36
|
# hooks
|
26
37
|
def before_running_migrations
|
38
|
+
require "open3"
|
27
39
|
state.migrating = true
|
28
40
|
set_database_config
|
29
41
|
prepare_replicas_dsns_table
|
@@ -38,9 +50,20 @@ class Alterity
|
|
38
50
|
def execute_alter(table, updates)
|
39
51
|
altered_table = table.delete("`")
|
40
52
|
alter_argument = %("#{updates.gsub('"', '\\"').gsub('`', '\\\`')}")
|
41
|
-
prepared_command = config.command.call(
|
53
|
+
prepared_command = config.command.call(altered_table, alter_argument).to_s.gsub(/\n/, "\\\n")
|
42
54
|
puts "[Alterity] Will execute: #{prepared_command}"
|
43
|
-
|
55
|
+
|
56
|
+
result_str = +""
|
57
|
+
exit_status = nil
|
58
|
+
Open3.popen2e(prepared_command) do |_stdin, stdout_and_stderr, wait_thr|
|
59
|
+
stdout_and_stderr.each do |line|
|
60
|
+
puts line
|
61
|
+
result_str << line
|
62
|
+
end
|
63
|
+
exit_status = wait_thr.value
|
64
|
+
end
|
65
|
+
|
66
|
+
raise("[Alterity] Command failed. Full output: #{result_str}") unless exit_status.success?
|
44
67
|
end
|
45
68
|
|
46
69
|
def set_database_config
|
@@ -54,6 +77,10 @@ class Alterity
|
|
54
77
|
def prepare_replicas_dsns_table
|
55
78
|
return if config.replicas_dsns_table.blank?
|
56
79
|
|
80
|
+
dsns = config.replicas_dsns.dup
|
81
|
+
# Automatically remove master
|
82
|
+
dsns.reject! { |dsn| dsn.split(",").include?("h=#{config.host}") }
|
83
|
+
|
57
84
|
database = config.replicas_dsns_database
|
58
85
|
table = "#{database}.#{config.replicas_dsns_table}"
|
59
86
|
connection = ActiveRecord::Base.connection
|
@@ -67,11 +94,11 @@ class Alterity
|
|
67
94
|
) ENGINE=InnoDB
|
68
95
|
SQL
|
69
96
|
connection.execute "TRUNCATE #{table}"
|
70
|
-
return if
|
97
|
+
return if dsns.empty?
|
71
98
|
|
72
99
|
connection.execute <<~SQL
|
73
|
-
INSERT INTO #{table} (dsn)
|
74
|
-
#{
|
100
|
+
INSERT INTO #{table} (dsn) VALUES
|
101
|
+
#{dsns.map { |dsn| "('#{dsn}')" }.join(',')}
|
75
102
|
SQL
|
76
103
|
end
|
77
104
|
end
|
@@ -13,46 +13,33 @@ class Alterity
|
|
13
13
|
class << self
|
14
14
|
def reset_state_and_configuration
|
15
15
|
self.config = Configuration.new
|
16
|
-
|
16
|
+
class << config
|
17
|
+
def replicas(database:, table:, dsns:)
|
18
|
+
return ArgumentError.new("database & table must be present") if database.blank? || table.blank?
|
19
|
+
|
20
|
+
self.replicas_dsns_database = database
|
21
|
+
self.replicas_dsns_table = table
|
22
|
+
self.replicas_dsns = dsns.uniq.map do |dsn|
|
23
|
+
parts = dsn.split(",")
|
24
|
+
# automatically add default port
|
25
|
+
parts << "P=3306" unless parts.any? { |part| part.start_with?("P=") }
|
26
|
+
parts.join(",")
|
27
|
+
end.compact
|
28
|
+
end
|
29
|
+
end
|
17
30
|
|
18
|
-
|
19
|
-
|
20
|
-
pt-online-schema-change
|
21
|
-
-h #{config.host}
|
22
|
-
-P #{config.port}
|
23
|
-
-u #{config.username}
|
24
|
-
--password=#{config.password}
|
25
|
-
--execute
|
26
|
-
D=#{config.database},t=#{altered_table}
|
27
|
-
--alter #{alter_argument}
|
28
|
-
SHELL
|
29
|
-
}
|
31
|
+
self.state = CurrentState.new
|
32
|
+
load "#{__dir__}/default_configuration.rb"
|
30
33
|
end
|
31
34
|
|
32
35
|
def configure
|
33
|
-
yield
|
36
|
+
yield config
|
34
37
|
end
|
35
38
|
|
36
39
|
def command=(new_command)
|
37
40
|
config.command = new_command
|
38
41
|
end
|
39
42
|
|
40
|
-
def replicas_dsns_table(database:, table:, dsns:)
|
41
|
-
return ArgumentError.new("database & table must be present") if database.blank? || table.blank?
|
42
|
-
|
43
|
-
config.replicas_dsns_database = database
|
44
|
-
config.replicas_dsns_table = table
|
45
|
-
config.replicas_dsns = dsns.uniq.map do |dsn|
|
46
|
-
parts = dsn.split(",")
|
47
|
-
# automatically add default port
|
48
|
-
parts << "P=3306" unless parts.any? { |part| part.start_with?("P=") }
|
49
|
-
# automatically remove master
|
50
|
-
next if parts.include?("h=#{config.host}") && parts.include?("P=#{config.port}")
|
51
|
-
|
52
|
-
parts.join(",")
|
53
|
-
end.compact
|
54
|
-
end
|
55
|
-
|
56
43
|
def disable
|
57
44
|
state.disabled = true
|
58
45
|
yield
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Alterity.configure do |config|
|
4
|
+
config.command = lambda { |altered_table, alter_argument|
|
5
|
+
parts = ["pt-online-schema-change"]
|
6
|
+
parts << %(-h "#{config.host}") if config.host.present?
|
7
|
+
parts << %(-P "#{config.port}") if config.port.present?
|
8
|
+
parts << %(-u "#{config.username}") if config.username.present?
|
9
|
+
parts << %(--password "#{config.password.gsub('"', '\\"')}") if config.password.present?
|
10
|
+
parts << "--execute"
|
11
|
+
parts << "D=#{config.database},t=#{altered_table}"
|
12
|
+
parts << "--alter #{alter_argument}"
|
13
|
+
parts.join(" ")
|
14
|
+
}
|
15
|
+
end
|
data/lib/alterity/version.rb
CHANGED
data/spec/alterity_spec.rb
CHANGED
@@ -9,8 +9,10 @@ RSpec.describe Alterity do
|
|
9
9
|
["CREATE INDEX `idx_users_on_col` ON `users` (col)", "`users`", "ADD INDEX `idx_users_on_col` (col)"],
|
10
10
|
["CREATE UNIQUE INDEX `idx_users_on_col` ON `users` (col)", "`users`", "ADD UNIQUE INDEX `idx_users_on_col` (col)"],
|
11
11
|
["DROP INDEX `idx_users_on_col` ON `users`", "`users`", "DROP INDEX `idx_users_on_col`"],
|
12
|
-
["alter table users drop col", "users", "drop col"]
|
12
|
+
["alter table users drop col", "users", "drop col"],
|
13
|
+
[" ALTER TABLE\n users\n DROP col", "users", "DROP col"],
|
13
14
|
].each do |(query, expected_table, expected_updates)|
|
15
|
+
puts query.inspect
|
14
16
|
expected_block = proc {}
|
15
17
|
expect(expected_block).not_to receive(:call)
|
16
18
|
expect(Alterity).to receive(:execute_alter).with(expected_table, expected_updates)
|
@@ -27,7 +29,11 @@ RSpec.describe Alterity do
|
|
27
29
|
"SHOW CREATE TABLE `users`",
|
28
30
|
"SHOW TABLE STATUS LIKE `users`",
|
29
31
|
"SHOW KEYS FROM `users`",
|
30
|
-
"SHOW FULL FIELDS FROM `users`"
|
32
|
+
"SHOW FULL FIELDS FROM `users`",
|
33
|
+
"ALTER TABLE `installment_events` DROP FOREIGN KEY _fk_rails_0123456789",
|
34
|
+
"ALTER TABLE `installment_events` DROP FOREIGN KEY _fk_rails_0123456789, DROP FOREIGN KEY _fk_rails_9876543210",
|
35
|
+
"ALTER TABLE `installment_events` ADD CONSTRAINT `fk_rails_0cb5590091` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)",
|
36
|
+
"ALTER TABLE `installment_events` ADD CONSTRAINT `fk_rails_0cb5590091` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE",
|
31
37
|
].each do |query|
|
32
38
|
expected_block = proc {}
|
33
39
|
expect(expected_block).to receive(:call)
|
@@ -35,5 +41,19 @@ RSpec.describe Alterity do
|
|
35
41
|
Alterity.process_sql_query(query, &expected_block)
|
36
42
|
end
|
37
43
|
end
|
44
|
+
|
45
|
+
it "raises an error if mixing FK change and other things" do
|
46
|
+
expected_block = proc {}
|
47
|
+
expect(expected_block).not_to receive(:call)
|
48
|
+
expect(Alterity).not_to receive(:execute_alter)
|
49
|
+
query = "ALTER TABLE `installment_events` ADD `col` VARCHAR(255), DROP FOREIGN KEY _fk_rails_0123456789"
|
50
|
+
expect do
|
51
|
+
Alterity.process_sql_query(query, &expected_block)
|
52
|
+
end.to raise_error(/FK/)
|
53
|
+
query = "ALTER TABLE `installment_events` ADD `col` VARCHAR(255), ADD CONSTRAINT `fk_rails_0cb5590091` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)"
|
54
|
+
expect do
|
55
|
+
Alterity.process_sql_query(query, &expected_block)
|
56
|
+
end.to raise_error(/FK/)
|
57
|
+
end
|
38
58
|
end
|
39
59
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Alterity.configure do |config|
|
4
|
+
config.command = lambda { |altered_table, alter_argument|
|
5
|
+
string = config.to_h.slice(
|
6
|
+
*%i[host port username database replicas_dsns_database replicas_dsns_table replicas_dsns]
|
7
|
+
).to_s
|
8
|
+
system("echo '#{string}' > /tmp/custom_command_result.txt")
|
9
|
+
system("echo '#{altered_table}' >> /tmp/custom_command_result.txt")
|
10
|
+
system("echo '#{alter_argument}' >> /tmp/custom_command_result.txt")
|
11
|
+
}
|
12
|
+
|
13
|
+
config.replicas(
|
14
|
+
database: "percona",
|
15
|
+
table: "replicas_dsns",
|
16
|
+
dsns: [
|
17
|
+
"h=host1",
|
18
|
+
"h=host2",
|
19
|
+
# we may encounter an app where the replica host is actually pointing to master;
|
20
|
+
# pt-osc doesn't deal well with this and will wait forever.
|
21
|
+
# So we're testing here that Alterity will detect that this is master (same as config.host),
|
22
|
+
# and will not insert it into the `replicas_dsns` table.
|
23
|
+
"h=#{ENV['MYSQL_HOST']}"
|
24
|
+
]
|
25
|
+
)
|
26
|
+
end
|
@@ -1,7 +1,71 @@
|
|
1
1
|
#!/usr/bin/env bash
|
2
2
|
|
3
|
-
set -
|
3
|
+
set -euox pipefail
|
4
|
+
|
5
|
+
unset BUNDLE_GEMFILE # because we're going to create a new rails app here and use bundler
|
4
6
|
|
5
|
-
printenv
|
6
|
-
sudo apt update
|
7
7
|
sudo apt install percona-toolkit
|
8
|
+
|
9
|
+
# Fixes: `Cannot connect to MySQL: Cannot get MySQL var character_set_server: DBD::mysql::db selectrow_array failed: Table 'performance_schema.session_variables' doesn't exist [for Statement "SHOW VARIABLES LIKE 'character_set_server'"] at /usr/local/Cellar/percona-toolkit/3.3.0/libexec/bin/pt-online-schema-change line 2415.`
|
10
|
+
mysql -h $MYSQL_HOST -u $MYSQL_USERNAME -e 'set @@global.show_compatibility_56=ON'
|
11
|
+
|
12
|
+
gem install rails -v $RAILS_VERSION
|
13
|
+
|
14
|
+
rails new testapp \
|
15
|
+
--skip-action-mailer \
|
16
|
+
--skip-action-mailbox \
|
17
|
+
--skip-action-text \
|
18
|
+
--skip-active-job \
|
19
|
+
--skip-active-storage \
|
20
|
+
--skip-puma \
|
21
|
+
--skip-action-cable \
|
22
|
+
--skip-sprockets \
|
23
|
+
--skip-spring \
|
24
|
+
--skip-listen--skip-javascript \
|
25
|
+
--skip-turbolinks \
|
26
|
+
--skip-jbuilder--skip-test \
|
27
|
+
--skip-system-test \
|
28
|
+
--skip-bootsnap \
|
29
|
+
--skip-javascript \
|
30
|
+
--skip-webpack-install
|
31
|
+
|
32
|
+
cd testapp
|
33
|
+
|
34
|
+
# Sanity check:
|
35
|
+
# echo 'gem "mysql2"' >> Gemfile
|
36
|
+
|
37
|
+
echo 'gem "alterity", path: "../"' >> Gemfile
|
38
|
+
|
39
|
+
bundle
|
40
|
+
|
41
|
+
# Local machine test
|
42
|
+
# echo 'development:
|
43
|
+
# adapter: mysql2
|
44
|
+
# database: alterity_test' > config/database.yml
|
45
|
+
# bundle e rails db:drop db:create
|
46
|
+
|
47
|
+
echo 'development:
|
48
|
+
adapter: mysql2
|
49
|
+
database: <%= ENV.fetch("MYSQL_DATABASE") %>
|
50
|
+
host: <%= ENV.fetch("MYSQL_HOST") %>
|
51
|
+
username: <%= ENV.fetch("MYSQL_USERNAME") %>' > config/database.yml
|
52
|
+
|
53
|
+
bundle e rails g model shirt
|
54
|
+
|
55
|
+
bundle e rails g migration add_color_to_shirts color:string
|
56
|
+
|
57
|
+
# Test default configuration works as expected
|
58
|
+
bundle e rails db:migrate --trace
|
59
|
+
bundle e rails runner 'Shirt.columns.map(&:name).include?("color") || exit(1)'
|
60
|
+
|
61
|
+
# Now test custom command and replication setup
|
62
|
+
cp ../spec/bin/custom_config.rb config/initializers/alterity.rb
|
63
|
+
|
64
|
+
bundle e rails g migration add_color2_to_shirts color2:string
|
65
|
+
|
66
|
+
bundle e rails db:migrate --trace
|
67
|
+
|
68
|
+
ruby ../spec/bin/test_custom_config_result.rb
|
69
|
+
|
70
|
+
# Also testing what's in replicas_dsns, also checking that master was detected and removed.
|
71
|
+
bundle e rails runner 'res=ActiveRecord::Base.connection.execute("select dsn from percona.replicas_dsns").to_a.flatten;p(res); res == ["h=host1,P=3306", "h=host2,P=3306"] || exit(1)'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
result = File.read("/tmp/custom_command_result.txt").downcase.strip
|
4
|
+
|
5
|
+
expected_result = %({:host=>"127.0.0.1", :port=>nil, :username=>"root", :database=>"alterity_test", :replicas_dsns_database=>"percona", :replicas_dsns_table=>"replicas_dsns", :replicas_dsns=>["h=host1,P=3306", "h=host2,P=3306", "h=127.0.0.1,P=3306"]}
|
6
|
+
shirts
|
7
|
+
"ADD \\`color2\\` VARCHAR(255)").downcase.strip
|
8
|
+
|
9
|
+
puts "Expected custom config result:"
|
10
|
+
puts expected_result
|
11
|
+
p expected_result.chars.map(&:hex)
|
12
|
+
|
13
|
+
puts "Custom config result:"
|
14
|
+
puts result
|
15
|
+
p result.chars.map(&:hex)
|
16
|
+
|
17
|
+
if result != expected_result
|
18
|
+
puts "=> mismatch"
|
19
|
+
exit(1)
|
20
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: alterity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Maximin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-06-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mysql2
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '6.1'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '6.1'
|
41
41
|
description: Execute your ActiveRecord migrations with Percona's pt-online-schema-change.
|
42
42
|
email:
|
43
43
|
- gems@chrismaximin.com
|
@@ -47,11 +47,14 @@ extra_rdoc_files: []
|
|
47
47
|
files:
|
48
48
|
- lib/alterity.rb
|
49
49
|
- lib/alterity/configuration.rb
|
50
|
+
- lib/alterity/default_configuration.rb
|
50
51
|
- lib/alterity/mysql_client_additions.rb
|
51
52
|
- lib/alterity/railtie.rb
|
52
53
|
- lib/alterity/version.rb
|
53
54
|
- spec/alterity_spec.rb
|
55
|
+
- spec/bin/custom_config.rb
|
54
56
|
- spec/bin/rails_app_migration_test.sh
|
57
|
+
- spec/bin/test_custom_config_result.rb
|
55
58
|
- spec/spec_helper.rb
|
56
59
|
homepage: https://github.com/gumroad/alterity
|
57
60
|
licenses:
|
@@ -81,5 +84,7 @@ specification_version: 4
|
|
81
84
|
summary: Execute your ActiveRecord migrations with Percona's pt-online-schema-change.
|
82
85
|
test_files:
|
83
86
|
- spec/alterity_spec.rb
|
87
|
+
- spec/bin/custom_config.rb
|
84
88
|
- spec/bin/rails_app_migration_test.sh
|
89
|
+
- spec/bin/test_custom_config_result.rb
|
85
90
|
- spec/spec_helper.rb
|