diff --git a/sbroad/sbroad-core/src/executor.rs b/sbroad/sbroad-core/src/executor.rs index 7d219ec286e04833de2c31f4c98b86c89d92ace2..039a197479a666023fd6a64fbb0e3243f7ac5a4a 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/engine/helpers.rs b/sbroad/sbroad-core/src/executor/engine/helpers.rs index d1d880d4fc5071819d3462b06dbcb937d11f0b9b..3321911e4a55607dca6eeaf28e55495da65dca26 100644 --- a/sbroad/sbroad-core/src/executor/engine/helpers.rs +++ b/sbroad/sbroad-core/src/executor/engine/helpers.rs @@ -649,7 +649,7 @@ pub fn build_insert_args<'t>( )), ) })?; - insert_tuple.push(value.cast(table_type)?); + insert_tuple.push(value.cast_and_encode(table_type)?); } TupleBuilderCommand::SetValue(value) => { insert_tuple.push(EncodedValue::Ref(MsgPackValue::from(value))); @@ -1693,7 +1693,7 @@ fn execute_sharded_update( )), ) })?; - insert_tuple.push(value.cast(table_type)?); + insert_tuple.push(value.cast_and_encode(table_type)?); } TupleBuilderCommand::CalculateBucketId(_) => { insert_tuple.push(EncodedValue::Ref(MsgPackValue::Unsigned(bucket_id))); @@ -1784,7 +1784,7 @@ pub fn build_update_args<'t>( )), ) })?; - key_tuple.push(value.cast(table_type)?); + key_tuple.push(value.cast_and_encode(table_type)?); } TupleBuilderCommand::UpdateColToCastedPos(table_col, pos, table_type) => { let value = vt_tuple.get(*pos).ok_or_else(|| { @@ -1798,7 +1798,7 @@ pub fn build_update_args<'t>( let op = [ EncodedValue::Ref(MsgPackValue::from(eq_op())), EncodedValue::Owned(LuaValue::Unsigned(*table_col as u64)), - value.cast(table_type)?, + value.cast_and_encode(table_type)?, ]; ops.push(op); } diff --git a/sbroad/sbroad-core/src/executor/result.rs b/sbroad/sbroad-core/src/executor/result.rs index fe274ce97c6d5e1702b412c0c6251017cc3ddb60..132f08004c8e6988bc90690bed4f810666b2a3d3 100644 --- a/sbroad/sbroad-core/src/executor/result.rs +++ b/sbroad/sbroad-core/src/executor/result.rs @@ -167,7 +167,7 @@ impl ProducerResult { if value.get_type() == column.r#type { tuple.push(value); } else { - tuple.push(Value::from(value.cast(&column.r#type)?)); + tuple.push(value.cast(column.r#type)?); } } data.push(tuple); 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/executor/vtable.rs b/sbroad/sbroad-core/src/executor/vtable.rs index 817afb0033aea559a45b66ac9f2746a87691a244..2152b2d28cf6c613bc8d2cbb2324d7935f2c25f1 100644 --- a/sbroad/sbroad-core/src/executor/vtable.rs +++ b/sbroad/sbroad-core/src/executor/vtable.rs @@ -194,7 +194,7 @@ impl VirtualTable { for tuple in self.get_mut_tuples() { for (i, v) in tuple.iter_mut().enumerate() { let (_, ty) = fixed_types.get(i).expect("Type expected."); - let cast_value = v.cast(ty)?; + let cast_value = v.cast_and_encode(ty)?; match cast_value { EncodedValue::Ref(_) => { // Value type is already ok. 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..924d1011b37717a4bf6d03aca3d2ab2cebaf6cd5 --- /dev/null +++ b/sbroad/sbroad-core/src/ir/explain/tests/cast_constants.rs @@ -0,0 +1,91 @@ +use crate::executor::{engine::mock::RouterRuntimeMock, Query}; +use pretty_assertions::assert_eq; + +fn assert_expain_matches(sql: &str, expected_explain: &str) { + let metadata = &RouterRuntimeMock::new(); + let mut query = Query::new(metadata, sql, vec![]).unwrap(); + let actual_explain = query.to_explain().unwrap(); + assert_eq!(actual_explain.as_str(), expected_explain); +} + +#[test] +fn select_values_rows() { + assert_expain_matches( + "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 +buckets = any +"#, + ); +} + +#[test] +fn insert_values_rows() { + assert_expain_matches( + "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 +buckets = unknown +"#, + ); +} + +#[test] +fn select_selection() { + assert_expain_matches( + "SELECT * FROM t3 WHERE a = 'kek'::text::text::text", + r#"projection ("t3"."a"::string -> "a", "t3"."b"::integer -> "b") + selection ROW("t3"."a"::string) = ROW('kek'::string) + scan "t3" +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +buckets = [1610] +"#, + ); +} + +#[test] +fn update_selection() { + assert_expain_matches( + "UPDATE t SET c = 2 WHERE a = 1::int::int and b = 2::unsigned::decimal", + 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) and ROW("t"."b"::unsigned) = ROW(2::decimal) + scan "t" +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +buckets = [550] +"#, + ); +} + +#[test] +fn delete_selection() { + assert_expain_matches( + r#"DELETE FROM "t2" where "e" = 3::unsigned and "f" = 2::decimal"#, + r#"delete "t2" + motion [policy: local] + projection ("t2"."g"::unsigned -> "pk_col_0", "t2"."h"::unsigned -> "pk_col_1") + selection ROW("t2"."e"::unsigned) = ROW(3::unsigned) and ROW("t2"."f"::unsigned) = ROW(2::decimal) + scan "t2" +execution options: + vdbe_max_steps = 45000 + vtable_max_rows = 5000 +buckets = [9374] +"#, + ); +} 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..6e284214cb7e794af7bcf3ebc52b96f2e19c2f6e --- /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.and_then(|x| x.cast(target_type).ok()) + } + _ => 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(()) + } +} diff --git a/sbroad/sbroad-core/src/ir/value.rs b/sbroad/sbroad-core/src/ir/value.rs index 000a7bba0c289e05fc2df71f623e2ae2dd5447c4..18d85399fb6a90b50c0f1ec014802b7d99a3dc1b 100644 --- a/sbroad/sbroad-core/src/ir/value.rs +++ b/sbroad/sbroad-core/src/ir/value.rs @@ -780,111 +780,140 @@ impl Value { } } - /// Cast a value to a different type and wrap into encoded value. - /// If the target type is the same as the current type, the value - /// is returned by reference. Otherwise, the value is cloned. - /// - /// # Errors - /// - the value cannot be cast to the given type. + /// Cast a value to a different type. #[allow(clippy::too_many_lines)] - pub fn cast(&self, column_type: &Type) -> Result<EncodedValue, SbroadError> { - let cast_error = SbroadError::Invalid( - Entity::Value, - Some(format_smolstr!("Failed to cast {self} to {column_type}.")), - ); + pub fn cast(self, column_type: Type) -> Result<Self, SbroadError> { + fn cast_error(value: &Value, column_type: Type) -> SbroadError { + SbroadError::Invalid( + Entity::Value, + Some(format_smolstr!("Failed to cast {value} to {column_type}.")), + ) + } match column_type { - Type::Any => Ok(self.into()), + Type::Any => Ok(self), Type::Array | Type::Map => match self { - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, Type::Boolean => match self { - Value::Boolean(_) => Ok(self.into()), - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + Value::Boolean(_) => Ok(self), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, Type::Datetime => match self { - Value::Null => Ok(Value::Null.into()), - Value::Datetime(_) => Ok(self.into()), - _ => Err(cast_error), + Value::Null => Ok(Value::Null), + Value::Datetime(_) => Ok(self), + _ => Err(cast_error(&self, column_type)), }, Type::Decimal => match self { - Value::Decimal(_) => Ok(self.into()), - Value::Double(v) => Ok(Value::Decimal( - Decimal::from_str(&format!("{v}")).map_err(|_| cast_error)?, - ) - .into()), - Value::Integer(v) => Ok(Value::Decimal(Decimal::from(*v)).into()), - Value::Unsigned(v) => Ok(Value::Decimal(Decimal::from(*v)).into()), - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + Value::Decimal(_) => Ok(self), + Value::Double(ref v) => Ok(Value::Decimal( + Decimal::from_str(&format!("{v}")) + .map_err(|_| cast_error(&self, column_type))?, + )), + Value::Integer(v) => Ok(Value::Decimal(Decimal::from(v))), + Value::Unsigned(v) => Ok(Value::Decimal(Decimal::from(v))), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, Type::Double => match self { - Value::Double(_) => Ok(self.into()), - Value::Decimal(v) => Ok(Value::Double(Double::from_str(&format!("{v}"))?).into()), - Value::Integer(v) => Ok(Value::Double(Double::from(*v)).into()), - Value::Unsigned(v) => Ok(Value::Double(Double::from(*v)).into()), - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + Value::Double(_) => Ok(self), + Value::Decimal(v) => Ok(Value::Double(Double::from_str(&format!("{v}"))?)), + Value::Integer(v) => Ok(Value::Double(Double::from(v))), + Value::Unsigned(v) => Ok(Value::Double(Double::from(v))), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, Type::Integer => match self { - Value::Integer(_) => Ok(self.into()), - Value::Decimal(v) => Ok(Value::Integer(v.to_i64().ok_or(cast_error)?).into()), - Value::Double(v) => v + Value::Integer(_) => Ok(self), + Value::Decimal(v) => Ok(Value::Integer( + v.to_i64().ok_or_else(|| cast_error(&self, column_type))?, + )), + Value::Double(ref v) => v .to_string() .parse::<i64>() .map(Value::Integer) - .map(EncodedValue::from) - .map_err(|_| cast_error), - Value::Unsigned(v) => { - Ok(Value::Integer(i64::try_from(*v).map_err(|_| cast_error)?).into()) - } - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + .map_err(|_| cast_error(&self, column_type)), + Value::Unsigned(v) => Ok(Value::Integer( + i64::try_from(v).map_err(|_| cast_error(&self, column_type))?, + )), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, Type::Scalar => match self { - Value::Tuple(_) => Err(cast_error), - _ => Ok(self.into()), + Value::Tuple(_) => Err(cast_error(&self, column_type)), + _ => Ok(self), }, Type::String => match self { - Value::String(_) => Ok(self.into()), - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + Value::String(_) => Ok(self), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, Type::Uuid => match self { - Value::Uuid(_) => Ok(self.into()), - Value::String(v) => { - Ok(Value::Uuid(Uuid::parse_str(v).map_err(|_| cast_error)?).into()) - } - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + Value::Uuid(_) => Ok(self), + Value::String(ref v) => Ok(Value::Uuid( + Uuid::parse_str(v).map_err(|_| cast_error(&self, column_type))?, + )), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, Type::Number => match self { Value::Integer(_) | Value::Decimal(_) | Value::Double(_) | Value::Unsigned(_) => { - Ok(self.into()) + Ok(self) } - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, Type::Unsigned => match self { - Value::Unsigned(_) => Ok(self.into()), - Value::Integer(v) => { - Ok(Value::Unsigned(u64::try_from(*v).map_err(|_| cast_error)?).into()) - } - Value::Decimal(v) => Ok(Value::Unsigned(v.to_u64().ok_or(cast_error)?).into()), - Value::Double(v) => v + Value::Unsigned(_) => Ok(self), + Value::Integer(v) => Ok(Value::Unsigned( + u64::try_from(v).map_err(|_| cast_error(&self, column_type))?, + )), + Value::Decimal(v) => Ok(Value::Unsigned( + v.to_u64().ok_or_else(|| cast_error(&self, column_type))?, + )), + Value::Double(ref v) => v .to_string() .parse::<u64>() .map(Value::Unsigned) - .map(EncodedValue::from) - .map_err(|_| cast_error), - Value::Null => Ok(Value::Null.into()), - _ => Err(cast_error), + .map_err(|_| cast_error(&self, column_type)), + Value::Null => Ok(Value::Null), + _ => Err(cast_error(&self, column_type)), }, } } + /// Cast a value to a different type and wrap into encoded value. + /// If the target type is the same as the current type, the value + /// is returned by reference. Otherwise, the value is cloned. + /// + /// # Errors + /// - the value cannot be cast to the given type. + #[allow(clippy::too_many_lines)] + pub fn cast_and_encode(&self, column_type: &Type) -> Result<EncodedValue, SbroadError> { + // First, try variants returning EncodedValue::Ref to avoid cloning. + match (column_type, self) { + (Type::Any | Type::Scalar, value) => return Ok(value.into()), + (Type::Boolean, Value::Boolean(_)) => return Ok(self.into()), + (Type::Datetime, Value::Datetime(_)) => return Ok(self.into()), + (Type::Decimal, Value::Decimal(_)) => return Ok(self.into()), + (Type::Double, Value::Double(_)) => return Ok(self.into()), + (Type::Integer, Value::Integer(_)) => return Ok(self.into()), + (Type::String, Value::String(_)) => return Ok(self.into()), + (Type::Uuid, Value::Uuid(_)) => return Ok(self.into()), + (Type::Unsigned, Value::Unsigned(_)) => return Ok(self.into()), + ( + Type::Number, + Value::Integer(_) | Value::Decimal(_) | Value::Double(_) | Value::Unsigned(_), + ) => return Ok(self.into()), + _ => (), + } + + // Then, apply cast with clone. + self.clone().cast(*column_type).map(Into::into) + } + #[must_use] pub fn get_type(&self) -> Type { match self { diff --git a/sbroad/sbroad-core/src/ir/value/tests.rs b/sbroad/sbroad-core/src/ir/value/tests.rs index a2d019fbdbe3195bb3733b32772f391aba6e539a..4067eaf45c7110a24a3cda09cbc75324437657b9 100644 --- a/sbroad/sbroad-core/src/ir/value/tests.rs +++ b/sbroad/sbroad-core/src/ir/value/tests.rs @@ -34,7 +34,9 @@ fn uuid() { Some(TrivalentOrdering::Equal) ); assert_eq!( - Value::String(uid.to_string()).cast(&Type::Uuid).is_ok(), + Value::String(uid.to_string()) + .cast_and_encode(&Type::Uuid) + .is_ok(), true ); assert_eq!(v_uid.partial_cmp(&Value::String(t_uid_2.to_string())), None); @@ -44,7 +46,7 @@ fn uuid() { fn uuid_negative() { assert_eq!( Value::String("hello".to_string()) - .cast(&Type::Uuid) + .cast_and_encode(&Type::Uuid) .unwrap_err(), SbroadError::Invalid( Entity::Value, diff --git a/src/sql.rs b/src/sql.rs index 11ca2985af7ec96b9101cfbfd726e036f1219018..7618f5e84dfc5b142336ddbd48e9c9eca7249ef3 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1135,7 +1135,7 @@ fn alter_system_ir_node_to_op_or_result( else { return Err(Error::other(format!("unknown parameter: '{param_name}'"))); }; - let Ok(casted_value) = param_value.cast(&expected_type) else { + let Ok(casted_value) = param_value.cast_and_encode(&expected_type) else { let actual_type = value_type_str(param_value); return Err(Error::other(format!( "invalid value for '{param_name}' expected {expected_type}, got {actual_type}",