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,657 @@
|
|
|
1
|
+
# ActiveRecord Migrations Reference
|
|
2
|
+
|
|
3
|
+
Comprehensive reference for database migrations: methods, reversibility, constraints, indexes, and safe patterns for production deployments.
|
|
4
|
+
|
|
5
|
+
## Core Migration Methods
|
|
6
|
+
|
|
7
|
+
### Creating Tables
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class CreateProducts < ActiveRecord::Migration[7.2]
|
|
11
|
+
def change
|
|
12
|
+
create_table :products do |t|
|
|
13
|
+
t.string :name, null: false
|
|
14
|
+
t.text :description
|
|
15
|
+
t.decimal :price, precision: 8, scale: 2
|
|
16
|
+
t.references :category, foreign_key: true
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Option | Purpose | Example |
|
|
24
|
+
|--------|---------|---------|
|
|
25
|
+
| `id: false` | No auto-incrementing primary key | Join tables |
|
|
26
|
+
| `id: :uuid` | UUID primary key | Distributed systems |
|
|
27
|
+
| `primary_key: :custom_id` | Custom PK column name | Legacy schemas |
|
|
28
|
+
| `if_not_exists: true` | Skip if table exists | Idempotent migrations |
|
|
29
|
+
| `force: true` | Drop table first | **Never in production** |
|
|
30
|
+
|
|
31
|
+
### Column Types
|
|
32
|
+
|
|
33
|
+
| Type | PostgreSQL | MySQL | SQLite | Notes |
|
|
34
|
+
|------|------------|-------|--------|-------|
|
|
35
|
+
| `:string` | varchar(255) | varchar(255) | varchar | Use for short text |
|
|
36
|
+
| `:text` | text | text | text | Use for long text |
|
|
37
|
+
| `:integer` | integer | int(11) | integer | 4 bytes |
|
|
38
|
+
| `:bigint` | bigint | bigint | integer | 8 bytes (default for PKs) |
|
|
39
|
+
| `:decimal` | decimal | decimal | decimal | Specify precision/scale |
|
|
40
|
+
| `:float` | float | float | float | Inexact, use decimal for money |
|
|
41
|
+
| `:boolean` | boolean | tinyint(1) | boolean | |
|
|
42
|
+
| `:date` | date | date | date | |
|
|
43
|
+
| `:datetime` | timestamp | datetime | datetime | Rails adds precision: 6 |
|
|
44
|
+
| `:time` | time | time | time | |
|
|
45
|
+
| `:binary` | bytea | blob | blob | |
|
|
46
|
+
| `:json` | json | json | text | Use `:jsonb` for PostgreSQL |
|
|
47
|
+
| `:jsonb` | jsonb | - | - | PostgreSQL only, indexed |
|
|
48
|
+
| `:uuid` | uuid | - | - | PostgreSQL/MySQL 8+ |
|
|
49
|
+
|
|
50
|
+
### Adding Columns
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
add_column :users, :role, :string, default: "member", null: false
|
|
54
|
+
add_column :products, :metadata, :jsonb, default: {}
|
|
55
|
+
|
|
56
|
+
# Reference columns (with index by default)
|
|
57
|
+
add_reference :products, :supplier, foreign_key: true
|
|
58
|
+
add_reference :comments, :commentable, polymorphic: true, index: true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Modifying Columns
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# Change type (IRREVERSIBLE without up/down)
|
|
65
|
+
change_column :products, :price, :decimal, precision: 10, scale: 2
|
|
66
|
+
|
|
67
|
+
# Change null constraint (REVERSIBLE)
|
|
68
|
+
change_column_null :users, :email, false
|
|
69
|
+
|
|
70
|
+
# Change default (REVERSIBLE with from/to)
|
|
71
|
+
change_column_default :users, :status, from: nil, to: "active"
|
|
72
|
+
|
|
73
|
+
# Rename column (REVERSIBLE)
|
|
74
|
+
rename_column :users, :name, :full_name
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Removing Columns
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
# Must include type for reversibility
|
|
81
|
+
remove_column :users, :legacy_field, :string
|
|
82
|
+
|
|
83
|
+
# Multiple columns (must include type option)
|
|
84
|
+
remove_columns :users, :temp1, :temp2, type: :string
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Reversibility
|
|
88
|
+
|
|
89
|
+
### Auto-Reversible Operations
|
|
90
|
+
|
|
91
|
+
These operations can use `change` - Rails knows how to reverse them:
|
|
92
|
+
|
|
93
|
+
| Operation | Reverse |
|
|
94
|
+
|-----------|---------|
|
|
95
|
+
| `add_column` | `remove_column` |
|
|
96
|
+
| `add_index` | `remove_index` |
|
|
97
|
+
| `add_reference` | `remove_reference` |
|
|
98
|
+
| `add_foreign_key` | `remove_foreign_key` |
|
|
99
|
+
| `add_timestamps` | `remove_timestamps` |
|
|
100
|
+
| `add_check_constraint` | `remove_check_constraint` |
|
|
101
|
+
| `create_table` | `drop_table` |
|
|
102
|
+
| `create_join_table` | `drop_join_table` |
|
|
103
|
+
| `rename_column` | Reverse rename |
|
|
104
|
+
| `rename_table` | Reverse rename |
|
|
105
|
+
| `rename_index` | Reverse rename |
|
|
106
|
+
| `enable_extension` | `disable_extension` |
|
|
107
|
+
|
|
108
|
+
### Operations Requiring Extra Info
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# change_column_default - MUST have from/to
|
|
112
|
+
change_column_default :posts, :status, from: nil, to: "draft"
|
|
113
|
+
|
|
114
|
+
# remove_column - MUST include type
|
|
115
|
+
remove_column :users, :age, :integer
|
|
116
|
+
|
|
117
|
+
# remove_index - MUST include column
|
|
118
|
+
remove_index :users, :email
|
|
119
|
+
remove_index :users, column: [:first_name, :last_name]
|
|
120
|
+
|
|
121
|
+
# remove_foreign_key - MUST include to_table
|
|
122
|
+
remove_foreign_key :orders, :customers
|
|
123
|
+
remove_foreign_key :orders, column: :buyer_id, to_table: :users
|
|
124
|
+
|
|
125
|
+
# drop_table - MUST include block for schema
|
|
126
|
+
drop_table :users do |t|
|
|
127
|
+
t.string :email
|
|
128
|
+
t.timestamps
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Irreversible Operations (Use up/down)
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class ChangeColumnType < ActiveRecord::Migration[7.2]
|
|
136
|
+
def up
|
|
137
|
+
change_column :products, :price, :decimal, precision: 10, scale: 2
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def down
|
|
141
|
+
change_column :products, :price, :decimal, precision: 8, scale: 2
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Using reversible Block
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
class AddConstraint < ActiveRecord::Migration[7.2]
|
|
150
|
+
def change
|
|
151
|
+
create_table :products do |t|
|
|
152
|
+
t.decimal :price
|
|
153
|
+
t.timestamps
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
reversible do |dir|
|
|
157
|
+
dir.up do
|
|
158
|
+
execute "ALTER TABLE products ADD CONSTRAINT price_positive CHECK (price > 0)"
|
|
159
|
+
end
|
|
160
|
+
dir.down do
|
|
161
|
+
execute "ALTER TABLE products DROP CONSTRAINT price_positive"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Testing Reversibility
|
|
169
|
+
|
|
170
|
+
Always test both directions:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
rails db:migrate && rails db:rollback && rails db:migrate
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Mark truly irreversible migrations explicitly:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
def down
|
|
180
|
+
raise ActiveRecord::IrreversibleMigration
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Database Constraints
|
|
185
|
+
|
|
186
|
+
### NOT NULL
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# On new column
|
|
190
|
+
add_column :users, :email, :string, null: false
|
|
191
|
+
|
|
192
|
+
# On existing column (validates all rows - can lock table!)
|
|
193
|
+
change_column_null :users, :email, false
|
|
194
|
+
|
|
195
|
+
# With default for existing NULLs
|
|
196
|
+
change_column_null :users, :email, false, "unknown@example.com"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Foreign Keys
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# With reference (most common)
|
|
203
|
+
add_reference :orders, :customer, foreign_key: true
|
|
204
|
+
|
|
205
|
+
# Standalone foreign key
|
|
206
|
+
add_foreign_key :orders, :customers
|
|
207
|
+
|
|
208
|
+
# With options
|
|
209
|
+
add_foreign_key :orders, :customers,
|
|
210
|
+
column: :buyer_id,
|
|
211
|
+
on_delete: :cascade,
|
|
212
|
+
on_update: :cascade
|
|
213
|
+
|
|
214
|
+
# Composite foreign key (PostgreSQL)
|
|
215
|
+
add_foreign_key :line_items, :orders, primary_key: [:shop_id, :order_id]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| on_delete/on_update | Behavior |
|
|
219
|
+
|---------------------|----------|
|
|
220
|
+
| `:nullify` | Set FK column to NULL |
|
|
221
|
+
| `:cascade` | Delete/update child rows |
|
|
222
|
+
| `:restrict` | Prevent if children exist |
|
|
223
|
+
| `:no_action` | Defer check to transaction end |
|
|
224
|
+
|
|
225
|
+
### Check Constraints
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
add_check_constraint :products, "price > 0", name: "products_price_positive"
|
|
229
|
+
add_check_constraint :orders, "quantity >= 1", name: "orders_quantity_min"
|
|
230
|
+
|
|
231
|
+
# PostgreSQL: validate separately for large tables
|
|
232
|
+
add_check_constraint :users, "email IS NOT NULL",
|
|
233
|
+
name: "users_email_not_null",
|
|
234
|
+
validate: false
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Unique Constraints
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# Via index (most common)
|
|
241
|
+
add_index :users, :email, unique: true
|
|
242
|
+
|
|
243
|
+
# Composite unique
|
|
244
|
+
add_index :memberships, [:user_id, :organization_id], unique: true
|
|
245
|
+
|
|
246
|
+
# Partial unique (PostgreSQL)
|
|
247
|
+
add_index :users, :email, unique: true, where: "deleted_at IS NULL"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Index Strategies
|
|
251
|
+
|
|
252
|
+
### When to Add Indexes
|
|
253
|
+
|
|
254
|
+
Add indexes for columns used in:
|
|
255
|
+
- `WHERE` clauses
|
|
256
|
+
- `ORDER BY` clauses
|
|
257
|
+
- `JOIN` conditions
|
|
258
|
+
- Foreign key columns
|
|
259
|
+
|
|
260
|
+
Skip indexes for:
|
|
261
|
+
- Tables with < 1,000 rows (unless expecting growth)
|
|
262
|
+
- Columns with low cardinality (few unique values)
|
|
263
|
+
- Write-heavy tables with infrequent reads
|
|
264
|
+
|
|
265
|
+
### Basic Indexes
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
add_index :users, :email
|
|
269
|
+
add_index :users, :email, unique: true
|
|
270
|
+
add_index :users, :email, name: "idx_users_email"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Composite Indexes
|
|
274
|
+
|
|
275
|
+
Column order matters - leftmost column is most important:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
# Good for: WHERE last_name = 'X' AND first_name = 'Y'
|
|
279
|
+
# Good for: WHERE last_name = 'X'
|
|
280
|
+
# NOT useful for: WHERE first_name = 'Y'
|
|
281
|
+
add_index :users, [:last_name, :first_name]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Put most selective column first.
|
|
285
|
+
|
|
286
|
+
### Partial Indexes (PostgreSQL, SQLite)
|
|
287
|
+
|
|
288
|
+
Index only subset of rows:
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
# Index only active users
|
|
292
|
+
add_index :users, :email, where: "active = true"
|
|
293
|
+
|
|
294
|
+
# Index only non-null values
|
|
295
|
+
add_index :sessions, :user_id, where: "user_id IS NOT NULL"
|
|
296
|
+
|
|
297
|
+
# Index only specific status
|
|
298
|
+
add_index :orders, :created_at, where: "status = 'pending'"
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Index Options
|
|
302
|
+
|
|
303
|
+
| Option | Purpose | Database |
|
|
304
|
+
|--------|---------|----------|
|
|
305
|
+
| `unique: true` | Enforce uniqueness | All |
|
|
306
|
+
| `where: "..."` | Partial index | PostgreSQL, SQLite |
|
|
307
|
+
| `include: [:col]` | Cover additional columns | PostgreSQL |
|
|
308
|
+
| `using: :gist` | Different index type | PostgreSQL |
|
|
309
|
+
| `order: { col: :desc }` | Index ordering | PostgreSQL, MySQL 8+ |
|
|
310
|
+
| `algorithm: :concurrently` | No table lock | PostgreSQL |
|
|
311
|
+
| `type: :fulltext` | Full-text search | MySQL |
|
|
312
|
+
|
|
313
|
+
### Concurrent Index Creation (PostgreSQL)
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
class AddIndexConcurrently < ActiveRecord::Migration[7.2]
|
|
317
|
+
disable_ddl_transaction!
|
|
318
|
+
|
|
319
|
+
def change
|
|
320
|
+
add_index :users, :email, algorithm: :concurrently
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Required** for large production tables to avoid blocking reads/writes.
|
|
326
|
+
|
|
327
|
+
## Safe Migration Patterns
|
|
328
|
+
|
|
329
|
+
### Zero-Downtime Deployment
|
|
330
|
+
|
|
331
|
+
For production applications, migrations must not block normal operation:
|
|
332
|
+
|
|
333
|
+
| Operation | Risk | Safe Alternative |
|
|
334
|
+
|-----------|------|------------------|
|
|
335
|
+
| Add column with default | Table rewrite (older Rails) | Add column, then set default |
|
|
336
|
+
| Add NOT NULL | Validates all rows | Check constraint first |
|
|
337
|
+
| Remove column | AR caches columns | Add `ignored_columns` first |
|
|
338
|
+
| Rename column | Code references old name | Dual-write pattern |
|
|
339
|
+
| Change column type | Table rewrite | Create new column, migrate data |
|
|
340
|
+
| Add index | Locks table | `algorithm: :concurrently` |
|
|
341
|
+
|
|
342
|
+
### Safe Column Removal (3-Release Process)
|
|
343
|
+
|
|
344
|
+
**Release 1**: Add to ignored_columns
|
|
345
|
+
```ruby
|
|
346
|
+
class User < ApplicationRecord
|
|
347
|
+
self.ignored_columns += ["legacy_column"]
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Release 2**: Drop column in post-deployment migration
|
|
352
|
+
```ruby
|
|
353
|
+
class RemoveLegacyColumn < ActiveRecord::Migration[7.2]
|
|
354
|
+
def change
|
|
355
|
+
remove_column :users, :legacy_column, :string
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Release 3**: Remove ignored_columns line
|
|
361
|
+
|
|
362
|
+
### Safe NOT NULL Addition (PostgreSQL)
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
class SafeAddNotNull < ActiveRecord::Migration[7.2]
|
|
366
|
+
def up
|
|
367
|
+
# Step 1: Add check constraint without validation
|
|
368
|
+
add_check_constraint :users, "email IS NOT NULL",
|
|
369
|
+
name: "users_email_not_null",
|
|
370
|
+
validate: false
|
|
371
|
+
|
|
372
|
+
# Step 2: Validate constraint (doesn't lock)
|
|
373
|
+
validate_check_constraint :users, name: "users_email_not_null"
|
|
374
|
+
|
|
375
|
+
# Step 3: Add actual NOT NULL (instant, uses constraint)
|
|
376
|
+
change_column_null :users, :email, false
|
|
377
|
+
|
|
378
|
+
# Step 4: Remove redundant constraint
|
|
379
|
+
remove_check_constraint :users, name: "users_email_not_null"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def down
|
|
383
|
+
change_column_null :users, :email, true
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Backfilling Data Safely
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
class BackfillStatus < ActiveRecord::Migration[7.2]
|
|
392
|
+
disable_ddl_transaction!
|
|
393
|
+
|
|
394
|
+
def up
|
|
395
|
+
User.unscoped.in_batches(of: 10_000) do |batch|
|
|
396
|
+
batch.update_all(status: "active")
|
|
397
|
+
sleep(0.1) # Throttle to reduce load
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Using strong_migrations Gem
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# Gemfile
|
|
407
|
+
gem "strong_migrations"
|
|
408
|
+
|
|
409
|
+
# config/initializers/strong_migrations.rb
|
|
410
|
+
StrongMigrations.start_after = 20240101000000
|
|
411
|
+
|
|
412
|
+
# Migration with safety override
|
|
413
|
+
class AddColumn < ActiveRecord::Migration[7.2]
|
|
414
|
+
def change
|
|
415
|
+
safety_assured { add_column :users, :data, :jsonb, default: {} }
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Data vs Schema Migrations
|
|
421
|
+
|
|
422
|
+
### Keep Them Separate
|
|
423
|
+
|
|
424
|
+
Schema migrations change database structure. Data migrations transform content.
|
|
425
|
+
|
|
426
|
+
**Problems with mixing**:
|
|
427
|
+
- Transaction rollback undoes both schema and data changes
|
|
428
|
+
- Data migrations can take much longer
|
|
429
|
+
- Different failure modes and recovery strategies
|
|
430
|
+
|
|
431
|
+
### Recommended Approaches
|
|
432
|
+
|
|
433
|
+
**1. maintenance_tasks gem** (Rails recommended):
|
|
434
|
+
```ruby
|
|
435
|
+
# lib/maintenance_tasks/tasks/backfill_user_status.rb
|
|
436
|
+
class BackfillUserStatus < MaintenanceTasks::Task
|
|
437
|
+
def collection
|
|
438
|
+
User.where(status: nil)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def process(user)
|
|
442
|
+
user.update!(status: "active")
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**2. Separate data migration with data_migrate gem**:
|
|
448
|
+
```bash
|
|
449
|
+
rails g data_migration backfill_user_status
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**3. Rake task / runner script**:
|
|
453
|
+
```ruby
|
|
454
|
+
# db/scripts/backfill_status.rb
|
|
455
|
+
User.where(status: nil).find_each do |user|
|
|
456
|
+
user.update!(status: "active")
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Anti-Patterns
|
|
461
|
+
|
|
462
|
+
### Never Edit Committed Migrations
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
# BAD - changing a deployed migration
|
|
466
|
+
class CreateUsers < ActiveRecord::Migration[7.2]
|
|
467
|
+
def change
|
|
468
|
+
create_table :users do |t|
|
|
469
|
+
t.string :name
|
|
470
|
+
t.string :email # Added later - breaks other environments!
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# GOOD - create new migration
|
|
476
|
+
class AddEmailToUsers < ActiveRecord::Migration[7.2]
|
|
477
|
+
def change
|
|
478
|
+
add_column :users, :email, :string
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Never Reference Models in Migrations
|
|
484
|
+
|
|
485
|
+
```ruby
|
|
486
|
+
# BAD - model may change, breaking old migrations
|
|
487
|
+
class BackfillUserNames < ActiveRecord::Migration[7.2]
|
|
488
|
+
def up
|
|
489
|
+
User.find_each do |user|
|
|
490
|
+
user.update!(display_name: user.generate_display_name)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# GOOD - define migration-local model
|
|
496
|
+
class BackfillUserNames < ActiveRecord::Migration[7.2]
|
|
497
|
+
class User < ApplicationRecord
|
|
498
|
+
self.table_name = "users"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def up
|
|
502
|
+
User.find_each do |user|
|
|
503
|
+
user.update_columns(display_name: "#{user.first_name} #{user.last_name}")
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# BEST - use raw SQL for simple transformations
|
|
509
|
+
class BackfillUserNames < ActiveRecord::Migration[7.2]
|
|
510
|
+
def up
|
|
511
|
+
execute <<~SQL
|
|
512
|
+
UPDATE users SET display_name = first_name || ' ' || last_name
|
|
513
|
+
SQL
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Reset Column Information When Using Models
|
|
519
|
+
|
|
520
|
+
```ruby
|
|
521
|
+
class AddAndBackfillRole < ActiveRecord::Migration[7.2]
|
|
522
|
+
def up
|
|
523
|
+
add_column :users, :role, :string
|
|
524
|
+
|
|
525
|
+
# REQUIRED - AR caches column info
|
|
526
|
+
User.reset_column_information
|
|
527
|
+
|
|
528
|
+
User.find_each do |user|
|
|
529
|
+
user.update_columns(role: "member")
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Never Use force: true in Production Migrations
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
# BAD - drops and recreates table, losing all data
|
|
539
|
+
create_table :users, force: true do |t|
|
|
540
|
+
t.string :email
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# OK - only in development/test for fixture setup
|
|
544
|
+
if Rails.env.development? || Rails.env.test?
|
|
545
|
+
create_table :users, force: true do |t|
|
|
546
|
+
t.string :email
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Avoid change_column When Possible
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
# BAD - irreversible, unclear intent
|
|
555
|
+
change_column :users, :status, :string, default: "active"
|
|
556
|
+
|
|
557
|
+
# GOOD - specific, reversible
|
|
558
|
+
change_column_default :users, :status, from: nil, to: "active"
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
## Decision Tree: Validation vs Constraint
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
Does the rule ALWAYS apply, regardless of business logic?
|
|
565
|
+
├── Yes → Database constraint
|
|
566
|
+
│ └── Examples: NOT NULL, foreign keys, unique emails, positive prices
|
|
567
|
+
│
|
|
568
|
+
└── No → Model validation
|
|
569
|
+
└── Examples: Format rules that change, conditional requirements
|
|
570
|
+
|
|
571
|
+
Is data integrity critical even with direct SQL?
|
|
572
|
+
├── Yes → Database constraint
|
|
573
|
+
│
|
|
574
|
+
└── No → Model validation is sufficient
|
|
575
|
+
|
|
576
|
+
Need helpful user-facing error messages?
|
|
577
|
+
├── Yes → Model validation (possibly WITH constraint)
|
|
578
|
+
│
|
|
579
|
+
└── No → Constraint alone is fine
|
|
580
|
+
|
|
581
|
+
Production constraint addition acceptable?
|
|
582
|
+
├── No → Model validation only (can't lock table)
|
|
583
|
+
│
|
|
584
|
+
└── Yes → Database constraint
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**Best Practice**: Use both for critical rules:
|
|
588
|
+
```ruby
|
|
589
|
+
# Database constraint (data integrity)
|
|
590
|
+
add_check_constraint :products, "price > 0"
|
|
591
|
+
|
|
592
|
+
# Model validation (user feedback)
|
|
593
|
+
validates :price, numericality: { greater_than: 0 }
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Transactional Migrations
|
|
597
|
+
|
|
598
|
+
### Default Behavior
|
|
599
|
+
|
|
600
|
+
DDL operations are wrapped in transactions (if adapter supports):
|
|
601
|
+
- Success: All changes committed
|
|
602
|
+
- Failure: All changes rolled back
|
|
603
|
+
|
|
604
|
+
### When to Disable Transactions
|
|
605
|
+
|
|
606
|
+
```ruby
|
|
607
|
+
class AddEnumValue < ActiveRecord::Migration[7.2]
|
|
608
|
+
disable_ddl_transaction! # Required
|
|
609
|
+
|
|
610
|
+
def up
|
|
611
|
+
execute "ALTER TYPE status_type ADD VALUE 'archived'"
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Required for:
|
|
617
|
+
- PostgreSQL enum modifications
|
|
618
|
+
- Concurrent index creation/removal
|
|
619
|
+
- Long-running data backfills
|
|
620
|
+
- Any operation incompatible with transactions
|
|
621
|
+
|
|
622
|
+
## PostgreSQL-Specific Features
|
|
623
|
+
|
|
624
|
+
```ruby
|
|
625
|
+
# Enable extension
|
|
626
|
+
enable_extension "pgcrypto"
|
|
627
|
+
enable_extension "citext"
|
|
628
|
+
|
|
629
|
+
# Create enum type
|
|
630
|
+
create_enum :status, ["draft", "published", "archived"]
|
|
631
|
+
|
|
632
|
+
# Use enum in table
|
|
633
|
+
add_column :posts, :status, :status, default: "draft"
|
|
634
|
+
|
|
635
|
+
# JSONB with GIN index
|
|
636
|
+
add_column :products, :metadata, :jsonb, default: {}
|
|
637
|
+
add_index :products, :metadata, using: :gin
|
|
638
|
+
|
|
639
|
+
# Partial unique index
|
|
640
|
+
add_index :users, :email, unique: true, where: "deleted_at IS NULL"
|
|
641
|
+
|
|
642
|
+
# Expression index
|
|
643
|
+
add_index :users, "lower(email)", unique: true
|
|
644
|
+
|
|
645
|
+
# Include columns in index
|
|
646
|
+
add_index :orders, :user_id, include: [:status, :total]
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
## Migration Timing Guidelines
|
|
650
|
+
|
|
651
|
+
| Migration Type | Max Duration | Use Case |
|
|
652
|
+
|----------------|--------------|----------|
|
|
653
|
+
| Regular | < 3 min | Critical schema changes before deploy |
|
|
654
|
+
| Post-deployment | < 10 min | Cleanup, non-critical indexes |
|
|
655
|
+
| Background job | > 10 min | Large data transformations |
|
|
656
|
+
|
|
657
|
+
Test migration duration against production-scale data in staging.
|