mysql_framework 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module MysqlFramework
2
+ VERSION = '0.0.1'
3
+ 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