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,215 @@
1
+ # Counter Cache Examples
2
+ # Optimizing association counts
3
+
4
+ # ============================================
5
+ # Basic Counter Cache
6
+ # ============================================
7
+
8
+ class Comment < ApplicationRecord
9
+ belongs_to :post, counter_cache: true
10
+ end
11
+
12
+ class Post < ApplicationRecord
13
+ has_many :comments, dependent: :destroy
14
+
15
+ # Prevent manual updates to counter
16
+ attr_readonly :comments_count
17
+ end
18
+
19
+ # Migration
20
+ # add_column :posts, :comments_count, :integer, default: 0, null: false
21
+
22
+ # ============================================
23
+ # Custom Column Name
24
+ # ============================================
25
+
26
+ class Like < ApplicationRecord
27
+ belongs_to :article, counter_cache: :likes_total
28
+ end
29
+
30
+ class Article < ApplicationRecord
31
+ has_many :likes, dependent: :destroy
32
+ attr_readonly :likes_total
33
+ end
34
+
35
+ # Migration
36
+ # add_column :articles, :likes_total, :integer, default: 0, null: false
37
+
38
+ # ============================================
39
+ # Multiple Counter Caches
40
+ # ============================================
41
+
42
+ class Reply < ApplicationRecord
43
+ belongs_to :topic, counter_cache: true
44
+ belongs_to :forum, counter_cache: true
45
+ end
46
+
47
+ class Topic < ApplicationRecord
48
+ belongs_to :forum
49
+ has_many :replies, dependent: :destroy
50
+ attr_readonly :replies_count
51
+ end
52
+
53
+ class Forum < ApplicationRecord
54
+ has_many :topics, dependent: :destroy
55
+ has_many :replies, dependent: :destroy
56
+ attr_readonly :replies_count
57
+ end
58
+
59
+ # ============================================
60
+ # Backfilling Counter Caches
61
+ # ============================================
62
+
63
+ # Option 1: Simple reset (small tables)
64
+ Post.find_each do |post|
65
+ Post.reset_counters(post.id, :comments)
66
+ end
67
+
68
+ # Option 2: Batch update (large tables, PostgreSQL)
69
+ Post.connection.execute(<<~SQL)
70
+ UPDATE posts
71
+ SET comments_count = (
72
+ SELECT COUNT(*)
73
+ FROM comments
74
+ WHERE comments.post_id = posts.id
75
+ )
76
+ SQL
77
+
78
+ # Option 3: Disable during backfill (prevents incorrect reads)
79
+ class Comment < ApplicationRecord
80
+ belongs_to :post, counter_cache: { active: false }
81
+ end
82
+
83
+ # After backfill complete, change to:
84
+ class Comment < ApplicationRecord
85
+ belongs_to :post, counter_cache: true
86
+ end
87
+
88
+ # ============================================
89
+ # Conditional Counter Cache (counter_culture gem)
90
+ # ============================================
91
+
92
+ # Built-in counter_cache doesn't support conditions
93
+ # Use counter_culture gem for advanced scenarios
94
+
95
+ # gem 'counter_culture'
96
+
97
+ class Review < ApplicationRecord
98
+ belongs_to :product
99
+ counter_culture :product
100
+ counter_culture :product,
101
+ column_name: proc { |r| r.approved? ? "approved_reviews_count" : nil },
102
+ column_names: { ["reviews.approved = ?", true] => "approved_reviews_count" }
103
+ end
104
+
105
+ class Product < ApplicationRecord
106
+ has_many :reviews
107
+ attr_readonly :reviews_count, :approved_reviews_count
108
+ end
109
+
110
+ # Migration
111
+ # add_column :products, :reviews_count, :integer, default: 0, null: false
112
+ # add_column :products, :approved_reviews_count, :integer, default: 0, null: false
113
+
114
+ # ============================================
115
+ # Counter Cache with Polymorphic
116
+ # ============================================
117
+
118
+ class Comment < ApplicationRecord
119
+ belongs_to :commentable, polymorphic: true, counter_cache: true
120
+ end
121
+
122
+ class Post < ApplicationRecord
123
+ has_many :comments, as: :commentable, dependent: :destroy
124
+ attr_readonly :comments_count
125
+ end
126
+
127
+ class Photo < ApplicationRecord
128
+ has_many :comments, as: :commentable, dependent: :destroy
129
+ attr_readonly :comments_count
130
+ end
131
+
132
+ # Both posts and photos need comments_count column
133
+
134
+ # ============================================
135
+ # Using Counter Cache Values
136
+ # ============================================
137
+
138
+ post = Post.find(1)
139
+
140
+ # These use counter cache (no query)
141
+ post.comments.size # Uses comments_count
142
+ post.comments.any? # Uses comments_count
143
+ post.comments.empty? # Uses comments_count
144
+ post.comments.count # Uses comments_count if loaded
145
+
146
+ # These always query
147
+ post.comments.length # Loads all records
148
+ Comment.where(post_id: post.id).count # Direct query
149
+
150
+ # ============================================
151
+ # Performance Comparison
152
+ # ============================================
153
+
154
+ # WITHOUT counter cache - N+1 COUNT queries
155
+ Post.limit(10).each do |post|
156
+ puts "#{post.title}: #{post.comments.count} comments"
157
+ # Executes COUNT(*) for each post
158
+ end
159
+
160
+ # WITH counter cache - no additional queries
161
+ Post.limit(10).each do |post|
162
+ puts "#{post.title}: #{post.comments.size} comments"
163
+ # Uses cached comments_count
164
+ end
165
+
166
+ # ============================================
167
+ # Database Trigger Alternative (High-Write)
168
+ # ============================================
169
+
170
+ # For high-write scenarios, database triggers avoid callback overhead
171
+ # PostgreSQL example:
172
+
173
+ # CREATE OR REPLACE FUNCTION update_comments_count()
174
+ # RETURNS TRIGGER AS $$
175
+ # BEGIN
176
+ # IF TG_OP = 'INSERT' THEN
177
+ # UPDATE posts SET comments_count = comments_count + 1 WHERE id = NEW.post_id;
178
+ # ELSIF TG_OP = 'DELETE' THEN
179
+ # UPDATE posts SET comments_count = comments_count - 1 WHERE id = OLD.post_id;
180
+ # ELSIF TG_OP = 'UPDATE' AND NEW.post_id != OLD.post_id THEN
181
+ # UPDATE posts SET comments_count = comments_count - 1 WHERE id = OLD.post_id;
182
+ # UPDATE posts SET comments_count = comments_count + 1 WHERE id = NEW.post_id;
183
+ # END IF;
184
+ # RETURN NULL;
185
+ # END;
186
+ # $$ LANGUAGE plpgsql;
187
+ #
188
+ # CREATE TRIGGER comments_count_trigger
189
+ # AFTER INSERT OR UPDATE OR DELETE ON comments
190
+ # FOR EACH ROW EXECUTE FUNCTION update_comments_count();
191
+
192
+ # ============================================
193
+ # Common Gotchas
194
+ # ============================================
195
+
196
+ # 1. delete bypasses callbacks (and counter cache)
197
+ Comment.where(post_id: 1).delete_all # Counter NOT updated!
198
+ Comment.where(post_id: 1).destroy_all # Counter updated
199
+
200
+ # 2. Counter gets out of sync over time
201
+ # Run periodic reset job
202
+ class CounterCacheResetJob < ApplicationJob
203
+ def perform
204
+ Post.find_each do |post|
205
+ Post.reset_counters(post.id, :comments)
206
+ end
207
+ end
208
+ end
209
+
210
+ # 3. Race conditions under high concurrency
211
+ # Database triggers or row-level locking may be needed
212
+ Post.transaction do
213
+ post = Post.lock.find(1)
214
+ post.comments.create!(body: "New comment")
215
+ end
@@ -0,0 +1,217 @@
1
+ # Polymorphic Association Examples
2
+ # Single association belonging to multiple model types
3
+
4
+ # ============================================
5
+ # Basic Polymorphic Association
6
+ # ============================================
7
+
8
+ # Picture can belong to Employee OR Product
9
+ #
10
+ # ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
11
+ # │ Employee │ │ Picture │ │ Product │
12
+ # ├──────────────┤ ├──────────────┤ ├──────────────┤
13
+ # │ id │◄──┐ │ id │ ┌──►│ id │
14
+ # │ name │ │ │ imageable_type│ │ │ name │
15
+ # │ │ └───│ imageable_id │───┘ │ │
16
+ # └──────────────┘ └──────────────┘ └──────────────┘
17
+
18
+ class Picture < ApplicationRecord
19
+ belongs_to :imageable, polymorphic: true
20
+ end
21
+
22
+ class Employee < ApplicationRecord
23
+ has_many :pictures, as: :imageable, dependent: :destroy
24
+ end
25
+
26
+ class Product < ApplicationRecord
27
+ has_many :pictures, as: :imageable, dependent: :destroy
28
+ end
29
+
30
+ # Migration
31
+ # create_table :pictures do |t|
32
+ # t.string :name
33
+ # t.belongs_to :imageable, polymorphic: true, index: true
34
+ # t.timestamps
35
+ # end
36
+ # Creates: imageable_type (string), imageable_id (bigint)
37
+
38
+ # ============================================
39
+ # Real-World Example: Comments
40
+ # ============================================
41
+
42
+ class Comment < ApplicationRecord
43
+ belongs_to :commentable, polymorphic: true
44
+ belongs_to :author, class_name: "User"
45
+
46
+ validates :body, presence: true
47
+ end
48
+
49
+ class Post < ApplicationRecord
50
+ has_many :comments, as: :commentable, dependent: :destroy
51
+ end
52
+
53
+ class Photo < ApplicationRecord
54
+ has_many :comments, as: :commentable, dependent: :destroy
55
+ end
56
+
57
+ class Video < ApplicationRecord
58
+ has_many :comments, as: :commentable, dependent: :destroy
59
+ end
60
+
61
+ # Usage
62
+ post = Post.find(1)
63
+ post.comments.create!(body: "Great post!", author: current_user)
64
+
65
+ comment = Comment.last
66
+ comment.commentable # Returns Post, Photo, or Video
67
+ comment.commentable_type # "Post", "Photo", or "Video"
68
+
69
+ # ============================================
70
+ # Real-World Example: Attachments
71
+ # ============================================
72
+
73
+ class Attachment < ApplicationRecord
74
+ belongs_to :attachable, polymorphic: true
75
+
76
+ has_one_attached :file
77
+ validates :file, presence: true
78
+ end
79
+
80
+ class Message < ApplicationRecord
81
+ has_many :attachments, as: :attachable, dependent: :destroy
82
+ end
83
+
84
+ class Task < ApplicationRecord
85
+ has_many :attachments, as: :attachable, dependent: :destroy
86
+ end
87
+
88
+ class Project < ApplicationRecord
89
+ has_many :attachments, as: :attachable, dependent: :destroy
90
+ end
91
+
92
+ # ============================================
93
+ # Real-World Example: Tags (Many-to-Many Polymorphic)
94
+ # ============================================
95
+
96
+ class Tag < ApplicationRecord
97
+ has_many :taggings, dependent: :destroy
98
+
99
+ validates :name, presence: true, uniqueness: true
100
+ end
101
+
102
+ class Tagging < ApplicationRecord
103
+ belongs_to :tag
104
+ belongs_to :taggable, polymorphic: true
105
+
106
+ validates :tag_id, uniqueness: { scope: [:taggable_type, :taggable_id] }
107
+ end
108
+
109
+ class Article < ApplicationRecord
110
+ has_many :taggings, as: :taggable, dependent: :destroy
111
+ has_many :tags, through: :taggings
112
+ end
113
+
114
+ class Question < ApplicationRecord
115
+ has_many :taggings, as: :taggable, dependent: :destroy
116
+ has_many :tags, through: :taggings
117
+ end
118
+
119
+ # Usage
120
+ article = Article.find(1)
121
+ article.tags.create!(name: "ruby")
122
+ article.tags << Tag.find_by(name: "rails")
123
+
124
+ Tag.find_by(name: "ruby").taggings.map(&:taggable) # All tagged items
125
+
126
+ # ============================================
127
+ # STI Compatibility
128
+ # ============================================
129
+
130
+ # When using polymorphic with STI, store base class
131
+
132
+ class Vehicle < ApplicationRecord
133
+ # STI base class
134
+ end
135
+
136
+ class Car < Vehicle
137
+ end
138
+
139
+ class Truck < Vehicle
140
+ end
141
+
142
+ class Insurance < ApplicationRecord
143
+ belongs_to :insurable, polymorphic: true
144
+
145
+ # Store base class for STI compatibility
146
+ def insurable_type=(class_name)
147
+ super(class_name.constantize.base_class.to_s)
148
+ end
149
+ end
150
+
151
+ # insurance.insurable_type stores "Vehicle" not "Car"
152
+
153
+ # ============================================
154
+ # Naming Conventions
155
+ # ============================================
156
+
157
+ # Use -able suffix when association is recipient
158
+ class Picture < ApplicationRecord
159
+ belongs_to :imageable, polymorphic: true # Good - picture receives image action
160
+ end
161
+
162
+ class Comment < ApplicationRecord
163
+ belongs_to :commentable, polymorphic: true # Good - receives comments
164
+ end
165
+
166
+ # Use subject form when acting
167
+ class Article < ApplicationRecord
168
+ belongs_to :author, polymorphic: true # Good - author is acting
169
+ # NOT: belongs_to :authorable, polymorphic: true
170
+ end
171
+
172
+ # ============================================
173
+ # Querying Polymorphic Associations
174
+ # ============================================
175
+
176
+ # Find all pictures for a specific type
177
+ Picture.where(imageable_type: "Employee")
178
+ Picture.where(imageable_type: "Product", imageable_id: 1)
179
+
180
+ # Eager loading - must use includes, NOT joins
181
+ Employee.includes(:pictures).each do |emp|
182
+ emp.pictures.each { |pic| puts pic.name }
183
+ end
184
+
185
+ # Cannot use joins with polymorphic (no FK)
186
+ # Employee.joins(:pictures) # Works
187
+ # Picture.joins(:imageable) # Error! Can't join polymorphic
188
+
189
+ # ============================================
190
+ # Limitations and Alternatives
191
+ # ============================================
192
+
193
+ # Limitations:
194
+ # 1. No database foreign key constraints
195
+ # 2. Cannot join in queries (only includes)
196
+ # 3. Type column stores class names (affects renaming)
197
+ # 4. Performance can suffer at scale
198
+
199
+ # Alternative: Delegated Types (Rails 6.1+)
200
+ # For inheritance hierarchies, consider delegated_type instead
201
+
202
+ class Entry < ApplicationRecord
203
+ delegated_type :entryable, types: %w[Message Comment]
204
+ end
205
+
206
+ class Message < ApplicationRecord
207
+ has_one :entry, as: :entryable, touch: true
208
+ end
209
+
210
+ class Comment < ApplicationRecord
211
+ has_one :entry, as: :entryable, touch: true
212
+ end
213
+
214
+ # Benefits of delegated_type:
215
+ # - Single table for shared attributes
216
+ # - Proper FK constraints possible
217
+ # - Better querying capabilities