rom-sql 3.0.1 → 3.1.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 +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 [![Join the chat at https://rom-rb.zulipchat.com](https://img.shields.io/badge/rom--rb-join%20chat-942283.svg)][chat]
|
8
8
|
|
9
9
|
[![Gem Version](https://badge.fury.io/rb/rom-sql.svg)][gem]
|
10
|
-
[![
|
10
|
+
[![CI Status](https://github.com/rom-rb/rom-sql/workflows/ci/badge.svg)][actions]
|
11
11
|
[![Code Climate](https://codeclimate.com/github/rom-rb/rom-sql/badges/gpa.svg)][codeclimate]
|
12
12
|
[![Test Coverage](https://codeclimate.com/github/rom-rb/rom-sql/badges/coverage.svg)][codeclimate]
|
13
13
|
[![Inline docs](http://inch-ci.org/github/rom-rb/rom-sql.svg?branch=master)][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
|