このコミットが含まれているのは:
@@ -0,0 +1,98 @@
|
||||
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
|
||||
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
|
||||
result = ''
|
||||
if all.any? { _1['field'] == '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 all.any? { _1['field'] == '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'))
|
||||
elsif axis.key?('grain')
|
||||
raise ArgumentError
|
||||
end
|
||||
end
|
||||
axes
|
||||
end
|
||||
end
|
||||
新しいイシューから参照
ユーザーをブロックする