activerecord-slotted_counters 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|