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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffee1566019b0d29f6ae829b45b0ff3ecef4fcbe05dca62d4dc592f6d3c4b13b
4
- data.tar.gz: 3ee5fbef69a228c8195d4312d24df37ec742c91821598b89041bd002f5b79182
3
+ metadata.gz: b030ca2cd0433b8f6edbe713ec5e75651cfd8b17b09ce8080c0fe9867b85799d
4
+ data.tar.gz: db4adfc88b256876c756cd7f216192b324c4c2a56892c57e00961d19c8121fea
5
5
  SHA512:
6
- metadata.gz: ffaf462ba56e68682f10540b6e972017e5ded7da75d95644a74a1bd8b2dc962cf84c85aac2561bc3663e121c61731e9bfab497e8329417a4db7254e32fdaa4fa
7
- data.tar.gz: 640d3df7bf66b5a7c1ba5a5a42d20f297de30f82e77644c85f6325746bb28a0e35c1c475dab375ac5013be838d14229d496475cd30ffa828f96d2ef20ed0d5a9
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
@@ -0,0 +1,5 @@
1
+ class DeleteRecursively::Railtie < ::Rails::Railtie
2
+ config.to_prepare do
3
+ DeleteRecursively::AssociatedClassFinder.clear_cache
4
+ end
5
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeleteRecursively
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.1'
5
5
  end
@@ -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
- # override ::valid_dependent_options to make the new
14
- # dependent option available to Association::Builder classes
15
- module OptionPermission
16
- def valid_dependent_options
17
- super + [NEW_DEPENDENT_OPTION]
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, owner_class, owner_ids, seen: [], force: false)
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
- return if seen.include?(reflection)
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 << reflection
29
+ seen << recursion_identifier
51
30
 
52
- associated_classes(reflection).each do |record_class|
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 = dependent_ids(owner_class, owner_ids, reflection, record_class)
57
- record_class.reflect_on_all_associations.each do |subref|
58
- delete_recursively(subref, record_class, record_ids, seen: seen, force: force)
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, record_class, record_ids, force: force)
63
- record_ids ||= dependent_ids(owner_class, owner_ids, reflection, record_class)
64
- record_class.send(dest_method, record_ids)
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, record_class, record_ids, force: force)
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
- associated_classes(reflection).each do |assoc_class|
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
- module ActiveRecord
184
- module Associations
185
- %w[BelongsTo HasMany HasOne].each do |assoc_name|
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
- assoc_class = const_get("#{assoc_name}Association")
190
- assoc_class.prepend(DeleteRecursively::DependencyHandling)
90
+ def destructive?(reflection)
91
+ %i[destroy destroy_all].include?(reflection.options[:dependent])
191
92
  end
192
- end
193
93
 
194
- class Base
195
- def delete_recursively(force: false)
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
- class Relation
201
- def delete_all_recursively(force: false)
202
- DeleteRecursively.delete_records_recursively(klass, ids, force: force)
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.0
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: 2022-11-29 00:00:00.000000000 Z
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.3'
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.3'
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.0.dev
169
+ rubygems_version: 3.4.1
166
170
  signing_key:
167
171
  specification_version: 4
168
172
  summary: Delete ActiveRecords efficiently