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