pakyow-data 1.0.0.rc1

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 (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