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


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DateExpressionsSuite.scala:
##########
@@ -3011,6 +3011,65 @@ class DateExpressionsSuite extends SparkFunSuite with 
ExpressionEvalHelper {
     }
   }
 
+  test("SPARK-57833: timestampdiff over nanosecond-precision timestamps") {
+    val sec = 1000000L // microseconds per second
+    def ntz(micros: Long, frac: Int, p: Int = 9): Literal =
+      Literal(TimestampNanosVal.fromParts(micros, frac.toShort), 
TimestampNTZNanosType(p))
+    def ltz(micros: Long, frac: Int, p: Int = 9): Literal =
+      Literal(TimestampNanosVal.fromParts(micros, frac.toShort), 
TimestampLTZNanosType(p))
+
+    // NANOSECOND unit reports the exact sub-microsecond difference (within a 
microsecond, across
+    // microseconds, and negative).
+    checkEvaluation(TimestampDiff("NANOSECOND", ntz(0, 100), ntz(0, 900)), 
800L)
+    checkEvaluation(TimestampDiff("NANOSECOND", ntz(0, 900), ntz(2, 100)), 
1200L)
+    checkEvaluation(TimestampDiff("NANOSECOND", ntz(2, 100), ntz(0, 900)), 
-1200L)
+
+    // The sub-microsecond fraction participates in the truncated count for 
coarser units too: a
+    // start fraction larger than the end fraction means a whole 
second/microsecond has NOT elapsed.
+    // SECOND: 1s + 100ns - 900ns = 0.9999992s -> 0 (the micros-only count 
would wrongly be 1).
+    checkEvaluation(TimestampDiff("SECOND", ntz(0, 900), ntz(sec, 100)), 0L)
+    // 1s + 900ns - 100ns = 1.0000008s -> 1.
+    checkEvaluation(TimestampDiff("SECOND", ntz(0, 100), ntz(sec, 900)), 1L)
+    // MICROSECOND: 2100ns - 900ns = 1200ns -> 1 (the micros-only count would 
wrongly be 2).
+    checkEvaluation(TimestampDiff("MICROSECOND", ntz(0, 900), ntz(2, 100)), 1L)
+
+    // Precision 7 (100ns step) and 8 (10ns step) fractions.
+    checkEvaluation(TimestampDiff("NANOSECOND", ntz(0, 100, 7), ntz(0, 300, 
7)), 200L)
+    checkEvaluation(TimestampDiff("NANOSECOND", ntz(0, 110, 8), ntz(0, 200, 
8)), 90L)
+
+    // LTZ (zone-aware), exact whole minute with equal fractions.
+    checkEvaluation(
+      TimestampDiff("MINUTE", ltz(0, 500), ltz(60 * sec, 500), Some("UTC")), 
1L)
+
+    // Mixed operands: a microsecond TIMESTAMP_LTZ start (zero fraction) and a 
nanosecond LTZ end.
+    checkEvaluation(
+      TimestampDiff("SECOND", Literal(0L, TimestampType), ltz(sec, 500), 
Some("UTC")), 1L)
+
+    // NANOSECOND between two microsecond timestamps is well-defined 
(fractions are zero): the
+    // difference is a whole number of microseconds times 1000.
+    checkEvaluation(
+      TimestampDiff("NANOSECOND", Literal(0L, TimestampType), Literal(1L, 
TimestampType)), 1000L)
+
+    // Null propagation on either operand.
+    checkEvaluation(
+      TimestampDiff("SECOND", Literal.create(null, TimestampNTZNanosType(9)), 
ntz(sec, 0)), null)
+    checkEvaluation(
+      TimestampDiff("NANOSECOND", ntz(0, 1), Literal.create(null, 
TimestampNTZNanosType(9))), null)
+
+    // A NANOSECOND difference wider than ~292 years overflows a 64-bit 
nanosecond count. It is
+    // surfaced as DATETIME_OVERFLOW (matching the timestampadd side) rather 
than a raw
+    // ArithmeticException, on both the microsecond and the nanosecond-carrier 
code paths.
+    checkErrorInExpression[SparkArithmeticException](
+      TimestampDiff("NANOSECOND",
+        Literal(-5000000000000000L, TimestampType), Literal(5000000000000000L, 
TimestampType)),
+      condition = "DATETIME_OVERFLOW",
+      parameters = Map("operation" -> "get the number of NANOSECOND between 
the two timestamps"))
+    checkErrorInExpression[SparkArithmeticException](
+      TimestampDiff("NANOSECOND", ntz(-5000000000000000L, 0), 
ntz(5000000000000000L, 0)),
+      condition = "DATETIME_OVERFLOW",
+      parameters = Map("operation" -> "get the number of NANOSECOND between 
the two timestamps"))
+  }
+

Review Comment:
   DateExpressionsSuite.scala / new golden queries -- test coverage gap that 
hid the regression: every added case pairs operands of the same zone family 
(NTZ+NTZ, LTZ+LTZ, and micros-LTZ + nanos-LTZ). There is no mixed NTZ/LTZ case 
(micros or nanos) and no DATE+NTZ case -- exactly the combinations broken 
above. Add them; they pin the correct pre-existing values and will fail until 
the zone-handling concern is fixed.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala:
##########
@@ -5179,27 +5184,73 @@ case class TimestampDiff(
   override def left: Expression = startTimestamp
   override def right: Expression = endTimestamp
 
-  override def inputTypes: Seq[AbstractDataType] = Seq(TimestampType, 
TimestampType)
+  // Micro-precision NTZ operands keep coercing to TIMESTAMP (LTZ), preserving 
the pre-nanos
+  // timestampdiff semantics for TIMESTAMP_NTZ; only the new 
nanosecond-precision types are accepted
+  // natively (there is no prior behavior to preserve for them).
+  override def inputTypes: Seq[AbstractDataType] =

Review Comment:
   TimestampDiff.inputTypes -- widening both operands from Seq(TimestampType, 
TimestampType) to Seq(TypeCollection(AnyTimestampType, AnyTimestampNanoType), 
...) silently regresses existing microsecond mixed-zone results. On master both 
operands were unified to TimestampType (LTZ), so the single zoneIdInEval = 
zoneIdForType(endTimestamp) applied to both was safe. With the wider accepted 
set, AnyTimestampType.acceptsType lets an NTZ operand stay NTZ (and a DATE 
coerces to LTZ via defaultConcreteType = TimestampType), so operands of 
different zone families reach the eval, which then applies one end-derived zone 
to both -- double/mis-applying the offset. Confirmed against master + CI: 
timestamp-ntz.sql q#20 timestampdiff(HOUR, timestamp_ntz'...01:02:03', 
timestamp_ltz'...02:03:04') now returns 9 (master golden: 1), and the PR's own 
regenerated timestampNTZ/timestamp.sql.out bakes in timestampdiff(SECOND, 
date'2022-02-15', ntz'...23:59:59') = -28801 (correct: -1; -28801 = the -8h 
session 
 offset applied to the NTZ/DATE operand, minus 1s). The new timestampDiffNanos 
helper shares the identical single-zoneId-for-both design, so mixed 
nanos-NTZ/nanos-LTZ operands have the same latent bug (untested). Feasible 
direction: derive the zone per operand (zoneIdForType(startTimestamp.dataType) 
/ ...(endTimestamp.dataType)) instead of one shared zone -- traced to restore 1 
for q#20 and to leave same-family cases unchanged; exact fix should be 
validated against the full golden suite (fix conf:med, concern conf:high).



-- 
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