このコミットが含まれているのは:
@@ -161,25 +161,6 @@ class CountsCondition
|
||||
scope.where(predicate, *binds)
|
||||
end
|
||||
|
||||
def tag_name_match(value)
|
||||
source = post_tag_source
|
||||
bound = @population.history ? 'v.created_at' : @connection.quote(point)
|
||||
source += ' LEFT JOIN nico_tag_versions external_snapshot ON external_snapshot.tag_id = ' +
|
||||
scalar('tag_snapshot.item', 'external_tag_id', :integer) +
|
||||
" AND external_snapshot.created_at <= #{ bound } AND " +
|
||||
@population.access_sql(NicoTagVersion, 'external_snapshot') +
|
||||
' AND NOT EXISTS (SELECT 1 FROM nico_tag_versions newer_external ' +
|
||||
'WHERE newer_external.tag_id = external_snapshot.tag_id ' +
|
||||
"AND newer_external.created_at <= #{ bound } " +
|
||||
'AND newer_external.version_no > external_snapshot.version_no)'
|
||||
document = "JSON_MERGE_PRESERVE(JSON_ARRAY(#{ scalar('tag_snapshot.item', 'name') }, " +
|
||||
"referenced_tag.name, CONCAT('nico:', external_snapshot.name)), " +
|
||||
"COALESCE(JSON_EXTRACT(referenced_tag.names, '$**.name'), JSON_ARRAY()))"
|
||||
source += ' JOIN ' + json_table(document, '$[*]', 'search_name')
|
||||
@definition.model.sanitize_sql_array([
|
||||
"EXISTS (SELECT 1 FROM #{ source } WHERE JSON_UNQUOTE(search_name.item) = ?)", value])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_condition(scope, entry, field)
|
||||
|
||||
@@ -210,23 +210,19 @@ class CountsPopulation
|
||||
|
||||
raw = params['tags']
|
||||
tokens = raw.split.reject { _1.downcase == 'type:or' }
|
||||
refs = PostTagSearch.parse_references(params, tokens, partial: true)
|
||||
locale = Locale.find_by(code: params.fetch('locale', 'ja'))
|
||||
raise ArgumentError unless locale
|
||||
|
||||
literals = PostTagSearch.literals(locale, tokens.join(' '), references: refs)
|
||||
literals = PostTagSearch.literals(locale, tokens.join(' '))
|
||||
identities = PostTagSearch.references_for(locale, literals)
|
||||
predicates = literals.map do |literal|
|
||||
reference = literal[:reference]
|
||||
expression = if reference
|
||||
parts = { 'internal' => reference['tag_id'],
|
||||
'external' => reference['external_tag_id'] }.filter_map do |kind, id|
|
||||
snapshot_membership("#{ kind }:#{ id }") if id
|
||||
end
|
||||
parts.empty? ? 'FALSE' : "(#{ parts.join(' OR ') })"
|
||||
else
|
||||
@condition ||= CountsCondition.new(self)
|
||||
@condition.tag_name_match(literal[:name])
|
||||
end
|
||||
# Resolve current names/aliases exactly as normal post search does. Only
|
||||
# membership comes from history; snapshot names never identify a tag.
|
||||
internal, external = identities.fetch(literal[:name])
|
||||
parts = { 'internal' => internal, 'external' => external }.flat_map do |kind, ids|
|
||||
Array(ids).map { |id| snapshot_membership("#{ kind }:#{ id }") }
|
||||
end
|
||||
expression = parts.empty? ? 'FALSE' : "(#{ parts.join(' OR ') })"
|
||||
literal[:negative] ? "NOT #{ expression }" : expression
|
||||
end
|
||||
return if predicates.empty?
|
||||
|
||||
@@ -4,7 +4,7 @@ class CountsTableQuery
|
||||
class TooManyColumns < ArgumentError; end
|
||||
|
||||
KEYS = ['resource', 'definition', 'mode', 'at', 'execution_at', 'from', 'through',
|
||||
'timezone', 'rows', 'cols', 'conditions', 'drill', 'tags', 'tag_refs', 'match',
|
||||
'timezone', 'rows', 'cols', 'conditions', 'drill', 'tags', 'match',
|
||||
'tag', 'kind', 'resource_id', 'user_id', 'page', 'limit', 'sort', 'locale',
|
||||
'measures', 'column_order', 'condition_values', 'name_scope'].freeze
|
||||
attr_reader :population, :axes, :page, :limit, :mode
|
||||
@@ -224,7 +224,7 @@ class CountsTableQuery
|
||||
|
||||
fields = QueryState.records(@params, 'conditions') + QueryState.records(@params, 'drill')
|
||||
private_fields = fields.any? { !CountsAxes::PUBLIC_FIELDS.include?(_1['field']) }
|
||||
keys = ['tags', 'tag_refs', 'tag', 'kind', 'resource_id', 'user_id']
|
||||
keys = ['tags', 'tag', 'kind', 'resource_id', 'user_id']
|
||||
private_parameters = keys.any? do |key|
|
||||
@params.any? { |name, value| (name == key || name.start_with?("#{ key }.")) && value != '' }
|
||||
end
|
||||
|
||||
@@ -1,52 +1,30 @@
|
||||
# Shared literal parsing and reference resolution for current and snapshot post search.
|
||||
class PostTagSearch
|
||||
def self.parse_references(raw, literals, partial: false)
|
||||
return nil unless raw.key?('tag_refs')
|
||||
|
||||
references = QueryState.records(raw, 'tag_refs')
|
||||
valid = references.is_a?(Array) && references.all? do |reference|
|
||||
next false unless reference.is_a?(Hash) && reference['name'].is_a?(String)
|
||||
|
||||
ids = [reference['tag_id'], reference['external_tag_id']].compact
|
||||
((ids.any? || reference['missing'] == true) &&
|
||||
ids.all? { _1.is_a?(Integer) && _1.positive? } &&
|
||||
(!reference.key?('missing') || (reference['missing'] == true && ids.empty?)) &&
|
||||
(!reference.key?('negative') || [true, false].include?(reference['negative'])))
|
||||
end
|
||||
raise ArgumentError unless valid
|
||||
|
||||
if partial
|
||||
remaining = references.dup
|
||||
result = literals.map do |literal|
|
||||
index = remaining.index { _1['name'] == literal }
|
||||
index ? remaining.delete_at(index) : nil
|
||||
end
|
||||
raise ArgumentError if remaining.any?
|
||||
|
||||
return result
|
||||
end
|
||||
references.map { _1['name'] } == literals ? references : nil
|
||||
end
|
||||
|
||||
def self.literals(locale, raw, references: nil)
|
||||
raw.split.each_with_index.filter_map do |token, index|
|
||||
next if token.downcase == 'type:or'
|
||||
|
||||
reference = references&.[](index)
|
||||
name = token.sub(/\Anot:/i, '')
|
||||
name = TagName.canonicalise(locale, name).first unless reference || locale.nil?
|
||||
negative = reference&.key?('negative') ? reference['negative'] :
|
||||
token.downcase.start_with?('not:')
|
||||
{ name:, negative:, reference: }
|
||||
def self.literals(locale, raw)
|
||||
tokens = raw.split.reject { _1.downcase == 'type:or' }
|
||||
names = tokens.map { _1.sub(/\Anot:/i, '') }
|
||||
registered = TagName.where(language_code: locale.language_code, name: names)
|
||||
.pluck(:name, :tag_id).to_h
|
||||
primary = TagName.where(language_code: locale.language_code,
|
||||
tag_id: registered.values, primary_flg: true).pluck(:tag_id, :name).to_h
|
||||
tokens.zip(names).map do |token, name|
|
||||
{ name: primary[registered[name]] || name,
|
||||
negative: token.downcase.start_with?('not:') }
|
||||
end
|
||||
end
|
||||
|
||||
def self.references(locale, literal)
|
||||
reference = literal[:reference]
|
||||
return [reference['tag_id'], reference['external_tag_id']] if reference
|
||||
references_for(locale, [literal]).fetch(literal[:name])
|
||||
end
|
||||
|
||||
[TagName.where(language_code: locale.language_code, name: literal[:name])
|
||||
.distinct.pluck(:tag_id),
|
||||
ExternalTag.where("CONCAT(platform, ':', name) = ?", literal[:name]).pluck(:id)]
|
||||
def self.references_for(locale, literals)
|
||||
names = literals.map { _1[:name] }.uniq
|
||||
internal = TagName.where(language_code: locale.language_code, name: names)
|
||||
.pluck(:name, :tag_id).group_by(&:first)
|
||||
external = names.empty? ? {} : ExternalTag.where(
|
||||
"CONCAT(platform, ':', name) IN (?)", names)
|
||||
.pluck(Arel.sql("CONCAT(platform, ':', name)"), :id).group_by(&:first)
|
||||
names.to_h { |name| [name, [internal.fetch(name, []).map(&:last).uniq,
|
||||
external.fetch(name, []).map(&:last).uniq]] }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,6 @@ class QueryState
|
||||
'conditions' => ['field', 'operator', 'value', 'end', 'blank', 'input', 'values', 'grain', 'value_type'],
|
||||
'condition_values' => ['condition', 'value', 'value_type'],
|
||||
'drill' => ['field', 'grain', 'value', 'value_type'],
|
||||
'tag_refs' => ['name', 'tag_id', 'external_tag_id', 'negative', 'missing'],
|
||||
'column_order' => ['value', 'value_type'],
|
||||
'measures' => ['key', 'axis', 'value', 'value_type'] }.freeze
|
||||
|
||||
@@ -41,11 +40,11 @@ class QueryState
|
||||
|
||||
_root, index, field = name.split('.')
|
||||
record = records[index.to_i] ||= {}
|
||||
if ['blank', 'negative', 'missing', 'values'].include?(field)
|
||||
if ['blank', 'values'].include?(field)
|
||||
raise ArgumentError unless ['0', '1'].include?(value)
|
||||
|
||||
value = value == '1'
|
||||
elsif ['tag_id', 'external_tag_id', 'axis', 'condition'].include?(field)
|
||||
elsif ['axis', 'condition'].include?(field)
|
||||
raise ArgumentError unless value.match?(/\A[0-9]{1,18}\z/)
|
||||
|
||||
value = value.to_i
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする