use-pg-constraint 0.1.0

PostgreSQL constraint primitives for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

use core::{fmt, str::FromStr};
use std::error::Error;

use use_pg_column::PgColumnName;
use use_pg_identifier::{PgIdentifier, PgIdentifierError};
use use_pg_table::PgTableRef;

/// PostgreSQL constraint name primitive.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgConstraintName(PgIdentifier);

impl PgConstraintName {
    /// Creates a constraint name.
    ///
    /// # Errors
    ///
    /// Returns [`PgConstraintError`] when identifier validation fails.
    pub fn new(input: impl AsRef<str>) -> Result<Self, PgConstraintError> {
        PgIdentifier::new(input)
            .map(Self)
            .map_err(PgConstraintError::Identifier)
    }

    /// Returns the constraint name text.
    #[must_use]
    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
}

impl fmt::Display for PgConstraintName {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

impl FromStr for PgConstraintName {
    type Err = PgConstraintError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        Self::new(input)
    }
}

/// PostgreSQL constraint kind labels.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgConstraintKind {
    /// Primary key constraint.
    #[default]
    PrimaryKey,
    /// Foreign key constraint.
    ForeignKey,
    /// Unique constraint.
    Unique,
    /// Check constraint.
    Check,
    /// Exclusion constraint.
    Exclusion,
    /// Not-null constraint label.
    NotNull,
}

impl PgConstraintKind {
    /// Returns the stable PostgreSQL constraint label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::PrimaryKey => "PRIMARY KEY",
            Self::ForeignKey => "FOREIGN KEY",
            Self::Unique => "UNIQUE",
            Self::Check => "CHECK",
            Self::Exclusion => "EXCLUDE",
            Self::NotNull => "NOT NULL",
        }
    }
}

impl fmt::Display for PgConstraintKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

impl FromStr for PgConstraintKind {
    type Err = PgConstraintError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match normalized_label(input)?.as_str() {
            "primary" | "primary key" => Ok(Self::PrimaryKey),
            "foreign" | "foreign key" => Ok(Self::ForeignKey),
            "unique" => Ok(Self::Unique),
            "check" => Ok(Self::Check),
            "exclude" | "exclusion" => Ok(Self::Exclusion),
            "not null" | "notnull" => Ok(Self::NotNull),
            _ => Err(PgConstraintError::UnknownKind),
        }
    }
}

/// Initial timing label for deferrable PostgreSQL constraints.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgInitially {
    /// Initially immediate timing.
    #[default]
    Immediate,
    /// Initially deferred timing.
    Deferred,
}

impl PgInitially {
    /// Returns a stable label.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Immediate => "INITIALLY IMMEDIATE",
            Self::Deferred => "INITIALLY DEFERRED",
        }
    }
}

impl fmt::Display for PgInitially {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

/// PostgreSQL constraint deferrability metadata.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgDeferrability {
    deferrable: bool,
    initially: PgInitially,
}

impl PgDeferrability {
    /// Creates a not-deferrable label.
    #[must_use]
    pub const fn not_deferrable() -> Self {
        Self {
            deferrable: false,
            initially: PgInitially::Immediate,
        }
    }

    /// Creates a deferrable label with initial timing.
    #[must_use]
    pub const fn deferrable(initially: PgInitially) -> Self {
        Self {
            deferrable: true,
            initially,
        }
    }

    /// Returns `true` when the constraint is deferrable.
    #[must_use]
    pub const fn is_deferrable(self) -> bool {
        self.deferrable
    }

    /// Returns the initial timing label.
    #[must_use]
    pub const fn initially(self) -> PgInitially {
        self.initially
    }
}

impl fmt::Display for PgDeferrability {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.deferrable {
            write!(formatter, "DEFERRABLE {}", self.initially)
        } else {
            formatter.write_str("NOT DEFERRABLE")
        }
    }
}

/// PostgreSQL constraint metadata without database introspection.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgConstraint {
    kind: PgConstraintKind,
    name: Option<PgConstraintName>,
    columns: Vec<PgColumnName>,
    referenced_table: Option<PgTableRef>,
    expression: Option<String>,
    deferrability: PgDeferrability,
}

impl PgConstraint {
    /// Creates constraint metadata from a kind.
    #[must_use]
    pub const fn new(kind: PgConstraintKind) -> Self {
        Self {
            kind,
            name: None,
            columns: Vec::new(),
            referenced_table: None,
            expression: None,
            deferrability: PgDeferrability::not_deferrable(),
        }
    }

    /// Adds a constraint name.
    #[must_use]
    pub fn with_name(mut self, name: PgConstraintName) -> Self {
        self.name = Some(name);
        self
    }

    /// Adds constrained columns.
    #[must_use]
    pub fn with_columns(mut self, columns: Vec<PgColumnName>) -> Self {
        self.columns = columns;
        self
    }

    /// Adds referenced table metadata for a foreign key.
    #[must_use]
    pub fn with_referenced_table(mut self, table: PgTableRef) -> Self {
        self.referenced_table = Some(table);
        self
    }

    /// Adds an expression label for check or exclusion metadata without parsing SQL.
    ///
    /// # Errors
    ///
    /// Returns [`PgConstraintError`] when the label is empty or contains control characters.
    pub fn with_expression(
        mut self,
        expression: impl AsRef<str>,
    ) -> Result<Self, PgConstraintError> {
        self.expression = Some(validate_expression(expression.as_ref())?.to_owned());
        Ok(self)
    }

    /// Sets the deferrability metadata.
    #[must_use]
    pub const fn with_deferrability(mut self, deferrability: PgDeferrability) -> Self {
        self.deferrability = deferrability;
        self
    }

    /// Returns the constraint kind.
    #[must_use]
    pub const fn kind(&self) -> PgConstraintKind {
        self.kind
    }

    /// Returns the optional constraint name.
    #[must_use]
    pub const fn name(&self) -> Option<&PgConstraintName> {
        self.name.as_ref()
    }

    /// Returns the constrained columns.
    #[must_use]
    pub fn columns(&self) -> &[PgColumnName] {
        &self.columns
    }

    /// Returns the optional referenced table.
    #[must_use]
    pub const fn referenced_table(&self) -> Option<&PgTableRef> {
        self.referenced_table.as_ref()
    }

    /// Returns the optional expression label.
    #[must_use]
    pub fn expression(&self) -> Option<&str> {
        self.expression.as_deref()
    }

    /// Returns the deferrability metadata.
    #[must_use]
    pub const fn deferrability(&self) -> PgDeferrability {
        self.deferrability
    }
}

impl fmt::Display for PgConstraint {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(name) = &self.name {
            write!(formatter, "CONSTRAINT {name} ")?;
        }
        write!(formatter, "{}", self.kind)
    }
}

/// Error returned when PostgreSQL constraint metadata is invalid.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PgConstraintError {
    Empty,
    UnknownKind,
    ControlCharacter,
    Identifier(PgIdentifierError),
}

impl fmt::Display for PgConstraintError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => formatter.write_str("PostgreSQL constraint label cannot be empty"),
            Self::UnknownKind => formatter.write_str("unknown PostgreSQL constraint kind"),
            Self::ControlCharacter => {
                formatter.write_str("PostgreSQL constraint label cannot contain control characters")
            }
            Self::Identifier(error) => {
                write!(
                    formatter,
                    "invalid PostgreSQL constraint identifier: {error}"
                )
            }
        }
    }
}

impl Error for PgConstraintError {}

fn normalized_label(input: &str) -> Result<String, PgConstraintError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(PgConstraintError::Empty);
    }
    Ok(trimmed
        .replace('_', " ")
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
        .to_ascii_lowercase())
}

fn validate_expression(input: &str) -> Result<&str, PgConstraintError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(PgConstraintError::Empty);
    }
    if trimmed.chars().any(char::is_control) {
        return Err(PgConstraintError::ControlCharacter);
    }
    Ok(trimmed)
}

#[cfg(test)]
mod tests {
    use super::{
        PgConstraint, PgConstraintError, PgConstraintKind, PgConstraintName, PgDeferrability,
        PgInitially,
    };
    use use_pg_column::PgColumnName;

    #[test]
    fn parses_and_renders_constraint_kinds() -> Result<(), PgConstraintError> {
        assert_eq!(
            "primary key".parse::<PgConstraintKind>()?,
            PgConstraintKind::PrimaryKey
        );
        assert_eq!(
            "exclude".parse::<PgConstraintKind>()?,
            PgConstraintKind::Exclusion
        );
        assert_eq!(PgConstraintKind::ForeignKey.to_string(), "FOREIGN KEY");
        Ok(())
    }

    #[test]
    fn creates_primary_key_metadata() -> Result<(), PgConstraintError> {
        let constraint = PgConstraint::new(PgConstraintKind::PrimaryKey)
            .with_name(PgConstraintName::new("users_pkey")?)
            .with_columns(vec![PgColumnName::new("id").expect("valid column")]);

        assert_eq!(constraint.to_string(), "CONSTRAINT users_pkey PRIMARY KEY");
        assert_eq!(constraint.columns().len(), 1);
        Ok(())
    }

    #[test]
    fn tracks_deferrability() {
        let deferrability = PgDeferrability::deferrable(PgInitially::Deferred);
        assert!(deferrability.is_deferrable());
        assert_eq!(deferrability.to_string(), "DEFERRABLE INITIALLY DEFERRED");
    }

    #[test]
    fn stores_check_expression_labels() -> Result<(), PgConstraintError> {
        let constraint =
            PgConstraint::new(PgConstraintKind::Check).with_expression("amount > 0")?;
        assert_eq!(constraint.expression(), Some("amount > 0"));
        Ok(())
    }
}