counter_culture 3.10.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 +15 -0
- data/lib/counter_culture/configuration.rb +4 -0
- data/lib/counter_culture/counter.rb +46 -17
- 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,18 @@
|
|
|
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
|
+
|
|
1
16
|
## 3.10.0 (April 15, 2025)
|
|
2
17
|
|
|
3
18
|
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,8 +139,11 @@ module CounterCulture
|
|
|
137
139
|
|
|
138
140
|
unless Thread.current[:aggregate_counter_updates]
|
|
139
141
|
execute_now_or_after_commit(obj) do
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
142
147
|
end
|
|
143
148
|
end
|
|
144
149
|
end
|
|
@@ -172,7 +177,7 @@ module CounterCulture
|
|
|
172
177
|
|
|
173
178
|
# the string to pass to order() in order to sort by primary key
|
|
174
179
|
def full_primary_key(klass)
|
|
175
|
-
"#{klass.quoted_table_name}.#{
|
|
180
|
+
Array.wrap(klass.quoted_primary_key).map { |pk| "#{klass.quoted_table_name}.#{pk}" }.join(', ')
|
|
176
181
|
end
|
|
177
182
|
|
|
178
183
|
# gets the value of the foreign key on the given relation
|
|
@@ -186,23 +191,24 @@ module CounterCulture
|
|
|
186
191
|
original_relation = relation
|
|
187
192
|
relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
|
|
188
193
|
|
|
189
|
-
if was
|
|
194
|
+
value = if was
|
|
190
195
|
first = relation.shift
|
|
191
196
|
foreign_key_value = attribute_was(obj, relation_foreign_key(first))
|
|
192
197
|
klass = relation_klass(first, source: obj, was: was)
|
|
193
198
|
if foreign_key_value
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
197
202
|
end
|
|
198
203
|
else
|
|
199
|
-
|
|
204
|
+
obj
|
|
200
205
|
end
|
|
201
206
|
while !value.nil? && relation.size > 0
|
|
202
207
|
value = value.send(relation.shift)
|
|
203
208
|
end
|
|
204
209
|
|
|
205
|
-
|
|
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
|
|
206
212
|
end
|
|
207
213
|
|
|
208
214
|
# gets the reflect object on the given relation
|
|
@@ -308,6 +314,7 @@ module CounterCulture
|
|
|
308
314
|
return reflect.options[:primary_key] if reflect.options.key?(:primary_key)
|
|
309
315
|
return relation_klass(relation, source: source, was: was).try(:primary_key)
|
|
310
316
|
end
|
|
317
|
+
|
|
311
318
|
reflect.association_primary_key(klass)
|
|
312
319
|
end
|
|
313
320
|
|
|
@@ -370,15 +377,31 @@ module CounterCulture
|
|
|
370
377
|
def assign_to_associated_object(obj, relation, change_counter_column, operator, delta_magnitude)
|
|
371
378
|
association_name = relation_reflect(relation).name
|
|
372
379
|
|
|
373
|
-
|
|
380
|
+
association_object = association_object_for_assign(obj, association_name)
|
|
381
|
+
return if association_object.blank?
|
|
374
382
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
change_counter_column
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
381
402
|
end
|
|
403
|
+
|
|
404
|
+
obj.public_send(association_name)
|
|
382
405
|
end
|
|
383
406
|
|
|
384
407
|
def association_object_clear_change(association_object, change_counter_column)
|
|
@@ -423,6 +446,12 @@ module CounterCulture
|
|
|
423
446
|
end
|
|
424
447
|
end
|
|
425
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
|
+
|
|
426
455
|
def counter_update_snippet(update, klass, id_to_change, operator, delta_magnitude)
|
|
427
456
|
if Thread.current[:aggregate_counter_updates]
|
|
428
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
|