From 2fb59e63d8fcb1f82f378b97bb94becc2399e01b Mon Sep 17 00:00:00 2001
From: Rodrigo Tomonari <rtomonari@gitlab.com>
Date: Thu, 27 Jun 2024 23:15:37 -0300
Subject: [PATCH 1/6] Add filters to import source users Graphql resolver

Update the resolver to filter and sort results
---
 app/finders/import/source_users_finder.rb     | 21 +++++-
 .../resolvers/import/source_users_resolver.rb | 13 ++++
 .../types/import/source_user_sort_enum.rb     | 15 ++++
 app/models/import/source_user.rb              | 40 ++++++++---
 app/policies/import/source_user_policy.rb     |  2 +-
 ...2648_add_indexes_to_import_source_users.rb | 30 ++++++++
 db/schema_migrations/20240627142648           |  1 +
 db/structure.sql                              |  6 ++
 doc/api/graphql/reference/index.md            | 57 ++++++++++++++-
 .../import/source_users_finder_spec.rb        | 71 ++++++++++++++++---
 .../import/source_users_resolver_spec.rb      | 14 ++++
 spec/models/import/source_user_spec.rb        | 68 ++++++++++++++++++
 .../helpers/database/duplicate_indexes.yml    |  3 +
 13 files changed, 321 insertions(+), 20 deletions(-)
 create mode 100644 app/graphql/types/import/source_user_sort_enum.rb
 create mode 100644 db/migrate/20240627142648_add_indexes_to_import_source_users.rb
 create mode 100644 db/schema_migrations/20240627142648

diff --git a/app/finders/import/source_users_finder.rb b/app/finders/import/source_users_finder.rb
index d4e573bfabc079..f3bc3d5f0ba0b8 100644
--- a/app/finders/import/source_users_finder.rb
+++ b/app/finders/import/source_users_finder.rb
@@ -11,7 +11,10 @@ def initialize(namespace, current_user, params = {})
     def execute
       return Import::SourceUser.none unless authorized?
 
-      namespace.import_source_users
+      collection = namespace.import_source_users
+      collection = by_statuses(collection)
+      collection = by_search(collection)
+      sort(collection)
     end
 
     private
@@ -21,5 +24,21 @@ def execute
     def authorized?
       Ability.allowed?(current_user, :admin_namespace, namespace)
     end
+
+    def by_statuses(collection)
+      return collection unless params[:statuses].present?
+
+      collection.by_statuses(params[:statuses])
+    end
+
+    def by_search(collection)
+      return collection unless params[:search].present?
+
+      collection.search(params[:search])
+    end
+
+    def sort(collection)
+      collection.sort_by_attribute(params[:sort] || :source_name_asc)
+    end
   end
 end
diff --git a/app/graphql/resolvers/import/source_users_resolver.rb b/app/graphql/resolvers/import/source_users_resolver.rb
index 57210e3b6b1faa..5a480736be0147 100644
--- a/app/graphql/resolvers/import/source_users_resolver.rb
+++ b/app/graphql/resolvers/import/source_users_resolver.rb
@@ -11,6 +11,19 @@ class SourceUsersResolver < BaseResolver
 
       type Types::Import::SourceUserType.connection_type, null: true
 
+      argument :statuses, [::Types::Import::SourceUserStatusEnum],
+        required: false,
+        description: 'Filter mapping of users on source instance to users on destination instance by status.'
+
+      argument :search, GraphQL::Types::String,
+        required: false,
+        description: 'Query to search mappings by name or username of users on source instance.'
+
+      argument :sort, Types::Import::SourceUserSortEnum,
+        description: 'Sort mapping of users on source instance to users on destination instance by the criteria.',
+        required: false,
+        default_value: :source_name_asc
+
       alias_method :namespace, :object
 
       def resolve_with_lookahead(**args)
diff --git a/app/graphql/types/import/source_user_sort_enum.rb b/app/graphql/types/import/source_user_sort_enum.rb
new file mode 100644
index 00000000000000..b7a8d4a4601d54
--- /dev/null
+++ b/app/graphql/types/import/source_user_sort_enum.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+  module Import
+    class SourceUserSortEnum < BaseEnum
+      graphql_name 'SourceUserSort'
+      description 'Values for sorting the mapping of users on source instance to users on destination instance.'
+
+      value 'STATUS_ASC', 'Status of the mapping by ascending order.', value: :status_asc
+      value 'STATUS_DESC', 'Status of the mapping by descending order.', value: :status_desc
+      value 'SOURCE_NAME_ASC', 'Instance source name by ascending order.', value: :source_name_asc
+      value 'SOURCE_NAME_DESC', 'Instance source name by descending order.', value: :source_name_desc
+    end
+  end
+end
diff --git a/app/models/import/source_user.rb b/app/models/import/source_user.rb
index 967d5e0b41b75f..9b7b24b53057da 100644
--- a/app/models/import/source_user.rb
+++ b/app/models/import/source_user.rb
@@ -2,8 +2,17 @@
 
 module Import
   class SourceUser < ApplicationRecord
+    include Gitlab::SQL::Pattern
+
     self.table_name = 'import_source_users'
 
+    SORT_ORDERS = {
+      source_name_asc: { order_by: 'source_name', sort: 'asc' },
+      source_name_desc: { order_by: 'source_name', sort: 'desc' },
+      status_asc: { order_by: 'status', sort: 'asc' },
+      status_desc: { order_by: 'status', sort: 'desc' }
+    }.freeze
+
     belongs_to :placeholder_user, class_name: 'User', optional: true
     belongs_to :reassign_to_user, class_name: 'User', optional: true
     belongs_to :reassigned_by_user, class_name: 'User', optional: true
@@ -12,6 +21,7 @@ class SourceUser < ApplicationRecord
     validates :namespace_id, :import_type, :source_hostname, :source_user_identifier, :status, presence: true
 
     scope :for_namespace, ->(namespace_id) { where(namespace_id: namespace_id) }
+    scope :by_statuses, ->(statuses) { where(status: statuses) }
 
     state_machine :status, initial: :pending_reassignment do
       state :pending_reassignment, value: 0
@@ -55,15 +65,29 @@ class SourceUser < ApplicationRecord
       end
     end
 
-    def self.find_source_user(source_user_identifier:, namespace:, source_hostname:, import_type:)
-      return unless namespace
+    class << self
+      def find_source_user(source_user_identifier:, namespace:, source_hostname:, import_type:)
+        return unless namespace
+
+        find_by(
+          source_user_identifier: source_user_identifier,
+          namespace_id: namespace.id,
+          source_hostname: source_hostname,
+          import_type: import_type
+        )
+      end
+
+      def search(query)
+        return none unless query.is_a?(String)
 
-      find_by(
-        source_user_identifier: source_user_identifier,
-        namespace_id: namespace.id,
-        source_hostname: source_hostname,
-        import_type: import_type
-      )
+        fuzzy_search(query, [:source_name, :source_username])
+      end
+
+      def sort_by_attribute(method)
+        sort_order = SORT_ORDERS[method&.to_sym] || SORT_ORDERS[:source_name_asc]
+
+        reorder(sort_order[:order_by] => sort_order[:sort])
+      end
     end
 
     def reassignable_status?
diff --git a/app/policies/import/source_user_policy.rb b/app/policies/import/source_user_policy.rb
index fa6e63e35fb171..44a4185aef90a7 100644
--- a/app/policies/import/source_user_policy.rb
+++ b/app/policies/import/source_user_policy.rb
@@ -2,8 +2,8 @@
 
 module Import
   class SourceUserPolicy < ::BasePolicy
-    condition(:admin_source_user_namespace) { can?(:admin_namespace, @subject.namespace) }
     desc "User can administrate namespace"
+    condition(:admin_source_user_namespace) { can?(:admin_namespace, @subject.namespace) }
 
     rule { admin_source_user_namespace }.policy do
       enable :admin_import_source_user
diff --git a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
new file mode 100644
index 00000000000000..3742aad52066c0
--- /dev/null
+++ b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexesToImportSourceUsers < Gitlab::Database::Migration[2.2]
+  disable_ddl_transaction!
+
+  milestone '17.2'
+
+  STATUS_INDEX = 'index_import_source_users_on_namespace_id_and_status'
+  # SOURCE_NAME_INDEX = 'index_import_source_users_on_namespace_id_status_source_name'
+
+  SOURCE_NAME_INDEX = 'index_import_source_users_source_name_trgm'
+  SOURCE_USERNAME_INDEX = 'index_import_source_users_source_username_trgm'
+
+  def up
+    add_concurrent_index :import_source_users, [:namespace_id, :status, :id], name: STATUS_INDEX
+    add_concurrent_index :import_source_users, :source_name, name: SOURCE_NAME_INDEX, using: :gin,
+      opclass: { path: :gin_trgm_ops }
+    add_concurrent_index :import_source_users, :source_username, name: SOURCE_USERNAME_INDEX, using: :gin,
+      opclass: { path: :gin_trgm_ops }
+  end
+
+  def down
+    remove_concurrent_index_by_name :import_source_users, name: STATUS_INDEX
+    remove_concurrent_index_by_name :import_source_users, name: SOURCE_NAME_INDEX
+    remove_concurrent_index_by_name :import_source_users, name: SOURCE_USERNAME_INDEX
+  end
+end
diff --git a/db/schema_migrations/20240627142648 b/db/schema_migrations/20240627142648
new file mode 100644
index 00000000000000..3870b50f668a3f
--- /dev/null
+++ b/db/schema_migrations/20240627142648
@@ -0,0 +1 @@
+434b51c7e1faea1ce5406ed78f97235247580053c4132dd5d0e325a3a2e7034e
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e127e7f413a468..aa1afc781e1756 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -27567,12 +27567,18 @@ CREATE INDEX index_import_source_user_placeholder_references_on_source_user_ ON
 
 CREATE INDEX index_import_source_users_on_namespace_id ON import_source_users USING btree (namespace_id);
 
+CREATE INDEX index_import_source_users_on_namespace_id_and_status ON import_source_users USING btree (namespace_id, status, id);
+
 CREATE INDEX index_import_source_users_on_placeholder_user_id ON import_source_users USING btree (placeholder_user_id);
 
 CREATE INDEX index_import_source_users_on_reassign_to_user_id ON import_source_users USING btree (reassign_to_user_id);
 
 CREATE INDEX index_import_source_users_on_reassigned_by_user_id ON import_source_users USING btree (reassigned_by_user_id);
 
+CREATE INDEX index_import_source_users_source_name_trgm ON import_source_users USING gin (source_name gin_trgm_ops);
+
+CREATE INDEX index_import_source_users_source_username_trgm ON import_source_users USING gin (source_username gin_trgm_ops);
+
 CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON projects USING btree (import_type, creator_id, created_at) WHERE (import_type IS NOT NULL);
 
 CREATE INDEX index_imported_projects_on_import_type_id ON projects USING btree (import_type, id) WHERE (import_type IS NOT NULL);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 244cf7966c5c17..7f67915ff62f91 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -22045,7 +22045,6 @@ GPG signature for a signed commit.
 | <a id="groupgooglecloudloggingconfigurations"></a>`googleCloudLoggingConfigurations` | [`GoogleCloudLoggingConfigurationTypeConnection`](#googlecloudloggingconfigurationtypeconnection) | Google Cloud logging configurations that receive audit events belonging to the group. (see [Connections](#connections)) |
 | <a id="groupgroupmemberscount"></a>`groupMembersCount` | [`Int!`](#int) | Count of direct members of this group. |
 | <a id="groupid"></a>`id` | [`ID!`](#id) | ID of the namespace. |
-| <a id="groupimportsourceusers"></a>`importSourceUsers` **{warning-solid}** | [`ImportSourceUserConnection`](#importsourceuserconnection) | **Introduced** in GitLab 17.2. **Status**: Experiment. Import source users of the namespace. This field can only be resolved for one namespace in any single request. |
 | <a id="groupisadjourneddeletionenabled"></a>`isAdjournedDeletionEnabled` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Indicates if delayed group deletion is enabled. |
 | <a id="grouplfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. |
 | <a id="grouplockduofeaturesenabled"></a>`lockDuoFeaturesEnabled` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in GitLab 16.10. **Status**: Experiment. Indicates if the GitLab Duo features enabled setting is enforced for all subgroups. |
@@ -22608,6 +22607,28 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="groupgroupmemberssearch"></a>`search` | [`String`](#string) | Search query. |
 | <a id="groupgroupmemberssort"></a>`sort` | [`MemberSort`](#membersort) | sort query. |
 
+##### `Group.importSourceUsers`
+
+Import source users of the namespace. This field can only be resolved for one namespace in any single request.
+
+DETAILS:
+**Introduced** in GitLab 17.2.
+**Status**: Experiment.
+
+Returns [`ImportSourceUserConnection`](#importsourceuserconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="groupimportsourceuserssearch"></a>`search` | [`String`](#string) | Query to search mappings by name or username of users on source instance. |
+| <a id="groupimportsourceuserssort"></a>`sort` | [`SourceUserSort`](#sourceusersort) | Sort mapping of users on source instance to users on destination instance by the criteria. |
+| <a id="groupimportsourceusersstatuses"></a>`statuses` | [`[ImportSourceUserStatus!]`](#importsourceuserstatus) | Filter mapping of users on source instance to users on destination instance by status. |
+
 ##### `Group.issues`
 
 Issues for projects in this group.
@@ -26501,7 +26522,6 @@ Product analytics events for a specific month and year.
 | <a id="namespacefullname"></a>`fullName` | [`String!`](#string) | Full name of the namespace. |
 | <a id="namespacefullpath"></a>`fullPath` | [`ID!`](#id) | Full path of the namespace. |
 | <a id="namespaceid"></a>`id` | [`ID!`](#id) | ID of the namespace. |
-| <a id="namespaceimportsourceusers"></a>`importSourceUsers` **{warning-solid}** | [`ImportSourceUserConnection`](#importsourceuserconnection) | **Introduced** in GitLab 17.2. **Status**: Experiment. Import source users of the namespace. This field can only be resolved for one namespace in any single request. |
 | <a id="namespacelfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if Large File Storage (LFS) is enabled for namespace. |
 | <a id="namespacename"></a>`name` | [`String!`](#string) | Name of the namespace. |
 | <a id="namespacepackagesettings"></a>`packageSettings` | [`PackageSettings`](#packagesettings) | Package settings for the namespace. |
@@ -26607,6 +26627,28 @@ four standard [pagination arguments](#pagination-arguments):
 | <a id="namespacecomplianceframeworksids"></a>`ids` | [`[ComplianceManagementFrameworkID!]`](#compliancemanagementframeworkid) | List of Global IDs of compliance frameworks to return. |
 | <a id="namespacecomplianceframeworkssearch"></a>`search` | [`String`](#string) | Search framework with most similar names. |
 
+##### `Namespace.importSourceUsers`
+
+Import source users of the namespace. This field can only be resolved for one namespace in any single request.
+
+DETAILS:
+**Introduced** in GitLab 17.2.
+**Status**: Experiment.
+
+Returns [`ImportSourceUserConnection`](#importsourceuserconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="namespaceimportsourceuserssearch"></a>`search` | [`String`](#string) | Query to search mappings by name or username of users on source instance. |
+| <a id="namespaceimportsourceuserssort"></a>`sort` | [`SourceUserSort`](#sourceusersort) | Sort mapping of users on source instance to users on destination instance by the criteria. |
+| <a id="namespaceimportsourceusersstatuses"></a>`statuses` | [`[ImportSourceUserStatus!]`](#importsourceuserstatus) | Filter mapping of users on source instance to users on destination instance by status. |
+
 ##### `Namespace.pagesDeployments`
 
 List of the namespaces's Pages Deployments.
@@ -36242,6 +36284,17 @@ Values for sort direction.
 | <a id="sortdirectionenumasc"></a>`ASC` | Ascending order. |
 | <a id="sortdirectionenumdesc"></a>`DESC` | Descending order. |
 
+### `SourceUserSort`
+
+Values for sorting the mapping of users on source instance to users on destination instance.
+
+| Value | Description |
+| ----- | ----------- |
+| <a id="sourceusersortsource_name_asc"></a>`SOURCE_NAME_ASC` | Instance source name by ascending order. |
+| <a id="sourceusersortsource_name_desc"></a>`SOURCE_NAME_DESC` | Instance source name by descending order. |
+| <a id="sourceusersortstatus_asc"></a>`STATUS_ASC` | Status of the mapping by ascending order. |
+| <a id="sourceusersortstatus_desc"></a>`STATUS_DESC` | Status of the mapping by descending order. |
+
 ### `TestCaseStatus`
 
 | Value | Description |
diff --git a/spec/finders/import/source_users_finder_spec.rb b/spec/finders/import/source_users_finder_spec.rb
index 27a117a4b78ac7..4d0d7fbf6b0c53 100644
--- a/spec/finders/import/source_users_finder_spec.rb
+++ b/spec/finders/import/source_users_finder_spec.rb
@@ -5,10 +5,23 @@
 RSpec.describe Import::SourceUsersFinder, feature_category: :importers do
   let_it_be(:user) { build_stubbed(:user) }
   let_it_be(:group) { create(:group) }
-  let_it_be(:import_source_users) { create_list(:import_source_user, 3, namespace: group) }
+  let_it_be(:source_user_1) { create(:import_source_user, namespace: group, status: 0, source_name: 'b') }
+  let_it_be(:source_user_2) { create(:import_source_user, namespace: group, status: 1, source_name: 'c') }
+  let_it_be(:source_user_3) { create(:import_source_user, namespace: group, status: 2, source_name: 'a') }
+  let_it_be(:import_source_users) { [source_user_1, source_user_2, source_user_3] }
+
+  let(:params) { {} }
 
   describe '#execute' do
-    subject(:source_user_result) { described_class.new(group, user).execute }
+    subject(:source_user_result) { described_class.new(group, user, params).execute }
+
+    context 'when user is not authorized to read the import source users' do
+      before do
+        stub_member_access_level(group, maintainer: user)
+      end
+
+      it { expect(source_user_result).to be_empty }
+    end
 
     context 'when user is authorized to read the import source users' do
       before do
@@ -18,15 +31,57 @@
       it 'returns all import source users' do
         expect(source_user_result).to match_array(import_source_users)
       end
-    end
 
-    context 'when user is not authorized to read the import source users' do
-      before do
-        stub_member_access_level(group, maintainer: user)
+      describe 'filtering by statuses' do
+        context 'when statuses are not provided' do
+          let(:params) { {} }
+
+          it 'returns all import source users' do
+            expect(source_user_result).to match_array(import_source_users)
+          end
+        end
+
+        context 'when statuses are is provided' do
+          let(:params) { { statuses: [0, 1] } }
+
+          it 'returns import source users with the corresponding status' do
+            expect(source_user_result.pluck(:status)).to match_array([0, 1])
+          end
+        end
       end
 
-      it 'is empty' do
-        expect(source_user_result).to be_empty
+      describe 'filtering by search' do
+        context 'when search are not provided' do
+          let(:params) { {} }
+
+          it 'returns all import source users' do
+            expect(source_user_result).to match_array(import_source_users)
+          end
+        end
+
+        context 'when search is is provided' do
+          let(:params) { { search: 'b' } }
+
+          it 'returns import source users with matches the search query' do
+            expect(source_user_result).to match_array(source_user_1)
+          end
+        end
+      end
+
+      describe 'sorting' do
+        let(:params) { { sort: 'source_name_desc' } }
+
+        it 'returns import source users sorted by the provided method' do
+          expect(source_user_result.pluck(:source_name)).to eq(%w[c b a])
+        end
+
+        context 'when sort is not provided' do
+          let(:params) { {} }
+
+          it 'returns import source users sorted by source_name_asc' do
+            expect(source_user_result.pluck(:source_name)).to eq(%w[a b c])
+          end
+        end
       end
     end
   end
diff --git a/spec/graphql/resolvers/import/source_users_resolver_spec.rb b/spec/graphql/resolvers/import/source_users_resolver_spec.rb
index c908791dc12d45..31dfc221f7e423 100644
--- a/spec/graphql/resolvers/import/source_users_resolver_spec.rb
+++ b/spec/graphql/resolvers/import/source_users_resolver_spec.rb
@@ -37,6 +37,20 @@
 
       it { expect(resolve_import_source_users).to be_empty }
     end
+
+    describe 'arguments' do
+      let(:args) { { search: 'search', statuses: ['AWAITING_APPROVAL'], sort: 'STATUS_ASC' } }
+
+      it 'calls Import::SourceUsersFinder with the expected arguments' do
+        expected_args = { search: 'search', statuses: [1], sort: :status_asc }
+
+        expect_next_instance_of(::Import::SourceUsersFinder, group, current_user, expected_args) do |finder|
+          expect(finder).to receive(:execute)
+        end
+
+        resolve_import_source_users
+      end
+    end
   end
 
   def resolve_import_source_users
diff --git a/spec/models/import/source_user_spec.rb b/spec/models/import/source_user_spec.rb
index c22e02f56571a0..931818105069a7 100644
--- a/spec/models/import/source_user_spec.rb
+++ b/spec/models/import/source_user_spec.rb
@@ -118,6 +118,74 @@
     end
   end
 
+  describe '.search' do
+    let!(:source_user) do
+      create(:import_source_user, source_name: 'Source Name', source_username: 'Username')
+    end
+
+    it 'searches by source_name or source_username' do
+      expect(described_class.search('name')).to eq([source_user])
+      expect(described_class.search('username')).to eq([source_user])
+      expect(described_class.search('source')).to eq([source_user])
+      expect(described_class.search('inexistent')).to eq([])
+    end
+  end
+
+  describe '.sort_by_attribute' do
+    let_it_be(:namespace) { create(:namespace) }
+    let_it_be(:source_user_1) { create(:import_source_user, namespace: namespace, status: 4, source_name: 'd') }
+    let_it_be(:source_user_2) { create(:import_source_user, namespace: namespace, status: 2, source_name: 'b') }
+    let_it_be(:source_user_3) { create(:import_source_user, namespace: namespace, status: 1, source_name: 'a') }
+    let_it_be(:source_user_4) { create(:import_source_user, namespace: namespace, status: 3, source_name: 'c') }
+
+    let(:sort_by_attribute) { described_class.sort_by_attribute(method).pluck(attribute) }
+
+    context 'with method status_asc' do
+      let(:method) { 'status_asc' }
+      let(:attribute) { :status }
+
+      it 'order by status_desc ascending' do
+        expect(sort_by_attribute).to eq([1, 2, 3, 4])
+      end
+    end
+
+    context 'with method status_desc' do
+      let(:method) { 'status_desc' }
+      let(:attribute) { :status }
+
+      it 'order by status_desc descending' do
+        expect(sort_by_attribute).to eq([4, 3, 2, 1])
+      end
+    end
+
+    context 'with method source_name_asc' do
+      let(:method) { 'source_name_asc' }
+      let(:attribute) { :source_name }
+
+      it 'order by source_name_asc ascending' do
+        expect(sort_by_attribute).to eq(%w[a b c d])
+      end
+    end
+
+    context 'with method source_name_desc' do
+      let(:method) { 'source_name_desc' }
+      let(:attribute) { :source_name }
+
+      it 'order by source_name_desc descending' do
+        expect(sort_by_attribute).to eq(%w[d c b a])
+      end
+    end
+
+    context 'with an unexpected method' do
+      let(:method) { 'id_asc' }
+      let(:attribute) { :source_name }
+
+      it 'order by source_name_asc ascending' do
+        expect(sort_by_attribute).to eq(%w[a b c d])
+      end
+    end
+  end
+
   describe '#reassignable_status?' do
     reassignable_statuses = [:pending_reassignment, :rejected]
     all_states = described_class.state_machines[:status].states
diff --git a/spec/support/helpers/database/duplicate_indexes.yml b/spec/support/helpers/database/duplicate_indexes.yml
index 784be0bf14085a..c9f763d09c43e4 100644
--- a/spec/support/helpers/database/duplicate_indexes.yml
+++ b/spec/support/helpers/database/duplicate_indexes.yml
@@ -20,6 +20,9 @@ error_tracking_errors:
 geo_node_namespace_links:
   index_geo_node_namespace_links_on_geo_node_id_and_namespace_id:
   - index_geo_node_namespace_links_on_geo_node_id
+import_source_users:
+  index_import_source_users_on_namespace_id_and_status:
+    - index_import_source_users_on_namespace_id
 incident_management_oncall_participants:
   index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id:
   - index_inc_mgmnt_oncall_participants_on_oncall_user_id
-- 
GitLab


From 081d1a98e516c28f7918b14d8f16d6a20c57179d Mon Sep 17 00:00:00 2001
From: Rodrigo Tomonari <rtomonari@gitlab.com>
Date: Tue, 2 Jul 2024 15:05:05 -0300
Subject: [PATCH 2/6] Use array in rspec

---
 spec/finders/import/source_users_finder_spec.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spec/finders/import/source_users_finder_spec.rb b/spec/finders/import/source_users_finder_spec.rb
index 4d0d7fbf6b0c53..ba08c54440d1f7 100644
--- a/spec/finders/import/source_users_finder_spec.rb
+++ b/spec/finders/import/source_users_finder_spec.rb
@@ -63,7 +63,7 @@
           let(:params) { { search: 'b' } }
 
           it 'returns import source users with matches the search query' do
-            expect(source_user_result).to match_array(source_user_1)
+            expect(source_user_result).to match_array([source_user_1])
           end
         end
       end
-- 
GitLab


From ca2116884f9e8dccc4bee2f38b6591ff49e74a8b Mon Sep 17 00:00:00 2001
From: Rodrigo Tomonari <rtomonari@gitlab.com>
Date: Tue, 2 Jul 2024 15:49:29 -0300
Subject: [PATCH 3/6] Remove comments from migration

---
 .../20240627142648_add_indexes_to_import_source_users.rb     | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
index 3742aad52066c0..e1caf3ced40827 100644
--- a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
+++ b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
@@ -1,16 +1,11 @@
 # frozen_string_literal: true
 
-# See https://docs.gitlab.com/ee/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-
 class AddIndexesToImportSourceUsers < Gitlab::Database::Migration[2.2]
   disable_ddl_transaction!
 
   milestone '17.2'
 
   STATUS_INDEX = 'index_import_source_users_on_namespace_id_and_status'
-  # SOURCE_NAME_INDEX = 'index_import_source_users_on_namespace_id_status_source_name'
-
   SOURCE_NAME_INDEX = 'index_import_source_users_source_name_trgm'
   SOURCE_USERNAME_INDEX = 'index_import_source_users_source_username_trgm'
 
-- 
GitLab


From 0a1646ee6ea88568dce1015fe301a11b98e1f4ab Mon Sep 17 00:00:00 2001
From: Rodrigo Tomonari <rtomonari@gitlab.com>
Date: Fri, 12 Jul 2024 10:55:51 -0300
Subject: [PATCH 4/6] Remove unnecessary indexes

---
 ...0627142648_add_indexes_to_import_source_users.rb | 13 +++++--------
 db/structure.sql                                    |  6 ------
 spec/support/helpers/database/duplicate_indexes.yml |  3 ---
 3 files changed, 5 insertions(+), 17 deletions(-)

diff --git a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
index e1caf3ced40827..b7fddcacbe6ad6 100644
--- a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
+++ b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
@@ -6,20 +6,17 @@ class AddIndexesToImportSourceUsers < Gitlab::Database::Migration[2.2]
   milestone '17.2'
 
   STATUS_INDEX = 'index_import_source_users_on_namespace_id_and_status'
-  SOURCE_NAME_INDEX = 'index_import_source_users_source_name_trgm'
-  SOURCE_USERNAME_INDEX = 'index_import_source_users_source_username_trgm'
+  NAMESPACE_INDEX = 'index_import_source_users_on_namespace_id'
 
   def up
+    remove_concurrent_index_by_name :import_source_users, name: NAMESPACE_INDEX
+
     add_concurrent_index :import_source_users, [:namespace_id, :status, :id], name: STATUS_INDEX
-    add_concurrent_index :import_source_users, :source_name, name: SOURCE_NAME_INDEX, using: :gin,
-      opclass: { path: :gin_trgm_ops }
-    add_concurrent_index :import_source_users, :source_username, name: SOURCE_USERNAME_INDEX, using: :gin,
-      opclass: { path: :gin_trgm_ops }
   end
 
   def down
+    add_concurrent_index :import_source_users, [:namespace_id], name: NAMESPACE_INDEX
+
     remove_concurrent_index_by_name :import_source_users, name: STATUS_INDEX
-    remove_concurrent_index_by_name :import_source_users, name: SOURCE_NAME_INDEX
-    remove_concurrent_index_by_name :import_source_users, name: SOURCE_USERNAME_INDEX
   end
 end
diff --git a/db/structure.sql b/db/structure.sql
index aa1afc781e1756..36fdeb3622810d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -27565,8 +27565,6 @@ CREATE INDEX index_import_source_user_placeholder_references_on_namespace_id ON
 
 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_namespace_id_and_status ON import_source_users USING btree (namespace_id, status, id);
 
 CREATE INDEX index_import_source_users_on_placeholder_user_id ON import_source_users USING btree (placeholder_user_id);
@@ -27575,10 +27573,6 @@ CREATE INDEX index_import_source_users_on_reassign_to_user_id ON import_source_u
 
 CREATE INDEX index_import_source_users_on_reassigned_by_user_id ON import_source_users USING btree (reassigned_by_user_id);
 
-CREATE INDEX index_import_source_users_source_name_trgm ON import_source_users USING gin (source_name gin_trgm_ops);
-
-CREATE INDEX index_import_source_users_source_username_trgm ON import_source_users USING gin (source_username gin_trgm_ops);
-
 CREATE INDEX index_imported_projects_on_import_type_creator_id_created_at ON projects USING btree (import_type, creator_id, created_at) WHERE (import_type IS NOT NULL);
 
 CREATE INDEX index_imported_projects_on_import_type_id ON projects USING btree (import_type, id) WHERE (import_type IS NOT NULL);
diff --git a/spec/support/helpers/database/duplicate_indexes.yml b/spec/support/helpers/database/duplicate_indexes.yml
index c9f763d09c43e4..784be0bf14085a 100644
--- a/spec/support/helpers/database/duplicate_indexes.yml
+++ b/spec/support/helpers/database/duplicate_indexes.yml
@@ -20,9 +20,6 @@ error_tracking_errors:
 geo_node_namespace_links:
   index_geo_node_namespace_links_on_geo_node_id_and_namespace_id:
   - index_geo_node_namespace_links_on_geo_node_id
-import_source_users:
-  index_import_source_users_on_namespace_id_and_status:
-    - index_import_source_users_on_namespace_id
 incident_management_oncall_participants:
   index_inc_mgmnt_oncall_participants_on_user_id_and_rotation_id:
   - index_inc_mgmnt_oncall_participants_on_oncall_user_id
-- 
GitLab


From c36fd652475e4055afe5e2949f58940dbde715b9 Mon Sep 17 00:00:00 2001
From: Rodrigo Tomonari <rtomonari@gitlab.com>
Date: Fri, 12 Jul 2024 11:58:15 -0300
Subject: [PATCH 5/6] Update migration milestone

---
 db/migrate/20240627142648_add_indexes_to_import_source_users.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
index b7fddcacbe6ad6..6519149a331fda 100644
--- a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
+++ b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
@@ -3,7 +3,7 @@
 class AddIndexesToImportSourceUsers < Gitlab::Database::Migration[2.2]
   disable_ddl_transaction!
 
-  milestone '17.2'
+  milestone '17.3'
 
   STATUS_INDEX = 'index_import_source_users_on_namespace_id_and_status'
   NAMESPACE_INDEX = 'index_import_source_users_on_namespace_id'
-- 
GitLab


From 432923d2877c16512459929b1fa1012b0f5ec857 Mon Sep 17 00:00:00 2001
From: Rodrigo Tomonari <rtomonari@gitlab.com>
Date: Mon, 15 Jul 2024 10:39:30 -0300
Subject: [PATCH 6/6] Change index order and remove :id from index

---
 .../20240627142648_add_indexes_to_import_source_users.rb      | 4 ++--
 db/structure.sql                                              | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
index 6519149a331fda..60a2eba763e011 100644
--- a/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
+++ b/db/migrate/20240627142648_add_indexes_to_import_source_users.rb
@@ -9,9 +9,9 @@ class AddIndexesToImportSourceUsers < Gitlab::Database::Migration[2.2]
   NAMESPACE_INDEX = 'index_import_source_users_on_namespace_id'
 
   def up
-    remove_concurrent_index_by_name :import_source_users, name: NAMESPACE_INDEX
+    add_concurrent_index :import_source_users, [:namespace_id, :status], name: STATUS_INDEX
 
-    add_concurrent_index :import_source_users, [:namespace_id, :status, :id], name: STATUS_INDEX
+    remove_concurrent_index_by_name :import_source_users, name: NAMESPACE_INDEX
   end
 
   def down
diff --git a/db/structure.sql b/db/structure.sql
index 36fdeb3622810d..0403afd7b7754a 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -27565,7 +27565,7 @@ CREATE INDEX index_import_source_user_placeholder_references_on_namespace_id ON
 
 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_and_status ON import_source_users USING btree (namespace_id, status, id);
+CREATE INDEX index_import_source_users_on_namespace_id_and_status ON import_source_users USING btree (namespace_id, status);
 
 CREATE INDEX index_import_source_users_on_placeholder_user_id ON import_source_users USING btree (placeholder_user_id);
 
-- 
GitLab