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 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