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
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MysqlFramework
|
4
|
+
# This class is used to represent and build a sql query
|
5
|
+
class SqlQuery
|
6
|
+
# This method is called to get any params required to execute this query as a prepared statement.
|
7
|
+
attr_reader :params
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@sql = ''
|
11
|
+
@params = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# This method is called to access the sql string for this query.
|
15
|
+
def sql
|
16
|
+
@sql.strip
|
17
|
+
end
|
18
|
+
|
19
|
+
# This method is called to start a select query
|
20
|
+
def select(*columns)
|
21
|
+
@sql = "select #{columns.join(',')}"
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# This method is called to start a delete query
|
26
|
+
def delete
|
27
|
+
@sql = 'delete'
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# This method is called to start an update query
|
32
|
+
def update(table, partition = nil)
|
33
|
+
@sql = "update #{table}"
|
34
|
+
@sql += " partition(p#{partition})" unless partition.nil?
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# This method is called to start an insert query
|
39
|
+
def insert(table, partition = nil)
|
40
|
+
@sql += "insert into #{table}"
|
41
|
+
@sql += " partition(p#{partition})" unless partition.nil?
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# This method is called to specify the columns to insert into.
|
46
|
+
def into(*columns)
|
47
|
+
@sql += " (#{columns.join(',')})"
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# This method is called to specify the values to insert.
|
52
|
+
def values(*values)
|
53
|
+
@sql += " values (#{values.map { |_v| '?' }.join(',')})"
|
54
|
+
values.each do |v|
|
55
|
+
@params << v
|
56
|
+
end
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# This method is called to specify the columns to update.
|
61
|
+
def set(values)
|
62
|
+
@sql += ' set '
|
63
|
+
values.each do |k, p|
|
64
|
+
@sql += "`#{k}` = ?, "
|
65
|
+
@params << p
|
66
|
+
end
|
67
|
+
@sql = @sql[0...-2]
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
# This method is called to specify the table/partition a select/delete query is for.
|
72
|
+
def from(table, partition = nil)
|
73
|
+
@sql += " from #{table}"
|
74
|
+
@sql += " partition(p#{partition})" unless partition.nil?
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# This method is called to specify a where clause for a query.
|
79
|
+
def where(*conditions)
|
80
|
+
@sql += ' where' unless @sql.include?('where')
|
81
|
+
@sql += " (#{conditions.join(' and ')}) "
|
82
|
+
conditions.each do |c|
|
83
|
+
@params << c.value
|
84
|
+
end
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
# This method is called to add an `and` keyword to a query to provide additional where clauses.
|
89
|
+
def and
|
90
|
+
@sql += 'and'
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
# This method is called to add an `or` keyword to a query to provide alternate where clauses.
|
95
|
+
def or
|
96
|
+
@sql += 'or'
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
# This method is called to add an `order by` statement to a query
|
101
|
+
def order(*columns)
|
102
|
+
@sql += " order by #{columns.join(',')}"
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# This method is called to add an `order by ... desc` statement to a query
|
107
|
+
def order_desc(*columns)
|
108
|
+
order(*columns)
|
109
|
+
@sql += ' desc'
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
# This method is called to add a limit to a query
|
114
|
+
def limit(count)
|
115
|
+
@sql += " limit #{count}"
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
# This method is called to add a join statement to a query.
|
120
|
+
def join(table)
|
121
|
+
@sql += " join #{table}"
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
# This method is called to add the `on` detail to a join statement.
|
126
|
+
def on(column_1, column_2)
|
127
|
+
@sql += " on #{column_1} = #{column_2}"
|
128
|
+
self
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MysqlFramework
|
4
|
+
# This class is used to represent a sql table
|
5
|
+
class SqlTable
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
# This method is called to get a sql column for this table
|
11
|
+
def [](column)
|
12
|
+
SqlColumn.new(table: @name, column: column)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"`#{@name}`"
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_sym
|
20
|
+
@name.to_sym
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe MysqlFramework::Connector do
|
6
|
+
let(:default_options) do
|
7
|
+
{
|
8
|
+
host: ENV.fetch('MYSQL_HOST'),
|
9
|
+
port: ENV.fetch('MYSQL_PORT'),
|
10
|
+
database: ENV.fetch('MYSQL_DATABASE'),
|
11
|
+
username: ENV.fetch('MYSQL_USERNAME'),
|
12
|
+
password: ENV.fetch('MYSQL_PASSWORD'),
|
13
|
+
reconnect: true
|
14
|
+
}
|
15
|
+
end
|
16
|
+
let(:options) do
|
17
|
+
{
|
18
|
+
host: 'host',
|
19
|
+
port: 'port',
|
20
|
+
database: 'database',
|
21
|
+
username: 'username',
|
22
|
+
password: 'password',
|
23
|
+
reconnect: true
|
24
|
+
}
|
25
|
+
end
|
26
|
+
let(:client) { double }
|
27
|
+
let(:gems) { MysqlFramework::SqlTable.new('gems') }
|
28
|
+
|
29
|
+
subject { described_class.new }
|
30
|
+
|
31
|
+
describe '#initialize' do
|
32
|
+
it 'sets default query options on the Mysql2 client' do
|
33
|
+
subject
|
34
|
+
expect(Mysql2::Client.default_query_options[:symbolize_keys]).to eq(true)
|
35
|
+
expect(Mysql2::Client.default_query_options[:cast_booleans]).to eq(true)
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'when options are provided' do
|
39
|
+
subject { described_class.new(options) }
|
40
|
+
|
41
|
+
it 'allows the default options to be overridden' do
|
42
|
+
expect(subject.instance_variable_get(:@options)).to eq(options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#check_out' do
|
48
|
+
it 'returns a Mysql2::Client instance from the pool' do
|
49
|
+
expect(Mysql2::Client).to receive(:new).with(default_options).and_return(client)
|
50
|
+
expect(subject.check_out).to eq(client)
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when the connection pool has a client available' do
|
54
|
+
it 'returns a client instance from the pool' do
|
55
|
+
subject.instance_variable_get(:@connection_pool).push(client)
|
56
|
+
expect(subject.check_out).to eq(client)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#check_in' do
|
62
|
+
it 'returns the provided client to the connection pool' do
|
63
|
+
expect(subject.instance_variable_get(:@connection_pool)).to receive(:push).with(client)
|
64
|
+
subject.check_in(client)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '#with_client' do
|
69
|
+
it 'obtains a client from the pool to use' do
|
70
|
+
allow(subject).to receive(:check_out).and_return(client)
|
71
|
+
expect { |b| subject.with_client(&b) }.to yield_with_args(client)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '#execute' do
|
76
|
+
let(:insert_query) do
|
77
|
+
MysqlFramework::SqlQuery.new.insert(gems)
|
78
|
+
.into(
|
79
|
+
gems[:id],
|
80
|
+
gems[:name],
|
81
|
+
gems[:author],
|
82
|
+
gems[:created_at],
|
83
|
+
gems[:updated_at]
|
84
|
+
)
|
85
|
+
.values(
|
86
|
+
SecureRandom.uuid,
|
87
|
+
'mysql_framework',
|
88
|
+
'sage',
|
89
|
+
Time.now,
|
90
|
+
Time.now
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'executes the query with parameters' do
|
95
|
+
guid = insert_query.params[0]
|
96
|
+
subject.execute(insert_query)
|
97
|
+
|
98
|
+
results = subject.query("SELECT * FROM `gems` WHERE id = '#{guid}';").to_a
|
99
|
+
expect(results.length).to eq(1)
|
100
|
+
expect(results[0][:id]).to eq(guid)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#query' do
|
105
|
+
before :each do
|
106
|
+
allow(subject).to receive(:check_out).and_return(client)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'retrieves a client and calls query' do
|
110
|
+
expect(client).to receive(:query).with('SELECT 1')
|
111
|
+
subject.query('SELECT 1')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#query_multiple_results' do
|
116
|
+
let(:test) { MysqlFramework::SqlTable.new('test') }
|
117
|
+
let(:manager) { MysqlFramework::Scripts::Manager.new }
|
118
|
+
let(:connector) { MysqlFramework::Connector.new }
|
119
|
+
let(:timestamp) { Time.at(628232400) } # 1989-11-28 00:00:00 -0500
|
120
|
+
let(:guid) { 'a3ccb138-48ae-437a-be52-f673beb12b51' }
|
121
|
+
let(:insert) do
|
122
|
+
MysqlFramework::SqlQuery.new.insert(test)
|
123
|
+
.into(test[:id],test[:name],test[:action],test[:created_at],test[:updated_at])
|
124
|
+
.values(guid,'name','action',timestamp,timestamp)
|
125
|
+
end
|
126
|
+
let(:obj) do
|
127
|
+
{
|
128
|
+
id: guid,
|
129
|
+
name: 'name',
|
130
|
+
action: 'action',
|
131
|
+
created_at: timestamp,
|
132
|
+
updated_at: timestamp,
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
before :each do
|
137
|
+
manager.initialize_script_history
|
138
|
+
manager.execute
|
139
|
+
|
140
|
+
connector.execute(insert)
|
141
|
+
end
|
142
|
+
|
143
|
+
after :each do
|
144
|
+
manager.drop_all_tables
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'returns the results from the stored procedure' do
|
148
|
+
query = "call test_procedure"
|
149
|
+
result = subject.query_multiple_results(query)
|
150
|
+
expect(result).to be_a(Array)
|
151
|
+
expect(result.length).to eq(2)
|
152
|
+
expect(result[0]).to eq([])
|
153
|
+
expect(result[1]).to eq([obj])
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe '#transaction' do
|
158
|
+
before :each do
|
159
|
+
allow(subject).to receive(:check_out).and_return(client)
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'wraps the client call with BEGIN and COMMIT statements' do
|
163
|
+
expect(client).to receive(:query).with('BEGIN')
|
164
|
+
expect(client).to receive(:query).with('SELECT 1')
|
165
|
+
expect(client).to receive(:query).with('COMMIT')
|
166
|
+
|
167
|
+
subject.transaction do
|
168
|
+
subject.query('SELECT 1')
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'when an exception occurs' do
|
173
|
+
it 'triggers a ROLLBACK' do
|
174
|
+
expect(client).to receive(:query).with('BEGIN')
|
175
|
+
expect(client).to receive(:query).with('ROLLBACK')
|
176
|
+
|
177
|
+
begin
|
178
|
+
subject.transaction do
|
179
|
+
raise
|
180
|
+
end
|
181
|
+
rescue
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe '#default_options' do
|
188
|
+
it 'returns the default options' do
|
189
|
+
expect(subject.default_options).to eq(default_options)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe MysqlFramework do
|
6
|
+
describe 'logger' do
|
7
|
+
it 'returns the logger' do
|
8
|
+
expect(subject.logger).to be_a(Logger)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'set_logger' do
|
13
|
+
let(:logger) { Logger.new(STDOUT) }
|
14
|
+
|
15
|
+
it 'sets the logger' do
|
16
|
+
subject.set_logger(logger)
|
17
|
+
expect(subject.logger).to eq(logger)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe MysqlFramework::Scripts::Base do
|
6
|
+
subject { described_class.new }
|
7
|
+
|
8
|
+
describe '#partitions' do
|
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
|
19
|
+
|
20
|
+
describe '#identifier' do
|
21
|
+
it 'throws a NotImplementedError' do
|
22
|
+
expect{ subject.identifier }.to raise_error(NotImplementedError)
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when @identifier is set' do
|
26
|
+
it 'returns the value' do
|
27
|
+
subject.instance_variable_set(:@identifier, 'foo')
|
28
|
+
expect(subject.identifier).to eq('foo')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#apply' do
|
34
|
+
it 'throws a NotImplementedError' do
|
35
|
+
expect{ subject.apply }.to raise_error(NotImplementedError)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '#rollback' do
|
40
|
+
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)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '.descendants' do
|
53
|
+
it 'returns all descendant classes' do
|
54
|
+
expect(described_class.descendants.length).to eq(3)
|
55
|
+
expect(described_class.descendants).to include(MysqlFramework::Support::Scripts::CreateTestTable,
|
56
|
+
MysqlFramework::Support::Scripts::CreateDemoTable,
|
57
|
+
MysqlFramework::Support::Scripts::CreateTestProc)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#tags' do
|
62
|
+
it 'returns an array' do
|
63
|
+
expect(subject.tags).to eq([])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '#update_procedure' do
|
68
|
+
let(:connector) { MysqlFramework::Connector.new }
|
69
|
+
let(:proc_file_path) { 'spec/support/procedure.sql' }
|
70
|
+
let(:drop_sql) do
|
71
|
+
<<~SQL
|
72
|
+
DROP PROCEDURE IF EXISTS test_procedure;
|
73
|
+
SQL
|
74
|
+
end
|
75
|
+
|
76
|
+
before :each do
|
77
|
+
subject.instance_variable_set(:@mysql_connector, connector)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'drops and then creates the named procedure' do
|
81
|
+
expect(connector).to receive(:query).with(drop_sql).once
|
82
|
+
expect(connector).to receive(:query).with(File.read(proc_file_path)).once
|
83
|
+
subject.update_procedure('test_procedure', proc_file_path)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'wraps the call in a transaction' do
|
87
|
+
expect(connector).to receive(:transaction)
|
88
|
+
subject.update_procedure('test_procedure', proc_file_path)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|