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 * FROM Parent
|
||||||
SELECT c0 FROM Parent
|
SELECT c0 FROM Parent
|
||||||
SELECT c0 FROM Parent WHERE c1 = 'bob'
|
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 c0 AS parent_name, 'seed' AS label FROM Parent
|
||||||
SELECT Parent.parent, Ancestor.child
|
SELECT Parent.parent, Ancestor.child
|
||||||
FROM Parent, Ancestor
|
FROM Parent, Ancestor
|
||||||
@ -164,6 +165,7 @@ Current limits:
|
|||||||
- joins currently use comma-separated tables plus `WHERE` filtering
|
- joins currently use comma-separated tables plus `WHERE` filtering
|
||||||
- multi-table queries require qualified column names such as `Parent.child`
|
- multi-table queries require qualified column names such as `Parent.child`
|
||||||
- table aliases are supported via `FROM Parent AS p`
|
- table aliases are supported via `FROM Parent AS p`
|
||||||
|
- `WHERE` supports equality predicates combined with `AND`
|
||||||
- no aggregates
|
- no aggregates
|
||||||
- projection aliases only via `AS`
|
- projection aliases only via `AS`
|
||||||
|
|
||||||
|
|||||||
@ -101,6 +101,9 @@ fn eval_predicate(
|
|||||||
LogicalExpr::Eq(left, right) => Ok(eval_expr(left, row, schema)?
|
LogicalExpr::Eq(left, right) => Ok(eval_expr(left, row, schema)?
|
||||||
.sql_eq(&eval_expr(right, row, schema)?)
|
.sql_eq(&eval_expr(right, row, schema)?)
|
||||||
.unwrap_or(false)),
|
.unwrap_or(false)),
|
||||||
|
LogicalExpr::And(left, right) => {
|
||||||
|
Ok(eval_predicate(left, row, schema)? && eval_predicate(right, row, schema)?)
|
||||||
|
}
|
||||||
_ => Ok(false),
|
_ => Ok(false),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,6 +126,9 @@ fn eval_expr(
|
|||||||
let right = eval_expr(right, row, schema)?;
|
let right = eval_expr(right, row, schema)?;
|
||||||
Ok(Value::Boolean(left.sql_eq(&right).unwrap_or(false)))
|
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]
|
#[test]
|
||||||
fn parse_schema_command() {
|
fn parse_schema_command() {
|
||||||
let command = parse_command("schema Parent(parent, child).").unwrap();
|
let command = parse_command("schema Parent(parent, child).").unwrap();
|
||||||
|
|||||||
@ -443,4 +443,19 @@ mod tests {
|
|||||||
assert!(output.contains("alice | carol"));
|
assert!(output.contains("alice | carol"));
|
||||||
assert!(output.contains("bob | dave"));
|
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),
|
Literal(Value),
|
||||||
/// Equality.
|
/// Equality.
|
||||||
Eq(Box<LogicalExpr>, Box<LogicalExpr>),
|
Eq(Box<LogicalExpr>, Box<LogicalExpr>),
|
||||||
|
/// Boolean conjunction.
|
||||||
|
And(Box<LogicalExpr>, Box<LogicalExpr>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A named output expression in a projection.
|
/// 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(left, schema)?),
|
||||||
Box::new(plan_expr(right, 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()[0].name(), "p.parent");
|
||||||
assert_eq!(schema.fields()[1].name(), "q.child");
|
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 {
|
pub enum BinaryOp {
|
||||||
/// Equality.
|
/// Equality.
|
||||||
Eq,
|
Eq,
|
||||||
|
/// Boolean conjunction.
|
||||||
|
And,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ enum Token {
|
|||||||
From,
|
From,
|
||||||
Where,
|
Where,
|
||||||
As,
|
As,
|
||||||
|
And,
|
||||||
Null,
|
Null,
|
||||||
Identifier(String),
|
Identifier(String),
|
||||||
String(String),
|
String(String),
|
||||||
@ -136,6 +137,22 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_expr(&mut self) -> Result<Expr, ParseError> {
|
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()?;
|
let left = self.parse_operand()?;
|
||||||
match self.next().ok_or(ParseError::UnexpectedEnd)? {
|
match self.next().ok_or(ParseError::UnexpectedEnd)? {
|
||||||
Token::Eq => {
|
Token::Eq => {
|
||||||
@ -219,6 +236,7 @@ fn tokenize(input: &str) -> Result<Vec<Token>, ParseError> {
|
|||||||
"FROM" => Token::From,
|
"FROM" => Token::From,
|
||||||
"WHERE" => Token::Where,
|
"WHERE" => Token::Where,
|
||||||
"AS" => Token::As,
|
"AS" => Token::As,
|
||||||
|
"AND" => Token::And,
|
||||||
"NULL" => Token::Null,
|
"NULL" => Token::Null,
|
||||||
_ => Token::Identifier(ident),
|
_ => Token::Identifier(ident),
|
||||||
};
|
};
|
||||||
@ -286,6 +304,7 @@ fn render_token(token: &Token) -> String {
|
|||||||
Token::From => "FROM".to_string(),
|
Token::From => "FROM".to_string(),
|
||||||
Token::Where => "WHERE".to_string(),
|
Token::Where => "WHERE".to_string(),
|
||||||
Token::As => "AS".to_string(),
|
Token::As => "AS".to_string(),
|
||||||
|
Token::And => "AND".to_string(),
|
||||||
Token::Null => "NULL".to_string(),
|
Token::Null => "NULL".to_string(),
|
||||||
Token::Identifier(name) => name.clone(),
|
Token::Identifier(name) => name.clone(),
|
||||||
Token::String(value) => format!("'{}'", value),
|
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()]
|
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