Skip to main content

use_pg_constraint/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_pg_column::PgColumnName;
8use use_pg_identifier::{PgIdentifier, PgIdentifierError};
9use use_pg_table::PgTableRef;
10
11/// PostgreSQL constraint name primitive.
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct PgConstraintName(PgIdentifier);
14
15impl PgConstraintName {
16    /// Creates a constraint name.
17    ///
18    /// # Errors
19    ///
20    /// Returns [`PgConstraintError`] when identifier validation fails.
21    pub fn new(input: impl AsRef<str>) -> Result<Self, PgConstraintError> {
22        PgIdentifier::new(input)
23            .map(Self)
24            .map_err(PgConstraintError::Identifier)
25    }
26
27    /// Returns the constraint name text.
28    #[must_use]
29    pub fn as_str(&self) -> &str {
30        self.0.as_str()
31    }
32}
33
34impl fmt::Display for PgConstraintName {
35    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36        self.0.fmt(formatter)
37    }
38}
39
40impl FromStr for PgConstraintName {
41    type Err = PgConstraintError;
42
43    fn from_str(input: &str) -> Result<Self, Self::Err> {
44        Self::new(input)
45    }
46}
47
48/// PostgreSQL constraint kind labels.
49#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub enum PgConstraintKind {
51    /// Primary key constraint.
52    #[default]
53    PrimaryKey,
54    /// Foreign key constraint.
55    ForeignKey,
56    /// Unique constraint.
57    Unique,
58    /// Check constraint.
59    Check,
60    /// Exclusion constraint.
61    Exclusion,
62    /// Not-null constraint label.
63    NotNull,
64}
65
66impl PgConstraintKind {
67    /// Returns the stable PostgreSQL constraint label.
68    #[must_use]
69    pub const fn as_str(self) -> &'static str {
70        match self {
71            Self::PrimaryKey => "PRIMARY KEY",
72            Self::ForeignKey => "FOREIGN KEY",
73            Self::Unique => "UNIQUE",
74            Self::Check => "CHECK",
75            Self::Exclusion => "EXCLUDE",
76            Self::NotNull => "NOT NULL",
77        }
78    }
79}
80
81impl fmt::Display for PgConstraintKind {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        formatter.write_str(self.as_str())
84    }
85}
86
87impl FromStr for PgConstraintKind {
88    type Err = PgConstraintError;
89
90    fn from_str(input: &str) -> Result<Self, Self::Err> {
91        match normalized_label(input)?.as_str() {
92            "primary" | "primary key" => Ok(Self::PrimaryKey),
93            "foreign" | "foreign key" => Ok(Self::ForeignKey),
94            "unique" => Ok(Self::Unique),
95            "check" => Ok(Self::Check),
96            "exclude" | "exclusion" => Ok(Self::Exclusion),
97            "not null" | "notnull" => Ok(Self::NotNull),
98            _ => Err(PgConstraintError::UnknownKind),
99        }
100    }
101}
102
103/// Initial timing label for deferrable PostgreSQL constraints.
104#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub enum PgInitially {
106    /// Initially immediate timing.
107    #[default]
108    Immediate,
109    /// Initially deferred timing.
110    Deferred,
111}
112
113impl PgInitially {
114    /// Returns a stable label.
115    #[must_use]
116    pub const fn as_str(self) -> &'static str {
117        match self {
118            Self::Immediate => "INITIALLY IMMEDIATE",
119            Self::Deferred => "INITIALLY DEFERRED",
120        }
121    }
122}
123
124impl fmt::Display for PgInitially {
125    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
126        formatter.write_str(self.as_str())
127    }
128}
129
130/// PostgreSQL constraint deferrability metadata.
131#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub struct PgDeferrability {
133    deferrable: bool,
134    initially: PgInitially,
135}
136
137impl PgDeferrability {
138    /// Creates a not-deferrable label.
139    #[must_use]
140    pub const fn not_deferrable() -> Self {
141        Self {
142            deferrable: false,
143            initially: PgInitially::Immediate,
144        }
145    }
146
147    /// Creates a deferrable label with initial timing.
148    #[must_use]
149    pub const fn deferrable(initially: PgInitially) -> Self {
150        Self {
151            deferrable: true,
152            initially,
153        }
154    }
155
156    /// Returns `true` when the constraint is deferrable.
157    #[must_use]
158    pub const fn is_deferrable(self) -> bool {
159        self.deferrable
160    }
161
162    /// Returns the initial timing label.
163    #[must_use]
164    pub const fn initially(self) -> PgInitially {
165        self.initially
166    }
167}
168
169impl fmt::Display for PgDeferrability {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        if self.deferrable {
172            write!(formatter, "DEFERRABLE {}", self.initially)
173        } else {
174            formatter.write_str("NOT DEFERRABLE")
175        }
176    }
177}
178
179/// PostgreSQL constraint metadata without database introspection.
180#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
181pub struct PgConstraint {
182    kind: PgConstraintKind,
183    name: Option<PgConstraintName>,
184    columns: Vec<PgColumnName>,
185    referenced_table: Option<PgTableRef>,
186    expression: Option<String>,
187    deferrability: PgDeferrability,
188}
189
190impl PgConstraint {
191    /// Creates constraint metadata from a kind.
192    #[must_use]
193    pub const fn new(kind: PgConstraintKind) -> Self {
194        Self {
195            kind,
196            name: None,
197            columns: Vec::new(),
198            referenced_table: None,
199            expression: None,
200            deferrability: PgDeferrability::not_deferrable(),
201        }
202    }
203
204    /// Adds a constraint name.
205    #[must_use]
206    pub fn with_name(mut self, name: PgConstraintName) -> Self {
207        self.name = Some(name);
208        self
209    }
210
211    /// Adds constrained columns.
212    #[must_use]
213    pub fn with_columns(mut self, columns: Vec<PgColumnName>) -> Self {
214        self.columns = columns;
215        self
216    }
217
218    /// Adds referenced table metadata for a foreign key.
219    #[must_use]
220    pub fn with_referenced_table(mut self, table: PgTableRef) -> Self {
221        self.referenced_table = Some(table);
222        self
223    }
224
225    /// Adds an expression label for check or exclusion metadata without parsing SQL.
226    ///
227    /// # Errors
228    ///
229    /// Returns [`PgConstraintError`] when the label is empty or contains control characters.
230    pub fn with_expression(
231        mut self,
232        expression: impl AsRef<str>,
233    ) -> Result<Self, PgConstraintError> {
234        self.expression = Some(validate_expression(expression.as_ref())?.to_owned());
235        Ok(self)
236    }
237
238    /// Sets the deferrability metadata.
239    #[must_use]
240    pub const fn with_deferrability(mut self, deferrability: PgDeferrability) -> Self {
241        self.deferrability = deferrability;
242        self
243    }
244
245    /// Returns the constraint kind.
246    #[must_use]
247    pub const fn kind(&self) -> PgConstraintKind {
248        self.kind
249    }
250
251    /// Returns the optional constraint name.
252    #[must_use]
253    pub const fn name(&self) -> Option<&PgConstraintName> {
254        self.name.as_ref()
255    }
256
257    /// Returns the constrained columns.
258    #[must_use]
259    pub fn columns(&self) -> &[PgColumnName] {
260        &self.columns
261    }
262
263    /// Returns the optional referenced table.
264    #[must_use]
265    pub const fn referenced_table(&self) -> Option<&PgTableRef> {
266        self.referenced_table.as_ref()
267    }
268
269    /// Returns the optional expression label.
270    #[must_use]
271    pub fn expression(&self) -> Option<&str> {
272        self.expression.as_deref()
273    }
274
275    /// Returns the deferrability metadata.
276    #[must_use]
277    pub const fn deferrability(&self) -> PgDeferrability {
278        self.deferrability
279    }
280}
281
282impl fmt::Display for PgConstraint {
283    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284        if let Some(name) = &self.name {
285            write!(formatter, "CONSTRAINT {name} ")?;
286        }
287        write!(formatter, "{}", self.kind)
288    }
289}
290
291/// Error returned when PostgreSQL constraint metadata is invalid.
292#[derive(Clone, Debug, Eq, PartialEq)]
293pub enum PgConstraintError {
294    Empty,
295    UnknownKind,
296    ControlCharacter,
297    Identifier(PgIdentifierError),
298}
299
300impl fmt::Display for PgConstraintError {
301    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302        match self {
303            Self::Empty => formatter.write_str("PostgreSQL constraint label cannot be empty"),
304            Self::UnknownKind => formatter.write_str("unknown PostgreSQL constraint kind"),
305            Self::ControlCharacter => {
306                formatter.write_str("PostgreSQL constraint label cannot contain control characters")
307            }
308            Self::Identifier(error) => {
309                write!(
310                    formatter,
311                    "invalid PostgreSQL constraint identifier: {error}"
312                )
313            }
314        }
315    }
316}
317
318impl Error for PgConstraintError {}
319
320fn normalized_label(input: &str) -> Result<String, PgConstraintError> {
321    let trimmed = input.trim();
322    if trimmed.is_empty() {
323        return Err(PgConstraintError::Empty);
324    }
325    Ok(trimmed
326        .replace('_', " ")
327        .split_whitespace()
328        .collect::<Vec<_>>()
329        .join(" ")
330        .to_ascii_lowercase())
331}
332
333fn validate_expression(input: &str) -> Result<&str, PgConstraintError> {
334    let trimmed = input.trim();
335    if trimmed.is_empty() {
336        return Err(PgConstraintError::Empty);
337    }
338    if trimmed.chars().any(char::is_control) {
339        return Err(PgConstraintError::ControlCharacter);
340    }
341    Ok(trimmed)
342}
343
344#[cfg(test)]
345mod tests {
346    use super::{
347        PgConstraint, PgConstraintError, PgConstraintKind, PgConstraintName, PgDeferrability,
348        PgInitially,
349    };
350    use use_pg_column::PgColumnName;
351
352    #[test]
353    fn parses_and_renders_constraint_kinds() -> Result<(), PgConstraintError> {
354        assert_eq!(
355            "primary key".parse::<PgConstraintKind>()?,
356            PgConstraintKind::PrimaryKey
357        );
358        assert_eq!(
359            "exclude".parse::<PgConstraintKind>()?,
360            PgConstraintKind::Exclusion
361        );
362        assert_eq!(PgConstraintKind::ForeignKey.to_string(), "FOREIGN KEY");
363        Ok(())
364    }
365
366    #[test]
367    fn creates_primary_key_metadata() -> Result<(), PgConstraintError> {
368        let constraint = PgConstraint::new(PgConstraintKind::PrimaryKey)
369            .with_name(PgConstraintName::new("users_pkey")?)
370            .with_columns(vec![PgColumnName::new("id").expect("valid column")]);
371
372        assert_eq!(constraint.to_string(), "CONSTRAINT users_pkey PRIMARY KEY");
373        assert_eq!(constraint.columns().len(), 1);
374        Ok(())
375    }
376
377    #[test]
378    fn tracks_deferrability() {
379        let deferrability = PgDeferrability::deferrable(PgInitially::Deferred);
380        assert!(deferrability.is_deferrable());
381        assert_eq!(deferrability.to_string(), "DEFERRABLE INITIALLY DEFERRED");
382    }
383
384    #[test]
385    fn stores_check_expression_labels() -> Result<(), PgConstraintError> {
386        let constraint =
387            PgConstraint::new(PgConstraintKind::Check).with_expression("amount > 0")?;
388        assert_eq!(constraint.expression(), Some("amount > 0"));
389        Ok(())
390    }
391}