familia 2.0.0 → 2.1.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/CHANGELOG.rst +45 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +11 -1
- data/docs/guides/writing-migrations.md +345 -0
- data/examples/migrations/v1_to_v2_serialization_migration.rb +374 -0
- data/examples/schemas/customer.json +33 -0
- data/examples/schemas/session.json +27 -0
- data/familia.gemspec +2 -0
- data/lib/familia/data_type/types/hashkey.rb +0 -238
- data/lib/familia/data_type/types/listkey.rb +4 -110
- data/lib/familia/data_type/types/sorted_set.rb +0 -365
- data/lib/familia/data_type/types/stringkey.rb +0 -139
- data/lib/familia/data_type/types/unsorted_set.rb +2 -122
- data/lib/familia/features/schema_validation.rb +139 -0
- data/lib/familia/migration/base.rb +447 -0
- data/lib/familia/migration/errors.rb +31 -0
- data/lib/familia/migration/model.rb +418 -0
- data/lib/familia/migration/pipeline.rb +226 -0
- data/lib/familia/migration/rake_tasks.rake +3 -0
- data/lib/familia/migration/rake_tasks.rb +160 -0
- data/lib/familia/migration/registry.rb +364 -0
- data/lib/familia/migration/runner.rb +311 -0
- data/lib/familia/migration/script.rb +234 -0
- data/lib/familia/migration.rb +43 -0
- data/lib/familia/schema_registry.rb +173 -0
- data/lib/familia/settings.rb +63 -1
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +1 -0
- data/try/features/schema_registry_try.rb +193 -0
- data/try/features/schema_validation_feature_try.rb +218 -0
- data/try/migration/base_try.rb +226 -0
- data/try/migration/errors_try.rb +67 -0
- data/try/migration/integration_try.rb +451 -0
- data/try/migration/model_try.rb +431 -0
- data/try/migration/pipeline_try.rb +460 -0
- data/try/migration/rake_tasks_try.rb +61 -0
- data/try/migration/registry_try.rb +199 -0
- data/try/migration/runner_try.rb +311 -0
- data/try/migration/schema_validation_try.rb +201 -0
- data/try/migration/script_try.rb +192 -0
- data/try/migration/v1_to_v2_serialization_try.rb +513 -0
- data/try/performance/benchmarks_try.rb +11 -12
- metadata +44 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e83013bddd8ff985ad1cd9ef73421146b5fea6c35cbb77aa0cf12656dd270f68
|
|
4
|
+
data.tar.gz: ba7ebef4af806d823317fd41e1875733b12971b3c6321e841c9ecbf0c9d727a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 126d79d60cad6c4ed4ed03a557f4435aec82d03ef2230ad64e4d2205f8c8856cc74b140c18fb5f1d567631f203f6b259e7d19d0bed8f36e09b5540fc004f4e6d
|
|
7
|
+
data.tar.gz: cb5a52b4622c9f193d3858b0f2766795f49000f56fccbffdc630dc5b05e68db0a8c200ad25137bf543d9fc64ee2f53cab891517a5d9f91c2dd8debeb2119b7e7
|
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,51 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
|
|
|
7
7
|
|
|
8
8
|
<!--scriv-insert-here-->
|
|
9
9
|
|
|
10
|
+
.. _changelog-2.1.0:
|
|
11
|
+
|
|
12
|
+
2.1.0 — 2026-02-01
|
|
13
|
+
==================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- Redis-native migration system with three patterns: Base (abstract foundation),
|
|
19
|
+
Model (record-by-record iteration via SCAN), and Pipeline (bulk updates with
|
|
20
|
+
Redis pipelining). Includes dependency resolution using topological sort,
|
|
21
|
+
dry-run mode, CLI support, and comprehensive Rake tasks.
|
|
22
|
+
|
|
23
|
+
- Migration registry for tracking applied migrations in Redis with rollback
|
|
24
|
+
support and schema drift detection.
|
|
25
|
+
|
|
26
|
+
- Lua script framework with atomic operations: rename_field, copy_field,
|
|
27
|
+
delete_field, rename_key_preserve_ttl, and backup_and_modify_field.
|
|
28
|
+
|
|
29
|
+
- Optional JSON Schema validation for Horreum models via ``feature :schema_validation``
|
|
30
|
+
with centralized SchemaRegistry supporting convention-based and explicit schema
|
|
31
|
+
discovery using the json_schemer gem.
|
|
32
|
+
|
|
33
|
+
- V1 to V2 serialization migration example at ``examples/migrations/v1_to_v2_serialization_migration.rb``
|
|
34
|
+
demonstrating how to upgrade Horreum objects from v1.x format (selective serialization
|
|
35
|
+
with type information loss) to v2.0 format (universal JSON encoding with type preservation).
|
|
36
|
+
Includes type detection heuristics, field type declarations, and batch processing.
|
|
37
|
+
|
|
38
|
+
Documentation
|
|
39
|
+
-------------
|
|
40
|
+
|
|
41
|
+
- Added comprehensive migration writing guide at ``docs/guides/writing-migrations.md``
|
|
42
|
+
covering all three migration patterns, CLI usage, dependencies, and best practices.
|
|
43
|
+
|
|
44
|
+
AI Assistance
|
|
45
|
+
-------------
|
|
46
|
+
|
|
47
|
+
- Claude Code assisted with test coverage analysis, identifying gaps in Model and
|
|
48
|
+
Pipeline test coverage. Implemented 67 new tests covering CLI entry points,
|
|
49
|
+
circular dependency detection, and comprehensive Model/Pipeline scenarios.
|
|
50
|
+
|
|
51
|
+
- Claude Code identified and fixed a bug where schema validation hooks were never
|
|
52
|
+
triggered in Model migrations, and optimized N+1 query patterns in Registry and
|
|
53
|
+
Runner classes.
|
|
54
|
+
|
|
10
55
|
.. _changelog-2.0.0:
|
|
11
56
|
|
|
12
57
|
2.0.0 — 2026-01-19
|
data/Gemfile
CHANGED
|
@@ -15,6 +15,8 @@ end
|
|
|
15
15
|
group :development, :test do
|
|
16
16
|
gem 'benchmark', '~> 0.4', require: false
|
|
17
17
|
gem 'debug', require: false
|
|
18
|
+
gem 'json_schemer', '~> 2.0', require: false
|
|
19
|
+
gem 'rake', '~> 13.0', require: false
|
|
18
20
|
gem 'irb', '~> 1.15.2', require: false
|
|
19
21
|
gem 'redcarpet', require: false
|
|
20
22
|
gem 'reek', require: false
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
familia (2.
|
|
4
|
+
familia (2.1.0)
|
|
5
5
|
concurrent-ruby (~> 1.3)
|
|
6
6
|
connection_pool (~> 2.5)
|
|
7
7
|
csv (~> 3.3)
|
|
@@ -58,12 +58,18 @@ GEM
|
|
|
58
58
|
erb (5.1.3)
|
|
59
59
|
ffi (1.17.2)
|
|
60
60
|
ffi (1.17.2-arm64-darwin)
|
|
61
|
+
hana (1.3.7)
|
|
61
62
|
io-console (0.8.1)
|
|
62
63
|
irb (1.15.3)
|
|
63
64
|
pp (>= 0.6.0)
|
|
64
65
|
rdoc (>= 4.0.0)
|
|
65
66
|
reline (>= 0.4.2)
|
|
66
67
|
json (2.15.1)
|
|
68
|
+
json_schemer (2.5.0)
|
|
69
|
+
bigdecimal
|
|
70
|
+
hana (~> 1.3)
|
|
71
|
+
regexp_parser (~> 2.0)
|
|
72
|
+
simpleidn (~> 0.2)
|
|
67
73
|
language_server-protocol (3.17.0.5)
|
|
68
74
|
lint_roller (1.1.0)
|
|
69
75
|
logger (1.7.0)
|
|
@@ -87,6 +93,7 @@ GEM
|
|
|
87
93
|
stringio
|
|
88
94
|
racc (1.8.1)
|
|
89
95
|
rainbow (3.1.1)
|
|
96
|
+
rake (13.3.1)
|
|
90
97
|
rbnacl (7.1.2)
|
|
91
98
|
ffi (~> 1)
|
|
92
99
|
rbs (3.9.5)
|
|
@@ -152,6 +159,7 @@ GEM
|
|
|
152
159
|
ruby-prof (1.7.2)
|
|
153
160
|
base64
|
|
154
161
|
ruby-progressbar (1.13.0)
|
|
162
|
+
simpleidn (0.2.3)
|
|
155
163
|
stackprof (0.2.27)
|
|
156
164
|
stringio (3.1.9)
|
|
157
165
|
timecop (0.9.10)
|
|
@@ -185,6 +193,8 @@ DEPENDENCIES
|
|
|
185
193
|
debug
|
|
186
194
|
familia!
|
|
187
195
|
irb (~> 1.15.2)
|
|
196
|
+
json_schemer (~> 2.0)
|
|
197
|
+
rake (~> 13.0)
|
|
188
198
|
rbnacl (~> 7.1, >= 7.1.1)
|
|
189
199
|
redcarpet
|
|
190
200
|
reek
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Writing Migrations
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
Minimal working migration:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class NormalizeEmails < Familia::Migration::Base
|
|
9
|
+
self.migration_id = '20260131_120000_normalize_emails'
|
|
10
|
+
self.description = 'Lowercase all email addresses'
|
|
11
|
+
|
|
12
|
+
def migration_needed?
|
|
13
|
+
redis.exists('needs:normalization') > 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def migrate
|
|
17
|
+
redis.scan_each(match: 'user:*:object') do |key|
|
|
18
|
+
for_realsies_this_time? do
|
|
19
|
+
# perform changes
|
|
20
|
+
end
|
|
21
|
+
track_stat(:processed)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run it:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bundle exec rake familia:migrate:dry_run # Preview
|
|
31
|
+
bundle exec rake familia:migrate # Apply
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Migration Types
|
|
35
|
+
|
|
36
|
+
| Type | Use When | Key Method |
|
|
37
|
+
|------|----------|------------|
|
|
38
|
+
| `Base` | Raw Redis operations, key patterns, config changes | `migrate` |
|
|
39
|
+
| `Model` | Iterating over Horreum objects with per-record logic | `process_record(obj, key)` |
|
|
40
|
+
| `Pipeline` | Bulk updates with Redis pipelining (1000+ records) | `should_process?(obj)`, `build_update_fields(obj)` |
|
|
41
|
+
|
|
42
|
+
### Base
|
|
43
|
+
|
|
44
|
+
Direct Redis access. Use for key renames, TTL changes, config migrations:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
class AddTTLToSessions < Familia::Migration::Base
|
|
48
|
+
self.migration_id = '20260131_add_ttl'
|
|
49
|
+
|
|
50
|
+
def migration_needed?
|
|
51
|
+
redis.exists('legacy:session:*') > 0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def migrate
|
|
55
|
+
cursor = '0'
|
|
56
|
+
loop do
|
|
57
|
+
cursor, keys = redis.scan(cursor, match: 'legacy:session:*', count: 1000)
|
|
58
|
+
keys.each do |key|
|
|
59
|
+
for_realsies_this_time? { redis.expire(key, 3600) }
|
|
60
|
+
track_stat(:keys_expired)
|
|
61
|
+
end
|
|
62
|
+
break if cursor == '0'
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Model
|
|
69
|
+
|
|
70
|
+
SCAN-based iteration over Horreum objects. Use for per-record transformations with error handling:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class CustomerEmailMigration < Familia::Migration::Model
|
|
74
|
+
self.migration_id = '20260131_customer_emails'
|
|
75
|
+
|
|
76
|
+
def prepare
|
|
77
|
+
@model_class = Customer
|
|
78
|
+
@batch_size = 500 # optional, default: 1000
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def process_record(customer, key)
|
|
82
|
+
return unless customer.email =~ /[A-Z]/
|
|
83
|
+
|
|
84
|
+
for_realsies_this_time? do
|
|
85
|
+
customer.email = customer.email.downcase
|
|
86
|
+
customer.save
|
|
87
|
+
end
|
|
88
|
+
track_stat(:records_updated)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Pipeline
|
|
94
|
+
|
|
95
|
+
Batched updates using Redis pipelining. Use for high-volume simple field updates:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class AddDefaultSettings < Familia::Migration::Pipeline
|
|
99
|
+
self.migration_id = '20260131_default_settings'
|
|
100
|
+
|
|
101
|
+
def prepare
|
|
102
|
+
@model_class = User
|
|
103
|
+
@batch_size = 100 # smaller batches for pipelines
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def should_process?(user)
|
|
107
|
+
user.settings.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_update_fields(user)
|
|
111
|
+
{ 'settings' => '{}' }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Override `execute_update` for custom pipeline operations:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
def execute_update(pipe, obj, fields, original_key)
|
|
120
|
+
dbkey = original_key || obj.dbkey
|
|
121
|
+
pipe.hmset(dbkey, *fields.flatten)
|
|
122
|
+
pipe.expire(dbkey, 86400) # also set TTL
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Class Attributes
|
|
127
|
+
|
|
128
|
+
| Attribute | Required | Description |
|
|
129
|
+
|-----------|----------|-------------|
|
|
130
|
+
| `migration_id` | Yes | Unique identifier. Format: `YYYYMMDD_HHMMSS_snake_case_name` |
|
|
131
|
+
| `description` | No | Human-readable summary for status output |
|
|
132
|
+
| `dependencies` | No | Array of migration IDs that must run first |
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
class BuildEmailIndex < Familia::Migration::Base
|
|
136
|
+
self.migration_id = '20260131_150000_build_index'
|
|
137
|
+
self.description = 'Create secondary index for email lookups'
|
|
138
|
+
self.dependencies = ['20260131_120000_normalize_emails']
|
|
139
|
+
# ...
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Lifecycle Methods
|
|
144
|
+
|
|
145
|
+
| Method | Purpose | Required |
|
|
146
|
+
|--------|---------|----------|
|
|
147
|
+
| `prepare` | Initialize config, set `@model_class` | Model/Pipeline only |
|
|
148
|
+
| `migration_needed?` | Idempotency check. Return `false` to skip. | Yes |
|
|
149
|
+
| `migrate` | Core migration logic | Base only |
|
|
150
|
+
| `process_record(obj, key)` | Per-record logic | Model only |
|
|
151
|
+
| `should_process?(obj)` | Filter predicate | Pipeline only |
|
|
152
|
+
| `build_update_fields(obj)` | Return Hash of field updates | Pipeline only |
|
|
153
|
+
| `down` | Rollback logic | No (enables rollback) |
|
|
154
|
+
|
|
155
|
+
## Dry Run vs Live
|
|
156
|
+
|
|
157
|
+
Wrap destructive operations with `for_realsies_this_time?`:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
def migrate
|
|
161
|
+
redis.scan_each(match: 'session:*') do |key|
|
|
162
|
+
for_realsies_this_time? do
|
|
163
|
+
redis.del(key) # only executes with --run
|
|
164
|
+
end
|
|
165
|
+
track_stat(:deleted)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| Mode | `for_realsies_this_time?` | Registry Updated |
|
|
171
|
+
|------|---------------------------|------------------|
|
|
172
|
+
| Dry run (`:dry_run` task) | Block skipped | No |
|
|
173
|
+
| Live (`:run` task) | Block executes | Yes |
|
|
174
|
+
|
|
175
|
+
## Dependencies
|
|
176
|
+
|
|
177
|
+
Dependencies ensure execution order. The runner uses topological sort (Kahn's algorithm).
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
class MigrationA < Familia::Migration::Base
|
|
181
|
+
self.migration_id = 'step_a'
|
|
182
|
+
self.dependencies = []
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
class MigrationB < Familia::Migration::Base
|
|
186
|
+
self.migration_id = 'step_b'
|
|
187
|
+
self.dependencies = ['step_a'] # runs after step_a
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Rollback is blocked if dependents are still applied:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
runner.rollback('step_a')
|
|
195
|
+
# => Errors::HasDependents if step_b is applied
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Rollback
|
|
199
|
+
|
|
200
|
+
Implement `down` to enable rollback:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
class AddFeatureFlag < Familia::Migration::Base
|
|
204
|
+
self.migration_id = '20260131_feature_flag'
|
|
205
|
+
|
|
206
|
+
def migration_needed?
|
|
207
|
+
!redis.exists?('config:feature:enabled')
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def migrate
|
|
211
|
+
for_realsies_this_time? do
|
|
212
|
+
redis.set('config:feature:enabled', 'true')
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def down
|
|
217
|
+
redis.del('config:feature:enabled')
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Check reversibility:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
instance = AddFeatureFlag.new
|
|
226
|
+
instance.reversible? # => true
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Lua Scripts
|
|
230
|
+
|
|
231
|
+
Use `Familia::Migration::Script` for atomic operations:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
# Rename hash field atomically
|
|
235
|
+
Familia::Migration::Script.execute(
|
|
236
|
+
redis,
|
|
237
|
+
:rename_field,
|
|
238
|
+
keys: ['user:123:object'],
|
|
239
|
+
argv: ['old_name', 'new_name']
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Built-in scripts:
|
|
244
|
+
|
|
245
|
+
| Script | Purpose | KEYS | ARGV |
|
|
246
|
+
|--------|---------|------|------|
|
|
247
|
+
| `:rename_field` | Rename hash field | `[hash_key]` | `[old, new]` |
|
|
248
|
+
| `:copy_field` | Copy field within hash | `[hash_key]` | `[src, dst]` |
|
|
249
|
+
| `:delete_field` | Delete hash field | `[hash_key]` | `[field]` |
|
|
250
|
+
| `:rename_key_preserve_ttl` | Rename key, keep TTL | `[src, dst]` | `[]` |
|
|
251
|
+
| `:backup_and_modify_field` | Backup old value, set new | `[hash, backup]` | `[field, value, ttl]` |
|
|
252
|
+
|
|
253
|
+
Register custom scripts:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
Familia::Migration::Script.register(:my_script, <<~LUA)
|
|
257
|
+
local key = KEYS[1]
|
|
258
|
+
return redis.call('GET', key)
|
|
259
|
+
LUA
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## CLI Reference
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# Status
|
|
266
|
+
bundle exec rake familia:migrate:status # Show applied/pending
|
|
267
|
+
bundle exec rake familia:migrate:validate # Check dependency issues
|
|
268
|
+
bundle exec rake familia:migrate:schema_drift # Models with changed schemas
|
|
269
|
+
|
|
270
|
+
# Execution
|
|
271
|
+
bundle exec rake familia:migrate:dry_run # Preview (no changes)
|
|
272
|
+
bundle exec rake familia:migrate # Apply all pending
|
|
273
|
+
bundle exec rake familia:migrate:run # Same as above
|
|
274
|
+
|
|
275
|
+
# Rollback
|
|
276
|
+
bundle exec rake "familia:migrate:rollback[20260131_120000_migration_id]"
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Statistics
|
|
280
|
+
|
|
281
|
+
Track operations with `track_stat`:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
def process_record(obj, key)
|
|
285
|
+
if obj.email.blank?
|
|
286
|
+
track_stat(:skipped_blank)
|
|
287
|
+
return
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
for_realsies_this_time? do
|
|
291
|
+
obj.email = obj.email.downcase
|
|
292
|
+
obj.save
|
|
293
|
+
end
|
|
294
|
+
track_stat(:records_updated)
|
|
295
|
+
end
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Access stats:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
instance.stats[:records_updated] # => 42
|
|
302
|
+
instance.stats[:skipped_blank] # => 7
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Configuration
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
Familia::Migration.configure do |config|
|
|
309
|
+
config.migrations_key = 'familia:migrations' # Registry key prefix
|
|
310
|
+
config.backup_ttl = 86_400 # Backup expiration (24h)
|
|
311
|
+
config.batch_size = 1000 # Default SCAN batch
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Best Practices
|
|
316
|
+
|
|
317
|
+
1. **Test locally first.** Run dry run, verify stats, then run live on staging before production.
|
|
318
|
+
|
|
319
|
+
2. **Deploy schema changes separately.** Avoid updating model definitions and running migrations in the same deploy. New model logic can break migration code.
|
|
320
|
+
|
|
321
|
+
3. **Keep migrations idempotent.** `migration_needed?` should return `false` after successful execution.
|
|
322
|
+
|
|
323
|
+
4. **Use descriptive IDs.** `20260131_120000_normalize_customer_emails` beats `20260131_fix_stuff`.
|
|
324
|
+
|
|
325
|
+
5. **Backup critical data.** Use `:backup_and_modify_field` or `registry.backup_field` before destructive changes.
|
|
326
|
+
|
|
327
|
+
## Error Reference
|
|
328
|
+
|
|
329
|
+
| Error | Cause |
|
|
330
|
+
|-------|-------|
|
|
331
|
+
| `NotReversible` | `down` not implemented |
|
|
332
|
+
| `NotApplied` | Rollback of unapplied migration |
|
|
333
|
+
| `DependencyNotMet` | Dependency not yet applied |
|
|
334
|
+
| `HasDependents` | Rollback blocked by dependents |
|
|
335
|
+
| `CircularDependency` | Dependency cycle detected |
|
|
336
|
+
| `PreconditionFailed` | `@model_class` not set in `prepare` |
|
|
337
|
+
|
|
338
|
+
## Source Files
|
|
339
|
+
|
|
340
|
+
- [`lib/familia/migration/base.rb`](../../lib/familia/migration/base.rb)
|
|
341
|
+
- [`lib/familia/migration/model.rb`](../../lib/familia/migration/model.rb)
|
|
342
|
+
- [`lib/familia/migration/pipeline.rb`](../../lib/familia/migration/pipeline.rb)
|
|
343
|
+
- [`lib/familia/migration/registry.rb`](../../lib/familia/migration/registry.rb)
|
|
344
|
+
- [`lib/familia/migration/runner.rb`](../../lib/familia/migration/runner.rb)
|
|
345
|
+
- [`lib/familia/migration/script.rb`](../../lib/familia/migration/script.rb)
|