diff --git a/app/models/import/source_user_placeholder_reference.rb b/app/models/import/source_user_placeholder_reference.rb new file mode 100644 index 0000000000000000000000000000000000000000..66c6647e3d1386f1b793f4828889626c8740a901 --- /dev/null +++ b/app/models/import/source_user_placeholder_reference.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Import + class SourceUserPlaceholderReference < ApplicationRecord + 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, 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 + + attribute :composite_key, :ind_jsonb + + private + + def validate_numeric_or_composite_key_present + return if numeric_key.present? ^ composite_key.present? + + errors.add(:base, :blank, message: 'numeric_key or composite_key must be present') + end + end +end diff --git a/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json b/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json new file mode 100644 index 0000000000000000000000000000000000000000..ccfbe0ec527bc7c5c18c20601abb7d002b190264 --- /dev/null +++ b/app/validators/json_schemas/import_source_user_placeholder_reference_composite_key.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Stores composite_key data for imported records that are mapped to placeholder users", + "type": "object", + "minProperties": 1, + "patternProperties": { + ".*": { + "oneOf": [ + { + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "type": "integer" + } + ] + } + } +} diff --git a/db/docs/import_source_user_placeholder_references.yml b/db/docs/import_source_user_placeholder_references.yml new file mode 100644 index 0000000000000000000000000000000000000000..87e1f72fc44ec82b800c7eb10708f2a3fc8b789c --- /dev/null +++ b/db/docs/import_source_user_placeholder_references.yml @@ -0,0 +1,14 @@ +--- +table_name: import_source_user_placeholder_references +classes: +- Import::SourceUserPlaceholderReference +feature_categories: +- importers +description: Used to map placeholder user references from imported data to real users +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156241 +milestone: '17.2' +gitlab_schema: gitlab_main_cell +allow_cross_foreign_keys: +- gitlab_main_clusterwide +sharding_key: + namespace_id: namespaces diff --git a/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb b/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb new file mode 100644 index 0000000000000000000000000000000000000000..0364d6322006ff0febefc497e41b9e627c5f7e31 --- /dev/null +++ b/db/migrate/20240612034702_create_import_source_user_placeholder_reference.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class CreateImportSourceUserPlaceholderReference < Gitlab::Database::Migration[2.2] + milestone '17.2' + + enable_lock_retries! + + INDEX_NAME = 'index_import_source_user_placeholder_references_on_source_user_' + + def change + create_table :import_source_user_placeholder_references do |t| + t.references :source_user, + index: { name: INDEX_NAME }, + null: false, + foreign_key: { to_table: :import_source_users, on_delete: :cascade } + t.references :namespace, null: false, index: true, foreign_key: { on_delete: :cascade } + t.bigint :numeric_key, null: true + t.datetime_with_timezone :created_at, null: false + t.text :model, limit: 150, null: false + t.text :user_reference_column, limit: 50, null: false + t.jsonb :composite_key, null: true + end + end +end diff --git a/db/schema_migrations/20240612034702 b/db/schema_migrations/20240612034702 new file mode 100644 index 0000000000000000000000000000000000000000..4c11c17c15d8fef5a4c47fff54bf7a3728030c8f --- /dev/null +++ b/db/schema_migrations/20240612034702 @@ -0,0 +1 @@ +91e467973c28e98ed562c70ae108f7b5cb1ed0353e3ce0c8b13f052077c75d5a \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 6688cf76c18065d4a40852d01e51395c87671c98..60ad4eb31d5d73a59786e47fddfa00a0b536b7de 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11167,6 +11167,28 @@ CREATE SEQUENCE import_failures_id_seq ALTER SEQUENCE import_failures_id_seq OWNED BY import_failures.id; +CREATE TABLE import_source_user_placeholder_references ( + id bigint NOT NULL, + source_user_id bigint NOT NULL, + namespace_id bigint NOT NULL, + numeric_key bigint, + created_at timestamp with time zone NOT NULL, + model text NOT NULL, + user_reference_column text NOT NULL, + composite_key jsonb, + CONSTRAINT check_782140eb9d CHECK ((char_length(user_reference_column) <= 50)), + CONSTRAINT check_d17bd9dd4d CHECK ((char_length(model) <= 150)) +); + +CREATE SEQUENCE import_source_user_placeholder_references_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE import_source_user_placeholder_references_id_seq OWNED BY import_source_user_placeholder_references.id; + CREATE TABLE import_source_users ( id bigint NOT NULL, placeholder_user_id bigint, @@ -20801,6 +20823,8 @@ ALTER TABLE ONLY import_export_uploads ALTER COLUMN id SET DEFAULT nextval('impo ALTER TABLE ONLY import_failures ALTER COLUMN id SET DEFAULT nextval('import_failures_id_seq'::regclass); +ALTER TABLE ONLY import_source_user_placeholder_references ALTER COLUMN id SET DEFAULT nextval('import_source_user_placeholder_references_id_seq'::regclass); + ALTER TABLE ONLY import_source_users ALTER COLUMN id SET DEFAULT nextval('import_source_users_id_seq'::regclass); ALTER TABLE ONLY incident_management_escalation_policies ALTER COLUMN id SET DEFAULT nextval('incident_management_escalation_policies_id_seq'::regclass); @@ -22987,6 +23011,9 @@ ALTER TABLE ONLY import_export_uploads ALTER TABLE ONLY import_failures ADD CONSTRAINT import_failures_pkey PRIMARY KEY (id); +ALTER TABLE ONLY import_source_user_placeholder_references + ADD CONSTRAINT import_source_user_placeholder_references_pkey PRIMARY KEY (id); + ALTER TABLE ONLY import_source_users ADD CONSTRAINT import_source_users_pkey PRIMARY KEY (id); @@ -27322,6 +27349,10 @@ CREATE INDEX index_import_failures_on_project_id_not_null ON import_failures USI CREATE INDEX index_import_failures_on_user_id_not_null ON import_failures USING btree (user_id) WHERE (user_id IS NOT NULL); +CREATE INDEX index_import_source_user_placeholder_references_on_namespace_id ON import_source_user_placeholder_references USING btree (namespace_id); + +CREATE INDEX index_import_source_user_placeholder_references_on_source_user_ ON import_source_user_placeholder_references USING btree (source_user_id); + CREATE INDEX index_import_source_users_on_namespace_id ON import_source_users USING btree (namespace_id); CREATE INDEX index_import_source_users_on_placeholder_user_id ON import_source_users USING btree (placeholder_user_id); @@ -33352,6 +33383,9 @@ ALTER TABLE ONLY namespaces_storage_limit_exclusions ALTER TABLE ONLY users_security_dashboard_projects ADD CONSTRAINT fk_rails_150cd5682c FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY import_source_user_placeholder_references + ADD CONSTRAINT fk_rails_158995b934 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY import_source_users ADD CONSTRAINT fk_rails_167f82fd95 FOREIGN KEY (reassign_to_user_id) REFERENCES users(id) ON DELETE SET NULL; @@ -34720,6 +34754,9 @@ ALTER TABLE ONLY upload_states ALTER TABLE ONLY epic_metrics ADD CONSTRAINT fk_rails_d071904753 FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE CASCADE; +ALTER TABLE ONLY import_source_user_placeholder_references + ADD CONSTRAINT fk_rails_d0b75c434e FOREIGN KEY (source_user_id) REFERENCES import_source_users(id) ON DELETE CASCADE; + ALTER TABLE ONLY subscriptions ADD CONSTRAINT fk_rails_d0c8bda804 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; diff --git a/spec/factories/import_source_user_placeholder_references.rb b/spec/factories/import_source_user_placeholder_references.rb new file mode 100644 index 0000000000000000000000000000000000000000..7aa19ad0a5a69eacbe4a81affed69bd6086518fb --- /dev/null +++ b/spec/factories/import_source_user_placeholder_references.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :import_source_user_placeholder_reference, class: 'Import::SourceUserPlaceholderReference' do + source_user factory: :import_source_user + namespace + model { 'Note' } + user_reference_column { 'author_id' } + numeric_key { 1 } + end +end diff --git a/spec/models/import/source_user_placeholder_reference_spec.rb b/spec/models/import/source_user_placeholder_reference_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e441e065f59d4f97fd0bc73a2517eabeb4c16195 --- /dev/null +++ b/spec/models/import/source_user_placeholder_reference_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::SourceUserPlaceholderReference, feature_category: :importers do + describe 'associations' do + it { is_expected.to belong_to(:source_user).class_name('Import::SourceUser') } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user_reference_column) } + it { is_expected.to validate_presence_of(:model) } + it { is_expected.to validate_presence_of(:namespace_id) } + it { is_expected.to validate_presence_of(:source_user_id) } + it { is_expected.to validate_numericality_of(:numeric_key).only_integer.is_greater_than(0) } + it { expect(described_class).to validate_jsonb_schema(['composite_key']) } + it { is_expected.to allow_value({ id: 1 }).for(:composite_key) } + it { is_expected.to allow_value({ id: '1' }).for(:composite_key) } + it { is_expected.to allow_value({ foo: '1', bar: 2 }).for(:composite_key) } + it { is_expected.not_to allow_value({}).for(:composite_key) } + it { is_expected.not_to allow_value({ id: 'foo' }).for(:composite_key) } + it { is_expected.not_to allow_value(1).for(:composite_key) } + + describe '#validate_numeric_or_composite_key_present' do + def validation_errors(...) + described_class.new(...).tap(&:validate) + .errors + .where(:base) + end + + it 'must have numeric_key or composite_key present', :aggregate_failures do + expect(validation_errors).to be_present + expect(validation_errors(numeric_key: 1)).to be_blank + expect(validation_errors(composite_key: { id: 1 })).to be_blank + expect(validation_errors(numeric_key: 1, composite_key: { id: 1 })).to be_present + end + end + end + + it 'is destroyed when source user is destroyed' do + reference = create(:import_source_user_placeholder_reference) + + expect { reference.source_user.destroy! }.to change { described_class.count }.by(-1) + end +end