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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. 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