mysql_framework 1.1.2 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3dea6c1520ad570dfaf0070efd3df024dbd8d8ba9bc745970ebc59a82ad38418
4
- data.tar.gz: 7f2e6a694727e104584c5304673841cf06231b3efc538fb1a0bce23f69772293
3
+ metadata.gz: 76573ef7ddc9a996d4bc02f28fad318ca94692eab6df1f7f592229d67f5df3ab
4
+ data.tar.gz: fcbf52818c3cf42d451b15d95fe9609a467ff78402ce31cd43cfae74b5d89b9f
5
5
  SHA512:
6
- metadata.gz: 6adf70afb9966e920ef972c187679847ea08d1638b3be8b5473bb031d5be41bc607a9143d313aafd106ff748f295b7679437f597f28a88b4e68afa6e1bcb9068
7
- data.tar.gz: '0057569f653959047e9c12feac892e9e0fc51f3fa99085bcd153f26374b4057e7dbdf45f6515d5254798064992065056e7d981369c7700986073390fc88a94e3'
6
+ metadata.gz: 7a8011fafcfaab143dc3e74ddd4d00b5f256ef8051e4139df564e96959a79d9b6f8128704303d93e803c7fe8e4482757105152bfc1a60ad8628c2706155e5169
7
+ data.tar.gz: a08b82318c591fe884d5276e1eb1447a8a3460b2f3901a74b29b90a4bafaa44e483bedaf5908696f28756fd265b49c80fe7841316df2826fff0f18363763c6dd
@@ -7,6 +7,7 @@ require_relative 'mysql_framework/logger'
7
7
  require_relative 'mysql_framework/scripts'
8
8
  require_relative 'mysql_framework/sql_column'
9
9
  require_relative 'mysql_framework/sql_condition'
10
+ require_relative 'mysql_framework/in_condition'
10
11
  require_relative 'mysql_framework/sql_query'
11
12
  require_relative 'mysql_framework/sql_table'
12
13
  require_relative 'mysql_framework/version'
@@ -59,10 +59,9 @@ module MysqlFramework
59
59
 
60
60
  # This method is called to check a client back in to the connection when no longer needed.
61
61
  def check_in(client)
62
- return client.close unless connection_pool_enabled?
63
-
64
- client = new_client if client.closed?
62
+ return client&.close unless connection_pool_enabled?
65
63
 
64
+ client = new_client if client.nil? || client.closed?
66
65
  @connection_pool.push(client)
67
66
  end
68
67
 
@@ -75,10 +74,19 @@ module MysqlFramework
75
74
  end
76
75
 
77
76
  # This method is called to execute a prepared statement
77
+ #
78
+ # @note Ensure we close each statement, otherwise we can run into
79
+ # a 'Commands out of sync' error if multiple threads are running different
80
+ # queries at the same time.
78
81
  def execute(query, provided_client = nil)
79
82
  with_client(provided_client) do |client|
80
- statement = client.prepare(query.sql)
81
- statement.execute(*query.params)
83
+ begin
84
+ statement = client.prepare(query.sql)
85
+ result = statement.execute(*query.params)
86
+ result.to_a if result
87
+ ensure
88
+ statement.close if statement
89
+ end
82
90
  end
83
91
  end
84
92
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MysqlFramework
4
+ # This class is used to represent a Sql IN Condition for a column.
5
+ class InCondition < SqlCondition
6
+ # This method is called to get the condition as a string for a sql prepared statement
7
+ def to_s
8
+ params = value.map { |_| '?' }
9
+ "#{@column} #{@comparison} (#{params.join(', ')})"
10
+ end
11
+ end
12
+ end
@@ -46,6 +46,10 @@ module MysqlFramework
46
46
  SqlCondition.new(column: to_s, comparison: '<=', value: value)
47
47
  end
48
48
 
49
+ def in(*values)
50
+ InCondition.new(column: to_s, comparison: 'IN', value: values)
51
+ end
52
+
49
53
  # This method is called to generate an alias statement for this column.
50
54
  def as(name)
51
55
  "#{self} as `#{name}`"
@@ -3,18 +3,54 @@
3
3
  module MysqlFramework
4
4
  # This class is used to represent a Sql Condition for a column.
5
5
  class SqlCondition
6
+ NIL_COMPARISONS = ['IS NULL', 'IS NOT NULL'].freeze
7
+
6
8
  # This method is called to get the value of this condition for prepared statements.
7
9
  attr_reader :value
8
10
 
9
- def initialize(column:, comparison:, value:)
11
+ # Creates a new SqlCondition using the given parameters.
12
+ #
13
+ # @raise ArgumentError if comparison is 'IS NULL' and value is not nil
14
+ # @raise ArgumentError if comparison is 'IS NOT NULL' and value is not nil
15
+ # @raise ArgumentError if comparison is neither 'IS NULL' or 'IS NOT NULL' and value is nil
16
+ #
17
+ # @param column [String] - the name of the column to use in the comparison
18
+ # @param comparison [String] - the MySQL comparison operator to use
19
+ # @param value [Object] - the value to use in the comparison (default nil)
20
+ def initialize(column:, comparison:, value: nil)
10
21
  @column = column
11
22
  @comparison = comparison
23
+
24
+ validate(value)
12
25
  @value = value
13
26
  end
14
27
 
15
28
  # This method is called to get the condition as a string for a sql prepared statement
29
+ #
30
+ # @return [String]
16
31
  def to_s
32
+ return "#{@column} #{@comparison.upcase}" if nil_comparison?
33
+
17
34
  "#{@column} #{@comparison} ?"
18
35
  end
36
+
37
+ private
38
+
39
+ def nil_comparison?
40
+ NIL_COMPARISONS.include?(@comparison.upcase)
41
+ end
42
+
43
+ def validate(value)
44
+ raise ArgumentError, "Cannot set value when comparison is #{@comparison}" if invalid_null_condition?(value)
45
+ raise ArgumentError, "Comparison of #{@comparison} requires value to be not nil" if invalid_nil_value?(value)
46
+ end
47
+
48
+ def invalid_null_condition?(value)
49
+ nil_comparison? && value != nil
50
+ end
51
+
52
+ def invalid_nil_value?(value)
53
+ nil_comparison? == false && value.nil?
54
+ end
19
55
  end
20
56
  end
@@ -9,11 +9,12 @@ module MysqlFramework
9
9
  def initialize
10
10
  @sql = ''
11
11
  @params = []
12
+ @lock = nil
12
13
  end
13
14
 
14
15
  # This method is called to access the sql string for this query.
15
16
  def sql
16
- @sql.strip
17
+ (@sql + @lock.to_s + @dup_query.to_s).strip
17
18
  end
18
19
 
19
20
  # This method is called to start a select query
@@ -76,19 +77,6 @@ module MysqlFramework
76
77
  self
77
78
  end
78
79
 
79
- # This method is called to specify the columns to bulk upsert.
80
- def bulk_upsert(columns)
81
- @sql += 'ON DUPLICATE KEY UPDATE '
82
-
83
- columns.each do |column|
84
- @sql += "#{column} = VALUES(#{column}), "
85
- end
86
-
87
- @sql = @sql.chomp(', ')
88
-
89
- self
90
- end
91
-
92
80
  # This method is called to specify the columns to update.
93
81
  def set(values)
94
82
  @sql += ' SET '
@@ -132,11 +120,20 @@ module MysqlFramework
132
120
  end
133
121
 
134
122
  # This method is called to specify a where clause for a query.
123
+ #
124
+ # Condition values are added to @params unless the value is nil.
135
125
  def where(*conditions)
136
126
  @sql += ' WHERE' unless @sql.include?('WHERE')
137
127
  @sql += " (#{conditions.join(' AND ')}) "
138
128
 
139
- conditions.each { |condition| @params << condition.value }
129
+ conditions.each do |condition|
130
+ next if condition.value.nil?
131
+ if condition.value.is_a?(Enumerable)
132
+ @params.concat(condition.value)
133
+ else
134
+ @params << condition.value
135
+ end
136
+ end
140
137
 
141
138
  self
142
139
  end
@@ -218,5 +215,53 @@ module MysqlFramework
218
215
 
219
216
  self
220
217
  end
218
+
219
+ # This method allows you to add a pessimistic lock to the record.
220
+ # The default lock is `FOR UPDATE`
221
+ # If you require any custom lock, e.g. FOR SHARE, just pass that in as the condition
222
+ # query.lock('FOR SHARE')
223
+ def lock(condition = nil)
224
+ raise 'This must be a SELECT query' unless @sql.start_with?('SELECT')
225
+
226
+ @lock = ' ' + (condition || 'FOR UPDATE')
227
+ self
228
+ end
229
+
230
+ # For insert queries if you need to handle that a primary key already exists and automatically do an update instead.
231
+ # If you do not pass in a hash specifying a column name and custom value for it.
232
+ # @param update_values [Hash] key is a column name. A nil value will make the query update
233
+ # the column with the value specified in the insert. Otherwise any value will be interpreted
234
+ # literally via mysql.
235
+ # @return SqlQuery
236
+ # e.g.
237
+ # query.insert('users')
238
+ # .into('id', first_name', 'login_count')
239
+ # .values(1, 'Bob', 1)
240
+ # .on_duplicate(
241
+ # {
242
+ # first_name: nil,
243
+ # login_count: 'login_count + 5'
244
+ # }
245
+ # )
246
+ # This would first create a record like => `1, 'Bob', 1`.
247
+ # The second time it would update it with => `'Bob', 6` (Note the 1 is not used in the update)
248
+ def on_duplicate(update_values = {})
249
+ raise 'This must be an INSERT query' unless @sql.start_with?('INSERT')
250
+
251
+ duplicates = []
252
+ update_values.each do |column, col_value|
253
+ if col_value.nil?
254
+ # value comes from what the INSERT intended
255
+ updated_value = "#{column} = VALUES (#{column})"
256
+ else
257
+ # custom value specified by col_value
258
+ updated_value = "#{column} = #{col_value}"
259
+ end
260
+ duplicates << updated_value
261
+ end
262
+ @dup_query = " ON DUPLICATE KEY UPDATE #{duplicates.join(', ')}"
263
+
264
+ self
265
+ end
221
266
  end
222
267
  end
@@ -5,11 +5,13 @@ module MysqlFramework
5
5
  class SqlTable
6
6
  def initialize(name)
7
7
  @name = name
8
+ @column_objects = {}
8
9
  end
9
10
 
10
11
  # This method is called to get a sql column for this table
11
12
  def [](column)
12
- SqlColumn.new(table: @name, column: column)
13
+ return @column_objects[column.to_sym] if @column_objects[column.to_sym]
14
+ @column_objects[column.to_sym] = SqlColumn.new(table: @name, column: column)
13
15
  end
14
16
 
15
17
  def to_s
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MysqlFramework
4
- VERSION = '1.1.2'
4
+ VERSION = '2.1.1'
5
5
  end
@@ -232,6 +232,24 @@ describe MysqlFramework::Connector do
232
232
  subject.check_in(client)
233
233
  end
234
234
  end
235
+
236
+ context 'when client is nil' do
237
+ let(:client) { nil }
238
+
239
+ context 'when connection pooling is enabled' do
240
+ it 'does not raise an error' do
241
+ expect { subject.check_in(client) }.not_to raise_error
242
+ end
243
+ end
244
+
245
+ context 'when connection pooling is disabled' do
246
+ let(:connection_pooling_enabled) { 'false' }
247
+
248
+ it 'does not raise an error' do
249
+ expect { subject.check_in(client) }.not_to raise_error
250
+ end
251
+ end
252
+ end
235
253
  end
236
254
 
237
255
  describe '#with_client' do
@@ -272,6 +290,33 @@ describe MysqlFramework::Connector do
272
290
  expect(results.length).to eq(1)
273
291
  expect(results[0][:id]).to eq(guid)
274
292
  end
293
+
294
+ it 'does not raise a commands out of sync error' do
295
+ threads = []
296
+ threads << Thread.new do
297
+ 350.times do
298
+ update_query = MysqlFramework::SqlQuery.new.update('gems')
299
+ .set(updated_at: Time.now)
300
+ expect { subject.execute(update_query) }.not_to raise_error
301
+ end
302
+ end
303
+
304
+ threads << Thread.new do
305
+ 350.times do
306
+ select_query = MysqlFramework::SqlQuery.new.select('*').from('demo')
307
+ expect { subject.execute(select_query) }.not_to raise_error
308
+ end
309
+ end
310
+
311
+ threads << Thread.new do
312
+ 350.times do
313
+ select_query = MysqlFramework::SqlQuery.new.select('*').from('test')
314
+ expect { subject.execute(select_query) }.not_to raise_error
315
+ end
316
+ end
317
+
318
+ threads.each(&:join)
319
+ end
275
320
  end
276
321
 
277
322
  describe '#query' do
@@ -63,6 +63,14 @@ describe MysqlFramework::SqlColumn do
63
63
  end
64
64
  end
65
65
 
66
+ describe '#in' do
67
+ it 'returns a SqlCondition for the comparison' do
68
+ condition = subject.in('a', 'b', 'c')
69
+ expect(condition).to be_a(MysqlFramework::InCondition)
70
+ expect(condition.to_s).to eq('`gems`.`version` IN (?, ?, ?)')
71
+ end
72
+ end
73
+
66
74
  describe '#as' do
67
75
  it 'returns the column specified as another name' do
68
76
  expect(subject.as('v')).to eq('`gems`.`version` as `v`')
@@ -8,4 +8,82 @@ describe MysqlFramework::SqlCondition do
8
8
  expect(subject.to_s).to eq('version = ?')
9
9
  end
10
10
  end
11
+
12
+ context 'when comparison is neither IS NULL or IS NOT NULL' do
13
+ context 'when value is nil' do
14
+ subject { described_class.new(column: 'version', comparison: '=', value: nil) }
15
+
16
+ it 'does raises an ArgumentError' do
17
+ expect { subject }.to raise_error(ArgumentError, "Comparison of = requires value to be not nil")
18
+ end
19
+ end
20
+ end
21
+
22
+ context 'when comparison is IS NULL' do
23
+ subject { described_class.new(column: 'version', comparison: 'IS NULL') }
24
+
25
+ it 'has a nil value by default' do
26
+ expect(subject.value).to be_nil
27
+ end
28
+
29
+ context 'when a value is passed to the constructor' do
30
+ subject { described_class.new(column: 'version', comparison: 'IS NULL', value: 'foo') }
31
+
32
+ describe '#new' do
33
+ it 'raises an ArgumentError if value is set' do
34
+ expect { subject }.to raise_error(ArgumentError, 'Cannot set value when comparison is IS NULL')
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#to_s' do
40
+ it 'does not include a value placeholder' do
41
+ expect(subject.to_s).to eq('version IS NULL')
42
+ end
43
+ end
44
+ end
45
+
46
+ context 'when comparison is lowercase is null' do
47
+ subject { described_class.new(column: 'version', comparison: 'is null') }
48
+
49
+ describe '#to_s' do
50
+ it 'ignores case' do
51
+ expect(subject.to_s).to eq 'version IS NULL'
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when comparison is IS NOT NULL' do
57
+ subject { described_class.new(column: 'version', comparison: 'IS NOT NULL') }
58
+
59
+ it 'has a nil value by default' do
60
+ expect(subject.value).to be_nil
61
+ end
62
+
63
+ context 'when a value is passed to the constructor' do
64
+ subject { described_class.new(column: 'version', comparison: 'IS NOT NULL', value: 'foo') }
65
+
66
+ describe '#new' do
67
+ it 'raises an ArgumentError if value is set' do
68
+ expect { subject }.to raise_error(ArgumentError, 'Cannot set value when comparison is IS NOT NULL')
69
+ end
70
+ end
71
+ end
72
+
73
+ describe '#to_s' do
74
+ it 'does not include a value placeholder' do
75
+ expect(subject.to_s).to eq('version IS NOT NULL')
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'when comparison is lowercase is not null' do
81
+ subject { described_class.new(column: 'version', comparison: 'is not null') }
82
+
83
+ describe '#to_s' do
84
+ it 'ignores case' do
85
+ expect(subject.to_s).to eq 'version IS NOT NULL'
86
+ end
87
+ end
88
+ end
11
89
  end
@@ -50,6 +50,19 @@ describe MysqlFramework::SqlQuery do
50
50
  expect(subject.params).to eq(['9876'])
51
51
  end
52
52
 
53
+ context 'when a select query contains conditions with nil values' do
54
+ it 'does not store them as parameters' do
55
+ subject.select('*')
56
+ .from(gems, 40)
57
+ .where(
58
+ MysqlFramework::SqlCondition.new(column: 'id', comparison: '=', value: 9876),
59
+ MysqlFramework::SqlCondition.new(column: 'foo', comparison: 'IS NOT NULL'),
60
+ )
61
+ expect(subject.sql).to eq('SELECT * FROM `gems` PARTITION (p40) WHERE (id = ? AND foo IS NOT NULL)')
62
+ expect(subject.params.size).to eq 1
63
+ end
64
+ end
65
+
53
66
  it 'builds a joined select query as expected' do
54
67
  subject.select('*')
55
68
  .from(gems, 40)
@@ -139,16 +152,6 @@ describe MysqlFramework::SqlQuery do
139
152
  end
140
153
  end
141
154
 
142
- describe '#bulk_upsert' do
143
- it 'sets the sql for the upsert statement' do
144
- columns = %w(column_1 column_2)
145
-
146
- subject.bulk_upsert(columns)
147
-
148
- expect(subject.sql).to eq('ON DUPLICATE KEY UPDATE column_1 = VALUES(column_1), column_2 = VALUES(column_2)')
149
- end
150
- end
151
-
152
155
  describe '#set' do
153
156
  it 'sets the sql for the set statement' do
154
157
  subject.set(name: 'mysql_framework', author: 'sage', created_at: '2016-06-28 10:00:00')
@@ -223,6 +226,13 @@ describe MysqlFramework::SqlQuery do
223
226
  expect(subject.sql).to eq('WHERE (`gems`.`author` = ? AND `gems`.`created_at` > ?) AND (`gems`.`name` = ?)')
224
227
  end
225
228
  end
229
+
230
+ context 'when the condition includes an array of parameters' do
231
+ it 'concats the parameter collections' do
232
+ subject.and.where(gems[:name].in('a','b'))
233
+ expect(subject.sql).to eq('WHERE (`gems`.`author` = ? AND `gems`.`created_at` > ?) AND (`gems`.`name` IN (?, ?))')
234
+ end
235
+ end
226
236
  end
227
237
 
228
238
  describe '#and' do
@@ -330,4 +340,69 @@ describe MysqlFramework::SqlQuery do
330
340
  end
331
341
  end
332
342
  end
343
+
344
+ describe '#lock' do
345
+ it 'appends `FOR_UPDATE` to the query' do
346
+ subject.select('*').from(gems).lock
347
+ expect(subject.sql).to end_with('FOR UPDATE')
348
+ end
349
+ end
350
+
351
+ describe '#on_duplicate' do
352
+ let(:query) do
353
+ subject.insert(gems)
354
+ .into(
355
+ gems[:id],
356
+ gems[:name],
357
+ gems[:author]
358
+ )
359
+ .values(
360
+ 1,
361
+ 'mysql_framework',
362
+ 'Bob Hope'
363
+ )
364
+ end
365
+
366
+ context 'when no custom values are specified' do
367
+ it 'updates with the value from the INSERT clause' do
368
+ query.on_duplicate(
369
+ {
370
+ gems[:name] => nil,
371
+ gems[:author] => nil
372
+ }
373
+ )
374
+
375
+ expect(query.sql)
376
+ .to end_with 'ON DUPLICATE KEY UPDATE `gems`.`name` = VALUES (`gems`.`name`), `gems`.`author` = VALUES (`gems`.`author`)'
377
+ end
378
+ end
379
+
380
+ context 'when a custom value is specified' do
381
+ it 'updates the value based on the custom value' do
382
+ query.on_duplicate(
383
+ {
384
+ gems[:name] => '"mysql_alternative"',
385
+ gems[:author] => '"Michael Caine"'
386
+ }
387
+ )
388
+
389
+ expect(query.sql)
390
+ .to end_with 'ON DUPLICATE KEY UPDATE `gems`.`name` = "mysql_alternative", `gems`.`author` = "Michael Caine"'
391
+ end
392
+ end
393
+
394
+ context 'when column names are specified instead of SqlColumn objects' do
395
+ it 'updates the value based on the custom column key names' do
396
+ query.on_duplicate(
397
+ {
398
+ name: '"mysql_alternative"',
399
+ author: '"Michael Caine"'
400
+ }
401
+ )
402
+
403
+ expect(query.sql)
404
+ .to end_with 'ON DUPLICATE KEY UPDATE name = "mysql_alternative", author = "Michael Caine"'
405
+ end
406
+ end
407
+ end
333
408
  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: 1.1.2
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sage
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-08-09 00:00:00.000000000 Z
11
+ date: 2021-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -89,6 +89,7 @@ extra_rdoc_files: []
89
89
  files:
90
90
  - lib/mysql_framework.rb
91
91
  - lib/mysql_framework/connector.rb
92
+ - lib/mysql_framework/in_condition.rb
92
93
  - lib/mysql_framework/logger.rb
93
94
  - lib/mysql_framework/scripts.rb
94
95
  - lib/mysql_framework/scripts/base.rb
@@ -136,8 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
137
  - !ruby/object:Gem::Version
137
138
  version: '0'
138
139
  requirements: []
139
- rubyforge_project:
140
- rubygems_version: 2.7.7
140
+ rubygems_version: 3.0.8
141
141
  signing_key:
142
142
  specification_version: 4
143
143
  summary: A lightweight framework to provide managers for working with MySQL.