activerecord-slotted_counters 0.1.4 → 0.2.0
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 +7 -0
- data/README.md +4 -7
- data/lib/activerecord_slotted_counters/adapters/mysql_upsert.rb +83 -0
- data/lib/activerecord_slotted_counters/adapters/pg_upsert.rb +10 -3
- data/lib/activerecord_slotted_counters/adapters/rails_upsert.rb +6 -2
- data/lib/activerecord_slotted_counters/adapters/sqlite_upsert.rb +107 -0
- data/lib/activerecord_slotted_counters/has_slotted_counter.rb +13 -8
- data/lib/activerecord_slotted_counters/version.rb +1 -1
- metadata +8 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 76c73b0a3e75e1fbddb355beedf5aa5967f43ffb5043d0051572423ef3a2188c
|
4
|
+
data.tar.gz: be40038645de42ee29e31c02cdafac846641d079404c65ac12a44726d22dc8fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5f7a7077bb02f35124a72417a2d60d2ea8da356a32e12020549fbacdb08d7ea76f2ff913e4fdd79376ab334cfec15221e808e62a76bc2652e779c31a9462ef7
|
7
|
+
data.tar.gz: a8f3881d903bbb5f032bc8763733d5f3abd770c44a9796cfbec79db171fcb1f0a453d4fe0124907315c662161d3c33a523fd20557fc2419a6c3b3c270323e71b
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.2.0 (2023-10-27)
|
6
|
+
|
7
|
+
- Add Mysql support [#18](https://github.com/evilmartians/activerecord-slotted_counters/pull/18) ([@prog-supdex][])
|
8
|
+
|
9
|
+
- Add SQLite support [#17](https://github.com/evilmartians/activerecord-slotted_counters/pull/17) ([@prog-supdex][])
|
10
|
+
|
5
11
|
## 0.1.4 (2023-04-19)
|
6
12
|
|
7
13
|
- Fix "can't modify frozen String" for the pg adapter (ruby 2.7) [#15](https://github.com/evilmartians/activerecord-slotted_counters/pull/15) ([@LukinEgor][])
|
@@ -25,3 +31,4 @@
|
|
25
31
|
[@palkan]: https://github.com/palkan
|
26
32
|
[@LukinEgor]: https://github.com/LukinEgor
|
27
33
|
[@danielwestendorf]: https://github.com/danielwestendorf
|
34
|
+
[@prog-supdex]: https://github.com/prog-supdex
|
data/README.md
CHANGED
@@ -22,9 +22,10 @@ Add to your project:
|
|
22
22
|
gem "activerecord-slotted_counters"
|
23
23
|
```
|
24
24
|
|
25
|
-
###
|
25
|
+
### Requirements
|
26
26
|
|
27
|
-
- Ruby
|
27
|
+
- Ruby >= 2.7.0
|
28
|
+
- Rails 6+
|
28
29
|
|
29
30
|
## Usage
|
30
31
|
|
@@ -67,7 +68,7 @@ user.comments_count #=> select * from slotted_counters where ...
|
|
67
68
|
user.comments_count #=> no sql
|
68
69
|
```
|
69
70
|
|
70
|
-
If you want to
|
71
|
+
If you want to preload counters for multiple records, you can use a convenient `#with_slotted_counters` method:
|
71
72
|
|
72
73
|
```ruby
|
73
74
|
User.all.with_slotted_counters(:comments).find_each do
|
@@ -77,10 +78,6 @@ end
|
|
77
78
|
|
78
79
|
Using `counter_cache: true` on `belongs_to` associations also works as expected.
|
79
80
|
|
80
|
-
## Limitations / TODO
|
81
|
-
|
82
|
-
- Gem supports only PostgreSQL for Rails 6
|
83
|
-
|
84
81
|
## Contributing
|
85
82
|
|
86
83
|
Bug reports and pull requests are welcome on GitHub at [https://github.com/evilmartians/activerecord-slotted_counters](https://github.com/evilmartians/activerecord-slotted_counters).
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordSlottedCounters
|
4
|
+
module Adapters
|
5
|
+
class MysqlUpsert
|
6
|
+
attr_reader :klass
|
7
|
+
|
8
|
+
def initialize(klass)
|
9
|
+
@klass = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply?(current_adapter_name)
|
13
|
+
return false unless defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
14
|
+
|
15
|
+
current_adapter_name == ActiveRecord::ConnectionAdapters::Mysql2Adapter::ADAPTER_NAME
|
16
|
+
end
|
17
|
+
|
18
|
+
def bulk_insert(attributes, on_duplicate: nil, **)
|
19
|
+
raise ArgumentError, "Values must not be empty" if attributes.empty?
|
20
|
+
|
21
|
+
keys = attributes.first.keys + klass.all_timestamp_attributes_in_model
|
22
|
+
|
23
|
+
current_time = klass.current_time_from_proper_timezone
|
24
|
+
data = attributes.map { |attr| attr.values + [current_time, current_time] }
|
25
|
+
|
26
|
+
columns = columns_for_attributes(keys)
|
27
|
+
|
28
|
+
fields_str = quote_column_names(columns)
|
29
|
+
values_str = quote_many_records(columns, data)
|
30
|
+
|
31
|
+
sql = <<~SQL
|
32
|
+
INSERT INTO #{klass.quoted_table_name}
|
33
|
+
(#{fields_str})
|
34
|
+
VALUES #{values_str}
|
35
|
+
SQL
|
36
|
+
|
37
|
+
if on_duplicate.present?
|
38
|
+
sql += " ON DUPLICATE KEY UPDATE #{on_duplicate};"
|
39
|
+
end
|
40
|
+
|
41
|
+
# insert/update and return amount of updated rows
|
42
|
+
klass.connection.update(sql)
|
43
|
+
end
|
44
|
+
|
45
|
+
def wrap_column_name(value)
|
46
|
+
"VALUES(#{value})"
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def columns_for_attributes(attributes)
|
52
|
+
attributes.map do |attribute|
|
53
|
+
klass.column_for_attribute(attribute)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def quote_column_names(columns, table_name: false)
|
58
|
+
columns.map do |column|
|
59
|
+
column_name = klass.connection.quote_column_name(column.name)
|
60
|
+
|
61
|
+
if table_name
|
62
|
+
"#{klass.quoted_table_name}.#{column_name}"
|
63
|
+
else
|
64
|
+
column_name
|
65
|
+
end
|
66
|
+
end.join(",")
|
67
|
+
end
|
68
|
+
|
69
|
+
def quote_record(columns, record_values)
|
70
|
+
values_str = record_values.each_with_index.map do |value, i|
|
71
|
+
type = klass.connection.lookup_cast_type_from_column(columns[i])
|
72
|
+
klass.connection.quote(type.serialize(value))
|
73
|
+
end.join(",")
|
74
|
+
|
75
|
+
"(#{values_str})"
|
76
|
+
end
|
77
|
+
|
78
|
+
def quote_many_records(columns, data)
|
79
|
+
data.map { |values| quote_record(columns, values) }.join(",")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -9,8 +9,11 @@ module ActiveRecordSlottedCounters
|
|
9
9
|
@klass = klass
|
10
10
|
end
|
11
11
|
|
12
|
-
def apply?
|
13
|
-
ActiveRecord::VERSION::MAJOR
|
12
|
+
def apply?(current_adapter_name)
|
13
|
+
return false if ActiveRecord::VERSION::MAJOR >= 7
|
14
|
+
return false unless defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
15
|
+
|
16
|
+
current_adapter_name == ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::ADAPTER_NAME
|
14
17
|
end
|
15
18
|
|
16
19
|
def bulk_insert(attributes, on_duplicate: nil, unique_by: nil)
|
@@ -46,7 +49,11 @@ module ActiveRecordSlottedCounters
|
|
46
49
|
|
47
50
|
sql += " RETURNING \"id\""
|
48
51
|
|
49
|
-
klass.connection.exec_query(sql)
|
52
|
+
klass.connection.exec_query(sql).rows.count
|
53
|
+
end
|
54
|
+
|
55
|
+
def wrap_column_name(value)
|
56
|
+
"EXCLUDED.#{value}"
|
50
57
|
end
|
51
58
|
|
52
59
|
private
|
@@ -9,12 +9,16 @@ module ActiveRecordSlottedCounters
|
|
9
9
|
@klass = klass
|
10
10
|
end
|
11
11
|
|
12
|
-
def apply?
|
12
|
+
def apply?(_)
|
13
13
|
ActiveRecord::VERSION::MAJOR >= 7
|
14
14
|
end
|
15
15
|
|
16
16
|
def bulk_insert(attributes, on_duplicate: nil, unique_by: nil)
|
17
|
-
klass.upsert_all(attributes, on_duplicate: on_duplicate, unique_by: unique_by)
|
17
|
+
klass.upsert_all(attributes, on_duplicate: on_duplicate, unique_by: unique_by).rows.count
|
18
|
+
end
|
19
|
+
|
20
|
+
def wrap_column_name(value)
|
21
|
+
"EXCLUDED.#{value}"
|
18
22
|
end
|
19
23
|
end
|
20
24
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordSlottedCounters
|
4
|
+
module Adapters
|
5
|
+
class SqliteUpsert
|
6
|
+
attr_reader :klass
|
7
|
+
|
8
|
+
def initialize(klass)
|
9
|
+
@klass = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def apply?(current_adapter_name)
|
13
|
+
return false if ActiveRecord::VERSION::MAJOR >= 7
|
14
|
+
return false unless defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
|
15
|
+
|
16
|
+
current_adapter_name == ActiveRecord::ConnectionAdapters::SQLite3Adapter::ADAPTER_NAME
|
17
|
+
end
|
18
|
+
|
19
|
+
def bulk_insert(attributes, on_duplicate: nil, unique_by: nil)
|
20
|
+
raise ArgumentError, "Values must not be empty" if attributes.empty?
|
21
|
+
|
22
|
+
keys = attributes.first.keys + klass.all_timestamp_attributes_in_model
|
23
|
+
|
24
|
+
current_time = klass.current_time_from_proper_timezone
|
25
|
+
data = attributes.map { |attr| attr.values + [current_time, current_time] }
|
26
|
+
|
27
|
+
columns = columns_for_attributes(keys)
|
28
|
+
|
29
|
+
fields_str = quote_column_names(columns)
|
30
|
+
values_str = quote_many_records(columns, data)
|
31
|
+
|
32
|
+
sql = <<~SQL
|
33
|
+
INSERT INTO #{klass.quoted_table_name}
|
34
|
+
(#{fields_str})
|
35
|
+
VALUES #{values_str}
|
36
|
+
SQL
|
37
|
+
|
38
|
+
if unique_by.present?
|
39
|
+
index = unique_indexes.find { |i| i.name.to_sym == unique_by }
|
40
|
+
columns = columns_for_attributes(index.columns)
|
41
|
+
fields = quote_column_names(columns)
|
42
|
+
|
43
|
+
sql += " ON CONFLICT (#{fields})"
|
44
|
+
end
|
45
|
+
|
46
|
+
if on_duplicate.present?
|
47
|
+
sql += " DO UPDATE SET #{on_duplicate}"
|
48
|
+
end
|
49
|
+
|
50
|
+
sql += " RETURNING \"id\""
|
51
|
+
|
52
|
+
klass.connection.exec_query(sql).rows.count
|
53
|
+
end
|
54
|
+
|
55
|
+
def wrap_column_name(value)
|
56
|
+
"EXCLUDED.#{value}"
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def unique_indexes
|
62
|
+
klass.connection.schema_cache.indexes(klass.table_name).select(&:unique)
|
63
|
+
end
|
64
|
+
|
65
|
+
def columns_for_attributes(attributes)
|
66
|
+
attributes.map do |attribute|
|
67
|
+
klass.column_for_attribute(attribute)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def quote_column_names(columns, table_name: false)
|
72
|
+
columns.map do |column|
|
73
|
+
column_name = klass.connection.quote_column_name(column.name)
|
74
|
+
if table_name
|
75
|
+
"#{klass.quoted_table_name}.#{column_name}"
|
76
|
+
else
|
77
|
+
column_name
|
78
|
+
end
|
79
|
+
end.join(",")
|
80
|
+
end
|
81
|
+
|
82
|
+
def quote_record(columns, record_values)
|
83
|
+
values_str = record_values.each_with_index.map do |value, i|
|
84
|
+
type = klass.connection.lookup_cast_type_from_column(columns[i])
|
85
|
+
klass.connection.quote(type.serialize(value))
|
86
|
+
end.join(",")
|
87
|
+
"(#{values_str})"
|
88
|
+
end
|
89
|
+
|
90
|
+
def quote_many_records(columns, data)
|
91
|
+
data.map { |values| quote_record(columns, values) }.join(",")
|
92
|
+
end
|
93
|
+
|
94
|
+
def postgresql_connection?(adapter_name)
|
95
|
+
return false unless defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
96
|
+
|
97
|
+
adapter_name == ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::ADAPTER_NAME
|
98
|
+
end
|
99
|
+
|
100
|
+
def sqlite_connection?(adapter_name)
|
101
|
+
return false unless defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
|
102
|
+
|
103
|
+
adapter_name == ActiveRecord::ConnectionAdapters::SQLite3Adapter::ADAPTER_NAME
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -5,6 +5,8 @@ require "activerecord_slotted_counters/utils"
|
|
5
5
|
|
6
6
|
require "activerecord_slotted_counters/adapters/rails_upsert"
|
7
7
|
require "activerecord_slotted_counters/adapters/pg_upsert"
|
8
|
+
require "activerecord_slotted_counters/adapters/sqlite_upsert"
|
9
|
+
require "activerecord_slotted_counters/adapters/mysql_upsert"
|
8
10
|
|
9
11
|
module ActiveRecordSlottedCounters
|
10
12
|
class SlottedCounter < ::ActiveRecord::Base
|
@@ -24,7 +26,8 @@ module ActiveRecordSlottedCounters
|
|
24
26
|
|
25
27
|
class << self
|
26
28
|
def bulk_insert(attributes)
|
27
|
-
on_duplicate_clause =
|
29
|
+
on_duplicate_clause =
|
30
|
+
"count = slotted_counters.count + #{slotted_counter_db_adapter.wrap_column_name("count")}"
|
28
31
|
|
29
32
|
slotted_counter_db_adapter.bulk_insert(
|
30
33
|
attributes,
|
@@ -41,15 +44,19 @@ module ActiveRecordSlottedCounters
|
|
41
44
|
|
42
45
|
def set_slotted_counter_db_adapter
|
43
46
|
available_adapters = [
|
44
|
-
ActiveRecordSlottedCounters::Adapters::
|
45
|
-
ActiveRecordSlottedCounters::Adapters::
|
47
|
+
ActiveRecordSlottedCounters::Adapters::MysqlUpsert,
|
48
|
+
ActiveRecordSlottedCounters::Adapters::SqliteUpsert,
|
49
|
+
ActiveRecordSlottedCounters::Adapters::PgUpsert,
|
50
|
+
ActiveRecordSlottedCounters::Adapters::RailsUpsert
|
46
51
|
]
|
47
52
|
|
53
|
+
current_adapter_name = connection.adapter_name
|
54
|
+
|
48
55
|
adapter = available_adapters
|
49
56
|
.map { |adapter| adapter.new(self) }
|
50
|
-
.detect { |adapter| adapter.apply? }
|
57
|
+
.detect { |adapter| adapter.apply?(current_adapter_name) }
|
51
58
|
|
52
|
-
raise NotSupportedAdapter.new(
|
59
|
+
raise NotSupportedAdapter.new(current_adapter_name) if adapter.nil?
|
53
60
|
|
54
61
|
adapter
|
55
62
|
end
|
@@ -205,9 +212,7 @@ module ActiveRecordSlottedCounters
|
|
205
212
|
def insert_counters_records(ids, counters)
|
206
213
|
counters_params = prepare_slotted_counters_params(ids, counters)
|
207
214
|
|
208
|
-
|
209
|
-
|
210
|
-
result.rows.count
|
215
|
+
ActiveRecordSlottedCounters::SlottedCounter.bulk_insert(counters_params)
|
211
216
|
end
|
212
217
|
|
213
218
|
def remove_counters_records(ids, counter_name)
|
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.
|
4
|
+
version: 0.2.0
|
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-10-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -67,34 +67,6 @@ dependencies:
|
|
67
67
|
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
69
|
version: '3.9'
|
70
|
-
- !ruby/object:Gem::Dependency
|
71
|
-
name: pg
|
72
|
-
requirement: !ruby/object:Gem::Requirement
|
73
|
-
requirements:
|
74
|
-
- - ">="
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
version: '1.4'
|
77
|
-
type: :development
|
78
|
-
prerelease: false
|
79
|
-
version_requirements: !ruby/object:Gem::Requirement
|
80
|
-
requirements:
|
81
|
-
- - ">="
|
82
|
-
- !ruby/object:Gem::Version
|
83
|
-
version: '1.4'
|
84
|
-
- !ruby/object:Gem::Dependency
|
85
|
-
name: sqlite3
|
86
|
-
requirement: !ruby/object:Gem::Requirement
|
87
|
-
requirements:
|
88
|
-
- - ">="
|
89
|
-
- !ruby/object:Gem::Version
|
90
|
-
version: '0'
|
91
|
-
type: :development
|
92
|
-
prerelease: false
|
93
|
-
version_requirements: !ruby/object:Gem::Requirement
|
94
|
-
requirements:
|
95
|
-
- - ">="
|
96
|
-
- !ruby/object:Gem::Version
|
97
|
-
version: '0'
|
98
70
|
- !ruby/object:Gem::Dependency
|
99
71
|
name: rspec-sqlimit
|
100
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -120,8 +92,10 @@ files:
|
|
120
92
|
- LICENSE.txt
|
121
93
|
- README.md
|
122
94
|
- lib/activerecord-slotted_counters.rb
|
95
|
+
- lib/activerecord_slotted_counters/adapters/mysql_upsert.rb
|
123
96
|
- lib/activerecord_slotted_counters/adapters/pg_upsert.rb
|
124
97
|
- lib/activerecord_slotted_counters/adapters/rails_upsert.rb
|
98
|
+
- lib/activerecord_slotted_counters/adapters/sqlite_upsert.rb
|
125
99
|
- lib/activerecord_slotted_counters/has_slotted_counter.rb
|
126
100
|
- lib/activerecord_slotted_counters/railtie.rb
|
127
101
|
- lib/activerecord_slotted_counters/utils.rb
|
@@ -137,7 +111,7 @@ metadata:
|
|
137
111
|
documentation_uri: http://github.com/evilmartians/activerecord-slotted_counters
|
138
112
|
homepage_uri: http://github.com/evilmartians/activerecord-slotted_counters
|
139
113
|
source_code_uri: http://github.com/evilmartians/activerecord-slotted_counters
|
140
|
-
post_install_message:
|
114
|
+
post_install_message:
|
141
115
|
rdoc_options: []
|
142
116
|
require_paths:
|
143
117
|
- lib
|
@@ -152,8 +126,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
126
|
- !ruby/object:Gem::Version
|
153
127
|
version: '0'
|
154
128
|
requirements: []
|
155
|
-
rubygems_version: 3.
|
156
|
-
signing_key:
|
129
|
+
rubygems_version: 3.4.20
|
130
|
+
signing_key:
|
157
131
|
specification_version: 4
|
158
132
|
summary: Active Record slotted counters support
|
159
133
|
test_files: []
|