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,275 @@
1
+ # Query Optimization Examples
2
+ # Demonstrates pluck, calculations, select, and memory-efficient patterns
3
+
4
+ # ============================================
5
+ # pluck vs map - Value Extraction
6
+ # ============================================
7
+
8
+ # BAD - loads full User objects into memory
9
+ emails = User.all.map(&:email)
10
+ # 1. Instantiates every User object
11
+ # 2. Allocates memory for all attributes
12
+ # 3. Then extracts just email
13
+
14
+ # GOOD - only fetches email column from database
15
+ emails = User.pluck(:email)
16
+ # SELECT email FROM users
17
+ # Returns Array of strings directly
18
+
19
+ # Multiple columns - returns array of arrays
20
+ User.pluck(:id, :email)
21
+ # => [[1, "a@example.com"], [2, "b@example.com"]]
22
+
23
+ # With conditions
24
+ User.where(active: true).pluck(:email)
25
+
26
+ # ============================================
27
+ # pluck Caveats
28
+ # ============================================
29
+
30
+ # pluck returns Array, not Relation - cannot chain after
31
+ User.pluck(:email).where(active: true) # NoMethodError!
32
+
33
+ # pluck ignores select
34
+ User.select(:id).pluck(:email) # Still plucks email
35
+
36
+ # pluck with association requires join
37
+ Post.joins(:author).pluck("authors.name")
38
+ # SELECT authors.name FROM posts INNER JOIN authors...
39
+
40
+ # ============================================
41
+ # ids - Shortcut for Primary Keys
42
+ # ============================================
43
+
44
+ # These are equivalent
45
+ User.pluck(:id)
46
+ User.ids
47
+
48
+ # With conditions
49
+ User.active.ids
50
+ # SELECT id FROM users WHERE active = true
51
+
52
+ # ============================================
53
+ # pick - Single Value
54
+ # ============================================
55
+
56
+ # Get first matching value (Rails 6+)
57
+ User.where(admin: true).pick(:email)
58
+ # Equivalent to: User.where(admin: true).limit(1).pluck(:email).first
59
+
60
+ # Multiple columns
61
+ User.pick(:id, :email)
62
+ # => [1, "admin@example.com"]
63
+
64
+ # ============================================
65
+ # Calculations - Database-Level Math
66
+ # ============================================
67
+
68
+ # count
69
+ User.count # COUNT(*)
70
+ User.count(:email) # COUNT(email) - excludes NULL
71
+ User.distinct.count(:role) # COUNT(DISTINCT role)
72
+
73
+ # sum, average, minimum, maximum
74
+ Order.sum(:total)
75
+ Order.average(:total)
76
+ User.minimum(:created_at)
77
+ User.maximum(:login_count)
78
+
79
+ # With conditions
80
+ Order.where(status: "completed").sum(:total)
81
+ User.active.average(:age)
82
+
83
+ # ============================================
84
+ # Grouped Calculations
85
+ # ============================================
86
+
87
+ # Group by single column
88
+ Order.group(:status).count
89
+ # => {"pending" => 10, "shipped" => 25, "delivered" => 50}
90
+
91
+ Order.group(:status).sum(:total)
92
+ # => {"pending" => 1000, "shipped" => 5000, "delivered" => 10000}
93
+
94
+ # Group by multiple columns
95
+ User.group(:role, :status).count
96
+ # => {["admin", "active"] => 5, ["user", "active"] => 100, ...}
97
+
98
+ # Group by date
99
+ Order.group("DATE(created_at)").count
100
+ # Daily order counts
101
+
102
+ Order.group("DATE_TRUNC('month', created_at)").sum(:total)
103
+ # Monthly revenue (PostgreSQL)
104
+
105
+ # ============================================
106
+ # select - Limit Loaded Columns
107
+ # ============================================
108
+
109
+ # Only load needed columns - reduces memory
110
+ users = User.select(:id, :name, :email)
111
+
112
+ # WARNING: Accessing non-selected columns raises error
113
+ users.first.password_digest
114
+ # => ActiveModel::MissingAttributeError
115
+
116
+ # Select with alias
117
+ User.select("name, email, created_at AS signup_date")
118
+
119
+ # Select with calculation
120
+ User.select("*, (SELECT COUNT(*) FROM posts WHERE user_id = users.id) AS posts_count")
121
+
122
+ # ============================================
123
+ # exists? vs any? vs count
124
+ # ============================================
125
+
126
+ # exists? - most efficient for checking presence
127
+ User.where(email: "test@example.com").exists?
128
+ # SELECT 1 FROM users WHERE email = '...' LIMIT 1
129
+
130
+ # any? - also efficient, slightly different semantics
131
+ User.active.any?
132
+ # SELECT 1 FROM users WHERE active = true LIMIT 1
133
+
134
+ # count - returns number, less efficient for just checking
135
+ User.active.count > 0 # Less efficient than any?
136
+
137
+ # BAD - loads all records
138
+ User.active.to_a.any? # Loads everything!
139
+ User.active.length > 0 # Also loads everything!
140
+
141
+ # ============================================
142
+ # size vs count vs length
143
+ # ============================================
144
+
145
+ users = User.where(active: true)
146
+
147
+ # count - always hits database
148
+ users.count # SELECT COUNT(*) FROM users WHERE active = true
149
+
150
+ # size - smart: uses count if not loaded, length if loaded
151
+ users.size # COUNT(*) if not loaded
152
+ users.to_a
153
+ users.size # Uses array length (no query)
154
+
155
+ # length - always loads all records first
156
+ users.length # SELECT * FROM users..., then .length on array
157
+
158
+ # Rule: Use size unless you specifically need count or length behavior
159
+
160
+ # ============================================
161
+ # Anti-Patterns and Fixes
162
+ # ============================================
163
+
164
+ # Anti-pattern 1: Ruby filtering instead of SQL
165
+ # BAD
166
+ User.all.select { |u| u.active? }
167
+ # GOOD
168
+ User.where(active: true)
169
+
170
+ # Anti-pattern 2: map instead of pluck
171
+ # BAD
172
+ User.all.map(&:email)
173
+ # GOOD
174
+ User.pluck(:email)
175
+
176
+ # Anti-pattern 3: each with update
177
+ # BAD - N queries
178
+ User.where(old: true).each { |u| u.update(archived: true) }
179
+ # GOOD - 1 query
180
+ User.where(old: true).update_all(archived: true)
181
+
182
+ # Anti-pattern 4: Loading for count
183
+ # BAD
184
+ User.all.length
185
+ User.all.size # If records not yet loaded, ok; if force loading, bad
186
+ # GOOD
187
+ User.count
188
+
189
+ # Anti-pattern 5: exists check with count
190
+ # BAD
191
+ User.where(email: "test@example.com").count > 0
192
+ # GOOD
193
+ User.where(email: "test@example.com").exists?
194
+
195
+ # ============================================
196
+ # Practical Optimization Examples
197
+ # ============================================
198
+
199
+ # Example 1: Dashboard statistics (efficient)
200
+ def dashboard_stats
201
+ {
202
+ total_users: User.count,
203
+ active_users: User.active.count,
204
+ new_today: User.where("created_at >= ?", Date.current).count,
205
+ orders_by_status: Order.group(:status).count,
206
+ monthly_revenue: Order.completed.this_month.sum(:total)
207
+ }
208
+ end
209
+
210
+ # Example 2: Dropdown options (efficient)
211
+ def category_options
212
+ # Instead of Category.all.map { |c| [c.name, c.id] }
213
+ Category.order(:name).pluck(:name, :id)
214
+ end
215
+
216
+ # Example 3: Bulk presence check
217
+ def users_exist?(emails)
218
+ existing = User.where(email: emails).pluck(:email)
219
+ emails.map { |e| existing.include?(e) }
220
+ end
221
+
222
+ # Example 4: Memory-efficient iteration with select
223
+ User.select(:id, :email).find_each do |user|
224
+ # Only id and email loaded, saves memory
225
+ SomeService.process(user.id, user.email)
226
+ end
227
+
228
+ # Example 5: Conditional eager loading
229
+ def load_posts(include_comments:)
230
+ posts = Post.includes(:author)
231
+ posts = posts.includes(:comments) if include_comments
232
+ posts
233
+ end
234
+
235
+ # ============================================
236
+ # EXPLAIN for Query Analysis
237
+ # ============================================
238
+
239
+ # See query execution plan
240
+ User.where(email: "test@example.com").explain
241
+ # EXPLAIN SELECT * FROM users WHERE email = '...'
242
+
243
+ # PostgreSQL specific
244
+ User.where(email: "test@example.com").explain(:analyze)
245
+ # EXPLAIN ANALYZE SELECT * FROM users WHERE email = '...'
246
+
247
+ # Check if index is used
248
+ # Look for "Index Scan" vs "Seq Scan" in output
249
+
250
+ # ============================================
251
+ # Raw SQL When Needed
252
+ # ============================================
253
+
254
+ # Complex queries with find_by_sql
255
+ User.find_by_sql([
256
+ "SELECT users.*, COUNT(posts.id) as post_count
257
+ FROM users
258
+ LEFT JOIN posts ON posts.user_id = users.id
259
+ WHERE users.active = ?
260
+ GROUP BY users.id
261
+ HAVING COUNT(posts.id) > ?",
262
+ true, 5
263
+ ])
264
+
265
+ # Pure SQL execution
266
+ result = ActiveRecord::Base.connection.execute(
267
+ "SELECT DATE(created_at), COUNT(*) FROM users GROUP BY 1"
268
+ )
269
+ result.to_a # Array of hashes
270
+
271
+ # Using Arel for complex conditions
272
+ users = User.arel_table
273
+ User.where(
274
+ users[:age].gt(18).and(users[:age].lt(65))
275
+ )
@@ -0,0 +1,260 @@
1
+ # Scopes Examples
2
+ # Demonstrates named scopes, scope vs class method, default_scope issues
3
+
4
+ # ============================================
5
+ # Named Scopes - Basic Usage
6
+ # ============================================
7
+
8
+ class Article < ApplicationRecord
9
+ belongs_to :author
10
+ has_many :comments
11
+
12
+ # Simple scope
13
+ scope :published, -> { where(published: true) }
14
+ scope :draft, -> { where(published: false) }
15
+
16
+ # Ordering scope
17
+ scope :recent, -> { order(created_at: :desc) }
18
+ scope :oldest, -> { order(created_at: :asc) }
19
+
20
+ # Scope with argument
21
+ scope :by_author, ->(author) { where(author:) }
22
+ scope :created_after, ->(date) { where("created_at > ?", date) }
23
+ scope :created_between, ->(start_date, end_date) { where(created_at: start_date..end_date) }
24
+
25
+ # Scope with optional argument
26
+ scope :tagged, ->(tag = nil) {
27
+ tag ? joins(:tags).where(tags: { name: tag }) : all
28
+ }
29
+
30
+ # Complex scope
31
+ scope :featured, -> {
32
+ published
33
+ .where(featured: true)
34
+ .where("published_at > ?", 1.week.ago)
35
+ }
36
+ end
37
+
38
+ # Usage - scopes are chainable
39
+ Article.published.recent
40
+ Article.published.by_author(current_user).recent
41
+ Article.draft.created_after(1.month.ago)
42
+
43
+ # ============================================
44
+ # Scope Chaining
45
+ # ============================================
46
+
47
+ # All scopes return Relations - fully chainable
48
+ articles = Article.published
49
+ .recent
50
+ .by_author(current_user)
51
+ .limit(10)
52
+
53
+ # Scopes work with associations
54
+ author.articles.published.recent
55
+
56
+ # Scopes work with finder methods
57
+ Article.published.find_by(slug: "hello-world")
58
+ Article.recent.first
59
+ Article.published.count
60
+
61
+ # ============================================
62
+ # Scope vs Class Method
63
+ # ============================================
64
+
65
+ class Post < ApplicationRecord
66
+ # SCOPE - always returns Relation
67
+ scope :active, -> {
68
+ if Feature.enabled?(:soft_delete)
69
+ where(deleted_at: nil)
70
+ end
71
+ # Returns nil if condition false, but Rails converts to .all
72
+ }
73
+
74
+ # CLASS METHOD - can return nil (breaks chaining!)
75
+ def self.active_method
76
+ if Feature.enabled?(:soft_delete)
77
+ where(deleted_at: nil)
78
+ end
79
+ # Returns nil if condition false - breaks chaining!
80
+ end
81
+ end
82
+
83
+ # Scope handles nil gracefully
84
+ Post.active.recent # Works even if condition returns nil
85
+
86
+ # Class method breaks
87
+ Post.active_method.recent # NoMethodError if active_method returns nil!
88
+
89
+ # CLASS METHOD is better when:
90
+ # - Complex logic with early returns
91
+ # - Need to accept block
92
+ # - Want explicit control over return value
93
+
94
+ # Good class method example
95
+ class Post < ApplicationRecord
96
+ def self.search(query)
97
+ return none if query.blank? # Explicit empty result
98
+
99
+ sanitized = sanitize_sql_like(query)
100
+ where("title ILIKE :q OR body ILIKE :q", q: "%#{sanitized}%")
101
+ end
102
+ end
103
+
104
+ # ============================================
105
+ # default_scope - USE WITH EXTREME CAUTION
106
+ # ============================================
107
+
108
+ # Anti-pattern example - DO NOT DO THIS
109
+ class Article < ApplicationRecord
110
+ default_scope { where(published: true) } # Dangerous!
111
+ end
112
+
113
+ # Problem 1: Affects NEW records
114
+ Article.new.published # => true (unexpected default!)
115
+ Article.create(title: "Draft") # published: true automatically!
116
+
117
+ # Problem 2: Hidden filtering everywhere
118
+ Article.all # Only published articles
119
+ Article.count # Only counts published
120
+ Article.find(id) # Fails silently if unpublished!
121
+
122
+ # Problem 3: Affects joins
123
+ Author.joins(:articles) # Only joins published articles
124
+
125
+ # Problem 4: Must remember to unscope
126
+ Article.unscoped.all # All articles
127
+ Article.unscoped { Article.all } # Block form
128
+
129
+ # ============================================
130
+ # Better Alternatives to default_scope
131
+ # ============================================
132
+
133
+ class Article < ApplicationRecord
134
+ # Alternative 1: Explicit scopes
135
+ scope :published, -> { where(published: true) }
136
+ scope :visible, -> { published } # Semantic alias
137
+
138
+ # Alternative 2: Query object
139
+ class PublishedArticles
140
+ def self.call
141
+ Article.where(published: true)
142
+ end
143
+ end
144
+
145
+ # Alternative 3: Explicit default in controller
146
+ def index
147
+ @articles = Article.published
148
+ end
149
+ end
150
+
151
+ # When default_scope IS acceptable (rare):
152
+ # - Ordering only (not filtering)
153
+ # - Truly universal constraint (multi-tenant tenant_id)
154
+
155
+ class Tenant < ApplicationRecord
156
+ # Ordering default_scope is less dangerous
157
+ default_scope { order(:name) }
158
+ end
159
+
160
+ class Document < ApplicationRecord
161
+ # Multi-tenant - MUST always filter
162
+ default_scope { where(tenant_id: Current.tenant_id) }
163
+ end
164
+
165
+ # ============================================
166
+ # Merging Scopes
167
+ # ============================================
168
+
169
+ class Author < ApplicationRecord
170
+ has_many :articles
171
+ scope :active, -> { where(active: true) }
172
+ scope :verified, -> { where(verified: true) }
173
+ end
174
+
175
+ class Article < ApplicationRecord
176
+ belongs_to :author
177
+ scope :published, -> { where(published: true) }
178
+ scope :recent, -> { where("created_at > ?", 1.week.ago) }
179
+ end
180
+
181
+ # Merge scopes from different models
182
+ Article.published
183
+ .joins(:author)
184
+ .merge(Author.active)
185
+ # SELECT articles.* FROM articles
186
+ # INNER JOIN authors ON authors.id = articles.author_id
187
+ # WHERE articles.published = true
188
+ # AND authors.active = true
189
+
190
+ # Merge multiple scopes
191
+ Article.published
192
+ .joins(:author)
193
+ .merge(Author.active.verified)
194
+
195
+ # Merge with association scope
196
+ class Author < ApplicationRecord
197
+ has_many :articles
198
+ has_many :published_articles, -> { published }, class_name: "Article"
199
+ end
200
+
201
+ # ============================================
202
+ # unscoped - Removing Scopes
203
+ # ============================================
204
+
205
+ # Remove all scopes (including default_scope)
206
+ Article.unscoped.all
207
+
208
+ # Block form - only affects block
209
+ Article.unscoped do
210
+ Article.where(id: 1) # No default_scope
211
+ end
212
+
213
+ # unscope - remove specific clauses
214
+ Article.published.recent.unscope(:order)
215
+ # Removes order, keeps where
216
+
217
+ Article.published.where(featured: true).unscope(where: :published)
218
+ # Removes published condition, keeps featured
219
+
220
+ # reorder - replace order
221
+ Article.order(:title).reorder(:created_at)
222
+ # Only ordered by created_at
223
+
224
+ # rewhere - replace where (Rails 7+)
225
+ Article.where(status: "draft").rewhere(status: "published")
226
+ # status = 'published' only
227
+
228
+ # ============================================
229
+ # Practical Scope Patterns
230
+ # ============================================
231
+
232
+ class Order < ApplicationRecord
233
+ # Status scopes
234
+ scope :pending, -> { where(status: "pending") }
235
+ scope :processing, -> { where(status: "processing") }
236
+ scope :completed, -> { where(status: "completed") }
237
+ scope :cancelled, -> { where(status: "cancelled") }
238
+
239
+ # Time-based scopes
240
+ scope :today, -> { where(created_at: Time.current.all_day) }
241
+ scope :this_week, -> { where(created_at: Time.current.all_week) }
242
+ scope :this_month, -> { where(created_at: Time.current.all_month) }
243
+
244
+ # Aggregate scopes
245
+ scope :high_value, -> { where("total > ?", 1000) }
246
+ scope :with_items, -> { joins(:order_items).distinct }
247
+
248
+ # Boolean shortcuts
249
+ scope :paid, -> { where(paid: true) }
250
+ scope :unpaid, -> { where(paid: false) }
251
+
252
+ # Negation pattern
253
+ scope :not_cancelled, -> { where.not(status: "cancelled") }
254
+ scope :active, -> { not_cancelled.where.not(status: "completed") }
255
+ end
256
+
257
+ # Combining scopes for reports
258
+ Order.this_month.completed.sum(:total)
259
+ Order.today.pending.count
260
+ Order.this_week.high_value.paid.includes(:customer)