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 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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module MysqlFramework
6
+ def self.logger
7
+ return @@logger
8
+ end
9
+
10
+ def self.set_logger(logger)
11
+ @@logger = logger
12
+ end
13
+
14
+ MysqlFramework.set_logger(Logger.new(STDOUT))
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'scripts/base'
4
+ require_relative 'scripts/manager'
5
+ require_relative 'scripts/table'
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlFramework
4
+ module Scripts
5
+ module Table
6
+ def register_table(name)
7
+ MysqlFramework::Scripts::Manager.all_tables << name
8
+ end
9
+ end
10
+ end
11
+ 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