Skip to main content

use_pg_table/
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_schema::PgSchemaName;
9
10/// PostgreSQL table name primitive.
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct PgTableName(PgIdentifier);
13
14impl PgTableName {
15    /// Creates a table name.
16    ///
17    /// # Errors
18    ///
19    /// Returns [`PgTableError`] when identifier validation fails.
20    pub fn new(input: impl AsRef<str>) -> Result<Self, PgTableError> {
21        PgIdentifier::new(input)
22            .map(Self)
23            .map_err(PgTableError::Identifier)
24    }
25
26    /// Returns the table name text.
27    #[must_use]
28    pub fn as_str(&self) -> &str {
29        self.0.as_str()
30    }
31}
32
33impl AsRef<str> for PgTableName {
34    fn as_ref(&self) -> &str {
35        self.as_str()
36    }
37}
38
39impl fmt::Display for PgTableName {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        self.0.fmt(formatter)
42    }
43}
44
45impl FromStr for PgTableName {
46    type Err = PgTableError;
47
48    fn from_str(input: &str) -> Result<Self, Self::Err> {
49        Self::new(input)
50    }
51}
52
53impl TryFrom<&str> for PgTableName {
54    type Error = PgTableError;
55
56    fn try_from(value: &str) -> Result<Self, Self::Error> {
57        Self::new(value)
58    }
59}
60
61/// PostgreSQL table-like object kinds.
62#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub enum PgTableKind {
64    /// Ordinary heap table.
65    #[default]
66    Ordinary,
67    /// Declarative partitioned table.
68    Partitioned,
69    /// Foreign table backed by a foreign data wrapper.
70    Foreign,
71    /// Temporary table object.
72    Temporary,
73    /// PostgreSQL view.
74    View,
75    /// PostgreSQL materialized view.
76    MaterializedView,
77    /// Toast table metadata label.
78    Toast,
79}
80
81impl PgTableKind {
82    /// Returns a stable label.
83    #[must_use]
84    pub const fn as_str(self) -> &'static str {
85        match self {
86            Self::Ordinary => "ordinary table",
87            Self::Partitioned => "partitioned table",
88            Self::Foreign => "foreign table",
89            Self::Temporary => "temporary table",
90            Self::View => "view",
91            Self::MaterializedView => "materialized view",
92            Self::Toast => "toast table",
93        }
94    }
95}
96
97impl fmt::Display for PgTableKind {
98    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99        formatter.write_str(self.as_str())
100    }
101}
102
103impl FromStr for PgTableKind {
104    type Err = PgTableError;
105
106    fn from_str(input: &str) -> Result<Self, Self::Err> {
107        match normalized_label(input)?.as_str() {
108            "ordinary" | "ordinary table" | "table" | "base table" => Ok(Self::Ordinary),
109            "partitioned" | "partitioned table" => Ok(Self::Partitioned),
110            "foreign" | "foreign table" => Ok(Self::Foreign),
111            "temporary" | "temporary table" | "temp" | "temp table" => Ok(Self::Temporary),
112            "view" => Ok(Self::View),
113            "materialized view" | "matview" => Ok(Self::MaterializedView),
114            "toast" | "toast table" => Ok(Self::Toast),
115            _ => Err(PgTableError::UnknownKind),
116        }
117    }
118}
119
120/// PostgreSQL table persistence labels.
121#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
122pub enum PgTablePersistence {
123    /// Normal persistent table storage.
124    #[default]
125    Permanent,
126    /// Unlogged table storage.
127    Unlogged,
128    /// Session-local temporary storage.
129    Temporary,
130}
131
132impl PgTablePersistence {
133    /// Returns a stable label.
134    #[must_use]
135    pub const fn as_str(self) -> &'static str {
136        match self {
137            Self::Permanent => "permanent",
138            Self::Unlogged => "unlogged",
139            Self::Temporary => "temporary",
140        }
141    }
142}
143
144impl fmt::Display for PgTablePersistence {
145    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146        formatter.write_str(self.as_str())
147    }
148}
149
150impl FromStr for PgTablePersistence {
151    type Err = PgTableError;
152
153    fn from_str(input: &str) -> Result<Self, Self::Err> {
154        match normalized_label(input)?.as_str() {
155            "permanent" | "persistent" => Ok(Self::Permanent),
156            "unlogged" => Ok(Self::Unlogged),
157            "temporary" | "temp" => Ok(Self::Temporary),
158            _ => Err(PgTableError::UnknownPersistence),
159        }
160    }
161}
162
163/// Schema-qualified PostgreSQL table reference metadata.
164#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
165pub struct PgTableRef {
166    schema: Option<PgSchemaName>,
167    name: PgTableName,
168}
169
170impl PgTableRef {
171    /// Creates an unqualified table reference.
172    #[must_use]
173    pub const fn new(name: PgTableName) -> Self {
174        Self { schema: None, name }
175    }
176
177    /// Creates a schema-qualified table reference.
178    #[must_use]
179    pub const fn qualified(schema: PgSchemaName, name: PgTableName) -> Self {
180        Self {
181            schema: Some(schema),
182            name,
183        }
184    }
185
186    /// Adds schema qualification.
187    #[must_use]
188    pub fn with_schema(mut self, schema: PgSchemaName) -> Self {
189        self.schema = Some(schema);
190        self
191    }
192
193    /// Returns the optional schema name.
194    #[must_use]
195    pub const fn schema(&self) -> Option<&PgSchemaName> {
196        self.schema.as_ref()
197    }
198
199    /// Returns the table name.
200    #[must_use]
201    pub const fn name(&self) -> &PgTableName {
202        &self.name
203    }
204}
205
206impl fmt::Display for PgTableRef {
207    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208        if let Some(schema) = &self.schema {
209            write!(formatter, "{schema}.")?;
210        }
211        write!(formatter, "{}", self.name)
212    }
213}
214
215/// PostgreSQL table metadata without database introspection.
216#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
217pub struct PgTable {
218    reference: PgTableRef,
219    kind: PgTableKind,
220    persistence: PgTablePersistence,
221}
222
223impl PgTable {
224    /// Creates table metadata from a table reference.
225    #[must_use]
226    pub const fn new(reference: PgTableRef) -> Self {
227        Self {
228            reference,
229            kind: PgTableKind::Ordinary,
230            persistence: PgTablePersistence::Permanent,
231        }
232    }
233
234    /// Sets the table kind.
235    #[must_use]
236    pub const fn with_kind(mut self, kind: PgTableKind) -> Self {
237        self.kind = kind;
238        self
239    }
240
241    /// Sets the persistence label.
242    #[must_use]
243    pub const fn with_persistence(mut self, persistence: PgTablePersistence) -> Self {
244        self.persistence = persistence;
245        self
246    }
247
248    /// Returns the table reference.
249    #[must_use]
250    pub const fn reference(&self) -> &PgTableRef {
251        &self.reference
252    }
253
254    /// Returns the table kind.
255    #[must_use]
256    pub const fn kind(&self) -> PgTableKind {
257        self.kind
258    }
259
260    /// Returns the persistence label.
261    #[must_use]
262    pub const fn persistence(&self) -> PgTablePersistence {
263        self.persistence
264    }
265}
266
267/// Error returned when PostgreSQL table metadata is invalid.
268#[derive(Clone, Debug, Eq, PartialEq)]
269pub enum PgTableError {
270    Empty,
271    UnknownKind,
272    UnknownPersistence,
273    Identifier(PgIdentifierError),
274}
275
276impl fmt::Display for PgTableError {
277    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
278        match self {
279            Self::Empty => formatter.write_str("PostgreSQL table label cannot be empty"),
280            Self::UnknownKind => formatter.write_str("unknown PostgreSQL table kind"),
281            Self::UnknownPersistence => {
282                formatter.write_str("unknown PostgreSQL table persistence label")
283            }
284            Self::Identifier(error) => {
285                write!(formatter, "invalid PostgreSQL table identifier: {error}")
286            }
287        }
288    }
289}
290
291impl Error for PgTableError {}
292
293fn normalized_label(input: &str) -> Result<String, PgTableError> {
294    let trimmed = input.trim();
295    if trimmed.is_empty() {
296        return Err(PgTableError::Empty);
297    }
298    Ok(trimmed
299        .replace('_', " ")
300        .split_whitespace()
301        .collect::<Vec<_>>()
302        .join(" ")
303        .to_ascii_lowercase())
304}
305
306#[cfg(test)]
307mod tests {
308    use super::{PgTable, PgTableError, PgTableKind, PgTableName, PgTablePersistence, PgTableRef};
309    use use_pg_schema::PgSchemaName;
310
311    #[test]
312    fn renders_schema_qualified_table_refs() -> Result<(), Box<dyn std::error::Error>> {
313        let table = PgTableRef::qualified(PgSchemaName::public(), PgTableName::new("users")?);
314        assert_eq!(table.to_string(), "public.users");
315        Ok(())
316    }
317
318    #[test]
319    fn parses_table_kind_and_persistence() -> Result<(), PgTableError> {
320        assert_eq!(
321            "partitioned table".parse::<PgTableKind>()?,
322            PgTableKind::Partitioned
323        );
324        assert_eq!("foreign".parse::<PgTableKind>()?, PgTableKind::Foreign);
325        assert_eq!(
326            "unlogged".parse::<PgTablePersistence>()?,
327            PgTablePersistence::Unlogged
328        );
329        assert_eq!(PgTableKind::Temporary.to_string(), "temporary table");
330        Ok(())
331    }
332
333    #[test]
334    fn creates_table_metadata() -> Result<(), Box<dyn std::error::Error>> {
335        let reference = PgTableRef::new(PgTableName::new("events")?);
336        let table = PgTable::new(reference)
337            .with_kind(PgTableKind::Ordinary)
338            .with_persistence(PgTablePersistence::Unlogged);
339        assert_eq!(table.reference().to_string(), "events");
340        assert_eq!(table.persistence(), PgTablePersistence::Unlogged);
341        Ok(())
342    }
343}