alterity 0.0.0 → 0.9.0

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