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

pan3793 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 bf794730ee4b [SPARK-57274][CONNECT] Support fetch/type accessors and 
getMoreResults for SparkConnectStatement
bf794730ee4b is described below

commit bf794730ee4b576f47e18d7f1ffa9589be41fb3a
Author: Jiwon Park <[email protected]>
AuthorDate: Tue Jun 9 17:24:49 2026 +0800

    [SPARK-57274][CONNECT] Support fetch/type accessors and getMoreResults for 
SparkConnectStatement
    
    ### What changes were proposed in this pull request?
    
    Implement the `SparkConnectStatement` accessors that previously threw 
`SQLFeatureNotSupportedException` 
(`setFetchSize`/`getFetchSize`,`setFetchDirection`/`getFetchDirection`, 
`getResultSetType`, `setQueryTimeout`/`getQueryTimeout`), fix `getMoreResults` 
(which threw, breaking JDBC result-drain loops), and implement the 
`createStatement` type/concurrency overloads on `SparkConnectConnection`. 
Follow-up to SPARK-54108 / SPARK-54014.
    
    Fetch size / query timeout are stored as hints (Spark Connect is 
forward-only and server-paginated); `createStatement` accepts forward-only / 
scroll-insensitive and rejects updatable / scroll-sensitive, mirroring the 
Spark Thrift Server's Hive JDBC policy. See SPARK-57274 for details.
    
    ### Why are the changes needed?
    
    JDBC client tools (e.g. DataGrip) call these methods around every query, so 
the throws abort the query path. `getMoreResults` throwing in particular makes 
the standard drain loop `while (getMoreResults() || getUpdateCount() != -1)` 
error out or spin forever; DataGrip hangs on a result-less command such as `USE 
<db>`.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, in the unreleased master only: methods that previously threw 
`SQLFeatureNotSupportedException` are now implemented. No existing behavior is 
removed.
    
    ### How was this patch tested?
    
    New cases in `SparkConnectStatementSuite`: accessor defaults/validation, 
drain-loop termination, and the typed `createStatement` overloads.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #56341 from j1wonpark/SPARK-57274.
    
    Authored-by: Jiwon Park <[email protected]>
    Signed-off-by: Cheng Pan <[email protected]>
---
 .../client/jdbc/SparkConnectStatement.scala        | 64 +++++++++++++++++-----
 .../client/jdbc/SparkConnectStatementSuite.scala   | 57 +++++++++++++++++++
 2 files changed, 106 insertions(+), 15 deletions(-)

diff --git 
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStatement.scala
 
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStatement.scala
index d1947ae93a40..245832087268 100644
--- 
a/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStatement.scala
+++ 
b/sql/connect/client/jdbc/src/main/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStatement.scala
@@ -20,6 +20,7 @@ package org.apache.spark.sql.connect.client.jdbc
 import java.sql.{Array => _, _}
 
 import org.apache.spark.sql.connect.client.SparkResult
+import org.apache.spark.sql.connect.client.jdbc.util.JdbcErrorUtils
 
 class SparkConnectStatement(conn: SparkConnectConnection) extends Statement {
 
@@ -28,6 +29,8 @@ class SparkConnectStatement(conn: SparkConnectConnection) 
extends Statement {
 
   private var maxRows: Int = 0
 
+  private var resultsExhausted: Boolean = false
+
   @volatile private var closed: Boolean = false
 
   override def isClosed: Boolean = closed
@@ -94,6 +97,7 @@ class SparkConnectStatement(conn: SparkConnectConnection) 
extends Statement {
     // reset before executing new query
     operationId = null
     resultSet = null
+    resultsExhausted = false
 
     var df = conn.spark.sql(sql)
     if (maxRows > 0) {
@@ -143,8 +147,13 @@ class SparkConnectStatement(conn: SparkConnectConnection) 
extends Statement {
     0
   }
 
-  override def setQueryTimeout(seconds: Int): Unit =
-    throw new SQLFeatureNotSupportedException
+  // This driver does not apply a query timeout; validate and silently drop 
the value.
+  override def setQueryTimeout(seconds: Int): Unit = {
+    checkOpen()
+    if (seconds < 0) {
+      throw new SQLException("Query timeout must be zero or a positive 
integer.")
+    }
+  }
 
   override def cancel(): Unit = {
     checkOpen()
@@ -164,35 +173,60 @@ class SparkConnectStatement(conn: SparkConnectConnection) 
extends Statement {
   override def getUpdateCount: Int = {
     checkOpen()
 
-    if (resultSet != null) {
+    if (resultsExhausted || resultSet != null) {
       -1
     } else {
       0 // always return 0 because affected rows is not supported yet
     }
   }
 
-  override def getMoreResults: Boolean =
-    throw new SQLFeatureNotSupportedException
+  // a single result per execute(), so there is no next one: close the current
+  // ResultSet and mark exhausted, flipping getUpdateCount() to -1 so drain 
loops end
+  override def getMoreResults: Boolean = {
+    checkOpen()
+    if (resultSet != null) {
+      resultSet.close()
+      resultSet = null
+    }
+    resultsExhausted = true
+    false
+  }
 
-  override def setFetchDirection(direction: Int): Unit =
-    throw new SQLFeatureNotSupportedException
+  override def setFetchDirection(direction: Int): Unit = {
+    checkOpen()
+    if (direction != ResultSet.FETCH_FORWARD) {
+      throw new SQLException(
+        s"Fetch direction ${JdbcErrorUtils.stringifyFetchDirection(direction)} 
is not supported.")
+    }
+  }
 
-  override def getFetchDirection: Int =
-    throw new SQLFeatureNotSupportedException
+  override def getFetchDirection: Int = {
+    checkOpen()
+    ResultSet.FETCH_FORWARD
+  }
 
-  override def setFetchSize(rows: Int): Unit =
-    throw new SQLFeatureNotSupportedException
+  // This driver does not apply a fetch size hint; validate and silently drop 
the value.
+  override def setFetchSize(rows: Int): Unit = {
+    checkOpen()
+    if (rows < 0) {
+      throw new SQLException("Fetch size must be zero or a positive integer.")
+    }
+  }
 
-  override def getFetchSize: Int =
-    throw new SQLFeatureNotSupportedException
+  override def getFetchSize: Int = {
+    checkOpen()
+    0
+  }
 
   override def getResultSetConcurrency: Int = {
     checkOpen()
     ResultSet.CONCUR_READ_ONLY
   }
 
-  override def getResultSetType: Int =
-    throw new SQLFeatureNotSupportedException
+  override def getResultSetType: Int = {
+    checkOpen()
+    ResultSet.TYPE_FORWARD_ONLY
+  }
 
   override def addBatch(sql: String): Unit =
     throw new SQLFeatureNotSupportedException
diff --git 
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStatementSuite.scala
 
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStatementSuite.scala
index fa9df3f1247f..6ace1fa9cf78 100644
--- 
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStatementSuite.scala
+++ 
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectStatementSuite.scala
@@ -116,4 +116,61 @@ class SparkConnectStatementSuite extends ConnectFunSuite 
with RemoteSparkSession
       }
     }
   }
+
+  test("fetch size, fetch direction, result set type and query timeout 
accessors") {
+    withStatement { stmt =>
+      // fetch size: validated then silently dropped, always reads back as 0
+      assert(stmt.getFetchSize === 0)
+      stmt.setFetchSize(42)
+      assert(stmt.getFetchSize === 0)
+      val se1 = intercept[SQLException] {
+        stmt.setFetchSize(-1)
+      }
+      assert(se1.getMessage === "Fetch size must be zero or a positive 
integer.")
+
+      // fetch direction: only FETCH_FORWARD is supported
+      stmt.setFetchDirection(ResultSet.FETCH_FORWARD)
+      assert(stmt.getFetchDirection === ResultSet.FETCH_FORWARD)
+      intercept[SQLException] {
+        stmt.setFetchDirection(ResultSet.FETCH_REVERSE)
+      }
+
+      // result set type is forward-only
+      assert(stmt.getResultSetType === ResultSet.TYPE_FORWARD_ONLY)
+
+      // query timeout: validated then silently dropped, always reads back as 0
+      assert(stmt.getQueryTimeout === 0)
+      stmt.setQueryTimeout(30)
+      assert(stmt.getQueryTimeout === 0)
+      val se2 = intercept[SQLException] {
+        stmt.setQueryTimeout(-1)
+      }
+      assert(se2.getMessage === "Query timeout must be zero or a positive 
integer.")
+    }
+  }
+
+  test("getMoreResults terminates JDBC drain loops") {
+    // A typical JDBC result-draining loop. With getMoreResults throwing (or 
not
+    // flipping getUpdateCount to -1) this would spin forever; assert it 
returns.
+    def drain(stmt: Statement): Unit = {
+      while (stmt.getMoreResults || stmt.getUpdateCount != -1) {}
+    }
+
+    withTable("t_drain") {
+      withStatement { stmt =>
+        // result-bearing command
+        assert(stmt.execute("SELECT id FROM range(3)"))
+        assert(stmt.getUpdateCount === -1)
+        drain(stmt)
+        assert(stmt.getResultSet === null)
+        assert(stmt.getUpdateCount === -1)
+
+        // result-less command
+        assert(!stmt.execute("CREATE TABLE t_drain (id INT) USING Parquet"))
+        assert(stmt.getUpdateCount === 0)
+        drain(stmt)
+        assert(stmt.getUpdateCount === -1)
+      }
+    }
+  }
 }


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

Reply via email to