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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61d1573038462b1d92925a95f8b67d8c0d53ffc250c429a742bb9a3e19fe715a
4
- data.tar.gz: e6cf943aae6b2ead8e6c04d335a63c1c1438c296381e066c513d0aefb99e6d13
3
+ metadata.gz: 5f4e8e6c49bdb9df63ae315e6f010823a6f0f0eff52a6e7a8650a71217ed51b9
4
+ data.tar.gz: c20ae01f25ebc0b93abcbf4634e2a66e2b7978184409000677fdd77ad6750101
5
5
  SHA512:
6
- metadata.gz: ea2e19e99394e1cacd831855e509363c4d164dabfe564ef7ac47945c61c9fc94071d965a3bc16efdd80530108fecd50ee582900d962f3fd63571ae1e6d71203f
7
- data.tar.gz: bd8f34566ced8252a587804d966c02e850d41a4d0faaf88bd0279886ea58286c4d70a7ca15b7ceafc17934bdce34f69b459b7546a1603acaee4b91ab26f89933
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 table (?<table>.+?) (?<updates>.+)/i
13
- execute_alter($~[:table], $~[:updates])
14
- when /^create index (?<index>.+?) on (?<table>.+?) (?<updates>.+)/i
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 unique index (?<index>.+?) on (?<table>.+?) (?<updates>.+)/i
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 index (?<index>.+?) on (?<table>.+)/i
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(config, altered_table, alter_argument).gsub(/\n/, "\\\n")
53
+ prepared_command = config.command.call(altered_table, alter_argument).to_s.gsub(/\n/, "\\\n")
42
54
  puts "[Alterity] Will execute: #{prepared_command}"
43
- system(prepared_command)
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 config.replicas_dsns.empty?
97
+ return if dsns.empty?
71
98
 
72
99
  connection.execute <<~SQL
73
- INSERT INTO #{table} (dsn)
74
- #{config.replicas_dsns.map { |dsn| "('#{dsn}')" }.join(',')}
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
- self.state = CurrentState.new
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
- config.command = lambda { |config, altered_table, alter_argument|
19
- <<~SHELL.squish
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 self
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "mysql2"
4
+
3
5
  class Alterity
4
6
  module MysqlClientAdditions
5
7
  def query(sql, options = {})
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Alterity
4
- VERSION = "0.9.0"
4
+ VERSION = "1.3.1"
5
5
  end
@@ -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 -euo pipefail
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: 0.9.0
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-04-05 00:00:00.000000000 Z
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: '5'
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: '5'
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