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,431 @@
1
+ # try/migration/model_try.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ require_relative '../support/helpers/test_helpers'
6
+ require_relative '../../lib/familia/migration'
7
+
8
+ Familia.debug = false
9
+
10
+ @redis = Familia.dbclient
11
+ @test_id = "#{Process.pid}_#{Time.now.to_i}"
12
+ @prefix = "familia:test:model:#{@test_id}"
13
+
14
+ @initial_migrations = Familia::Migration.migrations.dup
15
+
16
+ # Test model class for migrations - uses unique prefix for isolation
17
+ class ModelTestRecord < Familia::Horreum
18
+ identifier_field :record_id
19
+ field :record_id
20
+ field :name
21
+ field :status
22
+ field :legacy_field
23
+ field :new_field
24
+ end
25
+
26
+ # Simple Model migration for testing
27
+ class SimpleModelMigration < Familia::Migration::Model
28
+ self.migration_id = 'model_test_simple'
29
+ self.description = 'Simple model migration test'
30
+
31
+ class << self
32
+ attr_accessor :processed_keys
33
+ end
34
+ self.processed_keys = []
35
+
36
+ def prepare
37
+ @model_class = ModelTestRecord
38
+ @batch_size = 10
39
+ end
40
+
41
+ def process_record(obj, key)
42
+ self.class.processed_keys << key
43
+ track_stat(:records_updated)
44
+ end
45
+ end
46
+
47
+ # Model migration that skips records
48
+ class SkippingModelMigration < Familia::Migration::Model
49
+ self.migration_id = 'model_test_skipping'
50
+
51
+ class << self
52
+ attr_accessor :skipped_count, :processed_count
53
+ end
54
+ self.skipped_count = 0
55
+ self.processed_count = 0
56
+
57
+ def prepare
58
+ @model_class = ModelTestRecord
59
+ @batch_size = 10
60
+ end
61
+
62
+ def process_record(obj, key)
63
+ if obj.status == 'skip_me'
64
+ self.class.skipped_count += 1
65
+ track_stat(:skipped)
66
+ return
67
+ end
68
+
69
+ for_realsies_this_time? do
70
+ obj.new_field = 'migrated'
71
+ obj.save
72
+ self.class.processed_count += 1
73
+ end
74
+ track_stat(:records_updated)
75
+ end
76
+ end
77
+
78
+ # Model migration that raises errors
79
+ class ErrorModelMigration < Familia::Migration::Model
80
+ self.migration_id = 'model_test_error'
81
+
82
+ class << self
83
+ attr_accessor :error_triggered
84
+ end
85
+ self.error_triggered = false
86
+
87
+ def prepare
88
+ @model_class = ModelTestRecord
89
+ @batch_size = 10
90
+ end
91
+
92
+ def process_record(obj, key)
93
+ if obj.name == 'trigger_error'
94
+ self.class.error_triggered = true
95
+ raise 'Intentional test error'
96
+ end
97
+ track_stat(:records_updated)
98
+ end
99
+ end
100
+
101
+ # Model migration without model_class - should fail validation
102
+ class InvalidModelMigration < Familia::Migration::Model
103
+ self.migration_id = 'model_test_invalid'
104
+
105
+ def prepare
106
+ # Intentionally not setting @model_class
107
+ end
108
+
109
+ def process_record(obj, key)
110
+ # Never called
111
+ end
112
+ end
113
+
114
+ # Model migration with custom load_from_key
115
+ class CustomLoadMigration < Familia::Migration::Model
116
+ self.migration_id = 'model_test_custom_load'
117
+
118
+ class << self
119
+ attr_accessor :custom_load_called
120
+ end
121
+ self.custom_load_called = false
122
+
123
+ def prepare
124
+ @model_class = ModelTestRecord
125
+ @batch_size = 10
126
+ end
127
+
128
+ def load_from_key(key)
129
+ self.class.custom_load_called = true
130
+ super(key)
131
+ end
132
+
133
+ def process_record(obj, key)
134
+ track_stat(:records_updated)
135
+ end
136
+ end
137
+
138
+ # Model migration that returns migration_needed? false
139
+ class NotNeededMigration < Familia::Migration::Model
140
+ self.migration_id = 'model_test_not_needed'
141
+
142
+ def prepare
143
+ @model_class = ModelTestRecord
144
+ @batch_size = 10
145
+ end
146
+
147
+ def migration_needed?
148
+ false
149
+ end
150
+
151
+ def process_record(obj, key)
152
+ track_stat(:records_updated)
153
+ end
154
+ end
155
+
156
+ # Helper to create test records with unique prefix
157
+ def create_test_record(suffix, name: 'Test', status: 'active')
158
+ id = "#{@test_id}_#{suffix}"
159
+ record = ModelTestRecord.new(record_id: id, name: name, status: status)
160
+ record.save
161
+ record
162
+ end
163
+
164
+ # Helper to cleanup all test records
165
+ def cleanup_records
166
+ pattern = "model_test_record:#{@test_id}_*"
167
+ @redis.keys(pattern).each { |k| @redis.del(k) }
168
+ end
169
+
170
+ cleanup_records
171
+
172
+ ## Model class is a subclass of Base
173
+ Familia::Migration::Model < Familia::Migration::Base
174
+ #=> true
175
+
176
+ ## Model initializes with default counters
177
+ migration = SimpleModelMigration.new
178
+ [migration.total_scanned, migration.records_needing_update,
179
+ migration.records_updated, migration.error_count]
180
+ #=> [0, 0, 0, 0]
181
+
182
+ ## Model initializes with default batch_size from config
183
+ migration = SimpleModelMigration.new
184
+ migration.batch_size == Familia::Migration.config.batch_size
185
+ #=> true
186
+
187
+ ## Model prepare sets model_class
188
+ SimpleModelMigration.processed_keys = []
189
+ migration = SimpleModelMigration.new
190
+ migration.prepare
191
+ migration.model_class == ModelTestRecord
192
+ #=> true
193
+
194
+ ## Model prepare allows custom batch_size
195
+ migration = SimpleModelMigration.new
196
+ migration.prepare
197
+ migration.batch_size
198
+ #=> 10
199
+
200
+ ## Model validate raises when model_class not set
201
+ begin
202
+ migration = InvalidModelMigration.new
203
+ migration.prepare
204
+ migration.migrate
205
+ false
206
+ rescue Familia::Migration::Errors::PreconditionFailed => e
207
+ e.message.include?('Model class not set')
208
+ end
209
+ #=> true
210
+
211
+ ## Model validate raises for non-Horreum class
212
+ class NonHorreumMigration < Familia::Migration::Model
213
+ self.migration_id = 'model_test_non_horreum'
214
+ def prepare
215
+ @model_class = String
216
+ end
217
+ def process_record(obj, key); end
218
+ end
219
+ begin
220
+ migration = NonHorreumMigration.new
221
+ migration.prepare
222
+ migration.migrate
223
+ false
224
+ rescue Familia::Migration::Errors::PreconditionFailed => e
225
+ e.message.include?('must be a Familia::Horreum subclass')
226
+ end
227
+ #=> true
228
+
229
+ ## Model migration processes records via SCAN
230
+ cleanup_records
231
+ create_test_record('record1', name: 'Record 1')
232
+ create_test_record('record2', name: 'Record 2')
233
+ SimpleModelMigration.processed_keys = []
234
+ migration = SimpleModelMigration.new(run: true)
235
+ migration.prepare
236
+ migration.migrate
237
+ SimpleModelMigration.processed_keys.size >= 2
238
+ #=> true
239
+
240
+ ## Model migration tracks total_scanned
241
+ cleanup_records
242
+ create_test_record('a1', name: 'A1')
243
+ create_test_record('a2', name: 'A2')
244
+ create_test_record('a3', name: 'A3')
245
+ SimpleModelMigration.processed_keys = []
246
+ migration = SimpleModelMigration.new(run: true)
247
+ migration.prepare
248
+ migration.migrate
249
+ migration.total_scanned >= 3
250
+ #=> true
251
+
252
+ ## Model migration tracks records_needing_update
253
+ cleanup_records
254
+ create_test_record('b1')
255
+ create_test_record('b2')
256
+ SimpleModelMigration.processed_keys = []
257
+ migration = SimpleModelMigration.new(run: true)
258
+ migration.prepare
259
+ migration.migrate
260
+ migration.records_needing_update >= 2
261
+ #=> true
262
+
263
+ ## Model migration increments records_updated via track_stat
264
+ cleanup_records
265
+ create_test_record('c1')
266
+ SimpleModelMigration.processed_keys = []
267
+ migration = SimpleModelMigration.new(run: true)
268
+ migration.prepare
269
+ migration.migrate
270
+ migration.records_updated >= 1
271
+ #=> true
272
+
273
+ ## Model migration returns true when no errors
274
+ cleanup_records
275
+ create_test_record('d1')
276
+ SimpleModelMigration.processed_keys = []
277
+ migration = SimpleModelMigration.new(run: true)
278
+ migration.prepare
279
+ migration.migrate
280
+ #=> true
281
+
282
+ ## Model migration handles errors and increments error_count
283
+ cleanup_records
284
+ create_test_record('e1', name: 'trigger_error')
285
+ ErrorModelMigration.error_triggered = false
286
+ migration = ErrorModelMigration.new(run: true)
287
+ migration.prepare
288
+ migration.migrate
289
+ ErrorModelMigration.error_triggered && migration.error_count >= 1
290
+ #=> true
291
+
292
+ ## Model migration returns false when errors occurred
293
+ cleanup_records
294
+ create_test_record('f1', name: 'trigger_error')
295
+ ErrorModelMigration.error_triggered = false
296
+ migration = ErrorModelMigration.new(run: true)
297
+ migration.prepare
298
+ migration.migrate
299
+ #=> false
300
+
301
+ ## Model migration continues after errors
302
+ cleanup_records
303
+ create_test_record('g1', name: 'trigger_error')
304
+ create_test_record('g2', name: 'Normal')
305
+ ErrorModelMigration.error_triggered = false
306
+ migration = ErrorModelMigration.new(run: true)
307
+ migration.prepare
308
+ migration.migrate
309
+ migration.error_count >= 1 && migration.records_needing_update >= 2
310
+ #=> true
311
+
312
+ ## Skipping migration respects dry_run mode
313
+ cleanup_records
314
+ create_test_record('h1', status: 'active')
315
+ SkippingModelMigration.skipped_count = 0
316
+ SkippingModelMigration.processed_count = 0
317
+ migration = SkippingModelMigration.new(run: false)
318
+ migration.prepare
319
+ migration.migrate
320
+ [migration.dry_run?, SkippingModelMigration.processed_count]
321
+ #=> [true, 0]
322
+
323
+ ## Skipping migration executes in actual_run mode
324
+ cleanup_records
325
+ create_test_record('i1', status: 'active')
326
+ SkippingModelMigration.skipped_count = 0
327
+ SkippingModelMigration.processed_count = 0
328
+ migration = SkippingModelMigration.new(run: true)
329
+ migration.prepare
330
+ migration.migrate
331
+ SkippingModelMigration.processed_count >= 1
332
+ #=> true
333
+
334
+ ## Skipping migration tracks skipped records
335
+ cleanup_records
336
+ create_test_record('j1', status: 'skip_me')
337
+ create_test_record('j2', status: 'active')
338
+ SkippingModelMigration.skipped_count = 0
339
+ SkippingModelMigration.processed_count = 0
340
+ migration = SkippingModelMigration.new(run: true)
341
+ migration.prepare
342
+ migration.migrate
343
+ SkippingModelMigration.skipped_count >= 1 && SkippingModelMigration.processed_count >= 1
344
+ #=> true
345
+
346
+ ## Custom load_from_key is called
347
+ cleanup_records
348
+ create_test_record('k1')
349
+ CustomLoadMigration.custom_load_called = false
350
+ migration = CustomLoadMigration.new(run: true)
351
+ migration.prepare
352
+ migration.migrate
353
+ CustomLoadMigration.custom_load_called
354
+ #=> true
355
+
356
+ ## migration_needed? default returns true
357
+ migration = SimpleModelMigration.new
358
+ migration.prepare
359
+ migration.migration_needed?
360
+ #=> true
361
+
362
+ ## NotNeeded migration is skipped by run
363
+ cleanup_records
364
+ result = NotNeededMigration.run(run: true)
365
+ result.nil?
366
+ #=> true
367
+
368
+ ## track_stat correctly increments stats (via public stats accessor)
369
+ migration = SimpleModelMigration.new
370
+ migration.send(:track_stat, :custom_stat)
371
+ migration.send(:track_stat, :custom_stat, 5)
372
+ migration.stats[:custom_stat]
373
+ #=> 6
374
+
375
+ ## interactive mode defaults to false
376
+ migration = SimpleModelMigration.new
377
+ migration.prepare
378
+ migration.interactive
379
+ #=> false
380
+
381
+ ## dbclient returns Redis connection
382
+ migration = SimpleModelMigration.new
383
+ migration.prepare
384
+ migration.send(:dbclient).respond_to?(:scan)
385
+ #=> true
386
+
387
+ ## validate_before_transform? defaults to false
388
+ migration = SimpleModelMigration.new
389
+ migration.send(:validate_before_transform?)
390
+ #=> false
391
+
392
+ ## validate_after_transform? defaults to false
393
+ migration = SimpleModelMigration.new
394
+ migration.send(:validate_after_transform?)
395
+ #=> false
396
+
397
+ ## Base process_record raises NotImplementedError
398
+ class BareModel < Familia::Migration::Model
399
+ self.migration_id = 'bare_model'
400
+ def prepare
401
+ @model_class = ModelTestRecord
402
+ end
403
+ end
404
+ migration = BareModel.new
405
+ begin
406
+ migration.send(:process_record, nil, 'key')
407
+ false
408
+ rescue NotImplementedError
409
+ true
410
+ end
411
+ #=> true
412
+
413
+ ## Base prepare raises NotImplementedError
414
+ begin
415
+ Familia::Migration::Model.new.send(:prepare)
416
+ false
417
+ rescue NotImplementedError
418
+ true
419
+ end
420
+ #=> true
421
+
422
+ ## scan_pattern is set from model_class after validation
423
+ cleanup_records
424
+ migration = SimpleModelMigration.new
425
+ migration.prepare
426
+ migration.send(:validate_model_class!)
427
+ migration.scan_pattern.include?('model_test_record')
428
+ #=> true
429
+
430
+ cleanup_records
431
+ Familia::Migration.migrations.replace(@initial_migrations)