class Rollup::Aggregator
Public Class Methods
new(klass)
click to toggle source
# File lib/rollup/aggregator.rb, line 3 def initialize(klass) @klass = klass # or relation end
Public Instance Methods
determine_dimension_name(group)
click to toggle source
# File lib/rollup/aggregator.rb, line 97 def determine_dimension_name(group) # split by ., ->>, and -> and remove whitespace value = group.to_s.split(/\s*((\.)|(->>)|(->))\s*/).last # removing starting and ending quotes # for simplicity, they don't need to be the same value = value[1..-2] if value.match(/\A["'`].+["'`]\z/) unless value.match(/\A\w+\z/) raise "Cannot determine dimension name: #{group}. Use the dimension_names option" end value end
maybe_clear(clear, name, interval) { || ... }
click to toggle source
# File lib/rollup/aggregator.rb, line 157 def maybe_clear(clear, name, interval) if clear Rollup.transaction do Rollup.where(name: name, interval: interval).delete_all yield end else yield end end
perform_calculation(relation) { |relation| ... }
click to toggle source
calculation can mutate relation, but that's fine
# File lib/rollup/aggregator.rb, line 113 def perform_calculation(relation, &block) if block_given? yield(relation) else relation.count end end
perform_group(name, column:, interval:, time_zone:, current:, last:, clear:)
click to toggle source
# File lib/rollup/aggregator.rb, line 35 def perform_group(name, column:, interval:, time_zone:, current:, last:, clear:) raise ArgumentError, "Cannot use last and clear together" if last && clear time_zone ||= Rollup.time_zone gd_options = { current: current } # make sure Groupdate global options aren't applied gd_options[:time_zone] = time_zone gd_options[:week_start] = Rollup.week_start if interval.to_s == "week" gd_options[:day_start] = 0 if Utils.date_interval?(interval) if last gd_options[:last] = last elsif !clear # if no rollups, compute all intervals # if rollups, recompute last interval max_time = Rollup.where(name: name, interval: interval).maximum(Utils.time_sql(interval)) if max_time # for MySQL on Ubuntu 18.04 (and likely other platforms) if max_time.is_a?(String) utc = ActiveSupport::TimeZone["Etc/UTC"] max_time = Utils.date_interval?(interval) ? max_time.to_date : utc.parse(max_time).in_time_zone(time_zone) end # aligns perfectly if time zone doesn't change # if time zone does change, there are other problems besides this gd_options[:range] = max_time.. end end # intervals are stored as given # we don't normalize intervals (i.e. change 60s -> 1m) case interval.to_s when "hour", "day", "week", "month", "quarter", "year" @klass.group_by_period(interval, column, **gd_options) when /\A\d+s\z/ @klass.group_by_second(column, n: interval.to_i, **gd_options) when /\A\d+m\z/ @klass.group_by_minute(column, n: interval.to_i, **gd_options) else raise ArgumentError, "Invalid interval: #{interval}" end end
prepare_result(result, name, dimension_names, interval)
click to toggle source
# File lib/rollup/aggregator.rb, line 121 def prepare_result(result, name, dimension_names, interval) raise "Expected calculation to return Hash, not #{result.class.name}" unless result.is_a?(Hash) time_class = Utils.date_interval?(interval) ? Date : Time dimensions_supported = Utils.dimensions_supported? expected_key_size = dimension_names.size + 1 result.map do |key, value| dimensions = {} if dimensions_supported && dimension_names.any? unless key.is_a?(Array) && key.size == expected_key_size raise "Expected result key to be Array with size #{expected_key_size}" end time = key[-1] # may be able to support dimensions in SQLite by sorting dimension names dimension_names.each_with_index do |dn, i| dimensions[dn] = key[i] end else time = key end raise "Expected time to be #{time_class.name}, not #{time.class.name}" unless time.is_a?(time_class) raise "Expected value to be Numeric or nil, not #{value.class.name}" unless value.is_a?(Numeric) || value.nil? record = { name: name, interval: interval, time: time, value: value } record[:dimensions] = dimensions if dimensions_supported record end end
rollup(name, column: nil, interval: "day", dimension_names: nil, time_zone: nil, current: true, last: nil, clear: false, &block)
click to toggle source
# File lib/rollup/aggregator.rb, line 7 def rollup(name, column: nil, interval: "day", dimension_names: nil, time_zone: nil, current: true, last: nil, clear: false, &block) raise "Name can't be blank" if name.blank? column ||= @klass.rollup_column || :created_at validate_column(column) relation = perform_group(name, column: column, interval: interval, time_zone: time_zone, current: current, last: last, clear: clear) result = perform_calculation(relation, &block) dimension_names = set_dimension_names(dimension_names, relation) records = prepare_result(result, name, dimension_names, interval) maybe_clear(clear, name, interval) do save_records(records) if records.any? end end
save_records(records)
click to toggle source
# File lib/rollup/aggregator.rb, line 168 def save_records(records) # order must match unique index # consider using index name instead conflict_target = [:name, :interval, :time] conflict_target << :dimensions if Utils.dimensions_supported? if ActiveRecord::VERSION::MAJOR >= 6 options = Utils.mysql? ? {} : {unique_by: conflict_target} Rollup.upsert_all(records, **options) else update = Utils.mysql? ? [:value] : {columns: [:value], conflict_target: conflict_target} Rollup.import(records, on_duplicate_key_update: update, validate: false ) end end
set_dimension_names(dimension_names, relation)
click to toggle source
# File lib/rollup/aggregator.rb, line 82 def set_dimension_names(dimension_names, relation) groups = relation.group_values[0..-2] if dimension_names Utils.check_dimensions if dimension_names.size != groups.size raise ArgumentError, "Expected dimension_names to be size #{groups.size}, not #{dimension_names.size}" end dimension_names else Utils.check_dimensions if groups.any? groups.map { |group| determine_dimension_name(group) } end end
validate_column(column)
click to toggle source
basic version of Active Record disallow_raw_sql! symbol = column (safe), Arel node = SQL (safe), other = untrusted no need to quote/resolve column here, as Groupdate handles it TODO remove this method when gem depends on Groupdate 6+
# File lib/rollup/aggregator.rb, line 28 def validate_column(column) # matches table.column and column unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral) || /\A\w+(\.\w+)?\z/i.match(column.to_s) raise "Non-attribute argument: #{column}. Use Arel.sql() for known-safe values" end end