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.
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
- [travis]: https://travis-ci.org/rom-rb/rom-sql
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
- [![Build Status](https://travis-ci.org/rom-rb/rom-sql.svg?branch=master)][travis]
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
- docker-compose run --rm gem 'rspec'
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
@@ -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
@@ -151,8 +151,7 @@ module ROM
151
151
  private
152
152
 
153
153
  def cast(type, value)
154
- db_type = type.optional? ? type.right.meta[:type] : type.meta[:type]
155
- Sequel.cast(value, db_type)
154
+ Sequel.cast(value, type.meta[:type])
156
155
  end
157
156
  end
158
157
  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
- JSON = Type('json') do
20
- (SQL::Types::Array | SQL::Types::Hash)
21
- .constructor(Sequel.method(:pg_json))
22
- .meta(read: JSONRead)
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
- JSONB = Type('jsonb') do
26
- (SQL::Types::Array | SQL::Types::Hash)
27
- .constructor(Sequel.method(:pg_jsonb))
28
- .meta(read: JSONRead)
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
@@ -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
@@ -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
- [name, from: meta_options[:alias]]
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
@@ -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
- new(dataset.__send__(__method__, relation.dataset, options, &block))
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.unordered
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