bulk_dependency_eraser 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e51d0383cb5b2111181c9dc48b499b046d225adad1d1f8b985216a0d395cb612
4
+ data.tar.gz: 02ce5200f03d7a4cec2cfa72b64fbfdf84e73ce5b3e6b43ba073173b48e5139b
5
+ SHA512:
6
+ metadata.gz: 25857421c4e8cc6af0f952bacd106b39f19d34ac8d2292f74e9daa90c10ea07ab82e11fe584b7c0fcb60209fb1e4ba0d126e6e9102587b3d6ff6a69e495373e5
7
+ data.tar.gz: 68f2ec5423d953bf842de7c98ea778f44795a89cfa948b400c5171e08489446aba765467744175a5de69df6957fef1b04f0c017af7d63b1959d756caaac07a22
@@ -0,0 +1,54 @@
1
+ module BulkDependencyEraser
2
+ class Base
3
+ DEFAULT_OPTS = {}.freeze
4
+
5
+ DEFAULT_DB_WRAPPER = ->(block) { block.call }
6
+
7
+ attr_reader :errors
8
+
9
+ def initialize opts: {}
10
+ filtered_opts = opts.slice(*self.class::DEFAULT_OPTS.keys)
11
+ @opts_c = options_container.new(
12
+ self.class::DEFAULT_OPTS.merge(filtered_opts)
13
+ )
14
+ @errors = []
15
+ end
16
+
17
+ def execute
18
+ raise NotImplementedError
19
+ end
20
+
21
+ protected
22
+
23
+ # Create options container
24
+ def options_container
25
+ Struct.new(
26
+ *self.class::DEFAULT_OPTS.keys,
27
+ keyword_init: true
28
+ ).freeze
29
+ end
30
+
31
+ def report_error msg
32
+ @errors << msg
33
+ end
34
+
35
+ def merge_errors errors, prefix = nil
36
+ local_errors = errors.dup
37
+
38
+ unless local_errors.any?
39
+ local_errors << '<NO ERRORS FOUND TO MERGE>'
40
+ end
41
+
42
+ if prefix
43
+ local_errors = errors.map { |error| prefix + error }
44
+ end
45
+ @errors += local_errors
46
+ end
47
+
48
+ def uniqify_errors!
49
+ @errors.uniq!
50
+ end
51
+
52
+ attr_reader :opts_c
53
+ end
54
+ end
@@ -0,0 +1,286 @@
1
+ module BulkDependencyEraser
2
+ class Builder < Base
3
+ DEFAULT_OPTS = {
4
+ force_destroy_restricted: false,
5
+ verbose: false,
6
+ # Some associations scopes take parameters.
7
+ # - We would have to instantiate if we wanted to apply that scope filter.
8
+ instantiate_if_assoc_scope_with_arity: false,
9
+ db_read_wrapper: self::DEFAULT_DB_WRAPPER,
10
+ }.freeze
11
+
12
+ DEPENDENCY_NULLIFY = %i[
13
+ nullify
14
+ ].freeze
15
+
16
+ # Abort deletion if assoc dependency value is any of these.
17
+ # - exception if the :force_destroy_restricted option set true
18
+ DEPENDENCY_RESTRICT = %i[
19
+ restrict_with_error
20
+ restrict_with_exception
21
+ ].freeze
22
+
23
+ DEPENDENCY_DESTROY = (
24
+ %i[
25
+ destroy
26
+ delete_all
27
+ destroy_async
28
+ ] + self::DEPENDENCY_RESTRICT
29
+ ).freeze
30
+
31
+ DEPENDENCY_DESTROY_IGNORE_REFLECTION_TYPES = [
32
+ # Rails 6.1, when a has_and_delongs_to_many <assoc>, dependent: :destroy,
33
+ # will ignore the destroy. Will neither destroy the join table record nor the association record
34
+ # We will do the same, mirror the fuctionality, by ignoring any :dependent options on these types.
35
+ 'ActiveRecord::Reflection::HasAndBelongsToManyReflection'
36
+ ].freeze
37
+
38
+ attr_reader :deletion_list, :nullification_list
39
+
40
+ def initialize query:, opts: {}
41
+ @query = query
42
+ @deletion_list = {}
43
+ @nullification_list = {}
44
+ super(opts:)
45
+ end
46
+
47
+ def execute
48
+ begin
49
+ build_result = deletion_query_parser(@query)
50
+
51
+ uniqify_errors!
52
+
53
+ return errors.none?
54
+ rescue StandardError => e
55
+ if @query.is_a?(ActiveRecord::Relation)
56
+ klass = @query.klass
57
+ klass_name = @query.klass.name
58
+ else
59
+ # current_query is a normal rails class
60
+ klass = @query
61
+ klass_name = @query.name
62
+ end
63
+ report_error(
64
+ "
65
+ Error Encountered in 'execute' for '#{klass_name}':
66
+ #{e.class.name}
67
+ #{e.message}
68
+ "
69
+ )
70
+
71
+ return false
72
+ end
73
+ end
74
+
75
+ def deletion_query_parser query, association_parent = nil
76
+ # necessary for "ActiveRecord::Reflection::ThroughReflection" use-case
77
+ # force_through_destroy_chains = options[:force_destroy_chain] || {}
78
+ # do_not_destroy_self = options[:do_not_destroy] || {}
79
+
80
+ if query.is_a?(ActiveRecord::Relation)
81
+ klass = query.klass
82
+ klass_name = query.klass.name
83
+ table_klass_name = query.klass.table_name.classify
84
+ else
85
+ # current_query is a normal rails class
86
+ klass = query
87
+ klass_name = query.name
88
+ table_klass_name = query.table_name.classify
89
+ end
90
+
91
+ if opts_c.verbose
92
+ if association_parent
93
+ puts "Building #{klass_name}"
94
+ else
95
+ puts "Building #{association_parent} => #{klass_name}"
96
+ end
97
+ end
98
+
99
+ if klass.primary_key != 'id'
100
+ report_error(
101
+ "#{klass_name} - does not use primary_key 'id'. Cannot use this tool to bulk delete."
102
+ )
103
+ return
104
+ end
105
+
106
+ # Pluck IDs of the current query
107
+ query_ids = read_from_db do
108
+ query.pluck(:id)
109
+ end
110
+
111
+ deletion_list[table_klass_name] ||= []
112
+
113
+ # prevent infinite recursion here.
114
+ # - Remove any IDs that have been processed before
115
+ query_ids = query_ids - deletion_list[table_klass_name]
116
+ # If ids are nil, let's find that error
117
+ if query_ids.none? #|| query_ids.nil?
118
+ # quick cleanup, if turns out was an empty class
119
+ deletion_list.delete(table_klass_name) if deletion_list[table_klass_name].none?
120
+ return
121
+ end
122
+
123
+ # Use-case: We have more IDs to process
124
+ # - can now safely add to the list, since we've prevented infinite recursion
125
+ deletion_list[table_klass_name] += query_ids
126
+
127
+ # Hard to test if not sorted
128
+ # - if we had more advanced rspec matches, we could do away with this.
129
+ deletion_list[table_klass_name].sort! if Rails.env.test?
130
+
131
+ # ignore associations that aren't a dependent destroyable type
132
+ destroy_associations = query.reflect_on_all_associations.select do |reflection|
133
+ assoc_dependent_type = reflection.options&.dig(:dependent)&.to_sym
134
+ if DEPENDENCY_DESTROY_IGNORE_REFLECTION_TYPES.include?(reflection.class.name)
135
+ # Ignore those types of associations.
136
+ false
137
+ elsif DEPENDENCY_RESTRICT.include?(assoc_dependent_type) && opts_c.force_destroy_restricted != true
138
+ # If the dependency_type is restricted_with_..., and we're not supposed to destroy those, report errork
139
+ report_error(
140
+ "#{klass_name}'s assoc '#{reflection.name}' has a 'dependent: :#{assoc_dependent_type}' set. " \
141
+ "If you still wish to destroy, use the 'force_destroy_restricted: true' option"
142
+ )
143
+ false
144
+ else
145
+ DEPENDENCY_DESTROY.include?(assoc_dependent_type)
146
+ end
147
+ end
148
+
149
+ nullify_associations = query.reflect_on_all_associations.select do |reflection|
150
+ assoc_dependent_type = reflection.options&.dig(:dependent)&.to_sym
151
+ DEPENDENCY_NULLIFY.include?(assoc_dependent_type)
152
+ end
153
+
154
+ destroy_association_names = destroy_associations.map(&:name)
155
+ nullify_association_names = nullify_associations.map(&:name)
156
+
157
+ # Iterate through the assoc names, if there are any :through assocs, then remap
158
+ destroy_association_names = destroy_association_names.collect do |assoc_name|
159
+ find_root_association_from_through_assocs(klass, assoc_name)
160
+ end
161
+ nullify_association_names = nullify_association_names.collect do |assoc_name|
162
+ find_root_association_from_through_assocs(klass, assoc_name)
163
+ end
164
+
165
+ destroy_association_names.each do |destroy_association_name|
166
+ association_parser(klass, query, query_ids, destroy_association_name, :delete)
167
+ end
168
+
169
+ nullify_association_names.each do |nullify_association_name|
170
+ association_parser(klass, query, query_ids, nullify_association_name, :nullify)
171
+ end
172
+ end
173
+
174
+ # Iterate through each destroyable association, and recursively call 'deletion_query_parser'.
175
+ def association_parser parent_class, query, query_ids, association_name, type
176
+ reflection = parent_class.reflect_on_association(association_name)
177
+ reflection_type = reflection.class.name
178
+ assoc_klass = reflection.klass
179
+ assoc_table_klass_name = assoc_klass.table_name.classify
180
+
181
+ assoc_query = assoc_klass.unscoped
182
+
183
+ unless assoc_klass.primary_key == 'id'
184
+ report_error("#{parent_class.name}'s association '#{association_name}' - assoc class does not use 'id' as a primary_key")
185
+ return
186
+ end
187
+
188
+ # If there is an association scope present, check to see how many parameters it's using
189
+ # - if there's any parameter, we have to either skip it or instantiate it to find it's dependencies.
190
+ if reflection.scope&.arity&.nonzero?
191
+ # TODO!
192
+ if opts_c.instantiate_if_assoc_scope_with_arity
193
+ raise "TODO: instantiate and apply scope!"
194
+ else
195
+ report_error(
196
+ "#{parent_class.name} and '#{association_name}' - scope has instance parameters. Use :instantiate_if_assoc_scope_with_arity option?"
197
+ )
198
+ return
199
+ end
200
+ elsif reflection.scope
201
+ # I saw this used somewhere, too bad I didn't save the source for it.
202
+ s = parent_class.reflect_on_association(association_name).scope
203
+ assoc_query = assoc_query.instance_exec(&s)
204
+ end
205
+
206
+ specified_primary_key = reflection.options[:primary_key]&.to_s
207
+ specified_foreign_key = reflection.options[:foreign_key]&.to_s
208
+
209
+ # handle foreign_key edge cases
210
+ if specified_foreign_key.nil?
211
+ if reflection.options[:polymorphic]
212
+ assoc_query = assoc_query.where({(association_name.singularize + '_type').to_sym => parent_class.table_name.classify})
213
+ specified_foreign_key = association_name.singularize + "_id"
214
+ elsif reflection.options[:as]
215
+ assoc_query = assoc_query.where({(reflection.options[:as].to_s + '_type').to_sym => parent_class.table_name.classify})
216
+ specified_foreign_key = reflection.options[:as].to_s + "_id"
217
+ else
218
+ specified_foreign_key = parent_class.table_name.singularize + "_id"
219
+ end
220
+ end
221
+
222
+ # Check to see if foreign_key exists in association class's table
223
+ unless assoc_klass.column_names.include?(specified_foreign_key)
224
+ report_error(
225
+ "
226
+ For #{parent_class.name}'s assoc '#{assoc_klass.name}': Could not determine the assoc's foreign key.
227
+ Generated '#{specified_foreign_key}', but did not exist on the association table.
228
+ "
229
+ )
230
+ return
231
+ end
232
+
233
+ # Build association query, based on parent class's primary key and the assoc's foreign key
234
+ # - handle primary key edge cases
235
+ # - The associations might not be using the primary_key of the klass table, but we can support that here.
236
+ if specified_primary_key && specified_primary_key&.to_s != 'id'
237
+ alt_primary_ids = read_from_db do
238
+ query.pluck(specified_primary_key)
239
+ end
240
+ assoc_query = assoc_query.where(specified_foreign_key.to_sym => alt_primary_ids)
241
+ else
242
+ assoc_query = assoc_query.where(specified_foreign_key.to_sym => query_ids)
243
+ end
244
+
245
+ if type == :delete
246
+ # Recursively call 'deletion_query_parser' on association query, to delete any if the assoc's dependencies
247
+ deletion_query_parser(assoc_query, parent_class)
248
+ elsif type == :nullify
249
+ # No need for recursion here.
250
+ # - we're not destroying these assocs so we don't need to parse their dependencies.
251
+ assoc_ids = read_from_db do
252
+ assoc_query.pluck(:id)
253
+ end
254
+
255
+ # No assoc_ids, no need to add it to the nullification list
256
+ return if assoc_ids.none?
257
+
258
+ # puts "FOUND IDS: #{assoc_ids.insect}"
259
+ nullification_list[assoc_table_klass_name] ||= {}
260
+ nullification_list[assoc_table_klass_name][specified_foreign_key] ||= []
261
+ nullification_list[assoc_table_klass_name][specified_foreign_key] += assoc_ids
262
+ nullification_list[assoc_table_klass_name][specified_foreign_key].uniq!
263
+ else
264
+ raise "invalid parsing type: #{type}"
265
+ end
266
+ end
267
+
268
+ protected
269
+
270
+ # A dependent assoc may be through another association. Follow the throughs to find the correct assoc to destroy.
271
+ def find_root_association_from_through_assocs klass, association_name
272
+ reflection = klass.reflect_on_association(association_name)
273
+ options = reflection.options
274
+ if options.key?(:through)
275
+ return find_root_association_from_through_assocs(klass, options[:through])
276
+ else
277
+ association_name
278
+ end
279
+ end
280
+
281
+ def read_from_db(&block)
282
+ puts "Reading from DB..." if opts_c.verbose
283
+ opts_c.db_read_wrapper.call(block)
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,50 @@
1
+ module BulkDependencyEraser
2
+ class Deleter < Base
3
+ DEFAULT_OPTS = {
4
+ verbose: false,
5
+ db_delete_wrapper: self::DEFAULT_DB_WRAPPER,
6
+ }.freeze
7
+
8
+ def initialize class_names_and_ids: {}, opts: {}
9
+ @class_names_and_ids = class_names_and_ids
10
+ super(opts:)
11
+ end
12
+
13
+ def execute
14
+ ActiveRecord::Base.transaction do
15
+ current_class_name = 'N/A'
16
+ begin
17
+ class_names_and_ids.keys.reverse.each do |class_name|
18
+ current_class_name = class_name
19
+ ids = class_names_and_ids[class_name]
20
+ klass = class_name.constantize
21
+
22
+ # Disable any ActiveRecord::InvalidForeignKey raised errors.
23
+ # src https://stackoverflow.com/questions/41005849/rails-migrations-temporarily-ignore-foreign-key-constraint
24
+ # https://apidock.com/rails/ActiveRecord/ConnectionAdapters/AbstractAdapter/disable_referential_integrity
25
+ ActiveRecord::Base.connection.disable_referential_integrity do
26
+ delete_in_db do
27
+ klass.unscoped.where(id: ids).delete_all
28
+ end
29
+ end
30
+ end
31
+ rescue Exception => e
32
+ report_error("Issue attempting to delete '#{current_class_name}': #{e.name} - #{e.message}")
33
+ raise ActiveRecord::Rollback
34
+ end
35
+ end
36
+
37
+ return errors.none?
38
+ end
39
+
40
+ protected
41
+
42
+ attr_reader :class_names_and_ids
43
+
44
+ def delete_in_db(&block)
45
+ puts "Deleting from DB..." if opts_c.verbose
46
+ opts_c.db_delete_wrapper.call(block)
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,77 @@
1
+ module BulkDependencyEraser
2
+ class Manager < Base
3
+ DEFAULT_OPTS = {
4
+ verbose: false,
5
+ }.freeze
6
+
7
+ delegate :nullification_list, :deletion_list, to: :dependency_builder
8
+
9
+ # @param query [ActiveRecord::Base | ActiveRecord::Relation]
10
+ def initialize query:, opts: {}
11
+ @opts = opts
12
+ @dependency_builder = BulkDependencyEraser::Builder.new(query:, opts:)
13
+ @deleter = nil
14
+ @nullifier = nil
15
+
16
+ @built = false
17
+ super(opts:)
18
+ end
19
+
20
+ def execute
21
+ unless built
22
+ # return early if build fails
23
+ return false unless build
24
+ end
25
+
26
+ delete!
27
+ nullify!
28
+
29
+ return errors.none?
30
+ end
31
+
32
+ def build
33
+ builder_execution = @dependency_builder.execute
34
+
35
+ unless builder_execution
36
+ puts "Builder execution FAILED" if opts_c.verbose
37
+ merge_errors(dependency_builder.errors, 'Builder: ')
38
+ else
39
+ puts "Builder execution SUCCESSFUL" if opts_c.verbose
40
+ end
41
+
42
+ return builder_execution
43
+ end
44
+
45
+ def delete!
46
+ @deleter = BulkDependencyEraser::Deleter.new(class_names_and_ids: deletion_list, opts:)
47
+ deleter_execution = deleter.execute
48
+ unless deleter_execution
49
+ puts "Deleter execution FAILED" if opts_c.verbose
50
+ merge_errors(deleter.errors, 'Deleter: ')
51
+ else
52
+ puts "Deleter execution SUCCESSFUL" if opts_c.verbose
53
+ end
54
+
55
+ return deleter_execution
56
+ end
57
+
58
+ def nullify!
59
+ @nullifier = BulkDependencyEraser::Nullifier.new(class_names_columns_and_ids: nullification_list, opts:)
60
+ nullifier_execution = nullifier.execute
61
+
62
+ unless nullifier_execution
63
+ puts "Nullifier execution FAILED" if opts_c.verbose
64
+ merge_errors(nullifier.errors, 'Nullifier: ')
65
+ @errors += nullifier.errors
66
+ else
67
+ puts "Nullifier execution SUCCESSFUL" if opts_c.verbose
68
+ end
69
+
70
+ return nullifier_execution
71
+ end
72
+
73
+ protected
74
+
75
+ attr_reader :dependency_builder, :deleter, :nullifier, :opts, :built
76
+ end
77
+ end
@@ -0,0 +1,63 @@
1
+ module BulkDependencyEraser
2
+ class Nullifier < Base
3
+ DEFAULT_OPTS = {
4
+ verbose: false,
5
+ db_nullify_wrapper: self::DEFAULT_DB_WRAPPER
6
+ }.freeze
7
+
8
+ # @param class_names_columns_and_ids [Hash] - model names with columns to nullify pointing towards the record IDs that require the nullification.
9
+ # - structure:
10
+ # {
11
+ # <model_name>: {
12
+ # column_name: <array_of_ids>
13
+ # }
14
+ # }
15
+ # @param opts [Hash] - hash of options, allowlisted in DEFAULT_OPTS
16
+ def initialize class_names_columns_and_ids:, opts: {}
17
+ @class_names_columns_and_ids = class_names_columns_and_ids
18
+ super(opts:)
19
+ end
20
+
21
+ def execute
22
+ ActiveRecord::Base.transaction do
23
+ current_class_name = 'N/A'
24
+ current_column = 'N/A'
25
+ begin
26
+ class_names_columns_and_ids.keys.reverse.each do |class_name|
27
+ current_class_name = class_name
28
+ klass = class_name.constantize
29
+
30
+ columns_and_ids = class_names_columns_and_ids[class_name]
31
+
32
+ columns_and_ids.each do |column, ids|
33
+ current_column = column
34
+ # Disable any ActiveRecord::InvalidForeignKey raised errors.
35
+ # src https://stackoverflow.com/questions/41005849/rails-migrations-temporarily-ignore-foreign-key-constraint
36
+ # https://apidock.com/rails/ActiveRecord/ConnectionAdapters/AbstractAdapter/disable_referential_integrity
37
+ ActiveRecord::Base.connection.disable_referential_integrity do
38
+ nullify_in_db do
39
+ klass.unscoped.where(id: ids).update_all(column => nil)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ rescue Exception => e
45
+ report_error("Issue attempting to nullify '#{current_class_name}' column '#{current_column}': #{e.name} - #{e.message}")
46
+ raise ActiveRecord::Rollback
47
+ end
48
+ end
49
+
50
+ return errors.none?
51
+ end
52
+
53
+ protected
54
+
55
+ attr_reader :class_names_columns_and_ids
56
+
57
+ def nullify_in_db(&block)
58
+ puts "Nullifying from DB..." if opts_c.verbose
59
+ opts_c.db_nullify_wrapper.call(block)
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'bulk_dependency_eraser/base'
2
+ require_relative 'bulk_dependency_eraser/builder'
3
+ require_relative 'bulk_dependency_eraser/deleter'
4
+ require_relative 'bulk_dependency_eraser/nullifier'
5
+ require_relative 'bulk_dependency_eraser/manager'
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bulk_dependency_eraser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - benjamin.dana.software.dev@gmail.com
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.9'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.9'
55
+ - !ruby/object:Gem::Dependency
56
+ name: listen
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: database_cleaner
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.8'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.8'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.4'
111
+ description:
112
+ email:
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - lib/bulk_dependency_eraser.rb
118
+ - lib/bulk_dependency_eraser/base.rb
119
+ - lib/bulk_dependency_eraser/builder.rb
120
+ - lib/bulk_dependency_eraser/deleter.rb
121
+ - lib/bulk_dependency_eraser/manager.rb
122
+ - lib/bulk_dependency_eraser/nullifier.rb
123
+ homepage: https://github.com/danabr75/bulk_dependency_eraser
124
+ licenses:
125
+ - LGPL-3.0-only
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '3.1'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.4.6
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: A bulk deletion tool that deletes records and their dependencies without
146
+ instantiation
147
+ test_files: []