#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
use use_pg_identifier::{PgIdentifier, PgIdentifierError};
use use_pg_schema::PgSchemaName;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgTableName(PgIdentifier);
impl PgTableName {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgTableError> {
PgIdentifier::new(input)
.map(Self)
.map_err(PgTableError::Identifier)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl AsRef<str> for PgTableName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for PgTableName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for PgTableName {
type Err = PgTableError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for PgTableName {
type Error = PgTableError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgTableKind {
#[default]
Ordinary,
Partitioned,
Foreign,
Temporary,
View,
MaterializedView,
Toast,
}
impl PgTableKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ordinary => "ordinary table",
Self::Partitioned => "partitioned table",
Self::Foreign => "foreign table",
Self::Temporary => "temporary table",
Self::View => "view",
Self::MaterializedView => "materialized view",
Self::Toast => "toast table",
}
}
}
impl fmt::Display for PgTableKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PgTableKind {
type Err = PgTableError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"ordinary" | "ordinary table" | "table" | "base table" => Ok(Self::Ordinary),
"partitioned" | "partitioned table" => Ok(Self::Partitioned),
"foreign" | "foreign table" => Ok(Self::Foreign),
"temporary" | "temporary table" | "temp" | "temp table" => Ok(Self::Temporary),
"view" => Ok(Self::View),
"materialized view" | "matview" => Ok(Self::MaterializedView),
"toast" | "toast table" => Ok(Self::Toast),
_ => Err(PgTableError::UnknownKind),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgTablePersistence {
#[default]
Permanent,
Unlogged,
Temporary,
}
impl PgTablePersistence {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Permanent => "permanent",
Self::Unlogged => "unlogged",
Self::Temporary => "temporary",
}
}
}
impl fmt::Display for PgTablePersistence {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PgTablePersistence {
type Err = PgTableError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"permanent" | "persistent" => Ok(Self::Permanent),
"unlogged" => Ok(Self::Unlogged),
"temporary" | "temp" => Ok(Self::Temporary),
_ => Err(PgTableError::UnknownPersistence),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgTableRef {
schema: Option<PgSchemaName>,
name: PgTableName,
}
impl PgTableRef {
#[must_use]
pub const fn new(name: PgTableName) -> Self {
Self { schema: None, name }
}
#[must_use]
pub const fn qualified(schema: PgSchemaName, name: PgTableName) -> Self {
Self {
schema: Some(schema),
name,
}
}
#[must_use]
pub fn with_schema(mut self, schema: PgSchemaName) -> Self {
self.schema = Some(schema);
self
}
#[must_use]
pub const fn schema(&self) -> Option<&PgSchemaName> {
self.schema.as_ref()
}
#[must_use]
pub const fn name(&self) -> &PgTableName {
&self.name
}
}
impl fmt::Display for PgTableRef {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(schema) = &self.schema {
write!(formatter, "{schema}.")?;
}
write!(formatter, "{}", self.name)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgTable {
reference: PgTableRef,
kind: PgTableKind,
persistence: PgTablePersistence,
}
impl PgTable {
#[must_use]
pub const fn new(reference: PgTableRef) -> Self {
Self {
reference,
kind: PgTableKind::Ordinary,
persistence: PgTablePersistence::Permanent,
}
}
#[must_use]
pub const fn with_kind(mut self, kind: PgTableKind) -> Self {
self.kind = kind;
self
}
#[must_use]
pub const fn with_persistence(mut self, persistence: PgTablePersistence) -> Self {
self.persistence = persistence;
self
}
#[must_use]
pub const fn reference(&self) -> &PgTableRef {
&self.reference
}
#[must_use]
pub const fn kind(&self) -> PgTableKind {
self.kind
}
#[must_use]
pub const fn persistence(&self) -> PgTablePersistence {
self.persistence
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PgTableError {
Empty,
UnknownKind,
UnknownPersistence,
Identifier(PgIdentifierError),
}
impl fmt::Display for PgTableError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("PostgreSQL table label cannot be empty"),
Self::UnknownKind => formatter.write_str("unknown PostgreSQL table kind"),
Self::UnknownPersistence => {
formatter.write_str("unknown PostgreSQL table persistence label")
}
Self::Identifier(error) => {
write!(formatter, "invalid PostgreSQL table identifier: {error}")
}
}
}
}
impl Error for PgTableError {}
fn normalized_label(input: &str) -> Result<String, PgTableError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(PgTableError::Empty);
}
Ok(trimmed
.replace('_', " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase())
}
#[cfg(test)]
mod tests {
use super::{PgTable, PgTableError, PgTableKind, PgTableName, PgTablePersistence, PgTableRef};
use use_pg_schema::PgSchemaName;
#[test]
fn renders_schema_qualified_table_refs() -> Result<(), Box<dyn std::error::Error>> {
let table = PgTableRef::qualified(PgSchemaName::public(), PgTableName::new("users")?);
assert_eq!(table.to_string(), "public.users");
Ok(())
}
#[test]
fn parses_table_kind_and_persistence() -> Result<(), PgTableError> {
assert_eq!(
"partitioned table".parse::<PgTableKind>()?,
PgTableKind::Partitioned
);
assert_eq!("foreign".parse::<PgTableKind>()?, PgTableKind::Foreign);
assert_eq!(
"unlogged".parse::<PgTablePersistence>()?,
PgTablePersistence::Unlogged
);
assert_eq!(PgTableKind::Temporary.to_string(), "temporary table");
Ok(())
}
#[test]
fn creates_table_metadata() -> Result<(), Box<dyn std::error::Error>> {
let reference = PgTableRef::new(PgTableName::new("events")?);
let table = PgTable::new(reference)
.with_kind(PgTableKind::Ordinary)
.with_persistence(PgTablePersistence::Unlogged);
assert_eq!(table.reference().to_string(), "events");
assert_eq!(table.persistence(), PgTablePersistence::Unlogged);
Ok(())
}
}