anima-core 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.reek.yml +27 -1
- data/CHANGELOG.md +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 +3 -0
- 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 +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/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 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Transaction Callbacks Examples
|
|
2
|
+
# Demonstrates after_commit, after_rollback, and transaction gotchas
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# The Critical Difference: after_save vs after_commit
|
|
6
|
+
# =============================================================================
|
|
7
|
+
|
|
8
|
+
class Order < ApplicationRecord
|
|
9
|
+
# WRONG - Race condition with background jobs
|
|
10
|
+
# The job may start before the transaction commits
|
|
11
|
+
# after_save :enqueue_processing_wrong
|
|
12
|
+
|
|
13
|
+
# CORRECT - Job runs after transaction commits
|
|
14
|
+
after_commit :enqueue_processing, on: :create
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def enqueue_processing_wrong
|
|
19
|
+
# Sidekiq might query: "Couldn't find Order with 'id'=123"
|
|
20
|
+
# because the transaction hasn't committed yet
|
|
21
|
+
OrderProcessingJob.perform_later(id)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enqueue_processing
|
|
25
|
+
# Transaction committed - record guaranteed to exist
|
|
26
|
+
OrderProcessingJob.perform_later(id)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# =============================================================================
|
|
31
|
+
# Transaction Callback Variants (Rails 7.1+)
|
|
32
|
+
# =============================================================================
|
|
33
|
+
|
|
34
|
+
class Article < ApplicationRecord
|
|
35
|
+
# Fires on any commit (create, update, or destroy)
|
|
36
|
+
after_commit :clear_cache
|
|
37
|
+
|
|
38
|
+
# Scoped to specific actions
|
|
39
|
+
after_create_commit :notify_followers
|
|
40
|
+
after_update_commit :sync_to_search
|
|
41
|
+
after_destroy_commit :cleanup_attachments
|
|
42
|
+
|
|
43
|
+
# Fires on create OR update (not destroy)
|
|
44
|
+
after_save_commit :reindex_content
|
|
45
|
+
|
|
46
|
+
# Equivalent to after_create_commit + after_update_commit
|
|
47
|
+
after_commit :update_analytics, on: [:create, :update]
|
|
48
|
+
|
|
49
|
+
# Rollback callback
|
|
50
|
+
after_rollback :log_failure
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def clear_cache
|
|
55
|
+
Rails.cache.delete("article_#{id}")
|
|
56
|
+
Rails.cache.delete("articles_list")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def notify_followers
|
|
60
|
+
author.followers.find_each do |follower|
|
|
61
|
+
NotificationJob.perform_later(follower.id, "new_article", id)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sync_to_search
|
|
66
|
+
SearchIndexJob.perform_later("update", self.class.name, id)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def cleanup_attachments
|
|
70
|
+
AttachmentCleanupJob.perform_later(attachment_keys)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reindex_content
|
|
74
|
+
FullTextIndexJob.perform_later(id)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def update_analytics
|
|
78
|
+
AnalyticsJob.perform_later("article_saved", id: id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def log_failure
|
|
82
|
+
Rails.logger.error("Article #{id || 'new'} save failed, transaction rolled back")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# Gotcha: Callback Deduplication
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
class Product < ApplicationRecord
|
|
91
|
+
# WRONG - Only the LAST one runs due to deduplication
|
|
92
|
+
after_commit :sync_inventory
|
|
93
|
+
after_commit :sync_inventory # This one "wins"
|
|
94
|
+
|
|
95
|
+
# Also deduplicated across variants!
|
|
96
|
+
after_commit :notify_warehouse
|
|
97
|
+
after_create_commit :notify_warehouse # These are considered duplicates
|
|
98
|
+
after_save_commit :notify_warehouse
|
|
99
|
+
|
|
100
|
+
# CORRECT - Use :on option
|
|
101
|
+
after_commit :sync_inventory, on: [:create, :update]
|
|
102
|
+
after_commit :notify_warehouse, on: :create
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def sync_inventory
|
|
107
|
+
InventorySyncJob.perform_later(id)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def notify_warehouse
|
|
111
|
+
WarehouseNotificationJob.perform_later(id)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# =============================================================================
|
|
116
|
+
# Gotcha: Exception Handling in after_commit
|
|
117
|
+
# =============================================================================
|
|
118
|
+
|
|
119
|
+
class Payment < ApplicationRecord
|
|
120
|
+
after_commit :notify_external_service
|
|
121
|
+
after_commit :update_analytics
|
|
122
|
+
after_commit :send_receipt
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def notify_external_service
|
|
127
|
+
# If this raises, update_analytics and send_receipt won't run!
|
|
128
|
+
ExternalPaymentService.notify(self)
|
|
129
|
+
rescue ExternalPaymentService::Error => e
|
|
130
|
+
# Handle gracefully - don't let it bubble up
|
|
131
|
+
Rails.logger.error("External notification failed: #{e.message}")
|
|
132
|
+
ErrorTracker.capture(e)
|
|
133
|
+
# Don't re-raise - let other callbacks run
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def update_analytics
|
|
137
|
+
Analytics.track("payment_completed", amount:, user_id:)
|
|
138
|
+
rescue => e
|
|
139
|
+
Rails.logger.error("Analytics failed: #{e.message}")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def send_receipt
|
|
143
|
+
PaymentMailer.receipt(self).deliver_later
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# =============================================================================
|
|
148
|
+
# Gotcha: previous_changes in after_commit
|
|
149
|
+
# =============================================================================
|
|
150
|
+
|
|
151
|
+
class User < ApplicationRecord
|
|
152
|
+
after_commit :log_changes
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def log_changes
|
|
157
|
+
# WARNING: If the record was saved multiple times in one transaction,
|
|
158
|
+
# previous_changes only contains the LAST save's changes
|
|
159
|
+
|
|
160
|
+
# If transaction did: user.save, then user.save again
|
|
161
|
+
# previous_changes only shows changes from the second save
|
|
162
|
+
|
|
163
|
+
Rails.logger.info("User #{id} changed: #{previous_changes.inspect}")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# =============================================================================
|
|
168
|
+
# Gotcha: after_commit Also Fires on Destroy
|
|
169
|
+
# =============================================================================
|
|
170
|
+
|
|
171
|
+
class Subscription < ApplicationRecord
|
|
172
|
+
# Be careful! This fires on CREATE, UPDATE, AND DESTROY
|
|
173
|
+
after_commit :sync_to_billing
|
|
174
|
+
|
|
175
|
+
# If you only want create/update:
|
|
176
|
+
after_save_commit :sync_to_billing_safe
|
|
177
|
+
|
|
178
|
+
# Or be explicit:
|
|
179
|
+
after_commit :sync_to_billing_explicit, on: [:create, :update]
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def sync_to_billing
|
|
184
|
+
# This will run on destroy too - might not be what you want!
|
|
185
|
+
BillingService.sync(self)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def sync_to_billing_safe
|
|
189
|
+
BillingService.sync(self)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def sync_to_billing_explicit
|
|
193
|
+
BillingService.sync(self)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# =============================================================================
|
|
198
|
+
# Transaction Callback Ordering (Rails 7.1+)
|
|
199
|
+
# =============================================================================
|
|
200
|
+
|
|
201
|
+
class Invoice < ApplicationRecord
|
|
202
|
+
# Rails 7.1+ default: runs in definition order
|
|
203
|
+
after_commit :step_one # Runs first
|
|
204
|
+
after_commit :step_two # Runs second
|
|
205
|
+
after_commit :step_three # Runs third
|
|
206
|
+
|
|
207
|
+
# Pre-7.1 behavior (if configured): reverse order
|
|
208
|
+
# config.active_record.run_after_transaction_callbacks_in_order_defined = false
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
|
|
212
|
+
def step_one
|
|
213
|
+
Rails.logger.info("Step 1: Generate PDF")
|
|
214
|
+
PdfGeneratorJob.perform_later(id)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def step_two
|
|
218
|
+
Rails.logger.info("Step 2: Send email")
|
|
219
|
+
InvoiceMailer.send_invoice(self).deliver_later
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def step_three
|
|
223
|
+
Rails.logger.info("Step 3: Update dashboard")
|
|
224
|
+
DashboardRefreshJob.perform_later(user_id)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# =============================================================================
|
|
229
|
+
# Real-World Pattern: External System Sync
|
|
230
|
+
# =============================================================================
|
|
231
|
+
|
|
232
|
+
class Customer < ApplicationRecord
|
|
233
|
+
after_create_commit :create_in_crm
|
|
234
|
+
after_update_commit :update_in_crm
|
|
235
|
+
after_destroy_commit :delete_from_crm
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def create_in_crm
|
|
240
|
+
CrmSyncJob.perform_later("create", id)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def update_in_crm
|
|
244
|
+
# Only sync if relevant fields changed
|
|
245
|
+
relevant_changes = previous_changes.keys & %w[name email phone company]
|
|
246
|
+
return if relevant_changes.empty?
|
|
247
|
+
|
|
248
|
+
CrmSyncJob.perform_later("update", id, changes: previous_changes.slice(*relevant_changes))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def delete_from_crm
|
|
252
|
+
# Can't use id lookup since record is gone
|
|
253
|
+
CrmSyncJob.perform_later("delete", crm_external_id)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# =============================================================================
|
|
258
|
+
# Real-World Pattern: Search Index Management
|
|
259
|
+
# =============================================================================
|
|
260
|
+
|
|
261
|
+
class Post < ApplicationRecord
|
|
262
|
+
after_save_commit :update_search_index
|
|
263
|
+
after_destroy_commit :remove_from_search_index
|
|
264
|
+
|
|
265
|
+
private
|
|
266
|
+
|
|
267
|
+
def update_search_index
|
|
268
|
+
SearchIndexJob.perform_later(
|
|
269
|
+
action: "index",
|
|
270
|
+
type: "post",
|
|
271
|
+
id:,
|
|
272
|
+
body: {
|
|
273
|
+
title:,
|
|
274
|
+
content:,
|
|
275
|
+
author_name: author.name,
|
|
276
|
+
published_at:,
|
|
277
|
+
tags: tags.pluck(:name)
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def remove_from_search_index
|
|
283
|
+
SearchIndexJob.perform_later(
|
|
284
|
+
action: "delete",
|
|
285
|
+
type: "post",
|
|
286
|
+
id:
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# =============================================================================
|
|
292
|
+
# Real-World Pattern: Cache Invalidation
|
|
293
|
+
# =============================================================================
|
|
294
|
+
|
|
295
|
+
class Category < ApplicationRecord
|
|
296
|
+
has_many :products
|
|
297
|
+
|
|
298
|
+
after_commit :invalidate_caches
|
|
299
|
+
|
|
300
|
+
private
|
|
301
|
+
|
|
302
|
+
def invalidate_caches
|
|
303
|
+
Rails.cache.delete("category_#{id}")
|
|
304
|
+
Rails.cache.delete("category_#{id}_products")
|
|
305
|
+
Rails.cache.delete("categories_tree")
|
|
306
|
+
Rails.cache.delete("navigation_menu")
|
|
307
|
+
|
|
308
|
+
# Invalidate parent category caches if nested
|
|
309
|
+
parent&.invalidate_caches if parent_id_previously_changed?
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# =============================================================================
|
|
314
|
+
# Nested Transactions and Callbacks
|
|
315
|
+
# =============================================================================
|
|
316
|
+
|
|
317
|
+
class Transfer < ApplicationRecord
|
|
318
|
+
def self.create_with_ledger_entries!(from_account:, to_account:, amount:)
|
|
319
|
+
transaction do
|
|
320
|
+
transfer = create!(from_account:, to_account:, amount:)
|
|
321
|
+
|
|
322
|
+
# Nested transaction - callbacks still wait for outer commit
|
|
323
|
+
transaction(requires_new: true) do
|
|
324
|
+
LedgerEntry.create!(account: from_account, amount: -amount, transfer:)
|
|
325
|
+
LedgerEntry.create!(account: to_account, amount:, transfer:)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
transfer
|
|
329
|
+
end
|
|
330
|
+
# after_commit callbacks run here, after outer transaction commits
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
after_commit :notify_accounts, on: :create
|
|
334
|
+
|
|
335
|
+
private
|
|
336
|
+
|
|
337
|
+
def notify_accounts
|
|
338
|
+
TransferNotificationJob.perform_later(id)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# ActiveRecord Migration Examples: Indexes and Constraints
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# BASIC INDEXES
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
class AddBasicIndexes < ActiveRecord::Migration[7.2]
|
|
8
|
+
def change
|
|
9
|
+
# Single column index
|
|
10
|
+
add_index :users, :email
|
|
11
|
+
|
|
12
|
+
# Unique index
|
|
13
|
+
add_index :users, :username, unique: true
|
|
14
|
+
|
|
15
|
+
# Named index
|
|
16
|
+
add_index :users, :created_at, name: "idx_users_created"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# =============================================================================
|
|
21
|
+
# COMPOSITE INDEXES
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
# Column order matters! Most selective column first
|
|
25
|
+
class AddCompositeIndexes < ActiveRecord::Migration[7.2]
|
|
26
|
+
def change
|
|
27
|
+
# Good for: WHERE last_name = 'X' AND first_name = 'Y'
|
|
28
|
+
# Good for: WHERE last_name = 'X'
|
|
29
|
+
# NOT useful for: WHERE first_name = 'Y' alone
|
|
30
|
+
add_index :users, [:last_name, :first_name]
|
|
31
|
+
|
|
32
|
+
# Foreign key + status (common lookup pattern)
|
|
33
|
+
add_index :orders, [:user_id, :status]
|
|
34
|
+
|
|
35
|
+
# Polymorphic association (always add this!)
|
|
36
|
+
add_index :comments, [:commentable_type, :commentable_id]
|
|
37
|
+
|
|
38
|
+
# Unique composite
|
|
39
|
+
add_index :memberships, [:user_id, :organization_id], unique: true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# PARTIAL INDEXES (PostgreSQL, SQLite)
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
class AddPartialIndexes < ActiveRecord::Migration[7.2]
|
|
48
|
+
def change
|
|
49
|
+
# Index only active records (smaller, faster)
|
|
50
|
+
add_index :users, :email, where: "active = true", name: "idx_users_email_active"
|
|
51
|
+
|
|
52
|
+
# Index only non-null values
|
|
53
|
+
add_index :sessions, :user_id, where: "user_id IS NOT NULL"
|
|
54
|
+
|
|
55
|
+
# Index only pending orders
|
|
56
|
+
add_index :orders, :created_at, where: "status = 'pending'"
|
|
57
|
+
|
|
58
|
+
# Unique email only for non-deleted users (soft delete pattern)
|
|
59
|
+
add_index :users, :email, unique: true, where: "deleted_at IS NULL"
|
|
60
|
+
|
|
61
|
+
# Index only recent records
|
|
62
|
+
add_index :events, :created_at, where: "created_at > '2024-01-01'"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# EXPRESSION INDEXES (PostgreSQL)
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
class AddExpressionIndexes < ActiveRecord::Migration[7.2]
|
|
71
|
+
def change
|
|
72
|
+
# Case-insensitive email lookup
|
|
73
|
+
add_index :users, "lower(email)", unique: true, name: "idx_users_email_lower"
|
|
74
|
+
|
|
75
|
+
# Date extraction
|
|
76
|
+
add_index :events, "date(created_at)", name: "idx_events_created_date"
|
|
77
|
+
|
|
78
|
+
# JSON field indexing
|
|
79
|
+
add_index :users, "(preferences->>'theme')", name: "idx_users_theme"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# =============================================================================
|
|
84
|
+
# COVERING INDEXES (PostgreSQL)
|
|
85
|
+
# =============================================================================
|
|
86
|
+
|
|
87
|
+
class AddCoveringIndexes < ActiveRecord::Migration[7.2]
|
|
88
|
+
def change
|
|
89
|
+
# Include additional columns to avoid table lookups
|
|
90
|
+
# Useful when you always SELECT these columns with the WHERE clause
|
|
91
|
+
add_index :orders, :user_id, include: [:status, :total]
|
|
92
|
+
|
|
93
|
+
# SELECT status, total FROM orders WHERE user_id = ?
|
|
94
|
+
# Can be answered entirely from index!
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# =============================================================================
|
|
99
|
+
# CONCURRENT INDEXES (PostgreSQL)
|
|
100
|
+
# =============================================================================
|
|
101
|
+
|
|
102
|
+
class AddConcurrentIndex < ActiveRecord::Migration[7.2]
|
|
103
|
+
disable_ddl_transaction! # REQUIRED for concurrent operations
|
|
104
|
+
|
|
105
|
+
def change
|
|
106
|
+
# Won't lock the table during creation
|
|
107
|
+
add_index :users, :email, algorithm: :concurrently
|
|
108
|
+
|
|
109
|
+
# Remove index concurrently
|
|
110
|
+
remove_index :users, :old_column, algorithm: :concurrently
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Adding concurrent index with safety check
|
|
115
|
+
class AddEmailIndexSafely < ActiveRecord::Migration[7.2]
|
|
116
|
+
disable_ddl_transaction!
|
|
117
|
+
|
|
118
|
+
def change
|
|
119
|
+
# Check if index exists before creating
|
|
120
|
+
unless index_exists?(:users, :email, name: "index_users_on_email")
|
|
121
|
+
add_index :users, :email, algorithm: :concurrently
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# =============================================================================
|
|
127
|
+
# SPECIAL INDEX TYPES
|
|
128
|
+
# =============================================================================
|
|
129
|
+
|
|
130
|
+
class AddSpecialIndexes < ActiveRecord::Migration[7.2]
|
|
131
|
+
def change
|
|
132
|
+
# GIN index for JSONB (PostgreSQL)
|
|
133
|
+
add_index :products, :metadata, using: :gin
|
|
134
|
+
|
|
135
|
+
# GiST index for geometric/range types (PostgreSQL)
|
|
136
|
+
add_index :locations, :coordinates, using: :gist
|
|
137
|
+
|
|
138
|
+
# Full-text search index (PostgreSQL)
|
|
139
|
+
add_index :articles, "to_tsvector('english', title || ' ' || body)",
|
|
140
|
+
using: :gin, name: "idx_articles_fulltext"
|
|
141
|
+
|
|
142
|
+
# Trigram index for LIKE queries (PostgreSQL, requires pg_trgm)
|
|
143
|
+
add_index :products, :name, using: :gin, opclass: :gin_trgm_ops
|
|
144
|
+
|
|
145
|
+
# FULLTEXT index (MySQL)
|
|
146
|
+
# add_index :articles, [:title, :body], type: :fulltext
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# =============================================================================
|
|
151
|
+
# INDEX WITH ORDERING
|
|
152
|
+
# =============================================================================
|
|
153
|
+
|
|
154
|
+
class AddOrderedIndexes < ActiveRecord::Migration[7.2]
|
|
155
|
+
def change
|
|
156
|
+
# Descending order (useful for ORDER BY col DESC queries)
|
|
157
|
+
add_index :events, :created_at, order: :desc
|
|
158
|
+
|
|
159
|
+
# Mixed ordering
|
|
160
|
+
add_index :leaderboards, [:game_id, :score], order: { game_id: :asc, score: :desc }
|
|
161
|
+
|
|
162
|
+
# NULLS positioning (PostgreSQL)
|
|
163
|
+
add_index :tasks, :due_date, order: { due_date: "ASC NULLS LAST" }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# =============================================================================
|
|
168
|
+
# FOREIGN KEY CONSTRAINTS
|
|
169
|
+
# =============================================================================
|
|
170
|
+
|
|
171
|
+
class AddForeignKeys < ActiveRecord::Migration[7.2]
|
|
172
|
+
def change
|
|
173
|
+
# Basic foreign key
|
|
174
|
+
add_foreign_key :orders, :users
|
|
175
|
+
|
|
176
|
+
# With custom column name
|
|
177
|
+
add_foreign_key :orders, :users, column: :customer_id
|
|
178
|
+
|
|
179
|
+
# With ON DELETE behavior
|
|
180
|
+
add_foreign_key :comments, :posts, on_delete: :cascade
|
|
181
|
+
|
|
182
|
+
# With ON UPDATE behavior
|
|
183
|
+
add_foreign_key :order_items, :products, on_update: :cascade
|
|
184
|
+
|
|
185
|
+
# Referencing non-id primary key
|
|
186
|
+
add_foreign_key :profiles, :users, primary_key: :uuid, column: :user_uuid
|
|
187
|
+
|
|
188
|
+
# Self-referential
|
|
189
|
+
add_foreign_key :employees, :employees, column: :manager_id
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Foreign key with custom name
|
|
194
|
+
class AddNamedForeignKey < ActiveRecord::Migration[7.2]
|
|
195
|
+
def change
|
|
196
|
+
add_foreign_key :orders, :users,
|
|
197
|
+
column: :placed_by_id,
|
|
198
|
+
name: "fk_orders_placed_by_user"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# =============================================================================
|
|
203
|
+
# CHECK CONSTRAINTS
|
|
204
|
+
# =============================================================================
|
|
205
|
+
|
|
206
|
+
class AddCheckConstraints < ActiveRecord::Migration[7.2]
|
|
207
|
+
def change
|
|
208
|
+
# Positive price
|
|
209
|
+
add_check_constraint :products, "price > 0", name: "products_price_positive"
|
|
210
|
+
|
|
211
|
+
# Valid quantity
|
|
212
|
+
add_check_constraint :order_items, "quantity >= 1", name: "order_items_quantity_min"
|
|
213
|
+
|
|
214
|
+
# Status validation
|
|
215
|
+
add_check_constraint :orders, "status IN ('pending', 'processing', 'shipped', 'delivered')",
|
|
216
|
+
name: "orders_valid_status"
|
|
217
|
+
|
|
218
|
+
# Date range
|
|
219
|
+
add_check_constraint :events, "end_date >= start_date", name: "events_valid_dates"
|
|
220
|
+
|
|
221
|
+
# Email format (basic)
|
|
222
|
+
add_check_constraint :users, "email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'",
|
|
223
|
+
name: "users_valid_email"
|
|
224
|
+
|
|
225
|
+
# Percentage range
|
|
226
|
+
add_check_constraint :discounts, "percentage BETWEEN 0 AND 100",
|
|
227
|
+
name: "discounts_valid_percentage"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# PostgreSQL: Add constraint without validation (for large tables)
|
|
232
|
+
class AddConstraintSafely < ActiveRecord::Migration[7.2]
|
|
233
|
+
def change
|
|
234
|
+
# Step 1: Add without validation
|
|
235
|
+
add_check_constraint :products, "price > 0",
|
|
236
|
+
name: "products_price_positive",
|
|
237
|
+
validate: false
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
class ValidateConstraint < ActiveRecord::Migration[7.2]
|
|
242
|
+
def change
|
|
243
|
+
# Step 2: Validate in separate migration/deployment
|
|
244
|
+
validate_check_constraint :products, name: "products_price_positive"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# =============================================================================
|
|
249
|
+
# UNIQUE CONSTRAINTS
|
|
250
|
+
# =============================================================================
|
|
251
|
+
|
|
252
|
+
class AddUniqueConstraints < ActiveRecord::Migration[7.2]
|
|
253
|
+
def change
|
|
254
|
+
# Simple unique
|
|
255
|
+
add_index :users, :email, unique: true
|
|
256
|
+
|
|
257
|
+
# Composite unique
|
|
258
|
+
add_index :subscriptions, [:user_id, :plan_id], unique: true
|
|
259
|
+
|
|
260
|
+
# Unique with condition (soft delete)
|
|
261
|
+
add_index :users, :email, unique: true, where: "deleted_at IS NULL"
|
|
262
|
+
|
|
263
|
+
# Case-insensitive unique (PostgreSQL)
|
|
264
|
+
add_index :users, "lower(email)", unique: true, name: "idx_users_email_unique_lower"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# =============================================================================
|
|
269
|
+
# REMOVING INDEXES AND CONSTRAINTS
|
|
270
|
+
# =============================================================================
|
|
271
|
+
|
|
272
|
+
class RemoveIndexesAndConstraints < ActiveRecord::Migration[7.2]
|
|
273
|
+
def change
|
|
274
|
+
# Remove index by column (must specify column for reversibility)
|
|
275
|
+
remove_index :users, :email
|
|
276
|
+
|
|
277
|
+
# Remove index by name
|
|
278
|
+
remove_index :users, name: "idx_users_email_lower"
|
|
279
|
+
|
|
280
|
+
# Remove composite index
|
|
281
|
+
remove_index :orders, [:user_id, :status]
|
|
282
|
+
|
|
283
|
+
# Remove foreign key
|
|
284
|
+
remove_foreign_key :orders, :users
|
|
285
|
+
|
|
286
|
+
# Remove foreign key by column
|
|
287
|
+
remove_foreign_key :orders, column: :customer_id, to_table: :users
|
|
288
|
+
|
|
289
|
+
# Remove check constraint
|
|
290
|
+
remove_check_constraint :products, name: "products_price_positive"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# =============================================================================
|
|
295
|
+
# PRACTICAL PATTERNS
|
|
296
|
+
# =============================================================================
|
|
297
|
+
|
|
298
|
+
# Complete reference setup with all constraints
|
|
299
|
+
class CreateOrdersWithConstraints < ActiveRecord::Migration[7.2]
|
|
300
|
+
def change
|
|
301
|
+
create_table :orders do |t|
|
|
302
|
+
t.references :user, null: false
|
|
303
|
+
t.references :shipping_address, null: false
|
|
304
|
+
t.string :status, null: false, default: "pending"
|
|
305
|
+
t.decimal :subtotal, precision: 10, scale: 2, null: false
|
|
306
|
+
t.decimal :tax, precision: 10, scale: 2, null: false
|
|
307
|
+
t.decimal :total, precision: 10, scale: 2, null: false
|
|
308
|
+
t.timestamps
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Foreign keys
|
|
312
|
+
add_foreign_key :orders, :users, on_delete: :restrict
|
|
313
|
+
add_foreign_key :orders, :addresses, column: :shipping_address_id
|
|
314
|
+
|
|
315
|
+
# Check constraints
|
|
316
|
+
add_check_constraint :orders, "subtotal >= 0", name: "orders_subtotal_non_negative"
|
|
317
|
+
add_check_constraint :orders, "tax >= 0", name: "orders_tax_non_negative"
|
|
318
|
+
add_check_constraint :orders, "total = subtotal + tax", name: "orders_total_calculation"
|
|
319
|
+
add_check_constraint :orders, "status IN ('pending', 'paid', 'shipped', 'delivered', 'cancelled')",
|
|
320
|
+
name: "orders_valid_status"
|
|
321
|
+
|
|
322
|
+
# Indexes for common queries
|
|
323
|
+
add_index :orders, [:user_id, :status]
|
|
324
|
+
add_index :orders, [:status, :created_at]
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Index strategy for polymorphic association
|
|
329
|
+
class SetupPolymorphicIndexes < ActiveRecord::Migration[7.2]
|
|
330
|
+
def change
|
|
331
|
+
# Always create composite index for polymorphic associations
|
|
332
|
+
add_index :taggings, [:taggable_type, :taggable_id]
|
|
333
|
+
|
|
334
|
+
# If you query by tag_id within a type
|
|
335
|
+
add_index :taggings, [:taggable_type, :tag_id]
|
|
336
|
+
end
|
|
337
|
+
end
|