ファイル
btrc-hub/backend/app/services/counts_candidates.rb
T
2026-10-28 00:28:29 +09:00

56 行
2.5 KiB
Ruby

# Bounded, authorised candidates at the same recorded point as the Counts query.
class CountsCandidates
KEYS = ['resource', 'field', 'query', 'category', 'page', 'locale', 'at',
'execution_at', 'timezone'].freeze
LIMIT = 20
def initialize(params)
QueryState.validate!(params, KEYS)
raise ArgumentError unless params.fetch('resource') == 'posts'
raise ArgumentError unless ['tags', 'tags_json'].include?(params.fetch('field'))
@params = params
@page = Integer(params.fetch('page', '1'), 10)
raise ArgumentError unless @page.between?(1, 100_000)
raise ArgumentError if params.fetch('query', '').bytesize > 1000
category = params['category']
raise ArgumentError if category.present? && !Tag.categories.key?(category)
end
def call
options = @params.slice('locale', 'at', 'execution_at', 'timezone')
.merge('definition' => 'latest', 'resource' => 'tags')
internal = CountsPopulation.new(options)
external = CountsPopulation.new(options.merge('resource' => 'nico_tags',
'execution_at' => internal.at.iso8601(6)))
language = Locale.find_by!(code: @params.fetch('locale', 'ja')).language_code
connection = TagVersion.connection
path = connection.quote("$.#{ language.to_json }.name")
name = "COALESCE(JSON_UNQUOTE(JSON_EXTRACT(v.names, #{ path })), v.name)"
sources = [[internal, 'internal', name, 'v.category'],
[external, 'external', 'v.name', 'NULL']].map do |population, kind, title, category|
population.scope.except(:select).select(Arel.sql(
"CONCAT('#{ kind }:', v.tag_id) AS value, v.tag_id AS id, " +
"#{ title } AS name, #{ category } AS category, '#{ kind }' AS kind")).to_sql
end
clauses = []
binds = []
if @params['query'].present?
clauses << '(name LIKE ? OR CAST(id AS CHAR) = ?)'
binds += ["%#{ ApplicationRecord.sanitize_sql_like(@params['query']) }%", @params['query']]
end
if @params['category'].present?
clauses << 'category = ?'
binds << @params['category']
end
where = clauses.empty? ? '' : "WHERE #{ clauses.join(' AND ') }"
sql = <<~SQL
SELECT /*+ MAX_EXECUTION_TIME(10000) */ * FROM (#{ sources.join(' UNION ALL ') }) choices
#{ where } ORDER BY kind, id LIMIT #{ LIMIT + 1 } OFFSET #{ (@page - 1) * LIMIT }
SQL
rows = connection.select_all(ApplicationRecord.sanitize_sql_array([sql, *binds])).to_a
{ rows: rows.first(LIMIT), page: @page, more: rows.length > LIMIT,
execution_at: internal.at.iso8601(6) }
end
end