From cfbd92e5e38194e3b26bbff8f9afb5f496a53b46 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 30 Aug 2023 14:55:20 -0600
Subject: [PATCH 01/32] Add sorting and filtering specs

---
 .../groups/dependencies_controller.rb         | 10 ++++-
 .../groups/dependencies_controller_spec.rb    | 43 ++++++++++++++++++-
 2 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/ee/app/controllers/groups/dependencies_controller.rb b/ee/app/controllers/groups/dependencies_controller.rb
index 722539b3a1d8c8..d4e6ae41f250e6 100644
--- a/ee/app/controllers/groups/dependencies_controller.rb
+++ b/ee/app/controllers/groups/dependencies_controller.rb
@@ -47,7 +47,15 @@ def dependency_list_params
     end
 
     def collect_dependencies
-      @collect_dependencies ||= ::Sbom::DependenciesFinder.new(group, params: dependency_list_params).execute
+      query_params = params.permit(
+        :sort_by,
+        :sort,
+        component_names: [],
+        package_managers: []
+      )
+      ::Sbom::DependenciesFinder
+        .new(group, params: query_params)
+        .execute
     end
 
     def serialized_dependencies
diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index c20a37e1e14f1a..85af132a2034c5 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -200,7 +200,18 @@
             end
 
             context 'with sorting params' do
-              context 'when sorted by packager' do
+              context 'when sorted by packager in ascending order' do
+                let(:params) { { group_id: group.to_param, sort_by: 'packager', sort: 'asc' } }
+
+                it 'returns sorted list' do
+                  subject
+
+                  expect(json_response['dependencies'].first['packager']).to eq('bundler')
+                  expect(json_response['dependencies'].last['packager']).to eq('npm')
+                end
+              end
+
+              context 'when sorted by packager in descending order' do
                 let(:params) { { group_id: group.to_param, sort_by: 'packager', sort: 'desc' } }
 
                 it 'returns sorted list' do
@@ -211,7 +222,7 @@
                 end
               end
 
-              context 'when sorted by name' do
+              context 'when sorted by name in ascending order' do
                 let(:params) { { group_id: group.to_param, sort_by: 'name', sort: 'asc' } }
 
                 it 'returns sorted list' do
@@ -221,6 +232,17 @@
                   expect(json_response['dependencies'].last['name']).to eq(sbom_occurrence_bundler.name)
                 end
               end
+
+              context 'when sorted by name in descending order' do
+                let(:params) { { group_id: group.to_param, sort_by: 'name', sort: 'desc' } }
+
+                it 'returns sorted list' do
+                  subject
+
+                  expect(json_response['dependencies'].first['name']).to eq(sbom_occurrence_bundler.name)
+                  expect(json_response['dependencies'].last['name']).to eq(sbom_occurrence_npm.name)
+                end
+              end
             end
 
             context 'with filtering params' do
@@ -230,9 +252,26 @@
                 it 'returns filtered list' do
                   subject
 
+                  expect(json_response['dependencies'].count).to eq(1)
                   expect(json_response['dependencies'].pluck('packager')).to eq(['npm'])
                 end
               end
+
+              context 'when filtered by component_names' do
+                let(:params) do
+                  {
+                    group_id: group.to_param,
+                    component_names: [sbom_occurrence_bundler.name]
+                  }
+                end
+
+                it 'returns filtered list' do
+                  subject
+
+                  expect(json_response['dependencies'].count).to eq(1)
+                  expect(json_response['dependencies'].pluck('name')).to eq([sbom_occurrence_bundler.name])
+                end
+              end
             end
           end
 
-- 
GitLab


From b50535a0d948764ea0c772faaaa46462a987f9e0 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 30 Aug 2023 14:57:49 -0600
Subject: [PATCH 02/32] Remove params that are not supported

---
 ee/app/controllers/groups/dependencies_controller.rb | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/ee/app/controllers/groups/dependencies_controller.rb b/ee/app/controllers/groups/dependencies_controller.rb
index d4e6ae41f250e6..933d0a4e808ecf 100644
--- a/ee/app/controllers/groups/dependencies_controller.rb
+++ b/ee/app/controllers/groups/dependencies_controller.rb
@@ -42,10 +42,6 @@ def authorize_read_dependency_list!
       render_not_authorized
     end
 
-    def dependency_list_params
-      params.permit(:sort_by, :sort, :component_id, :search, package_managers: [])
-    end
-
     def collect_dependencies
       query_params = params.permit(
         :sort_by,
@@ -82,7 +78,7 @@ def locations_info
 
     def dependency_locations
       Sbom::DependencyLocationsFinder
-        .new(namespace: group, params: dependency_list_params.slice(:component_id, :search))
+        .new(namespace: group, params: params.permit(:component_id, :search))
         .execute
     end
 
-- 
GitLab


From b2a541ecf27e307057bf55ebec9a8110e675d4f8 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 30 Aug 2023 15:13:39 -0600
Subject: [PATCH 03/32] Inline methods

---
 .../groups/dependencies_controller.rb         | 34 +++++++------------
 1 file changed, 13 insertions(+), 21 deletions(-)

diff --git a/ee/app/controllers/groups/dependencies_controller.rb b/ee/app/controllers/groups/dependencies_controller.rb
index 933d0a4e808ecf..8062dc23c99989 100644
--- a/ee/app/controllers/groups/dependencies_controller.rb
+++ b/ee/app/controllers/groups/dependencies_controller.rb
@@ -25,13 +25,19 @@ def index
           render status: :ok
         end
         format.json do
-          render json: serialized_dependencies
+          render json: dependencies_serializer
+            .represent(dependencies_finder.execute)
         end
       end
     end
 
     def locations
-      render json: locations_info
+      locations_finder = Sbom::DependencyLocationsFinder.new(
+        namespace: group,
+        params: params.permit(:component_id, :search)
+      )
+      render json: ::Sbom::DependencyLocationListEntity
+        .represent(locations_finder.execute)
     end
 
     private
@@ -42,23 +48,19 @@ def authorize_read_dependency_list!
       render_not_authorized
     end
 
-    def collect_dependencies
-      query_params = params.permit(
-        :sort_by,
+    def dependencies_finder
+      ::Sbom::DependenciesFinder.new(group, params: params.permit(
         :sort,
+        :sort_by,
         component_names: [],
         package_managers: []
-      )
-      ::Sbom::DependenciesFinder
-        .new(group, params: query_params)
-        .execute
+      ))
     end
 
-    def serialized_dependencies
+    def dependencies_serializer
       DependencyListSerializer
         .new(project: nil, group: group, user: current_user)
         .with_pagination(request, response)
-        .represent(collect_dependencies)
     end
 
     def render_not_authorized
@@ -72,16 +74,6 @@ def render_not_authorized
       end
     end
 
-    def locations_info
-      ::Sbom::DependencyLocationListEntity.represent(dependency_locations)
-    end
-
-    def dependency_locations
-      Sbom::DependencyLocationsFinder
-        .new(namespace: group, params: params.permit(:component_id, :search))
-        .execute
-    end
-
     def set_enable_project_search
       @enable_project_search = group.count_within_namespaces <= GROUP_COUNT_LIMIT
     end
-- 
GitLab


From a1c6e23532591621fc7e6b56cb73cd475a839a19 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 30 Aug 2023 16:43:18 -0600
Subject: [PATCH 04/32] Start to sort dependencies by spdx_identifier

---
 .../groups/dependencies_controller.rb         | 10 ++++-----
 ee/app/finders/sbom/dependencies_finder.rb    |  2 ++
 ee/app/models/sbom/occurrence.rb              |  4 ++++
 ee/spec/factories/sbom/occurrences.rb         | 10 +++++++++
 .../finders/sbom/dependencies_finder_spec.rb  | 16 +++++++++++---
 .../groups/dependencies_controller_spec.rb    | 22 +++++++++++++++++++
 6 files changed, 56 insertions(+), 8 deletions(-)

diff --git a/ee/app/controllers/groups/dependencies_controller.rb b/ee/app/controllers/groups/dependencies_controller.rb
index 8062dc23c99989..2e7bd6650facf9 100644
--- a/ee/app/controllers/groups/dependencies_controller.rb
+++ b/ee/app/controllers/groups/dependencies_controller.rb
@@ -32,12 +32,12 @@ def index
     end
 
     def locations
-      locations_finder = Sbom::DependencyLocationsFinder.new(
-        namespace: group,
-        params: params.permit(:component_id, :search)
+      render json: ::Sbom::DependencyLocationListEntity.represent(
+        Sbom::DependencyLocationsFinder.new(
+          namespace: group,
+          params: params.permit(:component_id, :search)
+        ).execute
       )
-      render json: ::Sbom::DependencyLocationListEntity
-        .represent(locations_finder.execute)
     end
 
     private
diff --git a/ee/app/finders/sbom/dependencies_finder.rb b/ee/app/finders/sbom/dependencies_finder.rb
index 50dfc999ffbc25..4cb50f5810a266 100644
--- a/ee/app/finders/sbom/dependencies_finder.rb
+++ b/ee/app/finders/sbom/dependencies_finder.rb
@@ -15,6 +15,8 @@ def execute
         sbom_occurrences.order_by_component_name(sort_direction)
       when 'packager'
         sbom_occurrences.order_by_package_name(sort_direction)
+      when 'spdx_identifier'
+        sbom_occurrences.order_by_spdx_identifier(sort_direction)
       else
         sbom_occurrences.order_by_id
       end
diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index 4186cbbfacf6c6..6eceb4f57534c3 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -33,6 +33,10 @@ class Occurrence < ApplicationRecord
       order(package_manager: direction)
     end
 
+    scope :order_by_spdx_identifier, ->(_direction) do
+      order(Arel.sql("licenses#>'{0,spdx_identifier}' ASC"))
+    end
+
     scope :filter_by_package_managers, ->(package_managers) do
       where(package_manager: package_managers)
     end
diff --git a/ee/spec/factories/sbom/occurrences.rb b/ee/spec/factories/sbom/occurrences.rb
index 3b8872fc6fcaad..a0f9f4fd22a3b5 100644
--- a/ee/spec/factories/sbom/occurrences.rb
+++ b/ee/spec/factories/sbom/occurrences.rb
@@ -41,6 +41,16 @@
       end
     end
 
+    trait :mpl_2 do
+      after(:build) do |occurrence|
+        occurrence.licenses.push({
+          'spdx_identifier' => 'MPL-2.0',
+          'name' => 'Mozilla Public License 2.0',
+          'url' => 'https://spdx.org/licenses/MPL-2.0.html'
+        })
+      end
+    end
+
     after(:build) do |occurrence|
       occurrence.uuid = Sbom::OccurrenceUUID.generate(
         project_id: occurrence.project.id,
diff --git a/ee/spec/finders/sbom/dependencies_finder_spec.rb b/ee/spec/finders/sbom/dependencies_finder_spec.rb
index 24eb20cea1627c..d4510c53352c65 100644
--- a/ee/spec/finders/sbom/dependencies_finder_spec.rb
+++ b/ee/spec/finders/sbom/dependencies_finder_spec.rb
@@ -14,15 +14,15 @@
   let_it_be(:component_version_3) { create(:sbom_component_version, component: component_3) }
 
   let_it_be(:occurrence_1) do
-    create(:sbom_occurrence, component_version: component_version_1, packager_name: 'nuget', project: project)
+    create(:sbom_occurrence, :mit, component_version: component_version_1, packager_name: 'nuget', project: project)
   end
 
   let_it_be(:occurrence_2) do
-    create(:sbom_occurrence, component_version: component_version_2, packager_name: 'npm', project: project)
+    create(:sbom_occurrence, :apache_2, component_version: component_version_2, packager_name: 'npm', project: project)
   end
 
   let_it_be(:occurrence_3) do
-    create(:sbom_occurrence, component_version: component_version_3, source: nil, project: project)
+    create(:sbom_occurrence, :mpl_2, component_version: component_version_3, source: nil, project: project)
   end
 
   shared_examples 'filter and sorting' do
@@ -94,6 +94,16 @@
         end
       end
 
+      context 'when sorted asc by spdx_identifier' do
+        let_it_be(:params) { { sort: 'asc', sort_by: 'spdx_identifier' } }
+
+        it 'returns sorted results' do
+          expect(dependencies[0].licenses[0]["spdx_identifier"]).to eq('Apache-2.0')
+          expect(dependencies[1].licenses[0]["spdx_identifier"]).to eq('MIT')
+          expect(dependencies[2].licenses[0]["spdx_identifier"]).to eq('MPL-2.0')
+        end
+      end
+
       context 'when filtered by package name npm' do
         let_it_be(:params) do
           {
diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index 85af132a2034c5..d230bf59f7bd86 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -243,6 +243,28 @@
                   expect(json_response['dependencies'].last['name']).to eq(sbom_occurrence_npm.name)
                 end
               end
+
+              context 'when sorted by spdx_identifier in ascending order' do
+                let(:params) { { group_id: group.to_param, sort_by: 'spdx_identifier', sort: 'asc' } }
+
+                it 'returns sorted list' do
+                  subject
+
+                  expect(json_response['dependencies'].first['licenses'][0]['spdx_identifier']).to eq('Apache-2.0')
+                  expect(json_response['dependencies'].last['licenses'][0]['spdx_identifier']).to eq('MIT')
+                end
+              end
+
+              context 'when sorted by spdx_identifier in descending order' do
+                let(:params) { { group_id: group.to_param, sort_by: 'spdx_identifier', sort: 'desc' } }
+
+                it 'returns sorted list' do
+                  subject
+
+                  expect(json_response['dependencies'].first['licenses'][0]['spdx_identifier']).to eq('MIT')
+                  expect(json_response['dependencies'].last['licenses'][0]['spdx_identifier']).to eq('Apache-2.0')
+                end
+              end
             end
 
             context 'with filtering params' do
-- 
GitLab


From 162e6c6afe4ec69c48520c61bd8d5eabab3b75d1 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Thu, 31 Aug 2023 14:51:16 -0600
Subject: [PATCH 05/32] Sort occurrences by licenses

---
 ee/app/models/sbom/occurrence.rb       |  6 ++--
 ee/spec/factories/sbom/occurrences.rb  |  4 +++
 ee/spec/models/sbom/occurrence_spec.rb | 38 ++++++++++++++++++++++++++
 3 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index 6eceb4f57534c3..b866e22b22fff4 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -33,8 +33,10 @@ class Occurrence < ApplicationRecord
       order(package_manager: direction)
     end
 
-    scope :order_by_spdx_identifier, ->(_direction) do
-      order(Arel.sql("licenses#>'{0,spdx_identifier}' ASC"))
+    scope :order_by_spdx_identifier, ->(direction, depth: 1) do
+      0.upto(depth).inject(self) do |relation, index|
+        relation.order(Arel.sql("licenses#>'{#{index},spdx_identifier}' #{direction}"))
+      end
     end
 
     scope :filter_by_package_managers, ->(package_managers) do
diff --git a/ee/spec/factories/sbom/occurrences.rb b/ee/spec/factories/sbom/occurrences.rb
index a0f9f4fd22a3b5..1ff696f95ecc62 100644
--- a/ee/spec/factories/sbom/occurrences.rb
+++ b/ee/spec/factories/sbom/occurrences.rb
@@ -21,6 +21,10 @@
       packager_name { 'npm' }
     end
 
+    trait :nuget do
+      packager_name { 'nuget' }
+    end
+
     trait :apache_2 do
       after(:build) do |occurrence|
         occurrence.licenses.push({
diff --git a/ee/spec/models/sbom/occurrence_spec.rb b/ee/spec/models/sbom/occurrence_spec.rb
index 843647237b1b3b..fb56e3f950fb83 100644
--- a/ee/spec/models/sbom/occurrence_spec.rb
+++ b/ee/spec/models/sbom/occurrence_spec.rb
@@ -204,6 +204,44 @@
     end
   end
 
+  describe '.order_by_spdx_identifier' do
+    let_it_be(:mit_occurrence) { create(:sbom_occurrence, :mit) }
+    let_it_be(:apache_occurrence) { create(:sbom_occurrence, :apache_2) }
+    let_it_be(:apache_and_mpl_occurrence) { create(:sbom_occurrence, :apache_2, :mpl_2) }
+    let_it_be(:apache_and_mit_occurrence) { create(:sbom_occurrence, :apache_2, :mit) }
+    let_it_be(:mit_and_mpl_occurrence) { create(:sbom_occurrence, :mit, :mpl_2) }
+
+    subject(:relation) { described_class.order_by_spdx_identifier(order) }
+
+    context 'when sorting in ascending order' do
+      let(:order) { 'asc' }
+
+      it 'returns the sorted records' do
+        expect(relation.map(&:licenses)).to eq([
+          apache_and_mit_occurrence.licenses,
+          apache_and_mpl_occurrence.licenses,
+          apache_occurrence.licenses,
+          mit_and_mpl_occurrence.licenses,
+          mit_occurrence.licenses
+        ])
+      end
+    end
+
+    context 'when sorting in descending order' do
+      let(:order) { 'desc' }
+
+      it 'returns the sorted records' do
+        expect(relation.map(&:licenses)).to eq([
+          mit_occurrence.licenses,
+          mit_and_mpl_occurrence.licenses,
+          apache_occurrence.licenses,
+          apache_and_mpl_occurrence.licenses,
+          apache_and_mit_occurrence.licenses
+        ])
+      end
+    end
+  end
+
   describe '.filter_by_component_names' do
     let_it_be(:occurrence_1) { create(:sbom_occurrence) }
     let_it_be(:occurrence_2) { create(:sbom_occurrence) }
-- 
GitLab


From 0ca971a4f340e420f24ea2ed3d074bfa5b28d5a6 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 6 Sep 2023 15:34:52 -0600
Subject: [PATCH 06/32] Fix N+1 issue

---
 ee/app/finders/sbom/dependencies_finder.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ee/app/finders/sbom/dependencies_finder.rb b/ee/app/finders/sbom/dependencies_finder.rb
index 4cb50f5810a266..c16d251ac2c6e9 100644
--- a/ee/app/finders/sbom/dependencies_finder.rb
+++ b/ee/app/finders/sbom/dependencies_finder.rb
@@ -33,7 +33,7 @@ def filtered_collection
 
       collection = filter_by_component_names(collection) if params[:component_names].present?
 
-      collection
+      collection.with_component.with_version.with_source
     end
 
     def filter_by_package_managers(sbom_occurrences)
-- 
GitLab


From 4b486ce872c8aaea991422cdf523a4e51011e723 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 6 Sep 2023 15:44:24 -0600
Subject: [PATCH 07/32] Eager load Projects

---
 ee/app/finders/sbom/dependencies_finder.rb | 2 +-
 ee/app/models/sbom/occurrence.rb           | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/ee/app/finders/sbom/dependencies_finder.rb b/ee/app/finders/sbom/dependencies_finder.rb
index c16d251ac2c6e9..648da8288f9c70 100644
--- a/ee/app/finders/sbom/dependencies_finder.rb
+++ b/ee/app/finders/sbom/dependencies_finder.rb
@@ -33,7 +33,7 @@ def filtered_collection
 
       collection = filter_by_component_names(collection) if params[:component_names].present?
 
-      collection.with_component.with_version.with_source
+      collection.with_component.with_version.with_source.with_project
     end
 
     def filter_by_package_managers(sbom_occurrences)
diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index b866e22b22fff4..a2266ad2cfdf60 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -57,6 +57,7 @@ class Occurrence < ApplicationRecord
     end
 
     scope :with_component, -> { includes(:component) }
+    scope :with_project, -> { includes(:project) }
     scope :with_source, -> { includes(:source) }
     scope :with_version, -> { includes(:component_version) }
     scope :with_component_source_version_project_and_pipeline, -> do
-- 
GitLab


From 1ff09f569d71f2a7bf87b38505ddc8758f8dea53 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 6 Sep 2023 16:08:15 -0600
Subject: [PATCH 08/32] Permit the pagination parameters

---
 ee/app/controllers/groups/dependencies_controller.rb | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/ee/app/controllers/groups/dependencies_controller.rb b/ee/app/controllers/groups/dependencies_controller.rb
index 2e7bd6650facf9..a91e8cfa0ca992 100644
--- a/ee/app/controllers/groups/dependencies_controller.rb
+++ b/ee/app/controllers/groups/dependencies_controller.rb
@@ -50,6 +50,8 @@ def authorize_read_dependency_list!
 
     def dependencies_finder
       ::Sbom::DependenciesFinder.new(group, params: params.permit(
+        :page,
+        :per_page,
         :sort,
         :sort_by,
         component_names: [],
-- 
GitLab


From b17fed88bf8f7ab88c120fee5186b4049a3c2334 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 6 Sep 2023 16:13:22 -0600
Subject: [PATCH 09/32] rename query string parameter from spdx_identifier to
 license

---
 ee/app/finders/sbom/dependencies_finder.rb         |  2 +-
 ee/spec/finders/sbom/dependencies_finder_spec.rb   | 14 ++++++++++++--
 .../groups/dependencies_controller_spec.rb         |  8 ++++----
 3 files changed, 17 insertions(+), 7 deletions(-)

diff --git a/ee/app/finders/sbom/dependencies_finder.rb b/ee/app/finders/sbom/dependencies_finder.rb
index 648da8288f9c70..8313b75f3a1faa 100644
--- a/ee/app/finders/sbom/dependencies_finder.rb
+++ b/ee/app/finders/sbom/dependencies_finder.rb
@@ -15,7 +15,7 @@ def execute
         sbom_occurrences.order_by_component_name(sort_direction)
       when 'packager'
         sbom_occurrences.order_by_package_name(sort_direction)
-      when 'spdx_identifier'
+      when 'license'
         sbom_occurrences.order_by_spdx_identifier(sort_direction)
       else
         sbom_occurrences.order_by_id
diff --git a/ee/spec/finders/sbom/dependencies_finder_spec.rb b/ee/spec/finders/sbom/dependencies_finder_spec.rb
index d4510c53352c65..5b5d44635099a4 100644
--- a/ee/spec/finders/sbom/dependencies_finder_spec.rb
+++ b/ee/spec/finders/sbom/dependencies_finder_spec.rb
@@ -94,8 +94,8 @@
         end
       end
 
-      context 'when sorted asc by spdx_identifier' do
-        let_it_be(:params) { { sort: 'asc', sort_by: 'spdx_identifier' } }
+      context 'when sorted asc by license' do
+        let_it_be(:params) { { sort: 'asc', sort_by: 'license' } }
 
         it 'returns sorted results' do
           expect(dependencies[0].licenses[0]["spdx_identifier"]).to eq('Apache-2.0')
@@ -104,6 +104,16 @@
         end
       end
 
+      context 'when sorted desc by license' do
+        let_it_be(:params) { { sort: 'desc', sort_by: 'license' } }
+
+        it 'returns sorted results' do
+          expect(dependencies[0].licenses[0]["spdx_identifier"]).to eq('MPL-2.0')
+          expect(dependencies[1].licenses[0]["spdx_identifier"]).to eq('MIT')
+          expect(dependencies[2].licenses[0]["spdx_identifier"]).to eq('Apache-2.0')
+        end
+      end
+
       context 'when filtered by package name npm' do
         let_it_be(:params) do
           {
diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index d230bf59f7bd86..916b1bd50087dc 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -244,8 +244,8 @@
                 end
               end
 
-              context 'when sorted by spdx_identifier in ascending order' do
-                let(:params) { { group_id: group.to_param, sort_by: 'spdx_identifier', sort: 'asc' } }
+              context 'when sorted by license in ascending order' do
+                let(:params) { { group_id: group.to_param, sort_by: 'license', sort: 'asc' } }
 
                 it 'returns sorted list' do
                   subject
@@ -255,8 +255,8 @@
                 end
               end
 
-              context 'when sorted by spdx_identifier in descending order' do
-                let(:params) { { group_id: group.to_param, sort_by: 'spdx_identifier', sort: 'desc' } }
+              context 'when sorted by license in descending order' do
+                let(:params) { { group_id: group.to_param, sort_by: 'license', sort: 'desc' } }
 
                 it 'returns sorted list' do
                   subject
-- 
GitLab


From aca7671ef464e2c5debe267a100cac57f067ae3d Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Thu, 7 Sep 2023 11:27:07 -0600
Subject: [PATCH 10/32] Add spec to reproduce SELECT N+1 issue

---
 .../requests/groups/dependencies_controller_spec.rb  | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index 916b1bd50087dc..610c7d0e9bc72d 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -179,6 +179,18 @@
               expect(json_response).to eq(expected_response)
             end
 
+            it 'loads the data efficiently' do
+              queries = ActiveRecord::QueryRecorder.new do
+                subject
+              end
+
+              expect(queries.occurrences_starting_with('SELECT "projects"').values.sum).to eq(1)
+              expect(queries.occurrences_starting_with('SELECT "sbom_component_versions"').values.sum).to eq(1)
+              expect(queries.occurrences_starting_with('SELECT "sbom_components"').values.sum).to eq(1)
+              expect(queries.occurrences_starting_with('SELECT "sbom_sources"').values.sum).to eq(1)
+              expect(queries.occurrences_starting_with('SELECT sbom_occurrences').values.sum).to eq(1)
+            end
+
             it 'includes pagination headers in the response' do
               subject
 
-- 
GitLab


From e67ebc50dfc0a477ed54e66f5cd145b24f93d973 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Thu, 7 Sep 2023 14:13:07 -0600
Subject: [PATCH 11/32] Use helper to test for N+1

---
 .../groups/dependencies_controller_spec.rb     | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index 610c7d0e9bc72d..b212e957cc7345 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -179,16 +179,18 @@
               expect(json_response).to eq(expected_response)
             end
 
-            it 'loads the data efficiently' do
-              queries = ActiveRecord::QueryRecorder.new do
-                subject
+            it 'loads the data without N+1 queries' do
+              def render
+                get group_dependencies_path(group_id: group.full_path), params: params, as: :json
               end
 
-              expect(queries.occurrences_starting_with('SELECT "projects"').values.sum).to eq(1)
-              expect(queries.occurrences_starting_with('SELECT "sbom_component_versions"').values.sum).to eq(1)
-              expect(queries.occurrences_starting_with('SELECT "sbom_components"').values.sum).to eq(1)
-              expect(queries.occurrences_starting_with('SELECT "sbom_sources"').values.sum).to eq(1)
-              expect(queries.occurrences_starting_with('SELECT sbom_occurrences').values.sum).to eq(1)
+              control = ActiveRecord::QueryRecorder.new do
+                render
+              end
+
+              create(:sbom_occurrence, project: create(:project, group: group))
+
+              expect { render }.not_to exceed_query_limit(control)
             end
 
             it 'includes pagination headers in the response' do
-- 
GitLab


From 9bf8553516196d6729b90819e85d20c18f98810a Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Thu, 7 Sep 2023 14:54:35 -0600
Subject: [PATCH 12/32] Add index on sbom_occurrences.licenses

---
 ..._add_index_to_sbom_occurrences_licenses.rb | 23 +++++++++++++++++++
 db/schema_migrations/20230907204731           |  1 +
 db/structure.sql                              |  2 ++
 3 files changed, 26 insertions(+)
 create mode 100644 db/post_migrate/20230907204731_add_index_to_sbom_occurrences_licenses.rb
 create mode 100644 db/schema_migrations/20230907204731

diff --git a/db/post_migrate/20230907204731_add_index_to_sbom_occurrences_licenses.rb b/db/post_migrate/20230907204731_add_index_to_sbom_occurrences_licenses.rb
new file mode 100644
index 00000000000000..9ed60941c962cb
--- /dev/null
+++ b/db/post_migrate/20230907204731_add_index_to_sbom_occurrences_licenses.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class AddIndexToSbomOccurrencesLicenses < Gitlab::Database::Migration[2.1]
+  INDEX_NAME = "index_sbom_occurrences_on_licenses_spdx_identifier"
+
+  disable_ddl_transaction!
+
+  def up
+    return if index_exists_by_name?(:sbom_occurrences, INDEX_NAME)
+
+    disable_statement_timeout do
+      execute <<~SQL
+      CREATE INDEX CONCURRENTLY #{INDEX_NAME}
+      ON sbom_occurrences
+      USING BTREE (project_id, (licenses#>'{0,spdx_identifier}'), (licenses#>'{1,spdx_identifier}'))
+      SQL
+    end
+  end
+
+  def down
+    remove_concurrent_index_by_name :sbom_occurrences, INDEX_NAME
+  end
+end
diff --git a/db/schema_migrations/20230907204731 b/db/schema_migrations/20230907204731
new file mode 100644
index 00000000000000..9b10980eae00fb
--- /dev/null
+++ b/db/schema_migrations/20230907204731
@@ -0,0 +1 @@
+e5448d02414f99074a52337c271310277a1d76b386a9e2e1bd3c1b806a70c462
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index d89580e0a432e6..e9ce4f63cfcaa4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -33610,6 +33610,8 @@ CREATE INDEX index_sbom_occurrences_on_component_id_and_id ON sbom_occurrences U
 
 CREATE INDEX index_sbom_occurrences_on_component_version_id ON sbom_occurrences USING btree (component_version_id);
 
+CREATE INDEX index_sbom_occurrences_on_licenses_spdx_identifier ON sbom_occurrences USING btree (project_id, ((licenses #> '{0,spdx_identifier}'::text[])), ((licenses #> '{1,spdx_identifier}'::text[])));
+
 CREATE INDEX index_sbom_occurrences_on_pipeline_id ON sbom_occurrences USING btree (pipeline_id);
 
 CREATE INDEX index_sbom_occurrences_on_project_id ON sbom_occurrences USING btree (project_id);
-- 
GitLab


From da0b4604ad644ae6c445b6233b4518219f0d0b27 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Thu, 7 Sep 2023 16:15:29 -0600
Subject: [PATCH 13/32] Add spec to optimize Group#sbom_occurrences query

---
 ee/spec/factories/sbom/components.rb | 15 ++++++
 ee/spec/models/ee/group_spec.rb      | 74 ++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+)

diff --git a/ee/spec/factories/sbom/components.rb b/ee/spec/factories/sbom/components.rb
index d1cf8fc456cb42..8ae125b514ba04 100644
--- a/ee/spec/factories/sbom/components.rb
+++ b/ee/spec/factories/sbom/components.rb
@@ -6,5 +6,20 @@
     purl_type { :npm }
 
     sequence(:name) { |n| "component-#{n}" }
+
+    trait :bundler do
+      name { "bundler" }
+      purl_type { :gem }
+    end
+
+    trait :caddy do
+      name { "caddy" }
+      purl_type { :golang }
+    end
+
+    trait :webpack do
+      name { "webpack" }
+      purl_type { :npm }
+    end
   end
 end
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 517b68ff0a8628..f17c2ba71c64fc 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2878,6 +2878,80 @@ def webhook_headers
         end
       end
     end
+
+    context 'with multiple sub groups' do
+      let!(:group_a) { create(:group, parent: group) }
+      let!(:group_a_a) { create(:group, parent: group_a) }
+      let!(:group_a_b) { create(:group, parent: group_a) }
+
+      let!(:group_b) { create(:group, parent: group) }
+      let!(:group_b_a) { create(:group, parent: group_b) }
+
+      let!(:group_c) { create(:group, parent: group) }
+      let!(:group_c_a) { create(:group, parent: group_c) }
+      let!(:group_c_b) { create(:group, parent: group_c) }
+
+      context 'with multiple projects' do
+        let!(:project_a) { create(:project, namespace: group_a) }
+        let!(:project_b_a) { create(:project, namespace: group_b_a) }
+        let!(:project_c) { create(:project, namespace: group_c) }
+        let!(:project_c_b) { create(:project, namespace: group_c_b) }
+
+        context 'with multiple versions of the same component' do
+          let!(:bundler) { create(:sbom_component, :bundler) }
+          let!(:bundler_version_1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
+          let!(:bundler_version_2) { create(:sbom_component_version, component: bundler, version: "2.0.0") }
+          let!(:bundler_v1) { create(:sbom_occurrence, project: project_a, component: bundler, component_version: bundler_version_1) }
+          let!(:bundler_v2) { create(:sbom_occurrence, project: project_b_a, component: bundler, component_version: bundler_version_2) }
+
+          let!(:webpack) { create(:sbom_component, :webpack) }
+          let!(:webpack_version_4) { create(:sbom_component_version, component: webpack, version: "4.46.0") }
+          let!(:webpack_v4) { create(:sbom_occurrence, project: project_c, component: webpack, component_version: webpack_version_4) }
+          let!(:webpack_unknown) { create(:sbom_occurrence, project: project_c_b, component: webpack, component_version: nil) }
+
+          let!(:caddy) { create(:sbom_component, :caddy) }
+          let!(:caddy_version_1) { create(:sbom_component_version, component: caddy, version: "1.0.0") }
+          let!(:caddy_version_2) { create(:sbom_component_version, component: caddy, version: "2.7.4") }
+          let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_1) }
+          let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_2) }
+
+          it 'returns an occurrence for each version of each component' do
+            expect(subject).to match_array([
+              bundler_v1,
+              bundler_v2,
+              caddy_v1,
+              caddy_v2,
+              webpack_v4,
+              webpack_unknown
+            ])
+          end
+
+          # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
+          pending 'returns the project count for each version of each component' do
+            expect(subject.pluck(:component_name, :component_version_id, :project_count)).to match_array([
+              [bundler.name, bundler_version_1.id, 1],
+              [bundler.name, bundler_version_2.id, 1],
+              [caddy.name, caddy_version_1.id, 1],
+              [caddy.name, caddy_version_2.id, 1],
+              [webpack.name, webpack_version_4.id, 1],
+              [webpack.name, nil, 2]
+            ])
+          end
+
+          # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
+          pending 'returns the occurrence count for each version of each component' do
+            expect(subject.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([
+              [bundler.name, bundler_version_1.id, 1],
+              [bundler.name, bundler_version_2.id, 1],
+              [caddy.name, caddy_version_1.id, 1],
+              [caddy.name, caddy_version_2.id, 1],
+              [webpack.name, webpack_version_4.id, 1],
+              [webpack.name, nil, 2]
+            ])
+          end
+        end
+      end
+    end
   end
 
   describe '#reached_project_access_token_limit?' do
-- 
GitLab


From 06a0de6db19a28c073566734bdc63ea35551f85e Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Fri, 8 Sep 2023 10:42:23 -0600
Subject: [PATCH 14/32] Use Gitlab::Pagination::Keyset::Order to build ordering
 syntax

---
 ee/app/models/sbom/occurrence.rb | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index a2266ad2cfdf60..a73f31c9b34bdb 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -34,9 +34,17 @@ class Occurrence < ApplicationRecord
     end
 
     scope :order_by_spdx_identifier, ->(direction, depth: 1) do
-      0.upto(depth).inject(self) do |relation, index|
-        relation.order(Arel.sql("licenses#>'{#{index},spdx_identifier}' #{direction}"))
-      end
+      order(Gitlab::Pagination::Keyset::Order.build(
+        0.upto(depth).map do |index|
+          sql = Arel.sql("(licenses#>'{#{index},spdx_identifier}')::text")
+          Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+            attribute_name: "spdx_identifier_#{index}",
+            order_expression: direction == "desc" ? sql.desc : sql.asc,
+            distinct: false,
+            sql_type: 'text'
+          )
+        end
+      ))
     end
 
     scope :filter_by_package_managers, ->(package_managers) do
-- 
GitLab


From 3ac589d17782d8af157bba5021c6924d51c1f318 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Fri, 8 Sep 2023 11:32:40 -0600
Subject: [PATCH 15/32] Use CTE to optimize the Group#sbom_occurrences query

---
 ee/app/finders/sbom/dependencies_finder.rb |  2 +-
 ee/app/models/ee/group.rb                  | 29 ++++------------------
 ee/app/models/sbom/occurrence.rb           | 16 ++++++++++++
 ee/spec/models/ee/group_spec.rb            |  8 +++---
 4 files changed, 26 insertions(+), 29 deletions(-)

diff --git a/ee/app/finders/sbom/dependencies_finder.rb b/ee/app/finders/sbom/dependencies_finder.rb
index 8313b75f3a1faa..d64fde54ea8fe4 100644
--- a/ee/app/finders/sbom/dependencies_finder.rb
+++ b/ee/app/finders/sbom/dependencies_finder.rb
@@ -27,7 +27,7 @@ def execute
     attr_reader :project_or_group, :params
 
     def filtered_collection
-      collection = project_or_group.sbom_occurrences
+      collection = project_or_group.sbom_occurrences.with_totals
 
       collection = filter_by_package_managers(collection) if params[:package_managers].present?
 
diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index 5b69438637656c..f8f373affd689e 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -840,30 +840,11 @@ def code_owner_approval_required_available?
     end
 
     def sbom_occurrences
-      Sbom::Occurrence.includes(project: :route).select('sbom_occurrences.*, agg_occurrences.occurrence_count, agg_occurrences.project_count')
-      .from('sbom_occurrences')
-      .joins(
-        <<-SQL
-          INNER JOIN (
-            SELECT component_id,
-                  COUNT(DISTINCT id) AS occurrence_count,
-                  COUNT(DISTINCT project_id) AS project_count
-            FROM sbom_occurrences
-            WHERE project_id IN (
-              SELECT "projects"."id" FROM "projects"
-              WHERE "projects"."namespace_id" IN (
-                SELECT namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id
-                FROM "namespaces"
-                WHERE "namespaces"."type" = 'Group'
-                AND (traversal_ids @> (\'{#{id}}\'))
-              )
-            )
-            GROUP BY component_id
-          ) agg_occurrences ON sbom_occurrences.component_id = agg_occurrences.component_id
-        SQL
-      )
-      .where(project_id: all_projects.select(:id))
-      .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
+      ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
+        scope: Sbom::Occurrence.order_by_id,
+        array_scope: self.all_projects.select(:id),
+        array_mapping_scope: -> (project_id) { Sbom::Occurrence.where(Sbom::Occurrence.arel_table[:project_id].eq(project_id)) }
+      ).execute.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
     end
 
     override :reached_project_access_token_limit?
diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index a73f31c9b34bdb..1c510150b554d7 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -73,6 +73,22 @@ class Occurrence < ApplicationRecord
     end
     scope :filter_by_non_nil_component_version, -> { where.not(component_version: nil) }
 
+    scope :with_totals, -> do
+      select('sbom_occurrences.*, totals.*')
+        .from('sbom_occurrences')
+        .joins(
+          <<-SQL
+            INNER JOIN (
+              SELECT component_id,
+                    COUNT(DISTINCT id) AS occurrence_count,
+                    COUNT(DISTINCT project_id) AS project_count
+              FROM sbom_occurrences
+              GROUP BY component_id
+            ) totals ON sbom_occurrences.component_id = totals.component_id
+          SQL
+        )
+    end
+
     def location
       {
         blob_path: input_file_blob_path,
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index f17c2ba71c64fc..53ecb40bb8bf13 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2873,8 +2873,8 @@ def webhook_headers
 
         it 'returns occurrences with aggregated ids' do
           expect(subject).to eq([sbom_occurrence])
-          expect(subject.pluck(:project_count)).to eq([1])
-          expect(subject.pluck(:occurrence_count)).to eq([1])
+          expect(subject.with_totals.pluck(:project_count)).to eq([1])
+          expect(subject.with_totals.pluck(:occurrence_count)).to eq([1])
         end
       end
     end
@@ -2928,7 +2928,7 @@ def webhook_headers
 
           # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
           pending 'returns the project count for each version of each component' do
-            expect(subject.pluck(:component_name, :component_version_id, :project_count)).to match_array([
+            expect(subject.with_totals.pluck(:component_name, :component_version_id, :project_count)).to match_array([
               [bundler.name, bundler_version_1.id, 1],
               [bundler.name, bundler_version_2.id, 1],
               [caddy.name, caddy_version_1.id, 1],
@@ -2940,7 +2940,7 @@ def webhook_headers
 
           # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
           pending 'returns the occurrence count for each version of each component' do
-            expect(subject.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([
+            expect(subject.with_totals.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([
               [bundler.name, bundler_version_1.id, 1],
               [bundler.name, bundler_version_2.id, 1],
               [caddy.name, caddy_version_1.id, 1],
-- 
GitLab


From a38b3e5b7da425afa310dade596d1aab21b03a13 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Fri, 8 Sep 2023 15:34:19 -0600
Subject: [PATCH 16/32] Group by component_version_id

---
 ee/app/finders/sbom/dependencies_finder.rb |  4 +-
 ee/app/models/ee/group.rb                  |  7 ++-
 ee/app/models/sbom/occurrence.rb           | 24 ++++----
 ee/spec/models/ee/group_spec.rb            | 24 --------
 ee/spec/models/sbom/occurrence_spec.rb     | 65 ++++++++++++++++++++++
 5 files changed, 83 insertions(+), 41 deletions(-)

diff --git a/ee/app/finders/sbom/dependencies_finder.rb b/ee/app/finders/sbom/dependencies_finder.rb
index d64fde54ea8fe4..03015ecc03870a 100644
--- a/ee/app/finders/sbom/dependencies_finder.rb
+++ b/ee/app/finders/sbom/dependencies_finder.rb
@@ -27,13 +27,13 @@ def execute
     attr_reader :project_or_group, :params
 
     def filtered_collection
-      collection = project_or_group.sbom_occurrences.with_totals
+      collection = project_or_group.sbom_occurrences
 
       collection = filter_by_package_managers(collection) if params[:package_managers].present?
 
       collection = filter_by_component_names(collection) if params[:component_names].present?
 
-      collection.with_component.with_version.with_source.with_project
+      collection.with_component.with_version.with_source.with_project.with_totals
     end
 
     def filter_by_package_managers(sbom_occurrences)
diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index f8f373affd689e..692ad6268cc3d4 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -840,11 +840,14 @@ def code_owner_approval_required_available?
     end
 
     def sbom_occurrences
-      ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
+      builder = ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
         scope: Sbom::Occurrence.order_by_id,
         array_scope: self.all_projects.select(:id),
         array_mapping_scope: -> (project_id) { Sbom::Occurrence.where(Sbom::Occurrence.arel_table[:project_id].eq(project_id)) }
-      ).execute.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
+      )
+      Sbom::Occurrence
+        .where(id: builder.execute)
+        .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
     end
 
     override :reached_project_access_token_limit?
diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index 1c510150b554d7..a0895b4077c839 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -74,19 +74,17 @@ class Occurrence < ApplicationRecord
     scope :filter_by_non_nil_component_version, -> { where.not(component_version: nil) }
 
     scope :with_totals, -> do
-      select('sbom_occurrences.*, totals.*')
-        .from('sbom_occurrences')
-        .joins(
-          <<-SQL
-            INNER JOIN (
-              SELECT component_id,
-                    COUNT(DISTINCT id) AS occurrence_count,
-                    COUNT(DISTINCT project_id) AS project_count
-              FROM sbom_occurrences
-              GROUP BY component_id
-            ) totals ON sbom_occurrences.component_id = totals.component_id
-          SQL
-        )
+      joins(
+        <<-SQL
+        INNER JOIN (
+          SELECT s.component_version_id,
+            COUNT(DISTINCT id) AS occurrence_count,
+            COUNT(DISTINCT project_id) AS project_count
+          FROM sbom_occurrences s
+          GROUP BY s.component_version_id
+        ) totals ON sbom_occurrences.component_version_id = totals.component_version_id
+        SQL
+      ).select('sbom_occurrences.*, totals.*')
     end
 
     def location
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 53ecb40bb8bf13..45bfff7291cc7b 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2925,30 +2925,6 @@ def webhook_headers
               webpack_unknown
             ])
           end
-
-          # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
-          pending 'returns the project count for each version of each component' do
-            expect(subject.with_totals.pluck(:component_name, :component_version_id, :project_count)).to match_array([
-              [bundler.name, bundler_version_1.id, 1],
-              [bundler.name, bundler_version_2.id, 1],
-              [caddy.name, caddy_version_1.id, 1],
-              [caddy.name, caddy_version_2.id, 1],
-              [webpack.name, webpack_version_4.id, 1],
-              [webpack.name, nil, 2]
-            ])
-          end
-
-          # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
-          pending 'returns the occurrence count for each version of each component' do
-            expect(subject.with_totals.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([
-              [bundler.name, bundler_version_1.id, 1],
-              [bundler.name, bundler_version_2.id, 1],
-              [caddy.name, caddy_version_1.id, 1],
-              [caddy.name, caddy_version_2.id, 1],
-              [webpack.name, webpack_version_4.id, 1],
-              [webpack.name, nil, 2]
-            ])
-          end
         end
       end
     end
diff --git a/ee/spec/models/sbom/occurrence_spec.rb b/ee/spec/models/sbom/occurrence_spec.rb
index fb56e3f950fb83..06879f44a62822 100644
--- a/ee/spec/models/sbom/occurrence_spec.rb
+++ b/ee/spec/models/sbom/occurrence_spec.rb
@@ -318,6 +318,71 @@
     end
   end
 
+  describe ".with_totals" do
+    let_it_be(:project_a) { create(:project) }
+    let_it_be(:project_b) { create(:project) }
+    let_it_be(:project_c) { create(:project) }
+    let_it_be(:project_d) { create(:project) }
+
+    let_it_be(:bundler) { create(:sbom_component, :bundler) }
+    let_it_be(:bundler_version_1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
+    let_it_be(:bundler_version_2) { create(:sbom_component_version, component: bundler, version: "2.0.0") }
+    let_it_be(:bundler_v1) do
+      create(:sbom_occurrence, project: project_a, component: bundler, component_version: bundler_version_1)
+    end
+
+    let_it_be(:bundler_v2) do
+      create(:sbom_occurrence, project: project_b, component: bundler, component_version: bundler_version_2)
+    end
+
+    let_it_be(:webpack) { create(:sbom_component, :webpack) }
+    let_it_be(:webpack_version_4) { create(:sbom_component_version, component: webpack, version: "4.46.0") }
+    let_it_be(:webpack_v4) do
+      create(:sbom_occurrence, project: project_c, component: webpack, component_version: webpack_version_4)
+    end
+
+    let_it_be(:webpack_unknown) do
+      create(:sbom_occurrence, project: project_d, component: webpack, component_version: nil)
+    end
+
+    let_it_be(:caddy) { create(:sbom_component, :caddy) }
+    let_it_be(:caddy_version_1) { create(:sbom_component_version, component: caddy, version: "1.0.0") }
+    let_it_be(:caddy_version_2) { create(:sbom_component_version, component: caddy, version: "2.7.4") }
+    let_it_be(:caddy_v1) do
+      create(:sbom_occurrence, project: project_d, component: caddy, component_version: caddy_version_1)
+    end
+
+    let_it_be(:caddy_v2) do
+      create(:sbom_occurrence, project: project_d, component: caddy, component_version: caddy_version_2)
+    end
+
+    subject do
+      described_class
+        .where(project_id: [project_a, project_c, project_d])
+        .with_totals
+    end
+
+    # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
+    it 'returns the project count for each version of each component' do
+      expect(subject.with_totals.pluck(:component_name, :component_version_id, :project_count)).to match_array([
+        [bundler.name, bundler_version_1.id, 1],
+        [caddy.name, caddy_version_1.id, 1],
+        [caddy.name, caddy_version_2.id, 1],
+        [webpack.name, webpack_version_4.id, 1]
+      ])
+    end
+
+    # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
+    it 'returns the occurrence count for each version of each component' do
+      expect(subject.with_totals.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([
+        [bundler.name, bundler_version_1.id, 1],
+        [caddy.name, caddy_version_1.id, 1],
+        [caddy.name, caddy_version_2.id, 1],
+        [webpack.name, webpack_version_4.id, 1]
+      ])
+    end
+  end
+
   describe '#name' do
     let(:component) { build(:sbom_component, name: 'rails') }
     let(:occurrence) { build(:sbom_occurrence, component: component) }
-- 
GitLab


From 1c67eb0d1b89b8c776cc2ab017dbc4d0372d294b Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Sat, 9 Sep 2023 10:55:12 -0600
Subject: [PATCH 17/32] Generate sbom occurrences query with a recursive cte

---
 ee/app/models/ee/group.rb       | 15 +++++++++++++++
 ee/spec/models/ee/group_spec.rb | 16 ++++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index 692ad6268cc3d4..dae8a1d2211ce0 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -839,6 +839,21 @@ def code_owner_approval_required_available?
       feature_available?(:code_owner_approval_required)
     end
 
+    def sbom_occurrences_cte
+      cte = ::Gitlab::SQL::RecursiveCTE.new(:our_projects)
+      table = Arel::Table.new(:sbom_occurrences)
+
+      cte << Sbom::Occurrence
+        .where(table[:project_id].in(all_projects.select(:id).arel))
+      cte << Sbom::Occurrence
+        .from([table, cte.table])
+        .where(table[:project_id].eq(cte.table[:id]))
+
+      Sbom::Occurrence.with
+        .recursive(cte.to_arel)
+        .from(cte.alias_to(table))
+    end
+
     def sbom_occurrences
       builder = ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
         scope: Sbom::Occurrence.order_by_id,
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 45bfff7291cc7b..7c8a31d683fe43 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2915,7 +2915,23 @@ def webhook_headers
           let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_1) }
           let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_2) }
 
+          it 'uses a cte' do
+            relation = group.sbom_occurrences_cte
+            puts relation.to_sql
+
+            expect(relation).to match_array([
+              bundler_v1,
+              bundler_v2,
+              caddy_v1,
+              caddy_v2,
+              webpack_v4,
+              webpack_unknown
+            ])
+          end
+
           it 'returns an occurrence for each version of each component' do
+            puts subject.to_sql
+
             expect(subject).to match_array([
               bundler_v1,
               bundler_v2,
-- 
GitLab


From e77e8217217bbaf9e33e75cf867f303d7d0cc6e4 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Sat, 9 Sep 2023 12:39:43 -0600
Subject: [PATCH 18/32] Create a CTE for projects and occurrences

---
 ee/app/models/ee/group.rb       | 24 ++++++++++++++++++++++--
 ee/spec/models/ee/group_spec.rb | 28 ++++++++++++++++++++++++++++
 2 files changed, 50 insertions(+), 2 deletions(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index dae8a1d2211ce0..8dec5db8de22ce 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -839,8 +839,28 @@ def code_owner_approval_required_available?
       feature_available?(:code_owner_approval_required)
     end
 
+    def our_projects_cte
+      cte = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
+
+      Sbom::Occurrence
+        .with(cte.to_arel)
+        .where(Sbom::Occurrence.arel_table[:project_id].in(cte.table.project(cte.table[:id])))
+    end
+
     def sbom_occurrences_cte
-      cte = ::Gitlab::SQL::RecursiveCTE.new(:our_projects)
+      table = Arel::Table.new(:sbom_occurrences)
+      cte = ::Gitlab::SQL::CTE.new(
+        :our_occurrences,
+        Sbom::Occurrence.where(table[:project_id].in(all_projects.select(:id).arel))
+      )
+
+      Sbom::Occurrence
+        .with(cte.to_arel)
+        .from(cte.alias_to(table))
+    end
+
+    def sbom_occurrences_recursive_cte
+      cte = ::Gitlab::SQL::RecursiveCTE.new(:our_occurrences)
       table = Arel::Table.new(:sbom_occurrences)
 
       cte << Sbom::Occurrence
@@ -857,7 +877,7 @@ def sbom_occurrences_cte
     def sbom_occurrences
       builder = ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
         scope: Sbom::Occurrence.order_by_id,
-        array_scope: self.all_projects.select(:id),
+        array_scope: all_projects.select(:id),
         array_mapping_scope: -> (project_id) { Sbom::Occurrence.where(Sbom::Occurrence.arel_table[:project_id].eq(project_id)) }
       )
       Sbom::Occurrence
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 7c8a31d683fe43..092924788de572 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2915,6 +2915,20 @@ def webhook_headers
           let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_1) }
           let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_2) }
 
+          it 'uses a projects cte' do
+            relation = group.our_projects_cte
+            puts relation.to_sql
+
+            expect(relation).to match_array([
+              bundler_v1,
+              bundler_v2,
+              caddy_v1,
+              caddy_v2,
+              webpack_v4,
+              webpack_unknown
+            ])
+          end
+
           it 'uses a cte' do
             relation = group.sbom_occurrences_cte
             puts relation.to_sql
@@ -2929,6 +2943,20 @@ def webhook_headers
             ])
           end
 
+          it 'uses a recursive cte' do
+            relation = group.sbom_occurrences_recursive_cte
+            puts relation.to_sql
+
+            expect(relation).to match_array([
+              bundler_v1,
+              bundler_v2,
+              caddy_v1,
+              caddy_v2,
+              webpack_v4,
+              webpack_unknown
+            ])
+          end
+
           it 'returns an occurrence for each version of each component' do
             puts subject.to_sql
 
-- 
GitLab


From 4a2f421bf6fe941f370f400f740ff339cce473d4 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Sat, 9 Sep 2023 12:49:35 -0600
Subject: [PATCH 19/32] Combine CTEs in query

---
 ee/app/models/ee/group.rb       | 30 ++++++++++--------------------
 ee/spec/models/ee/group_spec.rb | 14 --------------
 2 files changed, 10 insertions(+), 34 deletions(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index 8dec5db8de22ce..a79454be3c4f80 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -848,30 +848,20 @@ def our_projects_cte
     end
 
     def sbom_occurrences_cte
-      table = Arel::Table.new(:sbom_occurrences)
-      cte = ::Gitlab::SQL::CTE.new(
+      table = Sbom::Occurrence.arel_table
+      our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
+      our_occurrences = ::Gitlab::SQL::CTE.new(
         :our_occurrences,
-        Sbom::Occurrence.where(table[:project_id].in(all_projects.select(:id).arel))
+        Sbom::Occurrence
+          .with(our_projects.to_arel)
+          .where(
+            table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
+          )
       )
 
       Sbom::Occurrence
-        .with(cte.to_arel)
-        .from(cte.alias_to(table))
-    end
-
-    def sbom_occurrences_recursive_cte
-      cte = ::Gitlab::SQL::RecursiveCTE.new(:our_occurrences)
-      table = Arel::Table.new(:sbom_occurrences)
-
-      cte << Sbom::Occurrence
-        .where(table[:project_id].in(all_projects.select(:id).arel))
-      cte << Sbom::Occurrence
-        .from([table, cte.table])
-        .where(table[:project_id].eq(cte.table[:id]))
-
-      Sbom::Occurrence.with
-        .recursive(cte.to_arel)
-        .from(cte.alias_to(table))
+        .with(our_occurrences.to_arel)
+        .from(our_occurrences.alias_to(table))
     end
 
     def sbom_occurrences
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 092924788de572..60e2d008d9cc0f 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2943,20 +2943,6 @@ def webhook_headers
             ])
           end
 
-          it 'uses a recursive cte' do
-            relation = group.sbom_occurrences_recursive_cte
-            puts relation.to_sql
-
-            expect(relation).to match_array([
-              bundler_v1,
-              bundler_v2,
-              caddy_v1,
-              caddy_v2,
-              webpack_v4,
-              webpack_unknown
-            ])
-          end
-
           it 'returns an occurrence for each version of each component' do
             puts subject.to_sql
 
-- 
GitLab


From 36f9ed3bcdb3d81f16e90a930d55ea171338bbae Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Sat, 9 Sep 2023 12:51:58 -0600
Subject: [PATCH 20/32] Remove Group#projects_cte method

---
 ee/app/models/ee/group.rb       |  8 --------
 ee/spec/models/ee/group_spec.rb | 14 --------------
 2 files changed, 22 deletions(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index a79454be3c4f80..6914e9678f65c0 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -839,14 +839,6 @@ def code_owner_approval_required_available?
       feature_available?(:code_owner_approval_required)
     end
 
-    def our_projects_cte
-      cte = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
-
-      Sbom::Occurrence
-        .with(cte.to_arel)
-        .where(Sbom::Occurrence.arel_table[:project_id].in(cte.table.project(cte.table[:id])))
-    end
-
     def sbom_occurrences_cte
       table = Sbom::Occurrence.arel_table
       our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 60e2d008d9cc0f..7c8a31d683fe43 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2915,20 +2915,6 @@ def webhook_headers
           let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_1) }
           let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_2) }
 
-          it 'uses a projects cte' do
-            relation = group.our_projects_cte
-            puts relation.to_sql
-
-            expect(relation).to match_array([
-              bundler_v1,
-              bundler_v2,
-              caddy_v1,
-              caddy_v2,
-              webpack_v4,
-              webpack_unknown
-            ])
-          end
-
           it 'uses a cte' do
             relation = group.sbom_occurrences_cte
             puts relation.to_sql
-- 
GitLab


From d985615071ef7c275c538100ab2109fdbbc41adf Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Sat, 9 Sep 2023 13:00:06 -0600
Subject: [PATCH 21/32] Combine methods and add option to switch based on
 algorithm

---
 ee/app/models/ee/group.rb       |  51 ++++++------
 ee/spec/models/ee/group_spec.rb | 138 ++++++++++++++------------------
 2 files changed, 88 insertions(+), 101 deletions(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index 6914e9678f65c0..49b23b49e1fa34 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -839,32 +839,33 @@ def code_owner_approval_required_available?
       feature_available?(:code_owner_approval_required)
     end
 
-    def sbom_occurrences_cte
-      table = Sbom::Occurrence.arel_table
-      our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
-      our_occurrences = ::Gitlab::SQL::CTE.new(
-        :our_occurrences,
-        Sbom::Occurrence
-          .with(our_projects.to_arel)
-          .where(
-            table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
-          )
-      )
+    def sbom_occurrences(algorithm: 'in-optimization')
+      if algorithm == 'cte'
+        our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
+        our_occurrences = ::Gitlab::SQL::CTE.new(
+          :our_occurrences,
+          Sbom::Occurrence
+            .with(our_projects.to_arel)
+            .where(
+              Sbom::Occurrence.arel_table[:project_id].in(
+                our_projects.table.project(our_projects.table[:id])
+              )
+            )
+        )
 
-      Sbom::Occurrence
-        .with(our_occurrences.to_arel)
-        .from(our_occurrences.alias_to(table))
-    end
-
-    def sbom_occurrences
-      builder = ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
-        scope: Sbom::Occurrence.order_by_id,
-        array_scope: all_projects.select(:id),
-        array_mapping_scope: -> (project_id) { Sbom::Occurrence.where(Sbom::Occurrence.arel_table[:project_id].eq(project_id)) }
-      )
-      Sbom::Occurrence
-        .where(id: builder.execute)
-        .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
+        Sbom::Occurrence
+          .with(our_occurrences.to_arel)
+          .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
+      else
+        builder = ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
+          scope: Sbom::Occurrence.order_by_id,
+          array_scope: all_projects.select(:id),
+          array_mapping_scope: -> (project_id) { Sbom::Occurrence.where(Sbom::Occurrence.arel_table[:project_id].eq(project_id)) }
+        )
+        Sbom::Occurrence
+          .where(id: builder.execute)
+          .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
+      end
     end
 
     override :reached_project_access_token_limit?
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 7c8a31d683fe43..079b7cd73ef692 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2858,88 +2858,74 @@ def webhook_headers
     end
   end
 
-  describe '#sbom_occurrences' do
-    subject { group.sbom_occurrences }
+  ['cte', 'in-optimization'].each do |algorithm|
+    describe "#sbom_occurrences (#{algorithm})" do
+      subject { group.sbom_occurrences(algorithm: algorithm) }
 
-    it { is_expected.to be_empty }
+      it { is_expected.to be_empty }
 
-    context 'with project' do
-      let!(:project) { create(:project, group: group) }
+      context 'with project' do
+        let!(:project) { create(:project, group: group) }
 
-      it { is_expected.to be_empty }
+        it { is_expected.to be_empty }
+
+        context 'with occurrences' do
+          let!(:sbom_occurrence) { create(:sbom_occurrence, project: project) }
 
-      context 'with occurrences' do
-        let!(:sbom_occurrence) { create(:sbom_occurrence, project: project) }
-
-        it 'returns occurrences with aggregated ids' do
-          expect(subject).to eq([sbom_occurrence])
-          expect(subject.with_totals.pluck(:project_count)).to eq([1])
-          expect(subject.with_totals.pluck(:occurrence_count)).to eq([1])
-        end
-      end
-    end
-
-    context 'with multiple sub groups' do
-      let!(:group_a) { create(:group, parent: group) }
-      let!(:group_a_a) { create(:group, parent: group_a) }
-      let!(:group_a_b) { create(:group, parent: group_a) }
-
-      let!(:group_b) { create(:group, parent: group) }
-      let!(:group_b_a) { create(:group, parent: group_b) }
-
-      let!(:group_c) { create(:group, parent: group) }
-      let!(:group_c_a) { create(:group, parent: group_c) }
-      let!(:group_c_b) { create(:group, parent: group_c) }
-
-      context 'with multiple projects' do
-        let!(:project_a) { create(:project, namespace: group_a) }
-        let!(:project_b_a) { create(:project, namespace: group_b_a) }
-        let!(:project_c) { create(:project, namespace: group_c) }
-        let!(:project_c_b) { create(:project, namespace: group_c_b) }
-
-        context 'with multiple versions of the same component' do
-          let!(:bundler) { create(:sbom_component, :bundler) }
-          let!(:bundler_version_1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
-          let!(:bundler_version_2) { create(:sbom_component_version, component: bundler, version: "2.0.0") }
-          let!(:bundler_v1) { create(:sbom_occurrence, project: project_a, component: bundler, component_version: bundler_version_1) }
-          let!(:bundler_v2) { create(:sbom_occurrence, project: project_b_a, component: bundler, component_version: bundler_version_2) }
-
-          let!(:webpack) { create(:sbom_component, :webpack) }
-          let!(:webpack_version_4) { create(:sbom_component_version, component: webpack, version: "4.46.0") }
-          let!(:webpack_v4) { create(:sbom_occurrence, project: project_c, component: webpack, component_version: webpack_version_4) }
-          let!(:webpack_unknown) { create(:sbom_occurrence, project: project_c_b, component: webpack, component_version: nil) }
-
-          let!(:caddy) { create(:sbom_component, :caddy) }
-          let!(:caddy_version_1) { create(:sbom_component_version, component: caddy, version: "1.0.0") }
-          let!(:caddy_version_2) { create(:sbom_component_version, component: caddy, version: "2.7.4") }
-          let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_1) }
-          let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_2) }
-
-          it 'uses a cte' do
-            relation = group.sbom_occurrences_cte
-            puts relation.to_sql
-
-            expect(relation).to match_array([
-              bundler_v1,
-              bundler_v2,
-              caddy_v1,
-              caddy_v2,
-              webpack_v4,
-              webpack_unknown
-            ])
+          it 'returns occurrences with aggregated ids' do
+            expect(subject).to eq([sbom_occurrence])
+            expect(subject.with_totals.pluck(:project_count)).to eq([1])
+            expect(subject.with_totals.pluck(:occurrence_count)).to eq([1])
           end
+        end
+      end
 
-          it 'returns an occurrence for each version of each component' do
-            puts subject.to_sql
-
-            expect(subject).to match_array([
-              bundler_v1,
-              bundler_v2,
-              caddy_v1,
-              caddy_v2,
-              webpack_v4,
-              webpack_unknown
-            ])
+      context 'with multiple sub groups' do
+        let!(:group_a) { create(:group, parent: group) }
+        let!(:group_a_a) { create(:group, parent: group_a) }
+        let!(:group_a_b) { create(:group, parent: group_a) }
+
+        let!(:group_b) { create(:group, parent: group) }
+        let!(:group_b_a) { create(:group, parent: group_b) }
+
+        let!(:group_c) { create(:group, parent: group) }
+        let!(:group_c_a) { create(:group, parent: group_c) }
+        let!(:group_c_b) { create(:group, parent: group_c) }
+
+        context 'with multiple projects' do
+          let!(:project_a) { create(:project, namespace: group_a) }
+          let!(:project_b_a) { create(:project, namespace: group_b_a) }
+          let!(:project_c) { create(:project, namespace: group_c) }
+          let!(:project_c_b) { create(:project, namespace: group_c_b) }
+
+          context 'with multiple versions of the same component' do
+            let!(:bundler) { create(:sbom_component, :bundler) }
+            let!(:bundler_version_1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
+            let!(:bundler_version_2) { create(:sbom_component_version, component: bundler, version: "2.0.0") }
+            let!(:bundler_v1) { create(:sbom_occurrence, project: project_a, component: bundler, component_version: bundler_version_1) }
+            let!(:bundler_v2) { create(:sbom_occurrence, project: project_b_a, component: bundler, component_version: bundler_version_2) }
+
+            let!(:webpack) { create(:sbom_component, :webpack) }
+            let!(:webpack_version_4) { create(:sbom_component_version, component: webpack, version: "4.46.0") }
+            let!(:webpack_v4) { create(:sbom_occurrence, project: project_c, component: webpack, component_version: webpack_version_4) }
+            let!(:webpack_unknown) { create(:sbom_occurrence, project: project_c_b, component: webpack, component_version: nil) }
+
+            let!(:caddy) { create(:sbom_component, :caddy) }
+            let!(:caddy_version_1) { create(:sbom_component_version, component: caddy, version: "1.0.0") }
+            let!(:caddy_version_2) { create(:sbom_component_version, component: caddy, version: "2.7.4") }
+            let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_1) }
+            let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_2) }
+
+            it 'returns an occurrence for each version of each component' do
+              expect(subject).to match_array([
+                bundler_v1,
+                bundler_v2,
+                caddy_v1,
+                caddy_v2,
+                webpack_v4,
+                webpack_unknown
+              ])
+            end
           end
         end
       end
-- 
GitLab


From b710f23d721e77f1071df595de01efc580e2e1a5 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Mon, 11 Sep 2023 13:57:50 -0600
Subject: [PATCH 22/32] Promote window function to top

---
 ee/app/models/ee/group.rb | 14 ++++----------
 1 file changed, 4 insertions(+), 10 deletions(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index 49b23b49e1fa34..cad8991a254a2a 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -842,18 +842,12 @@ def code_owner_approval_required_available?
     def sbom_occurrences(algorithm: 'in-optimization')
       if algorithm == 'cte'
         our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
-        our_occurrences = ::Gitlab::SQL::CTE.new(
-          :our_occurrences,
-          Sbom::Occurrence
-            .with(our_projects.to_arel)
-            .where(
-              Sbom::Occurrence.arel_table[:project_id].in(
-                our_projects.table.project(our_projects.table[:id])
-              )
-            )
-        )
+        our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, Sbom::Occurrence.where(
+          Sbom::Occurrence.arel_table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
+        ))
 
         Sbom::Occurrence
+          .with(our_projects.to_arel)
           .with(our_occurrences.to_arel)
           .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
       else
-- 
GitLab


From b60d4695ae955ed852a25a5610447b6f78c348ae Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Mon, 11 Sep 2023 14:37:27 -0600
Subject: [PATCH 23/32] Pass CTE into .with_totals scope

---
 ee/app/finders/sbom/dependencies_finder.rb |  2 +-
 ee/app/models/ee/group.rb                  | 18 ++++++++++------
 ee/app/models/sbom/occurrence.rb           | 10 ++++-----
 ee/spec/models/ee/group_spec.rb            | 24 ++++++++--------------
 ee/spec/models/sbom/occurrence_spec.rb     | 16 +++++++++++----
 5 files changed, 39 insertions(+), 31 deletions(-)

diff --git a/ee/app/finders/sbom/dependencies_finder.rb b/ee/app/finders/sbom/dependencies_finder.rb
index 03015ecc03870a..8313b75f3a1faa 100644
--- a/ee/app/finders/sbom/dependencies_finder.rb
+++ b/ee/app/finders/sbom/dependencies_finder.rb
@@ -33,7 +33,7 @@ def filtered_collection
 
       collection = filter_by_component_names(collection) if params[:component_names].present?
 
-      collection.with_component.with_version.with_source.with_project.with_totals
+      collection.with_component.with_version.with_source.with_project
     end
 
     def filter_by_package_managers(sbom_occurrences)
diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index cad8991a254a2a..c63b0fd105a9ae 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -839,17 +839,19 @@ def code_owner_approval_required_available?
       feature_available?(:code_owner_approval_required)
     end
 
-    def sbom_occurrences(algorithm: 'in-optimization')
-      if algorithm == 'cte'
-        our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
-        our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, Sbom::Occurrence.where(
-          Sbom::Occurrence.arel_table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
-        ))
+    def sbom_occurrences(algorithm: 'cte')
+      our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
+      our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, Sbom::Occurrence.where(
+        Sbom::Occurrence.arel_table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
+      ))
 
+      if algorithm == 'cte'
         Sbom::Occurrence
           .with(our_projects.to_arel)
           .with(our_occurrences.to_arel)
           .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
+          .with_totals(our_occurrences)
+          .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
       else
         builder = ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
           scope: Sbom::Occurrence.order_by_id,
@@ -857,7 +859,11 @@ def sbom_occurrences(algorithm: 'in-optimization')
           array_mapping_scope: -> (project_id) { Sbom::Occurrence.where(Sbom::Occurrence.arel_table[:project_id].eq(project_id)) }
         )
         Sbom::Occurrence
+          .with(our_projects.to_arel)
+          .with(our_occurrences.to_arel)
+          .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
           .where(id: builder.execute)
+          .with_totals(our_occurrences)
           .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
       end
     end
diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index a0895b4077c839..1addeac22f1d1b 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -73,16 +73,16 @@ class Occurrence < ApplicationRecord
     end
     scope :filter_by_non_nil_component_version, -> { where.not(component_version: nil) }
 
-    scope :with_totals, -> do
+    scope :with_totals, ->(sbom_occurrences_cte) do
       joins(
         <<-SQL
         INNER JOIN (
-          SELECT s.component_version_id,
+          SELECT s.component_version_id as _version_id,
             COUNT(DISTINCT id) AS occurrence_count,
             COUNT(DISTINCT project_id) AS project_count
-          FROM sbom_occurrences s
-          GROUP BY s.component_version_id
-        ) totals ON sbom_occurrences.component_version_id = totals.component_version_id
+          FROM #{sbom_occurrences_cte.table.name} s
+          GROUP BY _version_id
+        ) totals ON sbom_occurrences.component_version_id = totals._version_id
         SQL
       ).select('sbom_occurrences.*, totals.*')
     end
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 079b7cd73ef692..d3713db2c6c821 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2858,7 +2858,7 @@ def webhook_headers
     end
   end
 
-  ['cte', 'in-optimization'].each do |algorithm|
+  %w[cte in-optimization].each do |algorithm|
     describe "#sbom_occurrences (#{algorithm})" do
       subject { group.sbom_occurrences(algorithm: algorithm) }
 
@@ -2874,8 +2874,8 @@ def webhook_headers
 
           it 'returns occurrences with aggregated ids' do
             expect(subject).to eq([sbom_occurrence])
-            expect(subject.with_totals.pluck(:project_count)).to eq([1])
-            expect(subject.with_totals.pluck(:occurrence_count)).to eq([1])
+            expect(subject.pluck(:project_count)).to eq([1])
+            expect(subject.pluck(:occurrence_count)).to eq([1])
           end
         end
       end
@@ -2900,21 +2900,16 @@ def webhook_headers
 
           context 'with multiple versions of the same component' do
             let!(:bundler) { create(:sbom_component, :bundler) }
-            let!(:bundler_version_1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
-            let!(:bundler_version_2) { create(:sbom_component_version, component: bundler, version: "2.0.0") }
-            let!(:bundler_v1) { create(:sbom_occurrence, project: project_a, component: bundler, component_version: bundler_version_1) }
-            let!(:bundler_v2) { create(:sbom_occurrence, project: project_b_a, component: bundler, component_version: bundler_version_2) }
+            let!(:bundler_v1) { create(:sbom_occurrence, project: project_a, component: bundler, component_version: create(:sbom_component_version, component: bundler, version: "1.0.0")) }
+            let!(:bundler_v2) { create(:sbom_occurrence, project: project_b_a, component: bundler, component_version: create(:sbom_component_version, component: bundler, version: "2.0.0")) }
 
             let!(:webpack) { create(:sbom_component, :webpack) }
-            let!(:webpack_version_4) { create(:sbom_component_version, component: webpack, version: "4.46.0") }
-            let!(:webpack_v4) { create(:sbom_occurrence, project: project_c, component: webpack, component_version: webpack_version_4) }
+            let!(:webpack_v4) { create(:sbom_occurrence, project: project_c, component: webpack, component_version: create(:sbom_component_version, component: webpack, version: "4.46.0")) }
             let!(:webpack_unknown) { create(:sbom_occurrence, project: project_c_b, component: webpack, component_version: nil) }
 
             let!(:caddy) { create(:sbom_component, :caddy) }
-            let!(:caddy_version_1) { create(:sbom_component_version, component: caddy, version: "1.0.0") }
-            let!(:caddy_version_2) { create(:sbom_component_version, component: caddy, version: "2.7.4") }
-            let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_1) }
-            let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: caddy_version_2) }
+            let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: create(:sbom_component_version, component: caddy, version: "1.0.0")) }
+            let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: create(:sbom_component_version, component: caddy, version: "2.7.4")) }
 
             it 'returns an occurrence for each version of each component' do
               expect(subject).to match_array([
@@ -2922,8 +2917,7 @@ def webhook_headers
                 bundler_v2,
                 caddy_v1,
                 caddy_v2,
-                webpack_v4,
-                webpack_unknown
+                webpack_v4
               ])
             end
           end
diff --git a/ee/spec/models/sbom/occurrence_spec.rb b/ee/spec/models/sbom/occurrence_spec.rb
index 06879f44a62822..698c6b6655e72e 100644
--- a/ee/spec/models/sbom/occurrence_spec.rb
+++ b/ee/spec/models/sbom/occurrence_spec.rb
@@ -357,14 +357,22 @@
     end
 
     subject do
+      our_projects = ::Gitlab::SQL::CTE.new(:our_projects,
+        ::Project.where(id: [project_a, project_c, project_d]).select(:id))
+      our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, described_class.where(
+        described_class.arel_table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
+      ))
+
       described_class
-        .where(project_id: [project_a, project_c, project_d])
-        .with_totals
+        .with(our_projects.to_arel)
+        .with(our_occurrences.to_arel)
+        .from(our_occurrences.alias_to(described_class.arel_table))
+        .with_totals(our_occurrences)
     end
 
     # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
     it 'returns the project count for each version of each component' do
-      expect(subject.with_totals.pluck(:component_name, :component_version_id, :project_count)).to match_array([
+      expect(subject.pluck(:component_name, :component_version_id, :project_count)).to match_array([
         [bundler.name, bundler_version_1.id, 1],
         [caddy.name, caddy_version_1.id, 1],
         [caddy.name, caddy_version_2.id, 1],
@@ -374,7 +382,7 @@
 
     # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
     it 'returns the occurrence count for each version of each component' do
-      expect(subject.with_totals.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([
+      expect(subject.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([
         [bundler.name, bundler_version_1.id, 1],
         [caddy.name, caddy_version_1.id, 1],
         [caddy.name, caddy_version_2.id, 1],
-- 
GitLab


From c5db51198094db7eafefe346422b125ee0b0d600 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Mon, 11 Sep 2023 15:07:51 -0600
Subject: [PATCH 24/32] Remove InOperatorOptimization::QueryBuilder

---
 ee/app/models/ee/group.rb       | 29 +++--------
 ee/spec/models/ee/group_spec.rb | 92 ++++++++++++++++-----------------
 2 files changed, 52 insertions(+), 69 deletions(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index c63b0fd105a9ae..c2177508a2031d 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -839,33 +839,18 @@ def code_owner_approval_required_available?
       feature_available?(:code_owner_approval_required)
     end
 
-    def sbom_occurrences(algorithm: 'cte')
+    def sbom_occurrences
       our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
       our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, Sbom::Occurrence.where(
         Sbom::Occurrence.arel_table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
       ))
 
-      if algorithm == 'cte'
-        Sbom::Occurrence
-          .with(our_projects.to_arel)
-          .with(our_occurrences.to_arel)
-          .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
-          .with_totals(our_occurrences)
-          .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
-      else
-        builder = ::Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
-          scope: Sbom::Occurrence.order_by_id,
-          array_scope: all_projects.select(:id),
-          array_mapping_scope: -> (project_id) { Sbom::Occurrence.where(Sbom::Occurrence.arel_table[:project_id].eq(project_id)) }
-        )
-        Sbom::Occurrence
-          .with(our_projects.to_arel)
-          .with(our_occurrences.to_arel)
-          .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
-          .where(id: builder.execute)
-          .with_totals(our_occurrences)
-          .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
-      end
+      Sbom::Occurrence
+        .with(our_projects.to_arel)
+        .with(our_occurrences.to_arel)
+        .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
+        .with_totals(our_occurrences)
+        .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
     end
 
     override :reached_project_access_token_limit?
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index d3713db2c6c821..4f54bc763881f6 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2858,68 +2858,66 @@ def webhook_headers
     end
   end
 
-  %w[cte in-optimization].each do |algorithm|
-    describe "#sbom_occurrences (#{algorithm})" do
-      subject { group.sbom_occurrences(algorithm: algorithm) }
+  describe "#sbom_occurrences" do
+    subject { group.sbom_occurrences }
 
-      it { is_expected.to be_empty }
+    it { is_expected.to be_empty }
 
-      context 'with project' do
-        let!(:project) { create(:project, group: group) }
+    context 'with project' do
+      let!(:project) { create(:project, group: group) }
 
-        it { is_expected.to be_empty }
+      it { is_expected.to be_empty }
 
-        context 'with occurrences' do
-          let!(:sbom_occurrence) { create(:sbom_occurrence, project: project) }
+      context 'with occurrences' do
+        let!(:sbom_occurrence) { create(:sbom_occurrence, project: project) }
 
-          it 'returns occurrences with aggregated ids' do
-            expect(subject).to eq([sbom_occurrence])
-            expect(subject.pluck(:project_count)).to eq([1])
-            expect(subject.pluck(:occurrence_count)).to eq([1])
-          end
+        it 'returns occurrences with aggregated ids' do
+          expect(subject).to eq([sbom_occurrence])
+          expect(subject.pluck(:project_count)).to eq([1])
+          expect(subject.pluck(:occurrence_count)).to eq([1])
         end
       end
+    end
 
-      context 'with multiple sub groups' do
-        let!(:group_a) { create(:group, parent: group) }
-        let!(:group_a_a) { create(:group, parent: group_a) }
-        let!(:group_a_b) { create(:group, parent: group_a) }
+    context 'with multiple sub groups' do
+      let!(:group_a) { create(:group, parent: group) }
+      let!(:group_a_a) { create(:group, parent: group_a) }
+      let!(:group_a_b) { create(:group, parent: group_a) }
 
-        let!(:group_b) { create(:group, parent: group) }
-        let!(:group_b_a) { create(:group, parent: group_b) }
+      let!(:group_b) { create(:group, parent: group) }
+      let!(:group_b_a) { create(:group, parent: group_b) }
 
-        let!(:group_c) { create(:group, parent: group) }
-        let!(:group_c_a) { create(:group, parent: group_c) }
-        let!(:group_c_b) { create(:group, parent: group_c) }
+      let!(:group_c) { create(:group, parent: group) }
+      let!(:group_c_a) { create(:group, parent: group_c) }
+      let!(:group_c_b) { create(:group, parent: group_c) }
 
-        context 'with multiple projects' do
-          let!(:project_a) { create(:project, namespace: group_a) }
-          let!(:project_b_a) { create(:project, namespace: group_b_a) }
-          let!(:project_c) { create(:project, namespace: group_c) }
-          let!(:project_c_b) { create(:project, namespace: group_c_b) }
+      context 'with multiple projects' do
+        let!(:project_a) { create(:project, namespace: group_a) }
+        let!(:project_b_a) { create(:project, namespace: group_b_a) }
+        let!(:project_c) { create(:project, namespace: group_c) }
+        let!(:project_c_b) { create(:project, namespace: group_c_b) }
 
-          context 'with multiple versions of the same component' do
-            let!(:bundler) { create(:sbom_component, :bundler) }
-            let!(:bundler_v1) { create(:sbom_occurrence, project: project_a, component: bundler, component_version: create(:sbom_component_version, component: bundler, version: "1.0.0")) }
-            let!(:bundler_v2) { create(:sbom_occurrence, project: project_b_a, component: bundler, component_version: create(:sbom_component_version, component: bundler, version: "2.0.0")) }
+        context 'with multiple versions of the same component' do
+          let!(:bundler) { create(:sbom_component, :bundler) }
+          let!(:bundler_v1) { create(:sbom_occurrence, project: project_a, component: bundler, component_version: create(:sbom_component_version, component: bundler, version: "1.0.0")) }
+          let!(:bundler_v2) { create(:sbom_occurrence, project: project_b_a, component: bundler, component_version: create(:sbom_component_version, component: bundler, version: "2.0.0")) }
 
-            let!(:webpack) { create(:sbom_component, :webpack) }
-            let!(:webpack_v4) { create(:sbom_occurrence, project: project_c, component: webpack, component_version: create(:sbom_component_version, component: webpack, version: "4.46.0")) }
-            let!(:webpack_unknown) { create(:sbom_occurrence, project: project_c_b, component: webpack, component_version: nil) }
+          let!(:webpack) { create(:sbom_component, :webpack) }
+          let!(:webpack_v4) { create(:sbom_occurrence, project: project_c, component: webpack, component_version: create(:sbom_component_version, component: webpack, version: "4.46.0")) }
+          let!(:webpack_unknown) { create(:sbom_occurrence, project: project_c_b, component: webpack, component_version: nil) }
 
-            let!(:caddy) { create(:sbom_component, :caddy) }
-            let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: create(:sbom_component_version, component: caddy, version: "1.0.0")) }
-            let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: create(:sbom_component_version, component: caddy, version: "2.7.4")) }
+          let!(:caddy) { create(:sbom_component, :caddy) }
+          let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: create(:sbom_component_version, component: caddy, version: "1.0.0")) }
+          let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: create(:sbom_component_version, component: caddy, version: "2.7.4")) }
 
-            it 'returns an occurrence for each version of each component' do
-              expect(subject).to match_array([
-                bundler_v1,
-                bundler_v2,
-                caddy_v1,
-                caddy_v2,
-                webpack_v4
-              ])
-            end
+          it 'returns an occurrence for each version of each component' do
+            expect(subject).to match_array([
+              bundler_v1,
+              bundler_v2,
+              caddy_v1,
+              caddy_v2,
+              webpack_v4
+            ])
           end
         end
       end
-- 
GitLab


From 787b58ec1ff16bea67540e124cdd58a0cf9d23c6 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Mon, 11 Sep 2023 15:20:36 -0600
Subject: [PATCH 25/32] Include project routes

---
 ee/app/models/ee/group.rb                               | 1 +
 ee/spec/requests/groups/dependencies_controller_spec.rb | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index c2177508a2031d..b8e13f3c2a1086 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -846,6 +846,7 @@ def sbom_occurrences
       ))
 
       Sbom::Occurrence
+        .includes(project: :route)
         .with(our_projects.to_arel)
         .with(our_occurrences.to_arel)
         .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index b212e957cc7345..3700a1cde65e60 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -210,7 +210,7 @@ def render
               end
 
               expect(project_routes_count).to eq(1)
-              expect(project_count).to eq(1)
+              expect(project_count).to eq(2)
             end
 
             context 'with sorting params' do
-- 
GitLab


From 3bae60fea64d54a32c0f49f5ec63c8e8a5fe9f24 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Mon, 11 Sep 2023 15:52:37 -0600
Subject: [PATCH 26/32] Undo unneccessary change

---
 ee/spec/models/ee/group_spec.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 4f54bc763881f6..4b42ec0303e78e 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2858,7 +2858,7 @@ def webhook_headers
     end
   end
 
-  describe "#sbom_occurrences" do
+  describe '#sbom_occurrences' do
     subject { group.sbom_occurrences }
 
     it { is_expected.to be_empty }
-- 
GitLab


From e92852d8e5042b227a704353a1d41250eadad281 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Tue, 12 Sep 2023 10:45:10 -0600
Subject: [PATCH 27/32] Excluded soft deleted projects

---
 ee/app/models/ee/group.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index b8e13f3c2a1086..d8ce062d2ecee6 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -840,7 +840,7 @@ def code_owner_approval_required_available?
     end
 
     def sbom_occurrences
-      our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects.select(:id))
+      our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects_except_soft_deleted.select(:id))
       our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, Sbom::Occurrence.where(
         Sbom::Occurrence.arel_table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
       ))
-- 
GitLab


From 7d8e8526a8c4f7e8a3bd25abca75e3f9dd33f5eb Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Tue, 12 Sep 2023 15:48:35 -0600
Subject: [PATCH 28/32] Use a single non materialized CTE

---
 ee/app/models/ee/group.rb | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index d8ce062d2ecee6..2240dce60c8d64 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -840,14 +840,12 @@ def code_owner_approval_required_available?
     end
 
     def sbom_occurrences
-      our_projects = ::Gitlab::SQL::CTE.new(:our_projects, all_projects_except_soft_deleted.select(:id))
       our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, Sbom::Occurrence.where(
-        Sbom::Occurrence.arel_table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
-      ))
+        project_id: all_projects_except_soft_deleted.select(:id)
+      ), materialized: false)
 
       Sbom::Occurrence
         .includes(project: :route)
-        .with(our_projects.to_arel)
         .with(our_occurrences.to_arel)
         .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
         .with_totals(our_occurrences)
-- 
GitLab


From 3dcde825a724b14b7ad888aff1cb42a1ea3fee34 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 13 Sep 2023 15:29:07 -0600
Subject: [PATCH 29/32] Remove changes that are not related to sorting by
 license

---
 ee/app/finders/sbom/dependencies_finder.rb    |  2 +-
 ee/app/models/ee/group.rb                     | 34 ++++++---
 ee/app/models/sbom/occurrence.rb              | 15 ----
 ee/spec/factories/sbom/components.rb          | 15 ----
 ee/spec/models/ee/group_spec.rb               | 44 -----------
 ee/spec/models/sbom/occurrence_spec.rb        | 73 -------------------
 .../groups/dependencies_controller_spec.rb    | 16 +---
 7 files changed, 26 insertions(+), 173 deletions(-)

diff --git a/ee/app/finders/sbom/dependencies_finder.rb b/ee/app/finders/sbom/dependencies_finder.rb
index 8313b75f3a1faa..334e14a0be5adc 100644
--- a/ee/app/finders/sbom/dependencies_finder.rb
+++ b/ee/app/finders/sbom/dependencies_finder.rb
@@ -33,7 +33,7 @@ def filtered_collection
 
       collection = filter_by_component_names(collection) if params[:component_names].present?
 
-      collection.with_component.with_version.with_source.with_project
+      collection
     end
 
     def filter_by_package_managers(sbom_occurrences)
diff --git a/ee/app/models/ee/group.rb b/ee/app/models/ee/group.rb
index 2240dce60c8d64..5b69438637656c 100644
--- a/ee/app/models/ee/group.rb
+++ b/ee/app/models/ee/group.rb
@@ -840,16 +840,30 @@ def code_owner_approval_required_available?
     end
 
     def sbom_occurrences
-      our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, Sbom::Occurrence.where(
-        project_id: all_projects_except_soft_deleted.select(:id)
-      ), materialized: false)
-
-      Sbom::Occurrence
-        .includes(project: :route)
-        .with(our_occurrences.to_arel)
-        .from(our_occurrences.alias_to(Sbom::Occurrence.arel_table))
-        .with_totals(our_occurrences)
-        .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
+      Sbom::Occurrence.includes(project: :route).select('sbom_occurrences.*, agg_occurrences.occurrence_count, agg_occurrences.project_count')
+      .from('sbom_occurrences')
+      .joins(
+        <<-SQL
+          INNER JOIN (
+            SELECT component_id,
+                  COUNT(DISTINCT id) AS occurrence_count,
+                  COUNT(DISTINCT project_id) AS project_count
+            FROM sbom_occurrences
+            WHERE project_id IN (
+              SELECT "projects"."id" FROM "projects"
+              WHERE "projects"."namespace_id" IN (
+                SELECT namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id
+                FROM "namespaces"
+                WHERE "namespaces"."type" = 'Group'
+                AND (traversal_ids @> (\'{#{id}}\'))
+              )
+            )
+            GROUP BY component_id
+          ) agg_occurrences ON sbom_occurrences.component_id = agg_occurrences.component_id
+        SQL
+      )
+      .where(project_id: all_projects.select(:id))
+      .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
     end
 
     override :reached_project_access_token_limit?
diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb
index 1addeac22f1d1b..38be2d6e1c8398 100644
--- a/ee/app/models/sbom/occurrence.rb
+++ b/ee/app/models/sbom/occurrence.rb
@@ -65,7 +65,6 @@ class Occurrence < ApplicationRecord
     end
 
     scope :with_component, -> { includes(:component) }
-    scope :with_project, -> { includes(:project) }
     scope :with_source, -> { includes(:source) }
     scope :with_version, -> { includes(:component_version) }
     scope :with_component_source_version_project_and_pipeline, -> do
@@ -73,20 +72,6 @@ class Occurrence < ApplicationRecord
     end
     scope :filter_by_non_nil_component_version, -> { where.not(component_version: nil) }
 
-    scope :with_totals, ->(sbom_occurrences_cte) do
-      joins(
-        <<-SQL
-        INNER JOIN (
-          SELECT s.component_version_id as _version_id,
-            COUNT(DISTINCT id) AS occurrence_count,
-            COUNT(DISTINCT project_id) AS project_count
-          FROM #{sbom_occurrences_cte.table.name} s
-          GROUP BY _version_id
-        ) totals ON sbom_occurrences.component_version_id = totals._version_id
-        SQL
-      ).select('sbom_occurrences.*, totals.*')
-    end
-
     def location
       {
         blob_path: input_file_blob_path,
diff --git a/ee/spec/factories/sbom/components.rb b/ee/spec/factories/sbom/components.rb
index 8ae125b514ba04..d1cf8fc456cb42 100644
--- a/ee/spec/factories/sbom/components.rb
+++ b/ee/spec/factories/sbom/components.rb
@@ -6,20 +6,5 @@
     purl_type { :npm }
 
     sequence(:name) { |n| "component-#{n}" }
-
-    trait :bundler do
-      name { "bundler" }
-      purl_type { :gem }
-    end
-
-    trait :caddy do
-      name { "caddy" }
-      purl_type { :golang }
-    end
-
-    trait :webpack do
-      name { "webpack" }
-      purl_type { :npm }
-    end
   end
 end
diff --git a/ee/spec/models/ee/group_spec.rb b/ee/spec/models/ee/group_spec.rb
index 4b42ec0303e78e..517b68ff0a8628 100644
--- a/ee/spec/models/ee/group_spec.rb
+++ b/ee/spec/models/ee/group_spec.rb
@@ -2878,50 +2878,6 @@ def webhook_headers
         end
       end
     end
-
-    context 'with multiple sub groups' do
-      let!(:group_a) { create(:group, parent: group) }
-      let!(:group_a_a) { create(:group, parent: group_a) }
-      let!(:group_a_b) { create(:group, parent: group_a) }
-
-      let!(:group_b) { create(:group, parent: group) }
-      let!(:group_b_a) { create(:group, parent: group_b) }
-
-      let!(:group_c) { create(:group, parent: group) }
-      let!(:group_c_a) { create(:group, parent: group_c) }
-      let!(:group_c_b) { create(:group, parent: group_c) }
-
-      context 'with multiple projects' do
-        let!(:project_a) { create(:project, namespace: group_a) }
-        let!(:project_b_a) { create(:project, namespace: group_b_a) }
-        let!(:project_c) { create(:project, namespace: group_c) }
-        let!(:project_c_b) { create(:project, namespace: group_c_b) }
-
-        context 'with multiple versions of the same component' do
-          let!(:bundler) { create(:sbom_component, :bundler) }
-          let!(:bundler_v1) { create(:sbom_occurrence, project: project_a, component: bundler, component_version: create(:sbom_component_version, component: bundler, version: "1.0.0")) }
-          let!(:bundler_v2) { create(:sbom_occurrence, project: project_b_a, component: bundler, component_version: create(:sbom_component_version, component: bundler, version: "2.0.0")) }
-
-          let!(:webpack) { create(:sbom_component, :webpack) }
-          let!(:webpack_v4) { create(:sbom_occurrence, project: project_c, component: webpack, component_version: create(:sbom_component_version, component: webpack, version: "4.46.0")) }
-          let!(:webpack_unknown) { create(:sbom_occurrence, project: project_c_b, component: webpack, component_version: nil) }
-
-          let!(:caddy) { create(:sbom_component, :caddy) }
-          let!(:caddy_v1) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: create(:sbom_component_version, component: caddy, version: "1.0.0")) }
-          let!(:caddy_v2) { create(:sbom_occurrence, project: project_c_b, component: caddy, component_version: create(:sbom_component_version, component: caddy, version: "2.7.4")) }
-
-          it 'returns an occurrence for each version of each component' do
-            expect(subject).to match_array([
-              bundler_v1,
-              bundler_v2,
-              caddy_v1,
-              caddy_v2,
-              webpack_v4
-            ])
-          end
-        end
-      end
-    end
   end
 
   describe '#reached_project_access_token_limit?' do
diff --git a/ee/spec/models/sbom/occurrence_spec.rb b/ee/spec/models/sbom/occurrence_spec.rb
index 698c6b6655e72e..fb56e3f950fb83 100644
--- a/ee/spec/models/sbom/occurrence_spec.rb
+++ b/ee/spec/models/sbom/occurrence_spec.rb
@@ -318,79 +318,6 @@
     end
   end
 
-  describe ".with_totals" do
-    let_it_be(:project_a) { create(:project) }
-    let_it_be(:project_b) { create(:project) }
-    let_it_be(:project_c) { create(:project) }
-    let_it_be(:project_d) { create(:project) }
-
-    let_it_be(:bundler) { create(:sbom_component, :bundler) }
-    let_it_be(:bundler_version_1) { create(:sbom_component_version, component: bundler, version: "1.0.0") }
-    let_it_be(:bundler_version_2) { create(:sbom_component_version, component: bundler, version: "2.0.0") }
-    let_it_be(:bundler_v1) do
-      create(:sbom_occurrence, project: project_a, component: bundler, component_version: bundler_version_1)
-    end
-
-    let_it_be(:bundler_v2) do
-      create(:sbom_occurrence, project: project_b, component: bundler, component_version: bundler_version_2)
-    end
-
-    let_it_be(:webpack) { create(:sbom_component, :webpack) }
-    let_it_be(:webpack_version_4) { create(:sbom_component_version, component: webpack, version: "4.46.0") }
-    let_it_be(:webpack_v4) do
-      create(:sbom_occurrence, project: project_c, component: webpack, component_version: webpack_version_4)
-    end
-
-    let_it_be(:webpack_unknown) do
-      create(:sbom_occurrence, project: project_d, component: webpack, component_version: nil)
-    end
-
-    let_it_be(:caddy) { create(:sbom_component, :caddy) }
-    let_it_be(:caddy_version_1) { create(:sbom_component_version, component: caddy, version: "1.0.0") }
-    let_it_be(:caddy_version_2) { create(:sbom_component_version, component: caddy, version: "2.7.4") }
-    let_it_be(:caddy_v1) do
-      create(:sbom_occurrence, project: project_d, component: caddy, component_version: caddy_version_1)
-    end
-
-    let_it_be(:caddy_v2) do
-      create(:sbom_occurrence, project: project_d, component: caddy, component_version: caddy_version_2)
-    end
-
-    subject do
-      our_projects = ::Gitlab::SQL::CTE.new(:our_projects,
-        ::Project.where(id: [project_a, project_c, project_d]).select(:id))
-      our_occurrences = ::Gitlab::SQL::CTE.new(:our_occurrences, described_class.where(
-        described_class.arel_table[:project_id].in(our_projects.table.project(our_projects.table[:id]))
-      ))
-
-      described_class
-        .with(our_projects.to_arel)
-        .with(our_occurrences.to_arel)
-        .from(our_occurrences.alias_to(described_class.arel_table))
-        .with_totals(our_occurrences)
-    end
-
-    # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
-    it 'returns the project count for each version of each component' do
-      expect(subject.pluck(:component_name, :component_version_id, :project_count)).to match_array([
-        [bundler.name, bundler_version_1.id, 1],
-        [caddy.name, caddy_version_1.id, 1],
-        [caddy.name, caddy_version_2.id, 1],
-        [webpack.name, webpack_version_4.id, 1]
-      ])
-    end
-
-    # Related to https://gitlab.com/gitlab-org/gitlab/-/issues/424211
-    it 'returns the occurrence count for each version of each component' do
-      expect(subject.pluck(:component_name, :component_version_id, :occurrence_count)).to match_array([
-        [bundler.name, bundler_version_1.id, 1],
-        [caddy.name, caddy_version_1.id, 1],
-        [caddy.name, caddy_version_2.id, 1],
-        [webpack.name, webpack_version_4.id, 1]
-      ])
-    end
-  end
-
   describe '#name' do
     let(:component) { build(:sbom_component, name: 'rails') }
     let(:occurrence) { build(:sbom_occurrence, component: component) }
diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index 3700a1cde65e60..916b1bd50087dc 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -179,20 +179,6 @@
               expect(json_response).to eq(expected_response)
             end
 
-            it 'loads the data without N+1 queries' do
-              def render
-                get group_dependencies_path(group_id: group.full_path), params: params, as: :json
-              end
-
-              control = ActiveRecord::QueryRecorder.new do
-                render
-              end
-
-              create(:sbom_occurrence, project: create(:project, group: group))
-
-              expect { render }.not_to exceed_query_limit(control)
-            end
-
             it 'includes pagination headers in the response' do
               subject
 
@@ -210,7 +196,7 @@ def render
               end
 
               expect(project_routes_count).to eq(1)
-              expect(project_count).to eq(2)
+              expect(project_count).to eq(1)
             end
 
             context 'with sorting params' do
-- 
GitLab


From 0c0f3a7d56df02c0da9ae106538835d77de7539a Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 13 Sep 2023 16:09:37 -0600
Subject: [PATCH 30/32] Prevent sorting by license on large groups

---
 .../dependencies/components/dependencies_actions.vue     | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/ee/app/assets/javascripts/dependencies/components/dependencies_actions.vue b/ee/app/assets/javascripts/dependencies/components/dependencies_actions.vue
index d3fce2a689735b..ccd8ffbcee4bd2 100644
--- a/ee/app/assets/javascripts/dependencies/components/dependencies_actions.vue
+++ b/ee/app/assets/javascripts/dependencies/components/dependencies_actions.vue
@@ -24,7 +24,7 @@ export default {
     GlSortingItem,
   },
   mixins: [glFeatureFlagsMixin()],
-  inject: ['namespaceType'],
+  inject: ['namespaceType', 'enableProjectSearch'],
   props: {
     namespace: {
       type: String,
@@ -49,9 +49,10 @@ export default {
       return this.sortFields[this.sortField];
     },
     sortFields() {
-      const groupFields = this.glFeatures.groupLevelLicenses
-        ? SORT_FIELDS_GROUP
-        : omit(SORT_FIELDS_GROUP, SORT_FIELD_LICENSE);
+      const groupFields =
+        this.glFeatures.groupLevelLicenses && this.enableProjectSearch
+          ? SORT_FIELDS_GROUP
+          : omit(SORT_FIELDS_GROUP, SORT_FIELD_LICENSE);
 
       return this.isProjectNamespace ? SORT_FIELDS_PROJECT : groupFields;
     },
-- 
GitLab


From 3482582f05cea900d65a6bb8365071689850ee69 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 13 Sep 2023 16:17:59 -0600
Subject: [PATCH 31/32] Inject enableProjectSearch in specs

---
 .../dependencies/components/dependencies_actions_spec.js         | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ee/spec/frontend/dependencies/components/dependencies_actions_spec.js b/ee/spec/frontend/dependencies/components/dependencies_actions_spec.js
index 9530d2eac659c8..e68414c32e8324 100644
--- a/ee/spec/frontend/dependencies/components/dependencies_actions_spec.js
+++ b/ee/spec/frontend/dependencies/components/dependencies_actions_spec.js
@@ -17,6 +17,7 @@ describe('DependenciesActions component', () => {
 
   const objectBasicProp = {
     namespaceType: 'project',
+    enableProjectSearch: true,
   };
 
   const factory = ({ propsData, provide } = {}) => {
-- 
GitLab


From 66f402cd3a8770e5d367bfa8fe8e441e0f025c39 Mon Sep 17 00:00:00 2001
From: mo khan <mo@mokhan.ca>
Date: Wed, 13 Sep 2023 16:20:40 -0600
Subject: [PATCH 32/32] Add spec to disable sorting by license on large group
 hierarchies

---
 .../components/dependencies_actions_spec.js   | 27 +++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/ee/spec/frontend/dependencies/components/dependencies_actions_spec.js b/ee/spec/frontend/dependencies/components/dependencies_actions_spec.js
index e68414c32e8324..ce79cee2322a9c 100644
--- a/ee/spec/frontend/dependencies/components/dependencies_actions_spec.js
+++ b/ee/spec/frontend/dependencies/components/dependencies_actions_spec.js
@@ -84,6 +84,33 @@ describe('DependenciesActions component', () => {
       );
     });
 
+    describe('with the "enableProjectSearch" set to false', () => {
+      beforeEach(async () => {
+        factory({
+          propsData: { namespace },
+          provide: {
+            namespaceType: 'group',
+            enableProjectSearch: false,
+            glFeatures: { groupLevelLicenses: true },
+          },
+        });
+        store.state[namespace].endpoint = `${TEST_HOST}/dependencies.json`;
+        await nextTick();
+      });
+
+      it('does not dispatch the "license" action', () => {
+        const sortingItems = wrapper.findAllComponents(GlSortingItem).wrappers;
+
+        sortingItems.forEach((item) => {
+          item.vm.$emit('click');
+        });
+
+        expect(store.dispatch.mock.calls).not.toEqual(
+          expect.arrayContaining([[`${namespace}/setSortField`, 'license']]),
+        );
+      });
+    });
+
     describe('with the "groupLevelLicenses" feature flag disabled', () => {
       beforeEach(async () => {
         factory({
-- 
GitLab