viirya commented on code in PR #58850:
URL: https://github.com/apache/spark/pull/58850#discussion_r4029576496


##########
sql/core/src/test/scala/org/apache/spark/sql/execution/columnar/ArrowCachedBatchSerializerSuite.scala:
##########
@@ -1260,6 +1260,92 @@ class ArrowCachedBatchSerializerSuite extends QueryTest 
with SharedSparkSession
       spark.sparkContext.parallelize(values.map(v => Row(v)), 1),
       StructType(Seq(StructField("v", dt, nullable = true))))
 
+  // Helper: the InMemoryRelation behind a cached DataFrame, after populating 
the cache. The
+  // DataFrame's queryExecution is memoized, so this must run before anything 
else forces it.
+  private def cachedRelation(df: org.apache.spark.sql.DataFrame): 
InMemoryRelation = {
+    df.cache()
+    df.count()
+    df.queryExecution.executedPlan.collectFirst {
+      case scan: InMemoryTableScanExec => scan.relation
+    }.get
+  }
+
+  // Helper: (Arrow vector class, Spark type, row count, null count) per 
cached batch of a
+  // single-column relation, read back through the serializer's own columnar 
path. The Spark type
+  // is the one ArrowColumnVector recovers from the read-side Arrow field via
+  // ArrowUtils.fromArrowField, so a type whose fractional-second precision 
lives in that field's
+  // metadata is reported as it comes back rather than as it went in.
+  private def cachedVectors(relation: InMemoryRelation): Array[(String, 
DataType, Int, Int)] = {
+    val attrs = relation.output
+    relation.cacheBuilder.serializer
+      .convertCachedBatchToColumnarBatch(
+        relation.cacheBuilder.cachedColumnBuffers, attrs, attrs, 
spark.sessionState.conf)
+      .mapPartitions { batches =>
+        batches.map { batch =>
+          val column = batch.column(0).asInstanceOf[ArrowColumnVector]
+          (column.getValueVector.getClass.getSimpleName, column.dataType(), 
batch.numRows(),
+            column.numNulls())
+        }
+      }
+      .collect()
+  }
+
+  private val nullPatterns: Seq[(String, Int => Boolean)] = Seq(
+    ("every 31st row null", i => i % 31 == 0),
+    ("no nulls", _ => false),
+    ("all nulls", _ => true))
+
+  test("SPARK-59571: TIME keeps its precision and its nulls through the cache, 
at every " +
+      "precision") {
+    // Every precision of TIME is written to the same TimeNanoVector, so the 
precision rides in
+    // the Arrow field metadata rather than in the value. The cache read path 
rebuilds its Arrow
+    // schema from the cache schema and wraps each vector in an 
ArrowColumnVector, whose type is
+    // recovered from that field, so the column's type on the way out pins the 
tag-and-recover
+    // path: dropping the precision key on either side falls back to 
TimeType(6). The values are
+    // truncated to the declared precision so a precision loss could not hide 
behind a value the
+    // type would round anyway.
+    val rows = 1000
+    val nanosPerDay = 86400000000000L
+    for (precision <- Seq(0, 3, 6, 9); (pattern, isNull) <- nullPatterns) {
+      val unit = math.pow(10, 9 - precision).toLong
+      def timeAt(i: Int): LocalTime = {
+        val nanos = ((i.toLong * nanosPerDay) / rows + i.toLong * 1234567L) % 
nanosPerDay
+        LocalTime.ofNanoOfDay(nanos / unit * unit)
+      }
+      val values = (0 until rows).map(i => if (isNull(i)) null else timeAt(i))
+      val df = singlePartDf(values, TimeType(precision))
+      val relation = cachedRelation(df)
+      checkAnswer(df, values.map(Row(_)))
+      val vectors = cachedVectors(relation)
+      assert(vectors.map(_._1).toSet === Set("TimeNanoVector"), 
s"TIME($precision), $pattern")
+      assert(vectors.map(_._2).toSet === Set[DataType](TimeType(precision)),
+        s"TIME($precision), $pattern: the precision must survive the read-side 
Arrow field")
+      assert(vectors.map(_._3).sum === rows, s"TIME($precision), $pattern")
+      assert(vectors.map(_._4).sum === (0 until rows).count(isNull),
+        s"TIME($precision), $pattern: null count")
+      df.unpersist()
+    }
+  }
+
+  test("SPARK-59571: day-time intervals of both signs keep their microseconds 
through the " +
+      "cache") {
+    val rows = 1000
+    for ((pattern, isNull) <- nullPatterns) {
+      // Whole microseconds, negative for the first half of the rows and 
positive for the rest.
+      def intervalAt(i: Int): Duration = Duration.ofNanos((i.toLong - rows / 
2) * 7001000000L)

Review Comment:
   Could we include values with nonzero sub-millisecond components here? 
`7001000000L` nanoseconds is exactly 7001 milliseconds, so every generated 
interval is millisecond-aligned. A regression that truncates the stored 
microseconds with `micros / 1000 * 1000` would leave all these values 
unchanged, and the type/null-count assertions would still pass.
   
   Using a multiplier such as `7001001000L` would exercise microsecond 
precision while retaining both signs. Explicit `+/-1` and `+/-999` microsecond 
cases would also help cover truncation around zero.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/columnar/ArrowCachedBatchSerializerSuite.scala:
##########
@@ -1260,6 +1260,92 @@ class ArrowCachedBatchSerializerSuite extends QueryTest 
with SharedSparkSession
       spark.sparkContext.parallelize(values.map(v => Row(v)), 1),
       StructType(Seq(StructField("v", dt, nullable = true))))
 
+  // Helper: the InMemoryRelation behind a cached DataFrame, after populating 
the cache. The
+  // DataFrame's queryExecution is memoized, so this must run before anything 
else forces it.
+  private def cachedRelation(df: org.apache.spark.sql.DataFrame): 
InMemoryRelation = {
+    df.cache()
+    df.count()
+    df.queryExecution.executedPlan.collectFirst {
+      case scan: InMemoryTableScanExec => scan.relation
+    }.get
+  }
+
+  // Helper: (Arrow vector class, Spark type, row count, null count) per 
cached batch of a
+  // single-column relation, read back through the serializer's own columnar 
path. The Spark type
+  // is the one ArrowColumnVector recovers from the read-side Arrow field via
+  // ArrowUtils.fromArrowField, so a type whose fractional-second precision 
lives in that field's
+  // metadata is reported as it comes back rather than as it went in.
+  private def cachedVectors(relation: InMemoryRelation): Array[(String, 
DataType, Int, Int)] = {
+    val attrs = relation.output
+    relation.cacheBuilder.serializer
+      .convertCachedBatchToColumnarBatch(
+        relation.cacheBuilder.cachedColumnBuffers, attrs, attrs, 
spark.sessionState.conf)
+      .mapPartitions { batches =>
+        batches.map { batch =>
+          val column = batch.column(0).asInstanceOf[ArrowColumnVector]
+          (column.getValueVector.getClass.getSimpleName, column.dataType(), 
batch.numRows(),
+            column.numNulls())
+        }
+      }
+      .collect()
+  }
+
+  private val nullPatterns: Seq[(String, Int => Boolean)] = Seq(
+    ("every 31st row null", i => i % 31 == 0),
+    ("no nulls", _ => false),
+    ("all nulls", _ => true))
+
+  test("SPARK-59571: TIME keeps its precision and its nulls through the cache, 
at every " +
+      "precision") {
+    // Every precision of TIME is written to the same TimeNanoVector, so the 
precision rides in
+    // the Arrow field metadata rather than in the value. The cache read path 
rebuilds its Arrow
+    // schema from the cache schema and wraps each vector in an 
ArrowColumnVector, whose type is
+    // recovered from that field, so the column's type on the way out pins the 
tag-and-recover
+    // path: dropping the precision key on either side falls back to 
TimeType(6). The values are
+    // truncated to the declared precision so a precision loss could not hide 
behind a value the
+    // type would round anyway.
+    val rows = 1000
+    val nanosPerDay = 86400000000000L
+    for (precision <- Seq(0, 3, 6, 9); (pattern, isNull) <- nullPatterns) {

Review Comment:
   Small coverage/naming point: `TimeType` supports every integer precision 
from 0 through 9, so this leaves 1, 2, 4, 5, 7, and 8 untested despite “every 
precision” in the PR title and test name.
   
   Could we iterate over `TimeType.MIN_PRECISION to TimeType.MAX_PRECISION`? 
Alternatively, if these four representative precisions are intentional to limit 
test runtime, please adjust the wording accordingly.



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