From 1e1a71f7ee06fce56f88f437a20d47b775635332 Mon Sep 17 00:00:00 2001 From: "R. C. Howell" Date: Tue, 16 Jan 2024 09:44:36 -0800 Subject: [PATCH] Adds qualifiers to function call expressions (#1337) --- .../lang/syntax/impl/PartiQLPigVisitor.kt | 87 ++++++----- partiql-parser/src/main/antlr/PartiQL.g4 | 12 +- .../org/partiql/parser/PartiQLParser.kt | 2 +- .../partiql/parser/PartiQLParserBuilder.kt | 2 +- .../PartiQLParserDefault.kt | 35 +++-- .../{impl => internal}/util/DateTimeUtils.kt | 2 +- .../PartiQLParserFunctionCallTests.kt | 136 ++++++++++++++++++ .../PartiQLParserSessionAttributeTests.kt | 2 +- 8 files changed, 226 insertions(+), 52 deletions(-) rename partiql-parser/src/main/kotlin/org/partiql/parser/{impl => internal}/PartiQLParserDefault.kt (98%) rename partiql-parser/src/main/kotlin/org/partiql/parser/{impl => internal}/util/DateTimeUtils.kt (98%) create mode 100644 partiql-parser/src/test/kotlin/org/partiql/parser/internal/PartiQLParserFunctionCallTests.kt rename partiql-parser/src/test/kotlin/org/partiql/parser/{impl => internal}/PartiQLParserSessionAttributeTests.kt (98%) diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/syntax/impl/PartiQLPigVisitor.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/syntax/impl/PartiQLPigVisitor.kt index ee93af85ee..1d10d5ce49 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/syntax/impl/PartiQLPigVisitor.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/syntax/impl/PartiQLPigVisitor.kt @@ -36,7 +36,6 @@ import com.amazon.ionelement.api.loadSingleElement import org.antlr.v4.runtime.ParserRuleContext import org.antlr.v4.runtime.Token import org.antlr.v4.runtime.tree.TerminalNode -import org.partiql.ast.Identifier import org.partiql.errors.ErrorCode import org.partiql.errors.Property import org.partiql.errors.PropertyValueMap @@ -118,7 +117,7 @@ import java.time.format.DateTimeParseException */ internal class PartiQLPigVisitor( val customTypes: List = listOf(), - private val parameterIndexes: Map = mapOf() + private val parameterIndexes: Map = mapOf(), ) : PartiQLBaseVisitor() { @@ -648,19 +647,22 @@ internal class PartiQLPigVisitor( excludeTupleAttr(identifier(attr, caseSensitivity)) } - override fun visitExcludeExprCollectionIndex(ctx: PartiQLParser.ExcludeExprCollectionIndexContext) = PartiqlAst.build { - val index = ctx.index.text.toInteger().toLong() - excludeCollectionIndex(index) - } + override fun visitExcludeExprCollectionIndex(ctx: PartiQLParser.ExcludeExprCollectionIndexContext) = + PartiqlAst.build { + val index = ctx.index.text.toInteger().toLong() + excludeCollectionIndex(index) + } - override fun visitExcludeExprCollectionAttr(ctx: PartiQLParser.ExcludeExprCollectionAttrContext) = PartiqlAst.build { - val attr = ctx.attr.getStringValue() - excludeTupleAttr(identifier(attr, caseSensitive())) - } + override fun visitExcludeExprCollectionAttr(ctx: PartiQLParser.ExcludeExprCollectionAttrContext) = + PartiqlAst.build { + val attr = ctx.attr.getStringValue() + excludeTupleAttr(identifier(attr, caseSensitive())) + } - override fun visitExcludeExprCollectionWildcard(ctx: PartiQLParser.ExcludeExprCollectionWildcardContext) = PartiqlAst.build { - excludeCollectionWildcard() - } + override fun visitExcludeExprCollectionWildcard(ctx: PartiQLParser.ExcludeExprCollectionWildcardContext) = + PartiqlAst.build { + excludeCollectionWildcard() + } override fun visitExcludeExprTupleWildcard(ctx: PartiQLParser.ExcludeExprTupleWildcardContext) = PartiqlAst.build { excludeTupleWildcard() @@ -1292,17 +1294,20 @@ internal class PartiQLPigVisitor( canLosslessCast(expr, type, metas) } - override fun visitFunctionCallIdent(ctx: PartiQLParser.FunctionCallIdentContext) = PartiqlAst.build { - val name = ctx.name.getString().lowercase() - val args = ctx.expr().map { visitExpr(it) } - val metas = ctx.name.getSourceMetaContainer() - call(name, args = args, metas = metas) - } - - override fun visitFunctionCallReserved(ctx: PartiQLParser.FunctionCallReservedContext) = PartiqlAst.build { - val name = ctx.name.text.lowercase() + override fun visitFunctionCall(ctx: PartiQLParser.FunctionCallContext) = PartiqlAst.build { + val name = when (val nameCtx = ctx.functionName()) { + is PartiQLParser.FunctionNameReservedContext -> { + if (nameCtx.qualifier.isNotEmpty()) error("Legacy AST does not support qualified function names") + nameCtx.name.text.lowercase() + } + is PartiQLParser.FunctionNameSymbolContext -> { + if (nameCtx.qualifier.isNotEmpty()) error("Legacy AST does not support qualified function names") + nameCtx.name.getString().lowercase() + } + else -> error("Expected context FunctionNameReserved or FunctionNameSymbol") + } val args = ctx.expr().map { visitExpr(it) } - val metas = ctx.name.getSourceMetaContainer() + val metas = ctx.start.getSourceMetaContainer() call(name, args = args, metas = metas) } @@ -1693,7 +1698,7 @@ internal class PartiQLPigVisitor( lhs: ParserRuleContext?, rhs: ParserRuleContext?, op: Token?, - parent: ParserRuleContext? = null + parent: ParserRuleContext? = null, ) = PartiqlAst.build { if (parent != null) return@build visit(parent) as PartiqlAst.Expr val args = listOf(lhs!!, rhs!!).map { visit(it) as PartiqlAst.Expr } @@ -1847,7 +1852,7 @@ internal class PartiQLPigVisitor( withTimeZone: Boolean, precision: Long, stringNode: TerminalNode, - timeNode: TerminalNode + timeNode: TerminalNode, ) = PartiqlAst.build { val time: LocalTime val formatter = when (withTimeZone) { @@ -1871,7 +1876,7 @@ internal class PartiQLPigVisitor( private fun getTimestampStringAndPrecision( stringNode: TerminalNode, - integerNode: TerminalNode? + integerNode: TerminalNode?, ): Pair { val timestampString = stringNode.getStringValue() val precision = when (integerNode) { @@ -1890,7 +1895,7 @@ internal class PartiQLPigVisitor( private fun getTimestampDynamic( timestampString: String, precision: Long?, - node: TerminalNode + node: TerminalNode, ) = PartiqlAst.build { val timestamp = try { @@ -1901,9 +1906,14 @@ internal class PartiQLPigVisitor( val timeZone = timestamp.timeZone?.let { getTimeZone(it) } timestamp( timestampValue( - timestamp.year.toLong(), timestamp.month.toLong(), timestamp.day.toLong(), - timestamp.hour.toLong(), timestamp.minute.toLong(), ionDecimal(Decimal.valueOf(timestamp.decimalSecond)), - timeZone, precision + timestamp.year.toLong(), + timestamp.month.toLong(), + timestamp.day.toLong(), + timestamp.hour.toLong(), + timestamp.minute.toLong(), + ionDecimal(Decimal.valueOf(timestamp.decimalSecond)), + timeZone, + precision ) ) } @@ -1911,7 +1921,7 @@ internal class PartiQLPigVisitor( private fun getTimestampWithTimezone( timestampString: String, precision: Long?, - node: TerminalNode + node: TerminalNode, ) = PartiqlAst.build { val timestamp = try { DateTimeUtils.parseTimestamp(timestampString) @@ -1926,9 +1936,14 @@ internal class PartiQLPigVisitor( val timeZone = timestamp.timeZone?.let { getTimeZone(it) } timestamp( timestampValue( - timestamp.year.toLong(), timestamp.month.toLong(), timestamp.day.toLong(), - timestamp.hour.toLong(), timestamp.minute.toLong(), ionDecimal(Decimal.valueOf(timestamp.decimalSecond)), - timeZone, precision + timestamp.year.toLong(), + timestamp.month.toLong(), + timestamp.day.toLong(), + timestamp.hour.toLong(), + timestamp.minute.toLong(), + ionDecimal(Decimal.valueOf(timestamp.decimalSecond)), + timeZone, + precision ) ) } @@ -2124,13 +2139,13 @@ internal class PartiQLPigVisitor( msg: String, code: ErrorCode, ctx: PropertyValueMap = PropertyValueMap(), - cause: Throwable? = null + cause: Throwable? = null, ) = this.error(msg, code, ctx, cause) private fun Token?.err( msg: String, code: ErrorCode, ctx: PropertyValueMap = PropertyValueMap(), - cause: Throwable? = null + cause: Throwable? = null, ) = this.error(msg, code, ctx, cause) } diff --git a/partiql-parser/src/main/antlr/PartiQL.g4 b/partiql-parser/src/main/antlr/PartiQL.g4 index 3567e022ef..748d038366 100644 --- a/partiql-parser/src/main/antlr/PartiQL.g4 +++ b/partiql-parser/src/main/antlr/PartiQL.g4 @@ -709,11 +709,15 @@ trimFunction dateFunction : func=(DATE_ADD|DATE_DIFF) PAREN_LEFT dt=IDENTIFIER COMMA expr COMMA expr PAREN_RIGHT; +// SQL-99 10.4 — ::= functionCall - : name=( CHAR_LENGTH | CHARACTER_LENGTH | OCTET_LENGTH | - BIT_LENGTH | UPPER | LOWER | SIZE | EXISTS | COUNT ) - PAREN_LEFT ( expr ( COMMA expr )* )? PAREN_RIGHT # FunctionCallReserved - | name=symbolPrimitive PAREN_LEFT ( expr ( COMMA expr )* )? PAREN_RIGHT # FunctionCallIdent + : functionName PAREN_LEFT ( expr ( COMMA expr )* )? PAREN_RIGHT + ; + +// SQL-99 10.4 — ::= [ ] +functionName + : (qualifier+=symbolPrimitive PERIOD)* name=( CHAR_LENGTH | CHARACTER_LENGTH | OCTET_LENGTH | BIT_LENGTH | UPPER | LOWER | SIZE | EXISTS | COUNT ) # FunctionNameReserved + | (qualifier+=symbolPrimitive PERIOD)* name=symbolPrimitive # FunctionNameSymbol ; pathStep diff --git a/partiql-parser/src/main/kotlin/org/partiql/parser/PartiQLParser.kt b/partiql-parser/src/main/kotlin/org/partiql/parser/PartiQLParser.kt index 8cbf60dd6b..fe8efc2949 100644 --- a/partiql-parser/src/main/kotlin/org/partiql/parser/PartiQLParser.kt +++ b/partiql-parser/src/main/kotlin/org/partiql/parser/PartiQLParser.kt @@ -15,7 +15,7 @@ package org.partiql.parser import org.partiql.ast.Statement -import org.partiql.parser.impl.PartiQLParserDefault +import org.partiql.parser.internal.PartiQLParserDefault public interface PartiQLParser { diff --git a/partiql-parser/src/main/kotlin/org/partiql/parser/PartiQLParserBuilder.kt b/partiql-parser/src/main/kotlin/org/partiql/parser/PartiQLParserBuilder.kt index b985ed7b04..1a918b0094 100644 --- a/partiql-parser/src/main/kotlin/org/partiql/parser/PartiQLParserBuilder.kt +++ b/partiql-parser/src/main/kotlin/org/partiql/parser/PartiQLParserBuilder.kt @@ -14,7 +14,7 @@ package org.partiql.parser -import org.partiql.parser.impl.PartiQLParserDefault +import org.partiql.parser.internal.PartiQLParserDefault /** * A builder class to instantiate a [PartiQLParser]. diff --git a/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt b/partiql-parser/src/main/kotlin/org/partiql/parser/internal/PartiQLParserDefault.kt similarity index 98% rename from partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt rename to partiql-parser/src/main/kotlin/org/partiql/parser/internal/PartiQLParserDefault.kt index 870f427420..73a029b6e9 100644 --- a/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt +++ b/partiql-parser/src/main/kotlin/org/partiql/parser/internal/PartiQLParserDefault.kt @@ -12,7 +12,7 @@ * language governing permissions and limitations under the License. */ -package org.partiql.parser.impl +package org.partiql.parser.internal import com.amazon.ionelement.api.IntElement import com.amazon.ionelement.api.IntElementSize @@ -118,6 +118,7 @@ import org.partiql.ast.graphMatchSelectorShortestK import org.partiql.ast.graphMatchSelectorShortestKGroup import org.partiql.ast.groupBy import org.partiql.ast.groupByKey +import org.partiql.ast.identifierQualified import org.partiql.ast.identifierSymbol import org.partiql.ast.let import org.partiql.ast.letBinding @@ -208,7 +209,7 @@ import org.partiql.parser.PartiQLSyntaxException import org.partiql.parser.SourceLocation import org.partiql.parser.SourceLocations import org.partiql.parser.antlr.PartiQLBaseVisitor -import org.partiql.parser.impl.util.DateTimeUtils +import org.partiql.parser.internal.util.DateTimeUtils import org.partiql.value.NumericValue import org.partiql.value.PartiQLValueExperimental import org.partiql.value.StringValue @@ -1723,16 +1724,34 @@ internal class PartiQLParserDefault : PartiQLParser { exprCanLosslessCast(expr, type) } - override fun visitFunctionCallIdent(ctx: GeneratedParser.FunctionCallIdentContext) = translate(ctx) { - val function = visitSymbolPrimitive(ctx.name) + override fun visitFunctionCall(ctx: GeneratedParser.FunctionCallContext) = translate(ctx) { + val function = visit(ctx.functionName()) as Identifier val args = visitOrEmpty(ctx.expr()) exprCall(function, args) } - override fun visitFunctionCallReserved(ctx: GeneratedParser.FunctionCallReservedContext) = translate(ctx) { - val function = ctx.name.text.toIdentifier() - val args = visitOrEmpty(ctx.expr()) - exprCall(function, args) + override fun visitFunctionNameReserved(ctx: GeneratedParser.FunctionNameReservedContext): Identifier { + val path = ctx.qualifier.map { visitSymbolPrimitive(it) } + val name = identifierSymbol(ctx.name.text, Identifier.CaseSensitivity.INSENSITIVE) + return if (path.isEmpty()) { + name + } else { + val root = path.first() + val steps = path.drop(1) + listOf(name) + identifierQualified(root, steps) + } + } + + override fun visitFunctionNameSymbol(ctx: GeneratedParser.FunctionNameSymbolContext): Identifier { + val path = ctx.qualifier.map { visitSymbolPrimitive(it) } + val name = visitSymbolPrimitive(ctx.name) + return if (path.isEmpty()) { + name + } else { + val root = path.first() + val steps = path.drop(1) + listOf(name) + identifierQualified(root, steps) + } } /** diff --git a/partiql-parser/src/main/kotlin/org/partiql/parser/impl/util/DateTimeUtils.kt b/partiql-parser/src/main/kotlin/org/partiql/parser/internal/util/DateTimeUtils.kt similarity index 98% rename from partiql-parser/src/main/kotlin/org/partiql/parser/impl/util/DateTimeUtils.kt rename to partiql-parser/src/main/kotlin/org/partiql/parser/internal/util/DateTimeUtils.kt index ea4b9a1a70..02fa77c482 100644 --- a/partiql-parser/src/main/kotlin/org/partiql/parser/impl/util/DateTimeUtils.kt +++ b/partiql-parser/src/main/kotlin/org/partiql/parser/internal/util/DateTimeUtils.kt @@ -1,4 +1,4 @@ -package org.partiql.parser.impl.util +package org.partiql.parser.internal.util import org.partiql.value.datetime.Date import org.partiql.value.datetime.DateTimeException diff --git a/partiql-parser/src/test/kotlin/org/partiql/parser/internal/PartiQLParserFunctionCallTests.kt b/partiql-parser/src/test/kotlin/org/partiql/parser/internal/PartiQLParserFunctionCallTests.kt new file mode 100644 index 0000000000..a562805895 --- /dev/null +++ b/partiql-parser/src/test/kotlin/org/partiql/parser/internal/PartiQLParserFunctionCallTests.kt @@ -0,0 +1,136 @@ +package org.partiql.parser.internal + +import org.junit.jupiter.api.Test +import org.partiql.ast.AstNode +import org.partiql.ast.Expr +import org.partiql.ast.Identifier +import org.partiql.ast.exprCall +import org.partiql.ast.identifierQualified +import org.partiql.ast.identifierSymbol +import org.partiql.ast.statementQuery +import kotlin.test.assertEquals + +class PartiQLParserFunctionCallTests { + + private val parser = PartiQLParserDefault() + + private inline fun query(body: () -> Expr) = statementQuery(body()) + + @Test + fun callUnqualifiedNonReservedInsensitive() = assertExpression( + "foo()", + query { + exprCall( + function = identifierSymbol("foo", Identifier.CaseSensitivity.INSENSITIVE), + args = emptyList() + ) + } + ) + + @Test + fun callUnqualifiedNonReservedSensitive() = assertExpression( + "\"foo\"()", + query { + exprCall( + function = identifierSymbol("foo", Identifier.CaseSensitivity.SENSITIVE), + args = emptyList() + ) + } + ) + + @Test + fun callUnqualifiedReservedInsensitive() = assertExpression( + "upper()", + query { + exprCall( + function = identifierSymbol("upper", Identifier.CaseSensitivity.INSENSITIVE), + args = emptyList() + ) + } + ) + + @Test + fun callUnqualifiedReservedSensitive() = assertExpression( + "\"upper\"()", + query { + exprCall( + function = identifierSymbol("upper", Identifier.CaseSensitivity.SENSITIVE), + args = emptyList() + ) + } + ) + + @Test + fun callQualifiedNonReservedInsensitive() = assertExpression( + "my_catalog.my_schema.foo()", + query { + exprCall( + function = identifierQualified( + root = identifierSymbol("my_catalog", Identifier.CaseSensitivity.INSENSITIVE), + steps = listOf( + identifierSymbol("my_schema", Identifier.CaseSensitivity.INSENSITIVE), + identifierSymbol("foo", Identifier.CaseSensitivity.INSENSITIVE), + ) + ), + args = emptyList() + ) + } + ) + + @Test + fun callQualifiedNonReservedSensitive() = assertExpression( + "my_catalog.my_schema.\"foo\"()", + query { + exprCall( + function = identifierQualified( + root = identifierSymbol("my_catalog", Identifier.CaseSensitivity.INSENSITIVE), + steps = listOf( + identifierSymbol("my_schema", Identifier.CaseSensitivity.INSENSITIVE), + identifierSymbol("foo", Identifier.CaseSensitivity.SENSITIVE), + ) + ), + args = emptyList() + ) + } + ) + + @Test + fun callQualifiedReservedInsensitive() = assertExpression( + "my_catalog.my_schema.upper()", + query { + exprCall( + function = identifierQualified( + root = identifierSymbol("my_catalog", Identifier.CaseSensitivity.INSENSITIVE), + steps = listOf( + identifierSymbol("my_schema", Identifier.CaseSensitivity.INSENSITIVE), + identifierSymbol("upper", Identifier.CaseSensitivity.INSENSITIVE), + ) + ), + args = emptyList() + ) + } + ) + + @Test + fun callQualifiedReservedSensitive() = assertExpression( + "my_catalog.my_schema.\"upper\"()", + query { + exprCall( + function = identifierQualified( + root = identifierSymbol("my_catalog", Identifier.CaseSensitivity.INSENSITIVE), + steps = listOf( + identifierSymbol("my_schema", Identifier.CaseSensitivity.INSENSITIVE), + identifierSymbol("upper", Identifier.CaseSensitivity.SENSITIVE), + ) + ), + args = emptyList() + ) + } + ) + + private fun assertExpression(input: String, expected: AstNode) { + val result = parser.parse(input) + val actual = result.root + assertEquals(expected, actual) + } +} diff --git a/partiql-parser/src/test/kotlin/org/partiql/parser/impl/PartiQLParserSessionAttributeTests.kt b/partiql-parser/src/test/kotlin/org/partiql/parser/internal/PartiQLParserSessionAttributeTests.kt similarity index 98% rename from partiql-parser/src/test/kotlin/org/partiql/parser/impl/PartiQLParserSessionAttributeTests.kt rename to partiql-parser/src/test/kotlin/org/partiql/parser/internal/PartiQLParserSessionAttributeTests.kt index 1748db55fb..7d02f659ff 100644 --- a/partiql-parser/src/test/kotlin/org/partiql/parser/impl/PartiQLParserSessionAttributeTests.kt +++ b/partiql-parser/src/test/kotlin/org/partiql/parser/internal/PartiQLParserSessionAttributeTests.kt @@ -1,4 +1,4 @@ -package org.partiql.parser.impl +package org.partiql.parser.internal import org.junit.jupiter.api.Test import org.partiql.ast.AstNode