Repository: apache/incubator-gluten
Pull request #9394: [GLUTEN-9392] [VL] Support casting array element to varchar
Author: login: ArnavBalyan
Labels: name: CORE
description: works for Gluten Core
name: VELOX
Created: 2025-04-22T10:41:17Z | Merged: 2025-04-28T15:36:44Z

## What changes were proposed in this pull request?
 - Support array cast to varchar. 
 - More cast types support will be added in the upcoming PRs.

## How was this patch tested?
 - Unit tests added
 - Spark existing unit tests




Diff:
diff --git a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
index 15b6949c78b1..a91c96f039c8 100644
--- a/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
+++ b/cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
@@ -232,6 +232,22 @@ bool SubstraitToVeloxPlanValidator::validateScalarFunction(
   return true;
 }
 
+bool isSupportedArrayCast(const TypePtr& fromType, const TypePtr& toType) {
+  static const std::unordered_set<TypeKind> kAllowedArrayElementKinds = {
+      TypeKind::DOUBLE,
+      TypeKind::BOOLEAN,
+      TypeKind::TIMESTAMP,
+  };
+
+  // https://github.com/apache/incubator-gluten/issues/9392
+  // is currently WIP to add support for other types.
+  if (toType->isVarchar()) {
+    return kAllowedArrayElementKinds.count(fromType->kind()) > 0;
+  }
+
+  return false;
+}
+
 bool SubstraitToVeloxPlanValidator::isAllowedCast(const TypePtr& fromType, const TypePtr& toType) {
   // Currently cast is not allowed for various categories, code has a bunch of rules
   // which define the cast categories and if we should offload to velox. Currently,
@@ -275,6 +291,17 @@ bool SubstraitToVeloxPlanValidator::isAllowedCast(const TypePtr& fromType, const
     return false;
   }
 
+  if (fromType->isArray() && toType->isArray()) {
+    const auto& toElem = toType->asArray().elementType();
+    const auto& fromElem = fromType->asArray().elementType();
+
+    if (!isAllowedCast(fromElem, toElem)) {
+      return false;
+    }
+
+    return isSupportedArrayCast(fromElem, toElem);
+  }
+
   // Limited support for Complex types.
   if (fromType->isArray() || fromType->isMap() || fromType->isRow()) {
     return false;
diff --git a/gluten-ut/spark32/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala b/gluten-ut/spark32/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
index cdd250968081..ba21c3d6f491 100644
--- a/gluten-ut/spark32/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
+++ b/gluten-ut/spark32/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
@@ -609,6 +609,11 @@ class ClickHouseTestSettings extends BackendTestSettings {
     .exclude("SPARK-34727: cast from float II")
     .exclude("SPARK-35720: cast invalid string input to timestamp without time zone")
     .exclude("Cast should output null for invalid strings when ANSI is not enabled.")
+    .exclude("cast array element from integer to string")
+    .exclude("cast array element from double to string")
+    .exclude("cast array element from bool to string")
+    .exclude("cast array element from date to string")
+    .exclude("cast array from timestamp to string")
   enableSuite[GlutenCastSuiteWithAnsiModeOn]
     .exclude("null cast")
     .exclude("cast string to date")
diff --git a/gluten-ut/spark32/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala b/gluten-ut/spark32/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
index 31446aba0830..235c761062ec 100644
--- a/gluten-ut/spark32/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
+++ b/gluten-ut/spark32/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
@@ -43,6 +43,69 @@ class GlutenCastSuite extends CastSuite with GlutenTestsTrait {
   UDTRegistration.register(classOf[IExampleBaseType].getName, classOf[ExampleBaseTypeUDT].getName)
   UDTRegistration.register(classOf[IExampleSubType].getName, classOf[ExampleSubTypeUDT].getName)
 
+  test("cast array element from integer to string") {
+    val inputWithNull = Literal.create(Seq(1, null, 3), ArrayType(IntegerType))
+    val expectedWithNull = Seq("1", null, "3")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Int], ArrayType(IntegerType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+
+    val inputNegative = Literal.create(Seq(-1, 0, 999999), ArrayType(IntegerType))
+    val expectedNegative = Seq("-1", "0", "999999")
+    checkEvaluation(cast(inputNegative, ArrayType(StringType)), expectedNegative)
+  }
+
+  test("cast array element from double to string") {
+    val inputWithNull = Literal.create(Seq(1.1, null, 3.3), ArrayType(DoubleType))
+    val expectedWithNull = Seq("1.1", null, "3.3")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val inputScientific = Literal.create(Seq(1.23e4, -5.67e-3), ArrayType(DoubleType))
+    val expectedScientific = Seq("12300.0", "-0.00567")
+    checkEvaluation(cast(inputScientific, ArrayType(StringType)), expectedScientific)
+  }
+
+  test("cast array element from bool to string") {
+    val inputWithNull = Literal.create(Seq(true, null, false), ArrayType(BooleanType))
+    val expectedWithNull = Seq("true", null, "false")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Boolean], ArrayType(BooleanType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+  }
+
+  test("cast array element from date to string") {
+    val inputWithNull = Literal.create(
+      Seq(Date.valueOf("2024-01-01"), null, Date.valueOf("2024-01-03")),
+      ArrayType(DateType)
+    )
+    val expectedWithNull = Seq("2024-01-01", null, "2024-01-03")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val inputLeapYear = Literal.create(
+      Seq(Date.valueOf("2020-02-29")),
+      ArrayType(DateType)
+    )
+    val expectedLeapYear = Seq("2020-02-29")
+    checkEvaluation(cast(inputLeapYear, ArrayType(StringType)), expectedLeapYear)
+  }
+
+  test("cast array from timestamp to string") {
+    val inputWithNull = Literal.create(
+      Seq(Timestamp.valueOf("2023-01-01 12:00:00"), null, Timestamp.valueOf("2023-12-31 23:59:59")),
+      ArrayType(TimestampType)
+    )
+    val expectedWithNull = Seq("2023-01-01 12:00:00", null, "2023-12-31 23:59:59")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Timestamp], ArrayType(TimestampType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+  }
+
   testGluten("missing cases - from boolean") {
     (DataTypeTestUtils.numericTypeWithoutDecimal + BooleanType).foreach {
       t =>
diff --git a/gluten-ut/spark33/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala b/gluten-ut/spark33/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
index 0e4666c604c8..a41491412ab8 100644
--- a/gluten-ut/spark33/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
+++ b/gluten-ut/spark33/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
@@ -626,6 +626,11 @@ class ClickHouseTestSettings extends BackendTestSettings {
     .exclude("SPARK-36924: Cast YearMonthIntervalType to IntegralType")
     .exclude("SPARK-36924: Cast IntegralType to YearMonthIntervalType")
     .exclude("Cast should output null for invalid strings when ANSI is not enabled.")
+    .exclude("cast array element from integer to string")
+    .exclude("cast array element from double to string")
+    .exclude("cast array element from bool to string")
+    .exclude("cast array element from date to string")
+    .exclude("cast array from timestamp to string")
   enableSuite[GlutenCastSuiteWithAnsiModeOn]
     .exclude("null cast")
     .exclude("cast string to date")
diff --git a/gluten-ut/spark33/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala b/gluten-ut/spark33/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
index 2bc2f8b1a28d..13eb8fa14ca0 100644
--- a/gluten-ut/spark33/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
+++ b/gluten-ut/spark33/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
@@ -43,6 +43,69 @@ class GlutenCastSuite extends CastSuite with GlutenTestsTrait {
   UDTRegistration.register(classOf[IExampleBaseType].getName, classOf[ExampleBaseTypeUDT].getName)
   UDTRegistration.register(classOf[IExampleSubType].getName, classOf[ExampleSubTypeUDT].getName)
 
+  test("cast array element from integer to string") {
+    val inputWithNull = Literal.create(Seq(1, null, 3), ArrayType(IntegerType))
+    val expectedWithNull = Seq("1", null, "3")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Int], ArrayType(IntegerType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+
+    val inputNegative = Literal.create(Seq(-1, 0, 999999), ArrayType(IntegerType))
+    val expectedNegative = Seq("-1", "0", "999999")
+    checkEvaluation(cast(inputNegative, ArrayType(StringType)), expectedNegative)
+  }
+
+  test("cast array element from double to string") {
+    val inputWithNull = Literal.create(Seq(1.1, null, 3.3), ArrayType(DoubleType))
+    val expectedWithNull = Seq("1.1", null, "3.3")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val inputScientific = Literal.create(Seq(1.23e4, -5.67e-3), ArrayType(DoubleType))
+    val expectedScientific = Seq("12300.0", "-0.00567")
+    checkEvaluation(cast(inputScientific, ArrayType(StringType)), expectedScientific)
+  }
+
+  test("cast array element from bool to string") {
+    val inputWithNull = Literal.create(Seq(true, null, false), ArrayType(BooleanType))
+    val expectedWithNull = Seq("true", null, "false")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Boolean], ArrayType(BooleanType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+  }
+
+  test("cast array element from date to string") {
+    val inputWithNull = Literal.create(
+      Seq(Date.valueOf("2024-01-01"), null, Date.valueOf("2024-01-03")),
+      ArrayType(DateType)
+    )
+    val expectedWithNull = Seq("2024-01-01", null, "2024-01-03")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val inputLeapYear = Literal.create(
+      Seq(Date.valueOf("2020-02-29")), // Leap day
+      ArrayType(DateType)
+    )
+    val expectedLeapYear = Seq("2020-02-29")
+    checkEvaluation(cast(inputLeapYear, ArrayType(StringType)), expectedLeapYear)
+  }
+
+  test("cast array from timestamp to string") {
+    val inputWithNull = Literal.create(
+      Seq(Timestamp.valueOf("2023-01-01 12:00:00"), null, Timestamp.valueOf("2023-12-31 23:59:59")),
+      ArrayType(TimestampType)
+    )
+    val expectedWithNull = Seq("2023-01-01 12:00:00", null, "2023-12-31 23:59:59")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Timestamp], ArrayType(TimestampType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+  }
+
   testGluten("missing cases - from boolean") {
     (DataTypeTestUtils.numericTypeWithoutDecimal + BooleanType).foreach {
       t =>
diff --git a/gluten-ut/spark34/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala b/gluten-ut/spark34/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
index 9c0d966ef4c3..d8527ea639f1 100644
--- a/gluten-ut/spark34/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
+++ b/gluten-ut/spark34/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
@@ -565,6 +565,11 @@ class ClickHouseTestSettings extends BackendTestSettings {
     .exclude("SPARK-36924: Cast IntegralType to DayTimeIntervalType")
     .exclude("SPARK-36924: Cast YearMonthIntervalType to IntegralType")
     .exclude("SPARK-36924: Cast IntegralType to YearMonthIntervalType")
+    .exclude("cast array element from integer to string")
+    .exclude("cast array element from double to string")
+    .exclude("cast array element from bool to string")
+    .exclude("cast array element from date to string")
+    .exclude("cast array from timestamp to string")
   enableSuite[GlutenCollectionExpressionsSuite]
     .exclude("ArraysZip") // wait for https://github.com/ClickHouse/ClickHouse/pull/69576
     .exclude("Sequence of numbers")
diff --git a/gluten-ut/spark34/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala b/gluten-ut/spark34/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
index e948601f2948..07d35888854a 100644
--- a/gluten-ut/spark34/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
+++ b/gluten-ut/spark34/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
@@ -44,6 +44,56 @@ class GlutenCastSuite extends CastWithAnsiOffSuite with GlutenTestsTrait {
   UDTRegistration.register(classOf[IExampleBaseType].getName, classOf[ExampleBaseTypeUDT].getName)
   UDTRegistration.register(classOf[IExampleSubType].getName, classOf[ExampleSubTypeUDT].getName)
 
+  test("cast array element from integer to string") {
+    val inputWithNull = Literal.create(Seq(1, null, 3), ArrayType(IntegerType))
+    val expectedWithNull = Seq("1", null, "3")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Int], ArrayType(IntegerType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+
+    val inputNegative = Literal.create(Seq(-1, 0, 999999), ArrayType(IntegerType))
+    val expectedNegative = Seq("-1", "0", "999999")
+    checkEvaluation(cast(inputNegative, ArrayType(StringType)), expectedNegative)
+  }
+
+  test("cast array element from double to string") {
+    val inputWithNull = Literal.create(Seq(1.1, null, 3.3), ArrayType(DoubleType))
+    val expectedWithNull = Seq("1.1", null, "3.3")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val inputScientific = Literal.create(Seq(1.23e4, -5.67e-3), ArrayType(DoubleType))
+    val expectedScientific = Seq("12300.0", "-0.00567")
+    checkEvaluation(cast(inputScientific, ArrayType(StringType)), expectedScientific)
+  }
+
+  test("cast array element from bool to string") {
+    val inputWithNull = Literal.create(Seq(true, null, false), ArrayType(BooleanType))
+    val expectedWithNull = Seq("true", null, "false")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Boolean], ArrayType(BooleanType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+  }
+
+  test("cast array element from date to string") {
+    val inputWithNull = Literal.create(
+      Seq(Date.valueOf("2024-01-01"), null, Date.valueOf("2024-01-03")),
+      ArrayType(DateType)
+    )
+    val expectedWithNull = Seq("2024-01-01", null, "2024-01-03")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val inputLeapYear = Literal.create(
+      Seq(Date.valueOf("2020-02-29")),
+      ArrayType(DateType)
+    )
+    val expectedLeapYear = Seq("2020-02-29")
+    checkEvaluation(cast(inputLeapYear, ArrayType(StringType)), expectedLeapYear)
+  }
+
   testGluten("missing cases - from boolean") {
     (DataTypeTestUtils.numericTypeWithoutDecimal + BooleanType).foreach {
       case t @ BooleanType =>
@@ -55,6 +105,19 @@ class GlutenCastSuite extends CastWithAnsiOffSuite with GlutenTestsTrait {
     }
   }
 
+  test("cast array from timestamp to string") {
+    val inputWithNull = Literal.create(
+      Seq(Timestamp.valueOf("2023-01-01 12:00:00"), null, Timestamp.valueOf("2023-12-31 23:59:59")),
+      ArrayType(TimestampType)
+    )
+    val expectedWithNull = Seq("2023-01-01 12:00:00", null, "2023-12-31 23:59:59")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Timestamp], ArrayType(TimestampType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+  }
+
   testGluten("missing cases - from byte") {
     DataTypeTestUtils.numericTypeWithoutDecimal.foreach {
       t =>
diff --git a/gluten-ut/spark35/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala b/gluten-ut/spark35/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
index 0caa3008e1b3..11d3d6db6af2 100644
--- a/gluten-ut/spark35/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
+++ b/gluten-ut/spark35/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
@@ -382,6 +382,11 @@ class ClickHouseTestSettings extends BackendTestSettings {
     .exclude("SPARK-39749: cast Decimal to string")
     .exclude("SPARK-42176: cast boolean to timestamp")
     .exclude("null cast #2")
+    .exclude("cast array element from integer to string")
+    .exclude("cast array element from double to string")
+    .exclude("cast array element from bool to string")
+    .exclude("cast array element from date to string")
+    .exclude("cast array from timestamp to string")
   enableSuite[GlutenCoalesceShufflePartitionsSuite]
     .excludeByPrefix("determining the number of reducers")
     .excludeCH("SPARK-46590 adaptive query execution works correctly with broadcast join and union")
diff --git a/gluten-ut/spark35/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala b/gluten-ut/spark35/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
index da611ca846ba..7debbf0c805f 100644
--- a/gluten-ut/spark35/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
+++ b/gluten-ut/spark35/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
@@ -44,6 +44,69 @@ class GlutenCastSuite extends CastWithAnsiOffSuite with GlutenTestsTrait {
   UDTRegistration.register(classOf[IExampleBaseType].getName, classOf[ExampleBaseTypeUDT].getName)
   UDTRegistration.register(classOf[IExampleSubType].getName, classOf[ExampleSubTypeUDT].getName)
 
+  test("cast array element from integer to string") {
+    val inputWithNull = Literal.create(Seq(1, null, 3), ArrayType(IntegerType))
+    val expectedWithNull = Seq("1", null, "3")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Int], ArrayType(IntegerType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+
+    val inputNegative = Literal.create(Seq(-1, 0, 999999), ArrayType(IntegerType))
+    val expectedNegative = Seq("-1", "0", "999999")
+    checkEvaluation(cast(inputNegative, ArrayType(StringType)), expectedNegative)
+  }
+
+  test("cast array element from double to string") {
+    val inputWithNull = Literal.create(Seq(1.1, null, 3.3), ArrayType(DoubleType))
+    val expectedWithNull = Seq("1.1", null, "3.3")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val inputScientific = Literal.create(Seq(1.23e4, -5.67e-3), ArrayType(DoubleType))
+    val expectedScientific = Seq("12300.0", "-0.00567")
+    checkEvaluation(cast(inputScientific, ArrayType(StringType)), expectedScientific)
+  }
+
+  test("cast array element from bool to string") {
+    val inputWithNull = Literal.create(Seq(true, null, false), ArrayType(BooleanType))
+    val expectedWithNull = Seq("true", null, "false")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Boolean], ArrayType(BooleanType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+  }
+
+  test("cast array element from date to string") {
+    val inputWithNull = Literal.create(
+      Seq(Date.valueOf("2024-01-01"), null, Date.valueOf("2024-01-03")),
+      ArrayType(DateType)
+    )
+    val expectedWithNull = Seq("2024-01-01", null, "2024-01-03")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val inputLeapYear = Literal.create(
+      Seq(Date.valueOf("2020-02-29")),
+      ArrayType(DateType)
+    )
+    val expectedLeapYear = Seq("2020-02-29")
+    checkEvaluation(cast(inputLeapYear, ArrayType(StringType)), expectedLeapYear)
+  }
+
+  test("cast array from timestamp to string") {
+    val inputWithNull = Literal.create(
+      Seq(Timestamp.valueOf("2023-01-01 12:00:00"), null, Timestamp.valueOf("2023-12-31 23:59:59")),
+      ArrayType(TimestampType)
+    )
+    val expectedWithNull = Seq("2023-01-01 12:00:00", null, "2023-12-31 23:59:59")
+    checkEvaluation(cast(inputWithNull, ArrayType(StringType)), expectedWithNull)
+
+    val emptyInput = Literal.create(Seq.empty[Timestamp], ArrayType(TimestampType))
+    val expectedEmpty = Seq.empty[String]
+    checkEvaluation(cast(emptyInput, ArrayType(StringType)), expectedEmpty)
+  }
+
   testGluten("missing cases - from boolean") {
     (DataTypeTestUtils.numericTypeWithoutDecimal ++ Set(BooleanType)).foreach {
       t =>


Reviews:
user: login: philo-he
body: Please help confirm that involved Spark tests are enabled in Gluten.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T17:33:56Z
user: login: ArnavBalyan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T04:52:28Z
user: login: ArnavBalyan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T04:52:46Z
user: login: ArnavBalyan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T04:52:56Z
user: login: ArnavBalyan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T04:54:15Z
user: login: philo-he
body: @ArnavBalyan, thanks for your iterating! Two comments that may need your clarification.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T03:07:47Z
user: login: ArnavBalyan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T17:37:05Z
user: login: ArnavBalyan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T17:40:00Z
user: login: philo-he
body: Looks good! Please fix CI failure if it is related to this pr. Thanks!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-27T08:09:21Z

Review comments:
diff_hunk: @@ -232,6 +232,18 @@ bool SubstraitToVeloxPlanValidator::validateScalarFunction(
   return true;
 }
 
+bool isValidArrayCast(const TypePtr& fromType, const TypePtr& toType) {
path: cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
user: login: philo-he
body: Maybe, better to name it like `isSupportedArrayCast`. "valid" give me a feeling that some cast cases are acceptable in Spark semantic and some are not. But here, we just check whether it is supported in native execution.
created_at: 2025-04-22T17:06:53Z
author_association: MEMBER
diff_hunk: @@ -43,6 +43,56 @@ class GlutenCastSuite extends CastSuite with GlutenTestsTrait {
   UDTRegistration.register(classOf[IExampleBaseType].getName, classOf[ExampleBaseTypeUDT].getName)
   UDTRegistration.register(classOf[IExampleSubType].getName, classOf[ExampleSubTypeUDT].getName)
 
+  test("cast array from IntegerType to StringType") {
path: gluten-ut/spark33/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
user: login: philo-he
body: Are these tests designed by yourself? Can we put these new tests into `gluten-ut/test`? Generally, we only put revised Spark test in GlutenXXXSuite which is under spark32/33/34/35. And by putting them into `gluten-ut/test`, all Spark version tests and both backends will be covered.
created_at: 2025-04-22T17:19:05Z
author_association: MEMBER
diff_hunk: @@ -626,6 +626,10 @@ class ClickHouseTestSettings extends BackendTestSettings {
     .exclude("SPARK-36924: Cast YearMonthIntervalType to IntegralType")
     .exclude("SPARK-36924: Cast IntegralType to YearMonthIntervalType")
     .exclude("Cast should output null for invalid strings when ANSI is not enabled.")
+    .exclude("cast array from IntegerType to StringType")
+    .exclude("cast array from DoubleType to StringType")
+    .exclude("cast array from BooleanType to StringType")
+    .exclude("cast array from DateType to StringType")
path: gluten-ut/spark33/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
user: login: philo-he
body: Why we need exclude these tests for CH backend? In theory, the execution should fall back for CH backend.
created_at: 2025-04-22T17:31:14Z
author_association: MEMBER
diff_hunk: @@ -232,6 +232,18 @@ bool SubstraitToVeloxPlanValidator::validateScalarFunction(
   return true;
 }
 
+bool isValidArrayCast(const TypePtr& fromType, const TypePtr& toType) {
+  const auto& fromElem = fromType->asArray().elementType();
path: cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
user: login: philo-he
body: This variable is not used. Better to remove it?
created_at: 2025-04-22T17:33:10Z
author_association: MEMBER
diff_hunk: @@ -43,6 +43,56 @@ class GlutenCastSuite extends CastSuite with GlutenTestsTrait {
   UDTRegistration.register(classOf[IExampleBaseType].getName, classOf[ExampleBaseTypeUDT].getName)
   UDTRegistration.register(classOf[IExampleSubType].getName, classOf[ExampleSubTypeUDT].getName)
 
+  test("cast array from IntegerType to StringType") {
path: gluten-ut/spark33/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
user: login: ArnavBalyan
body: Yes I have added these tests in addition to Spark. There have been API changes for cast after 3.3 with introduction of `CastSuiteBase` which Gluten tests rely on, moved this test to all the versions. There is some duplication but ensures the tests run for all versions. 
created_at: 2025-04-23T04:52:28Z
author_association: MEMBER
diff_hunk: @@ -232,6 +232,18 @@ bool SubstraitToVeloxPlanValidator::validateScalarFunction(
   return true;
 }
 
+bool isValidArrayCast(const TypePtr& fromType, const TypePtr& toType) {
+  const auto& fromElem = fromType->asArray().elementType();
path: cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
user: login: ArnavBalyan
body: Removed
created_at: 2025-04-23T04:52:46Z
author_association: MEMBER
diff_hunk: @@ -232,6 +232,18 @@ bool SubstraitToVeloxPlanValidator::validateScalarFunction(
   return true;
 }
 
+bool isValidArrayCast(const TypePtr& fromType, const TypePtr& toType) {
path: cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
user: login: ArnavBalyan
body: Updated!
created_at: 2025-04-23T04:52:55Z
author_association: MEMBER
diff_hunk: @@ -626,6 +626,10 @@ class ClickHouseTestSettings extends BackendTestSettings {
     .exclude("SPARK-36924: Cast YearMonthIntervalType to IntegralType")
     .exclude("SPARK-36924: Cast IntegralType to YearMonthIntervalType")
     .exclude("Cast should output null for invalid strings when ANSI is not enabled.")
+    .exclude("cast array from IntegerType to StringType")
+    .exclude("cast array from DoubleType to StringType")
+    .exclude("cast array from BooleanType to StringType")
+    .exclude("cast array from DateType to StringType")
path: gluten-ut/spark33/src/test/scala/org/apache/gluten/utils/clickhouse/ClickHouseTestSettings.scala
user: login: ArnavBalyan
body: This change does not touch CH, but `DoubleType to StringType` fails in the Clickhouse CI due to some already existing issue in CH array cast. Excluded this test since the change is only touching Velox code thanks!
created_at: 2025-04-23T04:54:15Z
author_association: MEMBER
diff_hunk: @@ -43,6 +43,56 @@ class GlutenCastSuite extends CastSuite with GlutenTestsTrait {
   UDTRegistration.register(classOf[IExampleBaseType].getName, classOf[ExampleBaseTypeUDT].getName)
   UDTRegistration.register(classOf[IExampleSubType].getName, classOf[ExampleSubTypeUDT].getName)
 
+  test("cast array from IntegerType to StringType") {
path: gluten-ut/spark32/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
user: login: philo-he
body: Suggestion:
"cast array element from integer to string"

For other tests:
"cast array element from double to string"
"cast array element from bool to string"
"cast array element from date to string"



created_at: 2025-04-25T02:27:27Z
author_association: MEMBER
diff_hunk: @@ -232,6 +232,17 @@ bool SubstraitToVeloxPlanValidator::validateScalarFunction(
   return true;
 }
 
+bool isSupportedArrayCast(const TypePtr& fromType, const TypePtr& toType) {
path: cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
user: login: philo-he
body: Are all element types supported for `from` array? I note there is no check to limit it. Do we need to add more tests if other test cases are not covered by Spark UT? Especially, decimal, timestamp.

In Velox's implementation, does array element cast go into the same code path for primitive type cast?
created_at: 2025-04-25T02:55:14Z
author_association: MEMBER
diff_hunk: @@ -43,6 +43,56 @@ class GlutenCastSuite extends CastSuite with GlutenTestsTrait {
   UDTRegistration.register(classOf[IExampleBaseType].getName, classOf[ExampleBaseTypeUDT].getName)
   UDTRegistration.register(classOf[IExampleSubType].getName, classOf[ExampleSubTypeUDT].getName)
 
+  test("cast array from IntegerType to StringType") {
path: gluten-ut/spark32/src/test/scala/org/apache/spark/sql/catalyst/expressions/GlutenCastSuite.scala
user: login: ArnavBalyan
body: Updated!
created_at: 2025-04-25T17:37:05Z
author_association: MEMBER
diff_hunk: @@ -232,6 +232,17 @@ bool SubstraitToVeloxPlanValidator::validateScalarFunction(
   return true;
 }
 
+bool isSupportedArrayCast(const TypePtr& fromType, const TypePtr& toType) {
path: cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
user: login: ArnavBalyan
body: Agreed I was expecting the Spark UTs to cover it. Have added defensive checks to limit the from/to. Also added UTs for the supported casts. In most cases velox array goes through applyPeeled and same flow for individual elements. Limited the cast patterns and added UTs in addition to Spark UTs thanks!
created_at: 2025-04-25T17:40:00Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-22T10:41:37Z
body: https://github.com/apache/incubator-gluten/issues/9392
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-22T10:41:50Z
body: Run Gluten Clickhouse CI on x86
author_association: NONE
user: login: ArnavBalyan
created_at: 2025-04-22T12:12:36Z
body: cc @PHILO-HE could you please take a look thanks!
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-04-22T16:18:46Z
body: Run Gluten Clickhouse CI on x86
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-23T04:53:01Z
body: Run Gluten Clickhouse CI on x86
author_association: NONE
user: login: ArnavBalyan
created_at: 2025-04-23T04:56:07Z
body: > Please help confirm that involved Spark tests are enabled in Gluten.

I believe `CreateArray casts` in Spark is the test which runs the array cast, and it's not in any Gluten exclude list. There are probably other Spark tests which would also ensure this, I don't seem to see them excluded in Gluten. Is there some other way this could be confirmed? Thanks!
author_association: MEMBER
user: login: ArnavBalyan
created_at: 2025-04-24T04:56:50Z
body: cc @PHILO-HE gentle reminder thank you!
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-04-25T17:37:20Z
body: Run Gluten Clickhouse CI on x86
author_association: NONE
user: login: ArnavBalyan
created_at: 2025-04-27T14:51:31Z
body: > Looks good! Please fix CI failure if it is related to this pr. Thanks!

thanks!
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-04-27T14:51:53Z
body: Run Gluten Clickhouse CI on x86
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-28T07:03:49Z
body: Run Gluten Clickhouse CI on x86
author_association: NONE
user: login: ArnavBalyan
created_at: 2025-04-28T09:23:56Z
body: cc @PHILO-HE have added limited casts for now, and covered the cast with some UTs. Could you pls take a final look thanks
author_association: MEMBER