Class: Groupdate::Magic::Relation

Inherits:
Groupdate::Magic show all
Defined in:
lib/groupdate/magic.rb

Constant Summary

Constants inherited from Groupdate::Magic

DAYS

Instance Attribute Summary

Attributes inherited from Groupdate::Magic

#group_index, #n_seconds, #options, #period

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Groupdate::Magic

#day_start, #range, #series_builder, #time_range, #time_zone, #validate_arguments, #validate_keywords, validate_period, #week_start

Constructor Details

#initialize(**options) ⇒ Relation

Returns a new instance of Relation.



122
123
124
125
# 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

Class Method Details

.generate_relation(relation, field:, **options) ⇒ Object

Raises:



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# 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

.process_result(relation, result, **options) ⇒ Object

allow any options to keep flexible for future



261
262
263
264
265
266
# 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) ⇒ Object

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



253
254
255
256
257
# 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) ⇒ Object

basic version of Active Record disallow_raw_sql! symbol = column (safe), Arel node = SQL (safe), other = untrusted matches table.column and column



240
241
242
243
244
245
246
247
248
# 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

Instance Method Details

#cast_methodObject



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# 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) ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
# 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) ⇒ Object



193
194
195
196
197
198
199
200
201
202
# 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:) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# 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) ⇒ Boolean

Returns:

  • (Boolean)


182
183
184
185
186
187
188
189
190
191
# 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