alterity 0.9.0 → 1.3.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 +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
|