quo 0.6.0 → 1.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +4 -1
  3. data/Appraisals +11 -0
  4. data/CHANGELOG.md +78 -0
  5. data/Gemfile +6 -4
  6. data/LICENSE.txt +1 -1
  7. data/README.md +37 -36
  8. data/Steepfile +0 -2
  9. data/gemfiles/rails_7.0.gemfile +15 -0
  10. data/gemfiles/rails_7.1.gemfile +15 -0
  11. data/gemfiles/rails_7.2.gemfile +15 -0
  12. data/lib/quo/collection_backed_query.rb +87 -0
  13. data/lib/quo/collection_results.rb +44 -0
  14. data/lib/quo/composed_query.rb +168 -0
  15. data/lib/quo/engine.rb +11 -0
  16. data/lib/quo/minitest/helpers.rb +41 -0
  17. data/lib/quo/preloadable.rb +46 -0
  18. data/lib/quo/query.rb +97 -214
  19. data/lib/quo/relation_backed_query.rb +177 -0
  20. data/lib/quo/relation_results.rb +58 -0
  21. data/lib/quo/results.rb +48 -44
  22. data/lib/quo/rspec/helpers.rb +31 -9
  23. data/lib/quo/testing/collection_backed_fake.rb +29 -0
  24. data/lib/quo/testing/relation_backed_fake.rb +52 -0
  25. data/lib/quo/version.rb +3 -1
  26. data/lib/quo.rb +22 -30
  27. data/rbs_collection.yaml +0 -2
  28. data/sig/generated/quo/collection_backed_query.rbs +39 -0
  29. data/sig/generated/quo/collection_results.rbs +30 -0
  30. data/sig/generated/quo/composed_query.rbs +83 -0
  31. data/sig/generated/quo/engine.rbs +6 -0
  32. data/sig/generated/quo/preloadable.rbs +29 -0
  33. data/sig/generated/quo/query.rbs +98 -0
  34. data/sig/generated/quo/relation_backed_query.rbs +90 -0
  35. data/sig/generated/quo/relation_results.rbs +38 -0
  36. data/sig/generated/quo/results.rbs +39 -0
  37. data/sig/generated/quo/version.rbs +5 -0
  38. data/sig/generated/quo.rbs +9 -0
  39. metadata +67 -30
  40. data/lib/quo/eager_query.rb +0 -51
  41. data/lib/quo/loaded_query.rb +0 -18
  42. data/lib/quo/merged_query.rb +0 -36
  43. data/lib/quo/query_composer.rb +0 -78
  44. data/lib/quo/railtie.rb +0 -7
  45. data/lib/quo/utilities/callstack.rb +0 -21
  46. data/lib/quo/utilities/compose.rb +0 -18
  47. data/lib/quo/utilities/sanitize.rb +0 -19
  48. data/lib/quo/utilities/wrap.rb +0 -23
  49. data/lib/quo/wrapped_query.rb +0 -18
  50. data/sig/quo/eager_query.rbs +0 -15
  51. data/sig/quo/loaded_query.rbs +0 -7
  52. data/sig/quo/merged_query.rbs +0 -19
  53. data/sig/quo/query.rbs +0 -83
  54. data/sig/quo/query_composer.rbs +0 -32
  55. data/sig/quo/results.rbs +0 -22
  56. data/sig/quo/utilities/callstack.rbs +0 -7
  57. data/sig/quo/utilities/compose.rbs +0 -8
  58. data/sig/quo/utilities/sanitize.rbs +0 -9
  59. data/sig/quo/utilities/wrap.rbs +0 -11
  60. data/sig/quo/wrapped_query.rbs +0 -11
  61. data/sig/quo.rbs +0 -41
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Quo
6
+ module ComposedQuery
7
+ # Combine two Query classes into a new composed query class
8
+ # Combine two query-like or composeable entities:
9
+ # These can be Quo::Query, Quo::ComposedQuery, Quo::CollectionBackedQuery and ActiveRecord::Relations.
10
+ # See the `README.md` docs for more details.
11
+ # @rbs chosen_superclass: singleton(Quo::RelationBackedQuery | Quo::CollectionBackedQuery)
12
+ # @rbs left_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
13
+ # @rbs right_query_class: singleton(Quo::Query | ::ActiveRecord::Relation)
14
+ # @rbs joins: untyped
15
+ # @rbs return: singleton(Quo::ComposedQuery)
16
+ def composer(chosen_superclass, left_query_class, right_query_class, joins: nil)
17
+ unless left_query_class.respond_to?(:<) && right_query_class.respond_to?(:<)
18
+ raise ArgumentError, "Cannot compose #{left_query_class} and #{right_query_class}, are they both classes? If you want to use instances use `.merge_instances`"
19
+ end
20
+ props = {}
21
+ props.merge!(left_query_class.literal_properties.properties_index) if left_query_class < Quo::Query
22
+ props.merge!(right_query_class.literal_properties.properties_index) if right_query_class < Quo::Query
23
+
24
+ klass = Class.new(chosen_superclass) do
25
+ include Quo::ComposedQuery
26
+
27
+ class << self
28
+ attr_reader :_composing_joins, :_left_query, :_right_query
29
+
30
+ def inspect
31
+ left_desc = quo_operand_desc(_left_query)
32
+ right_desc = quo_operand_desc(_right_query)
33
+ klass_name = (self < Quo::RelationBackedQuery) ? Quo.relation_backed_query_base_class.name : Quo.collection_backed_query_base_class.name
34
+ "#{klass_name}<Quo::ComposedQuery>[#{left_desc}, #{right_desc}]"
35
+ end
36
+
37
+ # @rbs operand: Quo::ComposedQuery | Quo::Query | ::ActiveRecord::Relation
38
+ # @rbs return: String
39
+ def quo_operand_desc(operand)
40
+ if operand < Quo::ComposedQuery
41
+ operand.inspect
42
+ else
43
+ operand.name || operand.superclass&.name || "(anonymous)"
44
+ end
45
+ end
46
+ end
47
+
48
+ props.each do |name, property|
49
+ prop name, property.type, property.kind, reader: property.reader, writer: property.writer, default: property.default
50
+ end
51
+ end
52
+ klass.instance_variable_set(:@_composing_joins, joins)
53
+ klass.instance_variable_set(:@_left_query, left_query_class)
54
+ klass.instance_variable_set(:@_right_query, right_query_class)
55
+ klass
56
+ end
57
+ module_function :composer
58
+
59
+ # We can also merge instance of prepared queries
60
+ # @rbs left_instance: Quo::Query | ::ActiveRecord::Relation
61
+ # @rbs right_instance: Quo::Query | ::ActiveRecord::Relation
62
+ # @rbs joins: untyped
63
+ # @rbs return: Quo::ComposedQuery
64
+ def merge_instances(left_instance, right_instance, joins: nil)
65
+ raise ArgumentError, "Cannot merge, left has incompatible type #{left_instance.class}" unless left_instance.is_a?(Quo::Query) || left_instance.is_a?(::ActiveRecord::Relation)
66
+ raise ArgumentError, "Cannot merge, right has incompatible type #{right_instance.class}" unless right_instance.is_a?(Quo::Query) || right_instance.is_a?(::ActiveRecord::Relation)
67
+ if left_instance.is_a?(Quo::Query) && right_instance.is_a?(::ActiveRecord::Relation)
68
+ return composer(left_instance.is_a?(Quo::RelationBackedQuery) ? Quo.relation_backed_query_base_class : Quo.collection_backed_query_base_class, left_instance.class, right_instance, joins: joins).new(**left_instance.to_h)
69
+ elsif right_instance.is_a?(Quo::Query) && left_instance.is_a?(::ActiveRecord::Relation)
70
+ return composer(right_instance.is_a?(Quo::RelationBackedQuery) ? Quo.relation_backed_query_base_class : Quo.collection_backed_query_base_class, left_instance, right_instance.class, joins: joins).new(**right_instance.to_h)
71
+ elsif left_instance.is_a?(Quo::Query) && right_instance.is_a?(Quo::Query)
72
+ props = left_instance.to_h.merge(right_instance.to_h.compact)
73
+ return composer((left_instance.is_a?(Quo::RelationBackedQuery) && right_instance.is_a?(Quo::RelationBackedQuery)) ? Quo.relation_backed_query_base_class : Quo.collection_backed_query_base_class, left_instance.class, right_instance.class, joins: joins).new(**props)
74
+ end
75
+ composer(Quo.relation_backed_query_base_class, left_instance, right_instance, joins: joins).new # Both are AR relations
76
+ end
77
+ module_function :merge_instances
78
+
79
+ # @rbs override
80
+ def query
81
+ merge_left_and_right
82
+ end
83
+
84
+ # @rbs override
85
+ def inspect
86
+ klass_name = is_a?(Quo::RelationBackedQuery) ? Quo::RelationBackedQuery.name : Quo::CollectionBackedQuery.name
87
+ "#{klass_name}<Quo::ComposedQuery>[#{self.class.quo_operand_desc(left.class)}, #{self.class.quo_operand_desc(right.class)}](#{super})"
88
+ end
89
+
90
+ private
91
+
92
+ # @rbs return: Hash[Symbol, untyped]
93
+ def child_options(query_class)
94
+ names = property_names(query_class)
95
+ to_h.slice(*names)
96
+ end
97
+
98
+ # @rbs return: Array[Symbol]
99
+ def property_names(query_class)
100
+ query_class.literal_properties.properties_index.keys
101
+ end
102
+
103
+ # @rbs return: Quo::Query | ::ActiveRecord::Relation
104
+ def left
105
+ lq = self.class._left_query
106
+ return lq if is_relation?(lq)
107
+ lq.new(**child_options(lq))
108
+ end
109
+
110
+ # @rbs return: Quo::Query | ::ActiveRecord::Relation
111
+ def right
112
+ rq = self.class._right_query
113
+ return rq if is_relation?(rq)
114
+ rq.new(**child_options(rq))
115
+ end
116
+
117
+ # @rbs return: ActiveRecord::Relation | CollectionBackedQuery
118
+ def merge_left_and_right
119
+ left_rel = quo_unwrap_unpaginated_query(left)
120
+ right_rel = quo_unwrap_unpaginated_query(right)
121
+ if both_relations?(left_rel, right_rel)
122
+ apply_joins(left_rel).merge(right_rel) # ActiveRecord::Relation
123
+ elsif left_relation_right_enumerable?(left_rel, right_rel)
124
+ left_rel.to_a + right_rel
125
+ elsif left_enumerable_right_relation?(left_rel, right_rel) && left_rel.respond_to?(:+)
126
+ left_rel + right_rel.to_a
127
+ elsif left_rel.respond_to?(:+)
128
+ left_rel + right_rel
129
+ else
130
+ raise ArgumentError, "Cannot merge #{left.class} with #{right.class}"
131
+ end
132
+ end
133
+
134
+ # @rbs left_rel: ActiveRecord::Relation
135
+ # @rbs return: ActiveRecord::Relation
136
+ def apply_joins(left_rel)
137
+ joins = self.class._composing_joins
138
+ joins.present? ? left_rel.joins(joins) : left_rel
139
+ end
140
+
141
+ # @rbs rel: untyped
142
+ # @rbs return: bool
143
+ def is_relation?(rel)
144
+ rel.is_a?(::ActiveRecord::Relation)
145
+ end
146
+
147
+ # @rbs left: untyped
148
+ # @rbs right: untyped
149
+ # @rbs return: bool
150
+ def both_relations?(left, right)
151
+ is_relation?(left) && is_relation?(right)
152
+ end
153
+
154
+ # @rbs left: untyped
155
+ # @rbs right: untyped
156
+ # @rbs return: bool
157
+ def left_relation_right_enumerable?(left, right)
158
+ is_relation?(left) && !is_relation?(right)
159
+ end
160
+
161
+ # @rbs left: untyped
162
+ # @rbs right: untyped
163
+ # @rbs return: bool
164
+ def left_enumerable_right_relation?(left, right)
165
+ !is_relation?(left) && is_relation?(right)
166
+ end
167
+ end
168
+ end
data/lib/quo/engine.rb ADDED
@@ -0,0 +1,11 @@
1
+ # rbs_inline: enabled
2
+
3
+ module Quo
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Quo
6
+
7
+ rake_tasks do
8
+ load "tasks/quo.rake"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/mock"
4
+
5
+ require_relative "../testing/collection_backed_fake"
6
+ require_relative "../testing/relation_backed_fake"
7
+
8
+ module Quo
9
+ module Minitest
10
+ module Helpers
11
+ def fake_query(query_class, results: [], total_count: nil, page_count: nil, &block)
12
+ # make it so that results of instances of this class return a fake Result object
13
+ # of the right type which returns the results passed in
14
+ if query_class < Quo::CollectionBackedQuery
15
+ klass = Class.new(Quo::Testing::CollectionBackedFake) do
16
+ if query_class < Quo::Preloadable
17
+ include Quo::Preloadable
18
+
19
+ def query
20
+ collection
21
+ end
22
+ end
23
+ end
24
+ query_class.stub(:new, ->(**kwargs) {
25
+ klass.new(results: results, total_count: total_count, page_count: page_count)
26
+ }) do
27
+ yield
28
+ end
29
+ elsif query_class < Quo::RelationBackedQuery
30
+ query_class.stub(:new, ->(**kwargs) {
31
+ Quo::Testing::RelationBackedFake.new(results: results, total_count: total_count, page_count: page_count)
32
+ }) do
33
+ yield
34
+ end
35
+ else
36
+ raise ArgumentError, "Not a Query class: #{query_class}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Quo
6
+ module Preloadable
7
+ def self.included(base)
8
+ base.prop :_rel_preload, base._Nilable(base._Any), reader: false, writer: false
9
+ end
10
+
11
+ # This implementation of `query` calls `collection` and preloads the includes.
12
+ # @rbs return: Object & Enumerable[untyped]
13
+ def query
14
+ records = collection
15
+ preload_includes(records) if @_rel_preload
16
+ records
17
+ end
18
+
19
+ # For use with collections of ActiveRecord models.
20
+ # Configures ActiveRecord::Associations::Preloader to load associations of models in the collection
21
+ # @rbs *options: untyped
22
+ # @rbs return: Quo::Query
23
+ def preload(*options)
24
+ copy(_rel_preload: options)
25
+ end
26
+
27
+ # Alias for `preload`
28
+ # @rbs *options: untyped
29
+ # @rbs return: Quo::Query
30
+ def includes(*options)
31
+ preload(*options)
32
+ end
33
+
34
+ private
35
+
36
+ # @rbs @_rel_preload: untyped?
37
+
38
+ # @rbs (untyped records, ?untyped? preload) -> untyped
39
+ def preload_includes(records, preload = nil)
40
+ ::ActiveRecord::Associations::Preloader.new(
41
+ records: records,
42
+ associations: preload || @_rel_preload
43
+ ).call
44
+ end
45
+ end
46
+ end
data/lib/quo/query.rb CHANGED
@@ -1,302 +1,185 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./utilities/callstack"
4
- require_relative "./utilities/compose"
5
- require_relative "./utilities/sanitize"
6
- require_relative "./utilities/wrap"
3
+ # rbs_inline: enabled
7
4
 
8
- module Quo
9
- class Query
10
- include Quo::Utilities::Callstack
11
-
12
- extend Quo::Utilities::Compose
13
- extend Quo::Utilities::Sanitize
14
- extend Quo::Utilities::Wrap
15
-
16
- class << self
17
- def call(**options)
18
- new(**options).first
19
- end
20
-
21
- def call!(**options)
22
- new(**options).first!
23
- end
24
- end
25
-
26
- attr_reader :current_page, :page_size, :options
5
+ require "literal"
27
6
 
28
- def initialize(**options)
29
- @options = options
30
- @current_page = options[:page]&.to_i || options[:current_page]&.to_i
31
- @page_size = options[:page_size]&.to_i || Quo.configuration.default_page_size || 20
32
- end
33
-
34
- # Returns a active record query, or a Quo::Query instance
35
- def query
36
- raise NotImplementedError, "Query objects must define a 'query' method"
37
- end
7
+ module Quo
8
+ class Query < Literal::Struct
9
+ include Literal::Types
38
10
 
39
- # Combine (compose) this query object with another composeable entity, see notes for `.compose` above.
40
- # Compose is aliased as `+`. Can optionally take `joins()` parameters to perform a joins before the merge
41
- def compose(right, joins: nil)
42
- Quo::QueryComposer.new(self, right, joins).compose
11
+ def self.inspect
12
+ "#{name || "(anonymous)"}<#{superclass}>"
43
13
  end
44
14
 
45
- alias_method :+, :compose
46
-
47
- def copy(**options)
48
- self.class.new(**@options.merge(options))
15
+ def self.to_s
16
+ inspect
49
17
  end
50
18
 
51
- # Methods to prepare the query
52
- def limit(limit)
53
- copy(limit: limit)
19
+ def inspect
20
+ "#{self.class.name || "(anonymous)"}<#{self.class.superclass} #{paged? ? "" : "not "}paginated>#{super}"
54
21
  end
55
22
 
56
- def order(options)
57
- copy(order: options)
23
+ def to_s
24
+ inspect
58
25
  end
59
26
 
60
- def group(*options)
61
- copy(group: options)
27
+ # TODO: put this in a module with the composer and merge_instances methods
28
+ # Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation.
29
+ # @rbs right: Quo::Query | ActiveRecord::Relation | Object & Enumerable[untyped]
30
+ # @rbs joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]]
31
+ # @rbs return: Quo::Query & Quo::ComposedQuery
32
+ def self.compose(right, joins: nil)
33
+ super_class = if self < Quo::CollectionBackedQuery || right < Quo::CollectionBackedQuery
34
+ Quo.collection_backed_query_base_class
35
+ else
36
+ Quo.relation_backed_query_base_class
37
+ end
38
+ ComposedQuery.composer(super_class, self, right, joins: joins)
62
39
  end
40
+ singleton_class.alias_method :+, :compose
63
41
 
64
- def includes(*options)
65
- copy(includes: options)
42
+ COERCE_TO_INT = ->(value) do #: (untyped value) -> Integer?
43
+ return if value == Literal::Null
44
+ value&.to_i
66
45
  end
67
46
 
68
- def preload(*options)
69
- copy(preload: options)
70
- end
47
+ # @rbs!
48
+ # attr_accessor page (): Integer?
49
+ # attr_accessor page_size (): Integer?
50
+ # @current_page: Integer?
51
+ prop :page, _Nilable(Integer), &COERCE_TO_INT
52
+ prop(:page_size, _Nilable(Integer), default: -> { Quo.default_page_size || 20 }, &COERCE_TO_INT)
71
53
 
72
- def select(*options)
73
- copy(select: options)
54
+ def next_page_query #: Quo::Query
55
+ copy(page: page + 1)
74
56
  end
75
57
 
76
- # The following methods actually execute the underlying query
77
-
78
- # Delegate SQL calculation methods to the underlying query
79
- delegate :sum, :average, :minimum, :maximum, to: :query_with_logging
80
-
81
- # Gets the count of all results ignoring the current page and page size (if set)
82
- delegate :count, to: :underlying_query
83
- alias_method :total_count, :count
84
- alias_method :size, :count
85
-
86
- # Gets the actual count of elements in the page of results (assuming paging is being used, otherwise the count of
87
- # all results)
88
- def page_count
89
- query_with_logging.count
58
+ def previous_page_query #: Quo::Query
59
+ copy(page: [page - 1, 1].max)
90
60
  end
91
61
 
92
- # Delegate methods that let us get the model class (available on AR relations)
93
- delegate :model, :klass, to: :underlying_query
94
-
95
- # Get first elements
96
- def first(limit = nil)
97
- if transform?
98
- res = query_with_logging.first(limit)
99
- if res.is_a? Array
100
- res.map.with_index { |r, i| transformer&.call(r, i) }
101
- elsif !res.nil?
102
- transformer&.call(query_with_logging.first(limit))
103
- end
104
- elsif limit
105
- query_with_logging.first(limit)
62
+ def offset #: Integer
63
+ per_page = sanitised_page_size
64
+ page_with_default = if page&.positive?
65
+ page
106
66
  else
107
- # Array#first will not take nil as a limit
108
- query_with_logging.first
67
+ 1
109
68
  end
69
+ per_page * (page_with_default - 1)
110
70
  end
111
71
 
112
- def first!(limit = nil)
113
- item = first(limit)
114
- raise ActiveRecord::RecordNotFound, "No item could be found!" unless item
115
- item
72
+ # Returns a active record query, or a Quo::Query instance
73
+ def query #: Quo::Query | ::ActiveRecord::Relation
74
+ raise NotImplementedError, "Query objects must define a 'query' method"
116
75
  end
117
76
 
118
- # Get last elements
119
- def last(limit = nil)
120
- if transform?
121
- res = query_with_logging.last(limit)
122
- if res.is_a? Array
123
- res.map.with_index { |r, i| transformer&.call(r, i) }
124
- elsif !res.nil?
125
- transformer&.call(res)
126
- end
127
- elsif limit
128
- query_with_logging.last(limit)
129
- else
130
- query_with_logging.last
77
+ # @rbs **overrides: untyped
78
+ # @rbs return: Quo::Query
79
+ def copy(**overrides)
80
+ self.class.new(**to_h.merge(overrides)).tap do |q|
81
+ q.instance_variable_set(:@__transformer, transformer)
131
82
  end
132
83
  end
133
84
 
134
- # Convert to array
135
- def to_a
136
- arr = query_with_logging.to_a
137
- transform? ? arr.map.with_index { |r, i| transformer&.call(r, i) } : arr
138
- end
139
-
140
- def to_eager(more_opts = {})
141
- Quo::LoadedQuery.new(to_a, **options.merge(more_opts))
85
+ # Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation.
86
+ # @rbs right: Quo::Query | ::ActiveRecord::Relation
87
+ # @rbs joins: untyped
88
+ # @rbs return: Quo::ComposedQuery
89
+ def merge(right, joins: nil)
90
+ ComposedQuery.merge_instances(self, right, joins: joins)
142
91
  end
143
- alias_method :load, :to_eager
92
+ alias_method :+, :merge
144
93
 
145
- def results
146
- Quo::Results.new(self, transformer: transformer)
147
- end
148
94
 
149
- # Some convenience methods for working with results
150
- delegate :each,
151
- :find_each,
152
- :map,
153
- :flat_map,
154
- :reduce,
155
- :reject,
156
- :filter,
157
- :find,
158
- :include?,
159
- :each_with_object,
160
- to: :results
95
+ # @rbs @__transformer: nil | ^(untyped, ?Integer) -> untyped
161
96
 
162
97
  # Set a block used to transform data after query fetching
98
+ # @rbs block: ^(untyped, ?Integer) -> untyped
99
+ # @rbs return: self
163
100
  def transform(&block)
164
- @options[:__transformer] = block
101
+ @__transformer = block
165
102
  self
166
103
  end
167
104
 
168
- # Are there any results for this query?
169
- def exists?
170
- return query_with_logging.exists? if relation?
171
- query_with_logging.present?
172
- end
173
-
174
- # Are there no results for this query?
175
- def none?
176
- !exists?
177
- end
178
- alias_method :empty?, :none?
179
-
180
- # Is this query object a relation under the hood? (ie not eager loaded)
181
- def relation?
105
+ # Is this query object a ActiveRecord relation under the hood?
106
+ def relation? #: bool
182
107
  test_relation(configured_query)
183
108
  end
184
109
 
185
- # Is this query object eager loaded data under the hood? (ie not a relation)
186
- def eager?
187
- test_eager(configured_query)
110
+ # Is this query object loaded data/collection under the hood? (ie not a AR relation)
111
+ def collection? #: bool
112
+ is_collection?(configured_query)
188
113
  end
189
114
 
190
115
  # Is this query object paged? (ie is paging enabled)
191
- def paged?
192
- current_page.present?
116
+ def paged? #: bool
117
+ page.present?
193
118
  end
194
119
 
195
120
  # Is this query object transforming results?
196
- def transform?
121
+ def transform? #: bool
197
122
  transformer.present?
198
123
  end
199
124
 
200
- # Return the SQL string for this query if its a relation type query object
201
- def to_sql
202
- configured_query.to_sql if relation?
203
- end
204
-
205
125
  # Unwrap the paginated query
206
- def unwrap
126
+ def unwrap #: ActiveRecord::Relation
207
127
  configured_query
208
128
  end
209
129
 
210
130
  # Unwrap the un-paginated query
211
- def unwrap_unpaginated
131
+ def unwrap_unpaginated #: ActiveRecord::Relation
212
132
  underlying_query
213
133
  end
214
134
 
215
- delegate :distinct, to: :configured_query
216
-
217
135
  private
218
136
 
219
- def formatted_queries?
220
- !!Quo.configuration.formatted_query_log
221
- end
222
-
223
- # 'trim' a query, ie remove comments and remove newlines
224
- # This will remove dashes from inside strings too
225
- def trim_query(sql)
226
- sql.gsub(/--[^\n'"]*\n/m, " ").tr("\n", " ").strip
227
- end
228
-
229
- def format_query(sql_str)
230
- formatted_queries? ? sql_str : trim_query(sql_str)
137
+ def transformer
138
+ @__transformer
231
139
  end
232
140
 
233
- def transformer
234
- options[:__transformer]
141
+ def validated_query
142
+ raise NoMethodError, "Query objects must define a 'validated_query' method"
235
143
  end
236
144
 
237
- def offset
238
- per_page = sanitised_page_size
239
- page = if current_page && current_page&.positive?
240
- current_page
241
- else
242
- 1
243
- end
244
- per_page * (page - 1)
145
+ # The underlying query is essentially the configured query with optional extras setup
146
+ def underlying_query #: void
147
+ raise NoMethodError, "Query objects must define a 'underlying_query' method"
245
148
  end
246
149
 
247
150
  # The configured query is the underlying query with paging
248
- def configured_query
249
- q = underlying_query
250
- return q unless paged? && q.is_a?(ActiveRecord::Relation)
251
- q.offset(offset).limit(sanitised_page_size)
151
+ def configured_query #: void
152
+ raise NoMethodError, "Query objects must define a 'configured_query' method"
252
153
  end
253
154
 
254
- def sanitised_page_size
255
- if page_size && page_size.positive?
155
+ def sanitised_page_size #: Integer
156
+ if page_size&.positive?
256
157
  given_size = page_size.to_i
257
- max_page_size = Quo.configuration.max_page_size || 200
158
+ max_page_size = Quo.max_page_size || 200
258
159
  if given_size > max_page_size
259
160
  max_page_size
260
161
  else
261
162
  given_size
262
163
  end
263
164
  else
264
- Quo.configuration.default_page_size || 20
165
+ Quo.default_page_size || 20
265
166
  end
266
167
  end
267
168
 
268
- def query_with_logging
269
- debug_callstack
270
- configured_query
271
- end
272
-
273
- # The underlying query is essentially the configured query with optional extras setup
274
- def underlying_query
275
- @underlying_query ||=
276
- begin
277
- rel = unwrap_relation(query)
278
- unless test_eager(rel)
279
- rel = rel.group(@options[:group]) if @options[:group].present?
280
- rel = rel.order(@options[:order]) if @options[:order].present?
281
- rel = rel.limit(@options[:limit]) if @options[:limit].present?
282
- rel = rel.preload(@options[:preload]) if @options[:preload].present?
283
- rel = rel.includes(@options[:includes]) if @options[:includes].present?
284
- rel = rel.select(@options[:select]) if @options[:select].present?
285
- end
286
- rel
287
- end
288
- end
289
-
290
- def unwrap_relation(query)
291
- query.is_a?(Quo::Query) ? query.unwrap : query
292
- end
293
-
294
- def test_eager(rel)
295
- rel.is_a?(Quo::LoadedQuery) || (rel.is_a?(Enumerable) && !test_relation(rel))
169
+ # @rbs rel: untyped
170
+ # @rbs return: bool
171
+ def is_collection?(rel)
172
+ rel.is_a?(Quo::CollectionBackedQuery) || (rel.is_a?(Enumerable) && !test_relation(rel))
296
173
  end
297
174
 
175
+ # @rbs rel: untyped
176
+ # @rbs return: bool
298
177
  def test_relation(rel)
299
178
  rel.is_a?(ActiveRecord::Relation)
300
179
  end
180
+
181
+ def quo_unwrap_unpaginated_query(q)
182
+ q.is_a?(Quo::Query) ? q.unwrap_unpaginated : q
183
+ end
301
184
  end
302
185
  end