rom-sql 2.5.0 → 3.3.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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +456 -278
  3. data/LICENSE +20 -0
  4. data/README.md +14 -24
  5. data/lib/rom-sql.rb +2 -0
  6. data/lib/rom/plugins/relation/sql/auto_restrictions.rb +2 -0
  7. data/lib/rom/plugins/relation/sql/instrumentation.rb +2 -0
  8. data/lib/rom/plugins/relation/sql/postgres/explain.rb +6 -7
  9. data/lib/rom/plugins/relation/sql/postgres/full_text_search.rb +53 -0
  10. data/lib/rom/plugins/relation/sql/postgres/streaming.rb +97 -0
  11. data/lib/rom/sql.rb +2 -0
  12. data/lib/rom/sql/associations.rb +2 -0
  13. data/lib/rom/sql/associations/core.rb +10 -0
  14. data/lib/rom/sql/associations/many_to_many.rb +10 -2
  15. data/lib/rom/sql/associations/many_to_one.rb +2 -0
  16. data/lib/rom/sql/associations/one_to_many.rb +2 -0
  17. data/lib/rom/sql/associations/one_to_one.rb +2 -0
  18. data/lib/rom/sql/associations/one_to_one_through.rb +2 -0
  19. data/lib/rom/sql/associations/self_ref.rb +2 -0
  20. data/lib/rom/sql/attribute.rb +87 -29
  21. data/lib/rom/sql/attribute_aliasing.rb +88 -0
  22. data/lib/rom/sql/attribute_wrapping.rb +30 -0
  23. data/lib/rom/sql/commands.rb +2 -0
  24. data/lib/rom/sql/commands/create.rb +2 -0
  25. data/lib/rom/sql/commands/delete.rb +2 -0
  26. data/lib/rom/sql/commands/error_wrapper.rb +2 -0
  27. data/lib/rom/sql/commands/update.rb +2 -0
  28. data/lib/rom/sql/dsl.rb +39 -1
  29. data/lib/rom/sql/error.rb +2 -0
  30. data/lib/rom/sql/errors.rb +2 -0
  31. data/lib/rom/sql/extensions.rb +2 -0
  32. data/lib/rom/sql/extensions/active_support_notifications.rb +2 -0
  33. data/lib/rom/sql/extensions/mysql.rb +2 -0
  34. data/lib/rom/sql/extensions/mysql/type_builder.rb +2 -0
  35. data/lib/rom/sql/extensions/postgres.rb +4 -0
  36. data/lib/rom/sql/extensions/postgres/commands.rb +3 -1
  37. data/lib/rom/sql/extensions/postgres/type_builder.rb +6 -4
  38. data/lib/rom/sql/extensions/postgres/type_serializer.rb +2 -0
  39. data/lib/rom/sql/extensions/postgres/types.rb +2 -0
  40. data/lib/rom/sql/extensions/postgres/types/array.rb +9 -8
  41. data/lib/rom/sql/extensions/postgres/types/array_types.rb +3 -1
  42. data/lib/rom/sql/extensions/postgres/types/geometric.rb +2 -0
  43. data/lib/rom/sql/extensions/postgres/types/json.rb +76 -19
  44. data/lib/rom/sql/extensions/postgres/types/ltree.rb +27 -25
  45. data/lib/rom/sql/extensions/postgres/types/network.rb +2 -0
  46. data/lib/rom/sql/extensions/postgres/types/range.rb +6 -4
  47. data/lib/rom/sql/extensions/rails_log_subscriber.rb +2 -0
  48. data/lib/rom/sql/extensions/sqlite.rb +2 -0
  49. data/lib/rom/sql/extensions/sqlite/type_builder.rb +2 -0
  50. data/lib/rom/sql/extensions/sqlite/types.rb +2 -0
  51. data/lib/rom/sql/foreign_key.rb +3 -1
  52. data/lib/rom/sql/function.rb +84 -6
  53. data/lib/rom/sql/gateway.rb +9 -1
  54. data/lib/rom/sql/group_dsl.rb +2 -0
  55. data/lib/rom/sql/index.rb +2 -0
  56. data/lib/rom/sql/join_dsl.rb +11 -0
  57. data/lib/rom/sql/mapper_compiler.rb +14 -3
  58. data/lib/rom/sql/migration.rb +20 -3
  59. data/lib/rom/sql/migration/inline_runner.rb +2 -0
  60. data/lib/rom/sql/migration/migrator.rb +5 -3
  61. data/lib/rom/sql/migration/recorder.rb +2 -0
  62. data/lib/rom/sql/migration/runner.rb +4 -2
  63. data/lib/rom/sql/migration/schema_diff.rb +4 -2
  64. data/lib/rom/sql/migration/template.rb +2 -0
  65. data/lib/rom/sql/migration/writer.rb +12 -4
  66. data/lib/rom/sql/order_dsl.rb +2 -0
  67. data/lib/rom/sql/plugin/associates.rb +4 -3
  68. data/lib/rom/sql/plugin/nullify.rb +37 -0
  69. data/lib/rom/sql/plugin/pagination.rb +22 -0
  70. data/lib/rom/sql/plugins.rb +4 -0
  71. data/lib/rom/sql/projection_dsl.rb +10 -4
  72. data/lib/rom/sql/rake_task.rb +2 -0
  73. data/lib/rom/sql/relation.rb +3 -1
  74. data/lib/rom/sql/relation/reading.rb +105 -16
  75. data/lib/rom/sql/relation/writing.rb +2 -0
  76. data/lib/rom/sql/restriction_dsl.rb +8 -8
  77. data/lib/rom/sql/schema.rb +16 -2
  78. data/lib/rom/sql/schema/attributes_inferrer.rb +7 -5
  79. data/lib/rom/sql/schema/dsl.rb +3 -1
  80. data/lib/rom/sql/schema/index_dsl.rb +8 -3
  81. data/lib/rom/sql/schema/inferrer.rb +12 -8
  82. data/lib/rom/sql/schema/type_builder.rb +4 -2
  83. data/lib/rom/sql/spec/support.rb +5 -3
  84. data/lib/rom/sql/tasks/migration_tasks.rake +16 -11
  85. data/lib/rom/sql/transaction.rb +2 -0
  86. data/lib/rom/sql/type_dsl.rb +3 -1
  87. data/lib/rom/sql/type_extensions.rb +4 -4
  88. data/lib/rom/sql/type_serializer.rb +3 -1
  89. data/lib/rom/sql/types.rb +6 -4
  90. data/lib/rom/sql/version.rb +3 -1
  91. data/lib/rom/sql/wrap.rb +2 -0
  92. data/lib/rom/types/values.rb +2 -0
  93. metadata +39 -37
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2020 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,38 +1,28 @@
1
1
  [gem]: https://rubygems.org/gems/rom-sql
2
- [travis]: https://travis-ci.org/rom-rb/rom-sql
3
- [codeclimate]: https://codeclimate.com/github/rom-rb/rom-sql
2
+ [actions]: https://github.com/rom-rb/rom-sql/actions
3
+ [codacy]: https://www.codacy.com/gh/rom-rb/rom-sql
4
+ [chat]: https://rom-rb.zulipchat.com
4
5
  [inchpages]: http://inch-ci.org/github/rom-rb/rom-sql
5
6
 
6
- # rom-sql
7
+ # rom-sql [![Join the chat at https://rom-rb.zulipchat.com](https://img.shields.io/badge/rom--rb-join%20chat-%23346b7a.svg)][chat]
7
8
 
8
9
  [![Gem Version](https://badge.fury.io/rb/rom-sql.svg)][gem]
9
- [![Build Status](https://travis-ci.org/rom-rb/rom-sql.svg?branch=master)][travis]
10
- [![Code Climate](https://codeclimate.com/github/rom-rb/rom-sql/badges/gpa.svg)][codeclimate]
11
- [![Test Coverage](https://codeclimate.com/github/rom-rb/rom-sql/badges/coverage.svg)][codeclimate]
10
+ [![CI Status](https://github.com/rom-rb/rom-sql/workflows/ci/badge.svg)][actions]
11
+ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8e2cbaf78af44185876c8fa41540d7ea)][codacy]
12
+ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/8e2cbaf78af44185876c8fa41540d7ea)][codacy]
12
13
  [![Inline docs](http://inch-ci.org/github/rom-rb/rom-sql.svg?branch=master)][inchpages]
13
14
 
14
- SQL support for [rom-rb](https://github.com/rom-rb/rom).
15
+ ## Links
15
16
 
16
- Resources:
17
+ * [User documentation](http://rom-rb.org/learn/sql)
18
+ * [API documentation](http://rubydoc.info/gems/rom-sql)
17
19
 
18
- - [User Documentation](http://rom-rb.org/learn/sql/)
19
- - [API Documentation](http://rubydoc.info/gems/rom-sql)
20
+ ## Supported Ruby versions
20
21
 
21
- ## Installation
22
+ This library officially supports the following Ruby versions:
22
23
 
23
- Add this line to your application's Gemfile:
24
-
25
- ```ruby
26
- gem 'rom-sql'
27
- ```
28
-
29
- And then execute:
30
-
31
- $ bundle
32
-
33
- Or install it yourself as:
34
-
35
- $ gem install rom-sql
24
+ * MRI >= `2.5`
25
+ * jruby >= `9.2`
36
26
 
37
27
  ## License
38
28
 
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/support/notifications'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module Plugins
3
5
  module Relation
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module Plugins
3
5
  module Relation
@@ -24,14 +26,11 @@ module ROM
24
26
  #
25
27
  # @api public
26
28
  def explain(format: :text, **options)
27
- bool_options = options.map { |opt, value| "#{ opt.to_s.upcase } #{ !!value }" }
28
- format_option = "FORMAT #{ format.to_s.upcase }"
29
+ bool_options = options.map { |opt, value| "#{opt.to_s.upcase} #{!!value}" }
30
+ format_option = "FORMAT #{format.to_s.upcase}"
31
+ explain_value = [format_option, *bool_options].join(', ')
29
32
 
30
- query =
31
- "EXPLAIN (" <<
32
- [format_option, *bool_options].join(', ') <<
33
- ") " <<
34
- dataset.sql
33
+ query = "EXPLAIN (#{explain_value}) #{dataset.sql}"
35
34
 
36
35
  rows = dataset.with_sql(query).map(:'QUERY PLAN')
37
36
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module Plugins
5
+ module Relation
6
+ module SQL
7
+ module Postgres
8
+ # PG-specific extensions which adds `Relation#full_text_search` method
9
+ #
10
+ # @api public
11
+ module FullTextSearch
12
+ # Run a full text search on PostgreSQL.
13
+ # By default, searching for the inclusion of any of the terms in any of the cols.
14
+ #
15
+ # @example
16
+ # posts.full_text_search([:title, :content], 'apples', language: 'english') # => Relation which match the 'apples' phrase
17
+ #
18
+ # @option :headline [String] Append a expression to the selected columns aliased to headline that contains an extract of the matched text.
19
+ #
20
+ # @option :language [String] The language to use for the search (default: 'simple')
21
+ #
22
+ # @option :plain [Boolean] Whether a plain search should be used (default: false). In this case, terms should be a single string, and it will do a search where cols contains all of the words in terms. This ignores search operators in terms.
23
+ #
24
+ # @option :phrase [Boolean] Similar to :plain, but also adding an ILIKE filter to ensure that returned rows also include the exact phrase used.
25
+ #
26
+ # @option :rank [Boolean] Set to true to order by the rank, so that closer matches are returned first.
27
+ #
28
+ # @option :to_tsquery [Symbol] Can be set to :plain or :phrase to specify the function to use to convert the terms to a ts_query.
29
+ #
30
+ # @option :tsquery [Boolean] Specifies the terms argument is already a valid SQL expression returning a tsquery, and can be used directly in the query.
31
+ #
32
+ # @option :tsvector [Boolean] Specifies the cols argument is already a valid SQL expression returning a tsvector, and can be used directly in the query.
33
+ #
34
+ # @return [Relation]
35
+ #
36
+ # @see https://www.postgresql.org/docs/current/textsearch.html PostgreSQL docs
37
+ #
38
+ # @api public
39
+ def full_text_search(*args, &block)
40
+ new dataset.__send__(__method__, *args, &block)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ ROM.plugins do
50
+ adapter :sql do
51
+ register :pg_full_text_search, ROM::Plugins::Relation::SQL::Postgres::FullTextSearch, type: :relation
52
+ end
53
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module Plugins
5
+ module Relation
6
+ module SQL
7
+ module Postgres
8
+ # PG-specific extensions which adds `Relation#stream` method
9
+ #
10
+ # @api public
11
+ module Streaming
12
+ extend Notifications::Listener
13
+
14
+ class StreamingNotSupportedError < StandardError; end
15
+
16
+ subscribe("configuration.gateway.connected") do |opts|
17
+ conn = opts[:connection]
18
+
19
+ next unless conn.database_type.to_sym == :postgres
20
+
21
+ next if defined?(JRUBY_VERSION)
22
+
23
+ begin
24
+ require "sequel_pg"
25
+ rescue LoadError
26
+ raise StreamingNotSupportedError, "add sequel_pg to Gemfile to use pg_streaming"
27
+ end
28
+
29
+ unless Sequel::Postgres.supports_streaming?
30
+ raise StreamingNotSupportedError, "postgres version does not support streaming"
31
+ end
32
+
33
+ conn.extension(:pg_streaming)
34
+ end
35
+
36
+ def self.included(klass)
37
+ super
38
+ ROM::Relation::Graph.include(Combined)
39
+ end
40
+
41
+ if defined?(JRUBY_VERSION)
42
+ # Allows you to stream returned rows one at a time, instead of
43
+ # collecting the entire result set in memory. Requires the `sequel_pg` gem
44
+ #
45
+ # @see https://github.com/jeremyevans/sequel_pg#streaming- sequel_pg docs
46
+ #
47
+ # @example
48
+ # posts.steam_each { |post| puts CSV.generate_line(post) }
49
+ #
50
+ # @return [Relation]
51
+ #
52
+ # @api publicY_VERSION
53
+ def stream_each
54
+ raise StreamingNotSupportedError, "not supported on jruby"
55
+ end
56
+ else
57
+ # Allows you to stream returned rows one at a time, instead of
58
+ # collecting the entire result set in memory. Requires the `sequel_pg` gem
59
+ #
60
+ # @see https://github.com/jeremyevans/sequel_pg#streaming- sequel_pg docs
61
+ #
62
+ # @example
63
+ # posts.steam_each { |post| puts CSV.generate_line(post) }
64
+ #
65
+ # @return [Relation]
66
+ #
67
+ # @api public
68
+ def stream_each
69
+ return to_enum unless block_given?
70
+
71
+ ds = dataset.stream
72
+
73
+ if auto_map?
74
+ ds.each { |tuple| yield(mapper.([output_schema[tuple]]).first) }
75
+ else
76
+ ds.each { |tuple| yield(output_schema[tuple]) }
77
+ end
78
+ end
79
+
80
+ module Combined
81
+ def stream_each
82
+ raise StreamingNotSupportedError, "not supported on combined relations"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ ROM.plugins do
94
+ adapter :sql do
95
+ register :pg_streaming, ROM::Plugins::Relation::SQL::Postgres::Streaming, type: :relation
96
+ end
97
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/equalizer'
2
4
 
3
5
  require 'rom/core'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/associations/many_to_many'
2
4
  require 'rom/sql/associations/many_to_one'
3
5
  require 'rom/sql/associations/one_to_many'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  module Associations
@@ -14,6 +16,14 @@ module ROM
14
16
 
15
17
  target.where(target_key => target_pks)
16
18
  end
19
+
20
+ # @api private
21
+ def wrapped
22
+ new_target = view ? target.send(view) : target
23
+ to_wrap = self.class.allocate
24
+ to_wrap.send(:initialize, definition, **options, target: new_target)
25
+ to_wrap.wrap
26
+ end
17
27
  end
18
28
  end
19
29
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/associations/many_to_many'
2
4
  require 'rom/sql/associations/core'
3
5
  require 'rom/sql/associations/self_ref'
@@ -18,7 +20,7 @@ module ROM
18
20
  if target != self.target
19
21
  target.schema.merge(join_schema)
20
22
  else
21
- left.schema.project(*columns)
23
+ left.schema.uniq.project(*columns)
22
24
  end
23
25
  else
24
26
  target_schema
@@ -36,8 +38,14 @@ module ROM
36
38
  # @api public
37
39
  def join(type, source = self.source, target = self.target)
38
40
  through_assoc = source.associations[through]
41
+
42
+ # first we join source to intermediary
39
43
  joined = through_assoc.join(type, source)
40
- joined.__send__(type, target.name.dataset, join_keys).qualified
44
+
45
+ # then we join intermediary to target
46
+ target_ds = target.name.dataset
47
+ through_jk = through_assoc.target.associations[target_ds].join_keys
48
+ joined.__send__(type, target_ds, through_jk).qualified
41
49
  end
42
50
 
43
51
  # @api public
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/associations/many_to_one'
2
4
  require 'rom/sql/associations/core'
3
5
  require 'rom/sql/associations/self_ref'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/associations/one_to_many'
2
4
  require 'rom/sql/associations/core'
3
5
  require 'rom/sql/associations/self_ref'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/associations/one_to_many'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/associations/many_to_many'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  module Associations
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sequel/core'
2
4
  require 'dry/core/cache'
3
5
 
@@ -5,6 +7,8 @@ require 'rom/attribute'
5
7
 
6
8
  require 'rom/sql/type_extensions'
7
9
  require 'rom/sql/projection_dsl'
10
+ require 'rom/sql/attribute_wrapping'
11
+ require 'rom/sql/attribute_aliasing'
8
12
 
9
13
  module ROM
10
14
  module SQL
@@ -12,41 +16,31 @@ module ROM
12
16
  #
13
17
  # @api public
14
18
  class Attribute < ROM::Attribute
19
+ include AttributeWrapping
20
+ include AttributeAliasing
21
+
15
22
  OPERATORS = %i[>= <= > <].freeze
16
23
  NONSTANDARD_EQUALITY_VALUES = [true, false, nil].freeze
17
- META_KEYS = %i(index foreign_key target sql_expr qualified).freeze
24
+ META_KEYS = %i[index foreign_key target sql_expr qualified].freeze
18
25
 
19
26
  # Error raised when an attribute cannot be qualified
20
27
  QualifyError = Class.new(StandardError)
21
28
 
22
29
  extend Dry::Core::Cache
23
30
 
24
- # @api private
25
- def self.[](*args)
26
- fetch_or_store(args) { new(*args) }
27
- end
28
-
29
- option :extensions, type: Types::Hash, default: -> { TypeExtensions[type] }
30
-
31
- # Return a new attribute with an alias
32
- #
33
- # @example
34
- # users[:id].aliased(:user_id)
35
- #
36
- # @return [SQL::Attribute]
37
- #
38
- # @api public
39
- def aliased(name)
40
- super.meta(name: meta.fetch(:name, name), sql_expr: sql_expr.as(name))
31
+ class << self
32
+ # @api private
33
+ def [](type, options = EMPTY_HASH)
34
+ fetch_or_store([type, options]) { new(type, **options) }
35
+ end
41
36
  end
42
- alias_method :as, :aliased
43
37
 
44
38
  # Return a new attribute in its canonical form
45
39
  #
46
40
  # @api public
47
41
  def canonical
48
42
  if aliased?
49
- meta(alias: nil, sql_expr: nil)
43
+ with(alias: nil).meta(sql_expr: nil)
50
44
  else
51
45
  self
52
46
  end
@@ -62,16 +56,32 @@ module ROM
62
56
  # @api public
63
57
  def qualified(table_alias = nil)
64
58
  return self if qualified? && table_alias.nil?
59
+ return meta(qualified: false) unless qualifiable?
65
60
 
66
61
  case sql_expr
67
62
  when Sequel::SQL::AliasedExpression, Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier
68
- type = meta(qualified: table_alias || true)
69
- type.meta(sql_expr: type.to_sql_name)
63
+ attr = meta(qualified: table_alias || true)
64
+ attr.meta(sql_expr: attr.to_sql_name)
70
65
  else
71
66
  raise QualifyError, "can't qualify #{name.inspect} (#{sql_expr.inspect})"
72
67
  end
73
68
  end
74
69
 
70
+ # Return a new attribute that is aliased and marked as qualified
71
+ #
72
+ # Intended to be used when passing attributes to `dataset#select`
73
+ #
74
+ # @return [SQL::Attribute]
75
+ #
76
+ # @api public
77
+ def qualified_projection(table_alias = nil)
78
+ if aliased?
79
+ qualified(table_alias).aliased(self.alias)
80
+ else
81
+ qualified(table_alias)
82
+ end
83
+ end
84
+
75
85
  # Return a new attribute marked as joined
76
86
  #
77
87
  # Whenever you join two schemas, the right schema's attribute
@@ -114,6 +124,15 @@ module ROM
114
124
  meta[:qualified].equal?(true) || meta[:qualified].is_a?(Symbol)
115
125
  end
116
126
 
127
+ # Return if an attribute is qualifiable
128
+ #
129
+ # @return [Boolean]
130
+ #
131
+ # @api public
132
+ def qualifiable?
133
+ !source.nil?
134
+ end
135
+
117
136
  # Return a new attribute marked as a FK
118
137
  #
119
138
  # @return [SQL::Attribute]
@@ -239,7 +258,7 @@ module ROM
239
258
  # Create a function DSL from the attribute
240
259
  #
241
260
  # @example
242
- # users[:id].func { int::count(id).as(:count) }
261
+ # users[:id].func { integer::count(id).as(:count) }
243
262
  #
244
263
  # @return [SQL::Function]
245
264
  #
@@ -281,12 +300,12 @@ module ROM
281
300
  # @api private
282
301
  def to_sql_name
283
302
  @_to_sql_name ||=
284
- if qualified? && aliased?
285
- Sequel.qualify(table_name, name).as(meta[:alias])
303
+ if qualified? && aliased_projection?
304
+ Sequel.qualify(table_name, name).as(self.alias)
286
305
  elsif qualified?
287
306
  Sequel.qualify(table_name, name)
288
- elsif aliased?
289
- Sequel.as(name, meta[:alias])
307
+ elsif aliased_projection?
308
+ Sequel.as(name, self.alias)
290
309
  else
291
310
  Sequel[name]
292
311
  end
@@ -305,7 +324,7 @@ module ROM
305
324
  end
306
325
 
307
326
  # @api private
308
- def meta_ast
327
+ def meta_options_ast
309
328
  meta = super
310
329
  meta[:index] = true if indexed?
311
330
  meta
@@ -318,7 +337,41 @@ module ROM
318
337
  cleaned_meta = meta.reject { |k, _| META_KEYS.include?(k) }
319
338
  type = optional? ? right : self.type
320
339
 
321
- self.class.new(type.with(meta: cleaned_meta), options)
340
+ self.class.new(type.with(meta: cleaned_meta), **options)
341
+ end
342
+
343
+ # Wrap a value with the type, it allows using attribute and type specific methods
344
+ # on literals and things like this
345
+ #
346
+ # @param [Object] value any SQL-serializable value
347
+ # @return [SQL::Attribute]
348
+ #
349
+ # @api public
350
+ def value(value)
351
+ meta(sql_expr: Sequel[value])
352
+ end
353
+
354
+ # Build a case expression based on attribute. See SQL::Function#case
355
+ # when you don't have a specific expression after the CASE keyword.
356
+ # Pass the :else keyword to provide the catch-all case, it's mandatory
357
+ # because of the Sequel's API used underneath.
358
+ #
359
+ # @example
360
+ # users.select_append { id.case(1 => `'first'`, else: `'other'`).as(:first_or_not) }
361
+ #
362
+ # @param [Hash] mapping mapping between SQL expressions
363
+ # @return [SQL::Attribute]
364
+ #
365
+ # @api public
366
+ def case(mapping)
367
+ mapping = mapping.dup
368
+ otherwise = mapping.delete(:else) do
369
+ raise ArgumentError, 'provide the default case using the :else keyword'
370
+ end
371
+
372
+ type = mapping.values[0].type
373
+
374
+ Attribute[type].meta(sql_expr: ::Sequel.case(mapping, otherwise, self))
322
375
  end
323
376
 
324
377
  private
@@ -376,6 +429,11 @@ module ROM
376
429
  end
377
430
  end
378
431
 
432
+ # @api private
433
+ def extensions
434
+ TypeExtensions[type]
435
+ end
436
+
379
437
  memoize :joined, :to_sql_name, :table_name, :canonical
380
438
  end
381
439
  end