quo 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61bf7495c0092bf641ff6e85bed8dc0073ea61e15a5292f0825f50eca4348a92
4
- data.tar.gz: c10cc10dfe8ce323e4395640604a1c6358333bb7ea26b1c109d43eee67ed2d73
3
+ metadata.gz: 071f43a86ac28245731999ddf6f605c89802b71e3d51139d44263214ad25dbd5
4
+ data.tar.gz: 5eaf5d72a701679c599e5d07f60e8f1c7c9eb1f2e00a7f8ad1503270bc9cda64
5
5
  SHA512:
6
- metadata.gz: e8e4105330e725058ca4c94f2c58229a0018e9379c38ac189cc2dbd007163468b845dd9632d727f824a35d4bc8cebbb7e40f5d24267277b0c1cbe0b580abb100
7
- data.tar.gz: 7180ea2d74f1849628a1635c381685c5d2841c27229868280cd59a580dae759d61cc5236fba06b4044a93b667a23f670f4ba5960720165ccb1a6a103a4d06f37
6
+ metadata.gz: ed91b9a82feb1c8f165672ffa4d2b1212ea7e8780cb85980f82353eca7fdfb247e67101fb6156ceaad65be94f27406daa4a5832e4950ab0204f7a9dc326329a7
7
+ data.tar.gz: 503525140bd10ed28817e61dc0a2de8ef4e60ca32805427b72763a039982fd069a147ce8eb18d91896ac83202645a12092a8b186b530c97322f465baeeba687a
data/README.md CHANGED
@@ -201,50 +201,44 @@ Specify extra options to enable pagination:
201
201
  * `page`: the current page number to fetch
202
202
  * `page_size`: the number of elements to fetch in the page
203
203
 
204
- ## 'Eager loaded' Quo::Query objects
204
+ ### `Quo::EagerQuery` & `Quo::LoadedQuery` objects
205
205
 
206
- When a query object returns an `Array` from `query` it is assumed as 'eager loaded', ie that the query has actually
207
- already been executed and the array contains the return values. This can also be used to encapsulate data that doesn't
208
- actually come from an ActiveRecord query.
209
-
210
- For example, the Query below executes the query inside on the first call and memoises the resulting data. Note
211
- however that this then means that this Query is not reusuable to `merge` with other ActiveRecord queries. If it is
212
- `compose`d with other Query objects then it will be seen as an array-like, and concatenated to whatever it is being
213
- joined to.
206
+ `Quo::EagerQuery` is a subclass of `Quo::Query` which can be used to create query objects which are 'eager loaded' by
207
+ default. This is useful for encapsulating data that doesn't come from an ActiveRecord query or queries that
208
+ execute immediately. Subclass EasyQuery and override `collection` to return the data you want to encapsulate.
214
209
 
215
210
  ```ruby
216
- class CachedTags < Quo::Query
217
- def query
218
- @tags ||= Tag.where(active: true).to_a
211
+ class MyEagerQuery < Quo::EagerQuery
212
+ def collection
213
+ [1, 2, 3]
219
214
  end
220
215
  end
221
-
222
- q = CachedTags.new(active: false)
216
+ q = MyEagerQuery.new
223
217
  q.eager? # is it 'eager'? Yes it is!
224
- q.count # array size
218
+ q.count # '3'
225
219
  ```
226
220
 
227
- ### `Quo::EagerQuery` objects
228
-
229
- `Quo::EagerQuery` is a subclass of `Quo::Query` which takes a data value on instantiation and returns it on calls to `query`
221
+ Sometimes it is useful to create similar Queries without needing to create a explicit subclass of your own. For this
222
+ use `Quo::LoadedQuery`:
230
223
 
231
224
  ```ruby
232
- q = Quo::EagerQuery.new([1, 2, 3])
225
+ q = Quo::LoadedQuery.new([1, 2, 3])
233
226
  q.eager? # is it 'eager'? Yes it is!
234
227
  q.count # '3'
235
228
  ```
236
229
 
237
- This is useful to create eager loaded Queries without needing to create a explicit subclass of your own.
238
-
239
230
  `Quo::EagerQuery` also uses `total_count` option value as the specified 'total count', useful when the data is
240
231
  actually just a page of the data and not the total count.
241
232
 
242
233
  Example of an EagerQuery used to wrap a page of enumerable data:
243
234
 
244
235
  ```ruby
245
- Quo::EagerQuery.new(my_data, total_count: 100, page: current_page)
236
+ Quo::LoadedQuery.new(my_data, total_count: 100, page: current_page)
246
237
  ```
247
238
 
239
+ If a loaded query is `compose`d with other Query objects then it will be seen as an array-like, and concatenated to whatever
240
+ results are returned from the other queries. An loaded or eager query will force all other queries to be eager loaded.
241
+
248
242
  ### Composition
249
243
 
250
244
  Examples of composition of eager loaded queries
@@ -262,7 +256,7 @@ composed.last
262
256
  composed.first
263
257
  # => #<Tag id: ...>
264
258
 
265
- Quo::EagerQuery.new([3, 4]).compose(Quo::EagerQuery.new([1, 2])).last
259
+ Quo::LoadedQuery.new([3, 4]).compose(Quo::LoadedQuery.new([1, 2])).last
266
260
  # => 2
267
261
  Quo::Query.compose([1, 2], [3, 4]).last
268
262
  # => 4
@@ -292,7 +286,7 @@ maybe desirable.
292
286
 
293
287
  The spec helper method `stub_query(query_class, {results: ..., with: ...})` can do this for you.
294
288
 
295
- It stubs `.new` on the Query object and returns instances of `EagerQuery` instead with the given `results`.
289
+ It stubs `.new` on the Query object and returns instances of `LoadedQuery` instead with the given `results`.
296
290
  The `with` option is passed to the Query object on initialisation and used when setting up the method stub on the
297
291
  query class.
298
292
 
@@ -2,45 +2,22 @@
2
2
 
3
3
  module Quo
4
4
  class EagerQuery < Quo::Query
5
- class << self
6
- def call(**options)
7
- build_from_options(options).first
8
- end
9
-
10
- def call!(**options)
11
- build_from_options(options).first!
12
- end
13
-
14
- def build_from_options(options)
15
- collection = options[:collection]
16
- raise ArgumentError, "EagerQuery needs a collection" unless collection
17
- new(collection, **options.except(:collection))
18
- end
19
- end
20
-
21
- def initialize(collection, **options)
22
- @collection = Array.wrap(collection)
23
- super(**options)
24
- end
25
-
26
- def copy(**options)
27
- self.class.new(@collection, **@options.merge(options))
28
- end
29
-
30
5
  # Optionally return the `total_count` option if it has been set.
31
6
  # This is useful when the total count is known and not equal to size
32
7
  # of wrapped collection.
33
8
  def count
34
9
  options[:total_count] || super
35
10
  end
36
- alias_method :total_count, :count
37
- alias_method :size, :count
38
11
 
39
12
  # Is this query object paged? (when no total count)
40
13
  def paged?
41
14
  options[:total_count].nil? && current_page.present?
42
15
  end
43
16
 
17
+ def collection
18
+ raise NotImplementedError, "EagerQuery objects must define a 'collection' method"
19
+ end
20
+
44
21
  def query
45
22
  preload_includes(collection) if options[:includes]
46
23
  collection
@@ -56,7 +33,13 @@ module Quo
56
33
 
57
34
  private
58
35
 
59
- attr_reader :collection
36
+ def underlying_query
37
+ unwrap_relation(query)
38
+ end
39
+
40
+ def unwrap_relation(query)
41
+ query.is_a?(Quo::Query) ? query.unwrap : query
42
+ end
60
43
 
61
44
  def preload_includes(records, preload = nil)
62
45
  ::ActiveRecord::Associations::Preloader.new(
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quo
4
+ class LoadedQuery < Quo::EagerQuery
5
+ def initialize(collection, **options)
6
+ @collection = collection
7
+ super(**options)
8
+ end
9
+
10
+ def copy(**options)
11
+ self.class.new(@collection, **@options.merge(options))
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :collection
17
+ end
18
+ end
@@ -2,24 +2,6 @@
2
2
 
3
3
  module Quo
4
4
  class MergedQuery < Quo::Query
5
- class << self
6
- def call(**options)
7
- build_from_options(options).first
8
- end
9
-
10
- def call!(**options)
11
- build_from_options(options).first!
12
- end
13
-
14
- def build_from_options(options)
15
- merged_query = options[:merged_query]
16
- left = options[:left]
17
- right = options[:right]
18
- raise ArgumentError, "MergedQuery needs the merged result and operands" unless merged_query && left && right
19
- new(merged_query, left, right, **options)
20
- end
21
- end
22
-
23
5
  def initialize(merged_query, left, right, **options)
24
6
  @merged_query = merged_query
25
7
  @left = left
data/lib/quo/query.rb CHANGED
@@ -101,8 +101,11 @@ module Quo
101
101
  elsif !res.nil?
102
102
  transformer&.call(query_with_logging.first(limit))
103
103
  end
104
- else
104
+ elsif limit
105
105
  query_with_logging.first(limit)
106
+ else
107
+ # Array#first will not take nil as a limit
108
+ query_with_logging.first
106
109
  end
107
110
  end
108
111
 
@@ -121,8 +124,10 @@ module Quo
121
124
  elsif !res.nil?
122
125
  transformer&.call(res)
123
126
  end
124
- else
127
+ elsif limit
125
128
  query_with_logging.last(limit)
129
+ else
130
+ query_with_logging.last
126
131
  end
127
132
  end
128
133
 
@@ -132,9 +137,8 @@ module Quo
132
137
  transform? ? arr.map.with_index { |r, i| transformer&.call(r, i) } : arr
133
138
  end
134
139
 
135
- # Convert to EagerQuery, and load all data
136
140
  def to_eager(more_opts = {})
137
- Quo::EagerQuery.new(to_a, **options.merge(more_opts))
141
+ Quo::LoadedQuery.new(to_a, **options.merge(more_opts))
138
142
  end
139
143
  alias_method :load, :to_eager
140
144
 
@@ -282,7 +286,7 @@ module Quo
282
286
  end
283
287
 
284
288
  def test_eager(rel)
285
- rel.is_a?(Enumerable) && !test_relation(rel)
289
+ rel.is_a?(Quo::LoadedQuery) || (rel.is_a?(Enumerable) && !test_relation(rel))
286
290
  end
287
291
 
288
292
  def test_relation(rel)
@@ -39,10 +39,12 @@ module Quo
39
39
  apply_joins(unwrapped_left, joins).merge(unwrapped_right)
40
40
  elsif left_relation_right_enumerable?
41
41
  unwrapped_left.to_a + unwrapped_right
42
- elsif left_enumerable_right_relation?
42
+ elsif left_enumerable_right_relation? && unwrapped_left.respond_to?(:+)
43
43
  unwrapped_left + unwrapped_right.to_a
44
- else
44
+ elsif unwrapped_left.respond_to?(:+)
45
45
  unwrapped_left + unwrapped_right
46
+ else
47
+ raise ArgumentError, "Cannot merge #{left.class} with #{right.class}"
46
48
  end
47
49
  end
48
50
 
@@ -9,10 +9,10 @@ module Quo
9
9
  unless with.nil?
10
10
  return(
11
11
  allow(query_class).to receive(:new)
12
- .with(with) { ::Quo::EagerQuery.new(results) }
12
+ .with(with) { ::Quo::LoadedQuery.new(results) }
13
13
  )
14
14
  end
15
- allow(query_class).to receive(:new) { ::Quo::EagerQuery.new(results) }
15
+ allow(query_class).to receive(:new) { ::Quo::LoadedQuery.new(results) }
16
16
  end
17
17
  end
18
18
  end
@@ -15,7 +15,7 @@ module Quo
15
15
  if query_rel_or_data.is_a? ActiveRecord::Relation
16
16
  Quo::WrappedQuery.new(query_rel_or_data, **options)
17
17
  else
18
- Quo::EagerQuery.new(query_rel_or_data, **options)
18
+ Quo::LoadedQuery.new(query_rel_or_data, **options)
19
19
  end
20
20
  end
21
21
  end
data/lib/quo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quo
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -2,27 +2,15 @@
2
2
 
3
3
  module Quo
4
4
  class WrappedQuery < Quo::Query
5
- class << self
6
- def call(**options)
7
- build_from_options(**options).first
8
- end
9
-
10
- def call!(**options)
11
- build_from_options(**options).first!
12
- end
13
-
14
- def build_from_options(**options)
15
- query = options[:wrapped_query]
16
- raise ArgumentError, "WrappedQuery needs a scope" unless query
17
- new(query, **options)
18
- end
19
- end
20
-
21
5
  def initialize(wrapped_query, **options)
22
6
  @wrapped_query = wrapped_query
23
7
  super(**options)
24
8
  end
25
9
 
10
+ def copy(**options)
11
+ self.class.new(query, **@options.merge(options))
12
+ end
13
+
26
14
  def query
27
15
  @wrapped_query
28
16
  end
data/lib/quo.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "quo/version"
4
4
  require_relative "quo/railtie" if defined?(Rails)
5
5
  require_relative "quo/query"
6
6
  require_relative "quo/eager_query"
7
+ require_relative "quo/loaded_query"
7
8
  require_relative "quo/merged_query"
8
9
  require_relative "quo/wrapped_query"
9
10
  require_relative "quo/query_composer"
@@ -1,18 +1,15 @@
1
1
  module Quo
2
2
  class EagerQuery < Quo::Query
3
- def self.call: (**untyped) -> untyped
4
- def self.call!: (**untyped) -> untyped
5
- def self.build_from_options: (queryOptions) -> EagerQuery
3
+ def collection: () -> loadedQueryOrEnumerable
4
+ def query: () -> loadedQueryOrEnumerable
6
5
 
7
- def initialize: (enumerable, **untyped options) -> void
8
- def query: () -> enumerable
9
6
  def relation?: () -> false
10
7
  def eager?: () -> true
11
8
 
12
9
  private
13
10
 
14
- attr_reader collection: enumerable
15
-
16
11
  def preload_includes: (untyped records, ?untyped? preload) -> untyped
12
+ def underlying_query: () -> enumerable
13
+ def unwrap_relation: (loadedQueryOrEnumerable collection) -> enumerable
17
14
  end
18
15
  end
@@ -0,0 +1,7 @@
1
+ module Quo
2
+ class LoadedQuery < Quo::EagerQuery
3
+ @collection: enumerable
4
+
5
+ def initialize: (enumerable, **untyped options) -> void
6
+ end
7
+ end
data/sig/quo.rbs CHANGED
@@ -13,6 +13,7 @@ module Quo
13
13
  type queryOrRel = query | ActiveRecord::Relation
14
14
  type enumerable = Object & Enumerable[untyped]
15
15
  type relOrEnumerable = ActiveRecord::Relation | enumerable
16
+ type loadedQueryOrEnumerable = LoadedQuery | EagerQuery | enumerable
16
17
  type composable = query | relOrEnumerable
17
18
 
18
19
  # TODO: how can we do the known options, eg `page` and then allow anything else?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Ierodiaconou
@@ -66,6 +66,7 @@ files:
66
66
  - Steepfile
67
67
  - lib/quo.rb
68
68
  - lib/quo/eager_query.rb
69
+ - lib/quo/loaded_query.rb
69
70
  - lib/quo/merged_query.rb
70
71
  - lib/quo/query.rb
71
72
  - lib/quo/query_composer.rb
@@ -82,6 +83,7 @@ files:
82
83
  - rbs_collection.yaml
83
84
  - sig/quo.rbs
84
85
  - sig/quo/eager_query.rbs
86
+ - sig/quo/loaded_query.rbs
85
87
  - sig/quo/merged_query.rbs
86
88
  - sig/quo/query.rbs
87
89
  - sig/quo/query_composer.rbs