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.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +19 -0
- data/README.md +213 -43
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -6
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +5 -40
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +11 -20
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- 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
|