class Groupdate::Magic::Relation

Public Class Methods

generate_relation(relation, field:, **options) click to toggle source
# File lib/groupdate/magic.rb, line 204
def self.generate_relation(relation, field:, **options)
  magic = Groupdate::Magic::Relation.new(**options)

  adapter_name = relation.connection_pool.with_connection { |c| c.adapter_name }
  adapter = Groupdate.adapters[adapter_name]
  raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}" unless adapter

  # very important
  column = validate_column(field)
  column = resolve_column(relation, column)

  # generate ActiveRecord relation
  relation =
    adapter.new(
      relation,
      column: column,
      period: magic.period,
      time_zone: magic.time_zone,
      time_range: magic.time_range,
      week_start: magic.week_start,
      day_start: magic.day_start,
      n_seconds: magic.n_seconds,
      adapter_name: adapter_name
    ).generate

  # add Groupdate info
  magic.group_index = relation.group_values.size - 1
  (relation.groupdate_values ||= []) << magic

  relation
end
new(**options) click to toggle source
Calls superclass method Groupdate::Magic::new
# File lib/groupdate/magic.rb, line 122
def initialize(**options)
  super(**options.reject { |k, _| [:default_value, :carry_forward, :last, :current].include?(k) })
  @options = options
end
process_result(relation, result, **options) click to toggle source

allow any options to keep flexible for future

# File lib/groupdate/magic.rb, line 261
def self.process_result(relation, result, **options)
  relation.groupdate_values.reverse_each do |gv|
    result = gv.perform(relation, result, default_value: options[:default_value])
  end
  result
end
resolve_column(relation, column) click to toggle source

resolves eagerly need to convert both where_clause (easy) and group_clause (not easy) if want to avoid this

# File lib/groupdate/magic.rb, line 253
def resolve_column(relation, column)
  node = relation.send(:relation).send(:arel_columns, [column]).first
  node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
  relation.connection_pool.with_connection { |c| c.visitor.accept(node, Arel::Collectors::SQLString.new).value }
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 matches table.column and column

# File lib/groupdate/magic.rb, line 240
def validate_column(column)
  unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
    column = column.to_s
    unless /\A\w+(\.\w+)?\z/i.match?(column)
      raise ActiveRecord::UnknownAttributeReference, "Query method called with non-attribute argument(s): #{column.inspect}. Use Arel.sql() for known-safe values."
    end
  end
  column
end

Public Instance Methods

cast_method() click to toggle source
# File lib/groupdate/magic.rb, line 145
def cast_method
  @cast_method ||= begin
    case period
    when :minute_of_hour, :hour_of_day, :day_of_month, :day_of_year, :month_of_year
      lambda { |k| k.to_i }
    when :day_of_week
      lambda { |k| (k.to_i - 1 - week_start) % 7 }
    when :day, :week, :month, :quarter, :year
      # TODO keep as date
      if day_start != 0
        day_start_hour = day_start / 3600
        day_start_min = (day_start % 3600) / 60
        day_start_sec = (day_start % 3600) % 60
        lambda { |k| k.in_time_zone(time_zone).change(hour: day_start_hour, min: day_start_min, sec: day_start_sec) }
      else
        lambda { |k| k.in_time_zone(time_zone) }
      end
    else
      utc = ActiveSupport::TimeZone["UTC"]
      lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
    end
  end
end
cast_result(result, multiple_groups) click to toggle source
# File lib/groupdate/magic.rb, line 169
def cast_result(result, multiple_groups)
  new_result = {}
  result.each do |k, v|
    if multiple_groups
      k[group_index] = cast_method.call(k[group_index])
    else
      k = cast_method.call(k)
    end
    new_result[k] = v
  end
  new_result
end
check_nils(result, multiple_groups, relation) click to toggle source
# File lib/groupdate/magic.rb, line 193
def check_nils(result, multiple_groups, relation)
  has_nils = multiple_groups ? (result.keys.first && result.keys.first[group_index].nil?) : result.key?(nil)
  if has_nils
    if time_zone_support?(relation)
      raise Groupdate::Error, "Invalid query - be sure to use a date or time column"
    else
      raise Groupdate::Error, "Database missing time zone support for #{time_zone.tzinfo.name} - see https://github.com/ankane/groupdate#for-mysql"
    end
  end
end
perform(relation, result, default_value:) click to toggle source
# File lib/groupdate/magic.rb, line 127
def perform(relation, result, default_value:)
  if defined?(ActiveRecord::Promise) && result.is_a?(ActiveRecord::Promise)
    return result.then { |r| perform(relation, r, default_value: default_value) }
  end

  multiple_groups = relation.group_values.size > 1

  check_nils(result, multiple_groups, relation)
  result = cast_result(result, multiple_groups)

  series_builder.generate(
    result,
    default_value: options.key?(:default_value) ? options[:default_value] : default_value,
    multiple_groups: multiple_groups,
    group_index: group_index
  )
end
time_zone_support?(relation) click to toggle source
# File lib/groupdate/magic.rb, line 182
def time_zone_support?(relation)
  relation.connection_pool.with_connection do |connection|
    if connection.adapter_name.match?(/mysql|trilogy/i)
      sql = relation.send(:sanitize_sql_array, ["SELECT CONVERT_TZ(NOW(), '+00:00', ?)", time_zone.tzinfo.name])
      !connection.select_all(sql).to_a.first.values.first.nil?
    else
      true
    end
  end
end