@@ -28,9 +28,10 @@ import org.apache.spark.sql.catalyst.expressions.{Add, Alias, Cast, CurrentDate,
2828import org .apache .spark .sql .catalyst .plans .PlanTest
2929import org .apache .spark .sql .catalyst .plans .logical .{Filter , LocalRelation , LogicalPlan , Project }
3030import org .apache .spark .sql .catalyst .rules .RuleExecutor
31+ import org .apache .spark .sql .catalyst .trees .TreePattern
3132import org .apache .spark .sql .catalyst .util .DateTimeUtils
3233import org .apache .spark .sql .internal .SQLConf
33- import org .apache .spark .sql .types .{DateType , IntegerType , TimestampLTZNanosType , TimestampNTZNanosType , TimestampNTZType , TimestampType , TimeType }
34+ import org .apache .spark .sql .types .{DateType , IntegerType , StringType , TimestampLTZNanosType , TimestampNTZNanosType , TimestampNTZType , TimestampType , TimeType }
3435import org .apache .spark .unsafe .types .UTF8String
3536
3637class ComputeCurrentTimeSuite extends PlanTest {
@@ -342,4 +343,78 @@ class ComputeCurrentTimeSuite extends PlanTest {
342343 }
343344 literals
344345 }
346+
347+ test(" SPARK-57748: TIME->TIMESTAMP cast is rewritten even with no CURRENT_LIKE node" ) {
348+ val timeLit = Literal (0L , TimeType (6 ))
349+ Seq (TimestampNTZType , TimestampType ).foreach { target =>
350+ val in = Project (Seq (Alias (Cast (timeLit, target), " a" )()), LocalRelation ())
351+ val plan = Optimize .execute(in.analyze).asInstanceOf [Project ]
352+ val remaining = plan.expressions.flatMap(_.collect {
353+ case c : Cast if Cast .isTimeToTimestampNTZ(c.child.dataType, c.dataType)
354+ || Cast .isTimeToTimestampLTZ(c.child.dataType, c.dataType) => c
355+ })
356+ assert(remaining.isEmpty,
357+ s " TIME-> $target cast should be rewritten with no CURRENT_LIKE present " )
358+ }
359+ }
360+
361+ test(" SPARK-57748: CAST_TO_TIMESTAMP tree pattern is set for NTZ target types" ) {
362+ // Cast with TimestampNTZType target should contain CAST_TO_TIMESTAMP
363+ val ntzCast = Cast (Literal (0L , TimeType (6 )), TimestampNTZType )
364+ assert(ntzCast.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
365+ assert(ntzCast.containsPattern(TreePattern .CAST )) // existing CAST tag preserved
366+
367+ // Cast with TimestampNTZNanosType target should also contain CAST_TO_TIMESTAMP
368+ val ntzNanosCast = Cast (Literal (0L , TimeType (6 )), TimestampNTZNanosType (9 ))
369+ assert(ntzNanosCast.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
370+ assert(ntzNanosCast.containsPattern(TreePattern .CAST ))
371+ }
372+
373+ test(" SPARK-57748: CAST_TO_TIMESTAMP tree pattern is NOT set for non-timestamp targets" ) {
374+ // Cast to StringType should NOT contain CAST_TO_TIMESTAMP
375+ val stringCast = Cast (Literal (0L , TimeType (6 )), StringType )
376+ assert(! stringCast.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
377+ assert(stringCast.containsPattern(TreePattern .CAST ))
378+
379+ // Cast to IntegerType should NOT contain CAST_TO_TIMESTAMP
380+ val intCast = Cast (Literal (" 10" ), IntegerType )
381+ assert(! intCast.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
382+ assert(intCast.containsPattern(TreePattern .CAST ))
383+ }
384+
385+ test(" SPARK-57748: CAST_TO_TIMESTAMP tree pattern is set for LTZ targets" ) {
386+ // Cast to TimestampType (LTZ micro) should contain CAST_TO_TIMESTAMP because
387+ // ComputeCurrentTime rewrites TIME->LTZ casts via the same predicate.
388+ val ltzCast = Cast (Literal (0L , TimeType (6 )), TimestampType )
389+ assert(ltzCast.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
390+ assert(ltzCast.containsPattern(TreePattern .CAST ))
391+
392+ // Cast to TimestampLTZNanosType should also contain CAST_TO_TIMESTAMP
393+ val ltzNanosCast = Cast (Literal (0L , TimeType (6 )), TimestampLTZNanosType (9 ))
394+ assert(ltzNanosCast.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
395+ assert(ltzNanosCast.containsPattern(TreePattern .CAST ))
396+ }
397+
398+ test(" SPARK-57748: CAST_TO_TIMESTAMP is keyed on target type, not source type" ) {
399+ // Source type does not matter - only the target determines the pattern bit
400+ val fromString = Cast (Literal (" 2024-01-01" ), TimestampNTZType )
401+ assert(fromString.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
402+
403+ val fromInt = Cast (Literal (42 ), TimestampNTZType )
404+ assert(fromInt.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
405+
406+ // Even with an expression child (rand()), the target type determines the bit
407+ import org .apache .spark .sql .catalyst .expressions .Rand
408+ val fromRand = Cast (Rand (Literal (0L )), TimestampNTZType )
409+ assert(fromRand.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
410+ }
411+
412+ test(" SPARK-57748: plan with non-timestamp cast only does not contain CAST_TO_TIMESTAMP" ) {
413+ val timeLit = Literal (0L , TimeType (6 ))
414+ val plan = Project (Seq (
415+ Alias (Cast (timeLit, IntegerType ), " a" )()),
416+ LocalRelation ())
417+ assert(! plan.containsPattern(TreePattern .CAST_TO_TIMESTAMP ))
418+ assert(plan.containsPattern(TreePattern .CAST ))
419+ }
345420}
0 commit comments