Add SQL AND support for conjunctive WHERE filters
This commit is contained in:
parent
685804e60f
commit
77ef8c5ae9
@ -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`
|
||||
|
||||
|
||||
@ -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)?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,4 +56,6 @@ pub enum Literal {
|
||||
pub enum BinaryOp {
|
||||
/// Equality.
|
||||
Eq,
|
||||
/// Boolean conjunction.
|
||||
And,
|
||||
}
|
||||
|
||||
@ -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()))),
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user