module EventCounter::ActiveRecordExtension::CountableClassMethods

This module defines class methods for a countable model

Constants

INTERVALS

Public Instance Methods

at_tz(str, tz) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 206
def at_tz(str, tz)
  "#{str} AT TIME ZONE #{sanitize(tz)}"
end
counter_error!(*args) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 210
def counter_error!(*args)
  fail EventCounter::CounterError, args
end
data_for(name, id = nil, interval: nil, range: nil, raw: nil) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 71
      def data_for(name, id = nil, interval: nil, range: nil, raw: nil)
        interval = normalize_interval!(name, interval)

        range = normalize_range!(range, interval) if range

        tz = Time.zone.tzinfo.identifier
        tz_storage = (default_timezone == :utc ? 'UTC' : Time.now.zone)

        subq = EventCounter
          .select(subq_select(interval, tz))
          .where(name: name, countable_type: self)
          .where(id && { countable_id: id })
          .within(range)
          .group("1")
          .order("1")
          .to_sql

        q = <<-SQL.squish!
          SELECT created_at, value
          FROM (#{series(interval, tz, range)}) intervals
          LEFT OUTER JOIN (#{subq}) counters USING (created_at)
          ORDER BY 1
        SQL

        result = connection.execute(q).to_a

        raw ? result : normalize_counters_data!(result)
      end
default_interval_for(name) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 256
def default_interval_for(name)
  event_counters[name.to_sym]
end
dtrunc(interval, str, tz) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 202
def dtrunc(interval, str, tz)
  "date_trunc(#{sanitize(interval)}, #{at_tz("#{str}::timestamptz", tz)})"
end
floor_tstamp(tstamp, interval) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 118
      def floor_tstamp(tstamp, interval)
        <<-SQL
          floor(EXTRACT(EPOCH FROM #{tstamp}) /
          #{sanitize(interval)})::int * #{sanitize(interval)}
        SQL
      end
interval_as_integer(interval) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 246
def interval_as_integer(interval)
  interval.is_a?(Symbol) ? INTERVALS[interval] : interval
end
interval_symbol(interval) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 198
def interval_symbol(interval)
  "interval #{sanitize(interval).insert(1, '1 ')}"
end
less_then_default?(*args) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 234
def less_then_default?(*args)
  default, provided = args.map do |arg|
    interval_as_integer(arg)
  end
  provided < default
end
multiple_of_default?(default_interval, provided) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 241
def multiple_of_default?(default_interval, provided)
  return true if provided.is_a?(Symbol)
  provided.modulo(default_interval).zero?
end
normalize_counters_data!(data) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 250
def normalize_counters_data!(data)
  data.map do |i|
    [ Time.zone.parse(i['created_at']), i['value'].to_i ]
  end
end
normalize_interval!(name, interval) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 214
def normalize_interval!(name, interval)
  default_interval = interval_as_integer(default_interval_for(name))

  h = {
    default_interval: default_interval,
    interval: interval,
    model: self.class.name
  }

  return default_interval.to_i unless interval

  counter_error!(:not_found, name: name) unless default_interval
  counter_error!(:less, h) if less_then_default?(default_interval, interval)
  unless multiple_of_default?(default_interval, interval)
    counter_error!(:multiple, h)
  end

  interval.respond_to?(:to_i) ? interval.to_i : interval
end
normalize_range!(range, interval) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 260
def normalize_range!(range, interval)
  range_min, range_max =
    case interval
    when Symbol
      [
        range.min.send(:"beginning_of_#{interval}"),
        range.max.send(:"end_of_#{interval}")
      ]
    else
      [ range.min.floor(interval), range.max.floor(interval) ]
    end

  # TODO: ensure that range in time zone
  range_min..range_max
end
series(*args) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 125
def series(*args)
  args.first.is_a?(Symbol) ? series_symbol(*args) : series_integer(*args)
end
series_integer(interval, tz, range = nil) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 159
def series_integer(interval, tz, range = nil)
  if range
    series_integer_with_range(interval, tz, range)
  else
    series_integer_without_range(interval, tz)
  end
end
series_integer_with_range(interval, tz, range = nil) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 167
      def series_integer_with_range(interval, tz, range = nil)
        interval_sql = %Q(#{sanitize(interval)} * interval '1 seconds')
        range_min, range_max = range.min.to_s(:db), range.max.to_s(:db)

        a = [ sanitize(range_min), sanitize(range_max), interval_sql ]
        <<-SQL
          SELECT generate_series(#{a[0]}, #{a[1]}, #{a[2]}) AS created_at
        SQL
      end
series_integer_without_range(interval, tz) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 177
      def series_integer_without_range(interval, tz)
        interval_sql = sanitize(interval)
        if default_timezone == :utc
          a = [
            floor_tstamp('min(created_at)', interval),
            floor_tstamp('max(created_at)', interval),
            interval_sql
          ]
        else
          z = Time.new.zone
          a = [
            floor_tstamp(at_tz('min(created_at)', z), interval),
            floor_tstamp(at_tz('max(created_at)', z), interval),
            interval_sql
          ]
        end
        EventCounter.select(<<-SQL).to_sql
          to_timestamp(generate_series(#{a[0]}, #{a[1]}, #{a[2]})) AS created_at
        SQL
      end
series_symbol(interval, tz, range = nil) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 129
def series_symbol(interval, tz, range = nil)
  if range
    series_symbol_with_range(interval, tz, range)
  else
    series_symbol_without_range(interval, tz)
  end
end
series_symbol_with_range(interval, tz, range) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 137
def series_symbol_with_range(interval, tz, range)
  range_min, range_max = range.min, range.max
  a = [
    dtrunc(interval, sanitize(range_min.to_s(:db)), tz),
    dtrunc(interval, sanitize(range_max.to_s(:db)), tz),
    interval_symbol(interval)
  ]

  "SELECT generate_series(#{a[0]}, #{a[1]}, #{a[2]}) AS created_at"
end
series_symbol_without_range(interval, tz) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 148
      def series_symbol_without_range(interval, tz)
        a = [
          dtrunc(interval, 'min(created_at)', tz),
          dtrunc(interval, 'max(created_at)', tz),
          interval_symbol(interval)
        ]
        EventCounter.select(<<-SQL).to_sql
          generate_series(#{a[0]}, #{a[1]}, #{a[2]}) AS created_at
        SQL
      end
subq_extract(interval, tz) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 104
def subq_extract(interval, tz)
  case interval
  when Symbol
    dtrunc(interval, 'created_at', tz)
  else
    time = floor_tstamp('created_at', interval)
    if default_timezone == :utc
      "to_timestamp(#{time})"
    else
      at_tz("to_timestamp(#{time})::timestamp", Time.new.zone)
    end
  end
end
subq_select(interval, tz) click to toggle source
# File lib/event_counter/active_record_extension.rb, line 100
def subq_select(interval, tz)
  "#{subq_extract(interval, tz)} as created_at, sum(value) AS value"
end