このコミットが含まれているのは:
@@ -12,6 +12,7 @@ class CountsTableQuery
|
||||
raise ArgumentError if (params.keys - KEYS).any?
|
||||
raise ArgumentError unless params.values.all? { _1.is_a?(String) }
|
||||
|
||||
params = CountsDefaults.for(params).slice(*KEYS).merge(params)
|
||||
@params = params
|
||||
@population = CountsPopulation.new(params)
|
||||
@definition = population.definition
|
||||
@@ -26,7 +27,7 @@ class CountsTableQuery
|
||||
@sort = params.fetch('sort', '').split(',').map { _1.split(':', -1) }
|
||||
allowed = mode == 'summary' ? axes.all.each_index.map { "a#{ _1 }" } + ['count'] :
|
||||
@definition.fields.keys + ['is_latest'] +
|
||||
(@definition.resource == 'posts' ? ['tags', 'parents', 'original_created_at'] : []) +
|
||||
(@definition.resource == 'posts' ? ['tags', 'parents'] : []) +
|
||||
(@definition.resource == 'tags' ? ['parents', 'deprecated'] : [])
|
||||
raise ArgumentError if @sort.map(&:first).uniq.length != @sort.length
|
||||
@sort.each do |key, direction, *extra|
|
||||
@@ -72,25 +73,26 @@ class CountsTableQuery
|
||||
# Page whole row tuples so a pivot row is never split across display pages.
|
||||
row_selection = row_keys.empty? ? '1 AS singleton' : row_keys.join(', ')
|
||||
row_group = row_keys.empty? ? 'GROUP BY singleton' : "GROUP BY #{ row_keys.join(', ') }"
|
||||
measure_joins = []
|
||||
extras = []
|
||||
row_sort = @sort.each_with_index.filter_map do |(key, direction), index|
|
||||
if key == 'count' || key.start_with?('measure_')
|
||||
condition = if key == 'count'
|
||||
'TRUE'
|
||||
else
|
||||
measure_values(key).each_with_index.map do |value, offset|
|
||||
"a#{ axes.rows.length + offset } <=> #{ @connection.quote(value) }"
|
||||
end.join(' AND ').presence || 'TRUE'
|
||||
end
|
||||
["MAX(CASE WHEN #{ condition } THEN count END) AS sort#{ index }",
|
||||
"sort#{ index } IS NULL ASC, sort#{ index } #{ direction.upcase }"]
|
||||
values = key == 'count' ? [] : measure_values(key)
|
||||
relation = measure_relation(values)
|
||||
matches = row_keys.map { "grouped.#{ _1 } <=> sort#{ index }.#{ _1 }" }
|
||||
condition = matches.empty? ? 'TRUE' : matches.join(' AND ')
|
||||
measure_joins << "LEFT JOIN (#{ relation }) sort#{ index } ON #{ condition }"
|
||||
extras << "sort#{ index }.count AS sort#{ index }"
|
||||
"sort#{ index } IS NULL ASC, sort#{ index } #{ direction.upcase }"
|
||||
elsif row_keys.include?(key)
|
||||
[nil, "#{ key } IS NULL ASC, #{ key } #{ direction.upcase }"]
|
||||
"#{ key } IS NULL ASC, #{ key } #{ direction.upcase }"
|
||||
end
|
||||
end
|
||||
extras = row_sort.filter_map(&:first)
|
||||
order_rows = row_sort.map(&:last) + row_keys.map { "#{ _1 } IS NULL, #{ _1 }" }
|
||||
order_rows = row_sort + row_keys.map { "#{ _1 } IS NULL, #{ _1 }" }
|
||||
order_rows = ['singleton ASC'] if order_rows.empty?
|
||||
group_sql = "SELECT #{ ([row_selection] + extras).join(', ') } FROM cells #{ row_group }"
|
||||
grouped = "SELECT #{ row_selection } FROM cells #{ row_group }"
|
||||
selection = (['grouped.*'] + extras).join(', ')
|
||||
group_sql = "SELECT #{ selection } FROM (#{ grouped }) grouped #{ measure_joins.join(' ') }"
|
||||
total = @connection.select_value(<<~SQL).to_i
|
||||
#{ prefix }
|
||||
SELECT /*+ MAX_EXECUTION_TIME(10000) */ COUNT(*) FROM (#{ group_sql }) row_groups
|
||||
@@ -113,7 +115,30 @@ class CountsTableQuery
|
||||
SELECT /*+ MAX_EXECUTION_TIME(10000) */ cells.* FROM cells JOIN page_rows ON #{ join }
|
||||
ORDER BY #{ order.join(', ') }
|
||||
SQL
|
||||
{ group_count: total, column_values:, cells: records.map do |row|
|
||||
next_row = @connection.select_one(<<~SQL)
|
||||
#{ prefix }, row_groups AS (#{ group_sql }),
|
||||
numbered AS (SELECT row_groups.*,
|
||||
ROW_NUMBER() OVER (ORDER BY #{ order_rows.join(', ') }) AS position FROM row_groups)
|
||||
SELECT /*+ MAX_EXECUTION_TIME(10000) */ * FROM numbered
|
||||
WHERE position = #{ page * limit + 1 }
|
||||
SQL
|
||||
row_values = records.map { |record| row_keys.map { record[_1] } }.uniq
|
||||
row_depths = CountsPivotLayout.depths(row_keys, @sort)
|
||||
column_keys = axes.columns.each_index.map { "a#{ axes.rows.length + _1 }" }
|
||||
column_depths = CountsPivotLayout.depths(column_keys, @sort, measures: false)
|
||||
row_headers = CountsPivotLayout.headers(row_values, row_depths,
|
||||
next_values: next_row && row_keys.map { next_row[_1] }, grand: axes.rows.any?)
|
||||
if axes.rows.empty? && axes.columns.any?
|
||||
row_headers = [{ values: [], kind: 'grand_total' }]
|
||||
end
|
||||
column_headers = CountsPivotLayout.headers(column_values, column_depths,
|
||||
grand: axes.columns.any?)
|
||||
if axes.columns.empty? && axes.rows.any?
|
||||
column_headers = [{ values: [], kind: 'grand_total' }]
|
||||
end
|
||||
totals = CountsPivotTotals.new(population, axes, row_headers, column_headers).call
|
||||
{ group_count: total, column_values:, row_headers:, column_headers:, pivot_totals: totals,
|
||||
cells: records.map do |row|
|
||||
{ values: axes.all.each_index.map { row["a#{ _1 }"] }, count: row['count'].to_i }
|
||||
end, axis_labels: axis_labels(records, column_values) }
|
||||
end
|
||||
@@ -167,7 +192,11 @@ class CountsTableQuery
|
||||
keys = axes.columns.each_index.map { "a#{ axes.rows.length + _1 }" }
|
||||
return [[]] if keys.empty?
|
||||
|
||||
order = keys.map { "#{ _1 } IS NULL, #{ _1 }" }.join(', ')
|
||||
configured = @sort.select { |key, _direction| keys.include?(key) }
|
||||
directions = (configured + keys.map { [_1, 'asc'] }).uniq { _1.first }
|
||||
order = directions.map { |key, direction|
|
||||
"#{ key } IS NULL ASC, #{ key } #{ direction.upcase }"
|
||||
}.join(', ')
|
||||
values = @connection.select_rows(<<~SQL)
|
||||
#{ prefix }
|
||||
SELECT /*+ MAX_EXECUTION_TIME(10000) */ DISTINCT #{ keys.join(', ') } FROM cells
|
||||
@@ -178,9 +207,26 @@ class CountsTableQuery
|
||||
values
|
||||
end
|
||||
|
||||
def measure_relation(values)
|
||||
row_expressions = axes.expressions.first(axes.rows.length)
|
||||
source = population.scope.except(:select, :order)
|
||||
fields = axes.rows + axes.columns.first(values.length)
|
||||
joins = axes.joins(fields.map { _1.fetch('field') })
|
||||
source = source.joins(joins) unless joins.empty?
|
||||
values.each_with_index do |value, index|
|
||||
expression = axes.expressions[axes.rows.length + index]
|
||||
source = source.where("(#{ expression }) <=> ?", value)
|
||||
end
|
||||
selected = row_expressions.each_with_index.map { |expression, index|
|
||||
"#{ expression } AS a#{ index }"
|
||||
} + ['COUNT(DISTINCT v.id) AS count']
|
||||
source = source.group(*row_expressions.map { Arel.sql(_1) }) if row_expressions.any?
|
||||
source.select(Arel.sql(selected.join(', '))).to_sql
|
||||
end
|
||||
|
||||
def measure_values(key)
|
||||
value = JSON.parse([key.delete_prefix('measure_')].pack('H*'))
|
||||
unless (value.is_a?(Array) && value.length == axes.columns.length &&
|
||||
unless (value.is_a?(Array) && value.length <= axes.columns.length &&
|
||||
value.all? { _1.nil? || _1.is_a?(String) || _1.is_a?(Numeric) })
|
||||
raise ArgumentError
|
||||
end
|
||||
@@ -190,8 +236,7 @@ class CountsTableQuery
|
||||
def detail_expression(key)
|
||||
return "(#{ population.latest_sql })" if key == 'is_latest'
|
||||
aliases = { 'tags' => 'tags_json', 'parents' => @definition.resource == 'posts' ?
|
||||
'parent_post_ids' : 'parent_tag_ids', 'deprecated' => 'deprecated_at',
|
||||
'original_created_at' => 'original_created_from' }
|
||||
'parent_post_ids' : 'parent_tag_ids', 'deprecated' => 'deprecated_at' }
|
||||
field = aliases.fetch(key, key)
|
||||
expression = @definition.expression(field)
|
||||
return expression unless @definition.fields[field] == :json
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする