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

MaxGekk pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 03c44cc47684 [SPARK-57386][SQL] Render nanosecond timestamp types in 
HiveResult through the Types Framework
03c44cc47684 is described below

commit 03c44cc4768404a565606cd368bbd2c92561e81e
Author: Maxim Gekk <[email protected]>
AuthorDate: Tue Jun 16 10:27:05 2026 +0200

    [SPARK-57386][SQL] Render nanosecond timestamp types in HiveResult through 
the Types Framework
    
    ### What changes were proposed in this pull request?
    Unify nanosecond timestamp (`TIMESTAMP_NTZ(p)` / `TIMESTAMP_LTZ(p)`) 
rendering in `HiveResult` onto the Types Framework, removing the inline 
duplicate renderer.
    
    - `TimestampNanosTypeApiOps`: remove the `formatExternal(value, nested) = 
None` override so the Hive path shares each subclass's single-arg 
`formatExternal` renderer (the same one Row JSON uses). `nested` does not 
affect timestamp formatting.
    - `HiveResult.toHiveStringDefault`: remove the inline 
`TimestampLTZNanosType` / `TimestampNTZNanosType` cases. The legacy path keeps 
no nanos handling, so a nanos value that somehow reaches it (only possible with 
the framework off, which the gating forbids) is unsupported rather than 
silently rendered.
    - `TypeApiOps`: update the two-arg `formatExternal` scaladoc to reflect 
that Hive now shares the single-arg renderer.
    
    ### Why are the changes needed?
    The nanosecond timestamp types are a Types Framework feature, implemented 
solely through it. External-value rendering for the framework is centralized in 
`TypeApiOps.formatExternal`, which already backs Row JSON (`Row.json` / 
`Row.prettyJson`).
    
    The nanos ops previously overrode the two-arg `formatExternal(value, 
nested)` to return `None`, so `HiveResult` rendered nanos through inline 
pattern-matching in `toHiveStringDefault`. That duplicated the formatter logic 
and was documented in code as a temporary split "until nanos external rendering 
is unified across the zone-less (Row JSON) and zone-aware (Hive) paths".
    
    The types are gated by `timestampNanosTypesEnabled = 
timestampNanosTypes.enabled && types.framework.enabled`, so a nanos column 
cannot exist while the framework is off; the inline cases are therefore dead 
code in that mode and redundant when the framework is on.
    
    ### Does this PR introduce _any_ user-facing change?
    No. Nanos Hive output is identical: zone-aware LTZ, zone-independent NTZ, 
precision flooring, and trailing-zero trimming are all unchanged. Both the old 
inline path and the framework path ultimately call the same 
`TimestampFormatter` methods; the pre-flooring in the inline path is a no-op 
because values reaching Hive come from `executeCollectPublic()`, whose internal 
value is already stored floored to the column precision.
    
    ### How was this patch tested?
    Existing `HiveResultSuite` SPARK-57257 tests cover precision 7/8/9, 
pre-1970 epochs, nested arrays/maps/structs, NULLs, and session-zone vs 
zone-independent rendering, and now exercise the framework path. Also ran 
`TimestampNanosTypeOpsSuite` and `RowJsonSuite`. All pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: Cursor (Claude Opus 4.8)
    
    Closes #56513 from MaxGekk/nanos-update-HiveResult.
    
    Authored-by: Maxim Gekk <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
    (cherry picked from commit f49ca1497fe8d1a50e58f2f11457640369aa4568)
    Signed-off-by: Max Gekk <[email protected]>
---
 .../spark/sql/types/ops/TimestampNanosTypeApiOps.scala | 18 +++++++-----------
 .../org/apache/spark/sql/types/ops/TypeApiOps.scala    |  8 ++++----
 .../org/apache/spark/sql/execution/HiveResult.scala    | 10 ----------
 3 files changed, 11 insertions(+), 25 deletions(-)

diff --git 
a/sql/api/src/main/scala/org/apache/spark/sql/types/ops/TimestampNanosTypeApiOps.scala
 
b/sql/api/src/main/scala/org/apache/spark/sql/types/ops/TimestampNanosTypeApiOps.scala
index d9b3a431f1ec..32aac4dcfd9c 100644
--- 
a/sql/api/src/main/scala/org/apache/spark/sql/types/ops/TimestampNanosTypeApiOps.scala
+++ 
b/sql/api/src/main/scala/org/apache/spark/sql/types/ops/TimestampNanosTypeApiOps.scala
@@ -42,6 +42,13 @@ import org.apache.spark.unsafe.types.TimestampNanosVal
  * resolved session zone, while zone-less callers (Row JSON via 
formatExternal) accept the
  * default, the session-local time zone config.
  *
+ * External-value rendering (SPARK-57386): both consumers go through the 
single-arg
+ * [[formatExternal]] each subclass overrides - Row JSON (Row.json / 
Row.prettyJson) calls it
+ * directly, and the Hive result path (HiveResult.toHiveString) calls the 
two-arg overload whose
+ * default delegates to it (`nested` does not affect timestamp formatting). So 
both render the
+ * external value at the column precision rather than truncating to 
microseconds via the legacy
+ * path.
+ *
  * Dataset encoders are wired here to the precision-aware leaves added by 
SPARK-57033
  * (LocalDateTimeNanosEncoder / InstantNanosEncoder), so that turning on the 
Types Framework
  * matches the legacy RowEncoder.encoderForDataTypeDefault behavior rather 
than regressing it.
@@ -58,17 +65,6 @@ abstract class TimestampNanosTypeApiOps extends TypeApiOps 
with DataTypeErrorsBa
 
   // ==================== String Formatting ====================
 
-  // Row JSON (Row.json / Row.prettyJson) holds the external Row value 
(java.time.Instant for LTZ,
-  // java.time.LocalDateTime for NTZ); each subclass overrides the single-arg 
formatExternal to
-  // render it through the same formatter as its zone-aware cast-to-string, so 
Row JSON shows the
-  // nanosecond value rather than silently truncating to microseconds via the 
legacy path.
-
-  // The Hive result path (HiveResult.toHiveString) renders nanosecond 
timestamps through its own
-  // zone-aware default formatter, so return None here to fall through to it 
rather than to the
-  // subclass single-arg rendering. This is a temporary split until nanos 
external rendering is
-  // unified across the zone-less (Row JSON) and zone-aware (Hive) paths.
-  override def formatExternal(value: Any, nested: Boolean): Option[String] = 
None
-
   override def toSQLValue(v: Any): String = s"$sqlTypeName '${format(v)}'"
 
   // ==================== Row Encoding ====================
diff --git 
a/sql/api/src/main/scala/org/apache/spark/sql/types/ops/TypeApiOps.scala 
b/sql/api/src/main/scala/org/apache/spark/sql/types/ops/TypeApiOps.scala
index ab46e34c91e3..c76551aacd84 100644
--- a/sql/api/src/main/scala/org/apache/spark/sql/types/ops/TypeApiOps.scala
+++ b/sql/api/src/main/scala/org/apache/spark/sql/types/ops/TypeApiOps.scala
@@ -145,10 +145,10 @@ trait TypeApiOps extends Serializable {
    * Renders an external value for Hive-style output 
(HiveResult.toHiveString). `nested` indicates
    * whether the value appears inside an array/map/struct, which may 
format/quote it differently.
    * Semantics mirror the single-arg overload: Some(s) is used directly, None 
falls back to
-   * HiveResult's zone-aware legacy rendering. The default delegates to the 
single-arg overload;
-   * override it separately when the two consumers need different behavior 
(e.g. the nanosecond
-   * timestamp types render the external value on the zone-less Row JSON path 
but return None here
-   * so the zone-aware Hive path renders them through its own formatter).
+   * HiveResult's zone-aware legacy rendering. The default delegates to the 
single-arg overload,
+   * so Hive shares the same renderer as Row JSON (e.g. the nanosecond 
timestamp types render the
+   * external Instant/LocalDateTime at the column precision on both paths). 
Override it separately
+   * only when the two consumers need different behavior.
    */
   def formatExternal(value: Any, nested: Boolean): Option[String] = 
formatExternal(value)
 
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/HiveResult.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/HiveResult.scala
index 25170da800ae..671451087435 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/HiveResult.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/HiveResult.scala
@@ -131,16 +131,6 @@ object HiveResult extends SQLConfHelper {
     case (t: Timestamp, TimestampType) => formatters.timestamp.format(t)
     case (i: Instant, TimestampType) => formatters.timestamp.format(i)
     case (l: LocalDateTime, TimestampNTZType) => formatters.timestamp.format(l)
-    // Nanosecond-precision timestamps. The external values are `Instant` 
(LTZ) and
-    // `LocalDateTime` (NTZ); convert to the physical `TimestampNanosVal` at 
the column precision
-    // and render via the same formatter methods as the cast-to-string path 
(SPARK-57256), so the
-    // output stays consistent. LTZ uses the session zone; NTZ is 
zone-independent.
-    case (i: Instant, t: TimestampLTZNanosType) =>
-      formatters.timestamp.formatNanos(
-        DateTimeUtils.instantToTimestampNanos(i, t.precision), t.precision)
-    case (l: LocalDateTime, t: TimestampNTZNanosType) =>
-      formatters.timestamp.formatWithoutTimeZoneNanos(
-        DateTimeUtils.localDateTimeToTimestampNanos(l, t.precision), 
t.precision)
     case (bin: Array[Byte], BinaryType) => binaryFormatter(bin)
     case (decimal: java.math.BigDecimal, DecimalType()) => 
decimal.toPlainString
     case (n, _: NumericType) => n.toString


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

Reply via email to