use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Predicate {
Equals(String, PredicateValue),
NotEquals(String, PredicateValue),
GreaterThan(String, f64),
LessThan(String, f64),
GreaterOrEqual(String, f64),
LessOrEqual(String, f64),
In(String, Vec<String>),
Like(String, String),
And(Box<Predicate>, Box<Predicate>),
Or(Box<Predicate>, Box<Predicate>),
Not(Box<Predicate>),
}
#[derive(Debug, Clone)]
pub enum PredicateValue {
String(String),
Number(f64),
Bool(bool),
}
#[derive(Debug, Clone)]
pub enum AggregationFunc {
Count,
Sum(String),
Avg(String),
Min(String),
Max(String),
CountDistinct(String),
}
#[derive(Debug, Clone)]
pub struct GroupByClause {
pub columns: Vec<String>,
pub aggregations: Vec<(String, AggregationFunc)>,
}
#[derive(Debug, Clone)]
pub struct SelectStatement {
pub columns: Vec<String>,
pub from_table: String,
pub where_clause: Option<Predicate>,
pub group_by: Option<GroupByClause>,
pub limit: Option<usize>,
}
pub struct QueryPlanner {
estimated_selectivity: f64,
}
impl QueryPlanner {
pub fn new() -> Self {
Self {
estimated_selectivity: 1.0,
}
}
pub fn parse_where_clause(where_str: &str) -> Option<Predicate> {
let tokens = Self::tokenize(where_str);
if tokens.is_empty() {
return None;
}
Self::parse_or(&tokens, 0).map(|(pred, _)| pred)
}
fn tokenize(input: &str) -> Vec<String> {
input
.split(|c: char| c.is_whitespace())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
fn parse_or(tokens: &[String], mut pos: usize) -> Option<(Predicate, usize)> {
let (mut left, new_pos) = Self::parse_and(tokens, pos)?;
pos = new_pos;
while pos < tokens.len() && tokens[pos].to_uppercase() == "OR" {
pos += 1;
let (right, new_pos) = Self::parse_and(tokens, pos)?;
pos = new_pos;
left = Predicate::Or(Box::new(left), Box::new(right));
}
Some((left, pos))
}
fn parse_and(tokens: &[String], mut pos: usize) -> Option<(Predicate, usize)> {
let (mut left, new_pos) = Self::parse_comparison(tokens, pos)?;
pos = new_pos;
while pos < tokens.len() && tokens[pos].to_uppercase() == "AND" {
pos += 1;
let (right, new_pos) = Self::parse_comparison(tokens, pos)?;
pos = new_pos;
left = Predicate::And(Box::new(left), Box::new(right));
}
Some((left, pos))
}
fn parse_comparison(tokens: &[String], pos: usize) -> Option<(Predicate, usize)> {
if pos + 2 >= tokens.len() {
return None;
}
let col = tokens[pos].clone();
let op = tokens[pos + 1].to_uppercase();
let val_str = tokens[pos + 2].clone();
let pred = match op.as_str() {
"=" => Predicate::Equals(col, PredicateValue::String(val_str.trim_matches('\'').to_string())),
"!=" | "<>" => Predicate::NotEquals(col, PredicateValue::String(val_str.trim_matches('\'').to_string())),
">" => Predicate::GreaterThan(col, val_str.parse::<f64>().ok()?),
"<" => Predicate::LessThan(col, val_str.parse::<f64>().ok()?),
">=" => Predicate::GreaterOrEqual(col, val_str.parse::<f64>().ok()?),
"<=" => Predicate::LessOrEqual(col, val_str.parse::<f64>().ok()?),
"LIKE" => Predicate::Like(col, val_str.trim_matches('\'').to_string()),
_ => return None,
};
Some((pred, pos + 3))
}
pub fn estimate_selectivity(&mut self, predicate: &Predicate) -> f64 {
let sel = match predicate {
Predicate::Equals(_, _) => 0.01,
Predicate::GreaterThan(_, _) => 0.5,
Predicate::LessThan(_, _) => 0.5,
Predicate::In(_, vals) => vals.len() as f64 * 0.01,
Predicate::Like(_, _) => 0.1,
Predicate::And(left, right) => {
self.estimate_selectivity(left) * self.estimate_selectivity(right)
}
Predicate::Or(left, right) => {
let l = self.estimate_selectivity(left);
let r = self.estimate_selectivity(right);
l + r - (l * r)
}
Predicate::Not(inner) => 1.0 - self.estimate_selectivity(inner),
_ => 0.1,
};
self.estimated_selectivity = sel;
sel
}
}
pub struct RowFilter;
impl RowFilter {
pub fn matches(predicate: &Predicate, row: &HashMap<String, String>) -> bool {
match predicate {
Predicate::Equals(col, val) => {
row.get(col).map_or(false, |v| {
match val {
PredicateValue::String(s) => v == s,
_ => false,
}
})
}
Predicate::GreaterThan(col, threshold) => {
row.get(col)
.and_then(|v| v.parse::<f64>().ok())
.map_or(false, |v| v > *threshold)
}
Predicate::LessThan(col, threshold) => {
row.get(col)
.and_then(|v| v.parse::<f64>().ok())
.map_or(false, |v| v < *threshold)
}
Predicate::GreaterOrEqual(col, threshold) => {
row.get(col)
.and_then(|v| v.parse::<f64>().ok())
.map_or(false, |v| v >= *threshold)
}
Predicate::LessOrEqual(col, threshold) => {
row.get(col)
.and_then(|v| v.parse::<f64>().ok())
.map_or(false, |v| v <= *threshold)
}
Predicate::Like(col, pattern) => {
row.get(col).map_or(false, |v| Self::like_match(v, pattern))
}
Predicate::And(left, right) => {
Self::matches(left, row) && Self::matches(right, row)
}
Predicate::Or(left, right) => {
Self::matches(left, row) || Self::matches(right, row)
}
Predicate::Not(inner) => !Self::matches(inner, row),
_ => true,
}
}
fn like_match(text: &str, pattern: &str) -> bool {
let parts: Vec<&str> = pattern.split('%').collect();
match parts.len() {
1 => text == parts[0],
2 => {
if parts[0].is_empty() {
text.ends_with(parts[1])
} else if parts[1].is_empty() {
text.starts_with(parts[0])
} else {
text.starts_with(parts[0]) && text.ends_with(parts[1])
}
}
_ => {
let mut pos = 0;
for (i, part) in parts.iter().enumerate() {
if i == 0 && !part.is_empty() {
if !text.starts_with(part) {
return false;
}
pos = part.len();
} else if i == parts.len() - 1 && !part.is_empty() {
if !text[pos..].ends_with(part) {
return false;
}
} else if !part.is_empty() {
match text[pos..].find(part) {
Some(offset) => pos += offset + part.len(),
None => return false,
}
}
}
true
}
}
}
}
pub struct GroupByExecutor;
impl GroupByExecutor {
pub fn execute(
rows: Vec<HashMap<String, String>>,
group_cols: &[String],
aggs: &[(String, AggregationFunc)],
) -> Vec<HashMap<String, String>> {
let mut groups: HashMap<String, Vec<HashMap<String, String>>> = HashMap::new();
for row in rows {
let key = group_cols
.iter()
.map(|col| row.get(col).cloned().unwrap_or_default())
.collect::<Vec<_>>()
.join("|");
groups.entry(key).or_insert_with(Vec::new).push(row);
}
let mut results = Vec::new();
for (key, group) in groups {
let mut result = HashMap::new();
let key_parts: Vec<&str> = key.split('|').collect();
for (i, col) in group_cols.iter().enumerate() {
if i < key_parts.len() {
result.insert(col.clone(), key_parts[i].to_string());
}
}
for (alias, agg_func) in aggs {
let agg_val = match agg_func {
AggregationFunc::Count => group.len().to_string(),
AggregationFunc::Sum(col) => {
let sum: f64 = group
.iter()
.filter_map(|row| row.get(col).and_then(|v| v.parse::<f64>().ok()))
.sum();
sum.to_string()
}
AggregationFunc::Avg(col) => {
let values: Vec<f64> = group
.iter()
.filter_map(|row| row.get(col).and_then(|v| v.parse::<f64>().ok()))
.collect();
if !values.is_empty() {
(values.iter().sum::<f64>() / values.len() as f64).to_string()
} else {
"0".to_string()
}
}
AggregationFunc::Min(col) => {
group
.iter()
.filter_map(|row| row.get(col).cloned())
.min()
.unwrap_or_default()
}
AggregationFunc::Max(col) => {
group
.iter()
.filter_map(|row| row.get(col).cloned())
.max()
.unwrap_or_default()
}
AggregationFunc::CountDistinct(col) => {
let distinct: std::collections::HashSet<_> = group
.iter()
.filter_map(|row| row.get(col).cloned())
.collect();
distinct.len().to_string()
}
};
result.insert(alias.clone(), agg_val);
}
results.push(result);
}
results
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_where_clause() {
let pred = QueryPlanner::parse_where_clause("age > 30 AND city = NYC");
assert!(pred.is_some());
}
#[test]
fn test_row_filter_equals() {
let mut row = HashMap::new();
row.insert("name".to_string(), "Alice".to_string());
let pred = Predicate::Equals("name".to_string(), PredicateValue::String("Alice".to_string()));
assert!(RowFilter::matches(&pred, &row));
}
#[test]
fn test_like_matching() {
assert!(RowFilter::like_match("hello", "hel%"));
assert!(RowFilter::like_match("hello", "%llo"));
assert!(!RowFilter::like_match("hello", "world%"));
}
#[test]
fn test_group_by_execution() {
let mut row1 = HashMap::new();
row1.insert("category".to_string(), "A".to_string());
row1.insert("amount".to_string(), "100".to_string());
let mut row2 = HashMap::new();
row2.insert("category".to_string(), "A".to_string());
row2.insert("amount".to_string(), "50".to_string());
let rows = vec![row1, row2];
let groups = vec!["category".to_string()];
let aggs = vec![("total".to_string(), AggregationFunc::Sum("amount".to_string()))];
let results = GroupByExecutor::execute(rows, &groups, &aggs);
assert_eq!(results.len(), 1);
}
}