mysql_framework 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/mysql_framework.rb +2 -1
- data/lib/mysql_framework/connector.rb +47 -6
- data/lib/mysql_framework/logger.rb +3 -3
- data/lib/mysql_framework/scripts/base.rb +16 -24
- data/lib/mysql_framework/scripts/manager.rb +36 -29
- data/lib/mysql_framework/version.rb +1 -1
- data/spec/lib/mysql_framework/connector_spec.rb +88 -78
- data/spec/lib/mysql_framework/logger_spec.rb +2 -4
- data/spec/lib/mysql_framework/scripts/base_spec.rb +13 -39
- data/spec/lib/mysql_framework/scripts/manager_spec.rb +20 -31
- data/spec/lib/mysql_framework/sql_column_spec.rb +0 -2
- data/spec/lib/mysql_framework/sql_condition_spec.rb +0 -2
- data/spec/lib/mysql_framework/sql_query_spec.rb +0 -2
- data/spec/lib/mysql_framework/sql_table_spec.rb +0 -2
- data/spec/spec_helper.rb +12 -27
- data/spec/support/fixtures.rb +50 -0
- data/spec/support/procedure.sql +2 -2
- data/spec/support/scripts/create_demo_table.rb +16 -7
- data/spec/support/scripts/create_test_proc.rb +10 -4
- data/spec/support/scripts/create_test_table.rb +11 -5
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: efd0a31b4662bc56a6c491b52d176f7400d26b848c7580593dd3aec47406754e
|
4
|
+
data.tar.gz: 3f76b0e9586b56d5c0293590decdbd2240831ff03f5015c159b440d10b25b59b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b84eb01ebca2b310102b0dc84b4242d464349ac55c9f4f3b9133a6c06a16b73900c71de2b663e77c28553b9ad38b897036fc72344b2bbb969ab71e983dbd0595
|
7
|
+
data.tar.gz: 9be88e92dd00c7b12c6e936b93767badef60c3582599c2c093751f40dd5ba5f02c2e94170bb2cd17c8f8f799be8ebef68ffa23526df818c7b81778ad7d4fc051
|
data/lib/mysql_framework.rb
CHANGED
@@ -3,19 +3,50 @@
|
|
3
3
|
module MysqlFramework
|
4
4
|
class Connector
|
5
5
|
def initialize(options = {})
|
6
|
-
@connection_pool = ::Queue.new
|
7
|
-
|
8
6
|
@options = default_options.merge(options)
|
9
7
|
|
10
8
|
Mysql2::Client.default_query_options.merge!(symbolize_keys: true, cast_booleans: true)
|
11
9
|
end
|
12
10
|
|
13
|
-
# This method is called to
|
14
|
-
|
11
|
+
# This method is called to setup a pool of MySQL connections.
|
12
|
+
def setup
|
13
|
+
@connection_pool = ::Queue.new
|
14
|
+
|
15
|
+
start_pool_size.times { @connection_pool.push(Mysql2::Client.new(@options)) }
|
16
|
+
|
17
|
+
@created_connections = start_pool_size
|
18
|
+
end
|
19
|
+
|
20
|
+
# This method is called to close all MySQL connections in the pool and dispose of the pool itself.
|
21
|
+
def dispose
|
22
|
+
return if @connection_pool.nil?
|
23
|
+
|
24
|
+
until @connection_pool.empty?
|
25
|
+
conn = @connection_pool.pop(true)
|
26
|
+
conn.close
|
27
|
+
end
|
28
|
+
|
29
|
+
@connection_pool = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# This method is called to get the idle connection queue for this connector.
|
33
|
+
def connections
|
34
|
+
@connection_pool
|
35
|
+
end
|
36
|
+
|
37
|
+
# This method is called to fetch a client from the connection pool.
|
15
38
|
def check_out
|
16
39
|
@connection_pool.pop(true)
|
17
|
-
rescue
|
18
|
-
|
40
|
+
rescue ThreadError
|
41
|
+
if @created_connections < max_pool_size
|
42
|
+
conn = Mysql2::Client.new(@options)
|
43
|
+
@created_connections += 1
|
44
|
+
return conn
|
45
|
+
end
|
46
|
+
|
47
|
+
MysqlFramework.logger.error { "[#{self.class}] - Database connection pool depleted." }
|
48
|
+
|
49
|
+
raise 'Database connection pool depleted.'
|
19
50
|
end
|
20
51
|
|
21
52
|
# This method is called to check a client back in to the connection when no longer needed.
|
@@ -70,6 +101,8 @@ module MysqlFramework
|
|
70
101
|
end
|
71
102
|
end
|
72
103
|
|
104
|
+
private
|
105
|
+
|
73
106
|
def default_options
|
74
107
|
{
|
75
108
|
host: ENV.fetch('MYSQL_HOST'),
|
@@ -80,5 +113,13 @@ module MysqlFramework
|
|
80
113
|
reconnect: true
|
81
114
|
}
|
82
115
|
end
|
116
|
+
|
117
|
+
def start_pool_size
|
118
|
+
@start_pool_size ||= Integer(ENV.fetch('MYSQL_START_POOL_SIZE', 1))
|
119
|
+
end
|
120
|
+
|
121
|
+
def max_pool_size
|
122
|
+
@max_pool_size ||= Integer(ENV.fetch('MYSQL_MAX_POOL_SIZE', 5))
|
123
|
+
end
|
83
124
|
end
|
84
125
|
end
|
@@ -4,12 +4,12 @@ require 'logger'
|
|
4
4
|
|
5
5
|
module MysqlFramework
|
6
6
|
def self.logger
|
7
|
-
|
7
|
+
@@logger
|
8
8
|
end
|
9
9
|
|
10
|
-
def self.
|
10
|
+
def self.logger=(logger)
|
11
11
|
@@logger = logger
|
12
12
|
end
|
13
13
|
|
14
|
-
MysqlFramework.
|
14
|
+
MysqlFramework.logger = Logger.new(STDOUT)
|
15
15
|
end
|
@@ -3,31 +3,19 @@
|
|
3
3
|
module MysqlFramework
|
4
4
|
module Scripts
|
5
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
6
|
def identifier
|
15
7
|
raise NotImplementedError if @identifier.nil?
|
16
8
|
@identifier
|
17
9
|
end
|
18
10
|
|
19
|
-
def apply
|
11
|
+
def apply(_client)
|
20
12
|
raise NotImplementedError
|
21
13
|
end
|
22
14
|
|
23
|
-
def rollback
|
15
|
+
def rollback(_client)
|
24
16
|
raise NotImplementedError
|
25
17
|
end
|
26
18
|
|
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
19
|
def self.descendants
|
32
20
|
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
33
21
|
end
|
@@ -36,22 +24,26 @@ module MysqlFramework
|
|
36
24
|
[]
|
37
25
|
end
|
38
26
|
|
39
|
-
def update_procedure(proc_name, proc_file)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
SQL
|
27
|
+
def update_procedure(client, proc_name, proc_file)
|
28
|
+
client.query(<<~SQL)
|
29
|
+
DROP PROCEDURE IF EXISTS #{proc_name};
|
30
|
+
SQL
|
44
31
|
|
45
|
-
|
32
|
+
proc_sql = File.read(proc_file)
|
46
33
|
|
47
|
-
|
48
|
-
|
34
|
+
client.query(proc_sql)
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def generate_partition_sql
|
40
|
+
(1..partitions).each_with_index.map { |_, i| "PARTITION p#{i} VALUES IN (#{i})" }.join(",\n\t")
|
49
41
|
end
|
50
42
|
|
51
43
|
private
|
52
44
|
|
53
|
-
def
|
54
|
-
@
|
45
|
+
def partitions
|
46
|
+
@partitions ||= Integer(ENV.fetch('MYSQL_PARTITIONS', '500'))
|
55
47
|
end
|
56
48
|
end
|
57
49
|
end
|
@@ -3,40 +3,48 @@
|
|
3
3
|
module MysqlFramework
|
4
4
|
module Scripts
|
5
5
|
class Manager
|
6
|
+
def initialize(mysql_connector)
|
7
|
+
@mysql_connector = mysql_connector
|
8
|
+
end
|
9
|
+
|
6
10
|
def execute
|
7
|
-
lock_manager.lock(self.class,
|
11
|
+
lock_manager.lock(self.class, migration_ttl) do |locked|
|
8
12
|
raise unless locked
|
9
13
|
|
10
14
|
initialize_script_history
|
11
15
|
|
12
16
|
last_executed_script = retrieve_last_executed_script
|
13
17
|
|
14
|
-
mysql_connector.transaction do
|
18
|
+
mysql_connector.transaction do |client|
|
15
19
|
pending_scripts = calculate_pending_scripts(last_executed_script)
|
16
|
-
MysqlFramework.logger.info
|
20
|
+
MysqlFramework.logger.info do
|
21
|
+
"[#{self.class}] - #{pending_scripts.length} pending data store scripts found."
|
22
|
+
end
|
17
23
|
|
18
|
-
pending_scripts.each { |script| apply(script) }
|
24
|
+
pending_scripts.each { |script| apply(script, client) }
|
19
25
|
end
|
20
26
|
|
21
|
-
MysqlFramework.logger.
|
27
|
+
MysqlFramework.logger.debug { "[#{self.class}] - Migration script execution complete." }
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
25
31
|
def apply_by_tag(tags)
|
26
|
-
lock_manager.lock(self.class,
|
32
|
+
lock_manager.lock(self.class, migration_ttl) do |locked|
|
27
33
|
raise unless locked
|
28
34
|
|
29
35
|
initialize_script_history
|
30
36
|
|
31
|
-
mysql_connector.transaction do
|
37
|
+
mysql_connector.transaction do |client|
|
32
38
|
pending_scripts = calculate_pending_scripts(0)
|
33
|
-
MysqlFramework.logger.info
|
39
|
+
MysqlFramework.logger.info do
|
40
|
+
"[#{self.class}] - #{pending_scripts.length} pending data store scripts found."
|
41
|
+
end
|
34
42
|
|
35
43
|
pending_scripts.reject { |script| (script.tags & tags).empty? }.sort_by(&:identifier)
|
36
|
-
.each { |script| apply(script) }
|
44
|
+
.each { |script| apply(script, client) }
|
37
45
|
end
|
38
46
|
|
39
|
-
MysqlFramework.logger.
|
47
|
+
MysqlFramework.logger.debug { "[#{self.class}] - Migration script execution complete." }
|
40
48
|
end
|
41
49
|
end
|
42
50
|
|
@@ -46,10 +54,10 @@ module MysqlFramework
|
|
46
54
|
end
|
47
55
|
|
48
56
|
def retrieve_last_executed_script
|
49
|
-
MysqlFramework.logger.
|
57
|
+
MysqlFramework.logger.debug { "[#{self.class}] - Retrieving last executed script from history." }
|
50
58
|
|
51
59
|
result = mysql_connector.query(<<~SQL)
|
52
|
-
SELECT `identifier` FROM
|
60
|
+
SELECT `identifier` FROM `#{migration_table_name}` ORDER BY `identifier` DESC
|
53
61
|
SQL
|
54
62
|
|
55
63
|
if result.each.to_a.length.zero?
|
@@ -60,10 +68,10 @@ module MysqlFramework
|
|
60
68
|
end
|
61
69
|
|
62
70
|
def initialize_script_history
|
63
|
-
MysqlFramework.logger.
|
71
|
+
MysqlFramework.logger.debug { "[#{self.class}] - Initializing script history." }
|
64
72
|
|
65
73
|
mysql_connector.query(<<~SQL)
|
66
|
-
CREATE TABLE IF NOT EXISTS
|
74
|
+
CREATE TABLE IF NOT EXISTS `#{migration_table_name}` (
|
67
75
|
`identifier` CHAR(15) NOT NULL,
|
68
76
|
`timestamp` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
69
77
|
PRIMARY KEY (`identifier`),
|
@@ -73,10 +81,9 @@ module MysqlFramework
|
|
73
81
|
end
|
74
82
|
|
75
83
|
def calculate_pending_scripts(last_executed_script)
|
76
|
-
MysqlFramework.logger.
|
84
|
+
MysqlFramework.logger.debug { "[#{self.class}] - Calculating pending data store scripts." }
|
77
85
|
|
78
|
-
|
79
|
-
.select { |script| script.identifier > last_executed_script }.sort_by(&:identifier)
|
86
|
+
migrations.map(&:new).select { |script| script.identifier > last_executed_script }.sort_by(&:identifier)
|
80
87
|
end
|
81
88
|
|
82
89
|
def table_exists?(table_name)
|
@@ -92,7 +99,7 @@ module MysqlFramework
|
|
92
99
|
|
93
100
|
def drop_table(table_name)
|
94
101
|
mysql_connector.query(<<~SQL)
|
95
|
-
DROP TABLE IF EXISTS
|
102
|
+
DROP TABLE IF EXISTS `#{table_name}`
|
96
103
|
SQL
|
97
104
|
end
|
98
105
|
|
@@ -106,30 +113,30 @@ module MysqlFramework
|
|
106
113
|
|
107
114
|
private
|
108
115
|
|
109
|
-
|
110
|
-
@mysql_connector ||= MysqlFramework::Connector.new
|
111
|
-
end
|
116
|
+
attr_reader :mysql_connector
|
112
117
|
|
113
118
|
def lock_manager
|
114
119
|
@lock_manager ||= Redlock::Client.new([ENV.fetch('REDIS_URL')])
|
115
120
|
end
|
116
121
|
|
117
|
-
def
|
118
|
-
@
|
122
|
+
def migration_ttl
|
123
|
+
@migration_ttl ||= ENV.fetch('MYSQL_MIGRATION_LOCK_TTL', 2000)
|
119
124
|
end
|
120
125
|
|
121
126
|
def migration_table_name
|
122
|
-
|
127
|
+
@migration_table_name ||= ENV.fetch('MYSQL_MIGRATION_TABLE', 'migration_script_history')
|
128
|
+
end
|
123
129
|
|
124
|
-
|
130
|
+
def migrations
|
131
|
+
@migrations ||= MysqlFramework::Scripts::Base.descendants
|
125
132
|
end
|
126
133
|
|
127
|
-
def apply(script)
|
134
|
+
def apply(script, client)
|
128
135
|
MysqlFramework.logger.info { "[#{self.class}] - Applying script: #{script}." }
|
129
136
|
|
130
|
-
script.apply
|
131
|
-
|
132
|
-
INSERT INTO
|
137
|
+
script.apply(client)
|
138
|
+
client.query(<<~SQL)
|
139
|
+
INSERT INTO `#{migration_table_name}` (`identifier`, `timestamp`) VALUES ('#{script.identifier}', NOW())
|
133
140
|
SQL
|
134
141
|
end
|
135
142
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'spec_helper'
|
4
|
-
|
5
3
|
describe MysqlFramework::Connector do
|
4
|
+
let(:start_pool_size) { Integer(ENV.fetch('MYSQL_START_POOL_SIZE')) }
|
5
|
+
let(:max_pool_size) { Integer(ENV.fetch('MYSQL_MAX_POOL_SIZE')) }
|
6
6
|
let(:default_options) do
|
7
7
|
{
|
8
8
|
host: ENV.fetch('MYSQL_HOST'),
|
@@ -15,26 +15,28 @@ describe MysqlFramework::Connector do
|
|
15
15
|
end
|
16
16
|
let(:options) do
|
17
17
|
{
|
18
|
-
host: '
|
19
|
-
port: '
|
20
|
-
database: '
|
21
|
-
username: '
|
22
|
-
password: '
|
23
|
-
reconnect:
|
18
|
+
host: ENV.fetch('MYSQL_HOST'),
|
19
|
+
port: ENV.fetch('MYSQL_PORT'),
|
20
|
+
database: "#{ENV.fetch('MYSQL_DATABASE')}_2",
|
21
|
+
username: ENV.fetch('MYSQL_USERNAME'),
|
22
|
+
password: ENV.fetch('MYSQL_PASSWORD'),
|
23
|
+
reconnect: false
|
24
24
|
}
|
25
25
|
end
|
26
|
-
let(:client) { double }
|
26
|
+
let(:client) { double(close: true) }
|
27
27
|
let(:gems) { MysqlFramework::SqlTable.new('gems') }
|
28
28
|
let(:existing_client) { Mysql2::Client.new(default_options) }
|
29
29
|
|
30
30
|
subject { described_class.new }
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
subject
|
32
|
+
before(:each) { subject.setup }
|
33
|
+
after(:each) { subject.dispose }
|
35
34
|
|
36
|
-
|
37
|
-
|
35
|
+
describe '#initialize' do
|
36
|
+
context 'when options are not provided' do
|
37
|
+
it 'returns the default options' do
|
38
|
+
expect(subject.instance_variable_get(:@options)).to eq(default_options)
|
39
|
+
end
|
38
40
|
end
|
39
41
|
|
40
42
|
context 'when options are provided' do
|
@@ -44,26 +46,82 @@ describe MysqlFramework::Connector do
|
|
44
46
|
expect(subject.instance_variable_get(:@options)).to eq(options)
|
45
47
|
end
|
46
48
|
end
|
49
|
+
|
50
|
+
it 'sets default query options on the Mysql2 client' do
|
51
|
+
subject
|
52
|
+
|
53
|
+
expect(Mysql2::Client.default_query_options[:symbolize_keys]).to eq(true)
|
54
|
+
expect(Mysql2::Client.default_query_options[:cast_booleans]).to eq(true)
|
55
|
+
end
|
47
56
|
end
|
48
57
|
|
49
|
-
describe '#
|
50
|
-
it '
|
51
|
-
|
52
|
-
|
58
|
+
describe '#setup' do
|
59
|
+
it 'creates a connection pool with the specified number of conections' do
|
60
|
+
subject.setup
|
61
|
+
|
62
|
+
expect(subject.connections.length).to eq(start_pool_size)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#dispose' do
|
67
|
+
before do
|
68
|
+
subject.connections.clear
|
69
|
+
subject.connections.push(client)
|
53
70
|
end
|
54
71
|
|
55
|
-
|
72
|
+
it 'closes the idle connections and disposes of the queue' do
|
73
|
+
expect(client).to receive(:close)
|
74
|
+
|
75
|
+
subject.dispose
|
76
|
+
|
77
|
+
expect(subject.connections).to be_nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe '#check_out' do
|
82
|
+
context 'when there are available connections' do
|
83
|
+
before do
|
84
|
+
subject.connections.clear
|
85
|
+
subject.connections.push(client)
|
86
|
+
end
|
87
|
+
|
56
88
|
it 'returns a client instance from the pool' do
|
57
|
-
subject.
|
89
|
+
expect(subject.check_out).to eq(client)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "when there are no available connections, and the pool's max size has not been reached" do
|
94
|
+
before do
|
95
|
+
subject.connections.clear
|
96
|
+
subject.connections.push(client)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'instantiates a new connection and returns it' do
|
100
|
+
subject.check_out
|
58
101
|
|
102
|
+
expect(Mysql2::Client).to receive(:new).with(default_options).and_return(client)
|
59
103
|
expect(subject.check_out).to eq(client)
|
60
104
|
end
|
61
105
|
end
|
106
|
+
|
107
|
+
context "when there are no available connections, and the pool's max size has been reached" do
|
108
|
+
before do
|
109
|
+
subject.connections.clear
|
110
|
+
subject.instance_variable_set(:@created_connections, 5)
|
111
|
+
|
112
|
+
5.times { subject.check_in(client) }
|
113
|
+
5.times { subject.check_out }
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'throws a RuntimeError' do
|
117
|
+
expect { subject.check_out }.to raise_error(RuntimeError)
|
118
|
+
end
|
119
|
+
end
|
62
120
|
end
|
63
121
|
|
64
122
|
describe '#check_in' do
|
65
123
|
it 'returns the provided client to the connection pool' do
|
66
|
-
expect(subject.
|
124
|
+
expect(subject.connections).to receive(:push).with(client)
|
67
125
|
|
68
126
|
subject.check_in(client)
|
69
127
|
end
|
@@ -84,20 +142,8 @@ describe MysqlFramework::Connector do
|
|
84
142
|
describe '#execute' do
|
85
143
|
let(:insert_query) do
|
86
144
|
MysqlFramework::SqlQuery.new.insert(gems)
|
87
|
-
.into(
|
88
|
-
|
89
|
-
gems[:name],
|
90
|
-
gems[:author],
|
91
|
-
gems[:created_at],
|
92
|
-
gems[:updated_at]
|
93
|
-
)
|
94
|
-
.values(
|
95
|
-
SecureRandom.uuid,
|
96
|
-
'mysql_framework',
|
97
|
-
'sage',
|
98
|
-
Time.now,
|
99
|
-
Time.now
|
100
|
-
)
|
145
|
+
.into(gems[:id], gems[:name], gems[:author], gems[:created_at], gems[:updated_at])
|
146
|
+
.values(SecureRandom.uuid, 'mysql_framework', 'sage', Time.now, Time.now)
|
101
147
|
end
|
102
148
|
|
103
149
|
it 'executes the query with parameters' do
|
@@ -139,43 +185,14 @@ describe MysqlFramework::Connector do
|
|
139
185
|
end
|
140
186
|
|
141
187
|
describe '#query_multiple_results' do
|
142
|
-
let(:test) { MysqlFramework::SqlTable.new('test') }
|
143
|
-
let(:manager) { MysqlFramework::Scripts::Manager.new }
|
144
|
-
let(:connector) { MysqlFramework::Connector.new }
|
145
|
-
let(:timestamp) { Time.at(628_232_400) } # 1989-11-28 00:00:00 -0500
|
146
|
-
let(:guid) { 'a3ccb138-48ae-437a-be52-f673beb12b51' }
|
147
|
-
let(:insert) do
|
148
|
-
MysqlFramework::SqlQuery.new.insert(test)
|
149
|
-
.into(test[:id], test[:name], test[:action], test[:created_at], test[:updated_at])
|
150
|
-
.values(guid, 'name', 'action', timestamp, timestamp)
|
151
|
-
end
|
152
|
-
let(:obj) do
|
153
|
-
{
|
154
|
-
id: guid,
|
155
|
-
name: 'name',
|
156
|
-
action: 'action',
|
157
|
-
created_at: timestamp,
|
158
|
-
updated_at: timestamp
|
159
|
-
}
|
160
|
-
end
|
161
|
-
|
162
|
-
before :each do
|
163
|
-
manager.initialize_script_history
|
164
|
-
manager.execute
|
165
|
-
|
166
|
-
connector.execute(insert)
|
167
|
-
end
|
168
|
-
|
169
|
-
after(:each) { manager.drop_all_tables }
|
170
|
-
|
171
188
|
it 'returns the results from the stored procedure' do
|
172
189
|
query = 'call test_procedure'
|
173
190
|
result = subject.query_multiple_results(query)
|
174
191
|
|
175
192
|
expect(result).to be_a(Array)
|
176
193
|
expect(result.length).to eq(2)
|
177
|
-
expect(result[0]).to eq(
|
178
|
-
expect(result[1]).to eq(
|
194
|
+
expect(result[0].length).to eq(0)
|
195
|
+
expect(result[1].length).to eq(4)
|
179
196
|
end
|
180
197
|
|
181
198
|
it 'does not check out a new client when one is provided' do
|
@@ -186,8 +203,8 @@ describe MysqlFramework::Connector do
|
|
186
203
|
|
187
204
|
expect(result).to be_a(Array)
|
188
205
|
expect(result.length).to eq(2)
|
189
|
-
expect(result[0]).to eq(
|
190
|
-
expect(result[1]).to eq(
|
206
|
+
expect(result[0].length).to eq(0)
|
207
|
+
expect(result[1].length).to eq(4)
|
191
208
|
end
|
192
209
|
end
|
193
210
|
|
@@ -208,18 +225,11 @@ describe MysqlFramework::Connector do
|
|
208
225
|
expect(client).to receive(:query).with('ROLLBACK')
|
209
226
|
|
210
227
|
begin
|
211
|
-
subject.transaction
|
212
|
-
|
213
|
-
|
214
|
-
rescue StandardError
|
228
|
+
subject.transaction { raise }
|
229
|
+
rescue StandardError => e
|
230
|
+
e.message
|
215
231
|
end
|
216
232
|
end
|
217
233
|
end
|
218
234
|
end
|
219
|
-
|
220
|
-
describe '#default_options' do
|
221
|
-
it 'returns the default options' do
|
222
|
-
expect(subject.default_options).to eq(default_options)
|
223
|
-
end
|
224
|
-
end
|
225
235
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'spec_helper'
|
4
|
-
|
5
3
|
describe MysqlFramework do
|
6
4
|
describe 'logger' do
|
7
5
|
it 'returns the logger' do
|
@@ -9,11 +7,11 @@ describe MysqlFramework do
|
|
9
7
|
end
|
10
8
|
end
|
11
9
|
|
12
|
-
describe '
|
10
|
+
describe 'logger=' do
|
13
11
|
let(:logger) { Logger.new(STDOUT) }
|
14
12
|
|
15
13
|
it 'sets the logger' do
|
16
|
-
subject.
|
14
|
+
subject.logger = logger
|
17
15
|
expect(subject.logger).to eq(logger)
|
18
16
|
end
|
19
17
|
end
|
@@ -1,25 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'spec_helper'
|
4
|
-
|
5
3
|
describe MysqlFramework::Scripts::Base do
|
6
|
-
|
4
|
+
let(:client) { double }
|
7
5
|
|
8
|
-
|
9
|
-
it 'returns the number of paritions' do
|
10
|
-
expect(subject.partitions).to eq(5)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
describe '#database_name' do
|
15
|
-
it 'returns the database name' do
|
16
|
-
expect(subject.database_name).to eq('test_database')
|
17
|
-
end
|
18
|
-
end
|
6
|
+
subject { described_class.new }
|
19
7
|
|
20
8
|
describe '#identifier' do
|
21
9
|
it 'throws a NotImplementedError' do
|
22
|
-
expect{ subject.identifier }.to raise_error(NotImplementedError)
|
10
|
+
expect { subject.identifier }.to raise_error(NotImplementedError)
|
23
11
|
end
|
24
12
|
|
25
13
|
context 'when @identifier is set' do
|
@@ -32,29 +20,24 @@ describe MysqlFramework::Scripts::Base do
|
|
32
20
|
|
33
21
|
describe '#apply' do
|
34
22
|
it 'throws a NotImplementedError' do
|
35
|
-
expect{ subject.apply }.to raise_error(NotImplementedError)
|
23
|
+
expect { subject.apply(client) }.to raise_error(NotImplementedError)
|
36
24
|
end
|
37
25
|
end
|
38
26
|
|
39
27
|
describe '#rollback' do
|
40
28
|
it 'throws a NotImplementedError' do
|
41
|
-
expect{ subject.rollback }.to raise_error(NotImplementedError)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
describe '#generate_partition_sql' do
|
46
|
-
it 'generates the partition sql statement' do
|
47
|
-
expected = "PARTITION p0 VALUES IN (0),\n\tPARTITION p1 VALUES IN (1),\n\tPARTITION p2 VALUES IN (2),\n\tPARTITION p3 VALUES IN (3),\n\tPARTITION p4 VALUES IN (4)"
|
48
|
-
expect(subject.generate_partition_sql).to eq(expected)
|
29
|
+
expect { subject.rollback(client) }.to raise_error(NotImplementedError)
|
49
30
|
end
|
50
31
|
end
|
51
32
|
|
52
33
|
describe '.descendants' do
|
53
34
|
it 'returns all descendant classes' do
|
54
35
|
expect(described_class.descendants.length).to eq(3)
|
55
|
-
expect(described_class.descendants).to include(
|
56
|
-
|
57
|
-
|
36
|
+
expect(described_class.descendants).to include(
|
37
|
+
MysqlFramework::Support::Scripts::CreateTestTable,
|
38
|
+
MysqlFramework::Support::Scripts::CreateDemoTable,
|
39
|
+
MysqlFramework::Support::Scripts::CreateTestProc
|
40
|
+
)
|
58
41
|
end
|
59
42
|
end
|
60
43
|
|
@@ -65,7 +48,6 @@ describe MysqlFramework::Scripts::Base do
|
|
65
48
|
end
|
66
49
|
|
67
50
|
describe '#update_procedure' do
|
68
|
-
let(:connector) { MysqlFramework::Connector.new }
|
69
51
|
let(:proc_file_path) { 'spec/support/procedure.sql' }
|
70
52
|
let(:drop_sql) do
|
71
53
|
<<~SQL
|
@@ -73,19 +55,11 @@ describe MysqlFramework::Scripts::Base do
|
|
73
55
|
SQL
|
74
56
|
end
|
75
57
|
|
76
|
-
before :each do
|
77
|
-
subject.instance_variable_set(:@mysql_connector, connector)
|
78
|
-
end
|
79
|
-
|
80
58
|
it 'drops and then creates the named procedure' do
|
81
|
-
expect(
|
82
|
-
expect(
|
83
|
-
subject.update_procedure('test_procedure', proc_file_path)
|
84
|
-
end
|
59
|
+
expect(client).to receive(:query).with(drop_sql).once
|
60
|
+
expect(client).to receive(:query).with(File.read(proc_file_path)).once
|
85
61
|
|
86
|
-
|
87
|
-
expect(connector).to receive(:transaction)
|
88
|
-
subject.update_procedure('test_procedure', proc_file_path)
|
62
|
+
subject.update_procedure(client, 'test_procedure', proc_file_path)
|
89
63
|
end
|
90
64
|
end
|
91
65
|
end
|
@@ -1,18 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'spec_helper'
|
4
|
-
|
5
3
|
describe MysqlFramework::Scripts::Manager do
|
6
|
-
let(:connector)
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
let(:connector) do
|
5
|
+
connector = MysqlFramework::Connector.new
|
6
|
+
connector.setup
|
7
|
+
connector
|
10
8
|
end
|
11
9
|
|
10
|
+
subject { described_class.new(connector) }
|
11
|
+
|
12
12
|
describe '#execute' do
|
13
|
-
before
|
13
|
+
before(:each) do
|
14
|
+
subject.drop_all_tables
|
14
15
|
subject.initialize_script_history
|
15
16
|
end
|
17
|
+
after(:each) { subject.drop_all_tables }
|
16
18
|
|
17
19
|
it 'executes all pending scripts' do
|
18
20
|
expect(subject.table_exists?('demo')).to eq(false)
|
@@ -23,16 +25,14 @@ describe MysqlFramework::Scripts::Manager do
|
|
23
25
|
expect(subject.table_exists?('demo')).to eq(true)
|
24
26
|
expect(subject.table_exists?('test')).to eq(true)
|
25
27
|
end
|
26
|
-
|
27
|
-
after :each do
|
28
|
-
subject.drop_all_tables
|
29
|
-
end
|
30
28
|
end
|
31
29
|
|
32
30
|
describe '#apply_by_tag' do
|
33
|
-
before
|
31
|
+
before(:each) do
|
32
|
+
subject.drop_all_tables
|
34
33
|
subject.initialize_script_history
|
35
34
|
end
|
35
|
+
after(:each) { subject.drop_all_tables }
|
36
36
|
|
37
37
|
it 'executes all pending scripts that match the tag' do
|
38
38
|
expect(subject.table_exists?('demo')).to eq(false)
|
@@ -43,10 +43,6 @@ describe MysqlFramework::Scripts::Manager do
|
|
43
43
|
expect(subject.table_exists?('demo')).to eq(false)
|
44
44
|
expect(subject.table_exists?('test')).to eq(true)
|
45
45
|
end
|
46
|
-
|
47
|
-
after :each do
|
48
|
-
subject.drop_all_tables
|
49
|
-
end
|
50
46
|
end
|
51
47
|
|
52
48
|
describe '#drop_all_tables' do
|
@@ -60,9 +56,7 @@ describe MysqlFramework::Scripts::Manager do
|
|
60
56
|
end
|
61
57
|
|
62
58
|
describe '#retrieve_last_executed_script' do
|
63
|
-
before
|
64
|
-
subject.initialize_script_history
|
65
|
-
end
|
59
|
+
before(:each) { subject.initialize_script_history }
|
66
60
|
|
67
61
|
context 'when no scripts have been executed' do
|
68
62
|
it 'returns 0' do
|
@@ -71,18 +65,13 @@ describe MysqlFramework::Scripts::Manager do
|
|
71
65
|
end
|
72
66
|
|
73
67
|
context 'when scripts have been executed previously' do
|
74
|
-
before
|
75
|
-
|
76
|
-
end
|
68
|
+
before(:each) { subject.apply_by_tag([MysqlFramework::Support::Tables::TestTable::NAME]) }
|
69
|
+
after(:each) { subject.drop_script_history }
|
77
70
|
|
78
71
|
it 'returns the last executed script' do
|
79
72
|
expect(subject.retrieve_last_executed_script).to eq(201807031200)
|
80
73
|
end
|
81
74
|
end
|
82
|
-
|
83
|
-
after :each do
|
84
|
-
subject.drop_script_history
|
85
|
-
end
|
86
75
|
end
|
87
76
|
|
88
77
|
describe '#initialize_script_history' do
|
@@ -131,7 +120,7 @@ describe MysqlFramework::Scripts::Manager do
|
|
131
120
|
describe '#drop_script_history' do
|
132
121
|
it 'drops the migration script history table' do
|
133
122
|
query = <<~SQL
|
134
|
-
DROP TABLE IF EXISTS `#{ENV.fetch('
|
123
|
+
DROP TABLE IF EXISTS `#{ENV.fetch('MYSQL_MIGRATION_TABLE', 'migration_script_history')}`
|
135
124
|
SQL
|
136
125
|
expect(connector).to receive(:query).with(query)
|
137
126
|
subject.drop_script_history
|
@@ -141,21 +130,21 @@ describe MysqlFramework::Scripts::Manager do
|
|
141
130
|
describe '#drop_table' do
|
142
131
|
it 'drops the given table' do
|
143
132
|
expect(connector).to receive(:query).with(<<~SQL)
|
144
|
-
DROP TABLE IF EXISTS `
|
133
|
+
DROP TABLE IF EXISTS `some_table`
|
145
134
|
SQL
|
146
|
-
subject.drop_table('
|
135
|
+
subject.drop_table('some_table')
|
147
136
|
end
|
148
137
|
end
|
149
138
|
|
150
139
|
describe '#all_tables' do
|
151
140
|
it 'returns all registered tables' do
|
152
|
-
expect(subject.all_tables).to eq(
|
141
|
+
expect(subject.all_tables).to eq(%w(test demo))
|
153
142
|
end
|
154
143
|
end
|
155
144
|
|
156
145
|
describe '.all_tables' do
|
157
146
|
it 'stores a class level array of tables' do
|
158
|
-
expect(described_class.all_tables).to eq(
|
147
|
+
expect(described_class.all_tables).to eq(%w(test demo))
|
159
148
|
end
|
160
149
|
end
|
161
150
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -6,12 +6,18 @@ SimpleCov.start do
|
|
6
6
|
add_filter 'spec/'
|
7
7
|
end
|
8
8
|
|
9
|
-
ENV['
|
10
|
-
|
9
|
+
ENV['RACK_ENV'] = 'test'
|
10
|
+
|
11
|
+
ENV['MYSQL_START_POOL_SIZE'] ||= '1'
|
12
|
+
ENV['MYSQL_MAX_POOL_SIZE'] ||= '5'
|
11
13
|
ENV['MYSQL_PARTITIONS'] ||= '5'
|
12
|
-
|
14
|
+
|
15
|
+
ENV['MYSQL_HOST'] ||= '127.0.0.1'
|
13
16
|
ENV['MYSQL_PORT'] ||= '3306'
|
17
|
+
ENV['MYSQL_DATABASE'] ||= 'test_database'
|
14
18
|
ENV['MYSQL_USERNAME'] ||= 'root'
|
19
|
+
ENV['MYSQL_PASSWORD'] ||= ''
|
20
|
+
|
15
21
|
ENV['REDIS_URL'] ||= 'redis://127.0.0.1:6379'
|
16
22
|
|
17
23
|
require 'bundler'
|
@@ -23,10 +29,11 @@ require_relative 'support/scripts/create_demo_table'
|
|
23
29
|
require_relative 'support/scripts/create_test_proc'
|
24
30
|
require_relative 'support/tables/test'
|
25
31
|
require_relative 'support/tables/demo'
|
32
|
+
require_relative 'support/fixtures'
|
26
33
|
|
27
|
-
|
28
|
-
config.before(:each) { MysqlFramework.logger.level = Logger::ERROR }
|
34
|
+
MysqlFramework::Support::Fixtures.execute
|
29
35
|
|
36
|
+
RSpec.configure do |config|
|
30
37
|
config.expect_with :rspec do |expectations|
|
31
38
|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
32
39
|
end
|
@@ -35,25 +42,3 @@ RSpec.configure do |config|
|
|
35
42
|
mocks.verify_partial_doubles = true
|
36
43
|
end
|
37
44
|
end
|
38
|
-
|
39
|
-
client = Mysql2::Client.new(
|
40
|
-
host: ENV.fetch('MYSQL_HOST'),
|
41
|
-
port: ENV.fetch('MYSQL_PORT'),
|
42
|
-
username: ENV.fetch('MYSQL_USERNAME'),
|
43
|
-
password: ENV.fetch('MYSQL_PASSWORD')
|
44
|
-
)
|
45
|
-
client.query("DROP DATABASE IF EXISTS `#{ENV.fetch('MYSQL_DATABASE')}`;")
|
46
|
-
client.query("CREATE DATABASE `#{ENV.fetch('MYSQL_DATABASE')}`;")
|
47
|
-
|
48
|
-
connector = MysqlFramework::Connector.new
|
49
|
-
connector.query("DROP TABLE IF EXISTS `#{ENV.fetch('MYSQL_DATABASE')}`.`gems`")
|
50
|
-
connector.query(<<~SQL)
|
51
|
-
CREATE TABLE `#{ENV.fetch('MYSQL_DATABASE')}`.`gems` (
|
52
|
-
`id` CHAR(36) NOT NULL,
|
53
|
-
`name` VARCHAR(255) NULL,
|
54
|
-
`author` VARCHAR(255) NULL,
|
55
|
-
`created_at` DATETIME,
|
56
|
-
`updated_at` DATETIME,
|
57
|
-
PRIMARY KEY (`id`)
|
58
|
-
)
|
59
|
-
SQL
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MysqlFramework
|
4
|
+
module Support
|
5
|
+
class Fixtures
|
6
|
+
def self.execute
|
7
|
+
connector = MysqlFramework::Connector.new(
|
8
|
+
host: ENV.fetch('MYSQL_HOST'),
|
9
|
+
port: ENV.fetch('MYSQL_PORT'),
|
10
|
+
database: nil,
|
11
|
+
username: ENV.fetch('MYSQL_USERNAME'),
|
12
|
+
password: ENV.fetch('MYSQL_PASSWORD')
|
13
|
+
)
|
14
|
+
connector.setup
|
15
|
+
|
16
|
+
client = connector.check_out
|
17
|
+
|
18
|
+
client.query("DROP DATABASE IF EXISTS `#{ENV.fetch('MYSQL_DATABASE')}`;")
|
19
|
+
client.query("DROP DATABASE IF EXISTS `#{ENV.fetch('MYSQL_DATABASE')}_2`;")
|
20
|
+
client.query("CREATE DATABASE `#{ENV.fetch('MYSQL_DATABASE')}`;")
|
21
|
+
client.query("CREATE DATABASE `#{ENV.fetch('MYSQL_DATABASE')}_2`;")
|
22
|
+
client.query("USE `#{ENV.fetch('MYSQL_DATABASE')}`;")
|
23
|
+
client.query(<<~SQL)
|
24
|
+
CREATE TABLE `gems` (
|
25
|
+
`id` CHAR(36) NOT NULL,
|
26
|
+
`name` VARCHAR(255) NULL,
|
27
|
+
`author` VARCHAR(255) NULL,
|
28
|
+
`created_at` DATETIME,
|
29
|
+
`updated_at` DATETIME,
|
30
|
+
PRIMARY KEY (`id`)
|
31
|
+
)
|
32
|
+
SQL
|
33
|
+
client.query(<<~SQL)
|
34
|
+
INSERT INTO `gems`
|
35
|
+
(`id`, `name`, `author`, `created_at`, `updated_at`)
|
36
|
+
VALUES
|
37
|
+
('#{SecureRandom.uuid}', 'mysql_framework', 'Sage', NOW(), NOW()),
|
38
|
+
('#{SecureRandom.uuid}', 'sinject', 'Sage', NOW(), NOW())
|
39
|
+
SQL
|
40
|
+
|
41
|
+
connector.check_in(client)
|
42
|
+
|
43
|
+
manager = MysqlFramework::Scripts::Manager.new(connector)
|
44
|
+
manager.execute
|
45
|
+
|
46
|
+
connector.dispose
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/spec/support/procedure.sql
CHANGED
@@ -8,25 +8,34 @@ module MysqlFramework
|
|
8
8
|
@identifier = 201806021520 # 15:20 02/06/2018
|
9
9
|
end
|
10
10
|
|
11
|
-
def apply
|
12
|
-
|
13
|
-
CREATE TABLE IF NOT EXISTS `#{
|
11
|
+
def apply(client)
|
12
|
+
client.query(<<~SQL)
|
13
|
+
CREATE TABLE IF NOT EXISTS `#{table_name}` (
|
14
14
|
`id` CHAR(36) NOT NULL,
|
15
15
|
`name` VARCHAR(255) NULL,
|
16
16
|
`created_at` DATETIME NOT NULL,
|
17
|
-
|
18
17
|
`updated_at` DATETIME NOT NULL,
|
19
|
-
|
18
|
+
`partition` INT NOT NULL,
|
19
|
+
PRIMARY KEY (`id`, `partition`)
|
20
|
+
)
|
21
|
+
PARTITION BY LIST(`partition`) (
|
22
|
+
#{generate_partition_sql}
|
20
23
|
)
|
21
24
|
SQL
|
22
25
|
end
|
23
26
|
|
24
|
-
def rollback
|
27
|
+
def rollback(_client)
|
25
28
|
raise 'Rollback not supported in test.'
|
26
29
|
end
|
27
30
|
|
28
31
|
def tags
|
29
|
-
[
|
32
|
+
[table_name]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def table_name
|
38
|
+
MysqlFramework::Support::Tables::DemoTable::NAME
|
30
39
|
end
|
31
40
|
end
|
32
41
|
end
|
@@ -10,16 +10,22 @@ module MysqlFramework
|
|
10
10
|
|
11
11
|
PROC_FILE = 'spec/support/procedure.sql'
|
12
12
|
|
13
|
-
def apply
|
14
|
-
update_procedure('test_procedure', PROC_FILE)
|
13
|
+
def apply(client)
|
14
|
+
update_procedure(client, 'test_procedure', PROC_FILE)
|
15
15
|
end
|
16
16
|
|
17
|
-
def rollback
|
17
|
+
def rollback(_client)
|
18
18
|
raise 'Rollback not supported in test.'
|
19
19
|
end
|
20
20
|
|
21
21
|
def tags
|
22
|
-
[
|
22
|
+
[table_name, 'TestProc']
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def table_name
|
28
|
+
MysqlFramework::Support::Tables::TestTable::NAME
|
23
29
|
end
|
24
30
|
end
|
25
31
|
end
|
@@ -8,9 +8,9 @@ module MysqlFramework
|
|
8
8
|
@identifier = 201801011030 # 10:30 01/01/2018
|
9
9
|
end
|
10
10
|
|
11
|
-
def apply
|
12
|
-
|
13
|
-
CREATE TABLE IF NOT EXISTS `#{
|
11
|
+
def apply(client)
|
12
|
+
client.query(<<~SQL)
|
13
|
+
CREATE TABLE IF NOT EXISTS `#{table_name}` (
|
14
14
|
`id` CHAR(36) NOT NULL,
|
15
15
|
`name` VARCHAR(255) NULL,
|
16
16
|
`action` VARCHAR(255) NULL,
|
@@ -21,12 +21,18 @@ module MysqlFramework
|
|
21
21
|
SQL
|
22
22
|
end
|
23
23
|
|
24
|
-
def rollback
|
24
|
+
def rollback(_client)
|
25
25
|
raise 'Rollback not supported in test.'
|
26
26
|
end
|
27
27
|
|
28
28
|
def tags
|
29
|
-
[
|
29
|
+
[table_name]
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def table_name
|
35
|
+
MysqlFramework::Support::Tables::TestTable::NAME
|
30
36
|
end
|
31
37
|
end
|
32
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mysql_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sage
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-07-
|
11
|
+
date: 2018-07-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -124,6 +124,7 @@ files:
|
|
124
124
|
- spec/lib/mysql_framework/sql_query_spec.rb
|
125
125
|
- spec/lib/mysql_framework/sql_table_spec.rb
|
126
126
|
- spec/spec_helper.rb
|
127
|
+
- spec/support/fixtures.rb
|
127
128
|
- spec/support/procedure.sql
|
128
129
|
- spec/support/scripts/create_demo_table.rb
|
129
130
|
- spec/support/scripts/create_test_proc.rb
|