eugene 0.8.3

Careful with That Lock, Eugene
Documentation
pub fn hint_url<S: AsRef<str>>(id: S) -> String {
    format!("https://kaveland.no/eugene/hints/{}/", id.as_ref())
}

pub trait HintId {
    fn id(&self) -> &str;
    fn url(&self) -> String {
        hint_url::<&str>(self.id())
    }
}

pub struct StaticHintData {
    pub id: &'static str,
    pub name: &'static str,
    pub condition: &'static str,
    pub effect: &'static str,
    pub workaround: &'static str,
    pub bad_example: &'static str,
    pub good_example: Option<&'static str>,
}

impl HintId for StaticHintData {
    fn id(&self) -> &str {
        self.id
    }
}

pub const VALIDATE_CONSTRAINT_WITH_LOCK: StaticHintData = StaticHintData {
    id: "E1",
    name: "Validating table with a new constraint",
    condition: "A new constraint was added and it is already `VALID`",
    effect: "This blocks all table access until all rows are validated",
    workaround: "Add the constraint as `NOT VALID` and validate it with `ALTER TABLE ... VALIDATE CONSTRAINT` later",
    bad_example: include_str!("../examples/E1/bad.sql"),
    good_example: Some(include_str!("../examples/E1/good.sql")),
};
pub const MAKE_COLUMN_NOT_NULLABLE_WITH_LOCK: StaticHintData = StaticHintData {
    id: "E2",
    name: "Validating table with a new `NOT NULL` column",
    condition: "A column was changed from `NULL` to `NOT NULL`",
    workaround: "Add a `CHECK` constraint as `NOT VALID`, validate it later, then make the column `NOT NULL`",
    effect: "This blocks all table access until all rows are validated",
    bad_example: include_str!("../examples/E2/bad.sql"),
    good_example: Some(include_str!("../examples/E2/good.sql")),
};
pub const ADD_JSON_COLUMN: StaticHintData = StaticHintData {
    id: "E3",
    name: "Add a new JSON column",
    condition: "A new column of type `json` was added to a table",
    workaround: "Use the `jsonb` type instead, it supports all use-cases of `json` and is more robust and compact",
    effect: "This breaks `SELECT DISTINCT` queries or other operations that need equality checks on the column",
    bad_example: include_str!("../examples/E3/bad.sql"),
    good_example: Some(include_str!("../examples/E3/good.sql")),
};
pub const RUNNING_STATEMENT_WHILE_HOLDING_ACCESS_EXCLUSIVE: StaticHintData = StaticHintData {
    id: "E4",
    name: "Running more statements after taking `AccessExclusiveLock`",
    condition: "A transaction that holds an `AccessExclusiveLock` started a new statement",
    workaround: "Run this statement in a new transaction",
    effect: "This blocks all access to the table for the duration of this statement",
    bad_example: include_str!("../examples/E4/bad.sql"),
    good_example: Some(include_str!("../examples/E4/good.sql")),
};
pub const TYPE_CHANGE_REQUIRES_TABLE_REWRITE: StaticHintData = StaticHintData {
    id: "E5",
    name: "Type change requiring table rewrite",
    condition: "A column was changed to a data type that isn't binary compatible",
    workaround: "Add a new column, update it in batches, and drop the old column",
    effect: "This causes a full table rewrite while holding a lock that prevents all other use of the table",
    bad_example: include_str!("../examples/E5/bad.sql"),
    good_example: Some(include_str!("../examples/E5/good.sql")),
};
pub const NEW_INDEX_ON_EXISTING_TABLE_IS_NONCONCURRENT: StaticHintData = StaticHintData {
    id: "E6",
    name: "Creating a new index on an existing table",
    condition: "A new index was created on an existing table without the `CONCURRENTLY` keyword",
    workaround: "Run `CREATE INDEX CONCURRENTLY` instead of `CREATE INDEX`",
    effect: "This blocks all writes to the table while the index is being created",
    bad_example: include_str!("../examples/E6/bad.sql"),
    good_example: Some(include_str!("../examples/E6/good.sql")),
};
pub const NEW_UNIQUE_CONSTRAINT_CREATED_INDEX: StaticHintData = StaticHintData {
    id: "E7",
    name: "Creating a new unique constraint",
    condition: "Adding a new unique constraint implicitly creates index",
    workaround: "`CREATE UNIQUE INDEX CONCURRENTLY`, then add the constraint using the index",
    effect: "This blocks all writes to the table while the index is being created and validated",
    bad_example: include_str!("../examples/E7/bad.sql"),
    good_example: Some(include_str!("../examples/E7/good.sql")),
};
pub const NEW_EXCLUSION_CONSTRAINT_FOUND: StaticHintData = StaticHintData {
    id: "E8",
    name: "Creating a new exclusion constraint",
    condition: "Found a new exclusion constraint",
    workaround: "There is no safe way to add an exclusion constraint to an existing table",
    effect:
        "This blocks all reads and writes to the table while the constraint index is being created",
    bad_example: include_str!("../examples/E8/bad.sql"),
    good_example: None,
};
pub const TOOK_DANGEROUS_LOCK_WITHOUT_TIMEOUT: StaticHintData = StaticHintData {
    id: "E9",
    name: "Taking dangerous lock without timeout",
    condition: "A lock that would block many common operations was taken without a timeout",
    workaround: "Run `SET LOCAL lock_timeout = '2s';` before the statement and retry the migration if necessary",
    effect: "This can block all other operations on the table indefinitely if any other transaction \
    holds a conflicting lock while `idle in transaction` or `active`",
    bad_example: include_str!("../examples/E9/bad.sql"),
    good_example: Some(include_str!("../examples/E9/good.sql")),
};
pub const REWROTE_TABLE_WHILE_HOLDING_DANGEROUS_LOCK: StaticHintData = StaticHintData {
    id: "E10",
    name: "Rewrote table or index while holding dangerous lock",
    condition: "A table or index was rewritten while holding a lock that blocks many operations",
    workaround: "Build a new table or index, write to both, then swap them",
    effect: "This blocks many operations on the table or index while the rewrite is in progress",
    bad_example: include_str!("../examples/E10/bad.sql"),
    good_example: Some(include_str!("../examples/E10/good.sql")),
};
pub const ADDED_SERIAL_OR_STORED_GENERATED_COLUMN: StaticHintData = StaticHintData {
    id: "E11",
    name: "Adding a `SERIAL` or `GENERATED ... STORED` column",
    condition: "A new column was added with a `SERIAL` or `GENERATED` type",
    workaround: "Can not be done without a table rewrite",
    effect: "This blocks all table access until the table is rewritten",
    bad_example: include_str!("../examples/E11/bad.sql"),
    good_example: None,
};
pub const MULTIPLE_ALTER_TABLES_WHERE_ONE_WILL_DO: StaticHintData = StaticHintData {
    id: "W12",
    name: "Multiple `ALTER TABLE` statements where one will do",
    condition: "Multiple `ALTER TABLE` statements targets the same table",
    workaround: "Combine the statements into one, separating the action with commas",
    effect: "If the statements require table scans, there will be more scans than necessary",
    bad_example: include_str!("../examples/W12/bad.sql"),
    good_example: Some(include_str!("../examples/W12/good.sql")),
};
pub const CREATING_ENUM: StaticHintData = StaticHintData {
    id: "W13",
    name: "Creating an enum",
    condition: "A new enum was created",
    workaround: "Use a foreign key to a lookup table instead",
    effect: "Removing values from an enum requires difficult migrations, and associating more data with an enum value is difficult",
    bad_example: include_str!("../examples/W13/bad.sql"),
    good_example: Some(include_str!("../examples/W13/good.sql")),
};
pub const ADD_PRIMARY_KEY_USING_INDEX: StaticHintData = StaticHintData {
    id: "W14",
    name: "Adding a primary key using an index",
    condition: "A primary key was added using an index on the table",
    workaround: "Make sure that all the columns in the index are already `NOT NULL`",
    effect: "This can cause postgres to alter the index columns to be `NOT NULL`",
    bad_example: include_str!("../examples/W14/bad.sql"),
    good_example: Some(include_str!("../examples/W14/good.sql")),
};
pub const FOREIGN_KEY_NOT_BACKED_BY_INDEX: StaticHintData = StaticHintData {
    id: "E15",
    name: "Missing index",
    condition: "A foreign key is missing a complete index on the referencing side",
    effect: "Updates and deletes on the referenced table may cause table scan on referencing table",
    workaround: "Create the missing index",
    bad_example: include_str!("../examples/E15/bad.sql"),
    good_example: Some(include_str!("../examples/E15/good.sql")),
};

pub const ALL: &[&StaticHintData] = &[
    &VALIDATE_CONSTRAINT_WITH_LOCK,
    &MAKE_COLUMN_NOT_NULLABLE_WITH_LOCK,
    &ADD_JSON_COLUMN,
    &RUNNING_STATEMENT_WHILE_HOLDING_ACCESS_EXCLUSIVE,
    &TYPE_CHANGE_REQUIRES_TABLE_REWRITE,
    &NEW_INDEX_ON_EXISTING_TABLE_IS_NONCONCURRENT,
    &NEW_UNIQUE_CONSTRAINT_CREATED_INDEX,
    &NEW_EXCLUSION_CONSTRAINT_FOUND,
    &TOOK_DANGEROUS_LOCK_WITHOUT_TIMEOUT,
    &REWROTE_TABLE_WHILE_HOLDING_DANGEROUS_LOCK,
    &ADDED_SERIAL_OR_STORED_GENERATED_COLUMN,
    &MULTIPLE_ALTER_TABLES_WHERE_ONE_WILL_DO,
    &CREATING_ENUM,
    &ADD_PRIMARY_KEY_USING_INDEX,
    &FOREIGN_KEY_NOT_BACKED_BY_INDEX,
];

pub fn data_by_id<S: AsRef<str>>(id: S) -> Option<&'static StaticHintData> {
    ALL.iter().find(|hint| hint.id == id.as_ref()).copied()
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_no_duplicated_id_or_name() {
        let mut ids = std::collections::HashSet::new();
        let mut names = std::collections::HashSet::new();
        for hint in super::ALL {
            assert!(ids.insert(hint.id), "Duplicated id: {}", hint.id);
            assert!(ids.insert(&hint.id[1..]), "Duplicated id: {}", hint.id);
            assert!(names.insert(hint.name), "Duplicated name: {}", hint.name);
        }
    }
}