activerecord-slotted_counters 0.1.1 → 0.1.3

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