ファイル
btrc-hub/backend/app/services/counts_axes.rb
T
2026-10-26 21:53:09 +09:00

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 = JSON.parse(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