anima-core 0.2.1 → 1.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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  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 +3 -0
  11. data/app/channels/session_channel.rb +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,226 @@
1
+ # Batch Processing Examples
2
+ # Demonstrates find_each, find_in_batches, in_batches for large datasets
3
+
4
+ # ============================================
5
+ # The Problem: Loading All Records
6
+ # ============================================
7
+
8
+ # BAD - loads ALL records into memory at once
9
+ User.all.each do |user|
10
+ NewsMailer.weekly_digest(user).deliver_later
11
+ end
12
+ # With 1 million users = 1 million User objects in memory!
13
+
14
+ # GOOD - loads in batches of 1000, GC can clean up between batches
15
+ User.find_each do |user|
16
+ NewsMailer.weekly_digest(user).deliver_later
17
+ end
18
+ # Memory stays constant regardless of total count
19
+
20
+ # ============================================
21
+ # find_each - Process Individual Records
22
+ # ============================================
23
+
24
+ # Basic usage - yields one record at a time
25
+ User.find_each do |user|
26
+ user.calculate_monthly_stats
27
+ end
28
+
29
+ # With batch size
30
+ User.find_each(batch_size: 500) do |user|
31
+ # Process in batches of 500
32
+ end
33
+
34
+ # With start/finish - process subset by primary key
35
+ User.find_each(start: 1000, finish: 5000) do |user|
36
+ # Only users with id between 1000 and 5000
37
+ end
38
+
39
+ # With scope
40
+ User.active.find_each do |user|
41
+ # Only active users
42
+ end
43
+
44
+ # Enumerator form (for chaining)
45
+ User.find_each.map(&:email)
46
+ User.find_each.with_index { |user, i| puts "#{i}: #{user.name}" }
47
+
48
+ # ============================================
49
+ # find_in_batches - Process Batches of Records
50
+ # ============================================
51
+
52
+ # Yields arrays of records
53
+ User.find_in_batches(batch_size: 100) do |users|
54
+ # users is Array<User> with up to 100 elements
55
+ ExternalApi.bulk_sync(users.map(&:external_id))
56
+ end
57
+
58
+ # Practical: Bulk API calls
59
+ Product.find_in_batches(batch_size: 50) do |products|
60
+ SearchIndex.bulk_update(products)
61
+ sleep(0.5) # Rate limiting
62
+ end
63
+
64
+ # With eager loading
65
+ User.includes(:profile, :preferences).find_in_batches do |users|
66
+ users.each do |user|
67
+ user.profile # No N+1
68
+ end
69
+ end
70
+
71
+ # ============================================
72
+ # in_batches - Process Batches as Relations
73
+ # ============================================
74
+
75
+ # Yields ActiveRecord::Relation objects
76
+ User.in_batches do |batch|
77
+ # batch is a Relation, not Array
78
+ batch.update_all(newsletter_sent_at: Time.current)
79
+ end
80
+
81
+ # Bulk update pattern
82
+ User.where(legacy: true).in_batches.update_all(migrated: true)
83
+
84
+ # Bulk delete with throttling
85
+ User.where("created_at < ?", 5.years.ago).in_batches do |batch|
86
+ batch.delete_all
87
+ sleep(0.1) # Reduce database load
88
+ end
89
+
90
+ # Bulk operations via relation
91
+ Order.where(status: "pending").in_batches do |batch|
92
+ batch.update_all(status: "cancelled", cancelled_at: Time.current)
93
+ end
94
+
95
+ # With load: true to also get records
96
+ User.in_batches(load: true) do |batch|
97
+ batch.each { |user| user.some_instance_method }
98
+ end
99
+
100
+ # ============================================
101
+ # Comparison: When to Use Which
102
+ # ============================================
103
+
104
+ # find_each - Individual record processing
105
+ # - Sending emails one by one
106
+ # - Complex per-record logic
107
+ # - Instance methods needed
108
+ User.find_each { |u| u.send_notification }
109
+
110
+ # find_in_batches - Batch operations on loaded records
111
+ # - Bulk API calls with arrays
112
+ # - Batch exports
113
+ # - When you need the actual objects in groups
114
+ User.find_in_batches { |users| CsvExporter.export(users) }
115
+
116
+ # in_batches - SQL-level bulk operations
117
+ # - update_all, delete_all
118
+ # - Bulk SQL operations
119
+ # - Maximum efficiency, no Ruby object overhead
120
+ User.in_batches.update_all(processed: true)
121
+
122
+ # ============================================
123
+ # Important Caveats
124
+ # ============================================
125
+
126
+ # ORDERING IS IGNORED
127
+ # Batch processing always orders by primary key
128
+ User.order(:name).find_each { |u| }
129
+ # WARNING: Scoped order is ignored, will use primary key order
130
+
131
+ # RESULTS MAY BE INCONSISTENT
132
+ # If records are added/deleted during iteration, may skip/duplicate
133
+ # Solution: Use in_batches with explicit locking for critical operations
134
+
135
+ # CUSTOM ORDER WITH cursor (Rails 7.1+)
136
+ User.find_each(cursor: [:created_at, :id]) { |u| }
137
+ # Orders by created_at, then id for stability
138
+
139
+ # ============================================
140
+ # Practical Examples
141
+ # ============================================
142
+
143
+ # Example 1: Data migration
144
+ class BackfillUserSettings < ActiveRecord::Migration[7.1]
145
+ def up
146
+ User.in_batches do |batch|
147
+ batch.update_all(settings: { notifications: true }.to_json)
148
+ end
149
+ end
150
+ end
151
+
152
+ # Example 2: Export to CSV
153
+ require "csv"
154
+
155
+ def export_users_to_csv(file_path)
156
+ CSV.open(file_path, "w") do |csv|
157
+ csv << ["ID", "Name", "Email", "Created At"]
158
+
159
+ User.find_each do |user|
160
+ csv << [user.id, user.name, user.email, user.created_at]
161
+ end
162
+ end
163
+ end
164
+
165
+ # Example 3: Background job processing
166
+ class ProcessAllOrdersJob < ApplicationJob
167
+ def perform
168
+ Order.pending.find_each do |order|
169
+ ProcessOrderJob.perform_later(order.id)
170
+ end
171
+ end
172
+ end
173
+
174
+ # Example 4: Batch API sync with rate limiting
175
+ def sync_products_to_external_service
176
+ Product.active.find_in_batches(batch_size: 25) do |products|
177
+ ExternalService.bulk_upsert(
178
+ products.map { |p| p.as_external_format }
179
+ )
180
+ sleep(1) # Respect rate limits
181
+ end
182
+ end
183
+
184
+ # Example 5: Data cleanup with progress tracking
185
+ def cleanup_old_sessions
186
+ total = Session.where("created_at < ?", 30.days.ago).count
187
+ deleted = 0
188
+
189
+ Session.where("created_at < ?", 30.days.ago).in_batches do |batch|
190
+ count = batch.delete_all
191
+ deleted += count
192
+ Rails.logger.info "Deleted #{deleted}/#{total} old sessions"
193
+ sleep(0.05)
194
+ end
195
+ end
196
+
197
+ # Example 6: Memory-efficient aggregation
198
+ def calculate_total_balance
199
+ total = 0
200
+
201
+ Account.active.find_each do |account|
202
+ total += account.calculated_balance # Complex calculation
203
+ end
204
+
205
+ total
206
+ end
207
+
208
+ # Better: Use database when possible
209
+ Account.active.sum(:balance) # Single SQL query
210
+
211
+ # ============================================
212
+ # Batch Processing with Transactions
213
+ # ============================================
214
+
215
+ # Each batch in its own transaction
216
+ User.in_batches do |batch|
217
+ batch.transaction do
218
+ batch.update_all(processed: true)
219
+ AuditLog.create!(action: "batch_processed", count: batch.count)
220
+ end
221
+ end
222
+
223
+ # Whole operation in one transaction (careful with large datasets!)
224
+ User.transaction do
225
+ User.in_batches.update_all(processed: true)
226
+ end
@@ -0,0 +1,259 @@
1
+ # Eager Loading Examples
2
+ # Demonstrates includes, preload, eager_load, joins and N+1 prevention
3
+
4
+ # ============================================
5
+ # Sample Models for Examples
6
+ # ============================================
7
+
8
+ class Author < ApplicationRecord
9
+ has_many :posts
10
+ has_many :comments, through: :posts
11
+ end
12
+
13
+ class Post < ApplicationRecord
14
+ belongs_to :author
15
+ has_many :comments
16
+ has_many :tags, through: :taggings
17
+ has_many :taggings
18
+ end
19
+
20
+ class Comment < ApplicationRecord
21
+ belongs_to :post
22
+ belongs_to :user
23
+ end
24
+
25
+ # ============================================
26
+ # The N+1 Problem
27
+ # ============================================
28
+
29
+ # BAD - N+1 queries (1 query + N additional queries)
30
+ posts = Post.limit(10)
31
+ posts.each do |post|
32
+ puts post.author.name # Each iteration triggers a query!
33
+ end
34
+ # Query 1: SELECT * FROM posts LIMIT 10
35
+ # Query 2: SELECT * FROM authors WHERE id = 1
36
+ # Query 3: SELECT * FROM authors WHERE id = 2
37
+ # ... 10 more queries!
38
+
39
+ # GOOD - Eager loading (2 queries total)
40
+ posts = Post.includes(:author).limit(10)
41
+ posts.each do |post|
42
+ puts post.author.name # No additional queries!
43
+ end
44
+ # Query 1: SELECT * FROM posts LIMIT 10
45
+ # Query 2: SELECT * FROM authors WHERE id IN (1, 2, 3, ...)
46
+
47
+ # ============================================
48
+ # includes - Smart Default (Auto-chooses strategy)
49
+ # ============================================
50
+
51
+ # Separate queries (preload strategy) - when just accessing data
52
+ Post.includes(:author)
53
+ # SELECT * FROM posts
54
+ # SELECT * FROM authors WHERE id IN (1, 2, 3, ...)
55
+
56
+ # Single JOIN (eager_load strategy) - when filtering by association
57
+ Post.includes(:author).where(authors: { verified: true })
58
+ # SELECT posts.*, authors.*
59
+ # FROM posts LEFT OUTER JOIN authors ON authors.id = posts.author_id
60
+ # WHERE authors.verified = true
61
+
62
+ # Multiple associations
63
+ Post.includes(:author, :comments)
64
+ Post.includes(:author, comments: :user) # Nested
65
+
66
+ # ============================================
67
+ # references - Required for String Conditions
68
+ # ============================================
69
+
70
+ # ERROR - Rails doesn't know to JOIN
71
+ Post.includes(:author).where("authors.created_at > ?", 1.week.ago)
72
+ # PG::UndefinedTable: ERROR: missing FROM-clause entry for table "authors"
73
+
74
+ # CORRECT - explicitly reference the table
75
+ Post.includes(:author)
76
+ .where("authors.created_at > ?", 1.week.ago)
77
+ .references(:authors)
78
+ # SELECT posts.*, authors.*
79
+ # FROM posts LEFT OUTER JOIN authors ON ...
80
+ # WHERE authors.created_at > '2024-01-23'
81
+
82
+ # Hash conditions auto-reference
83
+ Post.includes(:author).where(authors: { verified: true }) # Works!
84
+
85
+ # ============================================
86
+ # preload - Always Separate Queries
87
+ # ============================================
88
+
89
+ # Forces separate queries regardless of conditions
90
+ Author.preload(:posts, :comments)
91
+ # SELECT * FROM authors
92
+ # SELECT * FROM posts WHERE author_id IN (1, 2, 3)
93
+ # SELECT * FROM comments WHERE author_id IN (1, 2, 3)
94
+
95
+ # CANNOT filter by preloaded association
96
+ Author.preload(:posts).where(posts: { published: true })
97
+ # ERROR! posts is not in the FROM clause
98
+
99
+ # Use When:
100
+ # - Large datasets (JOINs cause row multiplication)
101
+ # - Not filtering by associated data
102
+ # - Want predictable query behavior
103
+
104
+ # Example: preload is better here
105
+ authors = Author.preload(:posts) # 2 queries
106
+ # vs eager_load with 1000 posts per author = massive result set
107
+
108
+ # ============================================
109
+ # eager_load - Always LEFT OUTER JOIN
110
+ # ============================================
111
+
112
+ # Forces single query with LEFT OUTER JOIN
113
+ Author.eager_load(:posts)
114
+ # SELECT authors.*, posts.*
115
+ # FROM authors
116
+ # LEFT OUTER JOIN posts ON posts.author_id = authors.id
117
+
118
+ # Use When:
119
+ # - Filtering by association attributes
120
+ # - Sorting by association attributes
121
+ # - Need records even without associations (LEFT join includes NULLs)
122
+
123
+ # Example: authors sorted by latest post
124
+ Author.eager_load(:posts)
125
+ .order("posts.created_at DESC NULLS LAST")
126
+ .distinct
127
+
128
+ # ============================================
129
+ # joins - INNER JOIN (Filtering Only)
130
+ # ============================================
131
+
132
+ # Creates JOIN but does NOT load associated records
133
+ Author.joins(:posts).where(posts: { published: true }).distinct
134
+ # SELECT DISTINCT authors.*
135
+ # FROM authors
136
+ # INNER JOIN posts ON posts.author_id = authors.id
137
+ # WHERE posts.published = true
138
+
139
+ # WARNING: Accessing association still causes N+1!
140
+ Author.joins(:posts).each do |author|
141
+ author.posts # N+1 query here!
142
+ end
143
+
144
+ # Use When:
145
+ # - Only need to filter, not access associated data
146
+ # - Combine with includes for filtering + loading
147
+
148
+ # Pattern: joins + includes together
149
+ Author.joins(:posts)
150
+ .includes(:posts)
151
+ .where(posts: { published: true })
152
+ .distinct
153
+
154
+ # ============================================
155
+ # left_outer_joins - Include Records Without Association
156
+ # ============================================
157
+
158
+ # Authors including those without posts
159
+ Author.left_outer_joins(:posts)
160
+ .select("authors.*, COUNT(posts.id) AS posts_count")
161
+ .group("authors.id")
162
+
163
+ # Find authors with no posts
164
+ Author.left_outer_joins(:posts).where(posts: { id: nil })
165
+
166
+ # ============================================
167
+ # Nested Eager Loading
168
+ # ============================================
169
+
170
+ # One level
171
+ Post.includes(:author)
172
+
173
+ # Two levels
174
+ Post.includes(author: :profile)
175
+
176
+ # Multiple at same level
177
+ Post.includes(:author, :comments)
178
+
179
+ # Multiple nested
180
+ Post.includes(author: [:profile, :posts])
181
+
182
+ # Deep nesting
183
+ Post.includes(comments: { user: :profile })
184
+
185
+ # Mixed
186
+ Post.includes(:author, comments: { user: :profile }, tags: :category)
187
+
188
+ # ============================================
189
+ # Eager Loading Decision Tree (Applied)
190
+ # ============================================
191
+
192
+ # Scenario 1: Display posts with author names
193
+ # Need: Access author → Use includes
194
+ posts = Post.includes(:author).limit(20)
195
+ posts.each { |p| "#{p.title} by #{p.author.name}" }
196
+
197
+ # Scenario 2: Find posts by verified authors only
198
+ # Need: Filter by association → Use eager_load or includes with references
199
+ posts = Post.eager_load(:author).where(authors: { verified: true })
200
+
201
+ # Scenario 3: Count posts per author
202
+ # Need: Filter only, no data access → Use joins
203
+ Post.joins(:author)
204
+ .group("authors.id")
205
+ .count
206
+
207
+ # Scenario 4: Load authors with their many posts (large dataset)
208
+ # Need: Access data, large join explosion risk → Use preload
209
+ authors = Author.preload(:posts).limit(10)
210
+
211
+ # Scenario 5: Load posts with multiple associations
212
+ # Need: Mixed access patterns
213
+ Post.includes(:author) # Always accessed
214
+ .includes(:comments) # Sometimes accessed
215
+ .preload(:tags) # Large count, rarely accessed
216
+
217
+ # ============================================
218
+ # Performance Comparison
219
+ # ============================================
220
+
221
+ # Setup: 100 posts, each with 1 author and 50 comments
222
+
223
+ # N+1 (worst)
224
+ Post.all.each { |p| p.author; p.comments.to_a }
225
+ # 1 + 100 + 100 = 201 queries
226
+
227
+ # includes (better)
228
+ Post.includes(:author, :comments).each { |p| p.author; p.comments.to_a }
229
+ # 3 queries (posts, authors, comments)
230
+
231
+ # eager_load with many comments (can be slow)
232
+ Post.eager_load(:author, :comments).each { |p| p.author; p.comments.to_a }
233
+ # 1 query, but returns 100 * 50 = 5000 rows!
234
+
235
+ # preload is safer for large associations
236
+ Post.preload(:author, :comments).each { |p| p.author; p.comments.to_a }
237
+ # 3 queries, 100 + 100 + 5000 rows loaded separately
238
+
239
+ # ============================================
240
+ # Strict Loading - Prevent N+1 at Runtime
241
+ # ============================================
242
+
243
+ # Relation level - raises if lazy loading attempted
244
+ posts = Post.strict_loading
245
+ posts.first.author # ActiveRecord::StrictLoadingViolationError!
246
+
247
+ # Must eager load to access
248
+ posts = Post.strict_loading.includes(:author)
249
+ posts.first.author # Works!
250
+
251
+ # Model level - all queries are strict
252
+ class Post < ApplicationRecord
253
+ self.strict_loading_by_default = true
254
+ end
255
+
256
+ # Association level
257
+ class Author < ApplicationRecord
258
+ has_many :posts, strict_loading: true
259
+ end
@@ -0,0 +1,170 @@
1
+ # Finder Methods Examples
2
+ # Demonstrates find, find_by, where, and find_or_create_by
3
+
4
+ # ============================================
5
+ # Sample Models for Examples
6
+ # ============================================
7
+
8
+ class User < ApplicationRecord
9
+ has_many :posts
10
+ has_many :orders
11
+
12
+ scope :active, -> { where(active: true) }
13
+ scope :admins, -> { where(role: "admin") }
14
+ end
15
+
16
+ # ============================================
17
+ # find - Retrieve by Primary Key
18
+ # ============================================
19
+
20
+ # Single record - raises RecordNotFound if not found
21
+ user = User.find(1)
22
+ # SELECT * FROM users WHERE id = 1
23
+
24
+ # Multiple records - returns array, raises if ANY not found
25
+ users = User.find([1, 2, 3])
26
+ users = User.find(1, 2, 3) # Same result
27
+ # SELECT * FROM users WHERE id IN (1, 2, 3)
28
+
29
+ # Use when record MUST exist (will crash otherwise)
30
+ def show
31
+ @user = User.find(params[:id]) # 404 if not found
32
+ end
33
+
34
+ # ============================================
35
+ # find_by - Retrieve First Match
36
+ # ============================================
37
+
38
+ # Returns nil if not found
39
+ user = User.find_by(email: "test@example.com")
40
+ # SELECT * FROM users WHERE email = 'test@example.com' LIMIT 1
41
+
42
+ # Multiple conditions
43
+ user = User.find_by(email: "test@example.com", active: true)
44
+
45
+ # With string conditions (use placeholders!)
46
+ user = User.find_by("email LIKE ?", "%@company.com")
47
+
48
+ # find_by! raises RecordNotFound
49
+ user = User.find_by!(email: "test@example.com")
50
+
51
+ # Use when absence is acceptable
52
+ def authenticate(email, password)
53
+ user = User.find_by(email:)
54
+ return nil unless user
55
+ user if user.authenticate(password)
56
+ end
57
+
58
+ # ============================================
59
+ # where - Build Conditions (Returns Relation)
60
+ # ============================================
61
+
62
+ # Hash conditions - safest, auto-escaped
63
+ User.where(active: true)
64
+ User.where(role: ["admin", "moderator"]) # IN clause
65
+ User.where(age: 18..65) # BETWEEN
66
+ User.where(deleted_at: nil) # IS NULL
67
+
68
+ # Chaining - lazy evaluation
69
+ users = User.where(active: true)
70
+ .where(role: "admin")
71
+ .order(created_at: :desc)
72
+ # Query not executed until iteration
73
+
74
+ # String conditions - ALWAYS use placeholders
75
+ User.where("age > ?", 18)
76
+ User.where("name LIKE ?", "%#{User.sanitize_sql_like(query)}%")
77
+ User.where("created_at > :date", date: 1.week.ago)
78
+
79
+ # DANGER - SQL injection!
80
+ # User.where("name = '#{params[:name]}'") # NEVER do this!
81
+
82
+ # ============================================
83
+ # where.not - Negation
84
+ # ============================================
85
+
86
+ User.where.not(role: "admin")
87
+ # WHERE role != 'admin'
88
+
89
+ User.where.not(deleted_at: nil)
90
+ # WHERE deleted_at IS NOT NULL
91
+
92
+ User.where.not(status: ["banned", "suspended"])
93
+ # WHERE status NOT IN ('banned', 'suspended')
94
+
95
+ # ============================================
96
+ # where.associated / where.missing (Rails 7+)
97
+ # ============================================
98
+
99
+ # Users who have at least one post
100
+ User.where.associated(:posts)
101
+ # SELECT users.* FROM users
102
+ # INNER JOIN posts ON posts.user_id = users.id
103
+
104
+ # Users with no posts
105
+ User.where.missing(:posts)
106
+ # SELECT users.* FROM users
107
+ # LEFT OUTER JOIN posts ON posts.user_id = users.id
108
+ # WHERE posts.id IS NULL
109
+
110
+ # ============================================
111
+ # find_or_create_by / find_or_initialize_by
112
+ # ============================================
113
+
114
+ # Finds existing or creates new record
115
+ user = User.find_or_create_by(email: "new@example.com") do |u|
116
+ u.name = "New User" # Only set for new records
117
+ u.role = "member"
118
+ end
119
+
120
+ # Finds or builds (doesn't save)
121
+ user = User.find_or_initialize_by(email: "maybe@example.com")
122
+ user.new_record? # true if not found, false if found
123
+ user.save if user.new_record?
124
+
125
+ # With scoped relation
126
+ admin = User.admins.find_or_create_by(email: "admin@example.com")
127
+
128
+ # ============================================
129
+ # Handling Race Conditions
130
+ # ============================================
131
+
132
+ # find_or_create_by can fail under concurrent access
133
+ # when two requests try to create the same unique record
134
+
135
+ # Solution 1: Rescue and retry
136
+ def find_or_create_user(email)
137
+ User.find_or_create_by(email:)
138
+ rescue ActiveRecord::RecordNotUnique
139
+ retry
140
+ end
141
+
142
+ # Solution 2: Use create_or_find_by (Rails 6+)
143
+ # Creates first, finds on unique constraint violation
144
+ User.create_or_find_by(email: "user@example.com")
145
+
146
+ # ============================================
147
+ # Practical Examples
148
+ # ============================================
149
+
150
+ # API controller filtering
151
+ class UsersController < ApplicationController
152
+ def index
153
+ @users = User.all
154
+
155
+ @users = @users.where(role: params[:role]) if params[:role].present?
156
+ @users = @users.where(active: true) if params[:active] == "true"
157
+ @users = @users.where("created_at >= ?", params[:since]) if params[:since]
158
+
159
+ @users = @users.order(params[:sort] || :created_at)
160
+ @users = @users.limit(params[:limit] || 20)
161
+ end
162
+ end
163
+
164
+ # Search with sanitization
165
+ def search_users(query)
166
+ return User.none if query.blank?
167
+
168
+ sanitized = User.sanitize_sql_like(query)
169
+ User.where("name ILIKE :q OR email ILIKE :q", q: "%#{sanitized}%")
170
+ end