Class: Familia::Migration::Registry
- Inherits:
-
Object
- Object
- Familia::Migration::Registry
- Defined in:
- lib/familia/migration/registry.rb
Overview
Registry provides Redis-backed tracking for migration state.
Storage Schema (Redis keys): #prefix:applied - Sorted Set (member=migration_id, score=timestamp) #prefix:metadata - Hash (field=migration_id, value=JSON metadata) #prefix:schema - Hash (field=model_name, value=schema_digest) #prefix:backup:id - Hash with TTL for rollback data
Instance Attribute Summary collapse
-
#prefix ⇒ String
readonly
The key prefix for all registry data.
-
#redis ⇒ Redis?
readonly
The Redis client for this registry.
Instance Method Summary collapse
-
#all_applied ⇒ Array<Hash>
Get all applied migrations with their timestamps.
-
#applied?(migration_id) ⇒ Boolean
Check if a migration has been applied.
-
#applied_at(migration_id) ⇒ Time?
Get the timestamp when a migration was applied.
-
#backup_field(migration_id, key, field, value) ⇒ Object
Store a backup of a field value for potential rollback.
-
#clear_backup(migration_id) ⇒ Object
Clear the backup data for a migration.
-
#client ⇒ Redis
Get the Redis client, using lazy initialization.
-
#initialize(redis: nil, prefix: nil) ⇒ Registry
constructor
Initialize a new Registry instance.
-
#metadata(migration_id) ⇒ Hash?
Get metadata for a specific migration.
-
#pending(all_migrations) ⇒ Array<Class>
Filter a list of migrations to only those not yet applied.
-
#record_applied(migration, stats = {}) ⇒ Object
Record that a migration has been applied.
-
#record_rollback(migration_id) ⇒ Object
Record that a migration has been rolled back.
-
#restore_backup(migration_id) ⇒ Integer
Restore all backed up fields for a migration.
-
#schema_changed?(model_class) ⇒ Boolean
Check if the schema has changed for a model class.
-
#schema_digest(model_class) ⇒ String
Calculate the schema digest for a model class.
-
#schema_drift ⇒ Array<String>
Get a list of model classes with changed schemas.
-
#status(all_migrations) ⇒ Array<Hash>
Get the status of all migrations.
-
#store_schema(model_class) ⇒ Object
Store the current schema digest for a model class.
-
#stored_schema(model_class) ⇒ String?
Get the stored schema digest for a model class.
Constructor Details
Instance Attribute Details
#prefix ⇒ String (readonly)
Returns The key prefix for all registry data.
25 26 27 |
# File 'lib/familia/migration/registry.rb', line 25 def prefix @prefix end |
#redis ⇒ Redis? (readonly)
Returns The Redis client for this registry.
22 23 24 |
# File 'lib/familia/migration/registry.rb', line 22 def redis @redis end |
Instance Method Details
#all_applied ⇒ Array<Hash>
Get all applied migrations with their timestamps.
72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/familia/migration/registry.rb', line 72 def all_applied # ZRANGE with WITHSCORES returns [member, score, member, score, ...] results = client.zrange(applied_key, 0, -1, withscores: true) results.map do |migration_id, score| { migration_id: migration_id, applied_at: Time.at(score), } end end |
#applied?(migration_id) ⇒ Boolean
Check if a migration has been applied.
52 53 54 |
# File 'lib/familia/migration/registry.rb', line 52 def applied?(migration_id) client.zscore(applied_key, migration_id.to_s) != nil end |
#applied_at(migration_id) ⇒ Time?
Get the timestamp when a migration was applied.
61 62 63 64 65 66 |
# File 'lib/familia/migration/registry.rb', line 61 def applied_at(migration_id) score = client.zscore(applied_key, migration_id.to_s) return nil if score.nil? Time.at(score) end |
#backup_field(migration_id, key, field, value) ⇒ Object
Store a backup of a field value for potential rollback.
274 275 276 277 278 |
# File 'lib/familia/migration/registry.rb', line 274 def backup_field(migration_id, key, field, value) bkey = backup_key(migration_id) client.hset(bkey, "#{key}:#{field}", value) client.expire(bkey, Familia::Migration.config.backup_ttl) end |
#clear_backup(migration_id) ⇒ Object
Clear the backup data for a migration.
312 313 314 |
# File 'lib/familia/migration/registry.rb', line 312 def clear_backup(migration_id) client.del(backup_key(migration_id)) end |
#client ⇒ Redis
Get the Redis client, using lazy initialization.
41 42 43 |
# File 'lib/familia/migration/registry.rb', line 41 def client @redis ||= Familia.dbclient end |
#metadata(migration_id) ⇒ Hash?
Get metadata for a specific migration.
106 107 108 109 110 111 |
# File 'lib/familia/migration/registry.rb', line 106 def (migration_id) json = client.hget(, migration_id.to_s) return nil if json.nil? JSON.parse(json, symbolize_names: true) end |
#pending(all_migrations) ⇒ Array<Class>
Filter a list of migrations to only those not yet applied.
89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/familia/migration/registry.rb', line 89 def pending(all_migrations) return [] if all_migrations.nil? || all_migrations.empty? # Batch fetch all applied migration IDs in a single Redis call applied_ids = client.zrange(applied_key, 0, -1).to_set all_migrations.reject do |migration| migration_id = extract_migration_id(migration) applied_ids.include?(migration_id) end end |
#record_applied(migration, stats = {}) ⇒ Object
Record that a migration has been applied.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/familia/migration/registry.rb', line 150 def record_applied(migration, stats = {}) migration_id = extract_migration_id(migration) now = Time.now # ZADD to applied set with current timestamp client.zadd(applied_key, now.to_f, migration_id) # Build metadata = { status: 'applied', applied_at: now.iso8601, duration_ms: stats[:duration_ms] || 0, keys_scanned: stats[:keys_scanned] || 0, keys_modified: stats[:keys_modified] || 0, errors: stats[:errors] || 0, reversible: stats[:reversible] || false, } # HSET metadata JSON client.hset(, migration_id, JSON.generate()) end |
#record_rollback(migration_id) ⇒ Object
Record that a migration has been rolled back.
176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/familia/migration/registry.rb', line 176 def record_rollback(migration_id) migration_id = migration_id.to_s # Remove from applied set client.zrem(applied_key, migration_id) # Update metadata to show rolled_back status existing = (migration_id) = existing || {} [:status] = 'rolled_back' [:rolled_back_at] = Time.now.iso8601 client.hset(, migration_id, JSON.generate()) end |
#restore_backup(migration_id) ⇒ Integer
Restore all backed up fields for a migration.
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/familia/migration/registry.rb', line 285 def restore_backup(migration_id) bkey = backup_key(migration_id) backup_data = client.hgetall(bkey) return 0 if backup_data.empty? count = 0 backup_data.each do |composite_key, value| # Parse "redis_key:field_name" format # Note: field_name might contain colons, so we only split on the last colon parts = composite_key.rpartition(':') redis_key = parts[0] field_name = parts[2] next if redis_key.empty? || field_name.empty? client.hset(redis_key, field_name, value) count += 1 end count end |
#schema_changed?(model_class) ⇒ Boolean
Check if the schema has changed for a model class.
235 236 237 238 239 240 |
# File 'lib/familia/migration/registry.rb', line 235 def schema_changed?(model_class) stored = stored_schema(model_class) return false if stored.nil? # No stored schema = no drift stored != schema_digest(model_class) end |
#schema_digest(model_class) ⇒ String
Calculate the schema digest for a model class.
198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/familia/migration/registry.rb', line 198 def schema_digest(model_class) fields = model_class.fields.sort field_types = model_class.field_types field_strings = fields.map do |field| type = field_types[field] || 'unknown' "#{field}:#{type}" end Digest::SHA256.hexdigest(field_strings.join('|')) end |
#schema_drift ⇒ Array<String>
Get a list of model classes with changed schemas.
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'lib/familia/migration/registry.rb', line 246 def schema_drift # Get all stored schemas stored = client.hgetall(schema_key) return [] if stored.empty? drifted = [] stored.each do |model_name, stored_digest| # Try to find the model class model_class = find_model_class(model_name) next if model_class.nil? current_digest = schema_digest(model_class) drifted << model_name if stored_digest != current_digest end drifted end |
#status(all_migrations) ⇒ Array<Hash>
Get the status of all migrations.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/familia/migration/registry.rb', line 118 def status(all_migrations) return [] if all_migrations.nil? || all_migrations.empty? # Batch fetch all applied migrations with timestamps in a single Redis call applied_info = all_applied.each_with_object({}) do |entry, hash| hash[entry[:migration_id]] = entry[:applied_at] end all_migrations.map do |migration| migration_id = extract_migration_id(migration) = applied_info[migration_id] { migration_id: migration_id, status: ? :applied : :pending, applied_at: , } end end |
#store_schema(model_class) ⇒ Object
Store the current schema digest for a model class.
214 215 216 217 218 |
# File 'lib/familia/migration/registry.rb', line 214 def store_schema(model_class) model_name = model_class.name || model_class.to_s digest = schema_digest(model_class) client.hset(schema_key, model_name, digest) end |
#stored_schema(model_class) ⇒ String?
Get the stored schema digest for a model class.
225 226 227 228 |
# File 'lib/familia/migration/registry.rb', line 225 def stored_schema(model_class) model_name = model_class.name || model_class.to_s client.hget(schema_key, model_name) end |