activerecord-slotted_counters 0.1.2 → 0.1.4
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 +69 -8
- data/lib/activerecord_slotted_counters/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: efdd024fe0a5944a7cbcaa4e6990a05b74b80634affb59d3c0a30a0a3b08c52e
|
4
|
+
data.tar.gz: 70e2607be4d82c909002d01856d76b6663307b1ccd1204015db6e160a33c6193
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b5a4570082a3cf8264472ce8ccc59d50c52621323937cdfddc48cddfbbca01020e7e76f70457e98eeb5a5f32b960979f3315abafd880873788eb5ccd78e8a78
|
7
|
+
data.tar.gz: e221d1d149adbe60e4b7a60f76aced3a9d3f1c16f7e37950aad54ba44e9a7828a92e7a411be51bd0a91ecfe5827716e09080b491f375d0fe7f0cb18781e9981b
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.1.4 (2023-04-19)
|
6
|
+
|
7
|
+
- Fix "can't modify frozen String" for the pg adapter (ruby 2.7) [#15](https://github.com/evilmartians/activerecord-slotted_counters/pull/15) ([@LukinEgor][])
|
8
|
+
|
9
|
+
## 0.1.3 (2023-04-03)
|
10
|
+
|
11
|
+
- Add Rails 6 support (PostgreSQL only) [#13](https://github.com/evilmartians/activerecord-slotted_counters/pull/13) ([@LukinEgor][])
|
12
|
+
|
5
13
|
## 0.1.2 (2023-03-28)
|
6
14
|
|
7
15
|
- Fix preloading counters for multiple records [#12](https://github.com/evilmartians/activerecord-slotted_counters/pull/12) ([@LukinEgor][])
|
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,11 +3,57 @@
|
|
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
|
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
|
+
|
8
21
|
scope :associated_records, ->(counter_name, klass) do
|
9
22
|
where(counter_name: counter_name, associated_record_type: klass)
|
10
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
|
56
|
+
end
|
11
57
|
end
|
12
58
|
|
13
59
|
module BelongsToAssociation
|
@@ -41,8 +87,28 @@ module ActiveRecordSlottedCounters
|
|
41
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
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Egor Lukin
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-04-
|
12
|
+
date: 2023-04-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -120,6 +120,8 @@ files:
|
|
120
120
|
- LICENSE.txt
|
121
121
|
- README.md
|
122
122
|
- lib/activerecord-slotted_counters.rb
|
123
|
+
- lib/activerecord_slotted_counters/adapters/pg_upsert.rb
|
124
|
+
- lib/activerecord_slotted_counters/adapters/rails_upsert.rb
|
123
125
|
- lib/activerecord_slotted_counters/has_slotted_counter.rb
|
124
126
|
- lib/activerecord_slotted_counters/railtie.rb
|
125
127
|
- lib/activerecord_slotted_counters/utils.rb
|