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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f93d3d3e415f5d92e3870caf67067cdb75a6a76f68542b82c9ec2411987a0e4d
4
- data.tar.gz: 89cf5171b549aab368a1dd87117afd429bf557d93f198e220fbe3b2a24e81f20
3
+ metadata.gz: dbbb4f3f637d653c94426d45846cb656d5f199db3706f9d0b2187ca6a67e2be0
4
+ data.tar.gz: 55d928ec95802a130b82b9c25b2a970eca01ee9c07d9b7346731b9e6fb17aaa0
5
5
  SHA512:
6
- metadata.gz: 17b50c40207f615d0018c142062e536c3da69bebab209d4facb63ee03273363d97f83675d173f3096f98c68393f374e39aae0dba0db6364de9bc3c83d38af160
7
- data.tar.gz: d7bef1c4214a30fa9888843c35fe2f074ea89e78b9341b5ed458471934576dea9ef8bf12fac72bd549a532fc50d34b9e327c4f2b943e7965720f3b12bef503b6
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:
@@ -11,6 +11,10 @@ module CounterCulture
11
11
  @configuration = Configuration.new
12
12
  end
13
13
 
14
+ def self.supports_composite_keys?
15
+ Gem::Requirement.new('>= 7.2.0').satisfied_by?(ActiveRecord.version)
16
+ end
17
+
14
18
  class Configuration
15
19
  attr_reader :use_read_replica
16
20
 
@@ -113,7 +113,9 @@ module CounterCulture
113
113
  end
114
114
 
115
115
  if @with_papertrail
116
- instance = klass.where(primary_key => id_to_change).first
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
- klass.where(primary_key => id_to_change).update_all updates.join(', ')
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}.#{klass.quoted_primary_key}"
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
- value = klass.where(
194
- "#{klass.table_name}.#{relation_primary_key(first, source: obj, was: was)} = ?",
195
- foreign_key_value).first
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
- value = obj
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
- return value.try(relation_primary_key(original_relation, source: obj, was: was).try(:to_sym))
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
- "#{relation_class_table_name}.#{relation_class.primary_key}, " \
115
- "#{relation_class_table_name}.#{relation_reflect(relation).association_primary_key(relation_class)}, " \
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
- relation_class.where(relation_class.primary_key => record.send(relation_class.primary_key)).update_all(updates.join(', '))
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
- @count_select = "COUNT(#{self_table_name}.#{model.primary_key})*#{delta_magnitude}"
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 #{source_table}.#{source_table_key} = #{target_table_alias}.#{target_table_key}"
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
- joins_sql += " AND #{target_table_alias}.#{model.primary_key} IN (#{where.select(model.primary_key).to_sql})"
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
@@ -1,3 +1,3 @@
1
1
  module CounterCulture
2
- VERSION = '3.9.0'.freeze
2
+ VERSION = '3.11.0'.freeze
3
3
  end
@@ -35,9 +35,14 @@ module CounterCulture
35
35
  end.compact
36
36
 
37
37
  if update_snippets.any?
38
- klass
39
- .where(Thread.current[:primary_key_map][klass] => rec_id)
40
- .update_all(update_snippets.join(', '))
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.9.0
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-03-20 00:00:00.000000000 Z
11
+ date: 2025-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord