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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +17 -0
  3. data/.devcontainer/compose.yml +10 -0
  4. data/.devcontainer/devcontainer.json +12 -0
  5. data/Appraisals +4 -12
  6. data/CHANGELOG.md +112 -1
  7. data/CLAUDE.md +19 -0
  8. data/Gemfile +7 -1
  9. data/LICENSE.txt +1 -1
  10. data/README.md +496 -203
  11. data/Rakefile +66 -6
  12. data/UPGRADING.md +216 -0
  13. data/badges/coverage_badge_total.svg +35 -0
  14. data/badges/rubycritic_badge_score.svg +35 -0
  15. data/claude-skill/README.md +100 -0
  16. data/claude-skill/SKILL.md +442 -0
  17. data/claude-skill/references/API_REFERENCE.md +462 -0
  18. data/claude-skill/references/COMPOSITION.md +396 -0
  19. data/claude-skill/references/PAGINATION.md +396 -0
  20. data/claude-skill/references/QUERY_TYPES.md +297 -0
  21. data/claude-skill/references/TRANSFORMERS.md +282 -0
  22. data/context/01-core-architecture.md +247 -0
  23. data/context/02-query-types-implementation.md +355 -0
  24. data/context/03-composition-transformation.md +441 -0
  25. data/context/04-pagination-results.md +485 -0
  26. data/context/05-testing-configuration.md +491 -0
  27. data/context/06-advanced-patterns-examples.md +153 -0
  28. data/gemfiles/rails_8.0.gemfile +10 -5
  29. data/gemfiles/rails_8.1.gemfile +20 -0
  30. data/lib/generators/quo/install/USAGE +21 -0
  31. data/lib/generators/quo/install/install_generator.rb +63 -0
  32. data/lib/quo/collection_backed_query.rb +21 -15
  33. data/lib/quo/collection_results.rb +1 -0
  34. data/lib/quo/composed_collection_backed_query.rb +42 -0
  35. data/lib/quo/composed_instance.rb +144 -0
  36. data/lib/quo/composed_query.rb +43 -178
  37. data/lib/quo/composed_relation_backed_query.rb +42 -0
  38. data/lib/quo/composing/base_strategy.rb +22 -0
  39. data/lib/quo/composing/class_strategy.rb +86 -0
  40. data/lib/quo/composing/class_strategy_registry.rb +31 -0
  41. data/lib/quo/composing/query_classes_strategy.rb +38 -0
  42. data/lib/quo/composing.rb +81 -0
  43. data/lib/quo/engine.rb +1 -0
  44. data/lib/quo/minitest/helpers.rb +14 -24
  45. data/lib/quo/preloadable.rb +1 -0
  46. data/lib/quo/query.rb +22 -5
  47. data/lib/quo/relation_backed_query.rb +24 -18
  48. data/lib/quo/relation_backed_query_specification.rb +44 -25
  49. data/lib/quo/relation_results.rb +1 -0
  50. data/lib/quo/results.rb +31 -2
  51. data/lib/quo/rspec/helpers.rb +15 -26
  52. data/lib/quo/testing/collection_backed_fake.rb +1 -0
  53. data/lib/quo/testing/fake_helpers.rb +30 -0
  54. data/lib/quo/testing/relation_backed_fake.rb +1 -0
  55. data/lib/quo/version.rb +1 -1
  56. data/lib/quo/wrapped_collection_backed_query.rb +21 -0
  57. data/lib/quo/wrapped_relation_backed_query.rb +21 -0
  58. data/lib/quo.rb +8 -0
  59. data/quo.png +0 -0
  60. data/sig/generated/quo/collection_backed_query.rbs +10 -4
  61. data/sig/generated/quo/collection_results.rbs +1 -0
  62. data/sig/generated/quo/composed_collection_backed_query.rbs +25 -0
  63. data/sig/generated/quo/composed_instance.rbs +61 -0
  64. data/sig/generated/quo/composed_query.rbs +23 -56
  65. data/sig/generated/quo/composed_relation_backed_query.rbs +25 -0
  66. data/sig/generated/quo/composing/base_strategy.rbs +16 -0
  67. data/sig/generated/quo/composing/class_strategy.rbs +38 -0
  68. data/sig/generated/quo/composing/class_strategy_registry.rbs +16 -0
  69. data/sig/generated/quo/composing/query_classes_strategy.rbs +24 -0
  70. data/sig/generated/quo/composing.rbs +40 -0
  71. data/sig/generated/quo/engine.rbs +1 -0
  72. data/sig/generated/quo/minitest/helpers.rbs +12 -0
  73. data/sig/generated/quo/preloadable.rbs +1 -0
  74. data/sig/generated/quo/query.rbs +15 -4
  75. data/sig/generated/quo/relation_backed_query.rbs +15 -5
  76. data/sig/generated/quo/relation_backed_query_specification.rbs +47 -39
  77. data/sig/generated/quo/relation_results.rbs +1 -0
  78. data/sig/generated/quo/results.rbs +11 -0
  79. data/sig/generated/quo/rspec/helpers.rbs +12 -0
  80. data/sig/generated/quo/testing/collection_backed_fake.rbs +1 -0
  81. data/sig/generated/quo/testing/fake_helpers.rbs +14 -0
  82. data/sig/generated/quo/testing/relation_backed_fake.rbs +1 -0
  83. data/sig/generated/quo/wrapped_collection_backed_query.rbs +13 -0
  84. data/sig/generated/quo/wrapped_relation_backed_query.rbs +13 -0
  85. data/sig/generated/quo.rbs +1 -0
  86. data/website/.gitignore +6 -0
  87. data/website/.nojekyll +0 -0
  88. data/website/404.html +26 -0
  89. data/website/Gemfile +24 -0
  90. data/website/_config.yml +50 -0
  91. data/website/_data/navigation.yml +8 -0
  92. data/website/_data/sidebar.yml +2 -0
  93. data/website/_data/social_links.yml +3 -0
  94. data/website/_docs/api.md +261 -0
  95. data/website/_docs/get-started.md +289 -0
  96. data/website/assets/quo.png +0 -0
  97. data/website/index.md +141 -0
  98. metadata +70 -13
  99. data/gemfiles/rails_7.0.gemfile +0 -15
  100. data/gemfiles/rails_7.1.gemfile +0 -15
  101. data/gemfiles/rails_7.2.gemfile +0 -15
@@ -0,0 +1,491 @@
1
+ # Testing and Configuration
2
+
3
+ This document covers testing strategies for Quo query objects and configuration options.
4
+
5
+ ## Testing Support
6
+
7
+ Quo provides first-class testing support through helper modules that allow you to mock query results without hitting the database.
8
+
9
+ ### Testing Helpers Overview
10
+
11
+ ```ruby
12
+ # For Minitest
13
+ module Quo::Minitest::Helpers
14
+ def fake_query(query_class, results: [], total_count: nil, page_count: nil, &block)
15
+ # Mocks query_class.new to return fake results
16
+ end
17
+ end
18
+
19
+ # For RSpec
20
+ module Quo::RSpec::Helpers
21
+ def fake_query(query_class, results: [], total_count: nil, page_count: nil, &block)
22
+ # Same interface as Minitest version
23
+ end
24
+ end
25
+ ```
26
+
27
+ ### Minitest Testing
28
+
29
+ #### Basic Usage
30
+
31
+ ```ruby
32
+ class UserQueryTest < ActiveSupport::TestCase
33
+ include Quo::Minitest::Helpers
34
+
35
+ test "transforms users to presenters" do
36
+ users = [
37
+ User.new(id: 1, name: "Alice"),
38
+ User.new(id: 2, name: "Bob")
39
+ ]
40
+
41
+ fake_query(UsersQuery, results: users) do
42
+ query = UsersQuery.new.transform { |u| u.name.upcase }
43
+ results = query.results.to_a
44
+
45
+ assert_equal ["ALICE", "BOB"], results
46
+ end
47
+ end
48
+
49
+ test "applies pagination" do
50
+ users = (1..30).map { |i| User.new(id: i) }
51
+
52
+ fake_query(UsersQuery, results: users) do
53
+ query = UsersQuery.new(page: 2, page_size: 10)
54
+ results = query.results
55
+
56
+ assert_equal 10, results.page_count
57
+ assert_equal 30, results.total_count
58
+ assert_equal 11, results.first.id # Page 2 starts at user 11
59
+ end
60
+ end
61
+ end
62
+ ```
63
+
64
+ #### Testing Query Logic
65
+
66
+ ```ruby
67
+ class ComplexQueryTest < ActiveSupport::TestCase
68
+ include Quo::Minitest::Helpers
69
+
70
+ test "filters by multiple conditions" do
71
+ # Test the actual query logic
72
+ query = ComplexUsersQuery.new(
73
+ min_age: 21,
74
+ state: "CA",
75
+ active: true
76
+ )
77
+
78
+ sql = query.to_sql
79
+ assert_match /age >= 21/, sql
80
+ assert_match /state = 'CA'/, sql
81
+ assert_match /active = true/, sql
82
+ end
83
+
84
+ test "composes queries correctly" do
85
+ fake_query(ActiveUsersQuery, results: [User.new(id: 1)]) do
86
+ fake_query(PremiumUsersQuery, results: [User.new(id: 2)]) do
87
+ composed = ActiveUsersQuery.new + PremiumUsersQuery.new
88
+
89
+ # The composition strategy determines final results
90
+ results = composed.results.to_a
91
+ assert_equal 2, results.count
92
+ end
93
+ end
94
+ end
95
+ end
96
+ ```
97
+
98
+ ### RSpec Testing
99
+
100
+ #### Basic Usage
101
+
102
+ ```ruby
103
+ RSpec.describe UsersQuery do
104
+ include Quo::RSpec::Helpers
105
+
106
+ describe "#results" do
107
+ it "returns transformed users" do
108
+ users = [User.new(name: "Alice"), User.new(name: "Bob")]
109
+
110
+ fake_query(UsersQuery, results: users) do
111
+ query = UsersQuery.new.transform { |u| UserPresenter.new(u) }
112
+ results = query.results.to_a
113
+
114
+ expect(results).to all(be_a(UserPresenter))
115
+ expect(results.map(&:name)).to eq(["Alice", "Bob"])
116
+ end
117
+ end
118
+ end
119
+
120
+ describe "pagination" do
121
+ it "respects page boundaries" do
122
+ users = build_list(:user, 50)
123
+
124
+ fake_query(UsersQuery, results: users) do
125
+ query = UsersQuery.new(page: 3, page_size: 15)
126
+ results = query.results
127
+
128
+ expect(results.page_count).to eq(15)
129
+ expect(results.total_count).to eq(50)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ ```
135
+
136
+ #### Testing Composition
137
+
138
+ ```ruby
139
+ RSpec.describe "Query Composition" do
140
+ include Quo::RSpec::Helpers
141
+
142
+ it "combines results from multiple queries" do
143
+ active_users = [User.new(name: "Active")]
144
+ premium_users = [User.new(name: "Premium")]
145
+
146
+ fake_query(ActiveUsersQuery, results: active_users) do
147
+ fake_query(PremiumUsersQuery, results: premium_users) do
148
+ composed = ActiveUsersQuery.new + PremiumUsersQuery.new
149
+ all_users = composed.results.to_a
150
+
151
+ expect(all_users.map(&:name)).to contain_exactly("Active", "Premium")
152
+ end
153
+ end
154
+ end
155
+ end
156
+ ```
157
+
158
+ ### Testing Implementation Details
159
+
160
+ #### How fake_query Works
161
+
162
+ ```ruby
163
+ def fake_query(query_class, results: [], total_count: nil, page_count: nil, &block)
164
+ if query_class < Quo::CollectionBackedQuery
165
+ # Creates a fake collection-backed query
166
+ klass = Class.new(Quo::Testing::CollectionBackedFake) do
167
+ # Include Preloadable if original has it
168
+ if query_class < Quo::Preloadable
169
+ include Quo::Preloadable
170
+ end
171
+ end
172
+
173
+ query_class.stub(:new, ->(**kwargs) {
174
+ klass.new(results: results, total_count: total_count, page_count: page_count)
175
+ }) do
176
+ yield
177
+ end
178
+ elsif query_class < Quo::RelationBackedQuery
179
+ # Creates a fake relation-backed query
180
+ query_class.stub(:new, ->(**kwargs) {
181
+ Quo::Testing::RelationBackedFake.new(
182
+ results: results,
183
+ total_count: total_count,
184
+ page_count: page_count
185
+ )
186
+ }) do
187
+ yield
188
+ end
189
+ end
190
+ end
191
+ ```
192
+
193
+ #### Testing Fake Classes
194
+
195
+ ```ruby
196
+ # CollectionBackedFake
197
+ module Quo::Testing
198
+ class CollectionBackedFake < Quo::CollectionBackedQuery
199
+ prop :results, _Array(_Any), default: -> { [] }
200
+ prop :total_count, _Nilable(Integer)
201
+ prop :page_count, _Nilable(Integer)
202
+
203
+ def collection
204
+ @results
205
+ end
206
+ end
207
+ end
208
+
209
+ # RelationBackedFake
210
+ module Quo::Testing
211
+ class RelationBackedFake < Quo::RelationBackedQuery
212
+ prop :results, _Array(_Any), default: -> { [] }
213
+ prop :total_count, _Nilable(Integer)
214
+ prop :page_count, _Nilable(Integer)
215
+
216
+ def query
217
+ # Returns a relation-like object that behaves correctly
218
+ end
219
+ end
220
+ end
221
+ ```
222
+
223
+ ### Advanced Testing Patterns
224
+
225
+ #### Testing with Dependencies
226
+
227
+ ```ruby
228
+ class ServiceQueryTest < ActiveSupport::TestCase
229
+ include Quo::Minitest::Helpers
230
+
231
+ setup do
232
+ @api_client = mock('api_client')
233
+ end
234
+
235
+ test "fetches data from external service" do
236
+ expected_data = [{ id: 1, name: "Remote User" }]
237
+ @api_client.expects(:fetch_users).returns(expected_data)
238
+
239
+ query = ExternalUsersQuery.new(api_client: @api_client)
240
+ results = query.results.to_a
241
+
242
+ assert_equal expected_data, results
243
+ end
244
+ end
245
+ ```
246
+
247
+ #### Testing Error Cases
248
+
249
+ ```ruby
250
+ RSpec.describe UsersQuery do
251
+ include Quo::RSpec::Helpers
252
+
253
+ context "with invalid parameters" do
254
+ it "raises ArgumentError for invalid state" do
255
+ expect {
256
+ UsersQuery.new(state: "INVALID")
257
+ }.to raise_error(ArgumentError)
258
+ end
259
+ end
260
+
261
+ context "when no results found" do
262
+ it "returns empty results" do
263
+ fake_query(UsersQuery, results: []) do
264
+ query = UsersQuery.new(state: "XX")
265
+
266
+ expect(query.results).to be_empty
267
+ expect(query.results.exists?).to be false
268
+ expect(query.results.count).to eq 0
269
+ end
270
+ end
271
+ end
272
+ end
273
+ ```
274
+
275
+ ## Configuration
276
+
277
+ ### Global Configuration
278
+
279
+ Configure Quo in an initializer:
280
+
281
+ ```ruby
282
+ # config/initializers/quo.rb
283
+ module Quo
284
+ # Pagination defaults
285
+ self.default_page_size = 25
286
+ self.max_page_size = 100
287
+
288
+ # Custom base classes
289
+ self.relation_backed_query_base_class = "ApplicationQuery"
290
+ self.collection_backed_query_base_class = "ApplicationCollectionQuery"
291
+ end
292
+ ```
293
+
294
+ ### Configuration Options Explained
295
+
296
+ #### default_page_size
297
+
298
+ ```ruby
299
+ # Sets the default page size when not specified
300
+ Quo.default_page_size = 25
301
+
302
+ # Used in Query base class
303
+ prop :page_size, _Nilable(Integer),
304
+ default: -> { Quo.default_page_size || 20 }
305
+
306
+ # Example impact
307
+ query = UsersQuery.new(page: 1) # No page_size specified
308
+ query.page_size # => 25 (from configuration)
309
+ ```
310
+
311
+ #### max_page_size
312
+
313
+ ```ruby
314
+ # Prevents excessive resource usage
315
+ Quo.max_page_size = 100
316
+
317
+ # Applied during sanitization
318
+ def sanitised_page_size
319
+ if page_size&.positive?
320
+ given_size = page_size.to_i
321
+ max_page_size = Quo.max_page_size || 200
322
+ given_size > max_page_size ? max_page_size : given_size
323
+ else
324
+ Quo.default_page_size || 20
325
+ end
326
+ end
327
+
328
+ # Example protection
329
+ query = UsersQuery.new(page_size: 10000) # Excessive!
330
+ query.results # Actually uses page_size of 100
331
+ ```
332
+
333
+ #### Custom Base Classes
334
+
335
+ ```ruby
336
+ # app/queries/application_query.rb
337
+ class ApplicationQuery < Quo::RelationBackedQuery
338
+ # Shared functionality for all relation queries
339
+
340
+ # Add default scope
341
+ def query
342
+ super.where(tenant_id: Current.tenant_id)
343
+ end
344
+
345
+ # Add logging
346
+ def results
347
+ Rails.logger.info "Executing query: #{self.class.name}"
348
+ super
349
+ end
350
+ end
351
+
352
+ # app/queries/application_collection_query.rb
353
+ class ApplicationCollectionQuery < Quo::CollectionBackedQuery
354
+ # Shared functionality for all collection queries
355
+
356
+ include Quo::Preloadable # All collections can preload
357
+
358
+ # Add caching support
359
+ def cached_collection(key, expires_in: 1.hour)
360
+ Rails.cache.fetch(key, expires_in: expires_in) do
361
+ collection
362
+ end
363
+ end
364
+ end
365
+
366
+ # Configure Quo to use these
367
+ Quo.relation_backed_query_base_class = "ApplicationQuery"
368
+ Quo.collection_backed_query_base_class = "ApplicationCollectionQuery"
369
+ ```
370
+
371
+ ### Environment-Specific Configuration
372
+
373
+ ```ruby
374
+ # config/environments/development.rb
375
+ Quo.max_page_size = 50 # Smaller in development
376
+
377
+ # config/environments/production.rb
378
+ Quo.max_page_size = 200 # Larger in production
379
+
380
+ # config/environments/test.rb
381
+ Quo.default_page_size = 10 # Smaller for faster tests
382
+ ```
383
+
384
+ ### Dynamic Configuration
385
+
386
+ ```ruby
387
+ # Configuration can be changed at runtime
388
+ class AdminController < ApplicationController
389
+ around_action :with_admin_pagination
390
+
391
+ private
392
+
393
+ def with_admin_pagination
394
+ old_max = Quo.max_page_size
395
+ Quo.max_page_size = 500 # Admins can see more
396
+ yield
397
+ ensure
398
+ Quo.max_page_size = old_max
399
+ end
400
+ end
401
+ ```
402
+
403
+ ## Testing Best Practices
404
+
405
+ ### 1. Test Query Logic Separately
406
+
407
+ ```ruby
408
+ # Test the query building logic
409
+ test "builds correct query" do
410
+ query = ComplexQuery.new(filters: { active: true, role: "admin" })
411
+
412
+ sql = query.to_sql
413
+ assert_match /active = true/, sql
414
+ assert_match /role = 'admin'/, sql
415
+ end
416
+
417
+ # Test the results separately with fake_query
418
+ test "transforms results correctly" do
419
+ fake_query(ComplexQuery, results: [User.new]) do
420
+ query = ComplexQuery.new.transform { |u| u.name.upcase }
421
+ assert_equal "ALICE", query.results.first
422
+ end
423
+ end
424
+ ```
425
+
426
+ ### 2. Use Factories
427
+
428
+ ```ruby
429
+ # spec/support/query_helpers.rb
430
+ module QueryHelpers
431
+ def build_fake_users(count: 10, **attributes)
432
+ (1..count).map do |i|
433
+ User.new(id: i, name: "User #{i}", **attributes)
434
+ end
435
+ end
436
+ end
437
+
438
+ # In tests
439
+ test "paginates correctly" do
440
+ users = build_fake_users(count: 100)
441
+
442
+ fake_query(UsersQuery, results: users) do
443
+ # Test pagination
444
+ end
445
+ end
446
+ ```
447
+
448
+ ### 3. Test Edge Cases
449
+
450
+ ```ruby
451
+ describe "edge cases" do
452
+ it "handles empty results" do
453
+ fake_query(UsersQuery, results: []) do
454
+ query = UsersQuery.new
455
+ expect(query.results).to be_empty
456
+ end
457
+ end
458
+
459
+ it "handles nil transformer" do
460
+ fake_query(UsersQuery, results: [User.new]) do
461
+ query = UsersQuery.new # No transformer
462
+ expect(query.results.first).to be_a(User)
463
+ end
464
+ end
465
+
466
+ it "handles pagination beyond available results" do
467
+ fake_query(UsersQuery, results: [User.new]) do
468
+ query = UsersQuery.new(page: 999)
469
+ expect(query.results).to be_empty
470
+ end
471
+ end
472
+ end
473
+ ```
474
+
475
+ ### 4. Integration Tests
476
+
477
+ ```ruby
478
+ # Sometimes you want to test against real database
479
+ class IntegrationTest < ActiveSupport::TestCase
480
+ # Don't include Quo::Minitest::Helpers
481
+
482
+ test "actually queries database" do
483
+ user = User.create!(name: "Real User", state: "CA")
484
+
485
+ query = UsersQuery.new(state: "CA")
486
+ results = query.results
487
+
488
+ assert_includes results.to_a, user
489
+ end
490
+ end
491
+ ```
@@ -0,0 +1,153 @@
1
+ # Advanced Patterns and Examples
2
+
3
+ This document showcases advanced usage patterns and real-world examples of Quo query objects.
4
+
5
+ ## Repository Pattern
6
+
7
+ ### Basic Repository
8
+
9
+ ```ruby
10
+ # app/repositories/user_repository.rb
11
+ class UserRepository
12
+ class << self
13
+ def active
14
+ ActiveUsersQuery.new
15
+ end
16
+
17
+ def by_state(state)
18
+ UsersByStateQuery.new(state: state)
19
+ end
20
+
21
+ def premium
22
+ PremiumUsersQuery.new
23
+ end
24
+
25
+ def with_recent_activity(days: 7)
26
+ RecentlyActiveUsersQuery.new(days_ago: days)
27
+ end
28
+
29
+ # Compose common combinations
30
+ def active_premium_in_state(state)
31
+ active + premium + by_state(state)
32
+ end
33
+ end
34
+ end
35
+
36
+ # Usage in controllers
37
+ class UsersController < ApplicationController
38
+ def index
39
+ @users = UserRepository
40
+ .active_premium_in_state("CA")
41
+ .transform { |u| UserPresenter.new(u) }
42
+ .results
43
+ end
44
+ end
45
+ ```
46
+
47
+ ### Repository with Caching
48
+
49
+ ```ruby
50
+ class CachedUserRepository
51
+ class << self
52
+ def search(term, cached: true)
53
+ if cached
54
+ CachedSearchQuery.new(search_term: term)
55
+ else
56
+ LiveSearchQuery.new(search_term: term)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ class CachedSearchQuery < Quo::CollectionBackedQuery
63
+ prop :search_term, String
64
+
65
+ def collection
66
+ Rails.cache.fetch("search_#{search_term}", expires_in: 5.minutes) do
67
+ LiveSearchQuery.new(search_term:).results.to_a
68
+ end
69
+ end
70
+ end
71
+ ```
72
+
73
+ ## Composition
74
+
75
+ ```ruby
76
+ class UserOnboardingService
77
+ def initialize(user)
78
+ @user = user
79
+ end
80
+
81
+ def recommended_connections
82
+ # Compose multiple criteria
83
+ query = SimilarUsersQuery.new(interests: @user.interests) +
84
+ NearbyUsersQuery.new(location: @user.location, radius: 50) +
85
+ ActiveInLastWeekQuery.new
86
+
87
+ query.transform { |u| ConnectionRecommendation.new(@user, u) }
88
+ .results
89
+ .select(&:should_recommend?)
90
+ .first(5)
91
+ end
92
+ end
93
+ ```
94
+
95
+ ## Performance Monitoring
96
+
97
+ ### Instrumented Queries
98
+
99
+ ```ruby
100
+ class InstrumentedQuery < Quo::RelationBackedQuery
101
+ def results
102
+ ActiveSupport::Notifications.instrument(
103
+ "query.quo",
104
+ query_class: self.class.name,
105
+ properties: to_h
106
+ ) do
107
+ super
108
+ end
109
+ end
110
+ end
111
+
112
+ # Subscribe to notifications
113
+ ActiveSupport::Notifications.subscribe("query.quo") do |*args|
114
+ event = ActiveSupport::Notifications::Event.new(*args)
115
+
116
+ Rails.logger.info(
117
+ "Query: #{event.payload[:query_class]} " \
118
+ "Duration: #{event.duration}ms " \
119
+ "Properties: #{event.payload[:properties]}"
120
+ )
121
+
122
+ if event.duration > 1000 # Log slow queries
123
+ SlowQueryLogger.log(event)
124
+ end
125
+ end
126
+ ```
127
+
128
+ ### Query with Explain
129
+
130
+ ```ruby
131
+ class AnalyzedQuery < Quo::RelationBackedQuery
132
+ prop :analyze, Boolean, default: -> { Rails.env.development? }
133
+
134
+ def results
135
+ if analyze && relation?
136
+ log_query_plan
137
+ end
138
+ super
139
+ end
140
+
141
+ private
142
+
143
+ def log_query_plan
144
+ plan = configured_query.explain
145
+ Rails.logger.debug("Query Plan for #{self.class.name}:")
146
+ Rails.logger.debug(plan)
147
+
148
+ if plan.include?("Seq Scan") && configured_query.count > 1000
149
+ Rails.logger.warn("Sequential scan detected on large table!")
150
+ end
151
+ end
152
+ end
153
+ ```
@@ -2,14 +2,19 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "~> 8.0"
5
+ gem "rails", "~> 8.0.0"
6
6
 
7
7
  group :development, :test do
8
8
  gem "sqlite3"
9
- gem "rake"
10
- gem "minitest"
11
- gem "standard"
12
- gem "steep"
9
+ gem "rake", "~> 13.0"
10
+ gem "minitest", "~> 5.0"
11
+ gem "simplecov", require: false
12
+ gem "standard", require: false
13
+ gem "steep", require: false
14
+ gem "rbs-inline", "~> 0.11.0", require: false
15
+ gem "simplecov-small-badge", require: false
16
+ gem "rubycritic-small-badge", require: false
17
+ gem "repo-small-badge"
13
18
  end
14
19
 
15
20
  gemspec path: "../"
@@ -0,0 +1,20 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 8.1.0"
6
+
7
+ group :development, :test do
8
+ gem "sqlite3"
9
+ gem "rake", "~> 13.0"
10
+ gem "minitest", "~> 5.0"
11
+ gem "simplecov", require: false
12
+ gem "standard", require: false
13
+ gem "steep", require: false
14
+ gem "rbs-inline", "~> 0.11.0", require: false
15
+ gem "simplecov-small-badge", require: false
16
+ gem "rubycritic-small-badge", require: false
17
+ gem "repo-small-badge"
18
+ end
19
+
20
+ gemspec path: "../"
@@ -0,0 +1,21 @@
1
+ Description:
2
+ Installs the Quo Claude Code skill into the application's
3
+ .claude/skills/quo/ directory so Claude Code picks it up automatically.
4
+
5
+ Optionally appends a "Quo" section to the project's top-level CLAUDE.md
6
+ pointing Claude at the skill.
7
+
8
+ Example:
9
+ bin/rails generate quo:install
10
+
11
+ Copies the skill into .claude/skills/quo/ in the current working tree.
12
+
13
+ bin/rails generate quo:install --force
14
+
15
+ Re-runs the install, overwriting any existing files. Use this after
16
+ upgrading the Quo gem to refresh the skill content.
17
+
18
+ bin/rails generate quo:install --with-claude-md
19
+
20
+ Also appends a short "Quo" section to CLAUDE.md (creating the file if
21
+ it does not exist). Idempotent — safe to re-run.