feat: replace status lookup table with PostgreSQL enum#2363
Conversation
Reviewer's GuideReplaces 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 schemaerDiagram
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
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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"#, |
There was a problem hiding this comment.
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; | |||
There was a problem hiding this comment.
Keep use statement together.
| use sea_orm::LinkDef; | ||
| use sea_orm::entity::prelude::*; | ||
|
|
||
| use super::status::Status; |
There was a problem hiding this comment.
Keep use statements together
| EnumIter, | ||
| )] | ||
| #[strum(serialize_all = "snake_case")] | ||
| #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "status")] |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Why can't we use the enum type?
| vulnerability: vulnerability::Model, | ||
| cpe: trustify_entity::cpe::Model, | ||
| status: status::Model, | ||
| status: trustify_entity::status::Status, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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>
Summary
statuslookup table with a native PostgreSQL enum type to eliminate JOIN overhead onpurl_statusandproduct_statusqueriesStatusenum directlyJira
TC-4503
Test plan
cargo xtask precommitpasses (fmt, clippy, check, openapi validation, migration)🤖 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:
Build:
Tests: