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,622 @@
|
|
|
1
|
+
# ActiveRecord Basics Reference
|
|
2
|
+
|
|
3
|
+
Comprehensive reference for ActiveRecord conventions, CRUD operations, attribute handling, and inheritance patterns.
|
|
4
|
+
|
|
5
|
+
## Naming Conventions
|
|
6
|
+
|
|
7
|
+
ActiveRecord uses "convention over configuration" - follow these patterns and Rails works automatically.
|
|
8
|
+
|
|
9
|
+
### Models and Tables
|
|
10
|
+
|
|
11
|
+
| Ruby Class | Database Table | Notes |
|
|
12
|
+
|------------|----------------|-------|
|
|
13
|
+
| `User` | `users` | Automatic pluralization |
|
|
14
|
+
| `Person` | `people` | Irregular plurals handled |
|
|
15
|
+
| `BookClub` | `book_clubs` | CamelCase → snake_case |
|
|
16
|
+
| `Admin::User` | `admin_users` | Namespace becomes prefix |
|
|
17
|
+
|
|
18
|
+
Override when needed:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Product < ApplicationRecord
|
|
22
|
+
self.table_name = "inventory_items"
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Primary Keys
|
|
27
|
+
|
|
28
|
+
Default: `id` column (bigint for PostgreSQL/MySQL, integer for SQLite).
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class Product < ApplicationRecord
|
|
32
|
+
self.primary_key = "product_id" # Override default
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Foreign Keys
|
|
37
|
+
|
|
38
|
+
Pattern: `singularized_table_name_id`
|
|
39
|
+
|
|
40
|
+
| Association | Column Name | References |
|
|
41
|
+
|-------------|-------------|------------|
|
|
42
|
+
| `belongs_to :user` | `user_id` | `users.id` |
|
|
43
|
+
| `belongs_to :category` | `category_id` | `categories.id` |
|
|
44
|
+
| `belongs_to :admin_user` | `admin_user_id` | `admin_users.id` |
|
|
45
|
+
|
|
46
|
+
Override when needed:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
class Order < ApplicationRecord
|
|
50
|
+
belongs_to :customer, class_name: "User", foreign_key: "purchaser_id"
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Magic Columns
|
|
55
|
+
|
|
56
|
+
These optional columns add automatic functionality:
|
|
57
|
+
|
|
58
|
+
| Column | Type | Behavior |
|
|
59
|
+
|--------|------|----------|
|
|
60
|
+
| `created_at` | datetime | Set once on record creation |
|
|
61
|
+
| `updated_at` | datetime | Updated on creation and every update |
|
|
62
|
+
| `lock_version` | integer | Enables optimistic locking |
|
|
63
|
+
| `type` | string | Single Table Inheritance discriminator |
|
|
64
|
+
|
|
65
|
+
## CRUD Operations
|
|
66
|
+
|
|
67
|
+
### Create
|
|
68
|
+
|
|
69
|
+
**`new` + `save`** - Two-step creation:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
user = User.new
|
|
73
|
+
user.name = "Alice"
|
|
74
|
+
user.email = "alice@example.com"
|
|
75
|
+
user.save # Returns true/false
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**`create`** - One-step creation:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
user = User.create(name: "Alice", email: "alice@example.com")
|
|
82
|
+
# Returns user object (check user.persisted? or user.errors)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Bang methods** - Raise on failure:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
user = User.create!(name: "Alice") # Raises ActiveRecord::RecordInvalid
|
|
89
|
+
user.save! # Raises ActiveRecord::RecordInvalid
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Bulk insert** (skips validations and callbacks):
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
User.insert_all([
|
|
96
|
+
{ name: "Alice", email: "alice@example.com" },
|
|
97
|
+
{ name: "Bob", email: "bob@example.com" }
|
|
98
|
+
])
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Read
|
|
102
|
+
|
|
103
|
+
| Method | Returns | Not Found |
|
|
104
|
+
|--------|---------|-----------|
|
|
105
|
+
| `find(id)` | Record or raises | `RecordNotFound` (404) |
|
|
106
|
+
| `find_by(attr: val)` | Record or `nil` | `nil` |
|
|
107
|
+
| `where(conditions)` | `Relation` | Empty relation `[]` |
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# find - use when record MUST exist (controllers)
|
|
111
|
+
user = User.find(params[:id])
|
|
112
|
+
|
|
113
|
+
# find_by - use when missing record is expected
|
|
114
|
+
user = User.find_by(email: params[:email])
|
|
115
|
+
return "Not found" if user.nil?
|
|
116
|
+
|
|
117
|
+
# where - chainable queries
|
|
118
|
+
User.where(active: true).order(:name).limit(10)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Update
|
|
122
|
+
|
|
123
|
+
| Method | Validations | Callbacks | Updates `updated_at` |
|
|
124
|
+
|--------|------------|-----------|---------------------|
|
|
125
|
+
| `update` | Yes | Yes | Yes |
|
|
126
|
+
| `update!` | Yes (raises) | Yes | Yes |
|
|
127
|
+
| `update_attribute` | No | Yes | Yes |
|
|
128
|
+
| `update_column` | No | No | No |
|
|
129
|
+
| `update_columns` | No | No | No |
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Standard update (recommended)
|
|
133
|
+
user.update(name: "Bob")
|
|
134
|
+
user.update!(name: "Bob") # Raises on validation failure
|
|
135
|
+
|
|
136
|
+
# Skip validations (use sparingly)
|
|
137
|
+
user.update_attribute(:verified, true)
|
|
138
|
+
|
|
139
|
+
# Direct SQL (dangerous - skips everything)
|
|
140
|
+
user.update_column(:login_count, 5)
|
|
141
|
+
user.update_columns(login_count: 5, last_login: Time.current)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Delete vs Destroy
|
|
145
|
+
|
|
146
|
+
| Method | Callbacks | Dependent Associations | Returns |
|
|
147
|
+
|--------|-----------|------------------------|---------|
|
|
148
|
+
| `destroy` | Yes | Handled | Frozen object |
|
|
149
|
+
| `delete` | No | Ignored | Frozen object |
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# destroy - triggers callbacks and dependent associations
|
|
153
|
+
user.destroy
|
|
154
|
+
# Runs: before_destroy, destroy dependent children, after_destroy
|
|
155
|
+
|
|
156
|
+
# delete - direct SQL DELETE
|
|
157
|
+
user.delete
|
|
158
|
+
# Only runs: DELETE FROM users WHERE id = 123
|
|
159
|
+
|
|
160
|
+
# Bulk operations
|
|
161
|
+
User.where(status: :inactive).destroy_all # With callbacks
|
|
162
|
+
User.where(status: :inactive).delete_all # Without callbacks
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Best Practice**: Use `destroy` unless you need raw performance for bulk operations.
|
|
166
|
+
|
|
167
|
+
### Find or Create Patterns
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# Find or create by specific attributes
|
|
171
|
+
user = User.find_or_create_by(email: "alice@example.com")
|
|
172
|
+
|
|
173
|
+
# With block for additional attributes
|
|
174
|
+
user = User.find_or_create_by(email: "alice@example.com") do |u|
|
|
175
|
+
u.name = "Alice"
|
|
176
|
+
u.role = :member
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# find_or_initialize_by - doesn't save
|
|
180
|
+
user = User.find_or_initialize_by(email: "alice@example.com")
|
|
181
|
+
user.save if user.new_record?
|
|
182
|
+
|
|
183
|
+
# create_or_find_by - handles race conditions (unique constraint required)
|
|
184
|
+
user = User.create_or_find_by(email: "alice@example.com")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Dirty Tracking
|
|
188
|
+
|
|
189
|
+
Track attribute changes before and after saves.
|
|
190
|
+
|
|
191
|
+
### Before Save (Pending Changes)
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
user.name = "Bob"
|
|
195
|
+
|
|
196
|
+
user.changed? # => true
|
|
197
|
+
user.name_changed? # => true
|
|
198
|
+
user.name_was # => "Alice" (original value)
|
|
199
|
+
user.name_change # => ["Alice", "Bob"]
|
|
200
|
+
user.changes # => {"name" => ["Alice", "Bob"]}
|
|
201
|
+
|
|
202
|
+
# Check what will be saved
|
|
203
|
+
user.will_save_change_to_name? # => true
|
|
204
|
+
user.changes_to_save # => {"name" => ["Alice", "Bob"]}
|
|
205
|
+
user.name_in_database # => "Alice"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### After Save (Previous Changes)
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
user.save
|
|
212
|
+
|
|
213
|
+
user.saved_change_to_name? # => true
|
|
214
|
+
user.saved_change_to_name # => ["Alice", "Bob"]
|
|
215
|
+
user.name_before_last_save # => "Alice"
|
|
216
|
+
user.name_previously_was # => "Alice"
|
|
217
|
+
user.previous_changes # => {"name" => ["Alice", "Bob"]}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Reverting Changes
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
user.name = "Bob"
|
|
224
|
+
user.restore_name! # Reverts to original
|
|
225
|
+
user.name # => "Alice"
|
|
226
|
+
|
|
227
|
+
user.restore_attributes # Reverts all changes
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Using in Callbacks
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
class User < ApplicationRecord
|
|
234
|
+
after_save :notify_if_email_changed
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def notify_if_email_changed
|
|
239
|
+
if saved_change_to_email?
|
|
240
|
+
UserMailer.email_changed(self, email_before_last_save).deliver_later
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Partial Updates
|
|
247
|
+
|
|
248
|
+
By default, Rails only sends changed attributes in UPDATE queries:
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
# With partial_updates enabled (default)
|
|
252
|
+
user.name = "Bob"
|
|
253
|
+
user.save
|
|
254
|
+
# SQL: UPDATE users SET name = 'Bob', updated_at = '...' WHERE id = 1
|
|
255
|
+
|
|
256
|
+
# Disable partial updates (sends all attributes)
|
|
257
|
+
ActiveRecord::Base.partial_updates = false
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Type Casting and Serialization
|
|
261
|
+
|
|
262
|
+
### Attribute API
|
|
263
|
+
|
|
264
|
+
Define or override attribute types:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
class Product < ApplicationRecord
|
|
268
|
+
attribute :price, :decimal, precision: 8, scale: 2
|
|
269
|
+
attribute :metadata, :json
|
|
270
|
+
attribute :published_on, :date
|
|
271
|
+
attribute :active, :boolean, default: true
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Built-in types: `:boolean`, `:date`, `:datetime`, `:decimal`, `:float`, `:integer`, `:string`, `:text`, `:time`, `:json`
|
|
276
|
+
|
|
277
|
+
### Custom Types
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
class MoneyType < ActiveRecord::Type::Value
|
|
281
|
+
def cast(value)
|
|
282
|
+
return nil if value.blank?
|
|
283
|
+
BigDecimal(value.to_s.gsub(/[^\d.]/, ''))
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def serialize(value)
|
|
287
|
+
value&.to_s
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
ActiveRecord::Type.register(:money, MoneyType)
|
|
292
|
+
|
|
293
|
+
class Product < ApplicationRecord
|
|
294
|
+
attribute :price, :money
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Serialize (Legacy)
|
|
299
|
+
|
|
300
|
+
Store Ruby objects in text columns:
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
class User < ApplicationRecord
|
|
304
|
+
# Rails 7.2+ syntax (coder as keyword)
|
|
305
|
+
serialize :preferences, coder: JSON, type: Hash
|
|
306
|
+
serialize :tags, coder: YAML, type: Array
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Store Accessor
|
|
311
|
+
|
|
312
|
+
Key-value storage with accessors:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
class User < ApplicationRecord
|
|
316
|
+
# For serialized text column
|
|
317
|
+
store :settings, accessors: [:color, :language], coder: JSON
|
|
318
|
+
|
|
319
|
+
# For native JSON column (no serialization overhead)
|
|
320
|
+
store_accessor :preferences, :theme, :notifications
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
user.color = "blue"
|
|
324
|
+
user.theme = "dark"
|
|
325
|
+
user.settings # => {"color" => "blue"}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Best Practice**: Use native JSON columns (`jsonb` in PostgreSQL) with `store_accessor` instead of `serialize` for better performance and queryability.
|
|
329
|
+
|
|
330
|
+
### Accessing Raw Values
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
user.created_at # => Mon, 01 Jan 2024 00:00:00 UTC
|
|
334
|
+
user.created_at_before_type_cast # => "2024-01-01 00:00:00"
|
|
335
|
+
user.read_attribute_before_type_cast(:created_at)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Single Table Inheritance (STI)
|
|
339
|
+
|
|
340
|
+
Store multiple model types in one table using a `type` column.
|
|
341
|
+
|
|
342
|
+
### Setup
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
# Migration
|
|
346
|
+
class CreateVehicles < ActiveRecord::Migration[7.2]
|
|
347
|
+
def change
|
|
348
|
+
create_table :vehicles do |t|
|
|
349
|
+
t.string :type # Required for STI
|
|
350
|
+
t.string :make
|
|
351
|
+
t.string :model
|
|
352
|
+
t.integer :wheels
|
|
353
|
+
t.float :wing_span
|
|
354
|
+
t.timestamps
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Models
|
|
360
|
+
class Vehicle < ApplicationRecord; end
|
|
361
|
+
class Car < Vehicle; end
|
|
362
|
+
class Truck < Vehicle; end
|
|
363
|
+
class Airplane < Vehicle; end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Usage
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
car = Car.create(make: "Toyota", model: "Camry", wheels: 4)
|
|
370
|
+
car.type # => "Car"
|
|
371
|
+
|
|
372
|
+
Car.all # SELECT * FROM vehicles WHERE type = 'Car'
|
|
373
|
+
Vehicle.all # SELECT * FROM vehicles (all types)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### When to Use STI
|
|
377
|
+
|
|
378
|
+
| Use STI When | Avoid STI When |
|
|
379
|
+
|--------------|----------------|
|
|
380
|
+
| Subclasses share 80%+ attributes | Subclasses have divergent attributes |
|
|
381
|
+
| 2-4 subclasses | Many subclasses (5+) |
|
|
382
|
+
| Shared behavior across types | Types need different validations |
|
|
383
|
+
| Need to query all types together | Types rarely queried together |
|
|
384
|
+
|
|
385
|
+
### Common Pitfalls
|
|
386
|
+
|
|
387
|
+
**1. Sparse Tables**:
|
|
388
|
+
```ruby
|
|
389
|
+
# BAD - most columns null for most types
|
|
390
|
+
# vehicles: id, type, wheels, wing_span, sail_size, cargo_capacity
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**2. Polymorphic + STI Conflict**:
|
|
394
|
+
```ruby
|
|
395
|
+
# BAD - saves base class name, breaks subclass associations
|
|
396
|
+
class Comment < ApplicationRecord
|
|
397
|
+
belongs_to :commentable, polymorphic: true
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
car.comments # May not work as expected
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**3. Type Changes**:
|
|
404
|
+
```ruby
|
|
405
|
+
# Changing type is complex and error-prone
|
|
406
|
+
user.update(type: "Admin") # Requires validations for both types to pass
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Alternatives to STI
|
|
410
|
+
|
|
411
|
+
**Delegated Types** (Rails 6.1+):
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
class Entry < ApplicationRecord
|
|
415
|
+
delegated_type :entryable, types: %w[Message Comment]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
class Message < ApplicationRecord
|
|
419
|
+
has_one :entry, as: :entryable, touch: true
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
class Comment < ApplicationRecord
|
|
423
|
+
has_one :entry, as: :entryable, touch: true
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**Separate Tables with Shared Concerns**:
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
module Drivable
|
|
431
|
+
extend ActiveSupport::Concern
|
|
432
|
+
|
|
433
|
+
included do
|
|
434
|
+
validates :make, :model, presence: true
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def description
|
|
438
|
+
"#{make} #{model}"
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
class Car < ApplicationRecord
|
|
443
|
+
include Drivable
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
class Motorcycle < ApplicationRecord
|
|
447
|
+
include Drivable
|
|
448
|
+
end
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### STI Decision Tree
|
|
452
|
+
|
|
453
|
+
```
|
|
454
|
+
Do models share 80%+ attributes?
|
|
455
|
+
├── No → Use separate tables or delegated types
|
|
456
|
+
└── Yes
|
|
457
|
+
└── Are there 4 or fewer subclasses?
|
|
458
|
+
├── No → Use separate tables or delegated types
|
|
459
|
+
└── Yes
|
|
460
|
+
└── Do they share most behavior?
|
|
461
|
+
├── No → Use separate tables with concerns
|
|
462
|
+
└── Yes → STI is appropriate
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Anti-Patterns
|
|
466
|
+
|
|
467
|
+
### CRUD Anti-Patterns
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
# BAD - using delete when you need callbacks
|
|
471
|
+
user.delete # Orphans dependent records
|
|
472
|
+
|
|
473
|
+
# GOOD
|
|
474
|
+
user.destroy
|
|
475
|
+
|
|
476
|
+
# BAD - using update_column to skip validations
|
|
477
|
+
user.update_column(:email, "invalid") # No validation!
|
|
478
|
+
|
|
479
|
+
# GOOD - if you must skip validations, be explicit
|
|
480
|
+
user.update_attribute(:email, "valid@example.com")
|
|
481
|
+
|
|
482
|
+
# BAD - find for optional records
|
|
483
|
+
user = User.find(params[:user_id]) # Raises if not found
|
|
484
|
+
|
|
485
|
+
# GOOD
|
|
486
|
+
user = User.find_by(id: params[:user_id])
|
|
487
|
+
return "User not found" unless user
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Dirty Tracking Anti-Patterns
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
# BAD - checking changes in performance-critical code
|
|
494
|
+
users.each do |user|
|
|
495
|
+
user.update(processed: true)
|
|
496
|
+
log_changes(user.previous_changes) # Overhead for each user
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# GOOD - bulk update without instantiation
|
|
500
|
+
User.where(id: user_ids).update_all(processed: true)
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Type Casting Anti-Patterns
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
# BAD - serialize with JSON column (double serialization)
|
|
507
|
+
class User < ApplicationRecord
|
|
508
|
+
serialize :metadata, coder: JSON # column already jsonb
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# GOOD - use store_accessor directly
|
|
512
|
+
class User < ApplicationRecord
|
|
513
|
+
store_accessor :metadata, :key1, :key2
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# BAD - Rails 7.2+ positional coder
|
|
517
|
+
serialize :preferences, JSON # Deprecated
|
|
518
|
+
|
|
519
|
+
# GOOD
|
|
520
|
+
serialize :preferences, coder: JSON
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### STI Anti-Patterns
|
|
524
|
+
|
|
525
|
+
```ruby
|
|
526
|
+
# BAD - wide sparse table
|
|
527
|
+
class Vehicle < ApplicationRecord
|
|
528
|
+
# Has: type, wheels, wing_span, sail_size, cargo_capacity, pedals
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# BAD - using type column for non-STI
|
|
532
|
+
class Product < ApplicationRecord
|
|
533
|
+
# type column stores "clothing", "electronics" but no subclasses
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# GOOD - use explicit column for categorization
|
|
537
|
+
class Product < ApplicationRecord
|
|
538
|
+
enum :category, { clothing: 0, electronics: 1 }
|
|
539
|
+
end
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Model Lifecycle
|
|
543
|
+
|
|
544
|
+
### Object States
|
|
545
|
+
|
|
546
|
+
```ruby
|
|
547
|
+
user = User.new
|
|
548
|
+
user.new_record? # => true
|
|
549
|
+
user.persisted? # => false
|
|
550
|
+
user.destroyed? # => false
|
|
551
|
+
user.previously_new_record? # => false
|
|
552
|
+
|
|
553
|
+
user.save
|
|
554
|
+
user.new_record? # => false
|
|
555
|
+
user.persisted? # => true
|
|
556
|
+
user.previously_new_record? # => true
|
|
557
|
+
|
|
558
|
+
user.destroy
|
|
559
|
+
user.destroyed? # => true
|
|
560
|
+
user.persisted? # => false
|
|
561
|
+
user.frozen? # => true
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Reload
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
user = User.find(1)
|
|
568
|
+
user.name = "Changed"
|
|
569
|
+
user.reload # Refreshes from database, clears dirty tracking
|
|
570
|
+
user.name # => Original value from database
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Touch
|
|
574
|
+
|
|
575
|
+
```ruby
|
|
576
|
+
user.touch # Updates updated_at
|
|
577
|
+
user.touch(:last_login) # Updates last_login AND updated_at
|
|
578
|
+
user.touch(time: 1.hour.ago)
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## Performance Tips
|
|
582
|
+
|
|
583
|
+
### Avoid N+1 with Associations
|
|
584
|
+
|
|
585
|
+
```ruby
|
|
586
|
+
# BAD - N+1 queries
|
|
587
|
+
User.all.each { |u| puts u.posts.count }
|
|
588
|
+
|
|
589
|
+
# GOOD - eager loading
|
|
590
|
+
User.includes(:posts).each { |u| puts u.posts.size }
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Use pluck for Single Columns
|
|
594
|
+
|
|
595
|
+
```ruby
|
|
596
|
+
# BAD - instantiates all models
|
|
597
|
+
User.all.map(&:email)
|
|
598
|
+
|
|
599
|
+
# GOOD - returns array of values
|
|
600
|
+
User.pluck(:email)
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Use select for Limited Attributes
|
|
604
|
+
|
|
605
|
+
```ruby
|
|
606
|
+
# BAD - loads all columns
|
|
607
|
+
User.all.each { |u| puts u.name }
|
|
608
|
+
|
|
609
|
+
# GOOD - loads only needed columns
|
|
610
|
+
User.select(:id, :name).each { |u| puts u.name }
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Batch Processing
|
|
614
|
+
|
|
615
|
+
```ruby
|
|
616
|
+
# BAD - loads all into memory
|
|
617
|
+
User.all.each { |u| process(u) }
|
|
618
|
+
|
|
619
|
+
# GOOD - processes in batches of 1000
|
|
620
|
+
User.find_each { |u| process(u) }
|
|
621
|
+
User.find_each(batch_size: 500) { |u| process(u) }
|
|
622
|
+
```
|