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,441 @@
|
|
|
1
|
+
# Query Composition and Transformation
|
|
2
|
+
|
|
3
|
+
This document explains Quo's powerful composition system and transformation capabilities.
|
|
4
|
+
|
|
5
|
+
## Composition Overview
|
|
6
|
+
|
|
7
|
+
Quo implements a sophisticated composition system that allows queries to be combined using the `+` operator or `compose` method. The composition strategy is automatically selected based on the types being composed.
|
|
8
|
+
|
|
9
|
+
## Composition Architecture
|
|
10
|
+
|
|
11
|
+
### Strategy Pattern Implementation
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
module Quo
|
|
15
|
+
module Composing
|
|
16
|
+
# Main entry points
|
|
17
|
+
def self.composer(chosen_superclass, left_query_class, right_query_class, joins: nil, left_spec: nil, right_spec: nil)
|
|
18
|
+
registry = ClassStrategyRegistry.new
|
|
19
|
+
strategy = registry.find_strategy(left_query_class, right_query_class)
|
|
20
|
+
strategy.compose(chosen_superclass, left_query_class, right_query_class, joins: joins, left_spec: left_spec, right_spec: right_spec)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.merge_instances(left_instance, right_instance, joins: nil)
|
|
24
|
+
registry = InstanceStrategyRegistry.new
|
|
25
|
+
strategy = registry.find_strategy(left_instance, right_instance)
|
|
26
|
+
strategy.compose(left_instance, right_instance, joins: joins)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Composition Strategies
|
|
33
|
+
|
|
34
|
+
Quo implements different strategies based on what's being composed:
|
|
35
|
+
|
|
36
|
+
1. **RelationAndRelationStrategy** - Composing two relation-backed queries
|
|
37
|
+
2. **RelationAndQueryStrategy** - Composing a relation with any query
|
|
38
|
+
3. **QueryAndRelationStrategy** - Composing any query with a relation
|
|
39
|
+
4. **QueryAndQueryStrategy** - Composing two arbitrary queries
|
|
40
|
+
5. **QueryClassesStrategy** - Composing query classes (not instances)
|
|
41
|
+
|
|
42
|
+
## Class-Level Composition
|
|
43
|
+
|
|
44
|
+
### Basic Class Composition
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# Define base queries
|
|
48
|
+
class ActiveUsersQuery < Quo::RelationBackedQuery
|
|
49
|
+
def query
|
|
50
|
+
User.where(active: true)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class PremiumUsersQuery < Quo::RelationBackedQuery
|
|
55
|
+
def query
|
|
56
|
+
User.where(subscription: "premium")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compose at class level
|
|
61
|
+
ActivePremiumQuery = ActiveUsersQuery + PremiumUsersQuery
|
|
62
|
+
|
|
63
|
+
# Or with explicit method
|
|
64
|
+
ActivePremiumQuery = ActiveUsersQuery.compose(PremiumUsersQuery)
|
|
65
|
+
|
|
66
|
+
# Use the composed class
|
|
67
|
+
query = ActivePremiumQuery.new(page: 1)
|
|
68
|
+
results = query.results # Active AND premium users
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Composed Query Implementation
|
|
72
|
+
|
|
73
|
+
When queries are composed, Quo creates a new `ComposedQuery` class:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
module Quo
|
|
77
|
+
class ComposedQuery < Query
|
|
78
|
+
class << self
|
|
79
|
+
attr_accessor :_left_class, :_right_class, :_joins
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Properties from both queries are merged
|
|
83
|
+
# Left query properties take precedence in conflicts
|
|
84
|
+
|
|
85
|
+
def query
|
|
86
|
+
# Combines queries based on their types
|
|
87
|
+
merge_instances(left_query, right_query)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Property Inheritance
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
class FilteredUsersQuery < Quo::RelationBackedQuery
|
|
97
|
+
prop :min_age, Integer
|
|
98
|
+
prop :role, String
|
|
99
|
+
|
|
100
|
+
def query
|
|
101
|
+
User.where("age >= ?", min_age).where(role: role)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class LocationUsersQuery < Quo::RelationBackedQuery
|
|
106
|
+
prop :city, String
|
|
107
|
+
prop :state, String
|
|
108
|
+
|
|
109
|
+
def query
|
|
110
|
+
User.where(city: city, state: state)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Composed query has all properties
|
|
115
|
+
ComposedQuery = FilteredUsersQuery + LocationUsersQuery
|
|
116
|
+
|
|
117
|
+
query = ComposedQuery.new(
|
|
118
|
+
min_age: 21,
|
|
119
|
+
role: "admin",
|
|
120
|
+
city: "New York",
|
|
121
|
+
state: "NY"
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Instance-Level Composition
|
|
126
|
+
|
|
127
|
+
### Basic Instance Composition
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# Create query instances
|
|
131
|
+
active_users = ActiveUsersQuery.new
|
|
132
|
+
premium_users = PremiumUsersQuery.new
|
|
133
|
+
|
|
134
|
+
# Compose instances
|
|
135
|
+
combined = active_users + premium_users
|
|
136
|
+
# Or
|
|
137
|
+
combined = active_users.merge(premium_users)
|
|
138
|
+
|
|
139
|
+
results = combined.results
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Composition with Joins
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# Define queries that need joins
|
|
146
|
+
class PublishedPostsQuery < Quo::RelationBackedQuery
|
|
147
|
+
def query
|
|
148
|
+
Post.where(published: true)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class ActiveAuthorsQuery < Quo::RelationBackedQuery
|
|
153
|
+
def query
|
|
154
|
+
Author.where(active: true)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Compose with explicit join
|
|
159
|
+
posts = PublishedPostsQuery.new
|
|
160
|
+
authors = ActiveAuthorsQuery.new
|
|
161
|
+
|
|
162
|
+
# Method 1: Using joins parameter
|
|
163
|
+
combined = posts.compose(authors, joins: :author)
|
|
164
|
+
|
|
165
|
+
# Method 2: Chaining joins before composition
|
|
166
|
+
combined = posts.joins(:author) + authors
|
|
167
|
+
|
|
168
|
+
# Results in: SELECT posts.* FROM posts
|
|
169
|
+
# INNER JOIN authors ON authors.id = posts.author_id
|
|
170
|
+
# WHERE posts.published = true AND authors.active = true
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Complex Join Examples
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# Multiple joins
|
|
177
|
+
posts_with_comments = posts.compose(
|
|
178
|
+
CommentQuery.new,
|
|
179
|
+
joins: [:author, :comments]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Nested joins
|
|
183
|
+
posts_with_nested = posts.compose(
|
|
184
|
+
CategoryQuery.new,
|
|
185
|
+
joins: { author: :profile, comments: :user }
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Hash conditions in joins
|
|
189
|
+
posts_with_conditions = posts.compose(
|
|
190
|
+
AuthorQuery.new,
|
|
191
|
+
joins: { author: { profile: :preferences } }
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Mixed Type Composition
|
|
196
|
+
|
|
197
|
+
### Relation + Collection
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
# Start with a relation query
|
|
201
|
+
class BaseUsersQuery < Quo::RelationBackedQuery
|
|
202
|
+
def query
|
|
203
|
+
User.where(deleted_at: nil)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Have a collection of special users
|
|
208
|
+
class SpecialUsersQuery < Quo::CollectionBackedQuery
|
|
209
|
+
def collection
|
|
210
|
+
# From cache, API, etc
|
|
211
|
+
[
|
|
212
|
+
User.new(id: 1, name: "Admin"),
|
|
213
|
+
User.new(id: 2, name: "Support")
|
|
214
|
+
]
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Compose them
|
|
219
|
+
combined = BaseUsersQuery.new + SpecialUsersQuery.new
|
|
220
|
+
# Results include both database users AND special users
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### How Mixed Composition Works
|
|
224
|
+
|
|
225
|
+
When composing different types:
|
|
226
|
+
|
|
227
|
+
1. If either is a collection, result is collection-backed
|
|
228
|
+
2. Relations are converted to arrays via `to_a`
|
|
229
|
+
3. Collections are concatenated
|
|
230
|
+
4. Duplicates are NOT automatically removed
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# Under the hood for Relation + Collection
|
|
234
|
+
def compose_relation_and_collection(relation_query, collection_query)
|
|
235
|
+
relation_results = relation_query.results.to_a
|
|
236
|
+
collection_results = collection_query.results.to_a
|
|
237
|
+
|
|
238
|
+
Quo::CollectionBackedQuery.wrap(relation_results + collection_results)
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Transformation System
|
|
243
|
+
|
|
244
|
+
### Basic Transformation
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
# Transform results after fetching
|
|
248
|
+
users_query = UsersByStateQuery.new(state: "CA")
|
|
249
|
+
.transform { |user| UserPresenter.new(user) }
|
|
250
|
+
|
|
251
|
+
# All result methods return transformed objects
|
|
252
|
+
users_query.results.each do |presenter|
|
|
253
|
+
puts presenter.display_name # Not user.name
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
users_query.results.first # Returns UserPresenter, not User
|
|
257
|
+
users_query.results.map(&:to_json) # Maps over presenters
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Transformation Implementation
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
# Inside Quo::Query
|
|
264
|
+
def transform(&block)
|
|
265
|
+
@__transformer = block
|
|
266
|
+
self
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Inside Results classes
|
|
270
|
+
def transform_results(results)
|
|
271
|
+
return results unless transform?
|
|
272
|
+
|
|
273
|
+
if results.is_a?(Enumerable)
|
|
274
|
+
results.map.with_index { |item, i| @transformer.call(item, i) }
|
|
275
|
+
else
|
|
276
|
+
@transformer.call(results)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Index-Aware Transformation
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# Transformer receives optional index
|
|
285
|
+
query.transform do |user, index|
|
|
286
|
+
{
|
|
287
|
+
position: index + 1,
|
|
288
|
+
name: user.name,
|
|
289
|
+
email: user.email
|
|
290
|
+
}
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# First user gets position: 1, second gets position: 2, etc.
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Chaining Transformations
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
# Note: Only last transformation is applied
|
|
300
|
+
users_query
|
|
301
|
+
.transform { |u| UserDecorator.new(u) } # This is overwritten
|
|
302
|
+
.transform { |u| UserPresenter.new(u) } # This is applied
|
|
303
|
+
|
|
304
|
+
# To chain transformations, compose in one block
|
|
305
|
+
users_query.transform do |user|
|
|
306
|
+
decorated = UserDecorator.new(user)
|
|
307
|
+
UserPresenter.new(decorated)
|
|
308
|
+
end
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Advanced Composition Patterns
|
|
312
|
+
|
|
313
|
+
### Repository Pattern
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
class UserRepository
|
|
317
|
+
def active
|
|
318
|
+
@active ||= ActiveUsersQuery
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def premium
|
|
322
|
+
@premium ||= PremiumUsersQuery
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def verified
|
|
326
|
+
@verified ||= VerifiedUsersQuery
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def active_premium_verified
|
|
330
|
+
active + premium + verified
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
repo = UserRepository.new
|
|
335
|
+
users = repo.active_premium_verified.new.results
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Dynamic Composition
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
class SearchQuery < Quo::RelationBackedQuery
|
|
342
|
+
prop :filters, Hash, default: -> { {} }
|
|
343
|
+
|
|
344
|
+
def query
|
|
345
|
+
base = User.all
|
|
346
|
+
|
|
347
|
+
# Dynamically compose queries based on filters
|
|
348
|
+
queries_to_compose = []
|
|
349
|
+
|
|
350
|
+
queries_to_compose << ActiveUsersQuery if filters[:active]
|
|
351
|
+
queries_to_compose << PremiumUsersQuery if filters[:premium]
|
|
352
|
+
queries_to_compose << VerifiedUsersQuery if filters[:verified]
|
|
353
|
+
|
|
354
|
+
queries_to_compose.reduce(base) do |combined, query_class|
|
|
355
|
+
combined + query_class.new
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Conditional Composition
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
class ConditionalQuery < Quo::RelationBackedQuery
|
|
365
|
+
prop :include_archived, Boolean, default: -> { false }
|
|
366
|
+
prop :user_id, Integer
|
|
367
|
+
|
|
368
|
+
def query
|
|
369
|
+
base = Post.where(user_id: user_id)
|
|
370
|
+
|
|
371
|
+
if include_archived
|
|
372
|
+
base + ArchivedPostsQuery.new(user_id: user_id)
|
|
373
|
+
else
|
|
374
|
+
base
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Composition Performance
|
|
381
|
+
|
|
382
|
+
### Efficient Database Queries
|
|
383
|
+
|
|
384
|
+
When composing relation-backed queries:
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
# Good: Single database query
|
|
388
|
+
active_premium = ActiveUsersQuery.new + PremiumUsersQuery.new
|
|
389
|
+
users = active_premium.results # One query with merged conditions
|
|
390
|
+
|
|
391
|
+
# SQL: SELECT users.* FROM users WHERE active = true AND subscription = 'premium'
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Memory Considerations
|
|
395
|
+
|
|
396
|
+
When composing with collections:
|
|
397
|
+
|
|
398
|
+
```ruby
|
|
399
|
+
# Caution: Loads all records into memory
|
|
400
|
+
relation_query = User.where(active: true) # Could be millions
|
|
401
|
+
collection_query = SpecialUsersQuery.new # Just a few
|
|
402
|
+
|
|
403
|
+
combined = relation_query + collection_query
|
|
404
|
+
# This executes relation_query.to_a - loads ALL active users!
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Best Practices
|
|
408
|
+
|
|
409
|
+
1. **Compose similar types when possible** - Relation + Relation is most efficient
|
|
410
|
+
2. **Use joins parameter for associations** - Avoids N+1 queries
|
|
411
|
+
3. **Transform after composition** - More efficient than transforming each query
|
|
412
|
+
4. **Be mindful of collection size** - Mixed composition loads relations into memory
|
|
413
|
+
5. **Consider caching** - For expensive composed queries
|
|
414
|
+
|
|
415
|
+
## Testing Composed Queries
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
# Test composition behavior
|
|
419
|
+
class ComposedQueryTest < ActiveSupport::TestCase
|
|
420
|
+
test "combines filters from both queries" do
|
|
421
|
+
left = ActiveUsersQuery.new
|
|
422
|
+
right = PremiumUsersQuery.new
|
|
423
|
+
|
|
424
|
+
composed = left + right
|
|
425
|
+
sql = composed.to_sql
|
|
426
|
+
|
|
427
|
+
assert_includes sql, "active = true"
|
|
428
|
+
assert_includes sql, "subscription = 'premium'"
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
test "transformation applies to composed results" do
|
|
432
|
+
composed = (ActiveUsersQuery.new + PremiumUsersQuery.new)
|
|
433
|
+
.transform { |u| u.name.upcase }
|
|
434
|
+
|
|
435
|
+
fake_query(composed.__class__, results: [User.new(name: "john")]) do
|
|
436
|
+
result = composed.results.first
|
|
437
|
+
assert_equal "JOHN", result
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
```
|