Skip to main content

use_pg_column/
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_identifier::{PgIdentifier, PgIdentifierError};
8use use_pg_type::{PgBuiltInType, PgTypeName};
9
10/// PostgreSQL column name primitive.
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgColumnName(PgIdentifier);
13
14impl PgColumnName {
15    /// Creates a column name.
16    ///
17    /// # Errors
18    ///
19    /// Returns [`PgColumnError`] when identifier validation fails.
20    pub fn new(input: impl AsRef<str>) -> Result<Self, PgColumnError> {
21        PgIdentifier::new(input)
22            .map(Self)
23            .map_err(PgColumnError::Identifier)
24    }
25
26    /// Returns the column name text.
27    #[must_use]
28    pub fn as_str(&self) -> &str {
29        self.0.as_str()
30    }
31}
32
33impl AsRef<str> for PgColumnName {
34    fn as_ref(&self) -> &str {
35        self.as_str()
36    }
37}
38
39impl fmt::Display for PgColumnName {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        self.0.fmt(formatter)
42    }
43}
44
45impl FromStr for PgColumnName {
46    type Err = PgColumnError;
47
48    fn from_str(input: &str) -> Result<Self, Self::Err> {
49        Self::new(input)
50    }
51}
52
53impl TryFrom<&str> for PgColumnName {
54    type Error = PgColumnError;
55
56    fn try_from(value: &str) -> Result<Self, Self::Error> {
57        Self::new(value)
58    }
59}
60
61/// A PostgreSQL column default expression label.
62#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct PgColumnDefault(String);
64
65impl PgColumnDefault {
66    /// Creates a default-expression label without parsing SQL.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`PgColumnError`] when the label is empty or contains control characters.
71    pub fn new(input: impl AsRef<str>) -> Result<Self, PgColumnError> {
72        validate_label(input.as_ref(), PgColumnError::EmptyDefault)
73            .map(|value| Self(value.to_owned()))
74    }
75
76    /// Returns the stored default label.
77    #[must_use]
78    pub fn as_str(&self) -> &str {
79        &self.0
80    }
81}
82
83impl fmt::Display for PgColumnDefault {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter.write_str(self.as_str())
86    }
87}
88
89/// PostgreSQL column nullability labels.
90#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
91pub enum PgNullability {
92    /// The column accepts null values.
93    #[default]
94    Nullable,
95    /// The column is marked `NOT NULL`.
96    NotNull,
97}
98
99impl PgNullability {
100    /// Returns `true` when the column accepts null values.
101    #[must_use]
102    pub const fn is_nullable(self) -> bool {
103        matches!(self, Self::Nullable)
104    }
105
106    /// Returns a stable label.
107    #[must_use]
108    pub const fn as_str(self) -> &'static str {
109        match self {
110            Self::Nullable => "NULL",
111            Self::NotNull => "NOT NULL",
112        }
113    }
114}
115
116impl fmt::Display for PgNullability {
117    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118        formatter.write_str(self.as_str())
119    }
120}
121
122impl FromStr for PgNullability {
123    type Err = PgColumnError;
124
125    fn from_str(input: &str) -> Result<Self, Self::Err> {
126        match normalized_label(input, PgColumnError::UnknownNullability)?.as_str() {
127            "null" | "nullable" => Ok(Self::Nullable),
128            "not null" | "notnull" | "required" => Ok(Self::NotNull),
129            _ => Err(PgColumnError::UnknownNullability),
130        }
131    }
132}
133
134/// PostgreSQL generated-column labels.
135#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
136pub enum PgGeneratedKind {
137    /// Generated column stored on disk.
138    Stored,
139    /// Virtual generated-column label for compatibility metadata.
140    Virtual,
141}
142
143impl PgGeneratedKind {
144    /// Returns a stable label.
145    #[must_use]
146    pub const fn as_str(self) -> &'static str {
147        match self {
148            Self::Stored => "STORED",
149            Self::Virtual => "VIRTUAL",
150        }
151    }
152}
153
154impl fmt::Display for PgGeneratedKind {
155    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156        formatter.write_str(self.as_str())
157    }
158}
159
160/// PostgreSQL identity-column labels.
161#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
162pub enum PgIdentityKind {
163    /// `GENERATED ALWAYS AS IDENTITY`.
164    Always,
165    /// `GENERATED BY DEFAULT AS IDENTITY`.
166    ByDefault,
167}
168
169impl PgIdentityKind {
170    /// Returns a stable label.
171    #[must_use]
172    pub const fn as_str(self) -> &'static str {
173        match self {
174            Self::Always => "ALWAYS",
175            Self::ByDefault => "BY DEFAULT",
176        }
177    }
178}
179
180impl fmt::Display for PgIdentityKind {
181    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182        formatter.write_str(self.as_str())
183    }
184}
185
186/// PostgreSQL column metadata.
187#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
188pub struct PgColumn {
189    name: PgColumnName,
190    type_name: PgTypeName,
191    nullability: PgNullability,
192    default: Option<PgColumnDefault>,
193    generated: Option<PgGeneratedKind>,
194    identity: Option<PgIdentityKind>,
195}
196
197impl PgColumn {
198    /// Creates column metadata from a name and type name.
199    #[must_use]
200    pub const fn new(name: PgColumnName, type_name: PgTypeName) -> Self {
201        Self {
202            name,
203            type_name,
204            nullability: PgNullability::Nullable,
205            default: None,
206            generated: None,
207            identity: None,
208        }
209    }
210
211    /// Creates column metadata using a built-in PostgreSQL type.
212    #[must_use]
213    pub fn with_built_in_type(name: PgColumnName, ty: PgBuiltInType) -> Self {
214        Self::new(name, PgTypeName::built_in(ty))
215    }
216
217    /// Sets the nullability label.
218    #[must_use]
219    pub const fn with_nullability(mut self, nullability: PgNullability) -> Self {
220        self.nullability = nullability;
221        self
222    }
223
224    /// Sets a default-expression label.
225    #[must_use]
226    pub fn with_default(mut self, default: PgColumnDefault) -> Self {
227        self.default = Some(default);
228        self
229    }
230
231    /// Sets the generated-column label.
232    #[must_use]
233    pub const fn with_generated(mut self, generated: PgGeneratedKind) -> Self {
234        self.generated = Some(generated);
235        self
236    }
237
238    /// Sets the identity-column label.
239    #[must_use]
240    pub const fn with_identity(mut self, identity: PgIdentityKind) -> Self {
241        self.identity = Some(identity);
242        self
243    }
244
245    /// Returns the column name.
246    #[must_use]
247    pub const fn name(&self) -> &PgColumnName {
248        &self.name
249    }
250
251    /// Returns the type name.
252    #[must_use]
253    pub const fn type_name(&self) -> &PgTypeName {
254        &self.type_name
255    }
256
257    /// Returns the nullability label.
258    #[must_use]
259    pub const fn nullability(&self) -> PgNullability {
260        self.nullability
261    }
262
263    /// Returns the optional default label.
264    #[must_use]
265    pub const fn default(&self) -> Option<&PgColumnDefault> {
266        self.default.as_ref()
267    }
268
269    /// Returns the optional generated-column label.
270    #[must_use]
271    pub const fn generated(&self) -> Option<PgGeneratedKind> {
272        self.generated
273    }
274
275    /// Returns the optional identity-column label.
276    #[must_use]
277    pub const fn identity(&self) -> Option<PgIdentityKind> {
278        self.identity
279    }
280}
281
282/// Error returned when PostgreSQL column metadata is invalid.
283#[derive(Clone, Debug, Eq, PartialEq)]
284pub enum PgColumnError {
285    EmptyDefault,
286    ControlCharacter,
287    UnknownNullability,
288    Identifier(PgIdentifierError),
289}
290
291impl fmt::Display for PgColumnError {
292    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
293        match self {
294            Self::EmptyDefault => formatter.write_str("PostgreSQL column default cannot be empty"),
295            Self::ControlCharacter => {
296                formatter.write_str("PostgreSQL column label cannot contain control characters")
297            }
298            Self::UnknownNullability => {
299                formatter.write_str("unknown PostgreSQL column nullability label")
300            }
301            Self::Identifier(error) => {
302                write!(formatter, "invalid PostgreSQL column identifier: {error}")
303            }
304        }
305    }
306}
307
308impl Error for PgColumnError {}
309
310fn validate_label(input: &str, empty_error: PgColumnError) -> Result<&str, PgColumnError> {
311    let trimmed = input.trim();
312    if trimmed.is_empty() {
313        return Err(empty_error);
314    }
315    if trimmed.chars().any(char::is_control) {
316        return Err(PgColumnError::ControlCharacter);
317    }
318    Ok(trimmed)
319}
320
321fn normalized_label(input: &str, empty_error: PgColumnError) -> Result<String, PgColumnError> {
322    let trimmed = validate_label(input, empty_error)?;
323    Ok(trimmed
324        .replace('_', " ")
325        .split_whitespace()
326        .collect::<Vec<_>>()
327        .join(" ")
328        .to_ascii_lowercase())
329}
330
331#[cfg(test)]
332mod tests {
333    use super::{
334        PgColumn, PgColumnDefault, PgColumnError, PgColumnName, PgGeneratedKind, PgIdentityKind,
335        PgNullability,
336    };
337    use use_pg_type::PgBuiltInType;
338
339    #[test]
340    fn creates_column_metadata() -> Result<(), PgColumnError> {
341        let column = PgColumn::with_built_in_type(PgColumnName::new("id")?, PgBuiltInType::BigInt)
342            .with_nullability(PgNullability::NotNull)
343            .with_identity(PgIdentityKind::Always);
344
345        assert_eq!(column.name().as_str(), "id");
346        assert_eq!(column.type_name().as_str(), "bigint");
347        assert_eq!(column.nullability(), PgNullability::NotNull);
348        assert_eq!(column.identity(), Some(PgIdentityKind::Always));
349        Ok(())
350    }
351
352    #[test]
353    fn stores_default_and_generated_labels() -> Result<(), PgColumnError> {
354        let column = PgColumn::with_built_in_type(
355            PgColumnName::new("created_at")?,
356            PgBuiltInType::TimestampTz,
357        )
358        .with_default(PgColumnDefault::new("now()")?)
359        .with_generated(PgGeneratedKind::Stored);
360
361        assert_eq!(column.default().map(PgColumnDefault::as_str), Some("now()"));
362        assert_eq!(column.generated(), Some(PgGeneratedKind::Stored));
363        assert_eq!(PgNullability::NotNull.to_string(), "NOT NULL");
364        Ok(())
365    }
366
367    #[test]
368    fn parses_nullability_labels() -> Result<(), PgColumnError> {
369        assert_eq!(
370            "nullable".parse::<PgNullability>()?,
371            PgNullability::Nullable
372        );
373        assert_eq!("not null".parse::<PgNullability>()?, PgNullability::NotNull);
374        assert!(PgNullability::Nullable.is_nullable());
375        Ok(())
376    }
377}