quo 0.5.3 → 1.0.0.alpha1

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 (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 -69
  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 +101 -213
  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 -20
  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,297 +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
- # Unwrap the underlying query
206
- def unwrap
125
+ # Unwrap the paginated query
126
+ def unwrap #: ActiveRecord::Relation
207
127
  configured_query
208
128
  end
209
129
 
210
- delegate :distinct, to: :configured_query
211
-
212
- private
213
-
214
- def formatted_queries?
215
- !!Quo.configuration.formatted_query_log
130
+ # Unwrap the un-paginated query
131
+ def unwrap_unpaginated #: ActiveRecord::Relation
132
+ underlying_query
216
133
  end
217
134
 
218
- # 'trim' a query, ie remove comments and remove newlines
219
- # This will remove dashes from inside strings too
220
- def trim_query(sql)
221
- sql.gsub(/--[^\n'"]*\n/m, " ").tr("\n", " ").strip
222
- end
135
+ private
223
136
 
224
- def format_query(sql_str)
225
- formatted_queries? ? sql_str : trim_query(sql_str)
137
+ def transformer
138
+ @__transformer
226
139
  end
227
140
 
228
- def transformer
229
- options[:__transformer]
141
+ def validated_query
142
+ raise NoMethodError, "Query objects must define a 'validated_query' method"
230
143
  end
231
144
 
232
- def offset
233
- per_page = sanitised_page_size
234
- page = if current_page && current_page&.positive?
235
- current_page
236
- else
237
- 1
238
- end
239
- 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"
240
148
  end
241
149
 
242
150
  # The configured query is the underlying query with paging
243
- def configured_query
244
- q = underlying_query
245
- return q unless paged? && q.is_a?(ActiveRecord::Relation)
246
- q.offset(offset).limit(sanitised_page_size)
151
+ def configured_query #: void
152
+ raise NoMethodError, "Query objects must define a 'configured_query' method"
247
153
  end
248
154
 
249
- def sanitised_page_size
250
- if page_size && page_size.positive?
155
+ def sanitised_page_size #: Integer
156
+ if page_size&.positive?
251
157
  given_size = page_size.to_i
252
- max_page_size = Quo.configuration.max_page_size || 200
158
+ max_page_size = Quo.max_page_size || 200
253
159
  if given_size > max_page_size
254
160
  max_page_size
255
161
  else
256
162
  given_size
257
163
  end
258
164
  else
259
- Quo.configuration.default_page_size || 20
165
+ Quo.default_page_size || 20
260
166
  end
261
167
  end
262
168
 
263
- def query_with_logging
264
- debug_callstack
265
- configured_query
266
- end
267
-
268
- # The underlying query is essentially the configured query with optional extras setup
269
- def underlying_query
270
- @underlying_query ||=
271
- begin
272
- rel = unwrap_relation(query)
273
- unless test_eager(rel)
274
- rel = rel.group(@options[:group]) if @options[:group].present?
275
- rel = rel.order(@options[:order]) if @options[:order].present?
276
- rel = rel.limit(@options[:limit]) if @options[:limit].present?
277
- rel = rel.preload(@options[:preload]) if @options[:preload].present?
278
- rel = rel.includes(@options[:includes]) if @options[:includes].present?
279
- rel = rel.select(@options[:select]) if @options[:select].present?
280
- end
281
- rel
282
- end
283
- end
284
-
285
- def unwrap_relation(query)
286
- query.is_a?(Quo::Query) ? query.unwrap : query
287
- end
288
-
289
- def test_eager(rel)
290
- 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))
291
173
  end
292
174
 
175
+ # @rbs rel: untyped
176
+ # @rbs return: bool
293
177
  def test_relation(rel)
294
178
  rel.is_a?(ActiveRecord::Relation)
295
179
  end
180
+
181
+ def quo_unwrap_unpaginated_query(q)
182
+ q.is_a?(Quo::Query) ? q.unwrap_unpaginated : q
183
+ end
296
184
  end
297
185
  end