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.
- checksums.yaml +4 -4
- data/CHANGELOG.rst +94 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +12 -2
- data/README.md +1 -3
- data/docs/guides/feature-encrypted-fields.md +1 -1
- data/docs/guides/feature-expiration.md +1 -1
- data/docs/guides/feature-quantization.md +1 -1
- data/docs/guides/writing-migrations.md +345 -0
- data/docs/overview.md +7 -7
- data/docs/reference/api-technical.md +103 -7
- 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 +3 -2
- 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 +45 -27
- data/docs/migrating/v2.0.0-pre.md +0 -84
- data/docs/migrating/v2.0.0-pre11.md +0 -253
- data/docs/migrating/v2.0.0-pre12.md +0 -306
- data/docs/migrating/v2.0.0-pre13.md +0 -95
- data/docs/migrating/v2.0.0-pre14.md +0 -37
- data/docs/migrating/v2.0.0-pre18.md +0 -58
- data/docs/migrating/v2.0.0-pre19.md +0 -197
- data/docs/migrating/v2.0.0-pre22.md +0 -241
- data/docs/migrating/v2.0.0-pre5.md +0 -131
- data/docs/migrating/v2.0.0-pre6.md +0 -154
- 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)
|