diff --git a/sbroad-cartridge/test_app/test/integration/trim_test.lua b/sbroad-cartridge/test_app/test/integration/trim_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..bd88e166208864af92ab4d9be8f40a4d1b0de128
--- /dev/null
+++ b/sbroad-cartridge/test_app/test/integration/trim_test.lua
@@ -0,0 +1,78 @@
+
+local t = require('luatest')
+local g = t.group('integration_api.trim')
+
+local helper = require('test.helper.cluster_no_replication')
+
+g.before_all(
+        function()
+            helper.start_test_cluster(helper.cluster_config)
+            local api = helper.cluster:server("api-1").net_box
+
+            local r, err = api:call("sbroad.execute",
+                    { [[ INSERT INTO "t"("id", "a") VALUES (112211, 2211) ]], }
+            )
+            t.assert_equals(err, nil)
+            t.assert_equals(r, {row_count = 1})
+        end
+)
+
+g.after_all(function()
+    helper.stop_test_cluster()
+end)
+
+g.test_trim = function ()
+    local api = helper.cluster:server("api-1").net_box
+
+    -- basic trim test
+    local r, err = api:call("sbroad.execute",
+            { [[SELECT trim('  aabb  ') as "a" from "t"]], }
+    )
+    t.assert_equals(err, nil)
+    t.assert_equals(r, {
+        metadata = { {name = "a", type = "string"}, },
+        rows = { { 'aabb' } },
+    })
+
+    -- trim inside trim
+    r, err = api:call("sbroad.execute",
+            { [[SELECT trim(trim('  aabb  ')) from "t"]], }
+    )
+    t.assert_equals(err, nil)
+    t.assert_equals(r["rows"], { { 'aabb' } })
+
+    -- trim with literal in pattern
+    r, err = api:call("sbroad.execute",
+            { [[SELECT trim('a' from trim('  aabb  ')) from "t"]], }
+    )
+    t.assert_equals(err, nil)
+    t.assert_equals(r["rows"], { { 'bb' } })
+
+    -- trim with expression in pattern
+    r, err = api:call("sbroad.execute",
+            { [[SELECT trim(trim(' aabb ') from trim('  aabb  ')) from "t"]], }
+    )
+    t.assert_equals(err, nil)
+    t.assert_equals(r["rows"], { { '' } })
+
+    -- trim with leading modifier
+    r, err = api:call("sbroad.execute",
+            { [[SELECT trim(leading 'a' from trim('aabb  ')) from "t"]], }
+    )
+    t.assert_equals(err, nil)
+    t.assert_equals(r["rows"], { { 'bb' } })
+
+    -- trim with trailing modifier
+    r, err = api:call("sbroad.execute",
+            { [[SELECT trim(trailing 'b' from trim('aabb')) from "t"]], }
+    )
+    t.assert_equals(err, nil)
+    t.assert_equals(r["rows"], { { 'aa' } })
+
+    -- trim with both modifier
+    r, err = api:call("sbroad.execute",
+            { [[SELECT trim(both 'ab' from 'aabb') from "t"]], }
+    )
+    t.assert_equals(err, nil)
+    t.assert_equals(r["rows"], { { '' } })
+end
diff --git a/sbroad-core/src/backend/sql/ir.rs b/sbroad-core/src/backend/sql/ir.rs
index 6c5b85b7c6227195088127977e8f4e7b1e255d51..3b50577b7755d3e1c1e9c331439d8a5122253cdb 100644
--- a/sbroad-core/src/backend/sql/ir.rs
+++ b/sbroad-core/src/backend/sql/ir.rs
@@ -296,6 +296,7 @@ impl ExecutionPlan {
                     SyntaxData::Trailing => sql.push_str("TRAILING"),
                     SyntaxData::Operator(s) => sql.push_str(s.as_str()),
                     SyntaxData::OpenParenthesis => sql.push('('),
+                    SyntaxData::Trim => sql.push_str("TRIM"),
                     SyntaxData::PlanId(id) => {
                         let node = ir_plan.get_node(*id)?;
                         match node {
@@ -370,6 +371,7 @@ impl ExecutionPlan {
                                     | Expression::Cast { .. }
                                     | Expression::Concat { .. }
                                     | Expression::Row { .. }
+                                    | Expression::Trim { .. }
                                     | Expression::Unary { .. } => {}
                                     Expression::Constant { value, .. } => {
                                         write!(sql, "{value}").map_err(|e| {
diff --git a/sbroad-core/src/backend/sql/tree.rs b/sbroad-core/src/backend/sql/tree.rs
index 0b09f69a88190fc91d11aaa26e1b46f3618eb888..ef8941e74d1170eb0e175335424f4752f0a9ae5f 100644
--- a/sbroad-core/src/backend/sql/tree.rs
+++ b/sbroad-core/src/backend/sql/tree.rs
@@ -42,6 +42,8 @@ pub enum SyntaxData {
     Both,
     /// "trailing"
     Trailing,
+    /// "trim"
+    Trim,
     /// "("
     OpenParenthesis,
     /// "=, >, <, and, or, ..."
@@ -179,6 +181,14 @@ impl SyntaxNode {
         }
     }
 
+    fn new_trim() -> Self {
+        SyntaxNode {
+            data: SyntaxData::Trim,
+            left: None,
+            right: Vec::new(),
+        }
+    }
+
     fn new_operator(value: &str) -> Self {
         SyntaxNode {
             data: SyntaxData::Operator(value.into()),
@@ -1001,45 +1011,53 @@ impl<'p> SyntaxPlan<'p> {
                 } => {
                     let mut nodes: Vec<usize> =
                         vec![self.nodes.push_syntax_node(SyntaxNode::new_open())];
-                    if let Some(FunctionFeature::Trim(kind)) = feature {
-                        // `trim` function has a special format. For instance, here how we can
-                        // call it: trim(leading 'a' from 'ab').
-                        match kind {
-                            TrimKind::Leading => {
-                                nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_leading()));
-                            }
-                            TrimKind::Trailing => {
-                                nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_trailing()));
-                            }
-                            TrimKind::Both => {
-                                nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_both()));
-                            }
-                        }
-
-                        if let Some((string, removal_chars)) = children.split_last() {
-                            for child in removal_chars {
-                                nodes.push(self.nodes.get_syntax_node_id(*child)?);
-                            }
-                            nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_from()));
-                            nodes.push(self.nodes.get_syntax_node_id(*string)?);
-                        }
-                    } else {
-                        if let Some(FunctionFeature::Distinct) = feature {
-                            nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_distinct()));
-                        }
-                        if let Some((last, others)) = children.split_last() {
-                            for child in others {
-                                nodes.push(self.nodes.get_syntax_node_id(*child)?);
-                                nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_comma()));
-                            }
-                            nodes.push(self.nodes.get_syntax_node_id(*last)?);
+                    if let Some(FunctionFeature::Distinct) = feature {
+                        nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_distinct()));
+                    }
+                    if let Some((last, others)) = children.split_last() {
+                        for child in others {
+                            nodes.push(self.nodes.get_syntax_node_id(*child)?);
+                            nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_comma()));
                         }
+                        nodes.push(self.nodes.get_syntax_node_id(*last)?);
                     }
 
                     nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_close()));
                     let sn = SyntaxNode::new_pointer(id, None, nodes);
                     Ok(self.nodes.push_syntax_node(sn))
                 }
+                Expression::Trim {
+                    kind,
+                    pattern,
+                    target,
+                } => {
+                    let syn_kind = match kind {
+                        Some(TrimKind::Leading) => Some(SyntaxNode::new_leading()),
+                        Some(TrimKind::Trailing) => Some(SyntaxNode::new_trailing()),
+                        Some(TrimKind::Both) => Some(SyntaxNode::new_both()),
+                        None => None,
+                    };
+                    let mut nodes = Vec::with_capacity(6);
+                    nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_open()));
+                    let mut need_from = false;
+                    if let Some(kind) = syn_kind {
+                        nodes.push(self.nodes.push_syntax_node(kind));
+                        need_from = true;
+                    }
+                    if let Some(pattern) = pattern {
+                        nodes.push(self.nodes.get_syntax_node_id(*pattern)?);
+                        need_from = true;
+                    }
+                    if need_from {
+                        nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_from()));
+                    }
+                    nodes.push(self.nodes.get_syntax_node_id(*target)?);
+                    nodes.push(self.nodes.push_syntax_node(SyntaxNode::new_close()));
+
+                    let trim_id = self.nodes.push_syntax_node(SyntaxNode::new_trim());
+                    let sn = SyntaxNode::new_pointer(id, Some(trim_id), nodes);
+                    Ok(self.nodes.push_syntax_node(sn))
+                }
             },
         }
     }
diff --git a/sbroad-core/src/executor/ir.rs b/sbroad-core/src/executor/ir.rs
index 452eb0a1448b14e864803a9fffae562dba74e689..be9325eae222879f6c71938d0919439248469668 100644
--- a/sbroad-core/src/executor/ir.rs
+++ b/sbroad-core/src/executor/ir.rs
@@ -595,6 +595,32 @@ impl ExecutionPlan {
                             )
                         })?;
                     }
+                    Expression::Trim {
+                        ref mut pattern,
+                        ref mut target,
+                        ..
+                    } => {
+                        if let Some(pattern) = pattern {
+                            *pattern = *translation.get(pattern).ok_or_else(|| {
+                                SbroadError::FailedTo(
+                                    Action::Build,
+                                    Some(Entity::SubTree),
+                                    format_smolstr!(
+                                        "could not find pattern node id {pattern} in the map"
+                                    ),
+                                )
+                            })?;
+                        }
+                        *target = *translation.get(target).ok_or_else(|| {
+                            SbroadError::FailedTo(
+                                Action::Build,
+                                Some(Entity::SubTree),
+                                format_smolstr!(
+                                    "could not find target node id {target} in the map"
+                                ),
+                            )
+                        })?;
+                    }
                     Expression::Reference { ref mut parent, .. } => {
                         // The new parent node id MUST be set while processing the relational nodes.
                         *parent = None;
diff --git a/sbroad-core/src/frontend/sql.rs b/sbroad-core/src/frontend/sql.rs
index bead363580f779c62b5adbd10c755443ca8a2cf0..1fa642d8920aecd9e79b36b4a63eb63714018194 100644
--- a/sbroad-core/src/frontend/sql.rs
+++ b/sbroad-core/src/frontend/sql.rs
@@ -884,72 +884,63 @@ fn parse_grant_revoke(
     Ok((grant_revoke_type, grantee_name, timeout))
 }
 
-fn parse_trim_function_args<M: Metadata>(
-    function_name: String,
-    function_args: Pair<'_, Rule>,
+fn parse_trim<M: Metadata>(
+    pair: Pair<Rule>,
     referred_relation_ids: &[usize],
     worker: &mut ExpressionsWorker<M>,
     plan: &mut Plan,
 ) -> Result<ParseExpression, SbroadError> {
-    let normalized_name = function_name.to_lowercase();
-    if "trim" != normalized_name.as_str() {
-        return Err(SbroadError::Invalid(
-            Entity::Query,
-            Some(format_smolstr!(
-                "Trim function artuments format is allowed only inside \"trim\" function. Got: {normalized_name}",
-            ))
-        ));
-    }
-    let mut parse_exprs_args = Vec::new();
+    assert_eq!(pair.as_rule(), Rule::Trim);
     let mut kind = None;
-    let mut removal_chars = None;
-    let mut string = None;
+    let mut pattern = None;
+    let mut target = None;
 
-    let args_inner = function_args.into_inner();
-    for arg_pair in args_inner {
-        match arg_pair.as_rule() {
+    let inner_pairs = pair.into_inner();
+    for child_pair in &mut inner_pairs.into_iter() {
+        match child_pair.as_rule() {
             Rule::TrimKind => {
-                for kind_pair in arg_pair.into_inner() {
-                    match kind_pair.as_rule() {
-                        Rule::TrimKindBoth => kind = Some(TrimKind::Both),
-                        Rule::TrimKindLeading => kind = Some(TrimKind::Leading),
-                        Rule::TrimKindTrailing => kind = Some(TrimKind::Trailing),
-                        rule => unreachable!("Expected TrimKind variant. Got: {rule:?}"),
+                let kind_pair = child_pair
+                    .into_inner()
+                    .next()
+                    .expect("Expected child of TrimKind");
+                match kind_pair.as_rule() {
+                    Rule::TrimKindBoth => kind = Some(TrimKind::Both),
+                    Rule::TrimKindLeading => kind = Some(TrimKind::Leading),
+                    Rule::TrimKindTrailing => kind = Some(TrimKind::Trailing),
+                    _ => {
+                        panic!("Unexpected node: {kind_pair:?}");
                     }
                 }
             }
-            Rule::TrimChars => {
-                removal_chars = Some(parse_expr_pratt(
-                    arg_pair.into_inner(),
+            Rule::TrimPattern => {
+                let inner_pattern = child_pair.into_inner();
+                pattern = Some(Box::new(parse_expr_pratt(
+                    inner_pattern,
                     referred_relation_ids,
                     worker,
                     plan,
-                )?);
+                )?));
             }
-            Rule::TrimString => {
-                string = Some(parse_expr_pratt(
-                    arg_pair.into_inner(),
+            Rule::TrimTarget => {
+                let inner_target = child_pair.into_inner();
+                target = Some(Box::new(parse_expr_pratt(
+                    inner_target,
                     referred_relation_ids,
                     worker,
                     plan,
-                )?);
+                )?));
+            }
+            _ => {
+                panic!("Unexpected node: {child_pair:?}");
             }
-            rule => unreachable!("Unexpected rule under TrimFunctionArgs: {rule:?}"),
         }
     }
-    let string = string.expect("string is required by grammar");
-
-    if let Some(removal_chars) = removal_chars {
-        parse_exprs_args.push(removal_chars);
-    }
-    parse_exprs_args.push(string);
-    let trim_kind = kind.unwrap_or_default();
-
-    Ok(ParseExpression::Function {
-        name: function_name,
-        args: parse_exprs_args,
-        feature: Some(FunctionFeature::Trim(trim_kind)),
-    })
+    let trim = ParseExpression::Trim {
+        kind,
+        pattern,
+        target: target.expect("Trim target must be specified"),
+    };
+    Ok(trim)
 }
 
 /// Common logic for `SqlVdbeMaxSteps` and `VTableMaxRows` parsing.
@@ -1260,6 +1251,11 @@ enum ParseExpression {
         cast_type: CastType,
         child: Box<ParseExpression>,
     },
+    Trim {
+        kind: Option<TrimKind>,
+        pattern: Option<Box<ParseExpression>>,
+        target: Box<ParseExpression>,
+    },
     Between {
         is_not: bool,
         left: Box<ParseExpression>,
@@ -1288,6 +1284,7 @@ impl Plan {
             | Expression::Reference { .. }
             | Expression::Row { .. }
             | Expression::StableFunction { .. }
+            | Expression::Trim { .. }
             | Expression::Unary { .. }
             | Expression::CountAsterisk => Err(SbroadError::Invalid(
                 Entity::Expression,
@@ -1363,6 +1360,22 @@ impl ParseExpression {
                 let child_plan_id = child.populate_plan(plan, worker)?;
                 plan.add_cast(child_plan_id, cast_type.clone())?
             }
+            ParseExpression::Trim {
+                kind,
+                pattern,
+                target,
+            } => {
+                let pattern = match pattern {
+                    Some(p) => Some(p.populate_plan(plan, worker)?),
+                    None => None,
+                };
+                let trim_expr = Expression::Trim {
+                    kind: kind.clone(),
+                    pattern,
+                    target: target.populate_plan(plan, worker)?,
+                };
+                plan.nodes.push(Node::Expression(trim_expr))
+            }
             ParseExpression::Between {
                 is_not,
                 left,
@@ -1648,15 +1661,6 @@ where
                                                 parse_exprs_args.push(arg_expr);
                                             }
                                         }
-                                        Rule::TrimFunctionArgs => {
-                                            return parse_trim_function_args(
-                                                function_name,
-                                                function_args,
-                                                referred_relation_ids,
-                                                worker,
-                                                plan
-                                            );
-                                        }
                                         rule => unreachable!("{}", format!("Unexpected rule under FunctionInvocation: {rule:?}"))
                                     }
                                 }
@@ -1795,6 +1799,7 @@ where
                     )?;
                     ParseExpression::Exists { is_not: first_is_not, child: Box::new(child_parse_expr)}
                 }
+                Rule::Trim => parse_trim(primary, referred_relation_ids, worker, plan)?,
                 Rule::Cast => {
                     let mut inner_pairs = primary.into_inner();
                     let expr_pair = inner_pairs.next().expect("Cast has no expr child.");
diff --git a/sbroad-core/src/frontend/sql/ir.rs b/sbroad-core/src/frontend/sql/ir.rs
index 8bc901b6033c0049f153e20c64ce8701e1754369..a13d8a016e1d31dc05d037175a760dbb6f6e0b0d 100644
--- a/sbroad-core/src/frontend/sql/ir.rs
+++ b/sbroad-core/src/frontend/sql/ir.rs
@@ -333,6 +333,20 @@ impl Plan {
                         SbroadError::NotFound(Entity::SubTree, format_smolstr!("(id {id})"))
                     })?;
                 }
+                Expression::Trim {
+                    ref mut pattern,
+                    ref mut target,
+                    ..
+                } => {
+                    if let Some(pattern) = pattern {
+                        *pattern = *map.get(pattern).ok_or_else(|| {
+                            SbroadError::NotFound(Entity::SubTree, format_smolstr!("(id {id})"))
+                        })?;
+                    }
+                    *target = *map.get(target).ok_or_else(|| {
+                        SbroadError::NotFound(Entity::SubTree, format_smolstr!("(id {id})"))
+                    })?;
+                }
                 Expression::Row {
                     list: ref mut children,
                     ..
@@ -436,6 +450,16 @@ impl SubtreeCloner {
                 *left = self.get_new_id(*left)?;
                 *right = self.get_new_id(*right)?;
             }
+            Expression::Trim {
+                ref mut pattern,
+                ref mut target,
+                ..
+            } => {
+                if let Some(pattern) = pattern {
+                    *pattern = self.get_new_id(*pattern)?;
+                }
+                *target = self.get_new_id(*target)?;
+            }
             Expression::Row {
                 list: ref mut children,
                 distribution: _,
diff --git a/sbroad-core/src/frontend/sql/ir/tests/trim.rs b/sbroad-core/src/frontend/sql/ir/tests/trim.rs
index d5df6085d5f191cec0f56c9101f4e04f40455e35..1a0f4df0532ddc4f12deb91d835e0d734e47ffb0 100644
--- a/sbroad-core/src/frontend/sql/ir/tests/trim.rs
+++ b/sbroad-core/src/frontend/sql/ir/tests/trim.rs
@@ -7,7 +7,7 @@ fn trim() {
     let plan = sql_to_optimized_ir(sql, vec![]);
 
     let expected_explain = String::from(
-        r#"projection ("TRIM"(("test_space"."FIRST_NAME"::string))::string -> "COL_1")
+        r#"projection (TRIM("test_space"."FIRST_NAME"::string) -> "COL_1")
     scan "test_space"
 execution options:
 sql_vdbe_max_steps = 45000
@@ -24,7 +24,7 @@ fn trim_leading_from() {
     let plan = sql_to_optimized_ir(sql, vec![]);
 
     let expected_explain = String::from(
-        r#"projection ("TRIM"(leading  from "test_space"."FIRST_NAME"::string)::string -> "COL_1")
+        r#"projection (TRIM(leading from "test_space"."FIRST_NAME"::string) -> "COL_1")
     scan "test_space"
 execution options:
 sql_vdbe_max_steps = 45000
@@ -41,7 +41,7 @@ fn trim_both_space_from() {
     let plan = sql_to_optimized_ir(sql, vec![]);
 
     let expected_explain = String::from(
-        r#"projection ("TRIM"(both ' '::string from "test_space"."FIRST_NAME"::string)::string -> "COL_1")
+        r#"projection (TRIM(both ' '::string from "test_space"."FIRST_NAME"::string) -> "COL_1")
     scan "test_space"
 execution options:
 sql_vdbe_max_steps = 45000
diff --git a/sbroad-core/src/frontend/sql/query.pest b/sbroad-core/src/frontend/sql/query.pest
index adc104cf578c45de48063852da166f326b8d45e9..bf471acdef31d6cece5b8b10bf30722d3c07b40e 100644
--- a/sbroad-core/src/frontend/sql/query.pest
+++ b/sbroad-core/src/frontend/sql/query.pest
@@ -227,7 +227,7 @@ Expr = { ExprAtomValue ~ (ExprInfixOp ~ ExprAtomValue)* }
     ExprAtomValue = _{ UnaryNot* ~ AtomicExpr ~ IsNullPostfix? }
         UnaryNot   = @{ NotFlag }
         IsNullPostfix = { ^"is" ~ NotFlag? ~ ^"null" }
-        AtomicExpr = _{ Literal | Parameter | Cast | IdentifierWithOptionalContinuation | ExpressionInParentheses | UnaryOperator | SubQuery | Row }
+        AtomicExpr = _{ Literal | Parameter | Cast | Trim | IdentifierWithOptionalContinuation | ExpressionInParentheses | UnaryOperator | SubQuery | Row }
             Literal = { True | False | Null | Double | Decimal | Unsigned | Integer | SingleQuotedString }
                 True     = { ^"true" }
                 False    = { ^"false" }
@@ -242,17 +242,21 @@ Expr = { ExprAtomValue ~ (ExprInfixOp ~ ExprAtomValue)* }
                 PgParameter = { "$" ~ Unsigned }
             IdentifierWithOptionalContinuation = { Identifier ~ (ReferenceContinuation | FunctionInvocationContinuation)? }
                 ReferenceContinuation          = { "." ~ Identifier }
-                FunctionInvocationContinuation = { "(" ~ (CountAsterisk | TrimFunctionArgs | FunctionArgs)? ~ ")" }
+                FunctionInvocationContinuation = { "(" ~ (CountAsterisk | FunctionArgs)? ~ ")" }
                     FunctionArgs = { Distinct? ~ (Expr ~ ("," ~ Expr)*)? }
                     CountAsterisk = { "*" }
-                    TrimFunctionArgs = { ((TrimKind? ~ TrimChars) | TrimKind) ~ ^"from" ~ TrimString }
-                        TrimKind = { (TrimKindLeading | TrimKindTrailing | TrimKindBoth) }
-                            TrimKindLeading = { ^"leading" }
-                            TrimKindTrailing = { ^"trailing" }
-                            TrimKindBoth = { ^"both" }
-                        TrimChars = { Expr }
-                        TrimString = { Expr }
             ExpressionInParentheses = { "(" ~ Expr ~ ")" }
+            Trim = {
+                ^"trim" ~ "("
+                ~ (((TrimKind? ~ TrimPattern) | TrimKind) ~ ^"from")? ~ TrimTarget
+                ~ ")"
+            }
+                TrimKind = { (TrimKindLeading | TrimKindTrailing | TrimKindBoth) }
+                    TrimKindLeading = { ^"leading" }
+                    TrimKindTrailing = { ^"trailing" }
+                    TrimKindBoth = { ^"both" }
+                TrimPattern = { Expr }
+                TrimTarget = { Expr }
             Cast = { ^"cast" ~ "(" ~ Expr ~ ^"as" ~ TypeCast ~ ")" }
                 TypeCast = _{ TypeAny | ColumnDefType }
                 ColumnDefType = { TypeBool | TypeDecimal | TypeDouble | TypeInt | TypeNumber
diff --git a/sbroad-core/src/ir.rs b/sbroad-core/src/ir.rs
index dd6fd90fcc018d820cbf49e3b615400f0ec3b09d..e2b26238352fdf524b9c4eb9880db97d3fd31456 100644
--- a/sbroad-core/src/ir.rs
+++ b/sbroad-core/src/ir.rs
@@ -1024,6 +1024,20 @@ impl Plan {
                     return Ok(());
                 }
             }
+            Expression::Trim {
+                pattern, target, ..
+            } => {
+                if let Some(pattern_id) = pattern {
+                    if *pattern_id == old_id {
+                        *pattern_id = new_id;
+                        return Ok(());
+                    }
+                }
+                if *target == old_id {
+                    *target = new_id;
+                    return Ok(());
+                }
+            }
             Expression::Row { list: arr, .. } | StableFunction { children: arr, .. } => {
                 for child in arr.iter_mut() {
                     if *child == old_id {
diff --git a/sbroad-core/src/ir/api/parameter.rs b/sbroad-core/src/ir/api/parameter.rs
index e95c1509f23db86f603ab48b0f13ada481fb7a12..74e3dd5ec5490dcfb4ab37ecd1b9bfbb215cdaa0 100644
--- a/sbroad-core/src/ir/api/parameter.rs
+++ b/sbroad-core/src/ir/api/parameter.rs
@@ -240,6 +240,23 @@ impl Plan {
                             }
                         }
                     }
+                    Expression::Trim {
+                        ref pattern,
+                        ref target,
+                        ..
+                    } => {
+                        let params = match pattern {
+                            Some(p) => [Some(*p), Some(*target)],
+                            None => [None, Some(*target)],
+                        };
+                        for param_id in params.into_iter().flatten() {
+                            if param_node_ids.take(&param_id).is_some() {
+                                idx = idx.saturating_sub(1);
+                                let val_id = get_value(param_id, idx)?;
+                                row_ids.insert(param_id, self.nodes.add_row(vec![val_id], None));
+                            }
+                        }
+                    }
                     Expression::Row { ref list, .. }
                     | Expression::StableFunction {
                         children: ref list, ..
@@ -369,6 +386,23 @@ impl Plan {
                             }
                         }
                     }
+                    Expression::Trim {
+                        ref mut pattern,
+                        ref mut target,
+                        ..
+                    } => {
+                        let params = match pattern {
+                            Some(p) => [Some(p), Some(target)],
+                            None => [None, Some(target)],
+                        };
+                        for param_id in params.into_iter().flatten() {
+                            if param_node_ids_cloned.take(param_id).is_some() {
+                                idx = idx.saturating_sub(1);
+                                let row_id = get_row(*param_id)?;
+                                *param_id = row_id;
+                            }
+                        }
+                    }
                     Expression::Row { ref mut list, .. }
                     | Expression::StableFunction {
                         children: ref mut list,
diff --git a/sbroad-core/src/ir/explain.rs b/sbroad-core/src/ir/explain.rs
index 06f67eeba2e8b1269787de583e39a975385ae3c6..85e5b45b927cd6e41738105c941d8bd1439b4454 100644
--- a/sbroad-core/src/ir/explain.rs
+++ b/sbroad-core/src/ir/explain.rs
@@ -8,7 +8,7 @@ use smol_str::{format_smolstr, SmolStr, ToSmolStr};
 
 use crate::errors::{Entity, SbroadError};
 use crate::ir::expression::cast::Type as CastType;
-use crate::ir::expression::Expression;
+use crate::ir::expression::{Expression, TrimKind};
 use crate::ir::operator::{ConflictStrategy, JoinKind, Relational};
 use crate::ir::relation::Type;
 use crate::ir::transformation::redistribution::{
@@ -32,6 +32,7 @@ enum ColExpr {
     Cast(Box<ColExpr>, CastType),
     Concat(Box<ColExpr>, Box<ColExpr>),
     StableFunction(SmolStr, Vec<ColExpr>, Option<FunctionFeature>, Type),
+    Trim(Option<TrimKind>, Option<Box<ColExpr>>, Box<ColExpr>),
     Row(Row),
     None,
 }
@@ -59,24 +60,18 @@ impl Display for ColExpr {
             ColExpr::Concat(l, r) => format!("{l} || {r}"),
             ColExpr::StableFunction(name, args, feature, func_type) => {
                 let is_distinct = matches!(feature, Some(FunctionFeature::Distinct));
-                let formatted_args = if let Some(FunctionFeature::Trim(kind)) = feature {
-                    let (string, removal_chars) = args
-                        .split_last()
-                        .expect("string is required by the grammar");
-                    format!(
-                        "{} {} from {}",
-                        kind.as_str(),
-                        removal_chars.iter().format(""),
-                        string
-                    )
-                } else {
-                    format!("({})", args.iter().format(", "))
-                };
+                let formatted_args = format!("({})", args.iter().format(", "));
                 format!(
                     "{name}({}{formatted_args})::{func_type}",
                     if is_distinct { "distinct " } else { "" }
                 )
             }
+            ColExpr::Trim(kind, pattern, target) => match (kind, pattern) {
+                (Some(k), Some(p)) => format!("TRIM({} {p} from {target})", k.as_str()),
+                (Some(k), None) => format!("TRIM({} from {target})", k.as_str()),
+                (None, Some(p)) => format!("TRIM({p} from {target})"),
+                (None, None) => format!("TRIM({target})"),
+            },
             ColExpr::Row(row) => row.to_string(),
             ColExpr::None => String::new(),
         };
@@ -162,6 +157,14 @@ impl ColExpr {
                         ColExpr::Column(value.to_string(), current_node.calculate_type(plan)?);
                     stack.push((expr, id));
                 }
+                Expression::Trim { kind, .. } => {
+                    let (target, _) = stack
+                        .pop()
+                        .expect("stack is empty while processing TRIM expression");
+                    let pattern = stack.pop().map(|(pattern, _)| Box::new(pattern));
+                    let trim_expr = ColExpr::Trim(kind.clone(), pattern, Box::new(target));
+                    stack.push((trim_expr, id));
+                }
                 Expression::StableFunction {
                     name,
                     children,
diff --git a/sbroad-core/src/ir/expression.rs b/sbroad-core/src/ir/expression.rs
index 9cded1277353d9e0ffc7494e6e520809f93a2d61..548f5ed6d571242452f8e721d070f192b6f435fc 100644
--- a/sbroad-core/src/ir/expression.rs
+++ b/sbroad-core/src/ir/expression.rs
@@ -150,6 +150,15 @@ pub enum Expression {
         /// Function return type.
         func_type: Type,
     },
+    /// Trim expression.
+    Trim {
+        /// Trim kind.
+        kind: Option<TrimKind>,
+        /// Trim string pattern to remove (it can be an expression).
+        pattern: Option<usize>,
+        /// Target expression to trim.
+        target: usize,
+    },
     /// Unary expression returning boolean result.
     Unary {
         /// Unary operator.
@@ -168,8 +177,6 @@ pub enum Expression {
 pub enum FunctionFeature {
     /// Current function is an aggregate function and is marked as DISTINCT.
     Distinct,
-    /// Current function is `trim` function.
-    Trim(TrimKind),
 }
 
 /// This is the kind of `trim` function that can be set
@@ -608,6 +615,33 @@ impl<'plan> Comparator<'plan> {
                                 && self.are_subtrees_equal(*right_left, *right_right)?);
                         }
                     }
+                    Expression::Trim {
+                        kind: kind_left,
+                        pattern: pattern_left,
+                        target: target_left,
+                    } => {
+                        if let Expression::Trim {
+                            kind: kind_right,
+                            pattern: pattern_right,
+                            target: target_right,
+                        } = right
+                        {
+                            match (pattern_left, pattern_right) {
+                                (Some(p_left), Some(p_right)) => {
+                                    return Ok(*kind_left == *kind_right
+                                        && self.are_subtrees_equal(*p_left, *p_right)?
+                                        && self
+                                            .are_subtrees_equal(*target_left, *target_right)?);
+                                }
+                                (None, None) => {
+                                    return Ok(*kind_left == *kind_right
+                                        && self
+                                            .are_subtrees_equal(*target_left, *target_right)?);
+                                }
+                                _ => return Ok(false),
+                            }
+                        }
+                    }
                     Expression::Constant { value: value_left } => {
                         if let Expression::Constant { value: value_right } = right {
                             return Ok(*value_left == *value_right);
@@ -713,6 +747,17 @@ impl<'plan> Comparator<'plan> {
                 self.hash_for_expr(*left, state, depth - 1);
                 self.hash_for_expr(*right, state, depth - 1);
             }
+            Expression::Trim {
+                kind,
+                pattern,
+                target,
+            } => {
+                kind.hash(state);
+                if let Some(pattern) = pattern {
+                    self.hash_for_expr(*pattern, state, depth - 1);
+                }
+                self.hash_for_expr(*target, state, depth - 1);
+            }
             Expression::Constant { value } => {
                 value.hash(state);
             }
diff --git a/sbroad-core/src/ir/expression/types.rs b/sbroad-core/src/ir/expression/types.rs
index 23669fbbb719dd39279474ad5627129b2185abde..574d87f5454fd85c80226a9db79d4ea2874d1287 100644
--- a/sbroad-core/src/ir/expression/types.rs
+++ b/sbroad-core/src/ir/expression/types.rs
@@ -68,7 +68,7 @@ impl Expression {
                 }
             }
             Expression::Cast { to, .. } => Ok(to.as_relation_type()),
-            Expression::Concat { .. } => Ok(Type::String),
+            Expression::Trim { .. } | Expression::Concat { .. } => Ok(Type::String),
             Expression::Constant { value, .. } => Ok(value.get_type()),
             Expression::Reference { col_type, .. } => Ok(col_type.clone()),
             Expression::Row { list, .. } => {
diff --git a/sbroad-core/src/ir/helpers.rs b/sbroad-core/src/ir/helpers.rs
index 3f0dc9fd9041682f3ac96543c3058154db6d5160..d0f0a31e96888bf9946b640f7b870e88e3634d5b 100644
--- a/sbroad-core/src/ir/helpers.rs
+++ b/sbroad-core/src/ir/helpers.rs
@@ -153,6 +153,7 @@ impl Plan {
                     }
                 }
                 Expression::Cast { .. } => writeln!(buf, "Cast")?,
+                Expression::Trim { .. } => writeln!(buf, "Trim")?,
                 Expression::Concat { .. } => writeln!(buf, "Concat")?,
                 Expression::StableFunction { .. } => writeln!(buf, "StableFunction")?,
                 Expression::Unary { op, child } => {
diff --git a/sbroad-core/src/ir/transformation.rs b/sbroad-core/src/ir/transformation.rs
index 10c563a749dcb758d324b8ecf720e5591b214457..fcdbb5af98c6dc41ce4d1e818dbf040adb8048c7 100644
--- a/sbroad-core/src/ir/transformation.rs
+++ b/sbroad-core/src/ir/transformation.rs
@@ -219,6 +219,18 @@ impl Plan {
                             *right = *new_id;
                         }
                     }
+                    Expression::Trim {
+                        pattern, target, ..
+                    } => {
+                        if let Some(pattern) = pattern {
+                            if let Some(new_id) = map.get(pattern) {
+                                *pattern = *new_id;
+                            }
+                        }
+                        if let Some(new_id) = map.get(target) {
+                            *target = *new_id;
+                        }
+                    }
                     Expression::Row { list, .. }
                     | Expression::StableFunction { children: list, .. } => {
                         for id in list {
diff --git a/sbroad-core/src/ir/transformation/redistribution/eq_cols.rs b/sbroad-core/src/ir/transformation/redistribution/eq_cols.rs
index d97489542713ed87e14ce9483e91dc34f9e9f9de..8f0fd969e121bb8af7f88dc511080574302158d3 100644
--- a/sbroad-core/src/ir/transformation/redistribution/eq_cols.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/eq_cols.rs
@@ -82,6 +82,17 @@ impl ReferredMap {
                         .add(referred.get_or_none(*right));
                     referred.insert(node_id, res);
                 }
+                Expression::Trim {
+                    pattern, target, ..
+                } => {
+                    let res = match pattern {
+                        Some(pattern) => referred
+                            .get_or_none(*pattern)
+                            .add(referred.get_or_none(*target)),
+                        None => referred.get_or_none(*target).clone(),
+                    };
+                    referred.insert(node_id, res);
+                }
                 Expression::Constant { .. } | Expression::CountAsterisk => {
                     referred.insert(node_id, Referred::None);
                 }
diff --git a/sbroad-core/src/ir/transformation/redistribution/groupby.rs b/sbroad-core/src/ir/transformation/redistribution/groupby.rs
index a6718934f5e18feb6383dc5bc8c95c9c5aea9234..fd3d9905903b4c19bc2fee72666172ea67c1d03f 100644
--- a/sbroad-core/src/ir/transformation/redistribution/groupby.rs
+++ b/sbroad-core/src/ir/transformation/redistribution/groupby.rs
@@ -430,6 +430,33 @@ impl Plan {
                                 && self.are_subtrees_equal(*child_left, *child_right)?);
                         }
                     }
+                    Expression::Trim {
+                        kind: kind_left,
+                        pattern: pattern_left,
+                        target: target_left,
+                    } => {
+                        if let Expression::Trim {
+                            kind: kind_right,
+                            pattern: pattern_right,
+                            target: target_right,
+                        } = right
+                        {
+                            match (pattern_left, pattern_right) {
+                                (Some(p_left), Some(p_right)) => {
+                                    return Ok(*kind_left == *kind_right
+                                        && self.are_subtrees_equal(*p_left, *p_right)?
+                                        && self
+                                            .are_subtrees_equal(*target_left, *target_right)?);
+                                }
+                                (None, None) => {
+                                    return Ok(*kind_left == *kind_right
+                                        && self
+                                            .are_subtrees_equal(*target_left, *target_right)?);
+                                }
+                                _ => return Ok(false),
+                            }
+                        }
+                    }
                     Expression::Concat {
                         left: left_left,
                         right: right_left,
diff --git a/sbroad-core/src/ir/tree/expression.rs b/sbroad-core/src/ir/tree/expression.rs
index 1bbf6984f4c86759f65d6fef7512fb5c4aa4687c..616aa85f1bade2e6bb0d6ac9e9ab366960dd2beb 100644
--- a/sbroad-core/src/ir/tree/expression.rs
+++ b/sbroad-core/src/ir/tree/expression.rs
@@ -97,6 +97,7 @@ impl<'n> Iterator for AggregateIterator<'n> {
     }
 }
 
+#[allow(clippy::too_many_lines)]
 fn expression_next<'nodes>(
     iter: &mut impl ExpressionTreeIterator<'nodes>,
 ) -> Option<&'nodes usize> {
@@ -129,6 +130,28 @@ fn expression_next<'nodes>(
             }
             None
         }
+        Some(Node::Expression(Expression::Trim {
+            pattern, target, ..
+        })) => {
+            let child_step = *iter.get_child().borrow();
+            match child_step {
+                0 => {
+                    *iter.get_child().borrow_mut() += 1;
+                    match pattern {
+                        Some(_) => pattern.as_ref(),
+                        None => Some(target),
+                    }
+                }
+                1 => {
+                    *iter.get_child().borrow_mut() += 1;
+                    match pattern {
+                        Some(_) => Some(target),
+                        None => None,
+                    }
+                }
+                _ => None,
+            }
+        }
         Some(Node::Expression(Expression::Row { list, .. })) => {
             let child_step = *iter.get_child().borrow();
             let mut is_leaf = false;
diff --git a/sbroad-core/src/ir/tree/subtree.rs b/sbroad-core/src/ir/tree/subtree.rs
index 2fc9ed0421eca937d2c614f97cd022e73ad92b3b..fcca41a23a2ff32a4493c45b88211a8934093707 100644
--- a/sbroad-core/src/ir/tree/subtree.rs
+++ b/sbroad-core/src/ir/tree/subtree.rs
@@ -221,6 +221,28 @@ fn subtree_next<'plan>(
                     }
                     None
                 }
+                Expression::Trim {
+                    pattern, target, ..
+                } => {
+                    let child_step = *iter.get_child().borrow();
+                    match child_step {
+                        0 => {
+                            *iter.get_child().borrow_mut() += 1;
+                            match pattern {
+                                Some(_) => pattern.as_ref(),
+                                None => Some(target),
+                            }
+                        }
+                        1 => {
+                            *iter.get_child().borrow_mut() += 1;
+                            match pattern {
+                                Some(_) => Some(target),
+                                None => None,
+                            }
+                        }
+                        _ => None,
+                    }
+                }
                 Expression::Row { list, .. }
                 | Expression::StableFunction { children: list, .. } => {
                     let child_step = *iter.get_child().borrow();