use serde::Deserialize;
use std::sync::OnceLock;
#[derive(Debug, Clone)]
pub struct SqlMutation {
pub payload: String,
pub description: String,
pub rules_applied: Vec<&'static str>,
}
pub(crate) const COMMENT_TERMINATORS: &[&str] =
&["--", "-- ", "--+", "#", "/*", ";--", "-- -", ";#"];
pub(crate) const WHITESPACE_ALTERNATIVES: &[&str] = &[
" ", "\t", "\n", "/**/", "/**_***/", "+(+", "%0a", "%0d", "%0c", "%0b", "%a0",
];
const SQL_OPERATORS_TOML: &str = include_str!("../../../rules/sql/operators.toml");
#[derive(Debug, Clone, Deserialize)]
struct OrAlternative {
pattern: String,
#[allow(dead_code)]
description: String,
}
#[derive(Debug, Clone, Deserialize)]
struct AndAlternative {
pattern: String,
#[allow(dead_code)]
description: String,
}
#[derive(Debug, Clone, Deserialize)]
struct EqualityAlternative {
pattern: String,
#[allow(dead_code)]
description: String,
}
#[derive(Debug, Clone, Deserialize)]
struct SqlOperatorRules {
#[serde(default)]
or_alternative: Vec<OrAlternative>,
#[serde(default)]
and_alternative: Vec<AndAlternative>,
#[serde(default)]
equality_alternative: Vec<EqualityAlternative>,
}
impl Default for SqlOperatorRules {
fn default() -> Self {
Self {
or_alternative: vec![
OrAlternative {
pattern: "OR".into(),
description: "Standard SQL OR".into(),
},
OrAlternative {
pattern: "||".into(),
description: "SQLite/Oracle OR".into(),
},
],
and_alternative: vec![
AndAlternative {
pattern: "AND".into(),
description: "Standard SQL AND".into(),
},
AndAlternative {
pattern: "&&".into(),
description: "MySQL logical AND".into(),
},
],
equality_alternative: vec![
EqualityAlternative {
pattern: "=".into(),
description: "Standard equality".into(),
},
EqualityAlternative {
pattern: " LIKE ".into(),
description: "LIKE operator".into(),
},
EqualityAlternative {
pattern: " REGEXP ".into(),
description: "REGEXP operator".into(),
},
],
}
}
}
fn get_rules() -> &'static SqlOperatorRules {
static RULES: OnceLock<SqlOperatorRules> = OnceLock::new();
RULES.get_or_init(|| {
toml::from_str(SQL_OPERATORS_TOML).unwrap_or_else(|e| {
tracing::warn!(error = %e, "invalid TOML in rules/sql/operators.toml");
SqlOperatorRules::default()
})
})
}
pub(crate) fn or_alternatives() -> &'static [String] {
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE.get_or_init(|| {
get_rules()
.or_alternative
.iter()
.map(|a| a.pattern.clone())
.collect()
})
}
pub(crate) fn and_alternatives() -> &'static [String] {
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE.get_or_init(|| {
get_rules()
.and_alternative
.iter()
.map(|a| a.pattern.clone())
.collect()
})
}
pub(crate) fn equality_alternatives() -> &'static [String] {
static CACHE: OnceLock<Vec<String>> = OnceLock::new();
CACHE.get_or_init(|| {
get_rules()
.equality_alternative
.iter()
.map(|a| a.pattern.clone())
.collect()
})
}
pub(crate) fn extract_quoted_string(payload: &str) -> Option<String> {
let chars: Vec<char> = payload.chars().collect();
let mut start = None;
for (index, ch) in chars.iter().copied().enumerate() {
if ch != '\'' {
continue;
}
if index > 0 && chars[index - 1] == '\\' {
continue;
}
if let Some(open_index) = start {
let value: String = chars[open_index + 1..index].iter().collect();
if !value.is_empty() && value.len() <= 20 {
return Some(value);
}
start = None;
} else {
start = Some(index);
}
}
None
}