about summary refs log tree commit diff
path: root/app/models/concerns/account_counters.rb
blob: fd3f161ad53ae2e67aff0e44c6f4d7bfd4df720d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# frozen_string_literal: true

module AccountCounters
  extend ActiveSupport::Concern

  ALLOWED_COUNTER_KEYS = %i(statuses_count following_count followers_count).freeze

  included do
    has_one :account_stat, inverse_of: :account
    after_save :save_account_stat
  end

  delegate :statuses_count,
           :statuses_count=,
           :following_count,
           :following_count=,
           :followers_count,
           :followers_count=,
           :last_status_at,
           to: :account_stat

  # @param [Symbol] key
  def increment_count!(key)
    update_count!(key, 1)
  end

  # @param [Symbol] key
  def decrement_count!(key)
    update_count!(key, -1)
  end

  # @param [Symbol] key
  # @param [Integer] value
  def update_count!(key, value)
    raise ArgumentError, "Invalid key #{key}" unless ALLOWED_COUNTER_KEYS.include?(key)
    raise ArgumentError, 'Do not call update_count! on dirty objects' if association(:account_stat).loaded? && account_stat&.changed? && account_stat.changed_attribute_names_to_save == %w(id)

    value = value.to_i
    default_value = value.positive? ? value : 0

    # We do an upsert using manually written SQL, as Rails' upsert method does
    # not seem to support writing expressions in the UPDATE clause, but only
    # re-insert the provided values instead.
    # Even ARel seem to be missing proper handling of upserts.
    sql = if value.positive? && key == :statuses_count
            <<-SQL.squish
              INSERT INTO account_stats(account_id, #{key}, created_at, updated_at, last_status_at)
                VALUES (:account_id, :default_value, now(), now(), now())
              ON CONFLICT (account_id) DO UPDATE
              SET #{key} = account_stats.#{key} + :value,
                  last_status_at = now(),
                  lock_version = account_stats.lock_version + 1,
                  updated_at = now()
              RETURNING id;
            SQL
          else
            <<-SQL.squish
              INSERT INTO account_stats(account_id, #{key}, created_at, updated_at)
                VALUES (:account_id, :default_value, now(), now())
              ON CONFLICT (account_id) DO UPDATE
              SET #{key} = account_stats.#{key} + :value,
                  lock_version = account_stats.lock_version + 1,
                  updated_at = now()
              RETURNING id;
            SQL
          end

    sql = AccountStat.sanitize_sql([sql, account_id: id, default_value: default_value, value: value])
    account_stat_id = AccountStat.connection.exec_query(sql)[0]['id']

    # Reload account_stat if it was loaded, taking into account newly-created unsaved records
    if association(:account_stat).loaded?
      account_stat.id = account_stat_id if account_stat.new_record?
      account_stat.reload
    end
  end

  def account_stat
    super || build_account_stat
  end

  private

  def save_account_stat
    return unless association(:account_stat).loaded? && account_stat&.changed?

    account_stat.save
  end
end