diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a5c651f6..8bb52eab59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,8 +89,6 @@ Thank you to all who have contributed! ## [0.12.0] - 2023-06-14 -## [0.12.0] - 2023-06-14 - ### Added - Adds support for using EXCLUDED within DML ON-CONFLICT-ACTION conditions. Closes #1111. diff --git a/docs/wiki/tutorials/Pluggable Functions Tutorial.md b/docs/wiki/tutorials/Pluggable Functions Tutorial.md new file mode 100644 index 0000000000..4fd6aab39b --- /dev/null +++ b/docs/wiki/tutorials/Pluggable Functions Tutorial.md @@ -0,0 +1,136 @@ +# Pluggable Functions Tutorial + +The PartiQL ecosystem has introduced a new method for integrating custom logic - Pluggable Functions. This system allows PartiQL users to implement and introduce their custom functions into the PartiQL Command Line Interface (CLI) without the need for extensive knowledge of the PartiQL codebase or build system. + +This document will guide you on creating custom functions and loading them to the PartiQL CLI. The functions created by you will be pluggable, meaning they can be integrated as needed, enhancing the functionality of the CLI without modifying the underlying code. + +## Step 1: Add Dependencies and Build Your Module + +To build your module, you can use build automation tools like Maven or Gradle. These tools will handle the dependency management and compilation process for you. Here's how you can specify the necessary dependencies: + +```Kotlin +dependencies { +implementation("org.partiql:partiql-spi:") +implementation("org.partiql:partiql-types:") +} +``` + +## Step 2: Create a Custom Function + +To create a custom function, you need to implement the `PartiQLFunction` interface. This interface allows you to define the function's behavior and its signature, including its name, return type, parameters, determinism, and optional description. + +Here's a basic template to help you start: + +```Kotlin +import org.partiql.spi.connector.ConnectorSession +import org.partiql.spi.function.PartiQLFunction +import org.partiql.spi.function.PartiQLFunctionExperimental +import org.partiql.types.PartiQLValueType +import org.partiql.types.function.FunctionParameter +import org.partiql.types.function.FunctionSignature +import org.partiql.value.PartiQLValue +import org.partiql.value.PartiQLValueExperimental +import org.partiql.value.StringValue +import org.partiql.value.stringValue + +@OptIn(PartiQLFunctionExperimental::class) +object TrimLead : PartiQLFunction { + override val signature = FunctionSignature( + name = "trim_lead", // Specify your function name + returns = PartiQLValueType.STRING, // Specify the return type + parameters = listOf( + FunctionParameter.ValueParameter(name = "str", type = PartiQLValueType.STRING) // Specify parameters + ), + isDeterministic = true, // Specify determinism + description = "Trims leading whitespace of a [str]." // A brief description of your function + ) + + @OptIn(PartiQLValueExperimental::class) + override operator fun invoke(session: ConnectorSession, arguments: List): PartiQLValue { + // Implement the function logic here + val str = (arguments[0] as? StringValue)?.string ?: "" + val processed = str.trimStart() + return stringValue(processed) + } +} +``` + +Ensure that you replace the signature and function invoking with your actual implementations. + +## Step 3: Implement the Plugin Interface + +Next, you need to implement the `Plugin` interface in your code. This allows you to return a list of all the custom `PartiQLFunction` implementations you've created, using the `getFunctions()` method. This step is crucial as it allows the service loader to retrieve all your custom functions. + +Here's an example of a `Plugin` implementation: + +```Kotlin +package org.partiql.plugins.mockdb + +import org.partiql.spi.Plugin +import org.partiql.spi.connector.Connector +import org.partiql.spi.function.PartiQLFunction + +public class LocalPlugin implements Plugin { + override fun getConnectorFactories(): List = listOf() + + @PartiQLFunctionExperimental + override fun getFunctions(): List = listOf( + TrimLead // Specify the functions + ) +} +``` + +## Step 4: Create Service Provider Configuration file + +In order for the Java's ServiceLoader to recognize your plugin and load your custom functions, you'll need to specify your `Plugin` implementation in a Service Provider Configuration file: + +1. In your project's main resources directory (usually `src/main/resources`), create a new directory named `META-INF/services`. + +2. Within this directory, create a new file named after the full interface name as `org.partiql.spi.Plugin`. + +3. Inside this file, write the fully qualified name of your Plugin implementation class. If you have multiple implementations, each should be listed on a new line. + +Here's an example if your `Plugin` implementation class is `org.partiql.plugins.mockdb.LocalPlugin`: +```Kotlin +org.partiql.plugins.mockdb.LocalPlugin +``` + +## Step 5: Package Your Functions into a .jar File + +Compile your function and `Plugin` implementation into bytecode and package it into a .jar file. This .jar file will act as a plugin for the PartiQL CLI. + +For example, if you are using Gradle, you can simply run `./gradlew build` to compile your module. And your .jar file can be found under `build/libs`. + +## Step 6: Load the Functions into CLI + +Each of your .jar files should be stored in its own subdirectory under the `plugins` directory, which itself is inside the .partiql directory in your home directory. Here's what the directory structure should look like: + +``` +~/.partiql/plugins + ├── firstPlugin + │ └── firstPlugin.jar + └── secondPlugin + └── secondPlugin.jar +``` + +In the example above, `firstPlugin.jar` and `secondPlugin.jar` are the plugins you've created, each in its own directory under `~/.partiql/plugins`. + +By default, the PartiQL CLI will search the `~/.partiql/plugins` directory for plugins. However, you can specify a different directory when starting the CLI with the `--plugins` option: + +```shell +partiql --plugins /path/to/your/plugin/directory +``` + +With this command, the CLI will search for .jar files in the specified directory’s subdirectories and load them as plugins. This feature gives you the flexibility to organize your plugins in a manner that best suits your needs. + +## Step 7: Invoke Custom Functions in CLI + +To use the custom functions, you simply call them as you would any other function in the PartiQL CLI. For example, if you created a function named "trim_lead", you would invoke it like so: + +```shell +partiql> trim_lead(string) +``` + +Please replace "string" with the actual string you want to trim. + +That's all there is to it! With this mechanism, you can introduce any number of custom functions to PartiQL, and these functions will be usable just like built-in functions, without the need to modify the PartiQL codebase. It's an excellent way to extend the capabilities of PartiQL and adapt it to the specific needs of your project or organization. diff --git a/partiql-ast/src/main/kotlin/org/partiql/ast/helpers/ToLegacyAst.kt b/partiql-ast/src/main/kotlin/org/partiql/ast/helpers/ToLegacyAst.kt index 6436203763..58c81bfa3a 100644 --- a/partiql-ast/src/main/kotlin/org/partiql/ast/helpers/ToLegacyAst.kt +++ b/partiql-ast/src/main/kotlin/org/partiql/ast/helpers/ToLegacyAst.kt @@ -831,7 +831,7 @@ private class AstTranslator(val metas: Map) : AstBaseVisi translate(node) { metas -> val prefilter = node.prefilter?.let { visitExpr(it, ctx) } val variable = node.variable - val label = node.label?.let { visitGraphMatchLabel(it, ctx) } + val label = node.label node(prefilter, variable, label, metas) } @@ -849,7 +849,7 @@ private class AstTranslator(val metas: Map) : AstBaseVisi val quantifier = node.quantifier?.let { visitGraphMatchQuantifier(it, ctx) } val prefilter = node.prefilter?.let { visitExpr(it, ctx) } val variable = node.variable - val label = node.label?.let { visitGraphMatchLabel(it, ctx) } + val label = node.label edge(direction, quantifier, prefilter, variable, label, metas) } @@ -902,39 +902,6 @@ private class AstTranslator(val metas: Map) : AstBaseVisi selectorShortestKGroup(k) } - override fun visitGraphMatchLabel(node: GraphMatch.Label, ctx: Ctx) = - super.visitGraphMatchLabel(node, ctx) as PartiqlAst.GraphLabelSpec - - override fun visitGraphMatchLabelName(node: GraphMatch.Label.Name, ctx: Ctx) = - translate(node) { metas -> - graphLabelName(node.name, metas) - } - - override fun visitGraphMatchLabelWildcard(node: GraphMatch.Label.Wildcard, ctx: Ctx) = - translate(node) { metas -> - graphLabelWildcard(metas) - } - - override fun visitGraphMatchLabelNegation(node: GraphMatch.Label.Negation, ctx: Ctx) = - translate(node) { metas -> - val arg = visitGraphMatchLabel(node.arg, ctx) - graphLabelNegation(arg, metas) - } - - override fun visitGraphMatchLabelConj(node: GraphMatch.Label.Conj, ctx: Ctx) = - translate(node) { metas -> - val lhs = visitGraphMatchLabel(node.lhs, ctx) - val rhs = visitGraphMatchLabel(node.rhs, ctx) - graphLabelConj(lhs, rhs, metas) - } - - override fun visitGraphMatchLabelDisj(node: GraphMatch.Label.Disj, ctx: Ctx) = - translate(node) { metas -> - val lhs = visitGraphMatchLabel(node.lhs, ctx) - val rhs = visitGraphMatchLabel(node.rhs, ctx) - graphLabelDisj(lhs, rhs, metas) - } - /** * DML */ diff --git a/partiql-ast/src/main/pig/partiql.ion b/partiql-ast/src/main/pig/partiql.ion index 86268df3c0..8734632406 100644 --- a/partiql-ast/src/main/pig/partiql.ion +++ b/partiql-ast/src/main/pig/partiql.ion @@ -271,22 +271,13 @@ may then be further optimized by selecting better implementations of each operat (edge_left_or_right) (edge_left_or_undirected_or_right)) - // A label spec in a node pattern like `MATCH (x : )` or in an edge pattern like `MATCH −[t : ]−>` - (sum graph_label_spec - (graph_label_name name::symbol) // as in `MATCH (x:Account)` or `MATCH -[x:Transfer]->` - (graph_label_wildcard) // as in `MATCH (x: %)` - (graph_label_negation arg::graph_label_spec) // as in `MATCH (x: !Account)` - (graph_label_conj lhs::graph_label_spec rhs::graph_label_spec) // as in `MATCH (x: City&Country)` - Monaco can do - (graph_label_disj lhs::graph_label_spec rhs::graph_label_spec) // as in `MATCH (x: City|Country)` - either Paris or Germany will do - ) - // A part of a graph pattern (sum graph_match_pattern_part // A single node in a graph pattern. (node prefilter::(? expr) // an optional node pre-filter, e.g.: `WHERE c.name='Alarm'` in `MATCH (c WHERE c.name='Alarm')` variable::(? symbol) // the optional element variable of the node match, e.g.: `x` in `MATCH (x)` - label::(? graph_label_spec)) // the optional label spec to match for the node, e.g.: `Entity` in `MATCH (x:Entity)` + label::(* symbol 0)) // the optional label(s) to match for the node, e.g.: `Entity` in `MATCH (x:Entity)` // A single edge in a graph pattern. (edge @@ -294,8 +285,7 @@ may then be further optimized by selecting better implementations of each operat quantifier::(? graph_match_quantifier) // an optional quantifier for the entire pattern match, e.g. `{2,5}` in `MATCH (a:Account)−[:Transfer]−>{2,5}(b:Account)` prefilter::(? expr) // an optional edge pre-filter, e.g.: `WHERE t.capacity>100` in `MATCH −[t:hasSupply WHERE t.capacity>100]−>` variable::(? symbol) // the optional element variable of the edge match, e.g.: `t` in `MATCH −[t]−>` - label::(? graph_label_spec)) // the optional label spec to match for the edge. e.g.: `Target` in `MATCH −[t:Target]−>` - + label::(* symbol 0)) // the optional label(s) to match for the edge. e.g.: `Target` in `MATCH −[t:Target]−>` // A sub-pattern. (pattern pattern::graph_match_pattern)) diff --git a/partiql-ast/src/main/resources/partiql_ast.ion b/partiql-ast/src/main/resources/partiql_ast.ion index e4cf854f28..3c95d660cb 100644 --- a/partiql-ast/src/main/resources/partiql_ast.ion +++ b/partiql-ast/src/main/resources/partiql_ast.ion @@ -651,7 +651,7 @@ graph_match::{ node::{ prefilter: optional::expr, // An optional node pre-filter, e.g.: `WHERE c.name='Alarm'` in `MATCH (c WHERE c.name='Alarm')` variable: optional::string, // The optional element variable of the node match, e.g.: `x` in `MATCH (x)` - label: optional::label, // The optional label(s) to match for the node, e.g.: `Entity` in `MATCH (x:Entity)` + label: list::[string], // The optional label(s) to match for the node, e.g.: `Entity` in `MATCH (x:Entity)` }, // A single edge in a graph pattern edge::{ @@ -659,7 +659,7 @@ graph_match::{ quantifier: optional::quantifier, // An optional quantifier for the entire pattern match, e.g. `{2,5}` in `MATCH (a:Account)−[:Transfer]−>{2,5}(b:Account)` prefilter: optional::expr, // An optional edge pre-filter, e.g.: `WHERE t.capacity>100` in `MATCH −[t:hasSupply WHERE t.capacity>100]−>` variable: optional::string, // The optional element variable of the edge match, e.g.: `t` in `MATCH −[t]−>` - label: optional::label, // The optional label spec to match for the edge. e.g.: `Target` in `MATCH −[t:Target]−>` + label: list::[string], // The optional label(s) to match for the edge. e.g.: `Target` in `MATCH −[t:Target]−>` }, // A sub-pattern pattern::{ @@ -702,15 +702,6 @@ graph_match::{ shortest_k::{ k: long }, // SHORTEST k shortest_k_group::{ k: long }, // SHORTEST k GROUP ], // Fig. 8 — https://arxiv.org/abs/2112.06217 - - // A label spec in a node pattern like `MATCH (x : )` or in an edge pattern like `MATCH −[t : ]−>` - label::[ - name::{ name: string }, // as in `MATCH (x:Account)` or `MATCH -[x:Transfer]->` - wildcard::{}, // as in `MATCH (x: %)` - negation::{arg: label}, // as in `MATCH (x: !Account)` - conj::{lhs: label, rhs: label}, // as in `MATCH (x: City&Country)` - Monaco can do - disj::{lhs: label, rhs: label}, // as in `MATCH (x: City|Country)` - either Paris or Germany will do - ], ], } diff --git a/partiql-cli/src/main/kotlin/org/partiql/cli/utils/ServiceLoaderUtil.kt b/partiql-cli/src/main/kotlin/org/partiql/cli/utils/ServiceLoaderUtil.kt index fd6884fa22..1b649ce707 100644 --- a/partiql-cli/src/main/kotlin/org/partiql/cli/utils/ServiceLoaderUtil.kt +++ b/partiql-cli/src/main/kotlin/org/partiql/cli/utils/ServiceLoaderUtil.kt @@ -63,7 +63,6 @@ import org.partiql.value.NullableListValue import org.partiql.value.NullableSexpValue import org.partiql.value.NullableStringValue import org.partiql.value.NullableSymbolValue -import org.partiql.value.NullableTimeValue import org.partiql.value.PartiQLValue import org.partiql.value.PartiQLValueExperimental import org.partiql.value.SexpValue @@ -77,6 +76,9 @@ import org.partiql.value.boolValue import org.partiql.value.charValue import org.partiql.value.clobValue import org.partiql.value.dateValue +import org.partiql.value.datetime.DateTimeValue.date +import org.partiql.value.datetime.DateTimeValue.time +import org.partiql.value.datetime.TimeZone import org.partiql.value.decimalValue import org.partiql.value.float32Value import org.partiql.value.float64Value @@ -95,8 +97,10 @@ import org.partiql.value.symbolValue import org.partiql.value.timeValue import java.math.BigDecimal import java.math.BigInteger +import java.math.RoundingMode import java.net.URLClassLoader import java.nio.file.Path +import java.time.DateTimeException import java.util.ServiceLoader import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -252,25 +256,38 @@ class ServiceLoaderUtil { PartiQLValueType.CLOB -> (partiqlValue as? ClobValue)?.value?.let { newClob(it) } ?: throw PartiQLtoExprValueTypeMismatchException("CLOB", partiqlValue.type) - PartiQLValueType.DATE -> (partiqlValue as? DateValue)?.value?.let { newDate(it) } + PartiQLValueType.DATE -> (partiqlValue as? DateValue)?.value?.let { newDate(it.year, it.month, it.day) } ?: throw PartiQLtoExprValueTypeMismatchException("DATE", partiqlValue.type) - PartiQLValueType.TIME -> { - val timeValue = partiqlValue as? TimeValue - timeValue?.let { tv -> - val value = tv.value - val precision = tv.precision - val offset = tv.offset - val withzone = tv.withZone - if (withzone) { - offset?.let { - newTime(Time.of(value, precision, it)) - } - } else { - newTime(Time.of(value, precision, null)) + PartiQLValueType.TIME -> (partiqlValue as? TimeValue)?.value?.let { partiqlTime -> + val fraction = partiqlTime.decimalSecond.remainder(BigDecimal.ONE) + val precision = + when { + fraction.scale() > 9 -> throw DateTimeException("Precision greater than nano seconds not supported") + else -> fraction.scale() } - } ?: throw PartiQLtoExprValueTypeMismatchException("TIME", partiqlValue.type) - } + + val tzMinutes = when (val tz = partiqlTime.timeZone) { + is TimeZone.UnknownTimeZone -> 0 // Treat unknown offset as UTC (+00:00) + is TimeZone.UtcOffset -> tz.totalOffsetMinutes + else -> null + } + + try { + newTime( + Time.of( + partiqlTime.hour, + partiqlTime.minute, + partiqlTime.decimalSecond.setScale(0, RoundingMode.DOWN).toInt(), + fraction.movePointRight(9).setScale(0, RoundingMode.DOWN).toInt(), + precision, + tzMinutes + ) + ) + } catch (e: DateTimeException) { + throw e + } + } ?: throw PartiQLtoExprValueTypeMismatchException("TIME", partiqlValue.type) PartiQLValueType.TIMESTAMP -> TODO() // TODO: Implement @@ -363,23 +380,38 @@ class ServiceLoaderUtil { PartiQLValueType.NULLABLE_CLOB -> (partiqlValue as? NullableClobValue)?.value?.let { newClob(it) } ?: ExprValue.nullValue - PartiQLValueType.NULLABLE_DATE -> (partiqlValue as? NullableDateValue)?.value?.let { newDate(it) } + PartiQLValueType.NULLABLE_DATE -> (partiqlValue as? NullableDateValue)?.value?.let { newDate(it.year, it.month, it.day) } ?: ExprValue.nullValue - PartiQLValueType.NULLABLE_TIME -> { - (partiqlValue as? NullableTimeValue)?.let { tv -> - tv.value?.let { value -> - val precision = tv.precision - val offset = tv.offset - val withzone = tv.withZone - if (withzone) { - offset?.let { offsetValue -> newTime(Time.of(value, precision, offsetValue)) } ?: null - } else { - newTime(Time.of(value, precision, null)) - } + PartiQLValueType.NULLABLE_TIME -> (partiqlValue as? TimeValue)?.value?.let { partiqlTime -> + val fraction = partiqlTime.decimalSecond.remainder(BigDecimal.ONE) + val precision = + when { + fraction.scale() > 9 -> throw DateTimeException("Precision greater than nano seconds not supported") + else -> fraction.scale() } - } ?: ExprValue.nullValue - } + + val tzMinutes = when (val tz = partiqlTime.timeZone) { + is TimeZone.UnknownTimeZone -> 0 // Treat unknown offset as UTC (+00:00) + is TimeZone.UtcOffset -> tz.totalOffsetMinutes + else -> null + } + + try { + newTime( + Time.of( + partiqlTime.hour, + partiqlTime.minute, + partiqlTime.decimalSecond.setScale(0, RoundingMode.DOWN).toInt(), + fraction.movePointRight(9).setScale(0, RoundingMode.DOWN).toInt(), + precision, + tzMinutes + ) + ) + } catch (e: DateTimeException) { + throw e + } + } ?: ExprValue.nullValue PartiQLValueType.NULLABLE_TIMESTAMP -> TODO() // TODO: Implement @@ -397,17 +429,17 @@ class ServiceLoaderUtil { PartiQLValueType.NULLABLE_INTERVAL -> TODO() // add nullable interval conversion PartiQLValueType.NULLABLE_BAG -> { - (partiqlValue as? NullableBagValue<*>)?.elements?.map { PartiQLtoExprValue(it) } + (partiqlValue as? NullableBagValue<*>)?.promote()?.elements?.map { PartiQLtoExprValue(it) } ?.let { newBag(it.asSequence()) } ?: ExprValue.nullValue } PartiQLValueType.NULLABLE_LIST -> { - (partiqlValue as? NullableListValue<*>)?.elements?.map { PartiQLtoExprValue(it) } + (partiqlValue as? NullableListValue<*>)?.promote()?.map { PartiQLtoExprValue(it) } ?.let { newList(it.asSequence()) } ?: ExprValue.nullValue } PartiQLValueType.NULLABLE_SEXP -> { - (partiqlValue as? NullableSexpValue<*>)?.elements?.map { PartiQLtoExprValue(it) } + (partiqlValue as? NullableSexpValue<*>)?.promote()?.map { PartiQLtoExprValue(it) } ?.let { newSexp(it.asSequence()) } ?: ExprValue.nullValue } @@ -488,11 +520,18 @@ class ServiceLoaderUtil { } PartiQLValueType.DATE -> { checkType(ExprValueType.DATE) - dateValue(exprValue.dateValue()) + dateValue( + date(exprValue.dateValue().year, exprValue.dateValue().monthValue, exprValue.dateValue().dayOfMonth) + ) } PartiQLValueType.TIME -> { checkType(ExprValueType.TIME) - timeValue(exprValue.timeValue().localTime, exprValue.timeValue().precision, exprValue.timeValue().zoneOffset, true) + timeValue( + time( + exprValue.timeValue().localTime.hour, exprValue.timeValue().localTime.minute, exprValue.timeValue().localTime.second, exprValue.timeValue().localTime.nano, + exprValue.timeValue().timezoneMinute?.let { TimeZone.UtcOffset.of(it) } ?: null + ) + ) } PartiQLValueType.TIMESTAMP -> TODO() PartiQLValueType.INTERVAL -> TODO() @@ -594,12 +633,17 @@ class ServiceLoaderUtil { } PartiQLValueType.NULLABLE_DATE -> when (exprValue.type) { ExprValueType.NULL -> nullValue() - ExprValueType.DATE -> dateValue(exprValue.dateValue()) + ExprValueType.DATE -> dateValue(date(exprValue.dateValue().year, exprValue.dateValue().monthValue, exprValue.dateValue().dayOfMonth)) else -> throw ExprToPartiQLValueTypeMismatchException(PartiQLValueType.NULLABLE_DATE, ExprToPartiQLValueType(exprValue)) } PartiQLValueType.NULLABLE_TIME -> when (exprValue.type) { ExprValueType.NULL -> nullValue() - ExprValueType.TIME -> timeValue(exprValue.timeValue().localTime, exprValue.timeValue().precision, exprValue.timeValue().zoneOffset, true) + ExprValueType.TIME -> timeValue( + time( + exprValue.timeValue().localTime.hour, exprValue.timeValue().localTime.minute, exprValue.timeValue().localTime.second, exprValue.timeValue().localTime.nano, + exprValue.timeValue().timezoneMinute?.let { TimeZone.UtcOffset.of(it) } ?: null + ) + ) else -> throw ExprToPartiQLValueTypeMismatchException(PartiQLValueType.NULLABLE_TIME, ExprToPartiQLValueType(exprValue)) } PartiQLValueType.NULLABLE_TIMESTAMP -> TODO() diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/graph/GpmlTranslator.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/graph/GpmlTranslator.kt index 7c715b0541..dcff6496bf 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/graph/GpmlTranslator.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/graph/GpmlTranslator.kt @@ -2,6 +2,7 @@ package org.partiql.lang.graph import org.partiql.lang.domains.PartiqlAst import org.partiql.lang.graph.GpmlTranslator.normalizeElemList +import org.partiql.pig.runtime.SymbolPrimitive /** Translate an AST graph pattern into a "plan spec" to be executed by the graph engine. * Currently, the only non-trivial aspect is making sure (in [normalizeElemList]) that node and edge elements alternate. @@ -51,12 +52,11 @@ object GpmlTranslator { ) } - fun translateLabels(labelSpec: PartiqlAst.GraphLabelSpec?): LabelSpec { - return when (labelSpec) { - null -> LabelSpec.Wildcard - is PartiqlAst.GraphLabelSpec.GraphLabelName -> LabelSpec.Name(labelSpec.name.text) - is PartiqlAst.GraphLabelSpec.GraphLabelWildcard -> LabelSpec.Wildcard - else -> TODO("Not yet supported graph label pattern: $labelSpec") + fun translateLabels(labels: List): LabelSpec { + return when (labels.size) { + 0 -> LabelSpec.Whatever + 1 -> LabelSpec.OneOf(labels[0].text) + else -> TODO("Not yet supported in evaluating a GPML graph element pattern: multiple/alternating labels") } } @@ -76,7 +76,7 @@ object GpmlTranslator { * TODO: Deal with adjacent [NodeSpec]s -- by "unification" or prohibit. */ fun normalizeElemList(elems: List): List { - val fillerNode = NodeSpec(null, LabelSpec.Wildcard) + val fillerNode = NodeSpec(null, LabelSpec.Whatever) val normalized = mutableListOf() var expectNode = true for (x in elems) { diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/graph/Graph.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/graph/Graph.kt index ee203423ac..1d62535dfa 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/graph/Graph.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/graph/Graph.kt @@ -66,11 +66,11 @@ interface Graph { * based on labels at them. */ sealed class LabelSpec { - /** A graph element (node or edge) matches when its label set contains [name]. */ - data class Name(val name: String) : LabelSpec() + /** A graph element matches when one of its labels is [name].*/ + data class OneOf(val name: String) : LabelSpec() - /** A graph element matches as long as it has a label. */ - object Wildcard : LabelSpec() + /** A graph element always matches (even when it is not labeled at all).*/ + object Whatever : LabelSpec() // TODO: more LabelSpec features: alternation, negation, string patterns, ... } diff --git a/partiql-lang/src/main/kotlin/org/partiql/lang/graph/SimpleGraph.kt b/partiql-lang/src/main/kotlin/org/partiql/lang/graph/SimpleGraph.kt index cffda268e9..8d2fdf7123 100644 --- a/partiql-lang/src/main/kotlin/org/partiql/lang/graph/SimpleGraph.kt +++ b/partiql-lang/src/main/kotlin/org/partiql/lang/graph/SimpleGraph.kt @@ -35,8 +35,8 @@ class SimpleGraph( private fun labelsMatchSpec(labels: Set, spec: LabelSpec): Boolean = when (spec) { - LabelSpec.Wildcard -> true - is LabelSpec.Name -> labels.contains(spec.name) + LabelSpec.Whatever -> true + is LabelSpec.OneOf -> labels.contains(spec.name) } private fun Graph.Elem.matches(labelSpec: LabelSpec): Boolean = 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 8249d79c45..5e8043c1ba 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 @@ -781,36 +781,8 @@ internal class PartiQLPigVisitor( } } - override fun visitLabelSpecOr(ctx: PartiQLParser.LabelSpecOrContext): PartiqlAst.GraphLabelSpec = - PartiqlAst.build { - val lhs = visit(ctx.labelSpec()) as PartiqlAst.GraphLabelSpec - val rhs = visit(ctx.labelTerm()) as PartiqlAst.GraphLabelSpec - graphLabelDisj(lhs, rhs, ctx.VERTBAR().getSourceMetaContainer()) - } - - override fun visitLabelTermAnd(ctx: PartiQLParser.LabelTermAndContext): PartiqlAst.GraphLabelSpec = - PartiqlAst.build { - val lhs = visit(ctx.labelTerm()) as PartiqlAst.GraphLabelSpec - val rhs = visit(ctx.labelFactor()) as PartiqlAst.GraphLabelSpec - graphLabelConj(lhs, rhs, ctx.AMPERSAND().getSourceMetaContainer()) - } - - override fun visitLabelFactorNot(ctx: PartiQLParser.LabelFactorNotContext) = PartiqlAst.build { - val arg = visit(ctx.labelPrimary()) as PartiqlAst.GraphLabelSpec - graphLabelNegation(arg, ctx.BANG().getSourceMetaContainer()) - } - - override fun visitLabelPrimaryName(ctx: PartiQLParser.LabelPrimaryNameContext) = PartiqlAst.build { - val x = visitSymbolPrimitive(ctx.symbolPrimitive()) - graphLabelName_(x.name, x.metas) - } - - override fun visitLabelPrimaryWild(ctx: PartiQLParser.LabelPrimaryWildContext) = PartiqlAst.build { - graphLabelWildcard(ctx.PERCENT().getSourceMetaContainer()) - } - - override fun visitLabelPrimaryParen(ctx: PartiQLParser.LabelPrimaryParenContext) = - visit(ctx.labelSpec()) as PartiqlAst.GraphLabelSpec + override fun visitPatternPartLabel(ctx: PartiQLParser.PatternPartLabelContext) = + visitSymbolPrimitive(ctx.symbolPrimitive()) override fun visitPattern(ctx: PartiQLParser.PatternContext) = PartiqlAst.build { val restrictor = ctx.restrictor?.let { visitPatternRestrictor(it) } @@ -845,8 +817,13 @@ internal class PartiQLPigVisitor( val placeholderDirection = edgeRight() val variable = ctx.symbolPrimitive()?.let { visitSymbolPrimitive(it).name } val prefilter = ctx.whereClause()?.let { visitWhereClause(it) } - val label = ctx.labelSpec()?.let { visit(it) as PartiqlAst.GraphLabelSpec } - edge_(direction = placeholderDirection, variable = variable, prefilter = prefilter, label = label) + val label = ctx.patternPartLabel()?.let { visitPatternPartLabel(it).name } + edge_( + direction = placeholderDirection, + variable = variable, + prefilter = prefilter, + label = listOfNotNull(label) + ) } override fun visitEdgeSpecLeft(ctx: PartiQLParser.EdgeSpecLeftContext) = PartiqlAst.build { @@ -910,8 +887,8 @@ internal class PartiQLPigVisitor( override fun visitNode(ctx: PartiQLParser.NodeContext) = PartiqlAst.build { val variable = ctx.symbolPrimitive()?.let { visitSymbolPrimitive(it).name } val prefilter = ctx.whereClause()?.let { visitWhereClause(it) } - val label = ctx.labelSpec()?.let { visit(it) as PartiqlAst.GraphLabelSpec } - node_(variable = variable, prefilter = prefilter, label = label) + val label = ctx.patternPartLabel()?.let { visitPatternPartLabel(it).name } + node_(variable = variable, prefilter = prefilter, label = listOfNotNull(label)) } override fun visitPatternRestrictor(ctx: PartiQLParser.PatternRestrictorContext) = PartiqlAst.build { diff --git a/partiql-lang/src/test/kotlin/org/partiql/lang/eval/TypedNullTests.kt b/partiql-lang/src/test/kotlin/org/partiql/lang/eval/TypedNullTests.kt deleted file mode 100644 index 6734c4bd47..0000000000 --- a/partiql-lang/src/test/kotlin/org/partiql/lang/eval/TypedNullTests.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.partiql.lang.eval - -import com.amazon.ion.IonSystem -import com.amazon.ion.system.IonSystemBuilder -import org.junit.Test -import org.partiql.lang.CompilerPipeline - -/** - * The following tests demonstrate the current typed null behavior and tracks possible regressions in future commits. - * This behavior is what the Kotlin implementation defines and is subject to change pending resolution of - * https://github.com/partiql/partiql-spec/issues/61. - */ -class TypedNullTests : EvaluatorTestBase() { - // Round-tripped typed nulls (IonValue -> ExprValue -> IonValue) results in same typed null value - @Test - fun typedNullIonValueExprValueRoundTrip() { - val ion: IonSystem = IonSystemBuilder.standard().build() - val ionValue = ion.singleValue("null.int") - val exprValue = ExprValue.of(ionValue) - val roundTripped = exprValue.toIonValue(ion) - assertEquals(ionValue, roundTripped) - } - - // Evaluated typed nulls converted to an IonValue preserve the typed null. - @Test - fun typedNullIonLiteralEvaluation() { - val pipeline = CompilerPipeline.standard() - val ionValueAsString = "null.int" - val expr = pipeline.compile("`$ionValueAsString`") - val session = EvaluationSession.standard() - val result = expr.eval(session) - val ionValueRoundtripped = result.toIonValue(ion) - assertEquals(ion.singleValue(ionValueAsString), ionValueRoundtripped) - } - - // Evaluated typed nulls in an SFW projection. Converting result to an IonValue preserves the typed null. - @Test - fun typedNullInSFW() { - val pipeline = CompilerPipeline.standard() - val expr = pipeline.compile("SELECT t.a FROM [{'a': `null.int`}] AS t") - val session = EvaluationSession.standard() - val result = expr.eval(session) - val ionValueRoundtripped = result.toIonValue(ion) - assertEquals(ion.singleValue("\$bag::[{a: null.int}]"), ionValueRoundtripped) - } - - // Evaluated typed nulls in an arithmetic expression will NOT preserve the type when converting back to IonValue. - @Test - fun typedNullInArithmeticOperation() { - val pipeline = CompilerPipeline.standard() - val expr = pipeline.compile("`null.int` + `null.int`") - val session = EvaluationSession.standard() - val result = expr.eval(session) - val ionValueRoundtripped = result.toIonValue(ion) - assertEquals(ion.singleValue("null"), ionValueRoundtripped) - } -} diff --git a/partiql-lang/src/test/kotlin/org/partiql/lang/syntax/PartiQLParserMatchTest.kt b/partiql-lang/src/test/kotlin/org/partiql/lang/syntax/PartiQLParserMatchTest.kt index d45c4a4fb1..f80aae127b 100644 --- a/partiql-lang/src/test/kotlin/org/partiql/lang/syntax/PartiQLParserMatchTest.kt +++ b/partiql-lang/src/test/kotlin/org/partiql/lang/syntax/PartiQLParserMatchTest.kt @@ -3,6 +3,7 @@ package org.partiql.lang.syntax import com.amazon.ionelement.api.ionBool import com.amazon.ionelement.api.ionInt import com.amazon.ionelement.api.ionString +import org.junit.Ignore import org.junit.Test import org.partiql.lang.domains.PartiqlAst import org.partiql.lang.domains.id @@ -61,6 +62,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "x", + label = listOf() ) ) ), @@ -88,6 +90,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "x", + label = listOf() ) ) ) @@ -211,6 +214,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = null, + label = listOf() ) ) ) @@ -238,6 +242,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = null, + label = listOf() ) ) ) @@ -264,6 +269,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = null, + label = listOf() ) ) ) @@ -290,6 +296,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "x", + label = listOf() ) ) ) @@ -363,6 +370,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "x", + label = listOf() ) ) ) @@ -393,7 +401,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "x", - label = graphLabelName("Label") + label = listOf("Label") ) ) ) @@ -428,6 +436,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { quantifier = null, prefilter = null, variable = null, + label = listOf() ) ) ) @@ -457,6 +466,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { quantifier = null, prefilter = null, variable = null, + label = listOf() ), ) ), @@ -465,6 +475,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = null, + label = listOf() ) ) ) @@ -492,6 +503,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = null, + label = listOf() ) ) ), @@ -502,6 +514,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { quantifier = null, prefilter = null, variable = null, + label = listOf() ) ) ) @@ -514,7 +527,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { } val simpleGraphAST = - { direction: PartiqlAst.GraphMatchDirection, quantifier: PartiqlAst.GraphMatchQuantifier?, variable: String?, label: String? -> + { direction: PartiqlAst.GraphMatchDirection, quantifier: PartiqlAst.GraphMatchQuantifier?, variable: String?, label: List? -> PartiqlAst.build { select( project = projectList(projectExpr(id("a")), projectExpr(id("b"))), @@ -529,19 +542,19 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "a", - label = graphLabelName("A") + label = listOf("A") ), edge( direction = direction, quantifier = quantifier, prefilter = null, variable = variable, - label = label?.let { graphLabelName(it) } + label = label ?: emptyList() ), node( prefilter = null, variable = "b", - label = graphLabelName("B") + label = listOf("B") ), ) ) @@ -558,7 +571,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { fun rightDirected() = assertExpression( "SELECT a,b FROM g MATCH (a:A) -[e:E]-> (b:B)", ) { - simpleGraphAST(edgeRight(), null, "e", "E") + simpleGraphAST(edgeRight(), null, "e", listOf("E")) } @Test @@ -572,7 +585,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { fun leftDirected() = assertExpression( "SELECT a,b FROM g MATCH (a:A) <-[e:E]- (b:B)", ) { - simpleGraphAST(edgeLeft(), null, "e", "E") + simpleGraphAST(edgeLeft(), null, "e", listOf("E")) } @Test @@ -586,7 +599,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { fun undirected() = assertExpression( "SELECT a,b FROM g MATCH (a:A) ~[e:E]~ (b:B)", ) { - simpleGraphAST(edgeUndirected(), null, "e", "E") + simpleGraphAST(edgeUndirected(), null, "e", listOf("E")) } @Test @@ -600,7 +613,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { fun rightOrUnDirected() = assertExpression( "SELECT a,b FROM g MATCH (a:A) ~[e:E]~> (b:B)", ) { - simpleGraphAST(edgeUndirectedOrRight(), null, "e", "E") + simpleGraphAST(edgeUndirectedOrRight(), null, "e", listOf("E")) } @Test @@ -614,7 +627,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { fun leftOrUnDirected() = assertExpression( "SELECT a,b FROM g MATCH (a:A) <~[e:E]~ (b:B)", ) { - simpleGraphAST(edgeLeftOrUndirected(), null, "e", "E") + simpleGraphAST(edgeLeftOrUndirected(), null, "e", listOf("E")) } @Test @@ -628,7 +641,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { fun leftOrRight() = assertExpression( "SELECT a,b FROM g MATCH (a:A) <-[e:E]-> (b:B)", ) { - simpleGraphAST(edgeLeftOrRight(), null, "e", "E") + simpleGraphAST(edgeLeftOrRight(), null, "e", listOf("E")) } @Test @@ -642,7 +655,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { fun leftOrRightOrUndirected() = assertExpression( "SELECT a,b FROM g MATCH (a:A) -[e:E]- (b:B)", ) { - simpleGraphAST(edgeLeftOrUndirectedOrRight(), null, "e", "E") + simpleGraphAST(edgeLeftOrUndirectedOrRight(), null, "e", listOf("E")) } @Test @@ -656,28 +669,28 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { fun quantifierStar() = assertExpression( "SELECT a,b FROM g MATCH (a:A)-[:edge]->*(b:B)", ) { - simpleGraphAST(edgeRight(), graphMatchQuantifier(lower = 0, upper = null), null, "edge") + simpleGraphAST(edgeRight(), graphMatchQuantifier(lower = 0, upper = null), null, listOf("edge")) } @Test fun quantifierPlus() = assertExpression( "SELECT a,b FROM g MATCH (a:A)<-[:edge]-+(b:B)", ) { - simpleGraphAST(edgeLeft(), graphMatchQuantifier(lower = 1, upper = null), null, "edge") + simpleGraphAST(edgeLeft(), graphMatchQuantifier(lower = 1, upper = null), null, listOf("edge")) } @Test fun quantifierM() = assertExpression( "SELECT a,b FROM g MATCH (a:A)~[:edge]~{5,}(b:B)", ) { - simpleGraphAST(edgeUndirected(), graphMatchQuantifier(lower = 5, upper = null), null, "edge") + simpleGraphAST(edgeUndirected(), graphMatchQuantifier(lower = 5, upper = null), null, listOf("edge")) } @Test fun quantifierMN() = assertExpression( "SELECT a,b FROM g MATCH (a:A)-[e:edge]-{2,6}(b:B)", ) { - simpleGraphAST(edgeLeftOrUndirectedOrRight(), graphMatchQuantifier(lower = 2, upper = 6), "e", "edge") + simpleGraphAST(edgeLeftOrUndirectedOrRight(), graphMatchQuantifier(lower = 2, upper = 6), "e", listOf("edge")) } @Test @@ -733,19 +746,19 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "the_a", - label = graphLabelName("a") + label = listOf("a") ), edge( direction = edgeRight(), quantifier = null, prefilter = null, variable = "the_y", - label = graphLabelName("y") + label = listOf("y") ), node( prefilter = null, variable = "the_b", - label = graphLabelName("b") + label = listOf("b") ), ) ) @@ -781,17 +794,19 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "a", + label = listOf() ), edge( direction = edgeRight(), quantifier = null, prefilter = null, variable = null, - label = graphLabelName("has") + label = listOf("has") ), node( prefilter = null, variable = "x", + label = listOf() ), ) ), @@ -800,17 +815,19 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "x", + label = listOf() ), edge( direction = edgeRight(), quantifier = null, prefilter = null, variable = null, - label = graphLabelName("contains") + label = listOf("contains") ), node( prefilter = null, variable = "b", + label = listOf() ), ) ) @@ -840,28 +857,31 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { node( prefilter = null, variable = "a", + label = listOf() ), edge( direction = edgeRight(), quantifier = null, prefilter = null, variable = null, - label = graphLabelName("has") + label = listOf("has") ), node( prefilter = null, variable = null, + label = listOf() ), edge( direction = edgeRight(), quantifier = null, prefilter = null, variable = null, - label = graphLabelName("contains") + label = listOf("contains") ), node( prefilter = null, variable = "b", + label = listOf() ), ) ) @@ -889,16 +909,16 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { parts = listOf( node( variable = "a", - label = graphLabelName("A") + label = listOf("A") ), edge( direction = edgeRight(), variable = "e", - label = graphLabelName("E") + label = listOf("E") ), node( variable = "b", - label = graphLabelName("B") + label = listOf("B") ), ) ) @@ -935,16 +955,16 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { parts = listOf( node( variable = "a", - label = graphLabelName("A") + label = listOf("A") ), edge( direction = edgeRight(), variable = "e", - label = graphLabelName("Edge") + label = listOf("Edge") ), node( variable = "b", - label = graphLabelName("A") + label = listOf("A") ), ), ) @@ -977,7 +997,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { parts = listOf( node( variable = "a", - label = graphLabelName("A") + label = listOf("A") ), pattern( graphMatchPattern( @@ -987,7 +1007,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { edge( direction = edgeRight(), variable = "e", - label = graphLabelName("Edge") + label = listOf("Edge") ), node(), ) @@ -995,7 +1015,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { ), node( variable = "b", - label = graphLabelName("B") + label = listOf("B") ), ) ) @@ -1022,7 +1042,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { parts = listOf( node( variable = "a", - label = graphLabelName("A") + label = listOf("A") ), pattern( graphMatchPattern( @@ -1031,14 +1051,14 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { edge( direction = edgeRight(), variable = "e", - label = graphLabelName("Edge") + label = listOf("Edge") ), ) ) ), node( variable = "b", - label = graphLabelName("B") + label = listOf("B") ), ) ) @@ -1081,7 +1101,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { parts = listOf( node( variable = "p", - label = graphLabelName("Post"), + label = listOf("Post"), prefilter = eq( path(id("p"), pathExpr(lit(ionString("isFlagged")), caseInsensitive())), lit(ionBool(true)) @@ -1089,11 +1109,11 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { ), edge( direction = edgeLeft(), - label = graphLabelName("createdPost") + label = listOf("createdPost") ), node( variable = "u", - label = graphLabelName("Usr"), + label = listOf("Usr"), prefilter = and( eq( path( @@ -1110,11 +1130,11 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { ), edge( direction = edgeRight(), - label = graphLabelName("createdComment") + label = listOf("createdComment") ), node( variable = "c", - label = graphLabelName("Comment"), + label = listOf("Comment"), prefilter = eq( path(id("c"), pathExpr(lit(ionString("isFlagged")), caseInsensitive())), @@ -1159,7 +1179,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { edge( direction = edgeRight(), variable = "t", - label = graphLabelName("Transfer"), + label = listOf("Transfer"), quantifier = graphMatchQuantifier(0) ), node( @@ -1226,7 +1246,7 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { edge( direction = edgeRight(), variable = "t", - label = graphLabelName("Transfer"), + label = listOf("Transfer"), quantifier = graphMatchQuantifier(0) ), node( @@ -1393,139 +1413,45 @@ class PartiQLParserMatchTest : PartiQLParserTestBase() { } } - /** "SELECT x FROM g MATCH (x:$[spec])" */ - fun astSelectNodeWithLabelSpec(spec: PartiqlAst.GraphLabelSpec) = PartiqlAst.build { - select( - project = projectList(projectExpr(expr = id("x"))), - from = scan( - expr = graphMatch( - expr = id("g"), - gpmlPattern = gpmlPattern( - patterns = listOf( - graphMatchPattern( - parts = listOf( - node( - prefilter = null, - variable = "x", - label = spec, - ) - ) - ) - ) - ) - ) - ) - ) - } - - @Test - fun labelSimpleNamed() = assertExpression( - "SELECT x FROM g MATCH (x:A)" - ) { - astSelectNodeWithLabelSpec(spec = graphLabelName("A")) - } - + // TODO label combinators @Test + @Ignore fun labelDisjunction() = assertExpression( "SELECT x FROM g MATCH (x:Label|OtherLabel)", ) { - astSelectNodeWithLabelSpec( - spec = graphLabelDisj(graphLabelName("Label"), graphLabelName("OtherLabel")) - ) + TODO() } @Test + @Ignore fun labelConjunction() = assertExpression( "SELECT x FROM g MATCH (x:Label&OtherLabel)", ) { - astSelectNodeWithLabelSpec( - spec = graphLabelConj(graphLabelName("Label"), graphLabelName("OtherLabel")) - ) + TODO() } @Test + @Ignore fun labelNegation() = assertExpression( "SELECT x FROM g MATCH (x:!Label)", ) { - astSelectNodeWithLabelSpec(spec = graphLabelNegation(graphLabelName("Label"))) + TODO() } @Test + @Ignore fun labelWildcard() = assertExpression( "SELECT x FROM g MATCH (x:%)", ) { - astSelectNodeWithLabelSpec(spec = graphLabelWildcard()) + TODO() } - val astLabelCombo = PartiqlAst.build { - astSelectNodeWithLabelSpec( - spec = graphLabelDisj( - graphLabelDisj( - graphLabelDisj( - graphLabelName("L1"), - graphLabelConj(graphLabelName("L2"), graphLabelName("L3")) - ), - graphLabelNegation(graphLabelName("L4")) - ), - graphLabelConj(graphLabelName("L5"), graphLabelWildcard()) - ) - ) - } @Test + @Ignore fun labelCombo() = assertExpression( - "SELECT x FROM g MATCH (x: L1|L2&L3|!L4|(L5&%))", - ) { astLabelCombo } - - @Test - fun labelComboParens() = assertExpression( - "SELECT x FROM g MATCH (x: ((L1 | (L2&L3)) | !L4) | (L5&%))", - ) { astLabelCombo } - - /** (g MATCH <-[:$[spec]]-> ) */ - fun astMatchEdgeWithLabelSpec(spec: PartiqlAst.GraphLabelSpec) = PartiqlAst.build { - graphMatch( - expr = id("g"), - gpmlPattern = gpmlPattern( - patterns = listOf( - graphMatchPattern( - parts = listOf( - edge( - direction = edgeLeftOrRight(), - prefilter = null, - variable = null, - label = spec, - ) - ) - ) - ) - ) - ) - } - - @Test - fun edgeLabelSimpleNamed() = assertExpression( - "(g MATCH <-[:City]->)" - ) { - astMatchEdgeWithLabelSpec(graphLabelName("City")) - } - - @Test - fun edgeLabelOrAnd() = assertExpression( - "(g MATCH <-[ : Country | City & Sovereign ]->)" - ) { - astMatchEdgeWithLabelSpec( - graphLabelDisj( - graphLabelName("Country"), - graphLabelConj(graphLabelName("City"), graphLabelName("Sovereign")) - ) - ) - } - - @Test - fun edgeLabelUnlabeled() = assertExpression( - "(g MATCH <-[:!%]->)" + "SELECT x FROM g MATCH (x: L1|L2&L3|!L4|(L5&%)", ) { - astMatchEdgeWithLabelSpec(graphLabelNegation(graphLabelWildcard())) + TODO() } // TODO group variables (e.g., `MATCH ... WHERE SUM()...`) diff --git a/partiql-parser/src/main/antlr/PartiQL.g4 b/partiql-parser/src/main/antlr/PartiQL.g4 index b7b0561bf2..0390fbcf76 100644 --- a/partiql-parser/src/main/antlr/PartiQL.g4 +++ b/partiql-parser/src/main/antlr/PartiQL.g4 @@ -380,7 +380,7 @@ patternRestrictor // Should be TRAIL / ACYCLIC / SIMPLE : restrictor=IDENTIFIER; node - : PAREN_LEFT symbolPrimitive? ( COLON labelSpec )? whereClause? PAREN_RIGHT; + : PAREN_LEFT symbolPrimitive? patternPartLabel? whereClause? PAREN_RIGHT; edge : edgeWSpec quantifier=patternQuantifier? # EdgeWithSpec @@ -408,28 +408,10 @@ edgeWSpec ; edgeSpec - : BRACKET_LEFT symbolPrimitive? ( COLON labelSpec )? whereClause? BRACKET_RIGHT; + : BRACKET_LEFT symbolPrimitive? patternPartLabel? whereClause? BRACKET_RIGHT; -labelSpec - : labelSpec VERTBAR labelTerm # LabelSpecOr - | labelTerm # LabelSpecTerm - ; - -labelTerm - : labelTerm AMPERSAND labelFactor # LabelTermAnd - | labelFactor # LabelTermFactor - ; - -labelFactor - : BANG labelPrimary # LabelFactorNot - | labelPrimary # LabelFactorPrimary - ; - -labelPrimary - : symbolPrimitive # LabelPrimaryName - | PERCENT # LabelPrimaryWild - | PAREN_LEFT labelSpec PAREN_RIGHT # LabelPrimaryParen - ; +patternPartLabel + : COLON symbolPrimitive; edgeAbbrev : TILDE diff --git a/partiql-parser/src/main/antlr/PartiQLTokens.g4 b/partiql-parser/src/main/antlr/PartiQLTokens.g4 index dd0f561246..9cc6e7d158 100644 --- a/partiql-parser/src/main/antlr/PartiQLTokens.g4 +++ b/partiql-parser/src/main/antlr/PartiQLTokens.g4 @@ -326,9 +326,6 @@ PERCENT: '%'; AT_SIGN: '@'; TILDE: '~'; ASTERISK: '*'; -VERTBAR: '|'; -AMPERSAND: '&'; -BANG: '!'; LT_EQ: '<='; GT_EQ: '>='; EQ: '='; diff --git a/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt b/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt index 860a711ca6..1869f8572c 100644 --- a/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt +++ b/partiql-parser/src/main/kotlin/org/partiql/parser/impl/PartiQLParserDefault.kt @@ -1002,34 +1002,8 @@ internal class PartiQLParserDefault : PartiQLParser { } } - override fun visitLabelSpecOr(ctx: GeneratedParser.LabelSpecOrContext) = translate(ctx) { - val lhs = visit(ctx.labelSpec()) as GraphMatch.Label - val rhs = visit(ctx.labelTerm()) as GraphMatch.Label - graphMatchLabelDisj(lhs, rhs) - } - - override fun visitLabelTermAnd(ctx: GeneratedParser.LabelTermAndContext) = translate(ctx) { - val lhs = visit(ctx.labelTerm()) as GraphMatch.Label - val rhs = visit(ctx.labelFactor()) as GraphMatch.Label - graphMatchLabelConj(lhs, rhs) - } - - override fun visitLabelFactorNot(ctx: GeneratedParser.LabelFactorNotContext) = translate(ctx) { - val arg = visit(ctx.labelPrimary()) as GraphMatch.Label - graphMatchLabelNegation(arg) - } - - override fun visitLabelPrimaryName(ctx: GeneratedParser.LabelPrimaryNameContext) = translate(ctx) { - val x = visitSymbolPrimitive(ctx.symbolPrimitive()) - graphMatchLabelName(x.symbol) - } - - override fun visitLabelPrimaryWild(ctx: GeneratedParser.LabelPrimaryWildContext) = translate(ctx) { - graphMatchLabelWildcard() - } - - override fun visitLabelPrimaryParen(ctx: GeneratedParser.LabelPrimaryParenContext) = - visit(ctx.labelSpec()) as GraphMatch.Label + override fun visitPatternPartLabel(ctx: GeneratedParser.PatternPartLabelContext) = + visitSymbolPrimitive(ctx.symbolPrimitive()) override fun visitPattern(ctx: GeneratedParser.PatternContext) = translate(ctx) { val restrictor = visitRestrictor(ctx.restrictor) @@ -1043,7 +1017,7 @@ internal class PartiQLParserDefault : PartiQLParser { override fun visitEdgeAbbreviated(ctx: GeneratedParser.EdgeAbbreviatedContext) = translate(ctx) { val direction = visitEdge(ctx.edgeAbbrev()) val quantifier = visitOrNull(ctx.quantifier) - graphMatchPatternPartEdge(direction, quantifier, null, null, null) + graphMatchPatternPartEdge(direction, quantifier, null, null, emptyList()) } override fun visitEdgeWithSpec(ctx: GeneratedParser.EdgeWithSpecContext) = translate(ctx) { @@ -1056,8 +1030,8 @@ internal class PartiQLParserDefault : PartiQLParser { val placeholderDirection = GraphMatch.Direction.RIGHT val variable = visitOrNull(ctx.symbolPrimitive())?.symbol val prefilter = ctx.whereClause()?.let { visitExpr(it.expr()) } - val label = visitOrNull(ctx.labelSpec()) - graphMatchPatternPartEdge(placeholderDirection, null, prefilter, variable, label) + val label = visitOrNull(ctx.patternPartLabel())?.symbol + graphMatchPatternPartEdge(placeholderDirection, null, prefilter, variable, listOfNotNull(label)) } override fun visitEdgeSpecLeft(ctx: GeneratedParser.EdgeSpecLeftContext): AstNode { @@ -1126,8 +1100,8 @@ internal class PartiQLParserDefault : PartiQLParser { override fun visitNode(ctx: GeneratedParser.NodeContext) = translate(ctx) { val variable = visitOrNull(ctx.symbolPrimitive())?.symbol val prefilter = ctx.whereClause()?.let { visitExpr(it.expr()) } - val label = visitOrNull(ctx.labelSpec()) - graphMatchPatternPartNode(prefilter, variable, label) + val label = visitOrNull(ctx.patternPartLabel())?.symbol + graphMatchPatternPartNode(prefilter, variable, listOfNotNull(label)) } private fun visitRestrictor(ctx: GeneratedParser.PatternRestrictorContext?): GraphMatch.Restrictor? {