You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by ur...@apache.org on 2026/08/12 18:26:28 UTC
(spark) branch master updated: [SPARK-58739][SQL][TEST] Test UnsafeRow nested struct/map round-trip for nanos timestamps
This is an automated email from the ASF dual-hosted git repository.
uros-b 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 36b15ae916b5 [SPARK-58739][SQL][TEST] Test UnsafeRow nested struct/map round-trip for nanos timestamps
36b15ae916b5 is described below
commit 36b15ae916b5396026ae70bed2c76094afa5c59d
Author: Stevo Mitric <st...@gmail.com>
AuthorDate: Wed Aug 12 18:26:12 2026 +0000
[SPARK-58739][SQL][TEST] Test UnsafeRow nested struct/map round-trip for nanos timestamps
### What changes were proposed in this pull request?
Adds coverage for nanosecond timestamp values inside nested UnsafeRow structures, which was implemented but untested:
- Nested StructType fields (NTZ + LTZ), including null fields inside a non-null struct, and a nanos field alongside a variable-length StringType field.
- MapType values (NTZ nullable with an interior null; LTZ non-nullable), read back through UnsafeRow.getMap().valueArray().
Each case runs through both the codegen and interpreted UnsafeProjection paths via testBothCodegenAndInterpreted, mirroring the existing array round-trip tests.
### Why are the changes needed?
Extend the test coverage.
### Does this PR introduce _any_ user-facing change?
No. Test only change.
### How was this patch tested?
Test-only change.
### Was this patch authored or co-authored using generative AI tooling?
Co-Authored: Claude Opus 4.8
Closes #57961 from stevomitric/stevomitric/nanos-unsaferow-nested-roundtrip.
Authored-by: Stevo Mitric <st...@gmail.com>
Signed-off-by: Uros Bojanic <22...@users.noreply.github.com>
---
.../expressions/TimestampNanosRowSuite.scala | 94 +++++++++++++++++++++-
1 file changed, 92 insertions(+), 2 deletions(-)
diff --git a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/TimestampNanosRowSuite.scala b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/TimestampNanosRowSuite.scala
index d85350504f7f..cc29ae326933 100644
--- a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/TimestampNanosRowSuite.scala
+++ b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/TimestampNanosRowSuite.scala
@@ -20,9 +20,9 @@ package org.apache.spark.sql.catalyst.expressions
import org.apache.spark.SparkFunSuite
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.codegen.GenerateUnsafeProjection
-import org.apache.spark.sql.catalyst.util.GenericArrayData
+import org.apache.spark.sql.catalyst.util.{ArrayBasedMapData, GenericArrayData}
import org.apache.spark.sql.types._
-import org.apache.spark.unsafe.types.TimestampNanosVal
+import org.apache.spark.unsafe.types.{TimestampNanosVal, UTF8String}
import org.apache.spark.util.ArrayImplicits._
class TimestampNanosRowSuite extends SparkFunSuite with ExpressionEvalHelper {
@@ -156,6 +156,96 @@ class TimestampNanosRowSuite extends SparkFunSuite with ExpressionEvalHelper {
assert(arr.getTimestampNTZNanos(1) === ntzValue)
}
+ // Struct fields are stored in the UnsafeRow variable-length region. Exercises
+ // GenerateUnsafeProjection.writeStructToBuffer/writeExpressionsToBuffer (codegen) and the
+ // interpreted equivalent, then reads the nanos fields back through UnsafeRow.getStruct.
+ testBothCodegenAndInterpreted("UnsafeRow nested struct with nanos timestamp fields") {
+ val innerStruct = StructType(Seq(
+ StructField("inner_ntz", TimestampNTZNanosType(9), nullable = true),
+ StructField("inner_ltz", TimestampLTZNanosType(7), nullable = true)))
+ val fieldTypes = Array[DataType](innerStruct)
+ val converter = UnsafeProjection.create(fieldTypes)
+
+ val innerRow = new GenericInternalRow(Array[Any](ntzValue, ltzValue))
+ val input = new GenericInternalRow(Array[Any](innerRow))
+
+ val unsafeRow = converter.apply(input)
+ val nestedStruct = unsafeRow.getStruct(0, 2)
+ assert(nestedStruct.getTimestampNTZNanos(0) === ntzValue)
+ assert(nestedStruct.getTimestampLTZNanos(1) === ltzValue)
+
+ // Null nanos fields inside a non-null struct must be marked null after serialization.
+ val innerRowNulls = new GenericInternalRow(Array[Any](null, null))
+ val inputNulls = new GenericInternalRow(Array[Any](innerRowNulls))
+ val unsafeRowNulls = converter.apply(inputNulls)
+ val nestedStructNulls = unsafeRowNulls.getStruct(0, 2)
+ assert(nestedStructNulls.isNullAt(0))
+ assert(nestedStructNulls.isNullAt(1))
+ }
+
+ // A nanos field alongside a variable-length StringType field checks that the nanos composite
+ // (epochMicros Long + nanosWithinMicro Short) is written at the correct offset when other
+ // variable-length fields share the nested struct.
+ testBothCodegenAndInterpreted("UnsafeRow nested struct with nanos and non-nanos fields") {
+ val innerStruct = StructType(Seq(
+ StructField("ntz", TimestampNTZNanosType(8), nullable = true),
+ StructField("name", StringType, nullable = true)))
+ val fieldTypes = Array[DataType](innerStruct)
+ val converter = UnsafeProjection.create(fieldTypes)
+
+ val innerRow =
+ new GenericInternalRow(Array[Any](ntzValue, UTF8String.fromString("test_string")))
+ val input = new GenericInternalRow(Array[Any](innerRow))
+ val unsafeRow = converter.apply(input)
+
+ val nested = unsafeRow.getStruct(0, 2)
+ assert(nested.getTimestampNTZNanos(0) === ntzValue)
+ assert(nested.getUTF8String(1) === UTF8String.fromString("test_string"))
+ }
+
+ // Map values are serialized as an UnsafeArrayData via GenerateUnsafeProjection.writeMapToBuffer
+ // delegating to writeArrayToBuffer. Reads back through UnsafeRow.getMap().valueArray().
+ testBothCodegenAndInterpreted("UnsafeRow with map of nanos timestamp values") {
+ val mapType = MapType(StringType, TimestampNTZNanosType(9), valueContainsNull = true)
+ val fieldTypes = Array[DataType](mapType)
+ val converter = UnsafeProjection.create(fieldTypes)
+
+ val keys = new GenericArrayData(Array[Any](
+ UTF8String.fromString("a"), UTF8String.fromString("b"), UTF8String.fromString("c")))
+ val values = new GenericArrayData(Array[Any](ntzValue, null, ntzValue))
+ val mapData = new ArrayBasedMapData(keys, values)
+ val input = new GenericInternalRow(Array[Any](mapData))
+
+ val unsafeRow = converter.apply(input)
+ val unsafeMap = unsafeRow.getMap(0)
+ assert(unsafeMap.numElements() == 3)
+ val valueArray = unsafeMap.valueArray()
+ assert(valueArray.getTimestampNTZNanos(0) === ntzValue)
+ assert(valueArray.isNullAt(1))
+ assert(valueArray.getTimestampNTZNanos(2) === ntzValue)
+ }
+
+ // valueContainsNull = false exercises the codegen branch in writeArrayToBuffer that elides
+ // the per-element isNullAt check for map values.
+ testBothCodegenAndInterpreted("UnsafeRow with non-nullable map of nanos values") {
+ val mapType = MapType(StringType, TimestampLTZNanosType(7), valueContainsNull = false)
+ val fieldTypes = Array[DataType](mapType)
+ val converter = UnsafeProjection.create(fieldTypes)
+
+ val keys = new GenericArrayData(Array[Any](
+ UTF8String.fromString("x"), UTF8String.fromString("y")))
+ val values = new GenericArrayData(Array[Any](ltzValue, ltzValue))
+ val mapData = new ArrayBasedMapData(keys, values)
+ val input = new GenericInternalRow(Array[Any](mapData))
+
+ val unsafeRow = converter.apply(input)
+ val unsafeMap = unsafeRow.getMap(0)
+ assert(unsafeMap.numElements() == 2)
+ val valueArray = unsafeMap.valueArray()
+ assert(valueArray.getTimestampLTZNanos(0) === ltzValue)
+ assert(valueArray.getTimestampLTZNanos(1) === ltzValue)
+ }
+
testBothCodegenAndInterpreted("codegen projection reads nanos timestamp column") {
val boundRef = BoundReference(0, TimestampNTZNanosType(9), nullable = false)
val projection = GenerateUnsafeProjection.generate(Seq(boundRef))
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org