use std::sync::OnceLock;
pub const SURREAL_RESERVED_WORDS: &[&str] = &[
"select",
"from",
"where",
"group",
"order",
"limit",
"start",
"fetch",
"timeout",
"parallel",
"value",
"content",
"set",
"create",
"update",
"delete",
"relate",
"insert",
"define",
"remove",
"begin",
"commit",
"cancel",
"return",
"let",
"if",
"else",
"then",
"end",
"for",
"break",
"continue",
"throw",
"none",
"null",
"true",
"false",
"and",
"or",
"not",
"is",
"contains",
"inside",
"outside",
"intersects",
"type",
"table",
"field",
"index",
"event",
"namespace",
"database",
"scope",
"token",
"info",
"live",
"kill",
"sleep",
"use",
"in",
"out",
];
pub const EDGE_ALLOWED_RESERVED: &[&str] = &["in", "out"];
fn reserved_set() -> &'static std::collections::HashSet<&'static str> {
static SET: OnceLock<std::collections::HashSet<&'static str>> = OnceLock::new();
SET.get_or_init(|| SURREAL_RESERVED_WORDS.iter().copied().collect())
}
fn edge_allowed_set() -> &'static std::collections::HashSet<&'static str> {
static SET: OnceLock<std::collections::HashSet<&'static str>> = OnceLock::new();
SET.get_or_init(|| EDGE_ALLOWED_RESERVED.iter().copied().collect())
}
pub fn is_reserved_word(name: &str) -> bool {
let leaf = leaf_segment(name).to_ascii_lowercase();
reserved_set().contains(leaf.as_str())
}
pub fn check_reserved_word(name: &str, allow_edge_fields: bool) -> Option<String> {
let leaf = leaf_segment(name);
let lower = leaf.to_ascii_lowercase();
if !reserved_set().contains(lower.as_str()) {
return None;
}
if allow_edge_fields && edge_allowed_set().contains(lower.as_str()) {
return None;
}
Some(format!(
"Field name {name:?} collides with SurrealDB reserved word {lower:?}. \
This may cause unexpected query behavior."
))
}
fn leaf_segment(name: &str) -> &str {
name.rsplit('.').next().unwrap_or(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reserved_word_detected_case_insensitive() {
assert!(is_reserved_word("SELECT"));
assert!(is_reserved_word("select"));
assert!(is_reserved_word("Where"));
}
#[test]
fn safe_names_not_reserved() {
assert!(!is_reserved_word("username"));
assert!(!is_reserved_word("user_name"));
assert!(!is_reserved_word("email"));
}
#[test]
fn dot_notation_checks_leaf() {
assert!(check_reserved_word("user.name", false).is_none());
assert!(check_reserved_word("user.select", false).is_some());
}
#[test]
fn edge_allowed_words_pass_with_flag() {
assert!(check_reserved_word("in", true).is_none());
assert!(check_reserved_word("out", true).is_none());
assert!(check_reserved_word("in", false).is_some());
}
#[test]
fn warning_mentions_reserved_name() {
let msg = check_reserved_word("select", false).unwrap();
assert!(msg.contains("select"));
assert!(msg.contains("reserved word"));
}
}