Skip to main content

systemprompt_database/admin/
identifier.rs

1//! Validated `PostgreSQL` identifier wrapper used wherever a raw table name or
2//! column name flows from user input into a SQL string.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use std::fmt;
8
9use thiserror::Error;
10
11const MAX_IDENTIFIER_LEN: usize = 63;
12
13#[derive(Debug, Clone, Copy, Error)]
14pub enum IdentifierError {
15    #[error("identifier is empty")]
16    Empty,
17    #[error("identifier length {0} exceeds `PostgreSQL` limit of {MAX_IDENTIFIER_LEN}")]
18    TooLong(usize),
19    #[error("identifier must start with an ASCII letter or underscore")]
20    BadLead,
21    #[error("identifier contains invalid character {0:?}")]
22    InvalidChar(char),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct SafeIdentifier(String);
27
28impl SafeIdentifier {
29    pub fn parse(raw: &str) -> Result<Self, IdentifierError> {
30        if raw.is_empty() {
31            return Err(IdentifierError::Empty);
32        }
33        if raw.len() > MAX_IDENTIFIER_LEN {
34            return Err(IdentifierError::TooLong(raw.len()));
35        }
36        let mut chars = raw.chars();
37        let first = chars.next().ok_or(IdentifierError::Empty)?;
38        if !(first.is_ascii_alphabetic() || first == '_') {
39            return Err(IdentifierError::BadLead);
40        }
41        for c in chars {
42            if !(c.is_ascii_alphanumeric() || c == '_') {
43                return Err(IdentifierError::InvalidChar(c));
44            }
45        }
46        Ok(Self(raw.to_owned()))
47    }
48
49    pub fn as_str(&self) -> &str {
50        &self.0
51    }
52}
53
54impl fmt::Display for SafeIdentifier {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str(&self.0)
57    }
58}