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 584c87335982 [SPARK-57573][CONNECT] Support the TIME data type in the 
Protobuf connector
584c87335982 is described below

commit 584c87335982bffb05a95fa7a566c04064d353f5
Author: Maxim Gekk <[email protected]>
AuthorDate: Fri Jun 26 14:41:01 2026 +0200

    [SPARK-57573][CONNECT] Support the TIME data type in the Protobuf connector
    
    ### What changes were proposed in this pull request?
    
    This PR adds support for the Spark `TIME` (`TimeType`) data type in the 
Protobuf connector (`to_protobuf` / `from_protobuf`), matching the existing 
Avro support added in SPARK-54473.
    
    Spark `TimeType` is mapped to a plain Protobuf `int64` field holding the 
number of nanoseconds since midnight (nanoseconds-of-day). Since `TimeType` is 
stored internally as nanoseconds since midnight, the conversion is a direct 
copy of the `Long` value (no unit math).
    
    - `ProtobufSerializer`: added a `(TimeType, LONG)` case that writes a 
`TimeType` column into an `int64` field as nanos-of-day.
    - `ProtobufDeserializer`: added a `(LONG, TimeType)` case that reads an 
`int64` field back into a `TimeType` value.
    - `docs/sql-data-sources-protobuf.md`: documented the new `TimeType` -> 
`int64 (nanoseconds-of-day)` mapping.
    
    Note: a bare Protobuf `int64` carries no logical-type marker, and 
`from_protobuf` always derives its output schema from the descriptor via 
`SchemaConverters.toSqlType`. Therefore `from_protobuf` infers such a field as 
`LongType` (nanoseconds-of-day), which can be cast back to `TIME` with 
`CAST(... AS TIME)`. The `int64 -> TimeType` branch is currently reachable only 
by constructing a `ProtobufDeserializer` with an explicit `TimeType` schema 
(exercised by the round-trip test); it is th [...]
    
    ### Why are the changes needed?
    
    To extend support for the TIME data type to the Protobuf connector, closing 
the gap noted in SPARK-57573 where `SchemaConverters`/serializer/deserializer 
had no `TimeType` mapping. This is a sub-task of SPARK-57550 (Extend support 
for the TIME data type).
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. `to_protobuf` now accepts `TIME` columns, writing them to a Protobuf 
`int64` field as nanoseconds-of-day, and the Protobuf data source documentation 
lists the new mapping.
    
    ### How was this patch tested?
    
    Added tests in `ProtobufFunctionsSuite`:
    - `SPARK-57573: Handle TimeType between to_protobuf and from_protobuf` - 
verifies a `TIME` column serializes to the `int64` field and reads back as 
`LongType` equal to `LocalTime.toNanoOfDay`.
    - `SPARK-57573: TimeType roundtrip through ProtobufSerializer and 
ProtobufDeserializer` - direct serializer/deserializer round trip with an 
explicit `TimeType` schema, covering precisions (`TimeType(0)`, `TimeType(6)`) 
and boundary values (`00:00:00`, `23:59:59.999999`).
    
    All existing `ProtobufFunctionsSuite`, `ProtobufSerdeSuite`, and 
`ProtobufCatalystDataConversionSuite` tests pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Cursor
    
    Closes #56807 from MaxGekk/time-protobuf.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
---
 .../spark/sql/protobuf/ProtobufDeserializer.scala  |  5 ++
 .../spark/sql/protobuf/ProtobufSerializer.scala    |  5 ++
 .../test/resources/protobuf/functions_suite.proto  |  5 ++
 .../sql/protobuf/ProtobufFunctionsSuite.scala      | 65 +++++++++++++++++++++-
 docs/sql-data-sources-protobuf.md                  |  6 ++
 5 files changed, 85 insertions(+), 1 deletion(-)

diff --git 
a/connector/protobuf/src/main/scala/org/apache/spark/sql/protobuf/ProtobufDeserializer.scala
 
b/connector/protobuf/src/main/scala/org/apache/spark/sql/protobuf/ProtobufDeserializer.scala
index 9f2bafb8a180..baee05a29948 100644
--- 
a/connector/protobuf/src/main/scala/org/apache/spark/sql/protobuf/ProtobufDeserializer.scala
+++ 
b/connector/protobuf/src/main/scala/org/apache/spark/sql/protobuf/ProtobufDeserializer.scala
@@ -235,6 +235,11 @@ private[sql] class ProtobufDeserializer(
       case (LONG, LongType) =>
         (updater, ordinal, value) => updater.setLong(ordinal, 
value.asInstanceOf[Long])
 
+      case (LONG, _: TimeType) =>
+        // The int64 field stores nanoseconds-of-day, matching TimeType's 
internal
+        // nanos-since-midnight representation, so copy the value directly.
+        (updater, ordinal, value) => updater.setLong(ordinal, 
value.asInstanceOf[Long])
+
       case (LONG, DecimalType.LongDecimal) =>
         (updater, ordinal, value) =>
           updater.setDecimal(
diff --git 
a/connector/protobuf/src/main/scala/org/apache/spark/sql/protobuf/ProtobufSerializer.scala
 
b/connector/protobuf/src/main/scala/org/apache/spark/sql/protobuf/ProtobufSerializer.scala
index cd638f3ac12a..4ad954ed3c09 100644
--- 
a/connector/protobuf/src/main/scala/org/apache/spark/sql/protobuf/ProtobufSerializer.scala
+++ 
b/connector/protobuf/src/main/scala/org/apache/spark/sql/protobuf/ProtobufSerializer.scala
@@ -149,6 +149,11 @@ private[sql] class ProtobufSerializer(
       case (DateType, INT) =>
         (getter, ordinal) => getter.getInt(ordinal)
 
+      case (_: TimeType, LONG) =>
+        // TimeType is stored internally as nanoseconds-since-midnight; write 
it
+        // directly into the int64 field as nanos-of-day.
+        (getter, ordinal) => getter.getLong(ordinal)
+
       case (TimestampType, MESSAGE) =>
         (getter, ordinal) =>
           val millis = DateTimeUtils.microsToMillis(getter.getLong(ordinal))
diff --git 
a/connector/protobuf/src/test/resources/protobuf/functions_suite.proto 
b/connector/protobuf/src/test/resources/protobuf/functions_suite.proto
index 4cae7f9abf28..8a873682ec3d 100644
--- a/connector/protobuf/src/test/resources/protobuf/functions_suite.proto
+++ b/connector/protobuf/src/test/resources/protobuf/functions_suite.proto
@@ -175,6 +175,11 @@ message durationMsg {
   Duration duration = 2;
 }
 
+message timeMsg {
+  string key = 1;
+  int64 time_val = 2;
+}
+
 message OneOfEvent {
   string key = 1;
   oneof payload {
diff --git 
a/connector/protobuf/src/test/scala/org/apache/spark/sql/protobuf/ProtobufFunctionsSuite.scala
 
b/connector/protobuf/src/test/scala/org/apache/spark/sql/protobuf/ProtobufFunctionsSuite.scala
index 3f9b77d63d67..5c875989e714 100644
--- 
a/connector/protobuf/src/test/scala/org/apache/spark/sql/protobuf/ProtobufFunctionsSuite.scala
+++ 
b/connector/protobuf/src/test/scala/org/apache/spark/sql/protobuf/ProtobufFunctionsSuite.scala
@@ -17,7 +17,7 @@
 package org.apache.spark.sql.protobuf
 
 import java.sql.Timestamp
-import java.time.Duration
+import java.time.{Duration, LocalTime}
 
 import scala.jdk.CollectionConverters._
 
@@ -25,6 +25,7 @@ import com.google.protobuf.{Any => AnyProto, BoolValue, 
ByteString, BytesValue,
 import org.json4s.jackson.JsonMethods
 
 import org.apache.spark.sql.{AnalysisException, Column, DataFrame, Row}
+import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.functions.{array, lit, map, struct, typedLit}
 import org.apache.spark.sql.protobuf.protos.Proto2Messages.Proto2AllTypes
 import org.apache.spark.sql.protobuf.protos.SimpleMessageProtos._
@@ -34,6 +35,7 @@ import org.apache.spark.sql.protobuf.utils.ProtobufUtils
 import org.apache.spark.sql.test.SharedSparkSession
 import org.apache.spark.sql.types._
 import org.apache.spark.sql.util.{ProtobufUtils => CommonProtobufUtils}
+import org.apache.spark.unsafe.types.UTF8String
 
 class ProtobufFunctionsSuite extends SharedSparkSession with ProtobufTestBase
   with Serializable {
@@ -722,6 +724,67 @@ class ProtobufFunctionsSuite extends SharedSparkSession 
with ProtobufTestBase
     }
   }
 
+  test("SPARK-57573: Handle TimeType between to_protobuf and from_protobuf") {
+    val schema = StructType(
+      StructField("timeMsg",
+        StructType(
+          StructField("key", StringType, nullable = true) ::
+            StructField("time_val", TimeType(), nullable = true) :: Nil
+        ),
+        nullable = true
+      ) :: Nil
+    )
+
+    val localTime = LocalTime.of(12, 34, 56, 123456000)
+    val inputDf = spark.createDataFrame(
+      spark.sparkContext.parallelize(Seq(
+        Row(Row("key1", localTime))
+      )),
+      schema
+    )
+
+    checkWithFileAndClassName("timeMsg") {
+      case (name, descFilePathOpt) =>
+        val toProtoDf = inputDf
+          .select(to_protobuf_wrapper($"timeMsg", name,
+            descFilePathOpt) as Symbol("to_proto"))
+
+        val fromProtoDf = toProtoDf
+          .select(from_protobuf_wrapper($"to_proto", name,
+            descFilePathOpt) as Symbol("timeMsg"))
+
+        // The int64 field carries no logical-type marker, so from_protobuf 
infers it as
+        // LongType holding nanoseconds-of-day rather than TimeType.
+        
assert(fromProtoDf.schema("timeMsg").dataType.asInstanceOf[StructType]("time_val")
+          .dataType === LongType)
+        assert(fromProtoDf.select("timeMsg.key").first().get(0) === "key1")
+        assert(fromProtoDf.select("timeMsg.time_val").first().get(0) === 
localTime.toNanoOfDay)
+    }
+  }
+
+  test("SPARK-57573: TimeType roundtrip through ProtobufSerializer and 
ProtobufDeserializer") {
+    val descriptor = ProtobufUtils.buildDescriptor("timeMsg", 
Some(testFileDesc)).descriptor
+    Seq(
+      (TimeType(TimeType.MIN_PRECISION), LocalTime.of(0, 0, 0)),
+      (TimeType(TimeType.MICROS_PRECISION), LocalTime.of(23, 59, 59, 
999999000)),
+      (TimeType(TimeType.MICROS_PRECISION), LocalTime.of(12, 34, 56, 
123456000))
+    ).foreach { case (timeType, localTime) =>
+      val catalyst = StructType(
+        StructField("key", StringType) ::
+          StructField("time_val", timeType) :: Nil)
+      val serializer = new ProtobufSerializer(catalyst, descriptor, nullable = 
false)
+      val deserializer = new ProtobufDeserializer(descriptor, catalyst)
+      val nanos = localTime.toNanoOfDay
+      val input = InternalRow(UTF8String.fromString("key1"), nanos)
+
+      val message = serializer.serialize(input).asInstanceOf[DynamicMessage]
+      val result = deserializer.deserialize(message).get
+
+      assert(result.getUTF8String(0).toString === "key1")
+      assert(result.getLong(1) === nanos)
+    }
+  }
+
   test("raise protobuf descriptor error") {
     val df = Seq(ByteString.empty().toByteArray).toDF("value")
     val descWithoutImports = descriptorSetWithoutImports(testFileDesc, 
"BasicMessage")
diff --git a/docs/sql-data-sources-protobuf.md 
b/docs/sql-data-sources-protobuf.md
index 4dd6579f92cd..b9deaee1e06d 100644
--- a/docs/sql-data-sources-protobuf.md
+++ b/docs/sql-data-sources-protobuf.md
@@ -397,8 +397,14 @@ Spark supports the writing of all Spark SQL types into 
Protobuf. For most types,
     <td>MapType</td>
     <td>map</td>
   </tr>
+  <tr>
+    <td>TimeType</td>
+    <td>int64 (nanoseconds-of-day)</td>
+  </tr>
 </table>
 
+`TimeType` is written as a plain Protobuf `int64` holding the number of 
nanoseconds since midnight. Because a bare `int64` carries no logical-type 
marker, `from_protobuf` reads such a field back as `LongType` (the 
nanoseconds-of-day value), which can be cast back to a TIME value with 
`cast(... AS TIME)` if needed.
+
 ## Handling circular references protobuf fields
 
 One common issue that can arise when working with Protobuf data is the 
presence of circular references. In Protobuf, a circular reference occurs when 
a field refers back to itself or to another field that refers back to the 
original field. This can cause issues when parsing the data, as it can result 
in infinite loops or other unexpected behavior.


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

Reply via email to