mysql_framework 0.0.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 +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
|