delete_recursively 1.0.2 → 1.1.0

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: 8dd51a14780db543c8f623dcc7d65a1c64a0711c43e9f903b2dd2c1003290145
4
- data.tar.gz: ccaac450fe4293eb267bc0570cc055b097bf78d7f179349a351798578c828e57
3
+ metadata.gz: ffee1566019b0d29f6ae829b45b0ff3ecef4fcbe05dca62d4dc592f6d3c4b13b
4
+ data.tar.gz: 3ee5fbef69a228c8195d4312d24df37ec742c91821598b89041bd002f5b79182
5
5
  SHA512:
6
- metadata.gz: 604ba86881bd30346afa016ff584e1f94b8fa100ea5157e58750e3ff4fce9eb29ba24202b2c84ea1e3bcb0ddcfcac4abf0c5f60f093c6359bc121a952442dc87
7
- data.tar.gz: 61307162f1cf6f673371d04c32d75b6c920a653254c6d54d3e6806abfc059336a17437b2bd523acd8669de0a9e77506275bb341cd88ce8e84221ee1486832f68
6
+ metadata.gz: ffaf462ba56e68682f10540b6e972017e5ded7da75d95644a74a1bd8b2dc962cf84c85aac2561bc3663e121c61731e9bfab497e8329417a4db7254e32fdaa4fa
7
+ data.tar.gz: 640d3df7bf66b5a7c1ba5a5a42d20f297de30f82e77644c85f6325746bb28a0e35c1c475dab375ac5013be838d14229d496475cd30ffa828f96d2ef20ed0d5a9
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeleteRecursively
4
- VERSION = '1.0.2'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -21,34 +21,78 @@ module DeleteRecursively
21
21
  # override Association#handle_dependency to apply the new option if it is set
22
22
  module DependencyHandling
23
23
  def handle_dependency
24
- return super unless DeleteRecursively.enabled_for?(self)
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)
25
32
  if reflection.belongs_to?
26
- # can't use ::dependent_ids, owner is already destroyed at this point
27
- ids = load_target ? target.id : []
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)
28
39
  else
29
- ids = DeleteRecursively.dependent_ids(owner.class, owner.id, reflection)
40
+ DeleteRecursively.delete_recursively(reflection, owner.class, owner.id, force: force)
30
41
  end
31
- DeleteRecursively.delete_recursively(reflection, ids)
32
42
  end
33
43
  end
34
44
 
35
45
  class << self
36
- def delete_recursively(reflection, record_ids)
37
- record_class = reflection.klass
38
- if recurse_on?(reflection)
39
- record_class.reflect_on_all_associations.each do |sub_reflection|
40
- sub_ref_ids = dependent_ids(record_class, record_ids, sub_reflection)
41
- delete_recursively(sub_reflection, sub_ref_ids)
46
+ def delete_recursively(reflection, owner_class, owner_ids, seen: [], force: false)
47
+ # Dependent deletion can be bi-directional, so we need to avoid a loop.
48
+ return if seen.include?(reflection)
49
+
50
+ seen << reflection
51
+
52
+ associated_classes(reflection).each do |record_class|
53
+ record_ids = nil # fetched only when needed for recursion, deletion, or both
54
+
55
+ 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)
59
+ end
60
+ end
61
+
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)
65
+ end
66
+ end
67
+ end
68
+
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 }
42
78
  end
79
+ else
80
+ [reflection.klass]
43
81
  end
44
- destroy_or_delete(reflection, record_class, record_ids)
45
82
  end
46
83
 
47
- def destroy_or_delete(reflection, record_class, record_ids)
48
- if destructive?(reflection)
49
- record_class.destroy(record_ids)
50
- elsif enabled_for?(reflection) || deleting?(reflection)
51
- record_class.delete(record_ids)
84
+ def delete_records_recursively(record_class, record_ids, force: false)
85
+ record_class.reflect_on_all_associations.each do |ref|
86
+ delete_recursively(ref, record_class, record_ids, force: force)
87
+ end
88
+ record_class.delete(record_ids)
89
+ end
90
+
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
52
96
  end
53
97
  end
54
98
 
@@ -65,16 +109,19 @@ module DeleteRecursively
65
109
  end
66
110
 
67
111
  def deleting?(reflection)
68
- %i[delete delete_all].include?(reflection.options[:dependent])
112
+ [:delete, :delete_all, NEW_DEPENDENT_OPTION].include?(reflection.options[:dependent])
69
113
  end
70
114
 
71
- def dependent_ids(owner_class, owner_ids, reflection)
115
+ def dependent_ids(owner_class, owner_ids, reflection, assoc_class = nil)
72
116
  if reflection.belongs_to?
73
- owners_arel = owner_class.where(owner_class.primary_key => owner_ids)
74
- owners_arel.pluck(reflection.association_foreign_key).compact
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
75
122
  elsif reflection.through_reflection
76
123
  dependent_ids_with_through_option(owner_class, owner_ids, reflection)
77
- else
124
+ else # plain `has_many` or `has_one`
78
125
  owner_foreign_key = foreign_key(owner_class, reflection)
79
126
  reflection.klass.where(owner_foreign_key => owner_ids).ids
80
127
  end
@@ -102,16 +149,21 @@ module DeleteRecursively
102
149
  end
103
150
 
104
151
  def foreign_key(owner_class, reflection)
105
- custom_foreign_key = reflection && reflection.options[:foreign_key]
106
- custom_foreign_key || owner_class.to_s.foreign_key
152
+ reflection && reflection.foreign_key || owner_class.to_s.foreign_key
107
153
  end
108
154
 
109
- def all(record_class, criteria = {})
155
+ def all(record_class, criteria = {}, seen = [])
156
+ return if seen.include?(record_class)
157
+
158
+ seen << record_class
159
+
110
160
  record_class.reflect_on_all_associations.each do |reflection|
111
- if recurse_on?(reflection)
112
- all(reflection.klass, criteria)
113
- elsif deleting?(reflection)
114
- delete_with_applicable_criteria(reflection.klass, criteria)
161
+ associated_classes(reflection).each do |assoc_class|
162
+ if recurse_on?(reflection)
163
+ all(assoc_class, criteria, seen)
164
+ elsif deleting?(reflection)
165
+ delete_with_applicable_criteria(assoc_class, criteria)
166
+ end
115
167
  end
116
168
  end
117
169
  delete_with_applicable_criteria(record_class, criteria)
@@ -128,10 +180,26 @@ end
128
180
 
129
181
  require 'active_record'
130
182
 
131
- %w[BelongsTo HasMany HasOne].each do |assoc_name|
132
- assoc_builder = ActiveRecord::Associations::Builder.const_get(assoc_name)
133
- assoc_builder.singleton_class.prepend(DeleteRecursively::OptionPermission)
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)
134
188
 
135
- assoc_class = ActiveRecord::Associations.const_get("#{assoc_name}Association")
136
- assoc_class.prepend(DeleteRecursively::DependencyHandling)
189
+ assoc_class = const_get("#{assoc_name}Association")
190
+ assoc_class.prepend(DeleteRecursively::DependencyHandling)
191
+ end
192
+ end
193
+
194
+ class Base
195
+ def delete_recursively(force: false)
196
+ DeleteRecursively.delete_records_recursively(self.class, id, force: force)
197
+ end
198
+ end
199
+
200
+ class Relation
201
+ def delete_all_recursively(force: false)
202
+ DeleteRecursively.delete_records_recursively(klass, ids, force: force)
203
+ end
204
+ end
137
205
  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.0.2
4
+ version: 1.1.0
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: 2021-12-06 00:00:00.000000000 Z
11
+ date: 2022-11-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -162,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
162
  - !ruby/object:Gem::Version
163
163
  version: '0'
164
164
  requirements: []
165
- rubygems_version: 3.2.32
165
+ rubygems_version: 3.4.0.dev
166
166
  signing_key:
167
167
  specification_version: 4
168
168
  summary: Delete ActiveRecords efficiently