diff --git a/sbroad/sbroad-core/src/executor.rs b/sbroad/sbroad-core/src/executor.rs
index 5a572420d975e3dc4ed9fb5f5f15c99c95cdab4b..e4453abe674ec4c58582c9ede5600ebd9ff4dd81 100644
--- a/sbroad/sbroad-core/src/executor.rs
+++ b/sbroad/sbroad-core/src/executor.rs
@@ -57,6 +57,7 @@ impl Plan {
     /// # Errors
     /// - Failed to optimize the plan.
     pub fn optimize(&mut self) -> Result<(), SbroadError> {
+        self.cast_constants()?;
         self.replace_in_operator()?;
         self.push_down_not()?;
         self.split_columns()?;
diff --git a/sbroad/sbroad-core/src/executor/tests/concat.rs b/sbroad/sbroad-core/src/executor/tests/concat.rs
index c0a6aa822164e8434eba2025922323eb2b64e6ec..09df269f7dbf88482adce8569b3e9ed438249a93 100644
--- a/sbroad/sbroad-core/src/executor/tests/concat.rs
+++ b/sbroad/sbroad-core/src/executor/tests/concat.rs
@@ -5,7 +5,7 @@ use crate::ir::value::Value;
 fn concat1_test() {
     broadcast_check(
         r#"SELECT CAST('1' as string) || 'hello' FROM "t1""#,
-        r#"SELECT (CAST (? as string)) || (?) as "col_1" FROM "t1""#,
+        r#"SELECT (?) || (?) as "col_1" FROM "t1""#,
         vec![Value::from("1"), Value::from("hello")],
     );
 }
diff --git a/sbroad/sbroad-core/src/ir/explain/tests.rs b/sbroad/sbroad-core/src/ir/explain/tests.rs
index 0767a6c4b991511a33cf16a0cdc8b73c0dae5c7e..0c931a896ed6eb11b5f6ba970891dfcb16016d7d 100644
--- a/sbroad/sbroad-core/src/ir/explain/tests.rs
+++ b/sbroad/sbroad-core/src/ir/explain/tests.rs
@@ -585,3 +585,6 @@ mod delete;
 
 #[cfg(test)]
 mod query_explain;
+
+#[cfg(test)]
+mod cast_constants;
diff --git a/sbroad/sbroad-core/src/ir/explain/tests/cast_constants.rs b/sbroad/sbroad-core/src/ir/explain/tests/cast_constants.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9539ad2f1a3664e06e61d58adb62fc09099058fd
--- /dev/null
+++ b/sbroad/sbroad-core/src/ir/explain/tests/cast_constants.rs
@@ -0,0 +1,80 @@
+use super::*;
+
+#[test]
+fn select_values_rows() {
+    explain_check(
+        "SELECT * FROM (VALUES (1::int, 2::decimal::unsigned, 'txt'::text::text::text))",
+        r#"projection ("COLUMN_1"::integer -> "COLUMN_1", "COLUMN_2"::unsigned -> "COLUMN_2", "COLUMN_3"::string -> "COLUMN_3")
+    scan
+        values
+            value row (data=ROW(1::integer, 2::unsigned, 'txt'::string))
+execution options:
+    vdbe_max_steps = 45000
+    vtable_max_rows = 5000
+"#,
+    );
+}
+
+#[test]
+fn insert_values_rows() {
+    explain_check(
+        "INSERT INTO t1 VALUES ('txt'::text::text::text, 2::decimal::unsigned::double::integer)",
+        r#"insert "t1" on conflict: fail
+    motion [policy: segment([ref("COLUMN_1"), ref("COLUMN_2")])]
+        values
+            value row (data=ROW('txt'::string, 2::integer))
+execution options:
+    vdbe_max_steps = 45000
+    vtable_max_rows = 5000
+"#,
+    );
+}
+
+#[test]
+fn select_selection() {
+    explain_check(
+        "SELECT * FROM t1 WHERE a = 1::int::unsigned",
+        r#"projection ("t1"."a"::string -> "a", "t1"."b"::integer -> "b")
+    selection ROW("t1"."a"::string) = ROW(1::unsigned)
+        scan "t1"
+execution options:
+    vdbe_max_steps = 45000
+    vtable_max_rows = 5000
+"#,
+    );
+}
+
+#[test]
+fn update_selection() {
+    explain_check(
+        "UPDATE t SET c = 2 WHERE a = 1::int::int",
+        r#"update "t"
+"c" = "col_0"
+    motion [policy: local]
+        projection (2::unsigned -> "col_0", "t"."b"::unsigned -> "col_1")
+            selection ROW("t"."a"::unsigned) = ROW(1::integer)
+                scan "t"
+execution options:
+    vdbe_max_steps = 45000
+    vtable_max_rows = 5000
+"#,
+    );
+}
+
+#[test]
+fn delete_selection() {
+    explain_check(
+        r#"DELETE FROM "t1" where "a" = 3::int::decimal"#,
+        &format!(
+            "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n",
+            r#"delete "t1""#,
+            r#"    motion [policy: local]"#,
+            r#"        projection ("t1"."a"::string -> "pk_col_0", "t1"."b"::integer -> "pk_col_1")"#,
+            r#"            selection ROW("t1"."a"::string) = ROW(3::decimal)"#,
+            r#"                scan "t1""#,
+            r#"execution options:"#,
+            r#"    vdbe_max_steps = 45000"#,
+            r#"    vtable_max_rows = 5000"#,
+        ),
+    );
+}
diff --git a/sbroad/sbroad-core/src/ir/explain/tests/concat.rs b/sbroad/sbroad-core/src/ir/explain/tests/concat.rs
index 1e65e6c1efeb134a9bb0b09098a8b9c1ee140f41..cc3026cbbf2ab30ae7ae902474a874f255aa4148 100644
--- a/sbroad/sbroad-core/src/ir/explain/tests/concat.rs
+++ b/sbroad/sbroad-core/src/ir/explain/tests/concat.rs
@@ -6,7 +6,7 @@ fn concat1_test() {
         r#"SELECT CAST('1' as string) || 'hello' FROM "t1""#,
         &format!(
             "{}\n{}\n{}\n{}\n{}\n",
-            r#"projection (ROW('1'::string::string) || ROW('hello'::string) -> "col_1")"#,
+            r#"projection (ROW('1'::string) || ROW('hello'::string) -> "col_1")"#,
             r#"    scan "t1""#,
             r#"execution options:"#,
             r#"    vdbe_max_steps = 45000"#,
@@ -22,7 +22,7 @@ fn concat2_test() {
         &format!(
             "{}\n{}\n{}\n{}\n{}\n{}\n",
             r#"projection ("t1"."a"::string -> "a")"#,
-            r#"    selection ROW(ROW(ROW('1'::string::string) || ROW("func"(('hello'::string))::integer)) || ROW('2'::string)) = ROW(42::unsigned)"#,
+            r#"    selection ROW(ROW(ROW('1'::string) || ROW("func"(('hello'::string))::integer)) || ROW('2'::string)) = ROW(42::unsigned)"#,
             r#"        scan "t1""#,
             r#"execution options:"#,
             r#"    vdbe_max_steps = 45000"#,
diff --git a/sbroad/sbroad-core/src/ir/transformation.rs b/sbroad/sbroad-core/src/ir/transformation.rs
index 333c762bff43af620f78a1ca78d2cb205410590d..49adc95ca4b6c2ec5a37680ba896b0b38f4fca2e 100644
--- a/sbroad/sbroad-core/src/ir/transformation.rs
+++ b/sbroad/sbroad-core/src/ir/transformation.rs
@@ -3,6 +3,7 @@
 //! Contains rule-based transformations.
 
 pub mod bool_in;
+pub mod cast_constants;
 pub mod dnf;
 pub mod equality_propagation;
 pub mod merge_tuples;
diff --git a/sbroad/sbroad-core/src/ir/transformation/cast_constants.rs b/sbroad/sbroad-core/src/ir/transformation/cast_constants.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d319ecdc38da1f04c2a4b26f06c571b3b45b9b92
--- /dev/null
+++ b/sbroad/sbroad-core/src/ir/transformation/cast_constants.rs
@@ -0,0 +1,97 @@
+use crate::{
+    errors::SbroadError,
+    ir::{
+        node::{
+            expression::{Expression, MutExpression},
+            Cast, Constant, Node, NodeId, Row,
+        },
+        relation::Type,
+        tree::traversal::{LevelNode, PostOrderWithFilter},
+        value::Value,
+        Plan,
+    },
+};
+
+fn apply_cast(plan: &Plan, child_id: NodeId, target_type: Type) -> Option<Value> {
+    match plan.get_expression_node(child_id).ok()? {
+        Expression::Constant(Constant { value }) => {
+            let value = value.clone();
+            value.cast(target_type).ok()
+        }
+        Expression::Cast(Cast {
+            child: cast_child,
+            to: cast_type,
+        }) => {
+            let cast_type = cast_type.as_relation_type();
+            let value = apply_cast(plan, *cast_child, cast_type);
+            // Note: We don't throw errors if casting fails.
+            // It's possible that some type and value combinations are missing,
+            // but in such cases, we simply skip this evaluation and continue with other casts.
+            // An optimization failure should not prevent the execution of the plan.
+            value.map(|x| x.cast(target_type).ok()).flatten()
+        }
+        _ => None,
+    }
+}
+
+impl Plan {
+    /// Evaluates cast constant expressions and replaces them with actual values in the plan.
+    ///
+    /// This function focuses on simplifying the plan by eliminating unnecessary casts in selection
+    /// expressions, enabling bucket filtering and in value rows, enabling local materialization.
+    pub fn cast_constants(&mut self) -> Result<(), SbroadError> {
+        // For simplicity, we only evaluate constants wrapped in Row,
+        // e.g., Row(Cast(Constant), Cast(Cast(Constant))).
+        // This approach includes the target cases from the function comment
+        // (selection expressions and values rows).
+        let rows_filter = |node_id| {
+            matches!(
+                self.get_node(node_id),
+                Ok(Node::Expression(Expression::Row(_)))
+            )
+        };
+        let mut subtree = PostOrderWithFilter::with_capacity(
+            |node| self.subtree_iter(node, false),
+            self.nodes.len(),
+            Box::new(rows_filter),
+        );
+
+        let top_id = self.get_top()?;
+        subtree.populate_nodes(top_id);
+        let row_ids = subtree.take_nodes();
+        drop(subtree);
+
+        let mut new_list = Vec::new();
+        for LevelNode(_, row_id) in row_ids {
+            // Clone row children list to overcome borrow checker.
+            new_list.clear();
+            if let Expression::Row(Row { list, .. }) = self.get_expression_node(row_id)? {
+                new_list.clone_from(list);
+            }
+
+            // Try to apply cast to constants, push new values in the plan and remember ids in a
+            // copy if row children list.
+            for row_child in new_list.iter_mut() {
+                if let Expression::Cast(Cast {
+                    child: cast_child,
+                    to,
+                }) = self.get_expression_node(*row_child)?
+                {
+                    let to = to.as_relation_type();
+                    if let Some(value) = apply_cast(self, *cast_child, to) {
+                        *row_child = self.add_const(value);
+                    }
+                }
+            }
+
+            // Change row children to the new ones with casts applied.
+            if let MutExpression::Row(Row { ref mut list, .. }) =
+                self.get_mut_expression_node(row_id)?
+            {
+                new_list.clone_into(list);
+            }
+        }
+
+        Ok(())
+    }
+}