use crate::orm::expressions::Q;
use regex::Regex;
use std::sync::OnceLock;
#[cfg(test)]
use crate::orm::expressions::QOperator;
struct Patterns {
is_null: Regex,
is_not_null: Regex,
between: Regex,
in_clause: Regex,
like: Regex,
comparison: Regex,
}
impl Patterns {
fn new() -> Self {
Self {
is_null: Regex::new(r"(?i)^\s*(\w+)\s+IS\s+NULL\s*$").unwrap(),
is_not_null: Regex::new(r"(?i)^\s*(\w+)\s+IS\s+NOT\s+NULL\s*$").unwrap(),
between: Regex::new(r"(?i)^\s*(\w+)\s+BETWEEN\s+(.+?)\s+AND\s+(.+?)\s*$").unwrap(),
in_clause: Regex::new(r"(?i)^\s*(\w+)\s+IN\s*\((.+?)\)\s*$").unwrap(),
like: Regex::new(r"(?i)^\s*(\w+)\s+LIKE\s+(.+?)\s*$").unwrap(),
comparison: Regex::new(r"(?i)^\s*(\w+)\s*(=|!=|<>|<=|>=|<|>)\s*(.+?)\s*$").unwrap(),
}
}
}
static PATTERNS: OnceLock<Patterns> = OnceLock::new();
fn patterns() -> &'static Patterns {
PATTERNS.get_or_init(Patterns::new)
}
pub struct SqlConditionParser;
impl SqlConditionParser {
pub fn parse(sql: &str) -> Q {
let trimmed = sql.trim();
if let Some(q) = Self::try_parse_compound(trimmed) {
return q;
}
if let Some(q) = Self::try_parse_is_null(trimmed) {
return q;
}
if let Some(q) = Self::try_parse_is_not_null(trimmed) {
return q;
}
if let Some(q) = Self::try_parse_between(trimmed) {
return q;
}
if let Some(q) = Self::try_parse_in(trimmed) {
return q;
}
if let Some(q) = Self::try_parse_like(trimmed) {
return q;
}
if let Some(q) = Self::try_parse_comparison(trimmed) {
return q;
}
Q::Condition {
field: String::new(),
operator: String::new(),
value: trimmed.to_string(),
}
}
fn try_parse_is_null(sql: &str) -> Option<Q> {
let caps = patterns().is_null.captures(sql)?;
let field = caps.get(1)?.as_str().to_string();
Some(Q::Condition {
field,
operator: "IS NULL".to_string(),
value: String::new(),
})
}
fn try_parse_is_not_null(sql: &str) -> Option<Q> {
let caps = patterns().is_not_null.captures(sql)?;
let field = caps.get(1)?.as_str().to_string();
Some(Q::Condition {
field,
operator: "IS NOT NULL".to_string(),
value: String::new(),
})
}
fn try_parse_between(sql: &str) -> Option<Q> {
let caps = patterns().between.captures(sql)?;
let field = caps.get(1)?.as_str().to_string();
let val1 = caps.get(2)?.as_str().trim();
let val2 = caps.get(3)?.as_str().trim();
let q1 = Q::Condition {
field: field.clone(),
operator: ">=".to_string(),
value: val1.to_string(),
};
let q2 = Q::Condition {
field,
operator: "<=".to_string(),
value: val2.to_string(),
};
Some(q1.and(q2))
}
fn try_parse_in(sql: &str) -> Option<Q> {
let caps = patterns().in_clause.captures(sql)?;
let field = caps.get(1)?.as_str().to_string();
let values_str = caps.get(2)?.as_str();
let values: Vec<String> = values_str
.split(',')
.map(|v| v.trim().to_string())
.collect();
let conditions: Vec<Q> = values
.into_iter()
.map(|value| Q::Condition {
field: field.clone(),
operator: "=".to_string(),
value,
})
.collect();
if conditions.is_empty() {
return None;
}
Some(conditions.into_iter().reduce(|acc, q| acc.or(q)).unwrap())
}
fn try_parse_like(sql: &str) -> Option<Q> {
let caps = patterns().like.captures(sql)?;
let field = caps.get(1)?.as_str().to_string();
let pattern = caps.get(2)?.as_str().trim().to_string();
Some(Q::Condition {
field,
operator: "LIKE".to_string(),
value: pattern,
})
}
fn try_parse_comparison(sql: &str) -> Option<Q> {
let caps = patterns().comparison.captures(sql)?;
let field = caps.get(1)?.as_str().to_string();
let operator = caps.get(2)?.as_str().to_string();
let value = caps.get(3)?.as_str().trim().to_string();
Some(Q::Condition {
field,
operator,
value,
})
}
fn try_parse_compound(sql: &str) -> Option<Q> {
if let Some(and_pos) = Self::find_operator(sql, " AND ") {
let left = sql[..and_pos].trim();
let right = sql[and_pos + 5..].trim(); let left_q = Self::parse(left);
let right_q = Self::parse(right);
return Some(left_q.and(right_q));
}
if let Some(or_pos) = Self::find_operator(sql, " OR ") {
let left = sql[..or_pos].trim();
let right = sql[or_pos + 4..].trim(); let left_q = Self::parse(left);
let right_q = Self::parse(right);
return Some(left_q.or(right_q));
}
None
}
fn find_operator(sql: &str, op: &str) -> Option<usize> {
let upper_sql = sql.to_uppercase();
let upper_op = op.to_uppercase();
let mut in_quote = false;
let mut quote_char = ' ';
for (i, ch) in sql.chars().enumerate() {
if ch == '\'' || ch == '"' {
if in_quote && ch == quote_char {
in_quote = false;
} else if !in_quote {
in_quote = true;
quote_char = ch;
}
}
if !in_quote
&& i + upper_op.len() <= upper_sql.len()
&& upper_sql[i..i + upper_op.len()] == *upper_op
{
return Some(i);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_comparison() {
let q = SqlConditionParser::parse("age > 18");
match q {
Q::Condition {
field,
operator,
value,
} => {
assert_eq!(field, "age");
assert_eq!(operator, ">");
assert_eq!(value, "18");
}
_ => panic!("Expected Condition"),
}
}
#[test]
fn test_parse_is_null() {
let q = SqlConditionParser::parse("email IS NULL");
match q {
Q::Condition {
field,
operator,
value,
} => {
assert_eq!(field, "email");
assert_eq!(operator, "IS NULL");
assert_eq!(value, "");
}
_ => panic!("Expected Condition"),
}
}
#[test]
fn test_parse_is_not_null() {
let q = SqlConditionParser::parse("email IS NOT NULL");
match q {
Q::Condition {
field,
operator,
value,
} => {
assert_eq!(field, "email");
assert_eq!(operator, "IS NOT NULL");
assert_eq!(value, "");
}
_ => panic!("Expected Condition"),
}
}
#[test]
fn test_parse_like() {
let q = SqlConditionParser::parse("name LIKE '%John%'");
match q {
Q::Condition {
field,
operator,
value,
} => {
assert_eq!(field, "name");
assert_eq!(operator, "LIKE");
assert_eq!(value, "'%John%'");
}
_ => panic!("Expected Condition"),
}
}
#[test]
fn test_parse_between() {
let q = SqlConditionParser::parse("age BETWEEN 18 AND 65");
match q {
Q::Combined {
operator: QOperator::And,
conditions,
} => {
assert_eq!(conditions.len(), 2);
}
_ => panic!("Expected Combined with AND"),
}
}
#[test]
fn test_parse_in() {
let q = SqlConditionParser::parse("status IN ('active', 'pending')");
match q {
Q::Combined {
operator: QOperator::Or,
conditions,
} => {
assert_eq!(conditions.len(), 2);
}
_ => panic!("Expected Combined with OR"),
}
}
#[test]
fn test_parse_compound_and() {
let q = SqlConditionParser::parse("active = true AND verified = true");
match q {
Q::Combined {
operator: QOperator::And,
conditions,
} => {
assert_eq!(conditions.len(), 2);
}
_ => panic!("Expected Combined with AND"),
}
}
#[test]
fn test_parse_compound_or() {
let q = SqlConditionParser::parse("status = 'draft' OR status = 'pending'");
match q {
Q::Combined {
operator: QOperator::Or,
conditions,
} => {
assert_eq!(conditions.len(), 2);
}
_ => panic!("Expected Combined with OR"),
}
}
}