Skip to main content

use_sql_schema/
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_sql_ident::{SqlIdentifier, SqlIdentifierError};
8
9/// SQL schema name primitive.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct SqlSchemaName(SqlIdentifier);
12
13impl SqlSchemaName {
14    /// Creates a schema name.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`SqlSchemaError`] when identifier validation fails.
19    pub fn new(input: impl AsRef<str>) -> Result<Self, SqlSchemaError> {
20        SqlIdentifier::new(input)
21            .map(Self)
22            .map_err(SqlSchemaError::Identifier)
23    }
24
25    /// Returns the schema name text.
26    #[must_use]
27    pub fn as_str(&self) -> &str {
28        self.0.as_str()
29    }
30}
31
32impl AsRef<str> for SqlSchemaName {
33    fn as_ref(&self) -> &str {
34        self.as_str()
35    }
36}
37
38impl fmt::Display for SqlSchemaName {
39    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40        self.0.fmt(formatter)
41    }
42}
43
44impl FromStr for SqlSchemaName {
45    type Err = SqlSchemaError;
46
47    fn from_str(input: &str) -> Result<Self, Self::Err> {
48        Self::new(input)
49    }
50}
51
52/// SQL database/catalog name primitive.
53#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub struct SqlDatabaseName(SqlIdentifier);
55
56impl SqlDatabaseName {
57    /// Creates a database name.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`SqlSchemaError`] when identifier validation fails.
62    pub fn new(input: impl AsRef<str>) -> Result<Self, SqlSchemaError> {
63        SqlIdentifier::new(input)
64            .map(Self)
65            .map_err(SqlSchemaError::Identifier)
66    }
67
68    /// Returns the database name text.
69    #[must_use]
70    pub fn as_str(&self) -> &str {
71        self.0.as_str()
72    }
73}
74
75impl fmt::Display for SqlDatabaseName {
76    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77        self.0.fmt(formatter)
78    }
79}
80
81/// Generic SQL namespace metadata.
82#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub struct SqlNamespace {
84    database: Option<SqlDatabaseName>,
85    schema: SqlSchemaName,
86}
87
88impl SqlNamespace {
89    /// Creates namespace metadata from a schema name.
90    #[must_use]
91    pub const fn new(schema: SqlSchemaName) -> Self {
92        Self {
93            database: None,
94            schema,
95        }
96    }
97
98    /// Adds a database/catalog qualifier.
99    #[must_use]
100    pub fn with_database(mut self, database: SqlDatabaseName) -> Self {
101        self.database = Some(database);
102        self
103    }
104
105    /// Returns the optional database/catalog name.
106    #[must_use]
107    pub const fn database(&self) -> Option<&SqlDatabaseName> {
108        self.database.as_ref()
109    }
110
111    /// Returns the schema name.
112    #[must_use]
113    pub const fn schema(&self) -> &SqlSchemaName {
114        &self.schema
115    }
116}
117
118impl fmt::Display for SqlNamespace {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        if let Some(database) = &self.database {
121            write!(formatter, "{database}.")?;
122        }
123        write!(formatter, "{}", self.schema)
124    }
125}
126
127/// Generic SQL search-path metadata.
128#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
129pub struct SqlSearchPath {
130    schemas: Vec<SqlSchemaName>,
131}
132
133impl SqlSearchPath {
134    /// Creates a search path from schema names.
135    #[must_use]
136    pub const fn new(schemas: Vec<SqlSchemaName>) -> Self {
137        Self { schemas }
138    }
139
140    /// Returns the schema list.
141    #[must_use]
142    pub fn schemas(&self) -> &[SqlSchemaName] {
143        &self.schemas
144    }
145
146    /// Appends a schema to the search path.
147    pub fn push(&mut self, schema: SqlSchemaName) {
148        self.schemas.push(schema);
149    }
150}
151
152impl fmt::Display for SqlSearchPath {
153    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154        let mut schemas = self.schemas.iter();
155        if let Some(first) = schemas.next() {
156            write!(formatter, "{first}")?;
157        }
158        for schema in schemas {
159            write!(formatter, ", {schema}")?;
160        }
161        Ok(())
162    }
163}
164
165/// Error returned when SQL schema metadata is invalid.
166#[derive(Clone, Debug, Eq, PartialEq)]
167pub enum SqlSchemaError {
168    Identifier(SqlIdentifierError),
169}
170
171impl fmt::Display for SqlSchemaError {
172    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            Self::Identifier(error) => write!(formatter, "invalid SQL schema identifier: {error}"),
175        }
176    }
177}
178
179impl Error for SqlSchemaError {}
180
181#[cfg(test)]
182mod tests {
183    use super::{SqlDatabaseName, SqlNamespace, SqlSchemaError, SqlSchemaName, SqlSearchPath};
184
185    #[test]
186    fn creates_schema_names_and_paths() -> Result<(), SqlSchemaError> {
187        let schema = SqlSchemaName::new("public")?;
188        let namespace =
189            SqlNamespace::new(schema.clone()).with_database(SqlDatabaseName::new("app")?);
190        let path = SqlSearchPath::new(vec![schema]);
191
192        assert_eq!(namespace.to_string(), "app.public");
193        assert_eq!(path.to_string(), "public");
194        Ok(())
195    }
196}