diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 7204f65d1..cf6466302 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -48,6 +48,7 @@ mod m0002060_add_sbom_group_field; mod m0002070_alter_sbom_group_restrict_parent; mod m0002080_add_cargo_version_scheme; mod m0002090_vulnerability_id_sort_index; +mod m0002100_perf_fk_indexes; pub trait MigratorExt: Send { fn build_migrations() -> Migrations; @@ -112,6 +113,7 @@ impl MigratorExt for Migrator { .normal(m0002070_alter_sbom_group_restrict_parent::Migration) .normal(m0002080_add_cargo_version_scheme::Migration) .normal(m0002090_vulnerability_id_sort_index::Migration) + .normal(m0002100_perf_fk_indexes::Migration) } } diff --git a/migration/src/m0002100_perf_fk_indexes.rs b/migration/src/m0002100_perf_fk_indexes.rs new file mode 100644 index 000000000..1e83b4294 --- /dev/null +++ b/migration/src/m0002100_perf_fk_indexes.rs @@ -0,0 +1,209 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +#[allow(deprecated)] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // product_version.sbom_id — used in analysis graph loading: + // LEFT JOIN product_version ON sbom.sbom_id = product_version.sbom_id + // Without this index, every SBOM graph load triggers a sequential scan + // of the entire product_version table. + manager + .create_index( + Index::create() + .if_not_exists() + .table(ProductVersion::Table) + .name(Indexes::ProductVersionSbomIdIdx.to_string()) + .col(ProductVersion::SbomId) + .to_owned(), + ) + .await?; + + // product_version.product_id — used in analysis graph loading: + // LEFT JOIN product ON product_version.product_id = product.id + manager + .create_index( + Index::create() + .if_not_exists() + .table(ProductVersion::Table) + .name(Indexes::ProductVersionProductIdIdx.to_string()) + .col(ProductVersion::ProductId) + .to_owned(), + ) + .await?; + + // package_relates_to_package (sbom_id, relationship) — used in CPE + // context filter SQL and SBOM advisory queries: + // WHERE sbom_id = $1 AND relationship = 13 + // The existing PK (sbom_id, left_node_id, relationship, right_node_id) + // has left_node_id between sbom_id and relationship, forcing a scan of + // all left_node_id values per SBOM. + manager + .create_index( + Index::create() + .if_not_exists() + .table(PackageRelatesToPackage::Table) + .name(Indexes::PackageRelatesToPackageSbomRelIdx.to_string()) + .col(PackageRelatesToPackage::SbomId) + .col(PackageRelatesToPackage::Relationship) + .to_owned(), + ) + .await?; + + // purl_status.version_range_id — used in vulnerability analysis: + // INNER JOIN version_range ON purl_status.version_range_id = version_range.id + manager + .create_index( + Index::create() + .if_not_exists() + .table(PurlStatus::Table) + .name(Indexes::PurlStatusVersionRangeIdIdx.to_string()) + .col(PurlStatus::VersionRangeId) + .to_owned(), + ) + .await?; + + // cpe (vendor, product, version) — used in the generalized CPE lookup + // within product_advisory_info_sql(): + // WHERE (vendor, product, version) IN (SELECT ...) + manager + .create_index( + Index::create() + .if_not_exists() + .table(Cpe::Table) + .name(Indexes::CpeVendorProductVersionIdx.to_string()) + .col(Cpe::Vendor) + .col(Cpe::Product) + .col(Cpe::Version) + .to_owned(), + ) + .await?; + + // advisory.issuer_id — used in advisory listing/detail queries: + // LEFT JOIN organization ON advisory.issuer_id = organization.id + manager + .create_index( + Index::create() + .if_not_exists() + .table(Advisory::Table) + .name(Indexes::AdvisoryIssuerIdIdx.to_string()) + .col(Advisory::IssuerId) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_index( + Index::drop() + .if_exists() + .table(Advisory::Table) + .name(Indexes::AdvisoryIssuerIdIdx.to_string()) + .to_owned(), + ) + .await?; + + manager + .drop_index( + Index::drop() + .if_exists() + .table(Cpe::Table) + .name(Indexes::CpeVendorProductVersionIdx.to_string()) + .to_owned(), + ) + .await?; + + manager + .drop_index( + Index::drop() + .if_exists() + .table(PurlStatus::Table) + .name(Indexes::PurlStatusVersionRangeIdIdx.to_string()) + .to_owned(), + ) + .await?; + + manager + .drop_index( + Index::drop() + .if_exists() + .table(PackageRelatesToPackage::Table) + .name(Indexes::PackageRelatesToPackageSbomRelIdx.to_string()) + .to_owned(), + ) + .await?; + + manager + .drop_index( + Index::drop() + .if_exists() + .table(ProductVersion::Table) + .name(Indexes::ProductVersionProductIdIdx.to_string()) + .to_owned(), + ) + .await?; + + manager + .drop_index( + Index::drop() + .if_exists() + .table(ProductVersion::Table) + .name(Indexes::ProductVersionSbomIdIdx.to_string()) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[allow(clippy::enum_variant_names)] +#[derive(DeriveIden)] +pub enum Indexes { + ProductVersionSbomIdIdx, + ProductVersionProductIdIdx, + PackageRelatesToPackageSbomRelIdx, + PurlStatusVersionRangeIdIdx, + CpeVendorProductVersionIdx, + AdvisoryIssuerIdIdx, +} + +#[derive(DeriveIden)] +pub enum ProductVersion { + Table, + SbomId, + ProductId, +} + +#[derive(DeriveIden)] +pub enum PackageRelatesToPackage { + Table, + SbomId, + Relationship, +} + +#[derive(DeriveIden)] +pub enum PurlStatus { + Table, + VersionRangeId, +} + +#[derive(DeriveIden)] +pub enum Cpe { + Table, + Vendor, + Product, + Version, +} + +#[derive(DeriveIden)] +pub enum Advisory { + Table, + IssuerId, +}