anima-core 0.3.0 → 1.0.1

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 (270) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +219 -25
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +4 -1
  11. data/app/channels/session_channel.rb +76 -28
  12. data/app/jobs/agent_request_job.rb +24 -0
  13. data/app/jobs/analytical_brain_job.rb +33 -0
  14. data/app/jobs/count_event_tokens_job.rb +1 -1
  15. data/app/models/concerns/event/broadcasting.rb +20 -2
  16. data/app/models/event.rb +1 -1
  17. data/app/models/goal.rb +91 -0
  18. data/app/models/session.rb +347 -22
  19. data/config/application.rb +2 -0
  20. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  21. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  22. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  23. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  24. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  25. data/db/migrate/20260315140843_create_goals.rb +16 -0
  26. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  27. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  28. data/lib/agent_loop.rb +65 -9
  29. data/lib/agents/definition.rb +116 -0
  30. data/lib/agents/registry.rb +106 -0
  31. data/lib/analytical_brain/runner.rb +276 -0
  32. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  34. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  35. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  36. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  37. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  38. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  39. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  40. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  41. data/lib/analytical_brain.rb +23 -0
  42. data/lib/anima/cli/mcp/secrets.rb +76 -0
  43. data/lib/anima/cli/mcp.rb +197 -0
  44. data/lib/anima/cli.rb +4 -0
  45. data/lib/anima/installer.rb +182 -6
  46. data/lib/anima/settings.rb +226 -0
  47. data/lib/anima/version.rb +1 -1
  48. data/lib/anima.rb +9 -0
  49. data/lib/credential_store.rb +103 -0
  50. data/lib/environment_probe.rb +232 -0
  51. data/lib/llm/client.rb +29 -10
  52. data/lib/mcp/client_manager.rb +86 -0
  53. data/lib/mcp/config.rb +213 -0
  54. data/lib/mcp/health_check.rb +77 -0
  55. data/lib/mcp/secrets.rb +73 -0
  56. data/lib/mcp/stdio_transport.rb +206 -0
  57. data/lib/providers/anthropic.rb +8 -7
  58. data/lib/shell_session.rb +11 -10
  59. data/lib/skills/definition.rb +97 -0
  60. data/lib/skills/registry.rb +105 -0
  61. data/lib/tools/edit.rb +3 -4
  62. data/lib/tools/mcp_tool.rb +114 -0
  63. data/lib/tools/read.rb +15 -16
  64. data/lib/tools/registry.rb +14 -12
  65. data/lib/tools/request_feature.rb +121 -0
  66. data/lib/tools/return_result.rb +81 -0
  67. data/lib/tools/spawn_specialist.rb +109 -0
  68. data/lib/tools/spawn_subagent.rb +111 -0
  69. data/lib/tools/subagent_prompts.rb +12 -0
  70. data/lib/tools/web_get.rb +8 -9
  71. data/lib/tui/app.rb +332 -43
  72. data/lib/tui/message_store.rb +20 -0
  73. data/lib/tui/screens/chat.rb +207 -20
  74. data/lib/workflows/definition.rb +97 -0
  75. data/lib/workflows/registry.rb +89 -0
  76. data/skills/activerecord/SKILL.md +255 -0
  77. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  78. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  79. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  80. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  81. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  82. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  83. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  84. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  85. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  86. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  87. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  88. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  89. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  90. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  91. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  92. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  93. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  94. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  95. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  96. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  97. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  98. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  99. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  100. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  101. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  102. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  103. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  104. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  105. data/skills/activerecord/references/associations.md +709 -0
  106. data/skills/activerecord/references/basics.md +622 -0
  107. data/skills/activerecord/references/callbacks.md +738 -0
  108. data/skills/activerecord/references/migrations.md +657 -0
  109. data/skills/activerecord/references/querying.md +655 -0
  110. data/skills/activerecord/references/validations.md +596 -0
  111. data/skills/dragonruby/SKILL.md +250 -0
  112. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  113. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  114. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  115. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  116. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  117. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  118. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  119. data/skills/dragonruby/examples/core/labels.rb +22 -0
  120. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  121. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  122. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  123. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  124. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  125. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  126. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  127. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  128. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  129. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  130. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  131. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  132. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  133. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  134. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  135. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  136. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  137. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  138. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  139. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  140. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  141. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  142. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  143. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  144. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  145. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  146. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  147. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  148. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  149. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  150. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  151. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  152. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  153. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  154. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  155. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  156. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  157. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  158. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  159. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  160. data/skills/dragonruby/references/audio.md +396 -0
  161. data/skills/dragonruby/references/core.md +385 -0
  162. data/skills/dragonruby/references/distribution.md +434 -0
  163. data/skills/dragonruby/references/entities.md +516 -0
  164. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  165. data/skills/dragonruby/references/game-logic/state.md +389 -0
  166. data/skills/dragonruby/references/input.md +414 -0
  167. data/skills/dragonruby/references/rendering/animation.md +467 -0
  168. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  169. data/skills/dragonruby/references/scenes.md +443 -0
  170. data/skills/draper-decorators/SKILL.md +344 -0
  171. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  172. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  173. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  174. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  175. data/skills/draper-decorators/references/patterns.md +507 -0
  176. data/skills/draper-decorators/references/testing.md +559 -0
  177. data/skills/gh-issue.md +182 -0
  178. data/skills/mcp-server/SKILL.md +177 -0
  179. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  180. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  181. data/skills/mcp-server/examples/http_client.rb +48 -0
  182. data/skills/mcp-server/examples/http_server.rb +97 -0
  183. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  184. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  185. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  186. data/skills/mcp-server/references/gotchas.md +183 -0
  187. data/skills/mcp-server/references/prompts.md +98 -0
  188. data/skills/mcp-server/references/resources.md +53 -0
  189. data/skills/mcp-server/references/server.md +140 -0
  190. data/skills/mcp-server/references/tools.md +146 -0
  191. data/skills/mcp-server/references/transport.md +104 -0
  192. data/skills/ratatui-ruby/SKILL.md +315 -0
  193. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  194. data/skills/ratatui-ruby/references/events.md +387 -0
  195. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  196. data/skills/ratatui-ruby/references/layout.md +423 -0
  197. data/skills/ratatui-ruby/references/styling.md +268 -0
  198. data/skills/ratatui-ruby/references/testing.md +433 -0
  199. data/skills/ratatui-ruby/references/widgets.md +532 -0
  200. data/skills/rspec/SKILL.md +340 -0
  201. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  202. data/skills/rspec/examples/core/configuration.rb +126 -0
  203. data/skills/rspec/examples/core/hooks.rb +126 -0
  204. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  205. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  206. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  207. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  208. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  209. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  210. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  211. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  212. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  213. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  214. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  215. data/skills/rspec/examples/matchers/change.rb +115 -0
  216. data/skills/rspec/examples/matchers/collections.rb +154 -0
  217. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  218. data/skills/rspec/examples/matchers/composing.rb +155 -0
  219. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  220. data/skills/rspec/examples/matchers/equality.rb +58 -0
  221. data/skills/rspec/examples/matchers/errors.rb +136 -0
  222. data/skills/rspec/examples/matchers/output.rb +103 -0
  223. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  224. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  225. data/skills/rspec/examples/matchers/types.rb +82 -0
  226. data/skills/rspec/examples/matchers/yield.rb +147 -0
  227. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  228. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  229. data/skills/rspec/examples/mocks/constants.rb +177 -0
  230. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  231. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  232. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  233. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  234. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  235. data/skills/rspec/examples/mocks/responses.rb +223 -0
  236. data/skills/rspec/examples/mocks/spies.rb +149 -0
  237. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  238. data/skills/rspec/examples/rails/channels.rb +250 -0
  239. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  240. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  241. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  242. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  243. data/skills/rspec/examples/rails/matchers.rb +374 -0
  244. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  245. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  246. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  247. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  248. data/skills/rspec/examples/rails/transactions.rb +254 -0
  249. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  250. data/skills/rspec/references/core.md +816 -0
  251. data/skills/rspec/references/factory_bot.md +641 -0
  252. data/skills/rspec/references/matchers.md +516 -0
  253. data/skills/rspec/references/mocks.md +381 -0
  254. data/skills/rspec/references/rails.md +528 -0
  255. data/templates/soul.md +40 -0
  256. data/workflows/commit.md +45 -0
  257. data/workflows/create_handoff.md +98 -0
  258. data/workflows/create_note.md +82 -0
  259. data/workflows/create_plan.md +457 -0
  260. data/workflows/decompose_ticket.md +109 -0
  261. data/workflows/feature.md +91 -0
  262. data/workflows/implement_plan.md +87 -0
  263. data/workflows/iterate_plan.md +247 -0
  264. data/workflows/research_codebase.md +210 -0
  265. data/workflows/resume_handoff.md +217 -0
  266. data/workflows/review_pr.md +320 -0
  267. data/workflows/thoughts_init.md +71 -0
  268. data/workflows/validate_plan.md +166 -0
  269. metadata +284 -2
  270. data/.mise.toml +0 -2
@@ -0,0 +1,655 @@
1
+ # ActiveRecord Query Interface Reference
2
+
3
+ ## Finder Methods
4
+
5
+ ### find - Retrieve by Primary Key
6
+
7
+ Raises `RecordNotFound` if not found. Use when record must exist.
8
+
9
+ ```ruby
10
+ User.find(1) # Single record
11
+ User.find([1, 2, 3]) # Array of records
12
+ User.find(1, 2, 3) # Same as above
13
+ ```
14
+
15
+ **SQL Generated:**
16
+ ```sql
17
+ SELECT * FROM users WHERE id = 1
18
+ SELECT * FROM users WHERE id IN (1, 2, 3)
19
+ ```
20
+
21
+ ### find_by - Retrieve First Match
22
+
23
+ Returns `nil` if not found. Use when absence is acceptable.
24
+
25
+ ```ruby
26
+ User.find_by(email: "test@example.com")
27
+ User.find_by(email: "test@example.com", active: true)
28
+ User.find_by("email LIKE ?", "%@example.com")
29
+ ```
30
+
31
+ **find_by!** - Raises `RecordNotFound` if not found.
32
+
33
+ ### where - Build Conditions
34
+
35
+ Returns a `Relation` (chainable, lazy). Does NOT execute until needed.
36
+
37
+ ```ruby
38
+ # Hash conditions (safest, auto-escaped)
39
+ User.where(active: true)
40
+ User.where(role: ["admin", "moderator"]) # IN clause
41
+ User.where(age: 18..65) # BETWEEN
42
+ User.where(deleted_at: nil) # IS NULL
43
+
44
+ # String conditions (use placeholders!)
45
+ User.where("age > ?", 18)
46
+ User.where("name LIKE ?", "%#{User.sanitize_sql_like(query)}%")
47
+
48
+ # Named placeholders
49
+ User.where("created_at > :date", date: 1.week.ago)
50
+ ```
51
+
52
+ **where.not - Negation:**
53
+ ```ruby
54
+ User.where.not(role: "admin")
55
+ User.where.not(deleted_at: nil) # IS NOT NULL
56
+ User.where.not(status: ["banned", "suspended"]) # NOT IN
57
+ ```
58
+
59
+ **where.associated / where.missing (Rails 7+):**
60
+ ```ruby
61
+ Post.where.associated(:author) # Has author
62
+ Post.where.missing(:comments) # No comments
63
+ ```
64
+
65
+ ### find_or_create_by / find_or_initialize_by
66
+
67
+ ```ruby
68
+ # Finds or creates (saves to DB)
69
+ User.find_or_create_by(email: "test@example.com") do |user|
70
+ user.name = "New User" # Only for new records
71
+ end
72
+
73
+ # Finds or builds (doesn't save)
74
+ User.find_or_initialize_by(email: "test@example.com")
75
+ ```
76
+
77
+ **Race Condition Warning:** `find_or_create_by` can fail with `RecordNotUnique` under concurrent access. Use database constraints and rescue:
78
+
79
+ ```ruby
80
+ begin
81
+ User.find_or_create_by(email:)
82
+ rescue ActiveRecord::RecordNotUnique
83
+ retry
84
+ end
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Eager Loading
90
+
91
+ ### The N+1 Problem
92
+
93
+ ```ruby
94
+ # BAD - N+1 queries
95
+ posts = Post.limit(10)
96
+ posts.each { |post| puts post.author.name } # 1 + 10 queries!
97
+
98
+ # GOOD - Eager loading
99
+ posts = Post.includes(:author).limit(10)
100
+ posts.each { |post| puts post.author.name } # 2 queries
101
+ ```
102
+
103
+ ### Eager Loading Methods Comparison
104
+
105
+ | Method | Strategy | Queries | Best For |
106
+ |--------|----------|---------|----------|
107
+ | `includes` | Auto-choose | 2+ separate OR 1 JOIN | Default choice |
108
+ | `preload` | Separate queries | Always 2+ | Large datasets, no filtering |
109
+ | `eager_load` | LEFT OUTER JOIN | Always 1 | Filtering/sorting by association |
110
+ | `joins` | INNER JOIN | 1 (no loading) | Filtering only |
111
+
112
+ ### includes - Smart Default
113
+
114
+ Rails decides between `preload` and `eager_load` based on usage.
115
+
116
+ ```ruby
117
+ # Separate queries (preload strategy)
118
+ User.includes(:posts)
119
+ # SELECT * FROM users
120
+ # SELECT * FROM posts WHERE user_id IN (1,2,3,4,5)
121
+
122
+ # Single JOIN (eager_load strategy) - when filtering
123
+ User.includes(:posts).where(posts: { published: true })
124
+ # SELECT users.*, posts.* FROM users
125
+ # LEFT OUTER JOIN posts ON posts.user_id = users.id
126
+ # WHERE posts.published = true
127
+ ```
128
+
129
+ **references Required for String Conditions:**
130
+ ```ruby
131
+ # ERROR - Rails doesn't know to JOIN
132
+ User.includes(:posts).where("posts.created_at > ?", 1.week.ago)
133
+
134
+ # CORRECT - explicitly reference
135
+ User.includes(:posts).where("posts.created_at > ?", 1.week.ago).references(:posts)
136
+ ```
137
+
138
+ ### preload - Always Separate Queries
139
+
140
+ Forces separate queries regardless of conditions. Cannot filter by association.
141
+
142
+ ```ruby
143
+ User.preload(:posts, :comments)
144
+ # SELECT * FROM users
145
+ # SELECT * FROM posts WHERE user_id IN (...)
146
+ # SELECT * FROM comments WHERE user_id IN (...)
147
+ ```
148
+
149
+ **Use When:**
150
+ - Large datasets (JOINs create cartesian explosion)
151
+ - Not filtering by associated data
152
+ - Want predictable query count
153
+
154
+ ### eager_load - Always JOIN
155
+
156
+ Forces single LEFT OUTER JOIN query.
157
+
158
+ ```ruby
159
+ User.eager_load(:posts)
160
+ # SELECT users.*, posts.* FROM users
161
+ # LEFT OUTER JOIN posts ON posts.user_id = users.id
162
+ ```
163
+
164
+ **Use When:**
165
+ - Filtering by association attributes
166
+ - Sorting by association attributes
167
+ - Need to include records without associations (LEFT join)
168
+
169
+ ### joins - Filtering Without Loading
170
+
171
+ Creates INNER JOIN but does NOT load associated records.
172
+
173
+ ```ruby
174
+ User.joins(:posts).where(posts: { published: true }).distinct
175
+ # SELECT DISTINCT users.* FROM users
176
+ # INNER JOIN posts ON posts.user_id = users.id
177
+ # WHERE posts.published = true
178
+
179
+ # Accessing association still causes N+1!
180
+ User.joins(:posts).each { |u| u.posts } # N+1!
181
+ ```
182
+
183
+ **Use When:**
184
+ - Only need to filter, not access associated data
185
+ - Combined with `includes` for filtering + loading
186
+
187
+ ### left_outer_joins - Include Without Association
188
+
189
+ Like `joins` but uses LEFT OUTER JOIN.
190
+
191
+ ```ruby
192
+ User.left_outer_joins(:posts).where(posts: { id: nil })
193
+ # Users without any posts
194
+ ```
195
+
196
+ ### Eager Loading Decision Tree
197
+
198
+ ```
199
+ Need to access associated data?
200
+ ├── NO → Use `joins` (filtering only)
201
+ └── YES → Need to filter/sort by association?
202
+ ├── NO → Use `preload` (separate queries)
203
+ └── YES → Large dataset with many associations?
204
+ ├── YES → Use `includes` with `references`
205
+ └── NO → Use `eager_load` (single JOIN)
206
+ ```
207
+
208
+ ### Nested Eager Loading
209
+
210
+ ```ruby
211
+ User.includes(:posts) # One level
212
+ User.includes(posts: :comments) # Nested
213
+ User.includes(posts: [:comments, :tags]) # Multiple nested
214
+ User.includes(posts: { comments: :author }) # Deep nesting
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Scopes
220
+
221
+ ### Named Scopes
222
+
223
+ ```ruby
224
+ class Article < ApplicationRecord
225
+ scope :published, -> { where(published: true) }
226
+ scope :recent, -> { order(created_at: :desc) }
227
+ scope :by_author, ->(author) { where(author:) }
228
+ scope :created_after, ->(date) { where("created_at > ?", date) }
229
+ end
230
+
231
+ # Usage - chainable
232
+ Article.published.recent.by_author(user)
233
+ ```
234
+
235
+ ### Scope vs Class Method
236
+
237
+ Both are equivalent, but scopes guarantee a Relation return:
238
+
239
+ ```ruby
240
+ # Scope - always returns Relation (even if nil condition)
241
+ scope :active, -> { where(active: true) if some_condition }
242
+ # Returns all records if condition is false
243
+
244
+ # Class method - can return nil
245
+ def self.active
246
+ where(active: true) if some_condition
247
+ end
248
+ # Returns nil if condition is false - breaks chaining!
249
+ ```
250
+
251
+ **Recommendation:** Use scopes for simple queries, class methods for complex logic.
252
+
253
+ ### default_scope - Use With Extreme Caution
254
+
255
+ **Anti-Pattern Warning:** `default_scope` causes many subtle issues:
256
+
257
+ ```ruby
258
+ class Article < ApplicationRecord
259
+ default_scope { where(published: true) }
260
+ end
261
+
262
+ # Problems:
263
+ Article.new.published # => true (affects new records!)
264
+ Article.create # => published: true by default
265
+ Article.all # Always filtered
266
+ Article.unscoped.all # Must remember to unscope
267
+
268
+ # Joins become problematic
269
+ User.joins(:articles) # Silently filters articles
270
+ ```
271
+
272
+ **Better Alternatives:**
273
+
274
+ ```ruby
275
+ # 1. Explicit scope
276
+ scope :published, -> { where(published: true) }
277
+ scope :visible, -> { published } # Semantic alias
278
+
279
+ # 2. Query object
280
+ class PublishedArticles
281
+ def self.call
282
+ Article.where(published: true)
283
+ end
284
+ end
285
+ ```
286
+
287
+ ### Scope Merging
288
+
289
+ ```ruby
290
+ class Author < ApplicationRecord
291
+ has_many :posts
292
+ scope :active, -> { where(active: true) }
293
+ end
294
+
295
+ class Post < ApplicationRecord
296
+ belongs_to :author
297
+ scope :published, -> { where(published: true) }
298
+ end
299
+
300
+ # Merge scopes from different models
301
+ Post.published.joins(:author).merge(Author.active)
302
+ ```
303
+
304
+ ---
305
+
306
+ ## Calculations
307
+
308
+ ### count, sum, average, minimum, maximum
309
+
310
+ ```ruby
311
+ User.count # COUNT(*)
312
+ User.count(:age) # COUNT(age) - excludes NULL
313
+ User.distinct.count(:role) # COUNT(DISTINCT role)
314
+
315
+ User.sum(:balance) # SUM(balance)
316
+ User.average(:age) # AVG(age)
317
+ User.minimum(:created_at) # MIN(created_at)
318
+ User.maximum(:score) # MAX(score)
319
+ ```
320
+
321
+ ### Grouped Calculations
322
+
323
+ ```ruby
324
+ Order.group(:status).count
325
+ # => {"pending" => 5, "shipped" => 10, "delivered" => 8}
326
+
327
+ User.group(:role, :status).count
328
+ # => {["admin", "active"] => 2, ["user", "active"] => 50, ...}
329
+ ```
330
+
331
+ ### pluck - Efficient Value Extraction
332
+
333
+ **Use `pluck` instead of `map` for database values:**
334
+
335
+ ```ruby
336
+ # BAD - loads full records into memory
337
+ User.all.map(&:email)
338
+
339
+ # GOOD - only fetches needed columns
340
+ User.pluck(:email)
341
+ # SELECT email FROM users
342
+ # => ["a@example.com", "b@example.com", ...]
343
+
344
+ # Multiple columns
345
+ User.pluck(:id, :email)
346
+ # => [[1, "a@example.com"], [2, "b@example.com"]]
347
+ ```
348
+
349
+ **Key Points:**
350
+ - Returns Array, not Relation (not chainable after)
351
+ - Ignores any `.select()` - uses only pluck columns
352
+ - Type-casts values appropriately
353
+
354
+ ### ids - Shortcut for Primary Keys
355
+
356
+ ```ruby
357
+ User.ids # Equivalent to User.pluck(:id)
358
+ User.where(active: true).ids
359
+ ```
360
+
361
+ ### pick - Single Value
362
+
363
+ ```ruby
364
+ User.where(id: 1).pick(:name) # First value only
365
+ # Equivalent to: User.where(id: 1).limit(1).pluck(:name).first
366
+ ```
367
+
368
+ ---
369
+
370
+ ## Batch Processing
371
+
372
+ ### When to Use Batch Processing
373
+
374
+ ```ruby
375
+ # BAD - loads all records into memory
376
+ User.all.each { |user| user.some_operation }
377
+
378
+ # GOOD - processes in batches
379
+ User.find_each { |user| user.some_operation }
380
+ ```
381
+
382
+ ### find_each - Individual Records
383
+
384
+ Yields one record at a time, loads in batches of 1000.
385
+
386
+ ```ruby
387
+ User.find_each do |user|
388
+ NewsMailer.weekly_digest(user).deliver_later
389
+ end
390
+
391
+ # With options
392
+ User.find_each(batch_size: 500, start: 1000, finish: 5000) do |user|
393
+ # Process users with id 1000-5000 in batches of 500
394
+ end
395
+ ```
396
+
397
+ ### find_in_batches - Batches of Records
398
+
399
+ Yields arrays of records.
400
+
401
+ ```ruby
402
+ User.find_in_batches(batch_size: 100) do |users|
403
+ # users is an Array of 100 User objects
404
+ ExternalApi.bulk_sync(users)
405
+ end
406
+ ```
407
+
408
+ ### in_batches - Batches as Relations
409
+
410
+ Yields `ActiveRecord::Relation` objects. Best for bulk operations.
411
+
412
+ ```ruby
413
+ # Bulk update
414
+ User.where(status: "inactive").in_batches.update_all(archived: true)
415
+
416
+ # Bulk delete with throttling
417
+ User.where("created_at < ?", 1.year.ago).in_batches do |batch|
418
+ batch.delete_all
419
+ sleep(0.1) # Throttle to reduce DB load
420
+ end
421
+ ```
422
+
423
+ ### Batch Processing Comparison
424
+
425
+ | Method | Yields | Returns | Best For |
426
+ |--------|--------|---------|----------|
427
+ | `find_each` | Single record | nil | Individual processing |
428
+ | `find_in_batches` | Array of records | nil | Batch operations on loaded records |
429
+ | `in_batches` | Relation | BatchEnumerator | Bulk SQL operations |
430
+
431
+ ### Batch Processing Caveats
432
+
433
+ **Ordering is Ignored:**
434
+ ```ruby
435
+ User.order(:name).find_each { |u| }
436
+ # WARNING: Scoped order is ignored
437
+ # Always ordered by primary key
438
+ ```
439
+
440
+ **Cursor Column (Rails 7.1+):**
441
+ ```ruby
442
+ # Custom cursor column
443
+ User.find_each(cursor: [:created_at, :id]) { |u| }
444
+ ```
445
+
446
+ **Race Conditions:**
447
+ Batch processing is subject to race conditions if records are modified during iteration.
448
+
449
+ ---
450
+
451
+ ## Query Optimization
452
+
453
+ ### select - Limit Columns
454
+
455
+ ```ruby
456
+ # Only fetch needed columns
457
+ User.select(:id, :name, :email)
458
+
459
+ # Warning: accessing non-selected columns raises error
460
+ User.select(:id).first.email
461
+ # => ActiveModel::MissingAttributeError
462
+ ```
463
+
464
+ ### distinct - Remove Duplicates
465
+
466
+ ```ruby
467
+ User.joins(:posts).distinct
468
+ # SELECT DISTINCT users.* FROM users INNER JOIN posts...
469
+ ```
470
+
471
+ ### limit and offset
472
+
473
+ ```ruby
474
+ User.limit(10) # First 10
475
+ User.limit(10).offset(20) # Records 21-30
476
+ ```
477
+
478
+ ### order
479
+
480
+ ```ruby
481
+ User.order(:created_at) # ASC by default
482
+ User.order(created_at: :desc)
483
+ User.order(:role, created_at: :desc) # Multiple
484
+
485
+ # Prevent SQL injection - use symbols or Arel
486
+ User.order(Arel.sql("FIELD(status, 'active', 'pending', 'inactive')"))
487
+ ```
488
+
489
+ ### reorder - Replace Existing Order
490
+
491
+ ```ruby
492
+ User.order(:name).reorder(:created_at) # Only ordered by created_at
493
+ ```
494
+
495
+ ### unscope - Remove Specific Clauses
496
+
497
+ ```ruby
498
+ User.where(active: true).order(:name).unscope(:order)
499
+ User.where(active: true).unscope(where: :active)
500
+ ```
501
+
502
+ ---
503
+
504
+ ## exists?, any?, none?, one?, many?
505
+
506
+ ```ruby
507
+ User.exists?(1) # By ID
508
+ User.exists?(email: "test@example.com") # By conditions
509
+ User.where(active: true).exists? # From relation
510
+
511
+ # Comparison
512
+ User.any? # true if count > 0
513
+ User.none? # true if count == 0
514
+ User.one? # true if count == 1
515
+ User.many? # true if count > 1
516
+ ```
517
+
518
+ **Performance:** `exists?` is optimized - uses `SELECT 1 ... LIMIT 1`.
519
+
520
+ ---
521
+
522
+ ## strict_loading - Prevent N+1
523
+
524
+ ```ruby
525
+ # Relation level
526
+ User.strict_loading.first.posts
527
+ # => ActiveRecord::StrictLoadingViolationError
528
+
529
+ # Model level
530
+ class User < ApplicationRecord
531
+ self.strict_loading_by_default = true
532
+ end
533
+
534
+ # Association level
535
+ has_many :posts, strict_loading: true
536
+ ```
537
+
538
+ ---
539
+
540
+ ## Anti-Patterns
541
+
542
+ ### 1. N+1 Queries
543
+
544
+ ```ruby
545
+ # BAD
546
+ Post.all.each { |p| p.author.name }
547
+
548
+ # GOOD
549
+ Post.includes(:author).each { |p| p.author.name }
550
+ ```
551
+
552
+ ### 2. Loading All Records for Count
553
+
554
+ ```ruby
555
+ # BAD - loads all records
556
+ User.all.length
557
+ User.all.size # Also loads if not already loaded
558
+
559
+ # GOOD - SQL COUNT
560
+ User.count
561
+ ```
562
+
563
+ ### 3. Using select{} Instead of where
564
+
565
+ ```ruby
566
+ # BAD - loads all, filters in Ruby
567
+ User.all.select { |u| u.active? }
568
+
569
+ # GOOD - filters in database
570
+ User.where(active: true)
571
+ ```
572
+
573
+ ### 4. map Instead of pluck
574
+
575
+ ```ruby
576
+ # BAD - instantiates all User objects
577
+ User.all.map(&:email)
578
+
579
+ # GOOD - only fetches email column
580
+ User.pluck(:email)
581
+ ```
582
+
583
+ ### 5. each for Bulk Operations
584
+
585
+ ```ruby
586
+ # BAD - N queries
587
+ User.where(old: true).each { |u| u.update(archived: true) }
588
+
589
+ # GOOD - single query
590
+ User.where(old: true).update_all(archived: true)
591
+ ```
592
+
593
+ ### 6. Not Using Indexes
594
+
595
+ ```ruby
596
+ # If you query by email often, add index:
597
+ add_index :users, :email
598
+
599
+ # Composite queries need composite indexes:
600
+ add_index :orders, [:user_id, :status]
601
+ ```
602
+
603
+ ### 7. default_scope
604
+
605
+ See Scopes section above. Almost always an anti-pattern.
606
+
607
+ ### 8. Ignoring Query Cache
608
+
609
+ ActiveRecord caches identical queries within a request. Don't defeat it:
610
+
611
+ ```ruby
612
+ # BAD - 2 queries
613
+ User.find(1)
614
+ User.find(1) # Cache miss if called in different code paths
615
+
616
+ # Be aware of skip_query_cache! in batch processing
617
+ ```
618
+
619
+ ---
620
+
621
+ ## Raw SQL When Needed
622
+
623
+ ### Using Arel
624
+
625
+ ```ruby
626
+ users = User.arel_table
627
+ User.where(users[:age].gt(18).and(users[:age].lt(65)))
628
+ ```
629
+
630
+ ### find_by_sql
631
+
632
+ ```ruby
633
+ User.find_by_sql(["SELECT * FROM users WHERE age > ?", 18])
634
+ ```
635
+
636
+ ### execute for Non-AR Queries
637
+
638
+ ```ruby
639
+ ActiveRecord::Base.connection.execute("SELECT 1")
640
+ ```
641
+
642
+ ### EXPLAIN for Analysis
643
+
644
+ ```ruby
645
+ User.where(active: true).explain
646
+ # EXPLAIN SELECT * FROM users WHERE active = true
647
+ ```
648
+
649
+ ---
650
+
651
+ ## See Also
652
+
653
+ - `examples/querying/` - Working code examples
654
+ - `references/associations.md` - Eager loading with associations
655
+ - `references/migrations.md` - Index strategies