uros-b commented on code in PR #56969:
URL: https://github.com/apache/spark/pull/56969#discussion_r3983010805


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala:
##########
@@ -3032,24 +3032,46 @@ case class MonthsBetween(
   override def second: Expression = date2
   override def third: Expression = roundOff
 
-  override def inputTypes: Seq[AbstractDataType] = Seq(TimestampType, 
TimestampType, BooleanType)
+  // Nanosecond-precision timestamps are accepted alongside the microsecond 
types. The result is a
+  // fraction of a month derived from the whole-day and whole-second parts of 
each operand, so the
+  // sub-microsecond remainder cannot move it and each operand contributes 
only its epochMicros.
+  override def inputTypes: Seq[AbstractDataType] = Seq(
+    TypeCollection(AnyTimestampType, AnyTimestampNanoType),
+    TypeCollection(AnyTimestampType, AnyTimestampNanoType),
+    BooleanType)
 
   override def dataType: DataType = DoubleType
 
   override def withTimeZone(timeZoneId: String): TimeZoneAwareExpression =
     copy(timeZoneId = Option(timeZoneId))
 
+  @transient private lazy val zoneIdInEval: ZoneId = 
zoneIdForType(date1.dataType)
+
+  // For the nanosecond carrier the child value is a boxed TimestampNanosVal, 
so read its
+  // epochMicros; for the microsecond timestamp types it is already a boxed 
Long.
+  private def toMicros(value: Any): Long = value match {
+    case v: TimestampNanosVal => v.epochMicros
+    case n => n.asInstanceOf[Long]
+  }
+
   override def nullSafeEval(t1: Any, t2: Any, roundOff: Any): Any = {
     DateTimeUtils.monthsBetween(
-      t1.asInstanceOf[Long], t2.asInstanceOf[Long], 
roundOff.asInstanceOf[Boolean], zoneId)
+      toMicros(t1), toMicros(t2), roundOff.asInstanceOf[Boolean], zoneIdInEval)
   }
 
   override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
-    val zid = ctx.addReferenceObj("zoneId", zoneId, classOf[ZoneId].getName)
+    // The nanosecond carrier exposes epochMicros as a public field; the 
microsecond types are
+    // already primitive longs. Reduce each operand to microseconds before 
taking the difference.
+    def toMicrosCode(e: Expression): String => String = e.dataType match {
+      case _: AnyTimestampNanoType => c => s"$c.epochMicros"
+      case _ => c => c
+    }
+    val micros1 = toMicrosCode(date1)
+    val micros2 = toMicrosCode(date2)
+    val zid = ctx.addReferenceObj("zoneId", zoneIdInEval, 
classOf[ZoneId].getName)
     val dtu = DateTimeUtils.getClass.getName.stripSuffix("$")
-    defineCodeGen(ctx, ev, (d1, d2, roundOff) => {
-      s"""$dtu.monthsBetween($d1, $d2, $roundOff, $zid)"""
-    })
+    defineCodeGen(ctx, ev, (d1, d2, roundOff) =>
+      s"""$dtu.monthsBetween(${micros1(d1)}, ${micros2(d2)}, $roundOff, 
$zid)""")
   }
 

Review Comment:
   `@ExpressionDescription` still has no nanosecond-precision example 
(unchanged from iteration 1); sibling extended nanos exprs added one.



##########
sql/core/src/test/resources/sql-tests/inputs/timestamp-ntz.sql:
##########
@@ -36,6 +36,14 @@ select timestampdiff(HOUR, timestamp_ntz'2022-02-14 
01:02:03', timestamp_ltz'202
 select timestampdiff(YEAR, date'2022-02-15', timestamp_ntz'2023-02-15 
10:11:12');
 select timestampdiff(MILLISECOND, timestamp_ntz'2022-02-14 23:59:59.123', 
date'2022-02-15');
 
+-- SPARK-57819: months_between derives its time zone from the first operand's 
family, mirroring

Review Comment:
   The comment "mirroring the existing SubtractTimestamps / `timestamp - 
timestamp` convention" is false at current master. 
`DateTimeOperationsTypeCoercion` and `AnsiDateTimeOperationsTypeCoercion` 
(TypeCoercionHelper.scala:864-905) cast a cross-family `SubtractTimestamps(l, 
r)` pair to a common family (`subtractTimestampsCommonType`, unifying to the 
NTZ family) BEFORE eval, so its `zoneIdForType(left)` never spans mixed 
families and stays symmetric. `MonthsBetween` has no coercion rule (only 
`FunctionRegistry.scala:648`) and is `ImplicitCastInputTypes` over 
`TypeCollection` (each operand cast independently), so it is the ONE 
timestamp-diff expression that evaluates a genuinely mixed pair with `date1`'s 
zone applied to both. Correct the comment, or add the analogous coercion for 
true symmetry.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to