delete_recursively 1.1.0 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffee1566019b0d29f6ae829b45b0ff3ecef4fcbe05dca62d4dc592f6d3c4b13b
4
- data.tar.gz: 3ee5fbef69a228c8195d4312d24df37ec742c91821598b89041bd002f5b79182
3
+ metadata.gz: 29f88e1700c522e296195ab96eb64013bb981e0e2429ea2ca6757a424fd35059
4
+ data.tar.gz: 991c67f4fe3eab0c550c87fc593b65b297a25c2c0a0756d0e6b3b74318492c52
5
5
  SHA512:
6
- metadata.gz: ffaf462ba56e68682f10540b6e972017e5ded7da75d95644a74a1bd8b2dc962cf84c85aac2561bc3663e121c61731e9bfab497e8329417a4db7254e32fdaa4fa
7
- data.tar.gz: 640d3df7bf66b5a7c1ba5a5a42d20f297de30f82e77644c85f6325746bb28a0e35c1c475dab375ac5013be838d14229d496475cd30ffa828f96d2ef20ed0d5a9
6
+ metadata.gz: 3485c705152d4dd9cd8003fda660de77bad678d4880c0d0bd4910748297f8b77b18696010131d7efbf4443a6cb97f48c8fddf43bf91c6cbc73526ad9b7585e55
7
+ data.tar.gz: a34f8a1fc2a138458cf690bde31a34d2ec91b04f8e316f8bdbaed078f5df165487f7f5f04822704ec56f3b5d51135e92f2aa34ffaa89357bdae9fd3db7966ef7
@@ -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.2'
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) unless record_ids.empty?
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.2
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-27 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