diff --git a/doc/sql/query.ebnf b/doc/sql/query.ebnf index addedb7acde1b5d5ebeaecd20efc3d0183a402d3..c38100a648bb103516a18c4f1d74ed9b7a848714 100644 --- a/doc/sql/query.ebnf +++ b/doc/sql/query.ebnf @@ -38,7 +38,7 @@ expression ::= ('NOT'* ( | 'NOT'? 'EXISTS' '(' dql ')' | '(' dql ')' | '(' expression (',' expression)* ')' - ) ('IS' 'NOT'? 'NULL')?) + ) ('IS' 'NOT'? ('NULL' | 'TRUE' | 'FALSE' | 'UNKNOWN'))*) | expression ( 'NOT'? 'BETWEEN' expression 'AND' diff --git a/sbroad-core/src/frontend/sql.rs b/sbroad-core/src/frontend/sql.rs index 524486d5d7114df70cb72f97a4b1377d330f3c5d..42972581d535fd941d5501032908fdca5e0e46ea 100644 --- a/sbroad-core/src/frontend/sql.rs +++ b/sbroad-core/src/frontend/sql.rs @@ -1550,7 +1550,7 @@ fn parse_param<M: Metadata>( lazy_static::lazy_static! { static ref PRATT_PARSER: PrattParser<Rule> = { use pest::pratt_parser::{Assoc::{Left, Right}, Op}; - use Rule::{Add, And, Between, ConcatInfixOp, Divide, Eq, Escape, Gt, GtEq, In, IsNullPostfix, CastPostfix, Like, Lt, LtEq, Multiply, NotEq, Or, Subtract, UnaryNot}; + use Rule::{Add, And, Between, ConcatInfixOp, Divide, Eq, Escape, Gt, GtEq, In, IsPostfix, CastPostfix, Like, Lt, LtEq, Multiply, NotEq, Or, Subtract, UnaryNot}; // Precedence is defined lowest to highest. PrattParser::new() @@ -1568,7 +1568,7 @@ lazy_static::lazy_static! { ) .op(Op::infix(Add, Left) | Op::infix(Subtract, Left)) .op(Op::infix(Multiply, Left) | Op::infix(Divide, Left) | Op::infix(ConcatInfixOp, Left)) - .op(Op::postfix(IsNullPostfix)) + .op(Op::postfix(IsPostfix)) .op(Op::postfix(CastPostfix)) }; } @@ -1793,9 +1793,10 @@ enum ParseExpression { is_not: bool, child: Box<ParseExpression>, }, - IsNull { + Is { is_not: bool, child: Box<ParseExpression>, + value: Option<bool>, }, Cast { cast_type: CastType, @@ -2270,10 +2271,20 @@ impl ParseExpression { op_id } } - ParseExpression::IsNull { is_not, child } => { + ParseExpression::Is { + is_not, + child, + value, + } => { let child_plan_id = child.populate_plan(plan, worker)?; let child_covered_with_row = plan.row(child_plan_id)?; - let op_id = plan.add_unary(Unary::IsNull, child_covered_with_row)?; + let op_id = match value { + None => plan.add_unary(Unary::IsNull, child_covered_with_row)?, + Some(b) => { + let right_operand = plan.add_const(Value::Boolean(*b)); + plan.add_bool(child_covered_with_row, Bool::Eq, right_operand)? + } + }; if *is_not { plan.add_unary(Unary::Not, op_id)? } else { @@ -2826,14 +2837,25 @@ where let cast_type = cast_type_from_pair(ty_pair)?; Ok(ParseExpression::Cast { child: Box::new(child), cast_type }) } - Rule::IsNullPostfix => { - let is_not = match op.into_inner().len() { - 1 => true, - 0 => false, - _ => unreachable!("IsNull must have 0 or 1 children") + Rule::IsPostfix => { + let mut inner = op.into_inner(); + let (is_not, value_index) = match inner.len() { + 2 => (true, 1), + 1 => (false, 0), + _ => unreachable!("Is must have 1 or 2 children") }; - Ok(ParseExpression::IsNull { is_not, child: Box::new(child)}) - }, + let value_rule = inner + .nth(value_index) + .expect("Value must be present under Is") + .as_rule(); + let value = match value_rule { + Rule::True => Some(true), + Rule::False => Some(false), + Rule::Unknown | Rule::Null => None, + _ => unreachable!("Is value must be TRUE, FALSE, NULL or UNKNOWN") + }; + Ok(ParseExpression::Is { is_not, child: Box::new(child), value }) + } rule => unreachable!("Expr::parse expected postfix operator, found {:?}", rule), } }) diff --git a/sbroad-core/src/frontend/sql/ir/tests.rs b/sbroad-core/src/frontend/sql/ir/tests.rs index 0daa3b1dc656f08dc61fa523486fba1ee33ae5a8..0fe1f6c25d9229f7b9946168e273d8cc59ce41e2 100644 --- a/sbroad-core/src/frontend/sql/ir/tests.rs +++ b/sbroad-core/src/frontend/sql/ir/tests.rs @@ -24,6 +24,11 @@ fn sql_to_optimized_ir_add_motions_err(query: &str) -> SbroadError { plan.add_motions().unwrap_err() } +fn check_output(input: &str, params: Vec<Value>, expected_explain: &str) { + let plan = sql_to_optimized_ir(input, params); + assert_eq!(expected_explain, plan.as_explain().unwrap()); +} + #[test] fn front_sql1() { let input = r#"SELECT "identification_number", "product_code" FROM "hash_testing" @@ -409,6 +414,95 @@ execution options: assert_eq!(expected_explain, plan.as_explain().unwrap()); } +#[test] +fn front_sql_is_true() { + check_output( + "select true is true", + vec![], + r#"projection (ROW(true::boolean) = true::boolean -> "col_1") +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +"#, + ); + + check_output( + "select true is not true", + vec![], + r#"projection (not ROW(true::boolean) = true::boolean -> "col_1") +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +"#, + ); +} + +#[test] +fn front_sql_is_false() { + check_output( + "select true is false", + vec![], + r#"projection (ROW(true::boolean) = false::boolean -> "col_1") +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +"#, + ); + + check_output( + "select true is not false", + vec![], + r#"projection (not ROW(true::boolean) = false::boolean -> "col_1") +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +"#, + ); +} + +#[test] +fn front_sql_is_null_unknown() { + check_output( + "select true is null", + vec![], + r#"projection (ROW(true::boolean) is null -> "col_1") +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +"#, + ); + + check_output( + "select true is unknown", + vec![], + r#"projection (ROW(true::boolean) is null -> "col_1") +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +"#, + ); + + check_output( + "select true is not null", + vec![], + r#"projection (not ROW(true::boolean) is null -> "col_1") +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +"#, + ); + + check_output( + "select true is not unknown", + vec![], + r#"projection (not ROW(true::boolean) is null -> "col_1") +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +"#, + ); +} + #[test] fn front_sql_between_with_additional_non_bool_value_from_left() { let input = r#"SELECT * FROM "test_space" WHERE 42 and 1 between 2 and 3"#; diff --git a/sbroad-core/src/frontend/sql/query.pest b/sbroad-core/src/frontend/sql/query.pest index 78ce15ee3b4089d1973de641d1a4f44eabb2ae76..43ee7474ccedc70f3508cdecefca15400ba9cf93 100644 --- a/sbroad-core/src/frontend/sql/query.pest +++ b/sbroad-core/src/frontend/sql/query.pest @@ -320,10 +320,11 @@ Expr = ${ ExprAtomValue ~ (ExprInfixOpo ~ ExprAtomValue)* } Lt = { "<" } LtEq = { "<=" } NotEq = { "<>" | "!=" } - ExprAtomValue = _{ (UnaryNot ~ W)* ~ AtomicExpr ~ CastPostfix* ~ (W ~ IsNullPostfix)? } + ExprAtomValue = _{ (UnaryNot ~ W)* ~ AtomicExpr ~ CastPostfix* ~ (W ~ IsPostfix)* } UnaryNot = { NotFlag } CastPostfix = { "::" ~ ColumnDefType } - IsNullPostfix = ${ ^"is" ~ W ~ (NotFlag ~ W )? ~ ^"null" } + IsPostfix = ${ ^"is" ~ W ~ (NotFlag ~ W)? ~ (True | False | Unknown | Null) } + Unknown = { ^"unknown" } AtomicExpr = _{ Literal | Parameter | CastOp | Trim | CurrentDate | IdentifierWithOptionalContinuation | ExpressionInParentheses | UnaryOperator | Case | SubQuery | Row } Literal = { True | False | Null | Double | Decimal | Unsigned | Integer | SingleQuotedString } True = { ^"true" }