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