-
Notifications
You must be signed in to change notification settings - Fork 48
feat: replace status lookup table with PostgreSQL enum #2363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>, | ||
|
|
@@ -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)" | ||
|
|
@@ -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() | ||
|
|
||
| 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, | ||
|
|
@@ -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")] | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| } | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: Migration assumes all existing Because the migration later does |
||
| ) | ||
| .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, | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep use statement together.