rom-sql 2.5.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -2
- data/README.md +31 -0
- data/lib/rom/sql/associations/many_to_many.rb +7 -1
- data/lib/rom/sql/attribute.rb +60 -11
- data/lib/rom/sql/dsl.rb +31 -1
- data/lib/rom/sql/extensions/postgres/types/array.rb +2 -2
- data/lib/rom/sql/extensions/postgres/types/ltree.rb +2 -2
- data/lib/rom/sql/extensions/postgres/types/range.rb +4 -4
- data/lib/rom/sql/function.rb +56 -5
- data/lib/rom/sql/join_dsl.rb +9 -0
- data/lib/rom/sql/mapper_compiler.rb +3 -3
- data/lib/rom/sql/migration.rb +16 -1
- data/lib/rom/sql/migration/migrator.rb +1 -1
- data/lib/rom/sql/migration/schema_diff.rb +2 -2
- data/lib/rom/sql/plugin/associates.rb +1 -2
- data/lib/rom/sql/plugin/pagination.rb +20 -0
- data/lib/rom/sql/projection_dsl.rb +7 -3
- data/lib/rom/sql/relation/reading.rb +71 -10
- data/lib/rom/sql/restriction_dsl.rb +1 -9
- data/lib/rom/sql/schema/attributes_inferrer.rb +2 -2
- data/lib/rom/sql/schema/index_dsl.rb +4 -2
- data/lib/rom/sql/schema/type_builder.rb +2 -2
- data/lib/rom/sql/type_dsl.rb +1 -1
- data/lib/rom/sql/type_serializer.rb +1 -1
- data/lib/rom/sql/types.rb +4 -4
- data/lib/rom/sql/version.rb +1 -1
- metadata +17 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8e459278c95be56ea5da82ea6c6fec9a5bbc443f0865832cea04d3b1b56fbf5a
|
4
|
+
data.tar.gz: fa89dd7f60db5748c69eb85f804a1f2a9076ccfb22828e7dd9c27a7611c5bc2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9496cf5afbb38f14782b9104ce05519b2b35674a6fe1ad799f9f34a7f717ad67f4bb79063720818159ff723789bf15e823f55a3d350e7fb30ab6c93607fb8807
|
7
|
+
data.tar.gz: 7a39e507a4ee369e45693d835a47852b2ccad837251b4b426e41496f63650dccd05038eb4dd47126f88b023e1b49a5fae4a2e4ad7af3745f43fd69c6b31f5142
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,62 @@
|
|
1
|
+
## 3.0.0 2019-04-24
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* Join DSL so that you can use arbitrary conditions when joining relations (flash-gordon)
|
6
|
+
```ruby
|
7
|
+
users.join(tasks) { |users:, tasks:|
|
8
|
+
tasks[:user_id].is(users[:id]) & users[:name].is('John')
|
9
|
+
}
|
10
|
+
```
|
11
|
+
You also can use table aliases, however the setup is a bit hairy:
|
12
|
+
```ruby
|
13
|
+
# self-join "users" with itself using "authors" as an alias
|
14
|
+
authors = users.as(:authors).qualified(:authors)
|
15
|
+
result = users.join(authors) { |users: |
|
16
|
+
users[:id].is(authors[:id])
|
17
|
+
}.select(:name)
|
18
|
+
```
|
19
|
+
* Support for `CASE` expression (wmaciejak + flash-gordon)
|
20
|
+
```ruby
|
21
|
+
# matching expression result
|
22
|
+
users.select_append { id.case(1 => string('one'), else: string('something else')).as(:one_or_else) }
|
23
|
+
|
24
|
+
# searching for `true` result
|
25
|
+
users.select_append { string::case(id.is(1) => 'one', else: 'else').as(:one_or_else) }
|
26
|
+
```
|
27
|
+
* Relations can be accessed in DSLs with keyword arguments (flash-gordon)
|
28
|
+
```ruby
|
29
|
+
users.join(posts).select_append { |posts: | posts[:title] }
|
30
|
+
```
|
31
|
+
* Support for `.exists` in the projection DSL (flash-gordon)
|
32
|
+
```ruby
|
33
|
+
users.select_append { |posts: |
|
34
|
+
exists(posts.where(posts[:user_id] => id)).as(:has_posts)
|
35
|
+
}
|
36
|
+
```
|
37
|
+
* `Relation#unfiltered` returns an unrestricted relation (removes restrictions from `WHERE` and `HAVING`) (flash-gordon)
|
38
|
+
* Support for `WITHIN GROUP` in the function DSL has been enhanced with block syntax (flash-gordon)
|
39
|
+
```ruby
|
40
|
+
# previously available version
|
41
|
+
households.project { float::percentile_cont(0.5).within_group(income).as(:percentile) }
|
42
|
+
# using the new syntax
|
43
|
+
households.project { float::percentile_cont(0.5).within_group { income }.as(:percentile) }
|
44
|
+
```
|
45
|
+
* Support for migrator options ie `ROM::Configuration.new(:sql, migrator: { path: "my_migrations" })` (rawburt)
|
46
|
+
* `Relation#pluck` works with multiple args too (timriley)
|
47
|
+
|
48
|
+
### Changed
|
49
|
+
|
50
|
+
* [BREAKING] Updated to work with `dry-types 1.0.0` (flash-gordon)
|
51
|
+
* [BREAKING] `Types::Int` is now `Types::Integer` (GustavoCaso)
|
52
|
+
|
53
|
+
### Fixed
|
54
|
+
|
55
|
+
- Using `Relation#inner_join` with has-many-through produces correct query (issue #279) (doriantaylor + solnic)
|
56
|
+
- Aliased attributes are handled correctly in `Relation#where` (waiting-for-dev)
|
57
|
+
|
58
|
+
[Compare v2.5.0...v3.0.0](https://github.com/rom-rb/rom-sql/compare/v2.5.0...v3.0.0)
|
59
|
+
|
1
60
|
## v2.5.0 2018-06-08
|
2
61
|
|
3
62
|
### Added
|
@@ -7,7 +66,7 @@
|
|
7
66
|
tasks = relations[:tasks]
|
8
67
|
users = relations[:users]
|
9
68
|
user_tasks = tasks.where(tasks[:user_id].is(users[:id])
|
10
|
-
tasks_count = user_tasks.select {
|
69
|
+
tasks_count = user_tasks.select { integer::count(id) }
|
11
70
|
users.select_append(tasks_count.as(:tasks_count))
|
12
71
|
```
|
13
72
|
|
@@ -146,7 +205,7 @@
|
|
146
205
|
* Support for window function calls
|
147
206
|
|
148
207
|
```ruby
|
149
|
-
employees.select { [dep_no, salary,
|
208
|
+
employees.select { [dep_no, salary, integer::avg(salary).over(partition: dep_no, order: id).as(:avg_salary)] }
|
150
209
|
```
|
151
210
|
|
152
211
|
* Function result can be negated, also `ROM::SQL::Function#not` was added (flash-gordon)
|
data/README.md
CHANGED
@@ -34,6 +34,37 @@ Or install it yourself as:
|
|
34
34
|
|
35
35
|
$ gem install rom-sql
|
36
36
|
|
37
|
+
## Docker
|
38
|
+
|
39
|
+
### Development
|
40
|
+
|
41
|
+
In order to have reproducible environment for development, Docker can be used. Provided it's installed, in order to start developing, one can simply execute:
|
42
|
+
|
43
|
+
```bash
|
44
|
+
docker-compose run --rm gem "bash"
|
45
|
+
```
|
46
|
+
|
47
|
+
If this is the first time this command is executed, it will take some time to set up the dependencies and build the rom-sql container. This should happen only on first execution and in case dependency images are removed.
|
48
|
+
|
49
|
+
After dependencies are set container will be started in a bash shell.
|
50
|
+
|
51
|
+
### Testing
|
52
|
+
|
53
|
+
In order to test the changes, execute:
|
54
|
+
|
55
|
+
```bash
|
56
|
+
docker-compose build gem
|
57
|
+
docker-compose run --rm gem 'rspec'
|
58
|
+
```
|
59
|
+
|
60
|
+
### Stopping the dependencies
|
61
|
+
|
62
|
+
In order to stop the dependencies, execute:
|
63
|
+
|
64
|
+
```bash
|
65
|
+
docker-compose down --remove-orphans --volumes
|
66
|
+
```
|
67
|
+
|
37
68
|
## License
|
38
69
|
|
39
70
|
See `LICENSE` file.
|
@@ -36,8 +36,14 @@ module ROM
|
|
36
36
|
# @api public
|
37
37
|
def join(type, source = self.source, target = self.target)
|
38
38
|
through_assoc = source.associations[through]
|
39
|
+
|
40
|
+
# first we join source to intermediary
|
39
41
|
joined = through_assoc.join(type, source)
|
40
|
-
|
42
|
+
|
43
|
+
# then we join intermediary to target
|
44
|
+
target_ds = target.name.dataset
|
45
|
+
through_jk = through_assoc.target.associations[target_ds].join_keys
|
46
|
+
joined.__send__(type, target_ds, through_jk).qualified
|
41
47
|
end
|
42
48
|
|
43
49
|
# @api public
|
data/lib/rom/sql/attribute.rb
CHANGED
@@ -26,8 +26,6 @@ module ROM
|
|
26
26
|
fetch_or_store(args) { new(*args) }
|
27
27
|
end
|
28
28
|
|
29
|
-
option :extensions, type: Types::Hash, default: -> { TypeExtensions[type] }
|
30
|
-
|
31
29
|
# Return a new attribute with an alias
|
32
30
|
#
|
33
31
|
# @example
|
@@ -36,8 +34,10 @@ module ROM
|
|
36
34
|
# @return [SQL::Attribute]
|
37
35
|
#
|
38
36
|
# @api public
|
39
|
-
def aliased(
|
40
|
-
super.
|
37
|
+
def aliased(alias_name)
|
38
|
+
super.with(name: name || alias_name).meta(
|
39
|
+
sql_expr: sql_expr.as(alias_name)
|
40
|
+
)
|
41
41
|
end
|
42
42
|
alias_method :as, :aliased
|
43
43
|
|
@@ -46,7 +46,7 @@ module ROM
|
|
46
46
|
# @api public
|
47
47
|
def canonical
|
48
48
|
if aliased?
|
49
|
-
|
49
|
+
with(alias: nil).meta(sql_expr: nil)
|
50
50
|
else
|
51
51
|
self
|
52
52
|
end
|
@@ -62,11 +62,12 @@ module ROM
|
|
62
62
|
# @api public
|
63
63
|
def qualified(table_alias = nil)
|
64
64
|
return self if qualified? && table_alias.nil?
|
65
|
+
return meta(qualified: false) unless qualifiable?
|
65
66
|
|
66
67
|
case sql_expr
|
67
68
|
when Sequel::SQL::AliasedExpression, Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier
|
68
|
-
|
69
|
-
|
69
|
+
attr = meta(qualified: table_alias || true)
|
70
|
+
attr.meta(sql_expr: attr.to_sql_name)
|
70
71
|
else
|
71
72
|
raise QualifyError, "can't qualify #{name.inspect} (#{sql_expr.inspect})"
|
72
73
|
end
|
@@ -114,6 +115,15 @@ module ROM
|
|
114
115
|
meta[:qualified].equal?(true) || meta[:qualified].is_a?(Symbol)
|
115
116
|
end
|
116
117
|
|
118
|
+
# Return if an attribute is qualifiable
|
119
|
+
#
|
120
|
+
# @return [Boolean]
|
121
|
+
#
|
122
|
+
# @api public
|
123
|
+
def qualifiable?
|
124
|
+
!source.nil?
|
125
|
+
end
|
126
|
+
|
117
127
|
# Return a new attribute marked as a FK
|
118
128
|
#
|
119
129
|
# @return [SQL::Attribute]
|
@@ -239,7 +249,7 @@ module ROM
|
|
239
249
|
# Create a function DSL from the attribute
|
240
250
|
#
|
241
251
|
# @example
|
242
|
-
# users[:id].func {
|
252
|
+
# users[:id].func { integer::count(id).as(:count) }
|
243
253
|
#
|
244
254
|
# @return [SQL::Function]
|
245
255
|
#
|
@@ -282,11 +292,11 @@ module ROM
|
|
282
292
|
def to_sql_name
|
283
293
|
@_to_sql_name ||=
|
284
294
|
if qualified? && aliased?
|
285
|
-
Sequel.qualify(table_name, name).as(
|
295
|
+
Sequel.qualify(table_name, name).as(self.alias)
|
286
296
|
elsif qualified?
|
287
297
|
Sequel.qualify(table_name, name)
|
288
298
|
elsif aliased?
|
289
|
-
Sequel.as(name,
|
299
|
+
Sequel.as(name, self.alias)
|
290
300
|
else
|
291
301
|
Sequel[name]
|
292
302
|
end
|
@@ -305,7 +315,7 @@ module ROM
|
|
305
315
|
end
|
306
316
|
|
307
317
|
# @api private
|
308
|
-
def
|
318
|
+
def meta_options_ast
|
309
319
|
meta = super
|
310
320
|
meta[:index] = true if indexed?
|
311
321
|
meta
|
@@ -321,6 +331,40 @@ module ROM
|
|
321
331
|
self.class.new(type.with(meta: cleaned_meta), options)
|
322
332
|
end
|
323
333
|
|
334
|
+
# Wrap a value with the type, it allows using attribute and type specific methods
|
335
|
+
# on literals and things like this
|
336
|
+
#
|
337
|
+
# @param [Object] value any SQL-serializable value
|
338
|
+
# @return [SQL::Attribute]
|
339
|
+
#
|
340
|
+
# @api public
|
341
|
+
def value(value)
|
342
|
+
meta(sql_expr: Sequel[value])
|
343
|
+
end
|
344
|
+
|
345
|
+
# Build a case expression based on attribute. See SQL::Function#case
|
346
|
+
# when you don't have a specific expression after the CASE keyword.
|
347
|
+
# Pass the :else keyword to provide the catch-all case, it's mandatory
|
348
|
+
# because of the Sequel's API used underneath.
|
349
|
+
#
|
350
|
+
# @example
|
351
|
+
# users.select_append { id.case(1 => `'first'`, else: `'other'`).as(:first_or_not) }
|
352
|
+
#
|
353
|
+
# @param [Hash] mapping mapping between SQL expressions
|
354
|
+
# @return [SQL::Attribute]
|
355
|
+
#
|
356
|
+
# @api public
|
357
|
+
def case(mapping)
|
358
|
+
mapping = mapping.dup
|
359
|
+
otherwise = mapping.delete(:else) do
|
360
|
+
raise ArgumentError, 'provide the default case using the :else keyword'
|
361
|
+
end
|
362
|
+
|
363
|
+
type = mapping.values[0].type
|
364
|
+
|
365
|
+
Attribute[type].meta(sql_expr: ::Sequel.case(mapping, otherwise, self))
|
366
|
+
end
|
367
|
+
|
324
368
|
private
|
325
369
|
|
326
370
|
# Return Sequel Expression object for an attribute
|
@@ -376,6 +420,11 @@ module ROM
|
|
376
420
|
end
|
377
421
|
end
|
378
422
|
|
423
|
+
# @api private
|
424
|
+
def extensions
|
425
|
+
TypeExtensions[type]
|
426
|
+
end
|
427
|
+
|
379
428
|
memoize :joined, :to_sql_name, :table_name, :canonical
|
380
429
|
end
|
381
430
|
end
|
data/lib/rom/sql/dsl.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'concurrent/map'
|
1
2
|
require 'rom/support/inflector'
|
2
3
|
require 'rom/constants'
|
3
4
|
|
@@ -13,15 +14,20 @@ module ROM
|
|
13
14
|
# @return [Hash, RelationRegistry]
|
14
15
|
attr_reader :relations
|
15
16
|
|
17
|
+
# @!attribute [r] picked_relations
|
18
|
+
# @return [Concurrent::Map]
|
19
|
+
attr_reader :picked_relations
|
20
|
+
|
16
21
|
# @api private
|
17
22
|
def initialize(schema)
|
18
23
|
@schema = schema
|
19
24
|
@relations = schema.respond_to?(:relations) ? schema.relations : EMPTY_HASH
|
25
|
+
@picked_relations = ::Concurrent::Map.new
|
20
26
|
end
|
21
27
|
|
22
28
|
# @api private
|
23
29
|
def call(&block)
|
24
|
-
result = instance_exec(
|
30
|
+
result = instance_exec(select_relations(block.parameters), &block)
|
25
31
|
|
26
32
|
if result.is_a?(::Array)
|
27
33
|
result
|
@@ -41,6 +47,17 @@ module ROM
|
|
41
47
|
::Sequel.lit(value)
|
42
48
|
end
|
43
49
|
|
50
|
+
# Returns a result of SQL EXISTS clause.
|
51
|
+
#
|
52
|
+
# @example
|
53
|
+
# users.where { exists(users.where(name: 'John')) }
|
54
|
+
# users.select_append { |r| exists(r[:posts].where(r[:posts][:user_id] => id)).as(:has_posts) }
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def exists(relation)
|
58
|
+
::ROM::SQL::Attribute[Types::Bool].meta(sql_expr: relation.dataset.exists)
|
59
|
+
end
|
60
|
+
|
44
61
|
# @api private
|
45
62
|
def respond_to_missing?(name, include_private = false)
|
46
63
|
super || schema.key?(name)
|
@@ -58,6 +75,19 @@ module ROM
|
|
58
75
|
def types
|
59
76
|
::ROM::SQL::Types
|
60
77
|
end
|
78
|
+
|
79
|
+
# @api private
|
80
|
+
def select_relations(parameters)
|
81
|
+
@picked_relations.fetch_or_store(parameters.hash) do
|
82
|
+
keys = parameters.select { |type, _| type == :keyreq }
|
83
|
+
|
84
|
+
if keys.empty?
|
85
|
+
relations
|
86
|
+
else
|
87
|
+
keys.each_with_object({}) { |(_, k), rs| rs[k] = relations[k] }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
61
91
|
end
|
62
92
|
end
|
63
93
|
end
|
@@ -66,7 +66,7 @@ module ROM
|
|
66
66
|
# # @!method length
|
67
67
|
# # Return array size
|
68
68
|
# #
|
69
|
-
# # @return [SQL::Attribute<Types::
|
69
|
+
# # @return [SQL::Attribute<Types::Integer>]
|
70
70
|
# #
|
71
71
|
# # @api public
|
72
72
|
#
|
@@ -129,7 +129,7 @@ module ROM
|
|
129
129
|
end
|
130
130
|
|
131
131
|
def length(type, expr)
|
132
|
-
Attribute[SQL::Types::
|
132
|
+
Attribute[SQL::Types::Integer].meta(sql_expr: expr.pg_array.length)
|
133
133
|
end
|
134
134
|
|
135
135
|
def overlaps(type, expr, other_array)
|
@@ -248,9 +248,9 @@ module ROM
|
|
248
248
|
|
249
249
|
def build_array_query(query, array_type = 'lquery')
|
250
250
|
case query
|
251
|
-
when Array
|
251
|
+
when ::Array
|
252
252
|
ROM::SQL::Types::PG::Array(array_type)[query]
|
253
|
-
when String
|
253
|
+
when ::String
|
254
254
|
ROM::SQL::Types::PG::Array(array_type)[query.split(',')]
|
255
255
|
end
|
256
256
|
end
|
@@ -31,13 +31,13 @@ module ROM
|
|
31
31
|
|
32
32
|
@range_parsers = {
|
33
33
|
int4range: Sequel::Postgres::PGRange::Parser.new(
|
34
|
-
'int4range', SQL::Types::Coercible::
|
34
|
+
'int4range', SQL::Types::Coercible::Integer
|
35
35
|
),
|
36
36
|
int8range: Sequel::Postgres::PGRange::Parser.new(
|
37
|
-
'int8range', SQL::Types::Coercible::
|
37
|
+
'int8range', SQL::Types::Coercible::Integer
|
38
38
|
),
|
39
39
|
numrange: Sequel::Postgres::PGRange::Parser.new(
|
40
|
-
'numrange', SQL::Types::Coercible::
|
40
|
+
'numrange', SQL::Types::Coercible::Integer
|
41
41
|
),
|
42
42
|
tsrange: Sequel::Postgres::PGRange::Parser.new(
|
43
43
|
'tsrange', ::Time.method(:parse)
|
@@ -75,7 +75,7 @@ module ROM
|
|
75
75
|
# @api private
|
76
76
|
def self.range(name, read_type)
|
77
77
|
Type(name) do
|
78
|
-
type = SQL::Types.
|
78
|
+
type = SQL::Types.Nominal(Values::Range).constructor do |range|
|
79
79
|
format('%s%s,%s%s',
|
80
80
|
range.exclude_begin? ? :'(' : :'[',
|
81
81
|
range.lower,
|
data/lib/rom/sql/function.rb
CHANGED
@@ -48,7 +48,7 @@ module ROM
|
|
48
48
|
|
49
49
|
# @api private
|
50
50
|
def name
|
51
|
-
|
51
|
+
self.alias || super
|
52
52
|
end
|
53
53
|
|
54
54
|
# @see Attribute#qualified
|
@@ -60,6 +60,13 @@ module ROM
|
|
60
60
|
)
|
61
61
|
end
|
62
62
|
|
63
|
+
# @see Attribute#qualified?
|
64
|
+
#
|
65
|
+
# @api private
|
66
|
+
def qualified?(table_alias = nil)
|
67
|
+
meta[:func].args.all?(&:qualified?)
|
68
|
+
end
|
69
|
+
|
63
70
|
# @see ROM::SQL::Attribute#is
|
64
71
|
#
|
65
72
|
# @api public
|
@@ -80,8 +87,8 @@ module ROM
|
|
80
87
|
# @see https://www.postgresql.org/docs/9.6/static/tutorial-window.html
|
81
88
|
#
|
82
89
|
# @example
|
83
|
-
# users.select { [id,
|
84
|
-
# users.select { [id,
|
90
|
+
# users.select { [id, integer::row_number().over(partition: name, order: id).as(:row_no)] }
|
91
|
+
# users.select { [id, integer::row_number().over(partition: [first_name, last_name], order: id).as(:row_no)] }
|
85
92
|
#
|
86
93
|
# @example frame variants
|
87
94
|
# # ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
|
@@ -132,14 +139,34 @@ module ROM
|
|
132
139
|
Attribute[type].meta(sql_expr: ::Sequel.cast(expr, db_type))
|
133
140
|
end
|
134
141
|
|
142
|
+
# Add a CASE clause for handling if/then logic. This version of CASE search for the first
|
143
|
+
# branch which evaluates to `true`. See SQL::Attriubte#case if you're looking for the
|
144
|
+
# version that matches an expression result
|
145
|
+
#
|
146
|
+
# @example
|
147
|
+
# users.select { bool::case(status.is("active") => true, else: false).as(:activated) }
|
148
|
+
#
|
149
|
+
# @param [Hash] mapping mapping between boolean SQL expressions to arbitrary SQL expressions
|
150
|
+
# @return [ROM::SQL::Attribute]
|
151
|
+
#
|
152
|
+
# @api public
|
153
|
+
def case(mapping)
|
154
|
+
mapping = mapping.dup
|
155
|
+
otherwise = mapping.delete(:else) do
|
156
|
+
raise ArgumentError, 'provide the default case using the :else keyword'
|
157
|
+
end
|
158
|
+
|
159
|
+
Attribute[type].meta(sql_expr: ::Sequel.case(mapping, otherwise))
|
160
|
+
end
|
161
|
+
|
135
162
|
# Add a FILTER clause to aggregate function (supported by PostgreSQL 9.4+)
|
136
163
|
# @see https://www.postgresql.org/docs/current/static/sql-expressions.html
|
137
164
|
#
|
138
165
|
# Filter aggregate using the specified conditions
|
139
166
|
#
|
140
167
|
# @example
|
141
|
-
# users.project {
|
142
|
-
# users.project {
|
168
|
+
# users.project { integer::count(:id).filter(name.is("Jack")).as(:jacks) }.order(nil)
|
169
|
+
# users.project { integer::count(:id).filter { name.is("John") }).as(:johns) }.order(nil)
|
143
170
|
#
|
144
171
|
# @param [Hash,SQL::Attribute] Conditions
|
145
172
|
# @yield [block] A block with restrictions
|
@@ -158,6 +185,30 @@ module ROM
|
|
158
185
|
super(conditions)
|
159
186
|
end
|
160
187
|
|
188
|
+
# Add a WITHIN GROUP clause to aggregate function (supported by PostgreSQL)
|
189
|
+
# @see https://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES
|
190
|
+
#
|
191
|
+
# Establishes an order for an ordered-set aggregate, see the docs for more details
|
192
|
+
#
|
193
|
+
# @example
|
194
|
+
# households.project { fload::percentile_cont(0.5).within_group(income).as(:percentile) }
|
195
|
+
#
|
196
|
+
# @param [Array] A list of expressions for sorting within a group
|
197
|
+
# @yield [block] A block for getting the expressions using the Order DSL
|
198
|
+
#
|
199
|
+
# @return [SQL::Function]
|
200
|
+
#
|
201
|
+
# @api public
|
202
|
+
def within_group(*args, &block)
|
203
|
+
if block
|
204
|
+
group = args + ::ROM::SQL::OrderDSL.new(schema).(&block)
|
205
|
+
else
|
206
|
+
group = args
|
207
|
+
end
|
208
|
+
|
209
|
+
super(*group)
|
210
|
+
end
|
211
|
+
|
161
212
|
private
|
162
213
|
|
163
214
|
# @api private
|
@@ -4,10 +4,10 @@ module ROM
|
|
4
4
|
module SQL
|
5
5
|
class MapperCompiler < ROM::MapperCompiler
|
6
6
|
def visit_attribute(node)
|
7
|
-
name, _,
|
7
|
+
name, _, meta_options = node
|
8
8
|
|
9
|
-
if
|
10
|
-
[name, from:
|
9
|
+
if meta_options[:wrapped]
|
10
|
+
[name, from: meta_options[:alias]]
|
11
11
|
else
|
12
12
|
[name]
|
13
13
|
end
|
data/lib/rom/sql/migration.rb
CHANGED
@@ -98,7 +98,7 @@ module ROM
|
|
98
98
|
|
99
99
|
# @api private
|
100
100
|
def initialize(uri, options = EMPTY_HASH)
|
101
|
-
@migrator = options
|
101
|
+
@migrator = create_migrator(options[:migrator])
|
102
102
|
|
103
103
|
self.class.instance ||= self
|
104
104
|
end
|
@@ -146,6 +146,21 @@ module ROM
|
|
146
146
|
|
147
147
|
migrator.auto_migrate!(self, schemas, options)
|
148
148
|
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
# Create a `Migrator`. If `migrator_option` is a `Hash`, use it as options to `Migrator.new`.
|
153
|
+
#
|
154
|
+
# @api private
|
155
|
+
def create_migrator(migrator_option)
|
156
|
+
return Migrator.new(connection) unless migrator_option
|
157
|
+
|
158
|
+
if migrator_option.is_a?(Hash)
|
159
|
+
Migrator.new(connection, migrator_option)
|
160
|
+
else
|
161
|
+
migrator_option
|
162
|
+
end
|
163
|
+
end
|
149
164
|
end
|
150
165
|
end
|
151
166
|
end
|
@@ -20,7 +20,7 @@ module ROM
|
|
20
20
|
|
21
21
|
param :connection
|
22
22
|
|
23
|
-
option :path, type: ROM::Types.
|
23
|
+
option :path, type: ROM::Types.Nominal(Pathname), default: -> { DEFAULT_PATH }
|
24
24
|
|
25
25
|
option :inferrer, default: -> { DEFAULT_INFERRER }
|
26
26
|
|
@@ -146,8 +146,7 @@ module ROM
|
|
146
146
|
def with_association(name, opts = EMPTY_HASH)
|
147
147
|
self.class.build(
|
148
148
|
relation,
|
149
|
-
**options,
|
150
|
-
associations: associations.merge(name => opts)
|
149
|
+
{ **options, associations: associations.merge(name => opts) }
|
151
150
|
)
|
152
151
|
end
|
153
152
|
end
|
@@ -72,6 +72,26 @@ module ROM
|
|
72
72
|
(total / per_page.to_f).ceil
|
73
73
|
end
|
74
74
|
|
75
|
+
# Return one-based index of first tuple in page
|
76
|
+
#
|
77
|
+
# @return [Integer]
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def first_in_page
|
81
|
+
((current_page - 1) * per_page) + 1
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return one-based index of last tuple in page
|
85
|
+
#
|
86
|
+
# @return [Integer]
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def last_in_page
|
90
|
+
return total if current_page == total_pages
|
91
|
+
|
92
|
+
current_page * per_page
|
93
|
+
end
|
94
|
+
|
75
95
|
# @api private
|
76
96
|
def at(dataset, current_page, per_page = self.per_page)
|
77
97
|
current_page = current_page.to_i
|
@@ -34,8 +34,8 @@ module ROM
|
|
34
34
|
# @return [Rom::SQL::Function]
|
35
35
|
#
|
36
36
|
# @api public
|
37
|
-
def function(name,
|
38
|
-
::ROM::SQL::Function.new(::ROM::Types::Any, schema: schema).public_send(name,
|
37
|
+
def function(name, *attrs)
|
38
|
+
::ROM::SQL::Function.new(::ROM::Types::Any, schema: schema).public_send(name, *attrs)
|
39
39
|
end
|
40
40
|
alias_method :f, :function
|
41
41
|
|
@@ -54,7 +54,11 @@ module ROM
|
|
54
54
|
type = type(meth)
|
55
55
|
|
56
56
|
if type
|
57
|
-
|
57
|
+
if args.empty?
|
58
|
+
::ROM::SQL::Function.new(type, schema: schema)
|
59
|
+
else
|
60
|
+
::ROM::SQL::Attribute[type].value(args[0])
|
61
|
+
end
|
58
62
|
else
|
59
63
|
super
|
60
64
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rom/support/inflector'
|
2
|
+
require 'rom/sql/join_dsl'
|
2
3
|
|
3
4
|
module ROM
|
4
5
|
module SQL
|
@@ -147,15 +148,19 @@ module ROM
|
|
147
148
|
|
148
149
|
# Pluck values from a specific column
|
149
150
|
#
|
150
|
-
# @example
|
151
|
+
# @example Single value
|
151
152
|
# users.pluck(:id)
|
152
|
-
# # [1, 2
|
153
|
+
# # [1, 2]
|
154
|
+
#
|
155
|
+
# @example Multiple values
|
156
|
+
# users.pluck(:id, :name)
|
157
|
+
# # [[1, "Jane"] [2, "Joe"]]
|
153
158
|
#
|
154
159
|
# @return [Array]
|
155
160
|
#
|
156
161
|
# @api public
|
157
|
-
def pluck(
|
158
|
-
select(
|
162
|
+
def pluck(*names)
|
163
|
+
select(*names).map(names.length == 1 ? names.first : names)
|
159
164
|
end
|
160
165
|
|
161
166
|
# Rename columns in a relation
|
@@ -217,7 +222,7 @@ module ROM
|
|
217
222
|
# Project relation using column names and projection DSL
|
218
223
|
#
|
219
224
|
# @example using attributes
|
220
|
-
# users.select(:id) {
|
225
|
+
# users.select(:id) { integer::count(id).as(:count) }.group(:id).first
|
221
226
|
# # {:id => 1, :count => 1}
|
222
227
|
#
|
223
228
|
# users.select { [id, name] }
|
@@ -387,7 +392,7 @@ module ROM
|
|
387
392
|
# users.
|
388
393
|
# qualified.
|
389
394
|
# left_join(tasks).
|
390
|
-
# select { [id, name,
|
395
|
+
# select { [id, name, integer::count(:tasks__id).as(:task_count)] }.
|
391
396
|
# group(users[:id].qualified).
|
392
397
|
# having(task_count: 2)
|
393
398
|
# first
|
@@ -402,7 +407,7 @@ module ROM
|
|
402
407
|
# users.
|
403
408
|
# qualified.
|
404
409
|
# left_join(tasks).
|
405
|
-
# select { [id, name,
|
410
|
+
# select { [id, name, integer::count(:tasks__id).as(:task_count)] }.
|
406
411
|
# group(users[:id].qualified).
|
407
412
|
# having { count(id.qualified) >= 1 }.
|
408
413
|
# first
|
@@ -558,6 +563,16 @@ module ROM
|
|
558
563
|
#
|
559
564
|
# @param [Relation] relation A relation for join
|
560
565
|
#
|
566
|
+
# @overload join(relation, &block)
|
567
|
+
# Join with another relation using DSL
|
568
|
+
#
|
569
|
+
# @example
|
570
|
+
# users.join(tasks) { |users:, tasks:|
|
571
|
+
# tasks[:user_id].is(users[:id]) & users[:name].is('John')
|
572
|
+
# }
|
573
|
+
#
|
574
|
+
# @param [Relation] relation A relation for join
|
575
|
+
#
|
561
576
|
# @return [Relation]
|
562
577
|
#
|
563
578
|
# @api public
|
@@ -598,6 +613,16 @@ module ROM
|
|
598
613
|
#
|
599
614
|
# @param [Relation] relation A relation for left_join
|
600
615
|
#
|
616
|
+
# @overload join(relation, &block)
|
617
|
+
# Join with another relation using DSL
|
618
|
+
#
|
619
|
+
# @example
|
620
|
+
# users.left_join(tasks) { |users:, tasks:|
|
621
|
+
# tasks[:user_id].is(users[:id]) & users[:name].is('John')
|
622
|
+
# }
|
623
|
+
#
|
624
|
+
# @param [Relation] relation A relation for left_join
|
625
|
+
#
|
601
626
|
# @return [Relation]
|
602
627
|
#
|
603
628
|
# @api public
|
@@ -637,6 +662,16 @@ module ROM
|
|
637
662
|
#
|
638
663
|
# @param [Relation] relation A relation for right_join
|
639
664
|
#
|
665
|
+
# @overload join(relation, &block)
|
666
|
+
# Join with another relation using DSL
|
667
|
+
#
|
668
|
+
# @example
|
669
|
+
# users.right_join(tasks) { |users:, tasks:|
|
670
|
+
# tasks[:user_id].is(users[:id]) & users[:name].is('John')
|
671
|
+
# }
|
672
|
+
#
|
673
|
+
# @param [Relation] relation A relation for right_join
|
674
|
+
#
|
640
675
|
# @return [Relation]
|
641
676
|
#
|
642
677
|
# @api public
|
@@ -944,8 +979,8 @@ module ROM
|
|
944
979
|
# @example adding number of user tasks
|
945
980
|
# tasks = relations[:tasks]
|
946
981
|
# users = relations[:users]
|
947
|
-
# user_tasks = tasks.where(tasks[:user_id].is(users[:id])
|
948
|
-
# tasks_count = user_tasks.select {
|
982
|
+
# user_tasks = tasks.where(tasks[:user_id].is(users[:id]))
|
983
|
+
# tasks_count = user_tasks.select { integer::count(id) }
|
949
984
|
# users.select_append(tasks_count.as(:tasks_count))
|
950
985
|
#
|
951
986
|
# @return [SQL::Attribute]
|
@@ -955,6 +990,18 @@ module ROM
|
|
955
990
|
SQL::Attribute[attr.type].meta(sql_expr: subquery)
|
956
991
|
end
|
957
992
|
|
993
|
+
# Discard restrictions in `WHERE` and `HAVING` clauses
|
994
|
+
#
|
995
|
+
# @example calling .by_pk has no effect
|
996
|
+
# users.by_pk(1).unfiltered
|
997
|
+
#
|
998
|
+
# @return [SQL::Relation]
|
999
|
+
#
|
1000
|
+
# @api public
|
1001
|
+
def unfiltered
|
1002
|
+
new(dataset.__send__(__method__))
|
1003
|
+
end
|
1004
|
+
|
958
1005
|
private
|
959
1006
|
|
960
1007
|
# Build a locking clause
|
@@ -988,6 +1035,8 @@ module ROM
|
|
988
1035
|
if k.is_a?(Symbol) && schema.canonical.key?(k)
|
989
1036
|
type = schema.canonical[k]
|
990
1037
|
h[k] = v.is_a?(Array) ? v.map { |e| type[e] } : type[v]
|
1038
|
+
elsif k.is_a?(ROM::SQL::Attribute)
|
1039
|
+
h[k.canonical] = v
|
991
1040
|
else
|
992
1041
|
h[k] = v
|
993
1042
|
end
|
@@ -1008,7 +1057,19 @@ module ROM
|
|
1008
1057
|
elsif other.is_a?(Sequel::SQL::AliasedExpression)
|
1009
1058
|
new(dataset.__send__(type, other, join_cond, opts, &block))
|
1010
1059
|
elsif other.respond_to?(:name) && other.name.is_a?(Relation::Name)
|
1011
|
-
|
1060
|
+
if block
|
1061
|
+
join_cond = JoinDSL.new(schema).(&block)
|
1062
|
+
|
1063
|
+
if other.name.aliaz
|
1064
|
+
join_opts = { table_alias: other.name.aliaz }
|
1065
|
+
else
|
1066
|
+
join_opts = EMPTY_HASH
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
new(dataset.__send__(type, other.name.to_sym, join_cond, join_opts))
|
1070
|
+
else
|
1071
|
+
associations[other.name.key].join(type, self, other)
|
1072
|
+
end
|
1012
1073
|
else
|
1013
1074
|
raise ArgumentError, "+other+ must be either a symbol or a relation, #{other.class} given"
|
1014
1075
|
end
|
@@ -6,15 +6,7 @@ module ROM
|
|
6
6
|
class RestrictionDSL < DSL
|
7
7
|
# @api private
|
8
8
|
def call(&block)
|
9
|
-
instance_exec(
|
10
|
-
end
|
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
|
-
::ROM::SQL::Attribute[Types::Bool].meta(sql_expr: relation.dataset.exists)
|
9
|
+
instance_exec(select_relations(block.parameters), &block)
|
18
10
|
end
|
19
11
|
|
20
12
|
private
|
@@ -25,10 +25,10 @@ module ROM
|
|
25
25
|
inferred = columns.map do |(name, definition)|
|
26
26
|
type = type_builder.(definition)
|
27
27
|
|
28
|
-
attr_class.new(type.meta(
|
28
|
+
attr_class.new(type.meta(source: schema.name), name: name) if type
|
29
29
|
end.compact
|
30
30
|
|
31
|
-
missing = columns.map(&:first) - inferred.map { |attr| attr.
|
31
|
+
missing = columns.map(&:first) - inferred.map { |attr| attr.name }
|
32
32
|
|
33
33
|
[inferred, missing]
|
34
34
|
end
|
@@ -26,8 +26,10 @@ module ROM
|
|
26
26
|
end
|
27
27
|
|
28
28
|
# @api private
|
29
|
-
def call(schema_name,
|
30
|
-
attributes =
|
29
|
+
def call(schema_name, attrs)
|
30
|
+
attributes = attrs.map do |attr|
|
31
|
+
attr_class.new(attr[:type], attr[:options] || {}).meta(source: schema_name)
|
32
|
+
end
|
31
33
|
|
32
34
|
registry.map { |attr_names, options|
|
33
35
|
build_index(attributes, attr_names, options)
|
@@ -22,7 +22,7 @@ module ROM
|
|
22
22
|
DECIMAL_REGEX = /(?:decimal|numeric)\((\d+)(?:,\s*(\d+))?\)/.freeze
|
23
23
|
|
24
24
|
ruby_type_mapping(
|
25
|
-
integer: Types::
|
25
|
+
integer: Types::Integer,
|
26
26
|
string: Types::String,
|
27
27
|
time: Types::Time,
|
28
28
|
date: Types::Date,
|
@@ -37,7 +37,7 @@ module ROM
|
|
37
37
|
|
38
38
|
def call(primary_key:, db_type:, type:, allow_null:, **rest)
|
39
39
|
if primary_key
|
40
|
-
map_pk_type(type, db_type,
|
40
|
+
map_pk_type(type, db_type, rest)
|
41
41
|
else
|
42
42
|
mapped_type = map_type(type, db_type, rest)
|
43
43
|
|
data/lib/rom/sql/type_dsl.rb
CHANGED
data/lib/rom/sql/types.rb
CHANGED
@@ -17,10 +17,10 @@ module ROM
|
|
17
17
|
# @example with a custom type
|
18
18
|
# attribute :user_id, Types.ForeignKey(:users, Types::UUID)
|
19
19
|
#
|
20
|
-
# @return [Dry::Types::
|
20
|
+
# @return [Dry::Types::Nominal]
|
21
21
|
#
|
22
22
|
# @api public
|
23
|
-
def self.ForeignKey(relation, type = Types::
|
23
|
+
def self.ForeignKey(relation, type = Types::Integer.meta(index: true))
|
24
24
|
super
|
25
25
|
end
|
26
26
|
|
@@ -32,14 +32,14 @@ module ROM
|
|
32
32
|
# output { Types::Coercible::Hash }
|
33
33
|
# end
|
34
34
|
#
|
35
|
-
# @return [Dry::Types::
|
35
|
+
# @return [Dry::Types::Nominal]
|
36
36
|
#
|
37
37
|
# @api public
|
38
38
|
def self.define(value_type, &block)
|
39
39
|
TypeDSL.new(value_type).call(&block)
|
40
40
|
end
|
41
41
|
|
42
|
-
Serial =
|
42
|
+
Serial = Integer.meta(primary_key: true)
|
43
43
|
|
44
44
|
Blob = Constructor(Sequel::SQL::Blob, &Sequel::SQL::Blob.method(:new))
|
45
45
|
|
data/lib/rom/sql/version.rb
CHANGED
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:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -44,48 +44,48 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0
|
48
|
-
- - ">="
|
49
|
-
- !ruby/object:Gem::Version
|
50
|
-
version: 0.12.1
|
47
|
+
version: '1.0'
|
51
48
|
type: :runtime
|
52
49
|
prerelease: false
|
53
50
|
version_requirements: !ruby/object:Gem::Requirement
|
54
51
|
requirements:
|
55
52
|
- - "~>"
|
56
53
|
- !ruby/object:Gem::Version
|
57
|
-
version: '0
|
58
|
-
- - ">="
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: 0.12.1
|
54
|
+
version: '1.0'
|
61
55
|
- !ruby/object:Gem::Dependency
|
62
56
|
name: dry-core
|
63
57
|
requirement: !ruby/object:Gem::Requirement
|
64
58
|
requirements:
|
65
59
|
- - "~>"
|
66
60
|
- !ruby/object:Gem::Version
|
67
|
-
version: '0.
|
61
|
+
version: '0.4'
|
68
62
|
type: :runtime
|
69
63
|
prerelease: false
|
70
64
|
version_requirements: !ruby/object:Gem::Requirement
|
71
65
|
requirements:
|
72
66
|
- - "~>"
|
73
67
|
- !ruby/object:Gem::Version
|
74
|
-
version: '0.
|
68
|
+
version: '0.4'
|
75
69
|
- !ruby/object:Gem::Dependency
|
76
70
|
name: rom-core
|
77
71
|
requirement: !ruby/object:Gem::Requirement
|
78
72
|
requirements:
|
79
73
|
- - "~>"
|
80
74
|
- !ruby/object:Gem::Version
|
81
|
-
version: '
|
75
|
+
version: '5.0'
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 5.0.1
|
82
79
|
type: :runtime
|
83
80
|
prerelease: false
|
84
81
|
version_requirements: !ruby/object:Gem::Requirement
|
85
82
|
requirements:
|
86
83
|
- - "~>"
|
87
84
|
- !ruby/object:Gem::Version
|
88
|
-
version: '
|
85
|
+
version: '5.0'
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 5.0.1
|
89
89
|
- !ruby/object:Gem::Dependency
|
90
90
|
name: bundler
|
91
91
|
requirement: !ruby/object:Gem::Requirement
|
@@ -184,6 +184,7 @@ files:
|
|
184
184
|
- lib/rom/sql/gateway.rb
|
185
185
|
- lib/rom/sql/group_dsl.rb
|
186
186
|
- lib/rom/sql/index.rb
|
187
|
+
- lib/rom/sql/join_dsl.rb
|
187
188
|
- lib/rom/sql/mapper_compiler.rb
|
188
189
|
- lib/rom/sql/migration.rb
|
189
190
|
- lib/rom/sql/migration/inline_runner.rb
|
@@ -231,15 +232,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
231
232
|
requirements:
|
232
233
|
- - ">="
|
233
234
|
- !ruby/object:Gem::Version
|
234
|
-
version: 2.
|
235
|
+
version: 2.4.0
|
235
236
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
236
237
|
requirements:
|
237
238
|
- - ">="
|
238
239
|
- !ruby/object:Gem::Version
|
239
240
|
version: '0'
|
240
241
|
requirements: []
|
241
|
-
|
242
|
-
rubygems_version: 2.7.6
|
242
|
+
rubygems_version: 3.0.3
|
243
243
|
signing_key:
|
244
244
|
specification_version: 4
|
245
245
|
summary: SQL databases support for ROM
|