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 +4 -4
- data/lib/alterity.rb +77 -1
- data/lib/alterity/configuration.rb +63 -0
- data/lib/alterity/mysql_client_additions.rb +11 -0
- data/lib/alterity/railtie.rb +30 -0
- data/lib/alterity/version.rb +2 -2
- data/spec/alterity_spec.rb +35 -1
- data/spec/bin/rails_app_migration_test.sh +7 -0
- data/spec/spec_helper.rb +4 -0
- metadata +13 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61d1573038462b1d92925a95f8b67d8c0d53ffc250c429a742bb9a3e19fe715a
|
4
|
+
data.tar.gz: e6cf943aae6b2ead8e6c04d335a63c1c1438c296381e066c513d0aefb99e6d13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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,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
|
data/lib/alterity/version.rb
CHANGED
data/spec/alterity_spec.rb
CHANGED
@@ -1,5 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
RSpec.describe Alterity do
|
4
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2021-04-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: mysql2
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
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: '
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '8'
|
26
|
+
version: '0.3'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
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:
|
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:
|
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
|