bulk_dependency_eraser 1.2.2 → 1.3.1

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: 80c6e41e13092670b6f67fc6cdb8d117c0222b0d2ce7c0043e1f12dcf01c1273
4
- data.tar.gz: 9b93602042b1863caa6a1eac272547d76559e7ba62e97e29831c5d171050eafe
3
+ metadata.gz: 60f1df6c3a60ce0e7d3048cdcb263986148ca7989464f90859455afea67eae78
4
+ data.tar.gz: 5069d8a435cc8ac13a2b4e873318c1c0a4ada041a5ed292b8df58212af3bc887
5
5
  SHA512:
6
- metadata.gz: 26e795e7ecf8727eacf36267502454478922430dc86424f6ef0483bb195a09dc1c52c47067c0a87d70f9235173715a975422548ebd50da7fb1cf70ccbc7f0721
7
- data.tar.gz: e6722b7ca1fd1c58f1b4a9d619af965eddb440c159bc2b9fd6efd436c2e927fd6e58679bb25f71438d24987aea8726ceb69a5ee081654e6cb8fbf4336d9c7dce
6
+ metadata.gz: 590c7dc30ca2d04140332d0c97128dcddbfb9a95e07d8a413006faa32011d3343fa5363e1a5a43f3678ae2b96f7ca9e44cdc23775f69a6971d576f000e4254c3
7
+ data.tar.gz: 6bff0c448928f611a0c8ce7f684df5b636d66bf0570c793ced8204eb759a1e0ad020e3947caa562610a5410f967b8ee4ef2bdc191aa37abd2e5ec47d497d11c6
@@ -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,37 @@ 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
+ query_ids = []
156
+ read_from_db do
157
+ # If the query has a limit, then we don't want to clobber with batching.
158
+ if batching_disabled? || !query.where({}).limit_value.nil?
159
+ # query without batching
160
+ query_ids = query.pluck(column)
161
+ else
162
+ # query with batching
163
+ offset = 0
164
+ loop do
165
+ new_query_ids = query.offset(offset).limit(batch_size).pluck(column)
166
+ query_ids += new_query_ids
167
+
168
+ break if new_query_ids.size < batch_size
169
+
170
+ # Move to the next batch
171
+ offset += batch_size
172
+ end
173
+ end
174
+ end
175
+
176
+ return query_ids
177
+ end
178
+
130
179
  def batch_size
131
- opts_c.read_batch_size || opts_c.batch_size
180
+ opts_c.read_batch_size.nil? ? opts_c.batch_size : opts_c.read_batch_size
181
+ end
182
+
183
+ def batching_disabled?
184
+ opts_c.disable_read_batching.nil? ? opts_c.disable_batching : opts_c.disable_read_batching
132
185
  end
133
186
 
134
187
  def deletion_query_parser query, association_parent = nil
@@ -176,13 +229,8 @@ module BulkDependencyEraser
176
229
  return
177
230
  end
178
231
 
179
- # Pluck IDs of the current query (batchified)
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
232
+ # Pluck IDs of the current query
233
+ query_ids = pluck_from_query(query)
186
234
 
187
235
  deletion_list[klass_name] ||= []
188
236
 
@@ -203,7 +251,7 @@ module BulkDependencyEraser
203
251
 
204
252
  # Hard to test if not sorted
205
253
  # - if we had more advanced rspec matches, we could do away with this.
206
- deletion_list[klass_name].sort! if Rails.env.test?
254
+ # deletion_list[klass_name].sort! if Rails.env.test?
207
255
 
208
256
  # ignore associations that aren't a dependent destroyable type
209
257
  destroy_associations = query.reflect_on_all_associations.select do |reflection|
@@ -370,12 +418,7 @@ module BulkDependencyEraser
370
418
  # - handle primary key edge cases
371
419
  # - The associations might not be using the primary_key of the klass table, but we can support that here.
372
420
  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
421
+ alt_primary_ids = pluck_from_query(query, specified_primary_key)
379
422
  assoc_query = assoc_query.where(specified_foreign_key.to_sym => alt_primary_ids)
380
423
  else
381
424
  assoc_query = assoc_query.where(specified_foreign_key.to_sym => query_ids)
@@ -397,12 +440,7 @@ module BulkDependencyEraser
397
440
  elsif type == :nullify
398
441
  # No need for recursion here.
399
442
  # - 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
443
+ assoc_ids = pluck_from_query(assoc_query)
406
444
 
407
445
  # No assoc_ids, no need to add it to the nullification list
408
446
  return if assoc_ids.none?
@@ -412,7 +450,7 @@ module BulkDependencyEraser
412
450
  nullification_list[assoc_klass_name][specified_foreign_key] += assoc_ids
413
451
  nullification_list[assoc_klass_name][specified_foreign_key].uniq!
414
452
 
415
- nullification_list[assoc_klass_name][specified_foreign_key].sort! if Rails.env.test?
453
+ # nullification_list[assoc_klass_name][specified_foreign_key].sort! if Rails.env.test?
416
454
 
417
455
  # Also nullify the 'type' field, if the association is polymorphic
418
456
  if specified_foreign_type
@@ -420,7 +458,7 @@ module BulkDependencyEraser
420
458
  nullification_list[assoc_klass_name][specified_foreign_type] += assoc_ids
421
459
  nullification_list[assoc_klass_name][specified_foreign_type].uniq!
422
460
 
423
- nullification_list[assoc_klass_name][specified_foreign_type].sort! if Rails.env.test?
461
+ # nullification_list[assoc_klass_name][specified_foreign_type].sort! if Rails.env.test?
424
462
  end
425
463
  else
426
464
  raise "invalid parsing type: #{type}"
@@ -508,23 +546,20 @@ module BulkDependencyEraser
508
546
  return
509
547
  end
510
548
 
511
- query.in_batches(of: batch_size) do |batch|
512
- assoc_batch_query = read_from_db do
513
- assoc_query.where(
514
- specified_primary_key.to_sym => batch.pluck(specified_foreign_key)
515
- )
516
- end
549
+ foreign_keys = pluck_from_query(query, specified_foreign_key)
550
+ assoc_query = assoc_query.where(
551
+ specified_primary_key.to_sym => foreign_keys
552
+ )
517
553
 
518
- if type == :delete
519
- # Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
520
- deletion_query_parser(assoc_batch_query, parent_class)
521
- elsif type == :restricted
522
- if traverse_restricted_dependency?(parent_class, reflection, assoc_batch_query)
523
- deletion_query_parser(assoc_batch_query, parent_class)
524
- end
525
- else
526
- raise "invalid parsing type: #{type}"
554
+ if type == :delete
555
+ # Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
556
+ deletion_query_parser(assoc_query, parent_class)
557
+ elsif type == :restricted
558
+ if traverse_restricted_dependency?(parent_class, reflection, assoc_query)
559
+ deletion_query_parser(assoc_query, parent_class)
527
560
  end
561
+ else
562
+ raise "invalid parsing type: #{type}"
528
563
  end
529
564
  end
530
565
 
@@ -539,6 +574,8 @@ module BulkDependencyEraser
539
574
  # In this case, it's like a `belongs_to :polymorphicable, polymorphic: true, dependent: :destroy` use-case
540
575
  # - it's unusual, but valid use-case
541
576
  def association_parser_belongs_to_polymorphic(parent_class, query, query_ids, association_name, type)
577
+ # raise "Unsupported use-case: #{parent_class.name} -> belongs_to :polymorphicable, polymorphic: true, dependent: :destroy"
578
+
542
579
  reflection = parent_class.reflect_on_association(association_name)
543
580
  reflection_type = reflection.class.name
544
581
 
@@ -576,30 +613,30 @@ module BulkDependencyEraser
576
613
  return
577
614
  end
578
615
 
579
- query.in_batches(of: batch_size) do |batch|
580
- foreign_ids_by_type = read_from_db do
581
- batch.pluck(specified_foreign_key, specified_foreign_type).each_with_object({}) do |(id, type), hash|
582
- hash.key?(type) ? hash[type] << id : hash[type] = [id]
583
- end
616
+ # Not sure how to limit/offset batch this right now.
617
+ # - it's a rare use-case, let's just leave this as a TODO:
618
+ foreign_ids_by_type = read_from_db do
619
+ query.pluck(specified_foreign_key, specified_foreign_type).each_with_object({}) do |(id, type), hash|
620
+ hash.key?(type) ? hash[type] << id : hash[type] = [id]
584
621
  end
622
+ end
585
623
 
586
- if type == :delete
624
+ if type == :delete
625
+ # Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
626
+ foreign_ids_by_type.each do |type, ids|
627
+ assoc_klass = type.constantize
628
+ deletion_query_parser(assoc_klass.where(id: ids), assoc_klass)
629
+ end
630
+ elsif type == :restricted
631
+ if traverse_restricted_dependency_for_belongs_to_poly?(parent_class, reflection, foreign_ids_by_type)
587
632
  # Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
588
633
  foreign_ids_by_type.each do |type, ids|
589
634
  assoc_klass = type.constantize
590
635
  deletion_query_parser(assoc_klass.where(id: ids), assoc_klass)
591
636
  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
637
  end
638
+ else
639
+ raise "invalid parsing type: #{type}"
603
640
  end
604
641
  end
605
642
 
@@ -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
- ids.each_slice(batch_size) do |ids_subset|
78
+ if batching_disabled?
79
+ puts "Deleting without batching" if opts_c.verbose
71
80
  delete_in_db do
72
- klass.unscoped.where(id: ids_subset).delete_all
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
- ids.each_slice(batch_size) do |ids_subset|
146
+ if batching_disabled?
139
147
  nullify_in_db do
140
- klass.unscoped.where(id: ids_subset).update_all(nullify_columns)
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
@@ -1,3 +1,3 @@
1
1
  module BulkDependencyEraser
2
- VERSION = "1.2.2".freeze
2
+ VERSION = "1.3.1".freeze
3
3
  end
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.2.2
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - benjamin.dana.software.dev@gmail.com