From 5518eff3e6cdc09b51fceaf14367c4c637d08abe Mon Sep 17 00:00:00 2001 From: Mohammad Linjawi Date: Sat, 17 Jan 2026 15:00:18 +0300 Subject: [PATCH] [VL] Add ANSI mode support for cast string to boolean Implements ANSI-compliant string to boolean casting that throws exceptions for invalid inputs instead of returning null. Changes: - Add CastStringToBooleanAnsi.h with ANSI-compliant cast logic - Register spark_cast_string_to_boolean_ansi function in Velox - Update CastTransformer to route ANSI casts to custom function - Add literal evaluation optimization for compile-time casts - Include validation test suites for both ANSI and non-ANSI modes Contributes to issue #10134 (ANSI mode support) --- ...CastStringToBooleanAnsiValidateSuite.scala | 246 ++++++++++++++++++ .../CastStringToBooleanValidateSuite.scala | 230 ++++++++++++++++ .../functions/CastStringToBooleanAnsi.h | 108 ++++++++ .../functions/RegistrationAllFunctions.cc | 3 + .../UnaryExpressionTransformer.scala | 43 ++- 5 files changed, 626 insertions(+), 4 deletions(-) create mode 100644 backends-velox/src/test/scala/org/apache/gluten/functions/CastStringToBooleanAnsiValidateSuite.scala create mode 100644 backends-velox/src/test/scala/org/apache/gluten/functions/CastStringToBooleanValidateSuite.scala create mode 100644 cpp/velox/operators/functions/CastStringToBooleanAnsi.h diff --git a/backends-velox/src/test/scala/org/apache/gluten/functions/CastStringToBooleanAnsiValidateSuite.scala b/backends-velox/src/test/scala/org/apache/gluten/functions/CastStringToBooleanAnsiValidateSuite.scala new file mode 100644 index 000000000000..b88c7d31e6da --- /dev/null +++ b/backends-velox/src/test/scala/org/apache/gluten/functions/CastStringToBooleanAnsiValidateSuite.scala @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.gluten.functions + +import org.apache.gluten.config.GlutenConfig +import org.apache.gluten.execution.{FilterExecTransformer, GlutenQueryComparisonTest, ProjectExecTransformer, WholeStageTransformer} + +import org.apache.spark.SparkConf +import org.apache.spark.sql.DataFrame +import org.apache.spark.sql.internal.SQLConf + +import org.apache.logging.log4j.{Level, LogManager} +import org.apache.logging.log4j.core.Logger + +import java.util.UUID + +class CastStringToBooleanAnsiValidateSuite extends FunctionsValidateSuite { + + override protected def sparkConf: SparkConf = { + super.sparkConf + .set(GlutenConfig.GLUTEN_ANSI_FALLBACK_ENABLED.key, "false") + .set(SQLConf.ANSI_ENABLED.key, "true") + .set("spark.gluten.sql.columnar.backend.velox.glogSeverityLevel", "3") + } + + private def uniqueTableName(prefix: String): String = + s"${prefix}_${UUID.randomUUID().toString.replace("-", "")}" + + private def withStringValuesTable(tablePrefix: String, values: Seq[String])( + f: String => Unit): Unit = { + val tableName = uniqueTableName(tablePrefix) + withTable(tableName) { + sql(s"CREATE TABLE $tableName (str_col STRING) USING parquet") + val rows = values.map(value => s"($value)").mkString(", ") + sql(s"INSERT INTO $tableName VALUES $rows") + f(tableName) + } + } + + private def withLogLevels[T](level: Level, loggerNames: Seq[String])(f: => T): T = { + val loggers = loggerNames.map(name => LogManager.getLogger(name).asInstanceOf[Logger]) + val previousLevels = loggers.map(_.getLevel) + loggers.foreach(_.setLevel(level)) + try { + f + } finally { + loggers.zip(previousLevels).foreach { + case (logger, previousLevel) => logger.setLevel(previousLevel) + } + } + } + + private def substraitPlanJson(df: DataFrame): String = { + val planJson = df.queryExecution.executedPlan + .collectFirst { case stage: WholeStageTransformer => stage.substraitPlanJson } + assert( + planJson.nonEmpty, + s"Expected WholeStageTransformer in plan: ${df.queryExecution.executedPlan}") + planJson.get + } + + private def assertUsesAnsiStringToBooleanCast(df: DataFrame): Unit = { + val planJson = substraitPlanJson(df) + assert( + planJson.contains("spark_cast_string_to_boolean_ansi"), + s"Expected ANSI string-to-boolean cast in plan: ${df.queryExecution.executedPlan}") + } + + test("cast valid true strings to boolean") { + val validTrueStrings = + Seq("'t'", "'true'", "'y'", "'yes'", "'1'", "'T'", "'TRUE'", "'Y'", "'YES'") + + withStringValuesTable("test_cast_bool_true_ansi", validTrueStrings) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast valid false strings to boolean") { + val validFalseStrings = + Seq("'f'", "'false'", "'n'", "'no'", "'0'", "'F'", "'FALSE'", "'N'", "'NO'") + + withStringValuesTable("test_cast_bool_false_ansi", validFalseStrings) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast invalid string to boolean follows vanilla Spark behavior in ANSI mode") { + val invalidStrings = Seq("'invalid'", "'2'", "'maybe'", "''", "' '", "'yes '", "' no'") + + withLogLevels( + Level.OFF, + Seq( + "org.apache.spark.executor.Executor", + "org.apache.spark.scheduler.TaskSetManager", + "org.apache.spark.task.TaskResources")) { + invalidStrings.foreach { + str => + withStringValuesTable("test_cast_bool_invalid_ansi", Seq(str)) { + tableName => + val query = s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName" + val vanillaThrew = + try { + withSQLConf(vanillaSparkConfs(): _*) { + sql(query).collect() + } + false + } catch { + case _: Exception => true + } + + if (vanillaThrew) { + val df = sql(query) + GlutenQueryComparisonTest.checkFallBack(df, noFallback = true) + checkGlutenPlan[ProjectExecTransformer](df) + assertUsesAnsiStringToBooleanCast(df) + intercept[Exception] { + df.collect() + } + } else { + runQueryAndCompare(query) { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + } + } + } + + test("cast null string to boolean") { + withStringValuesTable("test_cast_bool_null_ansi", Seq("NULL")) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast string column to boolean with valid values") { + val tableName = uniqueTableName("test_cast_bool_valid") + withTable(tableName) { + sql(""" + CREATE TABLE %s (str_col STRING) + USING parquet + """.format(tableName)) + + sql(""" + INSERT INTO %s VALUES + ('true'), ('false'), ('1'), ('0'), ('yes'), ('no'), ('t'), ('f') + """.format(tableName)) + + runQueryAndCompare( + s"SELECT str_col, CAST(str_col AS BOOLEAN) FROM $tableName" + ) { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast string column to boolean with mixed valid and null values") { + val tableName = uniqueTableName("test_cast_bool_mixed") + withTable(tableName) { + sql(""" + CREATE TABLE %s (str_col STRING) + USING parquet + """.format(tableName)) + + sql(""" + INSERT INTO %s VALUES + ('true'), (NULL), ('false'), (NULL), ('1'), ('0') + """.format(tableName)) + + runQueryAndCompare( + s"SELECT str_col, CAST(str_col AS BOOLEAN) FROM $tableName" + ) { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast string to boolean in WHERE clause") { + val tableName = uniqueTableName("test_cast_bool_where") + withTable(tableName) { + sql(""" + CREATE TABLE %s (id INT, str_col STRING) + USING parquet + """.format(tableName)) + + sql(""" + INSERT INTO %s VALUES + (1, 'true'), (2, 'false'), (3, '1'), (4, '0'), (5, 'yes'), (6, 'no') + """.format(tableName)) + + runQueryAndCompare( + s"SELECT id, str_col FROM $tableName WHERE CAST(str_col AS BOOLEAN) = true" + ) { + checkGlutenPlan[FilterExecTransformer] + } + } + } + + test("cast string to boolean with case variations") { + val caseVariations = Seq( + "'TrUe'", + "'FaLsE'", + "'YeS'", + "'No'", + "'T'", + "'F'", + "'Y'", + "'N'" + ) + + withStringValuesTable("test_cast_bool_case_ansi", caseVariations) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + override protected def afterAll(): Unit = { + withLogLevels(Level.ERROR, Seq(this.getClass.getName)) { + super.afterAll() + } + } +} diff --git a/backends-velox/src/test/scala/org/apache/gluten/functions/CastStringToBooleanValidateSuite.scala b/backends-velox/src/test/scala/org/apache/gluten/functions/CastStringToBooleanValidateSuite.scala new file mode 100644 index 000000000000..6bc6d88fec53 --- /dev/null +++ b/backends-velox/src/test/scala/org/apache/gluten/functions/CastStringToBooleanValidateSuite.scala @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.gluten.functions + +import org.apache.gluten.config.GlutenConfig +import org.apache.gluten.execution.{FilterExecTransformer, ProjectExecTransformer, WholeStageTransformer} + +import org.apache.spark.SparkConf +import org.apache.spark.sql.DataFrame +import org.apache.spark.sql.internal.SQLConf + +import org.apache.logging.log4j.{Level, LogManager} +import org.apache.logging.log4j.core.Logger + +import java.util.UUID + +class CastStringToBooleanValidateSuite extends FunctionsValidateSuite { + + override protected def sparkConf: SparkConf = { + super.sparkConf + .set(GlutenConfig.GLUTEN_ANSI_FALLBACK_ENABLED.key, "false") + .set(SQLConf.ANSI_ENABLED.key, "false") // ANSI mode OFF + .set("spark.gluten.sql.columnar.backend.velox.glogSeverityLevel", "3") + } + + private def uniqueTableName(prefix: String): String = + s"${prefix}_${UUID.randomUUID().toString.replace("-", "")}" + + private def withStringValuesTable(tablePrefix: String, values: Seq[String])( + f: String => Unit): Unit = { + val tableName = uniqueTableName(tablePrefix) + withTable(tableName) { + sql(s"CREATE TABLE $tableName (str_col STRING) USING parquet") + val rows = values.map(value => s"($value)").mkString(", ") + sql(s"INSERT INTO $tableName VALUES $rows") + f(tableName) + } + } + + private def withLogLevels[T](level: Level, loggerNames: Seq[String])(f: => T): T = { + val loggers = loggerNames.map(name => LogManager.getLogger(name).asInstanceOf[Logger]) + val previousLevels = loggers.map(_.getLevel) + loggers.foreach(_.setLevel(level)) + try { + f + } finally { + loggers.zip(previousLevels).foreach { + case (logger, previousLevel) => logger.setLevel(previousLevel) + } + } + } + + private def substraitPlanJson(df: DataFrame): String = { + val planJson = df.queryExecution.executedPlan + .collectFirst { case stage: WholeStageTransformer => stage.substraitPlanJson } + assert( + planJson.nonEmpty, + s"Expected WholeStageTransformer in plan: ${df.queryExecution.executedPlan}") + planJson.get + } + + private def assertUsesGlutenStringToBooleanCast(df: DataFrame): Unit = { + val planJson = substraitPlanJson(df) + assert( + planJson.contains("\"cast\""), + s"Expected cast in Substrait plan: ${df.queryExecution.executedPlan}") + } + + test("cast invalid string to boolean returns null in non-ANSI mode") { + val invalidStrings = Seq("'invalid'", "'2'", "'maybe'", "''", "' '", "'yes '", "' no'") + + withStringValuesTable("test_cast_bool_invalid_non_ansi", invalidStrings) { + tableName => + runQueryAndCompare(s"SELECT str_col, CAST(str_col AS BOOLEAN) FROM $tableName") { + df => + checkGlutenPlan[ProjectExecTransformer](df) + assertUsesGlutenStringToBooleanCast(df) + } + } + } + + test("cast valid strings to boolean in non-ANSI mode") { + val validStrings = Seq("'true'", "'false'", "'1'", "'0'") + withStringValuesTable("test_cast_bool_valid_non_ansi", validStrings) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast string column with mixed valid and invalid values") { + val tableName = uniqueTableName("test_cast_bool_mixed_non_ansi") + withTable(tableName) { + sql(""" + CREATE TABLE %s (str_col STRING) + USING parquet + """.format(tableName)) + + sql(""" + INSERT INTO %s VALUES + ('true'), ('invalid'), ('false'), ('maybe'), ('1'), ('2'), ('0'), (NULL) + """.format(tableName)) + + runQueryAndCompare( + s"SELECT str_col, CAST(str_col AS BOOLEAN) FROM $tableName" + ) { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast string to boolean with whitespace in non-ANSI mode") { + val stringsWithWhitespace = Seq( + "' true'", + "'false '", + "' 1 '", + "' yes'", + "'no '" + ) + + withStringValuesTable("test_cast_bool_whitespace_non_ansi", stringsWithWhitespace) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast string to boolean in WHERE clause with invalid values") { + val tableName = uniqueTableName("test_cast_bool_where_non_ansi") + withTable(tableName) { + sql(""" + CREATE TABLE %s (id INT, str_col STRING) + USING parquet + """.format(tableName)) + + sql(""" + INSERT INTO %s VALUES + (1, 'true'), (2, 'invalid'), (3, 'false'), (4, 'maybe'), (5, '1'), (6, '0') + """.format(tableName)) + + // In non-ANSI mode, invalid strings cast to null, so they won't match + runQueryAndCompare( + s"""SELECT id, str_col FROM $tableName + |WHERE CAST(str_col AS BOOLEAN) IS NOT NULL""".stripMargin + ) { + checkGlutenPlan[FilterExecTransformer] + } + } + } + + test("cast all valid boolean string variations") { + val allValidStrings = Seq( + "'t'", + "'T'", + "'true'", + "'TRUE'", + "'True'", + "'TrUe'", + "'f'", + "'F'", + "'false'", + "'FALSE'", + "'False'", + "'FaLsE'", + "'y'", + "'Y'", + "'yes'", + "'YES'", + "'Yes'", + "'YeS'", + "'n'", + "'N'", + "'no'", + "'NO'", + "'No'", + "'nO'", + "'1'", + "'0'" + ) + + withStringValuesTable("test_cast_bool_all_valid_non_ansi", allValidStrings) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast null string to boolean in non-ANSI mode") { + withStringValuesTable("test_cast_bool_null_non_ansi", Seq("NULL")) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + test("cast empty and whitespace strings") { + val emptyAndWhitespace = Seq("''", "' '", "' '", "'\\t'", "'\\n'") + + withStringValuesTable("test_cast_bool_empty_non_ansi", emptyAndWhitespace) { + tableName => + runQueryAndCompare(s"SELECT CAST(str_col AS BOOLEAN) FROM $tableName") { + checkGlutenPlan[ProjectExecTransformer] + } + } + } + + override protected def afterAll(): Unit = { + withLogLevels(Level.ERROR, Seq(this.getClass.getName)) { + super.afterAll() + } + } +} diff --git a/cpp/velox/operators/functions/CastStringToBooleanAnsi.h b/cpp/velox/operators/functions/CastStringToBooleanAnsi.h new file mode 100644 index 000000000000..4ebcce409641 --- /dev/null +++ b/cpp/velox/operators/functions/CastStringToBooleanAnsi.h @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "velox/common/base/Exceptions.h" +#include "velox/functions/Macros.h" +#include "velox/type/StringView.h" + +namespace gluten { + +template +struct CastStringToBooleanAnsiFunction { + VELOX_DEFINE_FUNCTION_TYPES(T); + + FOLLY_ALWAYS_INLINE void call( + out_type& result, + const arg_type& input) { + const auto trimmed = trimAsciiWhitespace(input); + if (isTrueString(trimmed)) { + result = true; + return; + } + if (isFalseString(trimmed)) { + result = false; + return; + } + + VELOX_USER_FAIL( + "Invalid input syntax for boolean: '{}'", + std::string(input.data(), input.size())); + } + + private: + struct TrimmedView { + const char* data; + size_t size; + }; + + static FOLLY_ALWAYS_INLINE bool isAsciiWhitespace(char value) { + return static_cast(value) <= 0x20; + } + + static TrimmedView trimAsciiWhitespace( + const arg_type& input) { + const char* data = input.data(); + size_t size = input.size(); + size_t start = 0; + size_t end = size; + while (start < end && isAsciiWhitespace(data[start])) { + ++start; + } + while (end > start && isAsciiWhitespace(data[end - 1])) { + --end; + } + return TrimmedView{data + start, end - start}; + } + + static bool equalsIgnoreCase(const TrimmedView& input, const char* literal) { + const size_t literalSize = std::strlen(literal); + if (input.size != literalSize) { + return false; + } + for (size_t i = 0; i < literalSize; ++i) { + char c = input.data[i]; + if (c >= 'A' && c <= 'Z') { + c = static_cast(c + ('a' - 'A')); + } + if (c != literal[i]) { + return false; + } + } + return true; + } + + static bool isTrueString(const TrimmedView& input) { + return equalsIgnoreCase(input, "t") || equalsIgnoreCase(input, "true") || + equalsIgnoreCase(input, "y") || equalsIgnoreCase(input, "yes") || + equalsIgnoreCase(input, "1"); + } + + static bool isFalseString(const TrimmedView& input) { + return equalsIgnoreCase(input, "f") || equalsIgnoreCase(input, "false") || + equalsIgnoreCase(input, "n") || equalsIgnoreCase(input, "no") || + equalsIgnoreCase(input, "0"); + } +}; + +} // namespace gluten diff --git a/cpp/velox/operators/functions/RegistrationAllFunctions.cc b/cpp/velox/operators/functions/RegistrationAllFunctions.cc index dd1be7805c75..672a0660ff51 100644 --- a/cpp/velox/operators/functions/RegistrationAllFunctions.cc +++ b/cpp/velox/operators/functions/RegistrationAllFunctions.cc @@ -17,6 +17,7 @@ #include "operators/functions/RegistrationAllFunctions.h" #include "operators/functions/Arithmetic.h" +#include "operators/functions/CastStringToBooleanAnsi.h" #include "operators/functions/RowConstructorWithNull.h" #include "operators/functions/RowFunctionWithNull.h" #include "velox/expression/SpecialFormRegistry.h" @@ -56,6 +57,8 @@ void registerFunctionOverwrite() { velox::registerFunction({"round"}); velox::registerFunction({"round"}); velox::registerFunction({"round"}); + velox::registerFunction( + {"spark_cast_string_to_boolean_ansi"}); auto kRowConstructorWithNull = RowConstructorWithNullCallToSpecialForm::kRowConstructorWithNull; velox::exec::registerVectorFunction( diff --git a/gluten-substrait/src/main/scala/org/apache/gluten/expression/UnaryExpressionTransformer.scala b/gluten-substrait/src/main/scala/org/apache/gluten/expression/UnaryExpressionTransformer.scala index b762ec95b645..0cdead77ff18 100644 --- a/gluten-substrait/src/main/scala/org/apache/gluten/expression/UnaryExpressionTransformer.scala +++ b/gluten-substrait/src/main/scala/org/apache/gluten/expression/UnaryExpressionTransformer.scala @@ -17,6 +17,7 @@ package org.apache.gluten.expression import org.apache.gluten.backendsapi.BackendsApiManager +import org.apache.gluten.config.GlutenConfig import org.apache.gluten.exception.GlutenNotSupportException import org.apache.gluten.sql.shims.SparkShimLoader import org.apache.gluten.substrait.`type`.ListNode @@ -45,10 +46,44 @@ case class CastTransformer(substraitExprName: String, child: ExpressionTransform extends UnaryExpressionTransformer { override def doTransform(context: SubstraitContext): ExpressionNode = { val typeNode = ConverterUtils.getTypeNode(dataType, original.nullable) - ExpressionBuilder.makeCast( - typeNode, - child.doTransform(context), - SparkShimLoader.getSparkShims.withTryEvalMode(original)) + if ( + GlutenConfig.get.enableAnsiMode && + !SparkShimLoader.getSparkShims.withTryEvalMode(original) && + original.child.isInstanceOf[Literal] + ) { + val ansiEvalCast = + try { + val ctor = original.getClass.getConstructors.find(_.getParameterTypes.length == 4) + ctor + .map( + _.newInstance( + original.child, + original.dataType, + original.timeZoneId, + EvalMode.ANSI + ).asInstanceOf[Cast]) + .getOrElse(original) + } catch { + case _: Throwable => original + } + ansiEvalCast.eval() + } + val tryEval = SparkShimLoader.getSparkShims.withTryEvalMode(original) + val ansiEval = + SparkShimLoader.getSparkShims.withAnsiEvalMode(original) || GlutenConfig.get.enableAnsiMode + if ( + !tryEval && ansiEval && + original.child.dataType == StringType && + original.dataType == BooleanType + ) { + val functionId = context.registerFunction( + ConverterUtils + .makeFuncName("spark_cast_string_to_boolean_ansi", Seq(original.child.dataType))) + val expressionNodes = Lists.newArrayList(child.doTransform(context)) + return ExpressionBuilder.makeScalarFunction(functionId, expressionNodes, typeNode) + } + val isTryCast = tryEval || !ansiEval + ExpressionBuilder.makeCast(typeNode, child.doTransform(context), isTryCast) } }