101 行
3.6 KiB
Ruby
101 行
3.6 KiB
Ruby
class CountsAxes
|
|
GRAINS = ['year', 'quarter', 'month', 'week', 'day', 'hour'].freeze
|
|
|
|
def self.time_expression(expression, grain, timezone)
|
|
raise ArgumentError unless GRAINS.include?(grain)
|
|
|
|
connection = ApplicationRecord.connection
|
|
# Use the installed TZInfo database, including DST, rather than MySQL timezone tables.
|
|
first = Time.utc(1900)
|
|
last = Time.utc(2101)
|
|
offset = timezone.period_for_utc(first).utc_total_offset
|
|
clauses = timezone.transitions_up_to(last, first).filter_map do |transition|
|
|
next if transition.offset.utc_total_offset == offset
|
|
|
|
previous = offset
|
|
offset = transition.offset.utc_total_offset
|
|
"WHEN #{ expression } < #{ connection.quote(transition.at.to_time) } THEN #{ previous }"
|
|
end
|
|
seconds = clauses.empty? ? offset.to_s : "CASE #{ clauses.join(' ') } ELSE #{ offset } END"
|
|
local = "DATE_ADD(#{ expression }, INTERVAL (#{ seconds }) SECOND)"
|
|
case grain
|
|
when 'year' then "DATE_FORMAT(#{ local }, '%Y')"
|
|
when 'quarter' then "CONCAT(YEAR(#{ local }), '-Q', QUARTER(#{ local }))"
|
|
when 'month' then "DATE_FORMAT(#{ local }, '%Y-%m')"
|
|
when 'week' then "DATE_FORMAT(#{ local }, '%x-W%v')"
|
|
when 'day' then "DATE_FORMAT(#{ local }, '%Y-%m-%d')"
|
|
else "DATE_FORMAT(#{ local }, '%Y-%m-%d %H:00')"
|
|
end
|
|
end
|
|
|
|
attr_reader :rows, :columns, :all
|
|
|
|
def initialize(population)
|
|
@population = population
|
|
@definition = population.definition
|
|
@rows = parse(population.params.fetch('rows', ''))
|
|
@columns = parse(population.params.fetch('cols', ''))
|
|
@all = rows + columns
|
|
raise ArgumentError if all.length > 8 || all.map { _1['field'] }.uniq.length != all.length
|
|
end
|
|
|
|
def expressions
|
|
@expressions ||= all.map do |axis|
|
|
field = axis.fetch('field')
|
|
case field
|
|
when 'tags'
|
|
"CASE WHEN tag_entry.tag_id IS NOT NULL " +
|
|
"THEN CONCAT('internal:', tag_entry.tag_id) " +
|
|
"ELSE CONCAT('external:', tag_entry.external_tag_id) END"
|
|
when 'changed_field' then 'changed.field_key'
|
|
else
|
|
expression = @definition.expression(field)
|
|
@definition.fields[field] == :datetime ? self.class.time_expression(
|
|
expression, axis.fetch('grain', 'month'), @population.timezone) : expression
|
|
end
|
|
end
|
|
end
|
|
|
|
def joins(fields = all.map { _1['field'] })
|
|
result = ''
|
|
if fields.include?('tags')
|
|
result += <<~SQL.squish
|
|
JOIN JSON_TABLE(CAST(v.tags_json AS JSON), '$[*]' COLUMNS (
|
|
tag_id BIGINT PATH '$.tag_id' NULL ON EMPTY,
|
|
external_tag_id BIGINT PATH '$.external_tag_id' NULL ON EMPTY)) tag_entry
|
|
|
|
SQL
|
|
end
|
|
if fields.include?('changed_field')
|
|
fields = @definition.changes.map do |field|
|
|
"SELECT #{ ApplicationRecord.connection.quote(field) } AS field_key"
|
|
end.join(' UNION ALL ')
|
|
condition = @definition.changes.map do |field|
|
|
"(changed.field_key = '#{ field }' AND #{ @population.changed_sql(field) })"
|
|
end.join(' OR ')
|
|
result += " JOIN (#{ fields }) changed ON (#{ condition })"
|
|
end
|
|
result
|
|
end
|
|
|
|
private
|
|
|
|
def parse(value)
|
|
axes = QueryState.axes(value)
|
|
raise ArgumentError unless axes.is_a?(Array)
|
|
|
|
axes.each do |axis|
|
|
raise ArgumentError unless axis.is_a?(Hash) && (axis.keys - ['field', 'grain']).empty?
|
|
raise ArgumentError unless @definition.axes(@population.history).include?(axis['field'])
|
|
if @definition.fields[axis['field']] == :datetime
|
|
raise ArgumentError unless GRAINS.include?(axis.fetch('grain', 'month'))
|
|
|
|
axis['grain'] ||= 'month'
|
|
elsif axis.key?('grain')
|
|
raise ArgumentError
|
|
end
|
|
end
|
|
axes
|
|
end
|
|
end
|