counter_culture 3.5.0 → 3.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +52 -7
- data/Appraisals +5 -0
- data/CHANGELOG.md +30 -0
- data/README.md +107 -3
- data/counter_culture.gemspec +1 -1
- data/gemfiles/rails_5.2.gemfile +1 -0
- data/gemfiles/rails_6.0.gemfile +1 -0
- data/gemfiles/rails_6.1.gemfile +1 -0
- data/gemfiles/rails_7.0.gemfile +1 -0
- data/gemfiles/rails_7.1.gemfile +8 -0
- data/gemfiles/rails_7.2.gemfile +7 -0
- data/lib/counter_culture/counter.rb +97 -17
- data/lib/counter_culture/reconciler.rb +7 -3
- data/lib/counter_culture/version.rb +1 -1
- data/lib/counter_culture/with_connection.rb +33 -0
- data/lib/counter_culture.rb +32 -0
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 225d25bff8cd9226371b162e6761603271cbac02c74be1dd7398de2dc63dbb71
|
4
|
+
data.tar.gz: ea2b215508e285bab75efcbf3aeb153a15458c22db0cc0b79350bb2e9b3a9e05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5362e9b1db5c4a81944e3deeddec9c7945ae349cca9f663dae140fb1516f00b9bd41d4d34af4705c2d4600ee65dabe2a41bc0a53e1a98ba682385228b0f4e41a
|
7
|
+
data.tar.gz: 60d99dab523e3f339961c4d19393f27de41006c361dc190baf25676157dded5b98382d77cca3dd76533cb070e3eec14965bb90ffe3f13991ba6d409cdf3e779a
|
data/.circleci/config.yml
CHANGED
@@ -48,8 +48,8 @@ workflows:
|
|
48
48
|
- test:
|
49
49
|
matrix:
|
50
50
|
parameters:
|
51
|
-
ruby-version: ["2.6", "2.7", "3.0", "3.1", "3.2"]
|
52
|
-
rails-version: ["5.2", "6.0", "6.1", "7.0"]
|
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"]
|
53
53
|
database: ["postgresql", "sqlite3", "mysql2"]
|
54
54
|
exclude:
|
55
55
|
- ruby-version: "3.0"
|
@@ -57,28 +57,37 @@ workflows:
|
|
57
57
|
database: "postgresql"
|
58
58
|
- ruby-version: "3.0"
|
59
59
|
rails-version: "5.2"
|
60
|
-
database: "
|
60
|
+
database: "mysql2"
|
61
61
|
- ruby-version: "3.0"
|
62
62
|
rails-version: "5.2"
|
63
|
-
database: "
|
63
|
+
database: "sqlite3"
|
64
64
|
- ruby-version: "3.1"
|
65
65
|
rails-version: "5.2"
|
66
66
|
database: "postgresql"
|
67
|
+
- ruby-version: "3.1"
|
68
|
+
rails-version: "5.2"
|
69
|
+
database: "mysql2"
|
67
70
|
- ruby-version: "3.1"
|
68
71
|
rails-version: "5.2"
|
69
72
|
database: "sqlite3"
|
70
73
|
- ruby-version: "3.2"
|
71
74
|
rails-version: "5.2"
|
72
|
-
database: "
|
75
|
+
database: "postgresql"
|
73
76
|
- ruby-version: "3.2"
|
74
77
|
rails-version: "5.2"
|
75
|
-
database: "
|
78
|
+
database: "mysql2"
|
76
79
|
- ruby-version: "3.2"
|
77
80
|
rails-version: "5.2"
|
78
81
|
database: "sqlite3"
|
79
|
-
- ruby-version: "3.
|
82
|
+
- ruby-version: "3.3"
|
83
|
+
rails-version: "5.2"
|
84
|
+
database: "postgresql"
|
85
|
+
- ruby-version: "3.3"
|
80
86
|
rails-version: "5.2"
|
81
87
|
database: "mysql2"
|
88
|
+
- ruby-version: "3.3"
|
89
|
+
rails-version: "5.2"
|
90
|
+
database: "sqlite3"
|
82
91
|
- ruby-version: "2.6"
|
83
92
|
rails-version: "7.0"
|
84
93
|
database: "postgresql"
|
@@ -88,3 +97,39 @@ workflows:
|
|
88
97
|
- ruby-version: "2.6"
|
89
98
|
rails-version: "7.0"
|
90
99
|
database: "mysql2"
|
100
|
+
- ruby-version: "2.6"
|
101
|
+
rails-version: "7.1"
|
102
|
+
database: "postgresql"
|
103
|
+
- ruby-version: "2.6"
|
104
|
+
rails-version: "7.1"
|
105
|
+
database: "sqlite3"
|
106
|
+
- ruby-version: "2.6"
|
107
|
+
rails-version: "7.1"
|
108
|
+
database: "mysql2"
|
109
|
+
- ruby-version: "2.6"
|
110
|
+
rails-version: "7.2"
|
111
|
+
database: "postgresql"
|
112
|
+
- ruby-version: "2.6"
|
113
|
+
rails-version: "7.2"
|
114
|
+
database: "sqlite3"
|
115
|
+
- ruby-version: "2.6"
|
116
|
+
rails-version: "7.2"
|
117
|
+
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"
|
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,33 @@
|
|
1
|
+
## 3.8.0 (October 4, 2024)
|
2
|
+
|
3
|
+
New features:
|
4
|
+
- Prefer using `with_connection` where possible (#398)
|
5
|
+
|
6
|
+
## 3.7.0 (June 20, 2024)
|
7
|
+
|
8
|
+
New features:
|
9
|
+
- Combine multiple updates into a single SQL per target row (#393)
|
10
|
+
|
11
|
+
## 3.6.0 (June 18, 2024)
|
12
|
+
|
13
|
+
Improvements:
|
14
|
+
- Don't perform a query if the `delta_magnitude` is zero (#394)
|
15
|
+
|
16
|
+
## 3.5.3 (February 16, 2024)
|
17
|
+
|
18
|
+
Bugfixes:
|
19
|
+
- Correct polymorphic table name alias reference in join clauses (#388)
|
20
|
+
|
21
|
+
## 3.5.2 (January 16, 2024)
|
22
|
+
|
23
|
+
Bugfixes:
|
24
|
+
- Assign attributes to the duped model on a lower level when determining whether a model has changed to avoid invoking unrelated callbacks (#386)
|
25
|
+
|
26
|
+
## 3.5.1 (January 8, 2024)
|
27
|
+
|
28
|
+
Bugfixes:
|
29
|
+
- Fix touching for counted models without `updated_at` (#383)
|
30
|
+
|
1
31
|
## 3.5.0 (August 25, 2023)
|
2
32
|
|
3
33
|
Improvements:
|
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 and 3.
|
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 and 7.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
|
|
@@ -74,7 +74,7 @@ class Group < ActiveRecord::Base
|
|
74
74
|
has_many :members, through: :group_memberships, class: "User"
|
75
75
|
end
|
76
76
|
|
77
|
-
class
|
77
|
+
class GroupMembership < ActiveRecord::Base
|
78
78
|
belongs_to :group
|
79
79
|
belongs_to :member, class: "User"
|
80
80
|
counter_culture :group, column_name: "members_count"
|
@@ -285,6 +285,108 @@ You can also pass a `Proc` for dynamic control. This is useful for temporarily m
|
|
285
285
|
counter_culture :category, execute_after_commit: proc { !Thread.current[:update_counter_cache_in_transaction] }
|
286
286
|
```
|
287
287
|
|
288
|
+
### Aggregating multiple updates into single SQL queries
|
289
|
+
|
290
|
+
> **NOTE**: This does not have an effect on Papertrail callbacks
|
291
|
+
|
292
|
+
By default, every create/update/destroy will trigger separate `UPDATE` SQLs for each action and targeted column. For example, if the creation of 1 record needs to increment 3 counter cache columns, a transaction that creates 3 records of this kind will generate 9 `UPDATE` SQL queries adding `+1` to the value of said columns (3 created records x 3 counter cache columns to be incremented by each).
|
293
|
+
|
294
|
+
To avoid this, you can wrap the logic that creates/updates/destroys multiple records in `CounterCulture.aggregate_counter_updates`. This will sum all updates for counter cache columns and will take the latest value for timestamp updates. As a result, you will get just one `UPDATE` SQL query per target record that updates all of its columns at once.
|
295
|
+
|
296
|
+
#### Execute aggregated SQL queries before transaction `COMMIT`
|
297
|
+
```ruby
|
298
|
+
ActiveRecord::Base.transaction do
|
299
|
+
CounterCulture.aggregate_counter_updates do
|
300
|
+
# list of updates
|
301
|
+
end # => executes aggregated SQLs
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
#### Execute aggregated SQL queries after transaction `COMMIT`
|
306
|
+
```ruby
|
307
|
+
CounterCulture.aggregate_counter_updates do
|
308
|
+
ActiveRecord::Base.transaction do
|
309
|
+
# list of updates
|
310
|
+
end
|
311
|
+
end # => executes aggregated SQLs
|
312
|
+
```
|
313
|
+
|
314
|
+
#### Examples:
|
315
|
+
<details><summary>Sums multiple counter updates for single column of a single target record</summary>
|
316
|
+
|
317
|
+
```sql
|
318
|
+
-- Before
|
319
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
320
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
321
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
322
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
323
|
+
|
324
|
+
-- After
|
325
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 2 WHERE `authors`.`id` = 1
|
326
|
+
```
|
327
|
+
|
328
|
+
```sql
|
329
|
+
-- Before
|
330
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
331
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
332
|
+
|
333
|
+
-- After
|
334
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + -2 WHERE `authors`.`id` = 1
|
335
|
+
```
|
336
|
+
|
337
|
+
```sql
|
338
|
+
-- Before
|
339
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
340
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
341
|
+
|
342
|
+
-- After
|
343
|
+
No query
|
344
|
+
```
|
345
|
+
|
346
|
+
</details>
|
347
|
+
|
348
|
+
<details><summary>Combines multiple updates for multiple columns of a single target record</summary>
|
349
|
+
|
350
|
+
```sql
|
351
|
+
-- Before
|
352
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1, updated_at = '2024-06-06 01:00:00' WHERE `authors`.`id` = 1
|
353
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1, updated_at = '2024-06-06 02:00:00' WHERE `authors`.`id` = 1
|
354
|
+
|
355
|
+
-- After
|
356
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 2, updated_at = '2024-06-06 02:00:00' WHERE `authors`.`id` = 1
|
357
|
+
```
|
358
|
+
|
359
|
+
```sql
|
360
|
+
-- Before
|
361
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 1
|
362
|
+
UPDATE `authors` SET `authors`.`works_count` = COALESCE(`authors`.`works_count`, 0) + 1 WHERE `authors`.`id` = 1
|
363
|
+
|
364
|
+
-- After
|
365
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1, `authors`.`works_count` = COALESCE(`authors`.`works_count`, 0) + 1 WHERE `authors`.`id` = 1
|
366
|
+
```
|
367
|
+
|
368
|
+
</details>
|
369
|
+
|
370
|
+
</details>
|
371
|
+
|
372
|
+
<details><summary>Combines multiple updates of multiple target records</summary>
|
373
|
+
|
374
|
+
```sql
|
375
|
+
-- Before
|
376
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
377
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
378
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) - 1 WHERE `authors`.`id` = 1
|
379
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 2
|
380
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 2
|
381
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 1 WHERE `authors`.`id` = 2
|
382
|
+
|
383
|
+
-- After
|
384
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + -3 WHERE `authors`.`id` = 1
|
385
|
+
UPDATE `authors` SET `authors`.`books_count` = COALESCE(`authors`.`books_count`, 0) + 3 WHERE `authors`.`id` = 2
|
386
|
+
```
|
387
|
+
|
388
|
+
</details>
|
389
|
+
|
288
390
|
### Manually populating counter cache values
|
289
391
|
|
290
392
|
You will sometimes want to populate counter-cache values from primary data. This is required when adding counter-caches to existing data. It is also recommended to run this regularly (at BestVendor, we run it once a week) to catch any incorrect values in the counter caches.
|
@@ -358,6 +460,8 @@ Product.counter_culture_fix_counts touch: 'category_count_changed'
|
|
358
460
|
|
359
461
|
The options start and finish are especially useful if you want multiple workers dealing with the same processing queue. You can make worker 1 handle all the records between id 1 and 9999 and worker 2 handle from 10000 and beyond by setting the :start and :finish option on each worker.
|
360
462
|
|
463
|
+
> **! NOTE**: the IDs we pass as `start` and `finish` here are in fact `Category` IDs, not `Product`!
|
464
|
+
|
361
465
|
```ruby
|
362
466
|
Product.counter_culture_fix_counts start: 10_000
|
363
467
|
# will fix counts for all counter caches defined on Product from record 10000 and onwards.
|
@@ -374,7 +478,7 @@ Product.counter_culture_fix_counts start: 2001, finish: 3000
|
|
374
478
|
|
375
479
|
#### Fix counter cache using a replica database
|
376
480
|
|
377
|
-
When fixing counter caches the number of reads usually vastly exceeds the number of writes. It can make sense to offload the
|
481
|
+
When fixing counter caches the number of reads usually vastly exceeds the number of writes. It can make sense to offload the read load to a replica database in this case. Rails 6 introduced [native handling of multiple database connections](https://guides.rubyonrails.org/v6.0/active_record_multiple_databases.html). You can use this to send read traffic to a read-only replica using the option `db_connection_builder`:
|
378
482
|
|
379
483
|
```ruby
|
380
484
|
Product.counter_culture_fix_counts db_connection_builder: proc{|reading, block|
|
data/counter_culture.gemspec
CHANGED
@@ -43,7 +43,7 @@ Gem::Specification.new do |spec|
|
|
43
43
|
spec.add_development_dependency 'rspec-extra-formatters'
|
44
44
|
spec.add_development_dependency 'simplecov', '~> 0.16.1'
|
45
45
|
spec.add_development_dependency 'timecop'
|
46
|
-
spec.add_development_dependency 'sqlite3'
|
46
|
+
spec.add_development_dependency 'sqlite3', ">= 1.4"
|
47
47
|
spec.add_development_dependency 'mysql2'
|
48
48
|
spec.add_development_dependency 'pg'
|
49
49
|
end
|
data/gemfiles/rails_5.2.gemfile
CHANGED
data/gemfiles/rails_6.0.gemfile
CHANGED
data/gemfiles/rails_6.1.gemfile
CHANGED
data/gemfiles/rails_7.0.gemfile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative './with_connection'
|
2
|
+
|
1
3
|
module CounterCulture
|
2
4
|
class Counter
|
3
5
|
CONFIG_OPTIONS = [ :column_names, :counter_cache_name, :delta_column, :foreign_key_values, :touch, :delta_magnitude, :execute_after_commit ]
|
@@ -55,6 +57,8 @@ module CounterCulture
|
|
55
57
|
else
|
56
58
|
counter_delta_magnitude_for(obj)
|
57
59
|
end
|
60
|
+
return if delta_magnitude.zero?
|
61
|
+
|
58
62
|
# increment or decrement?
|
59
63
|
operator = options[:increment] ? '+' : '-'
|
60
64
|
|
@@ -63,38 +67,51 @@ module CounterCulture
|
|
63
67
|
# MySQL throws an ambiguous column error if any joins are present and we don't include the
|
64
68
|
# table name. We isolate this change to MySQL because sqlite has the opposite behavior and
|
65
69
|
# throws an exception if the table name is present after UPDATE.
|
66
|
-
quoted_column =
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
71
77
|
|
72
78
|
column_type = klass.type_for_attribute(change_counter_column).type
|
73
79
|
|
74
80
|
# we don't use Rails' update_counters because we support changing the timestamp
|
75
81
|
updates = []
|
76
|
-
|
77
|
-
|
78
|
-
|
82
|
+
|
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)
|
79
86
|
else
|
80
|
-
|
87
|
+
assemble_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
|
81
88
|
end
|
82
|
-
|
89
|
+
|
90
|
+
# and this will update the timestamp, if so desired
|
83
91
|
if touch
|
84
|
-
current_time =
|
85
|
-
timestamp_columns =
|
92
|
+
current_time = klass.send(:current_time_from_proper_timezone)
|
93
|
+
timestamp_columns = klass.send(:timestamp_attributes_for_update_in_model)
|
86
94
|
if touch != true
|
87
95
|
# starting in Rails 6 this is frozen
|
88
96
|
timestamp_columns = timestamp_columns.dup
|
89
97
|
timestamp_columns << touch
|
90
98
|
end
|
91
99
|
timestamp_columns.each do |timestamp_column|
|
92
|
-
updates <<
|
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
|
+
)
|
93
106
|
end
|
94
107
|
end
|
95
108
|
|
96
109
|
primary_key = relation_primary_key(relation, source: obj, was: options[:was])
|
97
110
|
|
111
|
+
if Thread.current[:aggregate_counter_updates]
|
112
|
+
Thread.current[:primary_key_map][klass] ||= primary_key
|
113
|
+
end
|
114
|
+
|
98
115
|
if @with_papertrail
|
99
116
|
instance = klass.where(primary_key => id_to_change).first
|
100
117
|
if instance
|
@@ -118,8 +135,10 @@ module CounterCulture
|
|
118
135
|
end
|
119
136
|
end
|
120
137
|
|
121
|
-
|
122
|
-
|
138
|
+
unless Thread.current[:aggregate_counter_updates]
|
139
|
+
execute_now_or_after_commit(obj) do
|
140
|
+
klass.where(primary_key => id_to_change).update_all updates.join(', ')
|
141
|
+
end
|
123
142
|
end
|
124
143
|
end
|
125
144
|
end
|
@@ -165,7 +184,7 @@ module CounterCulture
|
|
165
184
|
def foreign_key_value(obj, relation, was = false)
|
166
185
|
original_relation = relation
|
167
186
|
relation = relation.is_a?(Enumerable) ? relation.dup : [relation]
|
168
|
-
|
187
|
+
|
169
188
|
if was
|
170
189
|
first = relation.shift
|
171
190
|
foreign_key_value = attribute_was(obj, relation_foreign_key(first))
|
@@ -314,7 +333,9 @@ module CounterCulture
|
|
314
333
|
changes_method = ACTIVE_RECORD_VERSION >= Gem::Version.new("5.1.0") ? :saved_changes : :changed_attributes
|
315
334
|
obj.public_send(changes_method).each do |key, value|
|
316
335
|
old_value = ACTIVE_RECORD_VERSION >= Gem::Version.new("5.1.0") ? value.first : value
|
317
|
-
|
336
|
+
# We set old values straight to AR @attributes variable to avoid
|
337
|
+
# write_attribute callbacks from other gems (e.g. ArTransactionChanges)
|
338
|
+
prev.instance_variable_get(:@attributes).write_from_user(key, old_value)
|
318
339
|
end
|
319
340
|
|
320
341
|
prev
|
@@ -331,6 +352,7 @@ module CounterCulture
|
|
331
352
|
end
|
332
353
|
|
333
354
|
private
|
355
|
+
|
334
356
|
def attribute_was(obj, attr)
|
335
357
|
changes_method =
|
336
358
|
if ACTIVE_RECORD_VERSION >= Gem::Version.new("5.1.0")
|
@@ -340,5 +362,63 @@ module CounterCulture
|
|
340
362
|
end
|
341
363
|
obj.public_send("#{attr}#{changes_method}")
|
342
364
|
end
|
365
|
+
|
366
|
+
def assemble_money_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
|
367
|
+
counter_update_snippet(
|
368
|
+
"#{quoted_column} = COALESCE(CAST(#{quoted_column} as NUMERIC), 0)",
|
369
|
+
klass,
|
370
|
+
id_to_change,
|
371
|
+
operator,
|
372
|
+
delta_magnitude
|
373
|
+
)
|
374
|
+
end
|
375
|
+
|
376
|
+
def assemble_counter_update(klass, id_to_change, quoted_column, operator, delta_magnitude)
|
377
|
+
counter_update_snippet(
|
378
|
+
"#{quoted_column} = COALESCE(#{quoted_column}, 0)",
|
379
|
+
klass,
|
380
|
+
id_to_change,
|
381
|
+
operator,
|
382
|
+
delta_magnitude
|
383
|
+
)
|
384
|
+
end
|
385
|
+
|
386
|
+
def assemble_timestamp_update(klass, id_to_change, timestamp_column, value)
|
387
|
+
update = "#{timestamp_column} ="
|
388
|
+
|
389
|
+
if Thread.current[:aggregate_counter_updates]
|
390
|
+
remember_timestamp_update(klass, id_to_change, update, value)
|
391
|
+
else
|
392
|
+
"#{update} '#{value.call}'"
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def counter_update_snippet(update, klass, id_to_change, operator, delta_magnitude)
|
397
|
+
if Thread.current[:aggregate_counter_updates]
|
398
|
+
remember_counter_update(
|
399
|
+
klass,
|
400
|
+
id_to_change,
|
401
|
+
"#{update} +",
|
402
|
+
operator == '+' ? delta_magnitude : -delta_magnitude
|
403
|
+
)
|
404
|
+
else
|
405
|
+
"#{update} #{operator} #{delta_magnitude}"
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def remember_counter_update(klass, id, operation, value)
|
410
|
+
Thread.current[:aggregated_updates][klass] ||= {}
|
411
|
+
Thread.current[:aggregated_updates][klass][id] ||= {}
|
412
|
+
Thread.current[:aggregated_updates][klass][id][operation] ||= 0
|
413
|
+
|
414
|
+
Thread.current[:aggregated_updates][klass][id][operation] += value
|
415
|
+
end
|
416
|
+
|
417
|
+
def remember_timestamp_update(klass, id, operation, value)
|
418
|
+
Thread.current[:aggregated_updates][klass] ||= {}
|
419
|
+
Thread.current[:aggregated_updates][klass][id] ||= {}
|
420
|
+
|
421
|
+
Thread.current[:aggregated_updates][klass][id][operation] = value
|
422
|
+
end
|
343
423
|
end
|
344
424
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'active_support/core_ext/module/delegation'
|
2
2
|
require 'active_support/core_ext/module/attribute_accessors'
|
3
3
|
|
4
|
+
require_relative './with_connection'
|
5
|
+
|
4
6
|
module CounterCulture
|
5
7
|
class Reconciler
|
6
8
|
ACTIVE_RECORD_VERSION = Gem.loaded_specs["activerecord"].version
|
@@ -274,13 +276,13 @@ module CounterCulture
|
|
274
276
|
# child in a Single Table Inheritance
|
275
277
|
if reflect.active_record.column_names.include?('type') &&
|
276
278
|
!model.descends_from_active_record?
|
277
|
-
joins_sql += " AND #{
|
279
|
+
joins_sql += " AND #{target_table_alias}.type IN ('#{model.name}')"
|
278
280
|
end
|
279
281
|
if polymorphic?
|
280
282
|
# adds 'type' condition to JOIN clause if the current model is a
|
281
283
|
# polymorphic relation
|
282
284
|
# NB only works for one-level relations
|
283
|
-
joins_sql += " AND #{
|
285
|
+
joins_sql += " AND #{target_table_alias}.#{reflect.foreign_type} = '#{relation_class.name}'"
|
284
286
|
end
|
285
287
|
if index == reverse_relation.size - 1
|
286
288
|
# conditions must be applied to the join on which we are counting
|
@@ -312,7 +314,9 @@ module CounterCulture
|
|
312
314
|
# using Postgres with schema-namespaced tables. But then it's required,
|
313
315
|
# and otherwise it's just a no-op, so why not do it?
|
314
316
|
def quote_table_name(table_name)
|
315
|
-
relation_class.connection
|
317
|
+
WithConnection.new(relation_class).call do |connection|
|
318
|
+
connection.quote_table_name(table_name)
|
319
|
+
end
|
316
320
|
end
|
317
321
|
|
318
322
|
def parameterize(string)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module CounterCulture
|
2
|
+
class WithConnection
|
3
|
+
def initialize(recipient)
|
4
|
+
@recipient = recipient
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :recipient
|
8
|
+
|
9
|
+
def call
|
10
|
+
if rails_7_2_or_greater?
|
11
|
+
recipient.with_connection do |connection|
|
12
|
+
yield connection
|
13
|
+
end
|
14
|
+
elsif rails_7_1?
|
15
|
+
recipient.connection_pool.with_connection do |connection|
|
16
|
+
yield connection
|
17
|
+
end
|
18
|
+
else
|
19
|
+
yield recipient.connection
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def rails_7_1?
|
26
|
+
Gem::Requirement.new('~> 7.1.0').satisfied_by?(Gem::Version.new(Rails.version))
|
27
|
+
end
|
28
|
+
|
29
|
+
def rails_7_2_or_greater?
|
30
|
+
Gem::Requirement.new('>= 7.2.0').satisfied_by?(Gem::Version.new(Rails.version))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/counter_culture.rb
CHANGED
@@ -15,6 +15,38 @@ module CounterCulture
|
|
15
15
|
yield(self) if block_given?
|
16
16
|
self
|
17
17
|
end
|
18
|
+
|
19
|
+
def self.aggregate_counter_updates
|
20
|
+
return unless block_given?
|
21
|
+
|
22
|
+
Thread.current[:aggregate_counter_updates] = true
|
23
|
+
Thread.current[:aggregated_updates] = {}
|
24
|
+
Thread.current[:primary_key_map] = {}
|
25
|
+
|
26
|
+
result = yield
|
27
|
+
|
28
|
+
# aggregate the updates for each target record and execute SQL queries
|
29
|
+
Thread.current[:aggregated_updates].each do |klass, attrs|
|
30
|
+
attrs.each do |rec_id, updates|
|
31
|
+
update_snippets = updates.map do |operation, value|
|
32
|
+
value = value.call if value.is_a?(Proc)
|
33
|
+
%Q{#{operation} #{value.is_a?(String) ? "'#{value}'" : value}} unless value == 0
|
34
|
+
end.compact
|
35
|
+
|
36
|
+
if update_snippets.any?
|
37
|
+
klass
|
38
|
+
.where(Thread.current[:primary_key_map][klass] => rec_id)
|
39
|
+
.update_all(update_snippets.join(', '))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
45
|
+
ensure
|
46
|
+
Thread.current[:aggregate_counter_updates] = false
|
47
|
+
Thread.current[:aggregated_updates] = nil
|
48
|
+
Thread.current[:primary_key_map] = nil
|
49
|
+
end
|
18
50
|
end
|
19
51
|
|
20
52
|
# extend ActiveRecord with our own code here
|
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.8.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:
|
11
|
+
date: 2024-10-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -254,14 +254,14 @@ dependencies:
|
|
254
254
|
requirements:
|
255
255
|
- - ">="
|
256
256
|
- !ruby/object:Gem::Version
|
257
|
-
version: '
|
257
|
+
version: '1.4'
|
258
258
|
type: :development
|
259
259
|
prerelease: false
|
260
260
|
version_requirements: !ruby/object:Gem::Requirement
|
261
261
|
requirements:
|
262
262
|
- - ">="
|
263
263
|
- !ruby/object:Gem::Version
|
264
|
-
version: '
|
264
|
+
version: '1.4'
|
265
265
|
- !ruby/object:Gem::Dependency
|
266
266
|
name: mysql2
|
267
267
|
requirement: !ruby/object:Gem::Requirement
|
@@ -317,12 +317,15 @@ files:
|
|
317
317
|
- gemfiles/rails_6.0.gemfile
|
318
318
|
- gemfiles/rails_6.1.gemfile
|
319
319
|
- gemfiles/rails_7.0.gemfile
|
320
|
+
- gemfiles/rails_7.1.gemfile
|
321
|
+
- gemfiles/rails_7.2.gemfile
|
320
322
|
- lib/counter_culture.rb
|
321
323
|
- lib/counter_culture/counter.rb
|
322
324
|
- lib/counter_culture/extensions.rb
|
323
325
|
- lib/counter_culture/reconciler.rb
|
324
326
|
- lib/counter_culture/skip_updates.rb
|
325
327
|
- lib/counter_culture/version.rb
|
328
|
+
- lib/counter_culture/with_connection.rb
|
326
329
|
- lib/generators/counter_culture_generator.rb
|
327
330
|
- lib/generators/templates/counter_culture_migration.rb.erb
|
328
331
|
homepage: https://github.com/magnusvk/counter_culture
|
@@ -344,7 +347,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
344
347
|
- !ruby/object:Gem::Version
|
345
348
|
version: '0'
|
346
349
|
requirements: []
|
347
|
-
rubygems_version: 3.
|
350
|
+
rubygems_version: 3.5.10
|
348
351
|
signing_key:
|
349
352
|
specification_version: 4
|
350
353
|
summary: Turbo-charged counter caches for your Rails app.
|