Skip to main content

quack_rs/validate/
function_name.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. <https://github.com/tomtom215/>
3// My way of giving something small back to the open source community
4// and encouraging more Rust development!
5
6//! SQL function name validation for `DuckDB` extensions.
7//!
8//! Function names registered with `DuckDB` should follow safe naming conventions
9//! to avoid registration failures or unexpected behavior. This validator enforces
10//! conservative rules that are compatible with `DuckDB`'s internal function catalog.
11
12use crate::error::ExtensionError;
13
14/// Maximum length for a function name.
15///
16/// `DuckDB` does not publicly document a hard limit, but names beyond 256
17/// characters are unreasonable and may cause issues with catalog storage.
18const MAX_FUNCTION_NAME_LEN: usize = 256;
19
20/// Validates a `DuckDB` function name.
21///
22/// # Rules
23///
24/// - Must not be empty
25/// - Must not exceed 256 characters
26/// - Must start with a lowercase ASCII letter or underscore
27/// - Must contain only lowercase ASCII letters, digits, or underscores
28/// - Must not contain interior null bytes
29///
30/// These rules are intentionally conservative. `DuckDB` may accept a wider range
31/// of names, but restricting to this set avoids catalog issues and makes function
32/// names unambiguous in SQL queries.
33///
34/// # Errors
35///
36/// Returns `ExtensionError` describing the first rule violation found.
37///
38/// # Example
39///
40/// ```rust
41/// use quack_rs::validate::validate_function_name;
42///
43/// assert!(validate_function_name("word_count").is_ok());
44/// assert!(validate_function_name("my_func_v2").is_ok());
45/// assert!(validate_function_name("_internal").is_ok());
46/// assert!(validate_function_name("").is_err());        // empty
47/// assert!(validate_function_name("MyFunc").is_err());   // uppercase
48/// assert!(validate_function_name("my-func").is_err());  // hyphen
49/// assert!(validate_function_name("1func").is_err());    // starts with digit
50/// ```
51pub fn validate_function_name(name: &str) -> Result<(), ExtensionError> {
52    if name.is_empty() {
53        return Err(ExtensionError::new("function name must not be empty"));
54    }
55
56    if name.len() > MAX_FUNCTION_NAME_LEN {
57        return Err(ExtensionError::new(format!(
58            "function name must not exceed {MAX_FUNCTION_NAME_LEN} characters, got {}",
59            name.len()
60        )));
61    }
62
63    // Check for interior null bytes (would truncate the CString)
64    if name.bytes().any(|b| b == 0) {
65        return Err(ExtensionError::new(
66            "function name must not contain null bytes",
67        ));
68    }
69
70    let first = name.as_bytes()[0];
71    if !first.is_ascii_lowercase() && first != b'_' {
72        return Err(ExtensionError::new(format!(
73            "function name must start with a lowercase letter or underscore, got '{}'",
74            name.chars().next().unwrap_or('?')
75        )));
76    }
77
78    for (i, ch) in name.chars().enumerate() {
79        if !matches!(ch, 'a'..='z' | '0'..='9' | '_') {
80            return Err(ExtensionError::new(format!(
81                "function name contains invalid character '{ch}' at position {i}; \
82                 only lowercase letters, digits, and underscores are allowed"
83            )));
84        }
85    }
86
87    Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn valid_simple() {
96        assert!(validate_function_name("word_count").is_ok());
97    }
98
99    #[test]
100    fn valid_with_digits() {
101        assert!(validate_function_name("my_func_v2").is_ok());
102    }
103
104    #[test]
105    fn valid_underscore_prefix() {
106        assert!(validate_function_name("_internal").is_ok());
107    }
108
109    #[test]
110    fn valid_single_char() {
111        assert!(validate_function_name("f").is_ok());
112    }
113
114    #[test]
115    fn empty_rejected() {
116        let err = validate_function_name("").unwrap_err();
117        assert!(err.as_str().contains("empty"));
118    }
119
120    #[test]
121    fn uppercase_rejected() {
122        let err = validate_function_name("MyFunc").unwrap_err();
123        assert!(err.as_str().contains("lowercase letter or underscore"));
124    }
125
126    #[test]
127    fn uppercase_mid_rejected() {
128        let err = validate_function_name("myFunc").unwrap_err();
129        assert!(err.as_str().contains("invalid character"));
130    }
131
132    #[test]
133    fn hyphen_rejected() {
134        let err = validate_function_name("my-func").unwrap_err();
135        assert!(err.as_str().contains("invalid character"));
136    }
137
138    #[test]
139    fn starts_with_digit_rejected() {
140        let err = validate_function_name("1func").unwrap_err();
141        assert!(err.as_str().contains("lowercase letter or underscore"));
142    }
143
144    #[test]
145    fn space_rejected() {
146        let err = validate_function_name("my func").unwrap_err();
147        assert!(err.as_str().contains("invalid character"));
148    }
149
150    #[test]
151    fn special_char_rejected() {
152        let err = validate_function_name("my@func").unwrap_err();
153        assert!(err.as_str().contains("invalid character"));
154    }
155
156    #[test]
157    fn null_byte_rejected() {
158        let err = validate_function_name("my\0func").unwrap_err();
159        assert!(err.as_str().contains("null bytes"));
160    }
161
162    #[test]
163    fn too_long_rejected() {
164        let long_name: String = "a".repeat(257);
165        let err = validate_function_name(&long_name).unwrap_err();
166        assert!(err.as_str().contains("256 characters"));
167    }
168
169    #[test]
170    fn max_length_accepted() {
171        let max_name: String = "a".repeat(256);
172        assert!(validate_function_name(&max_name).is_ok());
173    }
174
175    #[test]
176    fn semicolon_rejected() {
177        let err = validate_function_name("func;drop").unwrap_err();
178        assert!(err.as_str().contains("invalid character"));
179    }
180
181    #[test]
182    fn quote_rejected() {
183        let err = validate_function_name("func'name").unwrap_err();
184        assert!(err.as_str().contains("invalid character"));
185    }
186}