bulk_dependency_eraser 2.1.0 → 3.0.0

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: 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: