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.
- checksums.yaml +4 -4
- data/.standard.yml +4 -1
- data/Appraisals +11 -0
- data/CHANGELOG.md +78 -0
- data/Gemfile +6 -4
- data/LICENSE.txt +1 -1
- data/README.md +37 -69
- data/Steepfile +0 -2
- data/gemfiles/rails_7.0.gemfile +15 -0
- data/gemfiles/rails_7.1.gemfile +15 -0
- data/gemfiles/rails_7.2.gemfile +15 -0
- data/lib/quo/collection_backed_query.rb +87 -0
- data/lib/quo/collection_results.rb +44 -0
- data/lib/quo/composed_query.rb +168 -0
- data/lib/quo/engine.rb +11 -0
- data/lib/quo/minitest/helpers.rb +41 -0
- data/lib/quo/preloadable.rb +46 -0
- data/lib/quo/query.rb +101 -213
- data/lib/quo/relation_backed_query.rb +177 -0
- data/lib/quo/relation_results.rb +58 -0
- data/lib/quo/results.rb +48 -44
- data/lib/quo/rspec/helpers.rb +31 -9
- data/lib/quo/testing/collection_backed_fake.rb +29 -0
- data/lib/quo/testing/relation_backed_fake.rb +52 -0
- data/lib/quo/version.rb +3 -1
- data/lib/quo.rb +22 -30
- data/rbs_collection.yaml +0 -2
- data/sig/generated/quo/collection_backed_query.rbs +39 -0
- data/sig/generated/quo/collection_results.rbs +30 -0
- data/sig/generated/quo/composed_query.rbs +83 -0
- data/sig/generated/quo/engine.rbs +6 -0
- data/sig/generated/quo/preloadable.rbs +29 -0
- data/sig/generated/quo/query.rbs +98 -0
- data/sig/generated/quo/relation_backed_query.rbs +90 -0
- data/sig/generated/quo/relation_results.rbs +38 -0
- data/sig/generated/quo/results.rbs +39 -0
- data/sig/generated/quo/version.rbs +5 -0
- data/sig/generated/quo.rbs +9 -0
- metadata +67 -30
- data/lib/quo/eager_query.rb +0 -51
- data/lib/quo/loaded_query.rb +0 -18
- data/lib/quo/merged_query.rb +0 -36
- data/lib/quo/query_composer.rb +0 -78
- data/lib/quo/railtie.rb +0 -7
- data/lib/quo/utilities/callstack.rb +0 -20
- data/lib/quo/utilities/compose.rb +0 -18
- data/lib/quo/utilities/sanitize.rb +0 -19
- data/lib/quo/utilities/wrap.rb +0 -23
- data/lib/quo/wrapped_query.rb +0 -18
- data/sig/quo/eager_query.rbs +0 -15
- data/sig/quo/loaded_query.rbs +0 -7
- data/sig/quo/merged_query.rbs +0 -19
- data/sig/quo/query.rbs +0 -83
- data/sig/quo/query_composer.rbs +0 -32
- data/sig/quo/results.rbs +0 -22
- data/sig/quo/utilities/callstack.rbs +0 -7
- data/sig/quo/utilities/compose.rbs +0 -8
- data/sig/quo/utilities/sanitize.rbs +0 -9
- data/sig/quo/utilities/wrap.rbs +0 -11
- data/sig/quo/wrapped_query.rbs +0 -11
- 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,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
|
-
|
4
|
-
require_relative "./utilities/compose"
|
5
|
-
require_relative "./utilities/sanitize"
|
6
|
-
require_relative "./utilities/wrap"
|
3
|
+
# rbs_inline: enabled
|
7
4
|
|
8
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
52
|
-
|
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
|
57
|
-
|
23
|
+
def to_s
|
24
|
+
inspect
|
58
25
|
end
|
59
26
|
|
60
|
-
|
61
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
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
|
73
|
-
copy(
|
54
|
+
def next_page_query #: Quo::Query
|
55
|
+
copy(page: page + 1)
|
74
56
|
end
|
75
57
|
|
76
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
108
|
-
query_with_logging.first
|
67
|
+
1
|
109
68
|
end
|
69
|
+
per_page * (page_with_default - 1)
|
110
70
|
end
|
111
71
|
|
112
|
-
|
113
|
-
|
114
|
-
raise
|
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
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
#
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
92
|
+
alias_method :+, :merge
|
144
93
|
|
145
|
-
def results
|
146
|
-
Quo::Results.new(self, transformer: transformer)
|
147
|
-
end
|
148
94
|
|
149
|
-
#
|
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
|
-
@
|
101
|
+
@__transformer = block
|
165
102
|
self
|
166
103
|
end
|
167
104
|
|
168
|
-
#
|
169
|
-
def
|
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
|
186
|
-
def
|
187
|
-
|
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
|
-
|
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
|
-
#
|
201
|
-
def
|
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
|
-
|
211
|
-
|
212
|
-
|
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
|
-
|
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
|
225
|
-
|
137
|
+
def transformer
|
138
|
+
@__transformer
|
226
139
|
end
|
227
140
|
|
228
|
-
def
|
229
|
-
|
141
|
+
def validated_query
|
142
|
+
raise NoMethodError, "Query objects must define a 'validated_query' method"
|
230
143
|
end
|
231
144
|
|
232
|
-
|
233
|
-
|
234
|
-
|
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
|
-
|
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
|
155
|
+
def sanitised_page_size #: Integer
|
156
|
+
if page_size&.positive?
|
251
157
|
given_size = page_size.to_i
|
252
|
-
max_page_size = Quo.
|
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.
|
165
|
+
Quo.default_page_size || 20
|
260
166
|
end
|
261
167
|
end
|
262
168
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|