use std::collections::HashSet;
pub static CYPHER_RESERVED: &[&str] = &[
"CREATE",
"MATCH",
"RETURN",
"WHERE",
"DELETE",
"SET",
"REMOVE",
"ORDER",
"BY",
"SKIP",
"LIMIT",
"WITH",
"UNWIND",
"AS",
"AND",
"OR",
"NOT",
"IN",
"IS",
"NULL",
"TRUE",
"FALSE",
"MERGE",
"ON",
"CALL",
"YIELD",
"DETACH",
"OPTIONAL",
"UNION",
"ALL",
"CASE",
"WHEN",
"THEN",
"ELSE",
"END",
"EXISTS",
"FOREACH",
"COUNT",
"SUM",
"AVG",
"MIN",
"MAX",
"COLLECT",
"REDUCE",
"FILTER",
"EXTRACT",
"ANY",
"NONE",
"SINGLE",
"STARTS",
"ENDS",
"CONTAINS",
"XOR",
"DISTINCT",
"LOAD",
"CSV",
"USING",
"PERIODIC",
"COMMIT",
"CONSTRAINT",
"INDEX",
"DROP",
"ASSERT",
];
pub fn escape_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('\'', "\\'")
.replace('"', "\\\"")
.replace(['\n', '\r', '\t'], " ")
}
pub fn sanitize_rel_type(rel_type: &str) -> String {
let safe: String = rel_type
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect();
let safe = if safe.is_empty() || safe.chars().next().is_some_and(|c| c.is_numeric()) {
format!("REL_{}", safe)
} else {
safe
};
let reserved: HashSet<&str> = CYPHER_RESERVED.iter().copied().collect();
if reserved.contains(safe.to_uppercase().as_str()) {
format!("REL_{}", safe)
} else {
safe
}
}
pub fn rel_type_pattern(rel_type: Option<&str>) -> String {
match rel_type {
Some(rt) => format!(":{}", sanitize_rel_type(rt)),
None => String::new(),
}
}
fn has_leading_zero(s: &str) -> bool {
let digits = s.strip_prefix('-').unwrap_or(s);
digits.len() > 1 && digits.starts_with('0') && !digits.starts_with("0.")
}
pub fn format_value(v: &str) -> String {
if has_leading_zero(v) {
return format!("'{}'", escape_string(v));
}
if v.parse::<i64>().is_ok()
|| v.parse::<f64>().is_ok()
|| v == "true"
|| v == "false"
|| v == "null"
{
v.to_string()
} else {
format!("'{}'", escape_string(v))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PropertyValue {
Text(String),
Integer(i64),
Float(f64),
Bool(bool),
}
impl PropertyValue {
pub fn to_cypher(&self) -> String {
match self {
PropertyValue::Text(s) => format!("'{}'", escape_string(s)),
PropertyValue::Integer(v) => v.to_string(),
PropertyValue::Float(v) => {
let s = v.to_string();
if s.contains('.') {
s
} else {
format!("{}.0", s)
}
}
PropertyValue::Bool(v) => v.to_string(),
}
}
}
impl From<&str> for PropertyValue {
fn from(s: &str) -> Self {
if has_leading_zero(s) {
return PropertyValue::Text(s.to_string());
}
if let Ok(v) = s.parse::<i64>() {
return PropertyValue::Integer(v);
}
if let Ok(v) = s.parse::<f64>() {
return PropertyValue::Float(v);
}
match s {
"true" => PropertyValue::Bool(true),
"false" => PropertyValue::Bool(false),
_ => PropertyValue::Text(s.to_string()),
}
}
}
impl From<String> for PropertyValue {
fn from(s: String) -> Self {
PropertyValue::from(s.as_str())
}
}
impl From<&String> for PropertyValue {
fn from(s: &String) -> Self {
PropertyValue::from(s.as_str())
}
}
impl From<i64> for PropertyValue {
fn from(v: i64) -> Self {
PropertyValue::Integer(v)
}
}
impl From<i32> for PropertyValue {
fn from(v: i32) -> Self {
PropertyValue::Integer(v as i64)
}
}
impl From<f64> for PropertyValue {
fn from(v: f64) -> Self {
PropertyValue::Float(v)
}
}
impl From<f32> for PropertyValue {
fn from(v: f32) -> Self {
PropertyValue::Float(v as f64)
}
}
impl From<bool> for PropertyValue {
fn from(v: bool) -> Self {
PropertyValue::Bool(v)
}
}
impl From<usize> for PropertyValue {
fn from(v: usize) -> Self {
PropertyValue::Integer(v as i64)
}
}