96 lines
3.2 KiB
Ruby
96 lines
3.2 KiB
Ruby
|
# frozen_string_literal: true
|
||
|
|
||
|
class MergeRequest::DiffCommitUser < ApplicationRecord
|
||
|
validates :name, length: { maximum: 512 }
|
||
|
validates :email, length: { maximum: 512 }
|
||
|
validates :name, presence: true, unless: :email
|
||
|
validates :email, presence: true, unless: :name
|
||
|
|
||
|
# Prepares a value to be inserted into a column in the table
|
||
|
# `merge_request_diff_commit_users`. Values in this table are limited to
|
||
|
# 512 characters.
|
||
|
#
|
||
|
# We treat empty strings as NULL values, as there's no point in (for
|
||
|
# example) storing a row where both the name and Email are an empty
|
||
|
# string. In addition, if we treated them differently we could end up with
|
||
|
# two rows: one where field X is NULL, and one where field X is an empty
|
||
|
# string. This is redundant, so we avoid storing such data.
|
||
|
def self.prepare(value)
|
||
|
value.present? ? value[0..511] : nil
|
||
|
end
|
||
|
|
||
|
# Creates a new row, or returns an existing one if a row already exists.
|
||
|
def self.find_or_create(name, email)
|
||
|
find_or_create_by!(name: name, email: email)
|
||
|
rescue ActiveRecord::RecordNotUnique
|
||
|
retry
|
||
|
end
|
||
|
|
||
|
# Finds many (name, email) pairs in bulk.
|
||
|
def self.bulk_find(pairs)
|
||
|
queries = {}
|
||
|
rows = []
|
||
|
|
||
|
pairs.each do |(name, email)|
|
||
|
queries[[name, email]] = where(name: name, email: email).to_sql
|
||
|
end
|
||
|
|
||
|
# We may end up having to query many users. To ensure we don't hit any
|
||
|
# query size limits, we get a fixed number of users at a time.
|
||
|
queries.values.each_slice(1_000).map do |slice|
|
||
|
rows.concat(from("(#{slice.join("\nUNION ALL\n")}) #{table_name}").to_a)
|
||
|
end
|
||
|
|
||
|
rows
|
||
|
end
|
||
|
|
||
|
# Finds or creates rows for the given pairs of names and Emails.
|
||
|
#
|
||
|
# The `names_and_emails` argument must be an Array/Set of tuples like so:
|
||
|
#
|
||
|
# [
|
||
|
# [name, email],
|
||
|
# [name, email],
|
||
|
# ...
|
||
|
# ]
|
||
|
#
|
||
|
# This method expects that the names and Emails have already been trimmed to
|
||
|
# at most 512 characters.
|
||
|
#
|
||
|
# The return value is a Hash that maps these tuples to instances of this
|
||
|
# model.
|
||
|
def self.bulk_find_or_create(pairs)
|
||
|
mapping = {}
|
||
|
create = []
|
||
|
|
||
|
# Over time, fewer new rows need to be created. We take advantage of that
|
||
|
# here by first finding all rows that already exist, using a limited number
|
||
|
# of queries (in most cases only one query will be needed).
|
||
|
bulk_find(pairs).each do |row|
|
||
|
mapping[[row.name, row.email]] = row
|
||
|
end
|
||
|
|
||
|
pairs.each do |(name, email)|
|
||
|
create << { name: name, email: email } unless mapping[[name, email]]
|
||
|
end
|
||
|
|
||
|
return mapping if create.empty?
|
||
|
|
||
|
# Sometimes we may need to insert new users into the table. We do this in
|
||
|
# bulk, so we only need one INSERT for all missing users.
|
||
|
insert_all(create, returning: %w[id name email]).each do |row|
|
||
|
mapping[[row['name'], row['email']]] =
|
||
|
new(id: row['id'], name: row['name'], email: row['email'])
|
||
|
end
|
||
|
|
||
|
# It's possible for (name, email) pairs to be inserted concurrently,
|
||
|
# resulting in the above insert not returning anything. Here we get any
|
||
|
# remaining users that were created concurrently.
|
||
|
bulk_find(pairs.reject { |pair| mapping.key?(pair) }).each do |row|
|
||
|
mapping[[row.name, row.email]] = row
|
||
|
end
|
||
|
|
||
|
mapping
|
||
|
end
|
||
|
end
|