#![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;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgConstraintName(PgIdentifier);
impl PgConstraintName {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgConstraintError> {
PgIdentifier::new(input)
.map(Self)
.map_err(PgConstraintError::Identifier)
}
#[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)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgConstraintKind {
#[default]
PrimaryKey,
ForeignKey,
Unique,
Check,
Exclusion,
NotNull,
}
impl PgConstraintKind {
#[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),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgInitially {
#[default]
Immediate,
Deferred,
}
impl PgInitially {
#[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())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgDeferrability {
deferrable: bool,
initially: PgInitially,
}
impl PgDeferrability {
#[must_use]
pub const fn not_deferrable() -> Self {
Self {
deferrable: false,
initially: PgInitially::Immediate,
}
}
#[must_use]
pub const fn deferrable(initially: PgInitially) -> Self {
Self {
deferrable: true,
initially,
}
}
#[must_use]
pub const fn is_deferrable(self) -> bool {
self.deferrable
}
#[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")
}
}
}
#[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 {
#[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(),
}
}
#[must_use]
pub fn with_name(mut self, name: PgConstraintName) -> Self {
self.name = Some(name);
self
}
#[must_use]
pub fn with_columns(mut self, columns: Vec<PgColumnName>) -> Self {
self.columns = columns;
self
}
#[must_use]
pub fn with_referenced_table(mut self, table: PgTableRef) -> Self {
self.referenced_table = Some(table);
self
}
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)
}
#[must_use]
pub const fn with_deferrability(mut self, deferrability: PgDeferrability) -> Self {
self.deferrability = deferrability;
self
}
#[must_use]
pub const fn kind(&self) -> PgConstraintKind {
self.kind
}
#[must_use]
pub const fn name(&self) -> Option<&PgConstraintName> {
self.name.as_ref()
}
#[must_use]
pub fn columns(&self) -> &[PgColumnName] {
&self.columns
}
#[must_use]
pub const fn referenced_table(&self) -> Option<&PgTableRef> {
self.referenced_table.as_ref()
}
#[must_use]
pub fn expression(&self) -> Option<&str> {
self.expression.as_deref()
}
#[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)
}
}
#[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(())
}
}