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 a6dc86c3df54 [SPARK-57207][SQL][FOLLOWUP] Fix StackOverflowError when 
setting timestampNanosTypes.enabled via SparkConf
a6dc86c3df54 is described below

commit a6dc86c3df54199177939785dffa2fd755a7232f
Author: Stevo Mitric <[email protected]>
AuthorDate: Wed Jun 10 21:03:54 2026 +0200

    [SPARK-57207][SQL][FOLLOWUP] Fix StackOverflowError when setting 
timestampNanosTypes.enabled via SparkConf
    
    ### What changes were proposed in this pull request?
    This PR deletes that validator and moves the requirement into the getter: 
`SQLConf.timestampNanosTypesEnabled` now returns true only when 
`spark.sql.types.framework.enabled` is also true.
    
    ### Why are the changes needed?
    Enabling the flag the normal way crashes Spark:
    
    ```scala
    val spark = SparkSession.builder()
      .master("local[1]")
      .config("spark.sql.timestampNanosTypes.enabled", "true")
      .getOrCreate()       // fine — options are applied lazily
    spark.sql("SELECT 1")  // java.lang.StackOverflowError
    ```
    
    The validator runs while the session conf is being built (during 
`mergeSparkConf`), and `SQLConf.get` asks for that same conf which starts 
building it again, forever.
    
    ### Does this PR introduce _any_ user-facing change?
    No
    
    ### How was this patch tested?
    Tests in this PR.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: Claude Code (Fable 5)
    
    Closes #56431 from 
stevomitric/stevomitric/fix-nanos-conf-checkvalue-recursion.
    
    Authored-by: Stevo Mitric <[email protected]>
    Signed-off-by: Max Gekk <[email protected]>
    (cherry picked from commit 0e8a75f044e27ab2d6960a41a9841d99216207ac)
    Signed-off-by: Max Gekk <[email protected]>
---
 .../org/apache/spark/sql/internal/SQLConf.scala    | 16 ++++++-------
 .../types/ops/TimestampNanosTypeOpsSuite.scala     | 28 +++++++++++-----------
 .../spark/sql/SparkSessionBuilderSuite.scala       | 13 ++++++++++
 3 files changed, 35 insertions(+), 22 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index 8a54fc55ba7a..a45ae4d5c113 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -657,17 +657,11 @@ object SQLConf {
         "types. Enabling this flag does not guarantee full SQL support: casts, 
Parquet read, " +
         "typed literals, and other operations may still fail until their 
respective features " +
         "are implemented. The nanosecond timestamp types are implemented 
solely through the " +
-        "Types Framework, so this flag can only be set to true when " +
+        "Types Framework, so this flag only takes effect when " +
         s"${TYPES_FRAMEWORK_ENABLED.key} is also true.")
       .version("4.3.0")
       .withBindingPolicy(ConfigBindingPolicy.SESSION)
       .booleanConf
-      .checkValue(
-        enabled => !enabled || SQLConf.get.typesFrameworkEnabled,
-        "REQUIREMENT",
-        _ => Map("confRequirement" ->
-          (s"'${TYPES_FRAMEWORK_ENABLED.key}' must be true to enable the 
nanosecond " +
-            "timestamp types.")))
       .createWithDefaultFunction(() => Utils.isTesting)
 
   val EXTENDED_EXPLAIN_PROVIDERS = 
buildConf("spark.sql.extendedExplainProviders")
@@ -7648,7 +7642,13 @@ class SQLConf extends Serializable with Logging with 
SqlApiConf {
 
   def typesFrameworkEnabled: Boolean = getConf(TYPES_FRAMEWORK_ENABLED)
 
-  def timestampNanosTypesEnabled: Boolean = 
getConf(TIMESTAMP_NANOS_TYPES_ENABLED)
+  // The nanos types are implemented solely through the Types Framework, so 
the flag has no
+  // effect without it. The requirement is enforced here instead of in a 
checkValue of
+  // TIMESTAMP_NANOS_TYPES_ENABLED: a validator must not call SQLConf.get, 
because validators
+  // run inside mergeSparkConf while the session's SQLConf is still being 
constructed, and the
+  // SQLConf.get lookup re-enters that construction (infinite recursion).
+  def timestampNanosTypesEnabled: Boolean =
+    getConf(TIMESTAMP_NANOS_TYPES_ENABLED) && getConf(TYPES_FRAMEWORK_ENABLED)
 
   def dataSourceV2JoinPushdown: Boolean = getConf(DATA_SOURCE_V2_JOIN_PUSHDOWN)
 
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/types/ops/TimestampNanosTypeOpsSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/types/ops/TimestampNanosTypeOpsSuite.scala
index 6154dff97be5..2d69729540cd 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/types/ops/TimestampNanosTypeOpsSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/types/ops/TimestampNanosTypeOpsSuite.scala
@@ -19,7 +19,7 @@ package org.apache.spark.sql.catalyst.types.ops
 
 import java.time.{Instant, LocalDateTime}
 
-import org.apache.spark.{SparkFunSuite, SparkIllegalArgumentException, 
SparkUnsupportedOperationException}
+import org.apache.spark.{SparkFunSuite, SparkUnsupportedOperationException}
 import org.apache.spark.sql.catalyst.{CatalystTypeConverters, InternalRow}
 import org.apache.spark.sql.catalyst.encoders.{AgnosticEncoder, 
ExpressionEncoder}
 import 
org.apache.spark.sql.catalyst.encoders.AgnosticEncoders.{InstantNanosEncoder, 
LocalDateTimeNanosEncoder, OptionEncoder}
@@ -204,19 +204,19 @@ class TimestampNanosTypeOpsSuite extends SparkFunSuite 
with SQLHelper {
     }
   }
 
-  test("enabling the nanos types requires the Types Framework to be enabled") {
-    withSQLConf(SQLConf.TYPES_FRAMEWORK_ENABLED.key -> "false") {
-      checkError(
-        exception = intercept[SparkIllegalArgumentException] {
-          SQLConf.get.setConfString(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key, 
"true")
-        },
-        condition = "INVALID_CONF_VALUE.REQUIREMENT",
-        parameters = Map(
-          "confName" -> SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key,
-          "confValue" -> "true",
-          "confRequirement" ->
-            (s"'${SQLConf.TYPES_FRAMEWORK_ENABLED.key}' must be true to enable 
the nanosecond " +
-              "timestamp types.")))
+  test("the nanos types flag only takes effect when the Types Framework is 
enabled") {
+    // Setting the flag must always succeed (a checkValue consulting 
SQLConf.get would recurse
+    // into the session conf's own construction); the framework requirement is 
enforced in the
+    // effective getter instead.
+    withSQLConf(
+        SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true",
+        SQLConf.TYPES_FRAMEWORK_ENABLED.key -> "false") {
+      assert(!SQLConf.get.timestampNanosTypesEnabled)
+    }
+    withSQLConf(
+        SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key -> "true",
+        SQLConf.TYPES_FRAMEWORK_ENABLED.key -> "true") {
+      assert(SQLConf.get.timestampNanosTypesEnabled)
     }
   }
 }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/SparkSessionBuilderSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/SparkSessionBuilderSuite.scala
index bbc396c879de..a2ae074d981d 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/SparkSessionBuilderSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/SparkSessionBuilderSuite.scala
@@ -85,6 +85,19 @@ class SparkSessionBuilderSuite extends SparkFunSuite with 
Eventually {
     assert(session.conf.get("some-config") == "v2")
   }
 
+  test("enabling timestampNanosTypes via SparkConf must not break the 
session") {
+    // Regression test: the conf's checkValue used to call SQLConf.get, which 
re-entered the
+    // session's sqlConf lazy val while mergeSparkConf was applying this very 
conf, so the
+    // first use of the session died with StackOverflowError.
+    val session = SparkSession.builder()
+      .master("local")
+      .config(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key, value = true)
+      .config(SQLConf.TYPES_FRAMEWORK_ENABLED.key, value = true)
+      .getOrCreate()
+    assert(session.conf.get(SQLConf.TIMESTAMP_NANOS_TYPES_ENABLED.key) == 
"true")
+    assert(session.sql("SELECT 1").collect() === Array(Row(1)))
+  }
+
   test("use global default session") {
     val session = SparkSession.builder().master("local").getOrCreate()
     assert(SparkSession.builder().getOrCreate() == session)


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

Reply via email to