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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +4 -0
- data/README.md +29 -0
- data/lib/pakyow/data/adapters/abstract.rb +58 -0
- data/lib/pakyow/data/adapters/sql/commands.rb +58 -0
- data/lib/pakyow/data/adapters/sql/dataset_methods.rb +29 -0
- data/lib/pakyow/data/adapters/sql/differ.rb +76 -0
- data/lib/pakyow/data/adapters/sql/migrator/adapter_methods.rb +95 -0
- data/lib/pakyow/data/adapters/sql/migrator.rb +181 -0
- data/lib/pakyow/data/adapters/sql/migrators/automator.rb +49 -0
- data/lib/pakyow/data/adapters/sql/migrators/finalizer.rb +96 -0
- data/lib/pakyow/data/adapters/sql/runner.rb +49 -0
- data/lib/pakyow/data/adapters/sql/source_extension.rb +31 -0
- data/lib/pakyow/data/adapters/sql/types.rb +50 -0
- data/lib/pakyow/data/adapters/sql.rb +247 -0
- data/lib/pakyow/data/behavior/config.rb +28 -0
- data/lib/pakyow/data/behavior/lookup.rb +75 -0
- data/lib/pakyow/data/behavior/serialization.rb +40 -0
- data/lib/pakyow/data/connection.rb +103 -0
- data/lib/pakyow/data/container.rb +273 -0
- data/lib/pakyow/data/errors.rb +169 -0
- data/lib/pakyow/data/framework.rb +42 -0
- data/lib/pakyow/data/helpers.rb +11 -0
- data/lib/pakyow/data/lookup.rb +85 -0
- data/lib/pakyow/data/migrator.rb +182 -0
- data/lib/pakyow/data/object.rb +98 -0
- data/lib/pakyow/data/proxy.rb +262 -0
- data/lib/pakyow/data/result.rb +53 -0
- data/lib/pakyow/data/sources/abstract.rb +82 -0
- data/lib/pakyow/data/sources/ephemeral.rb +72 -0
- data/lib/pakyow/data/sources/relational/association.rb +43 -0
- data/lib/pakyow/data/sources/relational/associations/belongs_to.rb +47 -0
- data/lib/pakyow/data/sources/relational/associations/has_many.rb +54 -0
- data/lib/pakyow/data/sources/relational/associations/has_one.rb +54 -0
- data/lib/pakyow/data/sources/relational/associations/through.rb +67 -0
- data/lib/pakyow/data/sources/relational/command.rb +531 -0
- data/lib/pakyow/data/sources/relational/migrator.rb +101 -0
- data/lib/pakyow/data/sources/relational.rb +587 -0
- data/lib/pakyow/data/subscribers/adapters/memory.rb +153 -0
- data/lib/pakyow/data/subscribers/adapters/redis/pipeliner.rb +45 -0
- data/lib/pakyow/data/subscribers/adapters/redis/scripts/_shared.lua +73 -0
- data/lib/pakyow/data/subscribers/adapters/redis/scripts/expire.lua +16 -0
- data/lib/pakyow/data/subscribers/adapters/redis/scripts/persist.lua +15 -0
- data/lib/pakyow/data/subscribers/adapters/redis/scripts/register.lua +37 -0
- data/lib/pakyow/data/subscribers/adapters/redis.rb +209 -0
- data/lib/pakyow/data/subscribers.rb +148 -0
- data/lib/pakyow/data/tasks/bootstrap.rake +18 -0
- data/lib/pakyow/data/tasks/create.rake +22 -0
- data/lib/pakyow/data/tasks/drop.rake +32 -0
- data/lib/pakyow/data/tasks/finalize.rake +56 -0
- data/lib/pakyow/data/tasks/migrate.rake +24 -0
- data/lib/pakyow/data/tasks/reset.rake +18 -0
- data/lib/pakyow/data/types.rb +37 -0
- data/lib/pakyow/data.rb +27 -0
- data/lib/pakyow/environment/data/auto_migrate.rb +31 -0
- data/lib/pakyow/environment/data/config.rb +54 -0
- data/lib/pakyow/environment/data/connections.rb +76 -0
- data/lib/pakyow/environment/data/memory_db.rb +23 -0
- data/lib/pakyow/validations/unique.rb +26 -0
- 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
|