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