Skip to content

feat: replace status lookup table with PostgreSQL enum#2363

Open
ctron wants to merge 1 commit into
guacsec:mainfrom
ctron:feature/tc_4503_1
Open

feat: replace status lookup table with PostgreSQL enum#2363
ctron wants to merge 1 commit into
guacsec:mainfrom
ctron:feature/tc_4503_1

Conversation

@ctron
Copy link
Copy Markdown
Contributor

@ctron ctron commented May 19, 2026

Summary

  • Replace the status lookup table with a native PostgreSQL enum type to eliminate JOIN overhead on purl_status and product_status queries
  • Migration snapshots existing data, drops old table/FK columns, creates the enum, and repopulates from snapshot
  • All entity, ingestor, fundamental, and vulnerability layers updated to use Status enum directly

Jira

TC-4503

Test plan

  • cargo xtask precommit passes (fmt, clippy, check, openapi validation, migration)
  • All existing tests compile and pass with enum-based status
  • Integration tests with advisory/SBOM ingestion verify status values round-trip correctly

🤖 Generated with Claude Code

Summary by Sourcery

Replace the status lookup table with a PostgreSQL enum and update all layers to use the enum-based status fields.

Enhancements:

  • Inline the status enum into purl_status and product_status entities and remove status table relations and associated lookup logic.
  • Simplify status handling across advisory, purl, sbom, vulnerability, and ingestor services to pass and store the strongly-typed Status enum instead of string or UUID identifiers.
  • Adjust raw SQL, query models, and JSON aggregation to work directly with the enum value while preserving existing API representations.

Build:

  • Add a new database migration that migrates existing status data to the PostgreSQL enum, updates schema columns, and provides a down migration back to the lookup table model.

Tests:

  • Update and adapt existing recommendation, advisory, and ingestion tests to use the Status enum values and verify expected status strings in responses.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 19, 2026

Reviewer's Guide

Replaces the old status lookup table and UUID foreign keys with a PostgreSQL enum-based status everywhere, removing JOINs and status-id plumbing across entity models, ingestors, SBOM/vulnerability queries, and adding a data migration to convert existing rows.

Entity relationship diagram for enum-based status schema

erDiagram
    PURL_STATUS {
        uuid id
        uuid advisory_id
        string vulnerability_id
        status status
        uuid base_purl_id
        uuid version_range_id
        uuid context_cpe_id
    }

    PRODUCT_STATUS {
        uuid id
        uuid advisory_id
        string vulnerability_id
        status status
        string package
        uuid product_version_range_id
        uuid context_cpe_id
    }

    STATUS_ENUM {
        string value
        string description
    }

    PURL_STATUS ||--o{ STATUS_ENUM : uses
    PRODUCT_STATUS ||--o{ STATUS_ENUM : uses

    PURL_STATUS }o--|| ADVISORY : references
    PRODUCT_STATUS }o--|| ADVISORY : references

    PURL_STATUS }o--|| VULNERABILITY : references
    PRODUCT_STATUS }o--|| VULNERABILITY : references

    PURL_STATUS }o--|| BASE_PURL : references
    PURL_STATUS }o--|| VERSION_RANGE : references

    PRODUCT_STATUS }o--|| PRODUCT_VERSION_RANGE : references
Loading

File-Level Changes

Change Details Files
Replace status lookup table with PostgreSQL enum and update entity models.
  • Delete the status Entity model table representation and replace it with a Status DeriveActiveEnum mapped to a PostgreSQL enum type.
  • Change product_status and purl_status models from status_id: Uuid with belongs_to Status relations to status: Status enum fields, removing the Status relation wiring.
  • Adjust deterministic UUID computation for PurlStatus and ProductStatus to derive from the enum string value instead of a UUID.
entity/src/status.rs
entity/src/product_status.rs
entity/src/purl_status.rs
modules/ingestor/src/graph/advisory/purl_status.rs
modules/ingestor/src/graph/advisory/product_status.rs
Update ingestors to work directly with enum Status instead of status slugs and DbContext lookups.
  • Change PurlStatusEntry, CSAF/OSV/CVE loaders, and advisory graph ingestion APIs to use trustify_entity::status::Status rather than String or &str status parameters.
  • Remove DbContext-based status_id resolution and the InvalidStatus error, passing Status enum values through to purl_status/product_status records.
  • Update tests to construct Status enums instead of creating or looking up status rows, and adjust expectations to enum-backed string values (e.g. under_investigation).
modules/ingestor/src/graph/purl/status_creator.rs
modules/ingestor/src/service/advisory/csaf/creator.rs
modules/ingestor/src/service/advisory/csaf/loader.rs
modules/ingestor/src/service/advisory/csaf/product_status.rs
modules/ingestor/src/service/advisory/osv/loader.rs
modules/ingestor/src/service/advisory/cve/loader.rs
modules/ingestor/src/graph/advisory/advisory_vulnerability.rs
modules/ingestor/src/graph/error.rs
modules/ingestor/src/graph/mod.rs
modules/fundamental/src/advisory/service/test.rs
modules/fundamental/src/purl/endpoints/test.rs
Refactor SBOM, vulnerability, and purl query paths to read/write status directly from enum columns instead of joining status table.
  • Update raw SQL and SeaORM queries to select purl_status/product_status.status::text as status_value, and filter on status enum text instead of status.slug.
  • Remove bulk status table prefetching and in-memory lookup maps in SBOM/vulnerability detail builders and instead carry status as a String or Status enum on intermediate models.
  • Adapt FromQueryResult helpers and catcher structs (e.g. SbomStatusCatcher, ProductStatusCatcher, QueryCatcher, PurlStatusCatcher) to read status_value or Status directly and propagate it into API models.
modules/fundamental/src/sbom/model/details.rs
modules/fundamental/src/sbom/model/raw_sql.rs
modules/fundamental/src/sbom/model/details/vulnerability_advisory.rs
modules/fundamental/src/sbom/service/sbom.rs
modules/fundamental/src/purl/model/details/purl.rs
modules/fundamental/src/purl/model/details/versioned_purl.rs
modules/fundamental/src/vulnerability/service/mod.rs
Add migration to convert existing status data from lookup table to PostgreSQL enum and back.
  • Create migration m0002200_status_enum to snapshot existing purl_status/product_status rows into a temp table, drop status_id columns and status table, create the PostgreSQL status enum type, add new enum columns, repopulate enum values from snapshot, and enforce NOT NULL.
  • Implement down() logic to recreate the status table with seeded IDs/slugs, restore status_id columns, backfill them from enum values, add foreign keys, and drop the enum columns/type.
  • Register the new migration in the Migrator so it runs after existing migrations.
migration/src/m0002200_status_enum.rs
migration/src/lib.rs

Possibly linked issues

  • #0: PR implements the issue’s proposal by dropping the status table, adding a PostgreSQL enum, and updating usages

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="migration/src/m0002200_status_enum.rs" line_range="15-21" />
<code_context>
+        // 1. Snapshot status slugs into a temp table, then drop old FK columns and status table.
+        //    The table name "status" also reserves the composite type name in PostgreSQL,
+        //    so it must be dropped before we can create the enum type with the same name.
+        db.execute_unprepared(
+            r#"CREATE TEMP TABLE _status_snapshot AS
+               SELECT ps.id AS row_id, 'purl' AS kind, s.slug
+               FROM purl_status ps JOIN status s ON ps.status_id = s.id
+               UNION ALL
+               SELECT ps.id AS row_id, 'product' AS kind, s.slug
+               FROM product_status ps JOIN status s ON ps.status_id = s.id"#,
+        )
+        .await?;
</code_context>
<issue_to_address>
**issue:** Migration assumes all existing `status.slug` values match the new enum variants, which may break on unexpected/custom slugs.

Because the migration later does `SET "status" = snap."slug"::status`, any slug not present in the new enum will cause the cast to fail and abort the migration. If such values might exist in production (e.g., legacy/custom slugs), consider either mapping them to a fallback enum value during migration or pre-checking for them and failing with a clear diagnostic before running the cast.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +15 to +21
db.execute_unprepared(
r#"CREATE TEMP TABLE _status_snapshot AS
SELECT ps.id AS row_id, 'purl' AS kind, s.slug
FROM purl_status ps JOIN status s ON ps.status_id = s.id
UNION ALL
SELECT ps.id AS row_id, 'product' AS kind, s.slug
FROM product_status ps JOIN status s ON ps.status_id = s.id"#,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Migration assumes all existing status.slug values match the new enum variants, which may break on unexpected/custom slugs.

Because the migration later does SET "status" = snap."slug"::status, any slug not present in the new enum will cause the cast to fail and abort the migration. If such values might exist in production (e.g., legacy/custom slugs), consider either mapping them to a fallback enum value during migration or pre-checking for them and failing with a clear diagnostic before running the cast.

@@ -1,13 +1,15 @@
use sea_orm::entity::prelude::*;

use super::status::Status;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep use statement together.

Comment thread entity/src/purl_status.rs
use sea_orm::LinkDef;
use sea_orm::entity::prelude::*;

use super::status::Status;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep use statements together

Comment thread entity/src/status.rs
EnumIter,
)]
#[strum(serialize_all = "snake_case")]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "status")]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a test to ensure the implementations of strum and sea_orm create the same string value.

}

/// Verifies that a custom vulnerability status is reflected in the recommendation response.
/// Verifies that a non-default vulnerability status is reflected in the recommendation response.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to keep that test? As there no longer can be a "custom" variant?

&product_status.vulnerability,
&product_status.advisory,
product_status.status.slug.clone(),
product_status.status.to_string(),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we use the enum type?

/// Compute a deterministic UUID for deduplication
pub fn uuid(&self, advisory_id: Uuid, vulnerability_id: String) -> Uuid {
let mut result = Uuid::new_v5(&NAMESPACE, self.status.as_bytes());
let mut result = Uuid::new_v5(&NAMESPACE, self.status.to_string().as_bytes());
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation changes the outcome of the ID. As the self.status.as_bytes() is referencing to the UUID, not the string slug. Add some tests to ensure previous ID match the new implementation IDs. Also do some data migration for existing IDs.

use trustify_entity::advisory_vulnerability_score::{ScoreType, Severity};
use trustify_entity::{labels::Labels, version_scheme::VersionScheme, vulnerability};
use trustify_entity::{
labels::Labels, status::Status as EntityStatus, version_scheme::VersionScheme, vulnerability,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, use status::Status as the type.

.await?
.map(|e| e.slug)
.unwrap_or("unknown".into());
let status = package_status.status.to_string();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we use the enum type?

vulnerability: vulnerability::Model,
cpe: trustify_entity::cpe::Model,
status: status::Model,
status: trustify_entity::status::Status,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use the full type prefix.

pub async fn from_entity<C: ConnectionTrait>(
vuln: &vulnerability::Model,
status_model: Option<status::Model>,
purl_status: &purl_status::Model,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be enough to pass the Status enum type?

Replace the `status` lookup table with a native PostgreSQL enum type
to eliminate JOIN overhead on purl_status and product_status queries.

The migration snapshots existing status values, drops the old table
and FK columns, creates the enum type, and repopulates from the
snapshot. The entity layer now uses SeaORM's DeriveActiveEnum with
strum for Display/EnumString support. All ingestor write paths,
fundamental read paths, raw SQL queries, and tests are updated to
use the Status enum directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ctron ctron force-pushed the feature/tc_4503_1 branch from 4421ba2 to e39e0a8 Compare May 19, 2026 11:58
@ctron ctron changed the title feat(entity): replace status lookup table with PostgreSQL enum feat: replace status lookup table with PostgreSQL enum May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant