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 75260f828fdd [SPARK-57416][SQL] Align the Types Framework Parquet read 
guard for TimeType with the legacy reader
75260f828fdd is described below

commit 75260f828fdd97ff571d2b027669a6afd67198a2
Author: Stevo Mitric <[email protected]>
AuthorDate: Sun Jun 21 07:19:41 2026 +0200

    [SPARK-57416][SQL] Align the Types Framework Parquet read guard for 
TimeType with the legacy reader
    
    ### What changes were proposed in this pull request?
    Follow-up to the Types Framework Phase 3a Parquet work (SPARK-55444).
    
    `TimeTypeParquetOps.requireCompatibleParquetType` (the row-based read 
guard) is relaxed to accept `INT64 TIME(MICROS)` regardless of the 
`isAdjustedToUTC` flag, by dropping the `&& !t.isAdjustedToUTC` condition.
    This mirrors the legacy `ParquetRowConverter` guard, which only checked the 
TIME annotation and the MICROS unit. All other encodings (raw `INT64`, 
`TIME(NANOS)`, `INT32 TIME(MILLIS)`, `TIMESTAMP(_)`, `DECIMAL`, group) are 
still rejected.
    
    ### Why are the changes needed?
    Phase 3a's guard was stricter than the guard it replaced, so reading an 
`INT64 TIME(MICROS, isAdjustedToUTC=true)` column as `TimeType` failed on the 
row-based reader (`FAILED_READ_FILE`) while the default vectorized reader still 
accepted it — an inconsistency between the two readers and a behavior 
regression versus pre-framework Spark. Since `TimeType` is zone-less, 
`isAdjustedToUTC` carries no information on read (the raw micros-of-day decodes 
identically), so the value is unchanged [...]
    
    ### Does this PR introduce _any_ user-facing change?
    Yes (within unreleased master). Reading an `INT64 TIME(MICROS, 
isAdjustedToUTC=true)` Parquet column as `TimeType` via the row-based reader 
(vectorized reader disabled, or the column nested under struct/array/map) now 
succeeds instead of throwing `FAILED_READ_FILE`. The vectorized read path is 
unchanged.
    
    ### How was this patch tested?
    - `TimeTypeParquetOpsSuite`: flipped the `isAdjustedToUTC=true` case from 
reject to accept; the genuine mis-decode rejections are retained
    (8/8 pass).
    - `ParquetIOSuite`: new end-to-end test reading an `INT64 TIME(MICROS, 
isAdjustedToUTC=true)` column as `TimeType`, asserting correct values
    on both the vectorized and row-based readers (via `withAllParquetReaders`).
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: Claude Code (Claude Opus 4.8)
    
    Closes #56610 from stevomitric/stevomitric/fix-time.
    
    Authored-by: Stevo Mitric <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 .../parquet/types/ops/TimeTypeParquetOps.scala     | 22 ++++++----
 .../datasources/parquet/ParquetIOSuite.scala       | 39 ++++++++++++++++++
 .../types/ops/TimeTypeParquetOpsSuite.scala        | 48 +++++++++++-----------
 3 files changed, 79 insertions(+), 30 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimeTypeParquetOps.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimeTypeParquetOps.scala
index 7f05361d8f6c..96c7bc30a1da 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimeTypeParquetOps.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimeTypeParquetOps.scala
@@ -93,12 +93,14 @@ case class TimeTypeParquetOps(t: TimeType) extends 
ParquetTypeOps {
 private[ops] object TimeTypeParquetOps {
 
   /**
-   * Validates that a Parquet field can be decoded as TimeType. TimeType is 
stored
-   * as INT64 with TIME(MICROS, isAdjustedToUTC=false). Any other encoding (raw
-   * INT64, INT64 TIME(NANOS), INT32 TIME(MILLIS), INT64 TIMESTAMP(_), decimal-
-   * annotated, etc.) cannot be decoded as TimeType - throw the same error as
-   * the legacy ParquetRowConverter path so reads fail loudly instead of
-   * silently misinterpreting bytes.
+   * Validates that a Parquet field can be decoded as TimeType. TimeType is 
written
+   * as INT64 with TIME(MICROS, isAdjustedToUTC=false). On read, any INT64 
TIME(MICROS)
+   * column is accepted regardless of the isAdjustedToUTC flag: Spark's 
zone-less TimeType
+   * decodes the raw micros-of-day identically either way, matching the legacy
+   * ParquetRowConverter guard (see SPARK-57416). Any other encoding (raw 
INT64, INT64
+   * TIME(NANOS), INT32 TIME(MILLIS), INT64 TIMESTAMP(_), decimal-annotated, 
etc.) cannot
+   * be decoded as TimeType - throw the same error as the legacy 
ParquetRowConverter path
+   * so reads fail loudly instead of silently misinterpreting bytes.
    */
   private[ops] def requireCompatibleParquetType(
       sparkType: TimeType, parquetType: Type): Unit = {
@@ -106,7 +108,13 @@ private[ops] object TimeTypeParquetOps {
       parquetType.asPrimitiveType.getPrimitiveTypeName == INT64 &&
       (parquetType.getLogicalTypeAnnotation match {
         case t: LogicalTypeAnnotation.TimeLogicalTypeAnnotation =>
-          t.getUnit == TimeUnit.MICROS && !t.isAdjustedToUTC
+          // Accept both isAdjustedToUTC=false and =true. Spark's TimeType is 
zone-less
+          // local time, so the UTC-adjustment flag carries no extra 
information on read:
+          // the raw micros-of-day value decodes identically either way. 
Mirroring the
+          // legacy ParquetRowConverter guard (which only checked the TIME 
annotation and
+          // the MICROS unit) keeps the framework row-based read path 
consistent with both
+          // the legacy row-based reader and the still-lenient vectorized 
reader. SPARK-57416.
+          t.getUnit == TimeUnit.MICROS
         case _ => false
       })
     if (!ok) {
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetIOSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetIOSuite.scala
index 0fc32b15d833..3a06b59b6a18 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetIOSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetIOSuite.scala
@@ -1888,6 +1888,45 @@ class ParquetIOSuite extends ParquetTest with 
SharedSparkSession {
     }
   }
 
+  test("SPARK-57416: read INT64 TIME(MICROS, isAdjustedToUTC=true) as TimeType 
" +
+    "on both readers") {
+    // The Parquet TIME logical type may carry isAdjustedToUTC=true. Spark's 
TimeType is
+    // zone-less local time, so such a column is decoded as the raw 
micros-of-day, exactly
+    // as isAdjustedToUTC=false. Before SPARK-57416 the Types Framework 
row-based read guard
+    // rejected this encoding (FAILED_READ_FILE) while the vectorized path 
accepted it,
+    // leaving the two readers inconsistent. This pins that both readers now 
accept it.
+    // The column must be supplied via an explicit read schema because schema 
inference
+    // maps isAdjustedToUTC=true to an unsupported type.
+    val schema = MessageTypeParser.parseMessageType(
+      """message root {
+        |  required int64 time_micros(TIME(MICROS,true));
+        |}""".stripMargin)
+    val readSchema = new StructType().add("time_micros", TimeType())
+
+    for (dictEnabled <- Seq(true, false)) {
+      withTempDir { dir =>
+        val tablePath = new Path(s"${dir.getCanonicalPath}/times_utc.parquet")
+        val numRecords = 100
+
+        val writer = createParquetWriter(schema, tablePath, dictionaryEnabled 
= dictEnabled)
+        (0 until numRecords).foreach { _ =>
+          val record = new SimpleGroup(schema)
+          record.add(0, localTime(23, 59, 59, 123456) / 
DateTimeConstants.NANOS_PER_MICROS)
+          writer.write(record)
+        }
+        writer.close
+
+        withAllParquetReaders {
+          val df = spark.read.schema(readSchema).parquet(tablePath.toString)
+          assertResult(df.schema)(readSchema)
+          val lt = LocalTime.of(23, 59, 59, 123456000)
+          val expected = (0 until numRecords).map { _ => lt }.toDF()
+          checkAnswer(df, expected)
+        }
+      }
+    }
+  }
+
   // Deterministic INT32 sample shared by the INT32 widening tests below. 
Mixes sign,
   // zero, and MIN/MAX boundaries to catch sign-extension and precision 
regressions.
   private def widenSampleAt(i: Int): Int = i % 5 match {
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimeTypeParquetOpsSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimeTypeParquetOpsSuite.scala
index 9a21b5e3f4bb..36221cec8b2c 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimeTypeParquetOpsSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimeTypeParquetOpsSuite.scala
@@ -28,17 +28,18 @@ import org.apache.spark.sql.types.TimeType
 /**
  * Unit tests for [[TimeTypeParquetOps.requireCompatibleParquetType]].
  *
- * TimeType is stored in Parquet as INT64 TIME(MICROS, isAdjustedToUTC=false).
- * The read-path guard accepts only that canonical encoding and rejects every
- * other primitive/annotation combination so that reading fails loudly rather
- * than silently mis-decoding (e.g. interpreting NANOS as MICROS, which would
- * be off by 1000x).
+ * TimeType is written to Parquet as INT64 TIME(MICROS, 
isAdjustedToUTC=false). The
+ * read-path guard accepts any INT64 TIME(MICROS) column - both 
isAdjustedToUTC values -
+ * and rejects every other primitive/annotation combination so that reading 
fails loudly
+ * rather than silently mis-decoding (e.g. interpreting NANOS as MICROS, which 
would be
+ * off by 1000x).
  *
- * Note: rejecting isAdjustedToUTC=true is stricter than the legacy
- * ParquetRowConverter guard, which accepts that encoding. This is a known,
- * intentional divergence between the framework and legacy paths for this
- * single case; reconciling it (either by relaxing the framework guard or
- * tightening the legacy one) is tracked by SPARK-57416.
+ * SPARK-57416: the guard accepts isAdjustedToUTC=true to mirror the legacy
+ * ParquetRowConverter guard (which only checks the TIME annotation and the 
MICROS unit).
+ * Spark's TimeType is zone-less local time, so the flag carries no extra 
information on
+ * read and the raw micros-of-day value decodes identically either way. This 
keeps the
+ * framework read path consistent with both the legacy row-based reader and 
the vectorized
+ * reader.
  */
 class TimeTypeParquetOpsSuite extends SparkFunSuite {
 
@@ -54,7 +55,20 @@ class TimeTypeParquetOpsSuite extends SparkFunSuite {
     TimeTypeParquetOps.requireCompatibleParquetType(timeMicros, field)
   }
 
-  // ---------- the four primary reject paths ----------
+  test("accepts INT64 TIME(MICROS, isAdjustedToUTC=true) - matches legacy 
lenient read") {
+    // SPARK-57416: the framework read guard mirrors the legacy 
ParquetRowConverter guard,
+    // which accepts INT64 TIME(MICROS) regardless of isAdjustedToUTC. Spark's 
TimeType is
+    // zone-less, so the raw micros-of-day value decodes identically either 
way; rejecting
+    // this encoding would diverge from both the legacy row-based reader and 
the (lenient)
+    // vectorized reader.
+    val field = Types.primitive(INT64, REQUIRED)
+      .as(LogicalTypeAnnotation.timeType(true, TimeUnit.MICROS))
+      .named("c")
+    // Must not throw.
+    TimeTypeParquetOps.requireCompatibleParquetType(timeMicros, field)
+  }
+
+  // ---------- the primary reject paths ----------
 
   test("rejects raw INT64 with no logical type annotation") {
     val field = Types.primitive(INT64, REQUIRED).named("c")
@@ -76,18 +90,6 @@ class TimeTypeParquetOpsSuite extends SparkFunSuite {
     assertRejects(timeMicros, field)
   }
 
-  test("rejects INT64 TIME(MICROS, isAdjustedToUTC=true)") {
-    // The intended framework behavior is to reject this encoding: the 
canonical
-    // TimeType representation is local-time (isAdjustedToUTC=false). The 
legacy
-    // ParquetRowConverter guard accepts the encoding, so this is a known,
-    // intentional framework-vs-legacy divergence; reconciliation is tracked by
-    // SPARK-57416.
-    val field = Types.primitive(INT64, REQUIRED)
-      .as(LogicalTypeAnnotation.timeType(true, TimeUnit.MICROS))
-      .named("c")
-    assertRejects(timeMicros, field)
-  }
-
   // ---------- additional rejects for full reject-set coverage ----------
 
   test("rejects INT64 TIMESTAMP(MICROS) - wrong annotation kind") {


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

Reply via email to