counter_culture 3.11.3 → 3.12.1

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: 75ac99d8d45f3e16859bb04854958dac048a449aa4afe94f12885cefe3a94787
4
- data.tar.gz: e90d1b87ea3e2d919434dbeea2263fa5556044dc273cab21a6a67b423519c012
3
+ metadata.gz: 4d37272100e5c67ce47b96f522793380e863db895f97387ce02dcc609ae1cb24
4
+ data.tar.gz: 24580fa8abe60d089e5993e8a95c3c1740124ad058d5bdbba2c9766a83a635ba
5
5
  SHA512:
6
- metadata.gz: dc1196535a528c5f87e39aca06d5e1a3e014f469c8aa3e8f01c56f75a59489c4db25dc9b67168b162ddf9ac24e45c55fe887c10176e76786dea5ffe72603f10b
7
- data.tar.gz: a949a3856773028e954e00c8ce5ba89ac0e999230c47af3d1b0e312fdef827cbbf0dc71ed91406c6b2b77ba4d194b108f21df8b904f0e09592d2b5ef2a0e25d9
6
+ metadata.gz: 673ede183fe87f83356185732c651bf9a9348030b4daaee26ff63cb2774b09b08c0dadb720a5ac58d331586ae085bdbc558dba2000f6ef70e7f0ec781fdb6c46
7
+ data.tar.gz: 6df694a1661bc69582106cb429a557f4110cf7d2336534d33d7e64dce120e000489ec94a1c6982308593f9b1853ed0926cad63c6b09998293bf3139d01eaf93b
data/.circleci/config.yml CHANGED
@@ -13,23 +13,17 @@ jobs:
13
13
  database:
14
14
  type: string
15
15
  docker:
16
- - image: cimg/ruby:<< parameters.ruby-version >>
16
+ - image: ruby:<< parameters.ruby-version >>
17
17
  - image: cimg/postgres:14.1
18
18
  - image: cimg/mysql:8.0
19
19
  steps:
20
20
  - checkout
21
- - run:
22
- name: gem update --system --no-ri --no-rdoc
23
- # bundles sporadically fail with a weird Psych error if I don't do
24
- # this. It's annoying since it's slow but it's better than
25
- # non-deterministic build failures.
26
- command: sudo gem update --system
27
21
  - run:
28
22
  name: bundle update --no-ri --no-rdoc
29
23
  command: BUNDLE_GEMFILE=gemfiles/rails_<< parameters.rails-version >>.gemfile bundle update
30
24
  - run:
31
25
  name: install dockerize
32
- command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
26
+ command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
33
27
  environment:
34
28
  DOCKERIZE_VERSION: v0.3.0
35
29
  - run:
@@ -48,124 +42,101 @@ workflows:
48
42
  - test:
49
43
  matrix:
50
44
  parameters:
51
- ruby-version: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3"]
52
- rails-version: ["5.2", "6.0", "6.1", "7.0", "7.1", "7.2", "8.0"]
45
+ ruby-version: ["3.0", "3.1", "3.2", "3.3", "4.0"]
46
+ rails-version: ["6.0", "6.1", "7.0", "7.1", "7.2", "8.0", "8.1"]
53
47
  database: ["postgresql", "sqlite3", "mysql2"]
54
48
  exclude:
49
+ # Rails 7.2 requires Ruby 3.1+
55
50
  - ruby-version: "3.0"
56
- rails-version: "5.2"
51
+ rails-version: "7.2"
57
52
  database: "postgresql"
58
53
  - ruby-version: "3.0"
59
- rails-version: "5.2"
54
+ rails-version: "7.2"
55
+ database: "sqlite3"
56
+ - ruby-version: "3.0"
57
+ rails-version: "7.2"
60
58
  database: "mysql2"
59
+ # Rails 8.0 requires Ruby 3.2+
61
60
  - ruby-version: "3.0"
62
- rails-version: "5.2"
61
+ rails-version: "8.0"
62
+ database: "postgresql"
63
+ - ruby-version: "3.0"
64
+ rails-version: "8.0"
63
65
  database: "sqlite3"
66
+ - ruby-version: "3.0"
67
+ rails-version: "8.0"
68
+ database: "mysql2"
64
69
  - ruby-version: "3.1"
65
- rails-version: "5.2"
70
+ rails-version: "8.0"
66
71
  database: "postgresql"
67
72
  - ruby-version: "3.1"
68
- rails-version: "5.2"
69
- database: "mysql2"
70
- - ruby-version: "3.1"
71
- rails-version: "5.2"
73
+ rails-version: "8.0"
72
74
  database: "sqlite3"
73
- - ruby-version: "3.2"
74
- rails-version: "5.2"
75
+ - ruby-version: "3.1"
76
+ rails-version: "8.0"
77
+ database: "mysql2"
78
+ # Rails 8.1 requires Ruby 3.2+
79
+ - ruby-version: "3.0"
80
+ rails-version: "8.1"
75
81
  database: "postgresql"
76
- - ruby-version: "3.2"
77
- rails-version: "5.2"
82
+ - ruby-version: "3.0"
83
+ rails-version: "8.1"
84
+ database: "sqlite3"
85
+ - ruby-version: "3.0"
86
+ rails-version: "8.1"
78
87
  database: "mysql2"
79
- - ruby-version: "3.2"
80
- rails-version: "5.2"
88
+ - ruby-version: "3.1"
89
+ rails-version: "8.1"
90
+ database: "postgresql"
91
+ - ruby-version: "3.1"
92
+ rails-version: "8.1"
81
93
  database: "sqlite3"
82
- - ruby-version: "3.3"
83
- rails-version: "5.2"
94
+ - ruby-version: "3.1"
95
+ rails-version: "8.1"
96
+ database: "mysql2"
97
+ # Ruby 4.0 requires Rails 8.0+
98
+ - ruby-version: "4.0"
99
+ rails-version: "6.0"
84
100
  database: "postgresql"
85
- - ruby-version: "3.3"
86
- rails-version: "5.2"
101
+ - ruby-version: "4.0"
102
+ rails-version: "6.0"
103
+ database: "sqlite3"
104
+ - ruby-version: "4.0"
105
+ rails-version: "6.0"
87
106
  database: "mysql2"
88
- - ruby-version: "3.3"
89
- rails-version: "5.2"
107
+ - ruby-version: "4.0"
108
+ rails-version: "6.1"
109
+ database: "postgresql"
110
+ - ruby-version: "4.0"
111
+ rails-version: "6.1"
90
112
  database: "sqlite3"
91
- - ruby-version: "2.6"
113
+ - ruby-version: "4.0"
114
+ rails-version: "6.1"
115
+ database: "mysql2"
116
+ - ruby-version: "4.0"
92
117
  rails-version: "7.0"
93
118
  database: "postgresql"
94
- - ruby-version: "2.6"
119
+ - ruby-version: "4.0"
95
120
  rails-version: "7.0"
96
121
  database: "sqlite3"
97
- - ruby-version: "2.6"
122
+ - ruby-version: "4.0"
98
123
  rails-version: "7.0"
99
124
  database: "mysql2"
100
- - ruby-version: "2.6"
125
+ - ruby-version: "4.0"
101
126
  rails-version: "7.1"
102
127
  database: "postgresql"
103
- - ruby-version: "2.6"
128
+ - ruby-version: "4.0"
104
129
  rails-version: "7.1"
105
130
  database: "sqlite3"
106
- - ruby-version: "2.6"
131
+ - ruby-version: "4.0"
107
132
  rails-version: "7.1"
108
133
  database: "mysql2"
109
- - ruby-version: "2.6"
134
+ - ruby-version: "4.0"
110
135
  rails-version: "7.2"
111
136
  database: "postgresql"
112
- - ruby-version: "2.6"
137
+ - ruby-version: "4.0"
113
138
  rails-version: "7.2"
114
139
  database: "sqlite3"
115
- - ruby-version: "2.6"
140
+ - ruby-version: "4.0"
116
141
  rails-version: "7.2"
117
142
  database: "mysql2"
118
- - ruby-version: "2.7"
119
- rails-version: "7.2"
120
- database: "postgresql"
121
- - ruby-version: "2.7"
122
- rails-version: "7.2"
123
- database: "sqlite3"
124
- - ruby-version: "2.7"
125
- rails-version: "7.2"
126
- database: "mysql2"
127
- - ruby-version: "3.0"
128
- rails-version: "7.2"
129
- database: "postgresql"
130
- - ruby-version: "3.0"
131
- rails-version: "7.2"
132
- database: "sqlite3"
133
- - ruby-version: "3.0"
134
- rails-version: "7.2"
135
- database: "mysql2"
136
- - ruby-version: "2.6"
137
- rails-version: "8.0"
138
- database: "postgresql"
139
- - ruby-version: "2.6"
140
- rails-version: "8.0"
141
- database: "sqlite3"
142
- - ruby-version: "2.6"
143
- rails-version: "8.0"
144
- database: "mysql2"
145
- - ruby-version: "2.7"
146
- rails-version: "8.0"
147
- database: "postgresql"
148
- - ruby-version: "2.7"
149
- rails-version: "8.0"
150
- database: "sqlite3"
151
- - ruby-version: "2.7"
152
- rails-version: "8.0"
153
- database: "mysql2"
154
- - ruby-version: "3.0"
155
- rails-version: "8.0"
156
- database: "postgresql"
157
- - ruby-version: "3.0"
158
- rails-version: "8.0"
159
- database: "sqlite3"
160
- - ruby-version: "3.0"
161
- rails-version: "8.0"
162
- database: "mysql2"
163
- - ruby-version: "3.1"
164
- rails-version: "8.0"
165
- database: "postgresql"
166
- - ruby-version: "3.1"
167
- rails-version: "8.0"
168
- database: "sqlite3"
169
- - ruby-version: "3.1"
170
- rails-version: "8.0"
171
- database: "mysql2"
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 3.12.1 (January 6, 2026)
2
+
3
+ Bugfixes:
4
+ - Fix Rails 8.1+ compatibility by using Arel-based updates to avoid ambiguous column errors with UPDATE...FROM syntax (#425)
5
+
6
+ Changes:
7
+ - Dropped support for Ruby 2.6, 2.7 and Rails 5.2 (all EOL)
8
+ - Added support and testing for Ruby 4.0 and Rails 8.1 (#425)
9
+
10
+ ## 3.11.4 (November 5, 2025)
11
+
12
+ Bugfixes:
13
+ - Fix counter cache not using the correct type with multiple STI models in the association chain (#421)
14
+
1
15
  ## 3.11.3 (October 22, 2025)
2
16
 
3
17
  Bugfixes:
data/README.md CHANGED
@@ -7,7 +7,7 @@ Turbo-charged counter caches for your Rails app. Huge improvements over the Rail
7
7
  * Supports dynamic column names, making it possible to split up the counter cache for different types of objects
8
8
  * Can keep a running count, or a running total
9
9
 
10
- Tested against Ruby 2.6, 2.7, 3.0, 3.1, 3.2 and 3.3 and against the latest patch releases of Rails 5.2, 6.0, 6.1, 7.0, 7.1, 7.2 and 8.0.
10
+ Tested against Ruby 3.0, 3.1, 3.2, 3.3 and 4.0 and against the latest patch releases of Rails 6.0, 6.1, 7.0, 7.1, 7.2, 8.0 and 8.1.
11
11
 
12
12
  Please note that -- unlike Rails' built-in counter-caches -- counter_culture does not currently change the behavior of the `.size` method on ActiveRecord associations. If you want to avoid a database query and read the cached value, please use the attribute name containing the counter cache directly.
13
13
 
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 8.1.0"
6
+
7
+ gemspec path: "../"
8
+
@@ -59,32 +59,20 @@ module CounterCulture
59
59
  end
60
60
  return if delta_magnitude.zero?
61
61
 
62
- # increment or decrement?
63
- operator = options[:increment] ? '+' : '-'
64
-
65
62
  klass = relation_klass(relation, source: obj, was: options[:was])
66
63
 
67
- # MySQL throws an ambiguous column error if any joins are present and we don't include the
68
- # table name. We isolate this change to MySQL because sqlite has the opposite behavior and
69
- # throws an exception if the table name is present after UPDATE.
70
- quoted_column = WithConnection.new(klass).call do |connection|
71
- if connection.adapter_name == 'Mysql2'
72
- "#{klass.quoted_table_name}.#{connection.quote_column_name(change_counter_column)}"
73
- else
74
- "#{connection.quote_column_name(change_counter_column)}"
75
- end
76
- end
77
-
78
64
  column_type = klass.type_for_attribute(change_counter_column).type
79
65
 
80
- # we don't use Rails' update_counters because we support changing the timestamp
81
- updates = []
66
+ # Calculate signed delta for both paths
67
+ signed_delta = options[:increment] ? delta_magnitude : -delta_magnitude
82
68
 
83
- # this will update the actual counter
84
- updates << if column_type == :money
85
- assemble_money_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
69
+ if Thread.current[:aggregate_counter_updates]
70
+ # Store structured data for later Arel assembly
71
+ remember_counter_update(klass, id_to_change, change_counter_column, signed_delta, column_type)
86
72
  else
87
- assemble_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
73
+ # Build Arel expression for non-aggregate case
74
+ counter_expr = Counter.build_arel_counter_expr(klass, change_counter_column, signed_delta, column_type)
75
+ arel_updates = { change_counter_column => counter_expr }
88
76
  end
89
77
 
90
78
  # and this will update the timestamp, if so desired
@@ -97,12 +85,11 @@ module CounterCulture
97
85
  timestamp_columns << touch
98
86
  end
99
87
  timestamp_columns.each do |timestamp_column|
100
- updates << assemble_timestamp_update(
101
- klass,
102
- id_to_change,
103
- timestamp_column,
104
- -> { klass.send(:current_time_from_proper_timezone).to_formatted_s(:db) }
105
- )
88
+ if Thread.current[:aggregate_counter_updates]
89
+ remember_timestamp_update(klass, id_to_change, timestamp_column)
90
+ else
91
+ arel_updates[timestamp_column] = current_time
92
+ end
106
93
  end
107
94
  end
108
95
 
@@ -149,7 +136,9 @@ module CounterCulture
149
136
  unless Thread.current[:aggregate_counter_updates]
150
137
  execute_now_or_after_commit(obj) do
151
138
  conditions = primary_key_conditions(primary_key, id_to_change)
152
- klass.where(conditions).update_all updates.join(', ')
139
+ # Use Arel-based updates which let Rails handle column qualification properly,
140
+ # avoiding ambiguous column errors in Rails 8.1+ UPDATE...FROM syntax.
141
+ klass.where(conditions).distinct(false).update_all(arel_updates)
153
142
  # Determine if we should update the in-memory counter on the associated object.
154
143
  # When updating the old counter (was: true), we need to carefully consider two scenarios:
155
144
  # 1) The belongs_to relation changed (e.g., moving a child from parent A to parent B):
@@ -171,6 +160,7 @@ module CounterCulture
171
160
  end
172
161
 
173
162
  if should_update_counter
163
+ operator = options[:increment] ? '+' : '-'
174
164
  assign_to_associated_object(obj, relation, change_counter_column, operator, delta_magnitude)
175
165
  end
176
166
  end
@@ -240,11 +230,11 @@ module CounterCulture
240
230
  Array.wrap(primary_key).map { |pk| value.try(pk&.to_sym) }.compact.presence
241
231
  end
242
232
 
243
- # gets the reflect object on the given relation
233
+ # gets the reflect object on the given relation and the model that defines this reflect
244
234
  #
245
235
  # relation: a symbol or array of symbols; specifies the relation
246
236
  # that has the counter cache column
247
- def relation_reflect(relation)
237
+ def relation_reflect_and_model(relation)
248
238
  relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
249
239
 
250
240
  # go from one relation to the next until we hit the last reflect object
@@ -262,7 +252,16 @@ module CounterCulture
262
252
  end
263
253
  end
264
254
 
265
- return reflect
255
+ return [reflect, klass]
256
+ end
257
+
258
+
259
+ # gets the reflect object on the given relation
260
+ #
261
+ # relation: a symbol or array of symbols; specifies the relation
262
+ # that has the counter cache column
263
+ def relation_reflect(relation)
264
+ relation_reflect_and_model(relation).first
266
265
  end
267
266
 
268
267
  # gets the class of the given relation
@@ -445,68 +444,47 @@ module CounterCulture
445
444
  (association_object.public_send(change_counter_column) || 0).public_send(operator, delta_magnitude)
446
445
  end
447
446
 
448
- def assemble_money_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
449
- counter_update_snippet(
450
- "#{quoted_column} = COALESCE(CAST(#{quoted_column} as NUMERIC), 0)",
451
- klass,
452
- id_to_change,
453
- operator,
454
- delta_magnitude
455
- )
456
- end
457
-
458
- def assemble_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
459
- counter_update_snippet(
460
- "#{quoted_column} = COALESCE(#{quoted_column}, 0)",
461
- klass,
462
- id_to_change,
463
- operator,
464
- delta_magnitude
465
- )
466
- end
467
-
468
- def assemble_timestamp_update(klass, id_to_change, timestamp_column, value)
469
- update = "#{timestamp_column} ="
470
-
471
- if Thread.current[:aggregate_counter_updates]
472
- remember_timestamp_update(klass, id_to_change, update, value)
473
- else
474
- "#{update} '#{value.call}'"
475
- end
476
- end
477
-
478
447
  def primary_key_conditions(primary_key, fk_value)
479
448
  Array.wrap(primary_key)
480
449
  .zip(Array.wrap(fk_value))
481
450
  .to_h
482
451
  end
483
452
 
484
- def counter_update_snippet(update, klass, id_to_change, operator, delta_magnitude)
485
- if Thread.current[:aggregate_counter_updates]
486
- remember_counter_update(
487
- klass,
488
- id_to_change,
489
- "#{update} +",
490
- operator == '+' ? delta_magnitude : -delta_magnitude
453
+ # Builds an Arel expression for counter updates: COALESCE(col, 0) +/- delta
454
+ # This is a class method so it can be called from CounterCulture.aggregate_counter_updates
455
+ def self.build_arel_counter_expr(klass, column, delta, column_type)
456
+ arel_column = klass.arel_table[column]
457
+ arel_delta = Arel::Nodes.build_quoted(delta.abs)
458
+
459
+ coalesce = if column_type == :money
460
+ Arel::Nodes::NamedFunction.new(
461
+ 'COALESCE',
462
+ [Arel::Nodes::NamedFunction.new('CAST', [arel_column.as('NUMERIC')]), 0]
491
463
  )
492
464
  else
493
- "#{update} #{operator} #{delta_magnitude}"
465
+ Arel::Nodes::NamedFunction.new('COALESCE', [arel_column, 0])
466
+ end
467
+
468
+ if delta > 0
469
+ Arel::Nodes::Addition.new(coalesce, arel_delta)
470
+ else
471
+ Arel::Nodes::Subtraction.new(coalesce, arel_delta)
494
472
  end
495
473
  end
496
474
 
497
- def remember_counter_update(klass, id, operation, value)
475
+ def remember_counter_update(klass, id, column, delta, column_type)
498
476
  Thread.current[:aggregated_updates][klass] ||= {}
499
- Thread.current[:aggregated_updates][klass][id] ||= {}
500
- Thread.current[:aggregated_updates][klass][id][operation] ||= 0
477
+ Thread.current[:aggregated_updates][klass][id] ||= { counters: {}, timestamps: [] }
478
+ Thread.current[:aggregated_updates][klass][id][:counters][column] ||= { delta: 0, type: column_type }
501
479
 
502
- Thread.current[:aggregated_updates][klass][id][operation] += value
480
+ Thread.current[:aggregated_updates][klass][id][:counters][column][:delta] += delta
503
481
  end
504
482
 
505
- def remember_timestamp_update(klass, id, operation, value)
483
+ def remember_timestamp_update(klass, id, column)
506
484
  Thread.current[:aggregated_updates][klass] ||= {}
507
- Thread.current[:aggregated_updates][klass][id] ||= {}
485
+ Thread.current[:aggregated_updates][klass][id] ||= { counters: {}, timestamps: [] }
508
486
 
509
- Thread.current[:aggregated_updates][klass][id][operation] = value
487
+ Thread.current[:aggregated_updates][klass][id][:timestamps] << column
510
488
  end
511
489
  end
512
490
  end
@@ -59,7 +59,7 @@ module CounterCulture
59
59
  class Reconciliation
60
60
  attr_reader :counter, :options, :relation_class
61
61
 
62
- delegate :model, :relation, :full_primary_key, :relation_reflect, :polymorphic?, :to => :counter
62
+ delegate :model, :relation, :full_primary_key, :relation_reflect_and_model, :relation_reflect, :polymorphic?, :to => :counter
63
63
  delegate *CounterCulture::Counter::CONFIG_OPTIONS, :to => :counter
64
64
 
65
65
  def initialize(counter, changes_holder, options, relation_class)
@@ -174,7 +174,7 @@ module CounterCulture
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
+ relation_class.where(conditions).distinct(false).update_all(updates.join(', '))
178
178
  end
179
179
  end
180
180
  end
@@ -257,11 +257,11 @@ module CounterCulture
257
257
  # store joins in an array so that we can later apply column-specific
258
258
  # conditions
259
259
  join_clauses = reverse_relation.each_with_index.map do |cur_relation, index|
260
- reflect = relation_reflect(cur_relation)
260
+ reflect, model = relation_reflect_and_model(cur_relation)
261
261
 
262
- target_table = quote_table_name(reflect.active_record.table_name)
262
+ target_table = quote_table_name(model.table_name)
263
263
  target_table_alias = parameterize(target_table)
264
- if relation_class.table_name == reflect.active_record.table_name
264
+ if relation_class.table_name == model.table_name
265
265
  # join with alias to avoid ambiguous table name in
266
266
  # self-referential models
267
267
  target_table_alias += "_#{target_table_alias}"
@@ -299,8 +299,8 @@ module CounterCulture
299
299
 
300
300
  # adds 'type' condition to JOIN clause if the current model is a
301
301
  # child in a Single Table Inheritance
302
- if reflect.active_record.column_names.include?('type') &&
303
- !model.descends_from_active_record?
302
+ if model.column_names.include?('type') &&
303
+ !model.descends_from_active_record?
304
304
  joins_sql += " AND #{target_table_alias}.type IN ('#{model.name}')"
305
305
  end
306
306
  if polymorphic?
@@ -1,3 +1,3 @@
1
1
  module CounterCulture
2
- VERSION = '3.11.3'.freeze
2
+ VERSION = '3.12.1'.freeze
3
3
  end
@@ -26,15 +26,26 @@ module CounterCulture
26
26
 
27
27
  result = yield
28
28
 
29
- # aggregate the updates for each target record and execute SQL queries
29
+ # aggregate the updates for each target record and execute SQL queries using Arel
30
30
  Thread.current[:aggregated_updates].each do |klass, attrs|
31
31
  attrs.each do |rec_id, updates|
32
- update_snippets = updates.map do |operation, value|
33
- value = value.call if value.is_a?(Proc)
34
- %Q{#{operation} #{value.is_a?(String) ? "'#{value}'" : value}} unless value == 0
35
- end.compact
32
+ arel_updates = {}
36
33
 
37
- if update_snippets.any?
34
+ # Build counter updates
35
+ updates[:counters].each do |column, info|
36
+ next if info[:delta] == 0
37
+ arel_updates[column] = Counter.build_arel_counter_expr(klass, column, info[:delta], info[:type])
38
+ end
39
+
40
+ # Build timestamp updates (compute timestamp at execution time)
41
+ if updates[:timestamps].any?
42
+ current_time = klass.send(:current_time_from_proper_timezone)
43
+ updates[:timestamps].each do |column|
44
+ arel_updates[column] = current_time
45
+ end
46
+ end
47
+
48
+ if arel_updates.any?
38
49
  primary_key = Thread.current[:primary_key_map][klass]
39
50
 
40
51
  conditions =
@@ -42,7 +53,7 @@ module CounterCulture
42
53
  .zip(Array.wrap(rec_id))
43
54
  .to_h
44
55
 
45
- klass.where(conditions).update_all(update_snippets.join(', '))
56
+ klass.where(conditions).distinct(false).update_all(arel_updates)
46
57
  end
47
58
  end
48
59
  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.11.3
4
+ version: 3.12.1
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-10-23 00:00:00.000000000 Z
11
+ date: 2026-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -320,6 +320,7 @@ files:
320
320
  - gemfiles/rails_7.1.gemfile
321
321
  - gemfiles/rails_7.2.gemfile
322
322
  - gemfiles/rails_8.0.gemfile
323
+ - gemfiles/rails_8.1.gemfile
323
324
  - lib/counter_culture.rb
324
325
  - lib/counter_culture/configuration.rb
325
326
  - lib/counter_culture/counter.rb