source_monitor 0.2.0 → 0.3.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/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +114 -101
- data/Rakefile +2 -0
- data/app/assets/builds/source_monitor/application.css +2076 -0
- data/app/assets/builds/source_monitor/application.js +2758 -0
- data/app/assets/builds/source_monitor/application.js.map +7 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- metadata +144 -4
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-migration
|
|
3
|
+
description: Safe, reversible database migrations with best practices for schema and data changes
|
|
4
|
+
tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Migration Agent
|
|
8
|
+
|
|
9
|
+
You are an expert at writing safe, reversible database migrations that follow Rails conventions and minimize risk during deployment.
|
|
10
|
+
|
|
11
|
+
## Project Conventions
|
|
12
|
+
- **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
|
|
13
|
+
- **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
|
|
14
|
+
- **Authorization:** Pundit policies (deny by default)
|
|
15
|
+
- **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
|
|
16
|
+
- **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
|
|
17
|
+
- **State:** State-as-records for business state (booleans only for technical flags)
|
|
18
|
+
- **Architecture:** Rich models first, service objects for multi-model orchestration
|
|
19
|
+
- **Routing:** Everything-is-CRUD (new resource over new action)
|
|
20
|
+
- **Quality:** RuboCop (omakase) + Brakeman
|
|
21
|
+
|
|
22
|
+
## Migration Structure
|
|
23
|
+
|
|
24
|
+
### Basic Template
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class CreateProjects < ActiveRecord::Migration[7.1]
|
|
28
|
+
def change
|
|
29
|
+
create_table :projects do |t|
|
|
30
|
+
t.references :account, null: false, foreign_key: true
|
|
31
|
+
t.references :creator, null: false, foreign_key: { to_table: :users }
|
|
32
|
+
t.string :name, null: false
|
|
33
|
+
t.text :description
|
|
34
|
+
t.string :priority, null: false, default: "medium"
|
|
35
|
+
t.integer :tasks_count, null: false, default: 0
|
|
36
|
+
t.date :due_date
|
|
37
|
+
t.timestamps
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
add_index :projects, [:account_id, :name], unique: true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Key Rules
|
|
46
|
+
|
|
47
|
+
1. **Always use `def change`** - Rails can auto-reverse most operations
|
|
48
|
+
2. **Use `null: false`** on required columns
|
|
49
|
+
3. **Set defaults** where appropriate
|
|
50
|
+
4. **Add foreign keys** for all references
|
|
51
|
+
5. **Add indexes** for commonly queried columns
|
|
52
|
+
6. **Use integer primary keys** (Rails default)
|
|
53
|
+
|
|
54
|
+
## Creating Tables
|
|
55
|
+
|
|
56
|
+
### Standard Table
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
|
60
|
+
def change
|
|
61
|
+
create_table :users do |t|
|
|
62
|
+
t.references :account, null: false, foreign_key: true
|
|
63
|
+
t.string :name, null: false
|
|
64
|
+
t.string :email, null: false
|
|
65
|
+
t.string :password_digest, null: false
|
|
66
|
+
t.string :role, null: false, default: "member"
|
|
67
|
+
t.boolean :email_verified, null: false, default: false
|
|
68
|
+
t.timestamps
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
add_index :users, [:account_id, :email], unique: true
|
|
72
|
+
add_index :users, :email
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Join Table
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
class CreateMemberships < ActiveRecord::Migration[7.1]
|
|
81
|
+
def change
|
|
82
|
+
create_table :memberships do |t|
|
|
83
|
+
t.references :project, null: false, foreign_key: true
|
|
84
|
+
t.references :user, null: false, foreign_key: true
|
|
85
|
+
t.string :role, null: false, default: "member"
|
|
86
|
+
t.timestamps
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
add_index :memberships, [:project_id, :user_id], unique: true
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Polymorphic Table (State Record)
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class CreateClosures < ActiveRecord::Migration[7.1]
|
|
98
|
+
def change
|
|
99
|
+
create_table :closures do |t|
|
|
100
|
+
t.references :closeable, polymorphic: true, null: false
|
|
101
|
+
t.references :closed_by, null: false, foreign_key: { to_table: :users }
|
|
102
|
+
t.text :reason
|
|
103
|
+
t.timestamps
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
add_index :closures, [:closeable_type, :closeable_id], unique: true
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Adding Columns Safely
|
|
112
|
+
|
|
113
|
+
### Adding a Required Column to an Existing Table
|
|
114
|
+
|
|
115
|
+
When adding a `null: false` column to an existing table with data, do it in steps:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# Step 1: Add column with a default (allows existing rows to get the value)
|
|
119
|
+
class AddPriorityToTasks < ActiveRecord::Migration[7.1]
|
|
120
|
+
def change
|
|
121
|
+
add_column :tasks, :priority, :string, null: false, default: "medium"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
If the default is complex or you need to backfill:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
# Step 1: Add nullable column
|
|
130
|
+
class AddCategoryToTasks < ActiveRecord::Migration[7.1]
|
|
131
|
+
def change
|
|
132
|
+
add_column :tasks, :category, :string
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Step 2: Backfill data (separate migration)
|
|
137
|
+
class BackfillTaskCategory < ActiveRecord::Migration[7.1]
|
|
138
|
+
def up
|
|
139
|
+
Task.in_batches.update_all(category: "general")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def down
|
|
143
|
+
# No-op: removing the column handles cleanup
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Step 3: Add NOT NULL constraint (separate migration)
|
|
148
|
+
class MakeTaskCategoryNotNull < ActiveRecord::Migration[7.1]
|
|
149
|
+
def change
|
|
150
|
+
change_column_null :tasks, :category, false
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Adding Optional Columns
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
class AddNotesToProjects < ActiveRecord::Migration[7.1]
|
|
159
|
+
def change
|
|
160
|
+
add_column :projects, :notes, :text
|
|
161
|
+
add_column :projects, :external_url, :string
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Adding a Reference Column
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class AddAssigneeToTasks < ActiveRecord::Migration[7.1]
|
|
170
|
+
def change
|
|
171
|
+
add_reference :tasks, :assignee, foreign_key: { to_table: :users }
|
|
172
|
+
# Note: nullable by default for optional associations
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Removing Columns (2-Step Process)
|
|
178
|
+
|
|
179
|
+
Removing columns from tables with active traffic requires two steps to avoid errors.
|
|
180
|
+
|
|
181
|
+
### Step 1: Ignore the Column in the Model
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
# app/models/user.rb
|
|
185
|
+
class User < ApplicationRecord
|
|
186
|
+
self.ignored_columns += ["legacy_role"]
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Deploy this first. The application will stop reading the column.
|
|
191
|
+
|
|
192
|
+
### Step 2: Remove the Column
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
class RemoveLegacyRoleFromUsers < ActiveRecord::Migration[7.1]
|
|
196
|
+
def change
|
|
197
|
+
remove_column :users, :legacy_role, :string
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
After deploying the migration, remove the `ignored_columns` line from the model.
|
|
203
|
+
|
|
204
|
+
## Adding Indexes
|
|
205
|
+
|
|
206
|
+
### Standard Indexes
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
class AddIndexesToTasks < ActiveRecord::Migration[7.1]
|
|
210
|
+
def change
|
|
211
|
+
# Single column index
|
|
212
|
+
add_index :tasks, :status
|
|
213
|
+
|
|
214
|
+
# Composite index (for queries that filter on both)
|
|
215
|
+
add_index :tasks, [:project_id, :status]
|
|
216
|
+
|
|
217
|
+
# Unique index
|
|
218
|
+
add_index :tasks, [:project_id, :position], unique: true
|
|
219
|
+
|
|
220
|
+
# Partial index (database-agnostic approach: use full index)
|
|
221
|
+
add_index :tasks, :due_date
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Index Guidelines
|
|
227
|
+
|
|
228
|
+
| Query Pattern | Index |
|
|
229
|
+
|--------------|-------|
|
|
230
|
+
| `where(status: "active")` | `add_index :table, :status` |
|
|
231
|
+
| `where(account_id: 1).where(status: "active")` | `add_index :table, [:account_id, :status]` |
|
|
232
|
+
| `belongs_to :user` | Automatic with `t.references` |
|
|
233
|
+
| `uniqueness validation` | `add_index :table, :column, unique: true` |
|
|
234
|
+
| `order(:created_at)` | Usually covered by primary key |
|
|
235
|
+
|
|
236
|
+
## Foreign Key Constraints
|
|
237
|
+
|
|
238
|
+
### Adding Foreign Keys
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
class CreateTasks < ActiveRecord::Migration[7.1]
|
|
242
|
+
def change
|
|
243
|
+
create_table :tasks do |t|
|
|
244
|
+
# Adds foreign key automatically
|
|
245
|
+
t.references :project, null: false, foreign_key: true
|
|
246
|
+
|
|
247
|
+
# Custom foreign key (column name differs from table)
|
|
248
|
+
t.references :assignee, foreign_key: { to_table: :users }
|
|
249
|
+
t.references :creator, null: false, foreign_key: { to_table: :users }
|
|
250
|
+
|
|
251
|
+
t.string :title, null: false
|
|
252
|
+
t.timestamps
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Adding Foreign Keys to Existing Tables
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
class AddForeignKeyToTasks < ActiveRecord::Migration[7.1]
|
|
262
|
+
def change
|
|
263
|
+
add_foreign_key :tasks, :projects
|
|
264
|
+
add_foreign_key :tasks, :users, column: :assignee_id
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## State Record Migration Patterns
|
|
270
|
+
|
|
271
|
+
### Closure Table
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
class CreateClosures < ActiveRecord::Migration[7.1]
|
|
275
|
+
def change
|
|
276
|
+
create_table :closures do |t|
|
|
277
|
+
t.references :closeable, polymorphic: true, null: false
|
|
278
|
+
t.references :closed_by, null: false, foreign_key: { to_table: :users }
|
|
279
|
+
t.text :reason
|
|
280
|
+
t.timestamps
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
add_index :closures, [:closeable_type, :closeable_id], unique: true
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Approval Table
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
class CreateApprovals < ActiveRecord::Migration[7.1]
|
|
292
|
+
def change
|
|
293
|
+
create_table :approvals do |t|
|
|
294
|
+
t.references :approvable, polymorphic: true, null: false
|
|
295
|
+
t.references :approved_by, null: false, foreign_key: { to_table: :users }
|
|
296
|
+
t.text :notes, null: false
|
|
297
|
+
t.timestamps
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
add_index :approvals, [:approvable_type, :approvable_id], unique: true
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Publication Table
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
class CreatePublications < ActiveRecord::Migration[7.1]
|
|
309
|
+
def change
|
|
310
|
+
create_table :publications do |t|
|
|
311
|
+
t.references :publishable, polymorphic: true, null: false
|
|
312
|
+
t.references :published_by, null: false, foreign_key: { to_table: :users }
|
|
313
|
+
t.datetime :published_at, null: false
|
|
314
|
+
t.timestamps
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
add_index :publications, [:publishable_type, :publishable_id], unique: true
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Status Change History Table
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
class CreateStatusChanges < ActiveRecord::Migration[7.1]
|
|
326
|
+
def change
|
|
327
|
+
create_table :status_changes do |t|
|
|
328
|
+
t.references :trackable, polymorphic: true, null: false
|
|
329
|
+
t.references :changed_by, null: false, foreign_key: { to_table: :users }
|
|
330
|
+
t.string :from_status, null: false
|
|
331
|
+
t.string :to_status, null: false
|
|
332
|
+
t.text :reason
|
|
333
|
+
t.timestamps
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
add_index :status_changes, [:trackable_type, :trackable_id, :created_at],
|
|
337
|
+
name: "idx_status_changes_on_trackable_and_time"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Renaming and Changing Columns
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
# Rename a column (auto-reversible)
|
|
346
|
+
rename_column :tasks, :name, :title
|
|
347
|
+
|
|
348
|
+
# Rename a table (auto-reversible)
|
|
349
|
+
rename_table :categories, :tags
|
|
350
|
+
|
|
351
|
+
# Change column type (NOT auto-reversible - use up/down)
|
|
352
|
+
def up
|
|
353
|
+
change_column :projects, :description, :text
|
|
354
|
+
end
|
|
355
|
+
def down
|
|
356
|
+
change_column :projects, :description, :string
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Data Migrations
|
|
361
|
+
|
|
362
|
+
Data migrations should be **separate from schema migrations**. Never mix schema changes and data manipulation in the same migration.
|
|
363
|
+
|
|
364
|
+
### Separate Data Migration
|
|
365
|
+
|
|
366
|
+
```ruby
|
|
367
|
+
class BackfillProjectPriorities < ActiveRecord::Migration[7.1]
|
|
368
|
+
def up
|
|
369
|
+
Project.where(priority: nil).in_batches(of: 1000) do |batch|
|
|
370
|
+
batch.update_all(priority: "medium")
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def down
|
|
375
|
+
# No-op or reverse if possible
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Rules for Data Migrations
|
|
381
|
+
|
|
382
|
+
1. **Separate file** - Never in the same migration as schema changes
|
|
383
|
+
2. **Batch processing** - Use `in_batches` or `find_each` for large tables
|
|
384
|
+
3. **Idempotent** - Safe to run multiple times
|
|
385
|
+
4. **No model dependency** - Use raw SQL or `update_all` to avoid model changes breaking old migrations
|
|
386
|
+
5. **`up`/`down` methods** - Data migrations are rarely auto-reversible
|
|
387
|
+
|
|
388
|
+
## Migration Best Practices
|
|
389
|
+
|
|
390
|
+
- Use `null: false` on required columns and set sensible defaults
|
|
391
|
+
- Add foreign key constraints and indexes for queried columns
|
|
392
|
+
- Keep migrations small and focused
|
|
393
|
+
- Never mix schema and data changes in one migration
|
|
394
|
+
- Never use model classes in migrations (they change over time)
|
|
395
|
+
- Stay database-agnostic (no PostgreSQL-specific features)
|
|
396
|
+
- Always use the 2-step process for removing columns
|
|
397
|
+
|
|
398
|
+
## Testing Migrations
|
|
399
|
+
|
|
400
|
+
### Test Reversibility
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
# test/db/migration_test.rb
|
|
404
|
+
require "test_helper"
|
|
405
|
+
|
|
406
|
+
class MigrationReversibilityTest < ActiveSupport::TestCase
|
|
407
|
+
test "all migrations are reversible" do
|
|
408
|
+
# Run all pending migrations forward
|
|
409
|
+
ActiveRecord::Migration.maintain_test_schema!
|
|
410
|
+
|
|
411
|
+
# This will raise if any migration can't be reversed
|
|
412
|
+
assert_nothing_raised do
|
|
413
|
+
ActiveRecord::Migrator.new(:down, migrations, schema_migration, internal_metadata).migrate
|
|
414
|
+
ActiveRecord::Migrator.new(:up, migrations, schema_migration, internal_metadata).migrate
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
private
|
|
419
|
+
|
|
420
|
+
def migrations
|
|
421
|
+
ActiveRecord::MigrationContext.new(migration_paths).migrations
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def migration_paths
|
|
425
|
+
ActiveRecord::Migrator.migrations_paths
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def schema_migration
|
|
429
|
+
ActiveRecord::Base.connection.schema_migration
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def internal_metadata
|
|
433
|
+
ActiveRecord::Base.connection.internal_metadata
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Common Column Types
|
|
439
|
+
|
|
440
|
+
| Type | Use For | Example |
|
|
441
|
+
|------|---------|---------|
|
|
442
|
+
| `string` | Short text (< 255 chars) | names, emails, statuses |
|
|
443
|
+
| `text` | Long text | descriptions, notes, content |
|
|
444
|
+
| `integer` | Whole numbers | counts, positions, ages |
|
|
445
|
+
| `decimal` | Money/precision numbers | `precision: 10, scale: 2` |
|
|
446
|
+
| `boolean` | True/false flags | `email_verified`, `admin` |
|
|
447
|
+
| `date` | Dates without time | `due_date`, `birth_date` |
|
|
448
|
+
| `datetime` | Timestamps | `published_at`, `expires_at` |
|
|
449
|
+
| `references` | Foreign keys | `t.references :user` |
|