From 45c29bfeff3e50c2299f53168abeaead58a993aa Mon Sep 17 00:00:00 2001
From: Denis Smirnov <sd@picodata.io>
Date: Tue, 26 Jul 2022 12:16:42 +0700
Subject: [PATCH] fix: merge tuple produced invalid compare expression

The error was produced by the assumption that (2, 1) > (1, 2) is
equivalent to (2 > 1) and (1 > 2). That assumption was incorrect,
so we don't merge AND chains for any operators other than Eq and
NotEq.
---
 src/executor/tests.rs                       |   2 +-
 src/ir/transformation/merge_tuples.rs       | 119 +++++++++-----------
 src/ir/transformation/merge_tuples/tests.rs |  13 ++-
 test_app/test/integration/api_test.lua      |  15 +++
 4 files changed, 77 insertions(+), 72 deletions(-)

diff --git a/src/executor/tests.rs b/src/executor/tests.rs
index 5535322979..1da3e01aba 100644
--- a/src/executor/tests.rs
+++ b/src/executor/tests.rs
@@ -349,7 +349,7 @@ WHERE "t3"."id" = 2 AND "t8"."identification_number" = 2"#;
                             r#"FROM ("#,
                             r#"SELECT "test_space"."id" as "id", "test_space"."FIRST_NAME" as "FIRST_NAME""#,
                             r#"FROM "test_space""#,
-                            r#"WHERE (?) > ("test_space"."sys_op") and ("test_space"."sysFrom") >= (?)"#,
+                            r#"WHERE ("test_space"."sysFrom") >= (?) and ("test_space"."sys_op") < (?)"#,
                             r#"UNION ALL"#,
                             r#"SELECT "test_space_hist"."id" as "id", "test_space_hist"."FIRST_NAME" as "FIRST_NAME""#,
                             r#"FROM "test_space_hist""#,
diff --git a/src/ir/transformation/merge_tuples.rs b/src/ir/transformation/merge_tuples.rs
index 5249b9f210..89c5154d09 100644
--- a/src/ir/transformation/merge_tuples.rs
+++ b/src/ir/transformation/merge_tuples.rs
@@ -1,4 +1,4 @@
-//! Merge tuples in a disjunction of boolean expressions
+//! Merge tuples in a disjunction of equality expressions
 //! into a single tuple.
 //!
 //! Example:
@@ -36,10 +36,10 @@ fn call_as_plan(chain: &Chain, plan: &mut Plan) -> Result<usize, QueryPlannerErr
 /// "AND" chain grouped by the operator type.
 #[derive(Debug)]
 pub struct Chain {
-    // Left and right sides of the boolean expression
+    // Left and right sides of the equality (and non-equality) expressions
     // grouped by the operator.
     grouped: HashMap<Bool, (Vec<usize>, Vec<usize>)>,
-    // Non-boolean expressions in the AND chain (true, false, null).
+    // Other boolean expressions in the AND chain (true, false, null, Lt, GtEq, etc).
     other: Vec<usize>,
 }
 
@@ -62,67 +62,65 @@ impl Chain {
     pub fn insert(&mut self, plan: &mut Plan, expr_id: usize) -> Result<(), QueryPlannerError> {
         let bool_expr = plan.get_expression_node(expr_id)?;
         if let Expression::Bool { left, op, right } = bool_expr {
-            let (left_id, right_id, group_op) = match *op {
-                Bool::And | Bool::Or => {
-                    // We expect only AND/OR expressions.
-                    return Err(QueryPlannerError::CustomError(format!(
-                        "AND/OR expressions are not supported: {:?}",
-                        bool_expr
-                    )));
-                }
-                Bool::Eq | Bool::NotEq => {
-                    // Try to put expressions with references to the left side.
+            if let Bool::And | Bool::Or = op {
+                // We don't expect nested AND/OR expressions in DNF.
+                return Err(QueryPlannerError::CustomError(format!(
+                    "AND/OR expressions are not supported: {:?}",
+                    bool_expr
+                )));
+            }
+
+            // Merge expression into tuples only for equality (and non-equality) operators.
+            if let Bool::Eq | Bool::NotEq = op {
+                // Try to put expressions with references to the left side.
+                let (left_id, right_id, group_op) =
                     match (plan.is_ref(*left)?, plan.is_ref(*right)?) {
                         (false, true) => (*right, *left, op.clone()),
                         _ => (*left, *right, op.clone()),
+                    };
+
+                // If boolean expression contains a reference to an additional
+                //  sub-query, it should be added to the "other" list.
+                let left_sq = plan.get_sub_query_from_row_node(left_id)?;
+                let right_sq = plan.get_sub_query_from_row_node(right_id)?;
+                for sq_id in [left_sq, right_sq].iter().flatten() {
+                    if plan.is_additional_child(*sq_id)? {
+                        self.other.push(expr_id);
+                        return Ok(());
                     }
                 }
-                Bool::Gt | Bool::GtEq | Bool::In => (*left, *right, op.clone()),
-                // Invert operator to unite expressions with Gt and GtEq.
-                Bool::Lt => (*right, *left, Bool::Gt),
-                Bool::LtEq => (*right, *left, Bool::GtEq),
-            };
 
-            // If boolean expression contains a reference to an additional
-            //  sub-query, it should be added to the "other" list.
-            let left_sq = plan.get_sub_query_from_row_node(left_id)?;
-            let right_sq = plan.get_sub_query_from_row_node(right_id)?;
-            for sq_id in [left_sq, right_sq].iter().flatten() {
-                if plan.is_additional_child(*sq_id)? {
-                    self.other.push(expr_id);
-                    return Ok(());
-                }
-            }
-
-            match self.grouped.entry(group_op) {
-                Entry::Occupied(mut entry) => {
-                    let (left, right) = entry.get_mut();
-                    let new_left_id = plan.expr_clone(left_id)?;
-                    let new_right_id = plan.expr_clone(right_id)?;
-                    plan.get_columns_or_self(new_left_id)?
-                        .iter()
-                        .for_each(|id| {
-                            left.push(*id);
-                        });
-                    plan.get_columns_or_self(new_right_id)?
-                        .iter()
-                        .for_each(|id| {
-                            right.push(*id);
-                        });
-                }
-                Entry::Vacant(entry) => {
-                    let new_left_id = plan.expr_clone(left_id)?;
-                    let new_right_id = plan.expr_clone(right_id)?;
-                    entry.insert((
-                        plan.get_columns_or_self(new_left_id)?,
-                        plan.get_columns_or_self(new_right_id)?,
-                    ));
+                match self.grouped.entry(group_op) {
+                    Entry::Occupied(mut entry) => {
+                        let (left, right) = entry.get_mut();
+                        let new_left_id = plan.expr_clone(left_id)?;
+                        let new_right_id = plan.expr_clone(right_id)?;
+                        plan.get_columns_or_self(new_left_id)?
+                            .iter()
+                            .for_each(|id| {
+                                left.push(*id);
+                            });
+                        plan.get_columns_or_self(new_right_id)?
+                            .iter()
+                            .for_each(|id| {
+                                right.push(*id);
+                            });
+                    }
+                    Entry::Vacant(entry) => {
+                        let new_left_id = plan.expr_clone(left_id)?;
+                        let new_right_id = plan.expr_clone(right_id)?;
+                        entry.insert((
+                            plan.get_columns_or_self(new_left_id)?,
+                            plan.get_columns_or_self(new_right_id)?,
+                        ));
+                    }
                 }
+                return Ok(());
             }
-        } else {
-            let new_expr_id = plan.expr_clone(expr_id)?;
-            self.other.push(new_expr_id);
         }
+
+        let new_expr_id = plan.expr_clone(expr_id)?;
+        self.other.push(new_expr_id);
         Ok(())
     }
 
@@ -142,16 +140,7 @@ impl Chain {
         // To make serialization non-flaky, we extract operators
         // in a deterministic order.
         let mut grouped_top_id: Option<usize> = None;
-        // No need for "And" and "Or" operators.
-        let ordered_ops = &[
-            Bool::Eq,
-            Bool::Gt,
-            Bool::GtEq,
-            Bool::In,
-            Bool::Lt,
-            Bool::LtEq,
-            Bool::NotEq,
-        ];
+        let ordered_ops = &[Bool::Eq, Bool::NotEq];
         for op in ordered_ops {
             if let Some((left, right)) = self.grouped.get(op) {
                 let left_row_id = plan.nodes.add_row(left.clone(), None);
diff --git a/src/ir/transformation/merge_tuples/tests.rs b/src/ir/transformation/merge_tuples/tests.rs
index 479ea130fa..18559b5381 100644
--- a/src/ir/transformation/merge_tuples/tests.rs
+++ b/src/ir/transformation/merge_tuples/tests.rs
@@ -15,13 +15,13 @@ fn merge_tuples1() {
         format!(
             "{} {}",
             r#"SELECT "t"."a" as "a" FROM "t""#,
-            r#"WHERE ("t"."a", "t"."b") = (?, ?) and (?, "t"."a") > ("t"."c", ?)"#,
+            r#"WHERE ("t"."a", "t"."b") = (?, ?) and (?) < ("t"."a") and ("t"."c") < (?)"#,
         ),
         vec![
             Value::from(1_u64),
             Value::from(2_u64),
-            Value::from(3_u64),
             Value::from(4_u64),
+            Value::from(3_u64),
         ],
     );
 
@@ -38,14 +38,14 @@ fn merge_tuples2() {
             "{} {} {}",
             r#"SELECT "t"."a" as "a" FROM "t""#,
             r#"WHERE (("t"."a", "t"."b") = (?, ?) and (?)"#,
-            r#"or ("t"."c", "t"."a") >= (?, ?) and (?))"#,
+            r#"or (?) <= ("t"."a") and ("t"."c") >= (?) and (?))"#,
         ),
         vec![
             Value::from(1_u64),
             Value::from(2_u64),
             Value::Null,
-            Value::from(3_u64),
             Value::from(4_u64),
+            Value::from(3_u64),
             Value::Boolean(true),
         ],
     );
@@ -84,9 +84,10 @@ fn merge_tuples5() {
     let expected = PatternWithParams::new(
         format!(
             "{} {}",
-            r#"SELECT "t"."a" as "a" FROM "t""#, r#"WHERE ("t"."c", "t"."a", "t"."b") > (?, ?, ?)"#,
+            r#"SELECT "t"."a" as "a" FROM "t""#,
+            r#"WHERE ("t"."a", "t"."b") > (?, ?) and (?) < ("t"."c")"#,
         ),
-        vec![Value::from(3_u64), Value::from(1_u64), Value::from(2_u64)],
+        vec![Value::from(1_u64), Value::from(2_u64), Value::from(3_u64)],
     );
 
     assert_eq!(sql_to_sql(input, &[], &merge_tuples), expected);
diff --git a/test_app/test/integration/api_test.lua b/test_app/test/integration/api_test.lua
index 4193ac1e67..b339549e85 100644
--- a/test_app/test/integration/api_test.lua
+++ b/test_app/test/integration/api_test.lua
@@ -558,6 +558,21 @@ g.test_decimal_double = function()
     })
 end
 
+g.test_compare = function()
+    local api = cluster:server("api-1").net_box
+
+    local r, err = api:call("query", { [[SELECT * FROM "t" where "id" < 2 and "a" > 5]], {} })
+
+    t.assert_equals(err, nil)
+    t.assert_equals(r, {
+        metadata = {
+            {name = "id", type = "integer"},
+            {name = "a", type = "number"},
+        },
+        rows = {},
+    })
+end
+
 g.test_invalid_explain = function()
     local api = cluster:server("api-1").net_box
 
-- 
GitLab