counter_culture 3.12.2 → 3.13.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +19 -0
- data/lib/counter_culture/counter.rb +2 -1
- data/lib/counter_culture/extensions.rb +113 -26
- data/lib/counter_culture/reconciler.rb +6 -2
- data/lib/counter_culture/skip_updates.rb +14 -18
- data/lib/counter_culture/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce955a1cc71481d0223d0477b44823a9ed539e2209a77a0db5a5f90f09d87259
|
|
4
|
+
data.tar.gz: 68e564d152930f8fd663acbb99e64c0b5ed7ff451358b4f8439364d807b672c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f77b8bece6b1242b3402c08cfece835f59d721ecef1f8330c66dc65a42a2ba6797c605f062e414a9d9a2f55d9113f6aa597176fd0e422221c16ee8f9a2648aaa
|
|
7
|
+
data.tar.gz: be715fdc0be1bfa8be1b555fcb28630f770012eee35459865520469a5211ab9f37e0894816cfcbd2d6f015d2704aa0b59eea44ed77a2c2cccdef230c373eac1e
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -587,6 +587,25 @@ class SoftDelete < ActiveRecord::Base
|
|
|
587
587
|
end
|
|
588
588
|
```
|
|
589
589
|
|
|
590
|
+
#### Counting soft-deleted records
|
|
591
|
+
|
|
592
|
+
By default, soft-deleted records are excluded from counter caches. If you want a
|
|
593
|
+
counter that includes soft-deleted records, use the `include_soft_deleted` option:
|
|
594
|
+
|
|
595
|
+
```ruby
|
|
596
|
+
class SoftDelete < ActiveRecord::Base
|
|
597
|
+
acts_as_paranoid # or: include Discard::Model
|
|
598
|
+
|
|
599
|
+
belongs_to :company
|
|
600
|
+
counter_culture :company, column_name: 'all_records_count', include_soft_deleted: true
|
|
601
|
+
end
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
With this option, soft-delete and restore operations will not change the counter.
|
|
605
|
+
Only hard-destroying a record (`really_destroy!` for Paranoia, `destroy` for
|
|
606
|
+
Discard) will decrement the counter. `counter_culture_fix_counts` will also
|
|
607
|
+
correctly include soft-deleted records when reconciling.
|
|
608
|
+
|
|
590
609
|
### PaperTrail integration
|
|
591
610
|
|
|
592
611
|
If you are using the [`paper_trail` gem](https://github.com/airblade/paper_trail)
|
|
@@ -2,7 +2,7 @@ require_relative './with_connection'
|
|
|
2
2
|
|
|
3
3
|
module CounterCulture
|
|
4
4
|
class Counter
|
|
5
|
-
CONFIG_OPTIONS = [ :column_names, :counter_cache_name, :delta_column, :foreign_key_values, :touch, :delta_magnitude, :execute_after_commit ]
|
|
5
|
+
CONFIG_OPTIONS = [ :column_names, :counter_cache_name, :delta_column, :foreign_key_values, :touch, :delta_magnitude, :execute_after_commit, :include_soft_deleted ]
|
|
6
6
|
ACTIVE_RECORD_VERSION = Gem.loaded_specs["activerecord"].version
|
|
7
7
|
|
|
8
8
|
attr_reader :model, :relation, *CONFIG_OPTIONS
|
|
@@ -19,6 +19,7 @@ module CounterCulture
|
|
|
19
19
|
@delta_magnitude = options[:delta_magnitude] || 1
|
|
20
20
|
@with_papertrail = options.fetch(:with_papertrail, false)
|
|
21
21
|
@execute_after_commit = options.fetch(:execute_after_commit, false)
|
|
22
|
+
@include_soft_deleted = options.fetch(:include_soft_deleted, false)
|
|
22
23
|
|
|
23
24
|
if @execute_after_commit
|
|
24
25
|
begin
|
|
@@ -17,28 +17,21 @@ module CounterCulture
|
|
|
17
17
|
unless @after_commit_counter_cache
|
|
18
18
|
# initialize callbacks only once
|
|
19
19
|
after_create :_update_counts_after_create
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
before_destroy :_update_counts_before_destroy
|
|
21
|
+
after_update :_update_counts_after_update
|
|
22
22
|
|
|
23
23
|
if respond_to?(:before_real_destroy) &&
|
|
24
24
|
instance_methods.include?(:paranoia_destroyed?)
|
|
25
|
-
before_real_destroy :
|
|
26
|
-
if: -> (model) { !model.paranoia_destroyed? }
|
|
25
|
+
before_real_destroy :_update_counts_before_real_destroy
|
|
27
26
|
end
|
|
28
27
|
|
|
29
|
-
after_update :_update_counts_after_update, unless: :destroyed_for_counter_culture?
|
|
30
|
-
|
|
31
28
|
if respond_to?(:before_restore)
|
|
32
|
-
before_restore :
|
|
33
|
-
if: -> (model) { model.deleted? }
|
|
29
|
+
before_restore :_update_counts_before_restore
|
|
34
30
|
end
|
|
35
31
|
|
|
36
32
|
if defined?(Discard::Model) && include?(Discard::Model)
|
|
37
|
-
before_discard :
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
before_undiscard :_update_counts_after_create,
|
|
41
|
-
if: ->(model) { model.discarded? }
|
|
33
|
+
before_discard :_update_counts_before_discard
|
|
34
|
+
before_undiscard :_update_counts_before_undiscard
|
|
42
35
|
end
|
|
43
36
|
|
|
44
37
|
# we keep a list of all counter caches we must maintain
|
|
@@ -104,29 +97,77 @@ module CounterCulture
|
|
|
104
97
|
end
|
|
105
98
|
|
|
106
99
|
private
|
|
107
|
-
|
|
100
|
+
|
|
108
101
|
def _update_counts_after_create
|
|
109
102
|
self.class.after_commit_counter_cache.each do |counter|
|
|
110
|
-
# increment counter cache
|
|
111
103
|
counter.change_counter_cache(self, :increment => true)
|
|
112
104
|
end
|
|
113
105
|
end
|
|
114
106
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
# before_destroy: For Paranoia models this is a soft-delete; for all others
|
|
108
|
+
# (including Discard) it is a hard-destroy.
|
|
109
|
+
def _update_counts_before_destroy
|
|
110
|
+
# If destroy is called again on an already-destroyed record, don't double-decrement
|
|
111
|
+
return if destroyed?
|
|
112
|
+
|
|
113
|
+
if respond_to?(:paranoia_destroyed?)
|
|
114
|
+
# Paranoia: destroy = soft-delete
|
|
115
|
+
# If already soft-deleted, this is a redundant destroy call — don't decrement again
|
|
116
|
+
return if paranoia_destroyed?
|
|
117
|
+
_decrement_counters(skip_include_soft_deleted: true)
|
|
118
|
+
_handle_include_soft_deleted_column_transition
|
|
119
|
+
elsif destroyed_for_counter_culture?
|
|
120
|
+
# Discard: hard-destroy of already-discarded record
|
|
121
|
+
_decrement_counters(only_include_soft_deleted: true)
|
|
122
|
+
else
|
|
123
|
+
# Regular model or undiscarded Discard: hard-destroy
|
|
124
|
+
_decrement_counters
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# before_real_destroy (Paranoia only)
|
|
129
|
+
def _update_counts_before_real_destroy
|
|
130
|
+
if paranoia_destroyed?
|
|
131
|
+
# Was already soft-deleted: only include_soft_deleted counters remain
|
|
132
|
+
_decrement_counters(only_include_soft_deleted: true)
|
|
133
|
+
else
|
|
134
|
+
# Not soft-deleted: decrement all counters
|
|
135
|
+
_decrement_counters
|
|
122
136
|
end
|
|
123
137
|
end
|
|
124
138
|
|
|
125
|
-
#
|
|
139
|
+
# before_restore (Paranoia only)
|
|
140
|
+
def _update_counts_before_restore
|
|
141
|
+
# Only increment if the record is currently soft-deleted (idempotency guard)
|
|
142
|
+
return unless deleted?
|
|
143
|
+
_increment_counters(skip_include_soft_deleted: true)
|
|
144
|
+
_handle_include_soft_deleted_column_transition
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# before_discard (Discard only)
|
|
148
|
+
# Note: Discard's discard/undiscard use update_attribute which triggers
|
|
149
|
+
# after_update, so include_soft_deleted column transitions are handled
|
|
150
|
+
# there via the normal column_name change detection — no need to call
|
|
151
|
+
# _handle_include_soft_deleted_column_transition here.
|
|
152
|
+
def _update_counts_before_discard
|
|
153
|
+
# Only decrement if not already discarded (idempotency guard)
|
|
154
|
+
return if discarded?
|
|
155
|
+
_decrement_counters(skip_include_soft_deleted: true)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# before_undiscard (Discard only)
|
|
159
|
+
def _update_counts_before_undiscard
|
|
160
|
+
# Only increment if currently discarded (idempotency guard)
|
|
161
|
+
return unless discarded?
|
|
162
|
+
_increment_counters(skip_include_soft_deleted: true)
|
|
163
|
+
end
|
|
164
|
+
|
|
126
165
|
def _update_counts_after_update
|
|
127
166
|
self.class.after_commit_counter_cache.each do |counter|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
167
|
+
# Don't update regular counters for soft-deleted records, but
|
|
168
|
+
# include_soft_deleted counters must still track association changes
|
|
169
|
+
next if destroyed_for_counter_culture? && !counter.include_soft_deleted
|
|
170
|
+
|
|
130
171
|
counter_cache_name_was = counter.counter_cache_name_for(counter.previous_model(self))
|
|
131
172
|
counter_cache_name = counter.counter_cache_name_for(self)
|
|
132
173
|
|
|
@@ -142,7 +183,53 @@ module CounterCulture
|
|
|
142
183
|
end
|
|
143
184
|
end
|
|
144
185
|
|
|
145
|
-
|
|
186
|
+
def _decrement_counters(skip_include_soft_deleted: false, only_include_soft_deleted: false)
|
|
187
|
+
self.class.after_commit_counter_cache.each do |counter|
|
|
188
|
+
next if skip_include_soft_deleted && counter.include_soft_deleted
|
|
189
|
+
next if only_include_soft_deleted && !counter.include_soft_deleted
|
|
190
|
+
counter.change_counter_cache(self, :increment => false)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def _increment_counters(skip_include_soft_deleted: false)
|
|
195
|
+
self.class.after_commit_counter_cache.each do |counter|
|
|
196
|
+
next if skip_include_soft_deleted && counter.include_soft_deleted
|
|
197
|
+
counter.change_counter_cache(self, :increment => true)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# For include_soft_deleted counters with dynamic column_name Procs,
|
|
202
|
+
# evaluate the Proc on both the current state and the projected future
|
|
203
|
+
# state (after soft-delete/restore completes) to detect column transitions.
|
|
204
|
+
def _handle_include_soft_deleted_column_transition
|
|
205
|
+
future = _soft_delete_future_model
|
|
206
|
+
self.class.after_commit_counter_cache.each do |counter|
|
|
207
|
+
next unless counter.include_soft_deleted
|
|
208
|
+
column_was = counter.counter_cache_name_for(self)
|
|
209
|
+
column_now = counter.counter_cache_name_for(future)
|
|
210
|
+
next if column_was == column_now
|
|
211
|
+
counter.change_counter_cache(self, :increment => true, :counter_column => column_now) if column_now
|
|
212
|
+
counter.change_counter_cache(self, :increment => false, :counter_column => column_was) if column_was
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Creates a dup of the model with the soft-delete column toggled,
|
|
217
|
+
# so dynamic column_name Procs can be evaluated against the future state.
|
|
218
|
+
def _soft_delete_future_model
|
|
219
|
+
future = self.dup
|
|
220
|
+
if respond_to?(:paranoia_destroyed?)
|
|
221
|
+
col = 'deleted_at'
|
|
222
|
+
val = paranoia_destroyed? ? nil : Time.current
|
|
223
|
+
elsif defined?(Discard::Model) && self.class.include?(Discard::Model)
|
|
224
|
+
col = self.class.discard_column.to_s
|
|
225
|
+
val = discarded? ? nil : Time.current
|
|
226
|
+
else
|
|
227
|
+
return future
|
|
228
|
+
end
|
|
229
|
+
future.instance_variable_get(:@attributes).write_from_user(col, val)
|
|
230
|
+
future
|
|
231
|
+
end
|
|
232
|
+
|
|
146
233
|
def destroyed_for_counter_culture?
|
|
147
234
|
if respond_to?(:paranoia_destroyed?)
|
|
148
235
|
paranoia_destroyed?
|
|
@@ -312,7 +312,9 @@ module CounterCulture
|
|
|
312
312
|
|
|
313
313
|
# respect the deleted_at column if it exists
|
|
314
314
|
if model.column_names.include?('deleted_at')
|
|
315
|
-
|
|
315
|
+
unless include_soft_deleted
|
|
316
|
+
joins_sql += " AND #{target_table_alias}.deleted_at IS NULL"
|
|
317
|
+
end
|
|
316
318
|
end
|
|
317
319
|
|
|
318
320
|
# respect the discard column if it exists
|
|
@@ -320,7 +322,9 @@ module CounterCulture
|
|
|
320
322
|
model.include?(Discard::Model) &&
|
|
321
323
|
model.column_names.include?(model.discard_column.to_s)
|
|
322
324
|
|
|
323
|
-
|
|
325
|
+
unless include_soft_deleted
|
|
326
|
+
joins_sql += " AND #{target_table_alias}.#{model.discard_column} IS NULL"
|
|
327
|
+
end
|
|
324
328
|
end
|
|
325
329
|
if index == reverse_relation.size - 1
|
|
326
330
|
# conditions must be applied to the join on which we are counting
|
|
@@ -2,24 +2,20 @@ module CounterCulture
|
|
|
2
2
|
module SkipUpdates
|
|
3
3
|
private
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# called by after_update callback
|
|
20
|
-
def _update_counts_after_update
|
|
21
|
-
unless Array(Thread.current[:skip_counter_culture_updates]).include?(self.class)
|
|
22
|
-
super
|
|
5
|
+
%i[
|
|
6
|
+
_update_counts_after_create
|
|
7
|
+
_update_counts_before_destroy
|
|
8
|
+
_update_counts_after_update
|
|
9
|
+
_update_counts_before_real_destroy
|
|
10
|
+
_update_counts_before_restore
|
|
11
|
+
_update_counts_before_discard
|
|
12
|
+
_update_counts_before_undiscard
|
|
13
|
+
_handle_include_soft_deleted_column_transition
|
|
14
|
+
].each do |method_name|
|
|
15
|
+
define_method(method_name) do
|
|
16
|
+
unless Array(Thread.current[:skip_counter_culture_updates]).include?(self.class)
|
|
17
|
+
super()
|
|
18
|
+
end
|
|
23
19
|
end
|
|
24
20
|
end
|
|
25
21
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: counter_culture
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Magnus von Koeller
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|