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.
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