rom-sql 3.0.1 → 3.1.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 +252 -224
- data/LICENSE +20 -0
- data/README.md +3 -3
- data/lib/rom/sql/associations/core.rb +8 -0
- data/lib/rom/sql/attribute.rb +5 -15
- data/lib/rom/sql/attribute_aliasing.rb +60 -0
- data/lib/rom/sql/attribute_wrapping.rb +30 -0
- data/lib/rom/sql/extensions/postgres/types/array.rb +1 -2
- data/lib/rom/sql/extensions/postgres/types/json.rb +63 -8
- data/lib/rom/sql/function.rb +16 -0
- data/lib/rom/sql/gateway.rb +3 -0
- data/lib/rom/sql/mapper_compiler.rb +10 -1
- data/lib/rom/sql/plugin/nullify.rb +35 -0
- data/lib/rom/sql/plugins.rb +2 -0
- data/lib/rom/sql/relation/reading.rb +26 -2
- data/lib/rom/sql/type_extensions.rb +1 -3
- data/lib/rom/sql/version.rb +1 -1
- metadata +25 -3
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015-2019 rom-rb team
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
[gem]: https://rubygems.org/gems/rom-sql
|
2
|
-
[
|
2
|
+
[actions]: https://github.com/rom-rb/rom-sql/actions
|
3
3
|
[codeclimate]: https://codeclimate.com/github/rom-rb/rom-sql
|
4
4
|
[inchpages]: http://inch-ci.org/github/rom-rb/rom-sql
|
5
5
|
[chat]: https://rom-rb.zulipchat.com
|
@@ -7,7 +7,7 @@
|
|
7
7
|
# rom-sql [][chat]
|
8
8
|
|
9
9
|
[][gem]
|
10
|
-
[][actions]
|
11
11
|
[][codeclimate]
|
12
12
|
[][codeclimate]
|
13
13
|
[][inchpages]
|
@@ -55,7 +55,7 @@ In order to test the changes, execute:
|
|
55
55
|
|
56
56
|
```bash
|
57
57
|
docker-compose build gem
|
58
|
-
|
58
|
+
bin/run-specs
|
59
59
|
```
|
60
60
|
|
61
61
|
### Stopping the dependencies
|
@@ -14,6 +14,14 @@ module ROM
|
|
14
14
|
|
15
15
|
target.where(target_key => target_pks)
|
16
16
|
end
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
def wrapped
|
20
|
+
new_target = view ? target.send(view) : target
|
21
|
+
to_wrap = self.class.allocate
|
22
|
+
to_wrap.send(:initialize, definition, options.merge(target: new_target))
|
23
|
+
to_wrap.wrap
|
24
|
+
end
|
17
25
|
end
|
18
26
|
end
|
19
27
|
end
|
data/lib/rom/sql/attribute.rb
CHANGED
@@ -5,6 +5,8 @@ require 'rom/attribute'
|
|
5
5
|
|
6
6
|
require 'rom/sql/type_extensions'
|
7
7
|
require 'rom/sql/projection_dsl'
|
8
|
+
require 'rom/sql/attribute_wrapping'
|
9
|
+
require 'rom/sql/attribute_aliasing'
|
8
10
|
|
9
11
|
module ROM
|
10
12
|
module SQL
|
@@ -12,6 +14,9 @@ module ROM
|
|
12
14
|
#
|
13
15
|
# @api public
|
14
16
|
class Attribute < ROM::Attribute
|
17
|
+
include AttributeWrapping
|
18
|
+
include AttributeAliasing
|
19
|
+
|
15
20
|
OPERATORS = %i[>= <= > <].freeze
|
16
21
|
NONSTANDARD_EQUALITY_VALUES = [true, false, nil].freeze
|
17
22
|
META_KEYS = %i(index foreign_key target sql_expr qualified).freeze
|
@@ -26,21 +31,6 @@ module ROM
|
|
26
31
|
fetch_or_store(args) { new(*args) }
|
27
32
|
end
|
28
33
|
|
29
|
-
# Return a new attribute with an alias
|
30
|
-
#
|
31
|
-
# @example
|
32
|
-
# users[:id].aliased(:user_id)
|
33
|
-
#
|
34
|
-
# @return [SQL::Attribute]
|
35
|
-
#
|
36
|
-
# @api public
|
37
|
-
def aliased(alias_name)
|
38
|
-
super.with(name: name || alias_name).meta(
|
39
|
-
sql_expr: sql_expr.as(alias_name)
|
40
|
-
)
|
41
|
-
end
|
42
|
-
alias_method :as, :aliased
|
43
|
-
|
44
34
|
# Return a new attribute in its canonical form
|
45
35
|
#
|
46
36
|
# @api public
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module SQL
|
5
|
+
# @api private
|
6
|
+
module AttributeAliasing
|
7
|
+
# Return a new attribute with an alias
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# users[:id].aliased(:user_id)
|
11
|
+
#
|
12
|
+
# @return [SQL::Attribute]
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def aliased(alias_name)
|
16
|
+
new_name, new_alias_name = extract_alias_names(alias_name)
|
17
|
+
|
18
|
+
super(new_alias_name).with(name: new_name).meta(
|
19
|
+
sql_expr: alias_sql_expr(sql_expr, new_alias_name)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
alias as aliased
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
def alias_sql_expr(sql_expr, new_alias)
|
28
|
+
case sql_expr
|
29
|
+
when Sequel::SQL::AliasedExpression
|
30
|
+
Sequel::SQL::AliasedExpression.new(sql_expr.expression, new_alias, sql_expr.columns)
|
31
|
+
else
|
32
|
+
sql_expr.as(new_alias)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @api private
|
37
|
+
def extract_alias_names(alias_name)
|
38
|
+
new_name, new_alias_name = nil
|
39
|
+
|
40
|
+
if wrapped? && aliased?
|
41
|
+
# If the attribute is wrapped *and* aliased, make sure that we name the
|
42
|
+
# attribute in a way that will map the the requested alias name.
|
43
|
+
# Without this, the attribute will silently ignore the requested alias
|
44
|
+
# name and default to the pre-existing name.
|
45
|
+
new_name = "#{meta[:wrapped]}_#{options[:alias]}".to_sym
|
46
|
+
|
47
|
+
# Essentially, this makes it so "wrapped" attributes aren't true
|
48
|
+
# aliases, in that we actually alias the wrapped attribute, we use
|
49
|
+
# the old alias.
|
50
|
+
new_alias_name = options[:alias]
|
51
|
+
else
|
52
|
+
new_name = name || alias_name
|
53
|
+
new_alias_name = alias_name
|
54
|
+
end
|
55
|
+
|
56
|
+
[new_name, new_alias_name]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module SQL
|
5
|
+
# @api private
|
6
|
+
module AttributeWrapping
|
7
|
+
# Return if the attribute type is from a wrapped relation
|
8
|
+
#
|
9
|
+
# Wrapped attributes are used when two schemas from different relations
|
10
|
+
# are merged together. This way we can identify them easily and handle
|
11
|
+
# correctly in places like auto-mapping.
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
def wrapped?
|
15
|
+
!meta[:wrapped].nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return attribute type wrapped for the specified relation name
|
19
|
+
#
|
20
|
+
# @param [Symbol] name The name of the source relation (defaults to source.dataset)
|
21
|
+
#
|
22
|
+
# @return [Attribute]
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
def wrapped(name = source.dataset)
|
26
|
+
meta(wrapped: name).prefixed(name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'sequel/core'
|
2
|
+
require 'singleton'
|
2
3
|
|
3
4
|
Sequel.extension(*%i(pg_json pg_json_ops))
|
4
5
|
|
@@ -16,16 +17,70 @@ module ROM
|
|
16
17
|
end
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
class JSONNullType
|
21
|
+
include ::Singleton
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
'null'
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
'null'
|
29
|
+
end
|
23
30
|
end
|
24
31
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
32
|
+
JSONNull = JSONNullType.instance.freeze
|
33
|
+
|
34
|
+
if ::Sequel.respond_to?(:pg_json_wrap)
|
35
|
+
primitive_json_types = [
|
36
|
+
SQL::Types::Array,
|
37
|
+
SQL::Types::Hash,
|
38
|
+
SQL::Types::Integer,
|
39
|
+
SQL::Types::Float,
|
40
|
+
SQL::Types::String,
|
41
|
+
SQL::Types::True,
|
42
|
+
SQL::Types::False
|
43
|
+
]
|
44
|
+
|
45
|
+
JSON = Type('json') do
|
46
|
+
casts = ::Hash.new(-> v { ::Sequel.pg_json(v) })
|
47
|
+
json_null = ::Sequel.pg_json_wrap(nil)
|
48
|
+
casts[JSONNullType] = -> _ { json_null }
|
49
|
+
casts[::NilClass] = -> _ { json_null }
|
50
|
+
primitive_json_types.each do |type|
|
51
|
+
casts[type.primitive] = -> v { ::Sequel.pg_json_wrap(v) }
|
52
|
+
end
|
53
|
+
casts.freeze
|
54
|
+
|
55
|
+
[*primitive_json_types, SQL::Types.Constant(JSONNull)]
|
56
|
+
.reduce(:|)
|
57
|
+
.constructor { |value| casts[value.class].(value) }
|
58
|
+
.meta(read: JSONRead)
|
59
|
+
end
|
60
|
+
|
61
|
+
JSONB = Type('jsonb') do
|
62
|
+
casts = ::Hash.new(-> v { ::Sequel.pg_jsonb(v) })
|
63
|
+
jsonb_null = ::Sequel.pg_jsonb_wrap(nil)
|
64
|
+
casts[JSONNullType] = -> _ { jsonb_null }
|
65
|
+
casts[::NilClass] = -> _ { jsonb_null }
|
66
|
+
primitive_json_types.each do |type|
|
67
|
+
casts[type.primitive] = -> v { ::Sequel.pg_jsonb_wrap(v) }
|
68
|
+
end
|
69
|
+
casts.freeze
|
70
|
+
|
71
|
+
[*primitive_json_types, SQL::Types.Constant(JSONNull)]
|
72
|
+
.reduce(:|)
|
73
|
+
.constructor { |value| casts[value.class].(value) }
|
74
|
+
.meta(read: JSONRead)
|
75
|
+
end
|
76
|
+
else
|
77
|
+
JSON = Type('json') do
|
78
|
+
(SQL::Types::Array | SQL::Types::Hash).constructor(Sequel.method(:pg_json)).meta(read: JSONRead)
|
79
|
+
end
|
80
|
+
|
81
|
+
JSONB = Type('jsonb') do
|
82
|
+
(SQL::Types::Array | SQL::Types::Hash).constructor(Sequel.method(:pg_jsonb)).meta(read: JSONRead)
|
83
|
+
end
|
29
84
|
end
|
30
85
|
|
31
86
|
# @!parse
|
data/lib/rom/sql/function.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rom/attribute'
|
2
|
+
require 'rom/sql/attribute_wrapping'
|
2
3
|
|
3
4
|
module ROM
|
4
5
|
module SQL
|
@@ -6,6 +7,8 @@ module ROM
|
|
6
7
|
#
|
7
8
|
# @api public
|
8
9
|
class Function < ROM::Attribute
|
10
|
+
include AttributeWrapping
|
11
|
+
|
9
12
|
class << self
|
10
13
|
# @api private
|
11
14
|
def frame_limit(value)
|
@@ -37,6 +40,19 @@ module ROM
|
|
37
40
|
WINDOW_FRAMES[:rows] = WINDOW_FRAMES[rows: [:start, :current]]
|
38
41
|
WINDOW_FRAMES[range: :current] = WINDOW_FRAMES[range: [:current, :current]]
|
39
42
|
|
43
|
+
# Return a new attribute with an alias
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# string::coalesce(users[:name], users[:id]).aliased(:display_name)
|
47
|
+
#
|
48
|
+
# @return [SQL::Function]
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def aliased(alias_name)
|
52
|
+
super.with(name: name || alias_name)
|
53
|
+
end
|
54
|
+
alias_method :as, :aliased
|
55
|
+
|
40
56
|
# @api private
|
41
57
|
def sql_literal(ds)
|
42
58
|
if name
|
data/lib/rom/sql/gateway.rb
CHANGED
@@ -234,6 +234,9 @@ module ROM
|
|
234
234
|
# this will be default in Sequel 5.0.0 and since we don't rely
|
235
235
|
# on dataset mutation it is safe to enable it already
|
236
236
|
connection.extension(:freeze_datasets) unless RUBY_ENGINE == 'rbx'
|
237
|
+
|
238
|
+
# for ROM::SQL::Relation#nullify
|
239
|
+
connection.extension(:null_dataset)
|
237
240
|
end
|
238
241
|
|
239
242
|
# @api private
|
@@ -7,11 +7,20 @@ module ROM
|
|
7
7
|
name, _, meta_options = node
|
8
8
|
|
9
9
|
if meta_options[:wrapped]
|
10
|
-
[
|
10
|
+
[extract_wrapped_name(node), from: meta_options[:alias]]
|
11
11
|
else
|
12
12
|
[name]
|
13
13
|
end
|
14
14
|
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def extract_wrapped_name(node)
|
19
|
+
_, _, meta_options = node
|
20
|
+
unwrapped_name = meta_options[:alias].to_s.dup
|
21
|
+
unwrapped_name.slice!("#{meta_options[:wrapped]}_")
|
22
|
+
unwrapped_name.to_sym
|
23
|
+
end
|
15
24
|
end
|
16
25
|
end
|
17
26
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ROM
|
2
|
+
module SQL
|
3
|
+
module Plugin
|
4
|
+
# Nullify relation by
|
5
|
+
#
|
6
|
+
# @api public
|
7
|
+
module Nullify
|
8
|
+
if defined? JRUBY_VERSION
|
9
|
+
# Returns a relation that will never issue a query to the database. It
|
10
|
+
# implements the null object pattern for relations.
|
11
|
+
# Dataset#nullify doesn't work on JRuby, hence we fall back to SQL
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
def nullify
|
15
|
+
where { `1 = 0` }
|
16
|
+
end
|
17
|
+
else
|
18
|
+
# Returns a relation that will never issue a query to the database. It
|
19
|
+
# implements the null object pattern for relations.
|
20
|
+
#
|
21
|
+
# @see http://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/null_dataset_rb.html
|
22
|
+
# @example result will always be empty, regardless if records exists
|
23
|
+
# users.where(name: 'Alice').nullify
|
24
|
+
#
|
25
|
+
# @return [SQL::Relation]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def nullify
|
29
|
+
new(dataset.where { `1 = 0` }.__send__(__method__))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rom/sql/plugins.rb
CHANGED
@@ -2,10 +2,12 @@ require 'rom/plugins/relation/sql/instrumentation'
|
|
2
2
|
require 'rom/plugins/relation/sql/auto_restrictions'
|
3
3
|
|
4
4
|
require 'rom/sql/plugin/associates'
|
5
|
+
require 'rom/sql/plugin/nullify'
|
5
6
|
require 'rom/sql/plugin/pagination'
|
6
7
|
|
7
8
|
ROM.plugins do
|
8
9
|
adapter :sql do
|
10
|
+
register :nullify, ROM::SQL::Plugin::Nullify, type: :relation
|
9
11
|
register :pagination, ROM::SQL::Plugin::Pagination, type: :relation
|
10
12
|
register :associates, ROM::SQL::Plugin::Associates, type: :command
|
11
13
|
end
|
@@ -807,7 +807,16 @@ module ROM
|
|
807
807
|
#
|
808
808
|
# @api public
|
809
809
|
def union(relation, options = EMPTY_HASH, &block)
|
810
|
-
|
810
|
+
# We use the original relation name here if both relations have the
|
811
|
+
# same name. This makes it so if the user at some point references
|
812
|
+
# the relation directly by name later on things won't break in
|
813
|
+
# confusing ways.
|
814
|
+
same_relation = name == relation.name
|
815
|
+
alias_name = same_relation ? name : "#{name.to_sym}__#{relation.name.to_sym}"
|
816
|
+
opts = { alias: alias_name.to_sym, **options }
|
817
|
+
|
818
|
+
new_schema = schema.qualified(opts[:alias])
|
819
|
+
new_schema.(new(dataset.__send__(__method__, relation.dataset, opts, &block)))
|
811
820
|
end
|
812
821
|
|
813
822
|
# Checks whether a relation has at least one tuple
|
@@ -986,7 +995,7 @@ module ROM
|
|
986
995
|
# @return [SQL::Attribute]
|
987
996
|
def query
|
988
997
|
attr = schema.to_a[0]
|
989
|
-
subquery = schema.project(attr).(self).dataset
|
998
|
+
subquery = schema.project(attr).(self).dataset
|
990
999
|
SQL::Attribute[attr.type].meta(sql_expr: subquery)
|
991
1000
|
end
|
992
1001
|
|
@@ -1002,6 +1011,21 @@ module ROM
|
|
1002
1011
|
new(dataset.__send__(__method__))
|
1003
1012
|
end
|
1004
1013
|
|
1014
|
+
# Wrap other relations using association names
|
1015
|
+
#
|
1016
|
+
# @example
|
1017
|
+
# tasks.wrap(:owner)
|
1018
|
+
#
|
1019
|
+
# @param [Array<Symbol>] names A list with association identifiers
|
1020
|
+
#
|
1021
|
+
# @return [Wrap]
|
1022
|
+
#
|
1023
|
+
# @api public
|
1024
|
+
def wrap(*names)
|
1025
|
+
others = names.map { |name| associations[name].wrapped }
|
1026
|
+
wrap_around(*others)
|
1027
|
+
end
|
1028
|
+
|
1005
1029
|
private
|
1006
1030
|
|
1007
1031
|
# Build a locking clause
|