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 +7 -0
- data/lib/bulk_dependency_eraser/base.rb +54 -0
- data/lib/bulk_dependency_eraser/builder.rb +286 -0
- data/lib/bulk_dependency_eraser/deleter.rb +50 -0
- data/lib/bulk_dependency_eraser/manager.rb +77 -0
- data/lib/bulk_dependency_eraser/nullifier.rb +63 -0
- data/lib/bulk_dependency_eraser.rb +5 -0
- metadata +147 -0
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
|
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: []
|