mysql_framework 1.1.1 → 2.1.0

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