bulk_dependency_eraser 1.2.2 → 1.3.2
Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e0b2a7c10501166d33e7076cf19b7c241ffd9de2704d94aca6f7c9fecc6e2d4
|
4
|
+
data.tar.gz: 6788ed915a4a6b4f7d7dbe2c8387244b0ff5e758098fe48f47b47127e64d9fb5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab13a12a9e5965890ae2e27cbf0971ba75aec2dbdc463d92c1631ac9cdb6dd98729e22cedb9c05cfb4cd4f897ca774c5204d0c3a61c804f8f7f8961de4e686cb
|
7
|
+
data.tar.gz: 9d0968970dd212e9e07c459134e829c8b1fd17c3c65d2b1e7665f983a68c4655044e3732ce7e2f6d28fb0c0e59606ffde0f343761a4730abb2f6dc74c3ba521d
|
@@ -13,10 +13,13 @@ 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
|
+
disable_batching: false,
|
16
17
|
# a general batching size
|
17
18
|
batch_size: 10_000,
|
18
19
|
# A specific batching size for this class, overrides the batch_size
|
19
20
|
read_batch_size: nil,
|
21
|
+
# A specific read batching disable option
|
22
|
+
disable_read_batching: nil,
|
20
23
|
}.freeze
|
21
24
|
|
22
25
|
DEFAULT_DB_WRAPPER = ->(block) do
|
@@ -85,6 +88,27 @@ module BulkDependencyEraser
|
|
85
88
|
end
|
86
89
|
end
|
87
90
|
|
91
|
+
# Reverse order IDs
|
92
|
+
# - if a class self-referenced itself the children will be deleted/nullified before parent
|
93
|
+
deletion_list.keys.each do |class_name|
|
94
|
+
deletion_list[class_name] = deletion_list.delete(class_name).reverse
|
95
|
+
|
96
|
+
# TODO: Hard to test if not sorted
|
97
|
+
deletion_list[class_name] = deletion_list[class_name].sort if Rails.env.test?
|
98
|
+
end
|
99
|
+
|
100
|
+
nullification_list.keys.each do |class_name|
|
101
|
+
columns_and_ids = nullification_list.delete(class_name)
|
102
|
+
# we don't need to reverse the column keys
|
103
|
+
columns_and_ids.keys.each do |column_name|
|
104
|
+
columns_and_ids[column_name] = columns_and_ids.delete(column_name).reverse
|
105
|
+
|
106
|
+
# TODO: Hard to test if not sorted
|
107
|
+
columns_and_ids[column_name] = columns_and_ids.delete(column_name).sort if Rails.env.test?
|
108
|
+
end
|
109
|
+
nullification_list[class_name] = columns_and_ids
|
110
|
+
end
|
111
|
+
|
88
112
|
return build_result
|
89
113
|
end
|
90
114
|
|
@@ -127,8 +151,40 @@ module BulkDependencyEraser
|
|
127
151
|
attr_reader :table_names_to_parsed_klass_names
|
128
152
|
attr_reader :ignore_table_name_and_dependencies, :ignore_klass_name_and_dependencies
|
129
153
|
|
154
|
+
def pluck_from_query query, column = :id
|
155
|
+
# ordering shouldn't matter in these queries, and would slow it down
|
156
|
+
# - we're ignoring default_scope ordering, but assoc-defined ordering would still take effect
|
157
|
+
query = query.reorder('')
|
158
|
+
query_ids = []
|
159
|
+
read_from_db do
|
160
|
+
# If the query has a limit, then we don't want to clobber with batching.
|
161
|
+
if batching_disabled? || !query.where({}).limit_value.nil?
|
162
|
+
# query without batching
|
163
|
+
query_ids = query.pluck(column)
|
164
|
+
else
|
165
|
+
# query with batching
|
166
|
+
offset = 0
|
167
|
+
loop do
|
168
|
+
new_query_ids = query.offset(offset).limit(batch_size).pluck(column)
|
169
|
+
query_ids += new_query_ids
|
170
|
+
|
171
|
+
break if new_query_ids.size < batch_size
|
172
|
+
|
173
|
+
# Move to the next batch
|
174
|
+
offset += batch_size
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
return query_ids
|
180
|
+
end
|
181
|
+
|
130
182
|
def batch_size
|
131
|
-
opts_c.read_batch_size
|
183
|
+
opts_c.read_batch_size.nil? ? opts_c.batch_size : opts_c.read_batch_size
|
184
|
+
end
|
185
|
+
|
186
|
+
def batching_disabled?
|
187
|
+
opts_c.disable_read_batching.nil? ? opts_c.disable_batching : opts_c.disable_read_batching
|
132
188
|
end
|
133
189
|
|
134
190
|
def deletion_query_parser query, association_parent = nil
|
@@ -176,13 +232,8 @@ module BulkDependencyEraser
|
|
176
232
|
return
|
177
233
|
end
|
178
234
|
|
179
|
-
# Pluck IDs of the current query
|
180
|
-
query_ids =
|
181
|
-
query.in_batches(of: batch_size) do |batch|
|
182
|
-
read_from_db do
|
183
|
-
query_ids += batch.pluck(:id)
|
184
|
-
end
|
185
|
-
end
|
235
|
+
# Pluck IDs of the current query
|
236
|
+
query_ids = pluck_from_query(query)
|
186
237
|
|
187
238
|
deletion_list[klass_name] ||= []
|
188
239
|
|
@@ -203,7 +254,7 @@ module BulkDependencyEraser
|
|
203
254
|
|
204
255
|
# Hard to test if not sorted
|
205
256
|
# - if we had more advanced rspec matches, we could do away with this.
|
206
|
-
deletion_list[klass_name].sort! if Rails.env.test?
|
257
|
+
# deletion_list[klass_name].sort! if Rails.env.test?
|
207
258
|
|
208
259
|
# ignore associations that aren't a dependent destroyable type
|
209
260
|
destroy_associations = query.reflect_on_all_associations.select do |reflection|
|
@@ -370,12 +421,7 @@ module BulkDependencyEraser
|
|
370
421
|
# - handle primary key edge cases
|
371
422
|
# - The associations might not be using the primary_key of the klass table, but we can support that here.
|
372
423
|
if specified_primary_key && specified_primary_key&.to_s != 'id'
|
373
|
-
alt_primary_ids =
|
374
|
-
query.in_batches(of: batch_size) do |batch|
|
375
|
-
read_from_db do
|
376
|
-
alt_primary_ids += query.pluck(specified_primary_key)
|
377
|
-
end
|
378
|
-
end
|
424
|
+
alt_primary_ids = pluck_from_query(query, specified_primary_key)
|
379
425
|
assoc_query = assoc_query.where(specified_foreign_key.to_sym => alt_primary_ids)
|
380
426
|
else
|
381
427
|
assoc_query = assoc_query.where(specified_foreign_key.to_sym => query_ids)
|
@@ -397,12 +443,7 @@ module BulkDependencyEraser
|
|
397
443
|
elsif type == :nullify
|
398
444
|
# No need for recursion here.
|
399
445
|
# - we're not destroying these assocs (just nullifying foreign_key columns) so we don't need to parse their dependencies.
|
400
|
-
assoc_ids =
|
401
|
-
assoc_query.in_batches(of: batch_size) do |batch|
|
402
|
-
read_from_db do
|
403
|
-
assoc_ids += batch.pluck(:id)
|
404
|
-
end
|
405
|
-
end
|
446
|
+
assoc_ids = pluck_from_query(assoc_query)
|
406
447
|
|
407
448
|
# No assoc_ids, no need to add it to the nullification list
|
408
449
|
return if assoc_ids.none?
|
@@ -412,7 +453,7 @@ module BulkDependencyEraser
|
|
412
453
|
nullification_list[assoc_klass_name][specified_foreign_key] += assoc_ids
|
413
454
|
nullification_list[assoc_klass_name][specified_foreign_key].uniq!
|
414
455
|
|
415
|
-
nullification_list[assoc_klass_name][specified_foreign_key].sort! if Rails.env.test?
|
456
|
+
# nullification_list[assoc_klass_name][specified_foreign_key].sort! if Rails.env.test?
|
416
457
|
|
417
458
|
# Also nullify the 'type' field, if the association is polymorphic
|
418
459
|
if specified_foreign_type
|
@@ -420,7 +461,7 @@ module BulkDependencyEraser
|
|
420
461
|
nullification_list[assoc_klass_name][specified_foreign_type] += assoc_ids
|
421
462
|
nullification_list[assoc_klass_name][specified_foreign_type].uniq!
|
422
463
|
|
423
|
-
nullification_list[assoc_klass_name][specified_foreign_type].sort! if Rails.env.test?
|
464
|
+
# nullification_list[assoc_klass_name][specified_foreign_type].sort! if Rails.env.test?
|
424
465
|
end
|
425
466
|
else
|
426
467
|
raise "invalid parsing type: #{type}"
|
@@ -508,23 +549,20 @@ module BulkDependencyEraser
|
|
508
549
|
return
|
509
550
|
end
|
510
551
|
|
511
|
-
query
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
)
|
516
|
-
end
|
552
|
+
foreign_keys = pluck_from_query(query, specified_foreign_key)
|
553
|
+
assoc_query = assoc_query.where(
|
554
|
+
specified_primary_key.to_sym => foreign_keys
|
555
|
+
)
|
517
556
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
end
|
525
|
-
else
|
526
|
-
raise "invalid parsing type: #{type}"
|
557
|
+
if type == :delete
|
558
|
+
# Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
|
559
|
+
deletion_query_parser(assoc_query, parent_class)
|
560
|
+
elsif type == :restricted
|
561
|
+
if traverse_restricted_dependency?(parent_class, reflection, assoc_query)
|
562
|
+
deletion_query_parser(assoc_query, parent_class)
|
527
563
|
end
|
564
|
+
else
|
565
|
+
raise "invalid parsing type: #{type}"
|
528
566
|
end
|
529
567
|
end
|
530
568
|
|
@@ -539,6 +577,8 @@ module BulkDependencyEraser
|
|
539
577
|
# In this case, it's like a `belongs_to :polymorphicable, polymorphic: true, dependent: :destroy` use-case
|
540
578
|
# - it's unusual, but valid use-case
|
541
579
|
def association_parser_belongs_to_polymorphic(parent_class, query, query_ids, association_name, type)
|
580
|
+
# raise "Unsupported use-case: #{parent_class.name} -> belongs_to :polymorphicable, polymorphic: true, dependent: :destroy"
|
581
|
+
|
542
582
|
reflection = parent_class.reflect_on_association(association_name)
|
543
583
|
reflection_type = reflection.class.name
|
544
584
|
|
@@ -576,30 +616,47 @@ module BulkDependencyEraser
|
|
576
616
|
return
|
577
617
|
end
|
578
618
|
|
579
|
-
|
580
|
-
|
581
|
-
|
619
|
+
foreign_ids_by_type = read_from_db do
|
620
|
+
if batching_disabled? || !query.where({}).limit_value.nil?
|
621
|
+
# query without batching
|
622
|
+
query.reorder('').pluck(specified_foreign_key, specified_foreign_type).each_with_object({}) do |(id, type), hash|
|
582
623
|
hash.key?(type) ? hash[type] << id : hash[type] = [id]
|
583
624
|
end
|
625
|
+
else
|
626
|
+
columns_and_ids = {}
|
627
|
+
offset = 0
|
628
|
+
loop do
|
629
|
+
counter = 0
|
630
|
+
query.reorder('').offset(offset).limit(batch_size).pluck(specified_foreign_key, specified_foreign_type).each do |id, type|
|
631
|
+
columns_and_ids.key?(type) ? columns_and_ids[type] << id : columns_and_ids[type] = [id]
|
632
|
+
counter += 1
|
633
|
+
end
|
634
|
+
|
635
|
+
break if counter < batch_size
|
636
|
+
|
637
|
+
# Move to the next batch
|
638
|
+
offset += batch_size
|
639
|
+
end
|
640
|
+
columns_and_ids
|
584
641
|
end
|
642
|
+
end
|
585
643
|
|
586
|
-
|
644
|
+
if type == :delete
|
645
|
+
# Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
|
646
|
+
foreign_ids_by_type.each do |type, ids|
|
647
|
+
assoc_klass = type.constantize
|
648
|
+
deletion_query_parser(assoc_klass.where(id: ids), assoc_klass)
|
649
|
+
end
|
650
|
+
elsif type == :restricted
|
651
|
+
if traverse_restricted_dependency_for_belongs_to_poly?(parent_class, reflection, foreign_ids_by_type)
|
587
652
|
# Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
|
588
653
|
foreign_ids_by_type.each do |type, ids|
|
589
654
|
assoc_klass = type.constantize
|
590
655
|
deletion_query_parser(assoc_klass.where(id: ids), assoc_klass)
|
591
656
|
end
|
592
|
-
elsif type == :restricted
|
593
|
-
if traverse_restricted_dependency_for_belongs_to_poly?(parent_class, reflection, foreign_ids_by_type)
|
594
|
-
# Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
|
595
|
-
foreign_ids_by_type.each do |type, ids|
|
596
|
-
assoc_klass = type.constantize
|
597
|
-
deletion_query_parser(assoc_klass.where(id: ids), assoc_klass)
|
598
|
-
end
|
599
|
-
end
|
600
|
-
else
|
601
|
-
raise "invalid parsing type: #{type}"
|
602
657
|
end
|
658
|
+
else
|
659
|
+
raise "invalid parsing type: #{type}"
|
603
660
|
end
|
604
661
|
end
|
605
662
|
|
@@ -5,10 +5,13 @@ module BulkDependencyEraser
|
|
5
5
|
db_delete_wrapper: self::DEFAULT_DB_WRAPPER,
|
6
6
|
# Set to true if you want 'ActiveRecord::InvalidForeignKey' errors raised during deletions
|
7
7
|
enable_invalid_foreign_key_detection: false,
|
8
|
+
disable_batching: false,
|
8
9
|
# a general batching size
|
9
10
|
batch_size: 300,
|
10
11
|
# A specific batching size for this class, overrides the batch_size
|
11
12
|
delete_batch_size: nil,
|
13
|
+
# A specific batching size for this class, overrides the batch_size
|
14
|
+
disable_delete_batching: nil,
|
12
15
|
}.freeze
|
13
16
|
|
14
17
|
DEFAULT_DB_WRAPPER = ->(block) do
|
@@ -30,6 +33,7 @@ module BulkDependencyEraser
|
|
30
33
|
ActiveRecord::Base.transaction do
|
31
34
|
current_class_name = 'N/A'
|
32
35
|
begin
|
36
|
+
# IDs should have already been reversed in builder
|
33
37
|
class_names_and_ids.keys.reverse.each do |class_name|
|
34
38
|
current_class_name = class_name
|
35
39
|
ids = class_names_and_ids[class_name]
|
@@ -65,11 +69,23 @@ module BulkDependencyEraser
|
|
65
69
|
opts_c.delete_batch_size || opts_c.batch_size
|
66
70
|
end
|
67
71
|
|
72
|
+
def batching_disabled?
|
73
|
+
opts_c.disable_delete_batching.nil? ? opts_c.disable_batching : opts_c.disable_delete_batching
|
74
|
+
end
|
75
|
+
|
68
76
|
def delete_by_klass_and_ids klass, ids
|
69
77
|
puts "Deleting #{klass.name}'s IDs: #{ids}" if opts_c.verbose
|
70
|
-
|
78
|
+
if batching_disabled?
|
79
|
+
puts "Deleting without batching" if opts_c.verbose
|
71
80
|
delete_in_db do
|
72
|
-
klass.unscoped.where(id:
|
81
|
+
klass.unscoped.where(id: ids).delete_all
|
82
|
+
end
|
83
|
+
else
|
84
|
+
puts "Deleting with batching" if opts_c.verbose
|
85
|
+
ids.each_slice(batch_size) do |ids_subset|
|
86
|
+
delete_in_db do
|
87
|
+
klass.unscoped.where(id: ids_subset).delete_all
|
88
|
+
end
|
73
89
|
end
|
74
90
|
end
|
75
91
|
end
|
@@ -78,6 +94,5 @@ module BulkDependencyEraser
|
|
78
94
|
puts "Deleting from DB..." if opts_c.verbose
|
79
95
|
opts_c.db_delete_wrapper.call(block)
|
80
96
|
end
|
81
|
-
|
82
97
|
end
|
83
98
|
end
|
@@ -7,10 +7,13 @@ module BulkDependencyEraser
|
|
7
7
|
# - I can't think of a use-case where a nullification would generate an invalid key error
|
8
8
|
# - Not hurting anything to leave it in, but might remove it in the future.
|
9
9
|
enable_invalid_foreign_key_detection: false,
|
10
|
+
disable_batching: false,
|
10
11
|
# a general batching size
|
11
12
|
batch_size: 300,
|
12
13
|
# A specific batching size for this class, overrides the batch_size
|
13
14
|
nullify_batch_size: nil,
|
15
|
+
# A specific batching size for this class, overrides the batch_size
|
16
|
+
disable_nullify_batching: nil,
|
14
17
|
}.freeze
|
15
18
|
|
16
19
|
DEFAULT_DB_WRAPPER = ->(block) do
|
@@ -84,11 +87,12 @@ module BulkDependencyEraser
|
|
84
87
|
current_class_name = 'N/A'
|
85
88
|
current_column = 'N/A'
|
86
89
|
begin
|
90
|
+
# column_and_ids should have already been reversed in builder
|
87
91
|
class_names_columns_and_ids.keys.reverse.each do |class_name|
|
88
92
|
current_class_name = class_name
|
89
93
|
klass = class_name.constantize
|
90
|
-
|
91
94
|
columns_and_ids = class_names_columns_and_ids[class_name]
|
95
|
+
|
92
96
|
columns_and_ids.each do |column, ids|
|
93
97
|
current_column = column
|
94
98
|
|
@@ -123,6 +127,10 @@ module BulkDependencyEraser
|
|
123
127
|
opts_c.nullify_batch_size || opts_c.batch_size
|
124
128
|
end
|
125
129
|
|
130
|
+
def batching_disabled?
|
131
|
+
opts_c.disable_nullify_batching.nil? ? opts_c.disable_batching : opts_c.disable_nullify_batching
|
132
|
+
end
|
133
|
+
|
126
134
|
def nullify_by_klass_column_and_ids klass, columns, ids
|
127
135
|
nullify_columns = {}
|
128
136
|
|
@@ -135,9 +143,15 @@ module BulkDependencyEraser
|
|
135
143
|
nullify_columns[columns] = nil
|
136
144
|
end
|
137
145
|
|
138
|
-
|
146
|
+
if batching_disabled?
|
139
147
|
nullify_in_db do
|
140
|
-
klass.unscoped.where(id:
|
148
|
+
klass.unscoped.where(id: ids).update_all(nullify_columns)
|
149
|
+
end
|
150
|
+
else
|
151
|
+
ids.each_slice(batch_size) do |ids_subset|
|
152
|
+
nullify_in_db do
|
153
|
+
klass.unscoped.where(id: ids_subset).update_all(nullify_columns)
|
154
|
+
end
|
141
155
|
end
|
142
156
|
end
|
143
157
|
end
|