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 d635a1f07fbf [SPARK-57570][SQL] Support TimeType in vectorized-reader 
column population
d635a1f07fbf is described below

commit d635a1f07fbfb1de2d2bb8313ff3591a578cccf3
Author: Maxim Gekk <[email protected]>
AuthorDate: Tue Jun 30 08:47:14 2026 +0200

    [SPARK-57570][SQL] Support TimeType in vectorized-reader column population
    
    ### What changes were proposed in this pull request?
    
    Make the vectorized reader handle `TimeType` when it populates partition, 
missing, and constant columns.
    
    The only production change is one branch in `ColumnVectorUtils.appendValue` 
(the `toBatch` path): a TIME value is now appended as its nanos-of-day `long` 
via `DateTimeUtils.localTimeToNanos`. Before, there was no `TimeType` case and 
it failed with "Datatype not supported". The `populate` path already worked, 
since `TimeType` is physically a `long`.
    
    This builds on SPARK-54203, which added the underlying 
`RowToColumnConverter` and column-vector allocation support.
    
    The unsupported-type branch still throws `_LEGACY_ERROR_TEMP_3192` for 
other types; renaming that legacy error is tracked separately in SPARK-57745.
    
    ### Why are the changes needed?
    
    This makes `TimeType` a first-class citizen in the vectorized (columnar) 
read path instead of a type that fails depending on where a column comes from. 
Concretely, it enables:
    
    - **TIME partition columns in vectorized reads.** A table partitioned by a 
TIME column previously errored when the partition value was materialized into 
the columnar batch. With this change such reads succeed with the vectorized 
reader engaged, so partition pruning and the columnar scan both work.
    - **Schema-evolution / "missing" TIME columns.** When a Parquet/ORC file 
predates a TIME column added to the table schema, the reader fills that column 
via the same population path; those reads now succeed instead of failing.
    - **Constant-folded TIME columns** injected into a scan populate correctly.
    - **`toBatch` round-trips with TIME**, e.g. row-to-columnar conversions 
that carry `java.time.LocalTime` values.
    
    Without this, queries touching TIME columns in these scenarios either fail 
with an unsupported-datatype error or fall back to the slower row-based reader. 
After the change, TIME behaves consistently with `DATE`, `TIMESTAMP`, and 
interval types in this layer, and downstream code built on `ColumnarBatch` can 
carry TIME columns through the population path without special-casing. It also 
clears a blocker in the SPARK-54203 umbrella and keeps the vectorized layer's 
hand-maintained type dis [...]
    
    Note: physically reading TIME data columns stored inside Parquet/ORC files 
(as opposed to populated partition / missing / constant columns) is a separate 
concern and is out of scope here.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. TIME is an in-progress, not-yet-released data type; this only widens 
internal vectorized support so that previously-failing TIME column population 
now succeeds.
    
    ### How was this patch tested?
    
    New unit tests:
    - `ColumnVectorUtilsSuite` (the `populate` / constant-column path): TIME 
across precisions 0/6/7/9, boundary values (`00:00:00`, `23:59:59.999999999`), 
null (missing column), and TIME nested in struct / array / map.
    - `ColumnarBatchSuite` (the `toBatch` path): `TimeType` added to the 
random-schema test and `compareStruct` (top-level and array element), a 
per-precision `testVector` (0/6/7/9 + boundaries), a nested struct/array 
`toBatch` test, and a negative unsupported-type case.
    
    Ran `build/sbt 'sql/testOnly *ColumnVectorUtilsSuite *ColumnarBatchSuite'` 
(93 tests pass). Scalastyle and Java checkstyle pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Cursor
    
    Closes #56858 from MaxGekk/time-vec-column-pop.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 .../execution/vectorized/ColumnVectorUtils.java    |  3 +
 .../vectorized/ColumnVectorUtilsSuite.scala        | 98 +++++++++++++++++++++-
 .../execution/vectorized/ColumnarBatchSuite.scala  | 87 ++++++++++++++++++-
 3 files changed, 184 insertions(+), 4 deletions(-)

diff --git 
a/sql/core/src/main/java/org/apache/spark/sql/execution/vectorized/ColumnVectorUtils.java
 
b/sql/core/src/main/java/org/apache/spark/sql/execution/vectorized/ColumnVectorUtils.java
index 44a50fc3f702..a2d016a015c4 100644
--- 
a/sql/core/src/main/java/org/apache/spark/sql/execution/vectorized/ColumnVectorUtils.java
+++ 
b/sql/core/src/main/java/org/apache/spark/sql/execution/vectorized/ColumnVectorUtils.java
@@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets;
 import java.sql.Date;
 import java.sql.Timestamp;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -236,6 +237,8 @@ public class ColumnVectorUtils {
         dst.appendLong(DateTimeUtils.fromJavaTimestamp((Timestamp) o));
       } else if (t instanceof TimestampNTZType) {
         dst.appendLong(DateTimeUtils.localDateTimeToMicros((LocalDateTime) o));
+      } else if (t instanceof TimeType) {
+        dst.appendLong(DateTimeUtils.localTimeToNanos((LocalTime) o));
       } else {
         throw new SparkUnsupportedOperationException(
           "UNSUPPORTED_DATATYPE", Map.of("typeName", 
QueryExecutionErrors.toSQLType(t)));
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/vectorized/ColumnVectorUtilsSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/vectorized/ColumnVectorUtilsSuite.scala
index 314eac92c195..a265dc895d19 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/vectorized/ColumnVectorUtilsSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/vectorized/ColumnVectorUtilsSuite.scala
@@ -17,9 +17,11 @@
 
 package org.apache.spark.sql.execution.vectorized
 
+import java.time.LocalTime
+
 import org.apache.spark.SparkFunSuite
 import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.util.{ArrayBasedMapData, GenericArrayData}
+import org.apache.spark.sql.catalyst.util.{ArrayBasedMapData, DateTimeUtils, 
GenericArrayData}
 import org.apache.spark.sql.types._
 import org.apache.spark.unsafe.types.{CalendarInterval, TimestampNanosVal}
 import org.apache.spark.unsafe.types.UTF8String
@@ -257,4 +259,98 @@ class ColumnVectorUtilsSuite extends SparkFunSuite {
     ColumnVectorUtils.populate(vector, InternalRow(null), 0)
     assert(vector.hasNull)
   }
+
+  private def timeNanos(s: String): Long = 
DateTimeUtils.localTimeToNanos(LocalTime.parse(s))
+
+  // TimeType is physically a long (nanoseconds since midnight). Precision 
affects display only,
+  // not storage, so every TimeType(p) is filled through the same 
PhysicalLongType code path.
+  Seq(
+    0 -> "12:30:45",
+    6 -> "12:30:45.123456",
+    7 -> "12:30:45.1234567",
+    9 -> "12:30:45.123456789").foreach { case (p, s) =>
+    testConstantColumnVector(s"fill time p=$p", 10, TimeType(p)) { vector =>
+      val nanos = timeNanos(s)
+      ColumnVectorUtils.populate(vector, InternalRow(nanos), 0)
+      (0 until 10).foreach { i =>
+        assert(vector.getLong(i) == nanos)
+      }
+    }
+  }
+
+  testConstantColumnVector("fill time boundaries", 10, TimeType(9)) { vector =>
+    Seq(0L, 86399999999999L).foreach { nanos =>
+      ColumnVectorUtils.populate(vector, InternalRow(nanos), 0)
+      (0 until 10).foreach { i =>
+        assert(vector.getLong(i) == nanos)
+      }
+    }
+  }
+
+  testConstantColumnVector("fill time null", 10, TimeType(6)) { vector =>
+    ColumnVectorUtils.populate(vector, InternalRow(null), 0)
+    assert(vector.hasNull)
+    assert(vector.numNulls() == 10)
+    (0 until 10).foreach { i =>
+      assert(vector.isNullAt(i))
+    }
+  }
+
+  testConstantColumnVector("fill struct with time field", 10,
+    new StructType().add("t", TimeType(6)).add("flag", BooleanType)) { vector 
=>
+    val nanos = timeNanos("01:02:03.456789")
+    ColumnVectorUtils.populate(vector, InternalRow(InternalRow(nanos, true)), 
0)
+    (0 until 10).foreach { i =>
+      assert(vector.getChild(0).getLong(i) == nanos)
+      assert(vector.getChild(1).getBoolean(i))
+    }
+  }
+
+  testConstantColumnVector("fill struct with null time field", 10,
+    new StructType().add("t", TimeType(6), nullable = true).add("flag", 
BooleanType)) { vector =>
+    ColumnVectorUtils.populate(vector, InternalRow(InternalRow(null, true)), 0)
+    (0 until 10).foreach { i =>
+      assert(vector.getChild(0).isNullAt(i))
+      assert(vector.getChild(1).getBoolean(i))
+    }
+  }
+
+  testConstantColumnVector("fill array of time", 10, ArrayType(TimeType(9))) { 
vector =>
+    val n0 = timeNanos("00:00:01")
+    val n1 = timeNanos("12:00:00.123456789")
+    val n2 = 86399999999999L
+    val arr = new GenericArrayData(Array[Any](n0, n1, n2))
+    ColumnVectorUtils.populate(vector, InternalRow(arr), 0)
+    (0 until 10).foreach { i =>
+      val a = vector.getArray(i)
+      assert(a.numElements() == 3)
+      assert(a.getLong(0) == n0)
+      assert(a.getLong(1) == n1)
+      assert(a.getLong(2) == n2)
+    }
+  }
+
+  testConstantColumnVector("fill null array of time", 10, 
ArrayType(TimeType(6))) { vector =>
+    ColumnVectorUtils.populate(vector, InternalRow(null), 0)
+    assert(vector.hasNull)
+  }
+
+  testConstantColumnVector("fill map of int -> time", 10,
+    MapType(IntegerType, TimeType(6))) { vector =>
+    val keys = new GenericArrayData(Array[Any](1, 2, 3))
+    val v0 = timeNanos("00:00:00")
+    val v1 = timeNanos("06:30:15.123456")
+    val v2 = 86399999999999L
+    val values = new GenericArrayData(Array[Any](v0, v1, v2))
+    val map = new ArrayBasedMapData(keys, values)
+    ColumnVectorUtils.populate(vector, InternalRow(map), 0)
+    (0 until 10).foreach { i =>
+      val m = vector.getMap(i)
+      assert(m.numElements() == 3)
+      assert(m.keyArray().toIntArray === Array(1, 2, 3))
+      assert(m.valueArray().getLong(0) == v0)
+      assert(m.valueArray().getLong(1) == v1)
+      assert(m.valueArray().getLong(2) == v2)
+    }
+  }
 }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/vectorized/ColumnarBatchSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/vectorized/ColumnarBatchSuite.scala
index 4825d662a295..6f0e39bb9c66 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/vectorized/ColumnarBatchSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/vectorized/ColumnarBatchSuite.scala
@@ -21,7 +21,7 @@ import java.nio.ByteBuffer
 import java.nio.ByteOrder
 import java.nio.charset.StandardCharsets
 import java.sql.{Date, Timestamp}
-import java.time.LocalDateTime
+import java.time.{LocalDateTime, LocalTime}
 import java.util
 
 import scala.collection.mutable
@@ -32,7 +32,7 @@ import scala.util.Random
 import org.apache.arrow.vector.IntVector
 import org.apache.parquet.bytes.ByteBufferInputStream
 
-import org.apache.spark.SparkFunSuite
+import org.apache.spark.{SparkFunSuite, SparkUnsupportedOperationException}
 import org.apache.spark.memory.MemoryMode
 import org.apache.spark.sql.{RandomDataGenerator, Row}
 import org.apache.spark.sql.catalyst.InternalRow
@@ -1432,6 +1432,10 @@ class ColumnarBatchSuite extends SparkFunSuite {
             assert(r1.getLong(ordinal) ==
               
DateTimeUtils.localDateTimeToMicros(r2.getAs[LocalDateTime](ordinal)),
               "Seed = " + seed)
+          case _: TimeType =>
+            assert(r1.getLong(ordinal) ==
+              DateTimeUtils.localTimeToNanos(r2.getAs[LocalTime](ordinal)),
+              "Seed = " + seed)
           case t: DecimalType =>
             val d1 = r1.getDecimal(ordinal, t.precision, t.scale).toBigDecimal
             val d2 = r2.getDecimal(ordinal)
@@ -1506,6 +1510,17 @@ class ColumnarBatchSuite extends SparkFunSuite {
                   }
                   i += 1
                 }
+              case _: TimeType =>
+                var i = 0
+                while (i < a1.length) {
+                  assert((a1(i) == null) == (a2(i) == null), "Seed = " + seed)
+                  if (a1(i) != null) {
+                    val i1 = a1(i).asInstanceOf[Long]
+                    val i2 = 
DateTimeUtils.localTimeToNanos(a2(i).asInstanceOf[LocalTime])
+                    assert(i1 === i2, "Seed = " + seed)
+                  }
+                  i += 1
+                }
               case t: DecimalType =>
                 var i = 0
                 while (i < a1.length) {
@@ -1562,7 +1577,8 @@ class ColumnarBatchSuite extends SparkFunSuite {
       DecimalType.ShortDecimal, DecimalType.IntDecimal, 
DecimalType.ByteDecimal,
       DecimalType.FloatDecimal, DecimalType.LongDecimal, new DecimalType(5, 2),
       new DecimalType(12, 2), new DecimalType(30, 10), CalendarIntervalType,
-      DateType, StringType, BinaryType, TimestampType, TimestampNTZType)
+      DateType, StringType, BinaryType, TimestampType, TimestampNTZType,
+      TimeType(0), TimeType(3), TimeType(), TimeType(TimeType.MAX_PRECISION))
     val seed = System.nanoTime()
     val NUM_ROWS = 200
     val NUM_ITERS = 1000
@@ -2126,6 +2142,71 @@ class ColumnarBatchSuite extends SparkFunSuite {
     }
   }
 
+  // TimeType is physically a long (nanoseconds since midnight); precision 
affects display only.
+  // The generic `get(int, DataType)` accessor is intentionally not extended 
for TimeType in this
+  // change (tracked separately), so values are read back via the typed 
`getLong` accessor.
+  Seq(0, 6, 7, 9).foreach { p =>
+    val dt = TimeType(p)
+    testVector(s"TIME(precision=$p)", 10, dt) {
+      column =>
+        val values = Array(0L, 86399999999999L) ++ (2 until 10).map(_.toLong * 
1000000000L)
+        (0 until 10).foreach { i =>
+          column.putLong(i, values(i))
+        }
+        val batchRow = new ColumnarBatchRow(Array(column))
+        (0 until 10).foreach { i =>
+          batchRow.rowId = i
+          assert(batchRow.getLong(0) == values(i))
+          val batchRowCopy = batchRow.copy()
+          assert(batchRowCopy.getLong(0) == values(i))
+        }
+    }
+  }
+
+  test("SPARK-57570: toBatch with TIME nested in struct and array") {
+    val schema = new StructType()
+      .add("s", new StructType().add("t", TimeType(6)).add("flag", 
BooleanType))
+      .add("a", ArrayType(TimeType(9)))
+    val t1 = LocalTime.parse("01:02:03.123456")
+    val t2 = LocalTime.parse("23:59:59.999999999")
+    val t3 = LocalTime.parse("00:00:00")
+    val n1 = DateTimeUtils.localTimeToNanos(t1)
+    val n2 = DateTimeUtils.localTimeToNanos(t2)
+    val n3 = DateTimeUtils.localTimeToNanos(t3)
+    val rows = Seq(
+      Row(Row(t1, true), Seq(t1, t2)),
+      Row(Row(t3, false), Seq(t3)))
+    Seq(MemoryMode.ON_HEAP, MemoryMode.OFF_HEAP).foreach { memMode =>
+      val batch = ColumnVectorUtils.toBatch(schema, memMode, 
rows.iterator.asJava)
+      try {
+        assert(batch.numRows() == 2)
+        val structCol = batch.column(0)
+        assert(structCol.getChild(0).getLong(0) == n1)
+        assert(structCol.getChild(1).getBoolean(0))
+        assert(structCol.getChild(0).getLong(1) == n3)
+        assert(!structCol.getChild(1).getBoolean(1))
+        val arrCol = batch.column(1)
+        val a0 = arrCol.getArray(0)
+        assert(a0.numElements() == 2)
+        assert(a0.getLong(0) == n1)
+        assert(a0.getLong(1) == n2)
+        val a1 = arrCol.getArray(1)
+        assert(a1.numElements() == 1)
+        assert(a1.getLong(0) == n3)
+      } finally {
+        batch.close()
+      }
+    }
+  }
+
+  test("SPARK-57570: toBatch throws on unsupported data type") {
+    val schema = new StructType().add("m", MapType(IntegerType, IntegerType))
+    intercept[SparkUnsupportedOperationException] {
+      ColumnVectorUtils.toBatch(
+        schema, MemoryMode.ON_HEAP, Seq(Row(Map(1 -> 2))).iterator.asJava)
+    }
+  }
+
   testVector("[SPARK-55552] Variant", 3, VariantType) {
     column =>
       val valueChild = column.getChild(0)


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

Reply via email to