#![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_type::{PgBuiltInType, PgTypeName};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgColumnName(PgIdentifier);
impl PgColumnName {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgColumnError> {
PgIdentifier::new(input)
.map(Self)
.map_err(PgColumnError::Identifier)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl AsRef<str> for PgColumnName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for PgColumnName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for PgColumnName {
type Err = PgColumnError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for PgColumnName {
type Error = PgColumnError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgColumnDefault(String);
impl PgColumnDefault {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgColumnError> {
validate_label(input.as_ref(), PgColumnError::EmptyDefault)
.map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for PgColumnDefault {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgNullability {
#[default]
Nullable,
NotNull,
}
impl PgNullability {
#[must_use]
pub const fn is_nullable(self) -> bool {
matches!(self, Self::Nullable)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Nullable => "NULL",
Self::NotNull => "NOT NULL",
}
}
}
impl fmt::Display for PgNullability {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PgNullability {
type Err = PgColumnError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input, PgColumnError::UnknownNullability)?.as_str() {
"null" | "nullable" => Ok(Self::Nullable),
"not null" | "notnull" | "required" => Ok(Self::NotNull),
_ => Err(PgColumnError::UnknownNullability),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgGeneratedKind {
Stored,
Virtual,
}
impl PgGeneratedKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Stored => "STORED",
Self::Virtual => "VIRTUAL",
}
}
}
impl fmt::Display for PgGeneratedKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgIdentityKind {
Always,
ByDefault,
}
impl PgIdentityKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Always => "ALWAYS",
Self::ByDefault => "BY DEFAULT",
}
}
}
impl fmt::Display for PgIdentityKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgColumn {
name: PgColumnName,
type_name: PgTypeName,
nullability: PgNullability,
default: Option<PgColumnDefault>,
generated: Option<PgGeneratedKind>,
identity: Option<PgIdentityKind>,
}
impl PgColumn {
#[must_use]
pub const fn new(name: PgColumnName, type_name: PgTypeName) -> Self {
Self {
name,
type_name,
nullability: PgNullability::Nullable,
default: None,
generated: None,
identity: None,
}
}
#[must_use]
pub fn with_built_in_type(name: PgColumnName, ty: PgBuiltInType) -> Self {
Self::new(name, PgTypeName::built_in(ty))
}
#[must_use]
pub const fn with_nullability(mut self, nullability: PgNullability) -> Self {
self.nullability = nullability;
self
}
#[must_use]
pub fn with_default(mut self, default: PgColumnDefault) -> Self {
self.default = Some(default);
self
}
#[must_use]
pub const fn with_generated(mut self, generated: PgGeneratedKind) -> Self {
self.generated = Some(generated);
self
}
#[must_use]
pub const fn with_identity(mut self, identity: PgIdentityKind) -> Self {
self.identity = Some(identity);
self
}
#[must_use]
pub const fn name(&self) -> &PgColumnName {
&self.name
}
#[must_use]
pub const fn type_name(&self) -> &PgTypeName {
&self.type_name
}
#[must_use]
pub const fn nullability(&self) -> PgNullability {
self.nullability
}
#[must_use]
pub const fn default(&self) -> Option<&PgColumnDefault> {
self.default.as_ref()
}
#[must_use]
pub const fn generated(&self) -> Option<PgGeneratedKind> {
self.generated
}
#[must_use]
pub const fn identity(&self) -> Option<PgIdentityKind> {
self.identity
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PgColumnError {
EmptyDefault,
ControlCharacter,
UnknownNullability,
Identifier(PgIdentifierError),
}
impl fmt::Display for PgColumnError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyDefault => formatter.write_str("PostgreSQL column default cannot be empty"),
Self::ControlCharacter => {
formatter.write_str("PostgreSQL column label cannot contain control characters")
}
Self::UnknownNullability => {
formatter.write_str("unknown PostgreSQL column nullability label")
}
Self::Identifier(error) => {
write!(formatter, "invalid PostgreSQL column identifier: {error}")
}
}
}
}
impl Error for PgColumnError {}
fn validate_label(input: &str, empty_error: PgColumnError) -> Result<&str, PgColumnError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(empty_error);
}
if trimmed.chars().any(char::is_control) {
return Err(PgColumnError::ControlCharacter);
}
Ok(trimmed)
}
fn normalized_label(input: &str, empty_error: PgColumnError) -> Result<String, PgColumnError> {
let trimmed = validate_label(input, empty_error)?;
Ok(trimmed
.replace('_', " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase())
}
#[cfg(test)]
mod tests {
use super::{
PgColumn, PgColumnDefault, PgColumnError, PgColumnName, PgGeneratedKind, PgIdentityKind,
PgNullability,
};
use use_pg_type::PgBuiltInType;
#[test]
fn creates_column_metadata() -> Result<(), PgColumnError> {
let column = PgColumn::with_built_in_type(PgColumnName::new("id")?, PgBuiltInType::BigInt)
.with_nullability(PgNullability::NotNull)
.with_identity(PgIdentityKind::Always);
assert_eq!(column.name().as_str(), "id");
assert_eq!(column.type_name().as_str(), "bigint");
assert_eq!(column.nullability(), PgNullability::NotNull);
assert_eq!(column.identity(), Some(PgIdentityKind::Always));
Ok(())
}
#[test]
fn stores_default_and_generated_labels() -> Result<(), PgColumnError> {
let column = PgColumn::with_built_in_type(
PgColumnName::new("created_at")?,
PgBuiltInType::TimestampTz,
)
.with_default(PgColumnDefault::new("now()")?)
.with_generated(PgGeneratedKind::Stored);
assert_eq!(column.default().map(PgColumnDefault::as_str), Some("now()"));
assert_eq!(column.generated(), Some(PgGeneratedKind::Stored));
assert_eq!(PgNullability::NotNull.to_string(), "NOT NULL");
Ok(())
}
#[test]
fn parses_nullability_labels() -> Result<(), PgColumnError> {
assert_eq!(
"nullable".parse::<PgNullability>()?,
PgNullability::Nullable
);
assert_eq!("not null".parse::<PgNullability>()?, PgNullability::NotNull);
assert!(PgNullability::Nullable.is_nullable());
Ok(())
}
}