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,350 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caching-strategies
|
|
3
|
+
description: Implements Rails caching patterns for performance optimization. Use when adding fragment caching, Russian doll caching, low-level caching, HTTP caching with ETags, cache invalidation, or when user mentions caching, performance, cache keys, or Solid Cache.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Caching Strategies for Rails 8
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Rails provides multiple caching layers:
|
|
12
|
+
- **HTTP caching**: ETags and `fresh_when` for 304 Not Modified
|
|
13
|
+
- **Fragment caching**: Cache view partials
|
|
14
|
+
- **Russian doll caching**: Nested cache fragments with `touch: true`
|
|
15
|
+
- **Low-level caching**: Cache arbitrary data with `Rails.cache.fetch`
|
|
16
|
+
- **Collection caching**: Efficient cached rendering of collections
|
|
17
|
+
- **Solid Cache**: Database-backed caching (Rails 8 default, no Redis)
|
|
18
|
+
|
|
19
|
+
## Cache Store Options
|
|
20
|
+
|
|
21
|
+
| Store | Use Case |
|
|
22
|
+
|-------|----------|
|
|
23
|
+
| `:memory_store` | Development |
|
|
24
|
+
| `:solid_cache_store` | Production (Rails 8 default) |
|
|
25
|
+
| `:redis_cache_store` | Production (if Redis available) |
|
|
26
|
+
| `:null_store` | Testing |
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# config/environments/production.rb
|
|
30
|
+
config.cache_store = :solid_cache_store
|
|
31
|
+
|
|
32
|
+
# config/environments/development.rb
|
|
33
|
+
config.cache_store = :memory_store
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Enable caching in development:
|
|
37
|
+
```bash
|
|
38
|
+
bin/rails dev:cache
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## HTTP Caching (ETags / fresh_when)
|
|
42
|
+
|
|
43
|
+
Use conditional GET to send 304 Not Modified when content has not changed.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
class EventsController < ApplicationController
|
|
47
|
+
def show
|
|
48
|
+
@event = current_account.events.find(params[:id])
|
|
49
|
+
fresh_when @event
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def index
|
|
53
|
+
@events = current_account.events.recent
|
|
54
|
+
fresh_when @events
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Composite ETags
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
def show
|
|
63
|
+
@event = current_account.events.find(params[:id])
|
|
64
|
+
fresh_when [@event, Current.user]
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### With stale? for JSON
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class Api::EventsController < Api::BaseController
|
|
72
|
+
def show
|
|
73
|
+
@event = current_account.events.find(params[:id])
|
|
74
|
+
if stale?(@event)
|
|
75
|
+
render json: @event
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Fragment Caching
|
|
82
|
+
|
|
83
|
+
```erb
|
|
84
|
+
<%# app/views/events/_event.html.erb %>
|
|
85
|
+
<% cache event do %>
|
|
86
|
+
<article class="event-card">
|
|
87
|
+
<h3><%= event.name %></h3>
|
|
88
|
+
<p><%= event.description %></p>
|
|
89
|
+
<time><%= l(event.event_date, format: :long) %></time>
|
|
90
|
+
</article>
|
|
91
|
+
<% end %>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Custom Cache Keys
|
|
95
|
+
|
|
96
|
+
```erb
|
|
97
|
+
<% cache [event, "v2"] do %>
|
|
98
|
+
...
|
|
99
|
+
<% end %>
|
|
100
|
+
|
|
101
|
+
<% cache [event, current_user] do %>
|
|
102
|
+
...
|
|
103
|
+
<% end %>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Russian Doll Caching
|
|
107
|
+
|
|
108
|
+
Nested caches with automatic invalidation through `touch: true`:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# app/models/comment.rb
|
|
112
|
+
class Comment < ApplicationRecord
|
|
113
|
+
belongs_to :event, touch: true
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```erb
|
|
118
|
+
<% cache @event do %>
|
|
119
|
+
<h1><%= @event.name %></h1>
|
|
120
|
+
<% @event.comments.each do |comment| %>
|
|
121
|
+
<% cache comment do %>
|
|
122
|
+
<%= render comment %>
|
|
123
|
+
<% end %>
|
|
124
|
+
<% end %>
|
|
125
|
+
<% end %>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
When a comment is updated, `touch: true` cascades up through `updated_at` timestamps, invalidating all parent caches automatically.
|
|
129
|
+
|
|
130
|
+
## Collection Caching
|
|
131
|
+
|
|
132
|
+
```erb
|
|
133
|
+
<%# Caches each item individually, multi-read from cache store %>
|
|
134
|
+
<%= render partial: "events/event", collection: @events, cached: true %>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Low-Level Caching
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
Rails.cache.fetch("stats/#{Date.current}", expires_in: 1.hour) do
|
|
141
|
+
{ total_events: Event.count, total_revenue: Order.sum(:total_cents) }
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### In Models
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
class Board < ApplicationRecord
|
|
149
|
+
def statistics
|
|
150
|
+
Rails.cache.fetch([self, "statistics"], expires_in: 1.hour) do
|
|
151
|
+
{
|
|
152
|
+
total_cards: cards.count,
|
|
153
|
+
completed_cards: cards.joins(:closure).count,
|
|
154
|
+
total_comments: cards.joins(:comments).count
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### With Race Condition Protection
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
Rails.cache.fetch([self, "stats"], expires_in: 1.hour, race_condition_ttl: 10.seconds) do
|
|
165
|
+
expensive_operation
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Cache Invalidation
|
|
170
|
+
|
|
171
|
+
### Key-Based (Automatic)
|
|
172
|
+
|
|
173
|
+
Cache keys include `updated_at`, so updates automatically expire old entries.
|
|
174
|
+
|
|
175
|
+
### Touch Cascade
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
class Card < ApplicationRecord
|
|
179
|
+
belongs_to :board, touch: true # Updates board.updated_at
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class Comment < ApplicationRecord
|
|
183
|
+
belongs_to :card, touch: true # Updates card.updated_at -> board.updated_at
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Manual Invalidation
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class Event < ApplicationRecord
|
|
191
|
+
after_commit :invalidate_caches
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def invalidate_caches
|
|
196
|
+
Rails.cache.delete([self, "statistics"])
|
|
197
|
+
Rails.cache.delete("featured_events")
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Sweeper Pattern
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
class CacheSweeper
|
|
206
|
+
def self.clear_board_caches(board)
|
|
207
|
+
Rails.cache.delete([board, "statistics"])
|
|
208
|
+
Rails.cache.delete([board, "card_distribution"])
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Counter Caching
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# Migration
|
|
217
|
+
add_column :events, :vendors_count, :integer, default: 0, null: false
|
|
218
|
+
|
|
219
|
+
# Model
|
|
220
|
+
class Vendor < ApplicationRecord
|
|
221
|
+
belongs_to :event, counter_cache: true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Usage (no query needed)
|
|
225
|
+
event.vendors_count
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Cache Warming
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
class CacheWarmerJob < ApplicationJob
|
|
232
|
+
queue_as :low
|
|
233
|
+
|
|
234
|
+
def perform(account)
|
|
235
|
+
account.boards.find_each do |board|
|
|
236
|
+
board.statistics
|
|
237
|
+
board.card_distribution
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Testing Caching
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# test/test_helper.rb (enable caching for specific tests)
|
|
247
|
+
class ActiveSupport::TestCase
|
|
248
|
+
def with_caching(&block)
|
|
249
|
+
caching = ActionController::Base.perform_caching
|
|
250
|
+
ActionController::Base.perform_caching = true
|
|
251
|
+
Rails.cache.clear
|
|
252
|
+
yield
|
|
253
|
+
ensure
|
|
254
|
+
ActionController::Base.perform_caching = caching
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Testing Touch Cascade
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# test/models/card_test.rb
|
|
263
|
+
require "test_helper"
|
|
264
|
+
|
|
265
|
+
class CardCachingTest < ActiveSupport::TestCase
|
|
266
|
+
test "touching card updates board updated_at" do
|
|
267
|
+
board = boards(:one)
|
|
268
|
+
card = cards(:one)
|
|
269
|
+
|
|
270
|
+
assert_changes -> { board.reload.updated_at } do
|
|
271
|
+
card.touch
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Testing HTTP Caching
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# test/controllers/boards_controller_test.rb
|
|
281
|
+
require "test_helper"
|
|
282
|
+
|
|
283
|
+
class BoardsControllerCachingTest < ActionDispatch::IntegrationTest
|
|
284
|
+
setup do
|
|
285
|
+
sign_in users(:one)
|
|
286
|
+
@board = boards(:one)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
test "returns 304 when board unchanged" do
|
|
290
|
+
get board_url(@board)
|
|
291
|
+
assert_response :success
|
|
292
|
+
etag = response.headers["ETag"]
|
|
293
|
+
|
|
294
|
+
get board_url(@board), headers: { "If-None-Match" => etag }
|
|
295
|
+
assert_response :not_modified
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
test "returns 200 when board updated" do
|
|
299
|
+
get board_url(@board)
|
|
300
|
+
etag = response.headers["ETag"]
|
|
301
|
+
|
|
302
|
+
@board.touch
|
|
303
|
+
|
|
304
|
+
get board_url(@board), headers: { "If-None-Match" => etag }
|
|
305
|
+
assert_response :success
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Testing Cache Invalidation
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
# test/models/board_test.rb
|
|
314
|
+
require "test_helper"
|
|
315
|
+
|
|
316
|
+
class BoardCacheInvalidationTest < ActiveSupport::TestCase
|
|
317
|
+
test "statistics cache is cleared after card update" do
|
|
318
|
+
board = boards(:one)
|
|
319
|
+
card = cards(:one)
|
|
320
|
+
|
|
321
|
+
board.statistics # Warm cache
|
|
322
|
+
|
|
323
|
+
card.update!(title: "New title")
|
|
324
|
+
|
|
325
|
+
assert_nil Rails.cache.read([board, "statistics"])
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Memoization
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
class EventPresenter < BasePresenter
|
|
334
|
+
def vendor_count
|
|
335
|
+
@vendor_count ||= event.vendors.count
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Checklist
|
|
341
|
+
|
|
342
|
+
- [ ] Cache store configured for environment
|
|
343
|
+
- [ ] `fresh_when` on show/index actions
|
|
344
|
+
- [ ] `touch: true` on belongs_to for Russian doll
|
|
345
|
+
- [ ] Collection caching with `cached: true`
|
|
346
|
+
- [ ] Low-level caching for expensive queries
|
|
347
|
+
- [ ] Cache invalidation strategy defined
|
|
348
|
+
- [ ] Counter caches for counts
|
|
349
|
+
- [ ] Cache warming jobs for cold starts
|
|
350
|
+
- [ ] All tests GREEN
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: database-migrations
|
|
3
|
+
description: Creates safe database migrations with proper indexes and rollback strategies. Use when creating tables, adding columns, creating indexes, handling zero-downtime migrations, or when user mentions migrations, schema changes, or database structure.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Database Migration Patterns for Rails 8
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Safe database migrations are critical for production stability:
|
|
12
|
+
- Zero-downtime deployments
|
|
13
|
+
- Reversible migrations
|
|
14
|
+
- Proper indexing
|
|
15
|
+
- Data integrity constraints
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bin/rails generate migration AddStatusToEvents status:integer
|
|
21
|
+
bin/rails db:migrate
|
|
22
|
+
bin/rails db:rollback
|
|
23
|
+
bin/rails db:migrate:status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Safety Checklist
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Migration Safety:
|
|
30
|
+
- [ ] Migration is reversible (has down or uses change)
|
|
31
|
+
- [ ] Large tables use batching for updates
|
|
32
|
+
- [ ] Indexes added concurrently (if needed)
|
|
33
|
+
- [ ] Foreign keys have indexes
|
|
34
|
+
- [ ] NOT NULL added in two steps (for existing columns)
|
|
35
|
+
- [ ] Default values don't lock table
|
|
36
|
+
- [ ] Tested rollback locally
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Safe Migration Patterns
|
|
40
|
+
|
|
41
|
+
### Pattern 1: Add Column (Safe)
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class AddStatusToEvents < ActiveRecord::Migration[8.0]
|
|
45
|
+
def change
|
|
46
|
+
add_column :events, :status, :integer, default: 0, null: false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Pattern 2: Add Column with NOT NULL (Two-Step)
|
|
52
|
+
|
|
53
|
+
For existing tables with data, add NOT NULL in two migrations:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# Step 1: Add column with default (allows NULL temporarily)
|
|
57
|
+
class AddPriorityToTasks < ActiveRecord::Migration[8.0]
|
|
58
|
+
def change
|
|
59
|
+
add_column :tasks, :priority, :integer, default: 0
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Step 2: Add NOT NULL constraint after backfill
|
|
64
|
+
class AddNotNullToTasksPriority < ActiveRecord::Migration[8.0]
|
|
65
|
+
def change
|
|
66
|
+
change_column_null :tasks, :priority, false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Pattern 3: Add Index (Production Safe)
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class AddIndexToEventsStatus < ActiveRecord::Migration[8.0]
|
|
75
|
+
disable_ddl_transaction!
|
|
76
|
+
|
|
77
|
+
def change
|
|
78
|
+
add_index :events, :status, algorithm: :concurrently, if_not_exists: true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Pattern 4: Add Foreign Key with Index
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class AddAccountToEvents < ActiveRecord::Migration[8.0]
|
|
87
|
+
def change
|
|
88
|
+
add_reference :events, :account, null: false, foreign_key: true, index: true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Pattern 5: Rename Column
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
class RenameNameToTitleOnEvents < ActiveRecord::Migration[8.0]
|
|
97
|
+
def change
|
|
98
|
+
rename_column :events, :name, :title
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Pattern 6: Remove Column
|
|
104
|
+
|
|
105
|
+
First remove references in code, then migrate:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
class RemoveLegacyFieldFromEvents < ActiveRecord::Migration[8.0]
|
|
109
|
+
def change
|
|
110
|
+
safety_assured { remove_column :events, :legacy_field, :string }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Pattern 7: Add Enum Column
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
class AddStatusEnumToOrders < ActiveRecord::Migration[8.0]
|
|
119
|
+
def change
|
|
120
|
+
add_column :orders, :status, :integer, default: 0, null: false
|
|
121
|
+
add_index :orders, :status
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
In model:
|
|
127
|
+
```ruby
|
|
128
|
+
class Order < ApplicationRecord
|
|
129
|
+
enum :status, { pending: 0, confirmed: 1, shipped: 2, delivered: 3, cancelled: 4 }
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Pattern 8: Create Table with State Record
|
|
134
|
+
|
|
135
|
+
For app-wide configuration (single-row tables):
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class CreateAppConfigs < ActiveRecord::Migration[8.0]
|
|
139
|
+
def change
|
|
140
|
+
create_table :app_configs do |t|
|
|
141
|
+
t.string :site_name, null: false, default: "My App"
|
|
142
|
+
t.boolean :maintenance_mode, null: false, default: false
|
|
143
|
+
t.text :settings
|
|
144
|
+
t.timestamps
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Dangerous Operations (Avoid)
|
|
151
|
+
|
|
152
|
+
### DON'T: Change Column Type Directly
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# DANGEROUS - can lose data or lock table
|
|
156
|
+
change_column :events, :budget, :decimal # DON'T DO THIS
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### DO: Add New Column, Migrate Data, Remove Old
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
# Step 1: Add new column
|
|
163
|
+
class AddBudgetDecimalToEvents < ActiveRecord::Migration[8.0]
|
|
164
|
+
def change
|
|
165
|
+
add_column :events, :budget_decimal, :decimal, precision: 10, scale: 2
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Step 2: Backfill data
|
|
170
|
+
class BackfillEventsBudget < ActiveRecord::Migration[8.0]
|
|
171
|
+
disable_ddl_transaction!
|
|
172
|
+
|
|
173
|
+
def up
|
|
174
|
+
Event.in_batches.update_all("budget_decimal = budget")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def down
|
|
178
|
+
# Data migration, no rollback needed
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Step 3: Remove old column (after code updated)
|
|
183
|
+
class RemoveOldBudgetFromEvents < ActiveRecord::Migration[8.0]
|
|
184
|
+
def change
|
|
185
|
+
safety_assured { remove_column :events, :budget, :integer }
|
|
186
|
+
rename_column :events, :budget_decimal, :budget
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Data Migrations
|
|
192
|
+
|
|
193
|
+
### Safe Backfill Pattern
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class BackfillEventStatus < ActiveRecord::Migration[8.0]
|
|
197
|
+
disable_ddl_transaction!
|
|
198
|
+
|
|
199
|
+
def up
|
|
200
|
+
Event.unscoped.in_batches(of: 1000) do |batch|
|
|
201
|
+
batch.where(status: nil).update_all(status: 0)
|
|
202
|
+
sleep(0.1) # Reduce database load
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def down
|
|
207
|
+
# No rollback for data migration
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Index Strategies
|
|
213
|
+
|
|
214
|
+
### Composite Indexes
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
# For queries: WHERE account_id = ? AND status = ?
|
|
218
|
+
add_index :events, [:account_id, :status]
|
|
219
|
+
|
|
220
|
+
# Order matters! Left-to-right prefix matching:
|
|
221
|
+
# Helps: WHERE account_id = ? AND status = ?
|
|
222
|
+
# Helps: WHERE account_id = ?
|
|
223
|
+
# Does NOT help: WHERE status = ?
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Partial Indexes
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# Index only active records
|
|
230
|
+
add_index :events, :event_date, where: "status = 0", name: "index_events_on_date_active"
|
|
231
|
+
|
|
232
|
+
# Index only non-null values
|
|
233
|
+
add_index :users, :reset_token, where: "reset_token IS NOT NULL"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Unique Indexes
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
add_index :users, :email, unique: true
|
|
240
|
+
add_index :event_vendors, [:event_id, :vendor_id], unique: true
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Foreign Keys
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
class AddForeignKeys < ActiveRecord::Migration[8.0]
|
|
247
|
+
def change
|
|
248
|
+
add_reference :events, :venue, foreign_key: true
|
|
249
|
+
add_foreign_key :events, :users, column: :organizer_id
|
|
250
|
+
|
|
251
|
+
# ON DELETE options
|
|
252
|
+
add_foreign_key :comments, :posts, on_delete: :cascade
|
|
253
|
+
add_foreign_key :posts, :users, column: :author_id, on_delete: :nullify
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Testing Migrations
|
|
259
|
+
|
|
260
|
+
### Schema Integrity Test
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
# test/db/schema_test.rb
|
|
264
|
+
require "test_helper"
|
|
265
|
+
|
|
266
|
+
class SchemaTest < ActiveSupport::TestCase
|
|
267
|
+
test "all foreign keys have indexes" do
|
|
268
|
+
connection = ActiveRecord::Base.connection
|
|
269
|
+
|
|
270
|
+
connection.tables.each do |table|
|
|
271
|
+
foreign_keys = connection.foreign_keys(table)
|
|
272
|
+
indexes = connection.indexes(table)
|
|
273
|
+
|
|
274
|
+
foreign_keys.each do |fk|
|
|
275
|
+
indexed = indexes.any? { |idx| idx.columns.first == fk.column }
|
|
276
|
+
assert indexed, "Missing index for #{fk.column} on #{table}"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Rollback Test (CLI)
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
bin/rails db:migrate
|
|
287
|
+
bin/rails db:rollback
|
|
288
|
+
bin/rails db:migrate
|
|
289
|
+
bin/rails db:migrate:status
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Reversible Migrations
|
|
293
|
+
|
|
294
|
+
### Using up/down (Manual Reversal)
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
class ChangeEventsStructure < ActiveRecord::Migration[8.0]
|
|
298
|
+
def up
|
|
299
|
+
execute <<-SQL
|
|
300
|
+
ALTER TABLE events ADD CONSTRAINT check_positive_budget
|
|
301
|
+
CHECK (budget_cents >= 0)
|
|
302
|
+
SQL
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def down
|
|
306
|
+
execute <<-SQL
|
|
307
|
+
ALTER TABLE events DROP CONSTRAINT check_positive_budget
|
|
308
|
+
SQL
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Irreversible Migrations
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
class DropLegacyTable < ActiveRecord::Migration[8.0]
|
|
317
|
+
def up
|
|
318
|
+
drop_table :legacy_events
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def down
|
|
322
|
+
raise ActiveRecord::IrreversibleMigration, "Cannot restore dropped table"
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Performance Tips
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# DON'T - Locks entire table
|
|
331
|
+
add_index :large_table, :column
|
|
332
|
+
|
|
333
|
+
# DO - Non-blocking
|
|
334
|
+
disable_ddl_transaction!
|
|
335
|
+
add_index :large_table, :column, algorithm: :concurrently
|
|
336
|
+
|
|
337
|
+
# DON'T - Updates all at once
|
|
338
|
+
Event.update_all(status: 0)
|
|
339
|
+
|
|
340
|
+
# DO - Updates in batches
|
|
341
|
+
Event.in_batches(of: 1000) do |batch|
|
|
342
|
+
batch.update_all(status: 0)
|
|
343
|
+
end
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Checklist
|
|
347
|
+
|
|
348
|
+
- [ ] Migration is reversible
|
|
349
|
+
- [ ] Indexes on foreign keys
|
|
350
|
+
- [ ] Concurrent index creation for large tables
|
|
351
|
+
- [ ] NOT NULL added safely (two-step)
|
|
352
|
+
- [ ] Data migrations use batching
|
|
353
|
+
- [ ] Tested rollback locally
|
|
354
|
+
- [ ] No table locks during deploy
|