activerecord-slotted_counters 0.1.2 → 0.1.4

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