quo 1.0.0.beta2 → 2.0.0
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/.devcontainer/Dockerfile +17 -0
- data/.devcontainer/compose.yml +10 -0
- data/.devcontainer/devcontainer.json +12 -0
- data/Appraisals +4 -12
- data/CHANGELOG.md +112 -1
- data/CLAUDE.md +19 -0
- data/Gemfile +7 -1
- data/LICENSE.txt +1 -1
- data/README.md +496 -203
- data/Rakefile +66 -6
- data/UPGRADING.md +216 -0
- data/badges/coverage_badge_total.svg +35 -0
- data/badges/rubycritic_badge_score.svg +35 -0
- data/claude-skill/README.md +100 -0
- data/claude-skill/SKILL.md +442 -0
- data/claude-skill/references/API_REFERENCE.md +462 -0
- data/claude-skill/references/COMPOSITION.md +396 -0
- data/claude-skill/references/PAGINATION.md +396 -0
- data/claude-skill/references/QUERY_TYPES.md +297 -0
- data/claude-skill/references/TRANSFORMERS.md +282 -0
- data/context/01-core-architecture.md +247 -0
- data/context/02-query-types-implementation.md +355 -0
- data/context/03-composition-transformation.md +441 -0
- data/context/04-pagination-results.md +485 -0
- data/context/05-testing-configuration.md +491 -0
- data/context/06-advanced-patterns-examples.md +153 -0
- data/gemfiles/rails_8.0.gemfile +10 -5
- data/gemfiles/rails_8.1.gemfile +20 -0
- data/lib/generators/quo/install/USAGE +21 -0
- data/lib/generators/quo/install/install_generator.rb +63 -0
- data/lib/quo/collection_backed_query.rb +21 -15
- data/lib/quo/collection_results.rb +1 -0
- data/lib/quo/composed_collection_backed_query.rb +42 -0
- data/lib/quo/composed_instance.rb +144 -0
- data/lib/quo/composed_query.rb +43 -178
- data/lib/quo/composed_relation_backed_query.rb +42 -0
- data/lib/quo/composing/base_strategy.rb +22 -0
- data/lib/quo/composing/class_strategy.rb +86 -0
- data/lib/quo/composing/class_strategy_registry.rb +31 -0
- data/lib/quo/composing/query_classes_strategy.rb +38 -0
- data/lib/quo/composing.rb +81 -0
- data/lib/quo/engine.rb +1 -0
- data/lib/quo/minitest/helpers.rb +14 -24
- data/lib/quo/preloadable.rb +1 -0
- data/lib/quo/query.rb +22 -5
- data/lib/quo/relation_backed_query.rb +24 -18
- data/lib/quo/relation_backed_query_specification.rb +44 -25
- data/lib/quo/relation_results.rb +1 -0
- data/lib/quo/results.rb +31 -2
- data/lib/quo/rspec/helpers.rb +15 -26
- data/lib/quo/testing/collection_backed_fake.rb +1 -0
- data/lib/quo/testing/fake_helpers.rb +30 -0
- data/lib/quo/testing/relation_backed_fake.rb +1 -0
- data/lib/quo/version.rb +1 -1
- data/lib/quo/wrapped_collection_backed_query.rb +21 -0
- data/lib/quo/wrapped_relation_backed_query.rb +21 -0
- data/lib/quo.rb +8 -0
- data/quo.png +0 -0
- data/sig/generated/quo/collection_backed_query.rbs +10 -4
- data/sig/generated/quo/collection_results.rbs +1 -0
- data/sig/generated/quo/composed_collection_backed_query.rbs +25 -0
- data/sig/generated/quo/composed_instance.rbs +61 -0
- data/sig/generated/quo/composed_query.rbs +23 -56
- data/sig/generated/quo/composed_relation_backed_query.rbs +25 -0
- data/sig/generated/quo/composing/base_strategy.rbs +16 -0
- data/sig/generated/quo/composing/class_strategy.rbs +38 -0
- data/sig/generated/quo/composing/class_strategy_registry.rbs +16 -0
- data/sig/generated/quo/composing/query_classes_strategy.rbs +24 -0
- data/sig/generated/quo/composing.rbs +40 -0
- data/sig/generated/quo/engine.rbs +1 -0
- data/sig/generated/quo/minitest/helpers.rbs +12 -0
- data/sig/generated/quo/preloadable.rbs +1 -0
- data/sig/generated/quo/query.rbs +15 -4
- data/sig/generated/quo/relation_backed_query.rbs +15 -5
- data/sig/generated/quo/relation_backed_query_specification.rbs +47 -39
- data/sig/generated/quo/relation_results.rbs +1 -0
- data/sig/generated/quo/results.rbs +11 -0
- data/sig/generated/quo/rspec/helpers.rbs +12 -0
- data/sig/generated/quo/testing/collection_backed_fake.rbs +1 -0
- data/sig/generated/quo/testing/fake_helpers.rbs +14 -0
- data/sig/generated/quo/testing/relation_backed_fake.rbs +1 -0
- data/sig/generated/quo/wrapped_collection_backed_query.rbs +13 -0
- data/sig/generated/quo/wrapped_relation_backed_query.rbs +13 -0
- data/sig/generated/quo.rbs +1 -0
- data/website/.gitignore +6 -0
- data/website/.nojekyll +0 -0
- data/website/404.html +26 -0
- data/website/Gemfile +24 -0
- data/website/_config.yml +50 -0
- data/website/_data/navigation.yml +8 -0
- data/website/_data/sidebar.yml +2 -0
- data/website/_data/social_links.yml +3 -0
- data/website/_docs/api.md +261 -0
- data/website/_docs/get-started.md +289 -0
- data/website/assets/quo.png +0 -0
- data/website/index.md +141 -0
- metadata +70 -13
- data/gemfiles/rails_7.0.gemfile +0 -15
- data/gemfiles/rails_7.1.gemfile +0 -15
- data/gemfiles/rails_7.2.gemfile +0 -15
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# Pagination and Results
|
|
2
|
+
|
|
3
|
+
This document covers Quo's pagination system and the Results objects that execute queries and provide access to data.
|
|
4
|
+
|
|
5
|
+
## Pagination System
|
|
6
|
+
|
|
7
|
+
### Core Pagination Properties
|
|
8
|
+
|
|
9
|
+
Every Quo query has built-in pagination support via two properties:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class Query < Literal::Struct
|
|
13
|
+
# Current page number (nil means no pagination)
|
|
14
|
+
prop :page, _Nilable(Integer), &COERCE_TO_INT
|
|
15
|
+
|
|
16
|
+
# Items per page (defaults to Quo.default_page_size)
|
|
17
|
+
prop :page_size, _Nilable(Integer),
|
|
18
|
+
default: -> { Quo.default_page_size || 20 },
|
|
19
|
+
&COERCE_TO_INT
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Pagination Implementation
|
|
24
|
+
|
|
25
|
+
#### For RelationBackedQuery
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
def configured_query
|
|
29
|
+
q = underlying_query
|
|
30
|
+
return q unless paged? # paged? returns true if page is set
|
|
31
|
+
|
|
32
|
+
q.offset(offset).limit(sanitised_page_size)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def offset
|
|
36
|
+
per_page = sanitised_page_size
|
|
37
|
+
page_with_default = page&.positive? ? page : 1
|
|
38
|
+
per_page * (page_with_default - 1)
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
SQL translation:
|
|
43
|
+
- Page 1, size 20: `LIMIT 20 OFFSET 0`
|
|
44
|
+
- Page 2, size 20: `LIMIT 20 OFFSET 20`
|
|
45
|
+
- Page 3, size 10: `LIMIT 10 OFFSET 20`
|
|
46
|
+
|
|
47
|
+
#### For CollectionBackedQuery
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
def configured_query
|
|
51
|
+
q = underlying_query
|
|
52
|
+
return q unless paged?
|
|
53
|
+
|
|
54
|
+
if q.respond_to?(:[])
|
|
55
|
+
q[offset, sanitised_page_size] # Array slicing
|
|
56
|
+
else
|
|
57
|
+
q # Non-sliceable collections return unchanged
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Page Size Sanitization
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
def sanitised_page_size
|
|
66
|
+
if page_size&.positive?
|
|
67
|
+
given_size = page_size.to_i
|
|
68
|
+
max_page_size = Quo.max_page_size || 200
|
|
69
|
+
|
|
70
|
+
# Enforce maximum to prevent resource abuse
|
|
71
|
+
given_size > max_page_size ? max_page_size : given_size
|
|
72
|
+
else
|
|
73
|
+
Quo.default_page_size || 20
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Navigation Methods
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
query = UsersQuery.new(page: 2, page_size: 20)
|
|
82
|
+
|
|
83
|
+
# Get next page query (immutable - returns new instance)
|
|
84
|
+
next_page = query.next_page_query
|
|
85
|
+
next_page.page # => 3
|
|
86
|
+
|
|
87
|
+
# Get previous page query
|
|
88
|
+
prev_page = query.previous_page_query
|
|
89
|
+
prev_page.page # => 1
|
|
90
|
+
|
|
91
|
+
# Previous page never goes below 1
|
|
92
|
+
first_page = UsersQuery.new(page: 1)
|
|
93
|
+
prev = first_page.previous_page_query
|
|
94
|
+
prev.page # => 1 (not 0)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Pagination Examples
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# Basic pagination
|
|
101
|
+
users = UsersQuery.new(page: 1, page_size: 25).results
|
|
102
|
+
|
|
103
|
+
# Iterate through pages
|
|
104
|
+
query = UsersQuery.new(page: 1, page_size: 100)
|
|
105
|
+
all_users = []
|
|
106
|
+
|
|
107
|
+
loop do
|
|
108
|
+
results = query.results
|
|
109
|
+
all_users.concat(results.to_a)
|
|
110
|
+
|
|
111
|
+
break if results.to_a.size < query.page_size
|
|
112
|
+
query = query.next_page_query
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Pagination with other options
|
|
116
|
+
query = UsersQuery.new(
|
|
117
|
+
state: "CA",
|
|
118
|
+
page: 3,
|
|
119
|
+
page_size: 50
|
|
120
|
+
).order(:created_at)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Results Objects
|
|
124
|
+
|
|
125
|
+
### Results Base Class
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
module Quo
|
|
129
|
+
class Results
|
|
130
|
+
def initialize(query, transformer: nil, **options)
|
|
131
|
+
@query = query
|
|
132
|
+
@transformer = transformer
|
|
133
|
+
@configured_query = query.unwrap
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Counting methods
|
|
137
|
+
def count # Total count (ignores pagination)
|
|
138
|
+
def total_count # Alias for count
|
|
139
|
+
def size # Alias for count
|
|
140
|
+
def page_count # Count on current page only
|
|
141
|
+
def page_size # Alias for page_count
|
|
142
|
+
|
|
143
|
+
# Existence methods
|
|
144
|
+
def exists?
|
|
145
|
+
def empty?
|
|
146
|
+
|
|
147
|
+
# Enumerable interface
|
|
148
|
+
def each(&block)
|
|
149
|
+
def map(&block)
|
|
150
|
+
def first(limit = nil)
|
|
151
|
+
def last(limit = nil)
|
|
152
|
+
|
|
153
|
+
# Delegation with transformation
|
|
154
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### RelationResults
|
|
160
|
+
|
|
161
|
+
Specialized for ActiveRecord relations:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class RelationResults < Results
|
|
165
|
+
delegate :model, :klass, to: :@query
|
|
166
|
+
|
|
167
|
+
def count
|
|
168
|
+
# Efficient SQL count
|
|
169
|
+
@unpaginated_relation.count
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def total_count
|
|
173
|
+
# For compatibility
|
|
174
|
+
count
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def page_count
|
|
178
|
+
# Only counts current page
|
|
179
|
+
@configured_query.count
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def exists?
|
|
183
|
+
@configured_query.exists?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def find(id)
|
|
187
|
+
result = @configured_query.find(id)
|
|
188
|
+
transform? ? @transformer.call(result) : result
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def find_by(conditions)
|
|
192
|
+
result = @configured_query.find_by(conditions)
|
|
193
|
+
return nil unless result
|
|
194
|
+
transform? ? @transformer.call(result) : result
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def where(conditions)
|
|
198
|
+
# Returns new Results with additional conditions
|
|
199
|
+
self.class.new(
|
|
200
|
+
@query.copy,
|
|
201
|
+
configured_query: @configured_query.where(conditions),
|
|
202
|
+
transformer: @transformer
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### CollectionResults
|
|
209
|
+
|
|
210
|
+
Specialized for enumerable collections:
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
class CollectionResults < Results
|
|
214
|
+
def initialize(query, transformer: nil, total_count: nil)
|
|
215
|
+
super(query, transformer: transformer)
|
|
216
|
+
@total_count = total_count
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def count
|
|
220
|
+
# Counts full collection or uses provided total
|
|
221
|
+
@total_count || @query.unwrap_unpaginated.count
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def page_count
|
|
225
|
+
# Counts items on current page
|
|
226
|
+
@configured_query.count
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def exists?
|
|
230
|
+
!@configured_query.empty?
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def to_a
|
|
234
|
+
arr = @configured_query.to_a
|
|
235
|
+
transform? ? arr.map.with_index { |x, i| @transformer.call(x, i) } : arr
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Working with Results
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
# Get results
|
|
244
|
+
query = UsersQuery.new(state: "CA", page: 1, page_size: 20)
|
|
245
|
+
results = query.results
|
|
246
|
+
|
|
247
|
+
# Counting
|
|
248
|
+
results.count # Total users in CA (ignores pagination)
|
|
249
|
+
results.page_count # Users on current page (max 20)
|
|
250
|
+
results.total_count # Same as count
|
|
251
|
+
|
|
252
|
+
# Existence checks
|
|
253
|
+
if results.exists?
|
|
254
|
+
puts "Found #{results.count} users"
|
|
255
|
+
else
|
|
256
|
+
puts "No users found"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Enumeration
|
|
260
|
+
results.each do |user|
|
|
261
|
+
puts user.name
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Get specific items
|
|
265
|
+
first_user = results.first
|
|
266
|
+
last_user = results.last
|
|
267
|
+
first_five = results.first(5)
|
|
268
|
+
|
|
269
|
+
# Map/Select/Reject with transformation
|
|
270
|
+
emails = results.map(&:email)
|
|
271
|
+
active = results.select(&:active?)
|
|
272
|
+
|
|
273
|
+
# For RelationResults - ActiveRecord methods
|
|
274
|
+
user = results.find(123)
|
|
275
|
+
admin = results.find_by(role: "admin")
|
|
276
|
+
californians = results.where(state: "CA")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Transformation in Results
|
|
280
|
+
|
|
281
|
+
### How Transformation Works
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# Set transformer on query
|
|
285
|
+
query = UsersQuery.new.transform { |user| UserPresenter.new(user) }
|
|
286
|
+
results = query.results
|
|
287
|
+
|
|
288
|
+
# All methods return transformed objects
|
|
289
|
+
results.first # => UserPresenter instance
|
|
290
|
+
results.to_a # => Array of UserPresenter instances
|
|
291
|
+
results.map(&:name) # => Calls name on UserPresenter, not User
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Transformation Implementation
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
# In Results base class
|
|
298
|
+
def transform_results(results)
|
|
299
|
+
return results unless transform?
|
|
300
|
+
|
|
301
|
+
if results.is_a?(Enumerable)
|
|
302
|
+
results.map.with_index { |item, i| @transformer.call(item, i) }
|
|
303
|
+
else
|
|
304
|
+
@transformer.call(results)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Method missing handles transformation
|
|
309
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
310
|
+
if block
|
|
311
|
+
@configured_query.send(method, *args, **kwargs) do |*block_args|
|
|
312
|
+
x = block_args.first
|
|
313
|
+
transformed = transform? ? @transformer.call(x) : x
|
|
314
|
+
block.call(transformed, *(block_args[1..] || []))
|
|
315
|
+
end
|
|
316
|
+
else
|
|
317
|
+
raw = @configured_query.send(method, *args, **kwargs)
|
|
318
|
+
transform_results(raw)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Special Case: group_by
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
# group_by has special handling to transform both keys and values
|
|
327
|
+
query = UsersQuery.new.transform { |u| UserPresenter.new(u) }
|
|
328
|
+
|
|
329
|
+
grouped = query.results.group_by(&:role)
|
|
330
|
+
# Returns: { "admin" => [UserPresenter, ...], "user" => [UserPresenter, ...] }
|
|
331
|
+
|
|
332
|
+
# Custom grouping
|
|
333
|
+
grouped = query.results.group_by { |presenter| presenter.created_at.year }
|
|
334
|
+
# Groups presenters by year
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Pagination Patterns
|
|
338
|
+
|
|
339
|
+
### API Pagination
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class UsersController < ApplicationController
|
|
343
|
+
def index
|
|
344
|
+
query = UsersQuery.new(
|
|
345
|
+
page: params[:page]&.to_i || 1,
|
|
346
|
+
page_size: params[:per_page]&.to_i || 25
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
results = query.results
|
|
350
|
+
|
|
351
|
+
render json: {
|
|
352
|
+
users: results.to_a,
|
|
353
|
+
pagination: {
|
|
354
|
+
current_page: query.page,
|
|
355
|
+
per_page: query.page_size,
|
|
356
|
+
total_count: results.total_count,
|
|
357
|
+
total_pages: (results.total_count.to_f / query.page_size).ceil,
|
|
358
|
+
has_next: results.page_count == query.page_size,
|
|
359
|
+
has_previous: query.page > 1
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Cursor-Based Pagination
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
class CursorPaginatedQuery < Quo::RelationBackedQuery
|
|
370
|
+
prop :cursor, _Nilable(String)
|
|
371
|
+
prop :limit, Integer, default: -> { 20 }
|
|
372
|
+
|
|
373
|
+
def query
|
|
374
|
+
scope = User.order(:id)
|
|
375
|
+
|
|
376
|
+
if cursor
|
|
377
|
+
decoded_id = Base64.decode64(cursor).to_i
|
|
378
|
+
scope = scope.where("id > ?", decoded_id)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
scope.limit(limit + 1) # Fetch one extra to check for more
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def results_with_cursor
|
|
385
|
+
items = results.to_a
|
|
386
|
+
has_more = items.size > limit
|
|
387
|
+
items = items[0...limit] if has_more
|
|
388
|
+
|
|
389
|
+
next_cursor = if has_more && items.any?
|
|
390
|
+
Base64.encode64(items.last.id.to_s).strip
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
{
|
|
394
|
+
data: items,
|
|
395
|
+
next_cursor: next_cursor,
|
|
396
|
+
has_more: has_more
|
|
397
|
+
}
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Infinite Scroll
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
class InfiniteScrollQuery < Quo::RelationBackedQuery
|
|
406
|
+
prop :last_id, _Nilable(Integer)
|
|
407
|
+
prop :batch_size, Integer, default: -> { 50 }
|
|
408
|
+
|
|
409
|
+
def query
|
|
410
|
+
scope = Post.order(created_at: :desc)
|
|
411
|
+
scope = scope.where("id < ?", last_id) if last_id
|
|
412
|
+
scope.limit(batch_size)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Frontend makes requests:
|
|
417
|
+
# GET /posts?last_id=123&batch_size=50
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Performance Considerations
|
|
421
|
+
|
|
422
|
+
### Count Performance
|
|
423
|
+
|
|
424
|
+
```ruby
|
|
425
|
+
# For RelationResults - uses SQL COUNT
|
|
426
|
+
results.count # SELECT COUNT(*) FROM users WHERE ...
|
|
427
|
+
|
|
428
|
+
# For CollectionResults - counts in memory
|
|
429
|
+
results.count # Calls .count on the array
|
|
430
|
+
|
|
431
|
+
# Optimization: Pass total_count when converting
|
|
432
|
+
relation_query = UsersQuery.new
|
|
433
|
+
total = relation_query.results.count # Get count via SQL
|
|
434
|
+
|
|
435
|
+
collection_query = relation_query.to_collection(total_count: total)
|
|
436
|
+
collection_query.results.count # Uses cached total, no counting needed
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Large Result Sets
|
|
440
|
+
|
|
441
|
+
```ruby
|
|
442
|
+
# Bad: Loads everything into memory
|
|
443
|
+
users = UsersQuery.new.results.to_a # Could be millions!
|
|
444
|
+
|
|
445
|
+
# Good: Process in batches
|
|
446
|
+
query = UsersQuery.new(page: 1, page_size: 1000)
|
|
447
|
+
|
|
448
|
+
loop do
|
|
449
|
+
results = query.results
|
|
450
|
+
|
|
451
|
+
results.each do |user|
|
|
452
|
+
# Process user
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
break if results.page_count < query.page_size
|
|
456
|
+
query = query.next_page_query
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Better: Use ActiveRecord's find_each for relations
|
|
460
|
+
UsersQuery.new.unwrap.find_each(batch_size: 1000) do |user|
|
|
461
|
+
# Process user
|
|
462
|
+
end
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Pagination Edge Cases
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
# Empty results
|
|
469
|
+
query = UsersQuery.new(page: 999, page_size: 20)
|
|
470
|
+
results = query.results
|
|
471
|
+
results.count # => 0 (total count)
|
|
472
|
+
results.page_count # => 0 (current page)
|
|
473
|
+
results.exists? # => false
|
|
474
|
+
|
|
475
|
+
# Single page
|
|
476
|
+
query = UsersQuery.new(page: 1, page_size: 1000)
|
|
477
|
+
results = query.results # If total users < 1000
|
|
478
|
+
next_page = query.next_page_query.results
|
|
479
|
+
next_page.empty? # => true
|
|
480
|
+
|
|
481
|
+
# No pagination
|
|
482
|
+
query = UsersQuery.new # No page set
|
|
483
|
+
query.paged? # => false
|
|
484
|
+
results = query.results # Returns all results
|
|
485
|
+
```
|