activerecord-slotted_counters 0.1.2 → 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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +1 -1
- data/lib/activerecord_slotted_counters/adapters/pg_upsert.rb +88 -0
- data/lib/activerecord_slotted_counters/adapters/rails_upsert.rb +21 -0
- data/lib/activerecord_slotted_counters/has_slotted_counter.rb +69 -8
- data/lib/activerecord_slotted_counters/version.rb +1 -1
- metadata +3 -1
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: bdd6573e6f4d045624e5d0e068f40728ece0ff47461b0c476606689b74de6c7d
         | 
| 4 | 
            +
              data.tar.gz: 79fc88a8fd00ff709b34b9cfd192a9275b95647639281ee36f10e2b61b5eb79a
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: d2fdb9401b2473f9d1ea8048bc14056665a310fe52fb4c0e1572b989ddab04f4907fa15df57821f7792b8861bd1d07259da9ddef425281c423c68d37e922ebbe
         | 
| 7 | 
            +
              data.tar.gz: 5e399bc56136750d40f5303677ce381ae791a56a96f52ac2df9b1c6f7ca6443c4eaef2cad71b631231d043023f1e6a1a8643f54bb607afd68c73462cdafc25db
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -2,6 +2,10 @@ | |
| 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 | 
            +
             | 
| 5 9 | 
             
            ## 0.1.2 (2023-03-28)
         | 
| 6 10 |  | 
| 7 11 | 
             
            - Fix preloading counters for multiple records [#12](https://github.com/evilmartians/activerecord-slotted_counters/pull/12) ([@LukinEgor][])
         | 
    
        data/README.md
    CHANGED
    
    
| @@ -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 | 
            -
                       | 
| 45 | 
            -
                      preload( | 
| 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. | 
| 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
         | 
    
        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. | 
| 4 | 
            +
              version: 0.1.3
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Egor Lukin
         | 
| @@ -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
         |