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