use-pg-sequence 0.1.0

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

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

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

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

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

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

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

impl FromStr for PgSequenceName {
    type Err = PgSequenceError;

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

/// PostgreSQL sequence ownership metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgSequenceOwner {
    /// No ownership metadata.
    Unowned,
    /// Owned by a table column label.
    OwnedBy {
        /// Referenced table metadata.
        table: PgTableRef,
        /// Referenced column identifier.
        column: PgIdentifier,
    },
}

impl PgSequenceOwner {
    /// Creates an ownership label from a table and column.
    #[must_use]
    pub const fn owned_by(table: PgTableRef, column: PgIdentifier) -> Self {
        Self::OwnedBy { table, column }
    }

    /// Returns `true` when ownership metadata points to a table column.
    #[must_use]
    pub const fn is_owned(&self) -> bool {
        matches!(self, Self::OwnedBy { .. })
    }
}

impl fmt::Display for PgSequenceOwner {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Unowned => formatter.write_str("unowned"),
            Self::OwnedBy { table, column } => write!(formatter, "owned by {table}.{column}"),
        }
    }
}

/// PostgreSQL sequence options using simple numeric primitives.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgSequenceOptions {
    increment: i64,
    min_value: Option<i64>,
    max_value: Option<i64>,
    start: i64,
    cache: u64,
    cycle: bool,
}

impl Default for PgSequenceOptions {
    fn default() -> Self {
        Self {
            increment: 1,
            min_value: None,
            max_value: None,
            start: 1,
            cache: 1,
            cycle: false,
        }
    }
}

impl PgSequenceOptions {
    /// Sets the increment.
    ///
    /// # Errors
    ///
    /// Returns [`PgSequenceError::InvalidIncrement`] when `increment` is zero.
    pub const fn with_increment(mut self, increment: i64) -> Result<Self, PgSequenceError> {
        if increment == 0 {
            return Err(PgSequenceError::InvalidIncrement);
        }
        self.increment = increment;
        Ok(self)
    }

    /// Sets min and max labels.
    #[must_use]
    pub const fn with_bounds(mut self, min_value: Option<i64>, max_value: Option<i64>) -> Self {
        self.min_value = min_value;
        self.max_value = max_value;
        self
    }

    /// Sets the start value.
    #[must_use]
    pub const fn with_start(mut self, start: i64) -> Self {
        self.start = start;
        self
    }

    /// Sets the cache value.
    ///
    /// # Errors
    ///
    /// Returns [`PgSequenceError::InvalidCache`] when `cache` is zero.
    pub const fn with_cache(mut self, cache: u64) -> Result<Self, PgSequenceError> {
        if cache == 0 {
            return Err(PgSequenceError::InvalidCache);
        }
        self.cache = cache;
        Ok(self)
    }

    /// Sets the cycle flag.
    #[must_use]
    pub const fn with_cycle(mut self, cycle: bool) -> Self {
        self.cycle = cycle;
        self
    }

    /// Returns the increment.
    #[must_use]
    pub const fn increment(self) -> i64 {
        self.increment
    }

    /// Returns the min value label.
    #[must_use]
    pub const fn min_value(self) -> Option<i64> {
        self.min_value
    }

    /// Returns the max value label.
    #[must_use]
    pub const fn max_value(self) -> Option<i64> {
        self.max_value
    }

    /// Returns the start value.
    #[must_use]
    pub const fn start(self) -> i64 {
        self.start
    }

    /// Returns the cache value.
    #[must_use]
    pub const fn cache(self) -> u64 {
        self.cache
    }

    /// Returns the cycle flag.
    #[must_use]
    pub const fn cycle(self) -> bool {
        self.cycle
    }
}

/// PostgreSQL sequence metadata.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgSequence {
    name: PgSequenceName,
    options: PgSequenceOptions,
    owner: PgSequenceOwner,
}

impl PgSequence {
    /// Creates sequence metadata from a name.
    #[must_use]
    pub fn new(name: PgSequenceName) -> Self {
        Self {
            name,
            options: PgSequenceOptions::default(),
            owner: PgSequenceOwner::Unowned,
        }
    }

    /// Sets sequence options.
    #[must_use]
    pub const fn with_options(mut self, options: PgSequenceOptions) -> Self {
        self.options = options;
        self
    }

    /// Sets ownership metadata.
    #[must_use]
    pub fn with_owner(mut self, owner: PgSequenceOwner) -> Self {
        self.owner = owner;
        self
    }

    /// Returns the sequence name.
    #[must_use]
    pub const fn name(&self) -> &PgSequenceName {
        &self.name
    }

    /// Returns sequence options.
    #[must_use]
    pub const fn options(&self) -> PgSequenceOptions {
        self.options
    }

    /// Returns ownership metadata.
    #[must_use]
    pub const fn owner(&self) -> &PgSequenceOwner {
        &self.owner
    }
}

/// Error returned when PostgreSQL sequence metadata is invalid.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PgSequenceError {
    InvalidIncrement,
    InvalidCache,
    Identifier(PgIdentifierError),
}

impl fmt::Display for PgSequenceError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidIncrement => {
                formatter.write_str("PostgreSQL sequence increment cannot be zero")
            }
            Self::InvalidCache => {
                formatter.write_str("PostgreSQL sequence cache must be greater than zero")
            }
            Self::Identifier(error) => {
                write!(formatter, "invalid PostgreSQL sequence identifier: {error}")
            }
        }
    }
}

impl Error for PgSequenceError {}

#[cfg(test)]
mod tests {
    use super::{PgSequence, PgSequenceError, PgSequenceName, PgSequenceOptions, PgSequenceOwner};
    use use_pg_identifier::PgIdentifier;
    use use_pg_schema::PgSchemaName;
    use use_pg_table::{PgTableName, PgTableRef};

    #[test]
    fn uses_simple_default_options() -> Result<(), PgSequenceError> {
        let sequence = PgSequence::new(PgSequenceName::new("users_id_seq")?);
        assert_eq!(sequence.options().increment(), 1);
        assert_eq!(sequence.options().cache(), 1);
        assert!(!sequence.options().cycle());
        Ok(())
    }

    #[test]
    fn validates_custom_options() -> Result<(), PgSequenceError> {
        let options = PgSequenceOptions::default()
            .with_increment(10)?
            .with_bounds(Some(1), Some(1000))
            .with_start(10)
            .with_cache(20)?
            .with_cycle(true);
        assert_eq!(options.increment(), 10);
        assert_eq!(options.min_value(), Some(1));
        assert_eq!(options.max_value(), Some(1000));
        assert!(options.cycle());
        assert_eq!(
            PgSequenceOptions::default().with_increment(0),
            Err(PgSequenceError::InvalidIncrement)
        );
        Ok(())
    }

    #[test]
    fn tracks_sequence_ownership() -> Result<(), Box<dyn std::error::Error>> {
        let table = PgTableRef::qualified(PgSchemaName::public(), PgTableName::new("users")?);
        let owner = PgSequenceOwner::owned_by(table, PgIdentifier::new("id")?);
        assert!(owner.is_owned());
        assert_eq!(owner.to_string(), "owned by public.users.id");
        Ok(())
    }
}