From 3826237375796f8ae91bf759f78f584512078d71 Mon Sep 17 00:00:00 2001
From: "ms.evilhat" <ms.evilhat@gmail.com>
Date: Mon, 6 Feb 2023 15:27:03 +0300
Subject: [PATCH] feat: add arithmetic expressions to projection

we need to support arbitrary expressions consisting of logical, comparison and arithmetic operations and as sub-expression of aggregates. previously we added arithmetic exprs to selection ans join. this commit supports arithmetic (and only arithmetic) as a part of projection
---
 .../test/integration/arithmetic_test.lua      | 594 +++++++++++++++---
 sbroad-core/src/backend/sql/tree/tests.rs     | 179 +++++-
 sbroad-core/src/frontend/sql.rs               | 153 +++--
 sbroad-core/src/frontend/sql/ast/tests.rs     |  81 ++-
 sbroad-core/src/frontend/sql/query.pest       |   2 +-
 sbroad-core/src/ir/expression.rs              |  38 +-
 .../sql/tree/arithmetic_projection_plan.yaml  | 259 ++++++++
 ...an.yaml => arithmetic_selection_plan.yaml} |   0
 .../sql/arithmetic_projection_ast.yaml        |  56 ++
 ...ast.yaml => arithmetic_selection_ast.yaml} |   0
 10 files changed, 1192 insertions(+), 170 deletions(-)
 create mode 100644 sbroad-core/tests/artifactory/backend/sql/tree/arithmetic_projection_plan.yaml
 rename sbroad-core/tests/artifactory/backend/sql/tree/{arithmetic_plan.yaml => arithmetic_selection_plan.yaml} (100%)
 create mode 100644 sbroad-core/tests/artifactory/frontend/sql/arithmetic_projection_ast.yaml
 rename sbroad-core/tests/artifactory/frontend/sql/{arithmetic_ast.yaml => arithmetic_selection_ast.yaml} (100%)

diff --git a/sbroad-cartridge/test_app/test/integration/arithmetic_test.lua b/sbroad-cartridge/test_app/test/integration/arithmetic_test.lua
index 0d127d0f50..c4ab8f7c1e 100644
--- a/sbroad-cartridge/test_app/test/integration/arithmetic_test.lua
+++ b/sbroad-cartridge/test_app/test/integration/arithmetic_test.lua
@@ -1,6 +1,7 @@
 local t = require('luatest')
-local g = t.group('arithmetic')
-local g1 = t.group('arithmetic.propetries')
+local g = t.group('arithmetic.selection')
+local g1 = t.group('arithmetic.join')
+local g2 = t.group('arithmetic.projection')
 local decimal = require("decimal")
 local helper = require('test.helper.cluster_no_replication')
 
@@ -66,14 +67,25 @@ g1.before_each(
     function()
         local api = cluster:server("api-1").net_box
 
-        for k = 1,10 do
+        for i = 1,10 do
             local r, err = api:call("sbroad.execute", {
                 [[
                     insert into "arithmetic_space"
                     ("id", "a", "b", "c", "d", "e", "f", "boolean_col", "string_col", "number_col")
                     values (?,?,?,?,?,?,?,?,?,?)
                 ]],
-                {k, k, k*2, k*3, k, k, k, true, "123", decimal.new('4.6')},
+                {i, i, i*2, i*3, i, i, i, true, "123", decimal.new('4.6')},
+            })
+            t.assert_equals(err, nil)
+            t.assert_equals(r, {row_count = 1})
+
+            r, err = api:call("sbroad.execute", {
+                [[
+                    insert into "arithmetic_space2"
+                    ("id", "a", "b", "c", "d", "e", "f", "boolean_col", "string_col", "number_col")
+                    values (?,?,?,?,?,?,?,?,?,?)
+                ]],
+                {i, i, i, i, i, i, i, false, "123", decimal.new('4.599999')},
             })
             t.assert_equals(err, nil)
             t.assert_equals(r, {row_count = 1})
@@ -85,9 +97,11 @@ g1.after_each(
     function()
         local storage1 = cluster:server("storage-1-1").net_box
         storage1:call("box.execute", { [[truncate table "arithmetic_space"]] })
+        storage1:call("box.execute", { [[truncate table "arithmetic_space2"]] })
 
         local storage2 = cluster:server("storage-2-1").net_box
         storage2:call("box.execute", { [[truncate table "arithmetic_space"]] })
+        storage2:call("box.execute", { [[truncate table "arithmetic_space2"]] })
     end
 )
 
@@ -95,6 +109,57 @@ g1.after_all(function()
     helper.stop_test_cluster()
 end)
 
+g2.before_all(function()
+    helper.start_test_cluster(helper.cluster_config)
+    cluster = helper.cluster
+end)
+
+g2.before_each(
+    function()
+        local api = cluster:server("api-1").net_box
+
+        for i = 1, 10 do
+            local r, err = api:call("sbroad.execute", {
+                [[
+                    insert into "arithmetic_space"
+                    ("id", "a", "b", "c", "d", "e", "f", "boolean_col", "string_col", "number_col")
+                    values (?,?,?,?,?,?,?,?,?,?)
+                ]],
+                {i, i, i*2, i*3, i, i, i, true, "123", decimal.new('4.6')},
+            })
+            t.assert_equals(err, nil)
+            t.assert_equals(r, {row_count = 1})
+
+            r, err = api:call("sbroad.execute", {
+                [[
+                    insert into "arithmetic_space2"
+                    ("id", "a", "b", "c", "d", "e", "f", "boolean_col", "string_col", "number_col")
+                    values (?,?,?,?,?,?,?,?,?,?)
+                ]],
+                {i, i, i, i, i, i, i, false, "123", decimal.new('4.599999')},
+            })
+            t.assert_equals(err, nil)
+            t.assert_equals(r, {row_count = 1})
+        end
+    end
+)
+
+g2.after_each(
+    function()
+        local storage1 = cluster:server("storage-1-1").net_box
+        storage1:call("box.execute", { [[truncate table "arithmetic_space"]] })
+        storage1:call("box.execute", { [[truncate table "arithmetic_space2"]] })
+
+        local storage2 = cluster:server("storage-2-1").net_box
+        storage2:call("box.execute", { [[truncate table "arithmetic_space"]] })
+        storage2:call("box.execute", { [[truncate table "arithmetic_space2"]] })
+    end
+)
+
+g2.after_all(function()
+    helper.stop_test_cluster()
+end)
+
 g.test_arithmetic_invalid = function()
     local api = cluster:server("api-1").net_box
 
@@ -150,6 +215,61 @@ g.test_arithmetic_invalid = function()
     )
 end
 
+g2.test_arithmetic_invalid = function()
+    local api = cluster:server("api-1").net_box
+
+    local _, err = api:call("sbroad.execute", { [[select "id" % 2 from "arithmetic_space"]], {} })
+    t.assert_str_contains(tostring(err), "rule parsing error")
+
+    local _, err = api:call("sbroad.execute", { [[select "id" ^ 2 from "arithmetic_space"]], {} })
+    t.assert_str_contains(tostring(err), "rule parsing error")
+
+    local _, err = api:call("sbroad.execute", { [[select "id" ++ 2 from "arithmetic_space"]], {} })
+    t.assert_str_contains(tostring(err), "rule parsing error")
+
+    local _, err = api:call("sbroad.execute", { [[select "id" ** 2 from "arithmetic_space"]], {} })
+    t.assert_str_contains(tostring(err), "rule parsing error")
+
+    local _, err = api:call("sbroad.execute", { [[select "id" // 2 from "arithmetic_space"]], {} })
+    t.assert_str_contains(tostring(err), "rule parsing error")
+
+    local _, err = api:call("sbroad.execute", { [[select "id" ** 2 from "arithmetic_space"]], {} })
+    t.assert_str_contains(tostring(err), "rule parsing error")
+
+    local _, err = api:call("sbroad.execute", { [[select "id" +- 2 from "arithmetic_space"]], {} })
+    t.assert_str_contains(tostring(err), "rule parsing error")
+
+    local _, err = api:call("sbroad.execute", { [[select "id" +* 2 from "arithmetic_space"]], {} })
+    t.assert_str_contains(tostring(err), "rule parsing error")
+
+    -- arithemic operation on boolean col
+    local _, err = api:call("sbroad.execute",
+        { [[select "boolean_col" + "boolean_col" from "arithmetic_space"]], {} }
+    )
+    t.assert_str_contains(
+        tostring(err),
+        "Type mismatch: can not convert boolean(TRUE) to integer, decimal, double, datetime or interval"
+    )
+
+    -- arithemic operation on string col
+    local _, err = api:call("sbroad.execute",
+        { [[select "string_col" + "string_col" from "arithmetic_space"]], {} }
+    )
+    t.assert_str_contains(
+        tostring(err),
+        "Type mismatch: can not convert string('123') to integer, decimal, double, datetime or interval"
+    )
+
+    -- arithemic operation on number col
+    local _, err = api:call("sbroad.execute",
+        { [[select "number_col" + "number_col" from "arithmetic_space"]], {} }
+    )
+    t.assert_str_contains(
+        tostring(err),
+        "Type mismatch: can not convert number(4.6) to integer, decimal, double, datetime or interval"
+    )
+end
+
 g.test_arithmetic_valid = function()
     local api = cluster:server("api-1").net_box
 
@@ -315,83 +435,6 @@ g.test_arithmetic_with_bool = function()
     )
 end
 
-g.test_join_simple_arithmetic = function()
-    local api = cluster:server("api-1").net_box
-
-    local r, err = api:call("sbroad.execute",
-    { [[
-    SELECT "t3"."id", "t3"."a", "t8"."b"
-    FROM
-        (SELECT "id", "a"
-            FROM "arithmetic_space"
-            WHERE "c" < 0
-        UNION ALL
-            SELECT "id", "a"
-            FROM "arithmetic_space"
-            WHERE "c" > 0) AS "t3"
-    INNER JOIN
-        (SELECT "id" as "id1", "b"
-            FROM "arithmetic_space2"
-            WHERE "b" < 0
-        UNION ALL
-        SELECT "id" as "id1", "b"
-            FROM "arithmetic_space2"
-            WHERE "b" > 0) AS "t8"
-    ON "t3"."id" + "t3"."a" * 2 = "t8"."id1" + "t8"."b"
-    WHERE "t3"."id" = 2]], { } })
-
-    t.assert_equals(err, nil)
-    t.assert_equals(
-        r.metadata,
-        {
-            {name = "t3.id", type = "integer"},
-            {name = "t3.a", type = "integer"},
-            {name = "t8.b", type = "integer"},
-        }
-    )
-    t.assert_equals(
-        r.rows,
-        { { 2, 2, 3 } }
-    )
-
-    -- check the same query with params
-    local r2, err = api:call("sbroad.execute",
-    { [[
-    SELECT "t3"."id", "t3"."a", "t8"."b"
-    FROM
-        (SELECT "id", "a"
-            FROM "arithmetic_space"
-            WHERE "c" < ?
-        UNION ALL
-            SELECT "id", "a"
-            FROM "arithmetic_space"
-            WHERE "c" > ?) AS "t3"
-    INNER JOIN
-        (SELECT "id" as "id1", "b"
-            FROM "arithmetic_space2"
-            WHERE "b" < ?
-        UNION ALL
-        SELECT "id" as "id1", "b"
-            FROM "arithmetic_space2"
-            WHERE "b" > ?) AS "t8"
-    ON "t3"."id" + "t3"."a" * ? = "t8"."id1" + "t8"."b"
-    WHERE "t3"."id" = ?]], { 0, 0, 0, 0, 2, 2} })
-
-    t.assert_equals(err, nil)
-    t.assert_equals(
-        r.metadata,
-        {
-            {name = "t3.id", type = "integer"},
-            {name = "t3.a", type = "integer"},
-            {name = "t8.b", type = "integer"},
-        }
-    )
-    t.assert_equals(
-        r.rows,
-        r2.rows
-    )
-end
-
 g.test_selection_simple_arithmetic = function()
     local api = cluster:server("api-1").net_box
 
@@ -441,7 +484,7 @@ g.test_selection_simple_arithmetic = function()
     )
 end
 
-g1.test_associativity = function()
+g.test_associativity = function()
     local api = cluster:server("api-1").net_box
 
     local res_all, err = api:call("sbroad.execute", { [[select "id" from "arithmetic_space"]], {} })
@@ -492,7 +535,7 @@ g1.test_associativity = function()
     t.assert_not_equals(res.rows, res_all.rows)
 end
 
-g1.test_commutativity = function()
+g.test_commutativity = function()
     local api = cluster:server("api-1").net_box
 
     local res_all, err = api:call("sbroad.execute", { [[select "id" from "arithmetic_space"]], {} })
@@ -534,7 +577,7 @@ g1.test_commutativity = function()
     t.assert_equals(res.rows, {})
 end
 
-g1.test_distributivity = function()
+g.test_distributivity = function()
     local api = cluster:server("api-1").net_box
 
     local res_all, err = api:call("sbroad.execute", { [[select "id" from "arithmetic_space"]], {} })
@@ -572,4 +615,387 @@ g1.test_distributivity = function()
     ]], {} })
     t.assert_equals(err, nil)
     t.assert_not_equals(res.rows, res_all.rows)
+end
+
+g1.test_join_simple_arithmetic = function()
+    local api = cluster:server("api-1").net_box
+
+    local r, err = api:call("sbroad.execute",
+    { [[
+    SELECT "t3"."id", "t3"."a", "t8"."b"
+    FROM
+        (SELECT "id", "a"
+            FROM "arithmetic_space"
+            WHERE "c" < 0
+        UNION ALL
+            SELECT "id", "a"
+            FROM "arithmetic_space"
+            WHERE "c" > 0) AS "t3"
+    INNER JOIN
+        (SELECT "id" as "id1", "b"
+            FROM "arithmetic_space2"
+            WHERE "b" < 0
+        UNION ALL
+        SELECT "id" as "id1", "b"
+            FROM "arithmetic_space2"
+            WHERE "b" > 0) AS "t8"
+    ON "t3"."id" + "t3"."a" * 2 = "t8"."id1" + "t8"."b"
+    WHERE "t3"."id" = 2]], { } })
+
+    t.assert_equals(err, nil)
+    t.assert_equals(
+        r.metadata,
+        {
+            {name = "t3.id", type = "integer"},
+            {name = "t3.a", type = "integer"},
+            {name = "t8.b", type = "integer"},
+        }
+    )
+    t.assert_equals(
+        r.rows,
+        { { 2, 2, 3 } }
+    )
+
+    -- check the same query with params
+    local r2, err = api:call("sbroad.execute",
+    { [[
+    SELECT "t3"."id", "t3"."a", "t8"."b"
+    FROM
+        (SELECT "id", "a"
+            FROM "arithmetic_space"
+            WHERE "c" < ?
+        UNION ALL
+            SELECT "id", "a"
+            FROM "arithmetic_space"
+            WHERE "c" > ?) AS "t3"
+    INNER JOIN
+        (SELECT "id" as "id1", "b"
+            FROM "arithmetic_space2"
+            WHERE "b" < ?
+        UNION ALL
+        SELECT "id" as "id1", "b"
+            FROM "arithmetic_space2"
+            WHERE "b" > ?) AS "t8"
+    ON "t3"."id" + "t3"."a" * ? = "t8"."id1" + "t8"."b"
+    WHERE "t3"."id" = ?]], { 0, 0, 0, 0, 2, 2} })
+
+    t.assert_equals(err, nil)
+    t.assert_equals(
+        r.metadata,
+        {
+            {name = "t3.id", type = "integer"},
+            {name = "t3.a", type = "integer"},
+            {name = "t8.b", type = "integer"},
+        }
+    )
+    t.assert_equals(
+        r.rows,
+        r2.rows
+    )
+end
+
+g1.test_projection_selection_join = function()
+    local api = cluster:server("api-1").net_box
+
+    local r, err = api:call("sbroad.execute",
+    { [[
+    SELECT "t3"."id", "t3"."a", "t8"."b", "t3"."id" + "t3"."a" + "t8"."b" as "sum"
+    FROM
+        (SELECT "id", "a"
+            FROM "arithmetic_space"
+            WHERE "c" < 0
+        UNION ALL
+            SELECT "id", "a"
+            FROM "arithmetic_space"
+            WHERE "c" > 0) AS "t3"
+    INNER JOIN
+        (SELECT "id" as "id1", "b"
+            FROM "arithmetic_space2"
+            WHERE "b" < 0
+        UNION ALL
+        SELECT "id" as "id1", "b"
+            FROM "arithmetic_space2"
+            WHERE "b" > 0) AS "t8"
+    ON "t3"."id" + "t3"."a" * 2 = "t8"."id1" + "t8"."b"
+    ]], { } })
+
+    t.assert_equals(err, nil)
+    t.assert_equals(
+        r.metadata,
+        {
+            {name = "t3.id", type = "integer"},
+            {name = "t3.a", type = "integer"},
+            {name = "t8.b", type = "integer"},
+            {name = "sum", type = "integer"},
+
+        }
+    )
+    for i=1,table.getn(r.rows) do
+        t.assert_equals(
+            r.rows[i][1] + r.rows[i][2] + r.rows[i][3],
+            r.rows[i][4]
+        )
+    end
+
+    local r, err = api:call("sbroad.execute",
+    { [[
+    SELECT "t3"."id", "t3"."a", "t8"."b", "t3"."id" * "t3"."a" * "t8"."b" + 1 as "mul"
+    FROM
+        (SELECT "id", "a"
+            FROM "arithmetic_space"
+            WHERE "c" < 0
+        UNION ALL
+            SELECT "id", "a"
+            FROM "arithmetic_space"
+            WHERE "c" > 0) AS "t3"
+    INNER JOIN
+        (SELECT "id" as "id1", "b"
+            FROM "arithmetic_space2"
+            WHERE "b" < 0
+        UNION ALL
+        SELECT "id" as "id1", "b"
+            FROM "arithmetic_space2"
+            WHERE "b" > 0) AS "t8"
+    ON "t3"."id" + "t3"."a" * 2 = "t8"."id1" + "t8"."b"
+    ]], { } })
+
+    t.assert_equals(err, nil)
+    t.assert_equals(
+        r.metadata,
+        {
+            {name = "t3.id", type = "integer"},
+            {name = "t3.a", type = "integer"},
+            {name = "t8.b", type = "integer"},
+            {name = "mul", type = "integer"},
+
+        }
+    )
+    for i=1,table.getn(r.rows) do
+        t.assert_equals(
+            r.rows[i][1] * r.rows[i][2] * r.rows[i][3] + 1,
+            r.rows[i][4]
+        )
+    end
+end
+
+g2.test_alias = function()
+    local api = cluster:server("api-1").net_box
+
+    local res, err = api:call("sbroad.execute", {
+        [[select "id", "id" + "a", "id" * "a" , "a" from "arithmetic_space"]], {}
+    })
+    t.assert_equals(err, nil)
+    t.assert_equals(res.metadata, {
+        {name = "id", type = "integer"},
+        {name = "COLUMN_1", type = "integer"},
+        {name = "COLUMN_2", type = "integer"},
+        {name = "a", type = "integer"},
+    })
+
+    local res, err = api:call("sbroad.execute", { [[
+        select "id", "id" + "a" as "sum", "id" * "a" as "mul", "a" from "arithmetic_space"
+        ]], {}})
+    t.assert_equals(err, nil)
+    t.assert_equals(res.metadata, {
+        {name = "id", type = "integer"},
+        {name = "sum", type = "integer"},
+        {name = "mul", type = "integer"},
+        {name = "a", type = "integer"},
+    })
+end
+
+g2.test_associativity = function()
+    local api = cluster:server("api-1").net_box
+
+    -- addition is associative
+    local res_add1, err = api:call("sbroad.execute", { [[
+        select "id", "a" + ("b" + "c") from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_add1.rows, {})
+
+    local res_add2, err = api:call("sbroad.execute", { [[
+        select "id", ("a" + "b") + "c" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_equals(res_add2.rows, res_add1.rows)
+
+    -- multiplication is associative
+    local res_mul1, err = api:call("sbroad.execute", { [[
+        select "id", "a" * ("b" * "c") from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_mul1.rows, {})
+
+    local res_mul2, err = api:call("sbroad.execute", { [[
+        select "id", ("a" * "b") * "c" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_equals(res_mul2.rows, res_mul1.rows)
+
+    -- subtraction is left-associative
+    local res_sub, err = api:call("sbroad.execute", { [[
+        select "id", "a" - "b" - "c" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_sub.rows, {})
+
+    local res_sub1, err = api:call("sbroad.execute", { [[
+        select "id", ("a" - "b") - "c" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_equals(res_sub1.rows, res_sub.rows)
+
+    local res_sub2, err = api:call("sbroad.execute", { [[
+        select "id", "a" - ("b" - "c" ) from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_sub2.rows, res_sub.rows)
+
+    -- division is left-associative
+    local res_div, err = api:call("sbroad.execute", { [[
+        select "id",
+            cast("a" as decimal) / cast("b" as decimal) / cast("c" as decimal) from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_div.rows, {})
+
+    local res_div1, err = api:call("sbroad.execute", { [[
+        select "id", (cast("a" as decimal) / cast("b" as decimal)) / cast("c" as decimal) from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_equals(res_div1.rows, res_div.rows)
+
+    local res_div2, err = api:call("sbroad.execute", { [[
+        select "id", cast("a" as decimal) / (cast("b" as decimal) / cast("c" as decimal)) from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_div2.rows, res_div.rows)
+end
+
+g2.test_commutativity = function()
+    local api = cluster:server("api-1").net_box
+
+    -- addition is commutative
+    local res_add1, err = api:call("sbroad.execute", { [[
+        select "id", "a" + "b" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_add1.rows, {})
+
+    local res_add2, err = api:call("sbroad.execute", { [[
+        select "id", "b" + "a" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_equals(res_add2.rows, res_add1.rows)
+
+    -- multiplication is commutative
+    local res_mul1, err = api:call("sbroad.execute", { [[
+        select "id", "a" * "b" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_mul1.rows, {})
+
+    local res_mul2, err = api:call("sbroad.execute", { [[
+        select "id", "b" * "a" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_equals(res_mul2.rows, res_mul1.rows)
+
+    -- subtraction is not commutative
+    -- and x [-|/] y = y [-|/] x is true only when
+    -- x, y have specific condition
+    local res_sub1, err = api:call("sbroad.execute", { [[
+        select "id", "a" - "b" from "arithmetic_space" where "a" != "b"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_sub1.rows, {})
+
+    local res_sub2, err = api:call("sbroad.execute", { [[
+        select "id", "b" - "a" from "arithmetic_space" where "a" != "b"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_sub2.rows, {})
+    t.assert_not_equals(res_sub2.rows, res_sub1.rows)
+
+    -- division is not commutative
+    -- and x [-|/] y = y [-|/] x is true only when
+    -- x, y have specific condition
+    local res_div1, err = api:call("sbroad.execute", { [[
+        select "id", cast("b" as decimal) / cast("a" as decimal) from "arithmetic_space"
+            where "a" != "b" or "a" != -1 * "b"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_div1.rows, {})
+
+    local res_div2, err = api:call("sbroad.execute", { [[
+        select "id", cast("a" as decimal) / cast("b" as decimal) from "arithmetic_space"
+            where "a" != "b" or "a" != -1 * "b"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_div2.rows, {})
+    t.assert_not_equals(res_div2.rows, res_div1.rows)
+end
+
+g2.test_distributivity = function()
+    local api = cluster:server("api-1").net_box
+
+    -- multiplication if left- and right-distributive over addition|subtraction
+    local res_mul, err = api:call("sbroad.execute", { [[
+        select "id", "a" * "b" + "a" * "c" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_mul.rows, {})
+
+    local res_mul1, err = api:call("sbroad.execute", { [[
+        select "id", "a" * ("b" + "c") from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_mul.rows, {})
+    t.assert_equals(res_mul.rows, res_mul1.rows)
+
+    local res_mul2, err = api:call("sbroad.execute", { [[
+        select "id", ("b" + "c") * "a" from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_mul2.rows, {})
+    t.assert_equals(res_mul.rows, res_mul2.rows)
+
+    -- division is right-distributive over addition|subtraction
+    local res_div, err = api:call("sbroad.execute", { [[
+        select
+            "id",
+            cast("a" as decimal) / cast("c" as decimal) + cast("b" as decimal) / cast("c" as decimal)
+        from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_div.rows, {})
+
+    local res_div1, err = api:call("sbroad.execute", { [[
+        select
+            "id",
+            (cast("a" as decimal) + cast("b" as decimal)) / cast("c" as decimal)
+        from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_div1.rows, {})
+    t.assert_equals(res_div.rows, res_div1.rows)
+
+    local res_div, err = api:call("sbroad.execute", { [[
+        select
+            "id",
+            cast("a" as decimal) / cast("b" as decimal) + cast("a" as decimal) / cast("c" as decimal)
+        from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_div.rows, {})
+
+    local res_div2, err = api:call("sbroad.execute", { [[
+        select
+            "id",
+            cast("a" as decimal) / (cast("b" as decimal) + cast("c" as decimal))
+        from "arithmetic_space"
+    ]], {} })
+    t.assert_equals(err, nil)
+    t.assert_not_equals(res_div.rows, res_div2.rows)
 end
\ No newline at end of file
diff --git a/sbroad-core/src/backend/sql/tree/tests.rs b/sbroad-core/src/backend/sql/tree/tests.rs
index 7be7d9ca16..44588c37ad 100644
--- a/sbroad-core/src/backend/sql/tree/tests.rs
+++ b/sbroad-core/src/backend/sql/tree/tests.rs
@@ -92,7 +92,7 @@ fn sql_order_selection() {
 
 #[test]
 #[allow(clippy::too_many_lines)]
-fn sql_arithmetic_plan() {
+fn sql_arithmetic_selection_plan() {
     // select a from t where a + (b/c + d*e) * f - b = 1
     let mut plan = Plan::default();
     let t = Table::new_seg(
@@ -163,7 +163,7 @@ fn sql_arithmetic_plan() {
         .join("backend")
         .join("sql")
         .join("tree")
-        .join("arithmetic_plan.yaml");
+        .join("arithmetic_selection_plan.yaml");
     let s = fs::read_to_string(path).unwrap();
     let expected_plan = Plan::from_yaml(&s).unwrap();
     assert_eq!(expected_plan, plan);
@@ -248,7 +248,7 @@ fn sql_arithmetic_plan() {
     assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
     // )
     assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
-    // arithmetic expression multiply: [(b/c + d*e)] * f
+    // arithmetic expression multiply: [(b/c + d*e)] * [f]
     assert_eq!(Some(&SyntaxData::PlanId(33)), nodes_iter.next());
     // arithmetic operator multiply (*)
     assert_eq!(Some(&SyntaxData::Operator("*".into())), nodes_iter.next());
@@ -286,3 +286,176 @@ fn sql_arithmetic_plan() {
     assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
     assert_eq!(None, nodes_iter.next());
 }
+
+#[test]
+fn sql_arithmetic_projection_plan() {
+    // select a + (b/c + d*e) * f - b from t
+    let mut plan = Plan::default();
+    let t = Table::new_seg(
+        "t",
+        vec![
+            Column::new("a", Type::Integer, ColumnRole::User),
+            Column::new("b", Type::Integer, ColumnRole::User),
+            Column::new("c", Type::Integer, ColumnRole::User),
+            Column::new("d", Type::Integer, ColumnRole::User),
+            Column::new("e", Type::Integer, ColumnRole::User),
+            Column::new("f", Type::Integer, ColumnRole::User),
+            Column::new("bucket_id", Type::Unsigned, ColumnRole::Sharding),
+        ],
+        &["a"],
+        SpaceEngine::Memtx,
+    )
+    .unwrap();
+    plan.add_rel(t);
+    let scan_id = plan.add_scan("t", None).unwrap();
+    let a_id = plan.add_row_from_child(scan_id, &["a"]).unwrap();
+    let b_id = plan.add_row_from_child(scan_id, &["b"]).unwrap();
+    let c_id = plan.add_row_from_child(scan_id, &["c"]).unwrap();
+    let d_id = plan.add_row_from_child(scan_id, &["d"]).unwrap();
+    let e_id = plan.add_row_from_child(scan_id, &["e"]).unwrap();
+    let f_id = plan.add_row_from_child(scan_id, &["f"]).unwrap();
+
+    // b/c
+    let arith_divide_id = plan
+        .add_arithmetic_to_plan(b_id, Arithmetic::Divide, c_id, false)
+        .unwrap();
+    // d*e
+    let arith_multiply_id = plan
+        .add_arithmetic_to_plan(d_id, Arithmetic::Multiply, e_id, false)
+        .unwrap();
+    // (b/c + d*e)
+    let arith_addition_id = plan
+        .add_arithmetic_to_plan(arith_divide_id, Arithmetic::Add, arith_multiply_id, true)
+        .unwrap();
+    // (b/c + d*e) * f
+    let arith_multiply_id2 = plan
+        .add_arithmetic_to_plan(arith_addition_id, Arithmetic::Multiply, f_id, false)
+        .unwrap();
+    // a + (b/c + d*e) * f
+    let arith_addition_id2 = plan
+        .add_arithmetic_to_plan(a_id, Arithmetic::Add, arith_multiply_id2, false)
+        .unwrap();
+    // a + (b/c + d*e) * f - b
+    let arith_subract_id = plan
+        .add_arithmetic_to_plan(arith_addition_id2, Arithmetic::Subtract, b_id, false)
+        .unwrap();
+
+    let proj_id = plan
+        .add_proj_internal(scan_id, &[arith_subract_id])
+        .unwrap();
+    plan.set_top(proj_id).unwrap();
+
+    // check the plan
+    let path = Path::new("")
+        .join("tests")
+        .join("artifactory")
+        .join("backend")
+        .join("sql")
+        .join("tree")
+        .join("arithmetic_projection_plan.yaml");
+    let s = fs::read_to_string(path).unwrap();
+    let expected_plan = Plan::from_yaml(&s).unwrap();
+    assert_eq!(expected_plan, plan);
+
+    let exec_plan = ExecutionPlan::from(plan.clone());
+    let top_id = exec_plan.get_ir_plan().get_top().unwrap();
+
+    // get nodes in the sql-convenient order
+    let sp = SyntaxPlan::new(&exec_plan, top_id, Snapshot::Latest).unwrap();
+    let ordered = OrderedSyntaxNodes::try_from(sp).unwrap();
+    let nodes = ordered.to_syntax_data().unwrap();
+    let mut nodes_iter = nodes.into_iter();
+
+    // projection
+    assert_eq!(Some(&SyntaxData::PlanId(35)), nodes_iter.next());
+    // row
+    assert_eq!(Some(&SyntaxData::PlanId(17)), nodes_iter.next());
+    // (
+    assert_eq!(Some(&SyntaxData::OpenParenthesis), nodes_iter.next());
+    // ref a
+    assert_eq!(Some(&SyntaxData::PlanId(16)), nodes_iter.next());
+    // )
+    assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
+    // arithmetic expression: [a] + [(b/c + d*e)]
+    assert_eq!(Some(&SyntaxData::PlanId(32)), nodes_iter.next());
+    // arithmetic operator add (+)
+    assert_eq!(Some(&SyntaxData::Operator("+".into())), nodes_iter.next());
+    // (
+    assert_eq!(Some(&SyntaxData::OpenParenthesis), nodes_iter.next());
+    // arithmetic expression add: ([b/c] + [d*e])
+    assert_eq!(Some(&SyntaxData::PlanId(30)), nodes_iter.next());
+    // row
+    assert_eq!(Some(&SyntaxData::PlanId(19)), nodes_iter.next());
+    // (
+    assert_eq!(Some(&SyntaxData::OpenParenthesis), nodes_iter.next());
+    // ref b
+    assert_eq!(Some(&SyntaxData::PlanId(18)), nodes_iter.next());
+    // )
+    assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
+    // arithmetic expression divide: [b] / [c]
+    assert_eq!(Some(&SyntaxData::PlanId(28)), nodes_iter.next());
+    // arithmetic operator divide (/)
+    assert_eq!(Some(&SyntaxData::Operator("/".into())), nodes_iter.next());
+    // row
+    assert_eq!(Some(&SyntaxData::PlanId(21)), nodes_iter.next());
+    // (
+    assert_eq!(Some(&SyntaxData::OpenParenthesis), nodes_iter.next());
+    // ref c
+    assert_eq!(Some(&SyntaxData::PlanId(20)), nodes_iter.next());
+    // )
+    assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
+    // arithmetic operator add (+)
+    assert_eq!(Some(&SyntaxData::Operator("+".into())), nodes_iter.next());
+    // row
+    assert_eq!(Some(&SyntaxData::PlanId(23)), nodes_iter.next());
+    // (
+    assert_eq!(Some(&SyntaxData::OpenParenthesis), nodes_iter.next());
+    // ref d
+    assert_eq!(Some(&SyntaxData::PlanId(22)), nodes_iter.next());
+    // )
+    assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
+    // arithmetic expression multiply: [d] * [e]
+    assert_eq!(Some(&SyntaxData::PlanId(29)), nodes_iter.next());
+    // arithmetic operator multiply (*)
+    assert_eq!(Some(&SyntaxData::Operator("*".into())), nodes_iter.next());
+    // row
+    assert_eq!(Some(&SyntaxData::PlanId(25)), nodes_iter.next());
+    // (
+    assert_eq!(Some(&SyntaxData::OpenParenthesis), nodes_iter.next());
+    // ref e
+    assert_eq!(Some(&SyntaxData::PlanId(24)), nodes_iter.next());
+    // )
+    assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
+    // )
+    assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
+    // arithmetic expression multiply: [(b/c + d*e)] * f
+    assert_eq!(Some(&SyntaxData::PlanId(31)), nodes_iter.next());
+    // arithmetic operator multiply (*)
+    assert_eq!(Some(&SyntaxData::Operator("*".into())), nodes_iter.next());
+    // row
+    assert_eq!(Some(&SyntaxData::PlanId(27)), nodes_iter.next());
+    // (
+    assert_eq!(Some(&SyntaxData::OpenParenthesis), nodes_iter.next());
+    // f
+    assert_eq!(Some(&SyntaxData::PlanId(26)), nodes_iter.next());
+    // )
+    assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
+    // arithmetic expression subtract: [a + (b/c + d*e) * f] - [b]
+    assert_eq!(Some(&SyntaxData::PlanId(33)), nodes_iter.next());
+    // arithmetic operator subtract (-)
+    assert_eq!(Some(&SyntaxData::Operator("-".into())), nodes_iter.next());
+    // row
+    assert_eq!(Some(&SyntaxData::PlanId(19)), nodes_iter.next());
+    // (
+    assert_eq!(Some(&SyntaxData::OpenParenthesis), nodes_iter.next());
+    // ref b
+    assert_eq!(Some(&SyntaxData::PlanId(18)), nodes_iter.next());
+    // )
+    assert_eq!(Some(&SyntaxData::CloseParenthesis), nodes_iter.next());
+    // from
+    assert_eq!(Some(&SyntaxData::From), nodes_iter.next());
+    // scan
+    assert_eq!(Some(&SyntaxData::PlanId(15)), nodes_iter.next());
+
+    assert_eq!(None, nodes_iter.next());
+}
diff --git a/sbroad-core/src/frontend/sql.rs b/sbroad-core/src/frontend/sql.rs
index 3e20382b69..f94346189e 100644
--- a/sbroad-core/src/frontend/sql.rs
+++ b/sbroad-core/src/frontend/sql.rs
@@ -131,6 +131,81 @@ impl Ast for AbstractSyntaxTree {
         let mut betweens: Vec<Between> = Vec::new();
         let mut arithmetic_expression_ids: Vec<usize> = Vec::new();
 
+        let get_arithmetic_plan_id = |plan: &mut Plan,
+                                      map: &Translation,
+                                      arithmetic_expression_ids: &mut Vec<usize>,
+                                      rows: &mut HashSet<usize>,
+                                      ast_id: usize| {
+            let plan_id;
+            // if child of current multiplication or addition is `(expr)` then
+            // we need to get expr that is child of `()` and add it to the plan
+            // also we will mark this expr to add in the future `()`
+            let arithmetic_parse_node = self.nodes.get_node(ast_id)?;
+            if arithmetic_parse_node.rule == Type::ArithParentheses {
+                let arithmetic_id = arithmetic_parse_node.children.first().ok_or_else(|| {
+                    SbroadError::UnexpectedNumberOfValues(
+                        "ArithParentheses has no children.".into(),
+                    )
+                })?;
+                plan_id = plan.as_row(map.get(*arithmetic_id)?, rows)?;
+                arithmetic_expression_ids.push(plan_id);
+            } else {
+                plan_id = plan.as_row(map.get(ast_id)?, rows)?;
+            }
+
+            Ok(plan_id)
+        };
+
+        let get_arithmetic_cond_id =
+            |plan: &mut Plan,
+             current_node: &ParseNode,
+             map: &Translation,
+             arithmetic_expression_ids: &mut Vec<usize>,
+             rows: &mut HashSet<usize>| {
+                let ast_left_id = current_node.children.first().ok_or_else(|| {
+                    SbroadError::UnexpectedNumberOfValues(
+                        "Multiplication or Addition has no children.".into(),
+                    )
+                })?;
+                let plan_left_id = get_arithmetic_plan_id(
+                    plan,
+                    map,
+                    arithmetic_expression_ids,
+                    rows,
+                    *ast_left_id,
+                )?;
+
+                let ast_right_id = current_node.children.get(2).ok_or_else(|| {
+                    SbroadError::NotFound(
+                        Entity::Node,
+                        "that is right node with index 2 among Multiplication or Addition children"
+                            .into(),
+                    )
+                })?;
+                let plan_right_id = get_arithmetic_plan_id(
+                    plan,
+                    map,
+                    arithmetic_expression_ids,
+                    rows,
+                    *ast_right_id,
+                )?;
+
+                let ast_op_id = current_node.children.get(1).ok_or_else(|| {
+                    SbroadError::NotFound(
+                        Entity::Node,
+                        "that is center node (operator) with index 1 among Multiplication or Addition children"
+                            .into(),
+                    )
+                })?;
+
+                let op_node = self.nodes.get_node(*ast_op_id)?;
+                let op = Arithmetic::from_node_type(&op_node.rule)?;
+
+                let cond_id =
+                    plan.add_arithmetic_to_plan(plan_left_id, op, plan_right_id, false)?;
+                Ok(cond_id)
+            };
+
         for (_, id) in dft_post.iter(top) {
             let node = self.nodes.get_node(id)?;
             match &node.rule {
@@ -722,11 +797,21 @@ impl Ast for AbstractSyntaxTree {
                                     ));
                                 }
                             }
+                            Type::Multiplication | Type::Addition => {
+                                let cond_id = get_arithmetic_cond_id(
+                                    &mut plan,
+                                    ast_column,
+                                    &map,
+                                    &mut arithmetic_expression_ids,
+                                    &mut rows,
+                                )?;
+                                columns.push(cond_id);
+                            }
                             _ => {
                                 return Err(SbroadError::Invalid(
                                     Entity::Type,
                                     Some(format!(
-                                        "expected a Column in projection, got {:?}.",
+                                        "expected a Column, Asterisk, Multiplication or Addition in projection, got {:?}.",
                                         ast_column.rule
                                     )),
                                 ));
@@ -737,65 +822,13 @@ impl Ast for AbstractSyntaxTree {
                     map.add(id, projection_id);
                 }
                 Type::Multiplication | Type::Addition => {
-                    let plan_left_id: usize;
-                    let plan_right_id: usize;
-
-                    let ast_left_id = node.children.first().ok_or_else(|| {
-                        SbroadError::UnexpectedNumberOfValues(
-                            "Multiplication or Addition has no children.".into(),
-                        )
-                    })?;
-
-                    // if left child of current multiplication or addition is `(expr)` then
-                    // we need to get expr that is child of `()` and add it to the plan
-                    // also we will mark this expr to add in the future `()`
-                    let ar_left = self.nodes.get_node(*ast_left_id)?;
-                    if ar_left.rule == Type::ArithParentheses {
-                        let arithmetic_id = ar_left.children.first().ok_or_else(|| {
-                            SbroadError::UnexpectedNumberOfValues(
-                                "ArithParentheses  has no children.".into(),
-                            )
-                        })?;
-                        plan_left_id = plan.as_row(map.get(*arithmetic_id)?, &mut rows)?;
-                        arithmetic_expression_ids.push(plan_left_id);
-                    } else {
-                        plan_left_id = plan.as_row(map.get(*ast_left_id)?, &mut rows)?;
-                    }
-
-                    let ast_right_id = node.children.get(2).ok_or_else(|| {
-                        SbroadError::NotFound(
-                            Entity::Node,
-                            "that is right node with index 2 among Multiplication or Addition children".into(),
-                        )
-                    })?;
-
-                    // if left child of current multiplication or addition is `(expr)` then
-                    // we need to get expr that is child of `()` and add it to the plan
-                    // also we will mark this expr to add in the future `()`
-                    let ar_right = self.nodes.get_node(*ast_right_id)?;
-                    if ar_right.rule == Type::ArithParentheses {
-                        let arithmetic_id = ar_right.children.first().ok_or_else(|| {
-                            SbroadError::UnexpectedNumberOfValues(
-                                "ArithParentheses  has no children.".into(),
-                            )
-                        })?;
-                        plan_right_id = plan.as_row(map.get(*arithmetic_id)?, &mut rows)?;
-                        arithmetic_expression_ids.push(plan_right_id);
-                    } else {
-                        plan_right_id = plan.as_row(map.get(*ast_right_id)?, &mut rows)?;
-                    }
-
-                    let ast_op_id = node.children.get(1).ok_or_else(|| {
-                        SbroadError::NotFound(
-                            Entity::Node,
-                            "that is center node (operator) with index 1 among Multiplication or Addition children".into(),
-                        )
-                    })?;
-                    let op_node = self.nodes.get_node(*ast_op_id)?;
-
-                    let op = Arithmetic::from_node_type(&op_node.rule)?;
-                    let cond_id =
-                        plan.add_arithmetic_to_plan(plan_left_id, op, plan_right_id, false)?;
+                    let cond_id = get_arithmetic_cond_id(
+                        &mut plan,
+                        node,
+                        &map,
+                        &mut arithmetic_expression_ids,
+                        &mut rows,
+                    )?;
                     map.add(id, cond_id);
                 }
                 Type::Except => {
diff --git a/sbroad-core/src/frontend/sql/ast/tests.rs b/sbroad-core/src/frontend/sql/ast/tests.rs
index 5232d0d27c..f60ace70c3 100644
--- a/sbroad-core/src/frontend/sql/ast/tests.rs
+++ b/sbroad-core/src/frontend/sql/ast/tests.rs
@@ -154,12 +154,12 @@ fn invalid_query() {
     let ast = AbstractSyntaxTree::new(query).unwrap_err();
     assert_eq!(
         format!(
-            r#"rule parsing error:  --> 1:8
+            r#"rule parsing error:  --> 1:10
   |
 1 | select a frAm t
-  |        ^---
+  |          ^---
   |
-  = expected Alias, Asterisk, Function, Cast, Concat, True, False, Null, Decimal, Double, Integer, Unsigned, Row, or Parameter"#,
+  = expected Multiply, Divide, Add, or Subtract"#,
         ),
         format!("{ast}"),
     );
@@ -185,7 +185,7 @@ fn invalid_condition() {
 
 #[test]
 #[allow(clippy::similar_names)]
-fn sql_arithmetic_ast() {
+fn sql_arithmetic_selection_ast() {
     let ast = AbstractSyntaxTree::new("select a from t where a + b = 1").unwrap();
 
     let path = Path::new("")
@@ -193,7 +193,7 @@ fn sql_arithmetic_ast() {
         .join("artifactory")
         .join("frontend")
         .join("sql")
-        .join("arithmetic_ast.yaml");
+        .join("arithmetic_selection_ast.yaml");
 
     let s = fs::read_to_string(path).unwrap();
     let expected_ast = AbstractSyntaxTree::from_yaml(&s).unwrap();
@@ -287,3 +287,74 @@ fn sql_arithmetic_ast() {
 
     assert_eq!(None, iter.next());
 }
+
+#[test]
+fn sql_arithmetic_projection_ast() {
+    let ast = AbstractSyntaxTree::new("select a + b from t").unwrap();
+
+    let path = Path::new("")
+        .join("tests")
+        .join("artifactory")
+        .join("frontend")
+        .join("sql")
+        .join("arithmetic_projection_ast.yaml");
+
+    let s = fs::read_to_string(path).unwrap();
+    let expected_ast = AbstractSyntaxTree::from_yaml(&s).unwrap();
+
+    assert_eq!(expected_ast, ast);
+
+    // note there is no AliasName or Alias type
+    let top = ast.top.unwrap();
+    let mut dft_post = PostOrder::with_capacity(|node| ast.nodes.ast_iter(node), 64);
+    let mut iter = dft_post.iter(top);
+
+    let (_, table_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(table_id).unwrap();
+    assert_eq!(node.rule, Type::Table);
+    assert_eq!(node.value, Some("t".to_string()));
+
+    let (_, scan_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(scan_id).unwrap();
+    assert_eq!(node.rule, Type::Scan);
+
+    let (_, sel_name_a_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(sel_name_a_id).unwrap();
+    assert_eq!(node.rule, Type::ColumnName);
+    assert_eq!(node.value, Some("a".to_string()));
+
+    let (_, a_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(a_id).unwrap();
+    assert_eq!(node.rule, Type::Reference);
+
+    let (_, col_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(col_id).unwrap();
+    assert_eq!(node.rule, Type::Column);
+
+    let (_, add_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(add_id).unwrap();
+    assert_eq!(node.rule, Type::Add);
+
+    let (_, sel_name_b_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(sel_name_b_id).unwrap();
+    assert_eq!(node.rule, Type::ColumnName);
+    assert_eq!(node.value, Some("b".to_string()));
+
+    let (_, b_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(b_id).unwrap();
+    assert_eq!(node.rule, Type::Reference);
+
+    let (_, col_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(col_id).unwrap();
+    assert_eq!(node.rule, Type::Column);
+
+    let (_, addition_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(addition_id).unwrap();
+    assert_eq!(node.rule, Type::Addition);
+
+    let (_, proj_id) = iter.next().unwrap();
+    let node = ast.nodes.get_node(proj_id).unwrap();
+    assert_eq!(node.rule, Type::Projection);
+
+    assert_eq!(None, iter.next());
+}
diff --git a/sbroad-core/src/frontend/sql/query.pest b/sbroad-core/src/frontend/sql/query.pest
index ba766cd92d..85e6b8d892 100644
--- a/sbroad-core/src/frontend/sql/query.pest
+++ b/sbroad-core/src/frontend/sql/query.pest
@@ -9,7 +9,7 @@ Query = _{ Except | UnionAll | Select | Values | Insert }
         (((^"inner" ~ ^"join") | ^"join") ~ InnerJoin ~
         ^"on" ~ Condition)? ~ (^"where" ~ Selection)?
     }
-        Projection = { (Asterisk | Column) ~ ("," ~ (Asterisk | Column))*? }
+        Projection = { (Asterisk | ArithmeticExpr | Column) ~ ("," ~ (Asterisk | ArithmeticExpr | Column))*? }
             Column = { Alias | Value }
                 Alias = {Value ~ ^"as" ~ AliasName }
                 AliasName = @{ Name }
diff --git a/sbroad-core/src/ir/expression.rs b/sbroad-core/src/ir/expression.rs
index b34d185d4a..976e1bf70e 100644
--- a/sbroad-core/src/ir/expression.rs
+++ b/sbroad-core/src/ir/expression.rs
@@ -400,25 +400,29 @@ impl Nodes {
     ) -> Result<usize, SbroadError> {
         let mut names: HashSet<String> = HashSet::with_capacity(list.len());
 
-        for alias_node in &list {
-            if let Node::Expression(Expression::Alias { name, .. }) =
-                self.arena.get(*alias_node).ok_or_else(|| {
-                    SbroadError::NotFound(
+        for node_id in &list {
+            let node = self.arena.get(*node_id).ok_or_else(|| {
+                SbroadError::NotFound(
+                    Entity::Node,
+                    format!("(Alias or Arithmetic) from arena with index {node_id}"),
+                )
+            })?;
+
+            match node {
+                Node::Expression(Expression::Alias { name, .. }) => {
+                    if !names.insert(String::from(name)) {
+                        return Err(SbroadError::DuplicatedValue(format!(
+                            "row can't be added because `{name}` already has an alias",
+                        )));
+                    }
+                }
+                Node::Expression(Expression::Arithmetic { .. }) => {}
+                _ => {
+                    return Err(SbroadError::Invalid(
                         Entity::Node,
-                        format!("(Alias) from arena with index {alias_node}"),
-                    )
-                })?
-            {
-                if !names.insert(String::from(name)) {
-                    return Err(SbroadError::DuplicatedValue(format!(
-                        "row can't be added because `{name}` already has an alias"
-                    )));
+                        Some("node is not Alias or Arithmetic type".into()),
+                    ));
                 }
-            } else {
-                return Err(SbroadError::Invalid(
-                    Entity::Node,
-                    Some("node is not Alias type".into()),
-                ));
             }
         }
         Ok(self.add_row(list, distribution))
diff --git a/sbroad-core/tests/artifactory/backend/sql/tree/arithmetic_projection_plan.yaml b/sbroad-core/tests/artifactory/backend/sql/tree/arithmetic_projection_plan.yaml
new file mode 100644
index 0000000000..396ce07aaf
--- /dev/null
+++ b/sbroad-core/tests/artifactory/backend/sql/tree/arithmetic_projection_plan.yaml
@@ -0,0 +1,259 @@
+nodes:
+  arena:
+    # 0
+    - Expression:
+        Reference:
+          parent: 15
+          targets: ~
+          position: 0
+    # 1
+    - Expression:
+        Alias:
+          name: a
+          child: 0
+    # 2
+    - Expression:
+        Reference:
+          parent: 15
+          targets: ~
+          position: 1
+    # 3
+    - Expression:
+        Alias:
+          name: b
+          child: 2
+    # 4
+    - Expression:
+        Reference:
+          parent: 15
+          targets: ~
+          position: 2
+    # 5
+    - Expression:
+        Alias:
+          name: c
+          child: 4
+    # 6
+    - Expression:
+        Reference:
+          parent: 15
+          targets: ~
+          position: 3
+    # 7
+    - Expression:
+        Alias:
+          name: d
+          child: 6
+    # 8
+    - Expression:
+        Reference:
+          parent: 15
+          targets: ~
+          position: 4
+    # 9
+    - Expression:
+        Alias:
+          name: e
+          child: 8
+    # 10
+    - Expression:
+        Reference:
+          parent: 15
+          targets: ~
+          position: 5
+    # 11
+    - Expression:
+        Alias:
+          name: f
+          child: 10
+    # 12
+    - Expression:
+        Reference:
+          parent: 15
+          targets: ~
+          position: 6
+    # 13
+    - Expression:
+        Alias:
+          name: bucket_id
+          child: 12
+    # 14
+    - Expression:
+        Row:
+          list:
+          - 1
+          - 3
+          - 5
+          - 7
+          - 9
+          - 11
+          - 13
+          distribution: ~
+    # 15
+    - Relational:
+        ScanRelation:
+          output: 14
+          relation: t
+    # 16
+    - Expression:
+        Reference:
+          parent: 35
+          targets: [0]
+          position: 0
+    # 17
+    - Expression:
+        Row:
+          list:
+            - 16
+          distribution: ~
+    # 18
+    - Expression:
+        Reference:
+          parent: 35
+          targets: [0]
+          position: 1
+    # 19
+    - Expression:
+        Row:
+          list:
+            - 18
+          distribution: ~
+    # 20
+    - Expression:
+        Reference:
+          parent: 35
+          targets: [0]
+          position: 2
+    # 21
+    - Expression:
+        Row:
+          list:
+            - 20
+          distribution: ~
+    # 22
+    - Expression:
+        Reference:
+          parent: 35
+          targets: [0]
+          position: 3
+    # 23
+    - Expression:
+        Row:
+          list:
+            - 22
+          distribution: ~
+    # 24
+    - Expression:
+        Reference:
+          parent: 35
+          targets: [0]
+          position: 4
+    # 25
+    - Expression:
+        Row:
+          list:
+            - 24
+          distribution: ~
+    # 26
+    - Expression:
+        Reference:
+          parent: 35
+          targets: [0]
+          position: 5
+    # 27
+    - Expression:
+        Row:
+          list:
+            - 26
+          distribution: ~
+    # 28
+    - Expression:
+        Arithmetic:
+          left: 19
+          op: divide
+          right: 21
+          with_parentheses: false
+    # 29
+    - Expression:
+        Arithmetic:
+          left: 23
+          op: multiply
+          right: 25
+          with_parentheses: false
+    # 30
+    - Expression:
+        Arithmetic:
+          left: 28
+          op: add
+          right: 29
+          with_parentheses: true
+    # 31
+    - Expression:
+        Arithmetic:
+          left: 30
+          op: multiply
+          right: 27
+          with_parentheses: false
+    # 32
+    - Expression:
+        Arithmetic:
+          left: 17
+          op: add
+          right: 31
+          with_parentheses: false
+    # 33
+    - Expression:
+        Arithmetic:
+          left: 32
+          op: subtract
+          right: 19
+          with_parentheses: false
+    # 34
+    - Expression:
+        Row:
+          list:
+            - 33
+          distribution: ~
+    # 35
+    - Relational:
+        Projection:
+          children:
+            - 15
+          output: 34
+relations:
+  tables:
+    t:
+      columns:
+        - name: a
+          type: Integer
+          role: User
+        - name: b
+          type: Integer
+          role: User
+        - name: c
+          type: Integer
+          role: User
+        - name: d
+          type: Integer
+          role: User
+        - name: e
+          type: Integer
+          role: User
+        - name: f
+          type: Integer
+          role: User
+        - name: bucket_id
+          type: Unsigned
+          role: Sharding
+      key:
+        positions:
+          - 0
+      name: t
+      engine: Memtx
+slices:
+  slices: []
+top: 35
+is_explain: false
+undo:
+  log: {}
+constants: {}
diff --git a/sbroad-core/tests/artifactory/backend/sql/tree/arithmetic_plan.yaml b/sbroad-core/tests/artifactory/backend/sql/tree/arithmetic_selection_plan.yaml
similarity index 100%
rename from sbroad-core/tests/artifactory/backend/sql/tree/arithmetic_plan.yaml
rename to sbroad-core/tests/artifactory/backend/sql/tree/arithmetic_selection_plan.yaml
diff --git a/sbroad-core/tests/artifactory/frontend/sql/arithmetic_projection_ast.yaml b/sbroad-core/tests/artifactory/frontend/sql/arithmetic_projection_ast.yaml
new file mode 100644
index 0000000000..9e0ea7e5e0
--- /dev/null
+++ b/sbroad-core/tests/artifactory/frontend/sql/arithmetic_projection_ast.yaml
@@ -0,0 +1,56 @@
+---
+nodes:
+  arena:
+    - children:
+        - 3
+      rule: Select
+      value: ~
+    - children:
+        - 2
+      rule: Scan
+      value: ~
+    - children: []
+      rule: Table
+      value: t
+    - children:
+        - 1
+        - 4
+      rule: Projection
+      value: ~
+    - children:
+        - 9
+        - 8
+        - 5
+      rule: Addition
+      value: ~
+    - children:
+        - 6
+      rule: Column
+      value: ~
+    - children:
+        - 7
+      rule: Reference
+      value: ~
+    - children: []
+      rule: ColumnName
+      value: "b"
+    - children: []
+      rule: Add
+      value: "+"
+    - children:
+        - 10
+      rule: Column
+      value: ~
+    - children:
+        - 11
+      rule: Reference
+      value: ~
+    - children: []
+      rule: ColumnName
+      value: "a"
+top: 3
+map:
+  10:
+    - 1
+  6:
+    - 1
\ No newline at end of file
diff --git a/sbroad-core/tests/artifactory/frontend/sql/arithmetic_ast.yaml b/sbroad-core/tests/artifactory/frontend/sql/arithmetic_selection_ast.yaml
similarity index 100%
rename from sbroad-core/tests/artifactory/frontend/sql/arithmetic_ast.yaml
rename to sbroad-core/tests/artifactory/frontend/sql/arithmetic_selection_ast.yaml
-- 
GitLab