bulk_dependency_eraser 2.1.0 → 3.0.0

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: e4bda32570c3319238a5ea03f6a71cbae1e20015caf5fe7bdba78cab3c7883a7
4
- data.tar.gz: 9e371750cd1a05b54bfe724873daaf2c7e9aa66942f168d6417505a1427d46c1
3
+ metadata.gz: 954e4ce7b9c7d42a0cdcdc1403592dce3e61c6c196e72858a5c2f66f5be3e3df
4
+ data.tar.gz: 18233fefb36153a49d43d4aed40e3f44ca0f3123d346429a0a0b50a355577a16
5
5
  SHA512:
6
- metadata.gz: 33130d09c651a9757af826cbbd5e1363a8f136a3d6e3bf1e2a128dad776185b544fd75a493013cd28d279690094e90cc3dc0f3b23597704df9842eb80e215808
7
- data.tar.gz: 27f4e6014155961a1286f2d1f490a7ca2709fd2b9594a7c8ab3fb3164c26bbdf99e8609c5494e15e8595b0db9eee94d485a20a772f5a37d58554b497eaee5154
6
+ metadata.gz: e57be3e7ab4203123f919530710dd4f63842abbf588b49896fa78b2cb3fcf0c58d7e64e2881b165396cb919a897acfe3279d2c3d740a3c8f0ace8dfd923eb3c0
7
+ data.tar.gz: 3b05c7edc262d1cf0b663d451084698b52f2e11fb8ec03a057ac70551f630d35ad67530921f24535288adf1761377981e69803bb7519e8f4449977dab8ec56ca
@@ -1,5 +1,11 @@
1
+ require_relative 'utils'
2
+
1
3
  module BulkDependencyEraser
2
4
  class Base
5
+ POLY_KLASS_NAME = "<POLY>"
6
+ include BulkDependencyEraser::Utils
7
+ extend BulkDependencyEraser::Utils
8
+
3
9
  # Default Custom Scope for all classes, no effect.
4
10
  DEFAULT_SCOPE_WRAPPER = ->(query) { nil }
5
11
  # Default Custom Scope for mapped-by-name classes, no effect.
@@ -39,6 +45,32 @@ module BulkDependencyEraser
39
45
  proc_scopes_per_class_name: {},
40
46
  }.freeze
41
47
 
48
+ DEPENDENCY_NULLIFY = %i[
49
+ nullify
50
+ ].freeze
51
+
52
+ # Abort deletion if assoc dependency value is any of these.
53
+ # - exception if the :force_destroy_restricted option set true
54
+ DEPENDENCY_RESTRICT = %i[
55
+ restrict_with_error
56
+ restrict_with_exception
57
+ ].freeze
58
+
59
+ DEPENDENCY_DESTROY = (
60
+ %i[
61
+ destroy
62
+ delete_all
63
+ destroy_async
64
+ ] + self::DEPENDENCY_RESTRICT
65
+ ).freeze
66
+
67
+ DEPENDENCY_DESTROY_IGNORE_REFLECTION_TYPES = [
68
+ # Rails 6.1, when a has_and_delongs_to_many <assoc>, dependent: :destroy,
69
+ # will ignore the destroy. Will neither destroy the join table record nor the association record
70
+ # We will do the same, mirror the fuctionality, by ignoring any :dependent options on these types.
71
+ 'ActiveRecord::Reflection::HasAndBelongsToManyReflection'
72
+ ].freeze
73
+
42
74
  attr_reader :errors
43
75
 
44
76
  def initialize opts: {}
@@ -53,8 +85,46 @@ module BulkDependencyEraser
53
85
  raise NotImplementedError
54
86
  end
55
87
 
88
+ def report_error msg
89
+ # remove new lines, surrounding white space, replace with semicolon delimiters
90
+ n = msg.strip.gsub(/\s*\n\s*/, ' ')
91
+ @errors << n
92
+ end
93
+
94
+ def merge_errors errors, prefix = nil
95
+ local_errors = errors.dup
96
+
97
+ unless local_errors.any?
98
+ local_errors << '<NO ERRORS FOUND TO MERGE>'
99
+ end
100
+
101
+ if prefix
102
+ local_errors = errors.map { |error| prefix + error }
103
+ end
104
+ @errors += local_errors
105
+ end
106
+
56
107
  protected
57
108
 
109
+ def constantize(klass_name)
110
+ # circular dependencies have suffixes, shave them off
111
+ klass_name.sub(/\.\d+$/, '').constantize
112
+ end
113
+
114
+ # A dependent assoc may be through another association. Follow the throughs to find the correct assoc to destroy.
115
+ # @return [Symbol] - association's name
116
+ def find_root_association_from_through_assocs klass, association_name
117
+ reflection = klass.reflect_on_association(association_name)
118
+ options = reflection.options
119
+ if options.key?(:through)
120
+ return find_root_association_from_through_assocs(klass, options[:through])
121
+ else
122
+ association_name
123
+ end
124
+ end
125
+
126
+ # We're supporting custom query scopes on by klass name.
127
+ # - apply them here
58
128
  def custom_scope_for_query(query)
59
129
  klass = query.klass
60
130
  if opts_c.proc_scopes_per_class_name.key?(klass.name)
@@ -79,25 +149,6 @@ module BulkDependencyEraser
79
149
  ).freeze
80
150
  end
81
151
 
82
- def report_error msg
83
- # remove new lines, surrounding white space, replace with semicolon delimiters
84
- n = msg.strip.gsub(/\s*\n\s*/, ' ')
85
- @errors << n
86
- end
87
-
88
- def merge_errors errors, prefix = nil
89
- local_errors = errors.dup
90
-
91
- unless local_errors.any?
92
- local_errors << '<NO ERRORS FOUND TO MERGE>'
93
- end
94
-
95
- if prefix
96
- local_errors = errors.map { |error| prefix + error }
97
- end
98
- @errors += local_errors
99
- end
100
-
101
152
  def uniqify_errors!
102
153
  @errors.uniq!
103
154
  end
@@ -1,8 +1,22 @@
1
1
  module BulkDependencyEraser
2
2
  class Builder < Base
3
+ DEFAULT_DB_BUILD_ALL_WRAPPER = ->(builder, block) do
4
+ begin
5
+ block.call
6
+ rescue StandardError => e
7
+ builder.report_error(
8
+ <<~STRING.strip
9
+ Issue attempting to build deletion query for '#{e.building_klass_name}'
10
+ => #{e.original_error_klass.name}: #{e.message}
11
+ STRING
12
+ )
13
+ end
14
+ end
15
+
3
16
  DEFAULT_OPTS = {
4
17
  force_destroy_restricted: false,
5
18
  verbose: false,
19
+ db_build_all_wrapper: self::DEFAULT_DB_BUILD_ALL_WRAPPER,
6
20
  # Some associations scopes take parameters.
7
21
  # - We would have to instantiate if we wanted to apply that scope filter.
8
22
  instantiate_if_assoc_scope_with_arity: false,
@@ -34,35 +48,13 @@ module BulkDependencyEraser
34
48
  reading_proc_scopes_per_class_name: {},
35
49
  }.freeze
36
50
 
37
- DEPENDENCY_NULLIFY = %i[
38
- nullify
39
- ].freeze
40
-
41
- # Abort deletion if assoc dependency value is any of these.
42
- # - exception if the :force_destroy_restricted option set true
43
- DEPENDENCY_RESTRICT = %i[
44
- restrict_with_error
45
- restrict_with_exception
46
- ].freeze
47
-
48
- DEPENDENCY_DESTROY = (
49
- %i[
50
- destroy
51
- delete_all
52
- destroy_async
53
- ] + self::DEPENDENCY_RESTRICT
54
- ).freeze
55
-
56
- DEPENDENCY_DESTROY_IGNORE_REFLECTION_TYPES = [
57
- # Rails 6.1, when a has_and_delongs_to_many <assoc>, dependent: :destroy,
58
- # will ignore the destroy. Will neither destroy the join table record nor the association record
59
- # We will do the same, mirror the fuctionality, by ignoring any :dependent options on these types.
60
- 'ActiveRecord::Reflection::HasAndBelongsToManyReflection'
61
- ].freeze
62
-
63
51
  # write access so that these can be edited in-place by end-users who might need to manually adjust deletion order.
64
52
  attr_accessor :deletion_list, :nullification_list
65
53
  attr_reader :ignore_table_deletion_list, :ignore_table_nullification_list
54
+ attr_reader :query_schema_parser
55
+ attr_reader :current_klass_name
56
+
57
+ delegate :circular_dependency_klasses, :flat_dependencies_per_klass, to: :query_schema_parser
66
58
 
67
59
  def initialize query:, opts: {}
68
60
  @query = query
@@ -79,9 +71,17 @@ module BulkDependencyEraser
79
71
 
80
72
  @ignore_table_name_and_dependencies = opts_c.ignore_tables_and_dependencies.collect { |table_name| table_name }
81
73
  @ignore_klass_name_and_dependencies = opts_c.ignore_klass_names_and_dependencies.collect { |klass_name| klass_name }
74
+ @query_schema_parser = BulkDependencyEraser::QuerySchemaParser.new(query:, opts:)
75
+ # Moving pointer, points to the current class that is being queries
76
+ @current_klass_name = query.is_a?(ActiveRecord::Relation) ? query.klass.name : query.name
82
77
  end
83
78
 
84
79
  def execute
80
+ unless query_schema_parser.execute
81
+ merge_errors(full_schema_parser.errors, 'QuerySchemaParser: ')
82
+ return false
83
+ end
84
+
85
85
  # go through deletion/nullification lists and remove any tables from 'ignore_tables' option
86
86
  build_result = build
87
87
 
@@ -89,8 +89,9 @@ module BulkDependencyEraser
89
89
  # - prior approach was to use table_name.classify, but we can't trust that approach.
90
90
  opts_c.ignore_tables.each do |table_name|
91
91
  table_names_to_parsed_klass_names.dig(table_name)&.each do |klass_name|
92
- ignore_table_deletion_list[klass_name] = deletion_list.delete(klass_name) if deletion_list.key?(klass_name)
93
- ignore_table_nullification_list[klass_name] = nullification_list.delete(klass_name) if nullification_list.key?(klass_name)
92
+ klass_index = deletion_index_key(klass_name.constantize)
93
+ ignore_table_deletion_list[klass_index] = deletion_list.delete(klass_index) if deletion_list.key?(klass_index)
94
+ ignore_table_nullification_list[klass_index] = nullification_list.delete(klass_index) if nullification_list.key?(klass_index)
94
95
  end
95
96
  end
96
97
 
@@ -98,35 +99,20 @@ module BulkDependencyEraser
98
99
  end
99
100
 
100
101
  def build
101
- begin
102
- if opts_c.verbose
103
- puts "Starting build for #{@query.is_a?(ActiveRecord::Relation) ? @query.klass.name : @query.name}"
104
- end
102
+ build_all_in_db do
103
+ begin
104
+ if opts_c.verbose
105
+ puts "Starting build for #{@query.is_a?(ActiveRecord::Relation) ? @query.klass.name : @query.name}"
106
+ end
105
107
 
106
- deletion_query_parser(@query)
108
+ deletion_query_parser(@query)
107
109
 
108
- uniqify_errors!
110
+ uniqify_errors!
109
111
 
110
- return errors.none?
111
- rescue StandardError => e
112
- if @query.is_a?(ActiveRecord::Relation)
113
- klass = @query.klass
114
- klass_name = @query.klass.name
115
- else
116
- # current_query is a normal rails class
117
- klass = @query
118
- klass_name = @query.name
112
+ return errors.none?
113
+ rescue StandardError => e
114
+ raise BulkDependencyEraser::Errors::BuilderError.new(e.class, e.message, building_klass_name: current_klass_name)
119
115
  end
120
- report_error(
121
- "
122
- Error Encountered in 'execute' for '#{klass_name}':
123
- #{e.class.name}
124
- #{e.message}
125
- "
126
- )
127
- raise e
128
-
129
- return false
130
116
  end
131
117
  end
132
118
 
@@ -145,7 +131,8 @@ module BulkDependencyEraser
145
131
  end
146
132
  end
147
133
 
148
- def pluck_from_query query, column = :id
134
+ def pluck_from_query(query, column = :id)
135
+ set_current_klass_name(query)
149
136
  # ordering shouldn't matter in these queries, and would slow it down
150
137
  # - we're ignoring default_scope ordering, but assoc-defined ordering would still take effect
151
138
  query = query.reorder('')
@@ -190,17 +177,15 @@ module BulkDependencyEraser
190
177
 
191
178
  if query.is_a?(ActiveRecord::Relation)
192
179
  klass = query.klass
193
- klass_name = query.klass.name
194
180
  else
195
181
  # current_query is a normal rails class
196
182
  klass = query
197
- klass_name = query.name
198
183
  end
199
184
 
200
185
  table_names_to_parsed_klass_names[klass.table_name] ||= []
201
186
  # Need to populate this list here, so we can have access to it later for the :ignore_tables option
202
- unless table_names_to_parsed_klass_names[klass.table_name].include?(klass_name)
203
- table_names_to_parsed_klass_names[klass.table_name] << klass_name
187
+ unless table_names_to_parsed_klass_names[klass.table_name].include?(klass.name)
188
+ table_names_to_parsed_klass_names[klass.table_name] << klass.name
204
189
  end
205
190
 
206
191
  if ignore_table_name_and_dependencies.include?(klass.table_name)
@@ -208,22 +193,22 @@ module BulkDependencyEraser
208
193
  return
209
194
  end
210
195
 
211
- if ignore_klass_name_and_dependencies.include?(klass_name)
196
+ if ignore_klass_name_and_dependencies.include?(klass.name)
212
197
  # Not parsing, table and dependencies ignorable
213
198
  return
214
199
  end
215
200
 
216
201
  if opts_c.verbose
217
202
  if association_parent
218
- puts "Building #{association_parent}, association of #{klass_name}"
203
+ puts "Building #{association_parent}, association of #{klass.name}"
219
204
  else
220
- puts "Building #{klass_name}"
205
+ puts "Building #{klass.name}"
221
206
  end
222
207
  end
223
208
 
224
209
  if klass.primary_key != 'id'
225
210
  report_error(
226
- "#{klass_name} - does not use primary_key 'id'. Cannot use this tool to bulk delete."
211
+ "#{klass.name} - does not use primary_key 'id'. Cannot use this tool to bulk delete."
227
212
  )
228
213
  return
229
214
  end
@@ -231,26 +216,22 @@ module BulkDependencyEraser
231
216
  # Pluck IDs of the current query
232
217
  query_ids = pluck_from_query(query)
233
218
 
234
- deletion_list[klass_name] ||= []
219
+ klass_index = initialize_deletion_list_for_klass(klass)
235
220
 
236
221
  # prevent infinite recursion here.
237
222
  # - Remove any IDs that have been processed before
238
- query_ids = query_ids - deletion_list[klass_name]
223
+ query_ids = remove_already_deletion_processed_ids(klass, query_ids)
239
224
 
240
225
  # If ids are nil, let's find that error
241
226
  if query_ids.none? #|| query_ids.nil?
242
227
  # quick cleanup, if turns out was an empty class
243
- deletion_list.delete(klass_name) if deletion_list[klass_name].none?
228
+ deletion_list.delete(klass_index) if deletion_list[klass_index].none?
244
229
  return
245
230
  end
246
231
 
247
232
  # Use-case: We have more IDs to process
248
233
  # - can now safely add to the list, since we've prevented infinite recursion
249
- deletion_list[klass_name] += query_ids
250
-
251
- # Hard to test if not sorted
252
- # - if we had more advanced rspec matches, we could do away with this.
253
- # deletion_list[klass_name].sort! if Rails.env.test?
234
+ deletion_list[klass_index] += query_ids
254
235
 
255
236
  # ignore associations that aren't a dependent destroyable type
256
237
  destroy_associations = query.reflect_on_all_associations.select do |reflection|
@@ -317,6 +298,7 @@ module BulkDependencyEraser
317
298
  is_polymorphic = reflection.options[:polymorphic]
318
299
  unless is_polymorphic
319
300
  klass = reflection.klass
301
+ set_current_klass_name(reflection.klass)
320
302
 
321
303
  if ignore_table_name_and_dependencies.include?(klass.table_name)
322
304
  # Not parsing, table and dependencies ignorable
@@ -446,15 +428,12 @@ module BulkDependencyEraser
446
428
  nullification_list[assoc_klass_name][specified_foreign_key] += assoc_ids
447
429
  nullification_list[assoc_klass_name][specified_foreign_key].uniq!
448
430
 
449
- # nullification_list[assoc_klass_name][specified_foreign_key].sort! if Rails.env.test?
450
431
 
451
432
  # Also nullify the 'type' field, if the association is polymorphic
452
433
  if specified_foreign_type
453
434
  nullification_list[assoc_klass_name][specified_foreign_type] ||= []
454
435
  nullification_list[assoc_klass_name][specified_foreign_type] += assoc_ids
455
436
  nullification_list[assoc_klass_name][specified_foreign_type].uniq!
456
-
457
- # nullification_list[assoc_klass_name][specified_foreign_type].sort! if Rails.env.test?
458
437
  end
459
438
  else
460
439
  raise "invalid parsing type: #{type}"
@@ -472,7 +451,6 @@ module BulkDependencyEraser
472
451
  assoc_klass = reflection.klass
473
452
  assoc_klass_name = assoc_klass.name
474
453
 
475
-
476
454
  # specified_primary_key = reflection.options[:primary_key]&.to_s
477
455
  # specified_foreign_key = reflection.options[:foreign_key]&.to_s
478
456
 
@@ -659,17 +637,6 @@ module BulkDependencyEraser
659
637
  end
660
638
  end
661
639
 
662
- # A dependent assoc may be through another association. Follow the throughs to find the correct assoc to destroy.
663
- def find_root_association_from_through_assocs klass, association_name
664
- reflection = klass.reflect_on_association(association_name)
665
- options = reflection.options
666
- if options.key?(:through)
667
- return find_root_association_from_through_assocs(klass, options[:through])
668
- else
669
- association_name
670
- end
671
- end
672
-
673
640
  # return [Boolean]
674
641
  # - true if valid
675
642
  # - false if not valid
@@ -711,5 +678,82 @@ module BulkDependencyEraser
711
678
  puts "Reading from DB..." if opts_c.verbose
712
679
  opts_c.db_read_wrapper.call(block)
713
680
  end
681
+
682
+ def remove_already_deletion_processed_ids(klass, new_ids)
683
+ already_processed_ids = []
684
+
685
+ if is_a_circular_dependency_klass?(klass)
686
+ klass_keys = find_circular_dependency_deletion_keys(klass)
687
+ klass_keys.each do |circular_class_key|
688
+ already_processed_ids += deletion_list[circular_class_key]
689
+ end
690
+ else
691
+ already_processed_ids = deletion_list[klass.name]
692
+ end
693
+
694
+ new_ids - already_processed_ids
695
+ end
696
+
697
+ # Initializes deletion_list index
698
+ # - increments the index for circular dependencies
699
+ def initialize_deletion_list_for_klass(klass)
700
+ klass_index = deletion_index_key(klass, increment_circular_index: true)
701
+
702
+ if is_a_circular_dependency_klass?(klass)
703
+ raise "circular_index already existed for klass: #{klass.name}" if deletion_list.key?(klass_index)
704
+
705
+ deletion_list[klass_index] = []
706
+ else
707
+ # Not a circular dependency, define as normal
708
+ deletion_list[klass_index] ||= []
709
+ end
710
+
711
+ klass_index
712
+ end
713
+
714
+ def is_a_circular_dependency_klass?(klass)
715
+ circular_dependency_klasses.values.flatten.include?(klass.name)
716
+ end
717
+
718
+ def find_circular_dependency_deletion_keys(klass)
719
+ escaped_prefix = Regexp.escape(klass.name)
720
+ # Define the key-matching regex: klass.name + '.' + <integer>
721
+ regex = /^#{escaped_prefix}\.\d+$/
722
+ # find the latest, indexed klass initialization
723
+ deletion_list.keys.select { |key| key.match?(regex) }
724
+ end
725
+
726
+ # If circular dependency, append a index suffix to the deletion hash key
727
+ # - they will be deleted in highest index to lowest index order.
728
+ def deletion_index_key(klass, increment_circular_index: false)
729
+ if is_a_circular_dependency_klass?(klass)
730
+ klass_keys = find_circular_dependency_deletion_keys(klass)
731
+ if klass_keys.none?
732
+ klass_index = "#{klass.name}.0" # first one, no need to consider increment
733
+ else
734
+ # Use map to extract the integers using a regular expression
735
+ circular_indexes = klass_keys.map { |s| s[/\.(\d+)$/, 1].to_i }
736
+ # Find the maximum value
737
+ current_circular_index = circular_indexes.max
738
+ current_circular_index += 1 if increment_circular_index
739
+ klass_index = "#{klass.name}.#{current_circular_index}"
740
+ end
741
+ else
742
+ klass_index = klass.name
743
+ end
744
+
745
+ return klass_index
746
+ end
747
+
748
+ def build_all_in_db(&block)
749
+ puts "Building all from DB..." if opts_c.verbose
750
+ opts_c.db_build_all_wrapper.call(self, block)
751
+ puts "Building all from DB complete." if opts_c.verbose
752
+ end
753
+
754
+ def set_current_klass_name(query_or_klass)
755
+ klass = query_or_klass.is_a?(ActiveRecord::Relation) ? query_or_klass.klass : query_or_klass
756
+ @current_klass_name = klass.name
757
+ end
714
758
  end
715
759
  end
@@ -1,10 +1,15 @@
1
1
  module BulkDependencyEraser
2
2
  class Deleter < Base
3
- DEFAULT_DB_DELETE_ALL_WRAPPER = ->(block) do
3
+ DEFAULT_DB_DELETE_ALL_WRAPPER = ->(deleter, block) do
4
4
  begin
5
5
  block.call
6
- rescue StandardError => e
7
- report_error("Issue attempting to delete '#{current_class_name}': #{e.class.name} - #{e.message}")
6
+ rescue BulkDependencyEraser::Errors::DeleterError => e
7
+ deleter.report_error(
8
+ <<~STRING.strip
9
+ Issue attempting to delete klass '#{e.deleting_klass_name}'
10
+ => #{e.original_error_klass.name}: #{e.message}
11
+ STRING
12
+ )
8
13
  end
9
14
  end
10
15
 
@@ -60,23 +65,27 @@ module BulkDependencyEraser
60
65
 
61
66
  current_class_name = 'N/A'
62
67
  delete_all_in_db do
63
- class_names_and_ids.keys.reverse.each do |class_name|
64
- current_class_name = class_name
65
- ids = class_names_and_ids[class_name].reverse
66
- klass = class_name.constantize
68
+ begin
69
+ class_names_and_ids.keys.reverse.each do |class_name|
70
+ current_class_name = class_name
71
+ ids = class_names_and_ids[class_name].reverse
72
+ klass = constantize(class_name)
67
73
 
68
- if opts_c.enable_invalid_foreign_key_detection
69
- # delete with referential integrity
70
- delete_by_klass_and_ids(klass, ids)
71
- else
72
- # delete without referential integrity
73
- # Disable any ActiveRecord::InvalidForeignKey raised errors.
74
- # - src: https://stackoverflow.com/questions/41005849/rails-migrations-temporarily-ignore-foreign-key-constraint
75
- # https://apidock.com/rails/ActiveRecord/ConnectionAdapters/AbstractAdapter/disable_referential_integrity
76
- ActiveRecord::Base.connection.disable_referential_integrity do
74
+ if opts_c.enable_invalid_foreign_key_detection
75
+ # delete with referential integrity
77
76
  delete_by_klass_and_ids(klass, ids)
77
+ else
78
+ # delete without referential integrity
79
+ # Disable any ActiveRecord::InvalidForeignKey raised errors.
80
+ # - src: https://stackoverflow.com/questions/41005849/rails-migrations-temporarily-ignore-foreign-key-constraint
81
+ # https://apidock.com/rails/ActiveRecord/ConnectionAdapters/AbstractAdapter/disable_referential_integrity
82
+ ActiveRecord::Base.connection.disable_referential_integrity do
83
+ delete_by_klass_and_ids(klass, ids)
84
+ end
78
85
  end
79
86
  end
87
+ rescue StandardError => e
88
+ raise BulkDependencyEraser::Errors::DeleterError.new(e.class, e.message, deleting_klass_name: current_class_name)
80
89
  end
81
90
  end
82
91
 
@@ -140,7 +149,7 @@ module BulkDependencyEraser
140
149
 
141
150
  def delete_all_in_db(&block)
142
151
  puts "Deleting all from DB..." if opts_c.verbose
143
- opts_c.db_delete_all_wrapper.call(block)
152
+ opts_c.db_delete_all_wrapper.call(self, block)
144
153
  puts "Deleting all from DB complete." if opts_c.verbose
145
154
  end
146
155
  end
@@ -0,0 +1,5 @@
1
+ module BulkDependencyEraser
2
+ module Errors
3
+ class BaseError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ module BulkDependencyEraser
2
+ module Errors
3
+ class BuilderError < BaseError
4
+ attr_reader :original_error_klass, :building_klass_name
5
+
6
+ def initialize(original_error_klass, message, building_klass_name:)
7
+ @original_error_klass = original_error_klass
8
+ @building_klass_name = building_klass_name
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module BulkDependencyEraser
2
+ module Errors
3
+ class DeleterError < BaseError
4
+ attr_reader :original_error_klass, :deleting_klass_name
5
+
6
+ def initialize(original_error_klass, message, deleting_klass_name:)
7
+ @original_error_klass = original_error_klass
8
+ @deleting_klass_name = deleting_klass_name
9
+ super(message)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module BulkDependencyEraser
2
+ module Errors
3
+ class NullifierError < BaseError
4
+ attr_reader :original_error_klass, :nullifying_klass_name, :nullifying_columns
5
+
6
+ def initialize(original_error_klass, message, nullifying_klass_name:, nullifying_columns:)
7
+ @original_error_klass = original_error_klass
8
+ @nullifying_klass_name = nullifying_klass_name
9
+ @nullifying_columns = nullifying_columns
10
+ super(message)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,179 @@
1
+ module BulkDependencyEraser
2
+ # Create a flat map hash for each class that lists every dependency.
3
+ class FullSchemaParser < Base
4
+ DEFAULT_OPTS = {
5
+ verbose: false,
6
+ }
7
+
8
+ attr_reader :flat_dependencies_per_klass
9
+
10
+ @cached_flat_dependencies_per_klass = nil
11
+ def self.reset_cache
12
+ @cached_flat_dependencies_per_klass = nil
13
+ end
14
+ def self.set_cache(value)
15
+ @cached_flat_dependencies_per_klass = value.freeze
16
+ end
17
+ def self.get_cache
18
+ @cached_flat_dependencies_per_klass
19
+ end
20
+
21
+ def initialize(opts: {})
22
+ # @flat_dependencies_per_klass Structure
23
+ # {
24
+ # <class_name> => {
25
+ # has_dependencies: <Boolean>,
26
+ # foreign_keys: {
27
+ # <column_name>: <association_class_name>,
28
+ # ...
29
+ # },
30
+ # nullify_dependencies: {
31
+ # <association_name>: <association_class_name>,
32
+ # ...
33
+ # },
34
+ # destroy_dependencies: {
35
+ # <association_name>: <association_class_name>,
36
+ # ...
37
+ # }
38
+ # }
39
+ # }
40
+ @flat_dependencies_per_klass = {}
41
+ super(opts:)
42
+ end
43
+
44
+ def execute
45
+ unless self.class.get_cache.nil?
46
+ @flat_dependencies_per_klass = self.class.get_cache
47
+ return true
48
+ end
49
+
50
+ Rails.application.eager_load!
51
+
52
+ ActiveRecord::Base.descendants.each do |model|
53
+ begin
54
+ next if model.abstract_class? # Skip abstract classes like ApplicationRecord
55
+ next unless model.connection.table_exists?(model.table_name)
56
+ rescue Exception => e
57
+ report_error("EXECPTION ON #{model.name}; #{e.class}: #{e.message}")
58
+ next
59
+ end
60
+
61
+ flat_dependencies_parser(model)
62
+ end
63
+
64
+ deep_freeze(@flat_dependencies_per_klass)
65
+ self.class.set_cache(@flat_dependencies_per_klass)
66
+
67
+ return true
68
+ end
69
+
70
+ def reset
71
+ @flat_dependencies_per_klass = {}
72
+ self.class.reset_cache
73
+ end
74
+
75
+ protected
76
+
77
+ # @param klass [ActiveRecord::Base]
78
+ # @param dependency_path [Array<ActiveRecord::Base>] - previously parsed klasses
79
+ def flat_dependencies_parser klass
80
+ raise "invalid klass: #{klass}" unless klass < ActiveRecord::Base
81
+
82
+ if flat_dependencies_per_klass.include?(klass.name)
83
+ raise "@dependencies_per_klass already contains #{klass.name}"
84
+ end
85
+
86
+ @flat_dependencies_per_klass[klass.name] ||= {
87
+ nullify_dependencies: {},
88
+ destroy_dependencies: {},
89
+ has_many: {},
90
+ belongs_to: {},
91
+ }
92
+
93
+ # We're including :restricted dependencies
94
+ destroy_associations = klass.reflect_on_all_associations.select do |reflection|
95
+ dependency_type = reflection.options&.dig(:dependent)
96
+ dependency_type.to_sym if dependency_type.is_a?(String)
97
+ DEPENDENCY_DESTROY.include?(dependency_type)
98
+ end
99
+
100
+ nullify_associations = klass.reflect_on_all_associations.select do |reflection|
101
+ dependency_type = reflection.options&.dig(:dependent)
102
+ dependency_type.to_sym if dependency_type.is_a?(String)
103
+ DEPENDENCY_NULLIFY.include?(dependency_type)
104
+ end
105
+
106
+ # Iterate through the assoc names, if there are any :through assocs, then rename the association
107
+ # - Rails interpretation of any dependencies of a :through association is to apply it to
108
+ # the leaf association at the end of the :through chain(s)
109
+ destroy_association_names = destroy_associations.map(&:name).collect do |assoc_name|
110
+ find_root_association_from_through_assocs(klass, assoc_name)
111
+ end
112
+ nullify_association_names = nullify_associations.map(&:name).collect do |assoc_name|
113
+ find_root_association_from_through_assocs(klass, assoc_name)
114
+ end
115
+
116
+ destroy_association_names.uniq.each do |association_name|
117
+ add_deletion_dependency_to_flat_map(klass, association_name)
118
+ end
119
+
120
+ nullify_association_names.uniq.each do |association_name|
121
+ add_nullification_dependency_to_flat_map(klass, association_name)
122
+ end
123
+
124
+ # add has_many relationships
125
+ (
126
+ klass.reflect_on_all_associations(:has_many) +
127
+ klass.reflect_on_all_associations(:has_one) +
128
+ klass.reflect_on_all_associations(:has_and_belongs_to_many)
129
+ ).each do |reflection|
130
+ next if reflection.options[:through].present?
131
+
132
+ add_has_many_to_flat_map(klass, reflection)
133
+ end
134
+
135
+ # add belongs_to relationships
136
+ klass.reflect_on_all_associations(:belongs_to).each do |reflection|
137
+ next if reflection.options[:through].present?
138
+
139
+ add_belongs_to_to_flat_map(klass, reflection)
140
+ end
141
+ end
142
+
143
+ # @param klass [ActiveRecord::Base]
144
+ # @param association_name [Symbol] - name of the association
145
+ def add_has_many_to_flat_map(klass, reflection)
146
+ association_name = reflection.name
147
+ @flat_dependencies_per_klass[klass.name][:has_many][association_name] = reflection.klass.name
148
+ end
149
+
150
+ # @param klass [ActiveRecord::Base]
151
+ # @param association_name [Symbol] - name of the association
152
+ def add_belongs_to_to_flat_map(klass, reflection)
153
+ association_name = reflection.name
154
+ reflection_klass_name = is_reflection_polymorphic?(reflection) ? POLY_KLASS_NAME : reflection.klass.name
155
+ @flat_dependencies_per_klass[klass.name][:belongs_to][association_name] = reflection_klass_name
156
+ end
157
+
158
+ # @param klass [ActiveRecord::Base]
159
+ # @param association_name [Symbol] - name of the association
160
+ def add_deletion_dependency_to_flat_map(klass, association_name)
161
+ reflection = klass.reflect_on_association(association_name)
162
+ reflection_klass_name = is_reflection_polymorphic?(reflection) ? POLY_KLASS_NAME : reflection.klass.name
163
+ @flat_dependencies_per_klass[klass.name][:destroy_dependencies][association_name] = reflection_klass_name
164
+ end
165
+
166
+ # @param klass [ActiveRecord::Base]
167
+ # @param association_name [Symbol] - name of the association
168
+ def add_nullification_dependency_to_flat_map(klass, association_name)
169
+ reflection = klass.reflect_on_association(association_name)
170
+ # nullifications can't be poly klass
171
+ @flat_dependencies_per_klass[klass.name][:nullify_dependencies][association_name] = reflection.klass.name
172
+ end
173
+
174
+ # @param reflection [ActiveRecord::Reflection::AssociationReflection]
175
+ def is_reflection_polymorphic?(reflection)
176
+ reflection.options&.dig(:polymorphic) == true
177
+ end
178
+ end
179
+ end
@@ -9,6 +9,7 @@ module BulkDependencyEraser
9
9
 
10
10
  delegate :nullification_list, :deletion_list, to: :dependency_builder
11
11
  delegate :ignore_table_deletion_list, :ignore_table_nullification_list, to: :dependency_builder
12
+ delegate :circular_dependency_klasses, :flat_dependencies_per_klass, to: :dependency_builder
12
13
 
13
14
  # @param query [ActiveRecord::Base | ActiveRecord::Relation]
14
15
  def initialize query:, opts: {}
@@ -1,10 +1,15 @@
1
1
  module BulkDependencyEraser
2
2
  class Nullifier < Base
3
- DEFAULT_DB_NULLIFY_ALL_WRAPPER = lambda do |block|
3
+ DEFAULT_DB_NULLIFY_ALL_WRAPPER = ->(nullifier, block) do
4
4
  begin
5
5
  block.call
6
- rescue StandardError => e
7
- report_error("Issue attempting to nullify '#{current_class_name}' column '#{current_column}': #{e.class.name} - #{e.message}")
6
+ rescue BulkDependencyEraser::Errors::NullifierError => e
7
+ nullifier.report_error(
8
+ <<~STRING.strip
9
+ Issue attempting to nullify klass '#{e.nullifying_klass_name}' on column(s) '#{e.nullifying_columns}'
10
+ => #{e.original_error_klass.name}: #{e.message}
11
+ STRING
12
+ )
8
13
  end
9
14
  end
10
15
 
@@ -114,6 +119,7 @@ module BulkDependencyEraser
114
119
  current_class_name = 'N/A'
115
120
  current_column = 'N/A'
116
121
  nullify_all_in_db do
122
+ begin
117
123
  # column_and_ids should have already been reversed in builder
118
124
  class_names_columns_and_ids.keys.reverse.each do |class_name|
119
125
  current_class_name = class_name
@@ -139,6 +145,14 @@ module BulkDependencyEraser
139
145
  end
140
146
  end
141
147
  end
148
+ rescue StandardError => e
149
+ raise BulkDependencyEraser::Errors::NullifierError.new(
150
+ e.class,
151
+ e.message,
152
+ nullifying_klass_name: current_class_name,
153
+ nullifying_columns: current_column.to_s # could be an array, string, or symbol
154
+ )
155
+ end
142
156
  end
143
157
 
144
158
  return errors.none?
@@ -211,7 +225,7 @@ module BulkDependencyEraser
211
225
 
212
226
  def nullify_all_in_db(&block)
213
227
  puts "Nullifying all from DB..." if opts_c.verbose
214
- opts_c.db_nullify_all_wrapper.call(block)
228
+ opts_c.db_nullify_all_wrapper.call(self, block)
215
229
  puts "Nullifying all from DB complete." if opts_c.verbose
216
230
  end
217
231
 
@@ -0,0 +1,281 @@
1
+ module BulkDependencyEraser
2
+ class QuerySchemaParser < Base
3
+ DEFAULT_OPTS = {
4
+ verbose: false,
5
+ # Some associations scopes take parameters.
6
+ # - We would have to instantiate if we wanted to apply that scope filter.
7
+ instantiate_if_assoc_scope_with_arity: false,
8
+ force_destroy_restricted: false,
9
+ }
10
+
11
+ # attr_accessor :deletion_list, :nullification_list
12
+ attr_reader :initial_class
13
+ attr_reader :dependencies_per_klass
14
+ attr_reader :circular_dependency_klasses
15
+ attr_reader :full_schema_parser
16
+
17
+ delegate :flat_dependencies_per_klass, to: :full_schema_parser
18
+
19
+ def initialize query:, opts: {}
20
+ if query.is_a?(ActiveRecord::Relation)
21
+ @initial_class = query.klass
22
+ else
23
+ # current_query is a normal rails class
24
+ @initial_class = query
25
+ end
26
+ # @dependencies_per_klass Structure
27
+ # {
28
+ # <ActiveRecord::Base> => {
29
+ # <ActiveRecord::Reflection::AssociationReflection> => <ActiveRecord::Base>
30
+ # }
31
+ # }
32
+ @dependencies_per_klass = {}
33
+ # @circular_dependency_klasses Structure
34
+ # {
35
+ # <ActiveRecord::Base> => [
36
+ # # Path of dependencies that start and end with the key class
37
+ # <ActiveRecord::Base>,
38
+ # <ActiveRecord::Base>,
39
+ # <ActiveRecord::Base>,
40
+ # ]
41
+ # }
42
+ @circular_dependency_klasses = {}
43
+ @full_schema_parser = BulkDependencyEraser::FullSchemaParser.new(opts:)
44
+ super(opts:)
45
+ end
46
+
47
+ def execute
48
+ unless full_schema_parser.execute
49
+ merge_errors(full_schema_parser.errors, 'FullSchemaParser: ')
50
+ return false
51
+ end
52
+ klass_dependencies_parser(initial_class, klass_action: :destroy)
53
+
54
+ @dependencies_per_klass.each do |key, values|
55
+ @dependencies_per_klass[key] = values.uniq
56
+ end
57
+
58
+ return true
59
+ end
60
+
61
+ # @param klass [ActiveRecord::Base, Array<ActiveRecord::Base>]
62
+ # - if was a dependency from a polymophic class, then iterate through the klasses.
63
+ # @param dependency_path [Array<ActiveRecord::Base>] - previously parsed klasses
64
+ def klass_dependencies_parser klass, klass_action:, dependency_path: []
65
+ if klass.is_a?(Array)
66
+ klass.each do |klass_subset|
67
+ klass_dependencies_parser(klass_subset, klass_action:, dependency_path:)
68
+ end
69
+ return
70
+ end
71
+
72
+ unless DEPENDENCY_DESTROY.include?(klass_action) || DEPENDENCY_NULLIFY.include?(klass_action)
73
+ raise "invalid klass action: #{klass_action}"
74
+ end
75
+ raise "invalid klass: #{klass}" unless klass < ActiveRecord::Base
76
+
77
+ # Not a circular dependency if the repetitious klass has a nullify action.
78
+ if DEPENDENCY_DESTROY.include?(klass_action) && dependency_path.include?(klass.name)
79
+ index = dependency_path.index(klass.name)
80
+ circular_dependency = dependency_path[index..] + [klass.name]
81
+ circular_dependency_klasses[klass.name] = circular_dependency
82
+ return
83
+ end
84
+
85
+ # We don't need to consider dependencies for a klass that is being nullified.
86
+ return if DEPENDENCY_NULLIFY.include?(klass_action)
87
+
88
+ # already parsed, doesn't need to be parsed again.
89
+ return if dependencies_per_klass.include?(klass.name)
90
+
91
+ @dependencies_per_klass[klass.name] = []
92
+
93
+ # We're including :restricted dependencies
94
+ destroy_associations = klass.reflect_on_all_associations.select do |reflection|
95
+ dependency_type = reflection.options&.dig(:dependent)&.to_sym
96
+ DEPENDENCY_DESTROY.include?(dependency_type)
97
+ end
98
+
99
+ nullify_associations = klass.reflect_on_all_associations.select do |reflection|
100
+ dependency_type = reflection.options&.dig(:dependent)&.to_sym
101
+ DEPENDENCY_NULLIFY.include?(dependency_type)
102
+ end
103
+
104
+ # Iterate through the assoc names, if there are any :through assocs, then rename the association
105
+ # - Rails interpretation of any dependencies of a :through association is to apply it to
106
+ # the leaf association at the end of the :through chain(s)
107
+ association_dependencies = {}
108
+ (
109
+ destroy_associations.map(&:name) +
110
+ nullify_associations.map(&:name)
111
+ ).collect do |assoc_name|
112
+ root_association_name = find_root_association_from_through_assocs(klass, assoc_name)
113
+ association_dependencies[root_association_name] = klass.reflect_on_association(assoc_name).options.dig(:dependent)
114
+ end
115
+
116
+ # Using association names as keys helps remove duplicates - from dependent options on through associations and root associations.
117
+ association_dependencies.each do |association_name, dependency_type|
118
+ association_parser(klass, association_name, dependency_type, dependency_path)
119
+ end
120
+ end
121
+
122
+ # Used to iterate through each destroyable association, and recursively call 'deletion_query_parser'.
123
+ # @param parent_class [ApplicationRecord]
124
+ # @param association_name [Symbol] - The association name from the parent_class
125
+ def association_parser(parent_class, association_name, dependency_type, dependency_path)
126
+ reflection = parent_class.reflect_on_association(association_name)
127
+ reflection_type = reflection.class.name
128
+
129
+ raise "No dependency set for #{parent_class} and it's association: #{association_name}" unless dependency_type
130
+
131
+ case reflection_type
132
+ when 'ActiveRecord::Reflection::HasManyReflection'
133
+ association_parser_has_many(parent_class, association_name, dependency_type, dependency_path)
134
+ when 'ActiveRecord::Reflection::HasOneReflection'
135
+ association_parser_has_many(parent_class, association_name, dependency_type, dependency_path)
136
+ when 'ActiveRecord::Reflection::BelongsToReflection'
137
+ association_parser_belongs_to(parent_class, association_name, dependency_type, dependency_path)
138
+ else
139
+ report_message("Unsupported association type for #{parent_class.name}'s association '#{association_name}': #{reflection_type}")
140
+ end
141
+ end
142
+
143
+ # Handles the :has_many association type
144
+ # - handles it's polymorphic associations internally (easier on the has_many)
145
+ def association_parser_has_many(parent_class, association_name, dependency_type, dependency_path)
146
+ raise "parent_class missing" unless parent_class
147
+ raise "#{parent_class} - association_name: missing" unless association_name
148
+ raise "#{parent_class} - dependency_type: missing" unless dependency_type
149
+ raise "#{parent_class} - dependency_path: nil" if dependency_path.nil?
150
+
151
+ reflection = parent_class.reflect_on_association(association_name)
152
+ reflection_type = reflection.class.name
153
+
154
+ assoc_klass = reflection.klass
155
+ assoc_klass_name = assoc_klass.name
156
+ @dependencies_per_klass[parent_class.name] << assoc_klass.name
157
+
158
+ # If there is an association scope present, check to see how many parameters it's using
159
+ # - if there's any parameter, we have to either skip it or instantiate it to find it's dependencies.
160
+ if reflection.scope&.arity&.nonzero? && opts_c.instantiate_if_assoc_scope_with_arity == false
161
+ report_error(
162
+ "#{parent_class.name} and '#{association_name}' - scope has instance parameters. Use :instantiate_if_assoc_scope_with_arity option?"
163
+ )
164
+ return
165
+ end
166
+
167
+ # Look for manually specified keys in the assocation first
168
+ specified_primary_key = reflection.options[:primary_key]&.to_s
169
+ specified_foreign_key = reflection.options[:foreign_key]&.to_s
170
+ # For polymorphic_associations
171
+ specified_foreign_type = nil
172
+
173
+ # handle foreign_key edge cases
174
+ if specified_foreign_key.nil?
175
+ if reflection.options[:as]
176
+ specified_foreign_type = "#{reflection.options[:as]}_type"
177
+ specified_foreign_key = "#{reflection.options[:as]}_id"
178
+ else
179
+ specified_foreign_key = parent_class.table_name.singularize + "_id"
180
+ end
181
+ end
182
+
183
+ # Check to see if foreign_key exists in association class's table
184
+ unless assoc_klass.column_names.include?(specified_foreign_key)
185
+ report_error(
186
+ "
187
+ For '#{assoc_klass.name}': Could not determine the assoc's foreign key.
188
+ Foreign key should have been '#{specified_foreign_key}', but did not exist on the #{assoc_klass.table_name} table.
189
+ "
190
+ )
191
+ return
192
+ end
193
+
194
+ unless specified_foreign_type.nil? || assoc_klass.column_names.include?(specified_foreign_type)
195
+ report_error(
196
+ "
197
+ For '#{assoc_klass.name}': Could not determine the assoc's foreign key type.
198
+ Foreign key type should have been '#{specified_foreign_type}', but did not exist on the #{assoc_klass.table_name} table.
199
+ "
200
+ )
201
+ end
202
+
203
+ if DEPENDENCY_RESTRICT.include?(dependency_type) && traverse_restricted_dependency?(parent_class, reflection)
204
+ klass_dependencies_parser(assoc_klass, klass_action: dependency_type, dependency_path: dependency_path.dup << parent_class.name)
205
+ else
206
+ klass_dependencies_parser(assoc_klass, klass_action: dependency_type, dependency_path: dependency_path.dup << parent_class.name)
207
+ end
208
+ end
209
+
210
+ def association_parser_belongs_to(parent_class, association_name, dependency_type, dependency_path)
211
+ raise "parent_class missing" unless parent_class
212
+ raise "#{parent_class} - association_name: missing" unless association_name
213
+ raise "#{parent_class} - dependency_type: missing" unless dependency_type
214
+ raise "#{parent_class} - dependency_path: nil" if dependency_path.nil?
215
+
216
+ reflection = parent_class.reflect_on_association(association_name)
217
+ reflection_type = reflection.class.name
218
+
219
+ is_polymorphic = reflection.options[:polymorphic]
220
+ if is_polymorphic
221
+ assoc_klass = find_klasses_from_polymorphic_dependency(parent_class).map(&:constantize)
222
+ @dependencies_per_klass[parent_class.name] += assoc_klass.map(&:name)
223
+ else
224
+ assoc_klass = reflection.klass
225
+ @dependencies_per_klass[parent_class.name] << assoc_klass.name
226
+ end
227
+
228
+ specified_primary_key = reflection.options[:primary_key] || 'id'
229
+ specified_foreign_key = reflection.options[:foreign_key] || "#{association_name}_id"
230
+
231
+ # Check to see if foreign_key exists in our parent table
232
+ unless parent_class.column_names.include?(specified_foreign_key)
233
+ report_error(
234
+ "
235
+ For #{parent_class.name}'s association '#{association_name}': Could not determine the assoc's foreign key.
236
+ Foreign key should have been '#{specified_foreign_key}', but did not exist on the #{parent_class.table_name} table.
237
+ "
238
+ )
239
+ return
240
+ end
241
+
242
+ if (
243
+ DEPENDENCY_DESTROY.include?(dependency_type) ||
244
+ DEPENDENCY_NULLIFY.include?(dependency_type) && traverse_restricted_dependency?(parent_class, reflection)
245
+ )
246
+ klass_dependencies_parser(assoc_klass, klass_action: dependency_type, dependency_path: dependency_path.dup << parent_class.name)
247
+ end
248
+ end
249
+
250
+ # In this example the klass would be the polymorphic klass
251
+ # - i.e. Attachment belongs_to: :attachable, dependent: :destroy
252
+ # We're looking for klasses in the flat map that have a has_many :attachments, as: :attachable
253
+ def find_klasses_from_polymorphic_dependency(klass)
254
+ found_klasses = []
255
+ flat_dependencies_per_klass.each do |flat_klass_name, klass_dependencies|
256
+ if klass_dependencies[:has_many].values.include?(klass.name)
257
+ found_klasses << flat_klass_name
258
+ end
259
+ end
260
+ found_klasses
261
+ end
262
+
263
+ # return [Boolean]
264
+ # - true if valid
265
+ # - false if not valid
266
+ def traverse_restricted_dependency? parent_class, reflection
267
+ # Return true if we're going to destroy all restricted
268
+ return true if opts_c.force_destroy_restricted
269
+
270
+ report_error(
271
+ "
272
+ #{parent_class.name}'s assoc '#{reflection.name}' has a restricted dependency type.
273
+ If you still wish to destroy, use the 'force_destroy_restricted: true' option
274
+ "
275
+ )
276
+
277
+ return false
278
+ end
279
+
280
+ end
281
+ end
@@ -0,0 +1,25 @@
1
+ module BulkDependencyEraser
2
+ module Utils
3
+ module Methods
4
+ # To freeze all nested structures including hashes, arrays, and strings
5
+ # Deep Freezing All Structures
6
+ def deep_freeze(obj)
7
+ case obj
8
+ when Hash
9
+ obj.each { |key, value| deep_freeze(key); deep_freeze(value) }
10
+ obj.freeze
11
+ when Array
12
+ obj.each { |value| deep_freeze(value) }
13
+ obj.freeze
14
+ when String
15
+ obj.freeze
16
+ else
17
+ obj.freeze if obj.respond_to?(:freeze)
18
+ end
19
+ end
20
+ end
21
+
22
+ include Methods
23
+ extend Methods
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
1
  module BulkDependencyEraser
2
- VERSION = "2.1.0".freeze
2
+ VERSION = "3.0.0".freeze
3
3
  end
@@ -1,6 +1,13 @@
1
1
  require_relative 'bulk_dependency_eraser/base'
2
2
  require_relative 'bulk_dependency_eraser/builder'
3
3
  require_relative 'bulk_dependency_eraser/deleter'
4
+ require_relative 'bulk_dependency_eraser/full_schema_parser'
4
5
  require_relative 'bulk_dependency_eraser/nullifier'
6
+ require_relative 'bulk_dependency_eraser/query_schema_parser'
5
7
  require_relative 'bulk_dependency_eraser/manager'
6
- require_relative 'bulk_dependency_eraser/version'
8
+ require_relative 'bulk_dependency_eraser/utils'
9
+ require_relative 'bulk_dependency_eraser/version'
10
+ require_relative 'bulk_dependency_eraser/errors/base_error'
11
+ require_relative 'bulk_dependency_eraser/errors/builder_error'
12
+ require_relative 'bulk_dependency_eraser/errors/deleter_error'
13
+ require_relative 'bulk_dependency_eraser/errors/nullifier_error'
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: 2.1.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - benjamin.dana.software.dev@gmail.com
@@ -146,8 +146,15 @@ files:
146
146
  - lib/bulk_dependency_eraser/base.rb
147
147
  - lib/bulk_dependency_eraser/builder.rb
148
148
  - lib/bulk_dependency_eraser/deleter.rb
149
+ - lib/bulk_dependency_eraser/errors/base_error.rb
150
+ - lib/bulk_dependency_eraser/errors/builder_error.rb
151
+ - lib/bulk_dependency_eraser/errors/deleter_error.rb
152
+ - lib/bulk_dependency_eraser/errors/nullifier_error.rb
153
+ - lib/bulk_dependency_eraser/full_schema_parser.rb
149
154
  - lib/bulk_dependency_eraser/manager.rb
150
155
  - lib/bulk_dependency_eraser/nullifier.rb
156
+ - lib/bulk_dependency_eraser/query_schema_parser.rb
157
+ - lib/bulk_dependency_eraser/utils.rb
151
158
  - lib/bulk_dependency_eraser/version.rb
152
159
  homepage: https://github.com/danabr75/bulk_dependency_eraser
153
160
  licenses: