Skip to content

Commit 9676b1c

Browse files
davidm-dbcloud-fan
authored andcommitted
[SPARK-48348][SPARK-48376][SQL] Introduce LEAVE and ITERATE statements
### What changes were proposed in this pull request? This PR proposes introduction of `LEAVE` and `ITERATE` statement types to SQL Scripting language: - `LEAVE` statement can be used in loops, as well as in `BEGIN ... END` compound blocks. - `ITERATE` statement can be used only in loops. This PR introduces: - Logical operators for both statement types. - Execution nodes for both statement types. - Interpreter changes required to build execution plans that support new statement types. - New error if statements are not used properly. - Minor changes required to support new keywords. ### Why are the changes needed? Adding support for new statement types to SQL Scripting language. ### Does this PR introduce _any_ user-facing change? This PR introduces new statement types that will be available to users. However, script execution logic hasn't been done yet, so the new changes are not accessible by users yet. ### How was this patch tested? Tests are introduced to all test suites related to SQL scripting. ### Was this patch authored or co-authored using generative AI tooling? No. Closes #47973 from davidm-db/sql_scripting_leave_iterate. Authored-by: David Milicevic <[email protected]> Signed-off-by: Wenchen Fan <[email protected]>
1 parent 182353d commit 9676b1c

File tree

15 files changed

+664
-15
lines changed

15 files changed

+664
-15
lines changed

common/utils/src/main/resources/error/error-conditions.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,6 +2495,24 @@
24952495
],
24962496
"sqlState" : "F0000"
24972497
},
2498+
"INVALID_LABEL_USAGE" : {
2499+
"message" : [
2500+
"The usage of the label <labelName> is invalid."
2501+
],
2502+
"subClass" : {
2503+
"DOES_NOT_EXIST" : {
2504+
"message" : [
2505+
"Label was used in the <statementType> statement, but the label does not belong to any surrounding block."
2506+
]
2507+
},
2508+
"ITERATE_IN_COMPOUND" : {
2509+
"message" : [
2510+
"ITERATE statement cannot be used with a label that belongs to a compound (BEGIN...END) body."
2511+
]
2512+
}
2513+
},
2514+
"sqlState" : "42K0L"
2515+
},
24982516
"INVALID_LAMBDA_FUNCTION_CALL" : {
24992517
"message" : [
25002518
"Invalid lambda function call."

docs/sql-ref-ansi-compliance.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,13 +556,15 @@ Below is a list of all the keywords in Spark SQL.
556556
|INVOKER|non-reserved|non-reserved|non-reserved|
557557
|IS|reserved|non-reserved|reserved|
558558
|ITEMS|non-reserved|non-reserved|non-reserved|
559+
|ITERATE|non-reserved|non-reserved|non-reserved|
559560
|JOIN|reserved|strict-non-reserved|reserved|
560561
|KEYS|non-reserved|non-reserved|non-reserved|
561562
|LANGUAGE|non-reserved|non-reserved|reserved|
562563
|LAST|non-reserved|non-reserved|non-reserved|
563564
|LATERAL|reserved|strict-non-reserved|reserved|
564565
|LAZY|non-reserved|non-reserved|non-reserved|
565566
|LEADING|reserved|non-reserved|reserved|
567+
|LEAVE|non-reserved|non-reserved|non-reserved|
566568
|LEFT|reserved|strict-non-reserved|reserved|
567569
|LIKE|non-reserved|non-reserved|reserved|
568570
|ILIKE|non-reserved|non-reserved|non-reserved|

sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,13 +276,15 @@ INTO: 'INTO';
276276
INVOKER: 'INVOKER';
277277
IS: 'IS';
278278
ITEMS: 'ITEMS';
279+
ITERATE: 'ITERATE';
279280
JOIN: 'JOIN';
280281
KEYS: 'KEYS';
281282
LANGUAGE: 'LANGUAGE';
282283
LAST: 'LAST';
283284
LATERAL: 'LATERAL';
284285
LAZY: 'LAZY';
285286
LEADING: 'LEADING';
287+
LEAVE: 'LEAVE';
286288
LEFT: 'LEFT';
287289
LIKE: 'LIKE';
288290
ILIKE: 'ILIKE';

sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ compoundStatement
6565
| beginEndCompoundBlock
6666
| ifElseStatement
6767
| whileStatement
68+
| leaveStatement
69+
| iterateStatement
6870
;
6971

7072
setStatementWithOptionalVarKeyword
@@ -83,6 +85,14 @@ ifElseStatement
8385
(ELSE elseBody=compoundBody)? END IF
8486
;
8587

88+
leaveStatement
89+
: LEAVE multipartIdentifier
90+
;
91+
92+
iterateStatement
93+
: ITERATE multipartIdentifier
94+
;
95+
8696
singleStatement
8797
: (statement|setResetStatement) SEMICOLON* EOF
8898
;
@@ -1578,10 +1588,12 @@ ansiNonReserved
15781588
| INTERVAL
15791589
| INVOKER
15801590
| ITEMS
1591+
| ITERATE
15811592
| KEYS
15821593
| LANGUAGE
15831594
| LAST
15841595
| LAZY
1596+
| LEAVE
15851597
| LIKE
15861598
| ILIKE
15871599
| LIMIT
@@ -1927,11 +1939,13 @@ nonReserved
19271939
| INVOKER
19281940
| IS
19291941
| ITEMS
1942+
| ITERATE
19301943
| KEYS
19311944
| LANGUAGE
19321945
| LAST
19331946
| LAZY
19341947
| LEADING
1948+
| LEAVE
19351949
| LIKE
19361950
| LONG
19371951
| ILIKE

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import scala.collection.mutable.{ArrayBuffer, ListBuffer, Set}
2424
import scala.jdk.CollectionConverters._
2525
import scala.util.{Left, Right}
2626

27-
import org.antlr.v4.runtime.{ParserRuleContext, Token}
27+
import org.antlr.v4.runtime.{ParserRuleContext, RuleContext, Token}
2828
import org.antlr.v4.runtime.misc.Interval
2929
import org.antlr.v4.runtime.tree.{ParseTree, RuleNode, TerminalNode}
3030

@@ -261,6 +261,56 @@ class AstBuilder extends DataTypeAstBuilder
261261
WhileStatement(condition, body, Some(labelText))
262262
}
263263

264+
private def leaveOrIterateContextHasLabel(
265+
ctx: RuleContext, label: String, isIterate: Boolean): Boolean = {
266+
ctx match {
267+
case c: BeginEndCompoundBlockContext
268+
if Option(c.beginLabel()).isDefined &&
269+
c.beginLabel().multipartIdentifier().getText.toLowerCase(Locale.ROOT).equals(label) =>
270+
if (isIterate) {
271+
throw SqlScriptingErrors.invalidIterateLabelUsageForCompound(CurrentOrigin.get, label)
272+
}
273+
true
274+
case c: WhileStatementContext
275+
if Option(c.beginLabel()).isDefined &&
276+
c.beginLabel().multipartIdentifier().getText.toLowerCase(Locale.ROOT).equals(label)
277+
=> true
278+
case _ => false
279+
}
280+
}
281+
282+
override def visitLeaveStatement(ctx: LeaveStatementContext): LeaveStatement =
283+
withOrigin(ctx) {
284+
val labelText = ctx.multipartIdentifier().getText.toLowerCase(Locale.ROOT)
285+
var parentCtx = ctx.parent
286+
287+
while (Option(parentCtx).isDefined) {
288+
if (leaveOrIterateContextHasLabel(parentCtx, labelText, isIterate = false)) {
289+
return LeaveStatement(labelText)
290+
}
291+
parentCtx = parentCtx.parent
292+
}
293+
294+
throw SqlScriptingErrors.labelDoesNotExist(
295+
CurrentOrigin.get, labelText, "LEAVE")
296+
}
297+
298+
override def visitIterateStatement(ctx: IterateStatementContext): IterateStatement =
299+
withOrigin(ctx) {
300+
val labelText = ctx.multipartIdentifier().getText.toLowerCase(Locale.ROOT)
301+
var parentCtx = ctx.parent
302+
303+
while (Option(parentCtx).isDefined) {
304+
if (leaveOrIterateContextHasLabel(parentCtx, labelText, isIterate = true)) {
305+
return IterateStatement(labelText)
306+
}
307+
parentCtx = parentCtx.parent
308+
}
309+
310+
throw SqlScriptingErrors.labelDoesNotExist(
311+
CurrentOrigin.get, labelText, "ITERATE")
312+
}
313+
264314
override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan = withOrigin(ctx) {
265315
Option(ctx.statement().asInstanceOf[ParserRuleContext])
266316
.orElse(Option(ctx.setResetStatement().asInstanceOf[ParserRuleContext]))

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/SqlScriptingLogicalOperators.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,21 @@ case class WhileStatement(
8989
condition: SingleStatement,
9090
body: CompoundBody,
9191
label: Option[String]) extends CompoundPlanStatement
92+
93+
/**
94+
* Logical operator for LEAVE statement.
95+
* The statement can be used both for compounds or any kind of loops.
96+
* When used, the corresponding body/loop execution is skipped and the execution continues
97+
* with the next statement after the body/loop.
98+
* @param label Label of the compound or loop to leave.
99+
*/
100+
case class LeaveStatement(label: String) extends CompoundPlanStatement
101+
102+
/**
103+
* Logical operator for ITERATE statement.
104+
* The statement can be used only for loops.
105+
* When used, the rest of the loop is skipped and the loop execution continues
106+
* with the next iteration.
107+
* @param label Label of the loop to iterate.
108+
*/
109+
case class IterateStatement(label: String) extends CompoundPlanStatement

sql/catalyst/src/main/scala/org/apache/spark/sql/errors/SqlScriptingErrors.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,27 @@ private[sql] object SqlScriptingErrors {
8484
cause = null,
8585
messageParameters = Map("invalidStatement" -> toSQLStmt(stmt)))
8686
}
87+
88+
def labelDoesNotExist(
89+
origin: Origin,
90+
labelName: String,
91+
statementType: String): Throwable = {
92+
new SqlScriptingException(
93+
origin = origin,
94+
errorClass = "INVALID_LABEL_USAGE.DOES_NOT_EXIST",
95+
cause = null,
96+
messageParameters = Map(
97+
"labelName" -> toSQLStmt(labelName),
98+
"statementType" -> statementType))
99+
}
100+
101+
def invalidIterateLabelUsageForCompound(
102+
origin: Origin,
103+
labelName: String): Throwable = {
104+
new SqlScriptingException(
105+
origin = origin,
106+
errorClass = "INVALID_LABEL_USAGE.ITERATE_IN_COMPOUND",
107+
cause = null,
108+
messageParameters = Map("labelName" -> toSQLStmt(labelName)))
109+
}
87110
}

0 commit comments

Comments
 (0)