//! Referential actions for foreign-key relationships. //! //! These map directly onto SQLite's `ON DELETE` / `ON UPDATE` //! clause vocabulary. `SET DEFAULT` is intentionally omitted //! until column DEFAULTs (C3 partial) are supported. use std::fmt; use std::str::FromStr; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ReferentialAction { /// Default — referenced rows can't be deleted while /// referencing rows exist (deferred check). NoAction, /// Like NoAction but immediate. Restrict, /// On parent row delete/update, set the FK column to NULL. SetNull, /// On parent row delete/update, propagate to dependent rows. Cascade, } impl ReferentialAction { /// The user-facing keyword as it appears in DSL input. Note /// `set null` is two words; the parser handles that as a /// single phrase. #[must_use] pub const fn keyword(self) -> &'static str { match self { Self::NoAction => "no action", Self::Restrict => "restrict", Self::SetNull => "set null", Self::Cascade => "cascade", } } /// The corresponding SQL clause as written in DDL. #[must_use] pub const fn sql_clause(self) -> &'static str { match self { Self::NoAction => "NO ACTION", Self::Restrict => "RESTRICT", Self::SetNull => "SET NULL", Self::Cascade => "CASCADE", } } /// All actions, in stable order. #[must_use] pub const fn all() -> &'static [Self] { &[Self::NoAction, Self::Restrict, Self::SetNull, Self::Cascade] } /// Default action when none is specified — matches the SQL /// standard. #[must_use] pub const fn default_action() -> Self { Self::NoAction } } impl Default for ReferentialAction { fn default() -> Self { Self::default_action() } } impl fmt::Display for ReferentialAction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.keyword()) } } /// Error returned when parsing an unknown action keyword. /// /// Display flows through the i18n catalog /// (`parse.custom.unknown_action`) so the wording can be /// localised or adjusted without touching the type. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnknownAction { pub found: String, pub expected: String, } impl std::fmt::Display for UnknownAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&crate::t!( "parse.custom.unknown_action", found = self.found, expected = self.expected, )) } } impl std::error::Error for UnknownAction {} impl FromStr for ReferentialAction { type Err = UnknownAction; fn from_str(s: &str) -> Result { // Case-insensitive comparison; tolerates internal // whitespace ("set null") by collapsing it. let normalised: String = s .split_whitespace() .collect::>() .join(" ") .to_ascii_lowercase(); for &action in Self::all() { if normalised == action.keyword() { return Ok(action); } } Err(UnknownAction { found: s.to_string(), expected: Self::all() .iter() .map(|a| a.keyword()) .collect::>() .join(", "), }) } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn keyword_round_trip() { for &a in ReferentialAction::all() { assert_eq!(a.keyword().parse::().unwrap(), a); } } #[test] fn parsing_is_case_insensitive_and_whitespace_tolerant() { assert_eq!( "Cascade".parse::().unwrap(), ReferentialAction::Cascade ); assert_eq!( "SET NULL".parse::().unwrap(), ReferentialAction::SetNull ); assert_eq!( "set null".parse::().unwrap(), ReferentialAction::SetNull ); assert_eq!( "No Action".parse::().unwrap(), ReferentialAction::NoAction ); } #[test] fn unknown_action_lists_alternatives() { let err = "destroy".parse::().unwrap_err(); assert_eq!(err.found, "destroy"); assert!(err.expected.contains("cascade")); assert!(err.expected.contains("set null")); } #[test] fn sql_clause_mapping() { assert_eq!(ReferentialAction::SetNull.sql_clause(), "SET NULL"); assert_eq!(ReferentialAction::NoAction.sql_clause(), "NO ACTION"); } }