Skip to content

Add stored procedure calls with unnamed args #345

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

Merged
merged 4 commits into from
Jan 11, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion lang/resources/org/partiql/type-domains/partiql.ion
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
(where where::(? expr)))

// Data definition operations also cannot be composed with other `expr` nodes.
(ddl op::ddl_op))
(ddl op::ddl_op)

// Stored procedure calls are only allowed at the top level of a query and cannot be used as an expression
// Currently supports stored procedure calls with the unnamed argument syntax:
// EXEC <symbol> [<expr>.*]
(exec procedure_name::symbol args::(* expr 0)))

// The expressions that can result in values.
(sum expr
Expand Down
1 change: 1 addition & 0 deletions lang/src/org/partiql/lang/ast/AstSerialization.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ private class AstSerializerImpl(val astVersion: AstVersion, val ion: IonSystem):
is DropTable -> case { writeDropTable(expr) }
is DropIndex -> case { writeDropIndex(expr) }
is Parameter -> case { writeParameter(expr)}
is Exec -> throw UnsupportedOperationException("EXEC clause not supported by the V0 AST")
}.toUnit()
}
}
Expand Down
19 changes: 16 additions & 3 deletions lang/src/org/partiql/lang/ast/ExprNodeToStatement.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fun ExprNode.toAstStatement(): PartiqlAst.Statement {

is CreateTable, is CreateIndex, is DropTable, is DropIndex -> toAstDdl()

is Exec -> toAstExec()
}
}

Expand All @@ -30,7 +31,7 @@ private fun ExprNode.toAstDdl(): PartiqlAst.Statement {
when(thiz) {
is Literal, is LiteralMissing, is VariableReference, is Parameter, is NAry, is CallAgg, is Typed,
is Path, is SimpleCase, is SearchedCase, is Select, is Struct, is Seq,
is DataManipulation -> error("Can't convert ${thiz.javaClass} to PartiqlAst.ddl")
is DataManipulation, is Exec -> error("Can't convert ${thiz.javaClass} to PartiqlAst.ddl")

is CreateTable -> ddl(createTable(thiz.tableName), metas)
is CreateIndex -> ddl(createIndex(identifier(thiz.tableName, caseSensitive()), thiz.keys.map { it.toAstExpr() }), metas)
Expand All @@ -48,6 +49,18 @@ private fun ExprNode.toAstDdl(): PartiqlAst.Statement {
}
}

private fun ExprNode.toAstExec() : PartiqlAst.Statement {
val node = this
val metas = metas.toElectrolyteMetaContainer()

return PartiqlAst.build {
when (node) {
is Exec -> exec(node.procedureName.name, node.args.map { it.toAstExpr() }, metas)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

procedureName.meta is not kept in PartiqlAst.Statement? Not to say it's important but just want to confirm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That particular meta may not be too important, but we should be copying metas wherever possible. I will add that in the next commit.

I noticed there are quite a few other metas that aren't copied when converting ExprNode<-> PartiqlAst, so I created an issue (#353) to track.

else -> error("Can't convert ${node.javaClass} to PartiqlAst.Statement.Exec")
}
}
}

fun ExprNode.toAstExpr(): PartiqlAst.Expr {
val node = this
val metas = this.metas.toElectrolyteMetaContainer()
Expand Down Expand Up @@ -147,8 +160,8 @@ fun ExprNode.toAstExpr(): PartiqlAst.Expr {
SeqType.BAG -> bag(node.values.map { it.toAstExpr() })
}

// These are handled by `toAstDml()`
is DataManipulation, is CreateTable, is CreateIndex, is DropTable, is DropIndex ->
// These are handled by `toAstDml()`, `toAstDdl()`, and `toAstExec()`
is DataManipulation, is CreateTable, is CreateIndex, is DropTable, is DropIndex, is Exec ->
error("Can't transform ${node.javaClass} to a PartiqlAst.expr }")
}
}
Expand Down
6 changes: 6 additions & 0 deletions lang/src/org/partiql/lang/ast/StatementToExprNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.partiql.lang.ast

import com.amazon.ion.IonSystem
import com.amazon.ionelement.api.toIonValue
import org.partiql.lang.domains.PartiqlAst
import org.partiql.lang.domains.PartiqlAst.CaseSensitivity
import org.partiql.lang.domains.PartiqlAst.DdlOp
import org.partiql.lang.domains.PartiqlAst.DmlOp
Expand Down Expand Up @@ -33,6 +34,7 @@ private class StatementTransformer(val ion: IonSystem) {
is Statement.Query -> stmt.toExprNode()
is Statement.Dml -> stmt.toExprNode()
is Statement.Ddl -> stmt.toExprNode()
is Statement.Exec -> stmt.toExprNode()
}

private fun ElectrolyteMetaContainer.toPartiQlMetaContainer(): PartiQlMetaContainer {
Expand Down Expand Up @@ -344,4 +346,8 @@ private class StatementTransformer(val ion: IonSystem) {
metas = metas)
}
}

private fun Statement.Exec.toExprNode(): ExprNode {
return Exec(procedureName.toSymbolicName(), this.args.toExprNodeList(), metas.toPartiQlMetaContainer())
}
}
16 changes: 16 additions & 0 deletions lang/src/org/partiql/lang/ast/ast.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ sealed class ExprNode : AstNode(), HasMetas {
is Parameter -> {
copy(metas = metas)
}
is Exec -> {
copy(metas = metas)
}
}
}
}
Expand Down Expand Up @@ -210,6 +213,19 @@ data class Typed(
override val children: List<AstNode> = listOf(expr, type)
}

//********************************
// Stored procedure clauses
//********************************

/** Represents a call to a stored procedure, i.e. `EXEC stored_procedure [<expr>.*]` */
data class Exec(
val procedureName: SymbolicName,
val args: List<ExprNode>,
override val metas: MetaContainer
) : ExprNode() {
override val children: List<AstNode> = args
}

//********************************
// Path expressions
//********************************
Expand Down
7 changes: 7 additions & 0 deletions lang/src/org/partiql/lang/ast/passes/AstRewriterBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ open class AstRewriterBase : AstRewriter {
is CreateIndex -> rewriteCreateIndex(node)
is DropTable -> rewriteDropTable(node)
is DropIndex -> rewriteDropIndex(node)
is Exec -> rewriteExec(node)
}

open fun rewriteMetas(itemWithMetas: HasMetas): MetaContainer = itemWithMetas.metas
Expand Down Expand Up @@ -398,4 +399,10 @@ open class AstRewriterBase : AstRewriter {
rewriteVariableReference(node.identifier) as VariableReference,
rewriteMetas(node))

open fun rewriteExec(node: Exec): Exec =
Exec(
rewriteSymbolicName(node.procedureName),
node.args.map { rewriteExprNode(it) },
rewriteMetas(node))

}
1 change: 1 addition & 0 deletions lang/src/org/partiql/lang/ast/passes/AstWalker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ open class AstWalker(private val visitor: AstVisitor) {
}
}
is CreateTable, is DropTable, is DropIndex -> case { }
is Exec -> case { }
}.toUnit()
}
}
Expand Down
10 changes: 10 additions & 0 deletions lang/src/org/partiql/lang/errors/ErrorCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,16 @@ enum class ErrorCode(private val category: ErrorCategory,
LOC_TOKEN,
"Aggregate function calls take 1 argument only"),

PARSE_NO_STORED_PROCEDURE_PROVIDED(
ErrorCategory.PARSER,
LOC_TOKEN,
"No stored procedure provided"),

PARSE_EXEC_AT_UNEXPECTED_LOCATION(
ErrorCategory.PARSER,
LOC_TOKEN,
"EXEC call found at unexpected location"),

PARSE_MALFORMED_JOIN(
ErrorCategory.PARSER,
LOC_TOKEN,
Expand Down
6 changes: 6 additions & 0 deletions lang/src/org/partiql/lang/eval/EvaluatingCompiler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ internal class EvaluatingCompiler(
is CreateIndex,
is DropIndex,
is DropTable -> compileDdl(expr)
is Exec -> compileExec(expr)

}
}

Expand Down Expand Up @@ -1924,6 +1926,10 @@ internal class EvaluatingCompiler(
}
}

private fun compileExec(node: ExprNode): ThunkEnv {
TODO()
}

/** A special wrapper for `UNPIVOT` values as a BAG. */
private class UnpivotedExprValue(private val values: Iterable<ExprValue>) : BaseExprValue() {
override val type = ExprValueType.BAG
Expand Down
46 changes: 44 additions & 2 deletions lang/src/org/partiql/lang/syntax/SqlParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ class SqlParser(private val ion: IonSystem) : Parser {
DROP_TABLE,
DROP_INDEX,
CREATE_INDEX,
PARAMETER;
PARAMETER,
EXEC;

val identifier = name.toLowerCase()
}
Expand Down Expand Up @@ -335,6 +336,10 @@ class SqlParser(private val ion: IonSystem) : Parser {
}
}
}
EXEC -> {
val procedureName = SymbolicName(token?.text!!.toLowerCase(), token.toSourceLocationMetaContainer())
Exec(procedureName, children.map { it.toExprNode() }, metas)
}
CALL_AGG -> {
val funcExpr =
VariableReference(
Expand Down Expand Up @@ -1015,6 +1020,7 @@ class SqlParser(private val ion: IonSystem) : Parser {
tail.tail.parseFunctionCall(head!!)
else -> err("Unexpected keyword", PARSE_UNEXPECTED_KEYWORD)
}
"exec" -> tail.parseExec()
else -> err("Unexpected keyword", PARSE_UNEXPECTED_KEYWORD)
}
LEFT_PAREN -> {
Expand Down Expand Up @@ -1712,6 +1718,28 @@ class SqlParser(private val ion: IonSystem) : Parser {
}
}

private fun List<Token>.parseExec(): ParseNode {
var rem = this
if (rem.head?.type == EOF) {
rem.err("No stored procedure provided", PARSE_NO_STORED_PROCEDURE_PROVIDED)
}

val procedureName = rem.head
rem = rem.tail

// Stored procedure call has no args
if (rem.head?.type == EOF) {
return ParseNode(EXEC, procedureName, emptyList(), rem)
}

else if (rem.head?.type == LEFT_PAREN) {
rem.err("Unexpected $LEFT_PAREN found following stored procedure call", PARSE_UNEXPECTED_TOKEN)
}

return rem.parseArgList(aliasSupportType = NONE, mode = NORMAL_ARG_LIST)
.copy(type = EXEC, token = procedureName)
}

/**
* Parses substring
*
Expand Down Expand Up @@ -2214,9 +2242,23 @@ class SqlParser(private val ion: IonSystem) : Parser {
return ParseNode(ARG_LIST, null, items, rem)
}

/**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other tokens that should only exist at top level right? Why is EXEC different and requires a separate check?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other tokens that should only exist at the top level (i.e. DML and DDL ops) weren't properly checked in the parser (created an issue #354 to add these checks in the future). I will rename the checkForUnexpectedExec() to something more general for all tokens that should only exist in the top level.

* Checks that the given [Token] list does not have `EXEC` calls outside of the top level query. Throws
* an error if so.
*/
private fun List<Token>.checkForUnexpectedExec() {
for ((index, token) in this.withIndex()) {
if (token.keywordText == "exec" && index != 0) {
token.err("EXEC call found at unexpected location", PARSE_EXEC_AT_UNEXPECTED_LOCATION)
}
}
}

/** Entry point into the parser. */
override fun parseExprNode(source: String): ExprNode {
val node = SqlLexer(ion).tokenize(source).parseExpression()
val tokens = SqlLexer(ion).tokenize(source)
tokens.checkForUnexpectedExec()
val node = tokens.parseExpression()
val rem = node.remaining
if (!rem.onlyEndOfStatement()) {
when(rem.head?.type ) {
Expand Down
83 changes: 83 additions & 0 deletions lang/test/org/partiql/lang/errors/ParserErrorsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1546,4 +1546,87 @@ class ParserErrorsTest : TestBase() {
Property.TOKEN_TYPE to TokenType.EOF,
Property.TOKEN_VALUE to ion.newSymbol("EOF")))

//****************************************
// EXEC clause parsing errors
//****************************************

@Test
fun execNoStoredProcedureProvided() = checkInputThrowingParserException(
"EXEC",
ErrorCode.PARSE_NO_STORED_PROCEDURE_PROVIDED,
mapOf(
Property.LINE_NUMBER to 1L,
Property.COLUMN_NUMBER to 5L,
Property.TOKEN_TYPE to TokenType.EOF,
Property.TOKEN_VALUE to ion.newSymbol("EOF")))

@Test
fun execCommaBetweenStoredProcedureAndArg() = checkInputThrowingParserException(
"EXEC foo, arg0, arg1",
ErrorCode.PARSE_UNEXPECTED_TERM,
mapOf(
Property.LINE_NUMBER to 1L,
Property.COLUMN_NUMBER to 9L,
Property.TOKEN_TYPE to TokenType.COMMA,
Property.TOKEN_VALUE to ion.newSymbol(",")))

@Test
fun execArgTrailingComma() = checkInputThrowingParserException(
"EXEC foo arg0, arg1,",
ErrorCode.PARSE_UNEXPECTED_TERM,
mapOf(
Property.LINE_NUMBER to 1L,
Property.COLUMN_NUMBER to 21L,
Property.TOKEN_TYPE to TokenType.EOF,
Property.TOKEN_VALUE to ion.newSymbol("EOF")))

@Test
fun execUnexpectedParen() = checkInputThrowingParserException(
"EXEC foo()",
ErrorCode.PARSE_UNEXPECTED_TOKEN,
mapOf(
Property.LINE_NUMBER to 1L,
Property.COLUMN_NUMBER to 9L,
Property.TOKEN_TYPE to TokenType.LEFT_PAREN,
Property.TOKEN_VALUE to ion.newSymbol("(")))

@Test
fun execUnexpectedParenWithArgs() = checkInputThrowingParserException(
"EXEC foo(arg0, arg1)",
ErrorCode.PARSE_UNEXPECTED_TOKEN,
mapOf(
Property.LINE_NUMBER to 1L,
Property.COLUMN_NUMBER to 9L,
Property.TOKEN_TYPE to TokenType.LEFT_PAREN,
Property.TOKEN_VALUE to ion.newSymbol("(")))

@Test
fun execAtUnexpectedLocation() = checkInputThrowingParserException(
"EXEC EXEC",
ErrorCode.PARSE_EXEC_AT_UNEXPECTED_LOCATION,
mapOf(
Property.LINE_NUMBER to 1L,
Property.COLUMN_NUMBER to 6L,
Property.TOKEN_TYPE to TokenType.KEYWORD,
Property.TOKEN_VALUE to ion.newSymbol("exec")))

@Test
fun execAtUnexpectedLocationAfterExec() = checkInputThrowingParserException(
"EXEC foo EXEC",
ErrorCode.PARSE_EXEC_AT_UNEXPECTED_LOCATION,
mapOf(
Property.LINE_NUMBER to 1L,
Property.COLUMN_NUMBER to 10L,
Property.TOKEN_TYPE to TokenType.KEYWORD,
Property.TOKEN_VALUE to ion.newSymbol("exec")))

@Test
fun execAtUnexpectedLocationInExpression() = checkInputThrowingParserException(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happen for SELECT * FROM EXEC or SELECT * FROM exec?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently for both of those queries, the PARSE_EXEC_AT_UNEXPECTED_LOCATION will be thrown. Perhaps I can make it more general for other tokens that should only be at the top-level.

"SELECT * FROM (EXEC undrop 'foo')",
ErrorCode.PARSE_EXEC_AT_UNEXPECTED_LOCATION,
mapOf(
Property.LINE_NUMBER to 1L,
Property.COLUMN_NUMBER to 16L,
Property.TOKEN_TYPE to TokenType.KEYWORD,
Property.TOKEN_VALUE to ion.newSymbol("exec")))
}
Loading