activerecord-slotted_counters 0.1.2 → 0.1.4

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: 94aead1f1be073e3dbf7548257d6db2b8eda15be09f2c199b6b3cdbf9b6ba0f3
4
- data.tar.gz: ac2e1b977ab04501cd1139bd1d410b9f58b0b9395add905b1034eebb54fe4c86
3
+ metadata.gz: efdd024fe0a5944a7cbcaa4e6990a05b74b80634affb59d3c0a30a0a3b08c52e
4
+ data.tar.gz: 70e2607be4d82c909002d01856d76b6663307b1ccd1204015db6e160a33c6193
5
5
  SHA512:
6
- metadata.gz: ba74654e981dbc83043dc295296939e85053b2c7c8543ca3472d2f659523b9a3ea5be60ac15a87a492f7fa3758defac451382ff518a96f4feee573eb0f68ed26
7
- data.tar.gz: 857ec7de346c584b1fcdd79bcb81becb7ecec9bb86b2087a8acf83d25ed13b55d25883439b6242b3f26bd726cc592897930ff9f1755c720b7aca41ed55d91503
6
+ metadata.gz: 5b5a4570082a3cf8264472ce8ccc59d50c52621323937cdfddc48cddfbbca01020e7e76f70457e98eeb5a5f32b960979f3315abafd880873788eb5ccd78e8a78
7
+ data.tar.gz: e221d1d149adbe60e4b7a60f76aced3a9d3f1c16f7e37950aad54ba44e9a7828a92e7a411be51bd0a91ecfe5827716e09080b491f375d0fe7f0cb18781e9981b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.1.4 (2023-04-19)
6
+
7
+ - Fix "can't modify frozen String" for the pg adapter (ruby 2.7) [#15](https://github.com/evilmartians/activerecord-slotted_counters/pull/15) ([@LukinEgor][])
8
+
9
+ ## 0.1.3 (2023-04-03)
10
+
11
+ - Add Rails 6 support (PostgreSQL only) [#13](https://github.com/evilmartians/activerecord-slotted_counters/pull/13) ([@LukinEgor][])
12
+
5
13
  ## 0.1.2 (2023-03-28)
6
14
 
7
15
  - Fix preloading counters for multiple records [#12](https://github.com/evilmartians/activerecord-slotted_counters/pull/12) ([@LukinEgor][])
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,11 +3,57 @@
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
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
+
8
21
  scope :associated_records, ->(counter_name, klass) do
9
22
  where(counter_name: counter_name, associated_record_type: klass)
10
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
56
+ end
11
57
  end
12
58
 
13
59
  module BelongsToAssociation
@@ -41,8 +87,28 @@ module ActiveRecordSlottedCounters
41
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordSlottedCounters # :nodoc:
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-slotted_counters
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Egor Lukin
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-04-19 00:00:00.000000000 Z
12
+ date: 2023-04-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -120,6 +120,8 @@ files:
120
120
  - LICENSE.txt
121
121
  - README.md
122
122
  - lib/activerecord-slotted_counters.rb
123
+ - lib/activerecord_slotted_counters/adapters/pg_upsert.rb
124
+ - lib/activerecord_slotted_counters/adapters/rails_upsert.rb
123
125
  - lib/activerecord_slotted_counters/has_slotted_counter.rb
124
126
  - lib/activerecord_slotted_counters/railtie.rb
125
127
  - lib/activerecord_slotted_counters/utils.rb