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]