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};
8
9pub const PUBLIC_SCHEMA: &str = "public";
11pub const PG_CATALOG_SCHEMA: &str = "pg_catalog";
13pub const INFORMATION_SCHEMA: &str = "information_schema";
15
16#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
18pub struct PgSchemaName(PgIdentifier);
19
20impl PgSchemaName {
21 pub fn new(input: impl AsRef<str>) -> Result<Self, PgSchemaError> {
27 PgIdentifier::new(input)
28 .map(Self)
29 .map_err(PgSchemaError::Identifier)
30 }
31
32 #[must_use]
38 pub fn public() -> Self {
39 Self::new(PUBLIC_SCHEMA).expect("public is a valid PostgreSQL schema name")
40 }
41
42 #[must_use]
48 pub fn pg_catalog() -> Self {
49 Self::new(PG_CATALOG_SCHEMA).expect("pg_catalog is a valid PostgreSQL schema name")
50 }
51
52 #[must_use]
58 pub fn information_schema() -> Self {
59 Self::new(INFORMATION_SCHEMA).expect("information_schema is a valid PostgreSQL schema name")
60 }
61
62 #[must_use]
64 pub fn as_str(&self) -> &str {
65 self.0.as_str()
66 }
67
68 #[must_use]
70 pub fn class(&self) -> PgSchemaClass {
71 classify_schema(self.as_str())
72 }
73
74 #[must_use]
76 pub fn is_system(&self) -> bool {
77 self.class().is_system()
78 }
79}
80
81impl AsRef<str> for PgSchemaName {
82 fn as_ref(&self) -> &str {
83 self.as_str()
84 }
85}
86
87impl fmt::Display for PgSchemaName {
88 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 self.0.fmt(formatter)
90 }
91}
92
93impl FromStr for PgSchemaName {
94 type Err = PgSchemaError;
95
96 fn from_str(input: &str) -> Result<Self, Self::Err> {
97 Self::new(input)
98 }
99}
100
101impl TryFrom<&str> for PgSchemaName {
102 type Error = PgSchemaError;
103
104 fn try_from(value: &str) -> Result<Self, Self::Error> {
105 Self::new(value)
106 }
107}
108
109#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
111pub enum PgSchemaClass {
112 Public,
114 SystemCatalog,
116 InformationSchema,
118 Temporary,
120 Toast,
122 #[default]
124 User,
125}
126
127impl PgSchemaClass {
128 #[must_use]
130 pub const fn as_str(self) -> &'static str {
131 match self {
132 Self::Public => "public",
133 Self::SystemCatalog => "system-catalog",
134 Self::InformationSchema => "information-schema",
135 Self::Temporary => "temporary",
136 Self::Toast => "toast",
137 Self::User => "user",
138 }
139 }
140
141 #[must_use]
143 pub const fn is_system(self) -> bool {
144 matches!(
145 self,
146 Self::SystemCatalog | Self::InformationSchema | Self::Temporary | Self::Toast
147 )
148 }
149}
150
151impl fmt::Display for PgSchemaClass {
152 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153 formatter.write_str(self.as_str())
154 }
155}
156
157#[must_use]
159pub fn classify_schema(input: &str) -> PgSchemaClass {
160 let normalized = input.trim().to_ascii_lowercase();
161 if normalized == PUBLIC_SCHEMA {
162 PgSchemaClass::Public
163 } else if normalized == PG_CATALOG_SCHEMA || normalized.starts_with("pg_catalog_") {
164 PgSchemaClass::SystemCatalog
165 } else if normalized == INFORMATION_SCHEMA {
166 PgSchemaClass::InformationSchema
167 } else if normalized == "pg_temp" || normalized.starts_with("pg_temp_") {
168 PgSchemaClass::Temporary
169 } else if normalized == "pg_toast" || normalized.starts_with("pg_toast") {
170 PgSchemaClass::Toast
171 } else {
172 PgSchemaClass::User
173 }
174}
175
176#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
178pub struct PgSearchPath {
179 schemas: Vec<PgSchemaName>,
180}
181
182impl PgSearchPath {
183 #[must_use]
185 pub const fn new(schemas: Vec<PgSchemaName>) -> Self {
186 Self { schemas }
187 }
188
189 #[must_use]
191 pub fn public() -> Self {
192 Self::new(vec![PgSchemaName::public()])
193 }
194
195 #[must_use]
197 pub fn schemas(&self) -> &[PgSchemaName] {
198 &self.schemas
199 }
200
201 #[must_use]
203 pub fn first(&self) -> Option<&PgSchemaName> {
204 self.schemas.first()
205 }
206
207 pub fn push(&mut self, schema: PgSchemaName) {
209 self.schemas.push(schema);
210 }
211
212 #[must_use]
214 pub fn contains(&self, schema: &PgSchemaName) -> bool {
215 self.schemas.iter().any(|candidate| candidate == schema)
216 }
217}
218
219impl fmt::Display for PgSearchPath {
220 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221 let mut schemas = self.schemas.iter();
222 if let Some(first) = schemas.next() {
223 write!(formatter, "{first}")?;
224 }
225 for schema in schemas {
226 write!(formatter, ", {schema}")?;
227 }
228 Ok(())
229 }
230}
231
232#[derive(Clone, Debug, Eq, PartialEq)]
234pub enum PgSchemaError {
235 Identifier(PgIdentifierError),
236}
237
238impl fmt::Display for PgSchemaError {
239 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240 match self {
241 Self::Identifier(error) => {
242 write!(formatter, "invalid PostgreSQL schema identifier: {error}")
243 }
244 }
245 }
246}
247
248impl Error for PgSchemaError {}
249
250#[cfg(test)]
251mod tests {
252 use super::{
253 INFORMATION_SCHEMA, PG_CATALOG_SCHEMA, PUBLIC_SCHEMA, PgSchemaClass, PgSchemaError,
254 PgSchemaName, PgSearchPath, classify_schema,
255 };
256
257 #[test]
258 fn creates_common_schema_names() {
259 assert_eq!(PgSchemaName::public().as_str(), PUBLIC_SCHEMA);
260 assert_eq!(PgSchemaName::pg_catalog().as_str(), PG_CATALOG_SCHEMA);
261 assert_eq!(
262 PgSchemaName::information_schema().as_str(),
263 INFORMATION_SCHEMA
264 );
265 }
266
267 #[test]
268 fn classifies_schema_names() {
269 assert_eq!(classify_schema("public"), PgSchemaClass::Public);
270 assert_eq!(classify_schema("pg_catalog"), PgSchemaClass::SystemCatalog);
271 assert_eq!(
272 classify_schema("information_schema"),
273 PgSchemaClass::InformationSchema
274 );
275 assert_eq!(classify_schema("pg_temp_3"), PgSchemaClass::Temporary);
276 assert_eq!(classify_schema("app"), PgSchemaClass::User);
277 assert!(PgSchemaName::pg_catalog().is_system());
278 }
279
280 #[test]
281 fn tracks_search_path_order() -> Result<(), PgSchemaError> {
282 let mut path = PgSearchPath::public();
283 let app = PgSchemaName::new("app")?;
284 path.push(app.clone());
285
286 assert_eq!(path.first(), Some(&PgSchemaName::public()));
287 assert!(path.contains(&app));
288 assert_eq!(path.to_string(), "public, app");
289 Ok(())
290 }
291}