mysql_framework 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/mysql_framework.rb +15 -0
- data/lib/mysql_framework/connector.rb +84 -0
- data/lib/mysql_framework/logger.rb +15 -0
- data/lib/mysql_framework/scripts.rb +5 -0
- data/lib/mysql_framework/scripts/base.rb +58 -0
- data/lib/mysql_framework/scripts/manager.rb +137 -0
- data/lib/mysql_framework/scripts/table.rb +11 -0
- data/lib/mysql_framework/sql_column.rb +54 -0
- data/lib/mysql_framework/sql_condition.rb +20 -0
- data/lib/mysql_framework/sql_query.rb +131 -0
- data/lib/mysql_framework/sql_table.rb +23 -0
- data/lib/mysql_framework/version.rb +3 -0
- data/spec/lib/mysql_framework/connector_spec.rb +192 -0
- data/spec/lib/mysql_framework/logger_spec.rb +20 -0
- data/spec/lib/mysql_framework/scripts/base_spec.rb +91 -0
- data/spec/lib/mysql_framework/scripts/manager_spec.rb +161 -0
- data/spec/lib/mysql_framework/sql_column_spec.rb +73 -0
- data/spec/lib/mysql_framework/sql_condition_spec.rb +13 -0
- data/spec/lib/mysql_framework/sql_query_spec.rb +223 -0
- data/spec/lib/mysql_framework/sql_table_spec.rb +26 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/support/procedure.sql +5 -0
- data/spec/support/scripts/create_demo_table.rb +34 -0
- data/spec/support/scripts/create_test_proc.rb +27 -0
- data/spec/support/scripts/create_test_table.rb +34 -0
- data/spec/support/tables/demo.rb +15 -0
- data/spec/support/tables/test.rb +15 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 889fe8ccc0b85df3fe457ceeacaf4ac66a8953fe6d68a62a066d0700c26d05fc
|
4
|
+
data.tar.gz: 619ace891bdf1d809774628ab8322298cb019ffa92d260467a13835fff4359d3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a6303e0423b54b92e7bf02dcd651a9da14b6a707b6848e046221ea769428e6633308da95389a83a16c1bc26343fcb106b15cb75cc1d79b2ee29c6c4cfe26c07b
|
7
|
+
data.tar.gz: 9f4c4372b49495c9660e269eec2eec9d3df901d15c4566c5694c2f463453eeeeb2a0f1eabd68e145aabf0d007249bdb1e5a1a963ddb942be4742544725a2b7a0
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "dynamodb_framework"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'mysql2'
|
2
|
+
require 'redlock'
|
3
|
+
|
4
|
+
require_relative 'mysql_framework/connector'
|
5
|
+
require_relative 'mysql_framework/logger'
|
6
|
+
require_relative 'mysql_framework/scripts'
|
7
|
+
require_relative 'mysql_framework/sql_column'
|
8
|
+
require_relative 'mysql_framework/sql_condition'
|
9
|
+
require_relative 'mysql_framework/sql_query'
|
10
|
+
require_relative 'mysql_framework/sql_table'
|
11
|
+
require_relative 'mysql_framework/version'
|
12
|
+
|
13
|
+
module MysqlFramework
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MysqlFramework
|
4
|
+
class Connector
|
5
|
+
def initialize(options = {})
|
6
|
+
@connection_pool = ::Queue.new
|
7
|
+
|
8
|
+
@options = default_options.merge(options)
|
9
|
+
|
10
|
+
Mysql2::Client.default_query_options.merge!(symbolize_keys: true, cast_booleans: true)
|
11
|
+
end
|
12
|
+
|
13
|
+
# This method is called to fetch a client from the connection pool or create a new client if no idle clients
|
14
|
+
# are available.
|
15
|
+
def check_out
|
16
|
+
@connection_pool.pop(true)
|
17
|
+
rescue StandardError
|
18
|
+
Mysql2::Client.new(@options)
|
19
|
+
end
|
20
|
+
|
21
|
+
# This method is called to check a client back in to the connection when no longer needed.
|
22
|
+
def check_in(client)
|
23
|
+
@connection_pool.push(client)
|
24
|
+
end
|
25
|
+
|
26
|
+
# This method is called to use a client from the connection pool.
|
27
|
+
def with_client
|
28
|
+
client = check_out
|
29
|
+
yield client
|
30
|
+
ensure
|
31
|
+
check_in(client) unless client.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
# This method is called to execute a prepared statement
|
35
|
+
def execute(query)
|
36
|
+
with_client do |client|
|
37
|
+
statement = client.prepare(query.sql)
|
38
|
+
statement.execute(*query.params)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# This method is called to execute a query
|
43
|
+
def query(query_string)
|
44
|
+
with_client { |client| client.query(query_string) }
|
45
|
+
end
|
46
|
+
|
47
|
+
# This method is called to execute a query which will return multiple result sets in an array
|
48
|
+
def query_multiple_results(query_string)
|
49
|
+
with_client do |client|
|
50
|
+
result = []
|
51
|
+
result << client.query(query_string).to_a
|
52
|
+
result << client.store_result&.to_a while client.next_result
|
53
|
+
result.compact
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# This method is called to use a client within a transaction
|
58
|
+
def transaction
|
59
|
+
raise ArgumentError, 'No block was given' unless block_given?
|
60
|
+
|
61
|
+
with_client do |client|
|
62
|
+
begin
|
63
|
+
client.query('BEGIN')
|
64
|
+
yield client
|
65
|
+
client.query('COMMIT')
|
66
|
+
rescue StandardError => e
|
67
|
+
client.query('ROLLBACK')
|
68
|
+
raise e
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def default_options
|
74
|
+
{
|
75
|
+
host: ENV.fetch('MYSQL_HOST'),
|
76
|
+
port: ENV.fetch('MYSQL_PORT'),
|
77
|
+
database: ENV.fetch('MYSQL_DATABASE'),
|
78
|
+
username: ENV.fetch('MYSQL_USERNAME'),
|
79
|
+
password: ENV.fetch('MYSQL_PASSWORD'),
|
80
|
+
reconnect: true
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MysqlFramework
|
4
|
+
module Scripts
|
5
|
+
class Base
|
6
|
+
def partitions
|
7
|
+
ENV.fetch('MYSQL_PARTITIONS', '500').to_i
|
8
|
+
end
|
9
|
+
|
10
|
+
def database_name
|
11
|
+
@database_name ||= ENV.fetch('MYSQL_DATABASE')
|
12
|
+
end
|
13
|
+
|
14
|
+
def identifier
|
15
|
+
raise NotImplementedError if @identifier.nil?
|
16
|
+
@identifier
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def rollback
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate_partition_sql
|
28
|
+
(1..partitions).each_with_index.map { |_, i| "PARTITION p#{i} VALUES IN (#{i})" }.join(",\n\t")
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.descendants
|
32
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
33
|
+
end
|
34
|
+
|
35
|
+
def tags
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_procedure(proc_name, proc_file)
|
40
|
+
mysql_connector.transaction do
|
41
|
+
mysql_connector.query(<<~SQL)
|
42
|
+
DROP PROCEDURE IF EXISTS #{proc_name};
|
43
|
+
SQL
|
44
|
+
|
45
|
+
proc_sql = File.read(proc_file)
|
46
|
+
|
47
|
+
mysql_connector.query(proc_sql)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def mysql_connector
|
54
|
+
@mysql_connector ||= MysqlFramework::Connector.new
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MysqlFramework
|
4
|
+
module Scripts
|
5
|
+
class Manager
|
6
|
+
def execute
|
7
|
+
lock_manager.lock(self.class, 2000) do |locked|
|
8
|
+
raise unless locked
|
9
|
+
|
10
|
+
initialize_script_history
|
11
|
+
|
12
|
+
last_executed_script = retrieve_last_executed_script
|
13
|
+
|
14
|
+
mysql_connector.transaction do
|
15
|
+
pending_scripts = calculate_pending_scripts(last_executed_script)
|
16
|
+
MysqlFramework.logger.info { "[#{self.class}] - #{pending_scripts.length} pending data store scripts found." }
|
17
|
+
|
18
|
+
pending_scripts.each { |script| apply(script) }
|
19
|
+
end
|
20
|
+
|
21
|
+
MysqlFramework.logger.info { "[#{self.class}] - Migration script execution complete." }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def apply_by_tag(tags)
|
26
|
+
lock_manager.lock(self.class, 2000) do |locked|
|
27
|
+
raise unless locked
|
28
|
+
|
29
|
+
initialize_script_history
|
30
|
+
|
31
|
+
mysql_connector.transaction do
|
32
|
+
pending_scripts = calculate_pending_scripts(0)
|
33
|
+
MysqlFramework.logger.info { "[#{self.class}] - #{pending_scripts.length} pending data store scripts found." }
|
34
|
+
|
35
|
+
pending_scripts.reject { |script| (script.tags & tags).empty? }.sort_by(&:identifier)
|
36
|
+
.each { |script| apply(script) }
|
37
|
+
end
|
38
|
+
|
39
|
+
MysqlFramework.logger.info { "[#{self.class}] - Migration script execution complete." }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def drop_all_tables
|
44
|
+
drop_script_history
|
45
|
+
all_tables.each { |table| drop_table(table) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def retrieve_last_executed_script
|
49
|
+
MysqlFramework.logger.info { "[#{self.class}] - Retrieving last executed script from history." }
|
50
|
+
|
51
|
+
result = mysql_connector.query(<<~SQL)
|
52
|
+
SELECT `identifier` FROM #{migration_table_name} ORDER BY `identifier` DESC
|
53
|
+
SQL
|
54
|
+
|
55
|
+
if result.each.to_a.length.zero?
|
56
|
+
0
|
57
|
+
else
|
58
|
+
Integer(result.first[:identifier])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize_script_history
|
63
|
+
MysqlFramework.logger.info { "[#{self.class}] - Initializing script history." }
|
64
|
+
|
65
|
+
mysql_connector.query(<<~SQL)
|
66
|
+
CREATE TABLE IF NOT EXISTS #{migration_table_name} (
|
67
|
+
`identifier` CHAR(15) NOT NULL,
|
68
|
+
`timestamp` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
69
|
+
PRIMARY KEY (`identifier`),
|
70
|
+
UNIQUE INDEX `identifier_UNIQUE` (`identifier` ASC)
|
71
|
+
)
|
72
|
+
SQL
|
73
|
+
end
|
74
|
+
|
75
|
+
def calculate_pending_scripts(last_executed_script)
|
76
|
+
MysqlFramework.logger.info { "[#{self.class}] - Calculating pending data store scripts." }
|
77
|
+
|
78
|
+
MysqlFramework::Scripts::Base.descendants.map(&:new)
|
79
|
+
.select { |script| script.identifier > last_executed_script }.sort_by(&:identifier)
|
80
|
+
end
|
81
|
+
|
82
|
+
def table_exists?(table_name)
|
83
|
+
result = mysql_connector.query(<<~SQL)
|
84
|
+
SHOW TABLES LIKE '#{table_name}'
|
85
|
+
SQL
|
86
|
+
result.count == 1
|
87
|
+
end
|
88
|
+
|
89
|
+
def drop_script_history
|
90
|
+
drop_table(migration_table_name)
|
91
|
+
end
|
92
|
+
|
93
|
+
def drop_table(table_name)
|
94
|
+
mysql_connector.query(<<~SQL)
|
95
|
+
DROP TABLE IF EXISTS #{table_name}
|
96
|
+
SQL
|
97
|
+
end
|
98
|
+
|
99
|
+
def all_tables
|
100
|
+
self.class.all_tables
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.all_tables
|
104
|
+
@all_tables ||= []
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def mysql_connector
|
110
|
+
@mysql_connector ||= MysqlFramework::Connector.new
|
111
|
+
end
|
112
|
+
|
113
|
+
def lock_manager
|
114
|
+
@lock_manager ||= Redlock::Client.new([ENV.fetch('REDIS_URL')])
|
115
|
+
end
|
116
|
+
|
117
|
+
def database
|
118
|
+
@database ||= ENV.fetch('MYSQL_DATABASE')
|
119
|
+
end
|
120
|
+
|
121
|
+
def migration_table_name
|
122
|
+
return @migration_table_name if @migration_table_name
|
123
|
+
|
124
|
+
@migration_table_name = "`#{database}`.`migration_script_history`"
|
125
|
+
end
|
126
|
+
|
127
|
+
def apply(script)
|
128
|
+
MysqlFramework.logger.info { "[#{self.class}] - Applying script: #{script}." }
|
129
|
+
|
130
|
+
script.apply
|
131
|
+
mysql_connector.query(<<~SQL)
|
132
|
+
INSERT INTO #{migration_table_name} (`identifier`, `timestamp`) VALUES ('#{script.identifier}', NOW())
|
133
|
+
SQL
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MysqlFramework
|
4
|
+
# This class is used to represent a sql column within a table
|
5
|
+
class SqlColumn
|
6
|
+
def initialize(table:, column:)
|
7
|
+
@table = table
|
8
|
+
@column = column
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"`#{@table}`.`#{@column}`"
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_sym
|
16
|
+
@column.to_sym
|
17
|
+
end
|
18
|
+
|
19
|
+
# This method is called to create a equals (=) condition for this column.
|
20
|
+
def eq(value)
|
21
|
+
SqlCondition.new(column: to_s, comparison: '=', value: value)
|
22
|
+
end
|
23
|
+
|
24
|
+
# This method is called to create a not equal (<>) condition for this column.
|
25
|
+
def not_eq(value)
|
26
|
+
SqlCondition.new(column: to_s, comparison: '<>', value: value)
|
27
|
+
end
|
28
|
+
|
29
|
+
# This method is called to create a greater than (>) condition for this column.
|
30
|
+
def gt(value)
|
31
|
+
SqlCondition.new(column: to_s, comparison: '>', value: value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# This method is called to create a greater than or equal (>=) condition for this column.
|
35
|
+
def gte(value)
|
36
|
+
SqlCondition.new(column: to_s, comparison: '>=', value: value)
|
37
|
+
end
|
38
|
+
|
39
|
+
# This method is called to create a less than (<) condition for this column.
|
40
|
+
def lt(value)
|
41
|
+
SqlCondition.new(column: to_s, comparison: '<', value: value)
|
42
|
+
end
|
43
|
+
|
44
|
+
# This method is called to create a less than or equal (<=) condition for this column.
|
45
|
+
def lte(value)
|
46
|
+
SqlCondition.new(column: to_s, comparison: '<=', value: value)
|
47
|
+
end
|
48
|
+
|
49
|
+
# This method is called to generate an alias statement for this column.
|
50
|
+
def as(name)
|
51
|
+
"#{self} as `#{name}`"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MysqlFramework
|
4
|
+
# This class is used to represent a Sql Condition for a column.
|
5
|
+
class SqlCondition
|
6
|
+
# This method is called to get the value of this condition for prepared statements.
|
7
|
+
attr_reader :value
|
8
|
+
|
9
|
+
def initialize(column:, comparison:, value:)
|
10
|
+
@column = column
|
11
|
+
@comparison = comparison
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
15
|
+
# This method is called to get the condition as a string for a sql prepared statement
|
16
|
+
def to_s
|
17
|
+
"#{@column} #{@comparison} ?"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|