rom-sql 2.5.0 → 3.0.0
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 +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
|