bulk_dependency_eraser 1.2.2 → 1.3.2
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: 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
|