Skip to content

Commit ac44ed3

Browse files
committed
Add expression index for sargable query
1 parent 6b9d483 commit ac44ed3

File tree

5 files changed

+108
-1
lines changed

5 files changed

+108
-1
lines changed

entity/src/product_status.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ pub struct Model {
1111
pub package: Option<String>,
1212
pub product_version_range_id: Uuid,
1313
pub context_cpe_id: Option<Uuid>,
14+
/// Generated column: namespace part of package (NULL if no '/' in package)
15+
pub package_namespace: Option<String>,
16+
/// Generated column: name part of package (everything after '/' or entire package if no '/')
17+
pub package_name: Option<String>,
1418
}
1519

1620
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

migration/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ mod m0001170_non_null_source_document_id;
3232
mod m0001180_expand_spdx_licenses_with_mappings_function;
3333
mod m0001190_optimize_product_advisory_query;
3434
mod m0001200_source_document_fk_indexes;
35+
mod m0001220_improve_product_status;
3536

3637
pub struct Migrator;
3738

@@ -71,6 +72,7 @@ impl MigratorTrait for Migrator {
7172
Box::new(m0001180_expand_spdx_licenses_with_mappings_function::Migration),
7273
Box::new(m0001190_optimize_product_advisory_query::Migration),
7374
Box::new(m0001200_source_document_fk_indexes::Migration),
75+
Box::new(m0001220_improve_product_status::Migration),
7476
]
7577
}
7678
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use sea_orm_migration::prelude::*;
2+
3+
#[derive(DeriveMigrationName)]
4+
pub struct Migration;
5+
6+
#[async_trait::async_trait]
7+
#[allow(deprecated)]
8+
impl MigrationTrait for Migration {
9+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
10+
// Add generated columns for package_namespace and package_name
11+
// These columns split the package field to enable indexed lookups:
12+
// - package_namespace: NULL for packages without '/', otherwise the part before '/'
13+
// - package_name: the part after '/' if present, otherwise the entire package value
14+
//
15+
// Examples:
16+
// package = "lodash" -> namespace=NULL, name="lodash"
17+
// package = "npmjs/lodash" -> namespace="npmjs", name="lodash"
18+
// package = "@types/node" -> namespace="@types", name="node"
19+
//
20+
// This maintains compatibility with existing query patterns:
21+
// - Match on name only: WHERE package_namespace IS NULL AND package_name = ?
22+
// - Match on namespace/name: WHERE package_namespace = ? AND package_name = ?
23+
manager
24+
.get_connection()
25+
.execute_unprepared(
26+
"ALTER TABLE product_status \
27+
ADD COLUMN IF NOT EXISTS package_namespace text GENERATED ALWAYS AS (\
28+
CASE WHEN package LIKE '%/%' THEN split_part(package, '/', 1) ELSE NULL END\
29+
) STORED",
30+
)
31+
.await?;
32+
33+
manager
34+
.get_connection()
35+
.execute_unprepared(
36+
"ALTER TABLE product_status \
37+
ADD COLUMN IF NOT EXISTS package_name text GENERATED ALWAYS AS (\
38+
CASE WHEN package LIKE '%/%' THEN split_part(package, '/', 2) ELSE package END\
39+
) STORED",
40+
)
41+
.await?;
42+
43+
// Backfill existing rows with UPDATE to trigger recalculation of generated columns
44+
manager
45+
.get_connection()
46+
.execute_unprepared(
47+
"UPDATE product_status SET package = package WHERE package_namespace IS NULL OR package_name IS NULL",
48+
)
49+
.await?;
50+
51+
// CONCURRENTLY (not supported by SeaORM) to avoid blocking writes
52+
manager
53+
.get_connection()
54+
.execute_unprepared(
55+
"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_product_status_package_lookup \
56+
ON product_status (package_namespace, package_name)",
57+
)
58+
.await?;
59+
60+
Ok(())
61+
}
62+
63+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
64+
manager
65+
.get_connection()
66+
.execute_unprepared("DROP INDEX IF EXISTS idx_product_status_package_lookup")
67+
.await?;
68+
69+
manager
70+
.get_connection()
71+
.execute_unprepared("ALTER TABLE product_status DROP COLUMN IF EXISTS package_name")
72+
.await?;
73+
74+
manager
75+
.get_connection()
76+
.execute_unprepared(
77+
"ALTER TABLE product_status DROP COLUMN IF EXISTS package_namespace",
78+
)
79+
.await?;
80+
81+
Ok(())
82+
}
83+
}

modules/fundamental/src/vulnerability/model/details/vulnerability_advisory.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,13 @@ impl VulnerabilityAdvisorySummary {
243243
JOIN "sbom" ON "product_version"."sbom_id" = "sbom"."sbom_id"
244244
245245
-- find purls belonging to the sboms having a name matching package patterns
246-
JOIN base_purl on "product_status"."package" LIKE CONCAT("base_purl"."namespace", '/', "base_purl"."name") OR "product_status"."package" = "base_purl"."name"
246+
JOIN "base_purl" ON (
247+
("product_status"."package_namespace" IS NULL AND "product_status"."package_name" = "base_purl"."name")
248+
OR
249+
("product_status"."package_namespace" IS NOT NULL
250+
AND "product_status"."package_namespace" IS NOT DISTINCT FROM "base_purl"."namespace"
251+
AND "product_status"."package_name" = "base_purl"."name")
252+
)
247253
JOIN "versioned_purl" ON "versioned_purl"."base_purl_id" = "base_purl"."id"
248254
JOIN "qualified_purl" ON "qualified_purl"."versioned_purl_id" = "versioned_purl"."id"
249255
JOIN sbom_package_purl_ref on sbom_package_purl_ref.qualified_purl_id = qualified_purl.id AND sbom_package_purl_ref.sbom_id = sbom.sbom_id

modules/ingestor/src/graph/advisory/product_status.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ impl ProductStatus {
6666
advisory_id: Uuid,
6767
vulnerability_id: String,
6868
) -> product_status::ActiveModel {
69+
let (package_namespace, package_name) =
70+
self.package
71+
.as_ref()
72+
.map_or((None, None), |pkg| match pkg.split_once('/') {
73+
Some((namespace, name)) => {
74+
(Some(namespace.to_string()), Some(name.to_string()))
75+
}
76+
None => (None, Some(pkg.to_string())),
77+
});
78+
6979
product_status::ActiveModel {
7080
id: Set(self.uuid(advisory_id, vulnerability_id.clone())),
7181
advisory_id: Set(advisory_id),
@@ -74,6 +84,8 @@ impl ProductStatus {
7484
package: Set(self.package),
7585
context_cpe_id: Set(self.cpe.as_ref().map(Cpe::uuid)),
7686
product_version_range_id: Set(self.product_version_range_id),
87+
package_namespace: Set(package_namespace),
88+
package_name: Set(package_name),
7789
}
7890
}
7991

0 commit comments

Comments
 (0)