mysql_framework 1.1.0 → 2.0.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: c803b7020c3d65742c29d1e354945c61cf1c097fccd13e6ec25861410ea2d5af
4
- data.tar.gz: b0a3ca6bf7946c34cce87aa797ffb6d91a5bf65e42cdb0092d990fc6f42f327d
3
+ metadata.gz: 3694d868584973bdad19a924b7b50961e2c5fcca8ea45e7222605b5145152c24
4
+ data.tar.gz: d92fe8d7004501fab5e05cd0277f04287bafd3a2352439a1ec4fa5d2cedefe10
5
5
  SHA512:
6
- metadata.gz: 576b1afe0fc7469168de50cb94a705efe00e565f7057fc72e3a41a44bd7a8cdff9187fcd1d0ec22298e56c4c5452eaa3eb3b62c858225d16eef6e77512fe915d
7
- data.tar.gz: c90070d20e9cf952dc0d395833f35dfa4d330756023242d0d997ccd3216ad9a1eca23f535cba65efcb4e7e665d851b9e6237ecfe882ab09473b53d922f6e9978
6
+ metadata.gz: a7c05de40f9bcecbbef52444d78a3d07cde007aa7122da4eebc94d411deee65e9805c42bf2f4ead59150668346108348eca7cb04e264aa36882e925a20bd8c60
7
+ data.tar.gz: abbc47dbbd4a95b3c3e1956089308b7e4f052185698cd36e00d0f887594e17e5491a5ee28e00988cabc24831b76e8a1c5e2ccc2d4fd0dc7517a4ac1bb8bf2598
@@ -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'
@@ -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.0'
4
+ VERSION = '2.0.1'
5
5
  end
@@ -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,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mysql_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.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-06-20 00:00:00.000000000 Z
11
+ date: 2020-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.11'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.11'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: rake
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -103,6 +89,7 @@ extra_rdoc_files: []
103
89
  files:
104
90
  - lib/mysql_framework.rb
105
91
  - lib/mysql_framework/connector.rb
92
+ - lib/mysql_framework/in_condition.rb
106
93
  - lib/mysql_framework/logger.rb
107
94
  - lib/mysql_framework/scripts.rb
108
95
  - lib/mysql_framework/scripts/base.rb
@@ -150,7 +137,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
137
  - !ruby/object:Gem::Version
151
138
  version: '0'
152
139
  requirements: []
153
- rubygems_version: 3.0.4
140
+ rubyforge_project:
141
+ rubygems_version: 2.7.7
154
142
  signing_key:
155
143
  specification_version: 4
156
144
  summary: A lightweight framework to provide managers for working with MySQL.