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 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: []