diff --git a/sbroad-cartridge/test_app/test/integration/groupby_test.lua b/sbroad-cartridge/test_app/test/integration/groupby_test.lua index 04c9608d9d396679a4ee6d8fec513f79fe4f44ba..e5a08d686ea0f394b672bd5aec4d4467073c637c 100644 --- a/sbroad-cartridge/test_app/test/integration/groupby_test.lua +++ b/sbroad-cartridge/test_app/test/integration/groupby_test.lua @@ -1724,3 +1724,750 @@ groupby_queries.test_total_with_groupby = function() {2, 6, 3}, }) end + +groupby_queries.test_having1 = function() + local api = cluster:server("api-1").net_box + + -- with having + local r, err = api:call("sbroad.execute", { + [[ + SELECT "a", sum("b") as "sum" from "arithmetic_space" + group by "a" + having sum("b") > 5 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "sum", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + {2, 6} + }) + -- without having + r, err = api:call("sbroad.execute", { + [[ + SELECT "a", sum("b") as "sum" from "arithmetic_space" + group by "a" + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "sum", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + {2, 6}, + {1, 3} + }) +end + +groupby_queries.test_having2 = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + SELECT "a", sum("b") as "sum" from "arithmetic_space" + group by "a" + having "a" = 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "sum", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + {1, 3} + }) +end + +groupby_queries.test_having3 = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + SELECT "boolean_col", sum(distinct "f") as "sum" from "arithmetic_space" + group by "boolean_col" + having "boolean_col" = true + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "boolean_col", type = "boolean" }, + { name = "sum", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + {true, 2} + }) +end + +groupby_queries.test_having_with_sq = function() + local api = cluster:server("api-1").net_box + local query_str = [[ + SELECT "a", sum(distinct "b") as "sum", count(distinct "b") as "count" from "arithmetic_space" + group by "a" + ]]; + -- without having + local r, err = api:call("sbroad.execute", { + query_str, {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "sum", type = "decimal" }, + { name = "count", type = "integer" } + }) + t.assert_items_equals(r.rows, { + {1, 3, 2}, + {2, 3, 1} + }) + -- with having + r, err = api:call("sbroad.execute", { + query_str .. + [[ having count(distinct "b") in (select "a" from "arithmetic_space" where "a" = 2)]], + {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "sum", type = "decimal" }, + { name = "count", type = "integer" } + }) + t.assert_items_equals(r.rows, { + {1, 3, 2}, + }) +end + +groupby_queries.test_having_no_groupby = function() + local api = cluster:server("api-1").net_box + + -- having condition is true + local r, err = api:call("sbroad.execute", { + [[ + SELECT sum("a"), sum(distinct "a"), count("a"), count(distinct "a") from "arithmetic_space" + having count(distinct "a") > 1 + ]], {} + }) + t.assert_equals(err, nil) + -- todo: if executed on single tarantool instance, types are: (integer, integer, integer, integer) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + { name = "COL_2", type = "decimal" }, + { name = "COL_3", type = "decimal" }, + { name = "COL_4", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + {6, 3, 4, 2} + }) + -- having condition is false + r, err = api:call("sbroad.execute", { + [[ + SELECT sum("a"), sum(distinct "a"), count("a"), count(distinct "a") from "arithmetic_space" + having count(distinct "a") > 100 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + { name = "COL_2", type = "decimal" }, + { name = "COL_3", type = "decimal" }, + { name = "COL_4", type = "integer" }, + }) + t.assert_items_equals(r.rows, {}) +end + +groupby_queries.test_having_selection = function() + local api = cluster:server("api-1").net_box + + -- with having + local r, err = api:call("sbroad.execute", { + [[ + SELECT "string_col", count(distinct "string_col"), count("string_col") + from "arithmetic_space" + where "id" > 2 or "string_col" = 'a' + group by "string_col" + having sum(distinct "a") > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "string_col", type = "string" }, + { name = "COL_1", type = "integer" }, + { name = "COL_2", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + { 'c', 1, 2 } + }) + -- without having + r, err = api:call("sbroad.execute", { + [[ + SELECT "string_col", count(distinct "string_col"), count("string_col") + from "arithmetic_space" + where "id" > 2 or "string_col" = 'a' + group by "string_col" + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "string_col", type = "string" }, + { name = "COL_1", type = "integer" }, + { name = "COL_2", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + { 'a', 1, 2 }, + { 'c', 1, 2 } + }) +end + +groupby_queries.test_having_join = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + SELECT sum(distinct "a"), "b", s + from "arithmetic_space" as t1 inner join + ( + select cast(sum("a") / 6 as integer) as s + from "arithmetic_space2" + having count(distinct "a") > 1 + ) as t2 + on t1."c" = t2.s + group by s, t1."b" + having sum(distinct "a") in (2, 3) and count(distinct t2.s) = 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + { name = "b", type = "integer" }, + { name = "S", type = "integer" } + }) + t.assert_items_equals(r.rows, { + { 2, 3, 1 } + }) +end + +groupby_queries.test_having_full_query = function() + local api = cluster:server("api-1").net_box + + -- data after join + where + local r, err = api:call("sbroad.execute", { + [[ + SELECT t1."a", t2.b, t2.s, t1."d" + from "arithmetic_space" as t1 inner join + (select "b" as b, "string_col" as s from "arithmetic_space2") as t2 + on t1."a" = t2.b + where t1."d" + t1."a" > 2 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "T1.a", type = "integer" }, + { name = "T2.B", type = "integer" }, + { name = "T2.S", type = "string" }, + { name = "T1.d", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + {1, 1, "a", 2}, + {1, 1, "b", 2}, + {1, 1, "b", 2}, + {2, 2, "a", 2}, + {2, 2, "a", 1}, + }) + r, err = api:call("sbroad.execute", { + [[ + SELECT t1."a", count(distinct s) + from "arithmetic_space" as t1 inner join + (select "b" as b, "string_col" as s from "arithmetic_space2") as t2 + on t1."a" = t2.b + where t1."d" + t1."a" > 2 + group by "a" + having count(distinct s) > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "COL_1", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + { 1, 2 } + }) +end + +groupby_queries.test_having_inside_sq = function() + local api = cluster:server("api-1").net_box + + -- check we can execute sq-s containing having clause + local r, err = api:call("sbroad.execute", { + [[ + SELECT t1.s1, t2.s2 from ( + select sum(distinct "a") as s1 from "arithmetic_space" + having sum("a") > 3 + ) as t1 inner join ( + select sum(distinct "a") as s2 from "arithmetic_space" + having sum("a") > 3 + ) as t2 on t1.s1 = t2.s2 and t1.s1 in ( + select sum(distinct "a") as s1 from "arithmetic_space" + having sum("a") > 3 + ) + where t2.s2 in ( + select sum(distinct "a") as s1 from "arithmetic_space" + having sum("a") > 3 + ) + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "T1.S1", type = "decimal" }, + { name = "T2.S2", type = "decimal" }, + }) + t.assert_items_equals(r.rows, { + { 3, 3 } + }) +end + +groupby_queries.test_having_inside_union = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + select sum(distinct "e") from "arithmetic_space" + having count(distinct "d") > 1 + union all + select "b" from "arithmetic_space" + group by "b" + having count(distinct "d") > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + }) + t.assert_items_equals(r.rows, { + {2}, + {3} + }) +end + +groupby_queries.test_having_inside_union1 = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + select "e" from "arithmetic_space" + union all + select "b" from "arithmetic_space" + group by "b" + having count(distinct "d") > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "e", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + {2}, + {2}, + {2}, + {2}, + {3} + }) +end + +groupby_queries.test_having_inside_except = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + select count("e") - 1 from "arithmetic_space" + having count("e") = 4 + except + select "b" from "arithmetic_space" + group by "b" + having count(distinct "d") > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + }) + t.assert_items_equals(r.rows, {}) +end + +groupby_queries.test_having_inside_except1 = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + select "d" from "arithmetic_space" + group by "d" + except + select sum(distinct "c") from "arithmetic_space" + having count(distinct "c") = 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "d", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + {2}, + }) +end + +groupby_queries.test_having1 = function() + local api = cluster:server("api-1").net_box + + -- with having + local r, err = api:call("sbroad.execute", { + [[ + SELECT "a", sum("b") as "sum" from "arithmetic_space" + group by "a" + having sum("b") > 5 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "sum", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + {2, 6} + }) + -- without having + r, err = api:call("sbroad.execute", { + [[ + SELECT "a", sum("b") as "sum" from "arithmetic_space" + group by "a" + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "sum", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + {2, 6}, + {1, 3} + }) +end + +groupby_queries.test_having2 = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + SELECT "a", sum("b") as "sum" from "arithmetic_space" + group by "a" + having "a" = 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "sum", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + {1, 3} + }) +end + +groupby_queries.test_having3 = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + SELECT "boolean_col", sum(distinct "f") as "sum" from "arithmetic_space" + group by "boolean_col" + having "boolean_col" = true + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "boolean_col", type = "boolean" }, + { name = "sum", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + {true, 2} + }) +end + +groupby_queries.test_having_no_groupby = function() + local api = cluster:server("api-1").net_box + + -- having condition is true + local r, err = api:call("sbroad.execute", { + [[ + SELECT sum("a"), sum(distinct "a"), count("a"), count(distinct "a") from "arithmetic_space" + having count(distinct "a") > 1 + ]], {} + }) + t.assert_equals(err, nil) + -- todo: if executed on single tarantool instance, types are: (integer, integer, integer, integer) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + { name = "COL_2", type = "decimal" }, + { name = "COL_3", type = "decimal" }, + { name = "COL_4", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + {6, 3, 4, 2} + }) + -- having condition is false + r, err = api:call("sbroad.execute", { + [[ + SELECT sum("a"), sum(distinct "a"), count("a"), count(distinct "a") from "arithmetic_space" + having count(distinct "a") > 100 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + { name = "COL_2", type = "decimal" }, + { name = "COL_3", type = "decimal" }, + { name = "COL_4", type = "integer" }, + }) + t.assert_items_equals(r.rows, {}) +end + +groupby_queries.test_having_selection = function() + local api = cluster:server("api-1").net_box + + -- with having + local r, err = api:call("sbroad.execute", { + [[ + SELECT "string_col", count(distinct "string_col"), count("string_col") + from "arithmetic_space" + where "id" > 2 or "string_col" = 'a' + group by "string_col" + having sum(distinct "a") > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "string_col", type = "string" }, + { name = "COL_1", type = "integer" }, + { name = "COL_2", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + { 'c', 1, 2 } + }) + -- without having + r, err = api:call("sbroad.execute", { + [[ + SELECT "string_col", count(distinct "string_col"), count("string_col") + from "arithmetic_space" + where "id" > 2 or "string_col" = 'a' + group by "string_col" + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "string_col", type = "string" }, + { name = "COL_1", type = "integer" }, + { name = "COL_2", type = "decimal" } + }) + t.assert_items_equals(r.rows, { + { 'a', 1, 2 }, + { 'c', 1, 2 } + }) +end + +groupby_queries.test_having_join = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + SELECT sum(distinct "a"), "b", s + from "arithmetic_space" as t1 inner join + ( + select cast(sum("a") / 6 as integer) as s + from "arithmetic_space2" + having count(distinct "a") > 1 + ) as t2 + on t1."c" = t2.s + group by s, t1."b" + having sum(distinct "a") in (2, 3) and count(distinct t2.s) = 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + { name = "b", type = "integer" }, + { name = "S", type = "integer" } + }) + t.assert_items_equals(r.rows, { + { 2, 3, 1 } + }) +end + +groupby_queries.test_having_full_query = function() + local api = cluster:server("api-1").net_box + + -- data after join + where + local r, err = api:call("sbroad.execute", { + [[ + SELECT t1."a", t2.b, t2.s, t1."d" + from "arithmetic_space" as t1 inner join + (select "b" as b, "string_col" as s from "arithmetic_space2") as t2 + on t1."a" = t2.b + where t1."d" + t1."a" > 2 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "T1.a", type = "integer" }, + { name = "T2.B", type = "integer" }, + { name = "T2.S", type = "string" }, + { name = "T1.d", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + {1, 1, "a", 2}, + {1, 1, "b", 2}, + {1, 1, "b", 2}, + {2, 2, "a", 2}, + {2, 2, "a", 1}, + }) + r, err = api:call("sbroad.execute", { + [[ + SELECT t1."a", count(distinct s) + from "arithmetic_space" as t1 inner join + (select "b" as b, "string_col" as s from "arithmetic_space2") as t2 + on t1."a" = t2.b + where t1."d" + t1."a" > 2 + group by "a" + having count(distinct s) > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "a", type = "integer" }, + { name = "COL_1", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + { 1, 2 } + }) +end + +groupby_queries.test_having_inside_sq = function() + local api = cluster:server("api-1").net_box + + -- check we can execute sq-s containing having clause + local r, err = api:call("sbroad.execute", { + [[ + SELECT t1.s1, t2.s2 from ( + select sum(distinct "a") as s1 from "arithmetic_space" + having sum("a") > 3 + ) as t1 inner join ( + select sum(distinct "a") as s2 from "arithmetic_space" + having sum("a") > 3 + ) as t2 on t1.s1 = t2.s2 and t1.s1 in ( + select sum(distinct "a") as s1 from "arithmetic_space" + having sum("a") > 3 + ) + where t2.s2 in ( + select sum(distinct "a") as s1 from "arithmetic_space" + having sum("a") > 3 + ) + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "T1.S1", type = "decimal" }, + { name = "T2.S2", type = "decimal" }, + }) + t.assert_items_equals(r.rows, { + { 3, 3 } + }) +end + +groupby_queries.test_having_inside_union = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + select sum(distinct "e") from "arithmetic_space" + having count(distinct "d") > 1 + union all + select "b" from "arithmetic_space" + group by "b" + having count(distinct "d") > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + }) + t.assert_items_equals(r.rows, { + {2}, + {3} + }) +end + +groupby_queries.test_having_inside_union1 = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + select "e" from "arithmetic_space" + union all + select "b" from "arithmetic_space" + group by "b" + having count(distinct "d") > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "e", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + {2}, + {2}, + {2}, + {2}, + {3} + }) +end + +groupby_queries.test_having_inside_except = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + select count("e") - 1 from "arithmetic_space" + having count("e") = 4 + except + select "b" from "arithmetic_space" + group by "b" + having count(distinct "d") > 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "COL_1", type = "decimal" }, + }) + t.assert_items_equals(r.rows, {}) +end + +groupby_queries.test_having_inside_except1 = function() + local api = cluster:server("api-1").net_box + + local r, err = api:call("sbroad.execute", { + [[ + select "d" from "arithmetic_space" + group by "d" + except + select sum(distinct "c") from "arithmetic_space" + having count(distinct "c") = 1 + ]], {} + }) + t.assert_equals(err, nil) + t.assert_equals(r.metadata, { + { name = "d", type = "integer" }, + }) + t.assert_items_equals(r.rows, { + {2}, + }) +end diff --git a/sbroad-core/src/backend/sql/ir.rs b/sbroad-core/src/backend/sql/ir.rs index 97e478cbf7f2bd5512ec71688aa5be1c34db08c8..207de27a6654747f644e9051fae4356f86e45a3c 100644 --- a/sbroad-core/src/backend/sql/ir.rs +++ b/sbroad-core/src/backend/sql/ir.rs @@ -302,6 +302,7 @@ impl ExecutionPlan { Node::Relational(rel) => match rel { Relational::Except { .. } => sql.push_str("EXCEPT"), Relational::GroupBy { .. } => sql.push_str("GROUP BY"), + Relational::Having { .. } => sql.push_str("HAVING"), Relational::Insert { relation, .. } => { sql.push_str("INSERT INTO "); sql.push_str(relation.as_str()); diff --git a/sbroad-core/src/backend/sql/tree.rs b/sbroad-core/src/backend/sql/tree.rs index 876e8c119becbad08972947576525d01369d6e70..9db619b1c000491770be4f3fd66069677a7845ab 100644 --- a/sbroad-core/src/backend/sql/tree.rs +++ b/sbroad-core/src/backend/sql/tree.rs @@ -383,6 +383,8 @@ struct Select { join: Option<usize>, /// GroupBy syntax node groupby: Option<usize>, + /// Having syntax node + having: Option<usize>, } type NodeAdder = fn(&mut Select, usize, &SyntaxPlan) -> Result<bool, SbroadError>; @@ -494,17 +496,60 @@ impl Select { } } - /// Constructor. + fn add_having(select: &mut Select, id: usize, sp: &SyntaxPlan) -> Result<bool, SbroadError> { + let sn = sp.nodes.get_syntax_node(id)?; + if let Node::Relational(Relational::Having { .. }) = sp.plan_node_or_err(&sn.data)? { + select.having = Some(id); + let left_id = sn.left_id_or_err()?; + let sn_left = sp.nodes.get_syntax_node(left_id)?; + let plan_node_left = sp.plan_node_or_err(&sn_left.data)?; + if let Node::Relational( + Relational::ScanRelation { .. } + | Relational::ScanSubQuery { .. } + | Relational::Motion { .. }, + ) = plan_node_left + { + select.scan = left_id; + return Ok(true); + } + if !Select::add_one_of( + left_id, + select, + sp, + &[ + Select::add_selection, + Select::add_inner_join, + Select::add_groupby, + ], + )? { + return Err(SbroadError::Invalid( + Entity::SyntaxPlan, + Some(format!( + "expected Scan or InnerJoin, or Selection, or GroupBy after Having. Got {plan_node_left:?}" + )))); + } + Ok(true) + } else { + Ok(false) + } + } + + /// `Select` node constructor. /// - /// There are several valid combinations of the `SELECT` command: - /// - projection -> selection -> join -> scan - /// - projection -> groupby -> selection -> join -> scan - /// - projection -> join -> scan - /// - projection -> groupby -> join -> scan - /// - projection -> selection -> scan - /// - projection -> groupby -> selection -> scan - /// - projection -> scan - /// - projection -> groupby -> scan + /// There are several valid combinations of the `SELECT` command. + /// The general view of all such commands is: + /// `Projection` -> set of additional relational operators (possibly empty) -> `Scan`, + /// where additional operators are: `Selection`, `Join`, `GroupBy`, `Having`. + /// + /// Some examples of valid sequences: + /// - `Projection` -> `Scan` + /// - `Projection` -> `Selection` -> `Join` -> `Scan` + /// - `Projection` -> `GroupBy` -> `Selection` -> `Join` -> `Scan` + /// - `Projection` -> `Having` -> `GroupBy` -> `Scan` + /// + /// Using functions `add_one_of` and `add_<rel_op_name>` constructor recursively traverses + /// `SyntaxNode`s tree and tries to add new children operators. In case none of allowed/required + /// children operators were met, it throws an error. fn new( sp: &SyntaxPlan, parent: Option<usize>, @@ -521,6 +566,7 @@ impl Select { selection: None, join: None, groupby: None, + having: None, }; let left_id = sn.left_id_or_err()?; let sn_left = sp.nodes.get_syntax_node(left_id)?; @@ -540,12 +586,13 @@ impl Select { Select::add_selection, Select::add_inner_join, Select::add_groupby, + Select::add_having, ], )? { return Err(SbroadError::Invalid( Entity::SyntaxPlan, Some(format!( - "expected Scan, InnerJoin, Selection, GroupBy after Projection. Got {plan_node_left:?}" + "expected Scan, InnerJoin, Selection, GroupBy, Having after Projection. Got {plan_node_left:?}" )))); } if select.scan != 0 { @@ -746,9 +793,12 @@ impl<'p> SyntaxPlan<'p> { } Relational::Selection { children, filter, .. + } + | Relational::Having { + children, filter, .. } => { let left_id = *children.first().ok_or_else(|| { - SbroadError::UnexpectedNumberOfValues("Selection has no children.".into()) + SbroadError::UnexpectedNumberOfValues(format!("{node:?} has no children.")) })?; let filter_id = match self.snapshot { Snapshot::Latest => *filter, diff --git a/sbroad-core/src/executor/bucket.rs b/sbroad-core/src/executor/bucket.rs index 6d3dffc17bb5747880ae946c1f9d3fe10b475821..18b65fcbb37d6d97526a391600520823a8d870c1 100644 --- a/sbroad-core/src/executor/bucket.rs +++ b/sbroad-core/src/executor/bucket.rs @@ -351,6 +351,9 @@ where | Relational::GroupBy { children, output, .. } + | Relational::Having { + children, output, .. + } | Relational::ScanSubQuery { children, output, .. } => { diff --git a/sbroad-core/src/executor/ir.rs b/sbroad-core/src/executor/ir.rs index 27fbf722bb2e9db495c8346e4474a72bbe1e4c11..6ecab455642b1f03d58c807b4a33ba9dc96bd77e 100644 --- a/sbroad-core/src/executor/ir.rs +++ b/sbroad-core/src/executor/ir.rs @@ -197,6 +197,7 @@ impl ExecutionPlan { | Relational::Selection { .. } | Relational::UnionAll { .. } | Relational::Values { .. } + | Relational::Having { .. } | Relational::ValuesRow { .. } => Ok(*top_id), Relational::Motion { .. } | Relational::Insert { .. } => Err(SbroadError::Invalid( Entity::Relational, @@ -398,12 +399,16 @@ impl ExecutionPlan { filter: ref mut expr_id, .. } + | Relational::Having { + filter: ref mut expr_id, + .. + } | Relational::Join { condition: ref mut expr_id, .. } = rel { - // We transform selection's filter and join's condition to DNF for a better bucket calculation. + // We transform selection's, having's filter and join's condition to DNF for a better bucket calculation. // But as a result we can produce an extremely verbose SQL query from such a plan (tarantool's // parser can fail to parse such SQL). diff --git a/sbroad-core/src/executor/tests/exec_plan.rs b/sbroad-core/src/executor/tests/exec_plan.rs index 2cd9343e2f8ab902c9fe9e53ef9d447cc2f491c2..905b3297c3696685015365204e5a4bc5b9b4989f 100644 --- a/sbroad-core/src/executor/tests/exec_plan.rs +++ b/sbroad-core/src/executor/tests/exec_plan.rs @@ -660,3 +660,191 @@ fn exec_plan_subtree_count_asterisk() { ) ); } + +#[test] +fn exec_plan_subtree_having() { + let sql = format!( + "{} {} {}", + r#"SELECT t1."sys_op" || t1."sys_op", count(t1."sys_op"*2) + count(distinct t1."sys_op"*2)"#, + r#"FROM "test_space" as t1 group by t1."sys_op""#, + r#"HAVING sum(distinct t1."sys_op"*2) > 1"# + ); + let coordinator = RouterRuntimeMock::new(); + + let mut query = Query::new(&coordinator, sql.as_str(), vec![]).unwrap(); + let motion_id = *query + .exec_plan + .get_ir_plan() + .clone_slices() + .slice(0) + .unwrap() + .position(0) + .unwrap(); + let mut virtual_table = VirtualTable::new(); + virtual_table.add_column(Column { + name: "column_63".into(), + r#type: Type::Integer, + role: ColumnRole::User, + }); + virtual_table.add_column(Column { + name: "column_12".into(), + r#type: Type::Integer, + role: ColumnRole::User, + }); + virtual_table.add_column(Column { + name: "count_58".into(), + r#type: Type::Integer, + role: ColumnRole::User, + }); + if let MotionPolicy::Segment(key) = get_motion_policy(query.exec_plan.get_ir_plan(), motion_id) + { + virtual_table.reshard(key, &query.coordinator).unwrap(); + } + + let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new(); + vtables.insert(motion_id, Rc::new(virtual_table)); + + let exec_plan = query.get_mut_exec_plan(); + exec_plan.set_vtables(vtables); + let top_id = exec_plan.get_ir_plan().get_top().unwrap(); + let motion_child_id = exec_plan.get_motion_subtree_root(motion_id).unwrap(); + + // Check groupby local stage + let subplan1 = exec_plan.take_subtree(motion_child_id).unwrap(); + let subplan1_top_id = subplan1.get_ir_plan().get_top().unwrap(); + let sp = SyntaxPlan::new(&subplan1, subplan1_top_id, Snapshot::Oldest).unwrap(); + let ordered = OrderedSyntaxNodes::try_from(sp).unwrap(); + let nodes = ordered.to_syntax_data().unwrap(); + let (sql, _) = subplan1.to_sql(&nodes, &Buckets::All, "test").unwrap(); + if let MotionPolicy::Segment(_) = exec_plan.get_motion_policy(motion_id).unwrap() { + } else { + panic!("Expected MotionPolicy::Segment for local aggregation stage"); + }; + assert_eq!( + sql, + PatternWithParams::new( + format!( + "{} {} {}", + r#"SELECT "T1"."sys_op" as "column_12", ("T1"."sys_op") * (?) as "column_63","#, + r#"count (("T1"."sys_op") * (?)) as "count_58" FROM "test_space" as "T1""#, + r#"GROUP BY "T1"."sys_op", ("T1"."sys_op") * (?)"#, + ), + vec![Value::Unsigned(2), Value::Unsigned(2), Value::Unsigned(2)] + ) + ); + + // Check main query + let subplan2 = exec_plan.take_subtree(top_id).unwrap(); + let subplan2_top_id = subplan2.get_ir_plan().get_top().unwrap(); + let sp = SyntaxPlan::new(&subplan2, subplan2_top_id, Snapshot::Oldest).unwrap(); + let ordered = OrderedSyntaxNodes::try_from(sp).unwrap(); + let nodes = ordered.to_syntax_data().unwrap(); + let (sql, _) = subplan2.to_sql(&nodes, &Buckets::All, "test").unwrap(); + assert_eq!( + sql, + PatternWithParams::new( + format!( + "{} {} {} {}", + r#"SELECT ("column_12") || ("column_12") as "COL_1","#, + r#"(sum ("count_58")) + (count (DISTINCT "column_63")) as "COL_2" FROM"#, + r#"(SELECT "column_63","column_12","count_58" FROM "TMP_test_22")"#, + r#"GROUP BY "column_12" HAVING (sum (DISTINCT "column_63")) > (?)"# + ), + vec![Value::Unsigned(1u64)] + ) + ); +} + +#[test] +fn exec_plan_subtree_having_without_groupby() { + let sql = format!( + "{} {} {}", + r#"SELECT count(t1."sys_op"*2) + count(distinct t1."sys_op"*2)"#, + r#"FROM "test_space" as t1"#, + r#"HAVING sum(distinct t1."sys_op"*2) > 1"# + ); + let coordinator = RouterRuntimeMock::new(); + + let mut query = Query::new(&coordinator, sql.as_str(), vec![]).unwrap(); + let motion_id = *query + .exec_plan + .get_ir_plan() + .clone_slices() + .slice(0) + .unwrap() + .position(0) + .unwrap(); + let mut virtual_table = VirtualTable::new(); + virtual_table.add_column(Column { + name: "column_63".into(), + r#type: Type::Integer, + role: ColumnRole::User, + }); + virtual_table.add_column(Column { + name: "column_12".into(), + r#type: Type::Integer, + role: ColumnRole::User, + }); + virtual_table.add_column(Column { + name: "count_58".into(), + r#type: Type::Integer, + role: ColumnRole::User, + }); + if let MotionPolicy::Segment(key) = get_motion_policy(query.exec_plan.get_ir_plan(), motion_id) + { + virtual_table.reshard(key, &query.coordinator).unwrap(); + } + + let mut vtables: HashMap<usize, Rc<VirtualTable>> = HashMap::new(); + vtables.insert(motion_id, Rc::new(virtual_table)); + + let exec_plan = query.get_mut_exec_plan(); + exec_plan.set_vtables(vtables); + let top_id = exec_plan.get_ir_plan().get_top().unwrap(); + let motion_child_id = exec_plan.get_motion_subtree_root(motion_id).unwrap(); + + // Check groupby local stage + let subplan1 = exec_plan.take_subtree(motion_child_id).unwrap(); + let subplan1_top_id = subplan1.get_ir_plan().get_top().unwrap(); + let sp = SyntaxPlan::new(&subplan1, subplan1_top_id, Snapshot::Oldest).unwrap(); + let ordered = OrderedSyntaxNodes::try_from(sp).unwrap(); + let nodes = ordered.to_syntax_data().unwrap(); + let (sql, _) = subplan1.to_sql(&nodes, &Buckets::All, "test").unwrap(); + if let MotionPolicy::Full = exec_plan.get_motion_policy(motion_id).unwrap() { + } else { + panic!("Expected MotionPolicy::Full after local stage"); + }; + + assert_eq!( + sql, + PatternWithParams::new( + format!( + "{} {} {}", + r#"SELECT ("T1"."sys_op") * (?) as "column_44","#, + r#"count (("T1"."sys_op") * (?)) as "count_39" FROM "test_space" as "T1""#, + r#"GROUP BY ("T1"."sys_op") * (?)"#, + ), + vec![Value::Unsigned(2), Value::Unsigned(2), Value::Unsigned(2)] + ) + ); + + // Check main query + let subplan2 = exec_plan.take_subtree(top_id).unwrap(); + let subplan2_top_id = subplan2.get_ir_plan().get_top().unwrap(); + let sp = SyntaxPlan::new(&subplan2, subplan2_top_id, Snapshot::Oldest).unwrap(); + let ordered = OrderedSyntaxNodes::try_from(sp).unwrap(); + let nodes = ordered.to_syntax_data().unwrap(); + let (sql, _) = subplan2.to_sql(&nodes, &Buckets::All, "test").unwrap(); + assert_eq!( + sql, + PatternWithParams::new( + format!( + "{} {} {}", + r#"SELECT (sum ("count_39")) + (count (DISTINCT "column_44")) as "COL_1""#, + r#"FROM (SELECT "column_63","column_12","count_58" FROM "TMP_test_14")"#, + r#"HAVING (sum (DISTINCT "column_44")) > (?)"#, + ), + vec![Value::Unsigned(1u64)] + ) + ); +} diff --git a/sbroad-core/src/frontend/sql.rs b/sbroad-core/src/frontend/sql.rs index 36b5e37440256963778bb3626dde5babb4971275..39ed7284000701f001057a53e5ed38476c62608b 100644 --- a/sbroad-core/src/frontend/sql.rs +++ b/sbroad-core/src/frontend/sql.rs @@ -135,10 +135,6 @@ impl Ast for AbstractSyntaxTree { // Is it global for every `ValuesRow` met in the AST. let mut col_idx: usize = 0; - let mut groupby_nodes: Vec<usize> = Vec::new(); - let mut scan_nodes: Vec<usize> = Vec::new(); - let mut sq_nodes: Vec<usize> = Vec::new(); - let mut betweens: Vec<Between> = Vec::new(); // Ids of arithmetic expressions that have parentheses. let mut arith_expr_with_parentheses_ids: Vec<usize> = Vec::new(); @@ -251,7 +247,6 @@ impl Ast for AbstractSyntaxTree { let t = metadata.table(table)?; plan.add_rel(t); let scan_id = plan.add_scan(&normalize_name_from_sql(table), None)?; - scan_nodes.push(scan_id); map.add(id, scan_id); } else { return Err(SbroadError::Invalid( @@ -268,7 +263,6 @@ impl Ast for AbstractSyntaxTree { })?; let plan_child_id = map.get(*ast_child_id)?; let plan_sq_id = plan.add_sub_query(plan_child_id, None)?; - sq_nodes.push(plan_sq_id); map.add(id, plan_sq_id); } Type::Reference => { @@ -788,7 +782,6 @@ impl Ast for AbstractSyntaxTree { children.push(plan_column_id); } let groupby_id = plan.add_groupby_from_ast(&children)?; - groupby_nodes.push(groupby_id); map.add(id, groupby_id); } Type::Join => { @@ -827,9 +820,9 @@ impl Ast for AbstractSyntaxTree { plan.add_join(plan_left_id, plan_right_id, plan_cond_id, kind)?; map.add(id, plan_join_id); } - Type::Selection => { + Type::Selection | Type::Having => { let ast_child_id = node.children.first().ok_or_else(|| { - SbroadError::UnexpectedNumberOfValues("Selection has no children.".into()) + SbroadError::UnexpectedNumberOfValues(format!("{node:?} has no children.")) })?; let plan_child_id = map.get(*ast_child_id)?; let ast_filter_id = node.children.get(1).ok_or_else(|| { @@ -839,8 +832,12 @@ impl Ast for AbstractSyntaxTree { ) })?; let plan_filter_id = map.get(*ast_filter_id)?; - let plan_selection_id = plan.add_select(&[plan_child_id], plan_filter_id)?; - map.add(id, plan_selection_id); + let plan_node_id = match &node.rule { + Type::Selection => plan.add_select(&[plan_child_id], plan_filter_id)?, + Type::Having => plan.add_having(&[plan_child_id], plan_filter_id)?, + _ => return Err(SbroadError::Invalid(Entity::AST, None)), // never happens + }; + map.add(id, plan_node_id); } Type::Projection => { let ast_child_id = node.children.first().ok_or_else(|| { diff --git a/sbroad-core/src/frontend/sql/ast.rs b/sbroad-core/src/frontend/sql/ast.rs index c559a831b2c10f3e8e21667217f3724ecb62cd28..eb48590810839c13f78b81bed145a0972da0acda 100644 --- a/sbroad-core/src/frontend/sql/ast.rs +++ b/sbroad-core/src/frontend/sql/ast.rs @@ -55,6 +55,7 @@ pub enum Type { GtEq, GroupBy, GroupingElement, + Having, In, InnerJoinKind, Join, @@ -143,6 +144,7 @@ impl Type { Rule::GroupingElement => Ok(Type::GroupingElement), Rule::Gt => Ok(Type::Gt), Rule::GtEq => Ok(Type::GtEq), + Rule::Having => Ok(Type::Having), Rule::In => Ok(Type::In), Rule::InnerJoinKind => Ok(Type::InnerJoinKind), Rule::Join => Ok(Type::Join), @@ -234,6 +236,7 @@ impl fmt::Display for Type { Type::FunctionName => "FunctionName".to_string(), Type::Gt => "Gt".to_string(), Type::GtEq => "GtEq".to_string(), + Type::Having => "Having".to_string(), Type::InnerJoinKind => "inner".to_string(), Type::In => "In".to_string(), Type::Join => "Join".to_string(), @@ -572,6 +575,7 @@ impl AbstractSyntaxTree { 3 => self.transform_select_3(*node, &children)?, 4 => self.transform_select_4(*node, &children)?, 5 => self.transform_select_5(*node, &children)?, + 6 => self.transform_select_6(*node, &children)?, _ => return Err(SbroadError::Invalid(Entity::AST, None)), } } @@ -676,13 +680,15 @@ impl AbstractSyntaxTree { [Type::Projection, Type::Scan, Type::Join], [Type::Projection, Type::Scan, Type::GroupBy], [Type::Projection, Type::Scan, Type::Selection], + [Type::Projection, Type::Scan, Type::Having], ]; self.check(&allowed, children)?; self.nodes .push_front_child(get_or_err(children, 2)?, get_or_err(children, 1)?)?; self.nodes .push_front_child(get_or_err(children, 0)?, get_or_err(children, 2)?)?; - self.nodes.set_children(select_id, vec![children[0]])?; + self.nodes + .set_children(select_id, vec![get_or_err(children, 0)?])?; Ok(()) } @@ -693,8 +699,11 @@ impl AbstractSyntaxTree { ) -> Result<(), SbroadError> { let allowed = [ [Type::Projection, Type::Scan, Type::Selection, Type::GroupBy], + [Type::Projection, Type::Scan, Type::Selection, Type::Having], + [Type::Projection, Type::Scan, Type::GroupBy, Type::Having], [Type::Projection, Type::Scan, Type::Join, Type::Selection], [Type::Projection, Type::Scan, Type::Join, Type::GroupBy], + [Type::Projection, Type::Scan, Type::Join, Type::Having], ]; self.check(&allowed, children)?; // insert Selection | InnerJoin as first child of GroupBy @@ -714,6 +723,58 @@ impl AbstractSyntaxTree { &mut self, select_id: usize, children: &[usize], + ) -> Result<(), SbroadError> { + let allowed = [ + [ + Type::Projection, + Type::Scan, + Type::Join, + Type::Selection, + Type::GroupBy, + ], + [ + Type::Projection, + Type::Scan, + Type::Join, + Type::Selection, + Type::Having, + ], + [ + Type::Projection, + Type::Scan, + Type::Join, + Type::GroupBy, + Type::Having, + ], + [ + Type::Projection, + Type::Scan, + Type::Selection, + Type::GroupBy, + Type::Having, + ], + ]; + self.check(&allowed, children)?; + // insert Selection as first child of GroupBy + self.nodes + .push_front_child(get_or_err(children, 4)?, get_or_err(children, 3)?)?; + // insert InnerJoin as first child of Selection + self.nodes + .push_front_child(get_or_err(children, 3)?, get_or_err(children, 2)?)?; + // insert Scan as first child of InnerJoin + self.nodes + .push_front_child(get_or_err(children, 2)?, get_or_err(children, 1)?)?; + // insert GroupBy as first child of Projection + self.nodes + .push_front_child(get_or_err(children, 0)?, get_or_err(children, 4)?)?; + self.nodes.set_children(select_id, vec![children[0]])?; + Ok(()) + } + + fn transform_select_6( + &mut self, + select_id: usize, + children: &[usize], ) -> Result<(), SbroadError> { let allowed = [[ Type::Projection, @@ -721,8 +782,12 @@ impl AbstractSyntaxTree { Type::Join, Type::Selection, Type::GroupBy, + Type::Having, ]]; self.check(&allowed, children)?; + // insert GroupBy as first child of Having + self.nodes + .push_front_child(get_or_err(children, 5)?, get_or_err(children, 4)?)?; // insert Selection as first child of GroupBy self.nodes .push_front_child(get_or_err(children, 4)?, get_or_err(children, 3)?)?; @@ -732,9 +797,9 @@ impl AbstractSyntaxTree { // insert Scan as first child of InnerJoin self.nodes .push_front_child(get_or_err(children, 2)?, get_or_err(children, 1)?)?; - // insert GroupBy as first child of Projection + // insert Having as first child of Projection self.nodes - .push_front_child(get_or_err(children, 0)?, get_or_err(children, 4)?)?; + .push_front_child(get_or_err(children, 0)?, get_or_err(children, 5)?)?; self.nodes.set_children(select_id, vec![children[0]])?; Ok(()) } @@ -862,11 +927,12 @@ impl AbstractSyntaxTree { } } } - Type::Selection => { + Type::Selection | Type::Having => { let rel_id = rel_node.children.first().ok_or_else(|| { - SbroadError::UnexpectedNumberOfValues( - "AST selection has no children.".into(), - ) + SbroadError::UnexpectedNumberOfValues(format!( + "AST {:?} has no children.", + rel_node.rule + )) })?; let filter = rel_node.children.get(1).ok_or_else(|| { SbroadError::NotFound( diff --git a/sbroad-core/src/frontend/sql/ast/tests.rs b/sbroad-core/src/frontend/sql/ast/tests.rs index ced600ae834c9c80d03a4fb779246fc3294eb7ea..958b81ed0a6caa8d6ac5a7055b45eb6c673a964e 100644 --- a/sbroad-core/src/frontend/sql/ast/tests.rs +++ b/sbroad-core/src/frontend/sql/ast/tests.rs @@ -71,7 +71,22 @@ fn transform_select_3_group_by() { } #[test] -fn transform_select_4() { +fn transform_select_3_having() { + let query = r#"select sum("a") from t having sum("a") > 1"#; + let ast = AbstractSyntaxTree::new(query).unwrap(); + let path = Path::new("") + .join("tests") + .join("artifactory") + .join("frontend") + .join("sql") + .join("transform_select_3_having.yaml"); + let s = fs::read_to_string(path).unwrap(); + let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); + assert_eq!(expected, ast); +} + +#[test] +fn transform_select_3_join() { let query = r#"select * from t1 inner join t2 on t1.a = t2.a"#; let ast = AbstractSyntaxTree::new(query).unwrap(); let path = Path::new("") @@ -79,7 +94,7 @@ fn transform_select_4() { .join("artifactory") .join("frontend") .join("sql") - .join("transform_select_4.yaml"); + .join("transform_select_3_join.yaml"); let s = fs::read_to_string(path).unwrap(); let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); assert_eq!(expected, ast); @@ -101,7 +116,37 @@ fn transform_select_4_1() { } #[test] -fn transform_select_5() { +fn transform_select_4_having_1() { + let query = r#"select sum("a") from t1 where "a" > 1 having sum("a") > 1"#; + let ast = AbstractSyntaxTree::new(query).unwrap(); + let path = Path::new("") + .join("tests") + .join("artifactory") + .join("frontend") + .join("sql") + .join("transform_select_4_having_1.yaml"); + let s = fs::read_to_string(path).unwrap(); + let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); + assert_eq!(expected, ast); +} + +#[test] +fn transform_select_4_having_2() { + let query = r#"select sum("a") from t1 group by "a" having sum("a") > 1"#; + let ast = AbstractSyntaxTree::new(query).unwrap(); + let path = Path::new("") + .join("tests") + .join("artifactory") + .join("frontend") + .join("sql") + .join("transform_select_4_having_2.yaml"); + let s = fs::read_to_string(path).unwrap(); + let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); + assert_eq!(expected, ast); +} + +#[test] +fn transform_select_4_join_filter() { let query = r#"select * from t1 inner join t2 on t1.a = t2.a where t1.a > 0"#; let ast = AbstractSyntaxTree::new(query).unwrap(); let path = Path::new("") @@ -109,14 +154,14 @@ fn transform_select_5() { .join("artifactory") .join("frontend") .join("sql") - .join("transform_select_5.yaml"); + .join("transform_select_4_join_filter.yaml"); let s = fs::read_to_string(path).unwrap(); let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); assert_eq!(expected, ast); } #[test] -fn transform_select_5_1() { +fn transform_select_4_join_groupby() { let query = r#"select t1.a, t2.b from t1 inner join t2 on t1.a = t2.a group by t1.a, t2.b"#; let ast = AbstractSyntaxTree::new(query).unwrap(); let path = Path::new("") @@ -124,16 +169,91 @@ fn transform_select_5_1() { .join("artifactory") .join("frontend") .join("sql") - .join("transform_select_5_1.yaml"); + .join("transform_select_4_join_groupby.yaml"); let s = fs::read_to_string(path).unwrap(); let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); assert_eq!(expected, ast); } #[test] -fn transform_select_6() { +fn transform_select_4_join_having() { + let query = r#"select sum(a) from t1 inner join t2 on t1.a = t2.a having sum(t1.a) > 1"#; + let ast = AbstractSyntaxTree::new(query).unwrap(); + let path = Path::new("") + .join("tests") + .join("artifactory") + .join("frontend") + .join("sql") + .join("transform_select_4_join_having.yaml"); + let s = fs::read_to_string(path).unwrap(); + let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); + assert_eq!(expected, ast); +} + +#[test] +fn transform_select_4_selection_having() { + let query = r#"select sum(a) from t1 where "a" > 1 group by a having sum(t1.a) > 1"#; + let ast = AbstractSyntaxTree::new(query).unwrap(); + let path = Path::new("") + .join("tests") + .join("artifactory") + .join("frontend") + .join("sql") + .join("transform_select_4_selection_having.yaml"); + let s = fs::read_to_string(path).unwrap(); + let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); + assert_eq!(expected, ast); +} + +#[test] +fn transform_select_5() { let query = r#"select t1.a, t2.b from t1 inner join t2 on t1.a = t2.a where t1.a > 1 group by t1.a, t2.b"#; let ast = AbstractSyntaxTree::new(query).unwrap(); + let path = Path::new("") + .join("tests") + .join("artifactory") + .join("frontend") + .join("sql") + .join("transform_select_5.yaml"); + let s = fs::read_to_string(path).unwrap(); + let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); + assert_eq!(expected, ast); +} + +#[test] +fn transform_select_5_having_1() { + let query = r#"select sum(a) + f(b) from t1 inner join t2 on t1.a = t2.a group by t1.a, t2.b having sum(t1.a) > 1 and avg(t2.b) > 10"#; + let ast = AbstractSyntaxTree::new(query).unwrap(); + let path = Path::new("") + .join("tests") + .join("artifactory") + .join("frontend") + .join("sql") + .join("transform_select_5_having_1.yaml"); + let s = fs::read_to_string(path).unwrap(); + let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); + assert_eq!(expected, ast); +} + +#[test] +fn transform_select_5_having_2() { + let query = r#"select sum(a) from t1 inner join t2 on t1.a = t2.a where a + b / 2 > 1 having avg(t2.b) > 10"#; + let ast = AbstractSyntaxTree::new(query).unwrap(); + let path = Path::new("") + .join("tests") + .join("artifactory") + .join("frontend") + .join("sql") + .join("transform_select_5_having_2.yaml"); + let s = fs::read_to_string(path).unwrap(); + let expected: AbstractSyntaxTree = AbstractSyntaxTree::from_yaml(&s).unwrap(); + assert_eq!(expected, ast); +} + +#[test] +fn transform_select_6() { + let query = r#"select sum(a) from t1 inner join t2 on t1.a = t2.a where a + b / 2 > 1 group by a having avg(t2.b) > 10"#; + let ast = AbstractSyntaxTree::new(query).unwrap(); let path = Path::new("") .join("tests") .join("artifactory") diff --git a/sbroad-core/src/frontend/sql/ir.rs b/sbroad-core/src/frontend/sql/ir.rs index 533be17e09591abf3dbfea33732f634f3ae3d8e6..dc3642afb41605f264c551fc9f7c8ff9af387153 100644 --- a/sbroad-core/src/frontend/sql/ir.rs +++ b/sbroad-core/src/frontend/sql/ir.rs @@ -186,7 +186,8 @@ impl Plan { Relational::Selection { filter: tree, .. } | Relational::Join { condition: tree, .. - }, + } + | Relational::Having { filter: tree, .. }, ) => { let capacity = self.nodes.len(); let mut expr_post = PostOrder::with_capacity( @@ -233,7 +234,9 @@ impl Plan { // Append sub-query to relational node if it is not already there (can happen with BETWEEN). match self.get_mut_node(sq.relational)? { Node::Relational( - Relational::Selection { children, .. } | Relational::Join { children, .. }, + Relational::Selection { children, .. } + | Relational::Join { children, .. } + | Relational::Having { children, .. }, ) => { // O(n) can become a problem. if !children.contains(&sq.sq) { @@ -251,7 +254,9 @@ impl Plan { // Generate a reference to the sub-query. let row_id: usize = match self.get_node(sq.relational)? { Node::Relational( - Relational::Selection { children, .. } | Relational::Join { children, .. }, + Relational::Selection { children, .. } + | Relational::Join { children, .. } + | Relational::Having { children, .. }, ) => { let nodes = children.clone(); let sq_output = self.get_relation_node(sq.sq)?.output(); diff --git a/sbroad-core/src/frontend/sql/ir/tests.rs b/sbroad-core/src/frontend/sql/ir/tests.rs index 2606e50f84b7681c4f0893b26101dc83b86df27a..9787436a776095e78d1dbf58457ea0e3ed264cbf 100644 --- a/sbroad-core/src/frontend/sql/ir/tests.rs +++ b/sbroad-core/src/frontend/sql/ir/tests.rs @@ -1530,6 +1530,210 @@ motion [policy: full] assert_eq!(expected_explain, plan.as_explain().unwrap()); } +#[test] +fn front_sql_having1() { + let input = r#"SELECT "a", sum("b") FROM "t" + group by "a" + having "a" > 1 and sum(distinct "b") > 1 + "#; + + let plan = sql_to_optimized_ir(input, vec![]); + let expected_explain = String::from( + r#"projection ("column_12" -> "a", sum(("sum_52")) -> "COL_1") + having ROW("column_12") > ROW(1) and ROW(sum(distinct ("column_30"))) > ROW(1) + group by ("column_12") output: ("column_30" -> "column_30", "column_12" -> "column_12", "sum_52" -> "sum_52") + motion [policy: segment([ref("column_12")])] + scan + projection ("t"."b" -> "column_30", "t"."a" -> "column_12", sum(("t"."b")) -> "sum_52") + group by ("t"."a", "t"."b") output: ("t"."a" -> "a", "t"."b" -> "b", "t"."c" -> "c", "t"."d" -> "d", "t"."bucket_id" -> "bucket_id") + scan "t" +"#, + ); + + assert_eq!(expected_explain, plan.as_explain().unwrap()); +} + +#[test] +fn front_sql_having2() { + let input = r#"SELECT sum("a") * count(distinct "b"), sum("a") FROM "t" + having sum(distinct "b") > 1 and sum("a") > 1 + "#; + + let plan = sql_to_optimized_ir(input, vec![]); + let expected_explain = String::from( + r#"projection ((sum(("sum_38"))) * (count(distinct ("column_39"))) -> "COL_1", sum(("sum_38")) -> "COL_2") + having ROW(sum(distinct ("column_39"))) > ROW(1) and ROW(sum(("sum_38"))) > ROW(1) + motion [policy: full] + scan + projection ("t"."b" -> "column_39", sum(("t"."a")) -> "sum_38") + group by ("t"."b") output: ("t"."a" -> "a", "t"."b" -> "b", "t"."c" -> "c", "t"."d" -> "d", "t"."bucket_id" -> "bucket_id") + scan "t" +"#, + ); + + assert_eq!(expected_explain, plan.as_explain().unwrap()); +} + +#[test] +fn front_sql_having3() { + let input = r#"SELECT sum("a") FROM "t" + having sum("a") > 1 + "#; + + let plan = sql_to_optimized_ir(input, vec![]); + let expected_explain = String::from( + r#"projection (sum(("sum_31")) -> "COL_1") + having ROW(sum(("sum_31"))) > ROW(1) + motion [policy: full] + scan + projection (sum(("t"."a")) -> "sum_31") + scan "t" +"#, + ); + + assert_eq!(expected_explain, plan.as_explain().unwrap()); +} + +#[test] +fn front_sql_having4() { + let input = r#"SELECT sum("a") FROM "t" + having sum("a") > 1 and "b" > 1 + "#; + + let metadata = &RouterConfigurationMock::new(); + let ast = AbstractSyntaxTree::new(input).unwrap(); + let mut plan = ast.resolve_metadata(metadata).unwrap(); + plan.replace_in_operator().unwrap(); + plan.split_columns().unwrap(); + plan.set_dnf().unwrap(); + plan.derive_equalities().unwrap(); + plan.merge_tuples().unwrap(); + let err = plan.add_motions().unwrap_err(); + + assert_eq!( + true, + err.to_string() + .contains("HAVING argument must appear in the GROUP BY clause or be used in an aggregate function") + ); +} + +#[test] +fn front_sql_having_with_sq() { + let input = r#" + SELECT "sysFrom", sum(distinct "id") as "sum", count(distinct "id") as "count" from "test_space" + group by "sysFrom" + having (select "sysFrom" from "test_space" where "sysFrom" = 2) > count(distinct "id") + "#; + + let plan = sql_to_optimized_ir(input, vec![]); + + let expected_explain = String::from( + r#"projection ("column_12" -> "sysFrom", sum(distinct ("column_80")) -> "sum", count(distinct ("column_80")) -> "count") + having ROW($0) > ROW(count(distinct ("column_80"))) + group by ("column_12") output: ("column_12" -> "column_12", "column_80" -> "column_80") + motion [policy: segment([ref("column_12")])] + scan + projection ("test_space"."sysFrom" -> "column_12", "test_space"."id" -> "column_80") + group by ("test_space"."sysFrom", "test_space"."id") output: ("test_space"."id" -> "id", "test_space"."sysFrom" -> "sysFrom", "test_space"."FIRST_NAME" -> "FIRST_NAME", "test_space"."sys_op" -> "sys_op", "test_space"."bucket_id" -> "bucket_id") + scan "test_space" +subquery $0: +motion [policy: full] + scan + projection ("test_space"."sysFrom" -> "sysFrom") + selection ROW("test_space"."sysFrom") = ROW(2) + scan "test_space" +"#, + ); + + assert_eq!(expected_explain, plan.as_explain().unwrap()); +} + +#[test] +fn front_sql_unmatched_column_in_having() { + let input = r#"SELECT sum("a"), "a" FROM "t" + group by "a" + having sum("a") > 1 and "a" > 1 or "c" = 1 + "#; + + let metadata = &RouterConfigurationMock::new(); + let ast = AbstractSyntaxTree::new(input).unwrap(); + let mut plan = ast.resolve_metadata(metadata).unwrap(); + plan.replace_in_operator().unwrap(); + plan.split_columns().unwrap(); + plan.set_dnf().unwrap(); + plan.derive_equalities().unwrap(); + plan.merge_tuples().unwrap(); + let err = plan.add_motions().unwrap_err(); + + assert_eq!( + true, + err.to_string() + .contains("column \"c\" is not found in grouping expressions!") + ); +} + +#[test] +fn front_sql_having_with_sq_segment_motion() { + // check subquery has Segment Motion on groupby columns + let input = r#" + SELECT "sysFrom", "sys_op", sum(distinct "id") as "sum", count(distinct "id") as "count" from "test_space" + group by "sysFrom", "sys_op" + having ("sysFrom", "sys_op") in (select "a", "d" from "t") + "#; + + let plan = sql_to_optimized_ir(input, vec![]); + + let expected_explain = String::from( + r#"projection ("column_12" -> "sysFrom", "column_13" -> "sys_op", sum(distinct ("column_70")) -> "sum", count(distinct ("column_70")) -> "count") + having ROW("column_12", "column_13") in ROW($0, $0) + group by ("column_12", "column_13") output: ("column_12" -> "column_12", "column_70" -> "column_70", "column_13" -> "column_13") + motion [policy: segment([ref("column_12"), ref("column_13")])] + scan + projection ("test_space"."sysFrom" -> "column_12", "test_space"."id" -> "column_70", "test_space"."sys_op" -> "column_13") + group by ("test_space"."sysFrom", "test_space"."sys_op", "test_space"."id") output: ("test_space"."id" -> "id", "test_space"."sysFrom" -> "sysFrom", "test_space"."FIRST_NAME" -> "FIRST_NAME", "test_space"."sys_op" -> "sys_op", "test_space"."bucket_id" -> "bucket_id") + scan "test_space" +subquery $0: +motion [policy: segment([ref("a"), ref("d")])] + scan + projection ("t"."a" -> "a", "t"."d" -> "d") + scan "t" +"#, + ); + + assert_eq!(expected_explain, plan.as_explain().unwrap()); +} + +#[test] +fn front_sql_having_with_sq_segment_local_motion() { + // check subquery has no Motion, as it has the same distribution + // as columns used in GroupBy + let input = r#" + SELECT "sysFrom", "sys_op", sum(distinct "id") as "sum", count(distinct "id") as "count" from "test_space" + group by "sysFrom", "sys_op" + having ("sysFrom", "sys_op") in (select "a", "b" from "t") + "#; + + let plan = sql_to_optimized_ir(input, vec![]); + + let expected_explain = String::from( + r#"projection ("column_12" -> "sysFrom", "column_13" -> "sys_op", sum(distinct ("column_70")) -> "sum", count(distinct ("column_70")) -> "count") + having ROW("column_12", "column_13") in ROW($0, $0) + group by ("column_12", "column_13") output: ("column_12" -> "column_12", "column_70" -> "column_70", "column_13" -> "column_13") + motion [policy: segment([ref("column_12"), ref("column_13")])] + scan + projection ("test_space"."sysFrom" -> "column_12", "test_space"."id" -> "column_70", "test_space"."sys_op" -> "column_13") + group by ("test_space"."sysFrom", "test_space"."sys_op", "test_space"."id") output: ("test_space"."id" -> "id", "test_space"."sysFrom" -> "sysFrom", "test_space"."FIRST_NAME" -> "FIRST_NAME", "test_space"."sys_op" -> "sys_op", "test_space"."bucket_id" -> "bucket_id") + scan "test_space" +subquery $0: +scan + projection ("t"."a" -> "a", "t"."b" -> "b") + scan "t" +"#, + ); + + assert_eq!(expected_explain, plan.as_explain().unwrap()); +} + #[test] fn front_sql_unique_local_aggregates() { // make sure we don't compute extra aggregates at local stage diff --git a/sbroad-core/src/frontend/sql/query.pest b/sbroad-core/src/frontend/sql/query.pest index 976ad55961cb7ad70fe546895166c686e3795e3a..cb938d450f8f84476dd5e2c33955f03aa8b70954 100644 --- a/sbroad-core/src/frontend/sql/query.pest +++ b/sbroad-core/src/frontend/sql/query.pest @@ -3,12 +3,12 @@ Command = _{ SOI ~ (ExplainQuery | Query) ~ EOF } ExplainQuery = _{ Explain } Explain = { ^"explain" ~ Query } -// todo(ars): rename InnerJoin to Join Query = _{ Except | UnionAll | Select | Values | Insert } Select = { ^"select" ~ Projection ~ ^"from" ~ Scan ~ Join? ~ (^"where" ~ Selection)? ~ - (^"group" ~ ^"by" ~ GroupBy)? + (^"group" ~ ^"by" ~ GroupBy)? ~ + (^"having" ~ Having)? } Projection = { (Asterisk | Column) ~ ("," ~ (Asterisk | Column))* } Column = { Alias | Expr | Value } @@ -28,6 +28,7 @@ Query = _{ Except | UnionAll | Select | Values | Insert } LeftJoinKind = { ^"left" ~ (^"outer")? } Condition = { Expr } GroupBy = { GroupingElement ~ ("," ~ GroupingElement)* } + Having = { Expr } UnionAll = { Select ~ ^"union" ~ ^"all" ~ Select } Except = { Select ~ ((^"except" ~ ^"distinct") | ^"except") ~ Select } SubQuery = { "(" ~ (Except | UnionAll | Select | Values) ~ ")" } diff --git a/sbroad-core/src/ir/aggregates.rs b/sbroad-core/src/ir/aggregates.rs index 5fb60d50045186be804594b634b2dba367a9165d..7ec5495214e2579a278852331f316bbd2faaf822 100644 --- a/sbroad-core/src/ir/aggregates.rs +++ b/sbroad-core/src/ir/aggregates.rs @@ -180,7 +180,7 @@ impl SimpleAggregate { /// - Invalid aggregate /// - Could not find local alias position in child output #[allow(clippy::too_many_lines)] - pub fn create_column_for_final_projection( + pub fn create_final_aggregate_expr( &self, parent: usize, plan: &mut Plan, diff --git a/sbroad-core/src/ir/explain.rs b/sbroad-core/src/ir/explain.rs index 9c8da5f2d1d427de21160d22adfd25d49d2654f5..11b0513c1256e796dc2817accdcfd5e27b8ce480 100644 --- a/sbroad-core/src/ir/explain.rs +++ b/sbroad-core/src/ir/explain.rs @@ -441,26 +441,32 @@ impl Row { Expression::Constant { value, .. } => { row.add_col(RowVal::Const(value.clone())); } - Expression::Reference { position, .. } => { + Expression::Reference { .. } => { let rel_id: usize = *plan.get_relational_from_reference_node(*child)?; let rel_node = plan.get_relation_node(rel_id)?; - - // If relation node doesn't have alias name it means - // that this node is a sub-query and acts as a part of - // the WHERE cause. - if rel_node.scan_name(plan, *position)?.is_some() { + if plan.is_additional_child(rel_id)? { + if let Relational::ScanSubQuery { .. } | Relational::Motion { .. } = + rel_node + { + let sq_offset = ref_map.get(&rel_id).ok_or_else(|| { + SbroadError::NotFound( + Entity::SubQuery, + format!("with index {rel_id} in the map"), + ) + })?; + row.add_col(RowVal::SqRef(Ref::new(*sq_offset))); + } else { + return Err(SbroadError::Invalid( + Entity::Plan, + Some(format!( + "additional child ({rel_id}) is not SQ or Motion: {rel_node:?}" + )), + )); + } + } else { let col = Col::new(plan, *child)?; row.add_col(RowVal::Column(col)); - } else { - let sq_offset = ref_map.get(&rel_id).ok_or_else(|| { - SbroadError::NotFound( - Entity::SubQuery, - format!("with index {rel_id} in the map"), - ) - })?; - - row.add_col(RowVal::SqRef(Ref::new(*sq_offset))); } } Expression::Bool { .. } @@ -724,6 +730,7 @@ enum ExplainNode { Projection(Projection), Scan(Scan), Selection(Selection), + Having(Selection), UnionAll, SubQuery(SubQuery), Motion(Motion), @@ -741,6 +748,7 @@ impl Display for ExplainNode { ExplainNode::GroupBy(p) => p.to_string(), ExplainNode::Scan(s) => s.to_string(), ExplainNode::Selection(s) => format!("selection {s}"), + ExplainNode::Having(s) => format!("having {s}"), ExplainNode::UnionAll => "union all".to_string(), ExplainNode::SubQuery(s) => s.to_string(), ExplainNode::Motion(m) => m.to_string(), @@ -879,6 +887,9 @@ impl FullExplain { } Relational::Selection { children, filter, .. + } + | Relational::Having { + children, filter, .. } => { let mut sq_ref_map: HashMap<usize, usize> = HashMap::with_capacity(children.len() - 1); @@ -906,7 +917,12 @@ impl FullExplain { } let filter_id = ir.undo.get_oldest(filter).map_or_else(|| *filter, |id| *id); let s = Selection::new(ir, filter_id, &sq_ref_map)?; - Some(ExplainNode::Selection(s)) + let explain_node = match &node { + Relational::Selection { .. } => ExplainNode::Selection(s), + Relational::Having { .. } => ExplainNode::Having(s), + _ => return Err(SbroadError::DoSkip), + }; + Some(explain_node) } Relational::UnionAll { .. } => { if let (Some(right), Some(left)) = (stack.pop(), stack.pop()) { diff --git a/sbroad-core/src/ir/helpers.rs b/sbroad-core/src/ir/helpers.rs index a8276362436a15a4b88a08ee9a1c562030ae3d35..3d16cd302d1add3bd931dc208b44641228fec534 100644 --- a/sbroad-core/src/ir/helpers.rs +++ b/sbroad-core/src/ir/helpers.rs @@ -204,6 +204,12 @@ impl Plan { writeln!(buf, "Filter")?; self.formatted_arena_node(buf, tabulation_number + 1, *filter)?; } + Relational::Having { filter, .. } => { + writeln!(buf, "Having")?; + formatted_tabulate(buf, tabulation_number + 1)?; + writeln!(buf, "Filter")?; + self.formatted_arena_node(buf, tabulation_number + 1, *filter)?; + } Relational::GroupBy { gr_cols, is_final, .. } => { @@ -240,6 +246,7 @@ impl Plan { | Relational::Values { children, .. } | Relational::Motion { children, .. } | Relational::UnionAll { children, .. } + | Relational::Having { children, .. } | Relational::GroupBy { children, .. } | Relational::ValuesRow { children, .. } => { formatted_tabulate(buf, tabulation_number + 1)?; @@ -264,6 +271,7 @@ impl Plan { | Relational::ScanSubQuery { output, .. } | Relational::GroupBy { output, .. } | Relational::Selection { output, .. } + | Relational::Having { output, .. } | Relational::Values { output, .. } | Relational::Motion { output, .. } | Relational::UnionAll { output, .. } diff --git a/sbroad-core/src/ir/operator.rs b/sbroad-core/src/ir/operator.rs index 69266f445b95f710db226966eef372f2ae9ca9e1..da17f7bb80f6f29834a774941fbc63b6224f5630 100644 --- a/sbroad-core/src/ir/operator.rs +++ b/sbroad-core/src/ir/operator.rs @@ -292,6 +292,11 @@ pub enum Relational { output: usize, is_final: bool, }, + Having { + children: Vec<usize>, + output: usize, + filter: usize, + }, UnionAll { /// Contains exactly two elements: left and right node indexes /// from the plan node arena. @@ -369,6 +374,7 @@ impl Relational { match self { Relational::Except { output, .. } | Relational::GroupBy { output, .. } + | Relational::Having { output, .. } | Relational::Join { output, .. } | Relational::Insert { output, .. } | Relational::Motion { output, .. } @@ -388,6 +394,7 @@ impl Relational { match self { Relational::Except { output, .. } | Relational::GroupBy { output, .. } + | Relational::Having { output, .. } | Relational::Join { output, .. } | Relational::Insert { output, .. } | Relational::Motion { output, .. } @@ -408,6 +415,7 @@ impl Relational { Relational::Except { children, .. } | Relational::GroupBy { children, .. } | Relational::Join { children, .. } + | Relational::Having { children, .. } | Relational::Insert { children, .. } | Relational::Motion { children, .. } | Relational::Projection { children, .. } @@ -430,6 +438,9 @@ impl Relational { | Relational::GroupBy { ref mut children, .. } + | Relational::Having { + ref mut children, .. + } | Relational::Join { ref mut children, .. } @@ -525,6 +536,10 @@ impl Relational { children: ref mut old, .. } + | Relational::Having { + children: ref mut old, + .. + } | Relational::ValuesRow { children: ref mut old, .. @@ -555,6 +570,7 @@ impl Relational { } => Ok(alias.as_deref().or(Some(relation.as_str()))), Relational::Projection { .. } | Relational::GroupBy { .. } + | Relational::Having { .. } | Relational::Selection { .. } | Relational::Join { .. } => { let output_row = plan.get_expression_node(self.output())?; @@ -1017,6 +1033,50 @@ impl Plan { Ok(select_id) } + /// Adds having node + /// + /// # Errors + /// - children list is empty + /// - filter expression is not boolean + /// - children nodes are not relational + /// - first child output tuple is not valid + pub fn add_having(&mut self, children: &[usize], filter: usize) -> Result<usize, SbroadError> { + let first_child: usize = match children.len() { + 0 => { + return Err(SbroadError::UnexpectedNumberOfValues( + "children list is empty".into(), + )) + } + _ => children[0], + }; + + if !self.is_trivalent(filter)? { + return Err(SbroadError::Invalid( + Entity::Expression, + Some("filter expression is not a trivalent expression.".into()), + )); + } + + for child in children { + if let Node::Relational(_) = self.get_node(*child)? { + } else { + return Err(SbroadError::Invalid(Entity::Relational, None)); + } + } + + let output = self.add_row_for_output(first_child, &[], true)?; + let having = Relational::Having { + children: children.into(), + filter, + output, + }; + + let having_id = self.nodes.push(Node::Relational(having)); + self.replace_parent_in_subtree(filter, None, Some(having_id))?; + self.replace_parent_in_subtree(output, None, Some(having_id))?; + Ok(having_id) + } + /// Adds sub query scan node. /// /// # Errors @@ -1309,7 +1369,7 @@ impl Plan { for (_, id) in rel_tree.iter(top_id) { let rel = self.get_relation_node(id)?; match rel { - Relational::Selection { children, .. } => { + Relational::Selection { children, .. } | Relational::Having { children, .. } => { if children.iter().skip(1).any(|&c| c == node_id) { return Ok(true); } @@ -1338,9 +1398,9 @@ impl Plan { return Ok(false); }; match self.get_relation_node(rel_id)? { - Relational::Selection { .. } | Relational::Projection { .. } => { - Ok(children.first() != Some(&sq_id)) - } + Relational::Selection { .. } + | Relational::Projection { .. } + | Relational::Having { .. } => Ok(children.first() != Some(&sq_id)), Relational::Join { .. } => { Ok(children.first() != Some(&sq_id) && children.get(1) != Some(&sq_id)) } diff --git a/sbroad-core/src/ir/transformation/redistribution.rs b/sbroad-core/src/ir/transformation/redistribution.rs index ee54b2319085132f338b0c171a24613a5b64f5ab..1b55fe29eeb8207cc61f5918070a7e60571f0256 100644 --- a/sbroad-core/src/ir/transformation/redistribution.rs +++ b/sbroad-core/src/ir/transformation/redistribution.rs @@ -1423,6 +1423,7 @@ impl Plan { | Relational::ScanSubQuery { output, .. } | Relational::Values { output, .. } | Relational::GroupBy { output, .. } + | Relational::Having { output, .. } | Relational::ValuesRow { output, .. } => { self.set_distribution(output)?; } diff --git a/sbroad-core/src/ir/transformation/redistribution/groupby.rs b/sbroad-core/src/ir/transformation/redistribution/groupby.rs index 6d9cbbc5b5b9a161b082117dd3ed6cb88bd8ed27..84d0d7c94ebcfc459a9dda54db54f97bdecb71b9 100644 --- a/sbroad-core/src/ir/transformation/redistribution/groupby.rs +++ b/sbroad-core/src/ir/transformation/redistribution/groupby.rs @@ -5,7 +5,7 @@ use crate::ir::expression::Expression; use crate::ir::expression::Expression::StableFunction; use crate::ir::operator::Relational; use crate::ir::transformation::redistribution::{MotionKey, MotionPolicy, Strategy, Target}; -use crate::ir::tree::traversal::{PostOrder, EXPR_CAPACITY}; +use crate::ir::tree::traversal::{BreadthFirst, PostOrder, EXPR_CAPACITY}; use crate::ir::{Node, Plan}; use std::collections::{HashMap, HashSet}; @@ -283,7 +283,24 @@ impl<'plan> AggrCollector<'plan> { } } +/// Maps id of `GroupBy` expression used in `GroupBy` (from local stage) +/// to list of locations where this expression is used in other relational +/// operators like `Having`, `Projection`. +/// +/// For example: +/// `select a from t group by a having a = 1` +/// Here expression in `GroupBy` is mapped to `a` in `Projection` and `a` in `Having` type GroupbyExpressionsMap = HashMap<usize, Vec<ExpressionLocation>>; +/// Maps id of `GroupBy` expression used in `GroupBy` (from local stage) +/// to corresponding local alias used in local Projection. Note: +/// this map does not contain mappings between grouping expressions from +/// distinct aggregates (it is stored in corresponding `AggrInfo` for that +/// aggregate) +/// +/// For example: +/// initial query: `select a, count(distinct b) from t group by a` +/// map query: `select a as l1, b group by a, b` +/// Then this map will map id of `a` to `l1` type LocalAliasesMap = HashMap<usize, Rc<String>>; type LocalAggrInfo = (AggregateKind, Vec<usize>, Rc<String>); @@ -292,11 +309,9 @@ type LocalAggrInfo = (AggregateKind, Vec<usize>, Rc<String>); struct ExpressionMapper<'plan> { /// List of expressions ids of `GroupBy` gr_exprs: &'plan Vec<usize>, - /// Maps `GroupBy` expression to expressions used `Projection` - /// First element in pair is `Projection` column id, - /// second one is the id of expression map: GroupbyExpressionsMap, plan: &'plan Plan, + /// Id of relational node (`Projection`, `Having`, `OrderBy`) node_id: Option<usize>, } @@ -316,6 +331,11 @@ impl<'plan> ExpressionMapper<'plan> { /// when match is found it is stored in map passed to [`ExpressionMapper`]'s /// constructor. /// + /// # Arguments + /// * `expr_root` - expression id from which matching will start + /// * `node_id` - id of relational node (`Having`, `Projection`, `OrderBy`), + /// where expression pointed by `expr_root` is located + /// /// # Errors /// - invalid references in any expression (`GroupBy`'s or node's one) /// - invalid query: node expression contains references that are not @@ -331,6 +351,23 @@ impl<'plan> ExpressionMapper<'plan> { /// Helper function for `find_matches` which compares current node to `GroupBy` expressions /// and if no match is found recursively calls itself. fn find(&mut self, current: usize, parent: Option<usize>) -> Result<(), SbroadError> { + let Some(node_id) = self.node_id else { + return Err(SbroadError::Invalid(Entity::ExpressionMapper, None)) + }; + let is_ref = matches!( + self.plan.get_expression_node(current), + Ok(Expression::Reference { .. }) + ); + let is_sq_ref = is_ref + && self.plan.is_additional_child_of_rel( + node_id, + *self.plan.get_relational_from_reference_node(current)?, + )?; + // Because subqueries are replaced with References, we must not + // try to match these references against any GroupBy expressions + if is_sq_ref { + return Ok(()); + } if let Some(gr_expr) = self .gr_exprs .iter() @@ -341,9 +378,6 @@ impl<'plan> ExpressionMapper<'plan> { }) .copied() { - let Some(node_id) = self.node_id else { - return Err(SbroadError::Invalid(Entity::ExpressionMapper, None)) - }; let location = ExpressionLocation::new(current, parent, node_id); if let Some(v) = self.map.get_mut(&gr_expr) { v.push(location); @@ -352,12 +386,12 @@ impl<'plan> ExpressionMapper<'plan> { } return Ok(()); } - let node = self.plan.get_expression_node(current)?; - if let Expression::Reference { .. } = node { + if is_ref { // We found a column which is not inside aggregate function // and it is not a grouping expression: // select a from t group by b - is invalid let column_name = { + let node = self.plan.get_expression_node(current)?; self.plan .get_alias_from_reference_node(node) .unwrap_or("'failed to get column name'") @@ -652,12 +686,13 @@ impl Plan { collector.collect_aggregates(*col, *node_id)?; } } + Relational::Having { filter, .. } => { + collector.collect_aggregates(*filter, *node_id)?; + } _ => { return Err(SbroadError::Invalid( Entity::Plan, - Some(format!( - "collect_aggregates: unexpected relational node ({node_id}): {node:?}" - )), + Some(format!("unexpected relational node ({node_id}): {node:?}")), )) } } @@ -718,24 +753,22 @@ impl Plan { .get_relational_children(rel_id)? .ok_or_else(|| { SbroadError::UnexpectedNumberOfValues(format!( - "split_reduce_stage: expected relation node ({rel_id}) to have children!" + "expected relation node ({rel_id}) to have children!" )) })? .first() .ok_or_else(|| { SbroadError::UnexpectedNumberOfValues(format!( - "split_reduce_stage: expected relation node ({rel_id}) to have children!" + "expected relation node ({rel_id}) to have children!" )) })?; Ok(c) }; let mut next: usize = final_proj_id; - // Currently in Reduce stage only Projection is possible. - // When any Having, OrderBy, Limit will be added this will change. - let max_reduce_nodes = 1; + let max_reduce_nodes = 2; for _ in 0..=max_reduce_nodes { match self.get_relation_node(next)? { - Relational::Projection { .. } => { + Relational::Projection { .. } | Relational::Having { .. } => { finals.push(next); next = get_first_child(next)?; } @@ -799,10 +832,16 @@ impl Plan { let mut mapper = ExpressionMapper::new(&grouping_expr, self); for node_id in finals { - if let Relational::Projection { output, .. } = self.get_relation_node(*node_id)? { - for col in self.get_row_list(*output)? { - mapper.find_matches(*col, *node_id)?; + match self.get_relation_node(*node_id)? { + Relational::Projection { output, .. } => { + for col in self.get_row_list(*output)? { + mapper.find_matches(*col, *node_id)?; + } + } + Relational::Having { filter, .. } => { + mapper.find_matches(*filter, *node_id)?; } + _ => {} } } gr_expr_map = mapper.get_matches(); @@ -812,26 +851,46 @@ impl Plan { // check that all column references are inside aggregate functions for id in finals { let node = self.get_relation_node(*id)?; - if let Relational::Projection { output, .. } = node { - for col in self.get_row_list(*output)? { - let mut dfs = PostOrder::with_capacity( + match node { + Relational::Projection { output, .. } => { + for col in self.get_row_list(*output)? { + let mut dfs = PostOrder::with_capacity( + |x| self.nodes.aggregate_iter(x, false), + EXPR_CAPACITY, + ); + dfs.populate_nodes(*col); + let nodes = dfs.take_nodes(); + for (_, id) in nodes { + let n = self.get_expression_node(id)?; + if let Expression::Reference { .. } = n { + let alias = match self.get_alias_from_reference_node(n) { + Ok(v) => v.to_string(), + Err(e) => e.to_string(), + }; + return Err(SbroadError::Invalid(Entity::Query, + Some(format!("found column reference ({alias}) outside aggregate function")))); + } + } + } + } + Relational::Having { filter, .. } => { + let mut bfs = BreadthFirst::with_capacity( |x| self.nodes.aggregate_iter(x, false), EXPR_CAPACITY, + EXPR_CAPACITY, ); - dfs.populate_nodes(*col); - let nodes = dfs.take_nodes(); + bfs.populate_nodes(*filter); + let nodes = bfs.take_nodes(); for (_, id) in nodes { - let n = self.get_expression_node(id)?; - if let Expression::Reference { .. } = n { - let alias = match self.get_alias_from_reference_node(n) { - Ok(v) => v.to_string(), - Err(e) => e.to_string(), - }; - return Err(SbroadError::Invalid(Entity::Query, - Some(format!("found column reference ({alias}) outside aggregate function")))); + if let Expression::Reference { .. } = self.get_expression_node(id)? { + return Err(SbroadError::Invalid( + Entity::Query, + Some("HAVING argument must appear in the GROUP BY clause or be used in an aggregate function".into()) + )); } } } + _ => {} } } } @@ -1208,6 +1267,18 @@ impl Plan { Ok(()) } + /// Add final `GroupBy` node in case `grouping_exprs` are not empty + /// + /// # Arguments + /// * `child_id` - id if relational node that will the child of `GroupBy` + /// * `grouping_exprs` - list of grouping expressions ids (which does not include + /// grouping expressions from distinct arguments) + /// * `local_aliases_map` - map between expression from `GroupBy` to alias used + /// at local stage + /// + /// # Returns + /// - if `GroupBy` node was created, return its id + /// - if `GroupBy` node was not created, return `child_id` fn add_final_groupby( &mut self, child_id: usize, @@ -1215,7 +1286,7 @@ impl Plan { local_aliases_map: &LocalAliasesMap, ) -> Result<usize, SbroadError> { if grouping_exprs.is_empty() { - // no GroupBy in the original query nothing to do + // no GroupBy in the original query, nothing to do return Ok(child_id); } let mut gr_cols: Vec<usize> = Vec::with_capacity(grouping_exprs.len()); @@ -1228,12 +1299,12 @@ impl Plan { for expr_id in grouping_exprs { let Some(local_alias) = local_aliases_map.get(expr_id) else { return Err(SbroadError::Invalid(Entity::Plan, - Some(format!("add_final_groupby: could not find local alias for GroupBy expr ({expr_id})")))) + Some(format!("could not find local alias for GroupBy expr ({expr_id})")))) }; let Some(position) = child_map.get(&*(*local_alias)) else { return Err(SbroadError::Invalid( Entity::Node, - Some(format!("add_final_groupby: did not find alias: {local_alias} in child ({child_id}) output!"))) + Some(format!("did not find alias: {local_alias} in child ({child_id}) output!"))) ) }; let new_col = Expression::Reference { @@ -1282,9 +1353,9 @@ impl Plan { HashMap<RelationalID, Vec<(GroupByExpressionID, ExpressionID, ExpressionParent)>>; let map: ParentExpressionMap = { let mut new_map: ParentExpressionMap = HashMap::with_capacity(map.len()); - for (k, v) in map { - for location in v { - let rec = (k, location.expr_id, location.parent_expr_id); + for (groupby_expr_id, locations) in map { + for location in locations { + let rec = (groupby_expr_id, location.expr_id, location.parent_expr_id); if let Some(u) = new_map.get_mut(&location.rel_id) { u.push(rec); } else { @@ -1300,13 +1371,13 @@ impl Plan { .get_relational_children(rel_id)? .ok_or_else(|| { SbroadError::UnexpectedNumberOfValues(format!( - "patch_grouping_exprs: expected relation node ({rel_id}) to have children!" + "expected relation node ({rel_id}) to have children!" )) })? .first() .ok_or_else(|| { SbroadError::UnexpectedNumberOfValues(format!( - "patch_grouping_exprs: expected relation node ({rel_id}) to have children!" + "expected relation node ({rel_id}) to have children!" )) })?; let alias_to_pos_map = self @@ -1319,11 +1390,11 @@ impl Plan { let Some(local_alias) = local_aliases_map.get(&gr_expr_id) else { return Err(SbroadError::Invalid( Entity::Plan, - Some(format!("patch_finals: failed to find local alias for groupby expression {gr_expr_id}")))) + Some(format!("failed to find local alias for groupby expression {gr_expr_id}")))) }; let Some(pos) = alias_to_pos_map.get(&*(*local_alias)).copied() else { return Err(SbroadError::Invalid(Entity::Plan, - Some(format!("patch_finals: failed to find alias '{local_alias}' in ({child_id}). Aliases: {}", alias_to_pos_map.keys().join(" "))))) + Some(format!("failed to find alias '{local_alias}' in ({child_id}). Aliases: {}", alias_to_pos_map.keys().join(" "))))) }; let new_ref = Expression::Reference { parent: Some(rel_id), @@ -1338,13 +1409,16 @@ impl Plan { Relational::Projection { .. } => { return Err(SbroadError::Invalid( Entity::Plan, - Some(format!("patch_finals: invalid mapping between groupby expression {gr_expr_id} and projection one: expression {expr_id} has no parent")) + Some(format!("invalid mapping between groupby expression {gr_expr_id} and projection one: expression {expr_id} has no parent")) )) } + Relational::Having { filter, .. } => { + *filter = ref_id; + } _ => { return Err(SbroadError::Invalid( Entity::Plan, - Some(format!("patch_finals: unexpected node in Reduce stage: {rel_id}")) + Some(format!("unexpected node in Reduce stage: {rel_id}")) )) } } @@ -1388,19 +1462,34 @@ impl Plan { // After we added a Map stage, we need to update output // of nodes in Reduce stage if let Some(last) = finals.last() { - self.get_mut_relation_node(*last)? - .set_children(vec![finals_child_id])?; + if let Some(children) = self.get_mut_relation_node(*last)?.mut_children() { + if let Some(first) = children.get_mut(0) { + *first = finals_child_id; + } + } } for node_id in finals.iter().rev() { let node = self.get_relation_node(*node_id)?; match node { + // Projection node is the top node in finals: its aliases + // must not be changed (because those are user aliases), so + // nothing to do here Relational::Projection { .. } => {} + Relational::Having { children, .. } => { + let child_id = *children.first().ok_or_else(|| { + SbroadError::Invalid( + Entity::Node, + Some(format!("Having ({node_id}) has no children!")), + ) + })?; + let output = self.add_row_for_output(child_id, &[], true)?; + *self.get_mut_relation_node(*node_id)?.mut_output() = output; + self.replace_parent_in_subtree(output, None, Some(*node_id))?; + } _ => { return Err(SbroadError::Invalid( Entity::Plan, - Some(format!( - "patch_finals: Unexpected node in reduce stage: {node:?}" - )), + Some(format!("Unexpected node in reduce stage: {node:?}")), )) } } @@ -1441,7 +1530,7 @@ impl Plan { .map(|(k, v)| (k.to_string(), v)) .collect::<HashMap<String, usize>>(); for info in infos { - let final_expr = info.aggr.create_column_for_final_projection( + let final_expr = info.aggr.create_final_aggregate_expr( parent, self, &alias_to_pos_map, @@ -1449,14 +1538,12 @@ impl Plan { )?; if let Some(parent_expr) = info.parent_expr { self.replace_expression(parent_expr, info.aggr.fun_id, final_expr)?; - } else if let Relational::Projection { .. } = self.get_mut_relation_node(parent)? { - // currently final stage may contain only Projection node, - // when Having will be added, here will be the logic of replacing - // its filter + } else { + let node = self.get_mut_relation_node(parent)?; return Err(SbroadError::Invalid( Entity::Aggregate, Some(format!( - "aggregate info from Projection has no parent! Info: {info:?}" + "aggregate info for {node:?} that hat no parent! Info: {info:?}" )), )); } @@ -1562,9 +1649,13 @@ impl Plan { )?; self.add_motion_to_2stage(&grouping_positions, finals_child_id, &finals)?; + let mut having_id: Option<usize> = None; // skip Projection for node_id in finals.iter().skip(1).rev() { self.set_distribution(self.get_relational_output(*node_id)?)?; + if let Relational::Having { .. } = self.get_relation_node(*node_id)? { + having_id = Some(*node_id); + } } if matches!( @@ -1579,6 +1670,14 @@ impl Plan { )?; } + // resolve subquery conflicts in HAVING + if let Some(having_id) = having_id { + if let Relational::Having { filter, .. } = self.get_relation_node(having_id)? { + let strategy = self.resolve_sub_query_conflicts(having_id, *filter)?; + self.create_motion_nodes(&strategy)?; + } + } + Ok(true) } } diff --git a/sbroad-core/src/ir/tree/relation.rs b/sbroad-core/src/ir/tree/relation.rs index 882c5c5565f578c2f9913250aafac9a697fa907f..93259769e6022f0452cd91d7e5d45c7f2b70c5a3 100644 --- a/sbroad-core/src/ir/tree/relation.rs +++ b/sbroad-core/src/ir/tree/relation.rs @@ -63,6 +63,7 @@ fn relational_next<'nodes>( | Relational::Projection { children, .. } | Relational::ScanSubQuery { children, .. } | Relational::Selection { children, .. } + | Relational::Having { children, .. } | Relational::UnionAll { children, .. } | Relational::Values { children, .. } | Relational::ValuesRow { children, .. }, diff --git a/sbroad-core/src/ir/tree/subtree.rs b/sbroad-core/src/ir/tree/subtree.rs index a99ec9694f97f6bf9bd67abef8a9e25373810e7a..9054cf25e7ad491680457167a78c0b640614a6a8 100644 --- a/sbroad-core/src/ir/tree/subtree.rs +++ b/sbroad-core/src/ir/tree/subtree.rs @@ -251,7 +251,11 @@ fn subtree_next<'plan>( // Check if the sub-query is an additional one. let parent = iter.get_plan().get_relation_node(parent_id); let mut is_additional = false; - if let Ok(Relational::Selection { children, .. }) = parent { + if let Ok( + Relational::Selection { children, .. } + | Relational::Having { children, .. }, + ) = parent + { if children.iter().skip(1).any(|&c| c == *rel_id) { is_additional = true; } @@ -398,6 +402,11 @@ fn subtree_next<'plan>( filter, output, .. + } + | Relational::Having { + children, + filter, + output, } => { let step = *iter.get_child().borrow(); diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_3_having.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_3_having.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b2404c57edeed5b5807a0e1ff258253a01ccab54 --- /dev/null +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_3_having.yaml @@ -0,0 +1,80 @@ +--- +nodes: + arena: + - children: #0 + - 10 + rule: Select + value: ~ + - children: #1 + - 8 + - 2 + rule: Having + value: ~ + - children: #2 + - 4 + - 3 + rule: Gt + value: ~ + - children: [] #3 + rule: Unsigned + value: "1" + - children: #4 + - 7 + - 5 + rule: Function + value: ~ + - children: #5 + - 6 + rule: Reference + value: ~ + - children: [] #6 + rule: ColumnName + value: "\"a\"" + - children: [] #7 + rule: FunctionName + value: sum + - children: #8 + - 9 + rule: Scan + value: ~ + - children: [] #9 + rule: Table + value: t + - children: #10 + - 1 + - 11 + rule: Projection + value: ~ + - children: #11 + - 17 + rule: Column + value: ~ + - children: #12 + - 15 + - 13 + rule: Function + value: ~ + - children: #13 + - 14 + rule: Reference + value: ~ + - children: [] #14 + rule: ColumnName + value: "\"a\"" + - children: [] #15 + rule: FunctionName + value: sum + - children: [] #16 + rule: AliasName + value: COL_1 + - children: #17 + - 12 + - 16 + rule: Alias + value: ~ +top: 10 +map: + 5: + - 8 + 13: + - 1 \ No newline at end of file diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_4.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_3_join.yaml similarity index 100% rename from sbroad-core/tests/artifactory/frontend/sql/transform_select_4.yaml rename to sbroad-core/tests/artifactory/frontend/sql/transform_select_3_join.yaml diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_having_1.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_having_1.yaml new file mode 100644 index 0000000000000000000000000000000000000000..47470283a3d5e5dde9501087d46bea5dbb232764 --- /dev/null +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_having_1.yaml @@ -0,0 +1,102 @@ +--- +nodes: + arena: + - children: #0 + - 15 + rule: Select + value: ~ + - children: #1 + - 8 + - 2 + rule: Having + value: ~ + - children: #2 + - 4 + - 3 + rule: Gt + value: ~ + - children: [] #3 + rule: Unsigned + value: "1" + - children: #4 + - 7 + - 5 + rule: Function + value: ~ + - children: #5 + - 6 + rule: Reference + value: ~ + - children: [] #6 + rule: ColumnName + value: "\"a\"" + - children: [] #7 + rule: FunctionName + value: sum + - children: #8 + - 13 + - 9 + rule: Selection + value: ~ + - children: #9 + - 11 + - 10 + rule: Gt + value: ~ + - children: [] #10 + rule: Unsigned + value: "1" + - children: #11 + - 12 + rule: Reference + value: ~ + - children: [] #12 + rule: ColumnName + value: "\"a\"" + - children: #13 + - 14 + rule: Scan + value: ~ + - children: [] #14 + rule: Table + value: t1 + - children: #15 + - 1 + - 16 + rule: Projection + value: ~ + - children: #16 + - 22 + rule: Column + value: ~ + - children: #17 + - 20 + - 18 + rule: Function + value: ~ + - children: #18 + - 19 + rule: Reference + value: ~ + - children: [] #19 + rule: ColumnName + value: "\"a\"" + - children: [] #20 + rule: FunctionName + value: sum + - children: [] #21 + rule: AliasName + value: COL_1 + - children: #22 + - 17 + - 21 + rule: Alias + value: ~ +top: 15 +map: + 18: + - 1 + 5: + - 8 + 11: + - 13 \ No newline at end of file diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_having_2.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_having_2.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7ab3d344a65abf8432b835f7968c0e9c2f92f24e --- /dev/null +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_having_2.yaml @@ -0,0 +1,94 @@ +--- +nodes: + arena: + - children: #0 + - 13 + rule: Select + value: ~ + - children: #1 + - 8 + - 2 + rule: Having + value: ~ + - children: #2 + - 4 + - 3 + rule: Gt + value: ~ + - children: [] #3 + rule: Unsigned + value: "1" + - children: #4 + - 7 + - 5 + rule: Function + value: ~ + - children: #5 + - 6 + rule: Reference + value: ~ + - children: [] #6 + rule: ColumnName + value: "\"a\"" + - children: [] #7 + rule: FunctionName + value: sum + - children: #8 + - 11 + - 9 + rule: GroupBy + value: ~ + - children: #9 + - 10 + rule: Reference + value: ~ + - children: [] #10 + rule: ColumnName + value: "\"a\"" + - children: #11 + - 12 + rule: Scan + value: ~ + - children: [] #12 + rule: Table + value: t1 + - children: #13 + - 1 + - 14 + rule: Projection + value: ~ + - children: #14 + - 20 + rule: Column + value: ~ + - children: #15 + - 18 + - 16 + rule: Function + value: ~ + - children: #16 + - 17 + rule: Reference + value: ~ + - children: [] #17 + rule: ColumnName + value: "\"a\"" + - children: [] #18 + rule: FunctionName + value: sum + - children: [] #19 + rule: AliasName + value: COL_1 + - children: #20 + - 15 + - 19 + rule: Alias + value: ~ +top: 13 +map: + 9: + - 11 + 16: + - 1 + 5: + - 8 \ No newline at end of file diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_join_filter.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_join_filter.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9d151c877058883b2f2c584590800102f76ae3a9 --- /dev/null +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_join_filter.yaml @@ -0,0 +1,106 @@ +--- +nodes: + arena: + - children: #0 + - 21 + rule: Select + value: ~ + - children: #1 + - 7 + - 2 + rule: Selection + value: ~ + - children: #2 + - 4 + - 3 + rule: Gt + value: ~ + - children: [] #3 + rule: Unsigned + value: "0" + - children: #4 + - 6 + - 5 + rule: Reference + value: ~ + - children: [] #5 + rule: ColumnName + value: a + - children: [] #6 + rule: ScanName + value: t1 + - children: #7 + - 19 + - 18 + - 16 + - 8 + rule: Join + value: ~ + - children: #8 + - 9 + rule: Condition + value: ~ + - children: #9 + - 13 + - 10 + rule: Eq + value: ~ + - children: #10 + - 12 + - 11 + rule: Reference + value: ~ + - children: [] #11 + rule: ColumnName + value: a + - children: [] #12 + rule: ScanName + value: t2 + - children: #13 + - 15 + - 14 + rule: Reference + value: ~ + - children: [] #14 + rule: ColumnName + value: a + - children: [] #15 + rule: ScanName + value: t1 + - children: #16 + - 17 + rule: Scan + value: ~ + - children: [] #17 + rule: Table + value: t2 + - children: [] #18 + rule: InnerJoinKind + value: inner + - children: #19 + - 20 + rule: Scan + value: ~ + - children: [] #20 + rule: Table + value: t1 + - children: #21 + - 1 + - 22 + rule: Projection + value: ~ + - children: [] #22 + rule: Asterisk + value: "*" +top: 21 +map: + 13: + - 19 + - 16 + 22: + - 1 + 10: + - 19 + - 16 + 4: + - 7 diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_5_1.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_join_groupby.yaml similarity index 100% rename from sbroad-core/tests/artifactory/frontend/sql/transform_select_5_1.yaml rename to sbroad-core/tests/artifactory/frontend/sql/transform_select_4_join_groupby.yaml diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_join_having.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_join_having.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e970c6504f7d50446804a09dbb909bace5c2b0f5 --- /dev/null +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_join_having.yaml @@ -0,0 +1,138 @@ +--- +nodes: + arena: + - children: #0 + - 23 + rule: Select + value: ~ + - children: #1 + - 9 + - 2 + rule: Having + value: ~ + - children: #2 + - 4 + - 3 + rule: Gt + value: ~ + - children: [] #3 + rule: Unsigned + value: "1" + - children: #4 + - 8 + - 5 + rule: Function + value: ~ + - children: #5 + - 7 + - 6 + rule: Reference + value: ~ + - children: [] #6 + rule: ColumnName + value: a + - children: [] #7 + rule: ScanName + value: t1 + - children: [] #8 + rule: FunctionName + value: sum + - children: #9 + - 21 + - 20 + - 18 + - 10 + rule: Join + value: ~ + - children: #10 + - 11 + rule: Condition + value: ~ + - children: #11 + - 15 + - 12 + rule: Eq + value: ~ + - children: #12 + - 14 + - 13 + rule: Reference + value: ~ + - children: [] #13 + rule: ColumnName + value: a + - children: [] #14 + rule: ScanName + value: t2 + - children: #15 + - 17 + - 16 + rule: Reference + value: ~ + - children: [] #16 + rule: ColumnName + value: a + - children: [] #17 + rule: ScanName + value: t1 + - children: #18 + - 19 + rule: Scan + value: ~ + - children: [] #19 + rule: Table + value: t2 + - children: [] #20 + rule: InnerJoinKind + value: inner + - children: #21 + - 22 + rule: Scan + value: ~ + - children: [] #22 + rule: Table + value: t1 + - children: #23 + - 1 + - 24 + rule: Projection + value: ~ + - children: #24 + - 30 + rule: Column + value: ~ + - children: #25 + - 28 + - 26 + rule: Function + value: ~ + - children: #26 + - 27 + rule: Reference + value: ~ + - children: [] #27 + rule: ColumnName + value: a + - children: [] #28 + rule: FunctionName + value: sum + - children: [] #29 + rule: AliasName + value: COL_1 + - children: #30 + - 25 + - 29 + rule: Alias + value: ~ +top: 23 +map: + 12: + - 21 + - 18 + 15: + - 21 + - 18 + 26: + - 1 + 5: + - 9 \ No newline at end of file diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_selection_having.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_selection_having.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2b78a8fb28d58b792ac411d642642c27858c1bfa --- /dev/null +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_4_selection_having.yaml @@ -0,0 +1,120 @@ +--- +nodes: + arena: + - children: #0 + - 19 + rule: Select + value: ~ + - children: #1 + - 9 + - 2 + rule: Having + value: ~ + - children: #2 + - 4 + - 3 + rule: Gt + value: ~ + - children: [] #3 + rule: Unsigned + value: "1" + - children: #4 + - 8 + - 5 + rule: Function + value: ~ + - children: #5 + - 7 + - 6 + rule: Reference + value: ~ + - children: [] #6 + rule: ColumnName + value: a + - children: [] #7 + rule: ScanName + value: t1 + - children: [] #8 + rule: FunctionName + value: sum + - children: #9 + - 12 + - 10 + rule: GroupBy + value: ~ + - children: #10 + - 11 + rule: Reference + value: ~ + - children: [] #11 + rule: ColumnName + value: a + - children: #12 + - 17 + - 13 + rule: Selection + value: ~ + - children: #13 + - 15 + - 14 + rule: Gt + value: ~ + - children: [] #14 + rule: Unsigned + value: "1" + - children: #15 + - 16 + rule: Reference + value: ~ + - children: [] #16 + rule: ColumnName + value: "\"a\"" + - children: #17 + - 18 + rule: Scan + value: ~ + - children: [] #18 + rule: Table + value: t1 + - children: #19 + - 1 + - 20 + rule: Projection + value: ~ + - children: #20 + - 26 + rule: Column + value: ~ + - children: #21 + - 24 + - 22 + rule: Function + value: ~ + - children: #22 + - 23 + rule: Reference + value: ~ + - children: [] #23 + rule: ColumnName + value: a + - children: [] #24 + rule: FunctionName + value: sum + - children: [] #25 + rule: AliasName + value: COL_1 + - children: #26 + - 21 + - 25 + rule: Alias + value: ~ +top: 19 +map: + 5: + - 9 + 15: + - 17 + 10: + - 12 + 22: + - 1 \ No newline at end of file diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_5.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_5.yaml index 9d151c877058883b2f2c584590800102f76ae3a9..3045f5f4d97c6b708eb24c992134d7c13e12e5eb 100644 --- a/sbroad-core/tests/artifactory/frontend/sql/transform_select_5.yaml +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_5.yaml @@ -2,105 +2,183 @@ nodes: arena: - children: #0 - - 21 + - 28 rule: Select value: ~ - children: #1 - - 7 + - 8 + - 5 - 2 - rule: Selection + rule: GroupBy value: ~ - children: #2 - 4 - 3 - rule: Gt + rule: Reference value: ~ - children: [] #3 - rule: Unsigned - value: "0" - - children: #4 + rule: ColumnName + value: b + - children: [] #4 + rule: ScanName + value: t2 + - children: #5 + - 7 - 6 - - 5 rule: Reference value: ~ - - children: [] #5 + - children: [] #6 rule: ColumnName value: a - - children: [] #6 + - children: [] #7 rule: ScanName value: t1 - - children: #7 - - 19 - - 18 - - 16 - - 8 - rule: Join - value: ~ - children: #8 + - 14 - 9 - rule: Condition + rule: Selection value: ~ - children: #9 - - 13 + - 11 - 10 - rule: Eq + rule: Gt value: ~ - - children: #10 + - children: [] #10 + rule: Unsigned + value: "1" + - children: #11 + - 13 - 12 - - 11 rule: Reference value: ~ - - children: [] #11 + - children: [] #12 rule: ColumnName value: a - - children: [] #12 + - children: [] #13 rule: ScanName - value: t2 - - children: #13 + value: t1 + - children: #14 + - 26 + - 25 + - 23 - 15 - - 14 + rule: Join + value: ~ + - children: #15 + - 16 + rule: Condition + value: ~ + - children: #16 + - 20 + - 17 + rule: Eq + value: ~ + - children: #17 + - 19 + - 18 + rule: Reference + value: ~ + - children: [] #18 + rule: ColumnName + value: a + - children: [] #19 + rule: ScanName + value: t2 + - children: #20 + - 22 + - 21 rule: Reference value: ~ - - children: [] #14 + - children: [] #21 rule: ColumnName value: a - - children: [] #15 + - children: [] #22 rule: ScanName value: t1 - - children: #16 - - 17 + - children: #23 + - 24 rule: Scan value: ~ - - children: [] #17 + - children: [] #24 rule: Table value: t2 - - children: [] #18 + - children: [] #25 rule: InnerJoinKind value: inner - - children: #19 - - 20 + - children: #26 + - 27 rule: Scan value: ~ - - children: [] #20 + - children: [] #27 rule: Table value: t1 - - children: #21 + - children: #28 - 1 - - 22 + - 33 + - 29 rule: Projection value: ~ - - children: [] #22 - rule: Asterisk - value: "*" -top: 21 + - children: #29 + - 40 + rule: Column + value: ~ + - children: #30 + - 32 + - 31 + rule: Reference + value: ~ + - children: [] #31 + rule: ColumnName + value: b + - children: [] #32 + rule: ScanName + value: t2 + - children: #33 + - 38 + rule: Column + value: ~ + - children: #34 + - 36 + - 35 + rule: Reference + value: ~ + - children: [] #35 + rule: ColumnName + value: a + - children: [] #36 + rule: ScanName + value: t1 + - children: [] #37 + rule: AliasName + value: a + - children: #38 + - 34 + - 37 + rule: Alias + value: ~ + - children: [] #39 + rule: AliasName + value: b + - children: #40 + - 30 + - 39 + rule: Alias + value: ~ +top: 28 map: - 13: - - 19 - - 16 - 22: + 2: + - 8 + 5: + - 8 + 34: + - 1 + 30: - 1 - 10: - - 19 - - 16 - 4: - - 7 + 20: + - 26 + - 23 + 17: + - 26 + - 23 + 11: + - 14 \ No newline at end of file diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_5_having_1.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_5_having_1.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f642eba5ae1902fcfc95a6bea1e2e8fad35b3c21 --- /dev/null +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_5_having_1.yaml @@ -0,0 +1,230 @@ +--- +nodes: + arena: + - children: #0 + - 38 + rule: Select + value: ~ + - children: #1 + - 17 + - 2 + rule: Having + value: ~ + - children: #2 + - 10 + - 3 + rule: And + value: ~ + - children: #3 + - 5 + - 4 + rule: Gt + value: ~ + - children: [] #4 + rule: Unsigned + value: "10" + - children: #5 + - 9 + - 6 + rule: Function + value: ~ + - children: #6 + - 8 + - 7 + rule: Reference + value: ~ + - children: [] #7 + rule: ColumnName + value: b + - children: [] #8 + rule: ScanName + value: t2 + - children: [] #9 + rule: FunctionName + value: avg + - children: #10 + - 12 + - 11 + rule: Gt + value: ~ + - children: [] #11 + rule: Unsigned + value: "1" + - children: #12 + - 16 + - 13 + rule: Function + value: ~ + - children: #13 + - 15 + - 14 + rule: Reference + value: ~ + - children: [] #14 + rule: ColumnName + value: a + - children: [] #15 + rule: ScanName + value: t1 + - children: [] #16 + rule: FunctionName + value: sum + - children: #17 + - 24 + - 21 + - 18 + rule: GroupBy + value: ~ + - children: #18 + - 20 + - 19 + rule: Reference + value: ~ + - children: [] #19 + rule: ColumnName + value: b + - children: [] #20 + rule: ScanName + value: t2 + - children: #21 + - 23 + - 22 + rule: Reference + value: ~ + - children: [] #22 + rule: ColumnName + value: a + - children: [] #23 + rule: ScanName + value: t1 + - children: #24 + - 36 + - 35 + - 33 + - 25 + rule: Join + value: ~ + - children: #25 + - 26 + rule: Condition + value: ~ + - children: #26 + - 30 + - 27 + rule: Eq + value: ~ + - children: #27 + - 29 + - 28 + rule: Reference + value: ~ + - children: [] #28 + rule: ColumnName + value: a + - children: [] #29 + rule: ScanName + value: t2 + - children: #30 + - 32 + - 31 + rule: Reference + value: ~ + - children: [] #31 + rule: ColumnName + value: a + - children: [] #32 + rule: ScanName + value: t1 + - children: #33 + - 34 + rule: Scan + value: ~ + - children: [] #34 + rule: Table + value: t2 + - children: [] #35 + rule: InnerJoinKind + value: inner + - children: #36 + - 37 + rule: Scan + value: ~ + - children: [] #37 + rule: Table + value: t1 + - children: #38 + - 1 + - 39 + rule: Projection + value: ~ + - children: #39 + - 51 + rule: Column + value: ~ + - children: #40 + - 46 + - 45 + - 41 + rule: Addition + value: ~ + - children: #41 + - 44 + - 42 + rule: Function + value: ~ + - children: #42 + - 43 + rule: Reference + value: ~ + - children: [] #43 + rule: ColumnName + value: b + - children: [] #44 + rule: FunctionName + value: f + - children: [] #45 + rule: Add + value: + + - children: #46 + - 49 + - 47 + rule: Function + value: ~ + - children: #47 + - 48 + rule: Reference + value: ~ + - children: [] #48 + rule: ColumnName + value: a + - children: [] #49 + rule: FunctionName + value: sum + - children: [] #50 + rule: AliasName + value: COL_1 + - children: #51 + - 40 + - 50 + rule: Alias + value: ~ +top: 38 +map: + 30: + - 36 + - 33 + 6: + - 17 + 18: + - 24 + 21: + - 24 + 47: + - 1 + 27: + - 36 + - 33 + 13: + - 17 + 42: + - 1 \ No newline at end of file diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_5_having_2.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_5_having_2.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a54df54fa14fcb80bf7adca6f50e05b8ffef3add --- /dev/null +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_5_having_2.yaml @@ -0,0 +1,190 @@ +--- +nodes: + arena: + - children: #0 + - 35 + rule: Select + value: ~ + - children: #1 + - 9 + - 2 + rule: Having + value: ~ + - children: #2 + - 4 + - 3 + rule: Gt + value: ~ + - children: [] #3 + rule: Unsigned + value: "10" + - children: #4 + - 8 + - 5 + rule: Function + value: ~ + - children: #5 + - 7 + - 6 + rule: Reference + value: ~ + - children: [] #6 + rule: ColumnName + value: b + - children: [] #7 + rule: ScanName + value: t2 + - children: [] #8 + rule: FunctionName + value: avg + - children: #9 + - 21 + - 10 + rule: Selection + value: ~ + - children: #10 + - 12 + - 11 + rule: Gt + value: ~ + - children: [] #11 + rule: Unsigned + value: "1" + - children: #12 + - 19 + - 18 + - 13 + rule: Addition + value: ~ + - children: #13 + - 16 + - 15 + - 14 + rule: Multiplication + value: ~ + - children: [] #14 + rule: Unsigned + value: "2" + - children: [] #15 + rule: Divide + value: / + - children: #16 + - 17 + rule: Reference + value: ~ + - children: [] #17 + rule: ColumnName + value: b + - children: [] #18 + rule: Add + value: + + - children: #19 + - 20 + rule: Reference + value: ~ + - children: [] #20 + rule: ColumnName + value: a + - children: #21 + - 33 + - 32 + - 30 + - 22 + rule: Join + value: ~ + - children: #22 + - 23 + rule: Condition + value: ~ + - children: #23 + - 27 + - 24 + rule: Eq + value: ~ + - children: #24 + - 26 + - 25 + rule: Reference + value: ~ + - children: [] #25 + rule: ColumnName + value: a + - children: [] #26 + rule: ScanName + value: t2 + - children: #27 + - 29 + - 28 + rule: Reference + value: ~ + - children: [] #28 + rule: ColumnName + value: a + - children: [] #29 + rule: ScanName + value: t1 + - children: #30 + - 31 + rule: Scan + value: ~ + - children: [] #31 + rule: Table + value: t2 + - children: [] #32 + rule: InnerJoinKind + value: inner + - children: #33 + - 34 + rule: Scan + value: ~ + - children: [] #34 + rule: Table + value: t1 + - children: #35 + - 1 + - 36 + rule: Projection + value: ~ + - children: #36 + - 42 + rule: Column + value: ~ + - children: #37 + - 40 + - 38 + rule: Function + value: ~ + - children: #38 + - 39 + rule: Reference + value: ~ + - children: [] #39 + rule: ColumnName + value: a + - children: [] #40 + rule: FunctionName + value: sum + - children: [] #41 + rule: AliasName + value: COL_1 + - children: #42 + - 37 + - 41 + rule: Alias + value: ~ +top: 35 +map: + 27: + - 33 + - 30 + 16: + - 21 + 38: + - 1 + 5: + - 9 + 24: + - 33 + - 30 + 19: + - 21 \ No newline at end of file diff --git a/sbroad-core/tests/artifactory/frontend/sql/transform_select_6.yaml b/sbroad-core/tests/artifactory/frontend/sql/transform_select_6.yaml index 3045f5f4d97c6b708eb24c992134d7c13e12e5eb..b55cda54cfd65c0472d5debe6f19ffd7711376ee 100644 --- a/sbroad-core/tests/artifactory/frontend/sql/transform_select_6.yaml +++ b/sbroad-core/tests/artifactory/frontend/sql/transform_select_6.yaml @@ -1,184 +1,204 @@ --- nodes: arena: - - children: #0 - - 28 + - children: + - 38 rule: Select value: ~ - - children: #1 - - 8 - - 5 + - children: + - 9 - 2 - rule: GroupBy + rule: Having value: ~ - - children: #2 + - children: - 4 - 3 + rule: Gt + value: ~ + - children: [] + rule: Unsigned + value: "10" + - children: + - 8 + - 5 + rule: Function + value: ~ + - children: + - 7 + - 6 rule: Reference value: ~ - - children: [] #3 + - children: [] rule: ColumnName value: b - - children: [] #4 + - children: [] rule: ScanName value: t2 - - children: #5 - - 7 - - 6 + - children: [] + rule: FunctionName + value: avg + - children: + - 12 + - 10 + rule: GroupBy + value: ~ + - children: + - 11 rule: Reference value: ~ - - children: [] #6 + - children: [] rule: ColumnName value: a - - children: [] #7 - rule: ScanName - value: t1 - - children: #8 - - 14 - - 9 + - children: + - 24 + - 13 rule: Selection value: ~ - - children: #9 - - 11 - - 10 + - children: + - 15 + - 14 rule: Gt value: ~ - - children: [] #10 + - children: [] rule: Unsigned value: "1" - - children: #11 - - 13 - - 12 + - children: + - 22 + - 21 + - 16 + rule: Addition + value: ~ + - children: + - 19 + - 18 + - 17 + rule: Multiplication + value: ~ + - children: [] + rule: Unsigned + value: "2" + - children: [] + rule: Divide + value: / + - children: + - 20 + rule: Reference + value: ~ + - children: [] + rule: ColumnName + value: b + - children: [] + rule: Add + value: + + - children: + - 23 rule: Reference value: ~ - - children: [] #12 + - children: [] rule: ColumnName value: a - - children: [] #13 - rule: ScanName - value: t1 - - children: #14 - - 26 + - children: + - 36 + - 35 + - 33 - 25 - - 23 - - 15 rule: Join value: ~ - - children: #15 - - 16 + - children: + - 26 rule: Condition value: ~ - - children: #16 - - 20 - - 17 + - children: + - 30 + - 27 rule: Eq value: ~ - - children: #17 - - 19 - - 18 + - children: + - 29 + - 28 rule: Reference value: ~ - - children: [] #18 + - children: [] rule: ColumnName value: a - - children: [] #19 + - children: [] rule: ScanName value: t2 - - children: #20 - - 22 - - 21 + - children: + - 32 + - 31 rule: Reference value: ~ - - children: [] #21 + - children: [] rule: ColumnName value: a - - children: [] #22 + - children: [] rule: ScanName value: t1 - - children: #23 - - 24 + - children: + - 34 rule: Scan value: ~ - - children: [] #24 + - children: [] rule: Table value: t2 - - children: [] #25 + - children: [] rule: InnerJoinKind value: inner - - children: #26 - - 27 + - children: + - 37 rule: Scan value: ~ - - children: [] #27 + - children: [] rule: Table value: t1 - - children: #28 + - children: - 1 - - 33 - - 29 + - 39 rule: Projection value: ~ - - children: #29 - - 40 + - children: + - 45 rule: Column value: ~ - - children: #30 - - 32 - - 31 - rule: Reference + - children: + - 43 + - 41 + rule: Function value: ~ - - children: [] #31 - rule: ColumnName - value: b - - children: [] #32 - rule: ScanName - value: t2 - - children: #33 - - 38 - rule: Column - value: ~ - - children: #34 - - 36 - - 35 + - children: + - 42 rule: Reference value: ~ - - children: [] #35 + - children: [] rule: ColumnName value: a - - children: [] #36 - rule: ScanName - value: t1 - - children: [] #37 - rule: AliasName - value: a - - children: #38 - - 34 - - 37 - rule: Alias - value: ~ - - children: [] #39 + - children: [] + rule: FunctionName + value: sum + - children: [] rule: AliasName - value: b - - children: #40 - - 30 - - 39 + value: COL_1 + - children: + - 40 + - 44 rule: Alias value: ~ -top: 28 +top: 38 map: - 2: - - 8 + 19: + - 24 5: - - 8 - 34: - - 1 + - 9 + 10: + - 12 30: - - 1 - 20: - - 26 - - 23 - 17: - - 26 - - 23 - 11: - - 14 \ No newline at end of file + - 36 + - 33 + 27: + - 36 + - 33 + 22: + - 24 + 41: + - 1 \ No newline at end of file