mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-10 01:31:45 +00:00
157 lines
6.1 KiB
Ruby
157 lines
6.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Import
|
|
class SourceUserPlaceholderReference < ApplicationRecord
|
|
include BulkInsertSafe
|
|
include EachBatch
|
|
|
|
self.table_name = 'import_source_user_placeholder_references'
|
|
|
|
belongs_to :source_user, class_name: 'Import::SourceUser'
|
|
belongs_to :namespace
|
|
|
|
validates :model, :namespace_id, :source_user_id, :user_reference_column, :alias_version, presence: true
|
|
validates :numeric_key, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
|
validates :composite_key,
|
|
json_schema: { filename: 'import_source_user_placeholder_reference_composite_key' },
|
|
allow_nil: true
|
|
validate :validate_numeric_or_composite_key_present
|
|
validate :validate_model_is_not_member
|
|
|
|
attribute :composite_key, ::Gitlab::Database::Type::IndifferentJsonb.new
|
|
|
|
scope :model_groups_for_source_user, ->(source_user) do
|
|
where(source_user: source_user)
|
|
.select(:model, :user_reference_column, :alias_version)
|
|
.group(:model, :user_reference_column, :alias_version)
|
|
end
|
|
|
|
MODEL_BATCH_LIMIT = 500
|
|
|
|
# If an element is ever added to this array, ensure that `.from_serialized` handles receiving
|
|
# older versions of the array by filling in missing values with defaults. We'd keep that in place
|
|
# for at least one release cycle to ensure backward compatibility.
|
|
SERIALIZABLE_ATTRIBUTES = %w[
|
|
composite_key
|
|
model
|
|
namespace_id
|
|
numeric_key
|
|
source_user_id
|
|
user_reference_column
|
|
alias_version
|
|
].freeze
|
|
|
|
SerializationError = Class.new(StandardError)
|
|
|
|
def aliased_model
|
|
PlaceholderReferences::AliasResolver.aliased_model(model, version: alias_version)
|
|
end
|
|
|
|
def aliased_user_reference_column
|
|
PlaceholderReferences::AliasResolver.aliased_column(model, user_reference_column, version: alias_version)
|
|
end
|
|
|
|
def aliased_composite_key
|
|
composite_key.transform_keys do |key|
|
|
PlaceholderReferences::AliasResolver.aliased_column(model, key, version: alias_version)
|
|
end
|
|
end
|
|
|
|
def to_serialized
|
|
Gitlab::Json.dump(attributes.slice(*SERIALIZABLE_ATTRIBUTES).to_h.values)
|
|
end
|
|
|
|
def model_record
|
|
model_class = aliased_model
|
|
|
|
model_relation = numeric_key? ? model_class.primary_key_in(numeric_key) : model_class.where(composite_key)
|
|
model_relation.where({ aliased_user_reference_column => source_user.placeholder_user_id }).first
|
|
end
|
|
|
|
class << self
|
|
def from_serialized(serialized_reference)
|
|
deserialized = Gitlab::Json.parse(serialized_reference)
|
|
|
|
raise SerializationError if deserialized.size != SERIALIZABLE_ATTRIBUTES.size
|
|
|
|
attributes = SERIALIZABLE_ATTRIBUTES.zip(deserialized).to_h
|
|
|
|
new(attributes.merge(created_at: Time.zone.now))
|
|
end
|
|
|
|
# Model relations are yielded in a block to ensure all relations will be batched, regardless of the model
|
|
def model_relations_for_source_user_reference(model:, source_user:, user_reference_column:, alias_version:)
|
|
aliased_model = PlaceholderReferences::AliasResolver.aliased_model(model, version: alias_version)
|
|
aliased_user_reference_column = PlaceholderReferences::AliasResolver.aliased_column(
|
|
model, user_reference_column, version: alias_version
|
|
)
|
|
primary_key = aliased_model.primary_key
|
|
|
|
where(model:, source_user:, user_reference_column:, alias_version:).each_batch(of: MODEL_BATCH_LIMIT) do
|
|
|placeholder_reference_batch|
|
|
model_relation = nil
|
|
|
|
# This is the simplest way to check for composite pkey for now. In Rails 7.1, composite primary keys will be
|
|
# fully supported: https://guides.rubyonrails.org/7_1_release_notes.html#composite-primary-keys.
|
|
# The `elseif primary_key.is_a?(Array)` block exists for Rails 7.1 support, so will not execute in Rails 7.0,
|
|
# thus the code is not covered by specs and we can ignore underecoverage reports about it until we upgrade.
|
|
# .pluck is used instead of .select to avoid CrossSchemaAccessErrors on CI tables
|
|
# rubocop: disable Database/AvoidUsingPluckWithoutLimit -- plucking limited by placeholder batch
|
|
if primary_key.nil?
|
|
composite_keys = placeholder_reference_batch.pluck(:composite_key)
|
|
|
|
model_relation = aliased_model.where(
|
|
"#{composite_key_columns(composite_keys)} IN #{composite_key_values(composite_keys)}"
|
|
)
|
|
elsif primary_key.is_a?(Array)
|
|
composite_keys = placeholder_reference_batch.pluck(:composite_key)
|
|
key = composite_keys.first.keys
|
|
values = composite_keys.map(&:values)
|
|
model_relation = aliased_model.where({ key => values })
|
|
else
|
|
model_relation = aliased_model.primary_key_in(placeholder_reference_batch.pluck(:numeric_key))
|
|
end
|
|
# rubocop: enable Database/AvoidUsingPluckWithoutLimit
|
|
|
|
model_relation = model_relation.where(aliased_user_reference_column => source_user.placeholder_user_id)
|
|
|
|
next if model_relation.empty?
|
|
|
|
yield([model_relation, placeholder_reference_batch])
|
|
end
|
|
end
|
|
|
|
def composite_key_columns(composite_keys)
|
|
composite_key_columns = composite_keys.first.keys
|
|
tuple(composite_key_columns)
|
|
end
|
|
|
|
def composite_key_values(composite_keys)
|
|
keys = composite_keys.map { |composite_key| tuple(composite_key.values) }
|
|
tuple(keys)
|
|
end
|
|
|
|
def tuple(values)
|
|
"(#{values.join(', ')})"
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def validate_numeric_or_composite_key_present
|
|
return if numeric_key.present? ^ composite_key.present?
|
|
|
|
errors.add(:base, :blank, message: 'one of numeric_key or composite_key must be present')
|
|
end
|
|
|
|
# Membership data is handled in `Import::Placeholders::Membership` records instead.
|
|
# Use `Import::PlaceholderMemberships::CreateService` to save the membership data.
|
|
def validate_model_is_not_member
|
|
model_class = model&.safe_constantize
|
|
return unless model_class.present? && model_class.new.is_a?(Member)
|
|
|
|
errors.add(:model, :invalid, message: 'cannot be a Member')
|
|
end
|
|
end
|
|
end
|