This is an automated email from the ASF dual-hosted git repository.

MaxGekk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new e7db3ddc65fb [SPARK-57748][SQL] Use a dedicated tree-pattern bit for 
the TIME to TIMESTAMP cast rewrite in ComputeCurrentTime
e7db3ddc65fb is described below

commit e7db3ddc65fb5d16ed70e23d8fa41b4b3b35e2d8
Author: Anupam Yadav <[email protected]>
AuthorDate: Wed Jul 1 11:51:08 2026 +0200

    [SPARK-57748][SQL] Use a dedicated tree-pattern bit for the TIME to 
TIMESTAMP cast rewrite in ComputeCurrentTime
    
    ### What changes were proposed in this pull request?
    
    Introduce a dedicated `CAST_TO_TIMESTAMP` tree-pattern bit, set on `Cast` 
nodes whose **target** type is any timestamp type (`TIMESTAMP_NTZ` / 
`TIMESTAMP_NTZ(p)` / `TIMESTAMP` / `TIMESTAMP_LTZ(p)`). `ComputeCurrentTime`'s 
pruning predicate changes from `CURRENT_LIKE || CAST` to `CURRENT_LIKE || 
CAST_TO_TIMESTAMP`. The node-level `isTimeToTimestampNTZ` / 
`isTimeToTimestampLTZ` guards are unchanged, so rewrite semantics are identical.
    
    ### Why are the changes needed?
    
    SPARK-57618 widened the rule's pruning predicate to the broad `CAST` bit so 
it could find `CAST(TIME AS TIMESTAMP_NTZ/LTZ)` casts (whose date fields derive 
from `CURRENT_DATE`). But `CAST` appears in nearly every query, so 
`ComputeCurrentTime` now traverses the full expression tree of essentially 
every plan even though the TIME -> TIMESTAMP rewrite fires rarely. A dedicated 
bit keyed on the timestamp **target** type restores effective pruning while 
keeping the rewrite reachable.
    
    Note: the JIRA proposed an NTZ-only bit. I broadened it to all timestamp 
targets because the rule also rewrites TIME -> TIMESTAMP_LTZ, and a standalone 
`CAST(TIME '10:00' AS TIMESTAMP)` does not co-occur with a `CURRENT_LIKE` node, 
so an NTZ-only bit would stop reaching those casts and regress them. Hence 
`CAST_TO_TIMESTAMP`.
    
    ### Does this PR introduce any user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    New `ComputeCurrentTimeSuite` assertions: the bit is set for NTZ and LTZ 
timestamp targets, absent for non-timestamp targets, independent of the source 
type, and the TIME -> TIMESTAMP rewrites still fire. Existing 
`ComputeCurrentTimeSuite` / `CastSuite` / `CastWithAnsiSuite` pass (158 tests).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Authored with assistance by Claude Opus 4.8.
    
    Closes #56888 from yadavay-amzn/SPARK-57748.
    
    Authored-by: Anupam Yadav <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 .../spark/sql/catalyst/expressions/Cast.scala      | 15 ++++-
 .../sql/catalyst/optimizer/finishAnalysis.scala    | 19 +++---
 .../spark/sql/catalyst/trees/TreePatterns.scala    |  1 +
 .../optimizer/ComputeCurrentTimeSuite.scala        | 77 +++++++++++++++++++++-
 4 files changed, 101 insertions(+), 11 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
index 96ebe62b76ad..eb7b1d269cdb 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/Cast.scala
@@ -689,7 +689,20 @@ case class Cast(
 
   override protected def withNewChildInternal(newChild: Expression): Cast = 
copy(child = newChild)
 
-  final override def nodePatternsInternal(): Seq[TreePattern] = Seq(CAST)
+  // CAST_TO_TIMESTAMP must be set on a superset of the targets accepted by
+  // Cast.isTimeToTimestampNTZ / isTimeToTimestampLTZ. ComputeCurrentTime 
relies on
+  // this bit to reach TIME->TIMESTAMP rewrites; if narrowed, those casts 
silently
+  // escape stabilization with no test catching the drift (two independent 
match lists).
+  //
+  // We key on the target `dataType` only, not `child.dataType`: node patterns 
are
+  // computed eagerly at construction before the child is resolved, so reading
+  // child.dataType can throw (the OuterReference / RETURNS TABLE case removed 
in
+  // commit 51136ecb5e2). Narrowing by source type would reintroduce that 
failure.
+  final override def nodePatternsInternal(): Seq[TreePattern] = dataType match 
{
+    case _: TimestampNTZType | _: TimestampNTZNanosType |
+         TimestampType | _: TimestampLTZNanosType => Seq(CAST, 
CAST_TO_TIMESTAMP)
+    case _ => Seq(CAST)
+  }
 
   override def contextIndependentFoldable: Boolean = {
     child.contextIndependentFoldable && !Cast.needsTimeZone(child.dataType, 
dataType)
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/finishAnalysis.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/finishAnalysis.scala
index 437b67d0855d..c8c00a3fa13a 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/finishAnalysis.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/finishAnalysis.scala
@@ -120,16 +120,17 @@ object ComputeCurrentTime extends Rule[LogicalPlan] {
     val currentDates = collection.mutable.HashMap.empty[ZoneId, Literal]
     val localTimestamps = collection.mutable.HashMap.empty[ZoneId, Literal]
 
-    // The CAST bit is included so this rule can find TIME -> TIMESTAMP_NTZ 
and TIME ->
-    // TIMESTAMP_LTZ casts (which derive their date fields from CURRENT_DATE) 
and stabilize them
-    // below. CAST is a broad pattern, so this widens the rule's traversal to 
most plans; the
-    // precise `Cast.isTimeToTimestampNTZ` / `Cast.isTimeToTimestampLTZ` 
guards keep the rewrite
-    // scoped. We intentionally do not tag these casts with CURRENT_LIKE 
instead: inline-table
-    // validation treats CURRENT_LIKE as safe to defer, so tagging would let 
unrelated non-foldable
-    // NTZ/LTZ-target casts (e.g. CAST(rand() AS TIMESTAMP_NTZ)) bypass that 
validation (see
-    // SPARK-57618 and ResolveInlineTablesSuite).
+    // CAST_TO_TIMESTAMP is a dedicated tree-pattern bit set on Cast nodes 
whose target type is
+    // any timestamp type (NTZ or LTZ family). This lets the rule reach both 
TIME -> TIMESTAMP_NTZ
+    // and TIME -> TIMESTAMP_LTZ rewrites (which derive date fields from 
CURRENT_DATE) without the
+    // broad CAST pattern that previously widened traversal to nearly every 
plan. Node-level
+    // isTimeToTimestamp{NTZ,LTZ} guards keep rewrite semantics unchanged.
+    // We intentionally do NOT tag these casts with CURRENT_LIKE: inline-table 
validation treats
+    // CURRENT_LIKE as safe to defer, so tagging would let unrelated 
non-foldable timestamp-target
+    // casts (e.g. CAST(rand() AS TIMESTAMP_NTZ)) bypass validation (see 
SPARK-57618).
     def transformCondition(treePatternbits: TreePatternBits): Boolean = {
-      treePatternbits.containsPattern(CURRENT_LIKE) || 
treePatternbits.containsPattern(CAST)
+      treePatternbits.containsPattern(CURRENT_LIKE) ||
+        treePatternbits.containsPattern(CAST_TO_TIMESTAMP)
     }
 
     plan.transformDownWithSubqueriesAndPruning(transformCondition) {
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/trees/TreePatterns.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/trees/TreePatterns.scala
index 173df28e2b24..94b4666a88a8 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/trees/TreePatterns.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/trees/TreePatterns.scala
@@ -41,6 +41,7 @@ object TreePattern extends Enumeration  {
   val BINARY_COMPARISON: Value = Value
   val CASE_WHEN: Value = Value
   val CAST: Value = Value
+  val CAST_TO_TIMESTAMP: Value = Value
   val COALESCE: Value = Value
   val COMMON_EXPR_REF: Value = Value
   val CONCAT: Value = Value
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/ComputeCurrentTimeSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/ComputeCurrentTimeSuite.scala
index a94bb6f6c5c1..be24f9c9f01f 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/ComputeCurrentTimeSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/ComputeCurrentTimeSuite.scala
@@ -28,9 +28,10 @@ import org.apache.spark.sql.catalyst.expressions.{Add, 
Alias, Cast, CurrentDate,
 import org.apache.spark.sql.catalyst.plans.PlanTest
 import org.apache.spark.sql.catalyst.plans.logical.{Filter, LocalRelation, 
LogicalPlan, Project}
 import org.apache.spark.sql.catalyst.rules.RuleExecutor
+import org.apache.spark.sql.catalyst.trees.TreePattern
 import org.apache.spark.sql.catalyst.util.DateTimeUtils
 import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types.{DateType, IntegerType, 
TimestampLTZNanosType, TimestampNTZNanosType, TimestampNTZType, TimestampType, 
TimeType}
+import org.apache.spark.sql.types.{DateType, IntegerType, StringType, 
TimestampLTZNanosType, TimestampNTZNanosType, TimestampNTZType, TimestampType, 
TimeType}
 import org.apache.spark.unsafe.types.UTF8String
 
 class ComputeCurrentTimeSuite extends PlanTest {
@@ -342,4 +343,78 @@ class ComputeCurrentTimeSuite extends PlanTest {
     }
     literals
   }
+
+  test("SPARK-57748: TIME->TIMESTAMP cast is rewritten even with no 
CURRENT_LIKE node") {
+    val timeLit = Literal(0L, TimeType(6))
+    Seq(TimestampNTZType, TimestampType).foreach { target =>
+      val in = Project(Seq(Alias(Cast(timeLit, target), "a")()), 
LocalRelation())
+      val plan = Optimize.execute(in.analyze).asInstanceOf[Project]
+      val remaining = plan.expressions.flatMap(_.collect {
+        case c: Cast if Cast.isTimeToTimestampNTZ(c.child.dataType, c.dataType)
+                     || Cast.isTimeToTimestampLTZ(c.child.dataType, 
c.dataType) => c
+      })
+      assert(remaining.isEmpty,
+        s"TIME->$target cast should be rewritten with no CURRENT_LIKE present")
+    }
+  }
+
+  test("SPARK-57748: CAST_TO_TIMESTAMP tree pattern is set for NTZ target 
types") {
+    // Cast with TimestampNTZType target should contain CAST_TO_TIMESTAMP
+    val ntzCast = Cast(Literal(0L, TimeType(6)), TimestampNTZType)
+    assert(ntzCast.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+    assert(ntzCast.containsPattern(TreePattern.CAST)) // existing CAST tag 
preserved
+
+    // Cast with TimestampNTZNanosType target should also contain 
CAST_TO_TIMESTAMP
+    val ntzNanosCast = Cast(Literal(0L, TimeType(6)), TimestampNTZNanosType(9))
+    assert(ntzNanosCast.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+    assert(ntzNanosCast.containsPattern(TreePattern.CAST))
+  }
+
+  test("SPARK-57748: CAST_TO_TIMESTAMP tree pattern is NOT set for 
non-timestamp targets") {
+    // Cast to StringType should NOT contain CAST_TO_TIMESTAMP
+    val stringCast = Cast(Literal(0L, TimeType(6)), StringType)
+    assert(!stringCast.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+    assert(stringCast.containsPattern(TreePattern.CAST))
+
+    // Cast to IntegerType should NOT contain CAST_TO_TIMESTAMP
+    val intCast = Cast(Literal("10"), IntegerType)
+    assert(!intCast.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+    assert(intCast.containsPattern(TreePattern.CAST))
+  }
+
+  test("SPARK-57748: CAST_TO_TIMESTAMP tree pattern is set for LTZ targets") {
+    // Cast to TimestampType (LTZ micro) should contain CAST_TO_TIMESTAMP 
because
+    // ComputeCurrentTime rewrites TIME->LTZ casts via the same predicate.
+    val ltzCast = Cast(Literal(0L, TimeType(6)), TimestampType)
+    assert(ltzCast.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+    assert(ltzCast.containsPattern(TreePattern.CAST))
+
+    // Cast to TimestampLTZNanosType should also contain CAST_TO_TIMESTAMP
+    val ltzNanosCast = Cast(Literal(0L, TimeType(6)), TimestampLTZNanosType(9))
+    assert(ltzNanosCast.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+    assert(ltzNanosCast.containsPattern(TreePattern.CAST))
+  }
+
+  test("SPARK-57748: CAST_TO_TIMESTAMP is keyed on target type, not source 
type") {
+    // Source type does not matter - only the target determines the pattern bit
+    val fromString = Cast(Literal("2024-01-01"), TimestampNTZType)
+    assert(fromString.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+
+    val fromInt = Cast(Literal(42), TimestampNTZType)
+    assert(fromInt.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+
+    // Even with an expression child (rand()), the target type determines the 
bit
+    import org.apache.spark.sql.catalyst.expressions.Rand
+    val fromRand = Cast(Rand(Literal(0L)), TimestampNTZType)
+    assert(fromRand.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+  }
+
+  test("SPARK-57748: plan with non-timestamp cast only does not contain 
CAST_TO_TIMESTAMP") {
+    val timeLit = Literal(0L, TimeType(6))
+    val plan = Project(Seq(
+      Alias(Cast(timeLit, IntegerType), "a")()),
+      LocalRelation())
+    assert(!plan.containsPattern(TreePattern.CAST_TO_TIMESTAMP))
+    assert(plan.containsPattern(TreePattern.CAST))
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to