activerecord-slotted_counters 0.1.1 → 0.1.3
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 +8 -0
- data/README.md +1 -1
- data/lib/activerecord_slotted_counters/adapters/pg_upsert.rb +88 -0
- data/lib/activerecord_slotted_counters/adapters/rails_upsert.rb +21 -0
- data/lib/activerecord_slotted_counters/has_slotted_counter.rb +74 -21
- data/lib/activerecord_slotted_counters/version.rb +1 -1
- metadata +22 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bdd6573e6f4d045624e5d0e068f40728ece0ff47461b0c476606689b74de6c7d
|
4
|
+
data.tar.gz: 79fc88a8fd00ff709b34b9cfd192a9275b95647639281ee36f10e2b61b5eb79a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2fdb9401b2473f9d1ea8048bc14056665a310fe52fb4c0e1572b989ddab04f4907fa15df57821f7792b8861bd1d07259da9ddef425281c423c68d37e922ebbe
|
7
|
+
data.tar.gz: 5e399bc56136750d40f5303677ce381ae791a56a96f52ac2df9b1c6f7ca6443c4eaef2cad71b631231d043023f1e6a1a8643f54bb607afd68c73462cdafc25db
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.1.3 (2023-04-03)
|
6
|
+
|
7
|
+
- Add Rails 6 support (PostgreSQL only) [#13](https://github.com/evilmartians/activerecord-slotted_counters/pull/13) ([@LukinEgor][])
|
8
|
+
|
9
|
+
## 0.1.2 (2023-03-28)
|
10
|
+
|
11
|
+
- Fix preloading counters for multiple records [#12](https://github.com/evilmartians/activerecord-slotted_counters/pull/12) ([@LukinEgor][])
|
12
|
+
|
5
13
|
## 0.1.1 (2023-01-17)
|
6
14
|
|
7
15
|
- Fix prevent double increment/decrement of native counter caches [#10](https://github.com/evilmartians/activerecord-slotted_counters/pull/10) ([@danielwestendorf][])
|
data/README.md
CHANGED
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordSlottedCounters
|
4
|
+
module Adapters
|
5
|
+
class PgUpsert
|
6
|
+
attr_reader :klass
|
7
|
+
|
8
|
+
def initialize(klass)
|
9
|
+
@klass = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply?
|
13
|
+
ActiveRecord::VERSION::MAJOR < 7 && klass.connection.adapter_name == ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::ADAPTER_NAME
|
14
|
+
end
|
15
|
+
|
16
|
+
def bulk_insert(attributes, on_duplicate: nil, unique_by: nil)
|
17
|
+
raise ArgumentError, "Values must not be empty" if attributes.empty?
|
18
|
+
|
19
|
+
keys = attributes.first.keys + klass.all_timestamp_attributes_in_model
|
20
|
+
|
21
|
+
current_time = klass.current_time_from_proper_timezone
|
22
|
+
data = attributes.map { |attr| attr.values + [current_time, current_time] }
|
23
|
+
|
24
|
+
columns = columns_for_attributes(keys)
|
25
|
+
|
26
|
+
fields_str = quote_column_names(columns)
|
27
|
+
values_str = quote_many_records(columns, data)
|
28
|
+
|
29
|
+
sql = <<~SQL
|
30
|
+
INSERT INTO #{klass.quoted_table_name}
|
31
|
+
(#{fields_str})
|
32
|
+
VALUES #{values_str}
|
33
|
+
SQL
|
34
|
+
|
35
|
+
if unique_by.present?
|
36
|
+
index = unique_indexes.find { |i| i.name.to_sym == unique_by }
|
37
|
+
columns = columns_for_attributes(index.columns)
|
38
|
+
fields = quote_column_names(columns)
|
39
|
+
|
40
|
+
sql << " ON CONFLICT (#{fields})"
|
41
|
+
end
|
42
|
+
|
43
|
+
if on_duplicate.present?
|
44
|
+
sql << " DO UPDATE SET #{on_duplicate}"
|
45
|
+
end
|
46
|
+
|
47
|
+
sql << " RETURNING \"id\""
|
48
|
+
|
49
|
+
klass.connection.exec_query(sql)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def unique_indexes
|
55
|
+
klass.connection.schema_cache.indexes(klass.table_name).select(&:unique)
|
56
|
+
end
|
57
|
+
|
58
|
+
def columns_for_attributes(attributes)
|
59
|
+
attributes.map do |attribute|
|
60
|
+
klass.column_for_attribute(attribute)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def quote_column_names(columns, table_name: false)
|
65
|
+
columns.map do |column|
|
66
|
+
column_name = klass.connection.quote_column_name(column.name)
|
67
|
+
if table_name
|
68
|
+
"#{klass.quoted_table_name}.#{column_name}"
|
69
|
+
else
|
70
|
+
column_name
|
71
|
+
end
|
72
|
+
end.join(",")
|
73
|
+
end
|
74
|
+
|
75
|
+
def quote_record(columns, record_values)
|
76
|
+
values_str = record_values.each_with_index.map do |value, i|
|
77
|
+
type = klass.connection.lookup_cast_type_from_column(columns[i])
|
78
|
+
klass.connection.quote(type.serialize(value))
|
79
|
+
end.join(",")
|
80
|
+
"(#{values_str})"
|
81
|
+
end
|
82
|
+
|
83
|
+
def quote_many_records(columns, data)
|
84
|
+
data.map { |values| quote_record(columns, values) }.join(",")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordSlottedCounters
|
4
|
+
module Adapters
|
5
|
+
class RailsUpsert
|
6
|
+
attr_reader :klass
|
7
|
+
|
8
|
+
def initialize(klass)
|
9
|
+
@klass = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply?
|
13
|
+
ActiveRecord::VERSION::MAJOR >= 7
|
14
|
+
end
|
15
|
+
|
16
|
+
def bulk_insert(attributes, on_duplicate: nil, unique_by: nil)
|
17
|
+
klass.upsert_all(attributes, on_duplicate: on_duplicate, unique_by: unique_by)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -3,10 +3,56 @@
|
|
3
3
|
require "active_support"
|
4
4
|
require "activerecord_slotted_counters/utils"
|
5
5
|
|
6
|
+
require "activerecord_slotted_counters/adapters/rails_upsert"
|
7
|
+
require "activerecord_slotted_counters/adapters/pg_upsert"
|
8
|
+
|
6
9
|
module ActiveRecordSlottedCounters
|
7
10
|
class SlottedCounter < ::ActiveRecord::Base
|
8
|
-
|
9
|
-
|
11
|
+
class NotSupportedAdapter < StandardError
|
12
|
+
def initialize(adapter)
|
13
|
+
@adapter = adapter
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
"The adapter not implemented yet (Rails #{ActiveRecord::VERSION::MAJOR}, #{@adapter})"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
scope :associated_records, ->(counter_name, klass) do
|
22
|
+
where(counter_name: counter_name, associated_record_type: klass)
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def bulk_insert(attributes)
|
27
|
+
on_duplicate_clause = "count = slotted_counters.count + excluded.count"
|
28
|
+
|
29
|
+
slotted_counter_db_adapter.bulk_insert(
|
30
|
+
attributes,
|
31
|
+
on_duplicate: Arel.sql(on_duplicate_clause),
|
32
|
+
unique_by: :index_slotted_counters
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def slotted_counter_db_adapter
|
39
|
+
@slotted_counter_association_name ||= set_slotted_counter_db_adapter
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_slotted_counter_db_adapter
|
43
|
+
available_adapters = [
|
44
|
+
ActiveRecordSlottedCounters::Adapters::RailsUpsert,
|
45
|
+
ActiveRecordSlottedCounters::Adapters::PgUpsert
|
46
|
+
]
|
47
|
+
|
48
|
+
adapter = available_adapters
|
49
|
+
.map { |adapter| adapter.new(self) }
|
50
|
+
.detect { |adapter| adapter.apply? }
|
51
|
+
|
52
|
+
raise NotSupportedAdapter.new(connection.adapter_name) if adapter.nil?
|
53
|
+
|
54
|
+
adapter
|
55
|
+
end
|
10
56
|
end
|
11
57
|
end
|
12
58
|
|
@@ -38,11 +84,31 @@ module ActiveRecordSlottedCounters
|
|
38
84
|
counter_name = slotted_counter_name(counter_type)
|
39
85
|
association_name = slotted_counter_association_name(counter_type)
|
40
86
|
|
41
|
-
has_many association_name, ->(model) { associated_records(counter_name, model.
|
87
|
+
has_many association_name, ->(model) { associated_records(counter_name, model.class.to_s) }, **SLOTTED_COUNTERS_ASSOCIATION_OPTIONS
|
42
88
|
|
43
89
|
scope :with_slotted_counters, ->(counter_type) do
|
44
|
-
|
45
|
-
preload(
|
90
|
+
scope_association_name = slotted_counter_association_name(counter_type)
|
91
|
+
return preload(scope_association_name) if ActiveRecord::VERSION::MAJOR >= 7
|
92
|
+
|
93
|
+
scope_counter_name = slotted_counter_name(counter_type)
|
94
|
+
counters = ActiveRecordSlottedCounters::SlottedCounter
|
95
|
+
.where(
|
96
|
+
counter_name: scope_counter_name,
|
97
|
+
associated_record_id: self,
|
98
|
+
associated_record_type: klass.to_s
|
99
|
+
)
|
100
|
+
|
101
|
+
grouped_counters = counters.group_by(&:associated_record_id)
|
102
|
+
|
103
|
+
each do |record|
|
104
|
+
assoc = record.association(scope_association_name)
|
105
|
+
assoc.target = grouped_counters[record.id] || []
|
106
|
+
assoc.loaded!
|
107
|
+
end
|
108
|
+
|
109
|
+
define_singleton_method(:find_each, method(:each))
|
110
|
+
|
111
|
+
self
|
46
112
|
end
|
47
113
|
|
48
114
|
_slotted_counters << counter_type
|
@@ -138,13 +204,8 @@ module ActiveRecordSlottedCounters
|
|
138
204
|
|
139
205
|
def insert_counters_records(ids, counters)
|
140
206
|
counters_params = prepare_slotted_counters_params(ids, counters)
|
141
|
-
on_duplicate_clause = "count = slotted_counters.count + excluded.count"
|
142
207
|
|
143
|
-
result = ActiveRecordSlottedCounters::SlottedCounter.
|
144
|
-
counters_params,
|
145
|
-
on_duplicate: Arel.sql(on_duplicate_clause),
|
146
|
-
unique_by: :index_slotted_counters
|
147
|
-
)
|
208
|
+
result = ActiveRecordSlottedCounters::SlottedCounter.bulk_insert(counters_params)
|
148
209
|
|
149
210
|
result.rows.count
|
150
211
|
end
|
@@ -191,16 +252,8 @@ module ActiveRecordSlottedCounters
|
|
191
252
|
|
192
253
|
def read_slotted_counter(counter_type)
|
193
254
|
association_name = slotted_counter_association_name(counter_type)
|
194
|
-
|
195
|
-
|
196
|
-
scope = association(association_name).scope
|
197
|
-
counter = scope.sum(&:count)
|
198
|
-
|
199
|
-
return counter
|
200
|
-
end
|
201
|
-
|
202
|
-
scope = send(association_name)
|
203
|
-
scope.sum(:count)
|
255
|
+
scope = association(association_name).load_target
|
256
|
+
scope.sum(&:count)
|
204
257
|
end
|
205
258
|
end
|
206
259
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-slotted_counters
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Egor Lukin
|
8
8
|
- Vladimir Dementyev
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-
|
12
|
+
date: 2023-04-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -95,6 +95,20 @@ dependencies:
|
|
95
95
|
- - ">="
|
96
96
|
- !ruby/object:Gem::Version
|
97
97
|
version: '0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: rspec-sqlimit
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
98
112
|
description: Active Record slotted counters support
|
99
113
|
email:
|
100
114
|
- dementiev.vm@gmail.com
|
@@ -106,6 +120,8 @@ files:
|
|
106
120
|
- LICENSE.txt
|
107
121
|
- README.md
|
108
122
|
- lib/activerecord-slotted_counters.rb
|
123
|
+
- lib/activerecord_slotted_counters/adapters/pg_upsert.rb
|
124
|
+
- lib/activerecord_slotted_counters/adapters/rails_upsert.rb
|
109
125
|
- lib/activerecord_slotted_counters/has_slotted_counter.rb
|
110
126
|
- lib/activerecord_slotted_counters/railtie.rb
|
111
127
|
- lib/activerecord_slotted_counters/utils.rb
|
@@ -121,7 +137,7 @@ metadata:
|
|
121
137
|
documentation_uri: http://github.com/evilmartians/activerecord-slotted_counters
|
122
138
|
homepage_uri: http://github.com/evilmartians/activerecord-slotted_counters
|
123
139
|
source_code_uri: http://github.com/evilmartians/activerecord-slotted_counters
|
124
|
-
post_install_message:
|
140
|
+
post_install_message:
|
125
141
|
rdoc_options: []
|
126
142
|
require_paths:
|
127
143
|
- lib
|
@@ -136,8 +152,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
152
|
- !ruby/object:Gem::Version
|
137
153
|
version: '0'
|
138
154
|
requirements: []
|
139
|
-
rubygems_version: 3.3.
|
140
|
-
signing_key:
|
155
|
+
rubygems_version: 3.3.3
|
156
|
+
signing_key:
|
141
157
|
specification_version: 4
|
142
158
|
summary: Active Record slotted counters support
|
143
159
|
test_files: []
|