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

wenchen pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new fceed47f9b08 [SPARK-55005][SQL] Fix CONTINUE HANDLER to continue loop 
execution after handling exceptions in loop body
fceed47f9b08 is described below

commit fceed47f9b08a110d6fd58c9d3fbce3e7584eac5
Author: Milan Dankovic <[email protected]>
AuthorDate: Mon Jan 12 09:54:49 2026 +0800

    [SPARK-55005][SQL] Fix CONTINUE HANDLER to continue loop execution after 
handling exceptions in loop body
    
    ### What changes were proposed in this pull request?
    This PR fixes a critical bug in `CONTINUE HANDLER` execution within loops. 
When a `CONTINUE HANDLER` handled an exception that occurred inside a loop 
body, the loop would exit completely instead of continuing to the next 
iteration.
    
    #### Root cause
    The `interruptConditionalStatements` method in 
`SqlScriptingExecution.scala` was designed to skip conditional statements when 
exceptions occurred in their condition evaluation (e.g., WHILE 1/0 > 0). 
However, it didn't distinguish between:
      - Exception in condition → loop should be skipped
      - Exception in body → loop should continue to next iteration
    
    The method unconditionally set `interrupted = true` on all conditional 
statements, causing loops to exit even when the error occurred during body 
execution.
    
    #### The fix
    The fix was to add `isInCondition: Boolean` method to 
`ConditionalStatementExec` trait to track whether a conditional statement is 
currently evaluating its condition or executing its body. It was also needed to 
implement `isInCondition` for all 6 `ConditionalStatementExec`:
    - IfElseStatementExec
    - WhileStatementExec
    - RepeatStatementExec
    - ForStatementExec
    - SearchedCaseStatementExec
    - SimpleCaseStatementExec
    
    ### Why are the changes needed?
    This ensures `CONTINUE HANDLER` only interrupts conditional statements when 
exceptions occur during condition evaluation, allowing loops to continue 
normally when exceptions occur in their bodies.
    
    ### Does this PR introduce _any_ user-facing change?
    No.
    
    ### How was this patch tested?
    Added comprehensive test coverage for `CONTINUE HANDLER` across all 
conditional statement types.  These tests verify that when an exception occurs 
inside a loop body and is handled by a `CONTINUE HANDLER`, the loop continues 
to the next iteration rather than exiting.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Closes #53759 from miland-db/milan-dankovic_data/continue-handler-fix-loops.
    
    Authored-by: Milan Dankovic <[email protected]>
    Signed-off-by: Wenchen Fan <[email protected]>
    (cherry picked from commit 7a40891ac13413960b9c09ee04cdc63ada9195ac)
    Signed-off-by: Wenchen Fan <[email protected]>
---
 .../sql/scripting/SqlScriptingExecution.scala      |   7 +-
 .../sql/scripting/SqlScriptingExecutionNode.scala  |  28 ++
 .../sql/scripting/SqlScriptingExecutionSuite.scala | 357 +++++++++++++++++++++
 3 files changed, 391 insertions(+), 1 deletion(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/scripting/SqlScriptingExecution.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/scripting/SqlScriptingExecution.scala
index 2a849aa2d604..c8f7172e59bd 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/scripting/SqlScriptingExecution.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/scripting/SqlScriptingExecution.scala
@@ -102,7 +102,12 @@ class SqlScriptingExecution(
 
     currExecPlan match {
       case exec: ConditionalStatementExec =>
-        exec.interrupted = true
+        // Only interrupt if the conditional statement is currently evaluating 
its condition.
+        // For loop statements, this means we should skip the loop when an 
exception occurs
+        // during condition evaluation, but NOT when an exception occurs in 
the loop body.
+        if (exec.isInCondition) {
+          exec.interrupted = true
+        }
       case _ =>
     }
   }
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/scripting/SqlScriptingExecutionNode.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/scripting/SqlScriptingExecutionNode.scala
index aa2c2f405021..c47df4b7a89e 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/scripting/SqlScriptingExecutionNode.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/scripting/SqlScriptingExecutionNode.scala
@@ -120,6 +120,22 @@ trait ConditionalStatementExec extends 
NonLeafStatementExec {
    * HANDLER.
    */
   protected[scripting] var interrupted: Boolean = false
+
+  /**
+   * Returns true if the conditional statement is currently evaluating its 
condition,
+   * false if it's executing its body. This is used by CONTINUE HANDLER to 
determine
+   * whether to interrupt the conditional statement when an exception occurs.
+   *
+   * For loop statements (WHILE, REPEAT, FOR), this should return true when 
evaluating
+   * the loop condition and false when executing the loop body. This 
distinction is
+   * critical because:
+   * - Exception in condition: loop should be skipped (interrupted)
+   * - Exception in body: loop should continue to next iteration (not 
interrupted)
+   *
+   * For IF/CASE statements, this should return true when evaluating the 
condition
+   * expression and false when executing any branch body.
+   */
+  protected[scripting] def isInCondition: Boolean
 }
 
 /**
@@ -479,6 +495,8 @@ class IfElseStatementExec(
 
   override def getTreeIterator: Iterator[CompoundStatementExec] = treeIterator
 
+  override protected[scripting] def isInCondition: Boolean = state == 
IfElseState.Condition
+
   override def reset(): Unit = {
     state = IfElseState.Condition
     curr = Some(conditions.head)
@@ -565,6 +583,8 @@ class WhileStatementExec(
 
   override def getTreeIterator: Iterator[CompoundStatementExec] = treeIterator
 
+  override protected[scripting] def isInCondition: Boolean = state == 
WhileState.Condition
+
   override def reset(): Unit = {
     state = WhileState.Condition
     curr = Some(condition)
@@ -654,6 +674,8 @@ class SearchedCaseStatementExec(
 
   override def getTreeIterator: Iterator[CompoundStatementExec] = treeIterator
 
+  override protected[scripting] def isInCondition: Boolean = state == 
CaseState.Condition
+
   override def reset(): Unit = {
     state = CaseState.Condition
     curr = Some(conditions.head)
@@ -793,6 +815,8 @@ class SimpleCaseStatementExec(
 
   override def getTreeIterator: Iterator[CompoundStatementExec] = treeIterator
 
+  override protected[scripting] def isInCondition: Boolean = state == 
CaseState.Condition
+
   override def reset(): Unit = {
     state = CaseState.Condition
     bodyExec = None
@@ -880,6 +904,8 @@ class RepeatStatementExec(
 
   override def getTreeIterator: Iterator[CompoundStatementExec] = treeIterator
 
+  override protected[scripting] def isInCondition: Boolean = state == 
RepeatState.Condition
+
   override def reset(): Unit = {
     state = RepeatState.Body
     curr = Some(body)
@@ -1215,6 +1241,8 @@ class ForStatementExec(
 
   override def getTreeIterator: Iterator[CompoundStatementExec] = treeIterator
 
+  override protected[scripting] def isInCondition: Boolean = state == 
ForState.VariableAssignment
+
   override def reset(): Unit = {
     state = ForState.VariableAssignment
     isResultCacheValid = false
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/scripting/SqlScriptingExecutionSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/scripting/SqlScriptingExecutionSuite.scala
index d080e1f05014..2ad715f671ed 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/scripting/SqlScriptingExecutionSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/scripting/SqlScriptingExecutionSuite.scala
@@ -1324,6 +1324,363 @@ class SqlScriptingExecutionSuite extends QueryTest with 
SharedSparkSession {
     verifySqlScriptResult(sqlScript, expected = expected)
   }
 
+  test("continue handler - should continue loop after handling error inside 
WHILE body") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE x, y = 1;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET y = -1;
+        |  END;
+        |
+        |  WHILE x < 5 DO
+        |    SET x = x + 1;
+        |    SET y = y / (x - 3);
+        |    SELECT x, y;
+        |  END WHILE;
+        |
+        |  SELECT x, y;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(2, -1)),
+      Seq(Row(3, -1)),
+      Seq(Row(4, -1)),
+      Seq(Row(5, 0)),
+      Seq(Row(5, 0))
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - should continue loop after handling error inside 
REPEAT body") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE x, y = 1;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET y = -1;
+        |  END;
+        |
+        |  REPEAT
+        |    SET x = x + 1;
+        |    SET y = y / (x - 3);
+        |    SELECT x, y;
+        |  UNTIL
+        |    x >= 5
+        |  END REPEAT;
+        |
+        |  SELECT x AS final_x, y AS final_y;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(2, -1)),  // iteration 1: y = 1 / -1 = -1
+      Seq(Row(3, -1)),  // iteration 2: y = -1 / 0 handler sets y = -1
+      Seq(Row(4, -1)),  // iteration 3: y = -1 / 1 = -1
+      Seq(Row(5, 0)),   // iteration 4: y = -1 / 2 = 0
+      Seq(Row(5, 0))    // final select
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - should continue loop after handling error inside 
FOR body") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE y = 1;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET y = -1;
+        |  END;
+        |
+        |  FOR iter AS SELECT 1 AS val UNION ALL SELECT 2 UNION ALL SELECT 3 
UNION ALL SELECT 4
+        |               UNION ALL SELECT 5 DO
+        |    SET y = y / (iter.val - 3);
+        |    SELECT iter.val, y;
+        |  END FOR;
+        |
+        |  SELECT y AS final_y;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(1, 0)),   // iteration 1: y = 1 / -2 = 0
+      Seq(Row(2, 0)),   // iteration 2: y = 0 / -1 = 0
+      Seq(Row(3, -1)),  // iteration 3: y = 0 / 0 handler sets y = -1
+      Seq(Row(4, -1)),  // iteration 4: y = -1 / 1 = -1
+      Seq(Row(5, 0)),   // iteration 5: y = -1 / 2 = 0
+      Seq(Row(0))       // final select
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - should continue after handling error inside IF 
body") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE x = 1;
+        |  DECLARE handled = 0;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET handled = 1;
+        |  END;
+        |
+        |  IF x = 1 THEN
+        |    SELECT 1 / 0;  -- This will throw divide by zero
+        |    SELECT 999;     -- This should execute after handler
+        |  END IF;
+        |
+        |  SELECT handled;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(999)),  // SELECT 999 executed after handler
+      Seq(Row(1))     // handled = 1
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - should continue after handling error inside ELSEIF 
body") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE x = 2;
+        |  DECLARE handled = 0;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET handled = 1;
+        |  END;
+        |
+        |  IF x = 1 THEN
+        |    SELECT 100;
+        |  ELSEIF x = 2 THEN
+        |    SELECT 1 / 0;  -- This will throw divide by zero
+        |    SELECT 888;     -- This should execute after handler
+        |  ELSE
+        |    SELECT 200;
+        |  END IF;
+        |
+        |  SELECT handled;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(888)),  // SELECT 888 executed after handler
+      Seq(Row(1))     // handled = 1
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - should continue after handling error inside ELSE 
body") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE x = 3;
+        |  DECLARE handled = 0;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET handled = 1;
+        |  END;
+        |
+        |  IF x = 1 THEN
+        |    SELECT 100;
+        |  ELSEIF x = 2 THEN
+        |    SELECT 200;
+        |  ELSE
+        |    SELECT 1 / 0;  -- This will throw divide by zero
+        |    SELECT 777;     -- This should execute after handler
+        |  END IF;
+        |
+        |  SELECT handled;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(777)),  // SELECT 777 executed after handler
+      Seq(Row(1))     // handled = 1
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - should continue after handling error inside 
SEARCHED CASE body") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE x = 2;
+        |  DECLARE handled = 0;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET handled = 1;
+        |  END;
+        |
+        |  CASE
+        |    WHEN x = 1 THEN SELECT 100;
+        |    WHEN x = 2 THEN
+        |      SELECT 1 / 0;  -- This will throw divide by zero
+        |      SELECT 666;     -- This should execute after handler
+        |    ELSE SELECT 200;
+        |  END CASE;
+        |
+        |  SELECT handled;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(666)),  // SELECT 666 executed after handler
+      Seq(Row(1))     // handled = 1
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - should continue after handling error inside SIMPLE 
CASE body") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE x = 2;
+        |  DECLARE handled = 0;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET handled = 1;
+        |  END;
+        |
+        |  CASE x
+        |    WHEN 1 THEN SELECT 100;
+        |    WHEN 2 THEN
+        |      SELECT 1 / 0;  -- This will throw divide by zero
+        |      SELECT 555;     -- This should execute after handler
+        |    ELSE SELECT 200;
+        |  END CASE;
+        |
+        |  SELECT handled;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(555)),  // SELECT 555 executed after handler
+      Seq(Row(1))     // handled = 1
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - nested WHILE loops with error in inner loop") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE outer = 0;
+        |  DECLARE inner = 0;
+        |  DECLARE handled = 0;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET handled = handled + 1;
+        |  END;
+        |
+        |  WHILE outer < 2 DO
+        |    SET outer = outer + 1;
+        |    SET inner = 0;
+        |    WHILE inner < 3 DO
+        |      SET inner = inner + 1;
+        |      IF outer = 1 AND inner = 2 THEN
+        |        SELECT 1 / 0;  -- Error in inner loop, iteration 2
+        |      END IF;
+        |      SELECT outer, inner, handled;
+        |    END WHILE;
+        |  END WHILE;
+        |
+        |  SELECT outer AS final_outer, handled AS final_handled;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(1, 1, 0)),  // outer=1, inner=1, no error yet
+      Seq(Row(1, 2, 1)),  // outer=1, inner=2, error handled
+      Seq(Row(1, 3, 1)),  // outer=1, inner=3, continuing
+      Seq(Row(2, 1, 1)),  // outer=2, inner=1
+      Seq(Row(2, 2, 1)),  // outer=2, inner=2, no error this time
+      Seq(Row(2, 3, 1)),  // outer=2, inner=3
+      Seq(Row(2, 1))      // final select
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - nested REPEAT loops with error in inner loop") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE outer = 0;
+        |  DECLARE inner = 0;
+        |  DECLARE handled = 0;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET handled = handled + 1;
+        |  END;
+        |
+        |  REPEAT
+        |    SET outer = outer + 1;
+        |    SET inner = 0;
+        |    REPEAT
+        |      SET inner = inner + 1;
+        |      IF outer = 2 AND inner = 1 THEN
+        |        SELECT 1 / 0;  -- Error in inner loop
+        |      END IF;
+        |      SELECT outer, inner, handled;
+        |    UNTIL inner >= 2
+        |    END REPEAT;
+        |  UNTIL outer >= 2
+        |  END REPEAT;
+        |
+        |  SELECT outer AS final_outer, handled AS final_handled;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(1, 1, 0)),  // outer=1, inner=1
+      Seq(Row(1, 2, 0)),  // outer=1, inner=2
+      Seq(Row(2, 1, 1)),  // outer=2, inner=1, error handled
+      Seq(Row(2, 2, 1)),  // outer=2, inner=2
+      Seq(Row(2, 1))      // final select
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
+  test("continue handler - nested FOR loops with error in inner loop") {
+    val sqlScript =
+      """
+        |BEGIN
+        |  DECLARE handled = 0;
+        |
+        |  DECLARE CONTINUE HANDLER FOR DIVIDE_BY_ZERO
+        |  BEGIN
+        |    SET handled = handled + 1;
+        |  END;
+        |
+        |  FOR o AS SELECT 1 AS val UNION ALL SELECT 2 DO
+        |    FOR i AS SELECT 1 AS val UNION ALL SELECT 2 UNION ALL SELECT 3 DO
+        |      IF o.val = 1 AND i.val = 3 THEN
+        |        SELECT 1 / 0;  -- Error in inner loop
+        |      END IF;
+        |      SELECT o.val AS outer, i.val AS inner, handled;
+        |    END FOR;
+        |  END FOR;
+        |
+        |  SELECT handled AS final_handled;
+        |END
+        |""".stripMargin
+    val expected = Seq(
+      Seq(Row(1, 1, 0)),  // o=1, i=1
+      Seq(Row(1, 2, 0)),  // o=1, i=2
+      Seq(Row(1, 3, 1)),  // o=1, i=3, error handled
+      Seq(Row(2, 1, 1)),  // o=2, i=1
+      Seq(Row(2, 2, 1)),  // o=2, i=2
+      Seq(Row(2, 3, 1)),  // o=2, i=3
+      Seq(Row(1))         // final select
+    )
+    verifySqlScriptResult(sqlScript, expected = expected)
+  }
+
   test("exit handler body without BEGIN-END propagates error properly") {
     val sqlScript =
       """


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

Reply via email to