activerecord-slotted_counters 0.1.1 → 0.1.3

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: 5dcee16f487cc8b48e6fb4c4b84069cc97b9647d5e710fa8d8c2e25038620815
4
- data.tar.gz: 43b9d1432b7aa8088c1508038d8927cf4aca1e8e33e3a159a3b915b7037f8084
3
+ metadata.gz: bdd6573e6f4d045624e5d0e068f40728ece0ff47461b0c476606689b74de6c7d
4
+ data.tar.gz: 79fc88a8fd00ff709b34b9cfd192a9275b95647639281ee36f10e2b61b5eb79a
5
5
  SHA512:
6
- metadata.gz: 9783f3d0166756156a12e2337539acca146ebb8cbec1298d3b3b06a8721c2a23c18e61bc38c2944cd2fb999f798129e30ef608c4f862fad4e3d7628573cd49d3
7
- data.tar.gz: a6a250dd5daed59fd0495954c9220e02de57e99232d2e19f7e159d9f838d4ad6b2a90acc3ece9f466a829afc99467ec4ef096130827be9d3ba13197c665cb367
6
+ metadata.gz: d2fdb9401b2473f9d1ea8048bc14056665a310fe52fb4c0e1572b989ddab04f4907fa15df57821f7792b8861bd1d07259da9ddef425281c423c68d37e922ebbe
7
+ data.tar.gz: 5e399bc56136750d40f5303677ce381ae791a56a96f52ac2df9b1c6f7ca6443c4eaef2cad71b631231d043023f1e6a1a8643f54bb607afd68c73462cdafc25db
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.3 (2023-04-03)
6
+
7
+ - Add Rails 6 support (PostgreSQL only) [#13](https://github.com/evilmartians/activerecord-slotted_counters/pull/13) ([@LukinEgor][])
8
+
9
+ ## 0.1.2 (2023-03-28)
10
+
11
+ - Fix preloading counters for multiple records [#12](https://github.com/evilmartians/activerecord-slotted_counters/pull/12) ([@LukinEgor][])
12
+
5
13
  ## 0.1.1 (2023-01-17)
6
14
 
7
15
  - Fix prevent double increment/decrement of native counter caches [#10](https://github.com/evilmartians/activerecord-slotted_counters/pull/10) ([@danielwestendorf][])
data/README.md CHANGED
@@ -79,7 +79,7 @@ Using `counter_cache: true` on `belongs_to` associations also works as expected.
79
79
 
80
80
  ## Limitations / TODO
81
81
 
82
- - Rails 6 support
82
+ - Gem supports only PostgreSQL for Rails 6
83
83
 
84
84
  ## Contributing
85
85
 
@@ -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,10 +3,56 @@
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
8
- scope :associated_records, ->(counter_name, id, klass) do
9
- where(counter_name: counter_name, associated_record_id: id, associated_record_type: klass)
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
+
21
+ scope :associated_records, ->(counter_name, klass) do
22
+ where(counter_name: counter_name, associated_record_type: klass)
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
10
56
  end
11
57
  end
12
58
 
@@ -38,11 +84,31 @@ module ActiveRecordSlottedCounters
38
84
  counter_name = slotted_counter_name(counter_type)
39
85
  association_name = slotted_counter_association_name(counter_type)
40
86
 
41
- has_many association_name, ->(model) { associated_records(counter_name, model.id, model.class.to_s) }, **SLOTTED_COUNTERS_ASSOCIATION_OPTIONS
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
- association_name = slotted_counter_association_name(counter_type)
45
- preload(association_name)
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.upsert_all(
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
@@ -191,16 +252,8 @@ module ActiveRecordSlottedCounters
191
252
 
192
253
  def read_slotted_counter(counter_type)
193
254
  association_name = slotted_counter_association_name(counter_type)
194
-
195
- if association_cached?(association_name)
196
- scope = association(association_name).scope
197
- counter = scope.sum(&:count)
198
-
199
- return counter
200
- end
201
-
202
- scope = send(association_name)
203
- scope.sum(:count)
255
+ scope = association(association_name).load_target
256
+ scope.sum(&:count)
204
257
  end
205
258
  end
206
259
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordSlottedCounters # :nodoc:
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
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.1
4
+ version: 0.1.3
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-01-17 00:00:00.000000000 Z
12
+ date: 2023-04-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -95,6 +95,20 @@ dependencies:
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rspec-sqlimit
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
98
112
  description: Active Record slotted counters support
99
113
  email:
100
114
  - dementiev.vm@gmail.com
@@ -106,6 +120,8 @@ files:
106
120
  - LICENSE.txt
107
121
  - README.md
108
122
  - lib/activerecord-slotted_counters.rb
123
+ - lib/activerecord_slotted_counters/adapters/pg_upsert.rb
124
+ - lib/activerecord_slotted_counters/adapters/rails_upsert.rb
109
125
  - lib/activerecord_slotted_counters/has_slotted_counter.rb
110
126
  - lib/activerecord_slotted_counters/railtie.rb
111
127
  - lib/activerecord_slotted_counters/utils.rb
@@ -121,7 +137,7 @@ metadata:
121
137
  documentation_uri: http://github.com/evilmartians/activerecord-slotted_counters
122
138
  homepage_uri: http://github.com/evilmartians/activerecord-slotted_counters
123
139
  source_code_uri: http://github.com/evilmartians/activerecord-slotted_counters
124
- post_install_message:
140
+ post_install_message:
125
141
  rdoc_options: []
126
142
  require_paths:
127
143
  - lib
@@ -136,8 +152,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
152
  - !ruby/object:Gem::Version
137
153
  version: '0'
138
154
  requirements: []
139
- rubygems_version: 3.3.11
140
- signing_key:
155
+ rubygems_version: 3.3.3
156
+ signing_key:
141
157
  specification_version: 4
142
158
  summary: Active Record slotted counters support
143
159
  test_files: []