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.
Files changed (280) hide show
  1. checksums.yaml +4 -4
  2. data/.reek.yml +27 -1
  3. data/CHANGELOG.md +19 -0
  4. data/README.md +213 -43
  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 +3 -0
  11. data/app/channels/session_channel.rb +195 -45
  12. data/app/decorators/user_message_decorator.rb +16 -5
  13. data/app/jobs/agent_request_job.rb +55 -2
  14. data/app/jobs/analytical_brain_job.rb +33 -0
  15. data/app/jobs/count_event_tokens_job.rb +15 -4
  16. data/app/models/concerns/event/broadcasting.rb +81 -0
  17. data/app/models/event.rb +20 -1
  18. data/app/models/goal.rb +91 -0
  19. data/app/models/session.rb +366 -21
  20. data/config/application.rb +2 -0
  21. data/config/initializers/event_subscribers.rb +0 -1
  22. data/config/routes.rb +0 -6
  23. data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
  24. data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -0
  25. data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
  26. data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
  27. data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
  28. data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
  29. data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
  30. data/db/migrate/20260315140843_create_goals.rb +16 -0
  31. data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
  32. data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
  33. data/lib/agent_loop.rb +65 -6
  34. data/lib/agents/definition.rb +116 -0
  35. data/lib/agents/registry.rb +106 -0
  36. data/lib/analytical_brain/runner.rb +276 -0
  37. data/lib/analytical_brain/tools/activate_skill.rb +52 -0
  38. data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
  39. data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
  40. data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
  41. data/lib/analytical_brain/tools/finish_goal.rb +62 -0
  42. data/lib/analytical_brain/tools/read_workflow.rb +58 -0
  43. data/lib/analytical_brain/tools/rename_session.rb +63 -0
  44. data/lib/analytical_brain/tools/set_goal.rb +60 -0
  45. data/lib/analytical_brain/tools/update_goal.rb +60 -0
  46. data/lib/analytical_brain.rb +23 -0
  47. data/lib/anima/cli/mcp/secrets.rb +76 -0
  48. data/lib/anima/cli/mcp.rb +197 -0
  49. data/lib/anima/cli.rb +5 -40
  50. data/lib/anima/installer.rb +168 -0
  51. data/lib/anima/settings.rb +226 -0
  52. data/lib/anima/version.rb +1 -1
  53. data/lib/anima.rb +9 -0
  54. data/lib/credential_store.rb +103 -0
  55. data/lib/environment_probe.rb +232 -0
  56. data/lib/events/subscribers/persister.rb +1 -0
  57. data/lib/events/user_message.rb +17 -0
  58. data/lib/llm/client.rb +29 -10
  59. data/lib/mcp/client_manager.rb +86 -0
  60. data/lib/mcp/config.rb +213 -0
  61. data/lib/mcp/health_check.rb +77 -0
  62. data/lib/mcp/secrets.rb +73 -0
  63. data/lib/mcp/stdio_transport.rb +206 -0
  64. data/lib/providers/anthropic.rb +11 -20
  65. data/lib/shell_session.rb +11 -10
  66. data/lib/skills/definition.rb +97 -0
  67. data/lib/skills/registry.rb +105 -0
  68. data/lib/tools/edit.rb +226 -0
  69. data/lib/tools/mcp_tool.rb +114 -0
  70. data/lib/tools/read.rb +151 -0
  71. data/lib/tools/registry.rb +14 -12
  72. data/lib/tools/request_feature.rb +121 -0
  73. data/lib/tools/return_result.rb +81 -0
  74. data/lib/tools/spawn_specialist.rb +109 -0
  75. data/lib/tools/spawn_subagent.rb +111 -0
  76. data/lib/tools/subagent_prompts.rb +12 -0
  77. data/lib/tools/web_get.rb +8 -9
  78. data/lib/tools/write.rb +86 -0
  79. data/lib/tui/app.rb +985 -26
  80. data/lib/tui/cable_client.rb +69 -31
  81. data/lib/tui/message_store.rb +103 -8
  82. data/lib/tui/screens/chat.rb +293 -45
  83. data/lib/workflows/definition.rb +97 -0
  84. data/lib/workflows/registry.rb +89 -0
  85. data/skills/activerecord/SKILL.md +255 -0
  86. data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
  87. data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
  88. data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
  89. data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
  90. data/skills/activerecord/examples/associations/self_referential.rb +302 -0
  91. data/skills/activerecord/examples/associations/through_associations.rb +203 -0
  92. data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
  93. data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
  94. data/skills/activerecord/examples/basics/inheritance.rb +377 -0
  95. data/skills/activerecord/examples/basics/type_casting.rb +317 -0
  96. data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
  97. data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
  98. data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
  99. data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
  100. data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
  101. data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
  102. data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
  103. data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
  104. data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
  105. data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
  106. data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
  107. data/skills/activerecord/examples/querying/optimization.rb +275 -0
  108. data/skills/activerecord/examples/querying/scopes.rb +260 -0
  109. data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
  110. data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
  111. data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
  112. data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
  113. data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
  114. data/skills/activerecord/references/associations.md +709 -0
  115. data/skills/activerecord/references/basics.md +622 -0
  116. data/skills/activerecord/references/callbacks.md +738 -0
  117. data/skills/activerecord/references/migrations.md +657 -0
  118. data/skills/activerecord/references/querying.md +655 -0
  119. data/skills/activerecord/references/validations.md +596 -0
  120. data/skills/dragonruby/SKILL.md +250 -0
  121. data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
  122. data/skills/dragonruby/examples/audio/background_music.rb +29 -0
  123. data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
  124. data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
  125. data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
  126. data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
  127. data/skills/dragonruby/examples/core/hello_world.rb +24 -0
  128. data/skills/dragonruby/examples/core/labels.rb +22 -0
  129. data/skills/dragonruby/examples/core/sprites.rb +35 -0
  130. data/skills/dragonruby/examples/core/state_management.rb +29 -0
  131. data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
  132. data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
  133. data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
  134. data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
  135. data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
  136. data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
  137. data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
  138. data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
  139. data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
  140. data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
  141. data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
  142. data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
  143. data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
  144. data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
  145. data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
  146. data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
  147. data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
  148. data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
  149. data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
  150. data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
  151. data/skills/dragonruby/examples/input/controller_input.rb +28 -0
  152. data/skills/dragonruby/examples/input/directional_input.rb +24 -0
  153. data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
  154. data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
  155. data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
  156. data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
  157. data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
  158. data/skills/dragonruby/examples/rendering/labels.rb +32 -0
  159. data/skills/dragonruby/examples/rendering/layering.rb +51 -0
  160. data/skills/dragonruby/examples/rendering/solids.rb +61 -0
  161. data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
  162. data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
  163. data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
  164. data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
  165. data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
  166. data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
  167. data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
  168. data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
  169. data/skills/dragonruby/references/audio.md +396 -0
  170. data/skills/dragonruby/references/core.md +385 -0
  171. data/skills/dragonruby/references/distribution.md +434 -0
  172. data/skills/dragonruby/references/entities.md +516 -0
  173. data/skills/dragonruby/references/game-logic/persistence.md +386 -0
  174. data/skills/dragonruby/references/game-logic/state.md +389 -0
  175. data/skills/dragonruby/references/input.md +414 -0
  176. data/skills/dragonruby/references/rendering/animation.md +467 -0
  177. data/skills/dragonruby/references/rendering/primitives.md +403 -0
  178. data/skills/dragonruby/references/scenes.md +443 -0
  179. data/skills/draper-decorators/SKILL.md +344 -0
  180. data/skills/draper-decorators/examples/application_decorator.rb +61 -0
  181. data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
  182. data/skills/draper-decorators/examples/model_decorator.rb +152 -0
  183. data/skills/draper-decorators/references/anti-patterns.md +640 -0
  184. data/skills/draper-decorators/references/patterns.md +507 -0
  185. data/skills/draper-decorators/references/testing.md +559 -0
  186. data/skills/gh-issue.md +182 -0
  187. data/skills/mcp-server/SKILL.md +177 -0
  188. data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
  189. data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
  190. data/skills/mcp-server/examples/http_client.rb +48 -0
  191. data/skills/mcp-server/examples/http_server.rb +97 -0
  192. data/skills/mcp-server/examples/rails_integration.rb +88 -0
  193. data/skills/mcp-server/examples/stdio_server.rb +108 -0
  194. data/skills/mcp-server/examples/streaming_client.rb +95 -0
  195. data/skills/mcp-server/references/gotchas.md +183 -0
  196. data/skills/mcp-server/references/prompts.md +98 -0
  197. data/skills/mcp-server/references/resources.md +53 -0
  198. data/skills/mcp-server/references/server.md +140 -0
  199. data/skills/mcp-server/references/tools.md +146 -0
  200. data/skills/mcp-server/references/transport.md +104 -0
  201. data/skills/ratatui-ruby/SKILL.md +315 -0
  202. data/skills/ratatui-ruby/references/core-concepts.md +340 -0
  203. data/skills/ratatui-ruby/references/events.md +387 -0
  204. data/skills/ratatui-ruby/references/frameworks.md +522 -0
  205. data/skills/ratatui-ruby/references/layout.md +423 -0
  206. data/skills/ratatui-ruby/references/styling.md +268 -0
  207. data/skills/ratatui-ruby/references/testing.md +433 -0
  208. data/skills/ratatui-ruby/references/widgets.md +532 -0
  209. data/skills/rspec/SKILL.md +340 -0
  210. data/skills/rspec/examples/core/basic_structure.rb +69 -0
  211. data/skills/rspec/examples/core/configuration.rb +126 -0
  212. data/skills/rspec/examples/core/hooks.rb +126 -0
  213. data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
  214. data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
  215. data/skills/rspec/examples/core/shared_examples.rb +145 -0
  216. data/skills/rspec/examples/factory_bot/associations.rb +314 -0
  217. data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
  218. data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
  219. data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
  220. data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
  221. data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
  222. data/skills/rspec/examples/factory_bot/traits.rb +293 -0
  223. data/skills/rspec/examples/factory_bot/transients.rb +229 -0
  224. data/skills/rspec/examples/matchers/change.rb +115 -0
  225. data/skills/rspec/examples/matchers/collections.rb +154 -0
  226. data/skills/rspec/examples/matchers/comparisons.rb +79 -0
  227. data/skills/rspec/examples/matchers/composing.rb +155 -0
  228. data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
  229. data/skills/rspec/examples/matchers/equality.rb +58 -0
  230. data/skills/rspec/examples/matchers/errors.rb +136 -0
  231. data/skills/rspec/examples/matchers/output.rb +103 -0
  232. data/skills/rspec/examples/matchers/predicates.rb +87 -0
  233. data/skills/rspec/examples/matchers/truthiness.rb +101 -0
  234. data/skills/rspec/examples/matchers/types.rb +82 -0
  235. data/skills/rspec/examples/matchers/yield.rb +147 -0
  236. data/skills/rspec/examples/mocks/any_instance.rb +172 -0
  237. data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
  238. data/skills/rspec/examples/mocks/constants.rb +177 -0
  239. data/skills/rspec/examples/mocks/doubles.rb +139 -0
  240. data/skills/rspec/examples/mocks/expectations.rb +137 -0
  241. data/skills/rspec/examples/mocks/message_chains.rb +173 -0
  242. data/skills/rspec/examples/mocks/ordering.rb +144 -0
  243. data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
  244. data/skills/rspec/examples/mocks/responses.rb +223 -0
  245. data/skills/rspec/examples/mocks/spies.rb +149 -0
  246. data/skills/rspec/examples/mocks/stubbing.rb +133 -0
  247. data/skills/rspec/examples/rails/channels.rb +250 -0
  248. data/skills/rspec/examples/rails/controller_specs.rb +302 -0
  249. data/skills/rspec/examples/rails/helper_specs.rb +245 -0
  250. data/skills/rspec/examples/rails/job_specs.rb +256 -0
  251. data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
  252. data/skills/rspec/examples/rails/matchers.rb +374 -0
  253. data/skills/rspec/examples/rails/model_specs.rb +193 -0
  254. data/skills/rspec/examples/rails/request_specs.rb +275 -0
  255. data/skills/rspec/examples/rails/routing_specs.rb +276 -0
  256. data/skills/rspec/examples/rails/system_specs.rb +294 -0
  257. data/skills/rspec/examples/rails/transactions.rb +254 -0
  258. data/skills/rspec/examples/rails/view_specs.rb +252 -0
  259. data/skills/rspec/references/core.md +816 -0
  260. data/skills/rspec/references/factory_bot.md +641 -0
  261. data/skills/rspec/references/matchers.md +516 -0
  262. data/skills/rspec/references/mocks.md +381 -0
  263. data/skills/rspec/references/rails.md +528 -0
  264. data/templates/soul.md +40 -0
  265. data/workflows/commit.md +45 -0
  266. data/workflows/create_handoff.md +98 -0
  267. data/workflows/create_note.md +82 -0
  268. data/workflows/create_plan.md +457 -0
  269. data/workflows/decompose_ticket.md +109 -0
  270. data/workflows/feature.md +91 -0
  271. data/workflows/implement_plan.md +87 -0
  272. data/workflows/iterate_plan.md +247 -0
  273. data/workflows/research_codebase.md +210 -0
  274. data/workflows/resume_handoff.md +217 -0
  275. data/workflows/review_pr.md +320 -0
  276. data/workflows/thoughts_init.md +71 -0
  277. data/workflows/validate_plan.md +166 -0
  278. metadata +290 -3
  279. data/app/controllers/api/sessions_controller.rb +0 -25
  280. data/lib/events/subscribers/action_cable_bridge.rb +0 -59
@@ -0,0 +1,420 @@
1
+ # ActiveRecord Migration Examples: Safe Production Patterns
2
+ # Patterns for zero-downtime deployments and large table migrations
3
+
4
+ # =============================================================================
5
+ # SAFE COLUMN REMOVAL (3-Release Process)
6
+ # =============================================================================
7
+
8
+ # RELEASE 1: Add to ignored_columns in model
9
+ # app/models/user.rb
10
+ class User < ApplicationRecord
11
+ # Step 1: Tell ActiveRecord to ignore this column
12
+ # This prevents errors when old code still references it
13
+ self.ignored_columns += ["legacy_field"]
14
+ end
15
+
16
+ # RELEASE 2: Drop the column
17
+ class RemoveLegacyField < ActiveRecord::Migration[7.2]
18
+ def change
19
+ # Safe to remove now - code doesn't reference it
20
+ remove_column :users, :legacy_field, :string
21
+ end
22
+ end
23
+
24
+ # RELEASE 3: Remove ignored_columns line from model
25
+
26
+ # =============================================================================
27
+ # SAFE NOT NULL ADDITION (PostgreSQL)
28
+ # =============================================================================
29
+
30
+ # Adding NOT NULL to existing column can lock table while validating all rows
31
+ # Split into multiple steps for large tables
32
+
33
+ # Step 1: Add check constraint without validation
34
+ class AddNotNullConstraintStep1 < ActiveRecord::Migration[7.2]
35
+ def change
36
+ add_check_constraint :users, "email IS NOT NULL",
37
+ name: "users_email_not_null",
38
+ validate: false
39
+ end
40
+ end
41
+
42
+ # Step 2: Validate constraint (separate deployment)
43
+ class AddNotNullConstraintStep2 < ActiveRecord::Migration[7.2]
44
+ def change
45
+ validate_check_constraint :users, name: "users_email_not_null"
46
+ end
47
+ end
48
+
49
+ # Step 3: Add actual NOT NULL and remove constraint
50
+ class AddNotNullConstraintStep3 < ActiveRecord::Migration[7.2]
51
+ def change
52
+ change_column_null :users, :email, false
53
+ remove_check_constraint :users, name: "users_email_not_null"
54
+ end
55
+ end
56
+
57
+ # =============================================================================
58
+ # SAFE COLUMN ADDITION WITH DEFAULT
59
+ # =============================================================================
60
+
61
+ # Modern Rails (5.2+) handles this efficiently, but for very large tables:
62
+
63
+ # Option 1: Add column, backfill, then set default
64
+ class AddStatusSafely < ActiveRecord::Migration[7.2]
65
+ disable_ddl_transaction!
66
+
67
+ def up
68
+ # Step 1: Add column without default
69
+ add_column :orders, :priority, :string
70
+
71
+ # Step 2: Backfill in batches
72
+ Order.unscoped.in_batches(of: 10_000) do |batch|
73
+ batch.update_all(priority: "normal")
74
+ sleep(0.1) # Throttle
75
+ end
76
+
77
+ # Step 3: Set default for new records
78
+ change_column_default :orders, :priority, "normal"
79
+
80
+ # Step 4: Add NOT NULL if needed
81
+ change_column_null :orders, :priority, false
82
+ end
83
+
84
+ def down
85
+ remove_column :orders, :priority
86
+ end
87
+ end
88
+
89
+ # =============================================================================
90
+ # SAFE COLUMN RENAME (Dual-Write Pattern)
91
+ # =============================================================================
92
+
93
+ # Renaming columns breaks running code that references old name
94
+ # Use alias_attribute + phased deployment
95
+
96
+ # RELEASE 1: Add new column, dual-write
97
+ class RenameNameToFullNameStep1 < ActiveRecord::Migration[7.2]
98
+ def change
99
+ add_column :users, :full_name, :string
100
+ end
101
+ end
102
+
103
+ # app/models/user.rb (Release 1)
104
+ class User < ApplicationRecord
105
+ # Dual-write to both columns
106
+ before_save :sync_name_columns
107
+
108
+ # Read from new column, fall back to old
109
+ def full_name
110
+ super || name
111
+ end
112
+
113
+ private
114
+
115
+ def sync_name_columns
116
+ self.full_name = name if name_changed?
117
+ self.name = full_name if full_name_changed?
118
+ end
119
+ end
120
+
121
+ # RELEASE 2: Backfill existing data
122
+ class RenameNameToFullNameStep2 < ActiveRecord::Migration[7.2]
123
+ disable_ddl_transaction!
124
+
125
+ def up
126
+ User.unscoped.where(full_name: nil).in_batches(of: 10_000) do |batch|
127
+ batch.update_all("full_name = name")
128
+ sleep(0.1)
129
+ end
130
+ end
131
+ end
132
+
133
+ # RELEASE 3: Switch to new column, stop dual-write
134
+ # Update all code to use full_name, remove sync callback
135
+
136
+ # RELEASE 4: Remove old column (use safe removal pattern)
137
+ class RenameNameToFullNameStep4 < ActiveRecord::Migration[7.2]
138
+ def change
139
+ remove_column :users, :name, :string
140
+ end
141
+ end
142
+
143
+ # =============================================================================
144
+ # SAFE TYPE CHANGE
145
+ # =============================================================================
146
+
147
+ # Changing column type often rewrites entire table
148
+ # Use new column + migration instead
149
+
150
+ class ChangeIdToUuid < ActiveRecord::Migration[7.2]
151
+ disable_ddl_transaction!
152
+
153
+ def up
154
+ # Add new UUID column
155
+ add_column :products, :uuid, :uuid, default: "gen_random_uuid()"
156
+
157
+ # Backfill existing records
158
+ Product.unscoped.in_batches(of: 10_000) do |batch|
159
+ batch.update_all("uuid = gen_random_uuid()")
160
+ sleep(0.1)
161
+ end
162
+
163
+ # Add unique index
164
+ add_index :products, :uuid, unique: true, algorithm: :concurrently
165
+
166
+ # Now update foreign keys and code to use uuid
167
+ # Then in later migration, remove old id column
168
+ end
169
+
170
+ def down
171
+ remove_column :products, :uuid
172
+ end
173
+ end
174
+
175
+ # =============================================================================
176
+ # SAFE INDEX CREATION
177
+ # =============================================================================
178
+
179
+ class AddIndexSafely < ActiveRecord::Migration[7.2]
180
+ disable_ddl_transaction! # Required for concurrent
181
+
182
+ def change
183
+ add_index :users, :email,
184
+ unique: true,
185
+ algorithm: :concurrently,
186
+ name: "index_users_on_email_unique"
187
+ end
188
+ end
189
+
190
+ # With if_not_exists for idempotency
191
+ class AddIndexIdempotent < ActiveRecord::Migration[7.2]
192
+ disable_ddl_transaction!
193
+
194
+ def change
195
+ add_index :users, :email, algorithm: :concurrently, if_not_exists: true
196
+ end
197
+ end
198
+
199
+ # =============================================================================
200
+ # SAFE FOREIGN KEY ADDITION
201
+ # =============================================================================
202
+
203
+ # Foreign keys validate all rows on creation, which can lock tables
204
+
205
+ # Step 1: Add without validation
206
+ class AddForeignKeySafely1 < ActiveRecord::Migration[7.2]
207
+ def change
208
+ add_foreign_key :orders, :users, validate: false
209
+ end
210
+ end
211
+
212
+ # Step 2: Validate in separate migration
213
+ class AddForeignKeySafely2 < ActiveRecord::Migration[7.2]
214
+ def change
215
+ validate_foreign_key :orders, :users
216
+ end
217
+ end
218
+
219
+ # =============================================================================
220
+ # SAFE DATA BACKFILL
221
+ # =============================================================================
222
+
223
+ class BackfillUserStatus < ActiveRecord::Migration[7.2]
224
+ disable_ddl_transaction!
225
+
226
+ def up
227
+ # Process in batches to avoid memory issues and long locks
228
+ loop do
229
+ # Find batch of records to update
230
+ count = User.unscoped
231
+ .where(status: nil)
232
+ .limit(10_000)
233
+ .update_all(status: "active")
234
+
235
+ break if count.zero?
236
+
237
+ sleep(0.1) # Throttle to reduce database load
238
+ end
239
+ end
240
+ end
241
+
242
+ # Using find_each for complex logic
243
+ class BackfillCalculatedField < ActiveRecord::Migration[7.2]
244
+ disable_ddl_transaction!
245
+
246
+ # Define local model to avoid depending on app code
247
+ class Order < ApplicationRecord
248
+ self.table_name = "orders"
249
+ end
250
+
251
+ def up
252
+ Order.unscoped.where(total_cents: nil).find_each(batch_size: 1000) do |order|
253
+ total = order.subtotal_cents.to_i + order.tax_cents.to_i
254
+ order.update_columns(total_cents: total)
255
+ end
256
+ end
257
+ end
258
+
259
+ # =============================================================================
260
+ # MIGRATION WITH LOCK TIMEOUT
261
+ # =============================================================================
262
+
263
+ class MigrationWithTimeout < ActiveRecord::Migration[7.2]
264
+ def change
265
+ # Set lock timeout to avoid blocking for too long
266
+ execute "SET lock_timeout = '5s'"
267
+
268
+ begin
269
+ add_column :users, :verified, :boolean, default: false
270
+ ensure
271
+ execute "SET lock_timeout = DEFAULT"
272
+ end
273
+ end
274
+ end
275
+
276
+ # =============================================================================
277
+ # SAFE TABLE COPY PATTERN
278
+ # =============================================================================
279
+
280
+ # For massive schema changes, copy to new table
281
+
282
+ class RestructureProductsTable < ActiveRecord::Migration[7.2]
283
+ def up
284
+ # Create new table with desired schema
285
+ create_table :products_v2 do |t|
286
+ t.string :name, null: false
287
+ t.decimal :price_cents, precision: 12, scale: 0, null: false
288
+ # ... new schema
289
+ t.timestamps
290
+ end
291
+
292
+ # Copy data (in production, do this in background job)
293
+ execute <<~SQL
294
+ INSERT INTO products_v2 (id, name, price_cents, created_at, updated_at)
295
+ SELECT id, name, (price * 100)::bigint, created_at, updated_at
296
+ FROM products
297
+ SQL
298
+
299
+ # Rename tables
300
+ rename_table :products, :products_legacy
301
+ rename_table :products_v2, :products
302
+
303
+ # Later: drop legacy table after verification
304
+ end
305
+
306
+ def down
307
+ rename_table :products, :products_v2
308
+ rename_table :products_legacy, :products
309
+ drop_table :products_v2
310
+ end
311
+ end
312
+
313
+ # =============================================================================
314
+ # ENUM CHANGES (PostgreSQL)
315
+ # =============================================================================
316
+
317
+ # Adding enum values requires disable_ddl_transaction
318
+ class AddEnumValue < ActiveRecord::Migration[7.2]
319
+ disable_ddl_transaction!
320
+
321
+ def up
322
+ execute "ALTER TYPE order_status ADD VALUE 'refunded'"
323
+ end
324
+
325
+ def down
326
+ # Cannot remove enum values easily in PostgreSQL
327
+ # Would need to recreate enum type
328
+ raise ActiveRecord::IrreversibleMigration
329
+ end
330
+ end
331
+
332
+ # =============================================================================
333
+ # IDEMPOTENT MIGRATIONS
334
+ # =============================================================================
335
+
336
+ # Make migrations safe to run multiple times (useful for retry scenarios)
337
+
338
+ class IdempotentMigration < ActiveRecord::Migration[7.2]
339
+ def change
340
+ # Check before creating
341
+ unless table_exists?(:audits)
342
+ create_table :audits do |t|
343
+ t.string :action
344
+ t.timestamps
345
+ end
346
+ end
347
+
348
+ # Check before adding column
349
+ unless column_exists?(:users, :audit_count)
350
+ add_column :users, :audit_count, :integer, default: 0
351
+ end
352
+
353
+ # Check before adding index
354
+ unless index_exists?(:users, :audit_count)
355
+ add_index :users, :audit_count
356
+ end
357
+ end
358
+ end
359
+
360
+ # =============================================================================
361
+ # POST-DEPLOYMENT MIGRATIONS
362
+ # =============================================================================
363
+
364
+ # For non-critical operations that can run after deployment
365
+ # These don't block the deploy and can take longer
366
+
367
+ # Place in db/post_migrate/ if using GitLab-style setup
368
+ # Or run via separate rake task
369
+
370
+ class PostDeploymentCleanup < ActiveRecord::Migration[7.2]
371
+ disable_ddl_transaction!
372
+
373
+ def up
374
+ # Remove unused indexes (non-blocking)
375
+ remove_index :orders, :legacy_column, algorithm: :concurrently, if_exists: true
376
+
377
+ # Clean up orphaned data
378
+ execute "DELETE FROM order_items WHERE order_id NOT IN (SELECT id FROM orders)"
379
+
380
+ # Add new index (non-blocking)
381
+ add_index :orders, :new_column, algorithm: :concurrently, if_not_exists: true
382
+ end
383
+ end
384
+
385
+ # =============================================================================
386
+ # USING strong_migrations GEM
387
+ # =============================================================================
388
+
389
+ # With strong_migrations installed, dangerous operations are blocked
390
+ # Use safety_assured only after careful review
391
+
392
+ class MigrationWithStrongMigrations < ActiveRecord::Migration[7.2]
393
+ def change
394
+ # This would normally be blocked
395
+ safety_assured do
396
+ add_column :users, :settings, :jsonb, default: {}
397
+ end
398
+ end
399
+ end
400
+
401
+ # Better: Follow recommended pattern
402
+ class SafeAddColumnWithDefault < ActiveRecord::Migration[7.2]
403
+ disable_ddl_transaction!
404
+
405
+ def change
406
+ # Add column without default first
407
+ add_column :users, :settings, :jsonb
408
+
409
+ # Backfill (for existing records)
410
+ User.unscoped.in_batches do |batch|
411
+ batch.update_all(settings: {})
412
+ end
413
+
414
+ # Set default for new records
415
+ change_column_default :users, :settings, {}
416
+
417
+ # Add NOT NULL if needed
418
+ change_column_null :users, :settings, false
419
+ end
420
+ end
@@ -0,0 +1,277 @@
1
+ # ActiveRecord Migration Examples: Schema Changes
2
+ # Run migrations: rails db:migrate
3
+ # Rollback: rails db:rollback
4
+
5
+ # =============================================================================
6
+ # CREATING TABLES
7
+ # =============================================================================
8
+
9
+ # Basic table creation
10
+ class CreateProducts < ActiveRecord::Migration[7.2]
11
+ def change
12
+ create_table :products do |t|
13
+ t.string :name, null: false
14
+ t.string :sku, null: false
15
+ t.text :description
16
+ t.decimal :price, precision: 10, scale: 2, null: false
17
+ t.integer :quantity, default: 0
18
+ t.boolean :active, default: true
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :products, :sku, unique: true
23
+ end
24
+ end
25
+
26
+ # Table with foreign key reference
27
+ class CreateOrderItems < ActiveRecord::Migration[7.2]
28
+ def change
29
+ create_table :order_items do |t|
30
+ t.references :order, null: false, foreign_key: true
31
+ t.references :product, null: false, foreign_key: true
32
+ t.integer :quantity, null: false, default: 1
33
+ t.decimal :unit_price, precision: 10, scale: 2, null: false
34
+ t.timestamps
35
+ end
36
+ end
37
+ end
38
+
39
+ # Join table (no primary key)
40
+ class CreateProductsCategories < ActiveRecord::Migration[7.2]
41
+ def change
42
+ create_join_table :products, :categories do |t|
43
+ t.index [:product_id, :category_id], unique: true
44
+ t.index :category_id
45
+ end
46
+ end
47
+ end
48
+
49
+ # Table with UUID primary key
50
+ class CreateApiKeys < ActiveRecord::Migration[7.2]
51
+ def change
52
+ create_table :api_keys, id: :uuid do |t|
53
+ t.references :user, null: false, foreign_key: true
54
+ t.string :name, null: false
55
+ t.string :key_digest, null: false
56
+ t.datetime :expires_at
57
+ t.datetime :last_used_at
58
+ t.timestamps
59
+ end
60
+
61
+ add_index :api_keys, :key_digest, unique: true
62
+ end
63
+ end
64
+
65
+ # Table with composite primary key
66
+ class CreateTenantUsers < ActiveRecord::Migration[7.2]
67
+ def change
68
+ create_table :tenant_users, primary_key: [:tenant_id, :user_id] do |t|
69
+ t.bigint :tenant_id, null: false
70
+ t.bigint :user_id, null: false
71
+ t.string :role, null: false, default: "member"
72
+ t.timestamps
73
+ end
74
+ end
75
+ end
76
+
77
+ # Polymorphic association table
78
+ class CreateComments < ActiveRecord::Migration[7.2]
79
+ def change
80
+ create_table :comments do |t|
81
+ t.references :commentable, polymorphic: true, null: false
82
+ t.references :author, null: false, foreign_key: { to_table: :users }
83
+ t.text :body, null: false
84
+ t.timestamps
85
+ end
86
+
87
+ add_index :comments, [:commentable_type, :commentable_id]
88
+ end
89
+ end
90
+
91
+ # =============================================================================
92
+ # ADDING COLUMNS
93
+ # =============================================================================
94
+
95
+ class AddFieldsToUsers < ActiveRecord::Migration[7.2]
96
+ def change
97
+ # Simple columns
98
+ add_column :users, :phone, :string
99
+ add_column :users, :verified_at, :datetime
100
+
101
+ # Column with default
102
+ add_column :users, :locale, :string, default: "en", null: false
103
+
104
+ # JSON/JSONB column (PostgreSQL)
105
+ add_column :users, :preferences, :jsonb, default: {}
106
+ add_column :users, :metadata, :jsonb, default: {}
107
+
108
+ # Add index on JSON field (PostgreSQL GIN index)
109
+ add_index :users, :preferences, using: :gin
110
+ end
111
+ end
112
+
113
+ # Adding reference column
114
+ class AddOrganizationToUsers < ActiveRecord::Migration[7.2]
115
+ def change
116
+ # Creates user_id column with index and foreign key
117
+ add_reference :users, :organization, foreign_key: true
118
+
119
+ # Without index
120
+ add_reference :users, :invited_by, foreign_key: { to_table: :users }, index: false
121
+
122
+ # Polymorphic reference (no foreign key possible)
123
+ add_reference :attachments, :attachable, polymorphic: true, index: true
124
+ end
125
+ end
126
+
127
+ # =============================================================================
128
+ # MODIFYING COLUMNS
129
+ # =============================================================================
130
+
131
+ # Renaming columns (reversible)
132
+ class RenameUserFields < ActiveRecord::Migration[7.2]
133
+ def change
134
+ rename_column :users, :name, :full_name
135
+ rename_column :users, :type, :account_type # Avoid STI conflict
136
+ end
137
+ end
138
+
139
+ # Changing defaults (reversible with from/to)
140
+ class ChangeUserDefaults < ActiveRecord::Migration[7.2]
141
+ def change
142
+ change_column_default :users, :status, from: nil, to: "pending"
143
+ change_column_default :users, :role, from: "user", to: "member"
144
+ end
145
+ end
146
+
147
+ # Changing null constraint
148
+ class AddNotNullToEmail < ActiveRecord::Migration[7.2]
149
+ def change
150
+ # Backfill NULLs first with default value
151
+ change_column_null :users, :email, false, "unknown@example.com"
152
+ end
153
+ end
154
+
155
+ # Changing column type (IRREVERSIBLE - requires up/down)
156
+ class ChangeDescriptionToText < ActiveRecord::Migration[7.2]
157
+ def up
158
+ change_column :products, :description, :text
159
+ end
160
+
161
+ def down
162
+ change_column :products, :description, :string
163
+ end
164
+ end
165
+
166
+ # Changing precision/scale
167
+ class IncreasePricePrecision < ActiveRecord::Migration[7.2]
168
+ def up
169
+ change_column :products, :price, :decimal, precision: 12, scale: 2
170
+ end
171
+
172
+ def down
173
+ change_column :products, :price, :decimal, precision: 10, scale: 2
174
+ end
175
+ end
176
+
177
+ # =============================================================================
178
+ # REMOVING COLUMNS
179
+ # =============================================================================
180
+
181
+ # Removing single column (must include type for reversibility)
182
+ class RemoveLegacyField < ActiveRecord::Migration[7.2]
183
+ def change
184
+ remove_column :users, :legacy_token, :string
185
+ end
186
+ end
187
+
188
+ # Removing multiple columns
189
+ class RemoveDeprecatedFields < ActiveRecord::Migration[7.2]
190
+ def change
191
+ remove_columns :users, :old_field1, :old_field2, type: :string
192
+ end
193
+ end
194
+
195
+ # Removing reference
196
+ class RemoveOrganizationFromUsers < ActiveRecord::Migration[7.2]
197
+ def change
198
+ remove_reference :users, :organization, foreign_key: true
199
+ end
200
+ end
201
+
202
+ # =============================================================================
203
+ # RENAMING TABLES
204
+ # =============================================================================
205
+
206
+ class RenameUsersToAccounts < ActiveRecord::Migration[7.2]
207
+ def change
208
+ rename_table :users, :accounts
209
+ end
210
+ end
211
+
212
+ # =============================================================================
213
+ # DROPPING TABLES
214
+ # =============================================================================
215
+
216
+ # Must include full schema for reversibility
217
+ class DropLegacyReports < ActiveRecord::Migration[7.2]
218
+ def change
219
+ drop_table :legacy_reports do |t|
220
+ t.string :name
221
+ t.text :data
222
+ t.references :user
223
+ t.timestamps
224
+ end
225
+ end
226
+ end
227
+
228
+ # Irreversible drop (when you don't need rollback)
229
+ class DropTemporaryTable < ActiveRecord::Migration[7.2]
230
+ def up
231
+ drop_table :temp_imports
232
+ end
233
+
234
+ def down
235
+ raise ActiveRecord::IrreversibleMigration
236
+ end
237
+ end
238
+
239
+ # =============================================================================
240
+ # PRACTICAL PATTERNS
241
+ # =============================================================================
242
+
243
+ # Adding column with index in one migration
244
+ class AddStatusToOrders < ActiveRecord::Migration[7.2]
245
+ def change
246
+ add_column :orders, :status, :string, default: "pending", null: false
247
+ add_index :orders, :status
248
+ end
249
+ end
250
+
251
+ # Adding timestamps to existing table
252
+ class AddTimestampsToProducts < ActiveRecord::Migration[7.2]
253
+ def change
254
+ # Adds created_at and updated_at with null: false, precision: 6
255
+ add_timestamps :products, default: -> { "CURRENT_TIMESTAMP" }
256
+ end
257
+ end
258
+
259
+ # Converting column with data preservation
260
+ class ConvertPriceToInteger < ActiveRecord::Migration[7.2]
261
+ def up
262
+ # Add new column
263
+ add_column :products, :price_cents, :integer
264
+
265
+ # Migrate data (for small tables)
266
+ execute "UPDATE products SET price_cents = (price * 100)::integer"
267
+
268
+ # Remove old column
269
+ remove_column :products, :price
270
+ end
271
+
272
+ def down
273
+ add_column :products, :price, :decimal, precision: 10, scale: 2
274
+ execute "UPDATE products SET price = price_cents / 100.0"
275
+ remove_column :products, :price_cents
276
+ end
277
+ end