Skip to content

Commit

Permalink
[SPARK-48348][SPARK-48376][SQL] Introduce LEAVE and ITERATE state…
Browse files Browse the repository at this point in the history
…ments

### 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]>
  • Loading branch information
davidm-db authored and cloud-fan committed Sep 5, 2024
1 parent 182353d commit 9676b1c
Show file tree
Hide file tree
Showing 15 changed files with 664 additions and 15 deletions.
18 changes: 18 additions & 0 deletions common/utils/src/main/resources/error/error-conditions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2495,6 +2495,24 @@
],
"sqlState" : "F0000"
},
"INVALID_LABEL_USAGE" : {
"message" : [
"The usage of the label <labelName> is invalid."
],
"subClass" : {
"DOES_NOT_EXIST" : {
"message" : [
"Label was used in the <statementType> statement, but the label does not belong to any surrounding block."
]
},
"ITERATE_IN_COMPOUND" : {
"message" : [
"ITERATE statement cannot be used with a label that belongs to a compound (BEGIN...END) body."
]
}
},
"sqlState" : "42K0L"
},
"INVALID_LAMBDA_FUNCTION_CALL" : {
"message" : [
"Invalid lambda function call."
Expand Down
2 changes: 2 additions & 0 deletions docs/sql-ref-ansi-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,13 +556,15 @@ Below is a list of all the keywords in Spark SQL.
|INVOKER|non-reserved|non-reserved|non-reserved|
|IS|reserved|non-reserved|reserved|
|ITEMS|non-reserved|non-reserved|non-reserved|
|ITERATE|non-reserved|non-reserved|non-reserved|
|JOIN|reserved|strict-non-reserved|reserved|
|KEYS|non-reserved|non-reserved|non-reserved|
|LANGUAGE|non-reserved|non-reserved|reserved|
|LAST|non-reserved|non-reserved|non-reserved|
|LATERAL|reserved|strict-non-reserved|reserved|
|LAZY|non-reserved|non-reserved|non-reserved|
|LEADING|reserved|non-reserved|reserved|
|LEAVE|non-reserved|non-reserved|non-reserved|
|LEFT|reserved|strict-non-reserved|reserved|
|LIKE|non-reserved|non-reserved|reserved|
|ILIKE|non-reserved|non-reserved|non-reserved|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,15 @@ INTO: 'INTO';
INVOKER: 'INVOKER';
IS: 'IS';
ITEMS: 'ITEMS';
ITERATE: 'ITERATE';
JOIN: 'JOIN';
KEYS: 'KEYS';
LANGUAGE: 'LANGUAGE';
LAST: 'LAST';
LATERAL: 'LATERAL';
LAZY: 'LAZY';
LEADING: 'LEADING';
LEAVE: 'LEAVE';
LEFT: 'LEFT';
LIKE: 'LIKE';
ILIKE: 'ILIKE';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ compoundStatement
| beginEndCompoundBlock
| ifElseStatement
| whileStatement
| leaveStatement
| iterateStatement
;

setStatementWithOptionalVarKeyword
Expand All @@ -83,6 +85,14 @@ ifElseStatement
(ELSE elseBody=compoundBody)? END IF
;

leaveStatement
: LEAVE multipartIdentifier
;

iterateStatement
: ITERATE multipartIdentifier
;

singleStatement
: (statement|setResetStatement) SEMICOLON* EOF
;
Expand Down Expand Up @@ -1578,10 +1588,12 @@ ansiNonReserved
| INTERVAL
| INVOKER
| ITEMS
| ITERATE
| KEYS
| LANGUAGE
| LAST
| LAZY
| LEAVE
| LIKE
| ILIKE
| LIMIT
Expand Down Expand Up @@ -1927,11 +1939,13 @@ nonReserved
| INVOKER
| IS
| ITEMS
| ITERATE
| KEYS
| LANGUAGE
| LAST
| LAZY
| LEADING
| LEAVE
| LIKE
| LONG
| ILIKE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import scala.collection.mutable.{ArrayBuffer, ListBuffer, Set}
import scala.jdk.CollectionConverters._
import scala.util.{Left, Right}

import org.antlr.v4.runtime.{ParserRuleContext, Token}
import org.antlr.v4.runtime.{ParserRuleContext, RuleContext, Token}
import org.antlr.v4.runtime.misc.Interval
import org.antlr.v4.runtime.tree.{ParseTree, RuleNode, TerminalNode}

Expand Down Expand Up @@ -261,6 +261,56 @@ class AstBuilder extends DataTypeAstBuilder
WhileStatement(condition, body, Some(labelText))
}

private def leaveOrIterateContextHasLabel(
ctx: RuleContext, label: String, isIterate: Boolean): Boolean = {
ctx match {
case c: BeginEndCompoundBlockContext
if Option(c.beginLabel()).isDefined &&
c.beginLabel().multipartIdentifier().getText.toLowerCase(Locale.ROOT).equals(label) =>
if (isIterate) {
throw SqlScriptingErrors.invalidIterateLabelUsageForCompound(CurrentOrigin.get, label)
}
true
case c: WhileStatementContext
if Option(c.beginLabel()).isDefined &&
c.beginLabel().multipartIdentifier().getText.toLowerCase(Locale.ROOT).equals(label)
=> true
case _ => false
}
}

override def visitLeaveStatement(ctx: LeaveStatementContext): LeaveStatement =
withOrigin(ctx) {
val labelText = ctx.multipartIdentifier().getText.toLowerCase(Locale.ROOT)
var parentCtx = ctx.parent

while (Option(parentCtx).isDefined) {
if (leaveOrIterateContextHasLabel(parentCtx, labelText, isIterate = false)) {
return LeaveStatement(labelText)
}
parentCtx = parentCtx.parent
}

throw SqlScriptingErrors.labelDoesNotExist(
CurrentOrigin.get, labelText, "LEAVE")
}

override def visitIterateStatement(ctx: IterateStatementContext): IterateStatement =
withOrigin(ctx) {
val labelText = ctx.multipartIdentifier().getText.toLowerCase(Locale.ROOT)
var parentCtx = ctx.parent

while (Option(parentCtx).isDefined) {
if (leaveOrIterateContextHasLabel(parentCtx, labelText, isIterate = true)) {
return IterateStatement(labelText)
}
parentCtx = parentCtx.parent
}

throw SqlScriptingErrors.labelDoesNotExist(
CurrentOrigin.get, labelText, "ITERATE")
}

override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan = withOrigin(ctx) {
Option(ctx.statement().asInstanceOf[ParserRuleContext])
.orElse(Option(ctx.setResetStatement().asInstanceOf[ParserRuleContext]))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,21 @@ case class WhileStatement(
condition: SingleStatement,
body: CompoundBody,
label: Option[String]) extends CompoundPlanStatement

/**
* Logical operator for LEAVE statement.
* The statement can be used both for compounds or any kind of loops.
* When used, the corresponding body/loop execution is skipped and the execution continues
* with the next statement after the body/loop.
* @param label Label of the compound or loop to leave.
*/
case class LeaveStatement(label: String) extends CompoundPlanStatement

/**
* Logical operator for ITERATE statement.
* The statement can be used only for loops.
* When used, the rest of the loop is skipped and the loop execution continues
* with the next iteration.
* @param label Label of the loop to iterate.
*/
case class IterateStatement(label: String) extends CompoundPlanStatement
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,27 @@ private[sql] object SqlScriptingErrors {
cause = null,
messageParameters = Map("invalidStatement" -> toSQLStmt(stmt)))
}

def labelDoesNotExist(
origin: Origin,
labelName: String,
statementType: String): Throwable = {
new SqlScriptingException(
origin = origin,
errorClass = "INVALID_LABEL_USAGE.DOES_NOT_EXIST",
cause = null,
messageParameters = Map(
"labelName" -> toSQLStmt(labelName),
"statementType" -> statementType))
}

def invalidIterateLabelUsageForCompound(
origin: Origin,
labelName: String): Throwable = {
new SqlScriptingException(
origin = origin,
errorClass = "INVALID_LABEL_USAGE.ITERATE_IN_COMPOUND",
cause = null,
messageParameters = Map("labelName" -> toSQLStmt(labelName)))
}
}
Loading

0 comments on commit 9676b1c

Please sign in to comment.