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.
Files changed (270) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +219 -25
  5. data/agents/codebase-analyzer.md +88 -0
  6. data/agents/codebase-pattern-finder.md +83 -0
  7. data/agents/documentation-researcher.md +59 -0
  8. data/agents/thoughts-analyzer.md +102 -0
  9. data/agents/web-search-researcher.md +71 -0
  10. data/anima-core.gemspec +4 -1
  11. data/app/channels/session_channel.rb +76 -28
  12. data/app/jobs/agent_request_job.rb +24 -0
  13. data/app/jobs/analytical_brain_job.rb +33 -0
  14. data/app/jobs/count_event_tokens_job.rb +1 -1
  15. data/app/models/concerns/event/broadcasting.rb +20 -2
  16. data/app/models/event.rb +1 -1
  17. data/app/models/goal.rb +91 -0
  18. data/app/models/session.rb +347 -22
  19. data/config/application.rb +2 -0
  20. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  21. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  22. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  23. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  24. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  25. data/db/migrate/20260315140843_create_goals.rb +16 -0
  26. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  27. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  28. data/lib/agent_loop.rb +65 -9
  29. data/lib/agents/definition.rb +116 -0
  30. data/lib/agents/registry.rb +106 -0
  31. data/lib/analytical_brain/runner.rb +276 -0
  32. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  33. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  34. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  35. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  36. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  37. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  38. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  39. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  40. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  41. data/lib/analytical_brain.rb +23 -0
  42. data/lib/anima/cli/mcp/secrets.rb +76 -0
  43. data/lib/anima/cli/mcp.rb +197 -0
  44. data/lib/anima/cli.rb +4 -0
  45. data/lib/anima/installer.rb +182 -6
  46. data/lib/anima/settings.rb +226 -0
  47. data/lib/anima/version.rb +1 -1
  48. data/lib/anima.rb +9 -0
  49. data/lib/credential_store.rb +103 -0
  50. data/lib/environment_probe.rb +232 -0
  51. data/lib/llm/client.rb +29 -10
  52. data/lib/mcp/client_manager.rb +86 -0
  53. data/lib/mcp/config.rb +213 -0
  54. data/lib/mcp/health_check.rb +77 -0
  55. data/lib/mcp/secrets.rb +73 -0
  56. data/lib/mcp/stdio_transport.rb +206 -0
  57. data/lib/providers/anthropic.rb +8 -7
  58. data/lib/shell_session.rb +11 -10
  59. data/lib/skills/definition.rb +97 -0
  60. data/lib/skills/registry.rb +105 -0
  61. data/lib/tools/edit.rb +3 -4
  62. data/lib/tools/mcp_tool.rb +114 -0
  63. data/lib/tools/read.rb +15 -16
  64. data/lib/tools/registry.rb +14 -12
  65. data/lib/tools/request_feature.rb +121 -0
  66. data/lib/tools/return_result.rb +81 -0
  67. data/lib/tools/spawn_specialist.rb +109 -0
  68. data/lib/tools/spawn_subagent.rb +111 -0
  69. data/lib/tools/subagent_prompts.rb +12 -0
  70. data/lib/tools/web_get.rb +8 -9
  71. data/lib/tui/app.rb +332 -43
  72. data/lib/tui/message_store.rb +20 -0
  73. data/lib/tui/screens/chat.rb +207 -20
  74. data/lib/workflows/definition.rb +97 -0
  75. data/lib/workflows/registry.rb +89 -0
  76. data/skills/activerecord/SKILL.md +255 -0
  77. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  78. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  79. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  80. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  81. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  82. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  83. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  84. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  85. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  86. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  87. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  88. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  89. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  90. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  91. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  92. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  93. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  94. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  95. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  96. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  97. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  98. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  99. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  100. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  101. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  102. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  103. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  104. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  105. data/skills/activerecord/references/associations.md +709 -0
  106. data/skills/activerecord/references/basics.md +622 -0
  107. data/skills/activerecord/references/callbacks.md +738 -0
  108. data/skills/activerecord/references/migrations.md +657 -0
  109. data/skills/activerecord/references/querying.md +655 -0
  110. data/skills/activerecord/references/validations.md +596 -0
  111. data/skills/dragonruby/SKILL.md +250 -0
  112. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  113. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  114. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  115. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  116. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  117. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  118. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  119. data/skills/dragonruby/examples/core/labels.rb +22 -0
  120. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  121. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  122. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  123. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  124. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  125. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  126. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  127. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  128. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  129. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  130. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  131. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  132. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  133. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  134. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  135. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  136. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  137. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  138. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  139. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  140. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  141. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  142. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  143. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  144. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  145. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  146. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  147. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  148. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  149. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  150. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  151. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  152. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  153. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  154. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  155. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  156. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  157. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  158. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  159. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  160. data/skills/dragonruby/references/audio.md +396 -0
  161. data/skills/dragonruby/references/core.md +385 -0
  162. data/skills/dragonruby/references/distribution.md +434 -0
  163. data/skills/dragonruby/references/entities.md +516 -0
  164. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  165. data/skills/dragonruby/references/game-logic/state.md +389 -0
  166. data/skills/dragonruby/references/input.md +414 -0
  167. data/skills/dragonruby/references/rendering/animation.md +467 -0
  168. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  169. data/skills/dragonruby/references/scenes.md +443 -0
  170. data/skills/draper-decorators/SKILL.md +344 -0
  171. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  172. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  173. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  174. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  175. data/skills/draper-decorators/references/patterns.md +507 -0
  176. data/skills/draper-decorators/references/testing.md +559 -0
  177. data/skills/gh-issue.md +182 -0
  178. data/skills/mcp-server/SKILL.md +177 -0
  179. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  180. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  181. data/skills/mcp-server/examples/http_client.rb +48 -0
  182. data/skills/mcp-server/examples/http_server.rb +97 -0
  183. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  184. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  185. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  186. data/skills/mcp-server/references/gotchas.md +183 -0
  187. data/skills/mcp-server/references/prompts.md +98 -0
  188. data/skills/mcp-server/references/resources.md +53 -0
  189. data/skills/mcp-server/references/server.md +140 -0
  190. data/skills/mcp-server/references/tools.md +146 -0
  191. data/skills/mcp-server/references/transport.md +104 -0
  192. data/skills/ratatui-ruby/SKILL.md +315 -0
  193. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  194. data/skills/ratatui-ruby/references/events.md +387 -0
  195. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  196. data/skills/ratatui-ruby/references/layout.md +423 -0
  197. data/skills/ratatui-ruby/references/styling.md +268 -0
  198. data/skills/ratatui-ruby/references/testing.md +433 -0
  199. data/skills/ratatui-ruby/references/widgets.md +532 -0
  200. data/skills/rspec/SKILL.md +340 -0
  201. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  202. data/skills/rspec/examples/core/configuration.rb +126 -0
  203. data/skills/rspec/examples/core/hooks.rb +126 -0
  204. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  205. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  206. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  207. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  208. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  209. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  210. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  211. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  212. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  213. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  214. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  215. data/skills/rspec/examples/matchers/change.rb +115 -0
  216. data/skills/rspec/examples/matchers/collections.rb +154 -0
  217. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  218. data/skills/rspec/examples/matchers/composing.rb +155 -0
  219. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  220. data/skills/rspec/examples/matchers/equality.rb +58 -0
  221. data/skills/rspec/examples/matchers/errors.rb +136 -0
  222. data/skills/rspec/examples/matchers/output.rb +103 -0
  223. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  224. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  225. data/skills/rspec/examples/matchers/types.rb +82 -0
  226. data/skills/rspec/examples/matchers/yield.rb +147 -0
  227. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  228. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  229. data/skills/rspec/examples/mocks/constants.rb +177 -0
  230. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  231. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  232. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  233. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  234. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  235. data/skills/rspec/examples/mocks/responses.rb +223 -0
  236. data/skills/rspec/examples/mocks/spies.rb +149 -0
  237. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  238. data/skills/rspec/examples/rails/channels.rb +250 -0
  239. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  240. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  241. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  242. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  243. data/skills/rspec/examples/rails/matchers.rb +374 -0
  244. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  245. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  246. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  247. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  248. data/skills/rspec/examples/rails/transactions.rb +254 -0
  249. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  250. data/skills/rspec/references/core.md +816 -0
  251. data/skills/rspec/references/factory_bot.md +641 -0
  252. data/skills/rspec/references/matchers.md +516 -0
  253. data/skills/rspec/references/mocks.md +381 -0
  254. data/skills/rspec/references/rails.md +528 -0
  255. data/templates/soul.md +40 -0
  256. data/workflows/commit.md +45 -0
  257. data/workflows/create_handoff.md +98 -0
  258. data/workflows/create_note.md +82 -0
  259. data/workflows/create_plan.md +457 -0
  260. data/workflows/decompose_ticket.md +109 -0
  261. data/workflows/feature.md +91 -0
  262. data/workflows/implement_plan.md +87 -0
  263. data/workflows/iterate_plan.md +247 -0
  264. data/workflows/research_codebase.md +210 -0
  265. data/workflows/resume_handoff.md +217 -0
  266. data/workflows/review_pr.md +320 -0
  267. data/workflows/thoughts_init.md +71 -0
  268. data/workflows/validate_plan.md +166 -0
  269. metadata +284 -2
  270. 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.