rom-sql 1.3.2 → 1.3.3

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
  SHA1:
3
- metadata.gz: dfb29b79f9b58b2fc9b30752951636130b525440
4
- data.tar.gz: 2f6b70d93abf536152d991cb689bd77c699a6e83
3
+ metadata.gz: df694b716a7959598f6ee28219b03220b77ef0ac
4
+ data.tar.gz: 1538e2d437b5d2f70558034265dd95f72744cb45
5
5
  SHA512:
6
- metadata.gz: ac6401a56919535a010c3ad049d63df969873c73169f629c5875edd343b404cf02ffc81f3458e9bbb6d3c20edc066fa5e26b2db30840a321f5ea10d5a86c5de4
7
- data.tar.gz: d9e27f5a201c515fcd2d2d0a2f5cacae6cfdd2f44d1dfd5c0260c2121714c0491f8c49f87ed6120690d8d7f1603dcbc7308598c7e1000c4da4b775cf8d5494fa
6
+ metadata.gz: c91a6eb888e659acded2991fc35f98a83db8bb7bd78aacbeda58a501d0f81e3f181a4314f023f0dea430162fd5542ecf7b66f92919340ea3612fd938ef3e5951
7
+ data.tar.gz: 22762cfe11ae565da6b0583e715f07079b1e2d652b74bce753739228b90052067d2185910ecd102d8a25780addb147955d60f978ff04ffe94475413c5aefcdd8
@@ -1,18 +1,43 @@
1
- ## v1.3.2 to-be-released
1
+ ## v1.3.3 to-be-released
2
2
 
3
- ## Added
3
+ ### Added
4
+
5
+ * `Relation#lock`, row-level locking using the `SELECT FOR UPDATE` clause (flash-gordon)
6
+ * `get` and `get_text` methods for the `PG::JSON` type (flash-gordon)
7
+ * Support for converting data type with `CAST` using the function DSL (flash-gordon)
8
+
9
+ ```ruby
10
+ users.select { string::cast(id, 'varchar').as(:id_str) }
11
+ ```
12
+
13
+ * Support for`EXISTS` (v-kolesnikov)
14
+
15
+ ```ruby
16
+ subquery = tasks.where(tasks[:user_id].qualified => users[:id].qualified)
17
+ users.where { exists(subquery) }
18
+ ```
19
+
20
+ ### Fixed
21
+
22
+ * Fixed a regression introduced in v1.3.2 caused by doing way more work processing the default dataset (flash-gordon)
23
+
24
+ ## v1.3.2 2017-05-13
25
+
26
+ ### Added
4
27
 
5
28
  * Support for filtering with a SQL function in the `WHERE` clause. Be sure you're using it wisely and don't call it on large datasets ;) (flash-gordon)
6
29
  * `Void` type for calling functions without returning value (flash-gordon)
7
- * Support for `PG::Array` transformations and queries (flash-gordon)
30
+ * Support for [`PG::Array` transformations and queries](https://github.com/rom-rb/rom-sql/blob/15019a40e2cf2a224476184c4cddab4062a2cc01/lib/rom/sql/extensions/postgres/types.rb#L23-L148) (flash-gordon)
8
31
 
9
- ## Fixed
32
+ ### Fixed
10
33
 
11
34
  * A bunch of warnings from Sequel 4.46
12
35
 
36
+ [Compare v1.3.1...v1.3.2](https://github.com/rom-rb/rom-sql/compare/v1.3.1...v1.3.2)
37
+
13
38
  ## v1.3.1 2017-05-05
14
39
 
15
- ## Changed
40
+ ### Changed
16
41
 
17
42
  * [internal] Compatibility with `dry-core` v0.3.0 (flash-gordon)
18
43
 
@@ -23,7 +48,7 @@
23
48
  ### Added
24
49
 
25
50
  * New `Relation#exist?` predicate checks if the relation has at least one tuple (flash-gordon)
26
- * Support for [JSONB transformations and queries](https://github.com/rom-rb/rom-sql/blob/master/lib/rom/sql/extensions/postgres/types.rb#L43-L190) using native DSL (flash-gordon)
51
+ * Support for [JSONB transformations and queries](https://github.com/rom-rb/rom-sql/blob/15019a40e2cf2a224476184c4cddab4062a2cc01/lib/rom/sql/extensions/postgres/types.rb#L170-L353) using native DSL (flash-gordon)
27
52
  * Add `ROM::SQL::Attribute#not` for negated boolean equality expressions (AMHOL)
28
53
  * Add `ROM::SQL::Attribute#!` for negated attribute's sql expressions (solnic)
29
54
  * Inferrer gets limit constraints for string data types and stores them in type's meta (v-kolesnikov)
data/Gemfile CHANGED
@@ -2,7 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: 'master'
5
+ gem 'rom', git: 'https://github.com/rom-rb/rom.git', branch: 'release-3.0'
6
6
 
7
7
  group :test do
8
8
  gem 'byebug', platforms: :mri
@@ -208,7 +208,7 @@ module ROM
208
208
  # #
209
209
  # # @param [Array<Integer>,Array<String>] path Path to extract
210
210
  # #
211
- # # @return [SQL::Attribute<Types::PG::JSONB>]
211
+ # # @return [SQL::Attribute<Types::PG::JSON>,SQL::Attribute<Types::PG::JSONB>]
212
212
  # #
213
213
  # # @api public
214
214
  #
@@ -228,7 +228,7 @@ module ROM
228
228
  # # @api public
229
229
  #
230
230
  # # @!method has_key(key)
231
- # # Does the JSON value has the specified top-level key
231
+ # # Does the JSON value have the specified top-level key
232
232
  # # Translates to ?
233
233
  # #
234
234
  # # @example
@@ -241,7 +241,7 @@ module ROM
241
241
  # # @api public
242
242
  #
243
243
  # # @!method has_any_key(*keys)
244
- # # Does the JSON value has any of the specified top-level keys
244
+ # # Does the JSON value have any of the specified top-level keys
245
245
  # # Translates to ?|
246
246
  # #
247
247
  # # @example
@@ -254,7 +254,7 @@ module ROM
254
254
  # # @api public
255
255
  #
256
256
  # # @!method has_all_keys(*keys)
257
- # # Does the JSON value has all the specified top-level keys
257
+ # # Does the JSON value have all the specified top-level keys
258
258
  # # Translates to ?&
259
259
  # #
260
260
  # # @example
@@ -302,54 +302,71 @@ module ROM
302
302
  # #
303
303
  # # @api public
304
304
  # end
305
- Attribute::TypeExtensions.register(JSONB) do
306
- def contain(type, expr, value)
307
- Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.contains(value))
308
- end
309
-
310
- def contained_by(type, expr, value)
311
- Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.contained_by(value))
305
+ module JSONMethods
306
+ def self.[](type, wrap)
307
+ parent = self
308
+ Module.new do
309
+ include parent
310
+ define_method(:json_type) { type }
311
+ define_method(:wrap, wrap)
312
+ end
312
313
  end
313
314
 
314
315
  def get(type, expr, *path)
315
- Attribute[JSONB].meta(sql_expr: expr.pg_jsonb[path_args(path)])
316
+ Attribute[json_type].meta(sql_expr: wrap(expr)[path_args(path)])
316
317
  end
317
318
 
318
319
  def get_text(type, expr, *path)
319
- Attribute[Types::String].meta(sql_expr: expr.pg_jsonb.get_text(path_args(path)))
320
+ Attribute[Types::String].meta(sql_expr: wrap(expr).get_text(path_args(path)))
321
+ end
322
+
323
+ private
324
+
325
+ def path_args(path)
326
+ case path.size
327
+ when 0 then raise ArgumentError, "wrong number of arguments (given 0, expected 1+)"
328
+ when 1 then path[0]
329
+ else path
330
+ end
331
+ end
332
+ end
333
+
334
+ Attribute::TypeExtensions.register(JSON) do
335
+ include JSONMethods[JSON, :pg_json.to_proc]
336
+ end
337
+
338
+ Attribute::TypeExtensions.register(JSONB) do
339
+ include JSONMethods[JSONB, :pg_jsonb.to_proc]
340
+
341
+ def contain(type, expr, value)
342
+ Attribute[Types::Bool].meta(sql_expr: wrap(expr).contains(value))
343
+ end
344
+
345
+ def contained_by(type, expr, value)
346
+ Attribute[Types::Bool].meta(sql_expr: wrap(expr).contained_by(value))
320
347
  end
321
348
 
322
349
  def has_key(type, expr, key)
323
- Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.has_key?(key))
350
+ Attribute[Types::Bool].meta(sql_expr: wrap(expr).has_key?(key))
324
351
  end
325
352
 
326
353
  def has_any_key(type, expr, *keys)
327
- Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.contain_any(keys))
354
+ Attribute[Types::Bool].meta(sql_expr: wrap(expr).contain_any(keys))
328
355
  end
329
356
 
330
357
  def has_all_keys(type, expr, *keys)
331
- Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.contain_all(keys))
358
+ Attribute[Types::Bool].meta(sql_expr: wrap(expr).contain_all(keys))
332
359
  end
333
360
 
334
361
  def merge(type, expr, value)
335
- Attribute[JSONB].meta(sql_expr: expr.pg_jsonb.concat(value))
362
+ Attribute[JSONB].meta(sql_expr: wrap(expr).concat(value))
336
363
  end
337
364
  alias_method :+, :merge
338
365
 
339
366
  def delete(type, expr, *path)
340
- sql_expr = path.size == 1 ? expr.pg_jsonb - path : expr.pg_jsonb.delete_path(path)
367
+ sql_expr = path.size == 1 ? wrap(expr) - path : wrap(expr).delete_path(path)
341
368
  Attribute[JSONB].meta(sql_expr: sql_expr)
342
369
  end
343
-
344
- private
345
-
346
- def path_args(path)
347
- case path.size
348
- when 0 then raise ArgumentError, "wrong number of arguments (given 0, expected 1+)"
349
- when 1 then path[0]
350
- else path
351
- end
352
- end
353
370
  end
354
371
 
355
372
  # HStore
@@ -26,6 +26,21 @@ module ROM
26
26
  ::Sequel::SQL::BooleanExpression.new(:'=', func, other)
27
27
  end
28
28
 
29
+ # Convert an expression result to another data type
30
+ #
31
+ # @example
32
+ # users.select { bool::cast(json_data.get_text('activated'), :boolean).as(:activated) }
33
+ #
34
+ # @param [ROM::SQL::Attribute] expr Expression to be cast
35
+ # @param [String] db_type Target database type
36
+ #
37
+ # @return [ROM::SQL::Attribute]
38
+ #
39
+ # @api private
40
+ def cast(expr, db_type)
41
+ Attribute[type].meta(sql_expr: ::Sequel.cast(expr, db_type))
42
+ end
43
+
29
44
  private
30
45
 
31
46
  def func
@@ -54,7 +54,7 @@ module ROM
54
54
 
55
55
  if db.table_exists?(table)
56
56
  if schema
57
- select(*schema).order(*schema.project(*schema.primary_key_names).qualified)
57
+ select(*schema.map(&:to_sql_name)).order(*schema.project(*schema.primary_key_names).qualified.map(&:to_sql_name))
58
58
  else
59
59
  select(*columns).order(*klass.primary_key_columns(db, table))
60
60
  end
@@ -7,6 +7,22 @@ module ROM
7
7
  #
8
8
  # @api public
9
9
  module Reading
10
+ # Row-level lock modes
11
+ ROW_LOCK_MODES = Hash.new(update: 'FOR UPDATE'.freeze).update(
12
+ # https://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE
13
+ postgres: {
14
+ update: 'FOR UPDATE'.freeze,
15
+ no_key_update: 'FOR NO KEY UPDATE'.freeze,
16
+ share: 'FOR SHARE'.freeze,
17
+ key_share: 'FOR KEY SHARE'.freeze
18
+ },
19
+ # https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
20
+ mysql: {
21
+ update: 'FOR UPDATE'.freeze,
22
+ share: 'LOCK IN SHARE MODE'.freeze
23
+ }
24
+ ).freeze
25
+
10
26
  # Fetch a tuple identified by the pk
11
27
  #
12
28
  # @example
@@ -810,8 +826,64 @@ module ROM
810
826
  new(dataset.db[sql], schema: schema.empty)
811
827
  end
812
828
 
829
+ # Lock rows with in the specified mode. Check out ROW_LOCK_MODES for the
830
+ # list of supported modes, keep in mind available lock modes heavily depend on
831
+ # the database type+version you're running on.
832
+ #
833
+ # @overload lock(options)
834
+ # @option options [Symbol] :mode Lock mode
835
+ # @option options [Boolean,Integer] :wait Controls the (NO)WAIT part
836
+ # @option options [Boolean] :skip_locked Skip locked rows
837
+ # @option options [Array,Symbol,String] :of List of objects in the OF part
838
+ #
839
+ # @return [SQL::Relation]
840
+ #
841
+ # @overload lock(options, &block)
842
+ # Runs the block inside a transaction. The relation will be materialized
843
+ # and passed inside the block so that the lock will be acquired right before
844
+ # the block gets executed.
845
+ #
846
+ # @param [Hash] options The same options as for the version without a block
847
+ # @yieldparam relation [Array]
848
+ #
849
+ # @api public
850
+ def lock(options = EMPTY_HASH, &block)
851
+ clause = lock_clause(options)
852
+
853
+ if block
854
+ dataset.db.transaction do
855
+ block.call(dataset.lock_style(clause).to_a)
856
+ end
857
+ else
858
+ new(dataset.lock_style(clause))
859
+ end
860
+ end
861
+
813
862
  private
814
863
 
864
+ # Build a locking clause
865
+ #
866
+ # @api private
867
+ def lock_clause(mode: :update, skip_locked: false, of: nil, wait: nil)
868
+ stmt = ROW_LOCK_MODES[dataset.db.database_type].fetch(mode).dup
869
+ stmt << ' OF ' << Array(of).join(', ') if of
870
+
871
+ if skip_locked
872
+ raise ArgumentError, "SKIP LOCKED cannot be used with (NO)WAIT clause" if !wait.nil?
873
+
874
+ stmt << ' SKIP LOCKED'
875
+ else
876
+ case wait
877
+ when Integer
878
+ stmt << ' WAIT ' << wait.to_s
879
+ when false
880
+ stmt << ' NOWAIT'
881
+ else
882
+ stmt
883
+ end
884
+ end
885
+ end
886
+
815
887
  # Apply input types to condition values
816
888
  #
817
889
  # @api private
@@ -9,6 +9,14 @@ module ROM
9
9
  instance_exec(&block)
10
10
  end
11
11
 
12
+ # Returns a result of SQL EXISTS clause.
13
+ #
14
+ # @example
15
+ # users.where { exists(users.where(name: 'John')) }
16
+ def exists(relation)
17
+ relation.dataset.exists
18
+ end
19
+
12
20
  private
13
21
 
14
22
  # @api private
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '1.3.2'.freeze
3
+ VERSION = '1.3.3'.freeze
4
4
  end
5
5
  end
@@ -1,9 +1,6 @@
1
1
  RSpec.describe 'ROM::SQL::Attribute', :postgres do
2
2
  include_context 'database setup'
3
3
 
4
- jsonb_hash = -> v { Sequel::Postgres::JSONBHash.new(v) }
5
- jsonb_array = -> v { Sequel::Postgres::JSONBArray.new(v) }
6
-
7
4
  before do
8
5
  conn.drop_table?(:pg_people)
9
6
  conn.drop_table?(:people)
@@ -16,113 +13,127 @@ RSpec.describe 'ROM::SQL::Attribute', :postgres do
16
13
  let(:people) { relations[:people] }
17
14
  let(:create_person) { commands[:people].create }
18
15
 
19
- describe 'using arrays' do
20
- before do
21
- conn.create_table :pg_people do
22
- primary_key :id
23
- String :name
24
- column :fields, :jsonb
16
+ %i(json jsonb).each do |type|
17
+ if type == :json
18
+ json_hash = Sequel::Postgres::JSONHash.method(:new)
19
+ json_array = Sequel::Postgres::JSONArray.method(:new)
20
+ else
21
+ json_hash = Sequel::Postgres::JSONBHash.method(:new)
22
+ json_array = Sequel::Postgres::JSONBArray.method(:new)
23
+ end
24
+
25
+ describe "using arrays in #{ type }" do
26
+ before do
27
+ conn.create_table :pg_people do
28
+ primary_key :id
29
+ String :name
30
+ column :fields, type
31
+ end
32
+
33
+ conf.commands(:people) do
34
+ define(:create)
35
+ define(:update)
36
+ end
37
+
38
+ create_person.(name: 'John Doe', fields: [{ name: 'age', value: '30' },
39
+ { name: 'height', value: 180 }])
40
+ create_person.(name: 'Jade Doe', fields: [{ name: 'age', value: '25' }])
25
41
  end
26
42
 
27
- conf.commands(:people) do
28
- define(:create)
29
- define(:update)
43
+ it 'fetches data from jsonb array by index' do
44
+ expect(people.select { [fields.get(1).as(:field)] }.where(name: 'John Doe').one).
45
+ to eql(field: json_hash['name' => 'height', 'value' => 180])
30
46
  end
31
47
 
32
- create_person.(name: 'John Doe', fields: [{ name: 'age', value: '30' },
33
- { name: 'height', value: 180 }])
34
- create_person.(name: 'Jade Doe', fields: [{ name: 'age', value: '25' }])
35
- end
36
-
37
- it 'allows to query jsonb by inclusion' do
38
- expect(people.select(:name).where { fields.contain([value: '30']) }.one).
39
- to eql(name: 'John Doe')
40
- end
41
-
42
- it 'cat project result of contains' do
43
- expect(people.select { fields.contain([value: '30']).as(:contains) }.to_a).
44
- to eql([{ contains: true }, { contains: false }])
45
- end
46
-
47
- it 'fetches data from jsonb array by index' do
48
- expect(people.select { [fields.get(1).as(:field)] }.where(name: 'John Doe').one).
49
- to eql(field: jsonb_hash['name' => 'height', 'value' => 180])
50
- end
51
-
52
- it 'fetches data from jsonb array' do
53
- expect(people.select { fields.get(1).get_text('value').as(:height) }.where(name: 'John Doe').one).
54
- to eql(height: '180')
55
- end
56
-
57
- it 'fetches data with path' do
58
- expect(people.select(people[:fields].get_text('1', 'value').as(:height)).to_a).
59
- to eql([{ height: '180' }, { height: nil }])
60
- end
61
-
62
- it 'deletes key from result' do
63
- expect(people.select { fields.delete(0).as(:result) }.limit(1).one).
64
- to eq(result: jsonb_array[['name' => 'height', 'value' => 180]])
65
- end
66
-
67
- it 'deletes by path' do
68
- expect(people.select { fields.delete('0', 'name').delete('1', 'name').as(:result) }.limit(1).one).
69
- to eq(result: jsonb_array[[{ 'value' => '30' }, { 'value' => 180 }]])
70
- end
71
-
72
- it 'concatenates JSON values' do
73
- expect(people.select { (fields + [name: 'height', value: 165]).as(:result) }.by_pk(2).one).
74
- to eq(result: jsonb_array[[{ 'name' => 'age', 'value' => '25' },
75
- { 'name' => 'height', 'value' => 165 }]])
76
- end
77
- end
78
-
79
- describe 'using map' do
80
- before do
81
- conn.create_table :pg_people do
82
- primary_key :id
83
- String :name
84
- column :data, :jsonb
48
+ it 'fetches data from jsonb array' do
49
+ expect(people.select { fields.get(1).get_text('value').as(:height) }.where(name: 'John Doe').one).
50
+ to eql(height: '180')
85
51
  end
86
52
 
87
- conf.commands(:people) do
88
- define(:create)
89
- define(:update)
53
+ it 'fetches data with path' do
54
+ expect(people.select(people[:fields].get_text('1', 'value').as(:height)).to_a).
55
+ to eql([{ height: '180' }, { height: nil }])
90
56
  end
91
57
 
92
- create_person.(name: 'John Doe', data: { age: 30, height: 180 })
93
- create_person.(name: 'Jade Doe', data: { age: 25 })
94
- end
95
-
96
- it 'queries data by inclusion' do
97
- expect(people.select(:name).where { data.contain(age: 30) }.one).
98
- to eql(name: 'John Doe')
99
- end
100
-
101
- it 'queries data by left inclusion' do
102
- expect(people.select(:name).where { data.contained_by(age: 25, foo: 'bar') }.one).
103
- to eql(name: 'Jade Doe')
104
- end
105
-
106
- it 'checks for key presence' do
107
- expect(people.select { data.has_key('height').as(:there) }.to_a).
108
- to eql([{ there: true }, { there: false }])
109
-
110
- expect(people.select(:name).where { data.has_any_key('height', 'width') }.one).
111
- to eql(name: 'John Doe')
112
-
113
- expect(people.select(:name).where { data.has_all_keys('height', 'age') }.one).
114
- to eql(name: 'John Doe')
115
- end
116
-
117
- it 'concatenates JSON values' do
118
- expect(people.select { data.merge(height: 165).as(:result) }.by_pk(2).one).
119
- to eql(result: jsonb_hash['age' => 25, 'height' => 165])
58
+ if type == :jsonb
59
+ it 'allows to query jsonb by inclusion' do
60
+ expect(people.select(:name).where { fields.contain([value: '30']) }.one).
61
+ to eql(name: 'John Doe')
62
+ end
63
+
64
+ it 'cat project result of contains' do
65
+ expect(people.select { fields.contain([value: '30']).as(:contains) }.to_a).
66
+ to eql([{ contains: true }, { contains: false }])
67
+ end
68
+
69
+ it 'deletes key from result' do
70
+ expect(people.select { fields.delete(0).as(:result) }.limit(1).one).
71
+ to eq(result: json_array[['name' => 'height', 'value' => 180]])
72
+ end
73
+
74
+ it 'deletes by path' do
75
+ expect(people.select { fields.delete('0', 'name').delete('1', 'name').as(:result) }.limit(1).one).
76
+ to eq(result: json_array[[{ 'value' => '30' }, { 'value' => 180 }]])
77
+ end
78
+
79
+ it 'concatenates JSON values' do
80
+ expect(people.select { (fields + [name: 'height', value: 165]).as(:result) }.by_pk(2).one).
81
+ to eq(result: json_array[[{ 'name' => 'age', 'value' => '25' },
82
+ { 'name' => 'height', 'value' => 165 }]])
83
+ end
84
+ end
120
85
  end
121
86
 
122
- it 'deletes key from result' do
123
- expect(people.select { data.delete('height').as(:result) }.to_a).
124
- to eql([{ result: jsonb_hash['age' => 30] },
125
- { result: jsonb_hash['age' => 25] }])
87
+ if type == :jsonb
88
+ describe "using maps in #{ type }" do
89
+ before do
90
+ conn.create_table :pg_people do
91
+ primary_key :id
92
+ String :name
93
+ column :data, type
94
+ end
95
+
96
+ conf.commands(:people) do
97
+ define(:create)
98
+ define(:update)
99
+ end
100
+
101
+ create_person.(name: 'John Doe', data: { age: 30, height: 180 })
102
+ create_person.(name: 'Jade Doe', data: { age: 25 })
103
+ end
104
+
105
+ it 'queries data by inclusion' do
106
+ expect(people.select(:name).where { data.contain(age: 30) }.one).
107
+ to eql(name: 'John Doe')
108
+ end
109
+
110
+ it 'queries data by left inclusion' do
111
+ expect(people.select(:name).where { data.contained_by(age: 25, foo: 'bar') }.one).
112
+ to eql(name: 'Jade Doe')
113
+ end
114
+
115
+ it 'checks for key presence' do
116
+ expect(people.select { data.has_key('height').as(:there) }.to_a).
117
+ to eql([{ there: true }, { there: false }])
118
+
119
+ expect(people.select(:name).where { data.has_any_key('height', 'width') }.one).
120
+ to eql(name: 'John Doe')
121
+
122
+ expect(people.select(:name).where { data.has_all_keys('height', 'age') }.one).
123
+ to eql(name: 'John Doe')
124
+ end
125
+
126
+ it 'concatenates JSON values' do
127
+ expect(people.select { data.merge(height: 165).as(:result) }.by_pk(2).one).
128
+ to eql(result: json_hash['age' => 25, 'height' => 165])
129
+ end
130
+
131
+ it 'deletes key from result' do
132
+ expect(people.select { data.delete('height').as(:result) }.to_a).
133
+ to eql([{ result: json_hash['age' => 30] },
134
+ { result: json_hash['age' => 25] }])
135
+ end
136
+ end
126
137
  end
127
138
  end
128
139
 
@@ -77,7 +77,7 @@ end
77
77
 
78
78
  def with_adapters(*args, &block)
79
79
  reset_adapter = Hash[*ADAPTERS.flat_map { |a| [a, false] }]
80
- adapters = args.empty? || args[0] == :all ? ADAPTERS : args
80
+ adapters = args.empty? || args[0] == :all ? ADAPTERS : (args & ADAPTERS)
81
81
 
82
82
  adapters.each do |adapter|
83
83
  context("with #{adapter}", **reset_adapter, adapter => true, &block)
@@ -38,4 +38,11 @@ RSpec.describe ROM::SQL::Function, :postgres do
38
38
  to raise_error(NoMethodError, /upper/)
39
39
  end
40
40
  end
41
+
42
+ describe '#cast' do
43
+ it 'transforms data' do
44
+ expect(func.cast(:id, 'varchar').sql_literal(ds)).
45
+ to eql(%(CAST("id" AS varchar(255))))
46
+ end
47
+ end
41
48
  end
@@ -0,0 +1,18 @@
1
+ RSpec.describe ROM::Relation, '#exists' do
2
+ include_context 'users and tasks'
3
+
4
+ let(:tasks) { container.relations.tasks }
5
+ let(:users) { container.relations.users }
6
+
7
+ with_adapters do
8
+ it 'returns true if subquery has at least one tuple' do
9
+ subquery = tasks.where(tasks[:user_id].qualified => users[:id].qualified)
10
+ expect(users.where { exists(subquery) }.count).to eql(2)
11
+ end
12
+
13
+ it 'returns false if subquery is empty' do
14
+ subquery = tasks.where(false)
15
+ expect(users.where { exists(subquery) }.count).to eql(0)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,93 @@
1
+ require 'concurrent/atomic/count_down_latch'
2
+
3
+ RSpec.describe ROM::Relation, '#lock' do
4
+ include_context 'users and tasks'
5
+
6
+ subject(:relation) { users }
7
+
8
+ def lock_style(relation)
9
+ relation.dataset.opts.fetch(:lock)
10
+ end
11
+
12
+ context 'with hitting the database' do
13
+ let(:latch) { Concurrent::CountDownLatch.new }
14
+
15
+ let(:timeout) { (defined? JRUBY_VERSION) ? 2 : 0.2 }
16
+
17
+ let!(:start) { Time.now }
18
+
19
+ def elapsed_time
20
+ Time.now.to_f - start.to_f
21
+ end
22
+
23
+ with_adapters :postgres, :mysql, :oracle do
24
+ it 'locks rows for update' do
25
+ Thread.new do
26
+ relation.lock do |rel|
27
+ latch.count_down
28
+
29
+ sleep timeout
30
+ end
31
+ end
32
+
33
+ latch.wait
34
+
35
+ expect(elapsed_time).to be < timeout
36
+
37
+ relation.lock.to_a
38
+
39
+ expect(elapsed_time).to be > timeout
40
+ end
41
+ end
42
+ end
43
+
44
+ with_adapters :postgres, :mysql, :oracle do
45
+ it 'selects rows for update' do
46
+ expect(lock_style(relation.lock)).to eql('FOR UPDATE')
47
+ end
48
+
49
+ it 'locks without wait' do
50
+ expect(lock_style(relation.lock(wait: false))).to eql('FOR UPDATE NOWAIT')
51
+ end
52
+
53
+ it 'skips locked rows' do
54
+ expect(lock_style(relation.lock(skip_locked: true))).to eql('FOR UPDATE SKIP LOCKED')
55
+ end
56
+
57
+ it 'raises an exception on attempt to use NOWAIT/WAIT with SKIP LOCKED' do
58
+ expect { relation.lock(wait: false, skip_locked: true) }
59
+ .to raise_error(ArgumentError, /cannot be used/)
60
+ end
61
+ end
62
+
63
+ with_adapters :postgres do
64
+ it 'locks with UPDATE OF' do
65
+ expect(lock_style(relation.lock(of: :users))).to eql('FOR UPDATE OF users')
66
+ expect(lock_style(relation.lock(of: :users, skip_locked: true))).to eql('FOR UPDATE OF users SKIP LOCKED')
67
+ end
68
+
69
+ it 'locks rows in different modes' do
70
+ expect(lock_style(relation.lock(mode: :update))).to eql('FOR UPDATE')
71
+ expect(lock_style(relation.lock(mode: :no_key_update))).to eql('FOR NO KEY UPDATE')
72
+ expect(lock_style(relation.lock(mode: :share))).to eql('FOR SHARE')
73
+ expect(lock_style(relation.lock(mode: :key_share))).to eql('FOR KEY SHARE')
74
+ end
75
+ end
76
+
77
+ with_adapters :mysql do
78
+ it 'locks rows in the SHARE mode' do
79
+ expect(lock_style(relation.lock(mode: :share))).to eql('LOCK IN SHARE MODE')
80
+ end
81
+ end
82
+
83
+ with_adapters :oracle do
84
+ it 'locks with timeout' do
85
+ expect(lock_style(relation.lock(wait: 10))).to eql('FOR UPDATE WAIT 10')
86
+ end
87
+
88
+ it 'locks with UPDATE OF' do
89
+ expect(lock_style(relation.lock(of: :name))).to eql('FOR UPDATE OF name')
90
+ expect(lock_style(relation.lock(of: %i(id name)))).to eql('FOR UPDATE OF id, name')
91
+ end
92
+ end
93
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rom-sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-13 00:00:00.000000000 Z
11
+ date: 2017-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -299,6 +299,7 @@ files:
299
299
  - spec/unit/relation/distinct_spec.rb
300
300
  - spec/unit/relation/exclude_spec.rb
301
301
  - spec/unit/relation/exist_predicate_spec.rb
302
+ - spec/unit/relation/exists_spec.rb
302
303
  - spec/unit/relation/fetch_spec.rb
303
304
  - spec/unit/relation/group_spec.rb
304
305
  - spec/unit/relation/having_spec.rb
@@ -307,6 +308,7 @@ files:
307
308
  - spec/unit/relation/instrument_spec.rb
308
309
  - spec/unit/relation/invert_spec.rb
309
310
  - spec/unit/relation/left_join_spec.rb
311
+ - spec/unit/relation/lock_spec.rb
310
312
  - spec/unit/relation/map_spec.rb
311
313
  - spec/unit/relation/max_spec.rb
312
314
  - spec/unit/relation/min_spec.rb
@@ -435,6 +437,7 @@ test_files:
435
437
  - spec/unit/relation/distinct_spec.rb
436
438
  - spec/unit/relation/exclude_spec.rb
437
439
  - spec/unit/relation/exist_predicate_spec.rb
440
+ - spec/unit/relation/exists_spec.rb
438
441
  - spec/unit/relation/fetch_spec.rb
439
442
  - spec/unit/relation/group_spec.rb
440
443
  - spec/unit/relation/having_spec.rb
@@ -443,6 +446,7 @@ test_files:
443
446
  - spec/unit/relation/instrument_spec.rb
444
447
  - spec/unit/relation/invert_spec.rb
445
448
  - spec/unit/relation/left_join_spec.rb
449
+ - spec/unit/relation/lock_spec.rb
446
450
  - spec/unit/relation/map_spec.rb
447
451
  - spec/unit/relation/max_spec.rb
448
452
  - spec/unit/relation/min_spec.rb