use crate::error::ExtensionError;
const MAX_FUNCTION_NAME_LEN: usize = 256;
pub fn validate_function_name(name: &str) -> Result<(), ExtensionError> {
if name.is_empty() {
return Err(ExtensionError::new("function name must not be empty"));
}
if name.len() > MAX_FUNCTION_NAME_LEN {
return Err(ExtensionError::new(format!(
"function name must not exceed {MAX_FUNCTION_NAME_LEN} characters, got {}",
name.len()
)));
}
if name.bytes().any(|b| b == 0) {
return Err(ExtensionError::new(
"function name must not contain null bytes",
));
}
let first = name.as_bytes()[0];
if !first.is_ascii_alphabetic() && first != b'_' {
return Err(ExtensionError::new(format!(
"function name must start with a letter or underscore, got '{}'",
name.chars().next().unwrap_or('?')
)));
}
for (i, ch) in name.chars().enumerate() {
if !ch.is_ascii_alphanumeric() && ch != '_' {
return Err(ExtensionError::new(format!(
"function name contains invalid character '{ch}' at position {i}; \
only letters, digits, and underscores are allowed (a name needing \
quotes in SQL is not worth the trouble it causes callers)"
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_simple() {
assert!(validate_function_name("word_count").is_ok());
}
#[test]
fn valid_with_digits() {
assert!(validate_function_name("my_func_v2").is_ok());
}
#[test]
fn valid_underscore_prefix() {
assert!(validate_function_name("_internal").is_ok());
}
#[test]
fn valid_single_char() {
assert!(validate_function_name("f").is_ok());
}
#[test]
fn mixed_case_is_accepted_because_duckdb_accepts_it() {
assert!(validate_function_name("formatReadableSize").is_ok());
assert!(validate_function_name("formatReadableDecimalSize").is_ok());
assert!(validate_function_name("MyFunc").is_ok());
assert!(validate_function_name("_Internal2").is_ok());
}
#[test]
fn names_needing_quotes_are_still_rejected() {
for name in [
"my-func", "my func", "my.func", "my\"func", "my'func", "1func", "+",
] {
assert!(
validate_function_name(name).is_err(),
"{name} should be rejected"
);
}
}
#[test]
fn empty_rejected() {
let err = validate_function_name("").unwrap_err();
assert!(err.as_str().contains("empty"));
}
#[test]
fn hyphen_rejected() {
let err = validate_function_name("my-func").unwrap_err();
assert!(err.as_str().contains("invalid character"));
}
#[test]
fn starts_with_digit_rejected() {
let err = validate_function_name("1func").unwrap_err();
assert!(err.as_str().contains("letter or underscore"));
}
#[test]
fn space_rejected() {
let err = validate_function_name("my func").unwrap_err();
assert!(err.as_str().contains("invalid character"));
}
#[test]
fn special_char_rejected() {
let err = validate_function_name("my@func").unwrap_err();
assert!(err.as_str().contains("invalid character"));
}
#[test]
fn null_byte_rejected() {
let err = validate_function_name("my\0func").unwrap_err();
assert!(err.as_str().contains("null bytes"));
}
#[test]
fn too_long_rejected() {
let long_name: String = "a".repeat(257);
let err = validate_function_name(&long_name).unwrap_err();
assert!(err.as_str().contains("256 characters"));
}
#[test]
fn max_length_accepted() {
let max_name: String = "a".repeat(256);
assert!(validate_function_name(&max_name).is_ok());
}
#[test]
fn semicolon_rejected() {
let err = validate_function_name("func;drop").unwrap_err();
assert!(err.as_str().contains("invalid character"));
}
#[test]
fn quote_rejected() {
let err = validate_function_name("func'name").unwrap_err();
assert!(err.as_str().contains("invalid character"));
}
}