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.
- 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 -36
- 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 +97 -214
- 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 -21
- 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,302 +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
|
-
# 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
|
220
|
-
|
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
|
234
|
-
|
141
|
+
def validated_query
|
142
|
+
raise NoMethodError, "Query objects must define a 'validated_query' method"
|
235
143
|
end
|
236
144
|
|
237
|
-
|
238
|
-
|
239
|
-
|
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
|
-
|
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
|
155
|
+
def sanitised_page_size #: Integer
|
156
|
+
if page_size&.positive?
|
256
157
|
given_size = page_size.to_i
|
257
|
-
max_page_size = Quo.
|
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.
|
165
|
+
Quo.default_page_size || 20
|
265
166
|
end
|
266
167
|
end
|
267
168
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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
|