# Counts uses recorded values only; history/diff endpoints retain their own contract. class CountsVersionRepr # language_code remains a compatibility argument; it never selects recorded values. def self.many(rows, resource:, language_code:, at: nil, inclusive: true, history_flg: false) new(rows.to_a, resource:, language_code:, at:, inclusive:, history_flg:).serialise end def initialize(rows, resource:, language_code:, at:, inclusive:, history_flg:) @rows = rows HistoryAccess.require_many!(rows) @at = at @inclusive = inclusive @resource = resource @history_flg = history_flg @users = User.where(id: rows.map(&:created_by_user_id).compact).pluck(:id, :name).to_h @languages = master_names(Language) @scripts = master_names(Script) @name_columns = CountsResourceDefinition.new('tags').name_columns @tag_versions = {} if resource == 'posts' load_tag_versions requests = @rows.flat_map do |row| Array(json(row.tags_json)).filter_map do |entry| [entry['external_tag_id'], parent_point(row)] if entry['external_tag_id'] end end.uniq @external_versions = load_related_versions(NicoTagVersion, requests) end load_parent_versions if resource == 'tags' end def serialise @rows.map do |row| resource_id = row.public_send(@resource == 'posts' ? :post_id : :tag_id) common = { id: row.id, resource_id:, version_no: row.version_no, latest: row.attributes['counts_latest'].to_i == 1, created_at: row.created_at.iso8601, created_by_user: row.created_by_user_id && { id: row.created_by_user_id, name: @users[row.created_by_user_id] } } common.merge(@resource == 'posts' ? post(row) : tag(row)) end end private def master_names(model) model.select('code AS counts_code', :name).to_h do |record| [record.attributes['counts_code'], record.name] end end def json(value) value.is_a?(String) ? JSON.parse(value) : value end def load_tag_versions pairs = @rows.flat_map { Array(json(_1.tags_json)) } .filter_map { |entry| [entry['tag_id'], entry['version_no']] if entry['tag_id'] } .uniq pairs.each_slice(500) do |batch| placeholders = batch.map { '(?, ?)' }.join(', ') scope = HistoryAccess.visible(TagVersion.all) if @at scope = scope.where("created_at #{ @inclusive ? '<=' : '<' } ?", @at) end scope.where(["(tag_id, version_no) IN (#{ placeholders })", *batch.flatten]) .select(:tag_id, :version_no, :name, :category, :names, :created_at).each do |version| @tag_versions[[version.tag_id, version.version_no]] = version end end end def names(value) (json(value) || {}).sort.map do |language, entry| aliases = entry.fetch('aliases', []).map do |alias_entry| alias_entry.merge('script_name' => @scripts[alias_entry['script_code']]) end { language_code: language, language_name: @languages[language], script_code: entry['script_code'], script_name: @scripts[entry['script_code']], name: entry['name'], auto_generated: entry['auto_generated'], aliases: } end end def locale_names(values) @name_columns.to_h do |field| locale = field.fetch(:name_locale) if field[:multiple_values] entries = values.select { _1[:language_code] == locale[:language_code] } aliases = entries.flat_map do |entry| entry[:aliases].select do |value| value['script_code'].to_s.downcase == locale[:script_code].to_s.downcase end.map { _1.merge('language_code' => entry[:language_code]) } end next [field.fetch(:key).to_sym, aliases] end entry = values.find do |value| value[:language_code] == locale[:language_code] && value[:script_code].to_s.downcase == locale[:script_code].to_s.downcase end [field.fetch(:key).to_sym, entry && entry[:name]] end end def post(row) snapshots = Array(json(row.tags_json)).map do |entry| version = @tag_versions[[entry['tag_id'], entry['version_no']]] version = nil if version && version.created_at > parent_point(row) external = @external_versions[[entry['external_tag_id'], parent_point(row).iso8601(6)]] recorded_names = version ? names(version.names) : [] aliases = recorded_names.flat_map do |value| value[:aliases].map { _1.merge('language_code' => value[:language_code], 'language_name' => value[:language_name]) } end { id: entry['tag_id'] || entry['external_tag_id'], kind: entry['tag_id'] ? 'internal' : 'external', tag_id: entry['tag_id'], external_tag_id: entry['external_tag_id'], version_no: entry['version_no'], name: entry['name'] || version&.name || external&.fetch('name'), category: entry['category'] || version&.category, sections: entry.fetch('sections', []), names: recorded_names, aliases: } .merge(locale_names(recorded_names)) end { title: row.title, url: row.url, thumbnail_base: row.thumbnail_base, tags: snapshots, parents: row.parent_post_ids.to_s.split.map { { id: _1.to_i } }, video_ms: row.video_ms, original_created_from: row.original_created_from&.iso8601, original_created_before: row.original_created_before&.iso8601 } end def parent_point(row) @history_flg ? [@at, row.created_at].compact.min : @at || row.created_at end def load_parent_versions requests = @rows.flat_map do |row| row.parent_tag_ids.to_s.split.map { [_1.to_i, parent_point(row)] } end.uniq @parent_versions = load_related_versions(TagVersion, requests) end def load_related_versions(model, requests) result = {} table = model.table_name comparison = @inclusive ? '<=' : '<' requests.each_slice(100) do |batch| queries = batch.map do |id, point| scope = HistoryAccess.visible(model.where(tag_id: id)) .where("created_at #{ comparison } ?", point) .where("NOT EXISTS (SELECT 1 FROM #{ table } newer " + "WHERE newer.tag_id = #{ table }.tag_id AND newer.created_at #{ comparison } ? " + "AND newer.version_no > #{ table }.version_no)", point) quoted = model.connection.quote(point.iso8601(6)) names = model == TagVersion ? 'names' : 'JSON_OBJECT() AS names' scope.select("tag_id, name, #{ names }, #{ quoted } AS reference_point").to_sql end next if queries.empty? model.connection.select_all(queries.map { "(#{ _1 })" }.join(' UNION ALL ')).each do |value| result[[value['tag_id'], value['reference_point']]] = value end end result end def tag(row) recorded_names = names(row.names) aliases = recorded_names.flat_map do |entry| entry[:aliases].map { _1.merge('language_code' => entry[:language_code], 'language_name' => entry[:language_name]) } end { name: row.name, names: recorded_names, category: row.category, aliases:, parents: row.parent_tag_ids.to_s.split.map do |id| value = @parent_versions[[id.to_i, parent_point(row).iso8601(6)]] snapshots = value && json(value['names']) recorded = names(snapshots) { id: id.to_i, name: value&.fetch('name'), names: recorded }.merge(locale_names(recorded)) end, deprecated: row.deprecated_at&.iso8601 }.merge(locale_names(recorded_names)) end end