diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/encoders/EncoderUtils.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/encoders/EncoderUtils.scala index 0fce96c159979..61d4be4d56cb8 100644 --- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/encoders/EncoderUtils.scala +++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/encoders/EncoderUtils.scala @@ -123,6 +123,7 @@ object EncoderUtils { case _: DecimalType => classOf[Decimal] case _: DayTimeIntervalType => classOf[java.lang.Long] case _: YearMonthIntervalType => classOf[java.lang.Integer] + case _: TimeType => classOf[java.lang.Long] case BinaryType => classOf[Array[Byte]] case _: StringType => classOf[UTF8String] case CalendarIntervalType => classOf[CalendarInterval] diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/encoders/EncoderUtilsSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/encoders/EncoderUtilsSuite.scala new file mode 100644 index 0000000000000..04c33e61debf6 --- /dev/null +++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/encoders/EncoderUtilsSuite.scala @@ -0,0 +1,52 @@ +/* + * 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.spark.sql.catalyst.encoders + +import org.apache.spark.{SparkFunSuite, SparkRuntimeException} +import org.apache.spark.sql.Row +import org.apache.spark.sql.catalyst.InternalRow +import org.apache.spark.sql.catalyst.expressions.BoundReference +import org.apache.spark.sql.catalyst.expressions.objects.{GetExternalRowField, ValidateExternalType} +import org.apache.spark.sql.types.{ObjectType, TimeType} + +class EncoderUtilsSuite extends SparkFunSuite { + + test("SPARK-53520: javaBoxedType for TimeType enables correct type validation") { + // ValidateExternalType uses javaBoxedType(dataType) in its fallback checkType case. + // Without TimeType in javaBoxedType, it returns java.lang.Object, which makes + // Object.isInstance(value) always true -- disabling type validation entirely. + // With the fix, it returns java.lang.Long, so only Long values pass validation. + val inputObject = BoundReference(0, ObjectType(classOf[Row]), nullable = true) + val dt = TimeType() + val validateType = ValidateExternalType( + GetExternalRowField(inputObject, index = 0, fieldName = "t"), + dt, + dt) + + // Valid: Long value (time as microseconds since midnight) should pass + val validRow = InternalRow.fromSeq(Seq(Row(61000000L))) + assert(validateType.eval(validRow) === 61000000L) + + // Invalid: String value must be rejected. + // Without the fix, javaBoxedType returns Object and this would incorrectly pass. + val invalidRow = InternalRow.fromSeq(Seq(Row("not-a-time"))) + intercept[SparkRuntimeException] { + validateType.eval(invalidRow) + } + } +}