counter_culture 3.9.0 → 3.11.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 +20 -0
- data/lib/counter_culture/configuration.rb +4 -0
- data/lib/counter_culture/counter.rb +66 -9
- data/lib/counter_culture/reconciler.rb +34 -8
- data/lib/counter_culture/version.rb +1 -1
- data/lib/counter_culture.rb +8 -3
- 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: dbbb4f3f637d653c94426d45846cb656d5f199db3706f9d0b2187ca6a67e2be0
|
|
4
|
+
data.tar.gz: 55d928ec95802a130b82b9c25b2a970eca01ee9c07d9b7346731b9e6fb17aaa0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89d37375626c427c86481ea57f0de65e59939982436d13968e223956473d94ab29a5b270715c577348bc8b194f46d6beaae80d8a6fc5554f04e9bf221934bad2
|
|
7
|
+
data.tar.gz: f6a7886d1d5e72b8e0e414e1b83e47b24ff87d2214597565a526f8797c605ee662d2cd3c3fdc3f540368c424559a2eedad2c634e8713725ed2f7921ebab3b701
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 3.11.0 (June 26, 2025)
|
|
2
|
+
|
|
3
|
+
New features:
|
|
4
|
+
- Support for composite primary keys in Rails v7.2+ (#413)
|
|
5
|
+
|
|
6
|
+
## 3.10.2 (June 24, 2025)
|
|
7
|
+
|
|
8
|
+
Bugfixes:
|
|
9
|
+
- Fix incorrect in-memory counter updates on `belongs_to` association (#415)
|
|
10
|
+
|
|
11
|
+
## 3.10.1 (April 30, 2025)
|
|
12
|
+
|
|
13
|
+
Bugfixes:
|
|
14
|
+
- Fix issue when using `delegate` instead of `has_many :through` (#411)
|
|
15
|
+
|
|
16
|
+
## 3.10.0 (April 15, 2025)
|
|
17
|
+
|
|
18
|
+
New features:
|
|
19
|
+
- Update counter cache values without requiring a reload (#407)
|
|
20
|
+
|
|
1
21
|
## 3.9.0 (March 20, 2025)
|
|
2
22
|
|
|
3
23
|
New features:
|
|
@@ -113,7 +113,9 @@ module CounterCulture
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
if @with_papertrail
|
|
116
|
-
|
|
116
|
+
conditions = primary_key_conditions(primary_key, id_to_change)
|
|
117
|
+
instance = klass.where(conditions).first
|
|
118
|
+
|
|
117
119
|
if instance
|
|
118
120
|
if instance.paper_trail.respond_to?(:save_with_version)
|
|
119
121
|
# touch_with_version is deprecated starting in PaperTrail 9.0.0
|
|
@@ -137,7 +139,11 @@ module CounterCulture
|
|
|
137
139
|
|
|
138
140
|
unless Thread.current[:aggregate_counter_updates]
|
|
139
141
|
execute_now_or_after_commit(obj) do
|
|
140
|
-
|
|
142
|
+
conditions = primary_key_conditions(primary_key, id_to_change)
|
|
143
|
+
klass.where(conditions).update_all updates.join(', ')
|
|
144
|
+
unless options[:was]
|
|
145
|
+
assign_to_associated_object(obj, relation, change_counter_column, operator, delta_magnitude)
|
|
146
|
+
end
|
|
141
147
|
end
|
|
142
148
|
end
|
|
143
149
|
end
|
|
@@ -171,7 +177,7 @@ module CounterCulture
|
|
|
171
177
|
|
|
172
178
|
# the string to pass to order() in order to sort by primary key
|
|
173
179
|
def full_primary_key(klass)
|
|
174
|
-
"#{klass.quoted_table_name}.#{
|
|
180
|
+
Array.wrap(klass.quoted_primary_key).map { |pk| "#{klass.quoted_table_name}.#{pk}" }.join(', ')
|
|
175
181
|
end
|
|
176
182
|
|
|
177
183
|
# gets the value of the foreign key on the given relation
|
|
@@ -185,23 +191,24 @@ module CounterCulture
|
|
|
185
191
|
original_relation = relation
|
|
186
192
|
relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
|
|
187
193
|
|
|
188
|
-
if was
|
|
194
|
+
value = if was
|
|
189
195
|
first = relation.shift
|
|
190
196
|
foreign_key_value = attribute_was(obj, relation_foreign_key(first))
|
|
191
197
|
klass = relation_klass(first, source: obj, was: was)
|
|
192
198
|
if foreign_key_value
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
primary_key = relation_primary_key(first, source: obj, was: was)
|
|
200
|
+
conditions = primary_key_conditions(primary_key, foreign_key_value)
|
|
201
|
+
klass.where(conditions).first
|
|
196
202
|
end
|
|
197
203
|
else
|
|
198
|
-
|
|
204
|
+
obj
|
|
199
205
|
end
|
|
200
206
|
while !value.nil? && relation.size > 0
|
|
201
207
|
value = value.send(relation.shift)
|
|
202
208
|
end
|
|
203
209
|
|
|
204
|
-
|
|
210
|
+
primary_key = relation_primary_key(original_relation, source: obj, was: was)
|
|
211
|
+
Array.wrap(primary_key).map { |pk| value.try(pk&.to_sym) }.compact.presence
|
|
205
212
|
end
|
|
206
213
|
|
|
207
214
|
# gets the reflect object on the given relation
|
|
@@ -307,6 +314,7 @@ module CounterCulture
|
|
|
307
314
|
return reflect.options[:primary_key] if reflect.options.key?(:primary_key)
|
|
308
315
|
return relation_klass(relation, source: source, was: was).try(:primary_key)
|
|
309
316
|
end
|
|
317
|
+
|
|
310
318
|
reflect.association_primary_key(klass)
|
|
311
319
|
end
|
|
312
320
|
|
|
@@ -365,6 +373,49 @@ module CounterCulture
|
|
|
365
373
|
obj.public_send("#{attr}#{changes_method}")
|
|
366
374
|
end
|
|
367
375
|
|
|
376
|
+
# update associated object counter attribute
|
|
377
|
+
def assign_to_associated_object(obj, relation, change_counter_column, operator, delta_magnitude)
|
|
378
|
+
association_name = relation_reflect(relation).name
|
|
379
|
+
|
|
380
|
+
association_object = association_object_for_assign(obj, association_name)
|
|
381
|
+
return if association_object.blank?
|
|
382
|
+
|
|
383
|
+
association_object.assign_attributes(
|
|
384
|
+
change_counter_column =>
|
|
385
|
+
association_object_new_counter(association_object, change_counter_column, operator, delta_magnitude)
|
|
386
|
+
)
|
|
387
|
+
association_object_clear_change(association_object, change_counter_column)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def association_object_for_assign(obj, association_name)
|
|
391
|
+
if obj.class.reflect_on_all_associations.
|
|
392
|
+
find { |assoc| assoc.name.to_sym == association_name.to_sym }.
|
|
393
|
+
blank?
|
|
394
|
+
# This means that this isn't defined as an association; either it
|
|
395
|
+
# doesn't exist at all, or it uses delegate. In either case, we can't
|
|
396
|
+
# update the count this way.
|
|
397
|
+
return
|
|
398
|
+
end
|
|
399
|
+
if !obj.association(association_name).loaded?
|
|
400
|
+
# If the association isn't loaded, no need to update the count
|
|
401
|
+
return
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
obj.public_send(association_name)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def association_object_clear_change(association_object, change_counter_column)
|
|
408
|
+
if ACTIVE_RECORD_VERSION >= Gem::Version.new("6.1.0")
|
|
409
|
+
association_object.public_send(:"clear_#{change_counter_column}_change")
|
|
410
|
+
else
|
|
411
|
+
association_object.send(:clear_attribute_change, change_counter_column)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def association_object_new_counter(association_object, change_counter_column, operator, delta_magnitude)
|
|
416
|
+
(association_object.public_send(change_counter_column) || 0).public_send(operator, delta_magnitude)
|
|
417
|
+
end
|
|
418
|
+
|
|
368
419
|
def assemble_money_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
|
|
369
420
|
counter_update_snippet(
|
|
370
421
|
"#{quoted_column} = COALESCE(CAST(#{quoted_column} as NUMERIC), 0)",
|
|
@@ -395,6 +446,12 @@ module CounterCulture
|
|
|
395
446
|
end
|
|
396
447
|
end
|
|
397
448
|
|
|
449
|
+
def primary_key_conditions(primary_key, fk_value)
|
|
450
|
+
Array.wrap(primary_key)
|
|
451
|
+
.zip(Array.wrap(fk_value))
|
|
452
|
+
.to_h
|
|
453
|
+
end
|
|
454
|
+
|
|
398
455
|
def counter_update_snippet(update, klass, id_to_change, operator, delta_magnitude)
|
|
399
456
|
if Thread.current[:aggregate_counter_updates]
|
|
400
457
|
remember_counter_update(
|
|
@@ -111,8 +111,8 @@ module CounterCulture
|
|
|
111
111
|
|
|
112
112
|
# select join column and count (from above) as well as cache column ('column_name') for later comparison
|
|
113
113
|
counts_query = scope.select(
|
|
114
|
-
"#{
|
|
115
|
-
"#{
|
|
114
|
+
"#{primary_key_select}, " \
|
|
115
|
+
"#{association_primary_key_select}, " \
|
|
116
116
|
"#{count_select} AS count, " \
|
|
117
117
|
"MAX(#{relation_class_table_name}.#{column_name}) AS #{column_name}"
|
|
118
118
|
)
|
|
@@ -173,7 +173,8 @@ module CounterCulture
|
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
with_writing_db_connection do
|
|
176
|
-
|
|
176
|
+
conditions = Array.wrap(relation_class.primary_key).map { |key| [key, record.send(key)] }.to_h
|
|
177
|
+
relation_class.where(conditions).update_all(updates.join(', '))
|
|
177
178
|
end
|
|
178
179
|
end
|
|
179
180
|
end
|
|
@@ -199,11 +200,12 @@ module CounterCulture
|
|
|
199
200
|
def track_change(record, column_name, count)
|
|
200
201
|
@changes_holder << {
|
|
201
202
|
:entity => relation_class.name,
|
|
202
|
-
relation_class.primary_key.to_sym => record.send(relation_class.primary_key),
|
|
203
203
|
:what => column_name,
|
|
204
204
|
:wrong => record.send(column_name),
|
|
205
205
|
:right => count
|
|
206
|
-
}
|
|
206
|
+
}.tap do |h|
|
|
207
|
+
Array.wrap(relation_class.primary_key).each { |pk| h[pk.to_sym] = record.send(pk) }
|
|
208
|
+
end
|
|
207
209
|
end
|
|
208
210
|
|
|
209
211
|
def count_select
|
|
@@ -217,10 +219,23 @@ module CounterCulture
|
|
|
217
219
|
@count_select = "SUM(COALESCE(#{self_table_name}.#{delta_column}, 0))"
|
|
218
220
|
end
|
|
219
221
|
else
|
|
220
|
-
|
|
222
|
+
primary_key = Array.wrap(model.primary_key).first
|
|
223
|
+
count_column = "#{self_table_name}.#{primary_key}"
|
|
224
|
+
@count_select = "COUNT(#{count_column})*#{delta_magnitude}"
|
|
221
225
|
end
|
|
222
226
|
end
|
|
223
227
|
|
|
228
|
+
def primary_key_select
|
|
229
|
+
relation_class_table_name = quote_table_name(relation_class.table_name)
|
|
230
|
+
Array.wrap(relation_class.primary_key).map { |pk| "#{relation_class_table_name}.#{pk}" }.join(', ')
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def association_primary_key_select
|
|
234
|
+
relation_class_table_name = quote_table_name(relation_class.table_name)
|
|
235
|
+
association_primary_key = relation_reflect(relation).association_primary_key(relation_class)
|
|
236
|
+
Array.wrap(association_primary_key).map { |apk| "#{relation_class_table_name}.#{apk}" }.join(', ')
|
|
237
|
+
end
|
|
238
|
+
|
|
224
239
|
def self_table_name
|
|
225
240
|
return @self_table_name if @self_table_name
|
|
226
241
|
|
|
@@ -271,8 +286,17 @@ module CounterCulture
|
|
|
271
286
|
[target_table_key, source_table_key]
|
|
272
287
|
end
|
|
273
288
|
|
|
289
|
+
source_table_key = Array.wrap(source_table_key)
|
|
290
|
+
target_table_key = Array.wrap(target_table_key)
|
|
291
|
+
|
|
292
|
+
join_conditions =
|
|
293
|
+
source_table_key
|
|
294
|
+
.zip(target_table_key).map do |source_key, target_key|
|
|
295
|
+
"#{source_table}.#{source_key} = #{target_table_alias}.#{target_key}"
|
|
296
|
+
end.join(' AND ')
|
|
274
297
|
joins_sql = "LEFT JOIN #{target_table} AS #{target_table_alias} "\
|
|
275
|
-
"ON #{
|
|
298
|
+
"ON #{join_conditions}"
|
|
299
|
+
|
|
276
300
|
# adds 'type' condition to JOIN clause if the current model is a
|
|
277
301
|
# child in a Single Table Inheritance
|
|
278
302
|
if reflect.active_record.column_names.include?('type') &&
|
|
@@ -289,7 +313,9 @@ module CounterCulture
|
|
|
289
313
|
# conditions must be applied to the join on which we are counting
|
|
290
314
|
if where
|
|
291
315
|
if where.respond_to?(:to_sql)
|
|
292
|
-
|
|
316
|
+
model_primary_key = Array.wrap(model.primary_key)
|
|
317
|
+
where_select = model_primary_key.map { |pk| "#{model.table_name}.#{pk}" }.join(', ')
|
|
318
|
+
joins_sql += " AND (#{target_table_alias}.#{model_primary_key.first}) IN (#{where.select(where_select).to_sql})"
|
|
293
319
|
else
|
|
294
320
|
joins_sql += " AND (#{model.send(:sanitize_sql_for_conditions, where)})"
|
|
295
321
|
end
|
data/lib/counter_culture.rb
CHANGED
|
@@ -35,9 +35,14 @@ module CounterCulture
|
|
|
35
35
|
end.compact
|
|
36
36
|
|
|
37
37
|
if update_snippets.any?
|
|
38
|
-
klass
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
primary_key = Thread.current[:primary_key_map][klass]
|
|
39
|
+
|
|
40
|
+
conditions =
|
|
41
|
+
Array.wrap(primary_key)
|
|
42
|
+
.zip(Array.wrap(rec_id))
|
|
43
|
+
.to_h
|
|
44
|
+
|
|
45
|
+
klass.where(conditions).update_all(update_snippets.join(', '))
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
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.11.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: 2025-
|
|
11
|
+
date: 2025-06-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|