bulk_dependency_eraser 1.0.1 → 1.0.4
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc034a38ab80a2e277860a5a69f6a654e1e461f691f54d6a510814b55279cebd
|
|
4
|
+
data.tar.gz: 88055bcbcb73bc260ef4718006f3149d95ea216badb4bd4ac142506e756c1882
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c9d1bc7851eaa2f51a853a99b24ff3842940f792938b2eac86ebfcc6471653c0d14de735d837f108ca59118a6661c086566b48475b87f1c98870cd80544dd79
|
|
7
|
+
data.tar.gz: 7e09f55e2baf0db2eefd7f2dfc028fda58b7f9ec2469a6bdbcfa56855bd4cf1ddc0b1502468554f56726fcbd08135daecc5e20940d18b2d9a2f49dc2558c5971
|
|
@@ -30,7 +30,9 @@ module BulkDependencyEraser
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def report_error msg
|
|
33
|
-
|
|
33
|
+
# remove new lines, surrounding white space, replace with semicolon delimiters
|
|
34
|
+
n = msg.strip.gsub(/\s*\n\s*/, ' ')
|
|
35
|
+
@errors << n
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def merge_errors errors, prefix = nil
|
|
@@ -13,6 +13,7 @@ module BulkDependencyEraser
|
|
|
13
13
|
# Won't parse any table in this list
|
|
14
14
|
ignore_tables_and_dependencies: [],
|
|
15
15
|
ignore_klass_names_and_dependencies: [],
|
|
16
|
+
batching_size_limit: 500,
|
|
16
17
|
}.freeze
|
|
17
18
|
|
|
18
19
|
DEFAULT_DB_WRAPPER = ->(block) do
|
|
@@ -106,11 +107,18 @@ module BulkDependencyEraser
|
|
|
106
107
|
#{e.message}
|
|
107
108
|
"
|
|
108
109
|
)
|
|
110
|
+
raise e
|
|
109
111
|
|
|
110
112
|
return false
|
|
111
113
|
end
|
|
112
114
|
end
|
|
113
115
|
|
|
116
|
+
protected
|
|
117
|
+
|
|
118
|
+
attr_reader :ignore_klass_and_dependencies
|
|
119
|
+
attr_reader :table_names_to_parsed_klass_names
|
|
120
|
+
attr_reader :ignore_table_name_and_dependencies, :ignore_klass_name_and_dependencies
|
|
121
|
+
|
|
114
122
|
def deletion_query_parser query, association_parent = nil
|
|
115
123
|
# necessary for "ActiveRecord::Reflection::ThroughReflection" use-case
|
|
116
124
|
# force_through_destroy_chains = options[:force_destroy_chain] || {}
|
|
@@ -145,7 +153,7 @@ module BulkDependencyEraser
|
|
|
145
153
|
if association_parent
|
|
146
154
|
puts "Building #{klass_name}"
|
|
147
155
|
else
|
|
148
|
-
puts "Building #{association_parent}
|
|
156
|
+
puts "Building #{association_parent}, assocation of #{klass_name}"
|
|
149
157
|
end
|
|
150
158
|
end
|
|
151
159
|
|
|
@@ -184,19 +192,12 @@ module BulkDependencyEraser
|
|
|
184
192
|
# ignore associations that aren't a dependent destroyable type
|
|
185
193
|
destroy_associations = query.reflect_on_all_associations.select do |reflection|
|
|
186
194
|
assoc_dependent_type = reflection.options&.dig(:dependent)&.to_sym
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"#{klass_name}'s assoc '#{reflection.name}' has a 'dependent: :#{assoc_dependent_type}' set. " \
|
|
194
|
-
"If you still wish to destroy, use the 'force_destroy_restricted: true' option"
|
|
195
|
-
)
|
|
196
|
-
false
|
|
197
|
-
else
|
|
198
|
-
DEPENDENCY_DESTROY.include?(assoc_dependent_type)
|
|
199
|
-
end
|
|
195
|
+
DEPENDENCY_DESTROY.include?(assoc_dependent_type) && !DEPENDENCY_RESTRICT.include?(assoc_dependent_type)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
restricted_associations = query.reflect_on_all_associations.select do |reflection|
|
|
199
|
+
assoc_dependent_type = reflection.options&.dig(:dependent)&.to_sym
|
|
200
|
+
DEPENDENCY_RESTRICT.include?(assoc_dependent_type)
|
|
200
201
|
end
|
|
201
202
|
|
|
202
203
|
nullify_associations = query.reflect_on_all_associations.select do |reflection|
|
|
@@ -204,16 +205,28 @@ module BulkDependencyEraser
|
|
|
204
205
|
DEPENDENCY_NULLIFY.include?(assoc_dependent_type)
|
|
205
206
|
end
|
|
206
207
|
|
|
207
|
-
destroy_association_names
|
|
208
|
-
nullify_association_names
|
|
208
|
+
destroy_association_names = destroy_associations.map(&:name)
|
|
209
|
+
nullify_association_names = nullify_associations.map(&:name)
|
|
210
|
+
restricted_association_names = restricted_associations.map(&:name)
|
|
209
211
|
|
|
210
|
-
# Iterate through the assoc names, if there are any :through assocs, then
|
|
212
|
+
# Iterate through the assoc names, if there are any :through assocs, then rename the association
|
|
213
|
+
# - Rails interpretation of any dependencies of a :through association is to apply it to
|
|
214
|
+
# the leaf association at the end of the :through chain(s)
|
|
211
215
|
destroy_association_names = destroy_association_names.collect do |assoc_name|
|
|
212
216
|
find_root_association_from_through_assocs(klass, assoc_name)
|
|
213
217
|
end
|
|
214
218
|
nullify_association_names = nullify_association_names.collect do |assoc_name|
|
|
215
219
|
find_root_association_from_through_assocs(klass, assoc_name)
|
|
216
220
|
end
|
|
221
|
+
restricted_association_names = restricted_association_names.collect do |assoc_name|
|
|
222
|
+
find_root_association_from_through_assocs(klass, assoc_name)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if opts_c.verbose
|
|
226
|
+
puts "Destroyable Associations: #{destroy_association_names}"
|
|
227
|
+
puts "Nullifiable Associations: #{nullify_association_names}"
|
|
228
|
+
puts " Restricted Associations: #{restricted_association_names}"
|
|
229
|
+
end
|
|
217
230
|
|
|
218
231
|
destroy_association_names.each do |destroy_association_name|
|
|
219
232
|
association_parser(klass, query, query_ids, destroy_association_name, :delete)
|
|
@@ -222,12 +235,49 @@ module BulkDependencyEraser
|
|
|
222
235
|
nullify_association_names.each do |nullify_association_name|
|
|
223
236
|
association_parser(klass, query, query_ids, nullify_association_name, :nullify)
|
|
224
237
|
end
|
|
238
|
+
|
|
239
|
+
restricted_association_names.each do |restricted_association_name|
|
|
240
|
+
association_parser(klass, query, query_ids, restricted_association_name, :restricted)
|
|
241
|
+
end
|
|
225
242
|
end
|
|
226
243
|
|
|
227
|
-
#
|
|
228
|
-
|
|
244
|
+
# Used to iterate through each destroyable association, and recursively call 'deletion_query_parser'.
|
|
245
|
+
# @param parent_class [ApplicationRecord]
|
|
246
|
+
# @param query [ActiveRecord_Relation] - We need the 'query' in case associations are tied to column other than 'id'
|
|
247
|
+
# @param query_ids [Array<Int | String>] - Array of parent's IDs (or UUIDs)
|
|
248
|
+
# @param association_name [Symbol] - The association name from the parent_class
|
|
249
|
+
# @param type [Symbol] - either :delete or :nullify or :restricted
|
|
250
|
+
def association_parser(parent_class, query, query_ids, association_name, type)
|
|
229
251
|
reflection = parent_class.reflect_on_association(association_name)
|
|
230
252
|
reflection_type = reflection.class.name
|
|
253
|
+
|
|
254
|
+
if self.class::DEPENDENCY_DESTROY_IGNORE_REFLECTION_TYPES.include?(reflection_type)
|
|
255
|
+
report_error("Dependency detected on #{parent_class.name}'s '#{association_name}' - association doesn't support dependency")
|
|
256
|
+
return
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
case reflection_type
|
|
260
|
+
when 'ActiveRecord::Reflection::HasManyReflection'
|
|
261
|
+
association_parser_has_many(parent_class, query, query_ids, association_name, type)
|
|
262
|
+
when 'ActiveRecord::Reflection::HasOneReflection'
|
|
263
|
+
association_parser_has_many(parent_class, query, query_ids, association_name, type, { limit: 1 })
|
|
264
|
+
when 'ActiveRecord::Reflection::BelongsToReflection'
|
|
265
|
+
if type == :nullify
|
|
266
|
+
report_error("#{parent_class.name}'s association '#{association_name}' - dependent 'nullify' invalid for 'belongs_to'")
|
|
267
|
+
else
|
|
268
|
+
association_parser_belongs_to(parent_class, query, query_ids, association_name, type)
|
|
269
|
+
end
|
|
270
|
+
else
|
|
271
|
+
report_message("Unsupported association type for #{parent_class.name}'s association '#{association_name}': #{reflection_type}")
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Handles the :has_many association type
|
|
276
|
+
# - handles it's polymorphic associations internally (easier on the has_many)
|
|
277
|
+
def association_parser_has_many(parent_class, query, query_ids, association_name, type, opts = {})
|
|
278
|
+
reflection = parent_class.reflect_on_association(association_name)
|
|
279
|
+
reflection_type = reflection.class.name
|
|
280
|
+
|
|
231
281
|
assoc_klass = reflection.klass
|
|
232
282
|
assoc_klass_name = assoc_klass.name
|
|
233
283
|
|
|
@@ -241,9 +291,9 @@ module BulkDependencyEraser
|
|
|
241
291
|
# If there is an association scope present, check to see how many parameters it's using
|
|
242
292
|
# - if there's any parameter, we have to either skip it or instantiate it to find it's dependencies.
|
|
243
293
|
if reflection.scope&.arity&.nonzero?
|
|
244
|
-
# TODO!
|
|
245
294
|
if opts_c.instantiate_if_assoc_scope_with_arity
|
|
246
|
-
|
|
295
|
+
association_parser_has_many_instantiation(parent_class, query, query_ids, association_name, type, opts)
|
|
296
|
+
return
|
|
247
297
|
else
|
|
248
298
|
report_error(
|
|
249
299
|
"#{parent_class.name} and '#{association_name}' - scope has instance parameters. Use :instantiate_if_assoc_scope_with_arity option?"
|
|
@@ -252,21 +302,23 @@ module BulkDependencyEraser
|
|
|
252
302
|
end
|
|
253
303
|
elsif reflection.scope
|
|
254
304
|
# I saw this used somewhere, too bad I didn't save the source for it.
|
|
255
|
-
|
|
256
|
-
assoc_query = assoc_query.instance_exec(&
|
|
305
|
+
assoc_scope = parent_class.reflect_on_association(association_name).scope
|
|
306
|
+
assoc_query = assoc_query.instance_exec(&assoc_scope)
|
|
257
307
|
end
|
|
258
308
|
|
|
309
|
+
# Look for manually specified keys in the assocation first
|
|
259
310
|
specified_primary_key = reflection.options[:primary_key]&.to_s
|
|
260
311
|
specified_foreign_key = reflection.options[:foreign_key]&.to_s
|
|
312
|
+
# For polymorphic_associations
|
|
313
|
+
specified_foreign_type = nil
|
|
261
314
|
|
|
262
315
|
# handle foreign_key edge cases
|
|
263
316
|
if specified_foreign_key.nil?
|
|
264
|
-
if reflection.options[:
|
|
265
|
-
|
|
266
|
-
specified_foreign_key =
|
|
267
|
-
|
|
268
|
-
assoc_query = assoc_query.where({
|
|
269
|
-
specified_foreign_key = reflection.options[:as].to_s + "_id"
|
|
317
|
+
if reflection.options[:as]
|
|
318
|
+
specified_foreign_type = "#{reflection.options[:as]}_type"
|
|
319
|
+
specified_foreign_key = "#{reflection.options[:as]}_id"
|
|
320
|
+
# Only filtering by type here, the extra work for a poly assoc. We filter by IDs later
|
|
321
|
+
assoc_query = assoc_query.where({ specified_foreign_type.to_sym => parent_class.name })
|
|
270
322
|
else
|
|
271
323
|
specified_foreign_key = parent_class.table_name.singularize + "_id"
|
|
272
324
|
end
|
|
@@ -277,7 +329,7 @@ module BulkDependencyEraser
|
|
|
277
329
|
report_error(
|
|
278
330
|
"
|
|
279
331
|
For #{parent_class.name}'s assoc '#{assoc_klass.name}': Could not determine the assoc's foreign key.
|
|
280
|
-
|
|
332
|
+
Foreign key should have been '#{specified_foreign_key}', but did not exist on the #{assoc_klass.table_name} table.
|
|
281
333
|
"
|
|
282
334
|
)
|
|
283
335
|
return
|
|
@@ -295,9 +347,18 @@ module BulkDependencyEraser
|
|
|
295
347
|
assoc_query = assoc_query.where(specified_foreign_key.to_sym => query_ids)
|
|
296
348
|
end
|
|
297
349
|
|
|
350
|
+
# Apply limit if has_one assocation (subset of has_many)
|
|
351
|
+
if opts[:limit]
|
|
352
|
+
assoc_query = assoc_query.limit(opts[:limit])
|
|
353
|
+
end
|
|
354
|
+
|
|
298
355
|
if type == :delete
|
|
299
356
|
# Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
|
|
300
357
|
deletion_query_parser(assoc_query, parent_class)
|
|
358
|
+
elsif type == :restricted
|
|
359
|
+
if traverse_restricted_dependency?(parent_class, reflection, assoc_query)
|
|
360
|
+
deletion_query_parser(assoc_query, parent_class)
|
|
361
|
+
end
|
|
301
362
|
elsif type == :nullify
|
|
302
363
|
# No need for recursion here.
|
|
303
364
|
# - we're not destroying these assocs (just nullifying foreign_key columns) so we don't need to parse their dependencies.
|
|
@@ -312,16 +373,193 @@ module BulkDependencyEraser
|
|
|
312
373
|
nullification_list[assoc_klass_name][specified_foreign_key] ||= []
|
|
313
374
|
nullification_list[assoc_klass_name][specified_foreign_key] += assoc_ids
|
|
314
375
|
nullification_list[assoc_klass_name][specified_foreign_key].uniq!
|
|
376
|
+
|
|
377
|
+
nullification_list[assoc_klass_name][specified_foreign_key].sort! if Rails.env.test?
|
|
378
|
+
|
|
379
|
+
# Also nullify the 'type' field, if the association is polymorphic
|
|
380
|
+
if specified_foreign_type
|
|
381
|
+
nullification_list[assoc_klass_name][specified_foreign_type] ||= []
|
|
382
|
+
nullification_list[assoc_klass_name][specified_foreign_type] += assoc_ids
|
|
383
|
+
nullification_list[assoc_klass_name][specified_foreign_type].uniq!
|
|
384
|
+
|
|
385
|
+
nullification_list[assoc_klass_name][specified_foreign_type].sort! if Rails.env.test?
|
|
386
|
+
end
|
|
315
387
|
else
|
|
316
388
|
raise "invalid parsing type: #{type}"
|
|
317
389
|
end
|
|
318
390
|
end
|
|
319
391
|
|
|
320
|
-
|
|
392
|
+
# So you've decided to attempt instantiation...
|
|
393
|
+
# This will be a lot slower than the rest of our logic here, but if needs must.
|
|
394
|
+
#
|
|
395
|
+
# This method will replicate association_parser, but instantiate and iterate in batches
|
|
396
|
+
def association_parser_has_many_instantiation(parent_class, query, query_ids, association_name, type, opts)
|
|
397
|
+
raise "Invalid State! Not ready to instantiate!"
|
|
398
|
+
reflection = parent_class.reflect_on_association(association_name)
|
|
399
|
+
reflection_type = reflection.class.name
|
|
400
|
+
assoc_klass = reflection.klass
|
|
401
|
+
assoc_klass_name = assoc_klass.name
|
|
321
402
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
403
|
+
|
|
404
|
+
# specified_primary_key = reflection.options[:primary_key]&.to_s
|
|
405
|
+
# specified_foreign_key = reflection.options[:foreign_key]&.to_s
|
|
406
|
+
|
|
407
|
+
# assoc_query = assoc_klass.unscoped
|
|
408
|
+
# query.in_batches
|
|
409
|
+
|
|
410
|
+
assoc_klass.in_batches(of: opts_c.batching_size_limit) do |batch|
|
|
411
|
+
batch.each do |record|
|
|
412
|
+
record.send(association_name)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def association_parser_belongs_to(parent_class, query, query_ids, association_name, type)
|
|
418
|
+
reflection = parent_class.reflect_on_association(association_name)
|
|
419
|
+
reflection_type = reflection.class.name
|
|
420
|
+
|
|
421
|
+
# Can't run certain checks on a polymorphic association, no definitive klass to use.
|
|
422
|
+
# - Usually, the polymorphic class is the leaf in a dependency tree.
|
|
423
|
+
# - In this case, i.e.: a `belongs_to :polymorphicable, polymorphic: true, dependent: :destroy` use-case
|
|
424
|
+
if reflection.options[:polymorphic]
|
|
425
|
+
# We'd have to pluck our various types, iterate through each, using each type as the assoc_query starting point
|
|
426
|
+
association_parser_belongs_to_polymorphic(parent_class, query, query_ids, association_name, type)
|
|
427
|
+
return
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
assoc_klass = reflection.klass
|
|
431
|
+
assoc_klass_name = assoc_klass.name
|
|
432
|
+
|
|
433
|
+
assoc_query = assoc_klass.unscoped
|
|
434
|
+
|
|
435
|
+
unless assoc_klass.primary_key == 'id'
|
|
436
|
+
report_error("#{parent_class.name}'s association '#{association_name}' - assoc class does not use 'id' as a primary_key")
|
|
437
|
+
return
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# If there is an association scope present, check to see how many parameters it's using
|
|
441
|
+
# - if there's any parameter, we have to either skip it or instantiate it to find it's dependencies.
|
|
442
|
+
if reflection.scope&.arity&.nonzero?
|
|
443
|
+
# TODO: PENDING:
|
|
444
|
+
# if opts_c.instantiate_if_assoc_scope_with_arity
|
|
445
|
+
# association_parser_belongs_to_instantiation(parent_class, query, query_ids, association_name, type)
|
|
446
|
+
# return
|
|
447
|
+
# else
|
|
448
|
+
# report_error(
|
|
449
|
+
# "#{parent_class.name} and '#{association_name}' - scope has instance parameters. Use :instantiate_if_assoc_scope_with_arity option?"
|
|
450
|
+
# )
|
|
451
|
+
# return
|
|
452
|
+
# end
|
|
453
|
+
elsif reflection.scope
|
|
454
|
+
# I saw this used somewhere, too bad I didn't save the source for it.
|
|
455
|
+
assoc_scope = parent_class.reflect_on_association(association_name).scope
|
|
456
|
+
assoc_query = assoc_query.instance_exec(&assoc_scope)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
specified_primary_key = reflection.options[:primary_key] || 'id'
|
|
460
|
+
specified_foreign_key = reflection.options[:foreign_key] || "#{association_name}_id"
|
|
461
|
+
|
|
462
|
+
# Check to see if foreign_key exists in our parent table
|
|
463
|
+
unless parent_class.column_names.include?(specified_foreign_key)
|
|
464
|
+
report_error(
|
|
465
|
+
"
|
|
466
|
+
For #{parent_class.name}'s association '#{association_name}': Could not determine the assoc's foreign key.
|
|
467
|
+
Foreign key should have been '#{specified_foreign_key}', but did not exist on the #{parent_class.table_name} table.
|
|
468
|
+
"
|
|
469
|
+
)
|
|
470
|
+
return
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
assoc_query = read_from_db do
|
|
474
|
+
assoc_query.where(
|
|
475
|
+
specified_primary_key.to_sym => query.pluck(specified_foreign_key)
|
|
476
|
+
)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
if type == :delete
|
|
480
|
+
# Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
|
|
481
|
+
deletion_query_parser(assoc_query, parent_class)
|
|
482
|
+
elsif type == :restricted
|
|
483
|
+
if traverse_restricted_dependency?(parent_class, reflection, assoc_query)
|
|
484
|
+
deletion_query_parser(assoc_query, parent_class)
|
|
485
|
+
end
|
|
486
|
+
else
|
|
487
|
+
raise "invalid parsing type: #{type}"
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# So you've decided to attempt instantiation...
|
|
492
|
+
# This will be a lot slower than the rest of our logic here, but if needs must.
|
|
493
|
+
#
|
|
494
|
+
# This method will replicate association_parser, but instantiate and iterate in batches
|
|
495
|
+
def association_parser_belongs_to_instantiation(parent_class, query, query_ids, association_name, type)
|
|
496
|
+
# pending
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# In this case, it's like a `belongs_to :polymorphicable, polymorphic: true, dependent: :destroy` use-case
|
|
500
|
+
# - it's unusual, but valid use-case
|
|
501
|
+
def association_parser_belongs_to_polymorphic(parent_class, query, query_ids, association_name, type)
|
|
502
|
+
reflection = parent_class.reflect_on_association(association_name)
|
|
503
|
+
reflection_type = reflection.class.name
|
|
504
|
+
|
|
505
|
+
# If there is an association scope present, check to see how many parameters it's using
|
|
506
|
+
# - if there's any parameter, we have to either skip it or instantiate it to find it's dependencies.
|
|
507
|
+
if reflection.scope&.arity&.nonzero?
|
|
508
|
+
raise 'PENDING'
|
|
509
|
+
elsif reflection.scope
|
|
510
|
+
# I saw this used somewhere, too bad I didn't save the source for it.
|
|
511
|
+
assoc_scope = parent_class.reflect_on_association(association_name).scope
|
|
512
|
+
assoc_query = assoc_query.instance_exec(&assoc_scope)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
specified_primary_key = reflection.options[:primary_key] || 'id'
|
|
516
|
+
specified_foreign_key = reflection.options[:foreign_key] || "#{association_name}_id"
|
|
517
|
+
specified_foreign_type = specified_foreign_key.sub(/_id$/, '_type')
|
|
518
|
+
|
|
519
|
+
# Check to see if foreign_key exists in our parent table
|
|
520
|
+
unless parent_class.column_names.include?(specified_foreign_key)
|
|
521
|
+
report_error(
|
|
522
|
+
"
|
|
523
|
+
For #{parent_class.name}'s association '#{association_name}': Could not determine the class's foreign key.
|
|
524
|
+
Foreign key should have been '#{specified_foreign_key}', but did not exist on the #{parent_class.table_name} table.
|
|
525
|
+
"
|
|
526
|
+
)
|
|
527
|
+
return
|
|
528
|
+
end
|
|
529
|
+
unless parent_class.column_names.include?(specified_foreign_type)
|
|
530
|
+
report_error(
|
|
531
|
+
"
|
|
532
|
+
For #{parent_class.name}'s association '#{association_name}': Could not determine the class's polymorphic type.
|
|
533
|
+
Foreign key should have been '#{specified_foreign_type}', but did not exist on the #{parent_class.table_name} table.
|
|
534
|
+
"
|
|
535
|
+
)
|
|
536
|
+
return
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
foreign_ids_by_type = read_from_db do
|
|
540
|
+
query.pluck(specified_foreign_key, specified_foreign_type).each_with_object({}) do |(id, type), hash|
|
|
541
|
+
hash.key?(type) ? hash[type] << id : hash[type] = [id]
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
if type == :delete
|
|
546
|
+
# Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
|
|
547
|
+
foreign_ids_by_type.each do |type, ids|
|
|
548
|
+
assoc_klass = type.constantize
|
|
549
|
+
deletion_query_parser(assoc_klass.where(id: ids), assoc_klass)
|
|
550
|
+
end
|
|
551
|
+
elsif type == :restricted
|
|
552
|
+
if traverse_restricted_dependency_for_belongs_to_poly?(parent_class, reflection, foreign_ids_by_type)
|
|
553
|
+
# Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
|
|
554
|
+
foreign_ids_by_type.each do |type, ids|
|
|
555
|
+
assoc_klass = type.constantize
|
|
556
|
+
deletion_query_parser(assoc_klass.where(id: ids), assoc_klass)
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
else
|
|
560
|
+
raise "invalid parsing type: #{type}"
|
|
561
|
+
end
|
|
562
|
+
end
|
|
325
563
|
|
|
326
564
|
# A dependent assoc may be through another association. Follow the throughs to find the correct assoc to destroy.
|
|
327
565
|
def find_root_association_from_through_assocs klass, association_name
|
|
@@ -334,6 +572,43 @@ module BulkDependencyEraser
|
|
|
334
572
|
end
|
|
335
573
|
end
|
|
336
574
|
|
|
575
|
+
# return [Boolean]
|
|
576
|
+
# - true if valid
|
|
577
|
+
# - false if not valid
|
|
578
|
+
def traverse_restricted_dependency? parent_class, reflection, association_query
|
|
579
|
+
# Return true if we're going to destroy all restricted
|
|
580
|
+
return true if opts_c.force_destroy_restricted
|
|
581
|
+
|
|
582
|
+
if association_query.any?
|
|
583
|
+
report_error(
|
|
584
|
+
"
|
|
585
|
+
#{parent_class.name}'s assoc '#{reflection.name}' has a restricted dependency type.
|
|
586
|
+
If you still wish to destroy, use the 'force_destroy_restricted: true' option
|
|
587
|
+
"
|
|
588
|
+
)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
return false
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# special use-case to detect restricted dependency for 'belongs_to polymorphic: true' use-case
|
|
595
|
+
def traverse_restricted_dependency_for_belongs_to_poly? parent_class, reflection, ids_by_type
|
|
596
|
+
# Return true if we're going to destroy all restricted
|
|
597
|
+
return true if opts_c.force_destroy_restricted
|
|
598
|
+
|
|
599
|
+
ids = ids_by_type.values.flatten
|
|
600
|
+
if ids.any?
|
|
601
|
+
report_error(
|
|
602
|
+
"
|
|
603
|
+
#{parent_class.name}'s assoc '#{reflection.name}' has a restricted dependency type.
|
|
604
|
+
If you still wish to destroy, use the 'force_destroy_restricted: true' option
|
|
605
|
+
"
|
|
606
|
+
)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
return false
|
|
610
|
+
end
|
|
611
|
+
|
|
337
612
|
def read_from_db(&block)
|
|
338
613
|
puts "Reading from DB..." if opts_c.verbose
|
|
339
614
|
opts_c.db_read_wrapper.call(block)
|
|
@@ -44,8 +44,8 @@ module BulkDependencyEraser
|
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
|
-
rescue
|
|
48
|
-
report_error("Issue attempting to delete '#{current_class_name}': #{e.name} - #{e.message}")
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
report_error("Issue attempting to delete '#{current_class_name}': #{e.class.name} - #{e.message}")
|
|
49
49
|
raise ActiveRecord::Rollback
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -2,7 +2,11 @@ module BulkDependencyEraser
|
|
|
2
2
|
class Nullifier < Base
|
|
3
3
|
DEFAULT_OPTS = {
|
|
4
4
|
verbose: false,
|
|
5
|
-
db_nullify_wrapper: self::DEFAULT_DB_WRAPPER
|
|
5
|
+
db_nullify_wrapper: self::DEFAULT_DB_WRAPPER,
|
|
6
|
+
# Set to true if you want 'ActiveRecord::InvalidForeignKey' errors raised during nullifications
|
|
7
|
+
# - I can't think of a use-case where a nullification would generate an invalid key error
|
|
8
|
+
# - Not hurting anything to leave it in, but might remove it in the future.
|
|
9
|
+
enable_invalid_foreign_key_detection: false
|
|
6
10
|
}.freeze
|
|
7
11
|
|
|
8
12
|
DEFAULT_DB_WRAPPER = ->(block) do
|
|
@@ -22,6 +26,53 @@ module BulkDependencyEraser
|
|
|
22
26
|
def initialize class_names_columns_and_ids:, opts: {}
|
|
23
27
|
@class_names_columns_and_ids = class_names_columns_and_ids
|
|
24
28
|
super(opts:)
|
|
29
|
+
|
|
30
|
+
if opts_c.verbose
|
|
31
|
+
puts "Combining nullification column groups (if groupable)"
|
|
32
|
+
puts "Before Combination: #{@class_names_columns_and_ids}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@class_names_columns_and_ids = combine_matching_columns(@class_names_columns_and_ids)
|
|
36
|
+
|
|
37
|
+
if opts_c.verbose
|
|
38
|
+
puts "After Combination: #{@class_names_columns_and_ids}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Combine columns if the IDs are the same
|
|
43
|
+
# - will do one SQL call instead of several
|
|
44
|
+
def combine_matching_columns(nullification_hash)
|
|
45
|
+
return {} if nullification_hash.none?
|
|
46
|
+
|
|
47
|
+
merged_hash = {}
|
|
48
|
+
|
|
49
|
+
nullification_hash.each do |klass_name, columns_and_ids|
|
|
50
|
+
merged_hash[klass_name] = {}
|
|
51
|
+
columns_and_ids.each do |key, array|
|
|
52
|
+
sorted_array = array.sort
|
|
53
|
+
|
|
54
|
+
# Find any existing key in merged_hash that has the same sorted array
|
|
55
|
+
matching_key = merged_hash[klass_name].keys.find { |k| merged_hash[klass_name][k].sort == sorted_array }
|
|
56
|
+
|
|
57
|
+
if matching_key
|
|
58
|
+
# Concatenate the matching keys and update the hash
|
|
59
|
+
new_key = key.is_a?(Array) ? key : [key]
|
|
60
|
+
if matching_key.is_a?(Array)
|
|
61
|
+
new_key += matching_key
|
|
62
|
+
else
|
|
63
|
+
new_key << matching_key
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
merged_hash[klass_name][new_key] = sorted_array
|
|
67
|
+
merged_hash[klass_name].delete(matching_key)
|
|
68
|
+
else
|
|
69
|
+
# Otherwise, just add the current key-value pair
|
|
70
|
+
merged_hash[klass_name][key] = sorted_array
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
merged_hash
|
|
25
76
|
end
|
|
26
77
|
|
|
27
78
|
def execute
|
|
@@ -34,21 +85,25 @@ module BulkDependencyEraser
|
|
|
34
85
|
klass = class_name.constantize
|
|
35
86
|
|
|
36
87
|
columns_and_ids = class_names_columns_and_ids[class_name]
|
|
37
|
-
|
|
38
88
|
columns_and_ids.each do |column, ids|
|
|
39
89
|
current_column = column
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
90
|
+
|
|
91
|
+
if opts_c.enable_invalid_foreign_key_detection
|
|
92
|
+
# nullify with referential integrity
|
|
93
|
+
nullify_by_klass_column_and_ids(klass, column, ids)
|
|
94
|
+
else
|
|
95
|
+
# nullify without referential integrity
|
|
96
|
+
# Disable any ActiveRecord::InvalidForeignKey raised errors.
|
|
97
|
+
# - src: https://stackoverflow.com/questions/41005849/rails-migrations-temporarily-ignore-foreign-key-constraint
|
|
98
|
+
# https://apidock.com/rails/ActiveRecord/ConnectionAdapters/AbstractAdapter/disable_referential_integrity
|
|
99
|
+
ActiveRecord::Base.connection.disable_referential_integrity do
|
|
100
|
+
nullify_by_klass_column_and_ids(klass, column, ids)
|
|
46
101
|
end
|
|
47
102
|
end
|
|
48
103
|
end
|
|
49
104
|
end
|
|
50
|
-
rescue
|
|
51
|
-
report_error("Issue attempting to nullify '#{current_class_name}' column '#{current_column}': #{e.name} - #{e.message}")
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
report_error("Issue attempting to nullify '#{current_class_name}' column '#{current_column}': #{e.class.name} - #{e.message}")
|
|
52
107
|
raise ActiveRecord::Rollback
|
|
53
108
|
end
|
|
54
109
|
end
|
|
@@ -60,6 +115,23 @@ module BulkDependencyEraser
|
|
|
60
115
|
|
|
61
116
|
attr_reader :class_names_columns_and_ids
|
|
62
117
|
|
|
118
|
+
def nullify_by_klass_column_and_ids klass, columns, ids
|
|
119
|
+
nullify_columns = {}
|
|
120
|
+
|
|
121
|
+
# supporting nullification of groups of columns simultaneously
|
|
122
|
+
if columns.is_a?(Array)
|
|
123
|
+
columns.each do |column|
|
|
124
|
+
nullify_columns[column] = nil
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
nullify_columns[columns] = nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
nullify_in_db do
|
|
131
|
+
klass.unscoped.where(id: ids).update_all(nullify_columns)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
63
135
|
def nullify_in_db(&block)
|
|
64
136
|
puts "Nullifying from DB..." if opts_c.verbose
|
|
65
137
|
opts_c.db_nullify_wrapper.call(block)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bulk_dependency_eraser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- benjamin.dana.software.dev@gmail.com
|
|
@@ -108,6 +108,34 @@ dependencies:
|
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: '1.4'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: factory_bot
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '6.4'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '6.4'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: faker
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '3.4'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '3.4'
|
|
111
139
|
description:
|
|
112
140
|
email:
|
|
113
141
|
executables: []
|
|
@@ -123,7 +151,8 @@ files:
|
|
|
123
151
|
homepage: https://github.com/danabr75/bulk_dependency_eraser
|
|
124
152
|
licenses:
|
|
125
153
|
- LGPL-3.0-only
|
|
126
|
-
metadata:
|
|
154
|
+
metadata:
|
|
155
|
+
source_code_uri: https://github.com/danabr75/bulk_dependency_eraser
|
|
127
156
|
post_install_message:
|
|
128
157
|
rdoc_options: []
|
|
129
158
|
require_paths:
|