Add SQL AND support for conjunctive WHERE filters

This commit is contained in:
Hassan Abedi 2026-04-10 10:00:55 +02:00
parent 685804e60f
commit 77ef8c5ae9
9 changed files with 122 additions and 0 deletions

View File

@ -118,6 +118,7 @@ Currently supported examples:
SELECT * FROM Parent
SELECT c0 FROM Parent
SELECT c0 FROM Parent WHERE c1 = 'bob'
SELECT c0 FROM Parent WHERE c1 = 'bob' AND c0 = 'alice'
SELECT c0 AS parent_name, 'seed' AS label FROM Parent
SELECT Parent.parent, Ancestor.child
FROM Parent, Ancestor
@ -164,6 +165,7 @@ Current limits:
- joins currently use comma-separated tables plus `WHERE` filtering
- multi-table queries require qualified column names such as `Parent.child`
- table aliases are supported via `FROM Parent AS p`
- `WHERE` supports equality predicates combined with `AND`
- no aggregates
- projection aliases only via `AS`

View File

@ -101,6 +101,9 @@ fn eval_predicate(
LogicalExpr::Eq(left, right) => Ok(eval_expr(left, row, schema)?
.sql_eq(&eval_expr(right, row, schema)?)
.unwrap_or(false)),
LogicalExpr::And(left, right) => {
Ok(eval_predicate(left, row, schema)? && eval_predicate(right, row, schema)?)
}
_ => Ok(false),
}
}
@ -123,6 +126,9 @@ fn eval_expr(
let right = eval_expr(right, row, schema)?;
Ok(Value::Boolean(left.sql_eq(&right).unwrap_or(false)))
}
LogicalExpr::And(left, right) => Ok(Value::Boolean(
eval_predicate(left, row, schema)? && eval_predicate(right, row, schema)?,
)),
}
}

View File

@ -440,6 +440,18 @@ mod tests {
}
}
#[test]
fn parse_sql_command_with_and_filter() {
let command =
parse_command("sql SELECT c0 FROM Parent WHERE c1 = 'bob' AND c0 = 'alice';").unwrap();
match command {
Command::Sql(select) => {
assert!(select.selection.is_some());
}
other => panic!("unexpected command: {:?}", other),
}
}
#[test]
fn parse_schema_command() {
let command = parse_command("schema Parent(parent, child).").unwrap();

View File

@ -443,4 +443,19 @@ mod tests {
assert!(output.contains("alice | carol"));
assert!(output.contains("bob | dave"));
}
#[test]
fn session_runs_sql_query_with_and_filter() {
let mut session = Session::new();
let output = session
.execute_script(
"fact Parent(alice, bob).\n\
fact Parent(bob, carol).\n\
sql SELECT c0 FROM Parent WHERE c1 = 'bob' AND c0 = 'alice';",
)
.unwrap();
assert!(output.contains("1 row(s)"));
assert!(output.contains("alice"));
}
}

View File

@ -9,6 +9,8 @@ pub enum LogicalExpr {
Literal(Value),
/// Equality.
Eq(Box<LogicalExpr>, Box<LogicalExpr>),
/// Boolean conjunction.
And(Box<LogicalExpr>, Box<LogicalExpr>),
}
/// A named output expression in a projection.

View File

@ -154,6 +154,10 @@ fn plan_expr(expr: &Expr, schema: &Schema) -> Result<LogicalExpr, PlannerError>
Box::new(plan_expr(left, schema)?),
Box::new(plan_expr(right, schema)?),
)),
BinaryOp::And => Ok(LogicalExpr::And(
Box::new(plan_expr(left, schema)?),
Box::new(plan_expr(right, schema)?),
)),
},
}
}
@ -332,4 +336,28 @@ mod tests {
assert_eq!(schema.fields()[0].name(), "p.parent");
assert_eq!(schema.fields()[1].name(), "q.child");
}
#[test]
fn plans_conjunctive_filter() {
let instance: Instance = vec![Atom::new(
"Parent",
vec![Term::constant("alice"), Term::constant("bob")],
)]
.into_iter()
.collect();
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
let select =
parse_select("SELECT c0 FROM Parent WHERE c1 = 'bob' AND c0 = 'alice'").unwrap();
let plan = plan_select(&select, &catalog).unwrap();
match plan {
LogicalPlan::Project { input, .. } => match *input {
LogicalPlan::Filter { predicate, .. } => {
assert!(matches!(predicate, LogicalExpr::And(_, _)));
}
other => panic!("unexpected input plan: {:?}", other),
},
other => panic!("unexpected plan: {:?}", other),
}
}
}

View File

@ -56,4 +56,6 @@ pub enum Literal {
pub enum BinaryOp {
/// Equality.
Eq,
/// Boolean conjunction.
And,
}

View File

@ -33,6 +33,7 @@ enum Token {
From,
Where,
As,
And,
Null,
Identifier(String),
String(String),
@ -136,6 +137,22 @@ impl Parser {
}
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
let mut expr = self.parse_equality()?;
while self.peek() == Some(&Token::And) {
self.index += 1;
let right = self.parse_equality()?;
expr = Expr::Binary {
left: Box::new(expr),
op: BinaryOp::And,
right: Box::new(right),
};
}
Ok(expr)
}
fn parse_equality(&mut self) -> Result<Expr, ParseError> {
let left = self.parse_operand()?;
match self.next().ok_or(ParseError::UnexpectedEnd)? {
Token::Eq => {
@ -219,6 +236,7 @@ fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
"FROM" => Token::From,
"WHERE" => Token::Where,
"AS" => Token::As,
"AND" => Token::And,
"NULL" => Token::Null,
_ => Token::Identifier(ident),
};
@ -286,6 +304,7 @@ fn render_token(token: &Token) -> String {
Token::From => "FROM".to_string(),
Token::Where => "WHERE".to_string(),
Token::As => "AS".to_string(),
Token::And => "AND".to_string(),
Token::Null => "NULL".to_string(),
Token::Identifier(name) => name.clone(),
Token::String(value) => format!("'{}'", value),
@ -395,4 +414,27 @@ mod tests {
]
);
}
#[test]
fn parses_conjunctive_where_clause() {
let select =
parse_select("SELECT c0 FROM Parent WHERE c1 = 'bob' AND c0 = 'alice'").unwrap();
assert_eq!(
select.selection,
Some(Expr::Binary {
left: Box::new(Expr::Binary {
left: Box::new(Expr::Identifier("c1".to_string())),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(Literal::String("bob".to_string()))),
}),
op: BinaryOp::And,
right: Box::new(Expr::Binary {
left: Box::new(Expr::Identifier("c0".to_string())),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(Literal::String("alice".to_string()))),
}),
})
);
}
}

View File

@ -204,3 +204,16 @@ fn select_self_join_uses_table_aliases() {
vec!["alice -> carol".to_string(), "bob -> dave".to_string()]
);
}
#[test]
fn select_where_and_applies_multiple_filters() {
let instance = parent_instance();
let catalog = PredicateCatalog::from_instance(&instance).unwrap();
let select = parse_select("SELECT c0 FROM Parent WHERE c1 = 'bob' AND c0 = 'alice'").unwrap();
let plan = plan_select(&select, &catalog).unwrap();
let result = execute(&plan, &instance).unwrap();
assert_eq!(result.rows().len(), 1);
assert_eq!(format!("{}", result.rows()[0].values()[0]), "alice");
}