familia 2.0.0.pre26 → 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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.rst +94 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +12 -2
  5. data/README.md +1 -3
  6. data/docs/guides/feature-encrypted-fields.md +1 -1
  7. data/docs/guides/feature-expiration.md +1 -1
  8. data/docs/guides/feature-quantization.md +1 -1
  9. data/docs/guides/writing-migrations.md +345 -0
  10. data/docs/overview.md +7 -7
  11. data/docs/reference/api-technical.md +103 -7
  12. data/examples/migrations/v1_to_v2_serialization_migration.rb +374 -0
  13. data/examples/schemas/customer.json +33 -0
  14. data/examples/schemas/session.json +27 -0
  15. data/familia.gemspec +3 -2
  16. data/lib/familia/features/schema_validation.rb +139 -0
  17. data/lib/familia/migration/base.rb +447 -0
  18. data/lib/familia/migration/errors.rb +31 -0
  19. data/lib/familia/migration/model.rb +418 -0
  20. data/lib/familia/migration/pipeline.rb +226 -0
  21. data/lib/familia/migration/rake_tasks.rake +3 -0
  22. data/lib/familia/migration/rake_tasks.rb +160 -0
  23. data/lib/familia/migration/registry.rb +364 -0
  24. data/lib/familia/migration/runner.rb +311 -0
  25. data/lib/familia/migration/script.rb +234 -0
  26. data/lib/familia/migration.rb +43 -0
  27. data/lib/familia/schema_registry.rb +173 -0
  28. data/lib/familia/settings.rb +63 -1
  29. data/lib/familia/version.rb +1 -1
  30. data/lib/familia.rb +1 -0
  31. data/try/features/schema_registry_try.rb +193 -0
  32. data/try/features/schema_validation_feature_try.rb +218 -0
  33. data/try/migration/base_try.rb +226 -0
  34. data/try/migration/errors_try.rb +67 -0
  35. data/try/migration/integration_try.rb +451 -0
  36. data/try/migration/model_try.rb +431 -0
  37. data/try/migration/pipeline_try.rb +460 -0
  38. data/try/migration/rake_tasks_try.rb +61 -0
  39. data/try/migration/registry_try.rb +199 -0
  40. data/try/migration/runner_try.rb +311 -0
  41. data/try/migration/schema_validation_try.rb +201 -0
  42. data/try/migration/script_try.rb +192 -0
  43. data/try/migration/v1_to_v2_serialization_try.rb +513 -0
  44. data/try/performance/benchmarks_try.rb +11 -12
  45. metadata +45 -27
  46. data/docs/migrating/v2.0.0-pre.md +0 -84
  47. data/docs/migrating/v2.0.0-pre11.md +0 -253
  48. data/docs/migrating/v2.0.0-pre12.md +0 -306
  49. data/docs/migrating/v2.0.0-pre13.md +0 -95
  50. data/docs/migrating/v2.0.0-pre14.md +0 -37
  51. data/docs/migrating/v2.0.0-pre18.md +0 -58
  52. data/docs/migrating/v2.0.0-pre19.md +0 -197
  53. data/docs/migrating/v2.0.0-pre22.md +0 -241
  54. data/docs/migrating/v2.0.0-pre5.md +0 -131
  55. data/docs/migrating/v2.0.0-pre6.md +0 -154
  56. data/docs/migrating/v2.0.0-pre7.md +0 -222
@@ -0,0 +1,451 @@
1
+ # try/migration/integration_try.rb
2
+ #
3
+ # Integration tests for complete migration workflow scenarios
4
+ #
5
+ # frozen_string_literal: true
6
+
7
+ require_relative '../support/helpers/test_helpers'
8
+ require_relative '../../lib/familia/migration'
9
+
10
+ Familia.debug = false
11
+
12
+ # Setup - unique prefix for this test run
13
+ @redis = Familia.dbclient
14
+ @prefix = "familia:test:integration:#{Process.pid}:#{Time.now.to_i}"
15
+ @registry = Familia::Migration::Registry.new(redis: @redis, prefix: @prefix)
16
+
17
+ # Store initial migrations
18
+ @initial_migrations = Familia::Migration.migrations.dup
19
+
20
+ # Helper to reset state
21
+ def reset_state
22
+ @redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
23
+ end
24
+
25
+ reset_state
26
+
27
+ # ============================================
28
+ # Define all migration classes upfront
29
+ # ============================================
30
+
31
+ # Scenario 1: Simple migration lifecycle
32
+ class IntegrationSimpleMigration < Familia::Migration::Base
33
+ self.migration_id = 'integration_simple'
34
+ self.description = 'Simple integration test'
35
+
36
+ class << self
37
+ attr_accessor :ran_count
38
+ end
39
+ self.ran_count = 0
40
+
41
+ def migration_needed?
42
+ self.class.ran_count < 1
43
+ end
44
+
45
+ def migrate
46
+ self.class.ran_count += 1
47
+ track_stat(:executed)
48
+ end
49
+ end
50
+
51
+ # Scenario 2: Migration with dependencies
52
+ class IntegrationDepA < Familia::Migration::Base
53
+ self.migration_id = 'integration_dep_a'
54
+ self.dependencies = []
55
+
56
+ class << self
57
+ attr_accessor :order
58
+ end
59
+ self.order = []
60
+
61
+ def migration_needed?; true; end
62
+ def migrate
63
+ self.class.order << :a
64
+ end
65
+ end
66
+
67
+ class IntegrationDepB < Familia::Migration::Base
68
+ self.migration_id = 'integration_dep_b'
69
+ self.dependencies = ['integration_dep_a']
70
+
71
+ def migration_needed?; true; end
72
+ def migrate
73
+ IntegrationDepA.order << :b
74
+ end
75
+ end
76
+
77
+ # Scenario 3: Dry run mode
78
+ class IntegrationDryRun < Familia::Migration::Base
79
+ self.migration_id = 'integration_dry'
80
+
81
+ class << self
82
+ attr_accessor :executed
83
+ end
84
+ self.executed = false
85
+
86
+ def migration_needed?; true; end
87
+ def migrate
88
+ self.class.executed = true
89
+ track_stat(:would_run)
90
+ end
91
+ end
92
+
93
+ # Scenario 4: Rollback flow
94
+ class IntegrationRollback < Familia::Migration::Base
95
+ self.migration_id = 'integration_rollback'
96
+
97
+ class << self
98
+ attr_accessor :state
99
+ end
100
+ self.state = :initial
101
+
102
+ def migration_needed?; true; end
103
+
104
+ def migrate
105
+ self.class.state = :migrated
106
+ end
107
+
108
+ def down
109
+ self.class.state = :rolled_back
110
+ end
111
+ end
112
+
113
+ # Scenario 7: Error handling
114
+ class IntegrationFailingMigration < Familia::Migration::Base
115
+ self.migration_id = 'integration_failing'
116
+
117
+ def migration_needed?; true; end
118
+
119
+ def migrate
120
+ raise "Intentional failure for testing"
121
+ end
122
+ end
123
+
124
+ # Scenario 8: Migration status reporting
125
+ class IntegrationStatusA < Familia::Migration::Base
126
+ self.migration_id = 'integration_status_a'
127
+ self.description = 'Status test A'
128
+
129
+ def migration_needed?; true; end
130
+ def migrate; end
131
+ end
132
+
133
+ class IntegrationStatusB < Familia::Migration::Base
134
+ self.migration_id = 'integration_status_b'
135
+ self.description = 'Status test B'
136
+
137
+ def migration_needed?; true; end
138
+ def migrate; end
139
+ def down; end
140
+ end
141
+
142
+ # Scenario 9: for_realsies_this_time? guard
143
+ class IntegrationGuardedMigration < Familia::Migration::Base
144
+ self.migration_id = 'integration_guarded'
145
+
146
+ class << self
147
+ attr_accessor :guarded_executed
148
+ end
149
+ self.guarded_executed = false
150
+
151
+ def migration_needed?; true; end
152
+
153
+ def migrate
154
+ for_realsies_this_time? do
155
+ self.class.guarded_executed = true
156
+ end
157
+ track_stat(:checked)
158
+ end
159
+ end
160
+
161
+ # Scenario 10: Validate dependencies
162
+ class IntegrationOrphanMigration < Familia::Migration::Base
163
+ self.migration_id = 'integration_orphan'
164
+ self.dependencies = ['nonexistent_parent']
165
+
166
+ def migration_needed?; true; end
167
+ def migrate; end
168
+ end
169
+
170
+ # Scenario 11: Run with limit
171
+ class IntegrationLimitA < Familia::Migration::Base
172
+ self.migration_id = 'integration_limit_a'
173
+ self.dependencies = []
174
+ def migration_needed?; true; end
175
+ def migrate; end
176
+ end
177
+
178
+ class IntegrationLimitB < Familia::Migration::Base
179
+ self.migration_id = 'integration_limit_b'
180
+ self.dependencies = []
181
+ def migration_needed?; true; end
182
+ def migrate; end
183
+ end
184
+
185
+ class IntegrationLimitC < Familia::Migration::Base
186
+ self.migration_id = 'integration_limit_c'
187
+ self.dependencies = []
188
+ def migration_needed?; true; end
189
+ def migrate; end
190
+ end
191
+
192
+ # ============================================
193
+ # Scenario 1: Simple migration lifecycle
194
+ # ============================================
195
+
196
+ ## Scenario 1: Migration runs and is recorded
197
+ runner = Familia::Migration::Runner.new(
198
+ migrations: [IntegrationSimpleMigration],
199
+ registry: @registry
200
+ )
201
+ results = runner.run(dry_run: false)
202
+ [results.first[:status], @registry.applied?('integration_simple')]
203
+ #=> [:success, true]
204
+
205
+ ## Scenario 1: Re-run skips already applied migration
206
+ runner = Familia::Migration::Runner.new(
207
+ migrations: [IntegrationSimpleMigration],
208
+ registry: @registry
209
+ )
210
+ runner.pending.empty?
211
+ #=> true
212
+
213
+ reset_state
214
+ IntegrationSimpleMigration.ran_count = 0
215
+
216
+ # ============================================
217
+ # Scenario 2: Migration with dependencies
218
+ # ============================================
219
+
220
+ ## Scenario 2: Dependencies run in correct order
221
+ IntegrationDepA.order = []
222
+ runner = Familia::Migration::Runner.new(
223
+ migrations: [IntegrationDepB, IntegrationDepA],
224
+ registry: @registry
225
+ )
226
+ runner.run(dry_run: false)
227
+ IntegrationDepA.order
228
+ #=> [:a, :b]
229
+
230
+ ## Scenario 2: Both are recorded as applied
231
+ [@registry.applied?('integration_dep_a'), @registry.applied?('integration_dep_b')]
232
+ #=> [true, true]
233
+
234
+ reset_state
235
+ IntegrationDepA.order = []
236
+
237
+ # ============================================
238
+ # Scenario 3: Dry run mode
239
+ # ============================================
240
+
241
+ ## Scenario 3: Dry run does not persist
242
+ IntegrationDryRun.executed = false
243
+ runner = Familia::Migration::Runner.new(
244
+ migrations: [IntegrationDryRun],
245
+ registry: @registry
246
+ )
247
+ @dry_run_results = runner.run(dry_run: true)
248
+ [@dry_run_results.first[:dry_run], @registry.applied?('integration_dry')]
249
+ #=> [true, false]
250
+
251
+ ## Scenario 3: Dry run still returns success status
252
+ @dry_run_results.first[:status]
253
+ #=> :success
254
+
255
+ reset_state
256
+ IntegrationDryRun.executed = false
257
+
258
+ # ============================================
259
+ # Scenario 4: Rollback flow
260
+ # ============================================
261
+
262
+ ## Scenario 4: Rollback executes down method
263
+ IntegrationRollback.state = :initial
264
+ runner = Familia::Migration::Runner.new(
265
+ migrations: [IntegrationRollback],
266
+ registry: @registry
267
+ )
268
+ runner.run(dry_run: false)
269
+ runner.rollback('integration_rollback')
270
+ IntegrationRollback.state
271
+ #=> :rolled_back
272
+
273
+ ## Scenario 4: Registry shows rollback
274
+ @registry.applied?('integration_rollback')
275
+ #=> false
276
+
277
+ ## Scenario 4: Metadata shows rolled_back status
278
+ @rollback_meta = @registry.metadata('integration_rollback')
279
+ @rollback_meta[:status]
280
+ #=> 'rolled_back'
281
+
282
+ reset_state
283
+ IntegrationRollback.state = :initial
284
+
285
+ # ============================================
286
+ # Scenario 5: Lua script atomicity
287
+ # ============================================
288
+
289
+ ## Scenario 5: rename_field is atomic
290
+ @test_key = "#{@prefix}:script_test"
291
+ @redis.hset(@test_key, 'old_field', 'test_value')
292
+ @redis.hset(@test_key, 'other_field', 'keep_me')
293
+
294
+ Familia::Migration::Script.execute(
295
+ @redis,
296
+ :rename_field,
297
+ keys: [@test_key],
298
+ argv: ['old_field', 'new_field']
299
+ )
300
+
301
+ [
302
+ @redis.hexists(@test_key, 'old_field'),
303
+ @redis.hget(@test_key, 'new_field'),
304
+ @redis.hget(@test_key, 'other_field')
305
+ ]
306
+ #=> [false, 'test_value', 'keep_me']
307
+
308
+ ## Scenario 5: backup_and_modify_field creates backup
309
+ @backup_key = "#{@prefix}:backup_test"
310
+ @hash_key = "#{@prefix}:data_test"
311
+ @redis.hset(@hash_key, 'target', 'original')
312
+
313
+ Familia::Migration::Script.execute(
314
+ @redis,
315
+ :backup_and_modify_field,
316
+ keys: [@hash_key, @backup_key],
317
+ argv: ['target', 'modified', '3600']
318
+ )
319
+
320
+ [
321
+ @redis.hget(@hash_key, 'target'),
322
+ @redis.hget(@backup_key, "#{@hash_key}:target")
323
+ ]
324
+ #=> ['modified', 'original']
325
+
326
+ reset_state
327
+
328
+ # ============================================
329
+ # Scenario 6: Configuration
330
+ # ============================================
331
+
332
+ ## Scenario 6: Configuration defaults are set
333
+ config = Familia::Migration.config
334
+ [config.migrations_key, config.backup_ttl, config.batch_size]
335
+ #=> ['familia:migrations', 86400, 1000]
336
+
337
+ ## Scenario 6: Configuration can be changed
338
+ Familia::Migration.configure do |c|
339
+ c.batch_size = 500
340
+ end
341
+ Familia::Migration.config.batch_size
342
+ #=> 500
343
+
344
+ # Reset config
345
+ Familia::Migration.config.batch_size = 1000
346
+
347
+ # ============================================
348
+ # Scenario 7: Error handling
349
+ # ============================================
350
+
351
+ ## Scenario 7: Failed migration returns error in result
352
+ runner = Familia::Migration::Runner.new(
353
+ migrations: [IntegrationFailingMigration],
354
+ registry: @registry
355
+ )
356
+ @fail_result = runner.run(dry_run: false).first
357
+ [@fail_result[:status], @fail_result[:error].include?('Intentional failure')]
358
+ #=> [:failed, true]
359
+
360
+ ## Scenario 7: Failed migration is not recorded as applied
361
+ @registry.applied?('integration_failing')
362
+ #=> false
363
+
364
+ reset_state
365
+
366
+ # ============================================
367
+ # Scenario 8: Migration status reporting
368
+ # ============================================
369
+
370
+ ## Scenario 8: Status shows pending and applied correctly
371
+ runner = Familia::Migration::Runner.new(
372
+ migrations: [IntegrationStatusA, IntegrationStatusB],
373
+ registry: @registry
374
+ )
375
+ runner.run_one(IntegrationStatusA, dry_run: false)
376
+ @status_list = runner.status
377
+ @statuses = @status_list.map { |s| [s[:migration_id], s[:status]] }.to_h
378
+ [@statuses['integration_status_a'], @statuses['integration_status_b']]
379
+ #=> [:applied, :pending]
380
+
381
+ ## Scenario 8: Status correctly reports reversibility
382
+ @reversible_map = @status_list.map { |s| [s[:migration_id], s[:reversible]] }.to_h
383
+ [@reversible_map['integration_status_a'], @reversible_map['integration_status_b']]
384
+ #=> [false, true]
385
+
386
+ reset_state
387
+
388
+ # ============================================
389
+ # Scenario 9: for_realsies_this_time? guard
390
+ # ============================================
391
+
392
+ ## Scenario 9: Guarded block skipped in dry run
393
+ IntegrationGuardedMigration.guarded_executed = false
394
+ runner = Familia::Migration::Runner.new(
395
+ migrations: [IntegrationGuardedMigration],
396
+ registry: @registry
397
+ )
398
+ runner.run(dry_run: true)
399
+ IntegrationGuardedMigration.guarded_executed
400
+ #=> false
401
+
402
+ ## Scenario 9: Guarded block executes in actual run
403
+ IntegrationGuardedMigration.guarded_executed = false
404
+ reset_state
405
+ runner = Familia::Migration::Runner.new(
406
+ migrations: [IntegrationGuardedMigration],
407
+ registry: @registry
408
+ )
409
+ runner.run(dry_run: false)
410
+ IntegrationGuardedMigration.guarded_executed
411
+ #=> true
412
+
413
+ reset_state
414
+ IntegrationGuardedMigration.guarded_executed = false
415
+
416
+ # ============================================
417
+ # Scenario 10: Validate dependencies
418
+ # ============================================
419
+
420
+ ## Scenario 10: Validate detects missing dependencies
421
+ runner = Familia::Migration::Runner.new(
422
+ migrations: [IntegrationOrphanMigration],
423
+ registry: @registry
424
+ )
425
+ issues = runner.validate
426
+ issues.any? { |i| i[:type] == :missing_dependency && i[:dependency] == 'nonexistent_parent' }
427
+ #=> true
428
+
429
+ reset_state
430
+
431
+ # ============================================
432
+ # Scenario 11: Run with limit
433
+ # ============================================
434
+
435
+ ## Scenario 11: Run with limit applies only N migrations
436
+ runner = Familia::Migration::Runner.new(
437
+ migrations: [IntegrationLimitA, IntegrationLimitB, IntegrationLimitC],
438
+ registry: @registry
439
+ )
440
+ @limit_results = runner.run(dry_run: false, limit: 2)
441
+ [@limit_results.size, runner.pending.size]
442
+ #=> [2, 1]
443
+
444
+ reset_state
445
+
446
+ # ============================================
447
+ # Teardown
448
+ # ============================================
449
+
450
+ reset_state
451
+ Familia::Migration.migrations.replace(@initial_migrations)