#![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_table::PgTableRef;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgIndexName(PgIdentifier);
impl PgIndexName {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
PgIdentifier::new(input)
.map(Self)
.map_err(PgIndexError::Identifier)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl fmt::Display for PgIndexName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl FromStr for PgIndexName {
type Err = PgIndexError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum PgIndexMethod {
#[default]
Btree,
Hash,
Gist,
Spgist,
Gin,
Brin,
}
impl PgIndexMethod {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Btree => "btree",
Self::Hash => "hash",
Self::Gist => "gist",
Self::Spgist => "spgist",
Self::Gin => "gin",
Self::Brin => "brin",
}
}
}
impl fmt::Display for PgIndexMethod {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PgIndexMethod {
type Err = PgIndexError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"btree" | "b tree" => Ok(Self::Btree),
"hash" => Ok(Self::Hash),
"gist" => Ok(Self::Gist),
"spgist" | "sp gist" => Ok(Self::Spgist),
"gin" => Ok(Self::Gin),
"brin" => Ok(Self::Brin),
_ => Err(PgIndexError::UnknownMethod),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgIndexColumn(PgIdentifier);
impl PgIndexColumn {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
PgIdentifier::new(input)
.map(Self)
.map_err(PgIndexError::Identifier)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl fmt::Display for PgIndexColumn {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgIndexExpression(String);
impl PgIndexExpression {
pub fn new(input: impl AsRef<str>) -> Result<Self, PgIndexError> {
validate_label(input.as_ref(), PgIndexError::EmptyExpression)
.map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for PgIndexExpression {
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 struct PgIndexFlags {
bits: u8,
}
const UNIQUE_FLAG: u8 = 1 << 0;
const PRIMARY_FLAG: u8 = 1 << 1;
const PARTIAL_FLAG: u8 = 1 << 2;
const EXPRESSION_FLAG: u8 = 1 << 3;
const CONCURRENT_FLAG: u8 = 1 << 4;
const INVALID_FLAG: u8 = 1 << 5;
impl PgIndexFlags {
#[must_use]
pub const fn unique(mut self, value: bool) -> Self {
self.set_flag(UNIQUE_FLAG, value);
self
}
#[must_use]
pub const fn primary(mut self, value: bool) -> Self {
self.set_flag(PRIMARY_FLAG, value);
self
}
#[must_use]
pub const fn partial(mut self, value: bool) -> Self {
self.set_flag(PARTIAL_FLAG, value);
self
}
#[must_use]
pub const fn expression(mut self, value: bool) -> Self {
self.set_flag(EXPRESSION_FLAG, value);
self
}
#[must_use]
pub const fn concurrent(mut self, value: bool) -> Self {
self.set_flag(CONCURRENT_FLAG, value);
self
}
#[must_use]
pub const fn invalid(mut self, value: bool) -> Self {
self.set_flag(INVALID_FLAG, value);
self
}
#[must_use]
pub const fn is_unique(self) -> bool {
self.has_flag(UNIQUE_FLAG)
}
#[must_use]
pub const fn is_primary(self) -> bool {
self.has_flag(PRIMARY_FLAG)
}
#[must_use]
pub const fn is_partial(self) -> bool {
self.has_flag(PARTIAL_FLAG)
}
#[must_use]
pub const fn is_expression(self) -> bool {
self.has_flag(EXPRESSION_FLAG)
}
#[must_use]
pub const fn is_concurrent(self) -> bool {
self.has_flag(CONCURRENT_FLAG)
}
#[must_use]
pub const fn is_invalid(self) -> bool {
self.has_flag(INVALID_FLAG)
}
const fn set_flag(&mut self, flag: u8, value: bool) {
if value {
self.bits |= flag;
} else {
self.bits &= !flag;
}
}
const fn has_flag(self, flag: u8) -> bool {
self.bits & flag != 0
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PgIndex {
name: PgIndexName,
table: Option<PgTableRef>,
method: PgIndexMethod,
columns: Vec<PgIndexColumn>,
expressions: Vec<PgIndexExpression>,
predicate: Option<String>,
flags: PgIndexFlags,
}
impl PgIndex {
#[must_use]
pub const fn new(name: PgIndexName) -> Self {
Self {
name,
table: None,
method: PgIndexMethod::Btree,
columns: Vec::new(),
expressions: Vec::new(),
predicate: None,
flags: PgIndexFlags { bits: 0 },
}
}
#[must_use]
pub fn with_table(mut self, table: PgTableRef) -> Self {
self.table = Some(table);
self
}
#[must_use]
pub const fn with_method(mut self, method: PgIndexMethod) -> Self {
self.method = method;
self
}
#[must_use]
pub fn with_columns(mut self, columns: Vec<PgIndexColumn>) -> Self {
self.columns = columns;
self
}
#[must_use]
pub fn with_expression(mut self, expression: PgIndexExpression) -> Self {
self.expressions.push(expression);
self.flags = self.flags.expression(true);
self
}
pub fn with_predicate(mut self, predicate: impl AsRef<str>) -> Result<Self, PgIndexError> {
self.predicate =
Some(validate_label(predicate.as_ref(), PgIndexError::EmptyPredicate)?.to_owned());
self.flags = self.flags.partial(true);
Ok(self)
}
#[must_use]
pub const fn with_flags(mut self, flags: PgIndexFlags) -> Self {
self.flags = flags;
self
}
#[must_use]
pub const fn name(&self) -> &PgIndexName {
&self.name
}
#[must_use]
pub const fn table(&self) -> Option<&PgTableRef> {
self.table.as_ref()
}
#[must_use]
pub const fn method(&self) -> PgIndexMethod {
self.method
}
#[must_use]
pub fn columns(&self) -> &[PgIndexColumn] {
&self.columns
}
#[must_use]
pub fn expressions(&self) -> &[PgIndexExpression] {
&self.expressions
}
#[must_use]
pub fn predicate(&self) -> Option<&str> {
self.predicate.as_deref()
}
#[must_use]
pub const fn flags(&self) -> PgIndexFlags {
self.flags
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PgIndexError {
Empty,
EmptyExpression,
EmptyPredicate,
UnknownMethod,
ControlCharacter,
Identifier(PgIdentifierError),
}
impl fmt::Display for PgIndexError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("PostgreSQL index label cannot be empty"),
Self::EmptyExpression => {
formatter.write_str("PostgreSQL index expression cannot be empty")
}
Self::EmptyPredicate => {
formatter.write_str("PostgreSQL index predicate cannot be empty")
}
Self::UnknownMethod => formatter.write_str("unknown PostgreSQL index method"),
Self::ControlCharacter => {
formatter.write_str("PostgreSQL index label cannot contain control characters")
}
Self::Identifier(error) => {
write!(formatter, "invalid PostgreSQL index identifier: {error}")
}
}
}
}
impl Error for PgIndexError {}
fn normalized_label(input: &str) -> Result<String, PgIndexError> {
let trimmed = validate_label(input, PgIndexError::Empty)?;
Ok(trimmed
.replace('_', " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase())
}
fn validate_label(input: &str, empty_error: PgIndexError) -> Result<&str, PgIndexError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(empty_error);
}
if trimmed.chars().any(char::is_control) {
return Err(PgIndexError::ControlCharacter);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{
PgIndex, PgIndexColumn, PgIndexError, PgIndexExpression, PgIndexFlags, PgIndexMethod,
PgIndexName,
};
#[test]
fn parses_and_renders_index_methods() -> Result<(), PgIndexError> {
assert_eq!("btree".parse::<PgIndexMethod>()?, PgIndexMethod::Btree);
assert_eq!("sp gist".parse::<PgIndexMethod>()?, PgIndexMethod::Spgist);
assert_eq!(PgIndexMethod::Brin.to_string(), "brin");
Ok(())
}
#[test]
fn tracks_index_flags() {
let flags = PgIndexFlags::default()
.unique(true)
.primary(true)
.concurrent(true)
.invalid(true);
assert!(flags.is_unique());
assert!(flags.is_primary());
assert!(flags.is_concurrent());
assert!(flags.is_invalid());
}
#[test]
fn creates_btree_index_metadata() -> Result<(), PgIndexError> {
let index = PgIndex::new(PgIndexName::new("users_email_idx")?)
.with_method(PgIndexMethod::Btree)
.with_columns(vec![PgIndexColumn::new("email")?])
.with_flags(PgIndexFlags::default().unique(true));
assert_eq!(index.name().as_str(), "users_email_idx");
assert_eq!(index.method(), PgIndexMethod::Btree);
assert_eq!(index.columns().len(), 1);
assert!(index.flags().is_unique());
Ok(())
}
#[test]
fn tracks_expression_and_partial_labels() -> Result<(), PgIndexError> {
let index = PgIndex::new(PgIndexName::new("users_lower_email_idx")?)
.with_expression(PgIndexExpression::new("lower(email)")?)
.with_predicate("deleted_at IS NULL")?;
assert_eq!(index.expressions().len(), 1);
assert_eq!(index.predicate(), Some("deleted_at IS NULL"));
assert!(index.flags().is_expression());
assert!(index.flags().is_partial());
Ok(())
}
}