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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: efdd024fe0a5944a7cbcaa4e6990a05b74b80634affb59d3c0a30a0a3b08c52e
4
- data.tar.gz: 70e2607be4d82c909002d01856d76b6663307b1ccd1204015db6e160a33c6193
3
+ metadata.gz: 76c73b0a3e75e1fbddb355beedf5aa5967f43ffb5043d0051572423ef3a2188c
4
+ data.tar.gz: be40038645de42ee29e31c02cdafac846641d079404c65ac12a44726d22dc8fa
5
5
  SHA512:
6
- metadata.gz: 5b5a4570082a3cf8264472ce8ccc59d50c52621323937cdfddc48cddfbbca01020e7e76f70457e98eeb5a5f32b960979f3315abafd880873788eb5ccd78e8a78
7
- data.tar.gz: e221d1d149adbe60e4b7a60f76aced3a9d3f1c16f7e37950aad54ba44e9a7828a92e7a411be51bd0a91ecfe5827716e09080b491f375d0fe7f0cb18781e9981b
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
- ### Supported Ruby versions
25
+ ### Requirements
26
26
 
27
- - Ruby (MRI) >= 2.7.0
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 want preload counters for multiple records, you can use a convenient `#with_slotted_counters` method:
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 < 7 && klass.connection.adapter_name == ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::ADAPTER_NAME
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 = "count = slotted_counters.count + excluded.count"
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::RailsUpsert,
45
- ActiveRecordSlottedCounters::Adapters::PgUpsert
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(connection.adapter_name) if adapter.nil?
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
- result = ActiveRecordSlottedCounters::SlottedCounter.bulk_insert(counters_params)
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordSlottedCounters # :nodoc:
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.0"
5
5
  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
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-04-20 00:00:00.000000000 Z
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.3.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: []