Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SPARK-48348][SPARK-48376][SQL] Introduce LEAVE and ITERATE statements #47973

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2489,6 +2489,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