alterity 0.0.0 → 0.9.0

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: 5f91f2ad32f8f398432906ae434fb824e9f94a63fc528a73a5c65c034de41ece
4
- data.tar.gz: 931ed885773f0070bb68d1b354d6b79f00f7909f84614fc8c7d6f82206f41a53
3
+ metadata.gz: 61d1573038462b1d92925a95f8b67d8c0d53ffc250c429a742bb9a3e19fe715a
4
+ data.tar.gz: e6cf943aae6b2ead8e6c04d335a63c1c1438c296381e066c513d0aefb99e6d13
5
5
  SHA512:
6
- metadata.gz: e8ecad73cb251e6aa10353eb9a6118c44e316fc85075ee8f77e8e5cf6be6d16a583f20783bfbaa7b1d7ce690b0e36f611bbf51e95bf63156588b3161adf880be
7
- data.tar.gz: 39e586c77a9e9fa5d60a2e78735839457ba72a063879921d48f4ef31c488567fcf75e1fc09031564c3af0737c6e40974839a8b1cf98bdf9f9352b9bb1c75f529
6
+ metadata.gz: ea2e19e99394e1cacd831855e509363c4d164dabfe564ef7ac47945c61c9fc94071d965a3bc16efdd80530108fecd50ee582900d962f3fd63571ae1e6d71203f
7
+ data.tar.gz: bd8f34566ced8252a587804d966c02e850d41a4d0faaf88bd0279886ea58286c4d70a7ca15b7ceafc17934bdce34f69b459b7546a1603acaee4b91ab26f89933
data/lib/alterity.rb CHANGED
@@ -1,4 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Alterity
3
+ require "rails"
4
+ require "alterity/configuration"
5
+ require "alterity/mysql_client_additions"
6
+ require "alterity/railtie"
7
+
8
+ class Alterity
9
+ class << self
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
15
+ execute_alter($~[:table], "ADD INDEX #{$~[:index]} #{$~[:updates]}")
16
+ when /^create unique index (?<index>.+?) on (?<table>.+?) (?<updates>.+)/i
17
+ execute_alter($~[:table], "ADD UNIQUE INDEX #{$~[:index]} #{$~[:updates]}")
18
+ when /^drop index (?<index>.+?) on (?<table>.+)/i
19
+ execute_alter($~[:table], "DROP INDEX #{$~[:index]}")
20
+ else
21
+ block.call
22
+ end
23
+ end
24
+
25
+ # hooks
26
+ def before_running_migrations
27
+ state.migrating = true
28
+ set_database_config
29
+ prepare_replicas_dsns_table
30
+ end
31
+
32
+ def after_running_migrations
33
+ state.migrating = false
34
+ end
35
+
36
+ private
37
+
38
+ def execute_alter(table, updates)
39
+ altered_table = table.delete("`")
40
+ alter_argument = %("#{updates.gsub('"', '\\"').gsub('`', '\\\`')}")
41
+ prepared_command = config.command.call(config, altered_table, alter_argument).gsub(/\n/, "\\\n")
42
+ puts "[Alterity] Will execute: #{prepared_command}"
43
+ system(prepared_command)
44
+ end
45
+
46
+ def set_database_config
47
+ db_config_hash = ActiveRecord::Base.connection_db_config.configuration_hash
48
+ %i[host port database username password].each do |key|
49
+ config[key] = db_config_hash[key]
50
+ end
51
+ end
52
+
53
+ # Optional: Automatically set up table PT-OSC will monitor for replica lag.
54
+ def prepare_replicas_dsns_table
55
+ return if config.replicas_dsns_table.blank?
56
+
57
+ database = config.replicas_dsns_database
58
+ table = "#{database}.#{config.replicas_dsns_table}"
59
+ connection = ActiveRecord::Base.connection
60
+ connection.execute "CREATE DATABASE IF NOT EXISTS #{database}"
61
+ connection.execute <<~SQL
62
+ CREATE TABLE IF NOT EXISTS #{table} (
63
+ id INT(11) NOT NULL AUTO_INCREMENT,
64
+ parent_id INT(11) DEFAULT NULL,
65
+ dsn VARCHAR(255) NOT NULL,
66
+ PRIMARY KEY (id)
67
+ ) ENGINE=InnoDB
68
+ SQL
69
+ connection.execute "TRUNCATE #{table}"
70
+ return if config.replicas_dsns.empty?
71
+
72
+ connection.execute <<~SQL
73
+ INSERT INTO #{table} (dsn)
74
+ #{config.replicas_dsns.map { |dsn| "('#{dsn}')" }.join(',')}
75
+ SQL
76
+ end
77
+ end
78
+
79
+ reset_state_and_configuration
4
80
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Alterity
4
+ Configuration = Struct.new(
5
+ :command,
6
+ :host, :port, :database, :username, :password,
7
+ :replicas_dsns_database, :replicas_dsns_table, :replicas_dsns
8
+ )
9
+ CurrentState = Struct.new(:migrating, :disabled)
10
+ cattr_accessor :state
11
+ cattr_accessor :config
12
+
13
+ class << self
14
+ def reset_state_and_configuration
15
+ self.config = Configuration.new
16
+ self.state = CurrentState.new
17
+
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
+ }
30
+ end
31
+
32
+ def configure
33
+ yield self
34
+ end
35
+
36
+ def command=(new_command)
37
+ config.command = new_command
38
+ end
39
+
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
+ def disable
57
+ state.disabled = true
58
+ yield
59
+ ensure
60
+ state.disabled = false
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Alterity
4
+ module MysqlClientAdditions
5
+ def query(sql, options = {})
6
+ return super(sql, options) unless Alterity.state.migrating
7
+
8
+ Alterity.process_sql_query(sql) { super(sql, options) }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Alterity
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :alterity
6
+
7
+ rake_tasks do
8
+ namespace :alterity do
9
+ task :intercept_table_alterations do
10
+ Alterity.before_running_migrations
11
+ Rake::Task["alterity:stop_intercepting_table_alterations"].reenable
12
+ ::Mysql2::Client.prepend(Alterity::MysqlClientAdditions)
13
+ end
14
+
15
+ task :stop_intercepting_table_alterations do
16
+ Rake::Task["alterity:intercept_table_alterations"].reenable
17
+ Alterity.after_running_migrations
18
+ end
19
+ end
20
+
21
+ unless %w[1 true].include?(ENV["DISABLE_ALTERITY"])
22
+ ["migrate", "migrate:up", "migrate:down", "migrate:redo", "rollback"].each do |task|
23
+ Rake::Task["db:#{task}"].enhance(["alterity:intercept_table_alterations"]) do
24
+ Rake::Task["alterity:stop_intercepting_table_alterations"].invoke
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Alterity
4
- VERSION = "0.0.0"
3
+ class Alterity
4
+ VERSION = "0.9.0"
5
5
  end
@@ -1,5 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe Alterity do
4
- # TODO
4
+ describe ".process_sql_query" do
5
+ it "executes command on table altering queries" do
6
+ [
7
+ ["ALTER TABLE `users` ADD `col` VARCHAR(255)", "`users`", "ADD `col` VARCHAR(255)"],
8
+ ["ALTER TABLE `users` ADD `col0` INT(11), DROP `col1`", "`users`", "ADD `col0` INT(11), DROP `col1`"],
9
+ ["CREATE INDEX `idx_users_on_col` ON `users` (col)", "`users`", "ADD INDEX `idx_users_on_col` (col)"],
10
+ ["CREATE UNIQUE INDEX `idx_users_on_col` ON `users` (col)", "`users`", "ADD UNIQUE INDEX `idx_users_on_col` (col)"],
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"]
13
+ ].each do |(query, expected_table, expected_updates)|
14
+ expected_block = proc {}
15
+ expect(expected_block).not_to receive(:call)
16
+ expect(Alterity).to receive(:execute_alter).with(expected_table, expected_updates)
17
+ Alterity.process_sql_query(query, &expected_block)
18
+ end
19
+ end
20
+
21
+ it "ignores non-altering queries" do
22
+ [
23
+ "select * from users",
24
+ "insert into users values (1)",
25
+ "delete from users",
26
+ "begin",
27
+ "SHOW CREATE TABLE `users`",
28
+ "SHOW TABLE STATUS LIKE `users`",
29
+ "SHOW KEYS FROM `users`",
30
+ "SHOW FULL FIELDS FROM `users`"
31
+ ].each do |query|
32
+ expected_block = proc {}
33
+ expect(expected_block).to receive(:call)
34
+ expect(Alterity).not_to receive(:execute_alter)
35
+ Alterity.process_sql_query(query, &expected_block)
36
+ end
37
+ end
38
+ end
5
39
  end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ printenv
6
+ sudo apt update
7
+ sudo apt install percona-toolkit
data/spec/spec_helper.rb CHANGED
@@ -15,4 +15,8 @@ RSpec.configure do |config|
15
15
  end
16
16
 
17
17
  config.filter_run_when_matching :focus
18
+
19
+ config.before do
20
+ Alterity.reset_state_and_configuration
21
+ end
18
22
  end
metadata CHANGED
@@ -1,55 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alterity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.9.0
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-03-28 00:00:00.000000000 Z
11
+ date: 2021-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord
14
+ name: mysql2
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '6.1'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '8'
19
+ version: '0.3'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
- version: '6.1'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '8'
26
+ version: '0.3'
33
27
  - !ruby/object:Gem::Dependency
34
- name: mysql2
28
+ name: rails
35
29
  requirement: !ruby/object:Gem::Requirement
36
30
  requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '0.5'
40
31
  - - ">="
41
32
  - !ruby/object:Gem::Version
42
- version: 0.5.3
33
+ version: '5'
43
34
  type: :runtime
44
35
  prerelease: false
45
36
  version_requirements: !ruby/object:Gem::Requirement
46
37
  requirements:
47
- - - "~>"
48
- - !ruby/object:Gem::Version
49
- version: '0.5'
50
38
  - - ">="
51
39
  - !ruby/object:Gem::Version
52
- version: 0.5.3
40
+ version: '5'
53
41
  description: Execute your ActiveRecord migrations with Percona's pt-online-schema-change.
54
42
  email:
55
43
  - gems@chrismaximin.com
@@ -58,8 +46,12 @@ extensions: []
58
46
  extra_rdoc_files: []
59
47
  files:
60
48
  - lib/alterity.rb
49
+ - lib/alterity/configuration.rb
50
+ - lib/alterity/mysql_client_additions.rb
51
+ - lib/alterity/railtie.rb
61
52
  - lib/alterity/version.rb
62
53
  - spec/alterity_spec.rb
54
+ - spec/bin/rails_app_migration_test.sh
63
55
  - spec/spec_helper.rb
64
56
  homepage: https://github.com/gumroad/alterity
65
57
  licenses:
@@ -89,4 +81,5 @@ specification_version: 4
89
81
  summary: Execute your ActiveRecord migrations with Percona's pt-online-schema-change.
90
82
  test_files:
91
83
  - spec/alterity_spec.rb
84
+ - spec/bin/rails_app_migration_test.sh
92
85
  - spec/spec_helper.rb