pakyow-data 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE +4 -0
  4. data/README.md +29 -0
  5. data/lib/pakyow/data/adapters/abstract.rb +58 -0
  6. data/lib/pakyow/data/adapters/sql/commands.rb +58 -0
  7. data/lib/pakyow/data/adapters/sql/dataset_methods.rb +29 -0
  8. data/lib/pakyow/data/adapters/sql/differ.rb +76 -0
  9. data/lib/pakyow/data/adapters/sql/migrator/adapter_methods.rb +95 -0
  10. data/lib/pakyow/data/adapters/sql/migrator.rb +181 -0
  11. data/lib/pakyow/data/adapters/sql/migrators/automator.rb +49 -0
  12. data/lib/pakyow/data/adapters/sql/migrators/finalizer.rb +96 -0
  13. data/lib/pakyow/data/adapters/sql/runner.rb +49 -0
  14. data/lib/pakyow/data/adapters/sql/source_extension.rb +31 -0
  15. data/lib/pakyow/data/adapters/sql/types.rb +50 -0
  16. data/lib/pakyow/data/adapters/sql.rb +247 -0
  17. data/lib/pakyow/data/behavior/config.rb +28 -0
  18. data/lib/pakyow/data/behavior/lookup.rb +75 -0
  19. data/lib/pakyow/data/behavior/serialization.rb +40 -0
  20. data/lib/pakyow/data/connection.rb +103 -0
  21. data/lib/pakyow/data/container.rb +273 -0
  22. data/lib/pakyow/data/errors.rb +169 -0
  23. data/lib/pakyow/data/framework.rb +42 -0
  24. data/lib/pakyow/data/helpers.rb +11 -0
  25. data/lib/pakyow/data/lookup.rb +85 -0
  26. data/lib/pakyow/data/migrator.rb +182 -0
  27. data/lib/pakyow/data/object.rb +98 -0
  28. data/lib/pakyow/data/proxy.rb +262 -0
  29. data/lib/pakyow/data/result.rb +53 -0
  30. data/lib/pakyow/data/sources/abstract.rb +82 -0
  31. data/lib/pakyow/data/sources/ephemeral.rb +72 -0
  32. data/lib/pakyow/data/sources/relational/association.rb +43 -0
  33. data/lib/pakyow/data/sources/relational/associations/belongs_to.rb +47 -0
  34. data/lib/pakyow/data/sources/relational/associations/has_many.rb +54 -0
  35. data/lib/pakyow/data/sources/relational/associations/has_one.rb +54 -0
  36. data/lib/pakyow/data/sources/relational/associations/through.rb +67 -0
  37. data/lib/pakyow/data/sources/relational/command.rb +531 -0
  38. data/lib/pakyow/data/sources/relational/migrator.rb +101 -0
  39. data/lib/pakyow/data/sources/relational.rb +587 -0
  40. data/lib/pakyow/data/subscribers/adapters/memory.rb +153 -0
  41. data/lib/pakyow/data/subscribers/adapters/redis/pipeliner.rb +45 -0
  42. data/lib/pakyow/data/subscribers/adapters/redis/scripts/_shared.lua +73 -0
  43. data/lib/pakyow/data/subscribers/adapters/redis/scripts/expire.lua +16 -0
  44. data/lib/pakyow/data/subscribers/adapters/redis/scripts/persist.lua +15 -0
  45. data/lib/pakyow/data/subscribers/adapters/redis/scripts/register.lua +37 -0
  46. data/lib/pakyow/data/subscribers/adapters/redis.rb +209 -0
  47. data/lib/pakyow/data/subscribers.rb +148 -0
  48. data/lib/pakyow/data/tasks/bootstrap.rake +18 -0
  49. data/lib/pakyow/data/tasks/create.rake +22 -0
  50. data/lib/pakyow/data/tasks/drop.rake +32 -0
  51. data/lib/pakyow/data/tasks/finalize.rake +56 -0
  52. data/lib/pakyow/data/tasks/migrate.rake +24 -0
  53. data/lib/pakyow/data/tasks/reset.rake +18 -0
  54. data/lib/pakyow/data/types.rb +37 -0
  55. data/lib/pakyow/data.rb +27 -0
  56. data/lib/pakyow/environment/data/auto_migrate.rb +31 -0
  57. data/lib/pakyow/environment/data/config.rb +54 -0
  58. data/lib/pakyow/environment/data/connections.rb +76 -0
  59. data/lib/pakyow/environment/data/memory_db.rb +23 -0
  60. data/lib/pakyow/validations/unique.rb +26 -0
  61. metadata +186 -0
@@ -0,0 +1,531 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/support/deep_dup"
4
+ require "pakyow/support/inflector"
5
+ require "pakyow/support/core_refinements/array/ensurable"
6
+
7
+ module Pakyow
8
+ module Data
9
+ module Sources
10
+ class Relational
11
+ class Command
12
+ using Support::DeepDup
13
+ using Support::Refinements::Array::Ensurable
14
+
15
+ def initialize(name, block:, source:, provides_dataset:, performs_create:, performs_update:, performs_delete:)
16
+ @name, @block, @source, @provides_dataset, @performs_create, @performs_update, @performs_delete = name, block, source, provides_dataset, performs_create, performs_update, performs_delete
17
+ end
18
+
19
+ def call(values = {})
20
+ future_associated_changes = []
21
+
22
+ if values
23
+ # Enforce required attributes.
24
+ #
25
+ @source.class.attributes.each do |attribute_name, attribute|
26
+ if attribute.meta[:required]
27
+ if @performs_create && !values.include?(attribute_name)
28
+ raise NotNullViolation.new_with_message(attribute: attribute_name)
29
+ end
30
+
31
+ if values.include?(attribute_name) && values[attribute_name].nil?
32
+ raise NotNullViolation.new_with_message(attribute: attribute_name)
33
+ end
34
+ end
35
+ end
36
+
37
+ # Fail if unexpected values were passed.
38
+ #
39
+ values.keys.each do |key|
40
+ key = key.to_sym
41
+ unless @source.class.attributes.include?(key) || @source.class.association_with_name?(key)
42
+ raise UnknownAttribute.new_with_message(attribute: key, source: @source.class.__object_name.name)
43
+ end
44
+ end
45
+
46
+ # Coerce values into the appropriate type.
47
+ #
48
+ final_values = values.each_with_object({}) { |(key, value), values_hash|
49
+ key = key.to_sym
50
+
51
+ begin
52
+ if attribute = @source.class.attributes[key]
53
+ if value.is_a?(Proxy) || value.is_a?(Result) || value.is_a?(Object)
54
+ raise TypeMismatch.new_with_message(type: value.class, mapping: attribute.meta[:mapping])
55
+ end
56
+
57
+ values_hash[key] = value.nil? ? value : attribute[value]
58
+ else
59
+ values_hash[key] = value
60
+ end
61
+ rescue TypeError, Dry::Types::CoercionError => error
62
+ raise TypeMismatch.build(error, type: value.class, mapping: attribute.meta[:mapping])
63
+ end
64
+ }
65
+
66
+ # Update timestamp fields.
67
+ #
68
+ if timestamp_fields = @source.class.timestamp_fields
69
+ if @performs_create
70
+ timestamp_fields.values.each do |timestamp_field|
71
+ final_values[timestamp_field] = Time.now
72
+ end
73
+ # Don't update timestamps if we aren't also updating other values.
74
+ #
75
+ elsif values.any? && timestamp_field = timestamp_fields[@name]
76
+ final_values[timestamp_field] = Time.now
77
+ end
78
+ end
79
+
80
+ if @performs_create
81
+ # Set default values.
82
+ #
83
+ @source.class.attributes.each do |attribute_name, attribute|
84
+ if !final_values.include?(attribute_name) && attribute.meta.include?(:default)
85
+ default = attribute.meta[:default]
86
+ final_values[attribute_name] = if default.is_a?(Proc)
87
+ default.call
88
+ else
89
+ default
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Enforce constraints on association values passed by access name.
96
+ #
97
+ @source.class.associations.values.flatten.select { |association|
98
+ final_values.key?(association.name)
99
+ }.each do |association|
100
+ association_value = raw_result(final_values[association.name])
101
+
102
+ case association_value
103
+ when Proxy
104
+ if association_value.source.class == association.associated_source
105
+ if association.result_type == :one && (association_value.count > 1 || (@performs_update && @source.count > 1))
106
+ raise ConstraintViolation.new_with_message(
107
+ :associate_multiple,
108
+ association: association.name
109
+ )
110
+ end
111
+ else
112
+ raise TypeMismatch.new_with_message(
113
+ :associate_wrong_source,
114
+ source: association_value.source.class.__object_name.name,
115
+ association: association.name
116
+ )
117
+ end
118
+ when Object
119
+ if association.result_type == :one
120
+ if association_value.originating_source
121
+ if association_value.originating_source == association.associated_source
122
+ if association.associated_source.instance.send(:"by_#{association.associated_source.primary_key_field}", association_value[association.associated_source.primary_key_field]).count == 0
123
+ raise ConstraintViolation.new_with_message(
124
+ :associate_missing,
125
+ source: association.name,
126
+ field: association.associated_source.primary_key_field,
127
+ value: association_value[association.associated_source.primary_key_field]
128
+ )
129
+ end
130
+ else
131
+ raise TypeMismatch.new_with_message(
132
+ :associate_wrong_object,
133
+ source: association_value.originating_source.__object_name.name,
134
+ association: association.name
135
+ )
136
+ end
137
+ else
138
+ raise TypeMismatch.new_with_message(
139
+ :associate_unknown_object,
140
+ association: association.name
141
+ )
142
+ end
143
+ else
144
+ raise TypeMismatch.new_with_message(
145
+ :associate_wrong_type,
146
+ type: association_value.class,
147
+ association: association.name
148
+ )
149
+ end
150
+ when Array
151
+ if association.result_type == :many
152
+ if association_value.any? { |value| !value.is_a?(Object) }
153
+ raise TypeMismatch.new_with_message(
154
+ :associate_many_not_object,
155
+ association: association.name
156
+ )
157
+ else
158
+ if association_value.any? { |value| value.originating_source.nil? }
159
+ raise TypeMismatch.new_with_message(
160
+ :associate_unknown_object,
161
+ association: association.name
162
+ )
163
+ else
164
+ if association_value.find { |value| value.originating_source != association.associated_source }
165
+ raise TypeMismatch.new_with_message(
166
+ :associate_many_wrong_source,
167
+ association: association.name,
168
+ source: association.associated_source_name
169
+ )
170
+ else
171
+ associated_column_value = association_value.map { |object| object[association.associated_source.primary_key_field] }
172
+ associated_object_query = association.associated_source.instance.send(
173
+ :"by_#{association.associated_source.primary_key_field}", associated_column_value
174
+ )
175
+
176
+ if associated_object_query.count != association_value.count
177
+ raise ConstraintViolation.new_with_message(
178
+ :associate_many_missing,
179
+ association: association.name
180
+ )
181
+ end
182
+ end
183
+ end
184
+ end
185
+ else
186
+ raise ConstraintViolation.new_with_message(
187
+ :associate_multiple,
188
+ association: association.name
189
+ )
190
+ end
191
+ when NilClass
192
+ else
193
+ raise TypeMismatch.new_with_message(
194
+ :associate_wrong_type,
195
+ type: association_value.class,
196
+ association: association.name
197
+ )
198
+ end
199
+ end
200
+
201
+ # Enforce constraints for association values passed by foreign key.
202
+ #
203
+ @source.class.associations.values.flatten.select { |association|
204
+ association.type == :belongs && final_values.key?(association.foreign_key_field) && !final_values[association.foreign_key_field].nil?
205
+ }.each do |association|
206
+ associated_column_value = final_values[association.foreign_key_field]
207
+ associated_object_query = association.associated_source.instance.send(
208
+ :"by_#{association.associated_query_field}", associated_column_value
209
+ )
210
+
211
+ if associated_object_query.count == 0
212
+ raise ConstraintViolation.new_with_message(
213
+ :associate_missing,
214
+ source: association.name,
215
+ field: association.associated_query_field,
216
+ value: associated_column_value
217
+ )
218
+ end
219
+ end
220
+
221
+ # Set values for associations passed by access name.
222
+ #
223
+ @source.class.associations.values.flatten.select { |association|
224
+ final_values.key?(association.name)
225
+ }.each do |association|
226
+ case association.specific_type
227
+ when :belongs_to
228
+ association_value = raw_result(final_values.delete(association.name))
229
+ final_values[association.query_field] = case association_value
230
+ when Proxy
231
+ if association_value.one.nil?
232
+ nil
233
+ else
234
+ association_value.one[association.associated_source.primary_key_field]
235
+ end
236
+ when Object
237
+ association_value[association.associated_source.primary_key_field]
238
+ when NilClass
239
+ nil
240
+ end
241
+ when :has_one, :has_many
242
+ future_associated_changes << [association, final_values.delete(association.name)]
243
+ end
244
+ end
245
+ end
246
+
247
+ original_dataset = if @performs_update
248
+ # Hold on to the original values so we can update them locally.
249
+ @source.dup.to_a
250
+ else
251
+ nil
252
+ end
253
+
254
+ unless @provides_dataset || @performs_update
255
+ # Cache the result prior to running the command.
256
+ @source.to_a
257
+ end
258
+
259
+ @source.transaction do
260
+ if @performs_delete
261
+ @source.class.associations.values.flatten.select(&:dependents?).each do |association|
262
+ dependent_values = @source.class.container.connection.adapter.restrict_to_attribute(
263
+ @source.class.primary_key_field, @source
264
+ )
265
+
266
+ # If objects are located in two different connections, fetch the raw values.
267
+ #
268
+ unless @source.class.container.connection == association.associated_source.container.connection
269
+ dependent_values = dependent_values.map { |dependent_value|
270
+ dependent_value[@source.class.primary_key_field]
271
+ }
272
+ end
273
+
274
+ if association.type == :through
275
+ joining_data = association.joining_source.instance.send(
276
+ :"by_#{association.right_foreign_key_field}",
277
+ dependent_values
278
+ )
279
+
280
+ dependent_data = association.associated_source.instance.send(
281
+ :"by_#{association.associated_source.primary_key_field}",
282
+ association.associated_source.container.connection.adapter.restrict_to_attribute(
283
+ association.left_foreign_key_field, joining_data
284
+ ).map { |result|
285
+ result[association.left_foreign_key_field]
286
+ }
287
+ )
288
+
289
+ case association.dependent
290
+ when :delete
291
+ joining_data.delete
292
+ when :nullify
293
+ joining_data.update(association.right_foreign_key_field => nil)
294
+ end
295
+ else
296
+ dependent_data = association.associated_source.instance.send(
297
+ :"by_#{association.associated_query_field}",
298
+ dependent_values
299
+ )
300
+ end
301
+
302
+ case association.dependent
303
+ when :delete
304
+ dependent_data.delete
305
+ when :nullify
306
+ unless association.type == :through
307
+ dependent_data.update(association.associated_query_field => nil)
308
+ end
309
+ when :raise
310
+ dependent_count = dependent_data.count
311
+ if dependent_count > 0
312
+ dependent_name = if dependent_count > 1
313
+ Support.inflector.pluralize(association.associated_source_name)
314
+ else
315
+ Support.inflector.singularize(association.associated_source_name)
316
+ end
317
+
318
+ raise ConstraintViolation.new_with_message(
319
+ :dependent_delete,
320
+ source: @source.class.__object_name.name,
321
+ count: dependent_count,
322
+ dependent: dependent_name
323
+ )
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ if @performs_create || @performs_update
330
+ # Ensure that has_one associations only have one associated object.
331
+ #
332
+ @source.class.associations[:belongs_to].flat_map { |belongs_to_association|
333
+ belongs_to_association.associated_source.associations[:has_one].select { |has_one_association|
334
+ has_one_association.associated_query_field == belongs_to_association.query_field
335
+ }
336
+ }.each do |association|
337
+ value = final_values.dig(
338
+ association.associated_name, association.query_field
339
+ ) || final_values.dig(association.associated_query_field)
340
+
341
+ if value
342
+ @source.class.instance.tap do |impacted_source|
343
+ impacted_source.__setobj__(
344
+ @source.class.container.connection.adapter.result_for_attribute_value(
345
+ association.associated_query_field, value, impacted_source
346
+ )
347
+ )
348
+
349
+ impacted_source.update(association.associated_query_field => nil)
350
+ end
351
+ end
352
+ end
353
+ end
354
+
355
+ command_result = @source.instance_exec(final_values, &@block)
356
+
357
+ final_result = if @performs_update
358
+ # For updates, we fetch the values prior to performing the update and
359
+ # return a source containing locally updated values. This lets us see
360
+ # the original values but prevents us from fetching twice.
361
+
362
+ @source.class.container.source(@source.class.__object_name.name).tap do |updated_source|
363
+ updated_source.__setobj__(
364
+ @source.class.container.connection.adapter.result_for_attribute_value(
365
+ @source.class.primary_key_field, command_result, updated_source
366
+ )
367
+ )
368
+
369
+ updated_source.instance_variable_set(:@results, original_dataset.map { |original_object|
370
+ new_object = original_object.class.new(original_object.values.merge(final_values))
371
+ new_object.originating_source = original_object.originating_source
372
+ new_object
373
+ })
374
+
375
+ updated_source.instance_variable_set(:@original_results, original_dataset)
376
+ end
377
+ elsif @provides_dataset
378
+ @source.dup.tap { |source|
379
+ source.__setobj__(command_result)
380
+ }
381
+ else
382
+ @source
383
+ end
384
+
385
+ if @performs_create || @performs_update
386
+ # Update records associated with the data we just changed.
387
+ #
388
+ future_associated_changes.each do |association, association_value|
389
+ association_value = raw_result(association_value)
390
+ associated_dataset = case association_value
391
+ when Proxy
392
+ association_value
393
+ when Object, Array
394
+ updatable = Array.ensure(association_value).map { |value|
395
+ case value
396
+ when Object
397
+ value[association.associated_source.primary_key_field]
398
+ else
399
+ value
400
+ end
401
+ }
402
+
403
+ association.associated_source.instance.send(
404
+ :"by_#{association.associated_source.primary_key_field}", updatable
405
+ )
406
+ when NilClass
407
+ nil
408
+ end
409
+
410
+ if association.type == :through
411
+ associated_column_value = final_result.class.container.connection.adapter.restrict_to_attribute(
412
+ association.query_field, final_result
413
+ )
414
+
415
+ # If objects are located in two different connections, fetch the raw values.
416
+ #
417
+ if association.joining_source.container.connection == final_result.class.container.connection
418
+ disassociate_column_value = associated_column_value
419
+ else
420
+ disassociate_column_value = associated_column_value.map { |value|
421
+ value[association.query_field]
422
+ }
423
+ end
424
+
425
+ # Disassociate old data.
426
+ #
427
+ association.joining_source.instance.send(
428
+ :"by_#{association.right_foreign_key_field}",
429
+ disassociate_column_value
430
+ ).delete
431
+
432
+ if associated_dataset
433
+ associated_dataset_source = case raw_result(associated_dataset)
434
+ when Proxy
435
+ associated_dataset.source
436
+ else
437
+ associated_dataset
438
+ end
439
+
440
+ # Ensure that has_one through associations only have one associated object.
441
+ #
442
+ if association.result_type == :one
443
+ joined_column_value = association.associated_source.container.connection.adapter.restrict_to_attribute(
444
+ association.associated_source.primary_key_field, associated_dataset_source
445
+ )
446
+
447
+ # If objects are located in two different connections, fetch the raw values.
448
+ #
449
+ unless association.joining_source.container.connection == association.associated_source.container.connection
450
+ joined_column_value = joined_column_value.map { |value|
451
+ value[association.associated_source.primary_key_field]
452
+ }
453
+ end
454
+
455
+ association.joining_source.instance.send(
456
+ :"by_#{association.left_foreign_key_field}",
457
+ joined_column_value
458
+ ).delete
459
+ end
460
+
461
+ # Associate the correct data.
462
+ #
463
+ associated_column_value.each do |result|
464
+ association.associated_source.container.connection.adapter.restrict_to_attribute(
465
+ association.associated_source.primary_key_field, associated_dataset_source
466
+ ).each do |associated_result|
467
+ association.joining_source.instance.command(:create).call(
468
+ association.left_foreign_key_field => associated_result[association.associated_source.primary_key_field],
469
+ association.right_foreign_key_field => result[association.source.primary_key_field]
470
+ )
471
+ end
472
+ end
473
+ end
474
+ else
475
+ if final_result.one
476
+ associated_column_value = final_result.one[association.query_field]
477
+
478
+ # Disassociate old data.
479
+ #
480
+ association.associated_source.instance.send(
481
+ :"by_#{association.associated_query_field}", associated_column_value
482
+ ).update(association.associated_query_field => nil)
483
+
484
+ # Associate the correct data.
485
+ #
486
+ if associated_dataset
487
+ associated_dataset.update(
488
+ association.associated_query_field => associated_column_value
489
+ )
490
+
491
+ # Update the column value in passed objects.
492
+ #
493
+ case association_value
494
+ when Proxy
495
+ association_value.source.reload
496
+ when Object, Array
497
+ Array.ensure(association_value).each do |object|
498
+ values = object.values.dup
499
+ values[association.associated_query_field] = associated_column_value
500
+ object.instance_variable_set(:@values, values.freeze)
501
+ end
502
+ end
503
+ end
504
+ end
505
+ end
506
+ end
507
+ end
508
+
509
+ yield final_result if block_given?
510
+ final_result
511
+ end
512
+ end
513
+
514
+ private
515
+
516
+ def raw_result(value)
517
+ if value.is_a?(Result)
518
+ value.__getobj__
519
+ elsif value.is_a?(Array)
520
+ value.map { |each_value|
521
+ raw_result(each_value)
522
+ }
523
+ else
524
+ value
525
+ end
526
+ end
527
+ end
528
+ end
529
+ end
530
+ end
531
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Sources
6
+ class Relational
7
+ class Migrator
8
+ def initialize(connection, sources: [])
9
+ @connection, @sources = connection, sources
10
+ end
11
+
12
+ def auto_migrate!
13
+ if @sources.any?
14
+ migrate!(automator)
15
+ end
16
+ end
17
+
18
+ def finalize!
19
+ if @sources.any?
20
+ migrator = finalizer
21
+ migrate!(migrator)
22
+
23
+ # Return the migrations that need to be created.
24
+ #
25
+ prefix = Time.now.strftime("%Y%m%d%H%M%S").to_i
26
+ migrator.migrations.each_with_object({}) { |(action, content), migrations|
27
+ migrations["#{prefix}_#{action}.rb"] = content
28
+
29
+ # Ensure that migration files appear in the correct order.
30
+ #
31
+ prefix += 1
32
+ }
33
+ else
34
+ {}
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def automator
41
+ self
42
+ end
43
+
44
+ def finalizer
45
+ self
46
+ end
47
+
48
+ def migrate!(migrator)
49
+ grouped_sources = @sources.group_by { |source|
50
+ source.dataset_table
51
+ }
52
+
53
+ # Create any new sources, without foreign keys since they could reference a source that does not yet exist.
54
+ #
55
+ grouped_sources.each do |_table, sources|
56
+ if migrator.create_source?(sources[0])
57
+ combined_attributes = sources.each_with_object({}) { |source, hash|
58
+ hash.merge!(source.attributes)
59
+ }.reject { |_name, attribute|
60
+ attribute.meta[:foreign_key]
61
+ }
62
+
63
+ migrator.create_source!(sources[0], combined_attributes)
64
+ end
65
+ end
66
+
67
+ # Create any new associations between sources, now that we're sure everything exists.
68
+ #
69
+ grouped_sources.each do |_table, sources|
70
+ combined_foreign_keys = sources.each_with_object({}) { |source, hash|
71
+ hash.merge!(source.attributes)
72
+ }.select { |_name, attribute|
73
+ attribute.meta[:foreign_key]
74
+ }
75
+
76
+ if migrator.change_source?(sources[0], combined_foreign_keys)
77
+ migrator.reassociate_source!(sources[0], combined_foreign_keys)
78
+ end
79
+ end
80
+
81
+ # Change any existing sources, including adding / removing attributes.
82
+ #
83
+ grouped_sources.each do |_table, sources|
84
+ unless migrator.create_source?(sources[0])
85
+ combined_attributes = sources.each_with_object({}) { |source, hash|
86
+ hash.merge!(source.attributes)
87
+ }.reject { |_name, attribute|
88
+ attribute.meta[:foreign_key]
89
+ }
90
+
91
+ if migrator.change_source?(sources[0], combined_attributes)
92
+ migrator.change_source!(sources[0], combined_attributes)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end