bulk_dependency_eraser 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []