mysql_framework 1.1.1 → 2.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e883779b80815c73bad5a4d6ca0fabe04821e47ac42244e2ec4de8f503079e5
4
- data.tar.gz: 563e8190f534c2c0f86f58785a32bfd102d80067c50fff5e3841e20ed7f36fcc
3
+ metadata.gz: f48c69762d95976838c6779c9acfc6591e2113d9a91128f69a2621890332bee0
4
+ data.tar.gz: 5d9d0250828d1e93626010796f79364d2f23b22554c38f4bda313ca718faffc6
5
5
  SHA512:
6
- metadata.gz: '097525d900a4c0122c705c40ce36d59d647e67f43944b98ed1c327f984a5d151f90cefd7d74af11641113f5797d3c1f3c252d614f3888c8df65fb8d65676db68'
7
- data.tar.gz: 344b2bacb1f09c0789ed23d2dcf7ed0e61e389d485159bb17150055282a96fcb30cfa77d77a4c5dc286727a1834295480a2761d607a59b302a123ee8e233c7e3
6
+ metadata.gz: 0e477a4a454360a80a979421aad3ba7db1b7399656062dc7cd129174af1f48953fb3c56d2ad2d959fafbf009a7c1e9f9aa57d34c91c5a06aeb853d0f0e518fb8
7
+ data.tar.gz: 2c0980c6a0c94718d0df07d56ae1f88367ce8716d5af457b1c9c6526c29878380bbaeaffaca314dcdf4943d1366c27940675d693fdec4fe961fc183e7a6bb3ed
@@ -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'
@@ -75,10 +75,19 @@ module MysqlFramework
75
75
  end
76
76
 
77
77
  # This method is called to execute a prepared statement
78
+ #
79
+ # @note Ensure we close each statement, otherwise we can run into
80
+ # a 'Commands out of sync' error if multiple threads are running different
81
+ # queries at the same time.
78
82
  def execute(query, provided_client = nil)
79
83
  with_client(provided_client) do |client|
80
- statement = client.prepare(query.sql)
81
- statement.execute(*query.params)
84
+ begin
85
+ statement = client.prepare(query.sql)
86
+ result = statement.execute(*query.params)
87
+ result.to_a if result
88
+ ensure
89
+ statement.close if statement
90
+ end
82
91
  end
83
92
  end
84
93
 
@@ -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
@@ -36,7 +36,7 @@ module MysqlFramework
36
36
 
37
37
  def column_exists?(client, table_name, column_name)
38
38
  result = client.query(<<~SQL)
39
- SHOW COLUMNS FROM "#{table_name}" WHERE Field="#{column_name}";
39
+ SHOW COLUMNS FROM #{table_name} WHERE Field="#{column_name}";
40
40
  SQL
41
41
 
42
42
  result.count == 1
@@ -44,7 +44,7 @@ module MysqlFramework
44
44
 
45
45
  def index_exists?(client, table_name, index_name)
46
46
  result = client.query(<<~SQL)
47
- SHOW INDEX FROM "#{table_name}" WHERE Key_name="#{index_name}" LIMIT 1;
47
+ SHOW INDEX FROM #{table_name} WHERE Key_name="#{index_name}" LIMIT 1;
48
48
  SQL
49
49
 
50
50
  result.count == 1
@@ -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.1'
4
+ VERSION = '2.1.0'
5
5
  end
@@ -272,6 +272,33 @@ describe MysqlFramework::Connector do
272
272
  expect(results.length).to eq(1)
273
273
  expect(results[0][:id]).to eq(guid)
274
274
  end
275
+
276
+ it 'does not raise a commands out of sync error' do
277
+ threads = []
278
+ threads << Thread.new do
279
+ 350.times do
280
+ update_query = MysqlFramework::SqlQuery.new.update('gems')
281
+ .set(updated_at: Time.now)
282
+ expect { subject.execute(update_query) }.not_to raise_error
283
+ end
284
+ end
285
+
286
+ threads << Thread.new do
287
+ 350.times do
288
+ select_query = MysqlFramework::SqlQuery.new.select('*').from('demo')
289
+ expect { subject.execute(select_query) }.not_to raise_error
290
+ end
291
+ end
292
+
293
+ threads << Thread.new do
294
+ 350.times do
295
+ select_query = MysqlFramework::SqlQuery.new.select('*').from('test')
296
+ expect { subject.execute(select_query) }.not_to raise_error
297
+ end
298
+ end
299
+
300
+ threads.each(&:join)
301
+ end
275
302
  end
276
303
 
277
304
  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.1
4
+ version: 2.1.0
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: 2020-12-15 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.