MaxGekk commented on code in PR #59641:
URL: https://github.com/apache/spark/pull/59641#discussion_r4326481644


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala:
##########
@@ -320,6 +320,16 @@ private[ops] object TimestampNanosParquetOps {
   private def epochNanosInRange(encode: => JLong): Boolean =
     try { encode; true } catch { case _: ArithmeticException => false }
 
+  // Below nanosecond precision, the reader truncates the stored 
epoch-nanoseconds to the read
+  // precision (see nanosConverter and TimestampNanosVectorUpdater), while a 
pushed down predicate
+  // compares the stored epoch-nanoseconds. E.g. a stored 00:00:00.123456789 
is read as
+  // 00:00:00.1234567 for TIMESTAMP_NTZ(7), so a pushed down `ts = 
00:00:00.1234567` would skip it.
+  private def acceptsNanosReadType(readType: DataType): Boolean = readType 
match {

Review Comment:
   Thanks for the quick follow-up. Same point as on #59606, but it costs more 
here: this withholds every value predicate below precision 9, although with `B 
= 10^(9-p)` and `read(s) = floor(s / B) * B`, `<` and `>=` on a literal on the 
read grid are exact against the stored values, and `>` / `!=` are only 
over-inclusive, which the retained post-scan Filter trims. Only `=`, `<=>`, 
`<=` and `IN` produce false negatives.
   
   The regression is measurable: the row-group skipping test in 
`ParquetTimestampNanosSuite` now asserts a full scan (`actual === 1024`) for a 
Spark-written TIMESTAMP_NTZ(7) file inferred back at precision 7, i.e. the 
default read without a user schema, where the stored values already sit on the 
read grid.
   
   A bound-widening rewrite keeps the skipping and is exact for foreign files 
too: `eq X` as `gtEq X AND lt X+B`, `ltEq X` as `lt X+B`, `gt X` as `gtEq X+B`, 
`lt` / `gtEq` unchanged, `notEq` / `In` as the complement / OR of such ranges, 
with `X+B` guarded by the existing `epochNanosInRange`. At minimum, 
`acceptsReadType` could take the operator so the four sound comparisons stay 
pushed.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala:
##########
@@ -339,6 +349,8 @@ private[ops] object TimestampNanosParquetOps {
       case _ => false
     }
 
+    override def acceptsReadType(readType: DataType): Boolean = 
acceptsNanosReadType(readType)

Review Comment:
   nit: this and `TimeTypeParquetOps.filterOps.acceptsReadType` are the same 
rule, "the read precision must equal the storage precision", with a constant 
swapped, and the trait default `acceptsReadType = true` means the next 
truncating type silently inherits the wrong-results bug if the override is 
forgotten. A `storagePrecision`-style hook on `ParquetFilterOps`, compared 
generically against the read type's precision in `ParquetFilters`, would 
implement it once and make the gate opt-out rather than opt-in.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFilterSuite.scala:
##########
@@ -2557,6 +2561,86 @@ abstract class ParquetFilterSuite extends ParquetTest 
with SharedSparkSession {
     }
   }
 
+  test("SPARK-60380: filter pushdown - nanosecond timestamp read with a lower 
precision") {

Review Comment:
   `withAllParquetReaders` toggles the vectorized vs row readers, not V1 vs V2, 
so the `ParquetScanBuilder.convertibleFilters` -> 
`ParquetPartitionReaderFactory` path with a lower-precision read schema is not 
exercised end to end. The SPARK-57102 test above shows the `USE_V1_SOURCE_LIST 
-> ""` pattern; wrapping this test in both modes would close the gap.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFilterSuite.scala:
##########
@@ -2416,11 +2416,14 @@ abstract class ParquetFilterSuite extends ParquetTest 
with SharedSparkSession {
     withSQLConf(
         SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true",
         SQLConf.PARQUET_FILTER_PUSHDOWN_INFILTERTHRESHOLD.key -> "1") {
+      // The columns are read with precision 9. A lower read precision 
truncates the values, so
+      // only IsNull and IsNotNull are pushed down then.
       Seq(7, 8, 9).foreach { p =>

Review Comment:
   With the read schema hard-coded to precision 9 and `convertToParquetType` 
ignoring `p` (always INT64 TIMESTAMP(NANOS)), the three iterations of this loop 
are now byte-identical. Either drop the loop, since the new test below iterates 
the read precision with `isDefined === (p == 9)`, or make `p` the read 
precision and fold the two tests together.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetFilterSuite.scala:
##########
@@ -2557,6 +2561,86 @@ abstract class ParquetFilterSuite extends ParquetTest 
with SharedSparkSession {
     }
   }
 
+  test("SPARK-60380: filter pushdown - nanosecond timestamp read with a lower 
precision") {
+    // A TIMESTAMP(9) value 1970-01-01 00:00:00.123456789 read as TIMESTAMP(7) 
is truncated to
+    // 00:00:00.1234567, while Parquet evaluates a pushed down predicate 
against the stored value.

Review Comment:
   nit: only read precision 7 is exercised end to end here; precision 8 (and 
8-written / 7-read) is covered only at the `createFilter` level, which checks 
`isDefined` only. Looping `Seq(7, 8)` for the read precision, as 
`ParquetTimestampNanosSuite` does, is a one-line change.



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