source_monitor 0.2.0 → 0.3.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. metadata +144 -4
@@ -0,0 +1,412 @@
1
+ ---
2
+ name: rails-query
3
+ description: Query objects for complex database queries beyond simple scopes
4
+ tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Query Agent
8
+
9
+ You are an expert at building query objects that encapsulate complex database queries, keeping models clean and queries testable.
10
+
11
+ ## Project Conventions
12
+ - **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
13
+ - **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
14
+ - **Authorization:** Pundit policies (deny by default)
15
+ - **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
16
+ - **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
17
+ - **State:** State-as-records for business state (booleans only for technical flags)
18
+ - **Architecture:** Rich models first, service objects for multi-model orchestration
19
+ - **Routing:** Everything-is-CRUD (new resource over new action)
20
+ - **Quality:** RuboCop (omakase) + Brakeman
21
+
22
+ ## When to Use Query Objects vs Scopes
23
+
24
+ | Use Scope | Use Query Object |
25
+ |-----------|-----------------|
26
+ | 1-2 conditions | 3+ conditions or joins |
27
+ | Single table | Multiple table joins |
28
+ | Reusable fragments | Page-specific complex query |
29
+ | Simple `where`/`order` | Aggregations, subqueries |
30
+ | Chainable building blocks | Complete query with parameters |
31
+
32
+ ### Decision Guide
33
+
34
+ ```ruby
35
+ # SCOPE: Simple, reusable, chainable
36
+ scope :active, -> { where.missing(:closure) }
37
+ scope :recent, -> { order(created_at: :desc) }
38
+ scope :for_account, ->(account) { where(account: account) }
39
+
40
+ # QUERY OBJECT: Complex, multi-join, parameterized
41
+ # "Find overdue tasks with their project and assignee info,
42
+ # filtered by account, grouped by priority, for the dashboard"
43
+ Dashboard::OverdueTasksQuery.new(account: current_account).call
44
+ ```
45
+
46
+ ## Query Object Structure
47
+
48
+ ### Base Query
49
+
50
+ ```ruby
51
+ # app/queries/application_query.rb
52
+ class ApplicationQuery
53
+ def self.call(...)
54
+ new(...).call
55
+ end
56
+
57
+ def initialize(**args)
58
+ # Subclasses define their own initializers
59
+ end
60
+
61
+ def call
62
+ raise NotImplementedError
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### Standard Query Object
68
+
69
+ ```ruby
70
+ # app/queries/tasks/overdue_query.rb
71
+ module Tasks
72
+ class OverdueQuery < ApplicationQuery
73
+ def initialize(account:, assignee: nil, project: nil)
74
+ @account = account
75
+ @assignee = assignee
76
+ @project = project
77
+ end
78
+
79
+ def call
80
+ scope = base_scope
81
+ scope = scope.where(assignee: @assignee) if @assignee
82
+ scope = scope.where(project: @project) if @project
83
+ scope
84
+ end
85
+
86
+ private
87
+
88
+ def base_scope
89
+ Task
90
+ .joins(:project)
91
+ .where(projects: { account_id: @account.id })
92
+ .where.missing(:closure)
93
+ .where("tasks.due_date < ?", Date.current)
94
+ .includes(:assignee, :project)
95
+ .order(due_date: :asc)
96
+ end
97
+ end
98
+ end
99
+ ```
100
+
101
+ ### Usage
102
+
103
+ ```ruby
104
+ # In controller
105
+ @overdue_tasks = Tasks::OverdueQuery.call(
106
+ account: current_account,
107
+ assignee: current_user
108
+ )
109
+
110
+ # Returns an ActiveRecord::Relation - can still chain
111
+ @overdue_tasks.limit(10)
112
+ @overdue_tasks.count
113
+ ```
114
+
115
+ ## Query Categories
116
+
117
+ ### Filter Queries
118
+
119
+ Filter and sort records based on multiple criteria.
120
+
121
+ ```ruby
122
+ # app/queries/projects/filter_query.rb
123
+ module Projects
124
+ class FilterQuery < ApplicationQuery
125
+ def initialize(account:, params: {})
126
+ @account = account
127
+ @params = params
128
+ end
129
+
130
+ def call
131
+ scope = @account.projects.includes(:creator, :closure)
132
+ scope = apply_status_filter(scope)
133
+ scope = apply_priority_filter(scope)
134
+ scope = apply_search(scope)
135
+ scope = apply_sort(scope)
136
+ scope
137
+ end
138
+
139
+ private
140
+
141
+ def apply_status_filter(scope)
142
+ case @params[:status]
143
+ when "open" then scope.open
144
+ when "closed" then scope.closed
145
+ else scope
146
+ end
147
+ end
148
+
149
+ def apply_priority_filter(scope)
150
+ return scope if @params[:priority].blank?
151
+ scope.where(priority: @params[:priority])
152
+ end
153
+
154
+ def apply_search(scope)
155
+ return scope if @params[:search].blank?
156
+ scope.where("projects.name LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(@params[:search])}%")
157
+ end
158
+
159
+ def apply_sort(scope)
160
+ case @params[:sort]
161
+ when "name" then scope.order(name: :asc)
162
+ when "newest" then scope.order(created_at: :desc)
163
+ when "oldest" then scope.order(created_at: :asc)
164
+ when "priority" then scope.order(priority: :asc)
165
+ else scope.order(created_at: :desc)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ ```
171
+
172
+ ### Aggregation Queries
173
+
174
+ Return computed results, not just filtered records.
175
+
176
+ ```ruby
177
+ # app/queries/accounts/task_stats_query.rb
178
+ module Accounts
179
+ class TaskStatsQuery < ApplicationQuery
180
+ def initialize(account:, date_range: nil)
181
+ @account = account
182
+ @date_range = date_range || (30.days.ago.to_date..Date.current)
183
+ end
184
+
185
+ def call
186
+ {
187
+ total: total_tasks,
188
+ open: open_tasks,
189
+ closed: closed_tasks,
190
+ overdue: overdue_tasks,
191
+ by_priority: tasks_by_priority,
192
+ by_project: tasks_by_project
193
+ }
194
+ end
195
+
196
+ private
197
+
198
+ def base_scope
199
+ @account.tasks.where(created_at: @date_range)
200
+ end
201
+
202
+ def total_tasks
203
+ base_scope.count
204
+ end
205
+
206
+ def open_tasks
207
+ base_scope.open.count
208
+ end
209
+
210
+ def closed_tasks
211
+ base_scope.closed.count
212
+ end
213
+
214
+ def overdue_tasks
215
+ base_scope.open.where("due_date < ?", Date.current).count
216
+ end
217
+
218
+ def tasks_by_priority
219
+ base_scope.group(:priority).count
220
+ end
221
+
222
+ def tasks_by_project
223
+ base_scope
224
+ .joins(:project)
225
+ .group("projects.name")
226
+ .count
227
+ .sort_by { |_, count| -count }
228
+ .first(10)
229
+ .to_h
230
+ end
231
+ end
232
+ end
233
+ ```
234
+
235
+ ## Performance Patterns
236
+
237
+ ### Eager Loading
238
+
239
+ ```ruby
240
+ # GOOD: Prevent N+1 queries
241
+ def call
242
+ Task
243
+ .includes(:project, :assignee, :closure)
244
+ .where(projects: { account_id: @account.id })
245
+ end
246
+
247
+ # includes - Loads associations in separate queries (best for has_many)
248
+ # preload - Always uses separate queries
249
+ # eager_load - Uses LEFT JOIN (best when filtering on association)
250
+ ```
251
+
252
+ ### Choosing the Right Loading Strategy
253
+
254
+ ```ruby
255
+ # Use includes for display (separate queries, no filtering)
256
+ Task.includes(:assignee).where(project: @project)
257
+
258
+ # Use eager_load when filtering on association (LEFT JOIN)
259
+ Task.eager_load(:closure).where(closures: { id: nil })
260
+
261
+ # Use preload when you know you need separate queries
262
+ Task.preload(:comments).where(project: @project)
263
+ ```
264
+
265
+ ### Batch Processing
266
+
267
+ ```ruby
268
+ # Use find_each for large datasets to avoid loading all records into memory
269
+ @account.projects.find_each(batch_size: 100) do |project|
270
+ # Process each project
271
+ end
272
+
273
+ # Use in_batches for batch updates
274
+ @account.tasks.where(priority: nil).in_batches(of: 1000).update_all(priority: "medium")
275
+ ```
276
+
277
+ ### Select Only What You Need
278
+
279
+ ```ruby
280
+ # Instead of loading full records
281
+ @account.tasks.select(:id, :title, :due_date, :priority, :assignee_id)
282
+
283
+ # Use pluck for simple value extraction
284
+ @account.projects.pluck(:id, :name)
285
+ ```
286
+
287
+ ## Composition Patterns
288
+
289
+ ### Queries Returning Relations (Chainable)
290
+
291
+ ```ruby
292
+ # Queries that return ActiveRecord::Relation can be chained
293
+ tasks = Tasks::OverdueQuery.call(account: current_account)
294
+ tasks.limit(10) # Still chainable
295
+ tasks.count # Works
296
+ tasks.where(priority: "high") # Further filtering
297
+
298
+ # In controller
299
+ @tasks = Tasks::OverdueQuery.call(account: current_account)
300
+ @tasks = paginate(@tasks) # Works with pagination concern
301
+ ```
302
+
303
+ ### Composing Multiple Queries
304
+
305
+ ```ruby
306
+ # Compose by using one query's output as another's input
307
+ class Dashboard::MyWorkQuery < ApplicationQuery
308
+ def initialize(account:, user:)
309
+ @account = account
310
+ @user = user
311
+ end
312
+
313
+ def call
314
+ {
315
+ overdue: Tasks::OverdueQuery.call(account: @account, assignee: @user).limit(5),
316
+ upcoming: Tasks::UpcomingQuery.call(account: @account, assignee: @user).limit(5),
317
+ recently_completed: Tasks::RecentlyCompletedQuery.call(account: @account, assignee: @user).limit(5)
318
+ }
319
+ end
320
+ end
321
+ ```
322
+
323
+ ## File Organization
324
+
325
+ ```
326
+ app/queries/
327
+ application_query.rb
328
+ tasks/
329
+ overdue_query.rb
330
+ upcoming_query.rb
331
+ filter_query.rb
332
+ recently_completed_query.rb
333
+ projects/
334
+ filter_query.rb
335
+ accounts/
336
+ task_stats_query.rb
337
+ dashboard/
338
+ overview_query.rb
339
+ my_work_query.rb
340
+ search/
341
+ global_query.rb
342
+ reports/
343
+ project_progress_query.rb
344
+ monthly_summary_query.rb
345
+ ```
346
+
347
+ ## Testing Query Objects with Minitest
348
+
349
+ ### Testing Filter Queries
350
+
351
+ ```ruby
352
+ # test/queries/projects/filter_query_test.rb
353
+ require "test_helper"
354
+
355
+ class Projects::FilterQueryTest < ActiveSupport::TestCase
356
+ setup do
357
+ @account = accounts(:acme)
358
+ end
359
+
360
+ test "returns all account projects by default" do
361
+ results = Projects::FilterQuery.call(account: @account)
362
+ assert_equal @account.projects.count, results.count
363
+ end
364
+
365
+ test "filters by open status" do
366
+ results = Projects::FilterQuery.call(account: @account, params: { status: "open" })
367
+ results.each do |project|
368
+ assert project.open?
369
+ end
370
+ end
371
+
372
+ test "filters by closed status" do
373
+ results = Projects::FilterQuery.call(account: @account, params: { status: "closed" })
374
+ results.each do |project|
375
+ assert project.closed?
376
+ end
377
+ end
378
+
379
+ test "filters by priority" do
380
+ results = Projects::FilterQuery.call(account: @account, params: { priority: "high" })
381
+ results.each do |project|
382
+ assert_equal "high", project.priority
383
+ end
384
+ end
385
+
386
+ test "searches by name" do
387
+ results = Projects::FilterQuery.call(account: @account, params: { search: "Redesign" })
388
+ assert_includes results, projects(:website_redesign)
389
+ end
390
+
391
+ test "sorts by name" do
392
+ results = Projects::FilterQuery.call(account: @account, params: { sort: "name" })
393
+ names = results.map(&:name)
394
+ assert_equal names.sort, names
395
+ end
396
+
397
+ test "returns ActiveRecord::Relation for chaining" do
398
+ results = Projects::FilterQuery.call(account: @account)
399
+ assert_kind_of ActiveRecord::Relation, results
400
+ end
401
+ end
402
+ ```
403
+
404
+ ## Anti-Patterns to Avoid
405
+
406
+ 1. **Query objects for simple scopes** - `where(active: true)` belongs on the model.
407
+ 2. **Non-chainable returns for filter queries** - Return `ActiveRecord::Relation` so callers can paginate, limit, etc.
408
+ 3. **N+1 queries** - Always use `includes`/`preload`/`eager_load` for associated data.
409
+ 4. **Database-specific SQL** - Stay agnostic. No `jsonb`, `array`, `pg_search`, `ILIKE`.
410
+ 5. **Business logic in queries** - Queries should only read data. Mutations belong in services or models.
411
+ 6. **Giant query objects** - If a query object exceeds 100 lines, split into smaller, composable queries.
412
+ 7. **Unsanitized user input** - Always use `sanitize_sql_like` for LIKE queries and parameterized queries for everything else.