delete_recursively 1.1.0 → 1.2.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 +4 -4
- data/lib/delete_recursively/active_record_extensions.rb +57 -0
- data/lib/delete_recursively/associated_class_finder.rb +57 -0
- data/lib/delete_recursively/dependent_id_finder.rb +47 -0
- data/lib/delete_recursively/railtie.rb +5 -0
- data/lib/delete_recursively/version.rb +1 -1
- data/lib/delete_recursively.rb +44 -143
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b030ca2cd0433b8f6edbe713ec5e75651cfd8b17b09ce8080c0fe9867b85799d
|
4
|
+
data.tar.gz: db4adfc88b256876c756cd7f216192b324c4c2a56892c57e00961d19c8121fea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90ec72416540144c258e6234142d9877596e3e42d17489d539b80ddaa1fe7cf80d2a5adb19269e4e3759393c83527edd994468fb220611a9a3752da4d37018c1
|
7
|
+
data.tar.gz: f4f0e2432412bf80ca7275d5fe43e03317643e35d6d1bae4cf890057915c90dc57cb682066366c5fe9d51f2a1e2c086810cad9c3e65e1f55561727f4ccdaf802
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# override ::valid_dependent_options to make the new
|
2
|
+
# dependent option available to Association::Builder classes
|
3
|
+
module DeleteRecursively::OptionPermission
|
4
|
+
def valid_dependent_options
|
5
|
+
super + [DeleteRecursively::NEW_DEPENDENT_OPTION]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# override Association#handle_dependency to apply the new option if it is set
|
10
|
+
module DeleteRecursively::DependencyHandling
|
11
|
+
def handle_dependency
|
12
|
+
if DeleteRecursively.enabled_for?(self)
|
13
|
+
delete_dependencies_recursively
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete_dependencies_recursively(force: false)
|
20
|
+
if reflection.belongs_to?
|
21
|
+
# Special case. The owner is already destroyed at this point,
|
22
|
+
# so we cannot use the efficient ::dependent_ids lookup. Note that this
|
23
|
+
# only happens for a single entry-record on #destroy, though.
|
24
|
+
return unless target = load_target
|
25
|
+
|
26
|
+
DeleteRecursively.delete_records_recursively(target.class, target.id, force: force)
|
27
|
+
else
|
28
|
+
DeleteRecursively.delete_recursively(reflection, nil, owner.id, force: force)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'active_record'
|
34
|
+
|
35
|
+
module ActiveRecord
|
36
|
+
module Associations
|
37
|
+
%w[BelongsTo HasMany HasOne].each do |assoc_name|
|
38
|
+
assoc_builder = Builder.const_get(assoc_name)
|
39
|
+
assoc_builder.singleton_class.prepend(DeleteRecursively::OptionPermission)
|
40
|
+
|
41
|
+
assoc_class = const_get("#{assoc_name}Association")
|
42
|
+
assoc_class.prepend(DeleteRecursively::DependencyHandling)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Base
|
47
|
+
def delete_recursively(force: false)
|
48
|
+
DeleteRecursively.delete_records_recursively(self.class, id, force: force)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Relation
|
53
|
+
def delete_all_recursively(force: false)
|
54
|
+
DeleteRecursively.delete_records_recursively(klass, ids, force: force)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module DeleteRecursively::AssociatedClassFinder
|
2
|
+
class << self
|
3
|
+
def call(reflection)
|
4
|
+
cache[reflection] ||= find_classes(reflection)
|
5
|
+
end
|
6
|
+
|
7
|
+
def clear_cache
|
8
|
+
cache.clear
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def cache
|
14
|
+
@cache ||= {}.tap(&:compare_by_identity)
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_classes(reflection)
|
18
|
+
result =
|
19
|
+
if reflection.polymorphic?
|
20
|
+
find_classes_for_polymorphic_reflection(reflection)
|
21
|
+
else
|
22
|
+
[reflection.klass]
|
23
|
+
end.compact
|
24
|
+
|
25
|
+
result.empty? && warn_empty_result(reflection)
|
26
|
+
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
# This ignores relatives where the inverse relation is not defined.
|
31
|
+
# The alternative to this approach would be to expensively select
|
32
|
+
# all distinct values from the *_type column:
|
33
|
+
# reflection.active_record.distinct.pluck(reflection.foreign_type)
|
34
|
+
def find_classes_for_polymorphic_reflection(reflection)
|
35
|
+
ActiveRecord::Base.descendants.select do |klass|
|
36
|
+
klass.reflect_on_all_associations.any? do |other_ref|
|
37
|
+
next other_ref.inverse_of == reflection unless other_ref.polymorphic?
|
38
|
+
|
39
|
+
# check if its a bi-directional polymorphic association
|
40
|
+
begin
|
41
|
+
other_ref.polymorphic_inverse_of(reflection.active_record)
|
42
|
+
rescue ActiveRecord::InverseOfAssociationNotFoundError
|
43
|
+
next
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def warn_empty_result(reflection)
|
50
|
+
warn "#{self} could not find associated class(es) for "\
|
51
|
+
"#{reflection.active_record}##{reflection.name}. "\
|
52
|
+
"You might need to define the inverse association(s) or, "\
|
53
|
+
"if they are already defined, add `as :#{reflection.name}` or "\
|
54
|
+
"`inverse_of :#{reflection.name}` to them."
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module DeleteRecursively::DependentIdFinder
|
2
|
+
class << self
|
3
|
+
def call(owner_ids, reflection, assoc_class)
|
4
|
+
owner_class = reflection.active_record
|
5
|
+
|
6
|
+
if reflection.belongs_to?
|
7
|
+
owners = owner_class.where(owner_class.primary_key => owner_ids)
|
8
|
+
if reflection.polymorphic?
|
9
|
+
owners = owners.where(reflection.foreign_type => assoc_class.to_s)
|
10
|
+
end
|
11
|
+
owners.pluck(reflection.foreign_key).compact
|
12
|
+
elsif reflection.through_reflection
|
13
|
+
dependent_ids_with_through_option(owner_ids, reflection)
|
14
|
+
else # plain `has_many` or `has_one`
|
15
|
+
owner_foreign_key = foreign_key(owner_class, reflection)
|
16
|
+
reflection.klass.where(owner_foreign_key => owner_ids).ids
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def dependent_ids_with_through_option(owner_ids, reflection)
|
23
|
+
through_reflection = reflection.through_reflection
|
24
|
+
owner_foreign_key = foreign_key(reflection.active_record, through_reflection)
|
25
|
+
|
26
|
+
dependent_class = reflection.klass
|
27
|
+
dependent_through_reflection = inverse_through_reflection(reflection)
|
28
|
+
dependent_foreign_key =
|
29
|
+
foreign_key(dependent_class, dependent_through_reflection)
|
30
|
+
|
31
|
+
through_reflection.klass
|
32
|
+
.where(owner_foreign_key => owner_ids)
|
33
|
+
.pluck(dependent_foreign_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def foreign_key(owner_class, reflection)
|
37
|
+
reflection && reflection.foreign_key || owner_class.to_s.foreign_key
|
38
|
+
end
|
39
|
+
|
40
|
+
def inverse_through_reflection(reflection)
|
41
|
+
through_class = reflection.through_reflection.klass
|
42
|
+
reflection.klass.reflect_on_all_associations
|
43
|
+
.map(&:through_reflection)
|
44
|
+
.find { |thr_ref| thr_ref && thr_ref.klass == through_class }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/delete_recursively.rb
CHANGED
@@ -6,159 +6,59 @@
|
|
6
6
|
# Adds a new dependent: option to ActiveRecord associations.
|
7
7
|
#
|
8
8
|
module DeleteRecursively
|
9
|
-
require_relative File.join('delete_recursively', 'version')
|
10
|
-
|
11
9
|
NEW_DEPENDENT_OPTION = :delete_recursively
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
# override Association#handle_dependency to apply the new option if it is set
|
22
|
-
module DependencyHandling
|
23
|
-
def handle_dependency
|
24
|
-
if DeleteRecursively.enabled_for?(self)
|
25
|
-
delete_dependencies_recursively
|
26
|
-
else
|
27
|
-
super
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def delete_dependencies_recursively(force: false)
|
32
|
-
if reflection.belongs_to?
|
33
|
-
# Special case. The owner is already destroyed at this point,
|
34
|
-
# so we cannot use the efficient ::dependent_ids lookup. Note that this
|
35
|
-
# only happens for a single entry-record on #destroy, though.
|
36
|
-
return unless target = load_target
|
37
|
-
|
38
|
-
DeleteRecursively.delete_records_recursively(target.class, target.id, force: force)
|
39
|
-
else
|
40
|
-
DeleteRecursively.delete_recursively(reflection, owner.class, owner.id, force: force)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
11
|
+
require_relative File.join('delete_recursively', 'active_record_extensions')
|
12
|
+
require_relative File.join('delete_recursively', 'associated_class_finder')
|
13
|
+
require_relative File.join('delete_recursively', 'dependent_id_finder')
|
14
|
+
require_relative File.join('delete_recursively', 'railtie') if defined?(::Rails::Railtie)
|
15
|
+
require_relative File.join('delete_recursively', 'version')
|
44
16
|
|
45
17
|
class << self
|
46
|
-
def delete_recursively(reflection,
|
18
|
+
def delete_recursively(reflection, _legacy_arg, owner_ids, seen: [], force: false)
|
19
|
+
owner_ids = Array(owner_ids)
|
20
|
+
return if owner_ids.empty?
|
21
|
+
|
47
22
|
# Dependent deletion can be bi-directional, so we need to avoid a loop.
|
48
|
-
|
23
|
+
# Note, however, that an association could be reached multiple times, from
|
24
|
+
# different starting points within the association tree, and having
|
25
|
+
# different owner_ids. In this case, we do need to go through it again.
|
26
|
+
recursion_identifier = [reflection, owner_ids]
|
27
|
+
return if seen.include?(recursion_identifier)
|
49
28
|
|
50
|
-
seen <<
|
29
|
+
seen << recursion_identifier
|
51
30
|
|
52
|
-
|
31
|
+
AssociatedClassFinder.call(reflection).each do |assoc_class|
|
53
32
|
record_ids = nil # fetched only when needed for recursion, deletion, or both
|
54
33
|
|
55
34
|
if recurse_on?(reflection)
|
56
|
-
record_ids =
|
57
|
-
|
58
|
-
delete_recursively(subref,
|
35
|
+
record_ids = DependentIdFinder.call(owner_ids, reflection, assoc_class)
|
36
|
+
assoc_class.reflect_on_all_associations.each do |subref|
|
37
|
+
delete_recursively(subref, nil, record_ids, seen: seen, force: force)
|
59
38
|
end
|
60
39
|
end
|
61
40
|
|
62
|
-
if dest_method = destructive_method(reflection,
|
63
|
-
record_ids ||=
|
64
|
-
|
41
|
+
if dest_method = destructive_method(reflection, force: force)
|
42
|
+
record_ids ||= DependentIdFinder.call(owner_ids, reflection, assoc_class)
|
43
|
+
assoc_class.send(dest_method, record_ids)
|
65
44
|
end
|
66
45
|
end
|
67
46
|
end
|
68
47
|
|
69
|
-
def associated_classes(reflection)
|
70
|
-
if reflection.polymorphic?
|
71
|
-
# This ignores relatives where the inverse relation is not defined.
|
72
|
-
# The alternative to this approach would be to expensively select
|
73
|
-
# all distinct values from the *_type column:
|
74
|
-
# reflection.active_record.distinct.pluck(reflection.foreign_type)
|
75
|
-
ActiveRecord::Base.descendants.select do |klass|
|
76
|
-
klass.reflect_on_all_associations
|
77
|
-
.any? { |ref| ref.inverse_of == reflection }
|
78
|
-
end
|
79
|
-
else
|
80
|
-
[reflection.klass]
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
48
|
def delete_records_recursively(record_class, record_ids, force: false)
|
85
49
|
record_class.reflect_on_all_associations.each do |ref|
|
86
|
-
delete_recursively(ref,
|
50
|
+
delete_recursively(ref, nil, record_ids, force: force)
|
87
51
|
end
|
88
52
|
record_class.delete(record_ids)
|
89
53
|
end
|
90
54
|
|
91
|
-
def destructive_method(reflection, record_class, record_ids, force: false)
|
92
|
-
if deleting?(reflection) || force && destructive?(reflection)
|
93
|
-
:delete
|
94
|
-
elsif destructive?(reflection)
|
95
|
-
:destroy
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def recurse_on?(reflection)
|
100
|
-
enabled_for?(reflection) || destructive?(reflection)
|
101
|
-
end
|
102
|
-
|
103
|
-
def enabled_for?(reflection)
|
104
|
-
reflection.options[:dependent] == NEW_DEPENDENT_OPTION
|
105
|
-
end
|
106
|
-
|
107
|
-
def destructive?(reflection)
|
108
|
-
%i[destroy destroy_all].include?(reflection.options[:dependent])
|
109
|
-
end
|
110
|
-
|
111
|
-
def deleting?(reflection)
|
112
|
-
[:delete, :delete_all, NEW_DEPENDENT_OPTION].include?(reflection.options[:dependent])
|
113
|
-
end
|
114
|
-
|
115
|
-
def dependent_ids(owner_class, owner_ids, reflection, assoc_class = nil)
|
116
|
-
if reflection.belongs_to?
|
117
|
-
owners = owner_class.where(owner_class.primary_key => owner_ids)
|
118
|
-
if reflection.polymorphic?
|
119
|
-
owners = owners.where(reflection.foreign_type => assoc_class.to_s)
|
120
|
-
end
|
121
|
-
owners.pluck(reflection.foreign_key).compact
|
122
|
-
elsif reflection.through_reflection
|
123
|
-
dependent_ids_with_through_option(owner_class, owner_ids, reflection)
|
124
|
-
else # plain `has_many` or `has_one`
|
125
|
-
owner_foreign_key = foreign_key(owner_class, reflection)
|
126
|
-
reflection.klass.where(owner_foreign_key => owner_ids).ids
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def dependent_ids_with_through_option(owner_class, owner_ids, reflection)
|
131
|
-
through_reflection = reflection.through_reflection
|
132
|
-
owner_foreign_key = foreign_key(owner_class, through_reflection)
|
133
|
-
|
134
|
-
dependent_class = reflection.klass
|
135
|
-
dependent_through_reflection = inverse_through_reflection(reflection)
|
136
|
-
dependent_foreign_key =
|
137
|
-
foreign_key(dependent_class, dependent_through_reflection)
|
138
|
-
|
139
|
-
through_reflection.klass
|
140
|
-
.where(owner_foreign_key => owner_ids)
|
141
|
-
.pluck(dependent_foreign_key)
|
142
|
-
end
|
143
|
-
|
144
|
-
def inverse_through_reflection(reflection)
|
145
|
-
through_class = reflection.through_reflection.klass
|
146
|
-
reflection.klass.reflect_on_all_associations
|
147
|
-
.map(&:through_reflection)
|
148
|
-
.find { |thr_ref| thr_ref && thr_ref.klass == through_class }
|
149
|
-
end
|
150
|
-
|
151
|
-
def foreign_key(owner_class, reflection)
|
152
|
-
reflection && reflection.foreign_key || owner_class.to_s.foreign_key
|
153
|
-
end
|
154
|
-
|
155
55
|
def all(record_class, criteria = {}, seen = [])
|
156
56
|
return if seen.include?(record_class)
|
157
57
|
|
158
58
|
seen << record_class
|
159
59
|
|
160
60
|
record_class.reflect_on_all_associations.each do |reflection|
|
161
|
-
|
61
|
+
AssociatedClassFinder.call(reflection).each do |assoc_class|
|
162
62
|
if recurse_on?(reflection)
|
163
63
|
all(assoc_class, criteria, seen)
|
164
64
|
elsif deleting?(reflection)
|
@@ -166,40 +66,41 @@ module DeleteRecursively
|
|
166
66
|
end
|
167
67
|
end
|
168
68
|
end
|
69
|
+
|
169
70
|
delete_with_applicable_criteria(record_class, criteria)
|
170
71
|
end
|
171
72
|
|
73
|
+
def enabled_for?(reflection)
|
74
|
+
reflection.options[:dependent] == NEW_DEPENDENT_OPTION
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
172
79
|
def delete_with_applicable_criteria(record_class, criteria)
|
173
80
|
applicable_criteria = criteria.select do |column_name, _value|
|
174
81
|
record_class.column_names.include?(column_name.to_s)
|
175
82
|
end
|
176
83
|
record_class.where(applicable_criteria).delete_all
|
177
84
|
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
require 'active_record'
|
182
85
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
assoc_builder = Builder.const_get(assoc_name)
|
187
|
-
assoc_builder.singleton_class.prepend(DeleteRecursively::OptionPermission)
|
86
|
+
def recurse_on?(reflection)
|
87
|
+
enabled_for?(reflection) || destructive?(reflection)
|
88
|
+
end
|
188
89
|
|
189
|
-
|
190
|
-
|
90
|
+
def destructive?(reflection)
|
91
|
+
%i[destroy destroy_all].include?(reflection.options[:dependent])
|
191
92
|
end
|
192
|
-
end
|
193
93
|
|
194
|
-
|
195
|
-
|
196
|
-
DeleteRecursively.delete_records_recursively(self.class, id, force: force)
|
94
|
+
def deleting?(reflection)
|
95
|
+
[:delete, :delete_all, NEW_DEPENDENT_OPTION].include?(reflection.options[:dependent])
|
197
96
|
end
|
198
|
-
end
|
199
97
|
|
200
|
-
|
201
|
-
|
202
|
-
|
98
|
+
def destructive_method(reflection, force: false)
|
99
|
+
if deleting?(reflection) || force && destructive?(reflection)
|
100
|
+
:delete
|
101
|
+
elsif destructive?(reflection)
|
102
|
+
:destroy
|
103
|
+
end
|
203
104
|
end
|
204
105
|
end
|
205
106
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delete_recursively
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janosch Müller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -36,14 +36,14 @@ dependencies:
|
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: '2.
|
39
|
+
version: '2.4'
|
40
40
|
type: :development
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '2.
|
46
|
+
version: '2.4'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: codecov
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,6 +142,10 @@ extensions: []
|
|
142
142
|
extra_rdoc_files: []
|
143
143
|
files:
|
144
144
|
- lib/delete_recursively.rb
|
145
|
+
- lib/delete_recursively/active_record_extensions.rb
|
146
|
+
- lib/delete_recursively/associated_class_finder.rb
|
147
|
+
- lib/delete_recursively/dependent_id_finder.rb
|
148
|
+
- lib/delete_recursively/railtie.rb
|
145
149
|
- lib/delete_recursively/version.rb
|
146
150
|
homepage: https://github.com/jaynetics/delete_recursively
|
147
151
|
licenses:
|
@@ -162,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
166
|
- !ruby/object:Gem::Version
|
163
167
|
version: '0'
|
164
168
|
requirements: []
|
165
|
-
rubygems_version: 3.4.
|
169
|
+
rubygems_version: 3.4.1
|
166
170
|
signing_key:
|
167
171
|
specification_version: 4
|
168
172
|
summary: Delete ActiveRecords efficiently
|