This is an automated email from the ASF dual-hosted git repository.

cloud-fan 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 c7b981d01177 [SPARK-55444][SQL] Add Framework support for the 
vectorized lazy-decoding gate for TimeType
c7b981d01177 is described below

commit c7b981d0117792bd7a0117f9a22f25fcb293f789
Author: Stevo Mitric <[email protected]>
AuthorDate: Mon Jul 6 22:52:34 2026 +0800

    [SPARK-55444][SQL] Add Framework support for the vectorized lazy-decoding 
gate for TimeType
    
    ## What changes were proposed in this pull request?
    
    Moves the last hardcoded `TimeType` check out of the vectorized reader into 
the Types Framework. `VectorizedColumnReader.isLazyDecodingSupported` decided 
lazy dictionary decoding for TIME columns via the annotation-keyed 
`ParquetVectorUpdaterFactory.isTimeTypeMatched(MICROS|NANOS)`; it now 
dispatches framework-first (like `getVectorUpdater`):
    
    - `ParquetTypeOps.supportsLazyDictionaryDecoding(descriptor): Boolean` 
(default `false`, opt-in — matching the sibling `isBatchReadSupported`) + Java 
entry point `supportsLazyDictionaryDecodingOrNull` (null for non-framework 
types).
    - `TimeTypeParquetOps` overrides it to `false`: the updater does per-value 
micros→nanos conversion + truncation to the requested precision, which lazy 
dictionary decoding would bypass.
    - `VectorizedColumnReader.isLazyDecodingSupported` dispatches 
framework-first; the two `isTimeTypeMatched` arms are removed.
    - Removes the now-dead `ParquetVectorUpdaterFactory.isTimeTypeMatched` and 
its unused import.
    
    ### Why are the changes needed?
    
    `isTimeTypeMatched` was the last hardcoded `TimeType` reference in the 
vectorized path after the updater moved into the framework. Routing the 
lazy-decoding gate through `ParquetTypeOps` finishes that migration: a 
framework type now fully owns its vectorized-read behavior (both the updater 
and the lazy-decoding capability) with no `TimeType`-specific code left in 
`VectorizedColumnReader` / `ParquetVectorUpdaterFactory`.
    
    The `false` default is fail-safe: a framework type that opts into 
vectorized reads (`isBatchReadSupported = true`) with a per-value updater but 
forgets to override this would, under a `true` default, silently get lazy 
dictionary decoding and return wrong results; `false` makes the worst case a 
missed optimization instead. A type whose updater is a plain identity copy can 
override to `true` to regain the optimization.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. Reading `TimeType` from Parquet behaves exactly as before; only the 
internal routing of the lazy-decoding decision changes.
    
    ### How was this patch tested?
    
    `TimeTypeParquetOpsSuite` (unit tests for `supportsLazyDictionaryDecoding` 
/ `supportsLazyDictionaryDecodingOrNull`), and `ParquetIOSuite` TIME tests 
under `withAllParquetReaders` (dict on/off) exercise the dictionary path 
end-to-end.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 4.8)
    
    Closes #56991 from stevomitric/stevomitric/parquet-tf-lazy-decoding.
    
    Authored-by: Stevo Mitric <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../parquet/ParquetVectorUpdaterFactory.java       |  6 -----
 .../parquet/VectorizedColumnReader.java            | 16 +++++++-----
 .../parquet/types/ops/ParquetTypeOps.scala         | 29 ++++++++++++++++++++++
 .../parquet/types/ops/TimeTypeParquetOps.scala     |  7 ++++++
 .../types/ops/TimeTypeParquetOpsSuite.scala        | 14 +++++++++++
 5 files changed, 60 insertions(+), 12 deletions(-)

diff --git 
a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java
 
b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java
index b15777eacc52..155d70685950 100644
--- 
a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java
+++ 
b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java
@@ -24,7 +24,6 @@ import org.apache.parquet.schema.LogicalTypeAnnotation;
 import 
org.apache.parquet.schema.LogicalTypeAnnotation.IntLogicalTypeAnnotation;
 import 
org.apache.parquet.schema.LogicalTypeAnnotation.DateLogicalTypeAnnotation;
 import 
org.apache.parquet.schema.LogicalTypeAnnotation.DecimalLogicalTypeAnnotation;
-import 
org.apache.parquet.schema.LogicalTypeAnnotation.TimeLogicalTypeAnnotation;
 import 
org.apache.parquet.schema.LogicalTypeAnnotation.TimestampLogicalTypeAnnotation;
 import 
org.apache.parquet.schema.LogicalTypeAnnotation.UnknownLogicalTypeAnnotation;
 import org.apache.parquet.schema.PrimitiveType;
@@ -250,11 +249,6 @@ public class ParquetVectorUpdaterFactory {
       annotation.getUnit() == unit;
   }
 
-  boolean isTimeTypeMatched(LogicalTypeAnnotation.TimeUnit unit) {
-    return logicalTypeAnnotation instanceof TimeLogicalTypeAnnotation 
annotation &&
-      annotation.getUnit() == unit;
-  }
-
   boolean isUnsignedIntTypeMatched(int bitWidth) {
     return logicalTypeAnnotation instanceof IntLogicalTypeAnnotation 
annotation &&
       !annotation.isSigned() && annotation.getBitWidth() == bitWidth;
diff --git 
a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedColumnReader.java
 
b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedColumnReader.java
index 01f4573557dc..63aa0951313f 100644
--- 
a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedColumnReader.java
+++ 
b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedColumnReader.java
@@ -39,6 +39,7 @@ import 
org.apache.parquet.schema.LogicalTypeAnnotation.TimeUnit;
 import org.apache.parquet.schema.PrimitiveType;
 
 import org.apache.spark.SparkUnsupportedOperationException;
+import 
org.apache.spark.sql.execution.datasources.parquet.types.ops.ParquetTypeOps$;
 import org.apache.spark.sql.execution.vectorized.WritableColumnVector;
 import org.apache.spark.sql.types.DataType;
 import org.apache.spark.sql.types.Decimal;
@@ -150,6 +151,14 @@ public class VectorizedColumnReader {
   private boolean isLazyDecodingSupported(
       PrimitiveType.PrimitiveTypeName typeName,
       DataType sparkType) {
+    // Types Framework: a framework-managed type decides whether its 
dictionary-encoded column can
+    // be lazily decoded (false when its vectorized updater does per-value 
processing that lazy
+    // decoding would bypass). Non-framework types fall through to the 
built-in cases below.
+    Boolean frameworkDecision =
+        
ParquetTypeOps$.MODULE$.supportsLazyDictionaryDecodingOrNull(sparkType, 
descriptor);
+    if (frameworkDecision != null) {
+      return frameworkDecision;
+    }
     boolean isSupported = false;
     // Don't use lazy dictionary decoding if the column needs extra 
processing: upcasting or date
     // rebasing.
@@ -166,13 +175,8 @@ public class VectorizedColumnReader {
       }
       case INT64: {
         boolean isDecimal = sparkType instanceof DecimalType;
-        // TIME columns (both MICROS and NANOS) need per-value processing in 
the updater: a unit
-        // conversion for MICROS and/or truncation to the requested precision. 
Lazy dictionary
-        // decoding would bypass the updater, so it must be disabled for them.
         boolean needsUpcast = (isDecimal && 
!DecimalType.is64BitDecimalType(sparkType)) ||
-          updaterFactory.isTimestampTypeMatched(TimeUnit.MILLIS) ||
-          updaterFactory.isTimeTypeMatched(TimeUnit.MICROS) ||
-          updaterFactory.isTimeTypeMatched(TimeUnit.NANOS);
+          updaterFactory.isTimestampTypeMatched(TimeUnit.MILLIS);
         boolean needsRebase = 
updaterFactory.isTimestampTypeMatched(TimeUnit.MICROS) &&
           !"CORRECTED".equals(datetimeRebaseMode);
         isSupported = !needsUpcast && !needsRebase && 
!needsDecimalScaleRebase(sparkType);
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/ParquetTypeOps.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/ParquetTypeOps.scala
index 4184efa82f84..63209572c94d 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/ParquetTypeOps.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/ParquetTypeOps.scala
@@ -214,6 +214,25 @@ private[parquet] trait ParquetTypeOps extends Serializable 
{
    * @param descriptor the Parquet column descriptor being read
    */
   def getVectorUpdater(descriptor: ColumnDescriptor): 
Option[ParquetVectorUpdater] = None
+
+  /**
+   * Whether a dictionary-encoded column of this type can be lazily 
dictionary-decoded on the
+   * vectorized path (the dictionary is attached to the column vector and 
decoded on read) rather
+   * than eagerly decoding every value up front. Consulted (Spark DataType -> 
ops) by
+   * `VectorizedColumnReader.isLazyDecodingSupported`.
+   *
+   * Default is false - a type must opt in by overriding to true, matching the 
opt-in stance of
+   * [[isBatchReadSupported]]. This is deliberately fail-safe: lazy decoding 
bypasses the
+   * per-value work a vectorized updater ([[getVectorUpdater]]) may do (unit 
conversion,
+   * truncation, rebasing), so a type that opts into vectorized reads but 
forgets to opt out of
+   * lazy decoding when it needs per-value processing would silently return 
wrong results. With a
+   * false default the worst case is a missed lazy-decode optimization, not 
incorrect data. Types
+   * whose updater is a plain identity copy (no per-value processing) should 
override to true to
+   * regain the optimization.
+   *
+   * @param descriptor the Parquet column descriptor being read
+   */
+  def supportsLazyDictionaryDecoding(descriptor: ColumnDescriptor): Boolean = 
false
 }
 
 /**
@@ -249,4 +268,14 @@ private[parquet] object ParquetTypeOps {
   private[parquet] def getVectorUpdaterOrNull(
       dt: DataType, descriptor: ColumnDescriptor): ParquetVectorUpdater =
     apply(dt).flatMap(_.getVectorUpdater(descriptor)).orNull
+
+  /**
+   * Java-friendly entry point for `VectorizedColumnReader`: whether `dt`'s 
dictionary-encoded
+   * column can be lazily decoded, or null if `dt` is not framework-managed 
(so the caller keeps
+   * its built-in decision).
+   */
+  private[parquet] def supportsLazyDictionaryDecodingOrNull(
+      dt: DataType, descriptor: ColumnDescriptor): java.lang.Boolean =
+    apply(dt).map(o => 
java.lang.Boolean.valueOf(o.supportsLazyDictionaryDecoding(descriptor)))
+      .orNull
 }
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 c868716e7fc9..8e6fcee925d7 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
@@ -121,6 +121,13 @@ case class TimeTypeParquetOps(t: TimeType) extends 
ParquetTypeOps {
       None
     }
   }
+
+  // TIME decoding converts micros->nanos (for TIME(MICROS)) and truncates to 
the requested
+  // precision per value in the updater; lazy dictionary decoding would attach 
the raw dictionary
+  // and skip that processing, so it must be disabled. This matches the 
fail-safe trait default,
+  // but is stated explicitly because TimeType opts into vectorized reads 
(isBatchReadSupported =
+  // true) and the per-value work is exactly why lazy decoding is unsafe here.
+  override def supportsLazyDictionaryDecoding(descriptor: ColumnDescriptor): 
Boolean = false
 }
 
 private[ops] object TimeTypeParquetOps {
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 363a21e7dec9..70091c6379bc 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
@@ -167,6 +167,20 @@ class TimeTypeParquetOpsSuite extends SparkFunSuite {
     assert(ParquetTypeOps.getVectorUpdaterOrNull(IntegerType, null) == null)
   }
 
+  test("supportsLazyDictionaryDecoding is false for TimeType (updater does 
per-value work)") {
+    // TIME decoding does per-value micros->nanos + truncation in the updater, 
so lazy dictionary
+    // decoding (which would bypass the updater) must stay disabled on the 
vectorized path.
+    assert(!TimeTypeParquetOps(timeMicros)
+      .supportsLazyDictionaryDecoding(timeColumn(TimeUnit.MICROS)))
+    assert(!TimeTypeParquetOps(timeNanos)
+      .supportsLazyDictionaryDecoding(timeColumn(TimeUnit.NANOS)))
+    // Java-friendly companion entry point used by VectorizedColumnReader: 
FALSE for TimeType,
+    // null for a non-framework type (so the reader keeps its built-in 
lazy-decoding decision).
+    assert(ParquetTypeOps.supportsLazyDictionaryDecodingOrNull(
+      timeMicros, timeColumn(TimeUnit.MICROS)) === java.lang.Boolean.FALSE)
+    assert(ParquetTypeOps.supportsLazyDictionaryDecodingOrNull(IntegerType, 
null) == null)
+  }
+
   // ---------- helper ----------
 
   private def assertRejects(sparkType: TimeType, field: Type): Unit = {


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

Reply via email to