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,311 @@
|
|
|
1
|
+
# try/migration/runner_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
|
+
@prefix = "familia:test:runner:#{Process.pid}:#{Time.now.to_i}"
|
|
12
|
+
@registry = Familia::Migration::Registry.new(redis: @redis, prefix: @prefix)
|
|
13
|
+
|
|
14
|
+
# Clean any existing test keys
|
|
15
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
16
|
+
|
|
17
|
+
# Store initial migration count
|
|
18
|
+
@initial_migrations = Familia::Migration.migrations.dup
|
|
19
|
+
|
|
20
|
+
# Define test migration classes
|
|
21
|
+
class RunnerTestMigrationA < Familia::Migration::Base
|
|
22
|
+
self.migration_id = 'runner_test_a'
|
|
23
|
+
self.description = 'First migration'
|
|
24
|
+
self.dependencies = []
|
|
25
|
+
|
|
26
|
+
def migration_needed?
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def migrate
|
|
31
|
+
track_stat(:a_ran)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class RunnerTestMigrationB < Familia::Migration::Base
|
|
36
|
+
self.migration_id = 'runner_test_b'
|
|
37
|
+
self.description = 'Depends on A'
|
|
38
|
+
self.dependencies = ['runner_test_a']
|
|
39
|
+
|
|
40
|
+
def migration_needed?
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def migrate
|
|
45
|
+
track_stat(:b_ran)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class RunnerTestReversible < Familia::Migration::Base
|
|
50
|
+
self.migration_id = 'runner_test_reversible'
|
|
51
|
+
self.description = 'Reversible migration'
|
|
52
|
+
self.dependencies = []
|
|
53
|
+
|
|
54
|
+
def migration_needed?
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def migrate
|
|
59
|
+
track_stat(:forward)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def down
|
|
63
|
+
track_stat(:backward)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@test_migrations = [RunnerTestMigrationA, RunnerTestMigrationB, RunnerTestReversible]
|
|
68
|
+
|
|
69
|
+
# Circular dependency test migrations
|
|
70
|
+
class CircularMigrationA < Familia::Migration::Base
|
|
71
|
+
self.migration_id = 'circular_a'
|
|
72
|
+
self.dependencies = ['circular_b']
|
|
73
|
+
def migration_needed?; true; end
|
|
74
|
+
def migrate; end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class CircularMigrationB < Familia::Migration::Base
|
|
78
|
+
self.migration_id = 'circular_b'
|
|
79
|
+
self.dependencies = ['circular_a']
|
|
80
|
+
def migration_needed?; true; end
|
|
81
|
+
def migrate; end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class CircularMigrationC < Familia::Migration::Base
|
|
85
|
+
self.migration_id = 'circular_c'
|
|
86
|
+
self.dependencies = ['circular_d']
|
|
87
|
+
def migration_needed?; true; end
|
|
88
|
+
def migrate; end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class CircularMigrationD < Familia::Migration::Base
|
|
92
|
+
self.migration_id = 'circular_d'
|
|
93
|
+
self.dependencies = ['circular_e']
|
|
94
|
+
def migration_needed?; true; end
|
|
95
|
+
def migrate; end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class CircularMigrationE < Familia::Migration::Base
|
|
99
|
+
self.migration_id = 'circular_e'
|
|
100
|
+
self.dependencies = ['circular_c']
|
|
101
|
+
def migration_needed?; true; end
|
|
102
|
+
def migrate; end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class SelfRefMigration < Familia::Migration::Base
|
|
106
|
+
self.migration_id = 'self_ref'
|
|
107
|
+
self.dependencies = ['self_ref']
|
|
108
|
+
def migration_needed?; true; end
|
|
109
|
+
def migrate; end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
## Runner initializes with default values
|
|
113
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
114
|
+
runner.is_a?(Familia::Migration::Runner)
|
|
115
|
+
#=> true
|
|
116
|
+
|
|
117
|
+
## status returns array of migration info
|
|
118
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
119
|
+
status = runner.status
|
|
120
|
+
status.is_a?(Array) && status.size == 3
|
|
121
|
+
#=> true
|
|
122
|
+
|
|
123
|
+
## status shows all as pending initially
|
|
124
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
125
|
+
runner.status.all? { |s| s[:status] == :pending }
|
|
126
|
+
#=> true
|
|
127
|
+
|
|
128
|
+
## pending returns all migrations initially
|
|
129
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
130
|
+
runner.pending.size
|
|
131
|
+
#=> 3
|
|
132
|
+
|
|
133
|
+
## validate returns empty array when dependencies valid
|
|
134
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
135
|
+
runner.validate
|
|
136
|
+
#=> []
|
|
137
|
+
|
|
138
|
+
## validate detects missing dependencies
|
|
139
|
+
class MissingDepMigration < Familia::Migration::Base
|
|
140
|
+
self.migration_id = 'missing_dep'
|
|
141
|
+
self.dependencies = ['nonexistent']
|
|
142
|
+
def migration_needed?; true; end
|
|
143
|
+
def migrate; end
|
|
144
|
+
end
|
|
145
|
+
runner = Familia::Migration::Runner.new(
|
|
146
|
+
migrations: [MissingDepMigration],
|
|
147
|
+
registry: @registry
|
|
148
|
+
)
|
|
149
|
+
issues = runner.validate
|
|
150
|
+
issues.any? { |i| i[:type] == :missing_dependency }
|
|
151
|
+
#=> true
|
|
152
|
+
|
|
153
|
+
## run executes migrations respecting dependencies
|
|
154
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
155
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
156
|
+
results = runner.run(dry_run: false)
|
|
157
|
+
ids = results.map { |r| r[:migration_id] }
|
|
158
|
+
ids.index('runner_test_a') < ids.index('runner_test_b')
|
|
159
|
+
#=> true
|
|
160
|
+
|
|
161
|
+
## run records applied migrations
|
|
162
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
163
|
+
@registry.applied?('runner_test_a')
|
|
164
|
+
#=> true
|
|
165
|
+
|
|
166
|
+
## run_one with dry_run returns dry_run flag
|
|
167
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
168
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
169
|
+
result = runner.run_one(RunnerTestMigrationA, dry_run: true)
|
|
170
|
+
result[:dry_run]
|
|
171
|
+
#=> true
|
|
172
|
+
|
|
173
|
+
## dry run does not mark as applied
|
|
174
|
+
@registry.applied?('runner_test_a')
|
|
175
|
+
#=> false
|
|
176
|
+
|
|
177
|
+
## run with limit stops after N migrations
|
|
178
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
179
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
180
|
+
results = runner.run(dry_run: false, limit: 1)
|
|
181
|
+
results.size
|
|
182
|
+
#=> 1
|
|
183
|
+
|
|
184
|
+
## run_one executes single migration by class
|
|
185
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
186
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
187
|
+
result = runner.run_one(RunnerTestMigrationA, dry_run: false)
|
|
188
|
+
result[:status]
|
|
189
|
+
#=> :success
|
|
190
|
+
|
|
191
|
+
## run_one executes single migration by ID
|
|
192
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
193
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
194
|
+
result = runner.run_one('runner_test_reversible', dry_run: false)
|
|
195
|
+
result[:status]
|
|
196
|
+
#=> :success
|
|
197
|
+
|
|
198
|
+
## run_one raises DependencyNotMet if dependencies not applied
|
|
199
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
200
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
201
|
+
begin
|
|
202
|
+
runner.run_one(RunnerTestMigrationB, dry_run: false)
|
|
203
|
+
false
|
|
204
|
+
rescue Familia::Migration::Errors::DependencyNotMet
|
|
205
|
+
true
|
|
206
|
+
end
|
|
207
|
+
#=> true
|
|
208
|
+
|
|
209
|
+
## rollback calls down method
|
|
210
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
211
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
212
|
+
runner.run_one(RunnerTestReversible, dry_run: false)
|
|
213
|
+
result = runner.rollback('runner_test_reversible')
|
|
214
|
+
result[:status]
|
|
215
|
+
#=> :rolled_back
|
|
216
|
+
|
|
217
|
+
## rollback raises NotApplied if not applied
|
|
218
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
219
|
+
begin
|
|
220
|
+
runner.rollback('runner_test_reversible')
|
|
221
|
+
false
|
|
222
|
+
rescue Familia::Migration::Errors::NotApplied
|
|
223
|
+
true
|
|
224
|
+
end
|
|
225
|
+
#=> true
|
|
226
|
+
|
|
227
|
+
## rollback raises HasDependents if others depend on it
|
|
228
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
229
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
230
|
+
runner.run(dry_run: false)
|
|
231
|
+
begin
|
|
232
|
+
runner.rollback('runner_test_a')
|
|
233
|
+
false
|
|
234
|
+
rescue Familia::Migration::Errors::HasDependents
|
|
235
|
+
true
|
|
236
|
+
end
|
|
237
|
+
#=> true
|
|
238
|
+
|
|
239
|
+
## rollback raises NotReversible if no down method
|
|
240
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
241
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
242
|
+
runner.run_one(RunnerTestMigrationA, dry_run: false)
|
|
243
|
+
begin
|
|
244
|
+
runner.rollback('runner_test_a')
|
|
245
|
+
false
|
|
246
|
+
rescue Familia::Migration::Errors::NotReversible
|
|
247
|
+
true
|
|
248
|
+
end
|
|
249
|
+
#=> true
|
|
250
|
+
|
|
251
|
+
## NotFound raised for unknown migration ID
|
|
252
|
+
runner = Familia::Migration::Runner.new(migrations: @test_migrations, registry: @registry)
|
|
253
|
+
begin
|
|
254
|
+
runner.run_one('nonexistent_migration', dry_run: false)
|
|
255
|
+
false
|
|
256
|
+
rescue Familia::Migration::Errors::NotFound
|
|
257
|
+
true
|
|
258
|
+
end
|
|
259
|
+
#=> true
|
|
260
|
+
|
|
261
|
+
## validate detects simple circular dependency (A -> B -> A)
|
|
262
|
+
runner = Familia::Migration::Runner.new(
|
|
263
|
+
migrations: [CircularMigrationA, CircularMigrationB],
|
|
264
|
+
registry: @registry
|
|
265
|
+
)
|
|
266
|
+
issues = runner.validate
|
|
267
|
+
issues.any? { |i| i[:type] == :circular_dependency }
|
|
268
|
+
#=> true
|
|
269
|
+
|
|
270
|
+
## validate detects complex circular dependency (C -> D -> E -> C)
|
|
271
|
+
runner = Familia::Migration::Runner.new(
|
|
272
|
+
migrations: [CircularMigrationC, CircularMigrationD, CircularMigrationE],
|
|
273
|
+
registry: @registry
|
|
274
|
+
)
|
|
275
|
+
issues = runner.validate
|
|
276
|
+
issues.any? { |i| i[:type] == :circular_dependency }
|
|
277
|
+
#=> true
|
|
278
|
+
|
|
279
|
+
## run raises CircularDependency for circular dependencies
|
|
280
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
281
|
+
runner = Familia::Migration::Runner.new(
|
|
282
|
+
migrations: [CircularMigrationA, CircularMigrationB],
|
|
283
|
+
registry: @registry
|
|
284
|
+
)
|
|
285
|
+
begin
|
|
286
|
+
runner.run(dry_run: false)
|
|
287
|
+
false
|
|
288
|
+
rescue Familia::Migration::Errors::CircularDependency
|
|
289
|
+
true
|
|
290
|
+
end
|
|
291
|
+
#=> true
|
|
292
|
+
|
|
293
|
+
## Self-referencing dependency is detected
|
|
294
|
+
runner = Familia::Migration::Runner.new(
|
|
295
|
+
migrations: [SelfRefMigration],
|
|
296
|
+
registry: @registry
|
|
297
|
+
)
|
|
298
|
+
issues = runner.validate
|
|
299
|
+
issues.any? { |i| i[:type] == :circular_dependency }
|
|
300
|
+
#=> true
|
|
301
|
+
|
|
302
|
+
# Teardown
|
|
303
|
+
@redis.keys("#{@prefix}:*").each { |k| @redis.del(k) }
|
|
304
|
+
Familia::Migration.migrations.replace(@initial_migrations)
|
|
305
|
+
Familia::Migration.migrations.delete(MissingDepMigration)
|
|
306
|
+
Familia::Migration.migrations.delete(CircularMigrationA)
|
|
307
|
+
Familia::Migration.migrations.delete(CircularMigrationB)
|
|
308
|
+
Familia::Migration.migrations.delete(CircularMigrationC)
|
|
309
|
+
Familia::Migration.migrations.delete(CircularMigrationD)
|
|
310
|
+
Familia::Migration.migrations.delete(CircularMigrationE)
|
|
311
|
+
Familia::Migration.migrations.delete(SelfRefMigration)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# try/migration/schema_validation_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require_relative '../support/helpers/test_helpers'
|
|
6
|
+
require_relative '../../lib/familia/migration'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'tmpdir'
|
|
9
|
+
require 'fileutils'
|
|
10
|
+
|
|
11
|
+
Familia.debug = false
|
|
12
|
+
|
|
13
|
+
# Setup
|
|
14
|
+
@schema_dir = Dir.mktmpdir('familia_migration_schemas')
|
|
15
|
+
|
|
16
|
+
# Store and configure
|
|
17
|
+
@original_schema_path = Familia.schema_path
|
|
18
|
+
@original_schemas = Familia.schemas
|
|
19
|
+
@original_validator = Familia.schema_validator
|
|
20
|
+
@initial_migrations = Familia::Migration.migrations.dup
|
|
21
|
+
|
|
22
|
+
# Test model - define at top level to avoid Tryouts class scoping issue
|
|
23
|
+
# The class will have a simple name 'MigrationSchemaModel' instead of
|
|
24
|
+
# '#<Class:0x...>::MigrationSchemaModel' which happens when defined inside
|
|
25
|
+
# Tryouts' evaluation context
|
|
26
|
+
Object.const_set(:MigrationSchemaModel, Class.new(Familia::Horreum) do
|
|
27
|
+
feature :schema_validation
|
|
28
|
+
identifier_field :custid
|
|
29
|
+
field :custid
|
|
30
|
+
field :email
|
|
31
|
+
field :status
|
|
32
|
+
end) unless defined?(::MigrationSchemaModel)
|
|
33
|
+
|
|
34
|
+
# Custom migration for testing validation hooks
|
|
35
|
+
Object.const_set(:TestValidationMigration, Class.new(Familia::Migration::Model) do
|
|
36
|
+
self.migration_id = 'test_validation_hooks'
|
|
37
|
+
|
|
38
|
+
define_method(:validate_before_transform?) { true }
|
|
39
|
+
define_method(:validate_after_transform?) { true }
|
|
40
|
+
define_method(:migration_needed?) { false }
|
|
41
|
+
define_method(:prepare) { @model_class = MigrationSchemaModel }
|
|
42
|
+
define_method(:process_record) { |obj, key| }
|
|
43
|
+
end) unless defined?(::TestValidationMigration)
|
|
44
|
+
|
|
45
|
+
# Create test schema after class is defined so we can use the actual name
|
|
46
|
+
File.write(File.join(@schema_dir, 'migration_schema_model.json'), JSON.generate({
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
email: { type: 'string', format: 'email' },
|
|
50
|
+
status: { type: 'string', enum: ['active', 'inactive', 'pending'] }
|
|
51
|
+
},
|
|
52
|
+
required: ['email']
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
Familia.schema_path = @schema_dir
|
|
56
|
+
Familia.schema_validator = :json_schemer
|
|
57
|
+
Familia::SchemaRegistry.reset!
|
|
58
|
+
Familia::SchemaRegistry.load!
|
|
59
|
+
|
|
60
|
+
## Base migration has validate_schema method
|
|
61
|
+
migration = Familia::Migration::Base.new
|
|
62
|
+
migration.respond_to?(:validate_schema)
|
|
63
|
+
#=> true
|
|
64
|
+
|
|
65
|
+
## Base migration has validate_schema! method
|
|
66
|
+
migration = Familia::Migration::Base.new
|
|
67
|
+
migration.respond_to?(:validate_schema!)
|
|
68
|
+
#=> true
|
|
69
|
+
|
|
70
|
+
## validate_schema returns valid result for conforming data
|
|
71
|
+
migration = Familia::Migration::Base.new
|
|
72
|
+
obj = MigrationSchemaModel.new(custid: 'c1', email: 'test@example.com', status: 'active')
|
|
73
|
+
result = migration.validate_schema(obj)
|
|
74
|
+
result[:valid]
|
|
75
|
+
#=> true
|
|
76
|
+
|
|
77
|
+
## validate_schema returns errors for non-conforming data
|
|
78
|
+
migration = Familia::Migration::Base.new
|
|
79
|
+
obj = MigrationSchemaModel.new(custid: 'c2', email: 'invalid-email', status: 'unknown')
|
|
80
|
+
result = migration.validate_schema(obj)
|
|
81
|
+
result[:valid]
|
|
82
|
+
#=> false
|
|
83
|
+
|
|
84
|
+
## validate_schema errors array has content for invalid data
|
|
85
|
+
migration = Familia::Migration::Base.new
|
|
86
|
+
obj = MigrationSchemaModel.new(custid: 'c3', email: 'bad')
|
|
87
|
+
result = migration.validate_schema(obj)
|
|
88
|
+
result[:errors].size > 0
|
|
89
|
+
#=> true
|
|
90
|
+
|
|
91
|
+
## validate_schema! raises for invalid data
|
|
92
|
+
migration = Familia::Migration::Base.new
|
|
93
|
+
obj = MigrationSchemaModel.new(custid: 'c4', status: 'active')
|
|
94
|
+
begin
|
|
95
|
+
migration.validate_schema!(obj)
|
|
96
|
+
false
|
|
97
|
+
rescue Familia::SchemaValidationError
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
#=> true
|
|
101
|
+
|
|
102
|
+
## validate_schema! returns true for valid data
|
|
103
|
+
migration = Familia::Migration::Base.new
|
|
104
|
+
obj = MigrationSchemaModel.new(custid: 'c5', email: 'valid@example.com', status: 'pending')
|
|
105
|
+
migration.validate_schema!(obj)
|
|
106
|
+
#=> true
|
|
107
|
+
|
|
108
|
+
## schema_validation_enabled? returns true by default
|
|
109
|
+
migration = Familia::Migration::Base.new
|
|
110
|
+
migration.schema_validation_enabled?
|
|
111
|
+
#=> true
|
|
112
|
+
|
|
113
|
+
## skip_schema_validation! disables validation
|
|
114
|
+
migration = Familia::Migration::Base.new
|
|
115
|
+
migration.skip_schema_validation!
|
|
116
|
+
migration.schema_validation_enabled?
|
|
117
|
+
#=> false
|
|
118
|
+
|
|
119
|
+
## validate_schema returns valid when validation disabled
|
|
120
|
+
migration = Familia::Migration::Base.new
|
|
121
|
+
migration.skip_schema_validation!
|
|
122
|
+
obj = MigrationSchemaModel.new(custid: 'c6', email: 'invalid')
|
|
123
|
+
result = migration.validate_schema(obj)
|
|
124
|
+
result[:valid]
|
|
125
|
+
#=> true
|
|
126
|
+
|
|
127
|
+
## Model migration has validate_before_transform? protected method
|
|
128
|
+
migration = Familia::Migration::Model.new
|
|
129
|
+
migration.respond_to?(:validate_before_transform?, true)
|
|
130
|
+
#=> true
|
|
131
|
+
|
|
132
|
+
## Model migration has validate_after_transform? protected method
|
|
133
|
+
migration = Familia::Migration::Model.new
|
|
134
|
+
migration.respond_to?(:validate_after_transform?, true)
|
|
135
|
+
#=> true
|
|
136
|
+
|
|
137
|
+
## validate_before_transform? defaults to false via send
|
|
138
|
+
migration = Familia::Migration::Model.new
|
|
139
|
+
migration.send(:validate_before_transform?)
|
|
140
|
+
#=> false
|
|
141
|
+
|
|
142
|
+
## validate_after_transform? defaults to false via send
|
|
143
|
+
migration = Familia::Migration::Model.new
|
|
144
|
+
migration.send(:validate_after_transform?)
|
|
145
|
+
#=> false
|
|
146
|
+
|
|
147
|
+
## Custom migration has before validation hook enabled
|
|
148
|
+
migration = TestValidationMigration.new
|
|
149
|
+
migration.send(:validate_before_transform?)
|
|
150
|
+
#=> true
|
|
151
|
+
|
|
152
|
+
## Custom migration has after validation hook enabled
|
|
153
|
+
migration = TestValidationMigration.new
|
|
154
|
+
migration.send(:validate_after_transform?)
|
|
155
|
+
#=> true
|
|
156
|
+
|
|
157
|
+
## process_record_with_validation is a protected method
|
|
158
|
+
migration = TestValidationMigration.new
|
|
159
|
+
migration.respond_to?(:process_record_with_validation, true)
|
|
160
|
+
#=> true
|
|
161
|
+
|
|
162
|
+
## validate_schema with context returns valid for good data
|
|
163
|
+
migration = Familia::Migration::Base.new
|
|
164
|
+
obj = MigrationSchemaModel.new(custid: 'c7', email: 'valid@example.com', status: 'active')
|
|
165
|
+
result = migration.validate_schema(obj, context: 'before transform')
|
|
166
|
+
result[:valid]
|
|
167
|
+
#=> true
|
|
168
|
+
|
|
169
|
+
## validate_schema! with context raises for invalid data
|
|
170
|
+
migration = Familia::Migration::Base.new
|
|
171
|
+
obj = MigrationSchemaModel.new(custid: 'c8', status: 'active')
|
|
172
|
+
begin
|
|
173
|
+
migration.validate_schema!(obj, context: 'after transform')
|
|
174
|
+
false
|
|
175
|
+
rescue Familia::SchemaValidationError
|
|
176
|
+
true
|
|
177
|
+
end
|
|
178
|
+
#=> true
|
|
179
|
+
|
|
180
|
+
## Model migration inherits schema validation from Base
|
|
181
|
+
Familia::Migration::Model.ancestors.include?(Familia::Migration::Base)
|
|
182
|
+
#=> true
|
|
183
|
+
|
|
184
|
+
## Model migration responds to all schema validation methods
|
|
185
|
+
migration = Familia::Migration::Model.new
|
|
186
|
+
[:validate_schema, :validate_schema!, :schema_validation_enabled?, :skip_schema_validation!].all? do |m|
|
|
187
|
+
migration.respond_to?(m)
|
|
188
|
+
end
|
|
189
|
+
#=> true
|
|
190
|
+
|
|
191
|
+
# Teardown
|
|
192
|
+
FileUtils.rm_rf(@schema_dir)
|
|
193
|
+
Familia.schema_path = @original_schema_path
|
|
194
|
+
Familia.schemas = @original_schemas || {}
|
|
195
|
+
Familia.schema_validator = @original_validator || :json_schemer
|
|
196
|
+
Familia::SchemaRegistry.reset!
|
|
197
|
+
Familia::Migration.migrations.replace(@initial_migrations)
|
|
198
|
+
Familia::Migration.migrations.delete(TestValidationMigration) if defined?(::TestValidationMigration)
|
|
199
|
+
# Clean up the top-level constants
|
|
200
|
+
Object.send(:remove_const, :MigrationSchemaModel) if defined?(::MigrationSchemaModel)
|
|
201
|
+
Object.send(:remove_const, :TestValidationMigration) if defined?(::TestValidationMigration)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# try/migration/script_try.rb
|
|
2
|
+
#
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require_relative '../support/helpers/test_helpers'
|
|
6
|
+
require_relative '../../lib/familia/migration/errors'
|
|
7
|
+
require_relative '../../lib/familia/migration/script'
|
|
8
|
+
|
|
9
|
+
Familia.debug = false
|
|
10
|
+
|
|
11
|
+
@redis = Familia.dbclient
|
|
12
|
+
@test_prefix = "familia:test:script:#{Process.pid}"
|
|
13
|
+
|
|
14
|
+
## Script class exists
|
|
15
|
+
Familia::Migration::Script.is_a?(Class)
|
|
16
|
+
#=> true
|
|
17
|
+
|
|
18
|
+
## Built-in scripts are registered
|
|
19
|
+
Familia::Migration::Script.scripts.keys.sort
|
|
20
|
+
#=> [:backup_and_modify_field, :copy_field, :delete_field, :rename_field, :rename_key_preserve_ttl]
|
|
21
|
+
|
|
22
|
+
## registered? returns true for built-in scripts
|
|
23
|
+
Familia::Migration::Script.registered?(:rename_field)
|
|
24
|
+
#=> true
|
|
25
|
+
|
|
26
|
+
## registered? returns false for unknown scripts
|
|
27
|
+
Familia::Migration::Script.registered?(:nonexistent_script)
|
|
28
|
+
#=> false
|
|
29
|
+
|
|
30
|
+
## sha_for returns SHA1 hash for registered script
|
|
31
|
+
sha = Familia::Migration::Script.sha_for(:rename_field)
|
|
32
|
+
sha.is_a?(String) && sha.length == 40
|
|
33
|
+
#=> true
|
|
34
|
+
|
|
35
|
+
## sha_for returns nil for unregistered script
|
|
36
|
+
Familia::Migration::Script.sha_for(:no_such_script)
|
|
37
|
+
#=> nil
|
|
38
|
+
|
|
39
|
+
## Custom script can be registered
|
|
40
|
+
Familia::Migration::Script.register(:test_script, "return 'hello'")
|
|
41
|
+
Familia::Migration::Script.registered?(:test_script)
|
|
42
|
+
#=> true
|
|
43
|
+
|
|
44
|
+
## register raises ArgumentError for non-Symbol name
|
|
45
|
+
begin
|
|
46
|
+
Familia::Migration::Script.register("string_name", "return 1")
|
|
47
|
+
false
|
|
48
|
+
rescue ArgumentError => e
|
|
49
|
+
e.message.include?('Symbol')
|
|
50
|
+
end
|
|
51
|
+
#=> true
|
|
52
|
+
|
|
53
|
+
## register raises ArgumentError for empty source
|
|
54
|
+
begin
|
|
55
|
+
Familia::Migration::Script.register(:empty_script, " ")
|
|
56
|
+
false
|
|
57
|
+
rescue ArgumentError => e
|
|
58
|
+
e.message.include?('empty')
|
|
59
|
+
end
|
|
60
|
+
#=> true
|
|
61
|
+
|
|
62
|
+
## rename_field script works correctly
|
|
63
|
+
@redis.del("#{@test_prefix}:hash1")
|
|
64
|
+
@redis.hset("#{@test_prefix}:hash1", "old_name", "test_value")
|
|
65
|
+
result = Familia::Migration::Script.execute(
|
|
66
|
+
@redis,
|
|
67
|
+
:rename_field,
|
|
68
|
+
keys: ["#{@test_prefix}:hash1"],
|
|
69
|
+
argv: ["old_name", "new_name"]
|
|
70
|
+
)
|
|
71
|
+
[@redis.hexists("#{@test_prefix}:hash1", "old_name"),
|
|
72
|
+
@redis.hget("#{@test_prefix}:hash1", "new_name"),
|
|
73
|
+
result]
|
|
74
|
+
#=> [false, "test_value", 1]
|
|
75
|
+
|
|
76
|
+
## rename_field returns 0 when source field does not exist
|
|
77
|
+
@redis.del("#{@test_prefix}:hash1a")
|
|
78
|
+
@redis.hset("#{@test_prefix}:hash1a", "other_field", "value")
|
|
79
|
+
result = Familia::Migration::Script.execute(
|
|
80
|
+
@redis,
|
|
81
|
+
:rename_field,
|
|
82
|
+
keys: ["#{@test_prefix}:hash1a"],
|
|
83
|
+
argv: ["missing_field", "new_field"]
|
|
84
|
+
)
|
|
85
|
+
result
|
|
86
|
+
#=> 0
|
|
87
|
+
|
|
88
|
+
## copy_field script works correctly
|
|
89
|
+
@redis.del("#{@test_prefix}:hash2")
|
|
90
|
+
@redis.hset("#{@test_prefix}:hash2", "source", "copied_value")
|
|
91
|
+
Familia::Migration::Script.execute(
|
|
92
|
+
@redis,
|
|
93
|
+
:copy_field,
|
|
94
|
+
keys: ["#{@test_prefix}:hash2"],
|
|
95
|
+
argv: ["source", "destination"]
|
|
96
|
+
)
|
|
97
|
+
[@redis.hget("#{@test_prefix}:hash2", "source"),
|
|
98
|
+
@redis.hget("#{@test_prefix}:hash2", "destination")]
|
|
99
|
+
#=> ["copied_value", "copied_value"]
|
|
100
|
+
|
|
101
|
+
## copy_field returns 0 when source field does not exist
|
|
102
|
+
@redis.del("#{@test_prefix}:hash2a")
|
|
103
|
+
result = Familia::Migration::Script.execute(
|
|
104
|
+
@redis,
|
|
105
|
+
:copy_field,
|
|
106
|
+
keys: ["#{@test_prefix}:hash2a"],
|
|
107
|
+
argv: ["missing", "destination"]
|
|
108
|
+
)
|
|
109
|
+
result
|
|
110
|
+
#=> 0
|
|
111
|
+
|
|
112
|
+
## delete_field script works correctly
|
|
113
|
+
@redis.del("#{@test_prefix}:hash3")
|
|
114
|
+
@redis.hset("#{@test_prefix}:hash3", "to_delete", "value")
|
|
115
|
+
result = Familia::Migration::Script.execute(
|
|
116
|
+
@redis,
|
|
117
|
+
:delete_field,
|
|
118
|
+
keys: ["#{@test_prefix}:hash3"],
|
|
119
|
+
argv: ["to_delete"]
|
|
120
|
+
)
|
|
121
|
+
[result, @redis.hexists("#{@test_prefix}:hash3", "to_delete")]
|
|
122
|
+
#=> [1, false]
|
|
123
|
+
|
|
124
|
+
## delete_field returns 0 when field does not exist
|
|
125
|
+
@redis.del("#{@test_prefix}:hash3a")
|
|
126
|
+
result = Familia::Migration::Script.execute(
|
|
127
|
+
@redis,
|
|
128
|
+
:delete_field,
|
|
129
|
+
keys: ["#{@test_prefix}:hash3a"],
|
|
130
|
+
argv: ["nonexistent"]
|
|
131
|
+
)
|
|
132
|
+
result
|
|
133
|
+
#=> 0
|
|
134
|
+
|
|
135
|
+
## rename_key_preserve_ttl works and preserves TTL
|
|
136
|
+
@redis.del("#{@test_prefix}:src_key", "#{@test_prefix}:dst_key")
|
|
137
|
+
@redis.set("#{@test_prefix}:src_key", "value")
|
|
138
|
+
@redis.expire("#{@test_prefix}:src_key", 3600)
|
|
139
|
+
Familia::Migration::Script.execute(
|
|
140
|
+
@redis,
|
|
141
|
+
:rename_key_preserve_ttl,
|
|
142
|
+
keys: ["#{@test_prefix}:src_key", "#{@test_prefix}:dst_key"],
|
|
143
|
+
argv: []
|
|
144
|
+
)
|
|
145
|
+
ttl = @redis.ttl("#{@test_prefix}:dst_key")
|
|
146
|
+
[@redis.exists("#{@test_prefix}:src_key"),
|
|
147
|
+
@redis.get("#{@test_prefix}:dst_key"),
|
|
148
|
+
ttl > 3500]
|
|
149
|
+
#=> [0, "value", true]
|
|
150
|
+
|
|
151
|
+
## backup_and_modify_field backs up and modifies correctly
|
|
152
|
+
@redis.del("#{@test_prefix}:hash4", "#{@test_prefix}:backup")
|
|
153
|
+
@redis.hset("#{@test_prefix}:hash4", "myfield", "original_value")
|
|
154
|
+
old_val = Familia::Migration::Script.execute(
|
|
155
|
+
@redis,
|
|
156
|
+
:backup_and_modify_field,
|
|
157
|
+
keys: ["#{@test_prefix}:hash4", "#{@test_prefix}:backup"],
|
|
158
|
+
argv: ["myfield", "new_value", "3600"]
|
|
159
|
+
)
|
|
160
|
+
[@redis.hget("#{@test_prefix}:hash4", "myfield"),
|
|
161
|
+
@redis.hget("#{@test_prefix}:backup", "#{@test_prefix}:hash4:myfield"),
|
|
162
|
+
old_val]
|
|
163
|
+
#=> ["new_value", "original_value", "original_value"]
|
|
164
|
+
|
|
165
|
+
## ScriptNotFound raised for unregistered script
|
|
166
|
+
begin
|
|
167
|
+
Familia::Migration::Script.execute(@redis, :no_such_script, keys: [], argv: [])
|
|
168
|
+
false
|
|
169
|
+
rescue Familia::Migration::Script::ScriptNotFound
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
#=> true
|
|
173
|
+
|
|
174
|
+
## ScriptEntry is immutable (frozen)
|
|
175
|
+
entry = Familia::Migration::Script.scripts[:rename_field]
|
|
176
|
+
[entry.source.frozen?, entry.sha.frozen?]
|
|
177
|
+
#=> [true, true]
|
|
178
|
+
|
|
179
|
+
## preload_all returns map of script names to SHAs
|
|
180
|
+
result = Familia::Migration::Script.preload_all(@redis)
|
|
181
|
+
result.is_a?(Hash) && result.key?(:rename_field) && result[:rename_field].length == 40
|
|
182
|
+
#=> true
|
|
183
|
+
|
|
184
|
+
## reset! restores only built-in scripts
|
|
185
|
+
Familia::Migration::Script.register(:temp_script, "return 1")
|
|
186
|
+
Familia::Migration::Script.reset!
|
|
187
|
+
[Familia::Migration::Script.registered?(:temp_script),
|
|
188
|
+
Familia::Migration::Script.registered?(:rename_field)]
|
|
189
|
+
#=> [false, true]
|
|
190
|
+
|
|
191
|
+
# Teardown
|
|
192
|
+
@redis.keys("#{@test_prefix}:*").each { |k| @redis.del(k) }
|