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
@@ -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
|