use once_cell::sync::Lazy;
use regex::Regex;
use crate::error::Error;
#[allow(clippy::expect_used)] static IDENTIFIER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_@#$]{0,127}$").expect("valid regex"));
pub(crate) fn validate_identifier(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::InvalidIdentifier(
"identifier cannot be empty".into(),
));
}
if !IDENTIFIER_RE.is_match(name) {
return Err(Error::InvalidIdentifier(format!(
"invalid identifier '{name}': must start with letter/underscore, \
contain only alphanumerics/_/@/#/$, and be 1-128 characters"
)));
}
Ok(())
}
pub(crate) fn validate_qualified_identifier(name: &str) -> Result<(), Error> {
if name.is_empty() {
return Err(Error::InvalidIdentifier(
"identifier cannot be empty".into(),
));
}
let parts: Vec<&str> = name.split('.').collect();
if parts.len() > 4 {
return Err(Error::InvalidIdentifier(format!(
"invalid qualified identifier '{name}': too many parts \
(max 4: server.catalog.schema.object)"
)));
}
for part in &parts {
validate_identifier(part)?;
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn test_validate_identifier_valid() {
assert!(validate_identifier("my_table").is_ok());
assert!(validate_identifier("Table123").is_ok());
assert!(validate_identifier("_private").is_ok());
assert!(validate_identifier("sp_test").is_ok());
assert!(validate_identifier("column@name").is_ok());
assert!(validate_identifier("temp#table").is_ok());
}
#[test]
fn test_validate_identifier_invalid() {
assert!(validate_identifier("").is_err());
assert!(validate_identifier("123abc").is_err());
assert!(validate_identifier("table-name").is_err());
assert!(validate_identifier("table name").is_err());
assert!(validate_identifier("table;DROP TABLE users").is_err());
}
#[test]
fn test_validate_qualified_identifier_valid() {
assert!(validate_qualified_identifier("dbo.Users").is_ok());
assert!(validate_qualified_identifier("my_proc").is_ok());
assert!(validate_qualified_identifier("catalog.dbo.Users").is_ok());
assert!(validate_qualified_identifier("server.catalog.dbo.Users").is_ok());
}
#[test]
fn test_validate_qualified_identifier_invalid() {
assert!(validate_qualified_identifier("").is_err());
assert!(validate_qualified_identifier("a.b.c.d.e").is_err());
assert!(validate_qualified_identifier("dbo.123bad").is_err());
assert!(validate_qualified_identifier("dbo.table;DROP").is_err());
}
}