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 +4 -4
- data/.circleci/config.yml +64 -93
- data/CHANGELOG.md +14 -0
- data/README.md +1 -1
- data/gemfiles/rails_8.1.gemfile +8 -0
- data/lib/counter_culture/counter.rb +53 -75
- data/lib/counter_culture/reconciler.rb +7 -7
- data/lib/counter_culture/version.rb +1 -1
- data/lib/counter_culture.rb +18 -7
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d37272100e5c67ce47b96f522793380e863db895f97387ce02dcc609ae1cb24
|
|
4
|
+
data.tar.gz: 24580fa8abe60d089e5993e8a95c3c1740124ad058d5bdbba2c9766a83a635ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
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 &&
|
|
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: ["
|
|
52
|
-
rails-version: ["
|
|
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: "
|
|
51
|
+
rails-version: "7.2"
|
|
57
52
|
database: "postgresql"
|
|
58
53
|
- ruby-version: "3.0"
|
|
59
|
-
rails-version: "
|
|
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: "
|
|
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: "
|
|
70
|
+
rails-version: "8.0"
|
|
66
71
|
database: "postgresql"
|
|
67
72
|
- ruby-version: "3.1"
|
|
68
|
-
rails-version: "
|
|
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.
|
|
74
|
-
rails-version: "
|
|
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.
|
|
77
|
-
rails-version: "
|
|
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.
|
|
80
|
-
rails-version: "
|
|
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.
|
|
83
|
-
rails-version: "
|
|
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: "
|
|
86
|
-
rails-version: "
|
|
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: "
|
|
89
|
-
rails-version: "
|
|
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: "
|
|
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: "
|
|
119
|
+
- ruby-version: "4.0"
|
|
95
120
|
rails-version: "7.0"
|
|
96
121
|
database: "sqlite3"
|
|
97
|
-
- ruby-version: "
|
|
122
|
+
- ruby-version: "4.0"
|
|
98
123
|
rails-version: "7.0"
|
|
99
124
|
database: "mysql2"
|
|
100
|
-
- ruby-version: "
|
|
125
|
+
- ruby-version: "4.0"
|
|
101
126
|
rails-version: "7.1"
|
|
102
127
|
database: "postgresql"
|
|
103
|
-
- ruby-version: "
|
|
128
|
+
- ruby-version: "4.0"
|
|
104
129
|
rails-version: "7.1"
|
|
105
130
|
database: "sqlite3"
|
|
106
|
-
- ruby-version: "
|
|
131
|
+
- ruby-version: "4.0"
|
|
107
132
|
rails-version: "7.1"
|
|
108
133
|
database: "mysql2"
|
|
109
|
-
- ruby-version: "
|
|
134
|
+
- ruby-version: "4.0"
|
|
110
135
|
rails-version: "7.2"
|
|
111
136
|
database: "postgresql"
|
|
112
|
-
- ruby-version: "
|
|
137
|
+
- ruby-version: "4.0"
|
|
113
138
|
rails-version: "7.2"
|
|
114
139
|
database: "sqlite3"
|
|
115
|
-
- ruby-version: "
|
|
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
|
|
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
|
|
|
@@ -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
|
-
#
|
|
81
|
-
|
|
66
|
+
# Calculate signed delta for both paths
|
|
67
|
+
signed_delta = options[:increment] ? delta_magnitude : -delta_magnitude
|
|
82
68
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
klass,
|
|
102
|
-
|
|
103
|
-
timestamp_column
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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,
|
|
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][
|
|
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][
|
|
480
|
+
Thread.current[:aggregated_updates][klass][id][:counters][column][:delta] += delta
|
|
503
481
|
end
|
|
504
482
|
|
|
505
|
-
def remember_timestamp_update(klass, id,
|
|
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][
|
|
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 =
|
|
260
|
+
reflect, model = relation_reflect_and_model(cur_relation)
|
|
261
261
|
|
|
262
|
-
target_table = quote_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 ==
|
|
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
|
|
303
|
-
|
|
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?
|
data/lib/counter_culture.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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).
|
|
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.
|
|
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:
|
|
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
|