rom-sql 1.3.2 → 1.3.3

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