Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions entity/src/product_status.rs
Original file line number Diff line number Diff line change
@@ -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.


#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "product_status")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub advisory_id: Uuid,
pub vulnerability_id: String,
pub status_id: Uuid,
pub status: Status,
pub package: Option<String>,
pub product_version_range_id: Uuid,
pub context_cpe_id: Option<Uuid>,
Expand All @@ -34,12 +36,6 @@ pub enum Relation {
)]
Advisory,

#[sea_orm(belongs_to = "super::status::Entity",
from = "Column::StatusId"
to = "super::status::Column::Id"
)]
Status,

#[sea_orm(belongs_to = "super::advisory_vulnerability::Entity",
from = "(Column::AdvisoryId, Column::VulnerabilityId)"
to = "(super::advisory_vulnerability::Column::AdvisoryId, super::advisory_vulnerability::Column::VulnerabilityId)"
Expand Down Expand Up @@ -77,12 +73,6 @@ impl Related<super::advisory::Entity> for Entity {
}
}

impl Related<super::status::Entity> for Entity {
fn to() -> RelationDef {
Relation::Status.def()
}
}

impl Related<super::advisory_vulnerability::Entity> for Entity {
fn to() -> RelationDef {
Relation::AdvisoryVulnerability.def()
Expand Down
16 changes: 3 additions & 13 deletions entity/src/purl_status.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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


#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "purl_status")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub advisory_id: Uuid,
pub vulnerability_id: String,
pub status_id: Uuid,
pub status: Status,
pub base_purl_id: Uuid,
pub version_range_id: Uuid,
pub context_cpe_id: Option<Uuid>,
Expand Down Expand Up @@ -43,12 +45,6 @@ pub enum Relation {
)]
Advisory,

#[sea_orm(belongs_to = "super::status::Entity",
from = "Column::StatusId"
to = "super::status::Column::Id"
)]
Status,

#[sea_orm(belongs_to = "super::advisory_vulnerability::Entity",
from = "(Column::AdvisoryId, Column::VulnerabilityId)"
to = "(super::advisory_vulnerability::Column::AdvisoryId, super::advisory_vulnerability::Column::VulnerabilityId)"
Expand Down Expand Up @@ -106,12 +102,6 @@ impl Related<super::advisory::Entity> for Entity {
}
}

impl Related<super::status::Entity> for Entity {
fn to() -> RelationDef {
Relation::Status.def()
}
}

impl Related<super::advisory_vulnerability::Entity> for Entity {
fn to() -> RelationDef {
Relation::AdvisoryVulnerability.def()
Expand Down
33 changes: 8 additions & 25 deletions entity/src/status.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,6 @@
use crate::purl_status;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "status")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub slug: String,
pub name: String,
pub description: Option<String>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::purl_status::Entity")]
PackageStatus,
}

impl Related<purl_status::Entity> for Entity {
fn to() -> RelationDef {
Relation::PackageStatus.def()
}
}

impl ActiveModelBehavior for ActiveModel {}

#[derive(
Copy,
Clone,
Expand All @@ -37,12 +12,20 @@ impl ActiveModelBehavior for ActiveModel {}
strum::Display,
Serialize,
Deserialize,
DeriveActiveEnum,
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.

pub enum Status {
#[sea_orm(string_value = "affected")]
Affected,
#[sea_orm(string_value = "fixed")]
Fixed,
#[sea_orm(string_value = "not_affected")]
NotAffected,
#[sea_orm(string_value = "under_investigation")]
UnderInvestigation,
#[sea_orm(string_value = "recommended")]
Recommended,
}
2 changes: 2 additions & 0 deletions migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mod m0002160_fix_ref_fk;
mod m0002170_drop_cvss_tables;
mod m0002180_advisory_fk_indexes;
mod m0002190_vulnerability_base_score_advisory;
mod m0002200_status_enum;

pub trait MigratorExt: Send {
fn build_migrations() -> Migrations;
Expand Down Expand Up @@ -132,6 +133,7 @@ impl MigratorExt for Migrator {
.normal(m0002170_drop_cvss_tables::Migration)
.normal(m0002180_advisory_fk_indexes::Migration)
.normal(m0002190_vulnerability_base_score_advisory::Migration)
.normal(m0002200_status_enum::Migration)
}
}

Expand Down
171 changes: 171 additions & 0 deletions migration/src/m0002200_status_enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use sea_orm_migration::prelude::*;
use sea_query::extension::postgres::Type;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();

// 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"#,
Comment on lines +15 to +21
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.

)
.await?;

db.execute_unprepared(r#"ALTER TABLE "purl_status" DROP COLUMN "status_id""#)
.await?;
db.execute_unprepared(r#"ALTER TABLE "product_status" DROP COLUMN "status_id""#)
.await?;
db.execute_unprepared(r#"DROP TABLE "status""#).await?;

// 2. Create PostgreSQL enum type
let builder = db.get_database_backend();
let stmt = builder
.build(Type::create().as_enum(StatusEnum::Table).values([
StatusEnum::Affected,
StatusEnum::Fixed,
StatusEnum::NotAffected,
StatusEnum::UnderInvestigation,
StatusEnum::Recommended,
]))
.to_string();
db.execute_unprepared(&stmt).await?;

// 3. Add nullable enum columns
db.execute_unprepared(r#"ALTER TABLE "purl_status" ADD COLUMN "status" "status" NULL"#)
.await?;
db.execute_unprepared(r#"ALTER TABLE "product_status" ADD COLUMN "status" "status" NULL"#)
.await?;

// 4. Populate from snapshot
db.execute_unprepared(
r#"UPDATE "purl_status" ps
SET "status" = snap."slug"::status
FROM _status_snapshot snap
WHERE snap.row_id = ps.id AND snap.kind = 'purl'"#,
)
.await?;
db.execute_unprepared(
r#"UPDATE "product_status" ps
SET "status" = snap."slug"::status
FROM _status_snapshot snap
WHERE snap.row_id = ps.id AND snap.kind = 'product'"#,
)
.await?;

db.execute_unprepared(r#"DROP TABLE _status_snapshot"#)
.await?;

// 5. Make NOT NULL
db.execute_unprepared(r#"ALTER TABLE "purl_status" ALTER COLUMN "status" SET NOT NULL"#)
.await?;
db.execute_unprepared(r#"ALTER TABLE "product_status" ALTER COLUMN "status" SET NOT NULL"#)
.await?;

Ok(())
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();

// 1. Recreate the status table
db.execute_unprepared(
r#"CREATE TABLE "status" (
"id" uuid PRIMARY KEY,
"slug" text NOT NULL,
"name" text NOT NULL,
"description" text
)"#,
)
.await?;

// 2. Seed status rows
db.execute_unprepared(
r#"INSERT INTO "status" ("id", "slug", "name") VALUES
('7cc29b44-e708-11ed-a05b-0242ac120003', 'affected', 'Affected'),
('7cc29e00-e708-11ed-a05b-0242ac120003', 'not_affected', 'Not affected'),
('7cc29f04-e708-11ed-a05b-0242ac120003', 'fixed', 'Fixed'),
('7cc2a01c-e708-11ed-a05b-0242ac120003', 'under_investigation', 'Under investigation'),
('7cc2a0ee-e708-11ed-a05b-0242ac120003', 'recommended', 'Recommended')"#,
)
.await?;

// 3. Add status_id columns back
db.execute_unprepared(r#"ALTER TABLE "purl_status" ADD COLUMN "status_id" uuid NULL"#)
.await?;
db.execute_unprepared(r#"ALTER TABLE "product_status" ADD COLUMN "status_id" uuid NULL"#)
.await?;

// 4. Populate from enum via JOIN
db.execute_unprepared(
r#"UPDATE "purl_status" ps
SET "status_id" = s."id"
FROM "status" s
WHERE ps."status"::text = s."slug""#,
)
.await?;
db.execute_unprepared(
r#"UPDATE "product_status" ps
SET "status_id" = s."id"
FROM "status" s
WHERE ps."status"::text = s."slug""#,
)
.await?;

// 5. Make NOT NULL and add FK
db.execute_unprepared(r#"ALTER TABLE "purl_status" ALTER COLUMN "status_id" SET NOT NULL"#)
.await?;
db.execute_unprepared(
r#"ALTER TABLE "product_status" ALTER COLUMN "status_id" SET NOT NULL"#,
)
.await?;
db.execute_unprepared(
r#"ALTER TABLE "purl_status" ADD CONSTRAINT "fk_purl_status_status"
FOREIGN KEY ("status_id") REFERENCES "status"("id")"#,
)
.await?;
db.execute_unprepared(
r#"ALTER TABLE "product_status" ADD CONSTRAINT "fk_product_status_status"
FOREIGN KEY ("status_id") REFERENCES "status"("id")"#,
)
.await?;

// 6. Drop enum columns and drop enum type
db.execute_unprepared(r#"ALTER TABLE "purl_status" DROP COLUMN "status""#)
.await?;
db.execute_unprepared(r#"ALTER TABLE "product_status" DROP COLUMN "status""#)
.await?;

manager
.drop_type(Type::drop().if_exists().name(StatusEnum::Table).to_owned())
.await?;

Ok(())
}
}

#[derive(DeriveIden)]
pub enum StatusEnum {
#[sea_orm(iden = "status")]
Table,
#[sea_orm(iden = "affected")]
Affected,
#[sea_orm(iden = "fixed")]
Fixed,
#[sea_orm(iden = "not_affected")]
NotAffected,
#[sea_orm(iden = "under_investigation")]
UnderInvestigation,
#[sea_orm(iden = "recommended")]
Recommended,
}
Loading
Loading