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 +4 -4
- data/CHANGELOG.md +31 -6
- data/Gemfile +1 -1
- data/lib/rom/sql/extensions/postgres/types.rb +45 -28
- data/lib/rom/sql/function.rb +15 -0
- data/lib/rom/sql/relation.rb +1 -1
- data/lib/rom/sql/relation/reading.rb +72 -0
- data/lib/rom/sql/restriction_dsl.rb +8 -0
- data/lib/rom/sql/version.rb +1 -1
- data/spec/extensions/postgres/attribute_spec.rb +111 -100
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/function_spec.rb +7 -0
- data/spec/unit/relation/exists_spec.rb +18 -0
- data/spec/unit/relation/lock_spec.rb +93 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df694b716a7959598f6ee28219b03220b77ef0ac
|
4
|
+
data.tar.gz: 1538e2d437b5d2f70558034265dd95f72744cb45
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c91a6eb888e659acded2991fc35f98a83db8bb7bd78aacbeda58a501d0f81e3f181a4314f023f0dea430162fd5542ecf7b66f92919340ea3612fd938ef3e5951
|
7
|
+
data.tar.gz: 22762cfe11ae565da6b0583e715f07079b1e2d652b74bce753739228b90052067d2185910ecd102d8a25780addb147955d60f978ff04ffe94475413c5aefcdd8
|
data/CHANGELOG.md
CHANGED
@@ -1,18 +1,43 @@
|
|
1
|
-
## v1.3.
|
1
|
+
## v1.3.3 to-be-released
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
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/
|
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
@@ -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
|
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
|
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
|
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
|
-
|
306
|
-
def
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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[
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
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
|
data/lib/rom/sql/function.rb
CHANGED
@@ -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
|
data/lib/rom/sql/relation.rb
CHANGED
@@ -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
|
data/lib/rom/sql/version.rb
CHANGED
@@ -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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
|
data/spec/spec_helper.rb
CHANGED
@@ -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)
|
data/spec/unit/function_spec.rb
CHANGED
@@ -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.
|
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-
|
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
|