quo 0.6.0 → 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 -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
|