#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
use use_pg_identifier::{PgIdentifier, PgIdentifierError};
pub const PUBLIC_SCHEMA: &str = "public";
pub const PG_CATALOG_SCHEMA: &str = "pg_catalog";
pub const INFORMATION_SCHEMA: &str = "information_schema";
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgSchemaName(PgIdentifier);
impl PgSchemaName {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgSchemaError> {
PgIdentifier::new(input)
.map(Self)
.map_err(PgSchemaError::Identifier)
}
#[must_use]
pub fn public() -> Self {
Self::new(PUBLIC_SCHEMA).expect("public is a valid PostgreSQL schema name")
}
#[must_use]
pub fn pg_catalog() -> Self {
Self::new(PG_CATALOG_SCHEMA).expect("pg_catalog is a valid PostgreSQL schema name")
}
#[must_use]
pub fn information_schema() -> Self {
Self::new(INFORMATION_SCHEMA).expect("information_schema is a valid PostgreSQL schema name")
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
#[must_use]
pub fn class(&self) -> PgSchemaClass {
classify_schema(self.as_str())
}
#[must_use]
pub fn is_system(&self) -> bool {
self.class().is_system()
}
}
impl AsRef<str> for PgSchemaName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for PgSchemaName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for PgSchemaName {
type Err = PgSchemaError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for PgSchemaName {
type Error = PgSchemaError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgSchemaClass {
Public,
SystemCatalog,
InformationSchema,
Temporary,
Toast,
#[default]
User,
}
impl PgSchemaClass {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Public => "public",
Self::SystemCatalog => "system-catalog",
Self::InformationSchema => "information-schema",
Self::Temporary => "temporary",
Self::Toast => "toast",
Self::User => "user",
}
}
#[must_use]
pub const fn is_system(self) -> bool {
matches!(
self,
Self::SystemCatalog | Self::InformationSchema | Self::Temporary | Self::Toast
)
}
}
impl fmt::Display for PgSchemaClass {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[must_use]
pub fn classify_schema(input: &str) -> PgSchemaClass {
let normalized = input.trim().to_ascii_lowercase();
if normalized == PUBLIC_SCHEMA {
PgSchemaClass::Public
} else if normalized == PG_CATALOG_SCHEMA || normalized.starts_with("pg_catalog_") {
PgSchemaClass::SystemCatalog
} else if normalized == INFORMATION_SCHEMA {
PgSchemaClass::InformationSchema
} else if normalized == "pg_temp" || normalized.starts_with("pg_temp_") {
PgSchemaClass::Temporary
} else if normalized == "pg_toast" || normalized.starts_with("pg_toast") {
PgSchemaClass::Toast
} else {
PgSchemaClass::User
}
}
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgSearchPath {
schemas: Vec<PgSchemaName>,
}
impl PgSearchPath {
#[must_use]
pub const fn new(schemas: Vec<PgSchemaName>) -> Self {
Self { schemas }
}
#[must_use]
pub fn public() -> Self {
Self::new(vec![PgSchemaName::public()])
}
#[must_use]
pub fn schemas(&self) -> &[PgSchemaName] {
&self.schemas
}
#[must_use]
pub fn first(&self) -> Option<&PgSchemaName> {
self.schemas.first()
}
pub fn push(&mut self, schema: PgSchemaName) {
self.schemas.push(schema);
}
#[must_use]
pub fn contains(&self, schema: &PgSchemaName) -> bool {
self.schemas.iter().any(|candidate| candidate == schema)
}
}
impl fmt::Display for PgSearchPath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut schemas = self.schemas.iter();
if let Some(first) = schemas.next() {
write!(formatter, "{first}")?;
}
for schema in schemas {
write!(formatter, ", {schema}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PgSchemaError {
Identifier(PgIdentifierError),
}
impl fmt::Display for PgSchemaError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Identifier(error) => {
write!(formatter, "invalid PostgreSQL schema identifier: {error}")
}
}
}
}
impl Error for PgSchemaError {}
#[cfg(test)]
mod tests {
use super::{
INFORMATION_SCHEMA, PG_CATALOG_SCHEMA, PUBLIC_SCHEMA, PgSchemaClass, PgSchemaError,
PgSchemaName, PgSearchPath, classify_schema,
};
#[test]
fn creates_common_schema_names() {
assert_eq!(PgSchemaName::public().as_str(), PUBLIC_SCHEMA);
assert_eq!(PgSchemaName::pg_catalog().as_str(), PG_CATALOG_SCHEMA);
assert_eq!(
PgSchemaName::information_schema().as_str(),
INFORMATION_SCHEMA
);
}
#[test]
fn classifies_schema_names() {
assert_eq!(classify_schema("public"), PgSchemaClass::Public);
assert_eq!(classify_schema("pg_catalog"), PgSchemaClass::SystemCatalog);
assert_eq!(
classify_schema("information_schema"),
PgSchemaClass::InformationSchema
);
assert_eq!(classify_schema("pg_temp_3"), PgSchemaClass::Temporary);
assert_eq!(classify_schema("app"), PgSchemaClass::User);
assert!(PgSchemaName::pg_catalog().is_system());
}
#[test]
fn tracks_search_path_order() -> Result<(), PgSchemaError> {
let mut path = PgSearchPath::public();
let app = PgSchemaName::new("app")?;
path.push(app.clone());
assert_eq!(path.first(), Some(&PgSchemaName::public()));
assert!(path.contains(&app));
assert_eq!(path.to_string(), "public, app");
Ok(())
}
}