diff --git a/core/trino-main/src/main/java/io/trino/json/CachingResolver.java b/core/trino-main/src/main/java/io/trino/json/CachingResolver.java new file mode 100644 index 000000000000..7b337a0114d1 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/CachingResolver.java @@ -0,0 +1,193 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import io.trino.FullConnectorSession; +import io.trino.Session; +import io.trino.collect.cache.NonEvictableCache; +import io.trino.json.ir.IrPathNode; +import io.trino.metadata.BoundSignature; +import io.trino.metadata.Metadata; +import io.trino.metadata.OperatorNotFoundException; +import io.trino.metadata.ResolvedFunction; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.function.OperatorType; +import io.trino.spi.type.Type; +import io.trino.spi.type.TypeManager; +import io.trino.type.TypeCoercion; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import static com.google.common.base.Preconditions.checkState; +import static io.trino.collect.cache.SafeCaches.buildNonEvictableCache; +import static io.trino.json.CachingResolver.ResolvedOperatorAndCoercions.RESOLUTION_ERROR; +import static io.trino.json.CachingResolver.ResolvedOperatorAndCoercions.operators; +import static java.util.Objects.requireNonNull; + +/** + * A resolver providing coercions and binary operators used for JSON path evaluation, + * based on the operation type and the input types. + *

+ * It is instantiated per-driver, and caches the resolved operators and coercions. + * Caching is applied to IrArithmeticBinary and IrComparisonPredicate path nodes. + * Its purpose is to avoid resolving operators and coercions on a per-row basis, assuming + * that the input types to the JSON path operations repeat across rows. + *

+ * If an operator or a component coercion cannot be resolved for certain input types, + * it is cached as RESOLUTION_ERROR. It depends on the caller to handle this condition. + */ +public class CachingResolver +{ + private static final int MAX_CACHE_SIZE = 1000; + + private final Metadata metadata; + private final Session session; + private final TypeCoercion typeCoercion; + private final NonEvictableCache operators = buildNonEvictableCache(CacheBuilder.newBuilder().maximumSize(MAX_CACHE_SIZE)); + + public CachingResolver(Metadata metadata, ConnectorSession connectorSession, TypeManager typeManager) + { + requireNonNull(metadata, "metadata is null"); + requireNonNull(connectorSession, "connectorSession is null"); + requireNonNull(typeManager, "typeManager is null"); + + this.metadata = metadata; + this.session = ((FullConnectorSession) connectorSession).getSession(); + this.typeCoercion = new TypeCoercion(typeManager::getType); + } + + public ResolvedOperatorAndCoercions getOperators(IrPathNode node, OperatorType operatorType, Type leftType, Type rightType) + { + try { + return operators + .get(new NodeAndTypes(IrPathNodeRef.of(node), leftType, rightType), () -> resolveOperators(operatorType, leftType, rightType)); + } + catch (ExecutionException e) { + throw new RuntimeException(e); + } + } + + private ResolvedOperatorAndCoercions resolveOperators(OperatorType operatorType, Type leftType, Type rightType) + { + ResolvedFunction operator; + try { + operator = metadata.resolveOperator(session, operatorType, ImmutableList.of(leftType, rightType)); + } + catch (OperatorNotFoundException e) { + return RESOLUTION_ERROR; + } + + BoundSignature signature = operator.getSignature(); + + Optional leftCast = Optional.empty(); + if (!signature.getArgumentTypes().get(0).equals(leftType) && !typeCoercion.isTypeOnlyCoercion(leftType, signature.getArgumentTypes().get(0))) { + try { + leftCast = Optional.of(metadata.getCoercion(session, leftType, signature.getArgumentTypes().get(0))); + } + catch (OperatorNotFoundException e) { + return RESOLUTION_ERROR; + } + } + + Optional rightCast = Optional.empty(); + if (!signature.getArgumentTypes().get(1).equals(rightType) && !typeCoercion.isTypeOnlyCoercion(rightType, signature.getArgumentTypes().get(1))) { + try { + rightCast = Optional.of(metadata.getCoercion(session, rightType, signature.getArgumentTypes().get(1))); + } + catch (OperatorNotFoundException e) { + return RESOLUTION_ERROR; + } + } + + return operators(operator, leftCast, rightCast); + } + + private static class NodeAndTypes + { + private final IrPathNodeRef node; + private final Type leftType; + private final Type rightType; + + public NodeAndTypes(IrPathNodeRef node, Type leftType, Type rightType) + { + this.node = node; + this.leftType = leftType; + this.rightType = rightType; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NodeAndTypes that = (NodeAndTypes) o; + return Objects.equals(node, that.node) && + Objects.equals(leftType, that.leftType) && + Objects.equals(rightType, that.rightType); + } + + @Override + public int hashCode() + { + return Objects.hash(node, leftType, rightType); + } + } + + public static class ResolvedOperatorAndCoercions + { + public static final ResolvedOperatorAndCoercions RESOLUTION_ERROR = new ResolvedOperatorAndCoercions(null, Optional.empty(), Optional.empty()); + + private final ResolvedFunction operator; + private final Optional leftCoercion; + private final Optional rightCoercion; + + public static ResolvedOperatorAndCoercions operators(ResolvedFunction operator, Optional leftCoercion, Optional rightCoercion) + { + return new ResolvedOperatorAndCoercions(requireNonNull(operator, "operator is null"), leftCoercion, rightCoercion); + } + + private ResolvedOperatorAndCoercions(ResolvedFunction operator, Optional leftCoercion, Optional rightCoercion) + { + this.operator = operator; + this.leftCoercion = requireNonNull(leftCoercion, "leftCoercion is null"); + this.rightCoercion = requireNonNull(rightCoercion, "rightCoercion is null"); + } + + public ResolvedFunction getOperator() + { + checkState(this != RESOLUTION_ERROR, "accessing operator on RESOLUTION_ERROR"); + return operator; + } + + public Optional getLeftCoercion() + { + checkState(this != RESOLUTION_ERROR, "accessing coercion on RESOLUTION_ERROR"); + return leftCoercion; + } + + public Optional getRightCoercion() + { + checkState(this != RESOLUTION_ERROR, "accessing coercion on RESOLUTION_ERROR"); + return rightCoercion; + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/IrPathNodeRef.java b/core/trino-main/src/main/java/io/trino/json/IrPathNodeRef.java new file mode 100644 index 000000000000..9926b6f98874 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/IrPathNodeRef.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import io.trino.json.ir.IrPathNode; + +import static java.lang.String.format; +import static java.lang.System.identityHashCode; +import static java.util.Objects.requireNonNull; + +public final class IrPathNodeRef +{ + public static IrPathNodeRef of(T pathNode) + { + return new IrPathNodeRef<>(pathNode); + } + + private final T pathNode; + + private IrPathNodeRef(T pathNode) + { + this.pathNode = requireNonNull(pathNode, "pathNode is null"); + } + + public T getNode() + { + return pathNode; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IrPathNodeRef other = (IrPathNodeRef) o; + return pathNode == other.pathNode; + } + + @Override + public int hashCode() + { + return identityHashCode(pathNode); + } + + @Override + public String toString() + { + return format( + "@%s: %s", + Integer.toHexString(identityHashCode(pathNode)), + pathNode); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/JsonEmptySequenceNode.java b/core/trino-main/src/main/java/io/trino/json/JsonEmptySequenceNode.java new file mode 100644 index 000000000000..6af48d13a691 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/JsonEmptySequenceNode.java @@ -0,0 +1,169 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.node.JsonNodeType; + +import java.io.IOException; +import java.util.List; + +public class JsonEmptySequenceNode + extends JsonNode +{ + public static final JsonEmptySequenceNode EMPTY_SEQUENCE = new JsonEmptySequenceNode(); + + private JsonEmptySequenceNode() {} + + @Override + public T deepCopy() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonToken asToken() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonParser.NumberType numberType() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode get(int index) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode path(String fieldName) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode path(int index) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonParser traverse() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonParser traverse(ObjectCodec codec) + { + throw new UnsupportedOperationException(); + } + + @Override + protected JsonNode _at(JsonPointer ptr) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNodeType getNodeType() + { + throw new UnsupportedOperationException(); + } + + @Override + public String asText() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode findValue(String fieldName) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode findPath(String fieldName) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode findParent(String fieldName) + { + throw new UnsupportedOperationException(); + } + + @Override + public List findValues(String fieldName, List foundSoFar) + { + throw new UnsupportedOperationException(); + } + + @Override + public List findValuesAsText(String fieldName, List foundSoFar) + { + throw new UnsupportedOperationException(); + } + + @Override + public List findParents(String fieldName, List foundSoFar) + { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() + { + return "EMPTY_SEQUENCE"; + } + + @Override + public boolean equals(Object o) + { + return o == this; + } + + @Override + public int hashCode() + { + return getClass().hashCode(); + } + + @Override + public void serialize(JsonGenerator gen, SerializerProvider serializers) + throws IOException + { + throw new UnsupportedOperationException(); + } + + @Override + public void serializeWithType(JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) + throws IOException + { + throw new UnsupportedOperationException(); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/JsonInputErrorNode.java b/core/trino-main/src/main/java/io/trino/json/JsonInputErrorNode.java new file mode 100644 index 000000000000..bb6898c6c1c6 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/JsonInputErrorNode.java @@ -0,0 +1,169 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.node.JsonNodeType; + +import java.io.IOException; +import java.util.List; + +public class JsonInputErrorNode + extends JsonNode +{ + public static final JsonInputErrorNode JSON_ERROR = new JsonInputErrorNode(); + + private JsonInputErrorNode() {} + + @Override + public T deepCopy() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonToken asToken() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonParser.NumberType numberType() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode get(int index) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode path(String fieldName) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode path(int index) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonParser traverse() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonParser traverse(ObjectCodec codec) + { + throw new UnsupportedOperationException(); + } + + @Override + protected JsonNode _at(JsonPointer ptr) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNodeType getNodeType() + { + throw new UnsupportedOperationException(); + } + + @Override + public String asText() + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode findValue(String fieldName) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode findPath(String fieldName) + { + throw new UnsupportedOperationException(); + } + + @Override + public JsonNode findParent(String fieldName) + { + throw new UnsupportedOperationException(); + } + + @Override + public List findValues(String fieldName, List foundSoFar) + { + throw new UnsupportedOperationException(); + } + + @Override + public List findValuesAsText(String fieldName, List foundSoFar) + { + throw new UnsupportedOperationException(); + } + + @Override + public List findParents(String fieldName, List foundSoFar) + { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() + { + return "JSON_ERROR"; + } + + @Override + public boolean equals(Object o) + { + return o == this; + } + + @Override + public int hashCode() + { + return getClass().hashCode(); + } + + @Override + public void serialize(JsonGenerator gen, SerializerProvider serializers) + throws IOException + { + throw new UnsupportedOperationException(); + } + + @Override + public void serializeWithType(JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) + throws IOException + { + throw new UnsupportedOperationException(); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/JsonPathEvaluator.java b/core/trino-main/src/main/java/io/trino/json/JsonPathEvaluator.java new file mode 100644 index 000000000000..8b7d10554504 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/JsonPathEvaluator.java @@ -0,0 +1,78 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import com.fasterxml.jackson.databind.JsonNode; +import io.trino.json.ir.IrJsonPath; +import io.trino.metadata.FunctionManager; +import io.trino.metadata.Metadata; +import io.trino.metadata.ResolvedFunction; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.TypeManager; +import io.trino.sql.InterpretedFunctionInvoker; + +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * Evaluates the JSON path expression using given JSON input and parameters, + * respecting the path mode `strict` or `lax`. + * Successful evaluation results in a sequence of objects. + * Each object in the sequence is either a `JsonNode` or a `TypedValue` + * Certain error conditions might be suppressed in `lax` mode. + * Any unsuppressed error condition causes evaluation failure. + * In such case, `PathEvaluationError` is thrown. + */ +public class JsonPathEvaluator +{ + private final IrJsonPath path; + private final Invoker invoker; + private final CachingResolver resolver; + + public JsonPathEvaluator(IrJsonPath path, ConnectorSession session, Metadata metadata, TypeManager typeManager, FunctionManager functionManager) + { + requireNonNull(path, "path is null"); + requireNonNull(session, "session is null"); + requireNonNull(metadata, "metadata is null"); + requireNonNull(typeManager, "typeManager is null"); + requireNonNull(functionManager, "functionManager is null"); + + this.path = path; + this.invoker = new Invoker(session, functionManager); + this.resolver = new CachingResolver(metadata, session, typeManager); + } + + public List evaluate(JsonNode input, Object[] parameters) + { + return new PathEvaluationVisitor(path.isLax(), input, parameters, invoker, resolver).process(path.getRoot(), new PathEvaluationContext()); + } + + public static class Invoker + { + private final ConnectorSession connectorSession; + private final InterpretedFunctionInvoker functionInvoker; + + public Invoker(ConnectorSession connectorSession, FunctionManager functionManager) + { + this.connectorSession = connectorSession; + this.functionInvoker = new InterpretedFunctionInvoker(functionManager); + } + + public Object invoke(ResolvedFunction function, List arguments) + { + return functionInvoker.invoke(function, connectorSession, arguments); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/JsonPathInvocationContext.java b/core/trino-main/src/main/java/io/trino/json/JsonPathInvocationContext.java new file mode 100644 index 000000000000..1b57cc60d566 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/JsonPathInvocationContext.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +/** + * A class representing state, used by JSON-processing functions: JSON_EXISTS, JSON_VALUE, and JSON_QUERY. + * It is instantiated per driver, and allows gathering and reusing information across the processed rows. + * It contains a JsonPathEvaluator object, which caches ResolvedFunctions used by certain path nodes. + * Caching the ResolvedFunctions addresses the assumption that all or most rows shall provide values + * of the same types to certain JSON path operators. + */ +public class JsonPathInvocationContext +{ + private JsonPathEvaluator evaluator; + + public JsonPathEvaluator getEvaluator() + { + return evaluator; + } + + public void setEvaluator(JsonPathEvaluator evaluator) + { + this.evaluator = evaluator; + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/PathEvaluationContext.java b/core/trino-main/src/main/java/io/trino/json/PathEvaluationContext.java new file mode 100644 index 000000000000..d55e724a47e6 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/PathEvaluationContext.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import io.trino.json.ir.TypedValue; + +import static com.google.common.base.Preconditions.checkArgument; +import static io.trino.spi.type.IntegerType.INTEGER; +import static java.util.Objects.requireNonNull; + +public class PathEvaluationContext +{ + // last index of the innermost enclosing array (indexed from 0) + private final TypedValue last; + + // current item processed by the innermost enclosing filter + private final Object currentItem; + + public PathEvaluationContext() + { + this(new TypedValue(INTEGER, -1), null); + } + + private PathEvaluationContext(TypedValue last, Object currentItem) + { + this.last = last; + this.currentItem = currentItem; + } + + public PathEvaluationContext withLast(long last) + { + checkArgument(last >= 0, "last array index must not be negative"); + return new PathEvaluationContext(new TypedValue(INTEGER, last), currentItem); + } + + public PathEvaluationContext withCurrentItem(Object currentItem) + { + requireNonNull(currentItem, "currentItem is null"); + return new PathEvaluationContext(last, currentItem); + } + + public TypedValue getLast() + { + if (last.getLongValue() < 0) { + throw new PathEvaluationError("accessing the last array index with no enclosing array"); + } + return last; + } + + public Object getCurrentItem() + { + if (currentItem == null) { + throw new PathEvaluationError("accessing current filter item with no enclosing filter"); + } + return currentItem; + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/PathEvaluationError.java b/core/trino-main/src/main/java/io/trino/json/PathEvaluationError.java new file mode 100644 index 000000000000..4cf246f43958 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/PathEvaluationError.java @@ -0,0 +1,56 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import io.trino.spi.TrinoException; + +import static io.trino.spi.StandardErrorCode.PATH_EVALUATION_ERROR; +import static java.lang.String.format; + +public class PathEvaluationError + extends TrinoException +{ + public PathEvaluationError(String message) + { + super(PATH_EVALUATION_ERROR, "path evaluation failed: " + message); + } + + public PathEvaluationError(Throwable cause) + { + super(PATH_EVALUATION_ERROR, "path evaluation failed: ", cause); + } + + /** + * An exception resulting from a structural error during JSON path evaluation. + *

+ * A structural error occurs when the JSON path expression attempts to access a + * non-existent element of a JSON array or a non-existent member of a JSON object. + *

+ * Note: in `lax` mode, the structural errors are suppressed, and the erroneous + * subexpression is evaluated to an empty sequence. In `strict` mode, the structural + * errors are propagated to the enclosing function (i.e. the function within which + * the path is evaluated, e.g. `JSON_EXISTS`), and there they are handled accordingly + * to the chosen `ON ERROR` option. Non-structural errors (e.g. numeric exceptions) + * are not suppressed in `lax` or `strict` mode. + */ + public static TrinoException structuralError(String format, Object... arguments) + { + return new PathEvaluationError("structural error: " + format(format, arguments)); + } + + public static TrinoException itemTypeError(String expected, String actual) + { + return new PathEvaluationError(format("invalid item type. Expected: %s, actual: %s", expected, actual)); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/PathEvaluationUtil.java b/core/trino-main/src/main/java/io/trino/json/PathEvaluationUtil.java new file mode 100644 index 000000000000..3c89b879b862 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/PathEvaluationUtil.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.stream.Stream; + +import static com.fasterxml.jackson.databind.node.JsonNodeType.ARRAY; +import static com.google.common.collect.ImmutableList.toImmutableList; + +public class PathEvaluationUtil +{ + private PathEvaluationUtil() {} + + public static List unwrapArrays(List sequence) + { + return sequence.stream() + .flatMap(object -> { + if (object instanceof JsonNode && ((JsonNode) object).getNodeType() == ARRAY) { + return ImmutableList.copyOf(((JsonNode) object).elements()).stream(); + } + return Stream.of(object); + }) + .collect(toImmutableList()); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/PathEvaluationVisitor.java b/core/trino-main/src/main/java/io/trino/json/PathEvaluationVisitor.java new file mode 100644 index 000000000000..f197211eab0c --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/PathEvaluationVisitor.java @@ -0,0 +1,1037 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Range; +import io.airlift.slice.Slice; +import io.trino.json.CachingResolver.ResolvedOperatorAndCoercions; +import io.trino.json.JsonPathEvaluator.Invoker; +import io.trino.json.ir.IrAbsMethod; +import io.trino.json.ir.IrArithmeticBinary; +import io.trino.json.ir.IrArithmeticUnary; +import io.trino.json.ir.IrArrayAccessor; +import io.trino.json.ir.IrCeilingMethod; +import io.trino.json.ir.IrConstantJsonSequence; +import io.trino.json.ir.IrContextVariable; +import io.trino.json.ir.IrDatetimeMethod; +import io.trino.json.ir.IrDoubleMethod; +import io.trino.json.ir.IrFilter; +import io.trino.json.ir.IrFloorMethod; +import io.trino.json.ir.IrJsonNull; +import io.trino.json.ir.IrJsonPathVisitor; +import io.trino.json.ir.IrKeyValueMethod; +import io.trino.json.ir.IrLastIndexVariable; +import io.trino.json.ir.IrLiteral; +import io.trino.json.ir.IrMemberAccessor; +import io.trino.json.ir.IrNamedJsonVariable; +import io.trino.json.ir.IrNamedValueVariable; +import io.trino.json.ir.IrPathNode; +import io.trino.json.ir.IrPredicateCurrentItemVariable; +import io.trino.json.ir.IrSizeMethod; +import io.trino.json.ir.IrTypeMethod; +import io.trino.json.ir.SqlJsonLiteralConverter; +import io.trino.json.ir.TypedValue; +import io.trino.spi.function.OperatorType; +import io.trino.spi.type.CharType; +import io.trino.spi.type.DecimalConversions; +import io.trino.spi.type.DecimalType; +import io.trino.spi.type.Int128; +import io.trino.spi.type.Int128Math; +import io.trino.spi.type.TimeType; +import io.trino.spi.type.TimeWithTimeZoneType; +import io.trino.spi.type.TimestampType; +import io.trino.spi.type.TimestampWithTimeZoneType; +import io.trino.spi.type.Type; +import io.trino.spi.type.VarcharType; +import io.trino.type.BigintOperators; +import io.trino.type.DecimalCasts; +import io.trino.type.DecimalOperators; +import io.trino.type.DoubleOperators; +import io.trino.type.IntegerOperators; +import io.trino.type.RealOperators; +import io.trino.type.SmallintOperators; +import io.trino.type.TinyintOperators; +import io.trino.type.VarcharOperators; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.airlift.slice.Slices.utf8Slice; +import static io.trino.json.CachingResolver.ResolvedOperatorAndCoercions.RESOLUTION_ERROR; +import static io.trino.json.JsonEmptySequenceNode.EMPTY_SEQUENCE; +import static io.trino.json.PathEvaluationError.itemTypeError; +import static io.trino.json.PathEvaluationError.structuralError; +import static io.trino.json.PathEvaluationUtil.unwrapArrays; +import static io.trino.json.ir.IrArithmeticUnary.Sign.PLUS; +import static io.trino.json.ir.SqlJsonLiteralConverter.getTextTypedValue; +import static io.trino.operator.scalar.MathFunctions.Ceiling.ceilingLong; +import static io.trino.operator.scalar.MathFunctions.Ceiling.ceilingLongShort; +import static io.trino.operator.scalar.MathFunctions.Ceiling.ceilingShort; +import static io.trino.operator.scalar.MathFunctions.Floor.floorLong; +import static io.trino.operator.scalar.MathFunctions.Floor.floorLongShort; +import static io.trino.operator.scalar.MathFunctions.Floor.floorShort; +import static io.trino.operator.scalar.MathFunctions.abs; +import static io.trino.operator.scalar.MathFunctions.absInteger; +import static io.trino.operator.scalar.MathFunctions.absSmallint; +import static io.trino.operator.scalar.MathFunctions.absTinyint; +import static io.trino.operator.scalar.MathFunctions.ceilingFloat; +import static io.trino.operator.scalar.MathFunctions.floorFloat; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.DateType.DATE; +import static io.trino.spi.type.Decimals.longTenToNth; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.RealType.REAL; +import static io.trino.spi.type.SmallintType.SMALLINT; +import static io.trino.spi.type.TinyintType.TINYINT; +import static io.trino.type.DecimalCasts.longDecimalToDouble; +import static io.trino.type.DecimalCasts.shortDecimalToDouble; +import static java.lang.Float.floatToRawIntBits; +import static java.lang.Float.intBitsToFloat; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +class PathEvaluationVisitor + extends IrJsonPathVisitor, PathEvaluationContext> +{ + private final boolean lax; + private final JsonNode input; + private final Object[] parameters; + private final PathPredicateEvaluationVisitor predicateVisitor; + private final Invoker invoker; + private final CachingResolver resolver; + private int objectId; + + public PathEvaluationVisitor(boolean lax, JsonNode input, Object[] parameters, Invoker invoker, CachingResolver resolver) + { + this.lax = lax; + this.input = requireNonNull(input, "input is null"); + this.parameters = requireNonNull(parameters, "parameters is null"); + this.invoker = requireNonNull(invoker, "invoker is null"); + this.resolver = requireNonNull(resolver, "resolver is null"); + this.predicateVisitor = new PathPredicateEvaluationVisitor(lax, this, invoker, resolver); + } + + @Override + protected List visitIrPathNode(IrPathNode node, PathEvaluationContext context) + { + throw new UnsupportedOperationException("JSON path evaluating visitor not implemented for " + node.getClass().getSimpleName()); + } + + @Override + protected List visitIrAbsMethod(IrAbsMethod node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + if (lax) { + sequence = unwrapArrays(sequence); + } + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + TypedValue value; + if (object instanceof JsonNode) { + value = getNumericTypedValue((JsonNode) object) + .orElseThrow(() -> itemTypeError("NUMBER", ((JsonNode) object).getNodeType().name())); + } + else { + value = (TypedValue) object; + } + outputSequence.add(getAbsoluteValue(value)); + } + + return outputSequence.build(); + } + + private static TypedValue getAbsoluteValue(TypedValue typedValue) + { + Type type = typedValue.getType(); + + if (type.equals(BIGINT)) { + long value = typedValue.getLongValue(); + if (value >= 0) { + return typedValue; + } + long absValue; + try { + absValue = abs(value); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, absValue); + } + if (type.equals(INTEGER)) { + long value = typedValue.getLongValue(); + if (value >= 0) { + return typedValue; + } + long absValue; + try { + absValue = absInteger(value); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, absValue); + } + if (type.equals(SMALLINT)) { + long value = typedValue.getLongValue(); + if (value >= 0) { + return typedValue; + } + long absValue; + try { + absValue = absSmallint(value); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, absValue); + } + if (type.equals(TINYINT)) { + long value = typedValue.getLongValue(); + if (value >= 0) { + return typedValue; + } + long absValue; + try { + absValue = absTinyint(value); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, absValue); + } + if (type.equals(DOUBLE)) { + double value = typedValue.getDoubleValue(); + if (value >= 0) { + return typedValue; + } + return new TypedValue(type, abs(value)); + } + if (type.equals(REAL)) { + float value = intBitsToFloat((int) typedValue.getLongValue()); + if (value > 0) { + return typedValue; + } + return new TypedValue(type, floatToRawIntBits(Math.abs(value))); + } + if (type instanceof DecimalType) { + if (((DecimalType) type).isShort()) { + long value = typedValue.getLongValue(); + if (value > 0) { + return typedValue; + } + return new TypedValue(type, -value); + } + Int128 value = (Int128) typedValue.getObjectValue(); + if (value.isNegative()) { + Int128 result; + try { + result = DecimalOperators.Negation.negate((Int128) typedValue.getObjectValue()); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, result); + } + return typedValue; + } + + throw itemTypeError("NUMBER", type.getDisplayName()); + } + + @Override + protected List visitIrArithmeticBinary(IrArithmeticBinary node, PathEvaluationContext context) + { + List leftSequence = process(node.getLeft(), context); + List rightSequence = process(node.getRight(), context); + + if (lax) { + leftSequence = unwrapArrays(leftSequence); + rightSequence = unwrapArrays(rightSequence); + } + + if (leftSequence.size() != 1 || rightSequence.size() != 1) { + throw new PathEvaluationError("arithmetic binary expression requires singleton operands"); + } + + TypedValue left; + Object leftObject = getOnlyElement(leftSequence); + if (leftObject instanceof JsonNode) { + left = getNumericTypedValue((JsonNode) leftObject) + .orElseThrow(() -> itemTypeError("NUMBER", ((JsonNode) leftObject).getNodeType().name())); + } + else { + left = (TypedValue) leftObject; + } + + TypedValue right; + Object rightObject = getOnlyElement(rightSequence); + if (rightObject instanceof JsonNode) { + right = getNumericTypedValue((JsonNode) rightObject) + .orElseThrow(() -> itemTypeError("NUMBER", ((JsonNode) rightObject).getNodeType().name())); + } + else { + right = (TypedValue) rightObject; + } + + ResolvedOperatorAndCoercions operators = resolver.getOperators(node, OperatorType.valueOf(node.getOperator().name()), left.getType(), right.getType()); + if (operators == RESOLUTION_ERROR) { + throw new PathEvaluationError(format("invalid operand types to %s operator (%s, %s)", node.getOperator().name(), left.getType(), right.getType())); + } + + Object leftInput = left.getValueAsObject(); + if (operators.getLeftCoercion().isPresent()) { + try { + leftInput = invoker.invoke(operators.getLeftCoercion().get(), ImmutableList.of(leftInput)); + } + catch (RuntimeException e) { + throw new PathEvaluationError(e); + } + } + + Object rightInput = right.getValueAsObject(); + if (operators.getRightCoercion().isPresent()) { + try { + rightInput = invoker.invoke(operators.getRightCoercion().get(), ImmutableList.of(rightInput)); + } + catch (RuntimeException e) { + throw new PathEvaluationError(e); + } + } + + Object result; + try { + result = invoker.invoke(operators.getOperator(), ImmutableList.of(leftInput, rightInput)); + } + catch (RuntimeException e) { + throw new PathEvaluationError(e); + } + + return ImmutableList.of(TypedValue.fromValueAsObject(operators.getOperator().getSignature().getReturnType(), result)); + } + + @Override + protected List visitIrArithmeticUnary(IrArithmeticUnary node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + if (lax) { + sequence = unwrapArrays(sequence); + } + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + TypedValue value; + if (object instanceof JsonNode) { + value = getNumericTypedValue((JsonNode) object) + .orElseThrow(() -> itemTypeError("NUMBER", ((JsonNode) object).getNodeType().name())); + } + else { + value = (TypedValue) object; + Type type = value.getType(); + if (!type.equals(BIGINT) && !type.equals(INTEGER) && !type.equals(SMALLINT) && !type.equals(TINYINT) && !type.equals(DOUBLE) && !type.equals(REAL) && !(type instanceof DecimalType)) { + throw itemTypeError("NUMBER", type.getDisplayName()); + } + } + if (node.getSign() == PLUS) { + outputSequence.add(value); + } + else { + outputSequence.add(negate(value)); + } + } + + return outputSequence.build(); + } + + private static TypedValue negate(TypedValue typedValue) + { + Type type = typedValue.getType(); + + if (type.equals(BIGINT)) { + long negatedValue; + try { + negatedValue = BigintOperators.negate(typedValue.getLongValue()); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, negatedValue); + } + if (type.equals(INTEGER)) { + long negatedValue; + try { + negatedValue = IntegerOperators.negate(typedValue.getLongValue()); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, negatedValue); + } + if (type.equals(SMALLINT)) { + long negatedValue; + try { + negatedValue = SmallintOperators.negate(typedValue.getLongValue()); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, negatedValue); + } + if (type.equals(TINYINT)) { + long negatedValue; + try { + negatedValue = TinyintOperators.negate(typedValue.getLongValue()); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, negatedValue); + } + if (type.equals(DOUBLE)) { + return new TypedValue(type, -typedValue.getDoubleValue()); + } + if (type.equals(REAL)) { + return new TypedValue(type, RealOperators.negate(typedValue.getLongValue())); + } + if (type instanceof DecimalType) { + if (((DecimalType) type).isShort()) { + return new TypedValue(type, -typedValue.getLongValue()); + } + Int128 negatedValue; + try { + negatedValue = DecimalOperators.Negation.negate((Int128) typedValue.getObjectValue()); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + return new TypedValue(type, negatedValue); + } + + throw new IllegalStateException("unexpected type" + type.getDisplayName()); + } + + @Override + protected List visitIrArrayAccessor(IrArrayAccessor node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + List elements; + if (object instanceof JsonNode) { + if (((JsonNode) object).isArray()) { + elements = ImmutableList.copyOf(((JsonNode) object).elements()); + } + else if (lax) { + elements = ImmutableList.of((object)); + } + else { + throw itemTypeError("ARRAY", ((JsonNode) object).getNodeType().name()); + } + } + else if (lax) { + elements = ImmutableList.of((object)); + } + else { + throw itemTypeError("ARRAY", ((TypedValue) object).getType().getDisplayName()); + } + + // handle wildcard accessor + if (node.getSubscripts().isEmpty()) { + outputSequence.addAll(elements); + continue; + } + + if (elements.isEmpty()) { + if (!lax) { + throw structuralError("invalid array subscript for empty array"); + } + // for lax mode, the result is empty sequence + continue; + } + + PathEvaluationContext arrayContext = context.withLast(elements.size() - 1); + for (IrArrayAccessor.Subscript subscript : node.getSubscripts()) { + List from = process(subscript.getFrom(), arrayContext); + Optional> to = subscript.getTo().map(path -> process(path, arrayContext)); + if (from.size() != 1) { + throw new PathEvaluationError("array subscript 'from' value must be singleton numeric"); + } + if (to.isPresent() && to.get().size() != 1) { + throw new PathEvaluationError("array subscript 'to' value must be singleton numeric"); + } + long fromIndex = asArrayIndex(getOnlyElement(from)); + long toIndex = to + .map(Iterables::getOnlyElement) + .map(PathEvaluationVisitor::asArrayIndex) + .orElse(fromIndex); + + if (!lax && (fromIndex < 0 || fromIndex >= elements.size() || toIndex < 0 || toIndex >= elements.size() || fromIndex > toIndex)) { + throw structuralError("invalid array subscript: [%s, %s] for array of size %s", fromIndex, toIndex, elements.size()); + } + + if (fromIndex <= toIndex) { + Range allElementsRange = Range.closed(0L, (long) elements.size() - 1); + Range subscriptRange = Range.closed(fromIndex, toIndex); + if (subscriptRange.isConnected(allElementsRange)) { // cannot intersect ranges which are not connected... + Range resultRange = subscriptRange.intersection(allElementsRange); + if (!resultRange.isEmpty()) { + for (long i = resultRange.lowerEndpoint(); i <= resultRange.upperEndpoint(); i++) { + outputSequence.add(elements.get((int) i)); + } + } + } + } + } + } + + return outputSequence.build(); + } + + private static long asArrayIndex(Object object) + { + if (object instanceof JsonNode) { + JsonNode jsonNode = (JsonNode) object; + if (jsonNode.getNodeType() != JsonNodeType.NUMBER) { + throw itemTypeError("NUMBER", (jsonNode.getNodeType().name())); + } + if (!jsonNode.canConvertToLong()) { + throw new PathEvaluationError(format("cannot convert value %s to long", jsonNode)); + } + return jsonNode.longValue(); + } + else { + TypedValue value = (TypedValue) object; + Type type = value.getType(); + if (type.equals(BIGINT) || type.equals(INTEGER) || type.equals(SMALLINT) || type.equals(TINYINT)) { + return value.getLongValue(); + } + if (type.equals(DOUBLE)) { + try { + return DoubleOperators.castToLong(value.getDoubleValue()); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + } + if (type.equals(REAL)) { + try { + return RealOperators.castToLong(value.getLongValue()); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + } + if (type instanceof DecimalType) { + DecimalType decimalType = (DecimalType) type; + int precision = decimalType.getPrecision(); + int scale = decimalType.getScale(); + if (((DecimalType) type).isShort()) { + long tenToScale = longTenToNth(DecimalConversions.intScale(scale)); + return DecimalCasts.shortDecimalToBigint(value.getLongValue(), precision, scale, tenToScale); + } + Int128 tenToScale = Int128Math.powerOfTen(DecimalConversions.intScale(scale)); + try { + return DecimalCasts.longDecimalToBigint((Int128) value.getObjectValue(), precision, scale, tenToScale); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + } + + throw itemTypeError("NUMBER", type.getDisplayName()); + } + } + + @Override + protected List visitIrCeilingMethod(IrCeilingMethod node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + if (lax) { + sequence = unwrapArrays(sequence); + } + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + TypedValue value; + if (object instanceof JsonNode) { + value = getNumericTypedValue((JsonNode) object) + .orElseThrow(() -> itemTypeError("NUMBER", ((JsonNode) object).getNodeType().name())); + } + else { + value = (TypedValue) object; + } + outputSequence.add(getCeiling(value)); + } + + return outputSequence.build(); + } + + private static TypedValue getCeiling(TypedValue typedValue) + { + Type type = typedValue.getType(); + + if (type.equals(BIGINT) || type.equals(INTEGER) || type.equals(SMALLINT) || type.equals(TINYINT)) { + return typedValue; + } + if (type.equals(DOUBLE)) { + return new TypedValue(type, Math.ceil(typedValue.getDoubleValue())); + } + if (type.equals(REAL)) { + return new TypedValue(type, ceilingFloat(typedValue.getLongValue())); + } + if (type instanceof DecimalType) { + DecimalType decimalType = (DecimalType) type; + int scale = decimalType.getScale(); + DecimalType resultType = DecimalType.createDecimalType(decimalType.getPrecision() - scale + Math.min(scale, 1), 0); + if (decimalType.isShort()) { + return new TypedValue(resultType, ceilingShort(scale, typedValue.getLongValue())); + } + if (resultType.isShort()) { + try { + return new TypedValue(resultType, ceilingLongShort(scale, (Int128) typedValue.getObjectValue())); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + } + try { + return new TypedValue(resultType, ceilingLong(scale, (Int128) typedValue.getObjectValue())); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + } + + throw itemTypeError("NUMBER", type.getDisplayName()); + } + + @Override + protected List visitIrConstantJsonSequence(IrConstantJsonSequence node, PathEvaluationContext context) + { + return ImmutableList.copyOf(node.getSequence()); + } + + @Override + protected List visitIrContextVariable(IrContextVariable node, PathEvaluationContext context) + { + return ImmutableList.of(input); + } + + @Override + protected List visitIrDatetimeMethod(IrDatetimeMethod node, PathEvaluationContext context) // TODO + { + throw new UnsupportedOperationException("date method is not yet supported"); + } + + @Override + protected List visitIrDoubleMethod(IrDoubleMethod node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + if (lax) { + sequence = unwrapArrays(sequence); + } + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + TypedValue value; + if (object instanceof JsonNode) { + value = getNumericTypedValue((JsonNode) object) + .orElseGet(() -> getTextTypedValue((JsonNode) object) + .orElseThrow(() -> itemTypeError("NUMBER or TEXT", ((JsonNode) object).getNodeType().name()))); + } + else { + value = (TypedValue) object; + } + outputSequence.add(getDouble(value)); + } + + return outputSequence.build(); + } + + private static TypedValue getDouble(TypedValue typedValue) + { + Type type = typedValue.getType(); + + if (type.equals(BIGINT) || type.equals(INTEGER) || type.equals(SMALLINT) || type.equals(TINYINT)) { + return new TypedValue(DOUBLE, (double) typedValue.getLongValue()); + } + if (type.equals(DOUBLE)) { + return typedValue; + } + if (type.equals(REAL)) { + return new TypedValue(DOUBLE, RealOperators.castToDouble(typedValue.getLongValue())); + } + if (type instanceof DecimalType) { + DecimalType decimalType = (DecimalType) type; + int precision = decimalType.getPrecision(); + int scale = decimalType.getScale(); + if (((DecimalType) type).isShort()) { + long tenToScale = longTenToNth(DecimalConversions.intScale(scale)); + return new TypedValue(DOUBLE, shortDecimalToDouble(typedValue.getLongValue(), precision, scale, tenToScale)); + } + Int128 tenToScale = Int128Math.powerOfTen(DecimalConversions.intScale(scale)); + return new TypedValue(DOUBLE, longDecimalToDouble((Int128) typedValue.getObjectValue(), precision, scale, tenToScale)); + } + if (type instanceof VarcharType || type instanceof CharType) { + try { + return new TypedValue(DOUBLE, VarcharOperators.castToDouble((Slice) typedValue.getObjectValue())); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + } + + throw itemTypeError("NUMBER or TEXT", type.getDisplayName()); + } + + @Override + protected List visitIrFilter(IrFilter node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + if (lax) { + sequence = unwrapArrays(sequence); + } + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + PathEvaluationContext currentItemContext = context.withCurrentItem(object); + Boolean result = predicateVisitor.process(node.getPredicate(), currentItemContext); + if (Boolean.TRUE.equals(result)) { + outputSequence.add(object); + } + } + + return outputSequence.build(); + } + + @Override + protected List visitIrFloorMethod(IrFloorMethod node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + if (lax) { + sequence = unwrapArrays(sequence); + } + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + TypedValue value; + if (object instanceof JsonNode) { + value = getNumericTypedValue((JsonNode) object) + .orElseThrow(() -> itemTypeError("NUMBER", ((JsonNode) object).getNodeType().name())); + } + else { + value = (TypedValue) object; + } + outputSequence.add(getFloor(value)); + } + + return outputSequence.build(); + } + + private static TypedValue getFloor(TypedValue typedValue) + { + Type type = typedValue.getType(); + + if (type.equals(BIGINT) || type.equals(INTEGER) || type.equals(SMALLINT) || type.equals(TINYINT)) { + return typedValue; + } + if (type.equals(DOUBLE)) { + return new TypedValue(type, Math.floor(typedValue.getDoubleValue())); + } + if (type.equals(REAL)) { + return new TypedValue(type, floorFloat(typedValue.getLongValue())); + } + if (type instanceof DecimalType) { + DecimalType decimalType = (DecimalType) type; + int scale = decimalType.getScale(); + DecimalType resultType = DecimalType.createDecimalType(decimalType.getPrecision() - scale + Math.min(scale, 1), 0); + if (((DecimalType) type).isShort()) { + return new TypedValue(resultType, floorShort(scale, typedValue.getLongValue())); + } + if (resultType.isShort()) { + try { + return new TypedValue(resultType, floorLongShort(scale, (Int128) typedValue.getObjectValue())); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + } + try { + return new TypedValue(resultType, floorLong(scale, (Int128) typedValue.getObjectValue())); + } + catch (Exception e) { + throw new PathEvaluationError(e); + } + } + + throw itemTypeError("NUMBER", type.getDisplayName()); + } + + @Override + protected List visitIrJsonNull(IrJsonNull node, PathEvaluationContext context) + { + return ImmutableList.of(NullNode.getInstance()); + } + + @Override + protected List visitIrKeyValueMethod(IrKeyValueMethod node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + if (lax) { + sequence = unwrapArrays(sequence); + } + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + if (!(object instanceof JsonNode)) { + throw itemTypeError("OBJECT", ((TypedValue) object).getType().getDisplayName()); + } + if (!((JsonNode) object).isObject()) { + throw itemTypeError("OBJECT", ((JsonNode) object).getNodeType().name()); + } + + // non-unique keys are not supported. if they were, we should follow the spec here on handling them. + // see the comment in `visitIrMemberAccessor` method. + ((JsonNode) object).fields().forEachRemaining( + field -> outputSequence.add(new ObjectNode( + JsonNodeFactory.instance, + ImmutableMap.of( + "name", TextNode.valueOf(field.getKey()), + "value", field.getValue(), + "id", IntNode.valueOf(objectId++))))); + } + + return outputSequence.build(); + } + + @Override + protected List visitIrLastIndexVariable(IrLastIndexVariable node, PathEvaluationContext context) + { + return ImmutableList.of(context.getLast()); + } + + @Override + protected List visitIrLiteral(IrLiteral node, PathEvaluationContext context) + { + return ImmutableList.of(TypedValue.fromValueAsObject(node.getType().orElseThrow(), node.getValue())); + } + + @Override + protected List visitIrMemberAccessor(IrMemberAccessor node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + if (lax) { + sequence = unwrapArrays(sequence); + } + + // due to the choice of JsonNode as JSON representation, there cannot be duplicate keys in a JSON object. + // according to the spec, it is implementation-dependent whether non-unique keys are allowed. + // in case when there are duplicate keys, the spec describes the way of handling them both + // by the wildcard member accessor and by the 'by-key' member accessor. + // this method needs to be revisited when switching to another JSON representation. + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + if (!lax) { + if (!(object instanceof JsonNode)) { + throw itemTypeError("OBJECT", ((TypedValue) object).getType().getDisplayName()); + } + if (!((JsonNode) object).isObject()) { + throw itemTypeError("OBJECT", ((JsonNode) object).getNodeType().name()); + } + } + + if (object instanceof JsonNode && ((JsonNode) object).isObject()) { + JsonNode jsonObject = (JsonNode) object; + // handle wildcard member accessor + if (node.getKey().isEmpty()) { + outputSequence.addAll(jsonObject.elements()); + } + else { + JsonNode boundValue = jsonObject.get(node.getKey().get()); + if (boundValue == null) { + if (!lax) { + throw structuralError("missing member '%s' in JSON object", node.getKey().get()); + } + } + else { + outputSequence.add(boundValue); + } + } + } + } + + return outputSequence.build(); + } + + @Override + protected List visitIrNamedJsonVariable(IrNamedJsonVariable node, PathEvaluationContext context) + { + Object value = parameters[node.getIndex()]; + checkState(value != null, "missing value for parameter"); + checkState(value instanceof JsonNode, "expected JSON, got SQL value"); + + if (value.equals(EMPTY_SEQUENCE)) { + return ImmutableList.of(); + } + return ImmutableList.of(value); + } + + @Override + protected List visitIrNamedValueVariable(IrNamedValueVariable node, PathEvaluationContext context) + { + Object value = parameters[node.getIndex()]; + checkState(value != null, "missing value for parameter"); + checkState(value instanceof TypedValue || value instanceof NullNode, "expected SQL value or JSON null, got non-null JSON"); + + return ImmutableList.of(value); + } + + @Override + protected List visitIrPredicateCurrentItemVariable(IrPredicateCurrentItemVariable node, PathEvaluationContext context) + { + return ImmutableList.of(context.getCurrentItem()); + } + + @Override + protected List visitIrSizeMethod(IrSizeMethod node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + ImmutableList.Builder outputSequence = ImmutableList.builder(); + for (Object object : sequence) { + if (object instanceof JsonNode && ((JsonNode) object).isArray()) { + outputSequence.add(new TypedValue(INTEGER, ((JsonNode) object).size())); + } + else { + if (lax) { + outputSequence.add(new TypedValue(INTEGER, 1)); + } + else { + String type; + if (object instanceof JsonNode) { + type = ((JsonNode) object).getNodeType().name(); + } + else { + type = ((TypedValue) object).getType().getDisplayName(); + } + throw itemTypeError("ARRAY", type); + } + } + } + + return outputSequence.build(); + } + + @Override + protected List visitIrTypeMethod(IrTypeMethod node, PathEvaluationContext context) + { + List sequence = process(node.getBase(), context); + + Type resultType = node.getType().orElseThrow(); + ImmutableList.Builder outputSequence = ImmutableList.builder(); + + // In case when a new type is supported in JSON path, it might be necessary to update the + // constant JsonPathAnalyzer.TYPE_METHOD_RESULT_TYPE, which determines the resultType. + // Today it is only enough to fit the longest of the result strings below. + for (Object object : sequence) { + if (object instanceof JsonNode) { + switch (((JsonNode) object).getNodeType()) { + case NUMBER: + outputSequence.add(new TypedValue(resultType, utf8Slice("number"))); + break; + case STRING: + outputSequence.add(new TypedValue(resultType, utf8Slice("string"))); + break; + case BOOLEAN: + outputSequence.add(new TypedValue(resultType, utf8Slice("boolean"))); + break; + case ARRAY: + outputSequence.add(new TypedValue(resultType, utf8Slice("array"))); + break; + case OBJECT: + outputSequence.add(new TypedValue(resultType, utf8Slice("object"))); + break; + case NULL: + outputSequence.add(new TypedValue(resultType, utf8Slice("null"))); + break; + default: + throw new IllegalArgumentException("unexpected Json node type: " + ((JsonNode) object).getNodeType()); + } + } + else { + Type type = ((TypedValue) object).getType(); + if (type.equals(BIGINT) || type.equals(INTEGER) || type.equals(SMALLINT) || type.equals(TINYINT) || type.equals(DOUBLE) || type.equals(REAL) || type instanceof DecimalType) { + outputSequence.add(new TypedValue(resultType, utf8Slice("number"))); + } + else if (type instanceof VarcharType || type instanceof CharType) { + outputSequence.add(new TypedValue(resultType, utf8Slice("string"))); + } + else if (type.equals(BOOLEAN)) { + outputSequence.add(new TypedValue(resultType, utf8Slice("boolean"))); + } + else if (type.equals(DATE)) { + outputSequence.add(new TypedValue(resultType, utf8Slice("date"))); + } + else if (type instanceof TimeType) { + outputSequence.add(new TypedValue(resultType, utf8Slice("time without time zone"))); + } + else if (type instanceof TimeWithTimeZoneType) { + outputSequence.add(new TypedValue(resultType, utf8Slice("time with time zone"))); + } + else if (type instanceof TimestampType) { + outputSequence.add(new TypedValue(resultType, utf8Slice("timestamp without time zone"))); + } + else if (type instanceof TimestampWithTimeZoneType) { + outputSequence.add(new TypedValue(resultType, utf8Slice("timestamp with time zone"))); + } + } + } + + return outputSequence.build(); + } + + private static Optional getNumericTypedValue(JsonNode jsonNode) + { + try { + return SqlJsonLiteralConverter.getNumericTypedValue(jsonNode); + } + catch (SqlJsonLiteralConverter.JsonLiteralConversionError e) { + throw new PathEvaluationError(e); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/PathPredicateEvaluationVisitor.java b/core/trino-main/src/main/java/io/trino/json/PathPredicateEvaluationVisitor.java new file mode 100644 index 000000000000..16c1b597b921 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/PathPredicateEvaluationVisitor.java @@ -0,0 +1,488 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; +import io.trino.json.CachingResolver.ResolvedOperatorAndCoercions; +import io.trino.json.JsonPathEvaluator.Invoker; +import io.trino.json.ir.IrComparisonPredicate; +import io.trino.json.ir.IrConjunctionPredicate; +import io.trino.json.ir.IrDisjunctionPredicate; +import io.trino.json.ir.IrExistsPredicate; +import io.trino.json.ir.IrIsUnknownPredicate; +import io.trino.json.ir.IrJsonPathVisitor; +import io.trino.json.ir.IrNegationPredicate; +import io.trino.json.ir.IrPathNode; +import io.trino.json.ir.IrPredicate; +import io.trino.json.ir.IrStartsWithPredicate; +import io.trino.json.ir.SqlJsonLiteralConverter; +import io.trino.json.ir.TypedValue; +import io.trino.operator.scalar.StringFunctions; +import io.trino.spi.function.OperatorType; +import io.trino.spi.type.CharType; +import io.trino.spi.type.Type; +import io.trino.sql.tree.ComparisonExpression; + +import java.util.List; +import java.util.Optional; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.trino.json.CachingResolver.ResolvedOperatorAndCoercions.RESOLUTION_ERROR; +import static io.trino.json.PathEvaluationUtil.unwrapArrays; +import static io.trino.json.ir.IrComparisonPredicate.Operator.EQUAL; +import static io.trino.json.ir.IrComparisonPredicate.Operator.NOT_EQUAL; +import static io.trino.json.ir.SqlJsonLiteralConverter.getTextTypedValue; +import static io.trino.json.ir.SqlJsonLiteralConverter.getTypedValue; +import static io.trino.spi.type.Chars.padSpaces; +import static io.trino.sql.analyzer.ExpressionAnalyzer.isCharacterStringType; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static java.util.Objects.requireNonNull; + +/** + * This visitor evaluates the JSON path predicate in JSON filter expression. + * The returned value is true, false or unknown. + *

+ * Filter predicate never throws error, both in lax or strict mode, even if evaluation + * of the nested JSON path fails or the predicate itself cannot be successfully evaluated + * (e.g. because it tries to compare incompatible types). + *

+ * NOTE Even though errors are suppressed both in lax and strict mode, the mode affects + * the predicate result. + * For example, let `$array` be a JSON array of size 3: `["a", "b", "c"]`, + * and let predicate be `exists($array[5])` + * The nested accessor expression `$array[5]` is a structural error (array index out of bounds). + * In lax mode, the error is suppressed, and `$array[5]` results in an empty sequence. + * Hence, the `exists` predicate returns `false`. + * In strict mode, the error is not suppressed, and the `exists` predicate returns `unknown`. + *

+ * NOTE on the semantics of comparison: + * The following comparison operators are supported in JSON path predicate: EQUAL, NOT EQUAL, LESS THAN, GREATER THAN, LESS THAN OR EQUAL, GREATER THAN OR EQUAL. + * Both operands are JSON paths, and so they are evaluated to sequences of objects. + *

+ * Technically, each of the objects is either a JsonNode, or a TypedValue. + * Logically, they can be divided into three categories: + * 1. scalar values. These are all the TypedValues and certain subtypes of JsonNode, e.g. IntNode, BooleanNode,... + * 2. non-scalars. These are JSON arrays and objects + * 3. NULL values. They are represented by JsonNode subtype NullNode. + *

+ * When comparing two objects, the following rules apply: + * 1. NULL can be successfully compared with any object. NULL equals NULL, and is neither equal, less than or greater than any other object. + * 2. non-scalars can be only compared with a NULL (the result being false). Comparing a non-scalar with any other object (including itself) results in error. + * 3. scalars can be compared with a NULL (the result being false). They can be also compared with other scalars, provided that the types of the + * compared scalars are eligible for comparison. Otherwise, comparing two scalars results in error. + *

+ * As mentioned before, the operands to comparison predicate produce sequences of objects. + * Comparing the sequences requires comparing every pair of objects from the first and the second sequence. + * The overall result of the comparison predicate depends on two factors: + * - if any comparison resulted in error, + * - if any comparison returned true. + * In strict mode, any error makes the overall result unknown. + * In lax mode, the SQL specification allows to either ignore errors, or return unknown in case of error. + * Our implementation choice is to finish the predicate evaluation as early as possible, that is, + * to return unknown on the first error or return true on the first comparison returning true. + * The result is deterministic, because the input sequences are processed in order. + * In case of no errors, the comparison predicate result is whether any comparison returned true. + *

+ * NOTE The starts with predicate, similarly to the comparison predicate, is applied to sequences of input items. + * It applies the same policy of translating errors into unknown result, and the same policy of returning true + * on the first success. + */ +class PathPredicateEvaluationVisitor + extends IrJsonPathVisitor +{ + private final boolean lax; + private final PathEvaluationVisitor pathVisitor; + private final Invoker invoker; + private final CachingResolver resolver; + + public PathPredicateEvaluationVisitor(boolean lax, PathEvaluationVisitor pathVisitor, Invoker invoker, CachingResolver resolver) + { + this.lax = lax; + this.pathVisitor = requireNonNull(pathVisitor, "pathVisitor is null"); + this.invoker = requireNonNull(invoker, "invoker is null"); + this.resolver = requireNonNull(resolver, "resolver is null"); + } + + @Override + protected Boolean visitIrPathNode(IrPathNode node, PathEvaluationContext context) + { + throw new IllegalStateException("JSON predicate evaluating visitor applied to a non-predicate node " + node.getClass().getSimpleName()); + } + + @Override + protected Boolean visitIrPredicate(IrPredicate node, PathEvaluationContext context) + { + throw new UnsupportedOperationException("JSON predicate evaluating visitor not implemented for " + node.getClass().getSimpleName()); + } + + @Override + protected Boolean visitIrComparisonPredicate(IrComparisonPredicate node, PathEvaluationContext context) + { + List leftSequence; + try { + leftSequence = pathVisitor.process(node.getLeft(), context); + } + catch (PathEvaluationError e) { + return null; + } + + List rightSequence; + try { + rightSequence = pathVisitor.process(node.getRight(), context); + } + catch (PathEvaluationError e) { + return null; + } + + if (lax) { + leftSequence = unwrapArrays(leftSequence); + rightSequence = unwrapArrays(rightSequence); + } + + if (leftSequence.isEmpty() || rightSequence.isEmpty()) { + return FALSE; + } + + boolean leftHasJsonNull = false; + boolean leftHasScalar = false; + boolean leftHasNonScalar = false; + for (Object object : leftSequence) { + if (object instanceof JsonNode) { + if (object instanceof NullNode) { + leftHasJsonNull = true; + } + else if (((JsonNode) object).isValueNode()) { + leftHasScalar = true; + } + else { + leftHasNonScalar = true; + } + } + else { + leftHasScalar = true; + } + } + + boolean rightHasJsonNull = false; + boolean rightHasScalar = false; + boolean rightHasNonScalar = false; + for (Object object : rightSequence) { + if (object instanceof JsonNode) { + if (((JsonNode) object).isNull()) { + rightHasJsonNull = true; + } + else if (((JsonNode) object).isValueNode()) { + rightHasScalar = true; + } + else { + rightHasNonScalar = true; + } + } + else { + rightHasScalar = true; + } + } + + // try to find a quick error, i.e. a pair of left and right items which are of non-comparable categories + if (leftHasNonScalar && rightHasNonScalar || + leftHasNonScalar && rightHasScalar || + leftHasScalar && rightHasNonScalar) { + return null; + } + + boolean found = false; + + // try to find a quick null-based answer for == and <> operators + if (node.getOperator() == EQUAL && leftHasJsonNull && rightHasJsonNull) { + found = true; + } + if (node.getOperator() == NOT_EQUAL) { + if (leftHasJsonNull && (rightHasScalar || rightHasNonScalar) || + rightHasJsonNull && (leftHasScalar || leftHasNonScalar)) { + found = true; + } + } + if (found && lax) { + return TRUE; + } + + // compare scalars from left and right sequence + if (!leftHasScalar || !rightHasScalar) { + return found; + } + List leftScalars = getScalars(leftSequence); + if (leftScalars == null) { + return null; + } + List rightScalars = getScalars(rightSequence); + if (rightScalars == null) { + return null; + } + for (TypedValue leftValue : leftScalars) { + for (TypedValue rightValue : rightScalars) { + Boolean result = compare(node, leftValue, rightValue); + if (result == null) { + return null; + } + if (TRUE.equals(result)) { + found = true; + if (lax) { + return TRUE; + } + } + } + } + + return found; + } + + private Boolean compare(IrComparisonPredicate node, TypedValue left, TypedValue right) + { + IrComparisonPredicate.Operator comparisonOperator = node.getOperator(); + ComparisonExpression.Operator operator; + Type firstType = left.getType(); + Object firstValue = left.getValueAsObject(); + Type secondType = right.getType(); + Object secondValue = right.getValueAsObject(); + switch (comparisonOperator) { + case EQUAL: + case NOT_EQUAL: + operator = ComparisonExpression.Operator.EQUAL; + break; + case LESS_THAN: + operator = ComparisonExpression.Operator.LESS_THAN; + break; + case GREATER_THAN: + operator = ComparisonExpression.Operator.LESS_THAN; + firstType = right.getType(); + firstValue = right.getValueAsObject(); + secondType = left.getType(); + secondValue = left.getValueAsObject(); + break; + case LESS_THAN_OR_EQUAL: + operator = ComparisonExpression.Operator.LESS_THAN_OR_EQUAL; + break; + case GREATER_THAN_OR_EQUAL: + operator = ComparisonExpression.Operator.LESS_THAN_OR_EQUAL; + firstType = right.getType(); + firstValue = right.getValueAsObject(); + secondType = left.getType(); + secondValue = left.getValueAsObject(); + break; + default: + throw new UnsupportedOperationException("Unexpected comparison operator " + comparisonOperator); + } + + ResolvedOperatorAndCoercions operators = resolver.getOperators(node, OperatorType.valueOf(operator.name()), firstType, secondType); + if (operators == RESOLUTION_ERROR) { + return null; + } + + if (operators.getLeftCoercion().isPresent()) { + try { + firstValue = invoker.invoke(operators.getLeftCoercion().get(), ImmutableList.of(firstValue)); + } + catch (RuntimeException e) { + return null; + } + } + + if (operators.getRightCoercion().isPresent()) { + try { + secondValue = invoker.invoke(operators.getRightCoercion().get(), ImmutableList.of(secondValue)); + } + catch (RuntimeException e) { + return null; + } + } + + Object result; + try { + result = invoker.invoke(operators.getOperator(), ImmutableList.of(firstValue, secondValue)); + } + catch (RuntimeException e) { + return null; + } + + if (comparisonOperator == NOT_EQUAL) { + return !(Boolean) result; + } + return (Boolean) result; + } + + @Override + protected Boolean visitIrConjunctionPredicate(IrConjunctionPredicate node, PathEvaluationContext context) + { + Boolean left = process(node.getLeft(), context); + if (FALSE.equals(left)) { + return FALSE; + } + Boolean right = process(node.getRight(), context); + if (FALSE.equals(right)) { + return FALSE; + } + if (left == null || right == null) { + return null; + } + return TRUE; + } + + @Override + protected Boolean visitIrDisjunctionPredicate(IrDisjunctionPredicate node, PathEvaluationContext context) + { + Boolean left = process(node.getLeft(), context); + if (TRUE.equals(left)) { + return TRUE; + } + Boolean right = process(node.getRight(), context); + if (TRUE.equals(right)) { + return TRUE; + } + if (left == null || right == null) { + return null; + } + return FALSE; + } + + @Override + protected Boolean visitIrExistsPredicate(IrExistsPredicate node, PathEvaluationContext context) + { + List sequence; + try { + sequence = pathVisitor.process(node.getPath(), context); + } + catch (PathEvaluationError e) { + return null; + } + + return !sequence.isEmpty(); + } + + @Override + protected Boolean visitIrIsUnknownPredicate(IrIsUnknownPredicate node, PathEvaluationContext context) + { + Boolean predicateResult = process(node.getPredicate(), context); + + return predicateResult == null; + } + + @Override + protected Boolean visitIrNegationPredicate(IrNegationPredicate node, PathEvaluationContext context) + { + Boolean predicateResult = process(node.getPredicate(), context); + + return predicateResult == null ? null : !predicateResult; + } + + @Override + protected Boolean visitIrStartsWithPredicate(IrStartsWithPredicate node, PathEvaluationContext context) + { + List valueSequence; + try { + valueSequence = pathVisitor.process(node.getValue(), context); + } + catch (PathEvaluationError e) { + return null; + } + + List prefixSequence; + try { + prefixSequence = pathVisitor.process(node.getPrefix(), context); + } + catch (PathEvaluationError e) { + return null; + } + if (prefixSequence.size() != 1) { + return null; + } + Slice prefix = getText(getOnlyElement(prefixSequence)); + if (prefix == null) { + return null; + } + + if (lax) { + valueSequence = unwrapArrays(valueSequence); + } + if (valueSequence.isEmpty()) { + return FALSE; + } + + boolean found = false; + for (Object object : valueSequence) { + Slice value = getText(object); + if (value == null) { + return null; + } + if (StringFunctions.startsWith(value, prefix)) { + found = true; + if (lax) { + return TRUE; + } + } + } + + return found; + } + + private static List getScalars(List sequence) + { + ImmutableList.Builder scalars = ImmutableList.builder(); + for (Object object : sequence) { + if (object instanceof TypedValue) { + scalars.add((TypedValue) object); + } + else { + JsonNode jsonNode = (JsonNode) object; + if (jsonNode.isValueNode() && !jsonNode.isNull()) { + Optional typedValue; + try { + typedValue = getTypedValue(jsonNode); + } + catch (SqlJsonLiteralConverter.JsonLiteralConversionError e) { + return null; + } + if (typedValue.isEmpty()) { + return null; + } + scalars.add(typedValue.get()); + } + } + } + + return scalars.build(); + } + + private static Slice getText(Object object) + { + if (object instanceof TypedValue) { + TypedValue typedValue = (TypedValue) object; + if (isCharacterStringType(typedValue.getType())) { + if (typedValue.getType() instanceof CharType) { + return padSpaces((Slice) typedValue.getObjectValue(), (CharType) typedValue.getType()); + } + return (Slice) typedValue.getObjectValue(); + } + return null; + } + JsonNode jsonNode = (JsonNode) object; + return getTextTypedValue(jsonNode) + .map(TypedValue::getObjectValue) + .map(Slice.class::cast) + .orElse(null); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrAbsMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrAbsMethod.java new file mode 100644 index 000000000000..6c0dc8ddd986 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrAbsMethod.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrAbsMethod + extends IrMethod +{ + @JsonCreator + public IrAbsMethod(@JsonProperty("base") IrPathNode base, @JsonProperty("type") Optional type) + { + super(base, type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrAbsMethod(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrAccessor.java b/core/trino-main/src/main/java/io/trino/json/ir/IrAccessor.java new file mode 100644 index 000000000000..3ff7af0a8db7 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrAccessor.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public abstract class IrAccessor + extends IrPathNode +{ + protected final IrPathNode base; + + IrAccessor(IrPathNode base, Optional type) + { + super(type); + this.base = requireNonNull(base, "accessor base is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrAccessor(this, context); + } + + @JsonProperty + public IrPathNode getBase() + { + return base; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrAccessor other = (IrAccessor) obj; + return Objects.equals(this.base, other.base); + } + + @Override + public int hashCode() + { + return Objects.hash(base); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrArithmeticBinary.java b/core/trino-main/src/main/java/io/trino/json/ir/IrArithmeticBinary.java new file mode 100644 index 000000000000..0a1361038444 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrArithmeticBinary.java @@ -0,0 +1,111 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.function.OperatorType; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class IrArithmeticBinary + extends IrPathNode +{ + private final Operator operator; + private final IrPathNode left; + private final IrPathNode right; + + @JsonCreator + public IrArithmeticBinary( + @JsonProperty("operator") Operator operator, + @JsonProperty("left") IrPathNode left, + @JsonProperty("right") IrPathNode right, + @JsonProperty("type") Optional resultType) + { + super(resultType); + this.operator = requireNonNull(operator, "operator is null"); + this.left = requireNonNull(left, "left is null"); + this.right = requireNonNull(right, "right is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrArithmeticBinary(this, context); + } + + @JsonProperty + public Operator getOperator() + { + return operator; + } + + @JsonProperty + public IrPathNode getLeft() + { + return left; + } + + @JsonProperty + public IrPathNode getRight() + { + return right; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrArithmeticBinary other = (IrArithmeticBinary) obj; + return this.operator == other.operator && + Objects.equals(this.left, other.left) && + Objects.equals(this.right, other.right); + } + + @Override + public int hashCode() + { + return Objects.hash(operator, left, right); + } + + public enum Operator + { + ADD(OperatorType.ADD), + SUBTRACT(OperatorType.SUBTRACT), + MULTIPLY(OperatorType.MULTIPLY), + DIVIDE(OperatorType.DIVIDE), + MODULUS(OperatorType.MODULUS); + + private final OperatorType type; + + Operator(OperatorType type) + { + this.type = requireNonNull(type, "type is null"); + } + + public OperatorType getType() + { + return type; + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrArithmeticUnary.java b/core/trino-main/src/main/java/io/trino/json/ir/IrArithmeticUnary.java new file mode 100644 index 000000000000..29892d7a6f1d --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrArithmeticUnary.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class IrArithmeticUnary + extends IrPathNode +{ + private final Sign sign; + private final IrPathNode base; + + @JsonCreator + public IrArithmeticUnary(@JsonProperty("sign") Sign sign, @JsonProperty("base") IrPathNode base, @JsonProperty("type") Optional type) + { + super(type); + this.sign = requireNonNull(sign, "sign is null"); + this.base = requireNonNull(base, "base is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrArithmeticUnary(this, context); + } + + @JsonProperty + public Sign getSign() + { + return sign; + } + + @JsonProperty + public IrPathNode getBase() + { + return base; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrArithmeticUnary other = (IrArithmeticUnary) obj; + return this.sign == other.sign && Objects.equals(this.base, other.base); + } + + @Override + public int hashCode() + { + return Objects.hash(sign, base); + } + + public enum Sign + { + PLUS("+"), + MINUS("-"); + + private final String sign; + + Sign(String sign) + { + this.sign = requireNonNull(sign, "sign is null"); + } + + public String getSign() + { + return sign; + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrArrayAccessor.java b/core/trino-main/src/main/java/io/trino/json/ir/IrArrayAccessor.java new file mode 100644 index 000000000000..4c05624af5c4 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrArrayAccessor.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class IrArrayAccessor + extends IrAccessor +{ + // list of subscripts or empty list for wildcard array accessor + private final List subscripts; + + @JsonCreator + public IrArrayAccessor(@JsonProperty("base") IrPathNode base, @JsonProperty("subscripts") List subscripts, @JsonProperty("type") Optional type) + { + super(base, type); + this.subscripts = requireNonNull(subscripts, "subscripts is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrArrayAccessor(this, context); + } + + @JsonProperty + public List getSubscripts() + { + return subscripts; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrArrayAccessor other = (IrArrayAccessor) obj; + return Objects.equals(this.base, other.base) && Objects.equals(this.subscripts, other.subscripts); + } + + @Override + public int hashCode() + { + return Objects.hash(base, subscripts); + } + + public static class Subscript + { + private final IrPathNode from; + private final Optional to; + + @JsonCreator + public Subscript(@JsonProperty("from") IrPathNode from, @JsonProperty("to") Optional to) + { + this.from = requireNonNull(from, "from is null"); + this.to = requireNonNull(to, "to is null"); + } + + @JsonProperty + public IrPathNode getFrom() + { + return from; + } + + @JsonProperty + public Optional getTo() + { + return to; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Subscript other = (Subscript) obj; + return Objects.equals(this.from, other.from) && Objects.equals(this.to, other.to); + } + + @Override + public int hashCode() + { + return Objects.hash(from, to); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrCeilingMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrCeilingMethod.java new file mode 100644 index 000000000000..05c00b8270bb --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrCeilingMethod.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrCeilingMethod + extends IrMethod +{ + @JsonCreator + public IrCeilingMethod(@JsonProperty("base") IrPathNode base, @JsonProperty("type") Optional type) + { + super(base, type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrCeilingMethod(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrComparisonPredicate.java b/core/trino-main/src/main/java/io/trino/json/ir/IrComparisonPredicate.java new file mode 100644 index 000000000000..ad3a40507d1c --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrComparisonPredicate.java @@ -0,0 +1,93 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class IrComparisonPredicate + extends IrPredicate +{ + private final Operator operator; + private final IrPathNode left; + private final IrPathNode right; + + @JsonCreator + public IrComparisonPredicate(@JsonProperty("operator") Operator operator, @JsonProperty("left") IrPathNode left, @JsonProperty("right") IrPathNode right) + { + super(); + this.operator = requireNonNull(operator, "operator is null"); + this.left = requireNonNull(left, "left is null"); + this.right = requireNonNull(right, "right is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrComparisonPredicate(this, context); + } + + @JsonProperty + public Operator getOperator() + { + return operator; + } + + @JsonProperty + public IrPathNode getLeft() + { + return left; + } + + @JsonProperty + public IrPathNode getRight() + { + return right; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrComparisonPredicate other = (IrComparisonPredicate) obj; + return this.operator == other.operator && + Objects.equals(this.left, other.left) && + Objects.equals(this.right, other.right); + } + + @Override + public int hashCode() + { + return Objects.hash(operator, left, right); + } + + public enum Operator + { + EQUAL, + NOT_EQUAL, + LESS_THAN, + GREATER_THAN, + LESS_THAN_OR_EQUAL, + GREATER_THAN_OR_EQUAL; + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrConjunctionPredicate.java b/core/trino-main/src/main/java/io/trino/json/ir/IrConjunctionPredicate.java new file mode 100644 index 000000000000..af604abb9a37 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrConjunctionPredicate.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class IrConjunctionPredicate + extends IrPredicate +{ + private final IrPredicate left; + private final IrPredicate right; + + @JsonCreator + public IrConjunctionPredicate(@JsonProperty("left") IrPredicate left, @JsonProperty("right") IrPredicate right) + { + super(); + this.left = requireNonNull(left, "left is null"); + this.right = requireNonNull(right, "right is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrConjunctionPredicate(this, context); + } + + @JsonProperty + public IrPathNode getLeft() + { + return left; + } + + @JsonProperty + public IrPathNode getRight() + { + return right; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrConjunctionPredicate other = (IrConjunctionPredicate) obj; + return Objects.equals(this.left, other.left) && + Objects.equals(this.right, other.right); + } + + @Override + public int hashCode() + { + return Objects.hash(left, right); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrConstantJsonSequence.java b/core/trino-main/src/main/java/io/trino/json/ir/IrConstantJsonSequence.java new file mode 100644 index 000000000000..5820f0af6b45 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrConstantJsonSequence.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import io.trino.spi.type.Type; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class IrConstantJsonSequence + extends IrPathNode +{ + public static final IrConstantJsonSequence EMPTY_SEQUENCE = new IrConstantJsonSequence(ImmutableList.of(), Optional.empty()); + + private final List sequence; + + public static IrConstantJsonSequence singletonSequence(JsonNode jsonNode, Optional type) + { + return new IrConstantJsonSequence(ImmutableList.of(jsonNode), type); + } + + @JsonCreator + public IrConstantJsonSequence(@JsonProperty("sequence") List sequence, @JsonProperty("type") Optional type) + { + super(type); + this.sequence = ImmutableList.copyOf(requireNonNull(sequence, "sequence is null")); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrConstantJsonSequence(this, context); + } + + @JsonProperty + public List getSequence() + { + return sequence; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrConstantJsonSequence other = (IrConstantJsonSequence) obj; + return Objects.equals(this.sequence, other.sequence); + } + + @Override + public int hashCode() + { + return Objects.hash(sequence); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrContextVariable.java b/core/trino-main/src/main/java/io/trino/json/ir/IrContextVariable.java new file mode 100644 index 000000000000..b4f633ae42e9 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrContextVariable.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrContextVariable + extends IrPathNode +{ + @JsonCreator + public IrContextVariable(@JsonProperty("type") Optional type) + { + super(type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrContextVariable(this, context); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + return obj != null && getClass() == obj.getClass(); + } + + @Override + public int hashCode() + { + return getClass().hashCode(); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrDatetimeMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrDatetimeMethod.java new file mode 100644 index 000000000000..5381e08b2708 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrDatetimeMethod.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class IrDatetimeMethod + extends IrMethod +{ + private final Optional format; // this is a string literal + + @JsonCreator + public IrDatetimeMethod(@JsonProperty("base") IrPathNode base, @JsonProperty("format") Optional format, @JsonProperty("type") Optional type) + { + super(base, type); + this.format = requireNonNull(format, "format is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrDatetimeMethod(this, context); + } + + @JsonProperty + public Optional getFormat() + { + return format; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrDatetimeMethod other = (IrDatetimeMethod) obj; + return Objects.equals(this.base, other.base) && Objects.equals(this.format, other.format); + } + + @Override + public int hashCode() + { + return Objects.hash(base, format); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrDisjunctionPredicate.java b/core/trino-main/src/main/java/io/trino/json/ir/IrDisjunctionPredicate.java new file mode 100644 index 000000000000..58df11a0a806 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrDisjunctionPredicate.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class IrDisjunctionPredicate + extends IrPredicate +{ + private final IrPredicate left; + private final IrPredicate right; + + @JsonCreator + public IrDisjunctionPredicate(@JsonProperty("left") IrPredicate left, @JsonProperty("right") IrPredicate right) + { + super(); + this.left = requireNonNull(left, "left is null"); + this.right = requireNonNull(right, "right is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrDisjunctionPredicate(this, context); + } + + @JsonProperty + public IrPathNode getLeft() + { + return left; + } + + @JsonProperty + public IrPathNode getRight() + { + return right; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrDisjunctionPredicate other = (IrDisjunctionPredicate) obj; + return Objects.equals(this.left, other.left) && + Objects.equals(this.right, other.right); + } + + @Override + public int hashCode() + { + return Objects.hash(left, right); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrDoubleMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrDoubleMethod.java new file mode 100644 index 000000000000..bd6e3042fdb1 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrDoubleMethod.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrDoubleMethod + extends IrMethod +{ + @JsonCreator + public IrDoubleMethod(@JsonProperty("base") IrPathNode base, @JsonProperty("type") Optional type) + { + super(base, type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrDoubleMethod(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrExistsPredicate.java b/core/trino-main/src/main/java/io/trino/json/ir/IrExistsPredicate.java new file mode 100644 index 000000000000..bfee654ed3d8 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrExistsPredicate.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class IrExistsPredicate + extends IrPredicate +{ + private final IrPathNode path; + + @JsonCreator + public IrExistsPredicate(@JsonProperty("path") IrPathNode path) + { + super(); + this.path = requireNonNull(path, "path is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrExistsPredicate(this, context); + } + + @JsonProperty + public IrPathNode getPath() + { + return path; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrExistsPredicate other = (IrExistsPredicate) obj; + return Objects.equals(this.path, other.path); + } + + @Override + public int hashCode() + { + return Objects.hash(path); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrFilter.java b/core/trino-main/src/main/java/io/trino/json/ir/IrFilter.java new file mode 100644 index 000000000000..d35068aa2f97 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrFilter.java @@ -0,0 +1,67 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class IrFilter + extends IrAccessor +{ + private final IrPredicate predicate; + + @JsonCreator + public IrFilter(@JsonProperty("base") IrPathNode base, @JsonProperty("predicate") IrPredicate predicate, @JsonProperty("type") Optional type) + { + super(base, type); + this.predicate = requireNonNull(predicate, "predicate is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrFilter(this, context); + } + + @JsonProperty + public IrPredicate getPredicate() + { + return predicate; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrFilter other = (IrFilter) obj; + return Objects.equals(this.base, other.base) && Objects.equals(this.predicate, other.predicate); + } + + @Override + public int hashCode() + { + return Objects.hash(base, predicate); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrFloorMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrFloorMethod.java new file mode 100644 index 000000000000..74acfbd567e9 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrFloorMethod.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrFloorMethod + extends IrMethod +{ + @JsonCreator + public IrFloorMethod(@JsonProperty("base") IrPathNode base, @JsonProperty("type") Optional type) + { + super(base, type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrFloorMethod(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrIsUnknownPredicate.java b/core/trino-main/src/main/java/io/trino/json/ir/IrIsUnknownPredicate.java new file mode 100644 index 000000000000..13ce6acc612d --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrIsUnknownPredicate.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class IrIsUnknownPredicate + extends IrPredicate +{ + private final IrPredicate predicate; + + @JsonCreator + public IrIsUnknownPredicate(@JsonProperty("predicate") IrPredicate predicate) + { + super(); + this.predicate = requireNonNull(predicate, "predicate is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrIsUnknownPredicate(this, context); + } + + @JsonProperty + public IrPredicate getPredicate() + { + return predicate; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrIsUnknownPredicate other = (IrIsUnknownPredicate) obj; + return Objects.equals(this.predicate, other.predicate); + } + + @Override + public int hashCode() + { + return Objects.hash(predicate); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrJsonNull.java b/core/trino-main/src/main/java/io/trino/json/ir/IrJsonNull.java new file mode 100644 index 000000000000..ac7495a9d195 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrJsonNull.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.Optional; + +public class IrJsonNull + extends IrPathNode +{ + @JsonCreator + public IrJsonNull() + { + super(Optional.empty()); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrJsonNull(this, context); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + return obj != null && getClass() == obj.getClass(); + } + + @Override + public int hashCode() + { + return getClass().hashCode(); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrJsonPath.java b/core/trino-main/src/main/java/io/trino/json/ir/IrJsonPath.java new file mode 100644 index 000000000000..2a98eddc072a --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrJsonPath.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class IrJsonPath +{ + private final boolean lax; + private final IrPathNode root; + + @JsonCreator + public IrJsonPath(@JsonProperty("lax") boolean lax, @JsonProperty("root") IrPathNode root) + { + this.lax = lax; + this.root = requireNonNull(root, "root is null"); + } + + @JsonProperty + public boolean isLax() + { + return lax; + } + + @JsonProperty + public IrPathNode getRoot() + { + return root; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrJsonPath other = (IrJsonPath) obj; + return this.lax == other.lax && + Objects.equals(this.root, other.root); + } + + @Override + public int hashCode() + { + return Objects.hash(lax, root); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrJsonPathVisitor.java b/core/trino-main/src/main/java/io/trino/json/ir/IrJsonPathVisitor.java new file mode 100644 index 000000000000..b65c8107856c --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrJsonPathVisitor.java @@ -0,0 +1,189 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import javax.annotation.Nullable; + +public abstract class IrJsonPathVisitor +{ + public R process(IrPathNode node) + { + return process(node, null); + } + + public R process(IrPathNode node, @Nullable C context) + { + return node.accept(this, context); + } + + protected R visitIrPathNode(IrPathNode node, C context) + { + return null; + } + + protected R visitIrAccessor(IrAccessor node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrComparisonPredicate(IrComparisonPredicate node, C context) + { + return visitIrPredicate(node, context); + } + + protected R visitIrConjunctionPredicate(IrConjunctionPredicate node, C context) + { + return visitIrPredicate(node, context); + } + + protected R visitIrDisjunctionPredicate(IrDisjunctionPredicate node, C context) + { + return visitIrPredicate(node, context); + } + + protected R visitIrExistsPredicate(IrExistsPredicate node, C context) + { + return visitIrPredicate(node, context); + } + + protected R visitIrMethod(IrMethod node, C context) + { + return visitIrAccessor(node, context); + } + + protected R visitIrAbsMethod(IrAbsMethod node, C context) + { + return visitIrMethod(node, context); + } + + protected R visitIrArithmeticBinary(IrArithmeticBinary node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrArithmeticUnary(IrArithmeticUnary node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrArrayAccessor(IrArrayAccessor node, C context) + { + return visitIrAccessor(node, context); + } + + protected R visitIrCeilingMethod(IrCeilingMethod node, C context) + { + return visitIrMethod(node, context); + } + + protected R visitIrConstantJsonSequence(IrConstantJsonSequence node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrContextVariable(IrContextVariable node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrDatetimeMethod(IrDatetimeMethod node, C context) + { + return visitIrMethod(node, context); + } + + protected R visitIrDoubleMethod(IrDoubleMethod node, C context) + { + return visitIrMethod(node, context); + } + + protected R visitIrFilter(IrFilter node, C context) + { + return visitIrAccessor(node, context); + } + + protected R visitIrFloorMethod(IrFloorMethod node, C context) + { + return visitIrMethod(node, context); + } + + protected R visitIrIsUnknownPredicate(IrIsUnknownPredicate node, C context) + { + return visitIrPredicate(node, context); + } + + protected R visitIrJsonNull(IrJsonNull node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrKeyValueMethod(IrKeyValueMethod node, C context) + { + return visitIrMethod(node, context); + } + + protected R visitIrLastIndexVariable(IrLastIndexVariable node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrLiteral(IrLiteral node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrMemberAccessor(IrMemberAccessor node, C context) + { + return visitIrAccessor(node, context); + } + + protected R visitIrNamedJsonVariable(IrNamedJsonVariable node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrNamedValueVariable(IrNamedValueVariable node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrNegationPredicate(IrNegationPredicate node, C context) + { + return visitIrPredicate(node, context); + } + + protected R visitIrPredicate(IrPredicate node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrPredicateCurrentItemVariable(IrPredicateCurrentItemVariable node, C context) + { + return visitIrPathNode(node, context); + } + + protected R visitIrSizeMethod(IrSizeMethod node, C context) + { + return visitIrMethod(node, context); + } + + protected R visitIrStartsWithPredicate(IrStartsWithPredicate node, C context) + { + return visitIrPredicate(node, context); + } + + protected R visitIrTypeMethod(IrTypeMethod node, C context) + { + return visitIrMethod(node, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrKeyValueMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrKeyValueMethod.java new file mode 100644 index 000000000000..5bf840a94686 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrKeyValueMethod.java @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Optional; + +public class IrKeyValueMethod + extends IrMethod +{ + @JsonCreator + public IrKeyValueMethod(@JsonProperty("base") IrPathNode base) + { + super(base, Optional.empty()); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrKeyValueMethod(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrLastIndexVariable.java b/core/trino-main/src/main/java/io/trino/json/ir/IrLastIndexVariable.java new file mode 100644 index 000000000000..c711e33471ec --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrLastIndexVariable.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrLastIndexVariable + extends IrPathNode +{ + @JsonCreator + public IrLastIndexVariable(@JsonProperty("type") Optional type) + { + super(type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrLastIndexVariable(this, context); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + return obj != null && getClass() == obj.getClass(); + } + + @Override + public int hashCode() + { + return getClass().hashCode(); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrLiteral.java b/core/trino-main/src/main/java/io/trino/json/ir/IrLiteral.java new file mode 100644 index 000000000000..8acfe90bcf4b --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrLiteral.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.block.Block; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static io.trino.spi.predicate.Utils.nativeValueToBlock; +import static io.trino.spi.type.TypeUtils.readNativeValue; +import static java.util.Objects.requireNonNull; + +public class IrLiteral + extends IrPathNode +{ + // (boxed) native representation + private final Object value; + + public IrLiteral(Type type, Object value) + { + super(Optional.of(type)); + this.value = requireNonNull(value, "value is null"); // no null values allowed + } + + @Deprecated // For JSON deserialization only + @JsonCreator + public static IrLiteral fromJson(@JsonProperty("type") Type type, @JsonProperty("valueAsBlock") Block value) + { + checkArgument(value.getPositionCount() == 1); + return new IrLiteral(type, readNativeValue(type, value, 0)); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrLiteral(this, context); + } + + @JsonIgnore + public Object getValue() + { + return value; + } + + @JsonProperty + public Block getValueAsBlock() + { + return nativeValueToBlock(getType().orElseThrow(), value); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrLiteral other = (IrLiteral) obj; + return Objects.equals(this.value, other.value) && Objects.equals(this.getType(), other.getType()); + } + + @Override + public int hashCode() + { + return Objects.hash(value, getType()); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrMemberAccessor.java b/core/trino-main/src/main/java/io/trino/json/ir/IrMemberAccessor.java new file mode 100644 index 000000000000..8e7c545a5fd8 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrMemberAccessor.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class IrMemberAccessor + extends IrAccessor +{ + // object member key or Optional.empty for wildcard member accessor + private final Optional key; + + @JsonCreator + public IrMemberAccessor(@JsonProperty("base") IrPathNode base, @JsonProperty("key") Optional key, @JsonProperty("type") Optional type) + { + super(base, type); + this.key = requireNonNull(key, "key is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrMemberAccessor(this, context); + } + + @JsonProperty + public Optional getKey() + { + return key; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrMemberAccessor other = (IrMemberAccessor) obj; + return Objects.equals(this.base, other.base) && Objects.equals(this.key, other.key); + } + + @Override + public int hashCode() + { + return Objects.hash(base, key); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrMethod.java new file mode 100644 index 000000000000..df5e5282fcfc --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrMethod.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import io.trino.spi.type.Type; + +import java.util.Optional; + +public abstract class IrMethod + extends IrAccessor +{ + IrMethod(IrPathNode base, Optional type) + { + super(base, type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrMethod(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrNamedJsonVariable.java b/core/trino-main/src/main/java/io/trino/json/ir/IrNamedJsonVariable.java new file mode 100644 index 000000000000..3c48143240a2 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrNamedJsonVariable.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; + +public class IrNamedJsonVariable + extends IrPathNode +{ + private final int index; + + @JsonCreator + public IrNamedJsonVariable(@JsonProperty("index") int index, @JsonProperty("type") Optional type) + { + super(type); + checkArgument(index >= 0, "parameter index is negative"); + this.index = index; + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrNamedJsonVariable(this, context); + } + + @JsonProperty + public int getIndex() + { + return index; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrNamedJsonVariable other = (IrNamedJsonVariable) obj; + return this.index == other.index; + } + + @Override + public int hashCode() + { + return Objects.hash(index); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrNamedValueVariable.java b/core/trino-main/src/main/java/io/trino/json/ir/IrNamedValueVariable.java new file mode 100644 index 000000000000..80f95c5eef6a --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrNamedValueVariable.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; + +public class IrNamedValueVariable + extends IrPathNode +{ + private final int index; + + @JsonCreator + public IrNamedValueVariable(@JsonProperty("index") int index, @JsonProperty("type") Optional type) + { + super(type); + checkArgument(index >= 0, "parameter index is negative"); + this.index = index; + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrNamedValueVariable(this, context); + } + + @JsonProperty + public int getIndex() + { + return index; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrNamedValueVariable other = (IrNamedValueVariable) obj; + return this.index == other.index; + } + + @Override + public int hashCode() + { + return Objects.hash(index); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrNegationPredicate.java b/core/trino-main/src/main/java/io/trino/json/ir/IrNegationPredicate.java new file mode 100644 index 000000000000..275fd764e188 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrNegationPredicate.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class IrNegationPredicate + extends IrPredicate +{ + private final IrPredicate predicate; + + @JsonCreator + public IrNegationPredicate(@JsonProperty("predicate") IrPredicate predicate) + { + super(); + this.predicate = requireNonNull(predicate, "predicate is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrNegationPredicate(this, context); + } + + @JsonProperty + public IrPredicate getPredicate() + { + return predicate; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrNegationPredicate other = (IrNegationPredicate) obj; + return Objects.equals(this.predicate, other.predicate); + } + + @Override + public int hashCode() + { + return Objects.hash(predicate); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrPathNode.java b/core/trino-main/src/main/java/io/trino/json/ir/IrPathNode.java new file mode 100644 index 000000000000..b7dc3f45b256 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrPathNode.java @@ -0,0 +1,96 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.trino.spi.type.Type; + +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "@type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = IrAbsMethod.class, name = "abs"), + @JsonSubTypes.Type(value = IrArithmeticBinary.class, name = "binary"), + @JsonSubTypes.Type(value = IrArithmeticUnary.class, name = "unary"), + @JsonSubTypes.Type(value = IrArrayAccessor.class, name = "arrayaccessor"), + @JsonSubTypes.Type(value = IrCeilingMethod.class, name = "ceiling"), + @JsonSubTypes.Type(value = IrComparisonPredicate.class, name = "comparison"), + @JsonSubTypes.Type(value = IrConjunctionPredicate.class, name = "conjunction"), + @JsonSubTypes.Type(value = IrConstantJsonSequence.class, name = "jsonsequence"), + @JsonSubTypes.Type(value = IrContextVariable.class, name = "contextvariable"), + @JsonSubTypes.Type(value = IrDatetimeMethod.class, name = "datetime"), + @JsonSubTypes.Type(value = IrDisjunctionPredicate.class, name = "disjunction"), + @JsonSubTypes.Type(value = IrDoubleMethod.class, name = "double"), + @JsonSubTypes.Type(value = IrExistsPredicate.class, name = "exists"), + @JsonSubTypes.Type(value = IrFilter.class, name = "filter"), + @JsonSubTypes.Type(value = IrFloorMethod.class, name = "floor"), + @JsonSubTypes.Type(value = IrIsUnknownPredicate.class, name = "isunknown"), + @JsonSubTypes.Type(value = IrJsonNull.class, name = "jsonnull"), + @JsonSubTypes.Type(value = IrKeyValueMethod.class, name = "keyvalue"), + @JsonSubTypes.Type(value = IrLastIndexVariable.class, name = "last"), + @JsonSubTypes.Type(value = IrLiteral.class, name = "literal"), + @JsonSubTypes.Type(value = IrMemberAccessor.class, name = "memberaccessor"), + @JsonSubTypes.Type(value = IrNamedJsonVariable.class, name = "namedjsonvariable"), + @JsonSubTypes.Type(value = IrNamedValueVariable.class, name = "namedvaluevariable"), + @JsonSubTypes.Type(value = IrNegationPredicate.class, name = "negation"), + @JsonSubTypes.Type(value = IrPredicateCurrentItemVariable.class, name = "currentitem"), + @JsonSubTypes.Type(value = IrSizeMethod.class, name = "size"), + @JsonSubTypes.Type(value = IrStartsWithPredicate.class, name = "startswith"), + @JsonSubTypes.Type(value = IrTypeMethod.class, name = "type"), +}) +public abstract class IrPathNode +{ + // `type` is intentionally skipped in equals() and hashCode() methods of all IrPathNodes, so that + // those methods consider te node's structure only. `type` is a function of the other properties, + // and it might be optionally set or not, depending on when and how the node is created - e.g. either + // initially or by some optimization that will be added in the future (like constant folding, tree flattening). + private final Optional type; + + protected IrPathNode(Optional type) + { + this.type = requireNonNull(type, "type is null"); + } + + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrPathNode(this, context); + } + + /** + * Get the result type, whenever known. + * Type might be known for IrPathNodes returning a singleton sequence (e.g. IrArithmeticBinary), + * as well as for IrPathNodes returning a sequence of arbitrary length (e.g. IrSizeMethod). + * If the node potentially returns a non-singleton sequence, this method shall return Type + * only if the type is the same for all elements of the sequence. + * NOTE: Type is not applicable to every IrPathNode. If the IrPathNode produces an empty sequence, + * a JSON null, or a sequence containing non-literal JSON items, Type cannot be determined. + */ + @JsonProperty + public final Optional getType() + { + return type; + } + + @Override + public abstract boolean equals(Object obj); + + @Override + public abstract int hashCode(); +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrPredicate.java b/core/trino-main/src/main/java/io/trino/json/ir/IrPredicate.java new file mode 100644 index 000000000000..2f2fe90d1e60 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrPredicate.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import java.util.Optional; + +import static io.trino.spi.type.BooleanType.BOOLEAN; + +public abstract class IrPredicate + extends IrPathNode +{ + IrPredicate() + { + super(Optional.of(BOOLEAN)); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrPredicate(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrPredicateCurrentItemVariable.java b/core/trino-main/src/main/java/io/trino/json/ir/IrPredicateCurrentItemVariable.java new file mode 100644 index 000000000000..fc75f571752f --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrPredicateCurrentItemVariable.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrPredicateCurrentItemVariable + extends IrPathNode +{ + @JsonCreator + public IrPredicateCurrentItemVariable(@JsonProperty("type") Optional type) + { + super(type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrPredicateCurrentItemVariable(this, context); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + return obj != null && getClass() == obj.getClass(); + } + + @Override + public int hashCode() + { + return getClass().hashCode(); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrSizeMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrSizeMethod.java new file mode 100644 index 000000000000..34a61af8c443 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrSizeMethod.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrSizeMethod + extends IrMethod +{ + @JsonCreator + public IrSizeMethod(@JsonProperty("base") IrPathNode base, @JsonProperty("type") Optional type) + { + super(base, type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrSizeMethod(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrStartsWithPredicate.java b/core/trino-main/src/main/java/io/trino/json/ir/IrStartsWithPredicate.java new file mode 100644 index 000000000000..7da485d7d8de --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrStartsWithPredicate.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public class IrStartsWithPredicate + extends IrPredicate +{ + private final IrPathNode value; + private final IrPathNode prefix; + + @JsonCreator + public IrStartsWithPredicate(@JsonProperty("value") IrPathNode value, @JsonProperty("prefix") IrPathNode prefix) + { + super(); + this.value = requireNonNull(value, "value is null"); + this.prefix = requireNonNull(prefix, "prefix is null"); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrStartsWithPredicate(this, context); + } + + @JsonProperty + public IrPathNode getValue() + { + return value; + } + + @JsonProperty + public IrPathNode getPrefix() + { + return prefix; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + IrStartsWithPredicate other = (IrStartsWithPredicate) obj; + return Objects.equals(this.value, other.value) && + Objects.equals(this.prefix, other.prefix); + } + + @Override + public int hashCode() + { + return Objects.hash(value, prefix); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/IrTypeMethod.java b/core/trino-main/src/main/java/io/trino/json/ir/IrTypeMethod.java new file mode 100644 index 000000000000..3545ee7457aa --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/IrTypeMethod.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.trino.spi.type.Type; + +import java.util.Optional; + +public class IrTypeMethod + extends IrMethod +{ + @JsonCreator + public IrTypeMethod(@JsonProperty("base") IrPathNode base, @JsonProperty("type") Optional type) + { + super(base, type); + } + + @Override + protected R accept(IrJsonPathVisitor visitor, C context) + { + return visitor.visitIrTypeMethod(this, context); + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/SqlJsonLiteralConverter.java b/core/trino-main/src/main/java/io/trino/json/ir/SqlJsonLiteralConverter.java new file mode 100644 index 000000000000..f74331d65069 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/SqlJsonLiteralConverter.java @@ -0,0 +1,183 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.BigIntegerNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.FloatNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.ShortNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.primitives.Shorts; +import io.airlift.slice.Slice; +import io.trino.spi.TrinoException; +import io.trino.spi.type.CharType; +import io.trino.spi.type.DecimalType; +import io.trino.spi.type.Int128; +import io.trino.spi.type.Type; +import io.trino.spi.type.VarcharType; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Optional; + +import static io.airlift.slice.Slices.utf8Slice; +import static io.trino.spi.StandardErrorCode.INVALID_JSON_LITERAL; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.Chars.padSpaces; +import static io.trino.spi.type.DecimalType.createDecimalType; +import static io.trino.spi.type.Decimals.MAX_PRECISION; +import static io.trino.spi.type.Decimals.encodeScaledValue; +import static io.trino.spi.type.Decimals.encodeShortScaledValue; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.RealType.REAL; +import static io.trino.spi.type.SmallintType.SMALLINT; +import static io.trino.spi.type.TinyintType.TINYINT; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static java.lang.Float.floatToRawIntBits; +import static java.lang.Float.intBitsToFloat; +import static java.lang.Math.toIntExact; +import static java.lang.String.format; + +public final class SqlJsonLiteralConverter +{ + private SqlJsonLiteralConverter() {} + + public static Optional getTypedValue(JsonNode jsonNode) + { + if (jsonNode.getNodeType() == JsonNodeType.BOOLEAN) { + return Optional.of(new TypedValue(BOOLEAN, jsonNode.booleanValue())); + } + if (jsonNode.getNodeType() == JsonNodeType.STRING) { + return Optional.of(new TypedValue(VARCHAR, utf8Slice(jsonNode.textValue()))); + } + return getNumericTypedValue(jsonNode); + } + + public static Optional getTextTypedValue(JsonNode jsonNode) + { + if (jsonNode.getNodeType() == JsonNodeType.STRING) { + return Optional.of(new TypedValue(VARCHAR, utf8Slice(jsonNode.textValue()))); + } + return Optional.empty(); + } + + public static Optional getNumericTypedValue(JsonNode jsonNode) + { + if (jsonNode.getNodeType() == JsonNodeType.NUMBER) { + if (jsonNode instanceof BigIntegerNode) { + if (jsonNode.canConvertToInt()) { + return Optional.of(new TypedValue(INTEGER, jsonNode.longValue())); + } + if (jsonNode.canConvertToLong()) { + return Optional.of(new TypedValue(BIGINT, jsonNode.longValue())); + } + throw conversionError(jsonNode, "value too big"); + } + if (jsonNode instanceof DecimalNode) { + BigDecimal jsonDecimal = jsonNode.decimalValue(); + int precision = jsonDecimal.precision(); + if (precision > MAX_PRECISION) { + throw conversionError(jsonNode, "precision too big"); + } + int scale = jsonDecimal.scale(); + DecimalType decimalType = createDecimalType(precision, scale); + Object value = decimalType.isShort() ? encodeShortScaledValue(jsonDecimal, scale) : encodeScaledValue(jsonDecimal, scale); + return Optional.of(TypedValue.fromValueAsObject(decimalType, value)); + } + if (jsonNode instanceof DoubleNode) { + return Optional.of(new TypedValue(DOUBLE, jsonNode.doubleValue())); + } + if (jsonNode instanceof FloatNode) { + return Optional.of(new TypedValue(REAL, floatToRawIntBits(jsonNode.floatValue()))); + } + if (jsonNode instanceof IntNode) { + return Optional.of(new TypedValue(INTEGER, jsonNode.longValue())); + } + if (jsonNode instanceof LongNode) { + return Optional.of(new TypedValue(BIGINT, jsonNode.longValue())); + } + if (jsonNode instanceof ShortNode) { + return Optional.of(new TypedValue(SMALLINT, jsonNode.longValue())); + } + } + + return Optional.empty(); + } + + public static Optional getJsonNode(TypedValue typedValue) + { + Type type = typedValue.getType(); + if (type.equals(BOOLEAN)) { + return Optional.of(BooleanNode.valueOf(typedValue.getBooleanValue())); + } + if (type instanceof CharType) { + return Optional.of(TextNode.valueOf(padSpaces((Slice) typedValue.getObjectValue(), (CharType) typedValue.getType()).toStringUtf8())); + } + if (type instanceof VarcharType) { + return Optional.of(TextNode.valueOf(((Slice) typedValue.getObjectValue()).toStringUtf8())); + } + if (type.equals(BIGINT)) { + return Optional.of(LongNode.valueOf(typedValue.getLongValue())); + } + if (type.equals(INTEGER)) { + return Optional.of(IntNode.valueOf(toIntExact(typedValue.getLongValue()))); + } + if (type.equals(SMALLINT)) { + return Optional.of(ShortNode.valueOf(Shorts.checkedCast(typedValue.getLongValue()))); + } + if (type.equals(TINYINT)) { + return Optional.of(ShortNode.valueOf(Shorts.checkedCast(typedValue.getLongValue()))); + } + if (type instanceof DecimalType) { + BigInteger unscaledValue; + if (((DecimalType) type).isShort()) { + unscaledValue = BigInteger.valueOf(typedValue.getLongValue()); + } + else { + unscaledValue = ((Int128) typedValue.getObjectValue()).toBigInteger(); + } + return Optional.of(DecimalNode.valueOf(new BigDecimal(unscaledValue, ((DecimalType) type).getScale()))); + } + if (type.equals(DOUBLE)) { + return Optional.of(DoubleNode.valueOf(typedValue.getDoubleValue())); + } + if (type.equals(REAL)) { + return Optional.of(FloatNode.valueOf(intBitsToFloat(toIntExact(typedValue.getLongValue())))); + } + + return Optional.empty(); + } + + public static TrinoException conversionError(JsonNode jsonNode, String cause) + { + return new JsonLiteralConversionError(jsonNode, cause); + } + + public static class JsonLiteralConversionError + extends TrinoException + { + public JsonLiteralConversionError(JsonNode jsonNode, String cause) + { + super(INVALID_JSON_LITERAL, format("cannot convert %s to Trino value (%s)", jsonNode, cause)); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/json/ir/TypedValue.java b/core/trino-main/src/main/java/io/trino/json/ir/TypedValue.java new file mode 100644 index 000000000000..9de8e8cd27ad --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/json/ir/TypedValue.java @@ -0,0 +1,136 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import io.trino.spi.type.Type; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class TypedValue +{ + private final Type type; + private final Object objectValue; + private final long longValue; + private final double doubleValue; + private final boolean booleanValue; + private final Object valueAsObject; + + public TypedValue(Type type, Object objectValue) + { + requireNonNull(type, "type is null"); + requireNonNull(objectValue, "value is null"); + checkArgument(type.getJavaType().isAssignableFrom(objectValue.getClass()), "%s value does not match the type %s", objectValue.getClass(), type); + + this.type = type; + this.objectValue = objectValue; + this.longValue = 0L; + this.doubleValue = 0e0; + this.booleanValue = false; + this.valueAsObject = objectValue; + } + + public TypedValue(Type type, long longValue) + { + requireNonNull(type, "type is null"); + checkArgument(long.class.equals(type.getJavaType()), "long value does not match the type %s", type); + + this.type = type; + this.objectValue = null; + this.longValue = longValue; + this.doubleValue = 0e0; + this.booleanValue = false; + this.valueAsObject = longValue; + } + + public TypedValue(Type type, double doubleValue) + { + requireNonNull(type, "type is null"); + checkArgument(double.class.equals(type.getJavaType()), "double value does not match the type %s", type); + + this.type = type; + this.objectValue = null; + this.longValue = 0L; + this.doubleValue = doubleValue; + this.booleanValue = false; + this.valueAsObject = doubleValue; + } + + public TypedValue(Type type, boolean booleanValue) + { + requireNonNull(type, "type is null"); + checkArgument(boolean.class.equals(type.getJavaType()), "boolean value does not match the type %s", type); + + this.type = type; + this.objectValue = null; + this.longValue = 0L; + this.doubleValue = 0e0; + this.booleanValue = booleanValue; + this.valueAsObject = booleanValue; + } + + public static TypedValue fromValueAsObject(Type type, Object valueAsObject) + { + if (long.class.equals(type.getJavaType())) { + checkState(valueAsObject instanceof Long, "%s value does not match the type %s", valueAsObject.getClass(), type); + return new TypedValue(type, (long) valueAsObject); + } + if (double.class.equals(type.getJavaType())) { + checkState(valueAsObject instanceof Double, "%s value does not match the type %s", valueAsObject.getClass(), type); + return new TypedValue(type, (double) valueAsObject); + } + if (boolean.class.equals(type.getJavaType())) { + checkState(valueAsObject instanceof Boolean, "%s value does not match the type %s", valueAsObject.getClass(), type); + return new TypedValue(type, (boolean) valueAsObject); + } + checkState(type.getJavaType().isAssignableFrom(valueAsObject.getClass()), "%s value does not match the type %s", valueAsObject.getClass(), type); + return new TypedValue(type, valueAsObject); + } + + public Type getType() + { + return type; + } + + public Object getObjectValue() + { + checkArgument(objectValue != null, "the type %s is represented as %s. call another method to retrieve the value", type, type.getJavaType()); + checkArgument(type.getJavaType().isAssignableFrom(objectValue.getClass()), "%s value does not match the type %s", objectValue.getClass(), type); + return objectValue; + } + + public long getLongValue() + { + checkArgument(long.class.equals(type.getJavaType()), "long value does not match the type %s", type); + return longValue; + } + + public double getDoubleValue() + { + checkArgument(double.class.equals(type.getJavaType()), "double value does not match the type %s", type); + return doubleValue; + } + + public boolean getBooleanValue() + { + checkArgument(boolean.class.equals(type.getJavaType()), "boolean value does not match the type %s", type); + return booleanValue; + } + + public Object getValueAsObject() + { + return valueAsObject; + } +} diff --git a/core/trino-main/src/main/java/io/trino/metadata/SystemFunctionBundle.java b/core/trino-main/src/main/java/io/trino/metadata/SystemFunctionBundle.java index d3e443bfafd0..ee08261726de 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/SystemFunctionBundle.java +++ b/core/trino-main/src/main/java/io/trino/metadata/SystemFunctionBundle.java @@ -157,6 +157,8 @@ import io.trino.operator.scalar.VersionFunction; import io.trino.operator.scalar.WilsonInterval; import io.trino.operator.scalar.WordStemFunction; +import io.trino.operator.scalar.json.JsonInputFunctions; +import io.trino.operator.scalar.json.JsonOutputFunctions; import io.trino.operator.scalar.time.LocalTimeFunction; import io.trino.operator.scalar.time.TimeFunctions; import io.trino.operator.scalar.time.TimeOperators; @@ -435,6 +437,8 @@ public static FunctionBundle create(FeaturesConfig featuresConfig, TypeOperators .scalars(DateTimeFunctions.class) .scalar(DateTimeFunctions.FromUnixtimeNanosDecimal.class) .scalars(JsonFunctions.class) + .scalars(JsonInputFunctions.class) + .scalars(JsonOutputFunctions.class) .scalars(ColorFunctions.class) .scalars(HyperLogLogFunctions.class) .scalars(QuantileDigestFunctions.class) diff --git a/core/trino-main/src/main/java/io/trino/metadata/TypeRegistry.java b/core/trino-main/src/main/java/io/trino/metadata/TypeRegistry.java index 8831054888f9..f2325a01b7b9 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/TypeRegistry.java +++ b/core/trino-main/src/main/java/io/trino/metadata/TypeRegistry.java @@ -94,6 +94,7 @@ import static io.trino.type.IntervalYearMonthType.INTERVAL_YEAR_MONTH; import static io.trino.type.IpAddressType.IPADDRESS; import static io.trino.type.JoniRegexpType.JONI_REGEXP; +import static io.trino.type.Json2016Type.JSON_2016; import static io.trino.type.JsonPathType.JSON_PATH; import static io.trino.type.JsonType.JSON; import static io.trino.type.LikePatternType.LIKE_PATTERN; @@ -146,6 +147,7 @@ public TypeRegistry(TypeOperators typeOperators, FeaturesConfig featuresConfig) addType(new Re2JRegexpType(featuresConfig.getRe2JDfaStatesLimit(), featuresConfig.getRe2JDfaRetries())); addType(LIKE_PATTERN); addType(JSON_PATH); + addType(JSON_2016); addType(COLOR); addType(JSON); addType(CODE_POINTS); diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonExistsFunction.java b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonExistsFunction.java new file mode 100644 index 000000000000..7aa986c6db55 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonExistsFunction.java @@ -0,0 +1,161 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import io.trino.annotation.UsedByGeneratedCode; +import io.trino.json.JsonPathEvaluator; +import io.trino.json.JsonPathInvocationContext; +import io.trino.json.PathEvaluationError; +import io.trino.json.ir.IrJsonPath; +import io.trino.metadata.BoundSignature; +import io.trino.metadata.FunctionManager; +import io.trino.metadata.FunctionMetadata; +import io.trino.metadata.Metadata; +import io.trino.metadata.Signature; +import io.trino.metadata.SqlScalarFunction; +import io.trino.operator.scalar.ChoicesScalarFunctionImplementation; +import io.trino.operator.scalar.ScalarFunctionImplementation; +import io.trino.spi.TrinoException; +import io.trino.spi.block.Block; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.Type; +import io.trino.spi.type.TypeManager; +import io.trino.spi.type.TypeSignature; +import io.trino.sql.tree.JsonExists.ErrorBehavior; +import io.trino.type.JsonPath2016Type; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Optional; + +import static io.trino.json.JsonInputErrorNode.JSON_ERROR; +import static io.trino.operator.scalar.json.ParameterUtil.getParametersArray; +import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.BOXED_NULLABLE; +import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.NEVER_NULL; +import static io.trino.spi.function.InvocationConvention.InvocationReturnConvention.NULLABLE_RETURN; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.StandardTypes.JSON_2016; +import static io.trino.spi.type.StandardTypes.TINYINT; +import static io.trino.util.Reflection.constructorMethodHandle; +import static io.trino.util.Reflection.methodHandle; +import static java.util.Objects.requireNonNull; + +public class JsonExistsFunction + extends SqlScalarFunction +{ + public static final String JSON_EXISTS_FUNCTION_NAME = "$json_exists"; + private static final MethodHandle METHOD_HANDLE = methodHandle(JsonExistsFunction.class, "jsonExists", FunctionManager.class, Metadata.class, TypeManager.class, Type.class, JsonPathInvocationContext.class, ConnectorSession.class, JsonNode.class, IrJsonPath.class, Block.class, long.class); + private static final TrinoException INPUT_ARGUMENT_ERROR = new JsonInputConversionError("malformed input argument to JSON_EXISTS function"); + private static final TrinoException PATH_PARAMETER_ERROR = new JsonInputConversionError("malformed JSON path parameter to JSON_EXISTS function"); + + private final FunctionManager functionManager; + private final Metadata metadata; + private final TypeManager typeManager; + + public JsonExistsFunction(FunctionManager functionManager, Metadata metadata, TypeManager typeManager) + { + super(FunctionMetadata.scalarBuilder() + .signature(Signature.builder() + .name(JSON_EXISTS_FUNCTION_NAME) + .typeVariable("T") + .returnType(BOOLEAN) + .argumentTypes(ImmutableList.of(new TypeSignature(JSON_2016), new TypeSignature(JsonPath2016Type.NAME), new TypeSignature("T"), new TypeSignature(TINYINT))) + .build()) + .nullable() + .argumentNullability(false, false, true, false) + .hidden() + .description("Determines whether a JSON value satisfies a path specification") + .build()); + + this.functionManager = requireNonNull(functionManager, "functionManager is null"); + this.metadata = requireNonNull(metadata, "metadata is null"); + this.typeManager = requireNonNull(typeManager, "typeManager is null"); + } + + @Override + protected ScalarFunctionImplementation specialize(BoundSignature boundSignature) + { + Type parametersRowType = boundSignature.getArgumentType(2); + MethodHandle methodHandle = METHOD_HANDLE + .bindTo(functionManager) + .bindTo(metadata) + .bindTo(typeManager) + .bindTo(parametersRowType); + MethodHandle instanceFactory = constructorMethodHandle(JsonPathInvocationContext.class); + return new ChoicesScalarFunctionImplementation( + boundSignature, + NULLABLE_RETURN, + ImmutableList.of(BOXED_NULLABLE, BOXED_NULLABLE, BOXED_NULLABLE, NEVER_NULL), + methodHandle, + Optional.of(instanceFactory)); + } + + @UsedByGeneratedCode + public static Boolean jsonExists( + FunctionManager functionManager, + Metadata metadata, + TypeManager typeManager, + Type parametersRowType, + JsonPathInvocationContext invocationContext, + ConnectorSession session, + JsonNode inputExpression, + IrJsonPath jsonPath, + Block parametersRow, + long errorBehavior) + { + if (inputExpression.equals(JSON_ERROR)) { + return handleError(errorBehavior, INPUT_ARGUMENT_ERROR); // ERROR ON ERROR was already handled by the input function + } + Object[] parameters = getParametersArray(parametersRowType, parametersRow); + for (Object parameter : parameters) { + if (parameter.equals(JSON_ERROR)) { + return handleError(errorBehavior, PATH_PARAMETER_ERROR); // ERROR ON ERROR was already handled by the input function + } + } + // The jsonPath argument is constant for every row. We use the first incoming jsonPath argument to initialize + // the JsonPathEvaluator, and ignore the subsequent jsonPath values. We could sanity-check that all the incoming + // jsonPath values are equal. We deliberately skip this costly check, since this is a hidden function. + JsonPathEvaluator evaluator = invocationContext.getEvaluator(); + if (evaluator == null) { + evaluator = new JsonPathEvaluator(jsonPath, session, metadata, typeManager, functionManager); + invocationContext.setEvaluator(evaluator); + } + List pathResult; + try { + pathResult = evaluator.evaluate(inputExpression, parameters); + } + catch (PathEvaluationError e) { + return handleError(errorBehavior, e); + } + + return !pathResult.isEmpty(); + } + + private static Boolean handleError(long errorBehavior, TrinoException error) + { + switch (ErrorBehavior.values()[(int) errorBehavior]) { + case FALSE: + return false; + case TRUE: + return true; + case UNKNOWN: + return null; + case ERROR: + throw error; + } + throw new IllegalStateException("unexpected error behavior"); + } +} diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonInputConversionError.java b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonInputConversionError.java new file mode 100644 index 000000000000..c230d4eb58da --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonInputConversionError.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar.json; + +import io.trino.spi.TrinoException; + +import static io.trino.spi.StandardErrorCode.JSON_INPUT_CONVERSION_ERROR; + +public class JsonInputConversionError + extends TrinoException +{ + public JsonInputConversionError(String message) + { + super(JSON_INPUT_CONVERSION_ERROR, "conversion to JSON failed: " + message); + } + + public JsonInputConversionError(Throwable cause) + { + super(JSON_INPUT_CONVERSION_ERROR, "conversion to JSON failed: ", cause); + } +} diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonInputFunctions.java b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonInputFunctions.java new file mode 100644 index 000000000000..65419086ec6e --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonInputFunctions.java @@ -0,0 +1,114 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.airlift.slice.Slice; +import io.trino.spi.TrinoException; +import io.trino.spi.function.ScalarFunction; +import io.trino.spi.function.SqlType; +import io.trino.spi.type.StandardTypes; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; + +import static io.trino.json.JsonInputErrorNode.JSON_ERROR; +import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR; +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Read string input as JSON. + *

+ * These functions are used by JSON_EXISTS, JSON_VALUE and JSON_QUERY functions + * for parsing the JSON input arguments and applicable JSON path parameters. + *

+ * If the error handling strategy of the enclosing JSON function is ERROR ON ERROR, + * these input functions throw exception in case of parse error. + * Otherwise, the parse error is suppressed, and a marker value JSON_ERROR + * is returned, so that the enclosing function can handle the error accordingly + * to its error handling strategy (e.g. return a default value). + */ +public final class JsonInputFunctions +{ + public static final String VARCHAR_TO_JSON = "$varchar_to_json"; + public static final String VARBINARY_TO_JSON = "$varbinary_to_json"; + public static final String VARBINARY_UTF8_TO_JSON = "$varbinary_utf8_to_json"; + public static final String VARBINARY_UTF16_TO_JSON = "$varbinary_utf16_to_json"; + public static final String VARBINARY_UTF32_TO_JSON = "$varbinary_utf32_to_json"; + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final Charset UTF_32LE = Charset.forName("UTF-32LE"); + + private JsonInputFunctions() {} + + @ScalarFunction(value = VARCHAR_TO_JSON, hidden = true) + @SqlType(StandardTypes.JSON_2016) + public static JsonNode varcharToJson(@SqlType(StandardTypes.VARCHAR) Slice inputExpression, @SqlType(StandardTypes.BOOLEAN) boolean failOnError) + { + Reader reader = new InputStreamReader(inputExpression.getInput(), UTF_8); + return toJson(reader, failOnError); + } + + @ScalarFunction(value = VARBINARY_TO_JSON, hidden = true) + @SqlType(StandardTypes.JSON_2016) + public static JsonNode varbinaryToJson(@SqlType(StandardTypes.VARBINARY) Slice inputExpression, @SqlType(StandardTypes.BOOLEAN) boolean failOnError) + { + return varbinaryUtf8ToJson(inputExpression, failOnError); + } + + @ScalarFunction(value = VARBINARY_UTF8_TO_JSON, hidden = true) + @SqlType(StandardTypes.JSON_2016) + public static JsonNode varbinaryUtf8ToJson(@SqlType(StandardTypes.VARBINARY) Slice inputExpression, @SqlType(StandardTypes.BOOLEAN) boolean failOnError) + { + Reader reader = new InputStreamReader(inputExpression.getInput(), UTF_8); + return toJson(reader, failOnError); + } + + @ScalarFunction(value = VARBINARY_UTF16_TO_JSON, hidden = true) + @SqlType(StandardTypes.JSON_2016) + public static JsonNode varbinaryUtf16ToJson(@SqlType(StandardTypes.VARBINARY) Slice inputExpression, @SqlType(StandardTypes.BOOLEAN) boolean failOnError) + { + Reader reader = new InputStreamReader(inputExpression.getInput(), UTF_16LE); + return toJson(reader, failOnError); + } + + @ScalarFunction(value = VARBINARY_UTF32_TO_JSON, hidden = true) + @SqlType(StandardTypes.JSON_2016) + public static JsonNode varbinaryUtf32ToJson(@SqlType(StandardTypes.VARBINARY) Slice inputExpression, @SqlType(StandardTypes.BOOLEAN) boolean failOnError) + { + Reader reader = new InputStreamReader(inputExpression.getInput(), UTF_32LE); + return toJson(reader, failOnError); + } + + private static JsonNode toJson(Reader reader, boolean failOnError) + { + try { + return MAPPER.readTree(reader); + } + catch (JsonProcessingException e) { + if (failOnError) { + throw new JsonInputConversionError(e); + } + return JSON_ERROR; + } + catch (IOException e) { + throw new TrinoException(GENERIC_INTERNAL_ERROR, e); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonOutputConversionError.java b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonOutputConversionError.java new file mode 100644 index 000000000000..3cec7b802337 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonOutputConversionError.java @@ -0,0 +1,32 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar.json; + +import io.trino.spi.TrinoException; + +import static io.trino.spi.StandardErrorCode.JSON_OUTPUT_CONVERSION_ERROR; + +public class JsonOutputConversionError + extends TrinoException +{ + public JsonOutputConversionError(String message) + { + super(JSON_OUTPUT_CONVERSION_ERROR, "conversion from JSON failed: " + message); + } + + public JsonOutputConversionError(Throwable cause) + { + super(JSON_OUTPUT_CONVERSION_ERROR, "conversion from JSON failed: ", cause); + } +} diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonOutputFunctions.java b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonOutputFunctions.java new file mode 100644 index 000000000000..3ba498f8abdd --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonOutputFunctions.java @@ -0,0 +1,168 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar.json; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.ByteArrayBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airlift.slice.Slice; +import io.airlift.slice.Slices; +import io.trino.spi.TrinoException; +import io.trino.spi.function.ScalarFunction; +import io.trino.spi.function.SqlNullable; +import io.trino.spi.function.SqlType; +import io.trino.spi.type.StandardTypes; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import static io.airlift.slice.Slices.wrappedBuffer; +import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR; +import static io.trino.sql.tree.JsonQuery.EmptyOrErrorBehavior.EMPTY_ARRAY; +import static io.trino.sql.tree.JsonQuery.EmptyOrErrorBehavior.EMPTY_OBJECT; +import static io.trino.sql.tree.JsonQuery.EmptyOrErrorBehavior.ERROR; +import static io.trino.sql.tree.JsonQuery.EmptyOrErrorBehavior.NULL; +import static java.util.Objects.requireNonNull; + +/** + * Format JSON as binary or character string, using given encoding. + *

+ * These functions are used to format the output of JSON_QUERY function. + * In case of error during JSON formatting, the error handling + * strategy of the enclosing JSON_QUERY function is applied. + *

+ * Additionally, the options KEEP / OMIT QUOTES [ON SCALAR STRING] + * are respected when formatting the output. + */ +public final class JsonOutputFunctions +{ + public static final String JSON_TO_VARCHAR = "$json_to_varchar"; + public static final String JSON_TO_VARBINARY = "$json_to_varbinary"; + public static final String JSON_TO_VARBINARY_UTF8 = "$json_to_varbinary_utf8"; + public static final String JSON_TO_VARBINARY_UTF16 = "$json_to_varbinary_utf16"; + public static final String JSON_TO_VARBINARY_UTF32 = "$json_to_varbinary_utf32"; + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final EncodingSpecificConstants UTF_8 = new EncodingSpecificConstants( + JsonEncoding.UTF8, + StandardCharsets.UTF_8, + Slices.copiedBuffer(new ArrayNode(JsonNodeFactory.instance).asText(), StandardCharsets.UTF_8), + Slices.copiedBuffer(new ObjectNode(JsonNodeFactory.instance).asText(), StandardCharsets.UTF_8)); + private static final EncodingSpecificConstants UTF_16 = new EncodingSpecificConstants( + JsonEncoding.UTF16_LE, + StandardCharsets.UTF_16LE, + Slices.copiedBuffer(new ArrayNode(JsonNodeFactory.instance).asText(), StandardCharsets.UTF_16LE), + Slices.copiedBuffer(new ObjectNode(JsonNodeFactory.instance).asText(), StandardCharsets.UTF_16LE)); + private static final EncodingSpecificConstants UTF_32 = new EncodingSpecificConstants( + JsonEncoding.UTF32_LE, + Charset.forName("UTF-32LE"), + Slices.copiedBuffer(new ArrayNode(JsonNodeFactory.instance).asText(), Charset.forName("UTF-32LE")), + Slices.copiedBuffer(new ObjectNode(JsonNodeFactory.instance).asText(), Charset.forName("UTF-32LE"))); + + private JsonOutputFunctions() {} + + @SqlNullable + @ScalarFunction(value = JSON_TO_VARCHAR, hidden = true) + @SqlType(StandardTypes.VARCHAR) + public static Slice jsonToVarchar(@SqlType(StandardTypes.JSON_2016) JsonNode jsonExpression, @SqlType(StandardTypes.TINYINT) long errorBehavior, @SqlType(StandardTypes.BOOLEAN) boolean omitQuotes) + { + return serialize(jsonExpression, UTF_8, errorBehavior, omitQuotes); + } + + @SqlNullable + @ScalarFunction(value = JSON_TO_VARBINARY, hidden = true) + @SqlType(StandardTypes.VARBINARY) + public static Slice jsonToVarbinary(@SqlType(StandardTypes.JSON_2016) JsonNode jsonExpression, @SqlType(StandardTypes.TINYINT) long errorBehavior, @SqlType(StandardTypes.BOOLEAN) boolean omitQuotes) + { + return jsonToVarbinaryUtf8(jsonExpression, errorBehavior, omitQuotes); + } + + @SqlNullable + @ScalarFunction(value = JSON_TO_VARBINARY_UTF8, hidden = true) + @SqlType(StandardTypes.VARBINARY) + public static Slice jsonToVarbinaryUtf8(@SqlType(StandardTypes.JSON_2016) JsonNode jsonExpression, @SqlType(StandardTypes.TINYINT) long errorBehavior, @SqlType(StandardTypes.BOOLEAN) boolean omitQuotes) + { + return serialize(jsonExpression, UTF_8, errorBehavior, omitQuotes); + } + + @SqlNullable + @ScalarFunction(value = JSON_TO_VARBINARY_UTF16, hidden = true) + @SqlType(StandardTypes.VARBINARY) + public static Slice jsonToVarbinaryUtf16(@SqlType(StandardTypes.JSON_2016) JsonNode jsonExpression, @SqlType(StandardTypes.TINYINT) long errorBehavior, @SqlType(StandardTypes.BOOLEAN) boolean omitQuotes) + { + return serialize(jsonExpression, UTF_16, errorBehavior, omitQuotes); + } + + @SqlNullable + @ScalarFunction(value = JSON_TO_VARBINARY_UTF32, hidden = true) + @SqlType(StandardTypes.VARBINARY) + public static Slice jsonToVarbinaryUtf32(@SqlType(StandardTypes.JSON_2016) JsonNode jsonExpression, @SqlType(StandardTypes.TINYINT) long errorBehavior, @SqlType(StandardTypes.BOOLEAN) boolean omitQuotes) + { + return serialize(jsonExpression, UTF_32, errorBehavior, omitQuotes); + } + + private static Slice serialize(JsonNode json, EncodingSpecificConstants constants, long errorBehavior, boolean omitQuotes) + { + if (omitQuotes && json.isTextual()) { + return Slices.copiedBuffer(json.asText(), constants.charset); + } + + ByteArrayBuilder builder = new ByteArrayBuilder(); + try (JsonGenerator generator = MAPPER.createGenerator(builder, constants.jsonEncoding)) { + MAPPER.writeTree(generator, json); + } + catch (JsonProcessingException e) { + if (errorBehavior == NULL.ordinal()) { + return null; + } + if (errorBehavior == ERROR.ordinal()) { + throw new JsonOutputConversionError(e); + } + if (errorBehavior == EMPTY_ARRAY.ordinal()) { + return constants.emptyArray; + } + if (errorBehavior == EMPTY_OBJECT.ordinal()) { + return constants.emptyObject; + } + throw new IllegalStateException("unexpected behavior"); + } + catch (IOException e) { + throw new TrinoException(GENERIC_INTERNAL_ERROR, e); + } + return wrappedBuffer(builder.toByteArray()); + } + + private static class EncodingSpecificConstants + { + private final JsonEncoding jsonEncoding; + private final Charset charset; + private final Slice emptyArray; + private final Slice emptyObject; + + public EncodingSpecificConstants(JsonEncoding jsonEncoding, Charset charset, Slice emptyArray, Slice emptyObject) + { + this.jsonEncoding = requireNonNull(jsonEncoding, "jsonEncoding is null"); + this.charset = requireNonNull(charset, "charset is null"); + this.emptyArray = requireNonNull(emptyArray, "emptyArray is null"); + this.emptyObject = requireNonNull(emptyObject, "emptyObject is null"); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonQueryFunction.java b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonQueryFunction.java new file mode 100644 index 000000000000..d75c0fe8ce4c --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonQueryFunction.java @@ -0,0 +1,224 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableList; +import io.trino.annotation.UsedByGeneratedCode; +import io.trino.json.JsonPathEvaluator; +import io.trino.json.JsonPathInvocationContext; +import io.trino.json.PathEvaluationError; +import io.trino.json.ir.IrJsonPath; +import io.trino.json.ir.TypedValue; +import io.trino.metadata.BoundSignature; +import io.trino.metadata.FunctionManager; +import io.trino.metadata.FunctionMetadata; +import io.trino.metadata.Metadata; +import io.trino.metadata.Signature; +import io.trino.metadata.SqlScalarFunction; +import io.trino.operator.scalar.ChoicesScalarFunctionImplementation; +import io.trino.operator.scalar.ScalarFunctionImplementation; +import io.trino.spi.TrinoException; +import io.trino.spi.block.Block; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.Type; +import io.trino.spi.type.TypeManager; +import io.trino.spi.type.TypeSignature; +import io.trino.sql.tree.JsonQuery.ArrayWrapperBehavior; +import io.trino.sql.tree.JsonQuery.EmptyOrErrorBehavior; +import io.trino.type.JsonPath2016Type; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Optional; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static io.trino.json.JsonInputErrorNode.JSON_ERROR; +import static io.trino.json.ir.SqlJsonLiteralConverter.getJsonNode; +import static io.trino.operator.scalar.json.ParameterUtil.getParametersArray; +import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.BOXED_NULLABLE; +import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.NEVER_NULL; +import static io.trino.spi.function.InvocationConvention.InvocationReturnConvention.NULLABLE_RETURN; +import static io.trino.spi.type.StandardTypes.JSON_2016; +import static io.trino.spi.type.StandardTypes.TINYINT; +import static io.trino.util.Reflection.constructorMethodHandle; +import static io.trino.util.Reflection.methodHandle; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class JsonQueryFunction + extends SqlScalarFunction +{ + public static final String JSON_QUERY_FUNCTION_NAME = "$json_query"; + private static final MethodHandle METHOD_HANDLE = methodHandle(JsonQueryFunction.class, "jsonQuery", FunctionManager.class, Metadata.class, TypeManager.class, Type.class, JsonPathInvocationContext.class, ConnectorSession.class, JsonNode.class, IrJsonPath.class, Block.class, long.class, long.class, long.class); + private static final JsonNode EMPTY_ARRAY_RESULT = new ArrayNode(JsonNodeFactory.instance); + private static final JsonNode EMPTY_OBJECT_RESULT = new ObjectNode(JsonNodeFactory.instance); + private static final TrinoException INPUT_ARGUMENT_ERROR = new JsonInputConversionError("malformed input argument to JSON_QUERY function"); + private static final TrinoException PATH_PARAMETER_ERROR = new JsonInputConversionError("malformed JSON path parameter to JSON_QUERY function"); + private static final TrinoException NO_ITEMS = new JsonOutputConversionError("JSON path found no items"); + private static final TrinoException MULTIPLE_ITEMS = new JsonOutputConversionError("JSON path found multiple items"); + + private final FunctionManager functionManager; + private final Metadata metadata; + private final TypeManager typeManager; + + public JsonQueryFunction(FunctionManager functionManager, Metadata metadata, TypeManager typeManager) + { + super(FunctionMetadata.scalarBuilder() + .signature(Signature.builder() + .name(JSON_QUERY_FUNCTION_NAME) + .typeVariable("T") + .returnType(new TypeSignature(JSON_2016)) + .argumentTypes(ImmutableList.of( + new TypeSignature(JSON_2016), + new TypeSignature(JsonPath2016Type.NAME), + new TypeSignature("T"), + new TypeSignature(TINYINT), + new TypeSignature(TINYINT), + new TypeSignature(TINYINT))) + .build()) + .nullable() + .argumentNullability(false, false, true, false, false, false) + .hidden() + .description("Extracts a JSON value from a JSON value") + .build()); + + this.functionManager = requireNonNull(functionManager, "functionManager is null"); + this.metadata = requireNonNull(metadata, "metadata is null"); + this.typeManager = requireNonNull(typeManager, "typeManager is null"); + } + + @Override + protected ScalarFunctionImplementation specialize(BoundSignature boundSignature) + { + Type parametersRowType = boundSignature.getArgumentType(2); + MethodHandle methodHandle = METHOD_HANDLE + .bindTo(functionManager) + .bindTo(metadata) + .bindTo(typeManager) + .bindTo(parametersRowType); + MethodHandle instanceFactory = constructorMethodHandle(JsonPathInvocationContext.class); + return new ChoicesScalarFunctionImplementation( + boundSignature, + NULLABLE_RETURN, + ImmutableList.of(BOXED_NULLABLE, BOXED_NULLABLE, BOXED_NULLABLE, NEVER_NULL, NEVER_NULL, NEVER_NULL), + methodHandle, + Optional.of(instanceFactory)); + } + + @UsedByGeneratedCode + public static JsonNode jsonQuery( + FunctionManager functionManager, + Metadata metadata, + TypeManager typeManager, + Type parametersRowType, + JsonPathInvocationContext invocationContext, + ConnectorSession session, + JsonNode inputExpression, + IrJsonPath jsonPath, + Block parametersRow, + long wrapperBehavior, + long emptyBehavior, + long errorBehavior) + { + if (inputExpression.equals(JSON_ERROR)) { + return handleSpecialCase(errorBehavior, INPUT_ARGUMENT_ERROR); // ERROR ON ERROR was already handled by the input function + } + Object[] parameters = getParametersArray(parametersRowType, parametersRow); + for (Object parameter : parameters) { + if (parameter.equals(JSON_ERROR)) { + return handleSpecialCase(errorBehavior, PATH_PARAMETER_ERROR); // ERROR ON ERROR was already handled by the input function + } + } + // The jsonPath argument is constant for every row. We use the first incoming jsonPath argument to initialize + // the JsonPathEvaluator, and ignore the subsequent jsonPath values. We could sanity-check that all the incoming + // jsonPath values are equal. We deliberately skip this costly check, since this is a hidden function. + JsonPathEvaluator evaluator = invocationContext.getEvaluator(); + if (evaluator == null) { + evaluator = new JsonPathEvaluator(jsonPath, session, metadata, typeManager, functionManager); + invocationContext.setEvaluator(evaluator); + } + List pathResult; + try { + pathResult = evaluator.evaluate(inputExpression, parameters); + } + catch (PathEvaluationError e) { + return handleSpecialCase(errorBehavior, e); + } + + // handle empty sequence + if (pathResult.isEmpty()) { + return handleSpecialCase(emptyBehavior, NO_ITEMS); + } + + // translate sequence to JSON items + List sequence = pathResult.stream() + .map(item -> { + if (item instanceof TypedValue) { + Optional jsonNode = getJsonNode((TypedValue) item); + if (jsonNode.isEmpty()) { + return handleSpecialCase(errorBehavior, new JsonOutputConversionError(format( + "JSON path returned a scalar SQL value of type %s that cannot be represented as JSON", + ((TypedValue) item).getType()))); + } + return jsonNode.get(); + } + return (JsonNode) item; + }) + .collect(toImmutableList()); + + // apply array wrapper behavior + switch (ArrayWrapperBehavior.values()[(int) wrapperBehavior]) { + case WITHOUT: + // do nothing + break; + case UNCONDITIONAL: + sequence = ImmutableList.of(new ArrayNode(JsonNodeFactory.instance, sequence)); + break; + case CONDITIONAL: + if (sequence.size() != 1 || (!sequence.get(0).isArray() && !sequence.get(0).isObject())) { + sequence = ImmutableList.of(new ArrayNode(JsonNodeFactory.instance, sequence)); + } + break; + default: + throw new IllegalStateException("unexpected array wrapper behavior"); + } + + // singleton sequence - return the only item + if (sequence.size() == 1) { + return sequence.get(0); + // if the only item is a TextNode, need to apply the KEEP / OMIT QUOTES behavior. this is done by the JSON output function + } + + return handleSpecialCase(errorBehavior, MULTIPLE_ITEMS); + } + + private static JsonNode handleSpecialCase(long behavior, TrinoException error) + { + switch (EmptyOrErrorBehavior.values()[(int) behavior]) { + case NULL: + return null; + case ERROR: + throw error; + case EMPTY_ARRAY: + return EMPTY_ARRAY_RESULT; + case EMPTY_OBJECT: + return EMPTY_OBJECT_RESULT; + } + throw new IllegalStateException("unexpected behavior"); + } +} diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonValueFunction.java b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonValueFunction.java new file mode 100644 index 000000000000..d337b5a33998 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/json/JsonValueFunction.java @@ -0,0 +1,352 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slice; +import io.trino.FullConnectorSession; +import io.trino.annotation.UsedByGeneratedCode; +import io.trino.json.JsonPathEvaluator; +import io.trino.json.JsonPathInvocationContext; +import io.trino.json.PathEvaluationError; +import io.trino.json.ir.IrJsonPath; +import io.trino.json.ir.SqlJsonLiteralConverter.JsonLiteralConversionError; +import io.trino.json.ir.TypedValue; +import io.trino.metadata.BoundSignature; +import io.trino.metadata.FunctionManager; +import io.trino.metadata.FunctionMetadata; +import io.trino.metadata.Metadata; +import io.trino.metadata.OperatorNotFoundException; +import io.trino.metadata.ResolvedFunction; +import io.trino.metadata.Signature; +import io.trino.metadata.SqlScalarFunction; +import io.trino.operator.scalar.ChoicesScalarFunctionImplementation; +import io.trino.operator.scalar.ScalarFunctionImplementation; +import io.trino.spi.TrinoException; +import io.trino.spi.block.Block; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.Type; +import io.trino.spi.type.TypeManager; +import io.trino.spi.type.TypeSignature; +import io.trino.sql.InterpretedFunctionInvoker; +import io.trino.sql.tree.JsonValue.EmptyOrErrorBehavior; +import io.trino.type.JsonPath2016Type; + +import java.lang.invoke.MethodHandle; +import java.util.List; +import java.util.Optional; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.trino.json.JsonInputErrorNode.JSON_ERROR; +import static io.trino.json.ir.SqlJsonLiteralConverter.getTypedValue; +import static io.trino.operator.scalar.json.ParameterUtil.getParametersArray; +import static io.trino.spi.StandardErrorCode.JSON_VALUE_RESULT_ERROR; +import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.BOXED_NULLABLE; +import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.NEVER_NULL; +import static io.trino.spi.function.InvocationConvention.InvocationReturnConvention.NULLABLE_RETURN; +import static io.trino.spi.type.StandardTypes.JSON_2016; +import static io.trino.spi.type.StandardTypes.TINYINT; +import static io.trino.util.Reflection.constructorMethodHandle; +import static io.trino.util.Reflection.methodHandle; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class JsonValueFunction + extends SqlScalarFunction +{ + public static final String JSON_VALUE_FUNCTION_NAME = "$json_value"; + private static final MethodHandle METHOD_HANDLE_LONG = methodHandle(JsonValueFunction.class, "jsonValueLong", FunctionManager.class, Metadata.class, TypeManager.class, Type.class, Type.class, JsonPathInvocationContext.class, ConnectorSession.class, JsonNode.class, IrJsonPath.class, Block.class, long.class, Long.class, long.class, Long.class); + private static final MethodHandle METHOD_HANDLE_DOUBLE = methodHandle(JsonValueFunction.class, "jsonValueDouble", FunctionManager.class, Metadata.class, TypeManager.class, Type.class, Type.class, JsonPathInvocationContext.class, ConnectorSession.class, JsonNode.class, IrJsonPath.class, Block.class, long.class, Double.class, long.class, Double.class); + private static final MethodHandle METHOD_HANDLE_BOOLEAN = methodHandle(JsonValueFunction.class, "jsonValueBoolean", FunctionManager.class, Metadata.class, TypeManager.class, Type.class, Type.class, JsonPathInvocationContext.class, ConnectorSession.class, JsonNode.class, IrJsonPath.class, Block.class, long.class, Boolean.class, long.class, Boolean.class); + private static final MethodHandle METHOD_HANDLE_SLICE = methodHandle(JsonValueFunction.class, "jsonValueSlice", FunctionManager.class, Metadata.class, TypeManager.class, Type.class, Type.class, JsonPathInvocationContext.class, ConnectorSession.class, JsonNode.class, IrJsonPath.class, Block.class, long.class, Slice.class, long.class, Slice.class); + private static final MethodHandle METHOD_HANDLE = methodHandle(JsonValueFunction.class, "jsonValue", FunctionManager.class, Metadata.class, TypeManager.class, Type.class, Type.class, JsonPathInvocationContext.class, ConnectorSession.class, JsonNode.class, IrJsonPath.class, Block.class, long.class, Object.class, long.class, Object.class); + private static final TrinoException INPUT_ARGUMENT_ERROR = new JsonInputConversionError("malformed input argument to JSON_VALUE function"); + private static final TrinoException PATH_PARAMETER_ERROR = new JsonInputConversionError("malformed JSON path parameter to JSON_VALUE function"); + private static final TrinoException NO_ITEMS = new JsonValueResultError("JSON path found no items"); + private static final TrinoException MULTIPLE_ITEMS = new JsonValueResultError("JSON path found multiple items"); + private static final TrinoException INCONVERTIBLE_ITEM = new JsonValueResultError("JSON path found an item that cannot be converted to an SQL value"); + + private final FunctionManager functionManager; + private final Metadata metadata; + private final TypeManager typeManager; + + public JsonValueFunction(FunctionManager functionManager, Metadata metadata, TypeManager typeManager) + { + super(FunctionMetadata.scalarBuilder() + .signature(Signature.builder() + .name(JSON_VALUE_FUNCTION_NAME) + .typeVariable("R") + .typeVariable("T") + .returnType(new TypeSignature("R")) + .argumentTypes(ImmutableList.of( + new TypeSignature(JSON_2016), + new TypeSignature(JsonPath2016Type.NAME), + new TypeSignature("T"), + new TypeSignature(TINYINT), + new TypeSignature("R"), + new TypeSignature(TINYINT), + new TypeSignature("R"))) + .build()) + .nullable() + .argumentNullability(false, false, true, false, true, false, true) + .hidden() + .description("Extracts an SQL scalar from a JSON value") + .build()); + + this.functionManager = requireNonNull(functionManager, "functionManager is null"); + this.metadata = requireNonNull(metadata, "metadata is null"); + this.typeManager = requireNonNull(typeManager, "typeManager is null"); + } + + @Override + protected ScalarFunctionImplementation specialize(BoundSignature boundSignature) + { + Type parametersRowType = boundSignature.getArgumentType(2); + Type returnType = boundSignature.getReturnType(); + MethodHandle handle; + if (returnType.getJavaType().equals(long.class)) { + handle = METHOD_HANDLE_LONG; + } + else if (returnType.getJavaType().equals(double.class)) { + handle = METHOD_HANDLE_DOUBLE; + } + else if (returnType.getJavaType().equals(boolean.class)) { + handle = METHOD_HANDLE_BOOLEAN; + } + else if (returnType.getJavaType().equals(Slice.class)) { + handle = METHOD_HANDLE_SLICE; + } + else { + handle = METHOD_HANDLE; + } + + MethodHandle methodHandle = handle + .bindTo(functionManager) + .bindTo(metadata) + .bindTo(typeManager) + .bindTo(parametersRowType) + .bindTo(returnType); + MethodHandle instanceFactory = constructorMethodHandle(JsonPathInvocationContext.class); + return new ChoicesScalarFunctionImplementation( + boundSignature, + NULLABLE_RETURN, + ImmutableList.of(BOXED_NULLABLE, BOXED_NULLABLE, BOXED_NULLABLE, NEVER_NULL, BOXED_NULLABLE, NEVER_NULL, BOXED_NULLABLE), + methodHandle, + Optional.of(instanceFactory)); + } + + @UsedByGeneratedCode + public static Long jsonValueLong( + FunctionManager functionManager, + Metadata metadata, + TypeManager typeManager, + Type parametersRowType, + Type returnType, + JsonPathInvocationContext invocationContext, + ConnectorSession session, + JsonNode inputExpression, + IrJsonPath jsonPath, + Block parametersRow, + long emptyBehavior, + Long emptyDefault, + long errorBehavior, + Long errorDefault) + { + return (Long) jsonValue(functionManager, metadata, typeManager, parametersRowType, returnType, invocationContext, session, inputExpression, jsonPath, parametersRow, emptyBehavior, emptyDefault, errorBehavior, errorDefault); + } + + @UsedByGeneratedCode + public static Double jsonValueDouble( + FunctionManager functionManager, + Metadata metadata, + TypeManager typeManager, + Type parametersRowType, + Type returnType, + JsonPathInvocationContext invocationContext, + ConnectorSession session, + JsonNode inputExpression, + IrJsonPath jsonPath, + Block parametersRow, + long emptyBehavior, + Double emptyDefault, + long errorBehavior, + Double errorDefault) + { + return (Double) jsonValue(functionManager, metadata, typeManager, parametersRowType, returnType, invocationContext, session, inputExpression, jsonPath, parametersRow, emptyBehavior, emptyDefault, errorBehavior, errorDefault); + } + + @UsedByGeneratedCode + public static Boolean jsonValueBoolean( + FunctionManager functionManager, + Metadata metadata, + TypeManager typeManager, + Type parametersRowType, + Type returnType, + JsonPathInvocationContext invocationContext, + ConnectorSession session, + JsonNode inputExpression, + IrJsonPath jsonPath, + Block parametersRow, + long emptyBehavior, + Boolean emptyDefault, + long errorBehavior, + Boolean errorDefault) + { + return (Boolean) jsonValue(functionManager, metadata, typeManager, parametersRowType, returnType, invocationContext, session, inputExpression, jsonPath, parametersRow, emptyBehavior, emptyDefault, errorBehavior, errorDefault); + } + + @UsedByGeneratedCode + public static Slice jsonValueSlice( + FunctionManager functionManager, + Metadata metadata, + TypeManager typeManager, + Type parametersRowType, + Type returnType, + JsonPathInvocationContext invocationContext, + ConnectorSession session, + JsonNode inputExpression, + IrJsonPath jsonPath, + Block parametersRow, + long emptyBehavior, + Slice emptyDefault, + long errorBehavior, + Slice errorDefault) + { + return (Slice) jsonValue(functionManager, metadata, typeManager, parametersRowType, returnType, invocationContext, session, inputExpression, jsonPath, parametersRow, emptyBehavior, emptyDefault, errorBehavior, errorDefault); + } + + @UsedByGeneratedCode + public static Object jsonValue( + FunctionManager functionManager, + Metadata metadata, + TypeManager typeManager, + Type parametersRowType, + Type returnType, + JsonPathInvocationContext invocationContext, + ConnectorSession session, + JsonNode inputExpression, + IrJsonPath jsonPath, + Block parametersRow, + long emptyBehavior, + Object emptyDefault, + long errorBehavior, + Object errorDefault) + { + if (inputExpression.equals(JSON_ERROR)) { + return handleSpecialCase(errorBehavior, errorDefault, INPUT_ARGUMENT_ERROR); // ERROR ON ERROR was already handled by the input function + } + Object[] parameters = getParametersArray(parametersRowType, parametersRow); + for (Object parameter : parameters) { + if (parameter.equals(JSON_ERROR)) { + return handleSpecialCase(errorBehavior, errorDefault, PATH_PARAMETER_ERROR); // ERROR ON ERROR was already handled by the input function + } + } + // The jsonPath argument is constant for every row. We use the first incoming jsonPath argument to initialize + // the JsonPathEvaluator, and ignore the subsequent jsonPath values. We could sanity-check that all the incoming + // jsonPath values are equal. We deliberately skip this costly check, since this is a hidden function. + JsonPathEvaluator evaluator = invocationContext.getEvaluator(); + if (evaluator == null) { + evaluator = new JsonPathEvaluator(jsonPath, session, metadata, typeManager, functionManager); + invocationContext.setEvaluator(evaluator); + } + List pathResult; + try { + pathResult = evaluator.evaluate(inputExpression, parameters); + } + catch (PathEvaluationError e) { + return handleSpecialCase(errorBehavior, errorDefault, e); // TODO by spec, we should cast the defaults only if they are used + } + + if (pathResult.isEmpty()) { + return handleSpecialCase(emptyBehavior, emptyDefault, NO_ITEMS); + } + + if (pathResult.size() > 1) { + return handleSpecialCase(errorBehavior, errorDefault, MULTIPLE_ITEMS); + } + + Object item = getOnlyElement(pathResult); + TypedValue typedValue; + if (item instanceof JsonNode) { + if (item.equals(NullNode.instance)) { + return null; + } + Optional itemValue; + try { + itemValue = getTypedValue((JsonNode) item); + } + catch (JsonLiteralConversionError e) { + return handleSpecialCase(errorBehavior, errorDefault, new JsonValueResultError("JSON path found an item that cannot be converted to an SQL value", e)); + } + if (itemValue.isEmpty()) { + return handleSpecialCase(errorBehavior, errorDefault, INCONVERTIBLE_ITEM); + } + typedValue = itemValue.get(); + } + else { + typedValue = (TypedValue) item; + } + if (returnType.equals(typedValue.getType())) { + return typedValue.getValueAsObject(); + } + ResolvedFunction coercion; + try { + coercion = metadata.getCoercion(((FullConnectorSession) session).getSession(), typedValue.getType(), returnType); + } + catch (OperatorNotFoundException e) { + return handleSpecialCase(errorBehavior, errorDefault, new JsonValueResultError(format( + "Cannot cast value of type %s to declared return type of function JSON_VALUE: %s", + typedValue.getType(), + returnType))); + } + try { + return new InterpretedFunctionInvoker(functionManager).invoke(coercion, session, ImmutableList.of(typedValue.getValueAsObject())); + } + catch (RuntimeException e) { + return handleSpecialCase(errorBehavior, errorDefault, new JsonValueResultError(format( + "Cannot cast value of type %s to declared return type of function JSON_VALUE: %s", + typedValue.getType(), + returnType))); + } + } + + private static Object handleSpecialCase(long behavior, Object defaultValue, TrinoException error) + { + switch (EmptyOrErrorBehavior.values()[(int) behavior]) { + case NULL: + return null; + case ERROR: + throw error; + case DEFAULT: + return defaultValue; + } + throw new IllegalStateException("unexpected behavior"); + } + + public static class JsonValueResultError + extends TrinoException + { + public JsonValueResultError(String message) + { + super(JSON_VALUE_RESULT_ERROR, "cannot extract SQL scalar from JSON: " + message); + } + + public JsonValueResultError(String message, Throwable cause) + { + super(JSON_VALUE_RESULT_ERROR, "cannot extract SQL scalar from JSON: " + message, cause); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/json/ParameterUtil.java b/core/trino-main/src/main/java/io/trino/operator/scalar/json/ParameterUtil.java new file mode 100644 index 000000000000..3275a2a6a534 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/json/ParameterUtil.java @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar.json; + +import com.fasterxml.jackson.databind.node.NullNode; +import io.trino.json.ir.TypedValue; +import io.trino.spi.block.Block; +import io.trino.spi.type.RowType; +import io.trino.spi.type.Type; +import io.trino.type.Json2016Type; + +import java.util.List; + +import static io.trino.json.JsonEmptySequenceNode.EMPTY_SEQUENCE; +import static io.trino.spi.type.TypeUtils.readNativeValue; +import static io.trino.sql.analyzer.ExpressionAnalyzer.JSON_NO_PARAMETERS_ROW_TYPE; + +public final class ParameterUtil +{ + private ParameterUtil() {} + + /** + * Converts the parameters passed to json path into appropriate values, + * respecting the proper SQL semantics for nulls in the context of + * a path parameter, and collects them in an array. + *

+ * All non-null values are passed as-is. Conversions apply in the following cases: + * - null value with FORMAT option is converted into an empty JSON sequence + * - null value without FORMAT option is converted into a JSON null. + * + * @param parametersRowType type of the Block containing parameters + * @param parametersRow a Block containing parameters + * @return an array containing the converted values + */ + public static Object[] getParametersArray(Type parametersRowType, Block parametersRow) + { + if (JSON_NO_PARAMETERS_ROW_TYPE.equals(parametersRowType)) { + return new Object[] {}; + } + + RowType rowType = (RowType) parametersRowType; + List parameterBlocks = parametersRow.getChildren(); + + Object[] array = new Object[rowType.getFields().size()]; + for (int i = 0; i < rowType.getFields().size(); i++) { + Type type = rowType.getFields().get(i).getType(); + Object value = readNativeValue(type, parameterBlocks.get(i), 0); + if (type.equals(Json2016Type.JSON_2016)) { + if (value == null) { + array[i] = EMPTY_SEQUENCE; // null as JSON value shall produce an empty sequence + } + else { + array[i] = value; + } + } + else if (value == null) { + array[i] = NullNode.getInstance(); // null as a non-JSON value shall produce a JSON null + } + else { + array[i] = TypedValue.fromValueAsObject(type, value); + } + } + + return array; + } +} diff --git a/core/trino-main/src/main/java/io/trino/server/ServerMainModule.java b/core/trino-main/src/main/java/io/trino/server/ServerMainModule.java index 060d2117e1d8..2e4fc888880b 100644 --- a/core/trino-main/src/main/java/io/trino/server/ServerMainModule.java +++ b/core/trino-main/src/main/java/io/trino/server/ServerMainModule.java @@ -101,6 +101,9 @@ import io.trino.operator.PagesIndexPageSorter; import io.trino.operator.TrinoOperatorFactories; import io.trino.operator.index.IndexJoinLookupStats; +import io.trino.operator.scalar.json.JsonExistsFunction; +import io.trino.operator.scalar.json.JsonQueryFunction; +import io.trino.operator.scalar.json.JsonValueFunction; import io.trino.server.ExpressionSerialization.ExpressionDeserializer; import io.trino.server.ExpressionSerialization.ExpressionSerializer; import io.trino.server.PluginManager.PluginsProvider; @@ -150,6 +153,7 @@ import io.trino.transaction.TransactionManagerConfig; import io.trino.type.BlockTypeOperators; import io.trino.type.InternalTypeManager; +import io.trino.type.JsonPath2016Type; import io.trino.type.TypeDeserializer; import io.trino.type.TypeOperatorsCache; import io.trino.type.TypeSignatureDeserializer; @@ -408,6 +412,7 @@ protected void setup(Binder binder) binder.bind(TypeRegistry.class).in(Scopes.SINGLETON); binder.bind(TypeManager.class).to(InternalTypeManager.class).in(Scopes.SINGLETON); newSetBinder(binder, Type.class); + binder.bind(RegisterJsonPath2016Type.class).asEagerSingleton(); // split manager binder.bind(SplitManager.class).in(Scopes.SINGLETON); @@ -526,6 +531,27 @@ public static FunctionBundle literalFunctionBundle(BlockEncodingSerde blockEncod return new InternalFunctionBundle(new LiteralFunction(blockEncodingSerde)); } + @ProvidesIntoSet + @Singleton + // not adding to system function bundle to avoid mutual dependency FunctionManager <-> MetadataManager in testing instance constructors + public static FunctionBundle jsonFunctionBundle(FunctionManager functionManager, Metadata metadata, TypeManager typeManager) + { + return new InternalFunctionBundle( + new JsonExistsFunction(functionManager, metadata, typeManager), + new JsonValueFunction(functionManager, metadata, typeManager), + new JsonQueryFunction(functionManager, metadata, typeManager)); + } + + // working around circular dependency Type <-> TypeManager + private static class RegisterJsonPath2016Type + { + @Inject + public RegisterJsonPath2016Type(BlockEncodingSerde blockEncodingSerde, TypeManager typeManager, TypeRegistry typeRegistry) + { + typeRegistry.addType(new JsonPath2016Type(new TypeDeserializer(typeManager), blockEncodingSerde)); + } + } + @Provides @Singleton public static TypeOperators createTypeOperators(TypeOperatorsCache typeOperatorsCache) diff --git a/core/trino-main/src/main/java/io/trino/sql/analyzer/AggregationAnalyzer.java b/core/trino-main/src/main/java/io/trino/sql/analyzer/AggregationAnalyzer.java index bb462c042aa3..244e8719a2ac 100644 --- a/core/trino-main/src/main/java/io/trino/sql/analyzer/AggregationAnalyzer.java +++ b/core/trino-main/src/main/java/io/trino/sql/analyzer/AggregationAnalyzer.java @@ -43,6 +43,11 @@ import io.trino.sql.tree.InPredicate; import io.trino.sql.tree.IsNotNullPredicate; import io.trino.sql.tree.IsNullPredicate; +import io.trino.sql.tree.JsonExists; +import io.trino.sql.tree.JsonPathInvocation; +import io.trino.sql.tree.JsonPathParameter; +import io.trino.sql.tree.JsonQuery; +import io.trino.sql.tree.JsonValue; import io.trino.sql.tree.LambdaExpression; import io.trino.sql.tree.LikePredicate; import io.trino.sql.tree.Literal; @@ -715,6 +720,35 @@ protected Boolean visitGroupingOperation(GroupingOperation node, Void context) return true; } + @Override + protected Boolean visitJsonExists(JsonExists node, Void context) + { + return process(node.getJsonPathInvocation(), context); + } + + @Override + protected Boolean visitJsonValue(JsonValue node, Void context) + { + return process(node.getJsonPathInvocation(), context) && + node.getEmptyDefault().map(expression -> process(expression, context)).orElse(true) && + node.getErrorDefault().map(expression -> process(expression, context)).orElse(true); + } + + @Override + protected Boolean visitJsonQuery(JsonQuery node, Void context) + { + return process(node.getJsonPathInvocation(), context); + } + + @Override + protected Boolean visitJsonPathInvocation(JsonPathInvocation node, Void context) + { + return process(node.getInputExpression(), context) && + node.getPathParameters().stream() + .map(JsonPathParameter::getParameter) + .allMatch(expression -> process(expression, context)); + } + @Override public Boolean process(Node node, @Nullable Void context) { diff --git a/core/trino-main/src/main/java/io/trino/sql/analyzer/Analysis.java b/core/trino-main/src/main/java/io/trino/sql/analyzer/Analysis.java index 8c630b74ced1..7a7490ab6a2d 100644 --- a/core/trino-main/src/main/java/io/trino/sql/analyzer/Analysis.java +++ b/core/trino-main/src/main/java/io/trino/sql/analyzer/Analysis.java @@ -45,6 +45,7 @@ import io.trino.spi.security.Identity; import io.trino.spi.type.Type; import io.trino.sql.analyzer.ExpressionAnalyzer.LabelPrefixedReference; +import io.trino.sql.analyzer.JsonPathAnalyzer.JsonPathAnalysis; import io.trino.sql.tree.AllColumns; import io.trino.sql.tree.DereferenceExpression; import io.trino.sql.tree.ExistsPredicate; @@ -152,6 +153,11 @@ public class Analysis private final Set> patternAggregations = new LinkedHashSet<>(); + // for JSON features + private final Map, JsonPathAnalysis> jsonPathAnalyses = new LinkedHashMap<>(); + private final Map, ResolvedFunction> jsonInputFunctions = new LinkedHashMap<>(); + private final Map, ResolvedFunction> jsonOutputFunctions = new LinkedHashMap<>(); + private final Map, List> aggregates = new LinkedHashMap<>(); private final Map, List> orderByAggregates = new LinkedHashMap<>(); private final Map, GroupingSetAnalysis> groupingSets = new LinkedHashMap<>(); @@ -980,6 +986,36 @@ public boolean isPatternAggregation(FunctionCall function) return patternAggregations.contains(NodeRef.of(function)); } + public void setJsonPathAnalyses(Map, JsonPathAnalysis> pathAnalyses) + { + jsonPathAnalyses.putAll(pathAnalyses); + } + + public JsonPathAnalysis getJsonPathAnalysis(Expression expression) + { + return jsonPathAnalyses.get(NodeRef.of(expression)); + } + + public void setJsonInputFunctions(Map, ResolvedFunction> functions) + { + jsonInputFunctions.putAll(functions); + } + + public ResolvedFunction getJsonInputFunction(Expression expression) + { + return jsonInputFunctions.get(NodeRef.of(expression)); + } + + public void setJsonOutputFunctions(Map, ResolvedFunction> functions) + { + jsonOutputFunctions.putAll(functions); + } + + public ResolvedFunction getJsonOutputFunction(Expression expression) + { + return jsonOutputFunctions.get(NodeRef.of(expression)); + } + public Map>> getTableColumnReferences() { return tableColumnReferences; diff --git a/core/trino-main/src/main/java/io/trino/sql/analyzer/ExpressionAnalyzer.java b/core/trino-main/src/main/java/io/trino/sql/analyzer/ExpressionAnalyzer.java index 667067f868f5..4ab4e8a05ed3 100644 --- a/core/trino-main/src/main/java/io/trino/sql/analyzer/ExpressionAnalyzer.java +++ b/core/trino-main/src/main/java/io/trino/sql/analyzer/ExpressionAnalyzer.java @@ -48,6 +48,7 @@ import io.trino.spi.type.TimestampType; import io.trino.spi.type.TimestampWithTimeZoneType; import io.trino.spi.type.Type; +import io.trino.spi.type.TypeId; import io.trino.spi.type.TypeNotFoundException; import io.trino.spi.type.TypeSignatureParameter; import io.trino.spi.type.VarcharType; @@ -55,6 +56,7 @@ import io.trino.sql.analyzer.Analysis.PredicateCoercions; import io.trino.sql.analyzer.Analysis.Range; import io.trino.sql.analyzer.Analysis.ResolvedWindow; +import io.trino.sql.analyzer.JsonPathAnalyzer.JsonPathAnalysis; import io.trino.sql.analyzer.PatternRecognitionAnalyzer.PatternRecognitionAnalysis; import io.trino.sql.planner.LiteralInterpreter; import io.trino.sql.planner.Symbol; @@ -95,6 +97,12 @@ import io.trino.sql.tree.IntervalLiteral; import io.trino.sql.tree.IsNotNullPredicate; import io.trino.sql.tree.IsNullPredicate; +import io.trino.sql.tree.JsonExists; +import io.trino.sql.tree.JsonPathInvocation; +import io.trino.sql.tree.JsonPathParameter; +import io.trino.sql.tree.JsonPathParameter.JsonFormat; +import io.trino.sql.tree.JsonQuery; +import io.trino.sql.tree.JsonValue; import io.trino.sql.tree.LambdaArgumentDeclaration; import io.trino.sql.tree.LambdaExpression; import io.trino.sql.tree.LikePredicate; @@ -132,6 +140,7 @@ import io.trino.sql.tree.WindowFrame; import io.trino.sql.tree.WindowOperation; import io.trino.type.FunctionType; +import io.trino.type.JsonPath2016Type; import io.trino.type.TypeCoercion; import io.trino.type.UnknownType; @@ -158,8 +167,22 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static io.trino.collect.cache.CacheUtils.uncheckedCacheGet; import static io.trino.collect.cache.SafeCaches.buildNonEvictableCache; +import static io.trino.operator.scalar.json.JsonExistsFunction.JSON_EXISTS_FUNCTION_NAME; +import static io.trino.operator.scalar.json.JsonInputFunctions.VARBINARY_TO_JSON; +import static io.trino.operator.scalar.json.JsonInputFunctions.VARBINARY_UTF16_TO_JSON; +import static io.trino.operator.scalar.json.JsonInputFunctions.VARBINARY_UTF32_TO_JSON; +import static io.trino.operator.scalar.json.JsonInputFunctions.VARBINARY_UTF8_TO_JSON; +import static io.trino.operator.scalar.json.JsonInputFunctions.VARCHAR_TO_JSON; +import static io.trino.operator.scalar.json.JsonOutputFunctions.JSON_TO_VARBINARY; +import static io.trino.operator.scalar.json.JsonOutputFunctions.JSON_TO_VARBINARY_UTF16; +import static io.trino.operator.scalar.json.JsonOutputFunctions.JSON_TO_VARBINARY_UTF32; +import static io.trino.operator.scalar.json.JsonOutputFunctions.JSON_TO_VARBINARY_UTF8; +import static io.trino.operator.scalar.json.JsonOutputFunctions.JSON_TO_VARCHAR; +import static io.trino.operator.scalar.json.JsonQueryFunction.JSON_QUERY_FUNCTION_NAME; +import static io.trino.operator.scalar.json.JsonValueFunction.JSON_VALUE_FUNCTION_NAME; import static io.trino.spi.StandardErrorCode.AMBIGUOUS_NAME; import static io.trino.spi.StandardErrorCode.COLUMN_NOT_FOUND; +import static io.trino.spi.StandardErrorCode.DUPLICATE_PARAMETER_NAME; import static io.trino.spi.StandardErrorCode.EXPRESSION_NOT_CONSTANT; import static io.trino.spi.StandardErrorCode.FUNCTION_NOT_AGGREGATE; import static io.trino.spi.StandardErrorCode.INVALID_ARGUMENTS; @@ -221,6 +244,9 @@ import static io.trino.sql.tree.FrameBound.Type.PRECEDING; import static io.trino.sql.tree.FrameBound.Type.UNBOUNDED_FOLLOWING; import static io.trino.sql.tree.FrameBound.Type.UNBOUNDED_PRECEDING; +import static io.trino.sql.tree.JsonQuery.ArrayWrapperBehavior.CONDITIONAL; +import static io.trino.sql.tree.JsonQuery.ArrayWrapperBehavior.UNCONDITIONAL; +import static io.trino.sql.tree.JsonValue.EmptyOrErrorBehavior.DEFAULT; import static io.trino.sql.tree.SortItem.Ordering.ASCENDING; import static io.trino.sql.tree.SortItem.Ordering.DESCENDING; import static io.trino.sql.tree.WindowFrame.Type.GROUPS; @@ -237,6 +263,7 @@ import static io.trino.type.DateTimes.timestampHasTimeZone; import static io.trino.type.IntervalDayTimeType.INTERVAL_DAY_TIME; import static io.trino.type.IntervalYearMonthType.INTERVAL_YEAR_MONTH; +import static io.trino.type.Json2016Type.JSON_2016; import static io.trino.type.JsonType.JSON; import static io.trino.type.UnknownType.UNKNOWN; import static java.lang.Math.toIntExact; @@ -251,6 +278,8 @@ public class ExpressionAnalyzer private static final int MAX_NUMBER_GROUPING_ARGUMENTS_BIGINT = 63; private static final int MAX_NUMBER_GROUPING_ARGUMENTS_INTEGER = 31; + public static final RowType JSON_NO_PARAMETERS_ROW_TYPE = RowType.anonymous(ImmutableList.of(UNKNOWN)); + private final PlannerContext plannerContext; private final AccessControl accessControl; private final BiFunction statementAnalyzerFactory; @@ -299,6 +328,11 @@ public class ExpressionAnalyzer private final Map, MeasureDefinition> measureDefinitions = new LinkedHashMap<>(); private final Set> patternAggregations = new LinkedHashSet<>(); + // for JSON functions + private final Map, JsonPathAnalysis> jsonPathAnalyses = new LinkedHashMap<>(); + private final Map, ResolvedFunction> jsonInputFunctions = new LinkedHashMap<>(); + private final Map, ResolvedFunction> jsonOutputFunctions = new LinkedHashMap<>(); + private final Session session; private final Map, Expression> parameters; private final WarningCollector warningCollector; @@ -523,6 +557,21 @@ public Set> getPatternAggregations() return patternAggregations; } + public Map, JsonPathAnalysis> getJsonPathAnalyses() + { + return jsonPathAnalyses; + } + + public Map, ResolvedFunction> getJsonInputFunctions() + { + return jsonInputFunctions; + } + + public Map, ResolvedFunction> getJsonOutputFunctions() + { + return jsonOutputFunctions; + } + private class Visitor extends StackableAstVisitor { @@ -1767,8 +1816,8 @@ private ArgumentLabel validateLabelConsistency(FunctionCall node, boolean labelR String name = node.getName().getSuffix(); List unlabeledInputColumns = Streams.concat( - extractExpressions(ImmutableList.of(node.getArguments().get(argumentIndex)), Identifier.class).stream(), - extractExpressions(ImmutableList.of(node.getArguments().get(argumentIndex)), DereferenceExpression.class).stream()) + extractExpressions(ImmutableList.of(node.getArguments().get(argumentIndex)), Identifier.class).stream(), + extractExpressions(ImmutableList.of(node.getArguments().get(argumentIndex)), DereferenceExpression.class).stream()) .filter(expression -> columnReferences.containsKey(NodeRef.of(expression))) .collect(toImmutableList()); List labeledInputColumns = extractExpressions(ImmutableList.of(node.getArguments().get(argumentIndex)), DereferenceExpression.class).stream() @@ -2496,6 +2545,370 @@ public Type visitGroupingOperation(GroupingOperation node, StackableAstVisitorCo } } + @Override + public Type visitJsonExists(JsonExists node, StackableAstVisitorContext context) + { + List pathInvocationArgumentTypes = analyzeJsonPathInvocation("JSON_EXISTS", node, node.getJsonPathInvocation(), context); + + // pass remaining information in the node : error behavior + List argumentTypes = ImmutableList.builder() + .addAll(pathInvocationArgumentTypes) + .add(TINYINT) // enum encoded as integer value + .build(); + + // resolve function + ResolvedFunction function; + try { + function = plannerContext.getMetadata().resolveFunction(session, QualifiedName.of(JSON_EXISTS_FUNCTION_NAME), fromTypes(argumentTypes)); + } + catch (TrinoException e) { + if (e.getLocation().isPresent()) { + throw e; + } + throw new TrinoException(e::getErrorCode, extractLocation(node), e.getMessage(), e); + } + accessControl.checkCanExecuteFunction(SecurityContext.of(session), JSON_EXISTS_FUNCTION_NAME); + resolvedFunctions.put(NodeRef.of(node), function); + Type type = function.getSignature().getReturnType(); + + return setExpressionType(node, type); + } + + @Override + public Type visitJsonValue(JsonValue node, StackableAstVisitorContext context) + { + List pathInvocationArgumentTypes = analyzeJsonPathInvocation("JSON_VALUE", node, node.getJsonPathInvocation(), context); + + // validate returned type + Type returnedType = VARCHAR; // default + if (node.getReturnedType().isPresent()) { + try { + returnedType = plannerContext.getTypeManager().getType(toTypeSignature(node.getReturnedType().get())); + } + catch (TypeNotFoundException e) { + throw semanticException(TYPE_MISMATCH, node, "Unknown type: %s", node.getReturnedType().get()); + } + } + + if (!isCharacterStringType(returnedType) && + !isNumericType(returnedType) && + !returnedType.equals(BOOLEAN) && + !isDateTimeType(returnedType) || + returnedType.equals(INTERVAL_DAY_TIME) || + returnedType.equals(INTERVAL_YEAR_MONTH)) { + throw semanticException(TYPE_MISMATCH, node, "Invalid return type of function JSON_VALUE: " + node.getReturnedType().get()); + } + + JsonPathAnalysis pathAnalysis = jsonPathAnalyses.get(NodeRef.of(node)); + Type resultType = pathAnalysis.getType(pathAnalysis.getPath()); + if (resultType != null && !resultType.equals(returnedType)) { + try { + plannerContext.getMetadata().getCoercion(session, resultType, returnedType); + } + catch (OperatorNotFoundException e) { + throw semanticException(TYPE_MISMATCH, node, "Return type of JSON path: %s incompatible with return type of function JSON_VALUE: %s", resultType, returnedType); + } + } + + // validate default values for empty and error behavior + if (node.getEmptyDefault().isPresent()) { + Expression emptyDefault = node.getEmptyDefault().get(); + if (node.getEmptyBehavior() != DEFAULT) { + throw semanticException(INVALID_FUNCTION_ARGUMENT, emptyDefault, "Default value specified for %s ON EMPTY behavior", node.getEmptyBehavior()); + } + Type type = process(emptyDefault, context); + // this would normally be done after function resolution, but we know that the default expression is always coerced to the returnedType + coerceType(emptyDefault, type, returnedType, "Function JSON_VALUE default ON EMPTY result"); + } + + if (node.getErrorDefault().isPresent()) { + Expression errorDefault = node.getErrorDefault().get(); + if (node.getErrorBehavior() != DEFAULT) { + throw semanticException(INVALID_FUNCTION_ARGUMENT, errorDefault, "Default value specified for %s ON ERROR behavior", node.getErrorBehavior()); + } + Type type = process(errorDefault, context); + // this would normally be done after function resolution, but we know that the default expression is always coerced to the returnedType + coerceType(errorDefault, type, returnedType, "Function JSON_VALUE default ON ERROR result"); + } + + // pass remaining information in the node : empty behavior, empty default, error behavior, error default + List argumentTypes = ImmutableList.builder() + .addAll(pathInvocationArgumentTypes) + .add(TINYINT) // empty behavior: enum encoded as integer value + .add(returnedType) // empty default + .add(TINYINT) // error behavior: enum encoded as integer value + .add(returnedType) // error default + .build(); + + // resolve function + ResolvedFunction function; + try { + function = plannerContext.getMetadata().resolveFunction(session, QualifiedName.of(JSON_VALUE_FUNCTION_NAME), fromTypes(argumentTypes)); + } + catch (TrinoException e) { + if (e.getLocation().isPresent()) { + throw e; + } + throw new TrinoException(e::getErrorCode, extractLocation(node), e.getMessage(), e); + } + + accessControl.checkCanExecuteFunction(SecurityContext.of(session), JSON_VALUE_FUNCTION_NAME); + resolvedFunctions.put(NodeRef.of(node), function); + Type type = function.getSignature().getReturnType(); + + return setExpressionType(node, type); + } + + @Override + public Type visitJsonQuery(JsonQuery node, StackableAstVisitorContext context) + { + List pathInvocationArgumentTypes = analyzeJsonPathInvocation("JSON_QUERY", node, node.getJsonPathInvocation(), context); + + // validate wrapper and quotes behavior + if ((node.getWrapperBehavior() == CONDITIONAL || node.getWrapperBehavior() == UNCONDITIONAL) && node.getQuotesBehavior().isPresent()) { + throw semanticException(INVALID_FUNCTION_ARGUMENT, node, "%s QUOTES behavior specified with WITH %s ARRAY WRAPPER behavior", node.getQuotesBehavior().get(), node.getWrapperBehavior()); + } + + // wrapper behavior, empty behavior and error behavior will be passed as arguments to function + // quotes behavior is handled by the corresponding output function + List argumentTypes = ImmutableList.builder() + .addAll(pathInvocationArgumentTypes) + .add(TINYINT) // wrapper behavior: enum encoded as integer value + .add(TINYINT) // empty behavior: enum encoded as integer value + .add(TINYINT) // error behavior: enum encoded as integer value + .build(); + + // resolve function + ResolvedFunction function; + try { + function = plannerContext.getMetadata().resolveFunction(session, QualifiedName.of(JSON_QUERY_FUNCTION_NAME), fromTypes(argumentTypes)); + } + catch (TrinoException e) { + if (e.getLocation().isPresent()) { + throw e; + } + throw new TrinoException(e::getErrorCode, extractLocation(node), e.getMessage(), e); + } + accessControl.checkCanExecuteFunction(SecurityContext.of(session), JSON_QUERY_FUNCTION_NAME); + resolvedFunctions.put(NodeRef.of(node), function); + + // analyze returned type and format + Type returnedType = VARCHAR; // default + if (node.getReturnedType().isPresent()) { + try { + returnedType = plannerContext.getTypeManager().getType(toTypeSignature(node.getReturnedType().get())); + } + catch (TypeNotFoundException e) { + throw semanticException(TYPE_MISMATCH, node, "Unknown type: %s", node.getReturnedType().get()); + } + } + JsonFormat outputFormat = node.getOutputFormat().orElse(JsonFormat.JSON); // default + + // resolve function to format output + ResolvedFunction outputFunction = getOutputFunction(returnedType, outputFormat, node); + jsonOutputFunctions.put(NodeRef.of(node), outputFunction); + + // cast the output value to the declared returned type if necessary + Type outputType = outputFunction.getSignature().getReturnType(); + if (!outputType.equals(returnedType)) { + try { + plannerContext.getMetadata().getCoercion(session, outputType, returnedType); + } + catch (OperatorNotFoundException e) { + throw semanticException(TYPE_MISMATCH, node, "Cannot cast %s to %s", outputType, returnedType); + } + } + + return setExpressionType(node, returnedType); + } + + private List analyzeJsonPathInvocation(String functionName, Expression node, JsonPathInvocation jsonPathInvocation, StackableAstVisitorContext context) + { + // ANALYZE THE CONTEXT ITEM + // analyze context item type + Expression inputExpression = jsonPathInvocation.getInputExpression(); + Type inputType = process(inputExpression, context); + + // resolve function to read the context item as JSON + JsonFormat inputFormat = jsonPathInvocation.getInputFormat(); + ResolvedFunction inputFunction = getInputFunction(inputType, inputFormat, inputExpression); + Type expectedType = inputFunction.getSignature().getArgumentType(0); + coerceType(inputExpression, inputType, expectedType, format("%s function input argument", functionName)); + jsonInputFunctions.put(NodeRef.of(inputExpression), inputFunction); + + // ANALYZE JSON PATH PARAMETERS + // TODO verify parameter count? Is there a limit on Row size? + + ImmutableMap.Builder types = ImmutableMap.builder(); // record parameter types for JSON path analysis + Set uniqueNames = new HashSet<>(); // validate parameter names + + // this node will be translated into a FunctionCall, and all the information it carries will be passed as arguments to the FunctionCall. + // all JSON path parameters are wrapped in a Row, and constitute a single FunctionCall argument. + ImmutableList.Builder fields = ImmutableList.builder(); + + List pathParameters = jsonPathInvocation.getPathParameters(); + for (JsonPathParameter pathParameter : pathParameters) { + Expression parameter = pathParameter.getParameter(); + String parameterName = pathParameter.getName().getCanonicalValue(); + Optional parameterFormat = pathParameter.getFormat(); + + // type of the parameter passed to the JSON path: + // - parameters of types numeric, string, boolean, date,... are passed as-is + // - parameters with explicit or implicit FORMAT, are converted to JSON (type JSON_2016) + // - all other parameters are cast to VARCHAR + Type passedType; + + if (!uniqueNames.add(parameterName)) { + throw semanticException(DUPLICATE_PARAMETER_NAME, pathParameter.getName(), "%s JSON path parameter is specified more than once", parameterName); + } + + if (parameter instanceof LambdaExpression || parameter instanceof BindExpression) { + throw semanticException(NOT_SUPPORTED, parameter, "%s is not supported as JSON path parameter", parameter.getClass().getSimpleName()); + } + // if the input expression is a JSON-returning function, there should be an explicit or implicit input format (spec p.817) + // JSON-returning functions are: JSON_OBJECT, JSON_OBJECTAGG, JSON_ARRAY, JSON_ARRAYAGG and JSON_QUERY + if (parameter instanceof JsonQuery && // TODO add JSON_OBJECT, JSON_OBJECTAGG, JSON_ARRAY, JSON_ARRAYAGG when supported + parameterFormat.isEmpty()) { + parameterFormat = Optional.of(JsonFormat.JSON); + } + + Type parameterType = process(parameter, context); + if (parameterFormat.isPresent()) { + // resolve function to read the parameter as JSON + ResolvedFunction parameterInputFunction = getInputFunction(parameterType, parameterFormat.get(), parameter); + Type expectedParameterType = parameterInputFunction.getSignature().getArgumentType(0); + coerceType(parameter, parameterType, expectedParameterType, format("%s function JSON path parameter", functionName)); + jsonInputFunctions.put(NodeRef.of(parameter), parameterInputFunction); + passedType = JSON_2016; + } + else { + if (isStringType(parameterType)) { + if (!isCharacterStringType(parameterType)) { + throw semanticException(NOT_SUPPORTED, parameter, "Unsupported type of JSON path parameter: %s", parameterType.getDisplayName()); + } + passedType = parameterType; + } + else if (isNumericType(parameterType) || parameterType.equals(BOOLEAN)) { + passedType = parameterType; + } + else if (isDateTimeType(parameterType)) { + if (parameterType.equals(INTERVAL_DAY_TIME) || parameterType.equals(INTERVAL_YEAR_MONTH)) { + throw semanticException(INVALID_FUNCTION_ARGUMENT, parameter, "Invalid type of JSON path parameter: %s", parameterType.getDisplayName()); + } + passedType = parameterType; + } + else { + if (!typeCoercion.canCoerce(parameterType, VARCHAR)) { + throw semanticException(INVALID_FUNCTION_ARGUMENT, parameter, "Invalid type of JSON path parameter: %s", parameterType.getDisplayName()); + } + coerceType(parameter, parameterType, VARCHAR, "JSON path parameter"); + passedType = VARCHAR; + } + } + + types.put(parameterName, passedType); + fields.add(new RowType.Field(Optional.of(parameterName), passedType)); + } + + Type parametersRowType = JSON_NO_PARAMETERS_ROW_TYPE; + if (!pathParameters.isEmpty()) { + parametersRowType = RowType.from(fields.build()); + } + + // ANALYZE JSON PATH + Map typesMap = types.buildOrThrow(); + JsonPathAnalysis pathAnalysis = new JsonPathAnalyzer( + plannerContext.getMetadata(), + session, + createConstantAnalyzer(plannerContext, accessControl, session, ExpressionAnalyzer.this.parameters, WarningCollector.NOOP)) + .analyzeJsonPath(jsonPathInvocation.getJsonPath(), typesMap); + jsonPathAnalyses.put(NodeRef.of(node), pathAnalysis); + + return ImmutableList.of( + JSON_2016, // input expression + plannerContext.getTypeManager().getType(TypeId.of(JsonPath2016Type.NAME)), // parsed JSON path representation + parametersRowType); // passed parameters + } + + private ResolvedFunction getInputFunction(Type type, JsonFormat format, Node node) + { + QualifiedName name; + switch (format) { + case JSON: + if (UNKNOWN.equals(type) || isCharacterStringType(type)) { + name = QualifiedName.of(VARCHAR_TO_JSON); + } + else if (isStringType(type)) { + name = QualifiedName.of(VARBINARY_TO_JSON); + } + else { + throw semanticException(TYPE_MISMATCH, node, format("Cannot read input of type %s as JSON using formatting %s", type, format)); + } + break; + case UTF8: + name = QualifiedName.of(VARBINARY_UTF8_TO_JSON); + break; + case UTF16: + name = QualifiedName.of(VARBINARY_UTF16_TO_JSON); + break; + case UTF32: + name = QualifiedName.of(VARBINARY_UTF32_TO_JSON); + break; + default: + throw new UnsupportedOperationException("Unexpected format: " + format); + } + try { + return plannerContext.getMetadata().resolveFunction(session, name, fromTypes(type, BOOLEAN)); + } + catch (TrinoException e) { + throw new TrinoException(TYPE_MISMATCH, extractLocation(node), format("Cannot read input of type %s as JSON using formatting %s", type, format), e); + } + } + + private ResolvedFunction getOutputFunction(Type type, JsonFormat format, Node node) + { + QualifiedName name; + switch (format) { + case JSON: + if (isCharacterStringType(type)) { + name = QualifiedName.of(JSON_TO_VARCHAR); + } + else if (isStringType(type)) { + name = QualifiedName.of(JSON_TO_VARBINARY); + } + else { + throw semanticException(TYPE_MISMATCH, node, format("Cannot output JSON value as %s using formatting %s", type, format)); + } + break; + case UTF8: + if (!VARBINARY.equals(type)) { + throw semanticException(TYPE_MISMATCH, node, format("Cannot output JSON value as %s using formatting %s", type, format)); + } + name = QualifiedName.of(JSON_TO_VARBINARY_UTF8); + break; + case UTF16: + if (!VARBINARY.equals(type)) { + throw semanticException(TYPE_MISMATCH, node, format("Cannot output JSON value as %s using formatting %s", type, format)); + } + name = QualifiedName.of(JSON_TO_VARBINARY_UTF16); + break; + case UTF32: + if (!VARBINARY.equals(type)) { + throw semanticException(TYPE_MISMATCH, node, format("Cannot output JSON value as %s using formatting %s", type, format)); + } + name = QualifiedName.of(JSON_TO_VARBINARY_UTF32); + break; + default: + throw new UnsupportedOperationException("Unexpected format: " + format); + } + try { + return plannerContext.getMetadata().resolveFunction(session, name, fromTypes(JSON_2016, TINYINT, BOOLEAN)); + } + catch (TrinoException e) { + throw new TrinoException(TYPE_MISMATCH, extractLocation(node), format("Cannot output JSON value as %s using formatting %s", type, format), e); + } + } + private Type getOperator(StackableAstVisitorContext context, Expression node, OperatorType operatorType, Expression... arguments) { ImmutableList.Builder argumentTypes = ImmutableList.builder(); @@ -2889,6 +3302,9 @@ private static void updateAnalysis(Analysis analysis, ExpressionAnalyzer analyze analysis.setUndefinedLabels(analyzer.getUndefinedLabels()); analysis.setMeasureDefinitions(analyzer.getMeasureDefinitions()); analysis.setPatternAggregations(analyzer.getPatternAggregations()); + analysis.setJsonPathAnalyses(analyzer.getJsonPathAnalyses()); + analysis.setJsonInputFunctions(analyzer.getJsonInputFunctions()); + analysis.setJsonOutputFunctions(analyzer.getJsonOutputFunctions()); analysis.addPredicateCoercions(analyzer.getPredicateCoercions()); } @@ -2999,6 +3415,16 @@ private static boolean isExactNumericWithScaleZero(Type type) type instanceof DecimalType && ((DecimalType) type).getScale() == 0; } + public static boolean isStringType(Type type) + { + return isCharacterStringType(type) || VARBINARY.equals(type); + } + + public static boolean isCharacterStringType(Type type) + { + return type instanceof VarcharType || type instanceof CharType; + } + public static class LabelPrefixedReference { private final String label; diff --git a/core/trino-main/src/main/java/io/trino/sql/analyzer/JsonPathAnalyzer.java b/core/trino-main/src/main/java/io/trino/sql/analyzer/JsonPathAnalyzer.java new file mode 100644 index 000000000000..5e904f72890d --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/sql/analyzer/JsonPathAnalyzer.java @@ -0,0 +1,535 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.analyzer; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.trino.Session; +import io.trino.metadata.BoundSignature; +import io.trino.metadata.Metadata; +import io.trino.metadata.OperatorNotFoundException; +import io.trino.spi.TrinoException; +import io.trino.spi.function.OperatorType; +import io.trino.spi.type.Type; +import io.trino.sql.jsonpath.PathNodeRef; +import io.trino.sql.jsonpath.PathParser; +import io.trino.sql.jsonpath.PathParser.Location; +import io.trino.sql.jsonpath.tree.AbsMethod; +import io.trino.sql.jsonpath.tree.ArithmeticBinary; +import io.trino.sql.jsonpath.tree.ArithmeticUnary; +import io.trino.sql.jsonpath.tree.ArrayAccessor; +import io.trino.sql.jsonpath.tree.ArrayAccessor.Subscript; +import io.trino.sql.jsonpath.tree.CeilingMethod; +import io.trino.sql.jsonpath.tree.ComparisonPredicate; +import io.trino.sql.jsonpath.tree.ConjunctionPredicate; +import io.trino.sql.jsonpath.tree.ContextVariable; +import io.trino.sql.jsonpath.tree.DatetimeMethod; +import io.trino.sql.jsonpath.tree.DisjunctionPredicate; +import io.trino.sql.jsonpath.tree.DoubleMethod; +import io.trino.sql.jsonpath.tree.ExistsPredicate; +import io.trino.sql.jsonpath.tree.Filter; +import io.trino.sql.jsonpath.tree.FloorMethod; +import io.trino.sql.jsonpath.tree.IsUnknownPredicate; +import io.trino.sql.jsonpath.tree.JsonNullLiteral; +import io.trino.sql.jsonpath.tree.JsonPath; +import io.trino.sql.jsonpath.tree.JsonPathTreeVisitor; +import io.trino.sql.jsonpath.tree.KeyValueMethod; +import io.trino.sql.jsonpath.tree.LastIndexVariable; +import io.trino.sql.jsonpath.tree.LikeRegexPredicate; +import io.trino.sql.jsonpath.tree.MemberAccessor; +import io.trino.sql.jsonpath.tree.NamedVariable; +import io.trino.sql.jsonpath.tree.NegationPredicate; +import io.trino.sql.jsonpath.tree.PathNode; +import io.trino.sql.jsonpath.tree.PredicateCurrentItemVariable; +import io.trino.sql.jsonpath.tree.SizeMethod; +import io.trino.sql.jsonpath.tree.SqlValueLiteral; +import io.trino.sql.jsonpath.tree.StartsWithPredicate; +import io.trino.sql.jsonpath.tree.TypeMethod; +import io.trino.sql.tree.Node; +import io.trino.sql.tree.QualifiedName; +import io.trino.sql.tree.StringLiteral; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkState; +import static io.trino.spi.StandardErrorCode.INVALID_PATH; +import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; +import static io.trino.spi.function.OperatorType.NEGATION; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.VarcharType.createVarcharType; +import static io.trino.sql.analyzer.ExpressionAnalyzer.isCharacterStringType; +import static io.trino.sql.analyzer.ExpressionAnalyzer.isNumericType; +import static io.trino.sql.analyzer.ExpressionAnalyzer.isStringType; +import static io.trino.sql.analyzer.ExpressionTreeUtils.extractLocation; +import static io.trino.sql.analyzer.SemanticExceptions.semanticException; +import static io.trino.sql.analyzer.TypeSignatureProvider.fromTypes; +import static io.trino.sql.jsonpath.tree.ArithmeticUnary.Sign.PLUS; +import static io.trino.type.Json2016Type.JSON_2016; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class JsonPathAnalyzer +{ + // the type() method returns a textual description of type as determined by the SQL standard, of length lower or equal to 27 + private static final Type TYPE_METHOD_RESULT_TYPE = createVarcharType(27); + + private final Metadata metadata; + private final Session session; + private final ExpressionAnalyzer literalAnalyzer; + private final Map, Type> types = new LinkedHashMap<>(); + private final Set> jsonParameters = new LinkedHashSet<>(); + + public JsonPathAnalyzer(Metadata metadata, Session session, ExpressionAnalyzer literalAnalyzer) + { + this.metadata = requireNonNull(metadata, "metadata is null"); + this.session = requireNonNull(session, "session is null"); + this.literalAnalyzer = requireNonNull(literalAnalyzer, "literalAnalyzer is null"); + } + + public JsonPathAnalysis analyzeJsonPath(StringLiteral path, Map parameterTypes) + { + Location pathStart = extractLocation(path) + .map(location -> new Location(location.getLineNumber(), location.getColumnNumber())) + .orElseThrow(() -> new IllegalStateException("missing NodeLocation in path")); + PathNode root = new PathParser(pathStart).parseJsonPath(path.getValue()); + new Visitor(parameterTypes, path).process(root); + return new JsonPathAnalysis((JsonPath) root, types, jsonParameters); + } + + /** + * This visitor determines and validates output types of PathNodes, whenever they can be deduced and represented as SQL types. + * In some cases, the type of a PathNode can be determined without context. E.g., the `double()` method always returns DOUBLE. + * In some other cases, the type depends on child nodes. E.g. the return type of the `abs()` method is the same as input type. + * In some cases, the type cannot be represented as SQL type. E.g. the `keyValue()` method returns JSON objects. + * Some PathNodes, including accessors, return objects whose types might or might not be representable as SQL types, + * but that cannot be determined upfront. + */ + private class Visitor + extends JsonPathTreeVisitor + { + private final Map parameterTypes; + private final Node pathNode; + + public Visitor(Map parameterTypes, Node pathNode) + { + this.parameterTypes = ImmutableMap.copyOf(requireNonNull(parameterTypes, "parameterTypes is null")); + this.pathNode = requireNonNull(pathNode, "pathNode is null"); + } + + @Override + protected Type visitPathNode(PathNode node, Void context) + { + throw new UnsupportedOperationException("not supported JSON path node: " + node.getClass().getSimpleName()); + } + + @Override + protected Type visitAbsMethod(AbsMethod node, Void context) + { + Type sourceType = process(node.getBase()); + if (sourceType != null) { + Type resultType; + try { + resultType = metadata.resolveFunction(session, QualifiedName.of("abs"), fromTypes(sourceType)).getSignature().getReturnType(); + } + catch (TrinoException e) { + throw semanticException(INVALID_PATH, pathNode, e, "cannot perform JSON path abs() method with %s argument: %s", sourceType.getDisplayName(), e.getMessage()); + } + types.put(PathNodeRef.of(node), resultType); + return resultType; + } + + return null; + } + + @Override + protected Type visitArithmeticBinary(ArithmeticBinary node, Void context) + { + Type leftType = process(node.getLeft()); + Type rightType = process(node.getRight()); + if (leftType != null && rightType != null) { + BoundSignature signature; + try { + signature = metadata.resolveOperator(session, OperatorType.valueOf(node.getOperator().name()), ImmutableList.of(leftType, rightType)).getSignature(); + } + catch (OperatorNotFoundException e) { + throw semanticException(INVALID_PATH, pathNode, e, "invalid operand types (%s and %s) in JSON path arithmetic binary expression: %s", leftType.getDisplayName(), rightType.getDisplayName(), e.getMessage()); + } + Type resultType = signature.getReturnType(); + types.put(PathNodeRef.of(node), resultType); + return resultType; + } + + return null; + } + + @Override + protected Type visitArithmeticUnary(ArithmeticUnary node, Void context) + { + Type sourceType = process(node.getBase()); + if (sourceType != null) { + if (node.getSign() == PLUS) { + if (!isNumericType(sourceType)) { + throw semanticException(INVALID_PATH, pathNode, "Invalid operand type (%s) in JSON path arithmetic unary expression", sourceType.getDisplayName()); + } + types.put(PathNodeRef.of(node), sourceType); + return sourceType; + } + Type resultType; + try { + resultType = metadata.resolveOperator(session, NEGATION, ImmutableList.of(sourceType)).getSignature().getReturnType(); + } + catch (OperatorNotFoundException e) { + throw semanticException(INVALID_PATH, pathNode, e, "invalid operand type (%s) in JSON path arithmetic unary expression: %s", sourceType.getDisplayName(), e.getMessage()); + } + types.put(PathNodeRef.of(node), resultType); + return resultType; + } + + return null; + } + + @Override + protected Type visitArrayAccessor(ArrayAccessor node, Void context) + { + process(node.getBase()); + for (Subscript subscript : node.getSubscripts()) { + process(subscript.getFrom()); + subscript.getTo().ifPresent(this::process); + } + + return null; + } + + @Override + protected Type visitCeilingMethod(CeilingMethod node, Void context) + { + Type sourceType = process(node.getBase()); + if (sourceType != null) { + Type resultType; + try { + resultType = metadata.resolveFunction(session, QualifiedName.of("ceiling"), fromTypes(sourceType)).getSignature().getReturnType(); + } + catch (TrinoException e) { + throw semanticException(INVALID_PATH, pathNode, e, "cannot perform JSON path ceiling() method with %s argument: %s", sourceType.getDisplayName(), e.getMessage()); + } + types.put(PathNodeRef.of(node), resultType); + return resultType; + } + + return null; + } + + @Override + protected Type visitContextVariable(ContextVariable node, Void context) + { + return null; + } + + @Override + protected Type visitDatetimeMethod(DatetimeMethod node, Void context) + { + Type sourceType = process(node.getBase()); + if (sourceType != null && !isCharacterStringType(sourceType)) { + throw semanticException(INVALID_PATH, pathNode, "JSON path datetime() method requires character string argument (found %s)", sourceType.getDisplayName()); + } + // TODO process the format template, record the processed format, and deduce the returned type + throw semanticException(NOT_SUPPORTED, pathNode, "datetime method in JSON path is not yet supported"); + } + + @Override + protected Type visitDoubleMethod(DoubleMethod node, Void context) + { + Type sourceType = process(node.getBase()); + if (sourceType != null) { + if (!isStringType(sourceType) && !isNumericType(sourceType)) { + throw semanticException(INVALID_PATH, pathNode, "cannot perform JSON path double() method with %s argument", sourceType.getDisplayName()); + } + try { + metadata.getCoercion(session, sourceType, DOUBLE); + } + catch (OperatorNotFoundException e) { + throw semanticException(INVALID_PATH, pathNode, e, "cannot perform JSON path double() method with %s argument: %s", sourceType.getDisplayName(), e.getMessage()); + } + } + + types.put(PathNodeRef.of(node), DOUBLE); + return DOUBLE; + } + + @Override + protected Type visitFilter(Filter node, Void context) + { + Type sourceType = process(node.getBase()); + Type predicateType = process(node.getPredicate()); + + requireNonNull(predicateType, "missing type of predicate expression"); + checkState(predicateType.equals(BOOLEAN), "invalid type of predicate expression: " + predicateType.getDisplayName()); + + if (sourceType != null) { + types.put(PathNodeRef.of(node), sourceType); + return sourceType; + } + + return null; + } + + @Override + protected Type visitFloorMethod(FloorMethod node, Void context) + { + Type sourceType = process(node.getBase()); + if (sourceType != null) { + Type resultType; + try { + resultType = metadata.resolveFunction(session, QualifiedName.of("floor"), fromTypes(sourceType)).getSignature().getReturnType(); + } + catch (TrinoException e) { + throw semanticException(INVALID_PATH, pathNode, e, "cannot perform JSON path floor() method with %s argument: %s", sourceType.getDisplayName(), e.getMessage()); + } + types.put(PathNodeRef.of(node), resultType); + return resultType; + } + + return null; + } + + @Override + protected Type visitJsonNullLiteral(JsonNullLiteral node, Void context) + { + return null; + } + + @Override + protected Type visitJsonPath(JsonPath node, Void context) + { + Type type = process(node.getRoot()); + if (type != null) { + types.put(PathNodeRef.of(node), type); + } + return type; + } + + @Override + protected Type visitKeyValueMethod(KeyValueMethod node, Void context) + { + process(node.getBase()); + return null; + } + + @Override + protected Type visitLastIndexVariable(LastIndexVariable node, Void context) + { + types.put(PathNodeRef.of(node), INTEGER); + return INTEGER; + } + + @Override + protected Type visitMemberAccessor(MemberAccessor node, Void context) + { + process(node.getBase()); + return null; + } + + @Override + protected Type visitNamedVariable(NamedVariable node, Void context) + { + Type parameterType = parameterTypes.get(node.getName()); + if (parameterType == null) { + // This condition might be caused by the unintuitive semantics: + // identifiers in JSON path are case-sensitive, while non-delimited identifiers in SQL are upper-cased. + // Hence, a function call like JSON_VALUE(x, 'lax $var.floor()` PASSING 2.5 AS var) + // is an error, since the variable name is "var", and the passed parameter name is "VAR". + // We try to identify such situation and produce an explanatory message. + Optional similarName = parameterTypes.keySet().stream() + .filter(name -> name.equalsIgnoreCase(node.getName())) + .findFirst(); + if (similarName.isPresent()) { + throw semanticException(INVALID_PATH, pathNode, format("no value passed for parameter %s. Try quoting \"%s\" in the PASSING clause to match case", node.getName(), node.getName())); + } + throw semanticException(INVALID_PATH, pathNode, "no value passed for parameter " + node.getName()); + } + + if (parameterType.equals(JSON_2016)) { + jsonParameters.add(PathNodeRef.of(node)); + return null; + } + + // in case of a non-JSON named variable, the type cannot be recorded and used as the result type of the node + // this is because any incoming null value shall be transformed into a JSON null, which is out of the SQL type system. + // however, for any incoming non-null value, the type will be preserved. + return null; + } + + @Override + protected Type visitPredicateCurrentItemVariable(PredicateCurrentItemVariable node, Void context) + { + return null; + } + + @Override + protected Type visitSizeMethod(SizeMethod node, Void context) + { + process(node.getBase()); + types.put(PathNodeRef.of(node), INTEGER); + return INTEGER; + } + + @Override + protected Type visitSqlValueLiteral(SqlValueLiteral node, Void context) + { + Type type = literalAnalyzer.analyze(node.getValue(), Scope.create()); + types.put(PathNodeRef.of(node), type); + return type; + } + + @Override + protected Type visitTypeMethod(TypeMethod node, Void context) + { + process(node.getBase()); + Type type = TYPE_METHOD_RESULT_TYPE; + types.put(PathNodeRef.of(node), type); + return type; + } + + // predicate + + @Override + protected Type visitComparisonPredicate(ComparisonPredicate node, Void context) + { + process(node.getLeft()); + process(node.getRight()); + types.put(PathNodeRef.of(node), BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitConjunctionPredicate(ConjunctionPredicate node, Void context) + { + Type leftType = process(node.getLeft()); + requireNonNull(leftType, "missing type of predicate expression"); + checkState(leftType.equals(BOOLEAN), "invalid type of predicate expression: " + leftType.getDisplayName()); + + Type rightType = process(node.getRight()); + requireNonNull(rightType, "missing type of predicate expression"); + checkState(rightType.equals(BOOLEAN), "invalid type of predicate expression: " + rightType.getDisplayName()); + + types.put(PathNodeRef.of(node), BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitDisjunctionPredicate(DisjunctionPredicate node, Void context) + { + Type leftType = process(node.getLeft()); + requireNonNull(leftType, "missing type of predicate expression"); + checkState(leftType.equals(BOOLEAN), "invalid type of predicate expression: " + leftType.getDisplayName()); + + Type rightType = process(node.getRight()); + requireNonNull(rightType, "missing type of predicate expression"); + checkState(rightType.equals(BOOLEAN), "invalid type of predicate expression: " + rightType.getDisplayName()); + + types.put(PathNodeRef.of(node), BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitExistsPredicate(ExistsPredicate node, Void context) + { + process(node.getPath()); + types.put(PathNodeRef.of(node), BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitLikeRegexPredicate(LikeRegexPredicate node, Void context) + { + throw semanticException(NOT_SUPPORTED, pathNode, "like_regex predicate in JSON path is not yet supported"); + // TODO when like_regex is supported, this method should do the following: + // process(node.getPath()); + // types.put(PathNodeRef.of(node), BOOLEAN); + // return BOOLEAN; + } + + @Override + protected Type visitNegationPredicate(NegationPredicate node, Void context) + { + Type predicateType = process(node.getPredicate()); + requireNonNull(predicateType, "missing type of predicate expression"); + checkState(predicateType.equals(BOOLEAN), "invalid type of predicate expression: " + predicateType.getDisplayName()); + + types.put(PathNodeRef.of(node), BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitStartsWithPredicate(StartsWithPredicate node, Void context) + { + process(node.getWhole()); + process(node.getInitial()); + types.put(PathNodeRef.of(node), BOOLEAN); + return BOOLEAN; + } + + @Override + protected Type visitIsUnknownPredicate(IsUnknownPredicate node, Void context) + { + Type predicateType = process(node.getPredicate()); + requireNonNull(predicateType, "missing type of predicate expression"); + checkState(predicateType.equals(BOOLEAN), "invalid type of predicate expression: " + predicateType.getDisplayName()); + + types.put(PathNodeRef.of(node), BOOLEAN); + return BOOLEAN; + } + } + + public static class JsonPathAnalysis + { + private final JsonPath path; + private final Map, Type> types; + private final Set> jsonParameters; + + public JsonPathAnalysis(JsonPath path, Map, Type> types, Set> jsonParameters) + { + this.path = requireNonNull(path, "path is null"); + this.types = ImmutableMap.copyOf(requireNonNull(types, "types is null")); + this.jsonParameters = ImmutableSet.copyOf(requireNonNull(jsonParameters, "jsonParameters is null")); + } + + public JsonPath getPath() + { + return path; + } + + public Type getType(PathNode pathNode) + { + return types.get(PathNodeRef.of(pathNode)); + } + + public Map, Type> getTypes() + { + return types; + } + + public Set> getJsonParameters() + { + return jsonParameters; + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/JsonPathTranslator.java b/core/trino-main/src/main/java/io/trino/sql/planner/JsonPathTranslator.java new file mode 100644 index 000000000000..276418214051 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/sql/planner/JsonPathTranslator.java @@ -0,0 +1,424 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.planner; + +import io.trino.Session; +import io.trino.json.ir.IrAbsMethod; +import io.trino.json.ir.IrArithmeticBinary; +import io.trino.json.ir.IrArithmeticBinary.Operator; +import io.trino.json.ir.IrArithmeticUnary; +import io.trino.json.ir.IrArithmeticUnary.Sign; +import io.trino.json.ir.IrArrayAccessor; +import io.trino.json.ir.IrArrayAccessor.Subscript; +import io.trino.json.ir.IrCeilingMethod; +import io.trino.json.ir.IrComparisonPredicate; +import io.trino.json.ir.IrConjunctionPredicate; +import io.trino.json.ir.IrContextVariable; +import io.trino.json.ir.IrDisjunctionPredicate; +import io.trino.json.ir.IrDoubleMethod; +import io.trino.json.ir.IrExistsPredicate; +import io.trino.json.ir.IrFilter; +import io.trino.json.ir.IrFloorMethod; +import io.trino.json.ir.IrIsUnknownPredicate; +import io.trino.json.ir.IrJsonNull; +import io.trino.json.ir.IrJsonPath; +import io.trino.json.ir.IrKeyValueMethod; +import io.trino.json.ir.IrLastIndexVariable; +import io.trino.json.ir.IrLiteral; +import io.trino.json.ir.IrMemberAccessor; +import io.trino.json.ir.IrNamedJsonVariable; +import io.trino.json.ir.IrNamedValueVariable; +import io.trino.json.ir.IrNegationPredicate; +import io.trino.json.ir.IrPathNode; +import io.trino.json.ir.IrPredicate; +import io.trino.json.ir.IrPredicateCurrentItemVariable; +import io.trino.json.ir.IrSizeMethod; +import io.trino.json.ir.IrStartsWithPredicate; +import io.trino.json.ir.IrTypeMethod; +import io.trino.spi.type.Type; +import io.trino.sql.PlannerContext; +import io.trino.sql.analyzer.JsonPathAnalyzer.JsonPathAnalysis; +import io.trino.sql.jsonpath.PathNodeRef; +import io.trino.sql.jsonpath.tree.AbsMethod; +import io.trino.sql.jsonpath.tree.ArithmeticBinary; +import io.trino.sql.jsonpath.tree.ArithmeticUnary; +import io.trino.sql.jsonpath.tree.ArrayAccessor; +import io.trino.sql.jsonpath.tree.CeilingMethod; +import io.trino.sql.jsonpath.tree.ComparisonPredicate; +import io.trino.sql.jsonpath.tree.ConjunctionPredicate; +import io.trino.sql.jsonpath.tree.ContextVariable; +import io.trino.sql.jsonpath.tree.DatetimeMethod; +import io.trino.sql.jsonpath.tree.DisjunctionPredicate; +import io.trino.sql.jsonpath.tree.DoubleMethod; +import io.trino.sql.jsonpath.tree.ExistsPredicate; +import io.trino.sql.jsonpath.tree.Filter; +import io.trino.sql.jsonpath.tree.FloorMethod; +import io.trino.sql.jsonpath.tree.IsUnknownPredicate; +import io.trino.sql.jsonpath.tree.JsonNullLiteral; +import io.trino.sql.jsonpath.tree.JsonPathTreeVisitor; +import io.trino.sql.jsonpath.tree.KeyValueMethod; +import io.trino.sql.jsonpath.tree.LastIndexVariable; +import io.trino.sql.jsonpath.tree.LikeRegexPredicate; +import io.trino.sql.jsonpath.tree.MemberAccessor; +import io.trino.sql.jsonpath.tree.NamedVariable; +import io.trino.sql.jsonpath.tree.NegationPredicate; +import io.trino.sql.jsonpath.tree.PathNode; +import io.trino.sql.jsonpath.tree.PredicateCurrentItemVariable; +import io.trino.sql.jsonpath.tree.SizeMethod; +import io.trino.sql.jsonpath.tree.SqlValueLiteral; +import io.trino.sql.jsonpath.tree.StartsWithPredicate; +import io.trino.sql.jsonpath.tree.TypeMethod; +import io.trino.sql.tree.Expression; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static io.trino.json.ir.IrArithmeticBinary.Operator.ADD; +import static io.trino.json.ir.IrArithmeticBinary.Operator.DIVIDE; +import static io.trino.json.ir.IrArithmeticBinary.Operator.MODULUS; +import static io.trino.json.ir.IrArithmeticBinary.Operator.MULTIPLY; +import static io.trino.json.ir.IrArithmeticBinary.Operator.SUBTRACT; +import static io.trino.json.ir.IrArithmeticUnary.Sign.MINUS; +import static io.trino.json.ir.IrArithmeticUnary.Sign.PLUS; +import static io.trino.json.ir.IrComparisonPredicate.Operator.EQUAL; +import static io.trino.json.ir.IrComparisonPredicate.Operator.GREATER_THAN; +import static io.trino.json.ir.IrComparisonPredicate.Operator.GREATER_THAN_OR_EQUAL; +import static io.trino.json.ir.IrComparisonPredicate.Operator.LESS_THAN; +import static io.trino.json.ir.IrComparisonPredicate.Operator.LESS_THAN_OR_EQUAL; +import static io.trino.json.ir.IrComparisonPredicate.Operator.NOT_EQUAL; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static java.util.Objects.requireNonNull; + +class JsonPathTranslator +{ + private final Session session; + private final PlannerContext plannerContext; + + public JsonPathTranslator(Session session, PlannerContext plannerContext) + { + this.session = requireNonNull(session, "session is null"); + this.plannerContext = requireNonNull(plannerContext, "plannerContext is null"); + } + + public IrJsonPath rewriteToIr(JsonPathAnalysis pathAnalysis, List parametersOrder) + { + PathNode root = pathAnalysis.getPath().getRoot(); + IrPathNode rewritten = new Rewriter(session, plannerContext, pathAnalysis.getTypes(), pathAnalysis.getJsonParameters(), parametersOrder).process(root); + + return new IrJsonPath(pathAnalysis.getPath().isLax(), rewritten); + } + + private static class Rewriter + extends JsonPathTreeVisitor + { + private final LiteralInterpreter literalInterpreter; + private final Map, Type> types; + private final Set> jsonParameters; + private final List parametersOrder; + + public Rewriter(Session session, PlannerContext plannerContext, Map, Type> types, Set> jsonParameters, List parametersOrder) + { + requireNonNull(session, "session is null"); + requireNonNull(plannerContext, "plannerContext is null"); + requireNonNull(types, "types is null"); + requireNonNull(jsonParameters, "jsonParameters is null"); + requireNonNull(jsonParameters, "jsonParameters is null"); + requireNonNull(parametersOrder, "parametersOrder is null"); + + this.literalInterpreter = new LiteralInterpreter(plannerContext, session); + this.types = types; + this.jsonParameters = jsonParameters; + this.parametersOrder = parametersOrder; + } + + @Override + protected IrPathNode visitPathNode(PathNode node, Void context) + { + throw new UnsupportedOperationException("rewrite not implemented for " + node.getClass().getSimpleName()); + } + + @Override + protected IrPathNode visitAbsMethod(AbsMethod node, Void context) + { + IrPathNode base = process(node.getBase()); + return new IrAbsMethod(base, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitArithmeticBinary(ArithmeticBinary node, Void context) + { + IrPathNode left = process(node.getLeft()); + IrPathNode right = process(node.getRight()); + return new IrArithmeticBinary(binaryOperator(node.getOperator()), left, right, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + private Operator binaryOperator(ArithmeticBinary.Operator operator) + { + switch (operator) { + case ADD: + return ADD; + case SUBTRACT: + return SUBTRACT; + case MULTIPLY: + return MULTIPLY; + case DIVIDE: + return DIVIDE; + case MODULUS: + return MODULUS; + } + throw new UnsupportedOperationException("Unexpected operator: " + operator); + } + + @Override + protected IrPathNode visitArithmeticUnary(ArithmeticUnary node, Void context) + { + IrPathNode base = process(node.getBase()); + Sign sign; + switch (node.getSign()) { + case PLUS: + sign = PLUS; + break; + case MINUS: + sign = MINUS; + break; + default: + throw new UnsupportedOperationException("Unexpected sign: " + node.getSign()); + } + return new IrArithmeticUnary(sign, base, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitArrayAccessor(ArrayAccessor node, Void context) + { + IrPathNode base = process(node.getBase()); + List subscripts = node.getSubscripts().stream() + .map(subscript -> { + IrPathNode from = process(subscript.getFrom()); + Optional to = subscript.getTo().map(this::process); + return new Subscript(from, to); + }) + .collect(toImmutableList()); + return new IrArrayAccessor(base, subscripts, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitCeilingMethod(CeilingMethod node, Void context) + { + IrPathNode base = process(node.getBase()); + return new IrCeilingMethod(base, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitContextVariable(ContextVariable node, Void context) + { + return new IrContextVariable(Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitDatetimeMethod(DatetimeMethod node, Void context) + { + // TODO + throw new IllegalStateException("datetime method is not yet supported. The query should have failed in JsonPathAnalyzer."); + +// IrPathNode base = process(node.getBase()); +// return new IrDatetimeMethod(base, /*parsed format*/, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitDoubleMethod(DoubleMethod node, Void context) + { + IrPathNode base = process(node.getBase()); + return new IrDoubleMethod(base, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitFilter(Filter node, Void context) + { + IrPathNode base = process(node.getBase()); + IrPredicate predicate = (IrPredicate) process(node.getPredicate()); + return new IrFilter(base, predicate, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitFloorMethod(FloorMethod node, Void context) + { + IrPathNode base = process(node.getBase()); + return new IrFloorMethod(base, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitJsonNullLiteral(JsonNullLiteral node, Void context) + { + return new IrJsonNull(); + } + + @Override + protected IrPathNode visitKeyValueMethod(KeyValueMethod node, Void context) + { + IrPathNode base = process(node.getBase()); + return new IrKeyValueMethod(base); + } + + @Override + protected IrPathNode visitLastIndexVariable(LastIndexVariable node, Void context) + { + return new IrLastIndexVariable(Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitMemberAccessor(MemberAccessor node, Void context) + { + IrPathNode base = process(node.getBase()); + return new IrMemberAccessor(base, node.getKey(), Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitNamedVariable(NamedVariable node, Void context) + { + if (jsonParameters.contains(PathNodeRef.of(node))) { + return new IrNamedJsonVariable(parametersOrder.indexOf(node.getName()), Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + return new IrNamedValueVariable(parametersOrder.indexOf(node.getName()), Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitPredicateCurrentItemVariable(PredicateCurrentItemVariable node, Void context) + { + return new IrPredicateCurrentItemVariable(Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitSizeMethod(SizeMethod node, Void context) + { + IrPathNode base = process(node.getBase()); + return new IrSizeMethod(base, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitSqlValueLiteral(SqlValueLiteral node, Void context) + { + Expression value = node.getValue(); + return new IrLiteral(types.get(PathNodeRef.of(node)), literalInterpreter.evaluate(value, types.get(PathNodeRef.of(node)))); + } + + @Override + protected IrPathNode visitTypeMethod(TypeMethod node, Void context) + { + IrPathNode base = process(node.getBase()); + return new IrTypeMethod(base, Optional.ofNullable(types.get(PathNodeRef.of(node)))); + } + + // predicate + + @Override + protected IrPathNode visitComparisonPredicate(ComparisonPredicate node, Void context) + { + checkArgument(BOOLEAN.equals(types.get(PathNodeRef.of(node))), "Wrong predicate type. Expected BOOLEAN"); + + IrPathNode left = process(node.getLeft()); + IrPathNode right = process(node.getRight()); + IrComparisonPredicate.Operator operator = comparisonOperator(node.getOperator()); + return new IrComparisonPredicate(operator, left, right); + } + + private IrComparisonPredicate.Operator comparisonOperator(ComparisonPredicate.Operator operator) + { + switch (operator) { + case EQUAL: + return EQUAL; + case NOT_EQUAL: + return NOT_EQUAL; + case LESS_THAN: + return LESS_THAN; + case GREATER_THAN: + return GREATER_THAN; + case LESS_THAN_OR_EQUAL: + return LESS_THAN_OR_EQUAL; + case GREATER_THAN_OR_EQUAL: + return GREATER_THAN_OR_EQUAL; + } + throw new UnsupportedOperationException("Unexpected comparison operator: " + operator); + } + + @Override + protected IrPathNode visitConjunctionPredicate(ConjunctionPredicate node, Void context) + { + checkArgument(BOOLEAN.equals(types.get(PathNodeRef.of(node))), "Wrong predicate type. Expected BOOLEAN"); + + IrPredicate left = (IrPredicate) process(node.getLeft()); + IrPredicate right = (IrPredicate) process(node.getRight()); + return new IrConjunctionPredicate(left, right); + } + + @Override + protected IrPathNode visitDisjunctionPredicate(DisjunctionPredicate node, Void context) + { + checkArgument(BOOLEAN.equals(types.get(PathNodeRef.of(node))), "Wrong predicate type. Expected BOOLEAN"); + + IrPredicate left = (IrPredicate) process(node.getLeft()); + IrPredicate right = (IrPredicate) process(node.getRight()); + return new IrDisjunctionPredicate(left, right); + } + + @Override + protected IrPathNode visitExistsPredicate(ExistsPredicate node, Void context) + { + checkArgument(BOOLEAN.equals(types.get(PathNodeRef.of(node))), "Wrong predicate type. Expected BOOLEAN"); + + IrPathNode path = process(node.getPath()); + return new IrExistsPredicate(path); + } + + @Override + protected IrPathNode visitIsUnknownPredicate(IsUnknownPredicate node, Void context) + { + checkArgument(BOOLEAN.equals(types.get(PathNodeRef.of(node))), "Wrong predicate type. Expected BOOLEAN"); + + IrPredicate predicate = (IrPredicate) process(node.getPredicate()); + return new IrIsUnknownPredicate(predicate); + } + + @Override + protected IrPathNode visitLikeRegexPredicate(LikeRegexPredicate node, Void context) + { + checkArgument(BOOLEAN.equals(types.get(PathNodeRef.of(node))), "Wrong predicate type. Expected BOOLEAN"); + + // TODO + throw new IllegalStateException("like_regex predicate is not yet supported. The query should have failed in JsonPathAnalyzer."); + } + + @Override + protected IrPathNode visitNegationPredicate(NegationPredicate node, Void context) + { + checkArgument(BOOLEAN.equals(types.get(PathNodeRef.of(node))), "Wrong predicate type. Expected BOOLEAN"); + + IrPredicate predicate = (IrPredicate) process(node.getPredicate()); + return new IrNegationPredicate(predicate); + } + + @Override + protected IrPathNode visitStartsWithPredicate(StartsWithPredicate node, Void context) + { + checkArgument(BOOLEAN.equals(types.get(PathNodeRef.of(node))), "Wrong predicate type. Expected BOOLEAN"); + + IrPathNode whole = process(node.getWhole()); + IrPathNode initial = process(node.getInitial()); + return new IrStartsWithPredicate(whole, initial); + } + } +} diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/LogicalPlanner.java b/core/trino-main/src/main/java/io/trino/sql/planner/LogicalPlanner.java index ef11d6fcf67a..4c0458897e4d 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/LogicalPlanner.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/LogicalPlanner.java @@ -839,7 +839,7 @@ private RelationPlan createTableExecutePlan(Analysis analysis, TableExecute stat TableHandle tableHandle = analysis.getTableHandle(table); RelationPlan tableScanPlan = createRelationPlan(analysis, table); - PlanBuilder sourcePlanBuilder = newPlanBuilder(tableScanPlan, analysis, ImmutableMap.of(), ImmutableMap.of()); + PlanBuilder sourcePlanBuilder = newPlanBuilder(tableScanPlan, analysis, ImmutableMap.of(), ImmutableMap.of(), session, plannerContext); if (statement.getWhere().isPresent()) { SubqueryPlanner subqueryPlanner = new SubqueryPlanner(analysis, symbolAllocator, idAllocator, buildLambdaDeclarationToSymbolMap(analysis, symbolAllocator), plannerContext, typeCoercion, Optional.empty(), session, ImmutableMap.of()); Expression whereExpression = statement.getWhere().get(); diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/PlanBuilder.java b/core/trino-main/src/main/java/io/trino/sql/planner/PlanBuilder.java index 3ef9f6214bcb..a00aa1848905 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/PlanBuilder.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/PlanBuilder.java @@ -14,6 +14,8 @@ package io.trino.sql.planner; import com.google.common.collect.ImmutableMap; +import io.trino.Session; +import io.trino.sql.PlannerContext; import io.trino.sql.analyzer.Analysis; import io.trino.sql.analyzer.Scope; import io.trino.sql.planner.plan.Assignments; @@ -46,15 +48,15 @@ public PlanBuilder(TranslationMap translations, PlanNode root) this.root = root; } - public static PlanBuilder newPlanBuilder(RelationPlan plan, Analysis analysis, Map, Symbol> lambdaArguments) + public static PlanBuilder newPlanBuilder(RelationPlan plan, Analysis analysis, Map, Symbol> lambdaArguments, Session session, PlannerContext plannerContext) { - return newPlanBuilder(plan, analysis, lambdaArguments, ImmutableMap.of()); + return newPlanBuilder(plan, analysis, lambdaArguments, ImmutableMap.of(), session, plannerContext); } - public static PlanBuilder newPlanBuilder(RelationPlan plan, Analysis analysis, Map, Symbol> lambdaArguments, Map, Symbol> mappings) + public static PlanBuilder newPlanBuilder(RelationPlan plan, Analysis analysis, Map, Symbol> lambdaArguments, Map, Symbol> mappings, Session session, PlannerContext plannerContext) { return new PlanBuilder( - new TranslationMap(plan.getOuterContext(), plan.getScope(), analysis, lambdaArguments, plan.getFieldMappings(), mappings), + new TranslationMap(plan.getOuterContext(), plan.getScope(), analysis, lambdaArguments, plan.getFieldMappings(), mappings, session, plannerContext), plan.getRoot()); } diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/QueryPlanner.java b/core/trino-main/src/main/java/io/trino/sql/planner/QueryPlanner.java index 494cc69adc01..9c647b3a077f 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/QueryPlanner.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/QueryPlanner.java @@ -486,7 +486,7 @@ public DeleteNode plan(Delete node) RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries) .process(table, null); - PlanBuilder builder = newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap); + PlanBuilder builder = newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext); if (node.getWhere().isPresent()) { builder = filter(builder, node.getWhere().get(), node); } @@ -541,7 +541,7 @@ public UpdateNode plan(Update node) RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries) .process(table, null); - PlanBuilder builder = newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap); + PlanBuilder builder = newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext); if (node.getWhere().isPresent()) { builder = filter(builder, node.getWhere().get(), node); @@ -605,7 +605,7 @@ private PlanBuilder planQueryBody(Query query) RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries) .process(query.getQueryBody(), null); - return newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap); + return newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext); } private PlanBuilder planFrom(QuerySpecification node) @@ -613,11 +613,11 @@ private PlanBuilder planFrom(QuerySpecification node) if (node.getFrom().isPresent()) { RelationPlan relationPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, outerContext, session, recursiveSubqueries) .process(node.getFrom().get(), null); - return newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap); + return newPlanBuilder(relationPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext); } return new PlanBuilder( - new TranslationMap(outerContext, analysis.getImplicitFromScope(node), analysis, lambdaDeclarationToSymbolMap, ImmutableList.of()), + new TranslationMap(outerContext, analysis.getImplicitFromScope(node), analysis, lambdaDeclarationToSymbolMap, ImmutableList.of(), session, plannerContext), new ValuesNode(idAllocator.getNextId(), 1)); } diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/RelationPlanner.java b/core/trino-main/src/main/java/io/trino/sql/planner/RelationPlanner.java index 29dec22e387a..b81e1de418fe 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/RelationPlanner.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/RelationPlanner.java @@ -271,7 +271,7 @@ public RelationPlan addRowFilters(Table node, RelationPlan plan, Function equiClauses = ImmutableList.builder(); @@ -707,7 +707,7 @@ else if (firstDependencies.stream().allMatch(right::canResolve) && secondDepende } } } - TranslationMap translationMap = new TranslationMap(outerContext, scope, analysis, lambdaDeclarationToSymbolMap, outputSymbols) + TranslationMap translationMap = new TranslationMap(outerContext, scope, analysis, lambdaDeclarationToSymbolMap, outputSymbols, session, plannerContext) .withAdditionalMappings(leftPlanBuilder.getTranslations().getMappings()) .withAdditionalMappings(rightPlanBuilder.getTranslations().getMappings()); @@ -895,12 +895,12 @@ private static Optional getLateral(Relation relation) private RelationPlan planCorrelatedJoin(Join join, RelationPlan leftPlan, Lateral lateral) { - PlanBuilder leftPlanBuilder = newPlanBuilder(leftPlan, analysis, lambdaDeclarationToSymbolMap); + PlanBuilder leftPlanBuilder = newPlanBuilder(leftPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext); RelationPlan rightPlan = new RelationPlanner(analysis, symbolAllocator, idAllocator, lambdaDeclarationToSymbolMap, plannerContext, Optional.of(leftPlanBuilder.getTranslations()), session, recursiveSubqueries) .process(lateral.getQuery(), null); - PlanBuilder rightPlanBuilder = newPlanBuilder(rightPlan, analysis, lambdaDeclarationToSymbolMap); + PlanBuilder rightPlanBuilder = newPlanBuilder(rightPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext); Expression filterExpression; if (join.getCriteria().isEmpty()) { @@ -918,7 +918,7 @@ private RelationPlan planCorrelatedJoin(Join join, RelationPlan leftPlan, Latera .addAll(leftPlan.getFieldMappings()) .addAll(rightPlan.getFieldMappings()) .build(); - TranslationMap translationMap = new TranslationMap(outerContext, analysis.getScope(join), analysis, lambdaDeclarationToSymbolMap, outputSymbols) + TranslationMap translationMap = new TranslationMap(outerContext, analysis.getScope(join), analysis, lambdaDeclarationToSymbolMap, outputSymbols, session, plannerContext) .withAdditionalMappings(leftPlanBuilder.getTranslations().getMappings()) .withAdditionalMappings(rightPlanBuilder.getTranslations().getMappings()); @@ -961,7 +961,7 @@ private RelationPlan planJoinUnnest(RelationPlan leftPlan, Join joinNode, Unnest } return planUnnest( - newPlanBuilder(leftPlan, analysis, lambdaDeclarationToSymbolMap), + newPlanBuilder(leftPlan, analysis, lambdaDeclarationToSymbolMap, session, plannerContext), node, leftPlan.getFieldMappings(), filterExpression, @@ -1041,7 +1041,7 @@ protected RelationPlan visitValues(Values node, Void context) outputSymbolsBuilder.add(symbol); } List outputSymbols = outputSymbolsBuilder.build(); - TranslationMap translationMap = new TranslationMap(outerContext, analysis.getScope(node), analysis, lambdaDeclarationToSymbolMap, outputSymbols); + TranslationMap translationMap = new TranslationMap(outerContext, analysis.getScope(node), analysis, lambdaDeclarationToSymbolMap, outputSymbols, session, plannerContext); ImmutableList.Builder rows = ImmutableList.builder(); for (Expression row : node.getRows()) { @@ -1082,7 +1082,7 @@ private PlanBuilder planSingleEmptyRow(Optional parent) parent.ifPresent(scope::withOuterQueryParent); PlanNode values = new ValuesNode(idAllocator.getNextId(), 1); - TranslationMap translations = new TranslationMap(outerContext, scope.build(), analysis, lambdaDeclarationToSymbolMap, ImmutableList.of()); + TranslationMap translations = new TranslationMap(outerContext, scope.build(), analysis, lambdaDeclarationToSymbolMap, ImmutableList.of(), session, plannerContext); return new PlanBuilder(translations, values); } diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/SubqueryPlanner.java b/core/trino-main/src/main/java/io/trino/sql/planner/SubqueryPlanner.java index 0be9271763e6..60663d4c66c2 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/SubqueryPlanner.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/SubqueryPlanner.java @@ -144,9 +144,9 @@ public PlanBuilder handleSubqueries(PlanBuilder builder, Expression expression, private List selectSubqueries(PlanBuilder subPlan, Expression parent, List candidates) { SuccessorsFunction recurse = expression -> { - if (expression instanceof Expression && - !analysis.isColumnReference((Expression) expression) && // no point in following dereference chains - !subPlan.canTranslate((Expression) expression)) { // don't consider subqueries under parts of the expression that have already been handled + if (!(expression instanceof Expression) || + (!analysis.isColumnReference((Expression) expression) && // no point in following dereference chains + !subPlan.canTranslate((Expression) expression))) { // don't consider subqueries under parts of the expression that have already been handled return expression.getChildren(); } @@ -235,7 +235,9 @@ private PlanBuilder planScalarSubquery(PlanBuilder subPlan, Cluster coercio relationPlan, analysis, lambdaDeclarationToSymbolMap, - ImmutableMap.of(scopeAwareKey(subquery, analysis, relationPlan.getScope()), column)); + ImmutableMap.of(scopeAwareKey(subquery, analysis, relationPlan.getScope()), column), + session, + plannerContext); RelationType descriptor = relationPlan.getDescriptor(); ImmutableList.Builder fields = ImmutableList.builder(); diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/TranslationMap.java b/core/trino-main/src/main/java/io/trino/sql/planner/TranslationMap.java index 0ebadb8c0356..2f8727cc9ca8 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/TranslationMap.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/TranslationMap.java @@ -15,13 +15,20 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.trino.Session; +import io.trino.json.ir.IrJsonPath; import io.trino.metadata.ResolvedFunction; import io.trino.spi.type.RowType; import io.trino.spi.type.Type; +import io.trino.spi.type.TypeId; +import io.trino.sql.PlannerContext; import io.trino.sql.analyzer.Analysis; import io.trino.sql.analyzer.ExpressionAnalyzer.LabelPrefixedReference; import io.trino.sql.analyzer.ResolvedField; import io.trino.sql.analyzer.Scope; +import io.trino.sql.analyzer.TypeSignatureTranslator; +import io.trino.sql.tree.BooleanLiteral; +import io.trino.sql.tree.Cast; import io.trino.sql.tree.DereferenceExpression; import io.trino.sql.tree.Expression; import io.trino.sql.tree.ExpressionRewriter; @@ -29,18 +36,26 @@ import io.trino.sql.tree.FieldReference; import io.trino.sql.tree.FunctionCall; import io.trino.sql.tree.GenericDataType; +import io.trino.sql.tree.GenericLiteral; import io.trino.sql.tree.Identifier; +import io.trino.sql.tree.JsonExists; +import io.trino.sql.tree.JsonPathParameter; +import io.trino.sql.tree.JsonQuery; +import io.trino.sql.tree.JsonValue; import io.trino.sql.tree.LabelDereference; import io.trino.sql.tree.LambdaArgumentDeclaration; import io.trino.sql.tree.LambdaExpression; import io.trino.sql.tree.LongLiteral; import io.trino.sql.tree.NodeRef; +import io.trino.sql.tree.NullLiteral; import io.trino.sql.tree.Parameter; +import io.trino.sql.tree.Row; import io.trino.sql.tree.RowDataType; import io.trino.sql.tree.SubscriptExpression; import io.trino.sql.tree.SymbolReference; import io.trino.sql.tree.Trim; import io.trino.sql.util.AstUtils; +import io.trino.type.JsonPath2016Type; import java.util.Arrays; import java.util.Collections; @@ -52,7 +67,13 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.sql.analyzer.ExpressionAnalyzer.JSON_NO_PARAMETERS_ROW_TYPE; +import static io.trino.sql.analyzer.TypeSignatureTranslator.toSqlType; import static io.trino.sql.planner.ScopeAware.scopeAwareKey; +import static io.trino.sql.tree.JsonQuery.QuotesBehavior.KEEP; +import static io.trino.sql.tree.JsonQuery.QuotesBehavior.OMIT; import static java.lang.String.format; import static java.util.Objects.requireNonNull; @@ -71,6 +92,8 @@ class TranslationMap private final Analysis analysis; private final Map, Symbol> lambdaArguments; private final Optional outerContext; + private final Session session; + private final PlannerContext plannerContext; // current mappings of underlying field -> symbol for translating direct field references private final Symbol[] fieldSymbols; @@ -78,22 +101,24 @@ class TranslationMap // current mappings of sub-expressions -> symbol private final Map, Symbol> astToSymbols; - public TranslationMap(Optional outerContext, Scope scope, Analysis analysis, Map, Symbol> lambdaArguments, List fieldSymbols) + public TranslationMap(Optional outerContext, Scope scope, Analysis analysis, Map, Symbol> lambdaArguments, List fieldSymbols, Session session, PlannerContext plannerContext) { - this(outerContext, scope, analysis, lambdaArguments, fieldSymbols.toArray(new Symbol[0]).clone(), ImmutableMap.of()); + this(outerContext, scope, analysis, lambdaArguments, fieldSymbols.toArray(new Symbol[0]).clone(), ImmutableMap.of(), session, plannerContext); } - public TranslationMap(Optional outerContext, Scope scope, Analysis analysis, Map, Symbol> lambdaArguments, List fieldSymbols, Map, Symbol> astToSymbols) + public TranslationMap(Optional outerContext, Scope scope, Analysis analysis, Map, Symbol> lambdaArguments, List fieldSymbols, Map, Symbol> astToSymbols, Session session, PlannerContext plannerContext) { - this(outerContext, scope, analysis, lambdaArguments, fieldSymbols.toArray(new Symbol[0]), astToSymbols); + this(outerContext, scope, analysis, lambdaArguments, fieldSymbols.toArray(new Symbol[0]), astToSymbols, session, plannerContext); } - public TranslationMap(Optional outerContext, Scope scope, Analysis analysis, Map, Symbol> lambdaArguments, Symbol[] fieldSymbols, Map, Symbol> astToSymbols) + public TranslationMap(Optional outerContext, Scope scope, Analysis analysis, Map, Symbol> lambdaArguments, Symbol[] fieldSymbols, Map, Symbol> astToSymbols, Session session, PlannerContext plannerContext) { this.outerContext = requireNonNull(outerContext, "outerContext is null"); this.scope = requireNonNull(scope, "scope is null"); this.analysis = requireNonNull(analysis, "analysis is null"); this.lambdaArguments = requireNonNull(lambdaArguments, "lambdaArguments is null"); + this.session = requireNonNull(session, "session is null"); + this.plannerContext = requireNonNull(plannerContext, "plannerContext is null"); requireNonNull(fieldSymbols, "fieldSymbols is null"); this.fieldSymbols = fieldSymbols.clone(); @@ -113,12 +138,12 @@ public TranslationMap(Optional outerContext, Scope scope, Analys public TranslationMap withScope(Scope scope, List fields) { - return new TranslationMap(outerContext, scope, analysis, lambdaArguments, fields.toArray(new Symbol[0]), astToSymbols); + return new TranslationMap(outerContext, scope, analysis, lambdaArguments, fields.toArray(new Symbol[0]), astToSymbols, session, plannerContext); } public TranslationMap withNewMappings(Map, Symbol> mappings, List fields) { - return new TranslationMap(outerContext, scope, analysis, lambdaArguments, fields, mappings); + return new TranslationMap(outerContext, scope, analysis, lambdaArguments, fields, mappings, session, plannerContext); } public TranslationMap withAdditionalMappings(Map, Symbol> mappings) @@ -127,7 +152,7 @@ public TranslationMap withAdditionalMappings(Map, Symbol> newMappings.putAll(this.astToSymbols); newMappings.putAll(mappings); - return new TranslationMap(outerContext, scope, analysis, lambdaArguments, fieldSymbols, newMappings); + return new TranslationMap(outerContext, scope, analysis, lambdaArguments, fieldSymbols, newMappings, session, plannerContext); } public List getFieldSymbols() @@ -391,6 +416,188 @@ public Expression rewriteRowDataType(RowDataType node, Void context, ExpressionT return node; } + @Override + public Expression rewriteJsonExists(JsonExists node, Void context, ExpressionTreeRewriter treeRewriter) + { + Optional mapped = tryGetMapping(node); + if (mapped.isPresent()) { + return coerceIfNecessary(node, mapped.get()); + } + + ResolvedFunction resolvedFunction = analysis.getResolvedFunction(node); + checkArgument(resolvedFunction != null, "Function has not been analyzed: %s", node); + + // rewrite the input expression and JSON path parameters + // the rewrite also applies any coercions necessary for the input functions, which are applied in the next step + JsonExists rewritten = treeRewriter.defaultRewrite(node, context); + + // apply the input function to the input expression + BooleanLiteral failOnError = new BooleanLiteral(node.getErrorBehavior() == JsonExists.ErrorBehavior.ERROR ? "true" : "false"); + ResolvedFunction inputToJson = analysis.getJsonInputFunction(node.getJsonPathInvocation().getInputExpression()); + Expression input = new FunctionCall(inputToJson.toQualifiedName(), ImmutableList.of(rewritten.getJsonPathInvocation().getInputExpression(), failOnError)); + + // apply the input functions to the JSON path parameters having FORMAT, + // and collect all JSON path parameters in a Row + ParametersRow orderedParameters = getParametersRow( + node.getJsonPathInvocation().getPathParameters(), + rewritten.getJsonPathInvocation().getPathParameters(), + resolvedFunction.getSignature().getArgumentType(2), + failOnError); + + IrJsonPath path = new JsonPathTranslator(session, plannerContext).rewriteToIr(analysis.getJsonPathAnalysis(node), orderedParameters.getParametersOrder()); + Expression pathExpression = new LiteralEncoder(plannerContext).toExpression(session, path, plannerContext.getTypeManager().getType(TypeId.of(JsonPath2016Type.NAME))); + + ImmutableList.Builder arguments = ImmutableList.builder() + .add(input) + .add(pathExpression) + .add(orderedParameters.getParametersRow()) + .add(new GenericLiteral("tinyint", String.valueOf(rewritten.getErrorBehavior().ordinal()))); + + Expression result = new FunctionCall(resolvedFunction.toQualifiedName(), arguments.build()); + + return coerceIfNecessary(node, result); + } + + @Override + public Expression rewriteJsonValue(JsonValue node, Void context, ExpressionTreeRewriter treeRewriter) + { + Optional mapped = tryGetMapping(node); + if (mapped.isPresent()) { + return coerceIfNecessary(node, mapped.get()); + } + + ResolvedFunction resolvedFunction = analysis.getResolvedFunction(node); + checkArgument(resolvedFunction != null, "Function has not been analyzed: %s", node); + + // rewrite the input expression, default expressions, and JSON path parameters + // the rewrite also applies any coercions necessary for the input functions, which are applied in the next step + JsonValue rewritten = treeRewriter.defaultRewrite(node, context); + + // apply the input function to the input expression + BooleanLiteral failOnError = new BooleanLiteral(node.getErrorBehavior() == JsonValue.EmptyOrErrorBehavior.ERROR ? "true" : "false"); + ResolvedFunction inputToJson = analysis.getJsonInputFunction(node.getJsonPathInvocation().getInputExpression()); + Expression input = new FunctionCall(inputToJson.toQualifiedName(), ImmutableList.of(rewritten.getJsonPathInvocation().getInputExpression(), failOnError)); + + // apply the input functions to the JSON path parameters having FORMAT, + // and collect all JSON path parameters in a Row + ParametersRow orderedParameters = getParametersRow( + node.getJsonPathInvocation().getPathParameters(), + rewritten.getJsonPathInvocation().getPathParameters(), + resolvedFunction.getSignature().getArgumentType(2), + failOnError); + + IrJsonPath path = new JsonPathTranslator(session, plannerContext).rewriteToIr(analysis.getJsonPathAnalysis(node), orderedParameters.getParametersOrder()); + Expression pathExpression = new LiteralEncoder(plannerContext).toExpression(session, path, plannerContext.getTypeManager().getType(TypeId.of(JsonPath2016Type.NAME))); + + ImmutableList.Builder arguments = ImmutableList.builder() + .add(input) + .add(pathExpression) + .add(orderedParameters.getParametersRow()) + .add(new GenericLiteral("tinyint", String.valueOf(rewritten.getEmptyBehavior().ordinal()))) + .add(rewritten.getEmptyDefault().orElse(new Cast(new NullLiteral(), toSqlType(resolvedFunction.getSignature().getReturnType())))) + .add(new GenericLiteral("tinyint", String.valueOf(rewritten.getErrorBehavior().ordinal()))) + .add(rewritten.getErrorDefault().orElse(new Cast(new NullLiteral(), toSqlType(resolvedFunction.getSignature().getReturnType())))); + + Expression result = new FunctionCall(resolvedFunction.toQualifiedName(), arguments.build()); + + return coerceIfNecessary(node, result); + } + + @Override + public Expression rewriteJsonQuery(JsonQuery node, Void context, ExpressionTreeRewriter treeRewriter) + { + Optional mapped = tryGetMapping(node); + if (mapped.isPresent()) { + return coerceIfNecessary(node, mapped.get()); + } + + ResolvedFunction resolvedFunction = analysis.getResolvedFunction(node); + checkArgument(resolvedFunction != null, "Function has not been analyzed: %s", node); + + // rewrite the input expression and JSON path parameters + // the rewrite also applies any coercions necessary for the input functions, which are applied in the next step + JsonQuery rewritten = treeRewriter.defaultRewrite(node, context); + + // apply the input function to the input expression + BooleanLiteral failOnError = new BooleanLiteral(node.getErrorBehavior() == JsonQuery.EmptyOrErrorBehavior.ERROR ? "true" : "false"); + ResolvedFunction inputToJson = analysis.getJsonInputFunction(node.getJsonPathInvocation().getInputExpression()); + Expression input = new FunctionCall(inputToJson.toQualifiedName(), ImmutableList.of(rewritten.getJsonPathInvocation().getInputExpression(), failOnError)); + + // apply the input functions to the JSON path parameters having FORMAT, + // and collect all JSON path parameters in a Row + ParametersRow orderedParameters = getParametersRow( + node.getJsonPathInvocation().getPathParameters(), + rewritten.getJsonPathInvocation().getPathParameters(), + resolvedFunction.getSignature().getArgumentType(2), + failOnError); + + IrJsonPath path = new JsonPathTranslator(session, plannerContext).rewriteToIr(analysis.getJsonPathAnalysis(node), orderedParameters.getParametersOrder()); + Expression pathExpression = new LiteralEncoder(plannerContext).toExpression(session, path, plannerContext.getTypeManager().getType(TypeId.of(JsonPath2016Type.NAME))); + + ImmutableList.Builder arguments = ImmutableList.builder() + .add(input) + .add(pathExpression) + .add(orderedParameters.getParametersRow()) + .add(new GenericLiteral("tinyint", String.valueOf(rewritten.getWrapperBehavior().ordinal()))) + .add(new GenericLiteral("tinyint", String.valueOf(rewritten.getEmptyBehavior().ordinal()))) + .add(new GenericLiteral("tinyint", String.valueOf(rewritten.getErrorBehavior().ordinal()))); + + Expression function = new FunctionCall(resolvedFunction.toQualifiedName(), arguments.build()); + + // apply function to format output + GenericLiteral errorBehavior = new GenericLiteral("tinyint", String.valueOf(rewritten.getErrorBehavior().ordinal())); + BooleanLiteral omitQuotes = new BooleanLiteral(node.getQuotesBehavior().orElse(KEEP) == OMIT ? "true" : "false"); + ResolvedFunction outputFunction = analysis.getJsonOutputFunction(node); + Expression result = new FunctionCall(outputFunction.toQualifiedName(), ImmutableList.of(function, errorBehavior, omitQuotes)); + + // cast to requested returned type + Type returnedType = node.getReturnedType() + .map(TypeSignatureTranslator::toTypeSignature) + .map(plannerContext.getTypeManager()::getType) + .orElse(VARCHAR); + + Type resultType = outputFunction.getSignature().getReturnType(); + if (!resultType.equals(returnedType)) { + result = new Cast(result, toSqlType(returnedType)); + } + + return coerceIfNecessary(node, result); + } + + private ParametersRow getParametersRow( + List pathParameters, + List rewrittenPathParameters, + Type parameterRowType, + BooleanLiteral failOnError) + { + Expression parametersRow; + List parametersOrder; + if (!pathParameters.isEmpty()) { + ImmutableList.Builder parameters = ImmutableList.builder(); + for (int i = 0; i < pathParameters.size(); i++) { + ResolvedFunction parameterToJson = analysis.getJsonInputFunction(pathParameters.get(i).getParameter()); + Expression rewrittenParameter = rewrittenPathParameters.get(i).getParameter(); + if (parameterToJson != null) { + parameters.add(new FunctionCall(parameterToJson.toQualifiedName(), ImmutableList.of(rewrittenParameter, failOnError))); + } + else { + parameters.add(rewrittenParameter); + } + } + parametersRow = new Cast(new Row(parameters.build()), toSqlType(parameterRowType)); + parametersOrder = pathParameters.stream() + .map(parameter -> parameter.getName().getCanonicalValue()) + .collect(toImmutableList()); + } + else { + checkState(JSON_NO_PARAMETERS_ROW_TYPE.equals(parameterRowType), "invalid type of parameters row when no parameters are passed"); + parametersRow = new Cast(new NullLiteral(), toSqlType(JSON_NO_PARAMETERS_ROW_TYPE)); + parametersOrder = ImmutableList.of(); + } + + return new ParametersRow(parametersRow, parametersOrder); + } + private Expression coerceIfNecessary(Expression original, Expression rewritten) { // Don't add a coercion for the top-level expression. That depends on the context the expression is used and it's the responsibility of the caller. @@ -441,4 +648,26 @@ public Scope getScope() { return scope; } + + private static class ParametersRow + { + private final Expression parametersRow; + private final List parametersOrder; + + public ParametersRow(Expression parametersRow, List parametersOrder) + { + this.parametersRow = requireNonNull(parametersRow, "parametersRow is null"); + this.parametersOrder = requireNonNull(parametersOrder, "parametersOrder is null"); + } + + public Expression getParametersRow() + { + return parametersRow; + } + + public List getParametersOrder() + { + return parametersOrder; + } + } } diff --git a/core/trino-main/src/main/java/io/trino/testing/LocalQueryRunner.java b/core/trino-main/src/main/java/io/trino/testing/LocalQueryRunner.java index 977be2069e65..09b277a4a7e5 100644 --- a/core/trino-main/src/main/java/io/trino/testing/LocalQueryRunner.java +++ b/core/trino-main/src/main/java/io/trino/testing/LocalQueryRunner.java @@ -115,6 +115,9 @@ import io.trino.operator.TaskContext; import io.trino.operator.TrinoOperatorFactories; import io.trino.operator.index.IndexJoinLookupStats; +import io.trino.operator.scalar.json.JsonExistsFunction; +import io.trino.operator.scalar.json.JsonQueryFunction; +import io.trino.operator.scalar.json.JsonValueFunction; import io.trino.plugin.base.security.AllowAllSystemAccessControl; import io.trino.security.GroupProviderManager; import io.trino.server.PluginManager; @@ -188,6 +191,8 @@ import io.trino.transaction.TransactionManagerConfig; import io.trino.type.BlockTypeOperators; import io.trino.type.InternalTypeManager; +import io.trino.type.JsonPath2016Type; +import io.trino.type.TypeDeserializer; import io.trino.util.FinalizerService; import org.intellij.lang.annotations.Language; @@ -244,6 +249,7 @@ public class LocalQueryRunner private final InMemoryNodeManager nodeManager; private final BlockTypeOperators blockTypeOperators; private final PlannerContext plannerContext; + private final TypeRegistry typeRegistry; private final GlobalFunctionCatalog globalFunctionCatalog; private final FunctionManager functionManager; private final StatsCalculator statsCalculator; @@ -347,7 +353,7 @@ private LocalQueryRunner( this.nodePartitioningManager = new NodePartitioningManager(nodeScheduler, blockTypeOperators); BlockEncodingManager blockEncodingManager = new BlockEncodingManager(); - TypeRegistry typeRegistry = new TypeRegistry(typeOperators, featuresConfig); + typeRegistry = new TypeRegistry(typeOperators, featuresConfig); TypeManager typeManager = new InternalTypeManager(typeRegistry); InternalBlockEncodingSerde blockEncodingSerde = new InternalBlockEncodingSerde(blockEncodingManager, typeManager); @@ -361,6 +367,11 @@ private LocalQueryRunner( transactionManager, globalFunctionCatalog, typeManager); + globalFunctionCatalog.addFunctions(new InternalFunctionBundle( + new JsonExistsFunction(functionManager, metadata, typeManager), + new JsonValueFunction(functionManager, metadata, typeManager), + new JsonQueryFunction(functionManager, metadata, typeManager))); + typeRegistry.addType(new JsonPath2016Type(new TypeDeserializer(typeManager), blockEncodingSerde)); this.plannerContext = new PlannerContext(metadata, typeOperators, blockEncodingSerde, typeManager, functionManager); this.splitManager = new SplitManager(new QueryManagerConfig()); this.planFragmenter = new PlanFragmenter(metadata, functionManager, this.nodePartitioningManager, new QueryManagerConfig()); @@ -601,12 +612,22 @@ public AnalyzePropertyManager getAnalyzePropertyManager() return analyzePropertyManager; } + public TypeRegistry getTypeRegistry() + { + return typeRegistry; + } + @Override public TypeManager getTypeManager() { return plannerContext.getTypeManager(); } + public GlobalFunctionCatalog getGlobalFunctionCatalog() + { + return globalFunctionCatalog; + } + @Override public QueryExplainer getQueryExplainer() { diff --git a/core/trino-main/src/main/java/io/trino/type/Json2016Type.java b/core/trino-main/src/main/java/io/trino/type/Json2016Type.java new file mode 100644 index 000000000000..3c6468939323 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/type/Json2016Type.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.type; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.airlift.slice.Slice; +import io.trino.operator.scalar.json.JsonInputConversionError; +import io.trino.operator.scalar.json.JsonOutputConversionError; +import io.trino.spi.block.Block; +import io.trino.spi.block.BlockBuilder; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.AbstractVariableWidthType; +import io.trino.spi.type.StandardTypes; +import io.trino.spi.type.TypeSignature; + +import static io.airlift.slice.Slices.utf8Slice; +import static io.trino.json.JsonInputErrorNode.JSON_ERROR; + +public class Json2016Type + extends AbstractVariableWidthType +{ + public static final Json2016Type JSON_2016 = new Json2016Type(); + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public Json2016Type() + { + super(new TypeSignature(StandardTypes.JSON_2016), JsonNode.class); + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + return getObject(block, position); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + throw new UnsupportedOperationException(); + } + + @Override + public Object getObject(Block block, int position) + { + if (block.isNull(position)) { + return null; + } + + String json = block.getSlice(position, 0, block.getSliceLength(position)).toStringUtf8(); + if (json.equals(JSON_ERROR.toString())) { + return JSON_ERROR; + } + try { + return MAPPER.readTree(json); + } + catch (JsonProcessingException e) { + throw new JsonInputConversionError(e); + } + } + + @Override + public void writeObject(BlockBuilder blockBuilder, Object value) + { + String json; + if (value == JSON_ERROR) { + json = JSON_ERROR.toString(); + } + else { + try { + json = MAPPER.writeValueAsString(value); + } + catch (JsonProcessingException e) { + throw new JsonOutputConversionError(e); + } + } + Slice bytes = utf8Slice(json); + blockBuilder.writeBytes(bytes, 0, bytes.length()).closeEntry(); + } +} diff --git a/core/trino-main/src/main/java/io/trino/type/JsonPath2016Type.java b/core/trino-main/src/main/java/io/trino/type/JsonPath2016Type.java new file mode 100644 index 000000000000..02e54b8757a4 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/type/JsonPath2016Type.java @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.type; + +import com.google.common.collect.ImmutableMap; +import io.airlift.json.JsonCodec; +import io.airlift.json.JsonCodecFactory; +import io.airlift.json.ObjectMapperProvider; +import io.airlift.slice.Slice; +import io.trino.block.BlockJsonSerde; +import io.trino.json.ir.IrJsonPath; +import io.trino.spi.block.Block; +import io.trino.spi.block.BlockBuilder; +import io.trino.spi.block.BlockEncodingSerde; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.type.AbstractVariableWidthType; +import io.trino.spi.type.Type; +import io.trino.spi.type.TypeSignature; + +import static io.airlift.slice.Slices.utf8Slice; + +public class JsonPath2016Type + extends AbstractVariableWidthType +{ + public static final String NAME = "JsonPath2016"; + + private final JsonCodec jsonPathCodec; + + public JsonPath2016Type(TypeDeserializer typeDeserializer, BlockEncodingSerde blockEncodingSerde) + { + super(new TypeSignature(NAME), IrJsonPath.class); + this.jsonPathCodec = getCodec(typeDeserializer, blockEncodingSerde); + } + + @Override + public Object getObjectValue(ConnectorSession session, Block block, int position) + { + throw new UnsupportedOperationException(); + } + + @Override + public void appendTo(Block block, int position, BlockBuilder blockBuilder) + { + throw new UnsupportedOperationException(); + } + + @Override + public Object getObject(Block block, int position) + { + if (block.isNull(position)) { + return null; + } + + Slice bytes = block.getSlice(position, 0, block.getSliceLength(position)); + return jsonPathCodec.fromJson(bytes.toStringUtf8()); + } + + @Override + public void writeObject(BlockBuilder blockBuilder, Object value) + { + String json = jsonPathCodec.toJson((IrJsonPath) value); + Slice bytes = utf8Slice(json); + blockBuilder.writeBytes(bytes, 0, bytes.length()).closeEntry(); + } + + private static JsonCodec getCodec(TypeDeserializer typeDeserializer, BlockEncodingSerde blockEncodingSerde) + { + ObjectMapperProvider provider = new ObjectMapperProvider(); + provider.setJsonSerializers(ImmutableMap.of(Block.class, new BlockJsonSerde.Serializer(blockEncodingSerde))); + provider.setJsonDeserializers(ImmutableMap.of( + Type.class, typeDeserializer, + Block.class, new BlockJsonSerde.Deserializer(blockEncodingSerde))); + return new JsonCodecFactory(provider).jsonCodec(IrJsonPath.class); + } +} diff --git a/core/trino-main/src/test/java/io/trino/json/TestJsonPathEvaluator.java b/core/trino-main/src/test/java/io/trino/json/TestJsonPathEvaluator.java new file mode 100644 index 000000000000..d3aaf7feb6e9 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/json/TestJsonPathEvaluator.java @@ -0,0 +1,1371 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.ShortNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.trino.json.ir.IrJsonPath; +import io.trino.json.ir.IrPathNode; +import io.trino.json.ir.IrPredicate; +import io.trino.json.ir.TypedValue; +import io.trino.spi.type.Int128; +import io.trino.spi.type.LongTimestamp; +import io.trino.spi.type.TestingTypeManager; +import io.trino.sql.planner.PathNodes; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.RecursiveComparisonAssert; +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import static io.airlift.slice.Slices.utf8Slice; +import static io.trino.json.JsonEmptySequenceNode.EMPTY_SEQUENCE; +import static io.trino.metadata.FunctionManager.createTestingFunctionManager; +import static io.trino.metadata.MetadataManager.createTestMetadataManager; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.CharType.createCharType; +import static io.trino.spi.type.DateType.DATE; +import static io.trino.spi.type.DecimalType.createDecimalType; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.SmallintType.SMALLINT; +import static io.trino.spi.type.TimestampType.createTimestampType; +import static io.trino.spi.type.TinyintType.TINYINT; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createVarcharType; +import static io.trino.sql.planner.PathNodes.abs; +import static io.trino.sql.planner.PathNodes.add; +import static io.trino.sql.planner.PathNodes.arrayAccessor; +import static io.trino.sql.planner.PathNodes.at; +import static io.trino.sql.planner.PathNodes.ceiling; +import static io.trino.sql.planner.PathNodes.conjunction; +import static io.trino.sql.planner.PathNodes.contextVariable; +import static io.trino.sql.planner.PathNodes.currentItem; +import static io.trino.sql.planner.PathNodes.disjunction; +import static io.trino.sql.planner.PathNodes.divide; +import static io.trino.sql.planner.PathNodes.emptySequence; +import static io.trino.sql.planner.PathNodes.equal; +import static io.trino.sql.planner.PathNodes.exists; +import static io.trino.sql.planner.PathNodes.filter; +import static io.trino.sql.planner.PathNodes.floor; +import static io.trino.sql.planner.PathNodes.greaterThan; +import static io.trino.sql.planner.PathNodes.greaterThanOrEqual; +import static io.trino.sql.planner.PathNodes.isUnknown; +import static io.trino.sql.planner.PathNodes.jsonNull; +import static io.trino.sql.planner.PathNodes.keyValue; +import static io.trino.sql.planner.PathNodes.last; +import static io.trino.sql.planner.PathNodes.lessThan; +import static io.trino.sql.planner.PathNodes.lessThanOrEqual; +import static io.trino.sql.planner.PathNodes.literal; +import static io.trino.sql.planner.PathNodes.memberAccessor; +import static io.trino.sql.planner.PathNodes.minus; +import static io.trino.sql.planner.PathNodes.modulus; +import static io.trino.sql.planner.PathNodes.multiply; +import static io.trino.sql.planner.PathNodes.negation; +import static io.trino.sql.planner.PathNodes.notEqual; +import static io.trino.sql.planner.PathNodes.path; +import static io.trino.sql.planner.PathNodes.plus; +import static io.trino.sql.planner.PathNodes.range; +import static io.trino.sql.planner.PathNodes.sequence; +import static io.trino.sql.planner.PathNodes.singletonSequence; +import static io.trino.sql.planner.PathNodes.size; +import static io.trino.sql.planner.PathNodes.startsWith; +import static io.trino.sql.planner.PathNodes.subtract; +import static io.trino.sql.planner.PathNodes.toDouble; +import static io.trino.sql.planner.PathNodes.type; +import static io.trino.sql.planner.PathNodes.wildcardArrayAccessor; +import static io.trino.sql.planner.PathNodes.wildcardMemberAccessor; +import static io.trino.testing.TestingSession.testSessionBuilder; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestJsonPathEvaluator +{ + private static final RecursiveComparisonConfiguration COMPARISON_CONFIGURATION = RecursiveComparisonConfiguration.builder().withStrictTypeChecking(true).build(); + + private static final Map PARAMETERS = ImmutableMap.builder() + .put("tinyint_parameter", new TypedValue(TINYINT, 1L)) + .put("bigint_parameter", new TypedValue(BIGINT, -2L)) + .put("short_decimal_parameter", new TypedValue(createDecimalType(3, 1), -123L)) + .put("long_decimal_parameter", new TypedValue(createDecimalType(30, 20), Int128.valueOf("100000000000000000000"))) // 1 + .put("double_parameter", new TypedValue(DOUBLE, 5e0)) + .put("string_parameter", new TypedValue(createCharType(5), utf8Slice("xyz"))) + .put("boolean_parameter", new TypedValue(BOOLEAN, true)) + .put("date_parameter", new TypedValue(DATE, 1234L)) + .put("timestamp_parameter", new TypedValue(createTimestampType(7), new LongTimestamp(20, 30))) + .put("empty_sequence_parameter", EMPTY_SEQUENCE) + .put("null_parameter", NullNode.instance) + .put("json_number_parameter", IntNode.valueOf(-6)) + .put("json_text_parameter", TextNode.valueOf("JSON text")) + .put("json_boolean_parameter", BooleanNode.FALSE) + .put("json_array_parameter", new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("element"), DoubleNode.valueOf(7e0), NullNode.instance))) + .put("json_object_parameter", new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("bound_value"), "key2", NullNode.instance))) + .buildOrThrow(); + + private static final List PARAMETERS_ORDER = ImmutableList.copyOf(PARAMETERS.keySet()); + + @Test + public void testLiterals() + { + assertThat(pathResult( + NullNode.instance, + path(true, literal(BIGINT, 1L)))) + .isEqualTo(singletonSequence(new TypedValue(BIGINT, 1L))); + + assertThat(pathResult( + NullNode.instance, + path(true, literal(createVarcharType(5), utf8Slice("abc"))))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(5), utf8Slice("abc")))); + + assertThat(pathResult( + NullNode.instance, + path(true, literal(BOOLEAN, false)))) + .isEqualTo(singletonSequence(new TypedValue(BOOLEAN, false))); + + assertThat(pathResult( + NullNode.instance, + path(true, literal(DATE, 1000L)))) + .isEqualTo(singletonSequence(new TypedValue(DATE, 1000L))); + } + + @Test + public void testNullLiteral() + { + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance), + path(true, jsonNull()))) + .isEqualTo(singletonSequence(NullNode.instance)); + } + + @Test + public void testContextVariable() + { + JsonNode input = new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(BooleanNode.TRUE, BooleanNode.FALSE)); + assertThat(pathResult( + input, + path(true, contextVariable()))) + .isEqualTo(singletonSequence(input)); + } + + @Test + public void testNamedVariable() + { + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance), + path(true, variable("tinyint_parameter")))) + .isEqualTo(singletonSequence(new TypedValue(TINYINT, 1L))); + + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance), + path(true, variable("null_parameter")))) + .isEqualTo(singletonSequence(NullNode.instance)); + + // variables of type IrNamedValueVariable can only take SQL values or JSON null. other JSON objects are handled by IrNamedJsonVariable + assertThatThrownBy(() -> evaluate( + new ArrayNode(JsonNodeFactory.instance), + path(true, variable("json_object_parameter")))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected SQL value or JSON null, got non-null JSON"); + } + + @Test + public void testNamedJsonVariable() + { + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance), + path(true, jsonVariable("null_parameter")))) + .isEqualTo(singletonSequence(NullNode.instance)); + + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance), + path(true, jsonVariable("json_object_parameter")))) + .isEqualTo(singletonSequence(new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("bound_value"), "key2", NullNode.instance)))); + + // variables of type IrNamedJsonVariable can only take JSON objects. SQL values are handled by IrNamedValueVariable + assertThatThrownBy(() -> evaluate( + new ArrayNode(JsonNodeFactory.instance), + path(true, jsonVariable("tinyint_parameter")))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("expected JSON, got SQL value"); + } + + @Test + public void testAbsMethod() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, abs(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 5L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, abs(variable("short_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createDecimalType(3, 1), 123L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, abs(jsonVariable("json_number_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 6L))); + + // multiple inputs + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(-1e0), IntNode.valueOf(2), ShortNode.valueOf((short) -3))), + path(true, abs(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 1e0), new TypedValue(INTEGER, 2L), new TypedValue(SMALLINT, 3L))); + + // multiple inputs -- array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(-1e0), IntNode.valueOf(2), ShortNode.valueOf((short) -3))), + path(true, abs(contextVariable())))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 1e0), new TypedValue(INTEGER, 2L), new TypedValue(SMALLINT, 3L))); + + // overflow + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, abs(literal(TINYINT, -128L))))) + .isInstanceOf(PathEvaluationError.class); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, abs(jsonVariable("null_parameter"))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: NUMBER, actual: NULL"); + } + + @Test + public void testArithmeticBinary() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, add(contextVariable(), variable("short_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createDecimalType(12, 1), -173L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, subtract(literal(DOUBLE, 0e0), variable("short_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(DOUBLE, 12.3e0))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, multiply(jsonVariable("json_number_parameter"), literal(BIGINT, 3L))))) + .isEqualTo(singletonSequence(new TypedValue(BIGINT, -18L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, subtract(variable("short_decimal_parameter"), variable("long_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createDecimalType(31, 20), Int128.valueOf("-1330000000000000000000")))); + + // division by 0 + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, divide(jsonVariable("json_number_parameter"), literal(BIGINT, 0L))))) + .isInstanceOf(PathEvaluationError.class); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, modulus(jsonVariable("json_number_parameter"), literal(BOOLEAN, true))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid operand types to MODULUS operator (integer, boolean)"); + + // left operand is not singleton + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, add(wildcardArrayAccessor(jsonVariable("json_array_parameter")), literal(BIGINT, 0L))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: arithmetic binary expression requires singleton operands"); + + // array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(-5))), + path(true, multiply(contextVariable(), literal(BIGINT, 3L))))) + .isEqualTo(singletonSequence(new TypedValue(BIGINT, -15L))); + } + + @Test + public void testArithmeticUnary() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, plus(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, -5L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, minus(variable("short_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createDecimalType(3, 1), 123L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, plus(jsonVariable("json_number_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, -6L))); + + // multiple inputs + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(-1e0), IntNode.valueOf(2), ShortNode.valueOf((short) -3))), + path(true, minus(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 1e0), new TypedValue(INTEGER, -2L), new TypedValue(SMALLINT, 3L))); + + // multiple inputs -- array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(-1e0), IntNode.valueOf(2), ShortNode.valueOf((short) -3))), + path(true, minus(contextVariable())))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 1e0), new TypedValue(INTEGER, -2L), new TypedValue(SMALLINT, 3L))); + + // overflow + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, minus(literal(TINYINT, -128L))))) + .isInstanceOf(PathEvaluationError.class); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, plus(jsonVariable("null_parameter"))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: NUMBER, actual: NULL"); + } + + @Test + public void testArrayAccessor() + { + // wildcard accessor + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(-1e0), BooleanNode.TRUE, TextNode.valueOf("some_text"))), + path(true, wildcardArrayAccessor(contextVariable())))) + .isEqualTo(sequence(DoubleNode.valueOf(-1e0), BooleanNode.TRUE, TextNode.valueOf("some_text"))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, wildcardArrayAccessor(jsonVariable("json_array_parameter"))))) + .isEqualTo(sequence(TextNode.valueOf("element"), DoubleNode.valueOf(7e0), NullNode.instance)); + + // single element subscript + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, arrayAccessor(jsonVariable("json_array_parameter"), at(literal(DOUBLE, 0e0)))))) + .isEqualTo(singletonSequence(TextNode.valueOf("element"))); + + // range subscript + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, arrayAccessor(jsonVariable("json_array_parameter"), range(literal(DOUBLE, 0e0), literal(INTEGER, 1L)))))) + .isEqualTo(sequence(TextNode.valueOf("element"), DoubleNode.valueOf(7e0))); + + // multiple overlapping subscripts + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("first"), TextNode.valueOf("second"), TextNode.valueOf("third"), TextNode.valueOf("fourth"), TextNode.valueOf("fifth"))), + path(true, arrayAccessor( + contextVariable(), + range(literal(INTEGER, 3L), literal(INTEGER, 4L)), + range(literal(INTEGER, 1L), literal(INTEGER, 2L)), + at(literal(INTEGER, 0L)))))) + .isEqualTo(sequence(TextNode.valueOf("fourth"), TextNode.valueOf("fifth"), TextNode.valueOf("second"), TextNode.valueOf("third"), TextNode.valueOf("first"))); + + // multiple input arrays + assertThat(pathResult( + new ArrayNode( + JsonNodeFactory.instance, + ImmutableList.of( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("first"), TextNode.valueOf("second"), TextNode.valueOf("third"))), + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1), IntNode.valueOf(2), IntNode.valueOf(3))))), + path(true, arrayAccessor(wildcardArrayAccessor(contextVariable()), range(literal(INTEGER, 1L), literal(INTEGER, 2L)))))) + .isEqualTo(sequence(TextNode.valueOf("second"), TextNode.valueOf("third"), IntNode.valueOf(2), IntNode.valueOf(3))); + + // usage of last variable + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, arrayAccessor(jsonVariable("json_array_parameter"), range(literal(DOUBLE, 1e0), last()))))) + .isEqualTo(sequence(DoubleNode.valueOf(7e0), NullNode.instance)); + + // incorrect usage of last variable: no enclosing array + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, last()))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: accessing the last array index with no enclosing array"); + + // last variable in nested arrays + assertThat(pathResult( + new ArrayNode( + JsonNodeFactory.instance, + ImmutableList.of( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(2), IntNode.valueOf(3), IntNode.valueOf(5))), + IntNode.valueOf(7))), + path(true, multiply( + arrayAccessor(arrayAccessor(contextVariable(), at(literal(INTEGER, 0L))), at(last())), // 5 + arrayAccessor(contextVariable(), at(last())))))) // 7 + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 35L))); + + // subscript out of bounds (lax mode) + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("first"), TextNode.valueOf("second"), TextNode.valueOf("third"), TextNode.valueOf("fourth"), TextNode.valueOf("fifth"))), + path(true, arrayAccessor(contextVariable(), at(literal(INTEGER, 100L)))))) + .isEqualTo(emptySequence()); + + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("first"), TextNode.valueOf("second"), TextNode.valueOf("third"), TextNode.valueOf("fourth"), TextNode.valueOf("fifth"))), + path(true, arrayAccessor(contextVariable(), range(literal(INTEGER, 3L), literal(INTEGER, 100L)))))) + .isEqualTo(sequence(TextNode.valueOf("fourth"), TextNode.valueOf("fifth"))); + + // incorrect subscript: from > to (lax mode) + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("first"), TextNode.valueOf("second"), TextNode.valueOf("third"), TextNode.valueOf("fourth"), TextNode.valueOf("fifth"))), + path(true, arrayAccessor(contextVariable(), range(literal(INTEGER, 3L), literal(INTEGER, 2L)))))) + .isEqualTo(emptySequence()); + + // subscript out of bounds (strict mode) + assertThatThrownBy(() -> evaluate( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("first"), TextNode.valueOf("second"), TextNode.valueOf("third"), TextNode.valueOf("fourth"), TextNode.valueOf("fifth"))), + path(false, arrayAccessor(contextVariable(), at(literal(INTEGER, 100L)))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: structural error: invalid array subscript: [100, 100] for array of size 5"); + + assertThatThrownBy(() -> evaluate( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("first"), TextNode.valueOf("second"), TextNode.valueOf("third"), TextNode.valueOf("fourth"), TextNode.valueOf("fifth"))), + path(false, arrayAccessor(contextVariable(), range(literal(INTEGER, 3L), literal(INTEGER, 100L)))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: structural error: invalid array subscript: [3, 100] for array of size 5"); + + // incorrect subscript: from > to (strict mode) + assertThatThrownBy(() -> evaluate( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("first"), TextNode.valueOf("second"), TextNode.valueOf("third"), TextNode.valueOf("fourth"), TextNode.valueOf("fifth"))), + path(false, arrayAccessor(contextVariable(), range(literal(INTEGER, 3L), literal(INTEGER, 2L)))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: structural error: invalid array subscript: [3, 2] for array of size 5"); + + // type mismatch (lax mode) -> the value is wrapped in a singleton array + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, arrayAccessor(contextVariable(), at(literal(INTEGER, 0L)))))) + .isEqualTo(singletonSequence(IntNode.valueOf(-5))); + + // type mismatch (strict mode) + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(false, arrayAccessor(contextVariable(), at(literal(INTEGER, 0L)))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: ARRAY, actual: NUMBER"); + } + + @Test + public void testCeilingMethod() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, ceiling(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, -5L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, ceiling(variable("short_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createDecimalType(3, 0), -12L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, ceiling(jsonVariable("json_number_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, -6L))); + + // multiple inputs + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), IntNode.valueOf(2), DecimalNode.valueOf(BigDecimal.valueOf(-15, 1)))), + path(true, ceiling(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 2e0), new TypedValue(INTEGER, 2L), new TypedValue(createDecimalType(2, 0), -1L))); + + // multiple inputs -- array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), IntNode.valueOf(2), DecimalNode.valueOf(BigDecimal.valueOf(-15, 1)))), + path(true, ceiling(contextVariable())))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 2e0), new TypedValue(INTEGER, 2L), new TypedValue(createDecimalType(2, 0), -1L))); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, ceiling(jsonVariable("null_parameter"))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: NUMBER, actual: NULL"); + } + + @Test + public void testDoubleMethod() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, toDouble(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(DOUBLE, -5e0))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, toDouble(variable("short_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(DOUBLE, -12.3e0))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, toDouble(jsonVariable("json_number_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(DOUBLE, -6e0))); + + assertThat(pathResult( + TextNode.valueOf("123"), + path(true, toDouble(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(DOUBLE, 123e0))); + + assertThat(pathResult( + TextNode.valueOf("-12.3e5"), + path(true, toDouble(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(DOUBLE, -12.3e5))); + + // multiple inputs + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), IntNode.valueOf(2), DecimalNode.valueOf(BigDecimal.valueOf(-15, 1)))), + path(true, toDouble(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 1.5e0), new TypedValue(DOUBLE, 2e0), new TypedValue(DOUBLE, -1.5e0))); + + // multiple inputs -- array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), IntNode.valueOf(2), DecimalNode.valueOf(BigDecimal.valueOf(-15, 1)))), + path(true, toDouble(contextVariable())))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 1.5e0), new TypedValue(DOUBLE, 2e0), new TypedValue(DOUBLE, -1.5e0))); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, toDouble(jsonVariable("null_parameter"))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: NUMBER or TEXT, actual: NULL"); + } + + @Test + public void testFloorMethod() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, floor(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, -5L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, floor(variable("short_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createDecimalType(3, 0), -13L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, floor(jsonVariable("json_number_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, -6L))); + + // multiple inputs + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), IntNode.valueOf(2), DecimalNode.valueOf(BigDecimal.valueOf(-15, 1)))), + path(true, floor(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 1e0), new TypedValue(INTEGER, 2L), new TypedValue(createDecimalType(2, 0), -2L))); + + // multiple inputs -- array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), IntNode.valueOf(2), DecimalNode.valueOf(BigDecimal.valueOf(-15, 1)))), + path(true, floor(contextVariable())))) + .isEqualTo(sequence(new TypedValue(DOUBLE, 1e0), new TypedValue(INTEGER, 2L), new TypedValue(createDecimalType(2, 0), -2L))); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, floor(jsonVariable("null_parameter"))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: NUMBER, actual: NULL"); + } + + @Test + public void testKeyValueMethod() + { + assertThat(pathResult( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("bound_value"), "key2", NullNode.instance)), + path(true, keyValue(contextVariable())))) + .isEqualTo(sequence( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key1"), + "value", TextNode.valueOf("bound_value"), + "id", IntNode.valueOf(0))), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key2"), + "value", NullNode.instance, + "id", IntNode.valueOf(0))))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, keyValue(jsonVariable("json_object_parameter"))))) + .isEqualTo(sequence( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key1"), + "value", TextNode.valueOf("bound_value"), + "id", IntNode.valueOf(0))), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key2"), + "value", NullNode.instance, + "id", IntNode.valueOf(0))))); + + // multiple input objects + assertThat(pathResult( + new ArrayNode( + JsonNodeFactory.instance, + ImmutableList.of( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("first"), "key2", BooleanNode.TRUE)), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key3", IntNode.valueOf(1), "key4", NullNode.instance)))), + path(true, keyValue(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key1"), + "value", TextNode.valueOf("first"), + "id", IntNode.valueOf(0))), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key2"), + "value", BooleanNode.TRUE, + "id", IntNode.valueOf(0))), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key3"), + "value", IntNode.valueOf(1), + "id", IntNode.valueOf(1))), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key4"), + "value", NullNode.instance, + "id", IntNode.valueOf(1))))); + + // multiple objects -- array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode( + JsonNodeFactory.instance, + ImmutableList.of( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("first"), "key2", BooleanNode.TRUE)), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key3", IntNode.valueOf(1), "key4", NullNode.instance)))), + path(true, keyValue(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key1"), + "value", TextNode.valueOf("first"), + "id", IntNode.valueOf(0))), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key2"), + "value", BooleanNode.TRUE, + "id", IntNode.valueOf(0))), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key3"), + "value", IntNode.valueOf(1), + "id", IntNode.valueOf(1))), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of( + "name", TextNode.valueOf("key4"), + "value", NullNode.instance, + "id", IntNode.valueOf(1))))); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, keyValue(jsonVariable("null_parameter"))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: OBJECT, actual: NULL"); + } + + @Test + public void testMemberAccessor() + { + // wildcard accessor + assertThat(pathResult( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("bound_value"), "key2", NullNode.instance)), + path(true, wildcardMemberAccessor(contextVariable())))) + .isEqualTo(sequence(TextNode.valueOf("bound_value"), NullNode.instance)); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, memberAccessor(jsonVariable("json_object_parameter"), "key1")))) + .isEqualTo(singletonSequence(TextNode.valueOf("bound_value"))); + + // multiple input objects + assertThat(pathResult( + new ArrayNode( + JsonNodeFactory.instance, + ImmutableList.of( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("first"), "key2", BooleanNode.TRUE)), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", IntNode.valueOf(1), "key2", NullNode.instance)))), + path(true, memberAccessor(wildcardArrayAccessor(contextVariable()), "key2")))) + .isEqualTo(sequence(BooleanNode.TRUE, NullNode.instance)); + + // multiple input objects -- array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode( + JsonNodeFactory.instance, + ImmutableList.of( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("first"), "key2", BooleanNode.TRUE)), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", IntNode.valueOf(1), "key2", NullNode.instance)))), + path(true, memberAccessor(contextVariable(), "key2")))) + .isEqualTo(sequence(BooleanNode.TRUE, NullNode.instance)); + + // key not found -- structural error is suppressed in lax mode + assertThat(pathResult( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("bound_value"), "key2", NullNode.instance)), + path(true, memberAccessor(contextVariable(), "wrong_key")))) + .isEqualTo(emptySequence()); + + // key not found -- strict mode + assertThatThrownBy(() -> evaluate( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("bound_value"), "key2", NullNode.instance)), + path(false, memberAccessor(contextVariable(), "wrong_key")))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: structural error: missing member 'wrong_key' in JSON object"); + + // multiple input objects, key not found in one of them -- lax mode + assertThat(pathResult( + new ArrayNode( + JsonNodeFactory.instance, + ImmutableList.of( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("first"), "key2", BooleanNode.TRUE)), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key3", IntNode.valueOf(1), "key4", NullNode.instance)))), + path(true, memberAccessor(wildcardArrayAccessor(contextVariable()), "key2")))) + .isEqualTo(singletonSequence(BooleanNode.TRUE)); + + // multiple input objects, key not found in one of them -- strict mode + assertThatThrownBy(() -> evaluate( + new ArrayNode( + JsonNodeFactory.instance, + ImmutableList.of( + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key1", TextNode.valueOf("first"), "key2", BooleanNode.TRUE)), + new ObjectNode(JsonNodeFactory.instance, ImmutableMap.of("key3", IntNode.valueOf(1), "key4", NullNode.instance)))), + path(false, memberAccessor(wildcardArrayAccessor(contextVariable()), "key2")))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: structural error: missing member 'key2' in JSON object"); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, keyValue(jsonVariable("null_parameter"))))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: OBJECT, actual: NULL"); + } + + @Test + public void testSizeMethod() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, size(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 1L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, size(variable("short_decimal_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 1L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, size(jsonVariable("json_boolean_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 1L))); + + assertThat(pathResult( + NullNode.instance, + path(true, size(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 1L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, size(jsonVariable("json_object_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 1L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, size(jsonVariable("json_array_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(INTEGER, 3L))); + + // multiple inputs + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(BooleanNode.TRUE, BooleanNode.FALSE)))), + path(true, size(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence(new TypedValue(INTEGER, 1L), new TypedValue(INTEGER, 2L))); + + // type mismatch + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(false, size(contextVariable())))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: invalid item type. Expected: ARRAY, actual: NUMBER"); + } + + @Test + public void testTypeMethod() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, type(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(27), utf8Slice("number")))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, type(variable("string_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(27), utf8Slice("string")))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, type(jsonVariable("json_boolean_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(27), utf8Slice("boolean")))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, type(jsonVariable("json_array_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(27), utf8Slice("array")))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, type(jsonVariable("json_object_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(27), utf8Slice("object")))); + + assertThat(pathResult( + NullNode.instance, + path(true, type(contextVariable())))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(27), utf8Slice("null")))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, type(variable("date_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(27), utf8Slice("date")))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, type(variable("timestamp_parameter"))))) + .isEqualTo(singletonSequence(new TypedValue(createVarcharType(27), utf8Slice("timestamp without time zone")))); + + // multiple inputs + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(BooleanNode.TRUE, BooleanNode.FALSE)))), + path(true, type(wildcardArrayAccessor(contextVariable()))))) + .isEqualTo(sequence(new TypedValue(createVarcharType(27), utf8Slice("number")), new TypedValue(createVarcharType(27), utf8Slice("array")))); + } + + // JSON PREDICATE + @Test + public void testComparisonPredicate() + { + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + equal(contextVariable(), currentItem()))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + notEqual(jsonVariable("json_number_parameter"), variable("double_parameter")))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + lessThan(literal(BOOLEAN, true), literal(BOOLEAN, false)))) + .isEqualTo(FALSE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + greaterThan(literal(VARCHAR, utf8Slice("xyz")), literal(VARCHAR, utf8Slice("abc"))))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + lessThanOrEqual(literal(DATE, 0L), variable("date_parameter")))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + greaterThanOrEqual(literal(BIGINT, 1L), literal(BIGINT, 2L)))) + .isEqualTo(FALSE); + + // uncomparable items -> result unknown + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + false, + lessThan(contextVariable(), currentItem()))) + .isEqualTo(null); + + // nulls can be compared with every item + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + equal(jsonNull(), jsonNull()))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + lessThan(jsonNull(), currentItem()))) + .isEqualTo(FALSE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + notEqual(jsonNull(), jsonVariable("json_object_parameter")))) + .isEqualTo(TRUE); + + // array / object can only be compared with null. otherwise the result is unknown + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + notEqual(jsonVariable("json_object_parameter"), jsonVariable("json_object_parameter")))) + .isEqualTo(null); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + notEqual(jsonVariable("json_array_parameter"), jsonVariable("json_object_parameter")))) + .isEqualTo(null); + + // array is automatically unwrapped in lax mode + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1))), + LongNode.valueOf(1L), + true, + equal(contextVariable(), literal(BIGINT, 1L)))) + .isEqualTo(TRUE); + + // array is not unwrapped in strict mode + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1))), + LongNode.valueOf(1L), + false, + equal(contextVariable(), literal(BIGINT, 1L)))) + .isEqualTo(null); + + // multiple items - first success or failure in lax mode + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1), IntNode.valueOf(2))), + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(3), BooleanNode.TRUE)), + true, + lessThan(contextVariable(), currentItem()))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(BooleanNode.TRUE, IntNode.valueOf(2))), + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(3), IntNode.valueOf(1))), + true, + lessThan(contextVariable(), currentItem()))) + .isEqualTo(null); + + // null based equal in lax mode + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1), NullNode.instance)), + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(3), NullNode.instance)), + true, + equal(contextVariable(), currentItem()))) + .isEqualTo(TRUE); + + // null based not equal in lax mode + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1), NullNode.instance)), + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(3), BooleanNode.TRUE)), + true, + notEqual(contextVariable(), currentItem()))) + .isEqualTo(TRUE); + + // multiple items - fail if any comparison returns error in strict mode + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1), IntNode.valueOf(2))), + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(3), BooleanNode.TRUE)), + false, + lessThan(contextVariable(), currentItem()))) + .isEqualTo(null); + + // error while evaluating nested path (floor method called on a text value) -> result unknown + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + lessThan(contextVariable(), floor(currentItem())))) + .isEqualTo(null); + + // left operand returns empty sequence -> result false + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1), IntNode.valueOf(2))), + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(3), IntNode.valueOf(4))), + true, + lessThan(arrayAccessor(contextVariable(), at(literal(BIGINT, 100L))), currentItem()))) + .isEqualTo(false); + + // right operand returns empty sequence -> result false + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(1), IntNode.valueOf(2))), + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(IntNode.valueOf(3), IntNode.valueOf(4))), + true, + lessThan(contextVariable(), arrayAccessor(currentItem(), at(literal(BIGINT, 100L)))))) + .isEqualTo(false); + } + + @Test + public void testConjunctionPredicate() + { + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + conjunction( + equal(literal(BIGINT, 1L), literal(BIGINT, 1L)), // true + equal(literal(BIGINT, 2L), literal(BIGINT, 2L))))) // true + .isEqualTo(TRUE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + conjunction( + equal(literal(BIGINT, 1L), literal(BIGINT, 1L)), // true + equal(literal(BIGINT, 2L), literal(BOOLEAN, false))))) // unknown + .isEqualTo(null); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + conjunction( + equal(literal(BIGINT, 1L), literal(BOOLEAN, false)), // unknown + equal(literal(BIGINT, 2L), literal(BIGINT, 3L))))) // false + .isEqualTo(FALSE); + } + + @Test + public void testDisjunctionPredicate() + { + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + disjunction( + equal(literal(BIGINT, 1L), literal(BIGINT, 2L)), // false + equal(literal(BIGINT, 2L), literal(BIGINT, 2L))))) // true + .isEqualTo(TRUE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + disjunction( + equal(literal(BIGINT, 1L), literal(BIGINT, 2L)), // false + equal(literal(BIGINT, 1L), literal(BOOLEAN, false))))) // unknown + .isEqualTo(null); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + disjunction( + equal(literal(BIGINT, 1L), literal(BIGINT, 2L)), // false + equal(literal(BIGINT, 2L), literal(BIGINT, 3L))))) // false + .isEqualTo(FALSE); + } + + @Test + public void testExistsPredicate() + { + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + exists(contextVariable()))) + .isEqualTo(TRUE); + + // member accessor with non-existent key returns empty sequence in lax mode + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + exists(memberAccessor(jsonVariable("json_object_parameter"), "wrong_key")))) + .isEqualTo(FALSE); + + // member accessor with non-existent key returns error in strict mode + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + false, + exists(memberAccessor(jsonVariable("json_object_parameter"), "wrong_key")))) + .isEqualTo(null); + } + + @Test + public void testIsUnknownPredicate() + { + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + isUnknown(equal(literal(BIGINT, 1L), literal(BIGINT, 1L))))) // true + .isEqualTo(FALSE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + isUnknown(equal(literal(BIGINT, 1L), literal(BIGINT, 2L))))) // false + .isEqualTo(FALSE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + LongNode.valueOf(1L), + true, + isUnknown(equal(literal(BIGINT, 1L), literal(BOOLEAN, true))))) // unknown + .isEqualTo(TRUE); + } + + @Test + public void testNegationPredicate() + { + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + negation(equal(literal(BIGINT, 1L), literal(BIGINT, 1L))))) + .isEqualTo(FALSE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + negation(notEqual(literal(BIGINT, 1L), literal(BIGINT, 1L))))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + DoubleNode.valueOf(1e0), + TextNode.valueOf("abc"), + true, + negation(equal(literal(BIGINT, 1L), literal(BOOLEAN, false))))) + .isEqualTo(null); + } + + @Test + public void testStartsWithPredicate() + { + assertThat(predicateResult( + TextNode.valueOf("abcde"), + TextNode.valueOf("abc"), + true, + startsWith(contextVariable(), currentItem()))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + TextNode.valueOf("abcde"), + TextNode.valueOf("abc"), + true, + startsWith(jsonVariable("json_text_parameter"), literal(createCharType(4), utf8Slice("JSON"))))) + .isEqualTo(TRUE); + + assertThat(predicateResult( + TextNode.valueOf("abcde"), + TextNode.valueOf("abc"), + true, + startsWith(literal(VARCHAR, utf8Slice("XYZ")), variable("string_parameter")))) + .isEqualTo(FALSE); + + // multiple inputs - returning true if any match is found + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("aBC"), TextNode.valueOf("abc"), TextNode.valueOf("Abc"))), + TextNode.valueOf("abc"), + true, + startsWith(wildcardArrayAccessor(contextVariable()), literal(createVarcharType(1), utf8Slice("A"))))) + .isEqualTo(TRUE); + + // multiple inputs - returning true if any match is found. array is automatically unwrapped in lax mode + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("aBC"), TextNode.valueOf("abc"), TextNode.valueOf("Abc"))), + TextNode.valueOf("abc"), + true, + startsWith(contextVariable(), literal(createVarcharType(1), utf8Slice("A"))))) + .isEqualTo(TRUE); + + // lax mode: true is returned on the first match, even if there is an uncomparable item + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("Abc"), NullNode.instance)), + TextNode.valueOf("abc"), + true, + startsWith(contextVariable(), literal(createVarcharType(1), utf8Slice("A"))))) + .isEqualTo(TRUE); + + // lax mode: unknown is returned because there is an uncomparable item, even if match is found first + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("Abc"), NullNode.instance)), + TextNode.valueOf("abc"), + false, + startsWith(contextVariable(), literal(createVarcharType(1), utf8Slice("A"))))) + .isEqualTo(null); + + // lax mode: unknown is returned because the uncomparable item is before the matching item + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(NullNode.instance, TextNode.valueOf("Abc"))), + TextNode.valueOf("abc"), + true, + startsWith(contextVariable(), literal(createVarcharType(1), utf8Slice("A"))))) + .isEqualTo(null); + + // error while evaluating the first operand (floor method called on a text value) -> result unknown + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(NullNode.instance, TextNode.valueOf("Abc"))), + TextNode.valueOf("abc"), + true, + startsWith(floor(literal(VARCHAR, utf8Slice("x"))), literal(VARCHAR, utf8Slice("A"))))) + .isEqualTo(null); + + // error while evaluating the second operand (floor method called on a text value) -> result unknown + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(NullNode.instance, TextNode.valueOf("Abc"))), + TextNode.valueOf("abc"), + true, + startsWith(literal(VARCHAR, utf8Slice("x")), floor(literal(VARCHAR, utf8Slice("A")))))) + .isEqualTo(null); + + // the second operand returns multiple items -> result unknown + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(TextNode.valueOf("A"), TextNode.valueOf("B"))), + TextNode.valueOf("abc"), + true, + startsWith(literal(VARCHAR, utf8Slice("x")), wildcardArrayAccessor(contextVariable())))) + .isEqualTo(null); + + // the second operand is not text -> result unknown + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(NullNode.instance, TextNode.valueOf("Abc"))), + TextNode.valueOf("abc"), + true, + startsWith(literal(VARCHAR, utf8Slice("x")), literal(BIGINT, 1L)))) + .isEqualTo(null); + + // the first operand returns empty sequence -> result false + assertThat(predicateResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(NullNode.instance, TextNode.valueOf("Abc"))), + TextNode.valueOf("abc"), + true, + startsWith(arrayAccessor(contextVariable(), at(literal(BIGINT, 100L))), literal(VARCHAR, utf8Slice("A"))))) + .isEqualTo(FALSE); + } + + @Test + public void testFilter() + { + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, filter(literal(BIGINT, 5L), greaterThan(currentItem(), literal(BIGINT, 3L)))))) // true + .isEqualTo(singletonSequence(new TypedValue(BIGINT, 5L))); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, filter(literal(BIGINT, 5L), lessThan(currentItem(), literal(BIGINT, 3L)))))) // false + .isEqualTo(emptySequence()); + + assertThat(pathResult( + IntNode.valueOf(-5), + path(true, filter(literal(BIGINT, 5L), lessThan(currentItem(), literal(BOOLEAN, true)))))) // unknown + .isEqualTo(emptySequence()); + + // multiple input items + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), IntNode.valueOf(2), LongNode.valueOf(5), ShortNode.valueOf((short) 10))), + path(true, filter(wildcardArrayAccessor(contextVariable()), greaterThan(currentItem(), literal(BIGINT, 3L)))))) + .isEqualTo(sequence(LongNode.valueOf(5), ShortNode.valueOf((short) 10))); + + // multiple input items -- array is automatically unwrapped in lax mode + assertThat(pathResult( + new ArrayNode(JsonNodeFactory.instance, ImmutableList.of(DoubleNode.valueOf(1.5e0), IntNode.valueOf(2), LongNode.valueOf(5), ShortNode.valueOf((short) 10))), + path(true, filter(contextVariable(), greaterThan(currentItem(), literal(BIGINT, 3L)))))) + .isEqualTo(sequence(LongNode.valueOf(5), ShortNode.valueOf((short) 10))); + } + + @Test + public void testCurrentItemVariable() + { + assertThatThrownBy(() -> evaluate( + IntNode.valueOf(-5), + path(true, currentItem()))) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: accessing current filter item with no enclosing filter"); + } + + private static IrPathNode variable(String name) + { + return PathNodes.variable(PARAMETERS_ORDER.indexOf(name)); + } + + private static IrPathNode jsonVariable(String name) + { + return PathNodes.jsonVariable(PARAMETERS_ORDER.indexOf(name)); + } + + private static AssertProvider> pathResult(JsonNode input, IrJsonPath path) + { + return () -> new RecursiveComparisonAssert<>(evaluate(input, path), COMPARISON_CONFIGURATION); + } + + private static List evaluate(JsonNode input, IrJsonPath path) + { + return createPathVisitor(input, path.isLax()).process(path.getRoot(), new PathEvaluationContext()); + } + + private static AssertProvider> predicateResult(JsonNode input, Object currentItem, boolean lax, IrPredicate predicate) + { + return () -> new RecursiveComparisonAssert<>(evaluatePredicate(input, currentItem, lax, predicate), COMPARISON_CONFIGURATION); + } + + private static Boolean evaluatePredicate(JsonNode input, Object currentItem, boolean lax, IrPredicate predicate) + { + return createPredicateVisitor(input, lax).process(predicate, new PathEvaluationContext().withCurrentItem(currentItem)); + } + + private static PathEvaluationVisitor createPathVisitor(JsonNode input, boolean lax) + { + return new PathEvaluationVisitor( + lax, + input, + PARAMETERS.values().toArray(), + new JsonPathEvaluator.Invoker(testSessionBuilder().build().toConnectorSession(), createTestingFunctionManager()), + new CachingResolver(createTestMetadataManager(), testSessionBuilder().build().toConnectorSession(), new TestingTypeManager())); + } + + private static PathPredicateEvaluationVisitor createPredicateVisitor(JsonNode input, boolean lax) + { + return new PathPredicateEvaluationVisitor( + lax, + createPathVisitor(input, lax), + new JsonPathEvaluator.Invoker(testSessionBuilder().build().toConnectorSession(), createTestingFunctionManager()), + new CachingResolver(createTestMetadataManager(), testSessionBuilder().build().toConnectorSession(), new TestingTypeManager())); + } +} diff --git a/core/trino-main/src/test/java/io/trino/json/ir/TestSqlJsonLiteralConverter.java b/core/trino-main/src/test/java/io/trino/json/ir/TestSqlJsonLiteralConverter.java new file mode 100644 index 000000000000..05009fbe8de6 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/json/ir/TestSqlJsonLiteralConverter.java @@ -0,0 +1,252 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.json.ir; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BigIntegerNode; +import com.fasterxml.jackson.databind.node.BinaryNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.FloatNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.ShortNode; +import com.fasterxml.jackson.databind.node.TextNode; +import io.trino.json.ir.SqlJsonLiteralConverter.JsonLiteralConversionError; +import io.trino.spi.type.Int128; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.RecursiveComparisonAssert; +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Optional; + +import static io.airlift.slice.Slices.utf8Slice; +import static io.trino.json.ir.SqlJsonLiteralConverter.getJsonNode; +import static io.trino.json.ir.SqlJsonLiteralConverter.getNumericTypedValue; +import static io.trino.json.ir.SqlJsonLiteralConverter.getTextTypedValue; +import static io.trino.json.ir.SqlJsonLiteralConverter.getTypedValue; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.CharType.createCharType; +import static io.trino.spi.type.DateType.DATE; +import static io.trino.spi.type.DecimalType.createDecimalType; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.RealType.REAL; +import static io.trino.spi.type.SmallintType.SMALLINT; +import static io.trino.spi.type.TinyintType.TINYINT; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createVarcharType; +import static java.lang.Float.floatToIntBits; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestSqlJsonLiteralConverter +{ + private static final RecursiveComparisonConfiguration COMPARISON_CONFIGURATION = RecursiveComparisonConfiguration.builder().withStrictTypeChecking(true).build(); + + @Test + public void testNumberToJson() + { + assertThat(json(new TypedValue(TINYINT, 1L))) + .isEqualTo(ShortNode.valueOf((short) 1)); + + assertThat(json(new TypedValue(SMALLINT, 1L))) + .isEqualTo(ShortNode.valueOf((short) 1)); + + assertThat(json(new TypedValue(INTEGER, 1L))) + .isEqualTo(IntNode.valueOf(1)); + + assertThat(json(new TypedValue(BIGINT, 1L))) + .isEqualTo(LongNode.valueOf(1)); + + assertThat(json(new TypedValue(DOUBLE, 1e0))) + .isEqualTo(DoubleNode.valueOf(1)); + + assertThat(json(new TypedValue(DOUBLE, Double.NaN))) + .isEqualTo(DoubleNode.valueOf(Double.NaN)); + + assertThat(json(new TypedValue(DOUBLE, Double.NEGATIVE_INFINITY))) + .isEqualTo(DoubleNode.valueOf(Double.NEGATIVE_INFINITY)); + + assertThat(json(new TypedValue(DOUBLE, Double.POSITIVE_INFINITY))) + .isEqualTo(DoubleNode.valueOf(Double.POSITIVE_INFINITY)); + + assertThat(json(new TypedValue(REAL, floatToIntBits(1F)))) + .isEqualTo(FloatNode.valueOf(1F)); + + assertThat(json(new TypedValue(REAL, floatToIntBits(Float.NaN)))) + .isEqualTo(FloatNode.valueOf(Float.NaN)); + + assertThat(json(new TypedValue(REAL, floatToIntBits(Float.NEGATIVE_INFINITY)))) + .isEqualTo(FloatNode.valueOf(Float.NEGATIVE_INFINITY)); + + assertThat(json(new TypedValue(REAL, floatToIntBits(Float.POSITIVE_INFINITY)))) + .isEqualTo(FloatNode.valueOf(Float.POSITIVE_INFINITY)); + + assertThat(json(new TypedValue(createDecimalType(2, 1), 1L))) + .isEqualTo(DecimalNode.valueOf(new BigDecimal(BigInteger.ONE, 1))); + + assertThat(json(new TypedValue(createDecimalType(30, 20), Int128.valueOf(BigInteger.ONE)))) + .isEqualTo(DecimalNode.valueOf(new BigDecimal(BigInteger.ONE, 20))); + } + + @Test + public void testCharacterStringToJson() + { + assertThat(json(new TypedValue(VARCHAR, utf8Slice("abc")))) + .isEqualTo(TextNode.valueOf("abc")); + + assertThat(json(new TypedValue(createVarcharType(10), utf8Slice("abc")))) + .isEqualTo(TextNode.valueOf("abc")); + + assertThat(json(new TypedValue(createCharType(10), utf8Slice("abc")))) + .isEqualTo(TextNode.valueOf("abc ")); + } + + @Test + public void testBooleanToJson() + { + assertThat(json(new TypedValue(BOOLEAN, true))) + .isEqualTo(BooleanNode.TRUE); + + assertThat(json(new TypedValue(BOOLEAN, false))) + .isEqualTo(BooleanNode.FALSE); + } + + @Test + public void testNoConversionToJson() + { + // datetime types are supported in the path engine, but they are not supported in JSON + assertThat(getJsonNode(new TypedValue(DATE, 1L))) + .isEqualTo(Optional.empty()); + } + + @Test + public void testJsonToNumber() + { + BigInteger bigValue = BigInteger.valueOf(1000000000000000000L).multiply(BigInteger.valueOf(1000000000000000000L)); + + assertThat(typedValueResult(BigIntegerNode.valueOf(BigInteger.ONE))) + .isEqualTo(new TypedValue(INTEGER, 1L)); + + assertThat(typedValueResult(BigIntegerNode.valueOf(BigInteger.valueOf(1000000000000000000L)))) + .isEqualTo(new TypedValue(BIGINT, 1000000000000000000L)); + + assertThatThrownBy(() -> getTypedValue(BigIntegerNode.valueOf(bigValue))) + .isInstanceOf(JsonLiteralConversionError.class) + .hasMessage("cannot convert 1000000000000000000000000000000000000 to Trino value (value too big)"); + + assertThat(typedValueResult(DecimalNode.valueOf(BigDecimal.ONE))) + .isEqualTo(new TypedValue(createDecimalType(1, 0), 1L)); + + assertThat(typedValueResult(DecimalNode.valueOf(new BigDecimal(bigValue, 20)))) + .isEqualTo(new TypedValue(createDecimalType(37, 20), Int128.valueOf(bigValue))); + + assertThatThrownBy(() -> getTypedValue(BigIntegerNode.valueOf(bigValue.multiply(bigValue)))) + .isInstanceOf(JsonLiteralConversionError.class) + .hasMessage("cannot convert 1000000000000000000000000000000000000000000000000000000000000000000000000 to Trino value (value too big)"); + + assertThat(typedValueResult(DoubleNode.valueOf(1e0))) + .isEqualTo(new TypedValue(DOUBLE, 1e0)); + + assertThat(typedValueResult(DoubleNode.valueOf(Double.NEGATIVE_INFINITY))) + .isEqualTo(new TypedValue(DOUBLE, Double.NEGATIVE_INFINITY)); + + assertThat(typedValueResult(DoubleNode.valueOf(Double.POSITIVE_INFINITY))) + .isEqualTo(new TypedValue(DOUBLE, Double.POSITIVE_INFINITY)); + + assertThat(typedValueResult(FloatNode.valueOf(1F))) + .isEqualTo(new TypedValue(REAL, floatToIntBits(1F))); + + assertThat(typedValueResult(FloatNode.valueOf(Float.NEGATIVE_INFINITY))) + .isEqualTo(new TypedValue(REAL, floatToIntBits(Float.NEGATIVE_INFINITY))); + + assertThat(typedValueResult(FloatNode.valueOf(Float.POSITIVE_INFINITY))) + .isEqualTo(new TypedValue(REAL, floatToIntBits(Float.POSITIVE_INFINITY))); + + assertThat(typedValueResult(IntNode.valueOf(1))) + .isEqualTo(new TypedValue(INTEGER, 1L)); + + assertThat(typedValueResult(LongNode.valueOf(1))) + .isEqualTo(new TypedValue(BIGINT, 1L)); + + assertThat(typedValueResult(ShortNode.valueOf((short) 1))) + .isEqualTo(new TypedValue(SMALLINT, 1L)); + } + + @Test + public void testJsonToCharacterString() + { + assertThat(typedValueResult(TextNode.valueOf("abc "))) + .isEqualTo(new TypedValue(VARCHAR, utf8Slice("abc "))); + } + + @Test + public void testJsonToBoolean() + { + assertThat(typedValueResult(BooleanNode.TRUE)) + .isEqualTo(new TypedValue(BOOLEAN, true)); + + assertThat(typedValueResult(BooleanNode.FALSE)) + .isEqualTo(new TypedValue(BOOLEAN, false)); + } + + @Test + public void testJsonToIncompatibleType() + { + assertThat(getNumericTypedValue(TextNode.valueOf("abc"))) + .isEqualTo(Optional.empty()); + + assertThat(getTextTypedValue(NullNode.instance)) + .isEqualTo(Optional.empty()); + } + + @Test + public void testNoConversionFromJson() + { + // unsupported node type + assertThat(getTextTypedValue(BinaryNode.valueOf(new byte[] {}))) + .isEqualTo(Optional.empty()); + + // not a value node + assertThat(getTextTypedValue(MissingNode.getInstance())) + .isEqualTo(Optional.empty()); + + assertThat(getTextTypedValue(new ObjectNode(JsonNodeFactory.instance))) + .isEqualTo(Optional.empty()); + + assertThat(getTextTypedValue(new ArrayNode(JsonNodeFactory.instance))) + .isEqualTo(Optional.empty()); + } + + private static JsonNode json(TypedValue value) + { + return getJsonNode(value).orElseThrow(); + } + + private static AssertProvider> typedValueResult(JsonNode node) + { + return () -> new RecursiveComparisonAssert<>(getTypedValue(node).orElseThrow(), COMPARISON_CONFIGURATION); + } +} diff --git a/core/trino-main/src/test/java/io/trino/operator/scalar/BenchmarkJsonFunctions.java b/core/trino-main/src/test/java/io/trino/operator/scalar/BenchmarkJsonFunctions.java new file mode 100644 index 000000000000..1f8fec41b6f9 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/operator/scalar/BenchmarkJsonFunctions.java @@ -0,0 +1,350 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar; + +import com.google.common.collect.ImmutableList; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.SliceOutput; +import io.trino.FullConnectorSession; +import io.trino.jmh.Benchmarks; +import io.trino.json.ir.IrContextVariable; +import io.trino.json.ir.IrJsonPath; +import io.trino.json.ir.IrMemberAccessor; +import io.trino.json.ir.IrPathNode; +import io.trino.metadata.TestingFunctionResolution; +import io.trino.operator.DriverYieldSignal; +import io.trino.operator.project.PageProcessor; +import io.trino.spi.Page; +import io.trino.spi.block.Block; +import io.trino.spi.block.BlockBuilder; +import io.trino.spi.security.ConnectorIdentity; +import io.trino.spi.type.Type; +import io.trino.spi.type.TypeId; +import io.trino.sql.relational.CallExpression; +import io.trino.sql.relational.RowExpression; +import io.trino.sql.tree.QualifiedName; +import io.trino.testing.TestingSession; +import io.trino.type.JsonPath2016Type; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.options.WarmupMode; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import static io.trino.memory.context.AggregatedMemoryContext.newSimpleAggregatedMemoryContext; +import static io.trino.operator.scalar.json.JsonInputFunctions.VARCHAR_TO_JSON; +import static io.trino.operator.scalar.json.JsonQueryFunction.JSON_QUERY_FUNCTION_NAME; +import static io.trino.operator.scalar.json.JsonValueFunction.JSON_VALUE_FUNCTION_NAME; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.TinyintType.TINYINT; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createVarcharType; +import static io.trino.sql.analyzer.ExpressionAnalyzer.JSON_NO_PARAMETERS_ROW_TYPE; +import static io.trino.sql.analyzer.TypeSignatureProvider.fromTypes; +import static io.trino.sql.planner.TestingPlannerContext.PLANNER_CONTEXT; +import static io.trino.sql.relational.Expressions.constant; +import static io.trino.sql.relational.Expressions.constantNull; +import static io.trino.sql.relational.Expressions.field; +import static io.trino.testing.TestingConnectorSession.SESSION; +import static io.trino.type.Json2016Type.JSON_2016; +import static io.trino.type.JsonPathType.JSON_PATH; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Benchmark new vs old JSON functions. + * `json_extract` and `json_extract_scalar` are JSON-processing functions which have very limited capabilities, and are not compliant with the spec. + * However, they have simple lightweight implementation, optimized for the use case. + * `json_query` and `json_value` are new spec-compliant JSON-processing functions, which support the complete specification for JSON path. + * Their implementation is much more complicated, resulting both from the scope of the supported feature, and the "streaming" semantics. + * This benchmark is to compare both implementations applied to the common use-case. + *

+ * Compare: `benchmarkJsonValueFunction` vs `benchmarkJsonExtractScalarFunction` + * and `benchmarkJsonQueryFunction` vs `benchmarkJsonExtractFunction`. + */ +@SuppressWarnings("MethodMayBeStatic") +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(2) +@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) +@BenchmarkMode(Mode.AverageTime) +public class BenchmarkJsonFunctions +{ + private static final int POSITION_COUNT = 100_000; + private static final FullConnectorSession FULL_CONNECTOR_SESSION = new FullConnectorSession(TestingSession.testSessionBuilder().build(), ConnectorIdentity.ofUser("test")); + + @Benchmark + @OperationsPerInvocation(POSITION_COUNT) + public List> benchmarkJsonValueFunction(BenchmarkData data) + { + return ImmutableList.copyOf( + data.getJsonValuePageProcessor().process( + FULL_CONNECTOR_SESSION, + new DriverYieldSignal(), + newSimpleAggregatedMemoryContext().newLocalMemoryContext(PageProcessor.class.getSimpleName()), + data.getPage())); + } + + @Benchmark + @OperationsPerInvocation(POSITION_COUNT) + public List> benchmarkJsonExtractScalarFunction(BenchmarkData data) + { + return ImmutableList.copyOf( + data.getJsonExtractScalarPageProcessor().process( + FULL_CONNECTOR_SESSION, + new DriverYieldSignal(), + newSimpleAggregatedMemoryContext().newLocalMemoryContext(PageProcessor.class.getSimpleName()), + data.getPage())); + } + + @Benchmark + @OperationsPerInvocation(POSITION_COUNT) + public List> benchmarkJsonQueryFunction(BenchmarkData data) + { + return ImmutableList.copyOf( + data.getJsonQueryPageProcessor().process( + FULL_CONNECTOR_SESSION, + new DriverYieldSignal(), + newSimpleAggregatedMemoryContext().newLocalMemoryContext(PageProcessor.class.getSimpleName()), + data.getPage())); + } + + @Benchmark + @OperationsPerInvocation(POSITION_COUNT) + public List> benchmarkJsonExtractFunction(BenchmarkData data) + { + return ImmutableList.copyOf( + data.getJsonExtractPageProcessor().process( + SESSION, + new DriverYieldSignal(), + newSimpleAggregatedMemoryContext().newLocalMemoryContext(PageProcessor.class.getSimpleName()), + data.getPage())); + } + + @SuppressWarnings("FieldMayBeFinal") + @State(Scope.Thread) + public static class BenchmarkData + { + @Param({"1", "3", "10"}) + private int depth; + + private Page page; + private PageProcessor jsonValuePageProcessor; + private PageProcessor jsonExtractScalarPageProcessor; + private PageProcessor jsonQueryPageProcessor; + private PageProcessor jsonExtractPageProcessor; + + @Setup + public void setup() + { + page = new Page(createChannel(POSITION_COUNT, depth)); + + TestingFunctionResolution functionResolution = new TestingFunctionResolution(); + Type jsonPath2016Type = PLANNER_CONTEXT.getTypeManager().getType(TypeId.of(JsonPath2016Type.NAME)); + + jsonValuePageProcessor = createJsonValuePageProcessor(depth, functionResolution, jsonPath2016Type); + jsonExtractScalarPageProcessor = createJsonExtractScalarPageProcessor(depth, functionResolution); + jsonQueryPageProcessor = createJsonQueryPageProcessor(depth, functionResolution, jsonPath2016Type); + jsonExtractPageProcessor = createJsonExtractPageProcessor(depth, functionResolution); + } + + private static PageProcessor createJsonValuePageProcessor(int depth, TestingFunctionResolution functionResolution, Type jsonPath2016Type) + { + IrPathNode pathRoot = new IrContextVariable(Optional.empty()); + for (int i = 1; i <= depth; i++) { + pathRoot = new IrMemberAccessor(pathRoot, Optional.of("key" + i), Optional.empty()); + } + List jsonValueProjection = ImmutableList.of(new CallExpression( + functionResolution.resolveFunction( + QualifiedName.of(JSON_VALUE_FUNCTION_NAME), + fromTypes(ImmutableList.of( + JSON_2016, + jsonPath2016Type, + JSON_NO_PARAMETERS_ROW_TYPE, + TINYINT, + VARCHAR, + TINYINT, + VARCHAR))), + ImmutableList.of( + new CallExpression( + functionResolution.resolveFunction(QualifiedName.of(VARCHAR_TO_JSON), fromTypes(VARCHAR, BOOLEAN)), + ImmutableList.of(field(0, VARCHAR), constant(true, BOOLEAN))), + constant(new IrJsonPath(false, pathRoot), jsonPath2016Type), + constantNull(JSON_NO_PARAMETERS_ROW_TYPE), + constant(0L, TINYINT), + constantNull(VARCHAR), + constant(0L, TINYINT), + constantNull(VARCHAR)))); + + return functionResolution.getExpressionCompiler() + .compilePageProcessor(Optional.empty(), jsonValueProjection) + .get(); + } + + private static PageProcessor createJsonExtractScalarPageProcessor(int depth, TestingFunctionResolution functionResolution) + { + StringBuilder pathString = new StringBuilder("$"); + for (int i = 1; i <= depth; i++) { + pathString + .append(".key") + .append(i); + } + Type boundedVarcharType = createVarcharType(100); + List jsonExtractScalarProjection = ImmutableList.of(new CallExpression( + functionResolution.resolveFunction(QualifiedName.of("json_extract_scalar"), fromTypes(ImmutableList.of(boundedVarcharType, JSON_PATH))), + ImmutableList.of(field(0, boundedVarcharType), constant(new JsonPath(pathString.toString()), JSON_PATH)))); + + return functionResolution.getExpressionCompiler() + .compilePageProcessor(Optional.empty(), jsonExtractScalarProjection) + .get(); + } + + private static PageProcessor createJsonQueryPageProcessor(int depth, TestingFunctionResolution functionResolution, Type jsonPath2016Type) + { + IrPathNode pathRoot = new IrContextVariable(Optional.empty()); + for (int i = 1; i <= depth - 1; i++) { + pathRoot = new IrMemberAccessor(pathRoot, Optional.of("key" + i), Optional.empty()); + } + List jsonQueryProjection = ImmutableList.of(new CallExpression( + functionResolution.resolveFunction( + QualifiedName.of(JSON_QUERY_FUNCTION_NAME), + fromTypes(ImmutableList.of( + JSON_2016, + jsonPath2016Type, + JSON_NO_PARAMETERS_ROW_TYPE, + TINYINT, + TINYINT, + TINYINT))), + ImmutableList.of( + new CallExpression( + functionResolution.resolveFunction(QualifiedName.of(VARCHAR_TO_JSON), fromTypes(VARCHAR, BOOLEAN)), + ImmutableList.of(field(0, VARCHAR), constant(true, BOOLEAN))), + constant(new IrJsonPath(false, pathRoot), jsonPath2016Type), + constantNull(JSON_NO_PARAMETERS_ROW_TYPE), + constant(0L, TINYINT), + constant(0L, TINYINT), + constant(0L, TINYINT)))); + + return functionResolution.getExpressionCompiler() + .compilePageProcessor(Optional.empty(), jsonQueryProjection) + .get(); + } + + private static PageProcessor createJsonExtractPageProcessor(int depth, TestingFunctionResolution functionResolution) + { + StringBuilder pathString = new StringBuilder("$"); + for (int i = 1; i <= depth - 1; i++) { + pathString + .append(".key") + .append(i); + } + Type boundedVarcharType = createVarcharType(100); + List jsonExtractScalarProjection = ImmutableList.of(new CallExpression( + functionResolution.resolveFunction(QualifiedName.of("json_extract"), fromTypes(ImmutableList.of(boundedVarcharType, JSON_PATH))), + ImmutableList.of(field(0, boundedVarcharType), constant(new JsonPath(pathString.toString()), JSON_PATH)))); + + return functionResolution.getExpressionCompiler() + .compilePageProcessor(Optional.empty(), jsonExtractScalarProjection) + .get(); + } + + private static Block createChannel(int positionCount, int depth) + { + BlockBuilder blockBuilder = VARCHAR.createBlockBuilder(null, positionCount); + for (int position = 0; position < positionCount; position++) { + SliceOutput slice = new DynamicSliceOutput(20); + for (int i = 1; i <= depth; i++) { + slice.appendBytes(("{\"key" + i + "\" : ").getBytes(UTF_8)); + } + slice.appendBytes(generateRandomJsonText().getBytes(UTF_8)); + for (int i = 1; i <= depth; i++) { + slice.appendByte('}'); + } + + VARCHAR.writeSlice(blockBuilder, slice.slice()); + } + return blockBuilder.build(); + } + + private static String generateRandomJsonText() + { + String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + + int length = ThreadLocalRandom.current().nextInt(10) + 1; + StringBuilder builder = new StringBuilder(length + 2); + builder.append('"'); + for (int i = 0; i < length; i++) { + builder.append(characters.charAt(ThreadLocalRandom.current().nextInt(characters.length()))); + } + builder.append('"'); + return builder.toString(); + } + + public PageProcessor getJsonValuePageProcessor() + { + return jsonValuePageProcessor; + } + + public PageProcessor getJsonExtractScalarPageProcessor() + { + return jsonExtractScalarPageProcessor; + } + + public PageProcessor getJsonQueryPageProcessor() + { + return jsonQueryPageProcessor; + } + + public PageProcessor getJsonExtractPageProcessor() + { + return jsonExtractPageProcessor; + } + + public Page getPage() + { + return page; + } + } + + @Test + public void verify() + { + BenchmarkData data = new BenchmarkData(); + data.setup(); + new BenchmarkJsonFunctions().benchmarkJsonValueFunction(data); + new BenchmarkJsonFunctions().benchmarkJsonExtractScalarFunction(data); + new BenchmarkJsonFunctions().benchmarkJsonQueryFunction(data); + new BenchmarkJsonFunctions().benchmarkJsonExtractFunction(data); + } + + public static void main(String[] args) + throws Exception + { + Benchmarks.benchmark(BenchmarkJsonFunctions.class, WarmupMode.BULK_INDI).run(); + } +} diff --git a/core/trino-main/src/test/java/io/trino/operator/scalar/BenchmarkJsonPathBinaryOperators.java b/core/trino-main/src/test/java/io/trino/operator/scalar/BenchmarkJsonPathBinaryOperators.java new file mode 100644 index 000000000000..840999375d47 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/operator/scalar/BenchmarkJsonPathBinaryOperators.java @@ -0,0 +1,289 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar; + +import com.google.common.collect.ImmutableList; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.SliceOutput; +import io.trino.FullConnectorSession; +import io.trino.jmh.Benchmarks; +import io.trino.json.ir.IrArithmeticBinary; +import io.trino.json.ir.IrContextVariable; +import io.trino.json.ir.IrJsonPath; +import io.trino.json.ir.IrMemberAccessor; +import io.trino.json.ir.IrPathNode; +import io.trino.metadata.TestingFunctionResolution; +import io.trino.operator.DriverYieldSignal; +import io.trino.operator.project.PageProcessor; +import io.trino.spi.Page; +import io.trino.spi.block.Block; +import io.trino.spi.block.BlockBuilder; +import io.trino.spi.security.ConnectorIdentity; +import io.trino.spi.type.Decimals; +import io.trino.spi.type.Type; +import io.trino.spi.type.TypeId; +import io.trino.sql.relational.CallExpression; +import io.trino.sql.relational.RowExpression; +import io.trino.sql.tree.QualifiedName; +import io.trino.testing.TestingSession; +import io.trino.type.JsonPath2016Type; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static io.trino.json.ir.IrArithmeticBinary.Operator.ADD; +import static io.trino.memory.context.AggregatedMemoryContext.newSimpleAggregatedMemoryContext; +import static io.trino.operator.scalar.json.JsonInputFunctions.VARCHAR_TO_JSON; +import static io.trino.operator.scalar.json.JsonValueFunction.JSON_VALUE_FUNCTION_NAME; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.TinyintType.TINYINT; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.sql.analyzer.ExpressionAnalyzer.JSON_NO_PARAMETERS_ROW_TYPE; +import static io.trino.sql.analyzer.TypeSignatureProvider.fromTypes; +import static io.trino.sql.planner.TestingPlannerContext.PLANNER_CONTEXT; +import static io.trino.sql.relational.Expressions.constant; +import static io.trino.sql.relational.Expressions.constantNull; +import static io.trino.sql.relational.Expressions.field; +import static io.trino.type.Json2016Type.JSON_2016; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; + +@SuppressWarnings("MethodMayBeStatic") +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(2) +@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) +@BenchmarkMode(Mode.AverageTime) +public class BenchmarkJsonPathBinaryOperators +{ + private static final int POSITION_COUNT = 100_000; + private static final FullConnectorSession FULL_CONNECTOR_SESSION = new FullConnectorSession(TestingSession.testSessionBuilder().build(), ConnectorIdentity.ofUser("test")); + + @Benchmark + @OperationsPerInvocation(POSITION_COUNT) + public List> benchmarkJsonValueFunctionConstantTypes(BenchmarkData data) + { + return ImmutableList.copyOf( + data.getJsonValuePageProcessor().process( + FULL_CONNECTOR_SESSION, + new DriverYieldSignal(), + newSimpleAggregatedMemoryContext().newLocalMemoryContext(PageProcessor.class.getSimpleName()), + data.getPageConstantTypes())); + } + + @Benchmark + @OperationsPerInvocation(POSITION_COUNT) + public List> benchmarkJsonValueFunctionVaryingTypes(BenchmarkData data) + { + return ImmutableList.copyOf( + data.getJsonValuePageProcessor().process( + FULL_CONNECTOR_SESSION, + new DriverYieldSignal(), + newSimpleAggregatedMemoryContext().newLocalMemoryContext(PageProcessor.class.getSimpleName()), + data.getPageVaryingTypes())); + } + + @Benchmark + @OperationsPerInvocation(POSITION_COUNT) + public List> benchmarkJsonValueFunctionMultipleVaryingTypes(BenchmarkData data) + { + return ImmutableList.copyOf( + data.getJsonValuePageProcessor().process( + FULL_CONNECTOR_SESSION, + new DriverYieldSignal(), + newSimpleAggregatedMemoryContext().newLocalMemoryContext(PageProcessor.class.getSimpleName()), + data.getPageMultipleVaryingTypes())); + } + + @SuppressWarnings("FieldMayBeFinal") + @State(Scope.Thread) + public static class BenchmarkData + { + private Page pageConstantTypes; + private Page pageVaryingTypes; + private Page pageMultipleVaryingTypes; + private PageProcessor jsonValuePageProcessor; + + @Setup + public void setup() + { + pageConstantTypes = new Page(createChannelConstantTypes(POSITION_COUNT)); + pageVaryingTypes = new Page(createChannelVaryingTypes(POSITION_COUNT)); + pageMultipleVaryingTypes = new Page(createChannelMultipleVaryingTypes(POSITION_COUNT)); + jsonValuePageProcessor = createJsonValuePageProcessor(); + } + + private static PageProcessor createJsonValuePageProcessor() + { + TestingFunctionResolution functionResolution = new TestingFunctionResolution(); + Type jsonPath2016Type = PLANNER_CONTEXT.getTypeManager().getType(TypeId.of(JsonPath2016Type.NAME)); + + IrPathNode path = new IrArithmeticBinary( + ADD, + new IrMemberAccessor(new IrContextVariable(Optional.empty()), Optional.of("first"), Optional.empty()), + new IrMemberAccessor(new IrContextVariable(Optional.empty()), Optional.of("second"), Optional.empty()), + Optional.empty()); + List jsonValueProjection = ImmutableList.of(new CallExpression( + functionResolution.resolveFunction( + QualifiedName.of(JSON_VALUE_FUNCTION_NAME), + fromTypes(ImmutableList.of( + JSON_2016, + jsonPath2016Type, + JSON_NO_PARAMETERS_ROW_TYPE, + TINYINT, + VARCHAR, + TINYINT, + VARCHAR))), + ImmutableList.of( + new CallExpression( + functionResolution.resolveFunction(QualifiedName.of(VARCHAR_TO_JSON), fromTypes(VARCHAR, BOOLEAN)), + ImmutableList.of(field(0, VARCHAR), constant(true, BOOLEAN))), + constant(new IrJsonPath(false, path), jsonPath2016Type), + constantNull(JSON_NO_PARAMETERS_ROW_TYPE), + constant(0L, TINYINT), + constantNull(VARCHAR), + constant(0L, TINYINT), + constantNull(VARCHAR)))); + + return functionResolution.getExpressionCompiler() + .compilePageProcessor(Optional.empty(), jsonValueProjection) + .get(); + } + + private static Block createChannelConstantTypes(int positionCount) + { + BlockBuilder blockBuilder = VARCHAR.createBlockBuilder(null, positionCount); + for (int position = 0; position < positionCount; position++) { + SliceOutput slice = new DynamicSliceOutput(20); + slice.appendBytes(("{\"first\" : ").getBytes(UTF_8)) + .appendBytes(format("%e", (double) position % 100).getBytes(UTF_8)) // real + .appendBytes((", \"second\" : ").getBytes(UTF_8)) + .appendBytes(format("%s", position % 10).getBytes(UTF_8)) // int + .appendByte('}'); + VARCHAR.writeSlice(blockBuilder, slice.slice()); + } + return blockBuilder.build(); + } + + private static Block createChannelVaryingTypes(int positionCount) + { + BlockBuilder blockBuilder = VARCHAR.createBlockBuilder(null, positionCount); + for (int position = 0; position < positionCount; position++) { + SliceOutput slice = new DynamicSliceOutput(20); + slice.appendBytes(("{\"first\" : ").getBytes(UTF_8)); + if (position % 3 == 0) { + slice.appendBytes(format("%e", (double) position % 100).getBytes(UTF_8)); // real + } + else if (position % 3 == 1) { + slice.appendBytes(format("%s", (position % 100) * 1000000000000L).getBytes(UTF_8)); // bigint + } + else { + slice.appendBytes(format("%s", position % 100).getBytes(UTF_8)); // int + } + slice.appendBytes((", \"second\" : ").getBytes(UTF_8)) + .appendBytes(format("%s", position % 10).getBytes(UTF_8)) // int + .appendByte('}'); + VARCHAR.writeSlice(blockBuilder, slice.slice()); + } + return blockBuilder.build(); + } + + private static Block createChannelMultipleVaryingTypes(int positionCount) + { + BlockBuilder blockBuilder = VARCHAR.createBlockBuilder(null, positionCount); + for (int position = 0; position < positionCount; position++) { + SliceOutput slice = new DynamicSliceOutput(20); + + slice.appendBytes(("{\"first\" : ").getBytes(UTF_8)); + if (position % 3 == 0) { + slice.appendBytes(format("%e", (double) position % 100).getBytes(UTF_8)); // real + } + else if (position % 3 == 1) { + slice.appendBytes(format("%s", (position % 100) * 1000000000000L).getBytes(UTF_8)); // bigint + } + else { + slice.appendBytes(format("%s", position % 100).getBytes(UTF_8)); // int + } + + slice.appendBytes((", \"second\" : ").getBytes(UTF_8)); + if (position % 4 == 0) { + slice.appendBytes(format("%e", (double) position % 10).getBytes(UTF_8)); // real + } + else if (position % 4 == 1) { + slice.appendBytes(Decimals.toString(position % 10, 2).getBytes(UTF_8)); // decimal + } + else if (position % 4 == 2) { + slice.appendBytes(format("%s", (position % 10) * 1000000000000L).getBytes(UTF_8)); // bigint + } + else { + slice.appendBytes(format("%s", position % 10).getBytes(UTF_8)); // int + } + + slice.appendByte('}'); + VARCHAR.writeSlice(blockBuilder, slice.slice()); + } + return blockBuilder.build(); + } + + public PageProcessor getJsonValuePageProcessor() + { + return jsonValuePageProcessor; + } + + public Page getPageConstantTypes() + { + return pageConstantTypes; + } + + public Page getPageVaryingTypes() + { + return pageVaryingTypes; + } + + public Page getPageMultipleVaryingTypes() + { + return pageMultipleVaryingTypes; + } + } + + @Test + public void verify() + { + BenchmarkData data = new BenchmarkData(); + data.setup(); + new BenchmarkJsonPathBinaryOperators().benchmarkJsonValueFunctionConstantTypes(data); + new BenchmarkJsonPathBinaryOperators().benchmarkJsonValueFunctionVaryingTypes(data); + new BenchmarkJsonPathBinaryOperators().benchmarkJsonValueFunctionMultipleVaryingTypes(data); + } + + public static void main(String[] args) + throws Exception + { + Benchmarks.benchmark(BenchmarkJsonPathBinaryOperators.class).run(); + } +} diff --git a/core/trino-main/src/test/java/io/trino/operator/scalar/TestJsonInputFunctions.java b/core/trino-main/src/test/java/io/trino/operator/scalar/TestJsonInputFunctions.java new file mode 100644 index 000000000000..9cbec24006cb --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/operator/scalar/TestJsonInputFunctions.java @@ -0,0 +1,229 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.Test; + +import java.nio.charset.Charset; + +import static com.google.common.io.BaseEncoding.base16; +import static io.trino.json.JsonInputErrorNode.JSON_ERROR; +import static io.trino.spi.StandardErrorCode.JSON_INPUT_CONVERSION_ERROR; +import static io.trino.type.Json2016Type.JSON_2016; +import static java.nio.charset.StandardCharsets.UTF_16BE; +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class TestJsonInputFunctions + extends AbstractTestFunctions +{ + private static final String INPUT = "{\"key1\" : 1e0, \"key2\" : true, \"key3\" : null}"; + private static final JsonNode JSON_OBJECT = new ObjectNode( + JsonNodeFactory.instance, + ImmutableMap.of("key1", DoubleNode.valueOf(1e0), "key2", BooleanNode.TRUE, "key3", NullNode.instance)); + private static final String ERROR_INPUT = "[..."; + + @Test + public void testVarcharToJson() + { + assertFunction( + "\"$varchar_to_json\"('[]', true)", + JSON_2016, + new ArrayNode(JsonNodeFactory.instance)); + + assertFunction( + "\"$varchar_to_json\"('" + INPUT + "', true)", + JSON_2016, + JSON_OBJECT); + + // with unsuppressed input conversion error + assertInvalidFunction( + "\"$varchar_to_json\"('" + ERROR_INPUT + "', true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + // with input conversion error suppressed and converted to JSON_ERROR + assertFunction( + "\"$varchar_to_json\"('" + ERROR_INPUT + "', false)", + JSON_2016, + JSON_ERROR); + } + + @Test + public void testVarbinaryUtf8ToJson() + { + byte[] bytes = INPUT.getBytes(UTF_8); + String varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertFunction( + "\"$varbinary_to_json\"(" + varbinaryLiteral + ", true)", + JSON_2016, + JSON_OBJECT); + + assertFunction( + "\"$varbinary_utf8_to_json\"(" + varbinaryLiteral + ", true)", + JSON_2016, + JSON_OBJECT); + + // wrong input encoding + bytes = INPUT.getBytes(UTF_16LE); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertInvalidFunction( + "\"$varbinary_utf8_to_json\"(" + varbinaryLiteral + ", true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + // wrong input encoding; conversion error suppressed and converted to JSON_ERROR + assertFunction( + "\"$varbinary_utf8_to_json\"(" + varbinaryLiteral + ", false)", + JSON_2016, + JSON_ERROR); + + // correct encoding, incorrect input + bytes = ERROR_INPUT.getBytes(UTF_8); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + // with unsuppressed input conversion error + assertInvalidFunction( + "\"$varbinary_utf8_to_json\"(" + varbinaryLiteral + ", true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + // with input conversion error suppressed and converted to JSON_ERROR + assertFunction( + "\"$varbinary_utf8_to_json\"(" + varbinaryLiteral + ", false)", + JSON_2016, + JSON_ERROR); + } + + @Test + public void testVarbinaryUtf16ToJson() + { + byte[] bytes = INPUT.getBytes(UTF_16LE); + String varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertFunction( + "\"$varbinary_utf16_to_json\"(" + varbinaryLiteral + ", true)", + JSON_2016, + JSON_OBJECT); + + // wrong input encoding + bytes = INPUT.getBytes(UTF_16BE); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertInvalidFunction( + "\"$varbinary_utf16_to_json\"(" + varbinaryLiteral + ", true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + bytes = INPUT.getBytes(UTF_8); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertInvalidFunction( + "\"$varbinary_utf16_to_json\"(" + varbinaryLiteral + ", true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + // wrong input encoding; conversion error suppressed and converted to JSON_ERROR + assertFunction( + "\"$varbinary_utf16_to_json\"(" + varbinaryLiteral + ", false)", + JSON_2016, + JSON_ERROR); + + // correct encoding, incorrect input + bytes = ERROR_INPUT.getBytes(UTF_16LE); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + // with unsuppressed input conversion error + assertInvalidFunction( + "\"$varbinary_utf16_to_json\"(" + varbinaryLiteral + ", true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + // with input conversion error suppressed and converted to JSON_ERROR + assertFunction( + "\"$varbinary_utf16_to_json\"(" + varbinaryLiteral + ", false)", + JSON_2016, + JSON_ERROR); + } + + @Test + public void testVarbinaryUtf32ToJson() + { + byte[] bytes = INPUT.getBytes(Charset.forName("UTF-32LE")); + String varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertFunction( + "\"$varbinary_utf32_to_json\"(" + varbinaryLiteral + ", true)", + JSON_2016, + JSON_OBJECT); + + // wrong input encoding + bytes = INPUT.getBytes(Charset.forName("UTF-32BE")); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertInvalidFunction( + "\"$varbinary_utf32_to_json\"(" + varbinaryLiteral + ", true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + bytes = INPUT.getBytes(UTF_8); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertInvalidFunction( + "\"$varbinary_utf32_to_json\"(" + varbinaryLiteral + ", true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + // wrong input encoding; conversion error suppressed and converted to JSON_ERROR + assertFunction( + "\"$varbinary_utf32_to_json\"(" + varbinaryLiteral + ", false)", + JSON_2016, + JSON_ERROR); + + // correct encoding, incorrect input + bytes = ERROR_INPUT.getBytes(Charset.forName("UTF-32LE")); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + // with unsuppressed input conversion error + assertInvalidFunction( + "\"$varbinary_utf32_to_json\"(" + varbinaryLiteral + ", true)", + JSON_INPUT_CONVERSION_ERROR, + "conversion to JSON failed: "); + + // with input conversion error suppressed and converted to JSON_ERROR + assertFunction( + "\"$varbinary_utf32_to_json\"(" + varbinaryLiteral + ", false)", + JSON_2016, + JSON_ERROR); + } + + @Test + public void testNullInput() + { + assertFunction( + "\"$varchar_to_json\"(null, true)", + JSON_2016, + null); + } +} diff --git a/core/trino-main/src/test/java/io/trino/operator/scalar/TestJsonOutputFunctions.java b/core/trino-main/src/test/java/io/trino/operator/scalar/TestJsonOutputFunctions.java new file mode 100644 index 000000000000..1bf06212af7d --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/operator/scalar/TestJsonOutputFunctions.java @@ -0,0 +1,114 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.operator.scalar; + +import io.trino.spi.type.SqlVarbinary; +import org.testng.annotations.Test; + +import java.nio.charset.Charset; + +import static io.trino.spi.type.VarbinaryType.VARBINARY; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class TestJsonOutputFunctions + extends AbstractTestFunctions +{ + private static final String JSON_EXPRESSION = "\"$varchar_to_json\"('{\"key1\" : 1e0, \"key2\" : true, \"key3\" : null}', true)"; + private static final String OUTPUT = "{\"key1\":1.0,\"key2\":true,\"key3\":null}"; + + @Test + public void testJsonToVarchar() + { + assertFunction( + "\"$json_to_varchar\"(" + JSON_EXPRESSION + ", TINYINT '1', true)", + VARCHAR, + OUTPUT); + } + + @Test + public void testJsonToVarbinaryUtf8() + { + byte[] bytes = OUTPUT.getBytes(UTF_8); + SqlVarbinary varbinaryOutput = new SqlVarbinary(bytes); + + assertFunction( + "\"$json_to_varbinary\"(" + JSON_EXPRESSION + ", TINYINT '1', true)", + VARBINARY, + varbinaryOutput); + + assertFunction( + "\"$json_to_varbinary_utf8\"(" + JSON_EXPRESSION + ", TINYINT '1', true)", + VARBINARY, + varbinaryOutput); + } + + @Test + public void testJsonToVarbinaryUtf16() + { + byte[] bytes = OUTPUT.getBytes(UTF_16LE); + SqlVarbinary varbinaryOutput = new SqlVarbinary(bytes); + + assertFunction( + "\"$json_to_varbinary_utf16\"(" + JSON_EXPRESSION + ", TINYINT '1', true)", + VARBINARY, + varbinaryOutput); + } + + @Test + public void testJsonToVarbinaryUtf32() + { + byte[] bytes = OUTPUT.getBytes(Charset.forName("UTF-32LE")); + SqlVarbinary varbinaryOutput = new SqlVarbinary(bytes); + + assertFunction( + "\"$json_to_varbinary_utf32\"(" + JSON_EXPRESSION + ", TINYINT '1', true)", + VARBINARY, + varbinaryOutput); + } + + @Test + public void testQuotesBehavior() + { + String jsonScalarString = "\"$varchar_to_json\"('\"some_text\"', true)"; + + // keep quotes on scalar string + assertFunction( + "\"$json_to_varchar\"(" + jsonScalarString + ", TINYINT '1', false)", + VARCHAR, + "\"some_text\""); + + // omit quotes on scalar string + assertFunction( + "\"$json_to_varchar\"(" + jsonScalarString + ", TINYINT '1', true)", + VARCHAR, + "some_text"); + + // quotes behavior does not apply to nested string. the quotes are preserved + assertFunction( + "\"$json_to_varchar\"(\"$varchar_to_json\"('[\"some_text\"]', true), TINYINT '1', true)", + VARCHAR, + "[\"some_text\"]"); + } + + @Test + public void testNullInput() + { + assertFunction( + "\"$json_to_varchar\"(null, TINYINT '1', true)", + VARCHAR, + null); + } +} diff --git a/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java b/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java index 2999e81b10b2..1aeaa201154d 100644 --- a/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java +++ b/core/trino-main/src/test/java/io/trino/sql/analyzer/TestAnalyzer.java @@ -94,6 +94,7 @@ import static io.trino.spi.StandardErrorCode.COLUMN_TYPE_UNKNOWN; import static io.trino.spi.StandardErrorCode.DUPLICATE_COLUMN_NAME; import static io.trino.spi.StandardErrorCode.DUPLICATE_NAMED_QUERY; +import static io.trino.spi.StandardErrorCode.DUPLICATE_PARAMETER_NAME; import static io.trino.spi.StandardErrorCode.DUPLICATE_PROPERTY; import static io.trino.spi.StandardErrorCode.DUPLICATE_WINDOW_NAME; import static io.trino.spi.StandardErrorCode.EXPRESSION_NOT_AGGREGATE; @@ -5296,6 +5297,348 @@ public void testAnalyzeMaterializedViewWithAccessControl() .hasMessage("Access Denied: Cannot select from columns [a, b] in table or view tpch.s1.fresh_materialized_view"); } + @Test + public void testJsonContextItemType() + { + analyze("SELECT JSON_EXISTS(json_column, 'lax $.abs()') FROM (VALUES '-1', 'ala') t(json_column)"); + analyze("SELECT JSON_EXISTS(json_column, 'lax $.abs()') FROM (VALUES X'65683F', X'65683E') t(json_column)"); + + assertFails("SELECT JSON_EXISTS(json_column, 'lax $.abs()') FROM (VALUES -1, -2) t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:20: Cannot read input of type integer as JSON using formatting JSON"); + } + + @Test + public void testJsonContextItemFormat() + { + // implicit FORMAT JSON + analyze("SELECT JSON_EXISTS(json_column, 'lax $.abs()') FROM (VALUES '-1', 'ala') t(json_column)"); + analyze("SELECT JSON_EXISTS(json_column, 'lax $.abs()') FROM (VALUES X'65683F', X'65683E') t(json_column)"); + + // explicit input format + analyze("SELECT JSON_EXISTS(json_column FORMAT JSON, 'lax $.abs()') FROM (VALUES '-1', 'ala') t(json_column)"); + analyze("SELECT JSON_EXISTS(json_column FORMAT JSON ENCODING UTF8, 'lax $.abs()') FROM (VALUES X'1A', X'2B') t(json_column)"); + analyze("SELECT JSON_EXISTS(json_column FORMAT JSON ENCODING UTF16, 'lax $.abs()') FROM (VALUES X'1A', X'2B') t(json_column)"); + analyze("SELECT JSON_EXISTS(json_column FORMAT JSON ENCODING UTF32, 'lax $.abs()') FROM (VALUES X'1A', X'2B') t(json_column)"); + + // incorrect format: ENCODING specified for character string input + assertFails("SELECT JSON_EXISTS(json_column FORMAT JSON ENCODING UTF8, 'lax $.abs()') FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:20: Cannot read input of type varchar(3) as JSON using formatting JSON ENCODING UTF8"); + } + + @Test + public void testJsonPathParameterNames() + { + analyze("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING " + + " 1 AS parameter_1, " + + " 'x' AS parameter_2, " + + " true AS parameter_3) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + assertFails("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING " + + " 1 AS parameter_1, " + + " 'x' AS parameter_2, " + + " true AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(DUPLICATE_PARAMETER_NAME) + .hasMessage("line 1:309: PARAMETER_1 JSON path parameter is specified more than once"); + } + + @Test + public void testCaseSensitiveNames() + { + // JSON path variable names are case-sensitive. Unquoted parameter names in the PASSING clause are upper-cased. + analyze("SELECT JSON_EXISTS(json_column, 'lax $some_name' PASSING 1 AS \"some_name\") FROM (VALUES '-1', 'ala') t(json_column)"); + analyze("SELECT JSON_EXISTS(json_column, 'lax $SOME_NAME' PASSING 1 AS some_name) FROM (VALUES '-1', 'ala') t(json_column)"); + + // no matching parameter, but similar parameter found with different case. provide a hint in the error message + assertFails("SELECT JSON_EXISTS(json_column, 'lax $some_name' PASSING 1 AS some_name) FROM (VALUES '-1', 'ala') t(json_column)") + .hasMessage("line 1:33: no value passed for parameter some_name. Try quoting \"some_name\" in the PASSING clause to match case"); + + assertFails("SELECT JSON_EXISTS(json_column, 'lax $some_NAME' PASSING 1 AS some_name) FROM (VALUES '-1', 'ala') t(json_column)") + .hasMessage("line 1:33: no value passed for parameter some_NAME. Try quoting \"some_NAME\" in the PASSING clause to match case"); + + // no matching parameter, and it is not the issue with case sensitivity. no hint in the error message + assertFails("SELECT JSON_EXISTS(json_column, 'lax $some_name' PASSING 1 AS some_other_name) FROM (VALUES '-1', 'ala') t(json_column)") + .hasMessage("line 1:33: no value passed for parameter some_name"); + } + + @Test + public void testJsonPathParameterFormats() + { + analyze("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING 'x' FORMAT JSON AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + analyze("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING X'65683F' FORMAT JSON ENCODING UTF8 AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + assertFails("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING 1 FORMAT JSON AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:110: Cannot read input of type integer as JSON using formatting JSON"); + + assertFails("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING 1 FORMAT JSON ENCODING UTF8 AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:110: Cannot read input of type integer as JSON using formatting JSON ENCODING UTF8"); + + // FORMAT JSON as the parameter format option is the same as the output format of the JSON_QUERY call + analyze("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING JSON_QUERY(json_column, 'lax $.abs()' RETURNING varchar FORMAT JSON) FORMAT JSON AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + // FORMAT JSON as the parameter format option is different than the output format of the JSON_QUERY call + analyze("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING JSON_QUERY(json_column, 'lax $.abs()' RETURNING varbinary FORMAT JSON) FORMAT JSON ENCODING UTF8 AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + // the parameter is a JSON_QUERY call, so the format option FORMAT JSON is implicit for the parameter + analyze("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING JSON_QUERY(json_column, 'lax $.abs()' RETURNING varchar FORMAT JSON) AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + } + + @Test + public void testJsonPathParameterTypes() + { + assertFails("SELECT JSON_EXISTS( " + + " json_column, " + + " 'lax $.abs()' PASSING INTERVAL '2' DAY AS parameter_1) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(INVALID_FUNCTION_ARGUMENT) + .hasMessage("line 1:110: Invalid type of JSON path parameter: interval day to second"); + } + + @Test + public void testJsonValueReturnedType() + { + analyze("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING char(30)) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + analyze("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.size()'" + + " RETURNING bigint) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + assertFails("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING tdigest) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:8: Invalid return type of function JSON_VALUE: tdigest"); + + assertFails("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING some_type(10)) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:8: Unknown type: some_type(10)"); + } + + @Test + public void testJsonValueDefaultValues() + { + // default value has the same type as the declared returned type + analyze("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.double()'" + + " RETURNING double" + + " DEFAULT 1e0 ON EMPTY) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + // default value can be coerced to the declared returned type + analyze("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.double()'" + + " RETURNING double" + + " DEFAULT 1.0 ON EMPTY) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + assertFails("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.double()'" + + " RETURNING double" + + " DEFAULT 'text' ON EMPTY) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:149: Function JSON_VALUE default ON EMPTY result must evaluate to a double (actual: varchar(4))"); + + // default value has the same type as the declared returned type + analyze("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.double()'" + + " RETURNING double" + + " DEFAULT 1e0 ON ERROR) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + // default value can be coerced to the declared returned type + analyze("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.double()'" + + " RETURNING double" + + " DEFAULT 1.0 ON ERROR) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + assertFails("SELECT JSON_VALUE( " + + " json_column, " + + " 'lax $.double()'" + + " RETURNING double" + + " DEFAULT 'text' ON ERROR) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:149: Function JSON_VALUE default ON ERROR result must evaluate to a double (actual: varchar(4))"); + } + + @Test + public void testJsonQueryOutputTypeAndFormat() + { + analyze("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING varchar) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + analyze("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING varchar FORMAT JSON) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + analyze("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING char(5) FORMAT JSON) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + analyze("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING varbinary FORMAT JSON ENCODING UTF8) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + assertFails("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING some_type(10)) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:8: Unknown type: some_type(10)"); + + assertFails("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING double) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:8: Cannot output JSON value as double using formatting JSON"); + + assertFails("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()'" + + " RETURNING varchar FORMAT JSON ENCODING UTF8) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(TYPE_MISMATCH) + .hasMessage("line 1:8: Cannot output JSON value as varchar using formatting JSON ENCODING UTF8"); + } + + @Test + public void testJsonQueryQuotesBehavior() + { + analyze("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()'" + + " OMIT QUOTES ON SCALAR STRING) " + + " FROM (VALUES '-1', 'ala') t(json_column)"); + + assertFails("SELECT JSON_QUERY( " + + " json_column, " + + " 'lax $.type()' " + + " WITH ARRAY WRAPPER " + + " OMIT QUOTES ON SCALAR STRING) " + + " FROM (VALUES '-1', 'ala') t(json_column)") + .hasErrorCode(INVALID_FUNCTION_ARGUMENT) + .hasMessage("line 1:8: OMIT QUOTES behavior specified with WITH UNCONDITIONAL ARRAY WRAPPER behavior"); + } + + @Test + public void testJsonExistsInAggregationContext() + { + analyze("SELECT JSON_EXISTS('-5', 'lax $.abs()') FROM (VALUES '-1', '-2') t(a) GROUP BY a"); + analyze("SELECT JSON_EXISTS(a, 'lax $.abs()') FROM (VALUES '-1', '-2') t(a) GROUP BY a"); + analyze("SELECT JSON_EXISTS(a, 'lax $.abs() + $some_number' PASSING b AS \"some_number\") FROM (VALUES ('-1', 10, 100), ('-2', 20, 200)) t(a, b, c) GROUP BY a, b"); + + assertFails("SELECT JSON_EXISTS(c, 'lax $.abs() + $some_number' PASSING b AS \"some_number\") FROM (VALUES ('-1', 10, '100'), ('-2', 20, '200')) t(a, b, c) GROUP BY a, b") + .hasErrorCode(EXPRESSION_NOT_AGGREGATE) + .hasMessage("line 1:8: 'JSON_EXISTS(c FORMAT JSON, 'lax $.abs() + $some_number' PASSING b AS \"some_number\" FALSE ON ERROR)' must be an aggregate expression or appear in GROUP BY clause"); + + assertFails("SELECT JSON_EXISTS(b, 'lax $.abs() + $some_number' PASSING c AS \"some_number\") FROM (VALUES (-1, '10', 100), (-2, '20', 200)) t(a, b, c) GROUP BY a, b") + .hasErrorCode(EXPRESSION_NOT_AGGREGATE) + .hasMessage("line 1:8: 'JSON_EXISTS(b FORMAT JSON, 'lax $.abs() + $some_number' PASSING c AS \"some_number\" FALSE ON ERROR)' must be an aggregate expression or appear in GROUP BY clause"); + } + + @Test + public void testJsonValueInAggregationContext() + { + analyze("SELECT JSON_VALUE('-5', 'lax $.abs()') FROM (VALUES '-1', '-2') t(a) GROUP BY a"); + analyze("SELECT JSON_VALUE(a, 'lax $.abs()') FROM (VALUES '-1', '-2') t(a) GROUP BY a"); + analyze("SELECT JSON_VALUE(a, 'lax $.abs() + $some_number' PASSING b AS \"some_number\") FROM (VALUES ('-1', 10, 100), ('-2', 20, 200)) t(a, b, c) GROUP BY a, b"); + analyze("SELECT JSON_VALUE(a, 'lax $.abs() + $some_number' PASSING b AS \"some_number\" DEFAULT lower(b) ON EMPTY DEFAULT upper(b) ON ERROR) FROM (VALUES ('-1', '10', 100), ('-2', '20', 200)) t(a, b, c) GROUP BY a, b"); + + assertFails("SELECT JSON_VALUE(c, 'lax $.abs() + $some_number' PASSING b AS \"some_number\") FROM (VALUES ('-1', 10, '100'), ('-2', 20, '200')) t(a, b, c) GROUP BY a, b") + .hasErrorCode(EXPRESSION_NOT_AGGREGATE) + .hasMessage("line 1:8: 'JSON_VALUE(c FORMAT JSON, 'lax $.abs() + $some_number' PASSING b AS \"some_number\" NULL ON EMPTY NULL ON ERROR)' must be an aggregate expression or appear in GROUP BY clause"); + + assertFails("SELECT JSON_VALUE(b, 'lax $.abs() + $some_number' PASSING c AS \"some_number\") FROM (VALUES (-1, '10', 100), (-2, '20', 200)) t(a, b, c) GROUP BY a, b") + .hasErrorCode(EXPRESSION_NOT_AGGREGATE) + .hasMessage("line 1:8: 'JSON_VALUE(b FORMAT JSON, 'lax $.abs() + $some_number' PASSING c AS \"some_number\" NULL ON EMPTY NULL ON ERROR)' must be an aggregate expression or appear in GROUP BY clause"); + + assertFails("SELECT JSON_VALUE(b, 'lax $.abs() + $some_number' PASSING b AS \"some_number\" DEFAULT c ON EMPTY) FROM (VALUES (-1, '10', '100'), (-2, '20', '200')) t(a, b, c) GROUP BY a, b") + .hasErrorCode(EXPRESSION_NOT_AGGREGATE) + .hasMessage("line 1:8: 'JSON_VALUE(b FORMAT JSON, 'lax $.abs() + $some_number' PASSING b AS \"some_number\" DEFAULT c ON EMPTY NULL ON ERROR)' must be an aggregate expression or appear in GROUP BY clause"); + + assertFails("SELECT JSON_VALUE(b, 'lax $.abs() + $some_number' PASSING b AS \"some_number\" DEFAULT c ON ERROR) FROM (VALUES (-1, '10', '100'), (-2, '20', '200')) t(a, b, c) GROUP BY a, b") + .hasErrorCode(EXPRESSION_NOT_AGGREGATE) + .hasMessage("line 1:8: 'JSON_VALUE(b FORMAT JSON, 'lax $.abs() + $some_number' PASSING b AS \"some_number\" NULL ON EMPTY DEFAULT c ON ERROR)' must be an aggregate expression or appear in GROUP BY clause"); + } + + @Test + public void testJsonQueryInAggregationContext() + { + analyze("SELECT JSON_QUERY('-5', 'lax $.abs()') FROM (VALUES '-1', '-2') t(a) GROUP BY a"); + analyze("SELECT JSON_QUERY(a, 'lax $.abs()') FROM (VALUES '-1', '-2') t(a) GROUP BY a"); + analyze("SELECT JSON_QUERY(a, 'lax $.abs() + $some_number' PASSING b AS \"some_number\") FROM (VALUES ('-1', 10, 100), ('-2', 20, 200)) t(a, b, c) GROUP BY a, b"); + + assertFails("SELECT JSON_QUERY(c, 'lax $.abs() + $some_number' PASSING b AS \"some_number\") FROM (VALUES ('-1', 10, '100'), ('-2', 20, '200')) t(a, b, c) GROUP BY a, b") + .hasErrorCode(EXPRESSION_NOT_AGGREGATE) + .hasMessage("line 1:8: 'JSON_QUERY(c FORMAT JSON, 'lax $.abs() + $some_number' PASSING b AS \"some_number\" WITHOUT ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR)' must be an aggregate expression or appear in GROUP BY clause"); + + assertFails("SELECT JSON_QUERY(b, 'lax $.abs() + $some_number' PASSING c AS \"some_number\") FROM (VALUES (-1, '10', 100), (-2, '20', 200)) t(a, b, c) GROUP BY a, b") + .hasErrorCode(EXPRESSION_NOT_AGGREGATE) + .hasMessage("line 1:8: 'JSON_QUERY(b FORMAT JSON, 'lax $.abs() + $some_number' PASSING c AS \"some_number\" WITHOUT ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR)' must be an aggregate expression or appear in GROUP BY clause"); + } + @BeforeClass public void setup() { diff --git a/core/trino-main/src/test/java/io/trino/sql/planner/PathNodes.java b/core/trino-main/src/test/java/io/trino/sql/planner/PathNodes.java new file mode 100644 index 000000000000..9adbe81503a9 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/sql/planner/PathNodes.java @@ -0,0 +1,293 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.planner; + +import com.google.common.collect.ImmutableList; +import io.trino.json.ir.IrAbsMethod; +import io.trino.json.ir.IrArithmeticBinary; +import io.trino.json.ir.IrArithmeticUnary; +import io.trino.json.ir.IrArrayAccessor; +import io.trino.json.ir.IrArrayAccessor.Subscript; +import io.trino.json.ir.IrCeilingMethod; +import io.trino.json.ir.IrComparisonPredicate; +import io.trino.json.ir.IrConjunctionPredicate; +import io.trino.json.ir.IrContextVariable; +import io.trino.json.ir.IrDisjunctionPredicate; +import io.trino.json.ir.IrDoubleMethod; +import io.trino.json.ir.IrExistsPredicate; +import io.trino.json.ir.IrFilter; +import io.trino.json.ir.IrFloorMethod; +import io.trino.json.ir.IrIsUnknownPredicate; +import io.trino.json.ir.IrJsonNull; +import io.trino.json.ir.IrJsonPath; +import io.trino.json.ir.IrKeyValueMethod; +import io.trino.json.ir.IrLastIndexVariable; +import io.trino.json.ir.IrLiteral; +import io.trino.json.ir.IrMemberAccessor; +import io.trino.json.ir.IrNamedJsonVariable; +import io.trino.json.ir.IrNamedValueVariable; +import io.trino.json.ir.IrNegationPredicate; +import io.trino.json.ir.IrPathNode; +import io.trino.json.ir.IrPredicate; +import io.trino.json.ir.IrPredicateCurrentItemVariable; +import io.trino.json.ir.IrSizeMethod; +import io.trino.json.ir.IrStartsWithPredicate; +import io.trino.json.ir.IrTypeMethod; +import io.trino.spi.type.Type; + +import java.util.List; +import java.util.Optional; + +import static io.trino.json.ir.IrArithmeticBinary.Operator.ADD; +import static io.trino.json.ir.IrArithmeticBinary.Operator.DIVIDE; +import static io.trino.json.ir.IrArithmeticBinary.Operator.MODULUS; +import static io.trino.json.ir.IrArithmeticBinary.Operator.MULTIPLY; +import static io.trino.json.ir.IrArithmeticBinary.Operator.SUBTRACT; +import static io.trino.json.ir.IrArithmeticUnary.Sign.MINUS; +import static io.trino.json.ir.IrArithmeticUnary.Sign.PLUS; +import static io.trino.json.ir.IrComparisonPredicate.Operator.EQUAL; +import static io.trino.json.ir.IrComparisonPredicate.Operator.GREATER_THAN; +import static io.trino.json.ir.IrComparisonPredicate.Operator.GREATER_THAN_OR_EQUAL; +import static io.trino.json.ir.IrComparisonPredicate.Operator.LESS_THAN; +import static io.trino.json.ir.IrComparisonPredicate.Operator.LESS_THAN_OR_EQUAL; +import static io.trino.json.ir.IrComparisonPredicate.Operator.NOT_EQUAL; +import static io.trino.spi.type.VarcharType.createVarcharType; + +public class PathNodes +{ + private PathNodes() {} + + public static IrJsonPath path(boolean lax, IrPathNode root) + { + return new IrJsonPath(lax, root); + } + + // PATH NODE + public static IrPathNode abs(IrPathNode base) + { + return new IrAbsMethod(base, Optional.empty()); + } + + public static IrPathNode add(IrPathNode left, IrPathNode right) + { + return new IrArithmeticBinary(ADD, left, right, Optional.empty()); + } + + public static IrPathNode subtract(IrPathNode left, IrPathNode right) + { + return new IrArithmeticBinary(SUBTRACT, left, right, Optional.empty()); + } + + public static IrPathNode multiply(IrPathNode left, IrPathNode right) + { + return new IrArithmeticBinary(MULTIPLY, left, right, Optional.empty()); + } + + public static IrPathNode divide(IrPathNode left, IrPathNode right) + { + return new IrArithmeticBinary(DIVIDE, left, right, Optional.empty()); + } + + public static IrPathNode modulus(IrPathNode left, IrPathNode right) + { + return new IrArithmeticBinary(MODULUS, left, right, Optional.empty()); + } + + public static IrPathNode plus(IrPathNode base) + { + return new IrArithmeticUnary(PLUS, base, Optional.empty()); + } + + public static IrPathNode minus(IrPathNode base) + { + return new IrArithmeticUnary(MINUS, base, Optional.empty()); + } + + public static IrPathNode wildcardArrayAccessor(IrPathNode base) + { + return new IrArrayAccessor(base, ImmutableList.of(), Optional.empty()); + } + + public static IrPathNode arrayAccessor(IrPathNode base, Subscript... subscripts) + { + return new IrArrayAccessor(base, ImmutableList.copyOf(subscripts), Optional.empty()); + } + + public static Subscript at(IrPathNode path) + { + return new Subscript(path, Optional.empty()); + } + + public static Subscript range(IrPathNode fromInclusive, IrPathNode toInclusive) + { + return new Subscript(fromInclusive, Optional.of(toInclusive)); + } + + public static IrPathNode ceiling(IrPathNode base) + { + return new IrCeilingMethod(base, Optional.empty()); + } + + public static IrPathNode contextVariable() + { + return new IrContextVariable(Optional.empty()); + } + + public static IrPathNode toDouble(IrPathNode base) + { + return new IrDoubleMethod(base, Optional.empty()); + } + + public static IrPathNode filter(IrPathNode base, IrPredicate predicate) + { + return new IrFilter(base, predicate, Optional.empty()); + } + + public static IrPathNode floor(IrPathNode base) + { + return new IrFloorMethod(base, Optional.empty()); + } + + public static IrPathNode jsonNull() + { + return new IrJsonNull(); + } + + public static IrPathNode keyValue(IrPathNode base) + { + return new IrKeyValueMethod(base); + } + + public static IrPathNode last() + { + return new IrLastIndexVariable(Optional.empty()); + } + + public static IrPathNode literal(Type type, Object value) + { + return new IrLiteral(type, value); + } + + public static IrPathNode wildcardMemberAccessor(IrPathNode base) + { + return new IrMemberAccessor(base, Optional.empty(), Optional.empty()); + } + + public static IrPathNode memberAccessor(IrPathNode base, String key) + { + return new IrMemberAccessor(base, Optional.of(key), Optional.empty()); + } + + public static IrPathNode jsonVariable(int index) + { + return new IrNamedJsonVariable(index, Optional.empty()); + } + + public static IrPathNode variable(int index) + { + return new IrNamedValueVariable(index, Optional.empty()); + } + + public static IrPathNode currentItem() + { + return new IrPredicateCurrentItemVariable(Optional.empty()); + } + + public static IrPathNode size(IrPathNode base) + { + return new IrSizeMethod(base, Optional.empty()); + } + + public static IrPathNode type(IrPathNode base) + { + return new IrTypeMethod(base, Optional.of(createVarcharType(27))); + } + + // PATH PREDICATE + public static IrPredicate equal(IrPathNode left, IrPathNode right) + { + return new IrComparisonPredicate(EQUAL, left, right); + } + + public static IrPredicate notEqual(IrPathNode left, IrPathNode right) + { + return new IrComparisonPredicate(NOT_EQUAL, left, right); + } + + public static IrPredicate lessThan(IrPathNode left, IrPathNode right) + { + return new IrComparisonPredicate(LESS_THAN, left, right); + } + + public static IrPredicate greaterThan(IrPathNode left, IrPathNode right) + { + return new IrComparisonPredicate(GREATER_THAN, left, right); + } + + public static IrPredicate lessThanOrEqual(IrPathNode left, IrPathNode right) + { + return new IrComparisonPredicate(LESS_THAN_OR_EQUAL, left, right); + } + + public static IrPredicate greaterThanOrEqual(IrPathNode left, IrPathNode right) + { + return new IrComparisonPredicate(GREATER_THAN_OR_EQUAL, left, right); + } + + public static IrPredicate conjunction(IrPredicate left, IrPredicate right) + { + return new IrConjunctionPredicate(left, right); + } + + public static IrPredicate disjunction(IrPredicate left, IrPredicate right) + { + return new IrDisjunctionPredicate(left, right); + } + + public static IrPredicate exists(IrPathNode path) + { + return new IrExistsPredicate(path); + } + + public static IrPredicate isUnknown(IrPredicate predicate) + { + return new IrIsUnknownPredicate(predicate); + } + + public static IrPredicate negation(IrPredicate predicate) + { + return new IrNegationPredicate(predicate); + } + + public static IrPredicate startsWith(IrPathNode whole, IrPathNode initial) + { + return new IrStartsWithPredicate(whole, initial); + } + + // SQL/JSON ITEM SEQUENCE + public static List sequence(Object... items) + { + return ImmutableList.copyOf(items); + } + + public static List singletonSequence(Object item) + { + return ImmutableList.of(item); + } + + public static List emptySequence() + { + return ImmutableList.of(); + } +} diff --git a/core/trino-main/src/test/java/io/trino/sql/planner/TestingPlannerContext.java b/core/trino-main/src/test/java/io/trino/sql/planner/TestingPlannerContext.java index b647cc56d105..242c4b742300 100644 --- a/core/trino-main/src/test/java/io/trino/sql/planner/TestingPlannerContext.java +++ b/core/trino-main/src/test/java/io/trino/sql/planner/TestingPlannerContext.java @@ -26,6 +26,9 @@ import io.trino.metadata.MetadataManager.TestMetadataManagerBuilder; import io.trino.metadata.SystemFunctionBundle; import io.trino.metadata.TypeRegistry; +import io.trino.operator.scalar.json.JsonExistsFunction; +import io.trino.operator.scalar.json.JsonQueryFunction; +import io.trino.operator.scalar.json.JsonValueFunction; import io.trino.spi.block.BlockEncodingSerde; import io.trino.spi.type.ParametricType; import io.trino.spi.type.Type; @@ -35,6 +38,8 @@ import io.trino.transaction.TransactionManager; import io.trino.type.BlockTypeOperators; import io.trino.type.InternalTypeManager; +import io.trino.type.JsonPath2016Type; +import io.trino.type.TypeDeserializer; import java.util.ArrayList; import java.util.List; @@ -127,12 +132,19 @@ public PlannerContext build() metadata = builder.build(); } + FunctionManager functionManager = new FunctionManager(globalFunctionCatalog); + globalFunctionCatalog.addFunctions(new InternalFunctionBundle( + new JsonExistsFunction(functionManager, metadata, typeManager), + new JsonValueFunction(functionManager, metadata, typeManager), + new JsonQueryFunction(functionManager, metadata, typeManager))); + typeRegistry.addType(new JsonPath2016Type(new TypeDeserializer(typeManager), blockEncodingSerde)); + return new PlannerContext( metadata, typeOperators, blockEncodingSerde, typeManager, - new FunctionManager(globalFunctionCatalog)); + functionManager); } } } diff --git a/core/trino-main/src/test/java/io/trino/sql/query/TestJsonExistsFunction.java b/core/trino-main/src/test/java/io/trino/sql/query/TestJsonExistsFunction.java new file mode 100644 index 000000000000..45bbd24e58e5 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/sql/query/TestJsonExistsFunction.java @@ -0,0 +1,237 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.query; + +import io.trino.json.PathEvaluationError; +import io.trino.operator.scalar.json.JsonInputConversionError; +import io.trino.sql.parser.ParsingException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.nio.charset.Charset; + +import static com.google.common.io.BaseEncoding.base16; +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@TestInstance(PER_CLASS) +public class TestJsonExistsFunction +{ + private static final String INPUT = "[\"a\", \"b\", \"c\"]"; + private static final String INCORRECT_INPUT = "[..."; + private QueryAssertions assertions; + + @BeforeAll + public void init() + { + assertions = new QueryAssertions(); + } + + @AfterAll + public void teardown() + { + assertions.close(); + assertions = null; + } + + @Test + public void testJsonExists() + { + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $[1]')")) + .matches("VALUES true"); + + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'strict $[1]')")) + .matches("VALUES true"); + + // structural error suppressed by the path engine in lax mode. empty sequence is returned, so exists returns false + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $[100]')")) + .matches("VALUES false"); + + // structural error not suppressed by the path engine in strict mode, and handled accordingly to the ON ERROR clause + + // default error behavior is FALSE ON ERROR + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'strict $[100]')")) + .matches("VALUES false"); + + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'strict $[100]' TRUE ON ERROR)")) + .matches("VALUES true"); + + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'strict $[100]' FALSE ON ERROR)")) + .matches("VALUES false"); + + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'strict $[100]' UNKNOWN ON ERROR)")) + .matches("VALUES cast(null AS boolean)"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_exists('" + INPUT + "', 'strict $[100]' ERROR ON ERROR)")) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: structural error: invalid array subscript: [100, 100] for array of size 3"); + } + + @Test + public void testInputFormat() + { + // FORMAT JSON is default for character string input + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $[1]')")) + .matches("VALUES true"); + + // FORMAT JSON is the only supported format for character string input + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "' FORMAT JSON, 'lax $[1]')")) + .matches("VALUES true"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_exists('" + INPUT + "' FORMAT JSON ENCODING UTF8, 'lax $[1]')")) + .hasMessage("line 1:20: Cannot read input of type varchar(15) as JSON using formatting JSON ENCODING UTF8"); + + // FORMAT JSON is default for binary string input + byte[] bytes = INPUT.getBytes(UTF_8); + String varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_exists(" + varbinaryLiteral + ", 'lax $[1]')")) + .matches("VALUES true"); + + assertThat(assertions.query( + "SELECT json_exists(" + varbinaryLiteral + " FORMAT JSON, 'lax $[1]')")) + .matches("VALUES true"); + + // FORMAT JSON ENCODING ... is supported for binary string input + assertThat(assertions.query( + "SELECT json_exists(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF8, 'lax $[1]')")) + .matches("VALUES true"); + + bytes = INPUT.getBytes(UTF_16LE); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_exists(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF16, 'lax $[1]')")) + .matches("VALUES true"); + + bytes = INPUT.getBytes(Charset.forName("UTF-32LE")); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_exists(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF32, 'lax $[1]')")) + .matches("VALUES true"); + + // the encoding must match the actual data + String finalVarbinaryLiteral = varbinaryLiteral; + assertThatThrownBy(() -> assertions.query( + "SELECT json_exists(" + finalVarbinaryLiteral + " FORMAT JSON ENCODING UTF8, 'lax $[1]' ERROR ON ERROR)")) + .hasMessage("conversion to JSON failed: "); + } + + @Test + public void testInputConversionError() + { + // input conversion error is handled accordingly to the ON ERROR clause + + // default error behavior is FALSE ON ERROR + assertThat(assertions.query( + "SELECT json_exists('" + INCORRECT_INPUT + "', 'lax $[1]')")) + .matches("VALUES false"); + + assertThat(assertions.query( + "SELECT json_exists('" + INCORRECT_INPUT + "', 'strict $[1]' TRUE ON ERROR)")) + .matches("VALUES true"); + + assertThat(assertions.query( + "SELECT json_exists('" + INCORRECT_INPUT + "', 'strict $[1]' FALSE ON ERROR)")) + .matches("VALUES false"); + + assertThat(assertions.query( + "SELECT json_exists('" + INCORRECT_INPUT + "', 'strict $[1]' UNKNOWN ON ERROR)")) + .matches("VALUES cast(null AS boolean)"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_exists('" + INCORRECT_INPUT + "', 'strict $[1]' ERROR ON ERROR)")) + .isInstanceOf(JsonInputConversionError.class) + .hasMessage("conversion to JSON failed: "); + } + + @Test + public void testPassingClause() + { + // watch out for case sensitive identifiers in JSON path + assertThatThrownBy(() -> assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $number + 1' PASSING 2 AS number)")) + .hasMessage("line 1:39: no value passed for parameter number. Try quoting \"number\" in the PASSING clause to match case"); + + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $number + 1' PASSING 5 AS \"number\")")) + .matches("VALUES true"); + + // JSON parameter + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $array[0]' PASSING '[1, 2, 3]' FORMAT JSON AS \"array\")")) + .matches("VALUES true"); + + // input conversion error of JSON parameter is handled accordingly to the ON ERROR clause + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $array[0]' PASSING '[...' FORMAT JSON AS \"array\")")) + .matches("VALUES false"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $array[0]' PASSING '[...' FORMAT JSON AS \"array\" ERROR ON ERROR)")) + .isInstanceOf(JsonInputConversionError.class) + .hasMessage("conversion to JSON failed: "); + + // array index out of bounds + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $[$number]' PASSING 5 AS \"number\")")) + .matches("VALUES false"); + } + + @Test + public void testIncorrectPath() + { + assertThatThrownBy(() -> assertions.query( + "SELECT json_exists('" + INPUT + "', 'certainly not a valid path')")) + .isInstanceOf(ParsingException.class) + .hasMessage("line 1:40: mismatched input 'certainly' expecting {'lax', 'strict'}"); + } + + @Test + public void testNullInput() + { + // null as input item + assertThat(assertions.query( + "SELECT json_exists(null, 'lax $')")) + .matches("VALUES cast(null AS boolean)"); + + // null as SQL-value parameter is evaluated to a JSON null + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $var' PASSING null AS \"var\")")) + .matches("VALUES true"); + + // null as JSON parameter is evaluated to empty sequence + assertThat(assertions.query( + "SELECT json_exists('" + INPUT + "', 'lax $var' PASSING null FORMAT JSON AS \"var\")")) + .matches("VALUES false"); + } +} diff --git a/core/trino-main/src/test/java/io/trino/sql/query/TestJsonQueryFunction.java b/core/trino-main/src/test/java/io/trino/sql/query/TestJsonQueryFunction.java new file mode 100644 index 000000000000..dab8314c2187 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/sql/query/TestJsonQueryFunction.java @@ -0,0 +1,412 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.query; + +import io.trino.json.PathEvaluationError; +import io.trino.operator.scalar.json.JsonInputConversionError; +import io.trino.operator.scalar.json.JsonOutputConversionError; +import io.trino.sql.parser.ParsingException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.nio.charset.Charset; + +import static com.google.common.io.BaseEncoding.base16; +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@TestInstance(PER_CLASS) +public class TestJsonQueryFunction +{ + private static final String INPUT = "[\"a\", \"b\", \"c\"]"; + private static final String OBJECT_INPUT = "{\"key\" : 1}"; + private static final String INCORRECT_INPUT = "[..."; + private QueryAssertions assertions; + + @BeforeAll + public void init() + { + assertions = new QueryAssertions(); + } + + @AfterAll + public void teardown() + { + assertions.close(); + assertions = null; + } + + @Test + public void testJsonQuery() + { + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $')")) + .matches("VALUES VARCHAR '[\"a\",\"b\",\"c\"]'"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'strict $')")) + .matches("VALUES VARCHAR '[\"a\",\"b\",\"c\"]'"); + + // structural error not suppressed by the path engine in strict mode, and handled accordingly to the ON ERROR clause + + // default error behavior is NULL ON ERROR + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'strict $[100]')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'strict $[100]' NULL ON ERROR)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'strict $[100]' EMPTY ARRAY ON ERROR)")) + .matches("VALUES VARCHAR '[]'"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'strict $[100]' EMPTY OBJECT ON ERROR)")) + .matches("VALUES VARCHAR '{}'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INPUT + "', 'strict $[100]' ERROR ON ERROR)")) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: structural error: invalid array subscript: [100, 100] for array of size 3"); + + // structural error suppressed by the path engine in lax mode. empty sequence is returned, so ON EMPTY behavior is applied + + // default empty behavior is NULL ON EMPTY + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[100]')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[100]' NULL ON EMPTY)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[100]' EMPTY ARRAY ON EMPTY)")) + .matches("VALUES VARCHAR '[]'"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[100]' EMPTY OBJECT ON EMPTY)")) + .matches("VALUES VARCHAR '{}'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[100]' ERROR ON EMPTY)")) + .isInstanceOf(JsonOutputConversionError.class) + .hasMessage("conversion from JSON failed: JSON path found no items"); + + // path returns multiple items (no array wrapper specified). this case is handled accordingly to the ON ERROR clause + + // default error behavior is NULL ON ERROR + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0 to 2]')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0 to 2]' NULL ON ERROR)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0 to 2]' EMPTY ARRAY ON ERROR)")) + .matches("VALUES VARCHAR '[]'"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0 to 2]' EMPTY OBJECT ON ERROR)")) + .matches("VALUES VARCHAR '{}'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0 to 2]' ERROR ON ERROR)")) + .isInstanceOf(JsonOutputConversionError.class) + .hasMessage("conversion from JSON failed: JSON path found multiple items"); + } + + @Test + public void testInputFormat() + { + // FORMAT JSON is default for character string input + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[1]')")) + .matches("VALUES VARCHAR '\"b\"'"); + + // FORMAT JSON is the only supported format for character string input + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "' FORMAT JSON, 'lax $[1]')")) + .matches("VALUES VARCHAR '\"b\"'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INPUT + "' FORMAT JSON ENCODING UTF8, 'lax $[1]')")) + .hasMessage("line 1:19: Cannot read input of type varchar(15) as JSON using formatting JSON ENCODING UTF8"); + + // FORMAT JSON is default for binary string input + byte[] bytes = INPUT.getBytes(UTF_8); + String varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_query(" + varbinaryLiteral + ", 'lax $[1]')")) + .matches("VALUES VARCHAR '\"b\"'"); + + assertThat(assertions.query( + "SELECT json_query(" + varbinaryLiteral + " FORMAT JSON, 'lax $[1]')")) + .matches("VALUES VARCHAR '\"b\"'"); + + // FORMAT JSON ENCODING ... is supported for binary string input + assertThat(assertions.query( + "SELECT json_query(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF8, 'lax $[1]')")) + .matches("VALUES VARCHAR '\"b\"'"); + + bytes = INPUT.getBytes(UTF_16LE); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_query(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF16, 'lax $[1]')")) + .matches("VALUES VARCHAR '\"b\"'"); + + bytes = INPUT.getBytes(Charset.forName("UTF-32LE")); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_query(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF32, 'lax $[1]')")) + .matches("VALUES VARCHAR '\"b\"'"); + + // the encoding must match the actual data + String finalVarbinaryLiteral = varbinaryLiteral; + assertThatThrownBy(() -> assertions.query( + "SELECT json_query(" + finalVarbinaryLiteral + " FORMAT JSON ENCODING UTF8, 'lax $[1]' ERROR ON ERROR)")) + .hasMessage("conversion to JSON failed: "); + } + + @Test + public void testInputConversionError() + { + // input conversion error is handled accordingly to the ON ERROR clause + + // default error behavior is NULL ON ERROR + assertThat(assertions.query( + "SELECT json_query('" + INCORRECT_INPUT + "', 'lax $[1]')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INCORRECT_INPUT + "', 'lax $[1]' NULL ON ERROR)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INCORRECT_INPUT + "', 'lax $[1]' EMPTY ARRAY ON ERROR)")) + .matches("VALUES VARCHAR '[]'"); + + assertThat(assertions.query( + "SELECT json_query('" + INCORRECT_INPUT + "', 'lax $[1]' EMPTY OBJECT ON ERROR)")) + .matches("VALUES VARCHAR '{}'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INCORRECT_INPUT + "', 'lax $[1]' ERROR ON ERROR)")) + .isInstanceOf(JsonInputConversionError.class) + .hasMessage("conversion to JSON failed: "); + } + + @Test + public void testPassingClause() + { + // watch out for case sensitive identifiers in JSON path + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $number + 1' PASSING 2 AS number)")) + .hasMessage("line 1:38: no value passed for parameter number. Try quoting \"number\" in the PASSING clause to match case"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $number + 1' PASSING 5 AS \"number\")")) + .matches("VALUES VARCHAR '6'"); + + // JSON parameter + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $array[0]' PASSING '[1, 2, 3]' FORMAT JSON AS \"array\")")) + .matches("VALUES VARCHAR '1'"); + + // input conversion error of JSON parameter is handled accordingly to the ON ERROR clause + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $array[0]' PASSING '[...' FORMAT JSON AS \"array\")")) + .matches("VALUES cast(null AS varchar)"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $array[0]' PASSING '[...' FORMAT JSON AS \"array\" ERROR ON ERROR)")) + .isInstanceOf(JsonInputConversionError.class) + .hasMessage("conversion to JSON failed: "); + + // array index out of bounds + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[$number]' PASSING 5 AS \"number\")")) + .matches("VALUES cast(null AS varchar)"); + } + + @Test + public void testOutput() + { + // default returned type is varchar, and default formatting is FORMAT JSON + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax 1')")) + .matches("VALUES VARCHAR '1'"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax true' RETURNING varchar FORMAT JSON)")) + .matches("VALUES VARCHAR 'true'"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax null' RETURNING varchar FORMAT JSON)")) + .matches("VALUES VARCHAR 'null'"); + + // explicit returned type + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[1]' RETURNING char(10))")) + .matches("VALUES cast('\"b\"' AS char(10))"); + + // truncating cast from varchar to expected returned type + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax \"text too long\"' RETURNING char(10))")) + .matches("VALUES cast('\"text too ' AS char(10))"); + + // invalid returned type + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INPUT + "', 'lax 1' RETURNING tinyint)")) + .hasMessage("line 1:8: Cannot output JSON value as tinyint using formatting JSON"); + + // returned type varbinary + + String output = "[\"a\",\"b\",\"c\"]"; // input without whitespace + byte[] bytes = output.getBytes(UTF_8); + String varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + // default formatting is FORMAT JSON + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $' RETURNING varbinary)")) + .matches("VALUES " + varbinaryLiteral); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $' RETURNING varbinary FORMAT JSON)")) + .matches("VALUES " + varbinaryLiteral); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $' RETURNING varbinary FORMAT JSON ENCODING UTF8)")) + .matches("VALUES " + varbinaryLiteral); + + bytes = output.getBytes(UTF_16LE); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $' RETURNING varbinary FORMAT JSON ENCODING UTF16)")) + .matches("VALUES " + varbinaryLiteral); + + bytes = output.getBytes(Charset.forName("UTF_32LE")); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $' RETURNING varbinary FORMAT JSON ENCODING UTF32)")) + .matches("VALUES " + varbinaryLiteral); + } + + @Test + public void testWrapperBehavior() + { + // by default, multiple output items cause error. the error is handled accordingly to the ON ERROR clause + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0 to 1]')")) + .matches("VALUES cast(null AS varchar)"); + + // WITH CONDITIONAL ARRAY WRAPPER -> wrap results in array unless there is singleton JSON array or object + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0 to 1]' WITH CONDITIONAL ARRAY WRAPPER)")) + .matches("VALUES cast('[\"a\",\"b\"]' AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0]' WITH CONDITIONAL ARRAY WRAPPER)")) + .matches("VALUES cast('[\"a\"]' AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $' WITH CONDITIONAL ARRAY WRAPPER)")) + .matches("VALUES cast('[\"a\",\"b\",\"c\"]' AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + OBJECT_INPUT + "', 'lax $' WITH CONDITIONAL ARRAY WRAPPER)")) + .matches("VALUES cast('{\"key\":1}' AS varchar)"); + + // WITH UNCONDITIONAL ARRAY WRAPPER -> wrap results in array + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0 to 1]' WITH UNCONDITIONAL ARRAY WRAPPER)")) + .matches("VALUES cast('[\"a\",\"b\"]' AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $[0]' WITH UNCONDITIONAL ARRAY WRAPPER)")) + .matches("VALUES cast('[\"a\"]' AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $' WITH UNCONDITIONAL ARRAY WRAPPER)")) + .matches("VALUES cast('[[\"a\",\"b\",\"c\"]]' AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + OBJECT_INPUT + "', 'lax $' WITH UNCONDITIONAL ARRAY WRAPPER)")) + .matches("VALUES cast('[{\"key\":1}]' AS varchar)"); + } + + @Test + public void testQuotesBehavior() + { + // when the result is a scalar string, the surrounding quptes are kept by default + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax \"some scalar text value\"')")) + .matches("VALUES cast('\"some scalar text value\"' AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax \"some scalar text value\"' KEEP QUOTES ON SCALAR STRING)")) + .matches("VALUES cast('\"some scalar text value\"' AS varchar)"); + + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax \"some scalar text value\"' OMIT QUOTES ON SCALAR STRING)")) + .matches("VALUES cast('some scalar text value' AS varchar)"); + + // the OMIT QUOTES clause does not affect nested string values + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $' OMIT QUOTES ON SCALAR STRING)")) + .matches("VALUES cast('[\"a\",\"b\",\"c\"]' AS varchar)"); + } + + @Test + public void testIncorrectPath() + { + assertThatThrownBy(() -> assertions.query( + "SELECT json_query('" + INPUT + "', 'certainly not a valid path')")) + .isInstanceOf(ParsingException.class) + .hasMessage("line 1:39: mismatched input 'certainly' expecting {'lax', 'strict'}"); + } + + @Test + public void testNullInput() + { + // null as input item + assertThat(assertions.query( + "SELECT json_query(null, 'lax $')")) + .matches("VALUES cast(null AS varchar)"); + + // null as SQL-value parameter is evaluated to a JSON null, and the corresponding returned SQL value is null + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $var' PASSING null AS \"var\")")) + .matches("VALUES cast('null' AS varchar)"); + + // null as JSON parameter is evaluated to empty sequence. this condition is handled accordingly to ON EMPTY behavior + assertThat(assertions.query( + "SELECT json_query('" + INPUT + "', 'lax $var' PASSING null FORMAT JSON AS \"var\" EMPTY ARRAY ON EMPTY)")) + .matches("VALUES cast('[]' AS varchar)"); + } +} diff --git a/core/trino-main/src/test/java/io/trino/sql/query/TestJsonValueFunction.java b/core/trino-main/src/test/java/io/trino/sql/query/TestJsonValueFunction.java new file mode 100644 index 000000000000..1d3a16dfc866 --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/sql/query/TestJsonValueFunction.java @@ -0,0 +1,343 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.query; + +import io.trino.json.PathEvaluationError; +import io.trino.operator.scalar.json.JsonInputConversionError; +import io.trino.operator.scalar.json.JsonValueFunction.JsonValueResultError; +import io.trino.sql.parser.ParsingException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.nio.charset.Charset; + +import static com.google.common.io.BaseEncoding.base16; +import static java.nio.charset.StandardCharsets.UTF_16LE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +@TestInstance(PER_CLASS) +public class TestJsonValueFunction +{ + private static final String INPUT = "[\"a\", \"b\", \"c\"]"; + private static final String INCORRECT_INPUT = "[..."; + private QueryAssertions assertions; + + @BeforeAll + public void init() + { + assertions = new QueryAssertions(); + } + + @AfterAll + public void teardown() + { + assertions.close(); + assertions = null; + } + + @Test + public void testJsonValue() + { + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'strict $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + // structural error suppressed by the path engine in lax mode. resulting sequence consists of the last item in the array + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[2 to 100]')")) + .matches("VALUES VARCHAR 'c'"); + + // structural error not suppressed by the path engine in strict mode, and handled accordingly to the ON ERROR clause + + // default error behavior is NULL ON ERROR + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'strict $[100]')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'strict $[100]' NULL ON ERROR)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'strict $[100]' DEFAULT 'x' ON ERROR)")) + .matches("VALUES VARCHAR 'x'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INPUT + "', 'strict $[100]' ERROR ON ERROR)")) + .isInstanceOf(PathEvaluationError.class) + .hasMessage("path evaluation failed: structural error: invalid array subscript: [100, 100] for array of size 3"); + + // structural error suppressed by the path engine in lax mode. empty sequence is returned, so ON EMPTY behavior is applied + + // default empty behavior is NULL ON EMPTY + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[100]')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[100]' NULL ON EMPTY)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[100]' DEFAULT 'x' ON EMPTY)")) + .matches("VALUES VARCHAR 'x'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[100]' ERROR ON EMPTY)")) + .isInstanceOf(JsonValueResultError.class) + .hasMessage("cannot extract SQL scalar from JSON: JSON path found no items"); + + // path returns multiple items. this case is handled accordingly to the ON ERROR clause + + // default error behavior is NULL ON ERROR + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[0 to 2]')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[0 to 2]' NULL ON ERROR)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[0 to 2]' DEFAULT 'x' ON ERROR)")) + .matches("VALUES VARCHAR 'x'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[0 to 2]' ERROR ON ERROR)")) + .isInstanceOf(JsonValueResultError.class) + .hasMessage("cannot extract SQL scalar from JSON: JSON path found multiple items"); + } + + @Test + public void testInputFormat() + { + // FORMAT JSON is default for character string input + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + // FORMAT JSON is the only supported format for character string input + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "' FORMAT JSON, 'lax $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INPUT + "' FORMAT JSON ENCODING UTF8, 'lax $[1]')")) + .hasMessage("line 1:19: Cannot read input of type varchar(15) as JSON using formatting JSON ENCODING UTF8"); + + // FORMAT JSON is default for binary string input + byte[] bytes = INPUT.getBytes(UTF_8); + String varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_value(" + varbinaryLiteral + ", 'lax $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + assertThat(assertions.query( + "SELECT json_value(" + varbinaryLiteral + " FORMAT JSON, 'lax $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + // FORMAT JSON ENCODING ... is supported for binary string input + assertThat(assertions.query( + "SELECT json_value(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF8, 'lax $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + bytes = INPUT.getBytes(UTF_16LE); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_value(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF16, 'lax $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + bytes = INPUT.getBytes(Charset.forName("UTF-32LE")); + varbinaryLiteral = "X'" + base16().encode(bytes) + "'"; + + assertThat(assertions.query( + "SELECT json_value(" + varbinaryLiteral + " FORMAT JSON ENCODING UTF32, 'lax $[1]')")) + .matches("VALUES VARCHAR 'b'"); + + // the encoding must match the actual data + String finalVarbinaryLiteral = varbinaryLiteral; + assertThatThrownBy(() -> assertions.query( + "SELECT json_value(" + finalVarbinaryLiteral + " FORMAT JSON ENCODING UTF8, 'lax $[1]' ERROR ON ERROR)")) + .hasMessage("conversion to JSON failed: "); + } + + @Test + public void testInputConversionError() + { + // input conversion error is handled accordingly to the ON ERROR clause + + // default error behavior is NULL ON ERROR + assertThat(assertions.query( + "SELECT json_value('" + INCORRECT_INPUT + "', 'lax $[1]')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INCORRECT_INPUT + "', 'lax $[1]' NULL ON ERROR)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INCORRECT_INPUT + "', 'lax $[1]' DEFAULT 'x' ON ERROR)")) + .matches("VALUES VARCHAR 'x'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INCORRECT_INPUT + "', 'lax $[1]' ERROR ON ERROR)")) + .isInstanceOf(JsonInputConversionError.class) + .hasMessage("conversion to JSON failed: "); + } + + @Test + public void testPassingClause() + { + // watch out for case sensitive identifiers in JSON path + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $number + 1' PASSING 2 AS number)")) + .hasMessage("line 1:38: no value passed for parameter number. Try quoting \"number\" in the PASSING clause to match case"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $number + 1' PASSING 5 AS \"number\")")) + .matches("VALUES VARCHAR '6'"); + + // JSON parameter + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $array[0]' PASSING '[1, 2, 3]' FORMAT JSON AS \"array\")")) + .matches("VALUES VARCHAR '1'"); + + // input conversion error of JSON parameter is handled accordingly to the ON ERROR clause + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $array[0]' PASSING '[...' FORMAT JSON AS \"array\")")) + .matches("VALUES cast(null AS varchar)"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $array[0]' PASSING '[...' FORMAT JSON AS \"array\" ERROR ON ERROR)")) + .isInstanceOf(JsonInputConversionError.class) + .hasMessage("conversion to JSON failed: "); + + // array index out of bounds + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[$number]' PASSING 5 AS \"number\")")) + .matches("VALUES cast(null AS varchar)"); + } + + @Test + public void testReturnedType() + { + // default returned type is varchar + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax 1')")) + .matches("VALUES VARCHAR '1'"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax true')")) + .matches("VALUES VARCHAR 'true'"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax null')")) + .matches("VALUES cast(null AS varchar)"); + + // explicit returned type + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[1]' RETURNING char(10))")) + .matches("VALUES cast('b' AS char(10))"); + + // the actual value does not fit in the expected returned type. the error is handled accordingly to the ON ERROR clause + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax 1000' RETURNING tinyint)")) + .matches("VALUES cast(null AS tinyint)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax 1000' RETURNING tinyint DEFAULT TINYINT '-1' ON ERROR)")) + .matches("VALUES TINYINT '-1'"); + + // default value cast to the expected returned type + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax 1000000000000 * 1000000000000' RETURNING bigint DEFAULT TINYINT '-1' ON ERROR)")) + .matches("VALUES BIGINT '-1'"); + } + + @Test + public void testPathResultNonScalar() + { + // JSON array ir object cannot be returned as SQL scalar value. the error is handled accordingly to the ON ERROR clause + + // default error behavior is NULL ON ERROR + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $')")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $' NULL ON ERROR)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $' DEFAULT 'x' ON ERROR)")) + .matches("VALUES VARCHAR 'x'"); + + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $' ERROR ON ERROR)")) + .isInstanceOf(JsonValueResultError.class) + .hasMessage("cannot extract SQL scalar from JSON: JSON path found an item that cannot be converted to an SQL value"); + } + + @Test + public void testIncorrectPath() + { + assertThatThrownBy(() -> assertions.query( + "SELECT json_value('" + INPUT + "', 'certainly not a valid path')")) + .isInstanceOf(ParsingException.class) + .hasMessage("line 1:39: mismatched input 'certainly' expecting {'lax', 'strict'}"); + } + + @Test + public void testNullInput() + { + // null as input item + assertThat(assertions.query( + "SELECT json_value(null, 'lax $')")) + .matches("VALUES cast(null AS varchar)"); + + // null as SQL-value parameter is evaluated to a JSON null, and the corresponding returned SQL value is null + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $var' PASSING null AS \"var\")")) + .matches("VALUES cast(null AS varchar)"); + + // null as JSON parameter is evaluated to empty sequence. this condition is handled accordingly to ON EMPTY behavior + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $var' PASSING null FORMAT JSON AS \"var\" DEFAULT 'was empty...' ON EMPTY)")) + .matches("VALUES cast('was empty...' AS varchar)"); + + // null as empty default and error default + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax 1' DEFAULT null ON EMPTY DEFAULT null ON ERROR)")) + .matches("VALUES cast(1 AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax $[100]' DEFAULT null ON EMPTY)")) + .matches("VALUES cast(null AS varchar)"); + + assertThat(assertions.query( + "SELECT json_value('" + INPUT + "', 'lax 1 + $[0]' DEFAULT null ON ERROR)")) + .matches("VALUES cast(null AS varchar)"); + } +} diff --git a/core/trino-main/src/test/java/io/trino/type/TestJsonPath2016TypeSerialization.java b/core/trino-main/src/test/java/io/trino/type/TestJsonPath2016TypeSerialization.java new file mode 100644 index 000000000000..8f00401b003e --- /dev/null +++ b/core/trino-main/src/test/java/io/trino/type/TestJsonPath2016TypeSerialization.java @@ -0,0 +1,211 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.type; + +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.google.common.collect.ImmutableList; +import io.trino.json.ir.IrAbsMethod; +import io.trino.json.ir.IrArithmeticBinary; +import io.trino.json.ir.IrArithmeticUnary; +import io.trino.json.ir.IrArrayAccessor; +import io.trino.json.ir.IrArrayAccessor.Subscript; +import io.trino.json.ir.IrCeilingMethod; +import io.trino.json.ir.IrConstantJsonSequence; +import io.trino.json.ir.IrContextVariable; +import io.trino.json.ir.IrDatetimeMethod; +import io.trino.json.ir.IrDoubleMethod; +import io.trino.json.ir.IrFloorMethod; +import io.trino.json.ir.IrJsonNull; +import io.trino.json.ir.IrJsonPath; +import io.trino.json.ir.IrKeyValueMethod; +import io.trino.json.ir.IrLastIndexVariable; +import io.trino.json.ir.IrLiteral; +import io.trino.json.ir.IrMemberAccessor; +import io.trino.json.ir.IrNamedJsonVariable; +import io.trino.json.ir.IrNamedValueVariable; +import io.trino.json.ir.IrSizeMethod; +import io.trino.json.ir.IrTypeMethod; +import io.trino.spi.block.Block; +import io.trino.spi.block.BlockBuilder; +import io.trino.spi.block.TestingBlockEncodingSerde; +import io.trino.spi.type.Type; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static io.airlift.slice.Slices.utf8Slice; +import static io.trino.json.ir.IrArithmeticBinary.Operator.ADD; +import static io.trino.json.ir.IrArithmeticBinary.Operator.MULTIPLY; +import static io.trino.json.ir.IrArithmeticUnary.Sign.MINUS; +import static io.trino.json.ir.IrArithmeticUnary.Sign.PLUS; +import static io.trino.json.ir.IrConstantJsonSequence.EMPTY_SEQUENCE; +import static io.trino.json.ir.IrConstantJsonSequence.singletonSequence; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.BooleanType.BOOLEAN; +import static io.trino.spi.type.DecimalType.createDecimalType; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.TimeType.DEFAULT_PRECISION; +import static io.trino.spi.type.TimeType.createTimeType; +import static io.trino.spi.type.VarcharType.VARCHAR; +import static io.trino.spi.type.VarcharType.createVarcharType; +import static io.trino.type.InternalTypeManager.TESTING_TYPE_MANAGER; +import static org.testng.Assert.assertEquals; + +public class TestJsonPath2016TypeSerialization +{ + private static final Type JSON_PATH_2016 = new JsonPath2016Type(new TypeDeserializer(TESTING_TYPE_MANAGER), new TestingBlockEncodingSerde()); + + @Test + public void testJsonPathMode() + { + assertJsonRoundTrip(new IrJsonPath(true, new IrJsonNull())); + assertJsonRoundTrip(new IrJsonPath(false, new IrJsonNull())); + } + + @Test + public void testLiterals() + { + assertJsonRoundTrip(new IrJsonPath(true, new IrLiteral(createDecimalType(2, 1), 1L))); + assertJsonRoundTrip(new IrJsonPath(true, new IrLiteral(DOUBLE, 1e0))); + assertJsonRoundTrip(new IrJsonPath(true, new IrLiteral(INTEGER, 1L))); + assertJsonRoundTrip(new IrJsonPath(true, new IrLiteral(BIGINT, 1000000000000L))); + assertJsonRoundTrip(new IrJsonPath(true, new IrLiteral(VARCHAR, utf8Slice("some_text")))); + assertJsonRoundTrip(new IrJsonPath(true, new IrLiteral(BOOLEAN, false))); + } + + @Test + public void testContextVariable() + { + assertJsonRoundTrip(new IrJsonPath(true, new IrContextVariable(Optional.empty()))); + assertJsonRoundTrip(new IrJsonPath(true, new IrContextVariable(Optional.of(DOUBLE)))); + } + + @Test + public void testNamedVariables() + { + // json variable + assertJsonRoundTrip(new IrJsonPath(true, new IrNamedJsonVariable(5, Optional.empty()))); + assertJsonRoundTrip(new IrJsonPath(true, new IrNamedJsonVariable(5, Optional.of(DOUBLE)))); + + // SQL value variable + assertJsonRoundTrip(new IrJsonPath(true, new IrNamedValueVariable(5, Optional.of(DOUBLE)))); + } + + @Test + public void testMethods() + { + assertJsonRoundTrip(new IrJsonPath(true, new IrAbsMethod(new IrLiteral(DOUBLE, 1e0), Optional.of(DOUBLE)))); + assertJsonRoundTrip(new IrJsonPath(true, new IrCeilingMethod(new IrLiteral(DOUBLE, 1e0), Optional.of(DOUBLE)))); + assertJsonRoundTrip(new IrJsonPath(true, new IrDatetimeMethod(new IrLiteral(BIGINT, 1L), Optional.of("some_time_format"), Optional.of(createTimeType(DEFAULT_PRECISION))))); + assertJsonRoundTrip(new IrJsonPath(true, new IrDoubleMethod(new IrLiteral(BIGINT, 1L), Optional.of(DOUBLE)))); + assertJsonRoundTrip(new IrJsonPath(true, new IrFloorMethod(new IrLiteral(DOUBLE, 1e0), Optional.of(DOUBLE)))); + assertJsonRoundTrip(new IrJsonPath(true, new IrKeyValueMethod(new IrJsonNull()))); + assertJsonRoundTrip(new IrJsonPath(true, new IrSizeMethod(new IrJsonNull(), Optional.of(INTEGER)))); + assertJsonRoundTrip(new IrJsonPath(true, new IrTypeMethod(new IrJsonNull(), Optional.of(createVarcharType(7))))); + } + + @Test + public void testArrayAccessor() + { + // wildcard accessor + assertJsonRoundTrip(new IrJsonPath(true, new IrArrayAccessor(new IrJsonNull(), ImmutableList.of(), Optional.empty()))); + + // with subscripts based on literals + assertJsonRoundTrip(new IrJsonPath(true, new IrArrayAccessor( + new IrJsonNull(), + ImmutableList.of( + new Subscript(new IrLiteral(INTEGER, 0L), Optional.of(new IrLiteral(INTEGER, 1L))), + new Subscript(new IrLiteral(INTEGER, 3L), Optional.of(new IrLiteral(INTEGER, 5L))), + new Subscript(new IrLiteral(INTEGER, 7L), Optional.empty())), + Optional.of(VARCHAR)))); + + // with LAST index variable + assertJsonRoundTrip(new IrJsonPath(true, new IrArrayAccessor( + new IrJsonNull(), + ImmutableList.of(new Subscript(new IrLastIndexVariable(Optional.of(INTEGER)), Optional.empty())), + Optional.empty()))); + } + + @Test + public void testMemberAccessor() + { + // wildcard accessor + assertJsonRoundTrip(new IrJsonPath(true, new IrMemberAccessor(new IrJsonNull(), Optional.empty(), Optional.empty()))); + + // accessor by field name + assertJsonRoundTrip(new IrJsonPath(true, new IrMemberAccessor(new IrJsonNull(), Optional.of("some_key"), Optional.of(BIGINT)))); + } + + @Test + public void testArithmeticBinary() + { + assertJsonRoundTrip(new IrJsonPath(true, new IrArithmeticBinary(ADD, new IrJsonNull(), new IrJsonNull(), Optional.empty()))); + + assertJsonRoundTrip(new IrJsonPath(true, new IrArithmeticBinary( + ADD, + new IrLiteral(INTEGER, 1L), + new IrLiteral(BIGINT, 2L), + Optional.of(BIGINT)))); + } + + @Test + public void testArithmeticUnary() + { + assertJsonRoundTrip(new IrJsonPath(true, new IrArithmeticUnary(PLUS, new IrJsonNull(), Optional.empty()))); + assertJsonRoundTrip(new IrJsonPath(true, new IrArithmeticUnary(MINUS, new IrJsonNull(), Optional.empty()))); + assertJsonRoundTrip(new IrJsonPath(true, new IrArithmeticUnary(MINUS, new IrLiteral(INTEGER, 1L), Optional.of(INTEGER)))); + } + + @Test + public void testConstantJsonSequence() + { + // empty sequence + assertJsonRoundTrip(new IrJsonPath(true, EMPTY_SEQUENCE)); + + // singleton sequence + assertJsonRoundTrip(new IrJsonPath(true, singletonSequence(NullNode.getInstance(), Optional.empty()))); + assertJsonRoundTrip(new IrJsonPath(true, singletonSequence(BooleanNode.TRUE, Optional.of(BOOLEAN)))); + + // long sequence + assertJsonRoundTrip(new IrJsonPath(true, new IrConstantJsonSequence( + ImmutableList.of(IntNode.valueOf(1), IntNode.valueOf(2), IntNode.valueOf(3)), + Optional.of(INTEGER)))); + } + + @Test + public void testNestedStructure() + { + assertJsonRoundTrip(new IrJsonPath( + true, + new IrTypeMethod( + new IrArithmeticBinary( + MULTIPLY, + new IrArithmeticUnary(MINUS, new IrAbsMethod(new IrFloorMethod(new IrLiteral(INTEGER, 1L), Optional.of(INTEGER)), Optional.of(INTEGER)), Optional.of(INTEGER)), + new IrCeilingMethod(new IrMemberAccessor(new IrContextVariable(Optional.empty()), Optional.of("some_key"), Optional.of(BIGINT)), Optional.of(BIGINT)), + Optional.of(BIGINT)), + Optional.of(createVarcharType(7))))); + } + + private static void assertJsonRoundTrip(IrJsonPath object) + { + BlockBuilder blockBuilder = JSON_PATH_2016.createBlockBuilder(null, 1); + JSON_PATH_2016.writeObject(blockBuilder, object); + Block serialized = blockBuilder.build(); + Object deserialized = JSON_PATH_2016.getObject(serialized, 0); + assertEquals(deserialized, object); + } +} diff --git a/core/trino-main/src/test/java/io/trino/type/TestRowOperators.java b/core/trino-main/src/test/java/io/trino/type/TestRowOperators.java index e0ebbac97a4f..ee63bc8ae0bb 100644 --- a/core/trino-main/src/test/java/io/trino/type/TestRowOperators.java +++ b/core/trino-main/src/test/java/io/trino/type/TestRowOperators.java @@ -304,11 +304,11 @@ public void testJsonToRow() asList("puppies", "[1,2,3]", null, "null")); assertFunction( - "CAST(JSON '{\"varchar_value\": \"puppies\", \"json_value\": [1, 2, 3], \"varchar_null\": null, \"json_null\": null}' " + - "AS ROW(varchar_value VARCHAR, json_value JSON, varchar_null VARCHAR, json_null JSON))", + "CAST(JSON '{\"varchar_value\": \"puppies\", \"json_value_field\": [1, 2, 3], \"varchar_null\": null, \"json_null\": null}' " + + "AS ROW(varchar_value VARCHAR, json_value_field JSON, varchar_null VARCHAR, json_null JSON))", RowType.from(ImmutableList.of( RowType.field("varchar_value", VARCHAR), - RowType.field("json_value", JSON), + RowType.field("json_value_field", JSON), RowType.field("varchar_null", VARCHAR), RowType.field("json_null", JSON))), asList("puppies", "[1,2,3]", null, "null")); diff --git a/core/trino-parser/src/main/antlr4/io/trino/jsonpath/JsonPath.g4 b/core/trino-parser/src/main/antlr4/io/trino/jsonpath/JsonPath.g4 new file mode 100644 index 000000000000..90dcd5e6cf1c --- /dev/null +++ b/core/trino-parser/src/main/antlr4/io/trino/jsonpath/JsonPath.g4 @@ -0,0 +1,200 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +grammar JsonPath; + +tokens { + DELIMITER +} + +path + : pathMode pathExpression EOF + ; + +pathMode + : LAX + | STRICT + ; + +pathExpression + : accessorExpression #expressionDefault + | sign=('+' | '-') pathExpression #signedUnary + | left=pathExpression operator=('*' | '/' | '%') right=pathExpression #binary + | left=pathExpression operator=('+' | '-') right=pathExpression #binary + ; + +accessorExpression + : pathPrimary #accessorExpressionDefault + | accessorExpression '.' identifier #memberAccessor + | accessorExpression '.' stringLiteral #memberAccessor + | accessorExpression '.' '*' #wildcardMemberAccessor + | accessorExpression '[' subscript (',' subscript)* ']' #arrayAccessor + | accessorExpression '[' '*' ']' #wildcardArrayAccessor + | accessorExpression '?' '(' predicate ')' #filter + | accessorExpression '.' TYPE '(' ')' #typeMethod + | accessorExpression '.' SIZE '(' ')' #sizeMethod + | accessorExpression '.' DOUBLE '(' ')' #doubleMethod + | accessorExpression '.' CEILING '(' ')' #ceilingMethod + | accessorExpression '.' FLOOR '(' ')' #floorMethod + | accessorExpression '.' ABS '(' ')' #absMethod + | accessorExpression '.' DATETIME '(' stringLiteral? ')' #datetimeMethod + | accessorExpression '.' KEYVALUE '(' ')' #keyValueMethod + ; + +identifier + : IDENTIFIER + | nonReserved + ; + +subscript + : singleton=pathExpression + | from=pathExpression TO to=pathExpression + ; + +pathPrimary + : literal #literalPrimary + | variable #variablePrimary + | '(' pathExpression ')' #parenthesizedPath + ; + +literal + : numericLiteral + | stringLiteral + | nullLiteral + | booleanLiteral + ; + +numericLiteral + : MINUS? DECIMAL_VALUE #decimalLiteral + | MINUS? DOUBLE_VALUE #doubleLiteral + | MINUS? INTEGER_VALUE #integerLiteral + ; + +stringLiteral + : STRING // add unicode (like SqlBase.g4), add quoting in single quotes (') + ; + +nullLiteral + : NULL + ; + +booleanLiteral + : TRUE | FALSE + ; + +variable + : '$' #contextVariable + | NAMED_VARIABLE #namedVariable + | LAST #lastIndexVariable + | '@' #predicateCurrentItemVariable + ; + +// the following part is dedicated to JSON path predicate +predicate + : predicatePrimary #predicateDefault + | '!' delimitedPredicate #negationPredicate + | left=predicate '&&' right=predicate #conjunctionPredicate + | left=predicate '||' right=predicate #disjunctionPredicate + ; + +predicatePrimary + : delimitedPredicate #predicatePrimaryDefault + | left=pathExpression comparisonOperator right=pathExpression #comparisonPredicate + | base=pathExpression LIKE_REGEX pattern=stringLiteral ( FLAG flag=stringLiteral )? #likeRegexPredicate + | whole=pathExpression STARTS WITH (string=stringLiteral | NAMED_VARIABLE) #startsWithPredicate + | '(' predicate ')' IS UNKNOWN #isUnknownPredicate + ; + +delimitedPredicate + : EXISTS '(' pathExpression ')' #existsPredicate + | '(' predicate ')' #parenthesizedPredicate + ; + +comparisonOperator + : '==' | '<>' | '!=' | '<' | '>' | '<=' | '>=' + ; + +// there shall be no reserved words in JSON path +nonReserved + : ABS | CEILING | DATETIME | DOUBLE | EXISTS | FALSE | FLAG | FLOOR | IS | KEYVALUE | LAST | LAX | LIKE_REGEX | MINUS | NULL | SIZE | STARTS | STRICT | TO | TRUE | TYPE | UNKNOWN | WITH + ; + +ABS: 'abs'; +CEILING: 'ceiling'; +DATETIME: 'datetime'; +DOUBLE: 'double'; +EXISTS: 'exists'; +FALSE: 'false'; +FLAG: 'flag'; +FLOOR: 'floor'; +IS: 'is'; +KEYVALUE: 'keyvalue'; +LAST: 'last'; +LAX: 'lax'; +LIKE_REGEX: 'like_regex'; +MINUS: '-'; +NULL: 'null'; +SIZE: 'size'; +STARTS: 'starts'; +STRICT: 'strict'; +TO: 'to'; +TRUE: 'true'; +TYPE: 'type'; +UNKNOWN: 'unknown'; +WITH: 'with'; + +DECIMAL_VALUE + : DIGIT+ '.' DIGIT* + | '.' DIGIT+ + ; + +DOUBLE_VALUE + : DIGIT+ ('.' DIGIT*)? EXPONENT + | '.' DIGIT+ EXPONENT + ; + +INTEGER_VALUE + : DIGIT+ + ; + +STRING + : '"' ( ~'"' | '""' )* '"' + ; + +IDENTIFIER + : (LETTER | '_') (LETTER | DIGIT | '_')* + ; + +NAMED_VARIABLE + : '$' IDENTIFIER + ; + +fragment EXPONENT + : ('E' | 'e') [+-]? DIGIT+ + ; + +fragment DIGIT + : [0-9] + ; + +fragment LETTER + : [a-z] | [A-Z] + ; + +WS + : [ \r\n\t]+ -> channel(HIDDEN) + ; + +// Catch-all for anything we can't recognize. +UNRECOGNIZED: .; diff --git a/core/trino-parser/src/main/antlr4/io/trino/sql/parser/SqlBase.g4 b/core/trino-parser/src/main/antlr4/io/trino/sql/parser/SqlBase.g4 index b8b526dce5fa..7837ef583e5d 100644 --- a/core/trino-parser/src/main/antlr4/io/trino/sql/parser/SqlBase.g4 +++ b/core/trino-parser/src/main/antlr4/io/trino/sql/parser/SqlBase.g4 @@ -526,6 +526,63 @@ primaryExpression | EXTRACT '(' identifier FROM valueExpression ')' #extract | '(' expression ')' #parenthesizedExpression | GROUPING '(' (qualifiedName (',' qualifiedName)*)? ')' #groupingOperation + | JSON_EXISTS '(' jsonPathInvocation (jsonExistsErrorBehavior ON ERROR)? ')' #jsonExists + | JSON_VALUE '(' + jsonPathInvocation + (RETURNING type)? + (emptyBehavior=jsonValueBehavior ON EMPTY)? + (errorBehavior=jsonValueBehavior ON ERROR)? + ')' #jsonValue + | JSON_QUERY '(' + jsonPathInvocation + (RETURNING type (FORMAT jsonRepresentation)?)? + (jsonQueryWrapperBehavior WRAPPER)? + ((KEEP | OMIT) QUOTES (ON SCALAR TEXT_STRING)?)? + (emptyBehavior=jsonQueryBehavior ON EMPTY)? + (errorBehavior=jsonQueryBehavior ON ERROR)? + ')' #jsonQuery + ; + +jsonPathInvocation + : jsonValueExpression ',' path=string + (PASSING jsonArgument (',' jsonArgument)*)? + ; + +jsonValueExpression + : expression (FORMAT jsonRepresentation)? + ; + +jsonRepresentation + : JSON (ENCODING (UTF8 | UTF16 | UTF32))? // TODO add implementation-defined JSON representation option + ; + +jsonArgument + : jsonValueExpression AS identifier + ; + +jsonExistsErrorBehavior + : TRUE + | FALSE + | UNKNOWN + | ERROR + ; + +jsonValueBehavior + : ERROR + | NULL + | DEFAULT expression + ; + +jsonQueryWrapperBehavior + : WITHOUT ARRAY? + | WITH (CONDITIONAL | UNCONDITIONAL)? ARRAY? + ; + +jsonQueryBehavior + : ERROR + | NULL + | EMPTY ARRAY + | EMPTY OBJECT ; processingMode @@ -752,9 +809,9 @@ nonReserved // IMPORTANT: this rule must only contain tokens. Nested rules are not supported. See SqlParser.exitNonReserved : ADD | ADMIN | AFTER | ALL | ANALYZE | ANY | ARRAY | ASC | AT | AUTHORIZATION | BERNOULLI | BOTH - | CALL | CASCADE | CATALOGS | COLUMN | COLUMNS | COMMENT | COMMIT | COMMITTED | COPARTITION | COUNT | CURRENT + | CALL | CASCADE | CATALOGS | COLUMN | COLUMNS | COMMENT | COMMIT | COMMITTED | CONDITIONAL | COPARTITION | COUNT | CURRENT | DATA | DATE | DAY | DEFAULT | DEFINE | DEFINER | DESC | DESCRIPTOR | DISTRIBUTED | DOUBLE - | EMPTY | ERROR | EXCLUDING | EXPLAIN + | EMPTY | ENCODING | ERROR | EXCLUDING | EXPLAIN | FETCH | FILTER | FINAL | FIRST | FOLLOWING | FORMAT | FUNCTIONS | GRANT | DENY | GRANTED | GRANTS | GRAPHVIZ | GROUPS | HOUR @@ -764,15 +821,16 @@ nonReserved | LAST | LATERAL | LEADING | LEVEL | LIMIT | LOCAL | LOGICAL | MAP | MATCH | MATCHED | MATCHES | MATCH_RECOGNIZE | MATERIALIZED | MEASURES | MERGE | MINUTE | MONTH | NEXT | NFC | NFD | NFKC | NFKD | NO | NONE | NULLIF | NULLS - | OF | OFFSET | OMIT | ONE | ONLY | OPTION | ORDINALITY | OUTPUT | OVER | OVERFLOW - | PARTITION | PARTITIONS | PAST | PATH | PATTERN | PER | PERMUTE | POSITION | PRECEDING | PRECISION | PRIVILEGES | PROPERTIES | PRUNE - | RANGE | READ | REFRESH | RENAME | REPEATABLE | REPLACE | RESET | RESPECT | RESTRICT | REVOKE | ROLE | ROLES | ROLLBACK | ROW | ROWS | RUNNING - | SCHEMA | SCHEMAS | SECOND | SECURITY | SEEK | SERIALIZABLE | SESSION | SET | SETS + | OBJECT | OF | OFFSET | OMIT | ONE | ONLY | OPTION | ORDINALITY | OUTPUT | OVER | OVERFLOW + | PARTITION | PARTITIONS | PASSING | PAST | PATH | PATTERN | PER | PERMUTE | POSITION | PRECEDING | PRECISION | PRIVILEGES | PROPERTIES | PRUNE + | QUOTES + | RANGE | READ | REFRESH | RENAME | REPEATABLE | REPLACE | RESET | RESPECT | RESTRICT | RETURNING | REVOKE | ROLE | ROLES | ROLLBACK | ROW | ROWS | RUNNING + | SCALAR | SCHEMA | SCHEMAS | SECOND | SECURITY | SEEK | SERIALIZABLE | SESSION | SET | SETS | SHOW | SOME | START | STATS | SUBSET | SUBSTRING | SYSTEM - | TABLES | TABLESAMPLE | TEXT | TIES | TIME | TIMESTAMP | TO | TRAILING | TRANSACTION | TRUNCATE | TRY_CAST | TYPE - | UNBOUNDED | UNCOMMITTED | UNMATCHED | UPDATE | USE | USER + | TABLES | TABLESAMPLE | TEXT | TEXT_STRING | TIES | TIME | TIMESTAMP | TO | TRAILING | TRANSACTION | TRUNCATE | TRY_CAST | TYPE + | UNBOUNDED | UNCOMMITTED | UNCONDITIONAL | UNKNOWN | UNMATCHED | UPDATE | USE | USER | UTF16 | UTF32 | UTF8 | VALIDATE | VERBOSE | VERSION | VIEW - | WINDOW | WITHIN | WITHOUT | WORK | WRITE + | WINDOW | WITHIN | WITHOUT | WORK | WRAPPER | WRITE | YEAR | ZONE ; @@ -804,6 +862,7 @@ COLUMNS: 'COLUMNS'; COMMENT: 'COMMENT'; COMMIT: 'COMMIT'; COMMITTED: 'COMMITTED'; +CONDITIONAL: 'CONDITIONAL'; CONSTRAINT: 'CONSTRAINT'; COUNT: 'COUNT'; COPARTITION: 'COPARTITION'; @@ -837,6 +896,7 @@ DOUBLE: 'DOUBLE'; DROP: 'DROP'; ELSE: 'ELSE'; EMPTY: 'EMPTY'; +ENCODING: 'ENCODING'; END: 'END'; ERROR: 'ERROR'; ESCAPE: 'ESCAPE'; @@ -883,6 +943,9 @@ IS: 'IS'; ISOLATION: 'ISOLATION'; JOIN: 'JOIN'; JSON: 'JSON'; +JSON_EXISTS: 'JSON_EXISTS'; +JSON_QUERY: 'JSON_QUERY'; +JSON_VALUE: 'JSON_VALUE'; KEEP: 'KEEP'; LAST: 'LAST'; LATERAL: 'LATERAL'; @@ -919,6 +982,7 @@ NOT: 'NOT'; NULL: 'NULL'; NULLIF: 'NULLIF'; NULLS: 'NULLS'; +OBJECT: 'OBJECT'; OFFSET: 'OFFSET'; OMIT: 'OMIT'; OF: 'OF'; @@ -935,6 +999,7 @@ OVER: 'OVER'; OVERFLOW: 'OVERFLOW'; PARTITION: 'PARTITION'; PARTITIONS: 'PARTITIONS'; +PASSING: 'PASSING'; PAST: 'PAST'; PATH: 'PATH'; PATTERN: 'PATTERN'; @@ -947,6 +1012,7 @@ PREPARE: 'PREPARE'; PRIVILEGES: 'PRIVILEGES'; PROPERTIES: 'PROPERTIES'; PRUNE: 'PRUNE'; +QUOTES: 'QUOTES'; RANGE: 'RANGE'; READ: 'READ'; RECURSIVE: 'RECURSIVE'; @@ -957,6 +1023,7 @@ REPLACE: 'REPLACE'; RESET: 'RESET'; RESPECT: 'RESPECT'; RESTRICT: 'RESTRICT'; +RETURNING: 'RETURNING'; REVOKE: 'REVOKE'; RIGHT: 'RIGHT'; ROLE: 'ROLE'; @@ -966,6 +1033,7 @@ ROLLUP: 'ROLLUP'; ROW: 'ROW'; ROWS: 'ROWS'; RUNNING: 'RUNNING'; +SCALAR: 'SCALAR'; SCHEMA: 'SCHEMA'; SCHEMAS: 'SCHEMAS'; SECOND: 'SECOND'; @@ -987,6 +1055,7 @@ TABLE: 'TABLE'; TABLES: 'TABLES'; TABLESAMPLE: 'TABLESAMPLE'; TEXT: 'TEXT'; +TEXT_STRING: 'STRING'; THEN: 'THEN'; TIES: 'TIES'; TIME: 'TIME'; @@ -1002,13 +1071,18 @@ TYPE: 'TYPE'; UESCAPE: 'UESCAPE'; UNBOUNDED: 'UNBOUNDED'; UNCOMMITTED: 'UNCOMMITTED'; +UNCONDITIONAL: 'UNCONDITIONAL'; UNION: 'UNION'; +UNKNOWN: 'UNKNOWN'; UNMATCHED: 'UNMATCHED'; UNNEST: 'UNNEST'; UPDATE: 'UPDATE'; USE: 'USE'; USER: 'USER'; USING: 'USING'; +UTF16: 'UTF16'; +UTF32: 'UTF32'; +UTF8: 'UTF8'; VALIDATE: 'VALIDATE'; VALUES: 'VALUES'; VERBOSE: 'VERBOSE'; @@ -1021,6 +1095,7 @@ WITH: 'WITH'; WITHIN: 'WITHIN'; WITHOUT: 'WITHOUT'; WORK: 'WORK'; +WRAPPER: 'WRAPPER'; WRITE: 'WRITE'; YEAR: 'YEAR'; ZONE: 'ZONE'; diff --git a/core/trino-parser/src/main/java/io/trino/sql/ExpressionFormatter.java b/core/trino-parser/src/main/java/io/trino/sql/ExpressionFormatter.java index a9624150b204..701d529d4112 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/ExpressionFormatter.java +++ b/core/trino-parser/src/main/java/io/trino/sql/ExpressionFormatter.java @@ -61,6 +61,11 @@ import io.trino.sql.tree.IntervalLiteral; import io.trino.sql.tree.IsNotNullPredicate; import io.trino.sql.tree.IsNullPredicate; +import io.trino.sql.tree.JsonExists; +import io.trino.sql.tree.JsonPathInvocation; +import io.trino.sql.tree.JsonPathParameter; +import io.trino.sql.tree.JsonQuery; +import io.trino.sql.tree.JsonValue; import io.trino.sql.tree.LabelDereference; import io.trino.sql.tree.LambdaArgumentDeclaration; import io.trino.sql.tree.LambdaExpression; @@ -105,9 +110,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.PrimitiveIterator; import java.util.function.Function; -import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; @@ -482,7 +487,7 @@ protected String visitLogicalExpression(LogicalExpression node, Void context) return "(" + node.getTerms().stream() .map(term -> process(term, context)) - .collect(Collectors.joining(" " + node.getOperator().toString() + " ")) + + .collect(joining(" " + node.getOperator().toString() + " ")) + ")"; } @@ -797,6 +802,97 @@ protected String visitLabelDereference(LabelDereference node, Void context) return "LABEL_DEREFERENCE(" + formatIdentifier(node.getLabel()) + ", " + node.getReference().map(this::process).orElse("*") + ")"; } + @Override + protected String visitJsonExists(JsonExists node, Void context) + { + StringBuilder builder = new StringBuilder(); + + builder.append("JSON_EXISTS(") + .append(formatJsonPathInvocation(node.getJsonPathInvocation())) + .append(" ") + .append(node.getErrorBehavior()) + .append(" ON ERROR") + .append(")"); + + return builder.toString(); + } + + @Override + protected String visitJsonValue(JsonValue node, Void context) + { + StringBuilder builder = new StringBuilder(); + + builder.append("JSON_VALUE(") + .append(formatJsonPathInvocation(node.getJsonPathInvocation())); + + if (node.getReturnedType().isPresent()) { + builder.append(" RETURNING ") + .append(process(node.getReturnedType().get())); + } + + builder.append(" ") + .append(node.getEmptyBehavior()) + .append(node.getEmptyDefault().map(expression -> " " + process(expression)).orElse("")) + .append(" ON EMPTY ") + .append(node.getErrorBehavior()) + .append(node.getErrorDefault().map(expression -> " " + process(expression)).orElse("")) + .append(" ON ERROR") + .append(")"); + + return builder.toString(); + } + + @Override + protected String visitJsonQuery(JsonQuery node, Void context) + { + StringBuilder builder = new StringBuilder(); + + builder.append("JSON_QUERY(") + .append(formatJsonPathInvocation(node.getJsonPathInvocation())); + + if (node.getReturnedType().isPresent()) { + builder.append(" RETURNING ") + .append(process(node.getReturnedType().get())) + .append(node.getOutputFormat().map(string -> " FORMAT " + string).orElse("")); + } + + switch (node.getWrapperBehavior()) { + case WITHOUT: + builder.append(" WITHOUT ARRAY WRAPPER"); + break; + case CONDITIONAL: + builder.append(" WITH CONDITIONAL ARRAY WRAPPER"); + break; + case UNCONDITIONAL: + builder.append((" WITH UNCONDITIONAL ARRAY WRAPPER")); + break; + default: + throw new IllegalStateException("unexpected array wrapper behavior: " + node.getWrapperBehavior()); + } + + if (node.getQuotesBehavior().isPresent()) { + switch (node.getQuotesBehavior().get()) { + case KEEP: + builder.append(" KEEP QUOTES ON SCALAR STRING"); + break; + case OMIT: + builder.append(" OMIT QUOTES ON SCALAR STRING"); + break; + default: + throw new IllegalStateException("unexpected quotes behavior: " + node.getQuotesBehavior()); + } + } + + builder.append(" ") + .append(node.getEmptyBehavior()) + .append(" ON EMPTY ") + .append(node.getErrorBehavior()) + .append(" ON ERROR") + .append(")"); + + return builder.toString(); + } + private String formatBinaryExpression(String operator, Expression left, Expression right) { return '(' + process(left, null) + ' ' + operator + ' ' + process(right, null) + ')'; @@ -1108,4 +1204,32 @@ private static Function sortItemFormatterFunction() return builder.toString(); }; } + + public static String formatJsonPathInvocation(JsonPathInvocation pathInvocation) + { + StringBuilder builder = new StringBuilder(); + + builder.append(formatJsonExpression(pathInvocation.getInputExpression(), Optional.of(pathInvocation.getInputFormat()))) + .append(", ") + .append(formatExpression(pathInvocation.getJsonPath())); + + if (!pathInvocation.getPathParameters().isEmpty()) { + builder.append(" PASSING "); + builder.append(formatJsonPathParameters(pathInvocation.getPathParameters())); + } + + return builder.toString(); + } + + private static String formatJsonExpression(Expression expression, Optional format) + { + return formatExpression(expression) + format.map(jsonFormat -> " FORMAT " + jsonFormat).orElse(""); + } + + private static String formatJsonPathParameters(List parameters) + { + return parameters.stream() + .map(parameter -> formatJsonExpression(parameter.getParameter(), parameter.getFormat()) + " AS " + formatExpression(parameter.getName())) + .collect(joining(", ")); + } } diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathNodeRef.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathNodeRef.java new file mode 100644 index 000000000000..0b56d09d8e42 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathNodeRef.java @@ -0,0 +1,68 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath; + +import io.trino.sql.jsonpath.tree.PathNode; + +import static java.lang.String.format; +import static java.lang.System.identityHashCode; +import static java.util.Objects.requireNonNull; + +public final class PathNodeRef +{ + public static PathNodeRef of(T pathNode) + { + return new PathNodeRef<>(pathNode); + } + + private final T pathNode; + + private PathNodeRef(T pathNode) + { + this.pathNode = requireNonNull(pathNode, "pathNode is null"); + } + + public T getNode() + { + return pathNode; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PathNodeRef other = (PathNodeRef) o; + return pathNode == other.pathNode; + } + + @Override + public int hashCode() + { + return identityHashCode(pathNode); + } + + @Override + public String toString() + { + return format( + "@%s: %s", + Integer.toHexString(identityHashCode(pathNode)), + pathNode); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathParser.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathParser.java new file mode 100644 index 000000000000..2e54556165d6 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathParser.java @@ -0,0 +1,167 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath; + +import io.trino.jsonpath.JsonPathBaseListener; +import io.trino.jsonpath.JsonPathLexer; +import io.trino.jsonpath.JsonPathParser; +import io.trino.sql.jsonpath.tree.PathNode; +import io.trino.sql.parser.ParsingException; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonToken; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.atn.PredictionMode; +import org.antlr.v4.runtime.misc.Pair; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.util.Arrays; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +public final class PathParser +{ + private final BaseErrorListener errorListener; + + public PathParser(Location startLocation) + { + requireNonNull(startLocation, "startLocation is null"); + + int pathStartLine = startLocation.line; + int pathStartColumn = startLocation.column; + this.errorListener = new BaseErrorListener() + { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String message, RecognitionException e) + { + // The line and charPositionInLine correspond to the character within the string literal with JSON path expression. + // Line and offset in error returned to the user should be computed based on the beginning of the whole query text. + // We re-position the exception relatively to the start of the path expression within the query. + int lineInQuery = pathStartLine - 1 + line; + int columnInQuery = line == 1 ? pathStartColumn + 1 + charPositionInLine : charPositionInLine + 1; + throw new ParsingException(message, e, lineInQuery, columnInQuery); + } + }; + } + + public PathNode parseJsonPath(String path) + { + try { + // according to the SQL specification, the path language is case-sensitive in both identifiers and key words + JsonPathLexer lexer = new JsonPathLexer(CharStreams.fromString(path)); + CommonTokenStream tokenStream = new CommonTokenStream(lexer); + JsonPathParser parser = new JsonPathParser(tokenStream); + + parser.addParseListener(new PostProcessor(Arrays.asList(parser.getRuleNames()), parser)); + + lexer.removeErrorListeners(); + lexer.addErrorListener(errorListener); + + parser.removeErrorListeners(); + parser.addErrorListener(errorListener); + + ParserRuleContext tree; + try { + // first, try parsing with potentially faster SLL mode + parser.getInterpreter().setPredictionMode(PredictionMode.SLL); + tree = parser.path(); + } + catch (ParseCancellationException ex) { + // if we fail, parse with LL mode + tokenStream.seek(0); // rewind input stream + parser.reset(); + + parser.getInterpreter().setPredictionMode(PredictionMode.LL); + tree = parser.path(); + } + + return new PathTreeBuilder().visit(tree); + } + catch (StackOverflowError e) { + throw new ParsingException("stack overflow while parsing JSON path"); + } + } + + private static class PostProcessor + extends JsonPathBaseListener + { + private final List ruleNames; + private final JsonPathParser parser; + + public PostProcessor(List ruleNames, JsonPathParser parser) + { + this.ruleNames = ruleNames; + this.parser = parser; + } + + @Override + public void exitNonReserved(JsonPathParser.NonReservedContext context) + { + // only a terminal can be replaced during rule exit event handling. Make sure that the nonReserved item is a token + if (!(context.getChild(0) instanceof TerminalNode)) { + int rule = ((ParserRuleContext) context.getChild(0)).getRuleIndex(); + throw new AssertionError("nonReserved can only contain tokens. Found nested rule: " + ruleNames.get(rule)); + } + + // replace nonReserved keyword with IDENTIFIER token + context.getParent().removeLastChild(); + + Token token = (Token) context.getChild(0).getPayload(); + Token newToken = new CommonToken( + new Pair<>(token.getTokenSource(), token.getInputStream()), + JsonPathLexer.IDENTIFIER, + token.getChannel(), + token.getStartIndex(), + token.getStopIndex()); + + context.getParent().addChild(parser.createTerminalNode(context.getParent(), newToken)); + } + } + + public static class Location + { + private final int line; + private final int column; + + public Location(int line, int column) + { + if (line < 1) { + throw new IllegalArgumentException("line must be at least 1"); + } + + if (column < 0) { + throw new IllegalArgumentException("column must be at least 0"); + } + + this.line = line; + this.column = column; + } + + public int getLine() + { + return line; + } + + public int getColumn() + { + return column; + } + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathTreeBuilder.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathTreeBuilder.java new file mode 100644 index 000000000000..aa8475276fa7 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/PathTreeBuilder.java @@ -0,0 +1,421 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath; + +import com.google.common.collect.ImmutableList; +import io.trino.jsonpath.JsonPathBaseVisitor; +import io.trino.jsonpath.JsonPathParser; +import io.trino.sql.jsonpath.tree.AbsMethod; +import io.trino.sql.jsonpath.tree.ArithmeticBinary; +import io.trino.sql.jsonpath.tree.ArithmeticBinary.Operator; +import io.trino.sql.jsonpath.tree.ArithmeticUnary; +import io.trino.sql.jsonpath.tree.ArithmeticUnary.Sign; +import io.trino.sql.jsonpath.tree.ArrayAccessor; +import io.trino.sql.jsonpath.tree.CeilingMethod; +import io.trino.sql.jsonpath.tree.ComparisonPredicate; +import io.trino.sql.jsonpath.tree.ConjunctionPredicate; +import io.trino.sql.jsonpath.tree.ContextVariable; +import io.trino.sql.jsonpath.tree.DatetimeMethod; +import io.trino.sql.jsonpath.tree.DisjunctionPredicate; +import io.trino.sql.jsonpath.tree.DoubleMethod; +import io.trino.sql.jsonpath.tree.ExistsPredicate; +import io.trino.sql.jsonpath.tree.Filter; +import io.trino.sql.jsonpath.tree.FloorMethod; +import io.trino.sql.jsonpath.tree.IsUnknownPredicate; +import io.trino.sql.jsonpath.tree.JsonPath; +import io.trino.sql.jsonpath.tree.KeyValueMethod; +import io.trino.sql.jsonpath.tree.LastIndexVariable; +import io.trino.sql.jsonpath.tree.LikeRegexPredicate; +import io.trino.sql.jsonpath.tree.MemberAccessor; +import io.trino.sql.jsonpath.tree.NamedVariable; +import io.trino.sql.jsonpath.tree.NegationPredicate; +import io.trino.sql.jsonpath.tree.PathNode; +import io.trino.sql.jsonpath.tree.Predicate; +import io.trino.sql.jsonpath.tree.PredicateCurrentItemVariable; +import io.trino.sql.jsonpath.tree.SizeMethod; +import io.trino.sql.jsonpath.tree.SqlValueLiteral; +import io.trino.sql.jsonpath.tree.StartsWithPredicate; +import io.trino.sql.jsonpath.tree.TypeMethod; +import io.trino.sql.tree.BooleanLiteral; +import io.trino.sql.tree.DecimalLiteral; +import io.trino.sql.tree.DoubleLiteral; +import io.trino.sql.tree.LongLiteral; +import io.trino.sql.tree.StringLiteral; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.util.Optional; + +import static io.trino.sql.jsonpath.tree.JsonNullLiteral.JSON_NULL; + +public class PathTreeBuilder + extends JsonPathBaseVisitor +{ + @Override + public PathNode visitPath(JsonPathParser.PathContext context) + { + boolean lax = context.pathMode().LAX() != null; + PathNode path = visit(context.pathExpression()); + return new JsonPath(lax, path); + } + + @Override + public PathNode visitDecimalLiteral(JsonPathParser.DecimalLiteralContext context) + { + return new SqlValueLiteral(new DecimalLiteral(context.getText())); + } + + @Override + public PathNode visitDoubleLiteral(JsonPathParser.DoubleLiteralContext context) + { + return new SqlValueLiteral(new DoubleLiteral(context.getText())); + } + + @Override + public PathNode visitIntegerLiteral(JsonPathParser.IntegerLiteralContext context) + { + return new SqlValueLiteral(new LongLiteral(context.getText())); + } + + @Override + public PathNode visitStringLiteral(JsonPathParser.StringLiteralContext context) + { + return new SqlValueLiteral(new StringLiteral(unquote(context.STRING().getText()))); + } + + private static String unquote(String quoted) + { + return quoted.substring(1, quoted.length() - 1) + .replace("\"\"", "\""); + } + + @Override + public PathNode visitNullLiteral(JsonPathParser.NullLiteralContext context) + { + return JSON_NULL; + } + + @Override + public PathNode visitBooleanLiteral(JsonPathParser.BooleanLiteralContext context) + { + return new SqlValueLiteral(new BooleanLiteral(context.getText())); + } + + @Override + public PathNode visitContextVariable(JsonPathParser.ContextVariableContext context) + { + return new ContextVariable(); + } + + @Override + public PathNode visitNamedVariable(JsonPathParser.NamedVariableContext context) + { + return namedVariable(context.NAMED_VARIABLE()); + } + + private static NamedVariable namedVariable(TerminalNode namedVariable) + { + // drop leading `$` + return new NamedVariable(namedVariable.getText().substring(1)); + } + + @Override + public PathNode visitLastIndexVariable(JsonPathParser.LastIndexVariableContext context) + { + return new LastIndexVariable(); + } + + @Override + public PathNode visitPredicateCurrentItemVariable(JsonPathParser.PredicateCurrentItemVariableContext context) + { + return new PredicateCurrentItemVariable(); + } + + @Override + public PathNode visitParenthesizedPath(JsonPathParser.ParenthesizedPathContext context) + { + return visit(context.pathExpression()); + } + + @Override + public PathNode visitMemberAccessor(JsonPathParser.MemberAccessorContext context) + { + PathNode base = visit(context.accessorExpression()); + Optional key = Optional.empty(); + if (context.stringLiteral() != null) { + key = Optional.of(unquote(context.stringLiteral().getText())); + } + else if (context.identifier() != null) { + key = Optional.of(context.identifier().getText()); + } + return new MemberAccessor(base, key); + } + + @Override + public PathNode visitWildcardMemberAccessor(JsonPathParser.WildcardMemberAccessorContext context) + { + PathNode base = visit(context.accessorExpression()); + return new MemberAccessor(base, Optional.empty()); + } + + @Override + public PathNode visitArrayAccessor(JsonPathParser.ArrayAccessorContext context) + { + PathNode base = visit(context.accessorExpression()); + ImmutableList.Builder subscripts = ImmutableList.builder(); + for (JsonPathParser.SubscriptContext subscript : context.subscript()) { + if (subscript.singleton != null) { + subscripts.add(new ArrayAccessor.Subscript(visit(subscript.singleton))); + } + else { + subscripts.add(new ArrayAccessor.Subscript(visit(subscript.from), visit(subscript.to))); + } + } + return new ArrayAccessor(base, subscripts.build()); + } + + @Override + public PathNode visitWildcardArrayAccessor(JsonPathParser.WildcardArrayAccessorContext context) + { + PathNode base = visit(context.accessorExpression()); + return new ArrayAccessor(base, ImmutableList.of()); + } + + @Override + public PathNode visitFilter(JsonPathParser.FilterContext context) + { + PathNode base = visit(context.accessorExpression()); + Predicate predicate = (Predicate) visit(context.predicate()); + return new Filter(base, predicate); + } + + @Override + public PathNode visitTypeMethod(JsonPathParser.TypeMethodContext context) + { + PathNode base = visit(context.accessorExpression()); + return new TypeMethod(base); + } + + @Override + public PathNode visitSizeMethod(JsonPathParser.SizeMethodContext context) + { + PathNode base = visit(context.accessorExpression()); + return new SizeMethod(base); + } + + @Override + public PathNode visitDoubleMethod(JsonPathParser.DoubleMethodContext context) + { + PathNode base = visit(context.accessorExpression()); + return new DoubleMethod(base); + } + + @Override + public PathNode visitCeilingMethod(JsonPathParser.CeilingMethodContext context) + { + PathNode base = visit(context.accessorExpression()); + return new CeilingMethod(base); + } + + @Override + public PathNode visitFloorMethod(JsonPathParser.FloorMethodContext context) + { + PathNode base = visit(context.accessorExpression()); + return new FloorMethod(base); + } + + @Override + public PathNode visitAbsMethod(JsonPathParser.AbsMethodContext context) + { + PathNode base = visit(context.accessorExpression()); + return new AbsMethod(base); + } + + @Override + public PathNode visitDatetimeMethod(JsonPathParser.DatetimeMethodContext context) + { + PathNode base = visit(context.accessorExpression()); + Optional format = Optional.empty(); + if (context.stringLiteral() != null) { + format = Optional.of(unquote(context.stringLiteral().getText())); + } + return new DatetimeMethod(base, format); + } + + @Override + public PathNode visitKeyValueMethod(JsonPathParser.KeyValueMethodContext context) + { + PathNode base = visit(context.accessorExpression()); + return new KeyValueMethod(base); + } + + @Override + public PathNode visitSignedUnary(JsonPathParser.SignedUnaryContext context) + { + PathNode base = visit(context.pathExpression()); + return new ArithmeticUnary(getSign(context.sign.getText()), base); + } + + private static Sign getSign(String operator) + { + switch (operator) { + case "+": + return Sign.PLUS; + case "-": + return Sign.MINUS; + default: + throw new UnsupportedOperationException("unexpected unary operator: " + operator); + } + } + + @Override + public PathNode visitBinary(JsonPathParser.BinaryContext context) + { + PathNode left = visit(context.left); + PathNode right = visit(context.right); + return new ArithmeticBinary(getOperator(context.operator.getText()), left, right); + } + + private static Operator getOperator(String operator) + { + switch (operator) { + case "+": + return Operator.ADD; + case "-": + return Operator.SUBTRACT; + case "*": + return Operator.MULTIPLY; + case "/": + return Operator.DIVIDE; + case "%": + return Operator.MODULUS; + default: + throw new UnsupportedOperationException("unexpected binary operator: " + operator); + } + } + + // predicate + + @Override + public PathNode visitComparisonPredicate(JsonPathParser.ComparisonPredicateContext context) + { + PathNode left = visit(context.left); + PathNode right = visit(context.right); + return new ComparisonPredicate(getComparisonOperator(context.comparisonOperator().getText()), left, right); + } + + private static ComparisonPredicate.Operator getComparisonOperator(String operator) + { + switch (operator) { + case "==": + return ComparisonPredicate.Operator.EQUAL; + case "<>": + case "!=": + return ComparisonPredicate.Operator.NOT_EQUAL; + case "<": + return ComparisonPredicate.Operator.LESS_THAN; + case ">": + return ComparisonPredicate.Operator.GREATER_THAN; + case "<=": + return ComparisonPredicate.Operator.LESS_THAN_OR_EQUAL; + case ">=": + return ComparisonPredicate.Operator.GREATER_THAN_OR_EQUAL; + default: + throw new UnsupportedOperationException("unexpected comparison operator: " + operator); + } + } + + @Override + public PathNode visitConjunctionPredicate(JsonPathParser.ConjunctionPredicateContext context) + { + Predicate left = (Predicate) visit(context.left); + Predicate right = (Predicate) visit(context.right); + return new ConjunctionPredicate(left, right); + } + + @Override + public PathNode visitDisjunctionPredicate(JsonPathParser.DisjunctionPredicateContext context) + { + Predicate left = (Predicate) visit(context.left); + Predicate right = (Predicate) visit(context.right); + return new DisjunctionPredicate(left, right); + } + + @Override + public PathNode visitExistsPredicate(JsonPathParser.ExistsPredicateContext context) + { + PathNode path = visit(context.pathExpression()); + return new ExistsPredicate(path); + } + + @Override + public PathNode visitIsUnknownPredicate(JsonPathParser.IsUnknownPredicateContext context) + { + Predicate predicate = (Predicate) visit(context.predicate()); + return new IsUnknownPredicate(predicate); + } + + @Override + public PathNode visitLikeRegexPredicate(JsonPathParser.LikeRegexPredicateContext context) + { + PathNode path = visit(context.base); + String pattern = unquote(context.pattern.getText()); + Optional flag = Optional.empty(); + if (context.flag != null) { + flag = Optional.of(unquote(context.flag.getText())); + } + return new LikeRegexPredicate(path, pattern, flag); + } + + @Override + public PathNode visitNegationPredicate(JsonPathParser.NegationPredicateContext context) + { + Predicate predicate = (Predicate) visit(context.delimitedPredicate()); + return new NegationPredicate(predicate); + } + + @Override + public PathNode visitParenthesizedPredicate(JsonPathParser.ParenthesizedPredicateContext context) + { + return visit(context.predicate()); + } + + @Override + public PathNode visitStartsWithPredicate(JsonPathParser.StartsWithPredicateContext context) + { + PathNode whole = visit(context.whole); + PathNode initial; + if (context.string != null) { + initial = visit(context.string); + } + else { + initial = namedVariable(context.NAMED_VARIABLE()); + } + return new StartsWithPredicate(whole, initial); + } + + @Override + protected PathNode aggregateResult(PathNode aggregate, PathNode nextResult) + { + // do not skip over unrecognized nodes + if (nextResult == null) { + throw new UnsupportedOperationException("not yet implemented"); + } + + if (aggregate == null) { + return nextResult; + } + + throw new UnsupportedOperationException("not yet implemented"); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/AbsMethod.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/AbsMethod.java new file mode 100644 index 000000000000..7ddfb3159690 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/AbsMethod.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class AbsMethod + extends Method +{ + public AbsMethod(PathNode base) + { + super(base); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitAbsMethod(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Accessor.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Accessor.java new file mode 100644 index 000000000000..1c8280269b98 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Accessor.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public abstract class Accessor + extends PathNode +{ + protected final PathNode base; + + Accessor(PathNode base) + { + this.base = requireNonNull(base, "accessor base is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitAccessor(this, context); + } + + public PathNode getBase() + { + return base; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArithmeticBinary.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArithmeticBinary.java new file mode 100644 index 000000000000..4010f0315069 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArithmeticBinary.java @@ -0,0 +1,61 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class ArithmeticBinary + extends PathNode +{ + private final Operator operator; + private final PathNode left; + private final PathNode right; + + public ArithmeticBinary(Operator operator, PathNode left, PathNode right) + { + this.operator = requireNonNull(operator, "operator is null"); + this.left = requireNonNull(left, "left is null"); + this.right = requireNonNull(right, "right is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitArithmeticBinary(this, context); + } + + public Operator getOperator() + { + return operator; + } + + public PathNode getLeft() + { + return left; + } + + public PathNode getRight() + { + return right; + } + + public enum Operator + { + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE, + MODULUS + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArithmeticUnary.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArithmeticUnary.java new file mode 100644 index 000000000000..1bbf0be704a2 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArithmeticUnary.java @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class ArithmeticUnary + extends PathNode +{ + private final Sign sign; + private final PathNode base; + + public ArithmeticUnary(Sign sign, PathNode base) + { + this.sign = requireNonNull(sign, "sign is null"); + this.base = requireNonNull(base, "base is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitArithmeticUnary(this, context); + } + + public Sign getSign() + { + return sign; + } + + public PathNode getBase() + { + return base; + } + + public enum Sign + { + PLUS, + MINUS + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArrayAccessor.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArrayAccessor.java new file mode 100644 index 000000000000..1e9b7b84b958 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ArrayAccessor.java @@ -0,0 +1,70 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import java.util.List; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class ArrayAccessor + extends Accessor +{ + private final List subscripts; + + public ArrayAccessor(PathNode base, List subscripts) + { + super(base); + this.subscripts = requireNonNull(subscripts, "subscripts is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitArrayAccessor(this, context); + } + + public List getSubscripts() + { + return subscripts; + } + + public static class Subscript + { + private final PathNode from; + private final Optional to; + + public Subscript(PathNode from) + { + this.from = requireNonNull(from, "from is null"); + this.to = Optional.empty(); + } + + public Subscript(PathNode from, PathNode to) + { + this.from = requireNonNull(from, "from is null"); + this.to = Optional.of(requireNonNull(to, "to is null")); + } + + public PathNode getFrom() + { + return from; + } + + public Optional getTo() + { + return to; + } + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/CeilingMethod.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/CeilingMethod.java new file mode 100644 index 000000000000..003961a8f158 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/CeilingMethod.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class CeilingMethod + extends Method +{ + public CeilingMethod(PathNode base) + { + super(base); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitCeilingMethod(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ComparisonPredicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ComparisonPredicate.java new file mode 100644 index 000000000000..c8f6a5af3cc4 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ComparisonPredicate.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class ComparisonPredicate + extends Predicate +{ + private final Operator operator; + private final PathNode left; + private final PathNode right; + + public ComparisonPredicate(Operator operator, PathNode left, PathNode right) + { + this.operator = requireNonNull(operator, "operator is null"); + this.left = requireNonNull(left, "left is null"); + this.right = requireNonNull(right, "right is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitComparisonPredicate(this, context); + } + + public Operator getOperator() + { + return operator; + } + + public PathNode getLeft() + { + return left; + } + + public PathNode getRight() + { + return right; + } + + public enum Operator + { + EQUAL, + NOT_EQUAL, + LESS_THAN, + GREATER_THAN, + LESS_THAN_OR_EQUAL, + GREATER_THAN_OR_EQUAL; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ConjunctionPredicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ConjunctionPredicate.java new file mode 100644 index 000000000000..35ba1b2ba2cc --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ConjunctionPredicate.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class ConjunctionPredicate + extends Predicate +{ + private final Predicate left; + private final Predicate right; + + public ConjunctionPredicate(Predicate left, Predicate right) + { + this.left = requireNonNull(left, "left is null"); + this.right = requireNonNull(right, "right is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitConjunctionPredicate(this, context); + } + + public Predicate getLeft() + { + return left; + } + + public Predicate getRight() + { + return right; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ContextVariable.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ContextVariable.java new file mode 100644 index 000000000000..aef058e7c44a --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ContextVariable.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class ContextVariable + extends PathNode +{ + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitContextVariable(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DatetimeMethod.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DatetimeMethod.java new file mode 100644 index 000000000000..becec84756f8 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DatetimeMethod.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class DatetimeMethod + extends Method +{ + private final Optional format; + + public DatetimeMethod(PathNode base, Optional format) + { + super(base); + this.format = requireNonNull(format, "format is null"); // TODO in IR, translate to input for java.time and create a formatter. + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitDatetimeMethod(this, context); + } + + public Optional getFormat() + { + return format; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DisjunctionPredicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DisjunctionPredicate.java new file mode 100644 index 000000000000..859026697750 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DisjunctionPredicate.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class DisjunctionPredicate + extends Predicate +{ + private final Predicate left; + private final Predicate right; + + public DisjunctionPredicate(Predicate left, Predicate right) + { + this.left = requireNonNull(left, "left is null"); + this.right = requireNonNull(right, "right is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitDisjunctionPredicate(this, context); + } + + public Predicate getLeft() + { + return left; + } + + public Predicate getRight() + { + return right; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DoubleMethod.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DoubleMethod.java new file mode 100644 index 000000000000..f15c0fda5b2c --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/DoubleMethod.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class DoubleMethod + extends Method +{ + public DoubleMethod(PathNode base) + { + super(base); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitDoubleMethod(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ExistsPredicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ExistsPredicate.java new file mode 100644 index 000000000000..51788fbdcf8a --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/ExistsPredicate.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class ExistsPredicate + extends Predicate +{ + private final PathNode path; + + public ExistsPredicate(PathNode path) + { + this.path = requireNonNull(path, "path is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitExistsPredicate(this, context); + } + + public PathNode getPath() + { + return path; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Filter.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Filter.java new file mode 100644 index 000000000000..7c1bd9ce4b29 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Filter.java @@ -0,0 +1,39 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class Filter + extends Accessor +{ + private final Predicate predicate; + + public Filter(PathNode base, Predicate predicate) + { + super(base); + this.predicate = requireNonNull(predicate, "predicate is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitFilter(this, context); + } + + public Predicate getPredicate() + { + return predicate; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/FloorMethod.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/FloorMethod.java new file mode 100644 index 000000000000..bdc62e97c5e4 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/FloorMethod.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class FloorMethod + extends Method +{ + public FloorMethod(PathNode base) + { + super(base); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitFloorMethod(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/IsUnknownPredicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/IsUnknownPredicate.java new file mode 100644 index 000000000000..6a26987e3287 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/IsUnknownPredicate.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class IsUnknownPredicate + extends Predicate +{ + private final Predicate predicate; + + public IsUnknownPredicate(Predicate predicate) + { + this.predicate = requireNonNull(predicate, "predicate is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitIsUnknownPredicate(this, context); + } + + public Predicate getPredicate() + { + return predicate; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonNullLiteral.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonNullLiteral.java new file mode 100644 index 000000000000..32e847bd3e2b --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonNullLiteral.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class JsonNullLiteral + extends Literal +{ + public static final JsonNullLiteral JSON_NULL = new JsonNullLiteral(); + + private JsonNullLiteral() {} + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitJsonNullLiteral(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonPath.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonPath.java new file mode 100644 index 000000000000..5b4ebb03e3a6 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonPath.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class JsonPath + extends PathNode +{ + private final boolean lax; + private final PathNode root; + + public JsonPath(boolean lax, PathNode root) + { + this.lax = lax; + this.root = requireNonNull(root, "root is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitJsonPath(this, context); + } + + public boolean isLax() + { + return lax; + } + + public PathNode getRoot() + { + return root; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonPathTreeVisitor.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonPathTreeVisitor.java new file mode 100644 index 000000000000..beabf01e56f1 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/JsonPathTreeVisitor.java @@ -0,0 +1,194 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import javax.annotation.Nullable; + +public abstract class JsonPathTreeVisitor +{ + public R process(PathNode node) + { + return process(node, null); + } + + public R process(PathNode node, @Nullable C context) + { + return node.accept(this, context); + } + + protected R visitPathNode(PathNode node, C context) + { + return null; + } + + protected R visitAbsMethod(AbsMethod node, C context) + { + return visitMethod(node, context); + } + + protected R visitAccessor(Accessor node, C context) + { + return visitPathNode(node, context); + } + + protected R visitArithmeticBinary(ArithmeticBinary node, C context) + { + return visitPathNode(node, context); + } + + protected R visitArithmeticUnary(ArithmeticUnary node, C context) + { + return visitPathNode(node, context); + } + + protected R visitArrayAccessor(ArrayAccessor node, C context) + { + return visitAccessor(node, context); + } + + protected R visitCeilingMethod(CeilingMethod node, C context) + { + return visitMethod(node, context); + } + + protected R visitComparisonPredicate(ComparisonPredicate node, C context) + { + return visitPredicate(node, context); + } + + protected R visitConjunctionPredicate(ConjunctionPredicate node, C context) + { + return visitPredicate(node, context); + } + + protected R visitContextVariable(ContextVariable node, C context) + { + return visitPathNode(node, context); + } + + protected R visitDatetimeMethod(DatetimeMethod node, C context) + { + return visitMethod(node, context); + } + + protected R visitDisjunctionPredicate(DisjunctionPredicate node, C context) + { + return visitPredicate(node, context); + } + + protected R visitDoubleMethod(DoubleMethod node, C context) + { + return visitMethod(node, context); + } + + protected R visitExistsPredicate(ExistsPredicate node, C context) + { + return visitPredicate(node, context); + } + + protected R visitFilter(Filter node, C context) + { + return visitAccessor(node, context); + } + + protected R visitFloorMethod(FloorMethod node, C context) + { + return visitMethod(node, context); + } + + protected R visitIsUnknownPredicate(IsUnknownPredicate node, C context) + { + return visitPredicate(node, context); + } + + protected R visitJsonNullLiteral(JsonNullLiteral node, C context) + { + return visitLiteral(node, context); + } + + protected R visitJsonPath(JsonPath node, C context) + { + return visitPathNode(node, context); + } + + protected R visitKeyValueMethod(KeyValueMethod node, C context) + { + return visitMethod(node, context); + } + + protected R visitLastIndexVariable(LastIndexVariable node, C context) + { + return visitPathNode(node, context); + } + + protected R visitLikeRegexPredicate(LikeRegexPredicate node, C context) + { + return visitPredicate(node, context); + } + + protected R visitLiteral(Literal node, C context) + { + return visitPathNode(node, context); + } + + protected R visitMemberAccessor(MemberAccessor node, C context) + { + return visitAccessor(node, context); + } + + protected R visitMethod(Method node, C context) + { + return visitAccessor(node, context); + } + + protected R visitNamedVariable(NamedVariable node, C context) + { + return visitPathNode(node, context); + } + + protected R visitNegationPredicate(NegationPredicate node, C context) + { + return visitPredicate(node, context); + } + + protected R visitPredicate(Predicate node, C context) + { + return visitPathNode(node, context); + } + + protected R visitPredicateCurrentItemVariable(PredicateCurrentItemVariable node, C context) + { + return visitPathNode(node, context); + } + + protected R visitSizeMethod(SizeMethod node, C context) + { + return visitMethod(node, context); + } + + protected R visitSqlValueLiteral(SqlValueLiteral node, C context) + { + return visitLiteral(node, context); + } + + protected R visitStartsWithPredicate(StartsWithPredicate node, C context) + { + return visitPredicate(node, context); + } + + protected R visitTypeMethod(TypeMethod node, C context) + { + return visitMethod(node, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/KeyValueMethod.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/KeyValueMethod.java new file mode 100644 index 000000000000..a475926ede69 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/KeyValueMethod.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class KeyValueMethod + extends Method +{ + public KeyValueMethod(PathNode base) + { + super(base); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitKeyValueMethod(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/LastIndexVariable.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/LastIndexVariable.java new file mode 100644 index 000000000000..a34dbaf981e8 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/LastIndexVariable.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class LastIndexVariable + extends PathNode +{ + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitLastIndexVariable(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/LikeRegexPredicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/LikeRegexPredicate.java new file mode 100644 index 000000000000..5bbaf117f114 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/LikeRegexPredicate.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class LikeRegexPredicate + extends Predicate +{ + private final PathNode path; + private final String pattern; + private final Optional flag; + + public LikeRegexPredicate(PathNode path, String pattern, Optional flag) + { + this.path = requireNonNull(path, "path is null"); + this.pattern = requireNonNull(pattern, "pattern is null"); + this.flag = requireNonNull(flag, "flag is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitLikeRegexPredicate(this, context); + } + + public PathNode getPath() + { + return path; + } + + public String getPattern() + { + return pattern; + } + + public Optional getFlag() + { + return flag; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Literal.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Literal.java new file mode 100644 index 000000000000..fc76d3b68bce --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Literal.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public abstract class Literal + extends PathNode +{ + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitLiteral(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/MemberAccessor.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/MemberAccessor.java new file mode 100644 index 000000000000..0e3ea555de9a --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/MemberAccessor.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class MemberAccessor + extends Accessor +{ + private final Optional key; + + public MemberAccessor(PathNode base, Optional key) + { + super(base); + this.key = requireNonNull(key, "key is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitMemberAccessor(this, context); + } + + public Optional getKey() + { + return key; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Method.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Method.java new file mode 100644 index 000000000000..a3199e157559 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Method.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public abstract class Method + extends Accessor +{ + Method(PathNode base) + { + super(base); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitMethod(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/NamedVariable.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/NamedVariable.java new file mode 100644 index 000000000000..99a86c9bd5de --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/NamedVariable.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class NamedVariable + extends PathNode +{ + private final String name; + + public NamedVariable(String name) + { + this.name = requireNonNull(name, "name is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitNamedVariable(this, context); + } + + public String getName() + { + return name; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/NegationPredicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/NegationPredicate.java new file mode 100644 index 000000000000..2df191fa6ad9 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/NegationPredicate.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class NegationPredicate + extends Predicate +{ + private final Predicate predicate; + + public NegationPredicate(Predicate predicate) + { + this.predicate = requireNonNull(predicate, "predicate is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitNegationPredicate(this, context); + } + + public Predicate getPredicate() + { + return predicate; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/PathNode.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/PathNode.java new file mode 100644 index 000000000000..6ec60ef0b0c2 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/PathNode.java @@ -0,0 +1,22 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public abstract class PathNode +{ + protected R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitPathNode(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Predicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Predicate.java new file mode 100644 index 000000000000..a35c5e80ecd9 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/Predicate.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public abstract class Predicate + extends PathNode +{ + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitPredicate(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/PredicateCurrentItemVariable.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/PredicateCurrentItemVariable.java new file mode 100644 index 000000000000..9b617f889d1a --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/PredicateCurrentItemVariable.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class PredicateCurrentItemVariable + extends PathNode +{ + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitPredicateCurrentItemVariable(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/SizeMethod.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/SizeMethod.java new file mode 100644 index 000000000000..b0315f24f193 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/SizeMethod.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class SizeMethod + extends Method +{ + public SizeMethod(PathNode base) + { + super(base); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitSizeMethod(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/SqlValueLiteral.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/SqlValueLiteral.java new file mode 100644 index 000000000000..d79feb5195f6 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/SqlValueLiteral.java @@ -0,0 +1,39 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import static java.util.Objects.requireNonNull; + +public class SqlValueLiteral + extends Literal +{ + private final io.trino.sql.tree.Literal value; + + public SqlValueLiteral(io.trino.sql.tree.Literal value) + { + super(); + this.value = requireNonNull(value, "value is null"); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitSqlValueLiteral(this, context); + } + + public io.trino.sql.tree.Literal getValue() + { + return value; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/StartsWithPredicate.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/StartsWithPredicate.java new file mode 100644 index 000000000000..54c46057eaf6 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/StartsWithPredicate.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +import io.trino.sql.tree.StringLiteral; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public class StartsWithPredicate + extends Predicate +{ + private final PathNode whole; + private final PathNode initial; + + public StartsWithPredicate(PathNode whole, PathNode initial) + { + requireNonNull(whole, "whole is null"); + requireNonNull(initial, "initial is null"); + checkArgument(initial instanceof NamedVariable || (initial instanceof SqlValueLiteral && ((SqlValueLiteral) initial).getValue() instanceof StringLiteral), "initial must be a named variable or a string literal"); + + this.whole = whole; + this.initial = initial; + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitStartsWithPredicate(this, context); + } + + public PathNode getWhole() + { + return whole; + } + + public PathNode getInitial() + { + return initial; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/TypeMethod.java b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/TypeMethod.java new file mode 100644 index 000000000000..0c427a8a6a1b --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/jsonpath/tree/TypeMethod.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath.tree; + +public class TypeMethod + extends Method +{ + public TypeMethod(PathNode base) + { + super(base); + } + + @Override + public R accept(JsonPathTreeVisitor visitor, C context) + { + return visitor.visitTypeMethod(this, context); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java b/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java index 01b246c2354c..1e8f08b67020 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java +++ b/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java @@ -113,6 +113,12 @@ import io.trino.sql.tree.JoinCriteria; import io.trino.sql.tree.JoinOn; import io.trino.sql.tree.JoinUsing; +import io.trino.sql.tree.JsonExists; +import io.trino.sql.tree.JsonPathInvocation; +import io.trino.sql.tree.JsonPathParameter; +import io.trino.sql.tree.JsonPathParameter.JsonFormat; +import io.trino.sql.tree.JsonQuery; +import io.trino.sql.tree.JsonValue; import io.trino.sql.tree.LambdaArgumentDeclaration; import io.trino.sql.tree.LambdaExpression; import io.trino.sql.tree.Lateral; @@ -263,6 +269,22 @@ import static io.trino.sql.tree.AnchorPattern.Type.PARTITION_START; import static io.trino.sql.tree.DescriptorArgument.descriptorArgument; import static io.trino.sql.tree.DescriptorArgument.nullDescriptorArgument; +import static io.trino.sql.tree.JsonExists.ErrorBehavior.ERROR; +import static io.trino.sql.tree.JsonExists.ErrorBehavior.FALSE; +import static io.trino.sql.tree.JsonExists.ErrorBehavior.TRUE; +import static io.trino.sql.tree.JsonExists.ErrorBehavior.UNKNOWN; +import static io.trino.sql.tree.JsonPathParameter.JsonFormat.JSON; +import static io.trino.sql.tree.JsonPathParameter.JsonFormat.UTF16; +import static io.trino.sql.tree.JsonPathParameter.JsonFormat.UTF32; +import static io.trino.sql.tree.JsonPathParameter.JsonFormat.UTF8; +import static io.trino.sql.tree.JsonQuery.ArrayWrapperBehavior.CONDITIONAL; +import static io.trino.sql.tree.JsonQuery.ArrayWrapperBehavior.UNCONDITIONAL; +import static io.trino.sql.tree.JsonQuery.ArrayWrapperBehavior.WITHOUT; +import static io.trino.sql.tree.JsonQuery.EmptyOrErrorBehavior.EMPTY_ARRAY; +import static io.trino.sql.tree.JsonQuery.EmptyOrErrorBehavior.EMPTY_OBJECT; +import static io.trino.sql.tree.JsonQuery.QuotesBehavior.KEEP; +import static io.trino.sql.tree.JsonQuery.QuotesBehavior.OMIT; +import static io.trino.sql.tree.JsonValue.EmptyOrErrorBehavior.DEFAULT; import static io.trino.sql.tree.PatternRecognitionRelation.RowsPerMatch.ALL_OMIT_EMPTY; import static io.trino.sql.tree.PatternRecognitionRelation.RowsPerMatch.ALL_SHOW_EMPTY; import static io.trino.sql.tree.PatternRecognitionRelation.RowsPerMatch.ALL_WITH_UNMATCHED; @@ -2247,6 +2269,209 @@ private static Trim.Specification toTrimSpecification(Token token) throw new IllegalArgumentException("Unsupported trim specification: " + token.getText()); } + @Override + public Node visitJsonExists(SqlBaseParser.JsonExistsContext context) + { + JsonPathInvocation jsonPathInvocation = (JsonPathInvocation) visit(context.jsonPathInvocation()); + + SqlBaseParser.JsonExistsErrorBehaviorContext errorBehaviorContext = context.jsonExistsErrorBehavior(); + JsonExists.ErrorBehavior errorBehavior; + if (errorBehaviorContext == null || errorBehaviorContext.FALSE() != null) { + errorBehavior = FALSE; + } + else if (errorBehaviorContext.TRUE() != null) { + errorBehavior = TRUE; + } + else if (errorBehaviorContext.UNKNOWN() != null) { + errorBehavior = UNKNOWN; + } + else if (errorBehaviorContext.ERROR() != null) { + errorBehavior = ERROR; + } + else { + throw parseError("Unexpected error behavior: " + errorBehaviorContext.getText(), errorBehaviorContext); + } + + return new JsonExists( + Optional.of(getLocation(context)), + jsonPathInvocation, + errorBehavior); + } + + @Override + public Node visitJsonValue(SqlBaseParser.JsonValueContext context) + { + JsonPathInvocation jsonPathInvocation = (JsonPathInvocation) visit(context.jsonPathInvocation()); + + Optional returnedType = visitIfPresent(context.type(), DataType.class); + + SqlBaseParser.JsonValueBehaviorContext emptyBehaviorContext = context.emptyBehavior; + JsonValue.EmptyOrErrorBehavior emptyBehavior; + Optional emptyDefault = Optional.empty(); + if (emptyBehaviorContext == null || emptyBehaviorContext.NULL() != null) { + emptyBehavior = JsonValue.EmptyOrErrorBehavior.NULL; + } + else if (emptyBehaviorContext.ERROR() != null) { + emptyBehavior = JsonValue.EmptyOrErrorBehavior.ERROR; + } + else if (emptyBehaviorContext.DEFAULT() != null) { + emptyBehavior = DEFAULT; + emptyDefault = visitIfPresent(emptyBehaviorContext.expression(), Expression.class); + } + else { + throw parseError("Unexpected empty behavior: " + emptyBehaviorContext.getText(), emptyBehaviorContext); + } + + SqlBaseParser.JsonValueBehaviorContext errorBehaviorContext = context.errorBehavior; + JsonValue.EmptyOrErrorBehavior errorBehavior; + Optional errorDefault = Optional.empty(); + if (errorBehaviorContext == null || errorBehaviorContext.NULL() != null) { + errorBehavior = JsonValue.EmptyOrErrorBehavior.NULL; + } + else if (errorBehaviorContext.ERROR() != null) { + errorBehavior = JsonValue.EmptyOrErrorBehavior.ERROR; + } + else if (errorBehaviorContext.DEFAULT() != null) { + errorBehavior = DEFAULT; + errorDefault = visitIfPresent(errorBehaviorContext.expression(), Expression.class); + } + else { + throw parseError("Unexpected error behavior: " + errorBehaviorContext.getText(), errorBehaviorContext); + } + + return new JsonValue( + Optional.of(getLocation(context)), + jsonPathInvocation, + returnedType, + emptyBehavior, + emptyDefault, + errorBehavior, + errorDefault); + } + + @Override + public Node visitJsonQuery(SqlBaseParser.JsonQueryContext context) + { + JsonPathInvocation jsonPathInvocation = (JsonPathInvocation) visit(context.jsonPathInvocation()); + + Optional returnedType = visitIfPresent(context.type(), DataType.class); + + Optional jsonOutputFormat = Optional.empty(); + if (context.FORMAT() != null) { + jsonOutputFormat = Optional.of(getJsonFormat(context.jsonRepresentation())); + } + + SqlBaseParser.JsonQueryWrapperBehaviorContext wrapperBehaviorContext = context.jsonQueryWrapperBehavior(); + JsonQuery.ArrayWrapperBehavior wrapperBehavior; + if (wrapperBehaviorContext == null || wrapperBehaviorContext.WITHOUT() != null) { + wrapperBehavior = WITHOUT; + } + else if (wrapperBehaviorContext.CONDITIONAL() != null) { + wrapperBehavior = CONDITIONAL; + } + else { + wrapperBehavior = UNCONDITIONAL; + } + + Optional quotesBehavior = Optional.empty(); + if (context.KEEP() != null) { + quotesBehavior = Optional.of(KEEP); + } + else if (context.OMIT() != null) { + quotesBehavior = Optional.of(OMIT); + } + + SqlBaseParser.JsonQueryBehaviorContext emptyBehaviorContext = context.emptyBehavior; + JsonQuery.EmptyOrErrorBehavior emptyBehavior; + if (emptyBehaviorContext == null || emptyBehaviorContext.NULL() != null) { + emptyBehavior = JsonQuery.EmptyOrErrorBehavior.NULL; + } + else if (emptyBehaviorContext.ERROR() != null) { + emptyBehavior = JsonQuery.EmptyOrErrorBehavior.ERROR; + } + else if (emptyBehaviorContext.ARRAY() != null) { + emptyBehavior = EMPTY_ARRAY; + } + else if (emptyBehaviorContext.OBJECT() != null) { + emptyBehavior = EMPTY_OBJECT; + } + else { + throw parseError("Unexpected empty behavior: " + emptyBehaviorContext.getText(), emptyBehaviorContext); + } + + SqlBaseParser.JsonQueryBehaviorContext errorBehaviorContext = context.errorBehavior; + JsonQuery.EmptyOrErrorBehavior errorBehavior; + if (errorBehaviorContext == null || errorBehaviorContext.NULL() != null) { + errorBehavior = JsonQuery.EmptyOrErrorBehavior.NULL; + } + else if (errorBehaviorContext.ERROR() != null) { + errorBehavior = JsonQuery.EmptyOrErrorBehavior.ERROR; + } + else if (errorBehaviorContext.ARRAY() != null) { + errorBehavior = EMPTY_ARRAY; + } + else if (errorBehaviorContext.OBJECT() != null) { + errorBehavior = EMPTY_OBJECT; + } + else { + throw parseError("Unexpected error behavior: " + errorBehaviorContext.getText(), errorBehaviorContext); + } + + return new JsonQuery( + Optional.of(getLocation(context)), + jsonPathInvocation, + returnedType, + jsonOutputFormat, + wrapperBehavior, + quotesBehavior, + emptyBehavior, + errorBehavior); + } + + @Override + public Node visitJsonPathInvocation(SqlBaseParser.JsonPathInvocationContext context) + { + Expression jsonInput = (Expression) visit(context.jsonValueExpression().expression()); + + JsonFormat inputFormat; + if (context.jsonValueExpression().FORMAT() == null) { + inputFormat = JSON; // default + } + else { + inputFormat = getJsonFormat(context.jsonValueExpression().jsonRepresentation()); + } + + StringLiteral jsonPath = (StringLiteral) visit(context.path); + List pathParameters = visit(context.jsonArgument(), JsonPathParameter.class); + + return new JsonPathInvocation(Optional.of(getLocation(context)), jsonInput, inputFormat, jsonPath, pathParameters); + } + + private JsonFormat getJsonFormat(SqlBaseParser.JsonRepresentationContext context) + { + if (context.UTF8() != null) { + return UTF8; + } + if (context.UTF16() != null) { + return UTF16; + } + if (context.UTF32() != null) { + return UTF32; + } + return JSON; + } + + @Override + public Node visitJsonArgument(SqlBaseParser.JsonArgumentContext context) + { + return new JsonPathParameter( + Optional.of(getLocation(context)), + (Identifier) visit(context.identifier()), + (Expression) visit(context.jsonValueExpression().expression()), + Optional.ofNullable(context.jsonValueExpression().jsonRepresentation()) + .map(this::getJsonFormat)); + } + @Override public Node visitSubstring(SqlBaseParser.SubstringContext context) { diff --git a/core/trino-parser/src/main/java/io/trino/sql/parser/ParsingException.java b/core/trino-parser/src/main/java/io/trino/sql/parser/ParsingException.java index 3882d54f09ce..443123ec4882 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/parser/ParsingException.java +++ b/core/trino-parser/src/main/java/io/trino/sql/parser/ParsingException.java @@ -40,6 +40,11 @@ public ParsingException(String message) this(message, null, 1, 1); } + public ParsingException(String message, RecognitionException cause) + { + this(message, cause, 1, 1); + } + public ParsingException(String message, NodeLocation nodeLocation) { this(message, null, nodeLocation.getLineNumber(), nodeLocation.getColumnNumber()); diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/AstVisitor.java b/core/trino-parser/src/main/java/io/trino/sql/tree/AstVisitor.java index 38da35912c40..a7506a6a7c7f 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/tree/AstVisitor.java +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/AstVisitor.java @@ -1116,4 +1116,24 @@ protected R visitDescriptorField(DescriptorField node, C context) { return visitNode(node, context); } + + protected R visitJsonExists(JsonExists node, C context) + { + return visitExpression(node, context); + } + + protected R visitJsonValue(JsonValue node, C context) + { + return visitExpression(node, context); + } + + protected R visitJsonQuery(JsonQuery node, C context) + { + return visitExpression(node, context); + } + + protected R visitJsonPathInvocation(JsonPathInvocation node, C context) + { + return visitNode(node, context); + } } diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/DefaultTraversalVisitor.java b/core/trino-parser/src/main/java/io/trino/sql/tree/DefaultTraversalVisitor.java index a4c89adff8ee..034d6054c428 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/tree/DefaultTraversalVisitor.java +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/DefaultTraversalVisitor.java @@ -920,4 +920,41 @@ protected Void visitLabelDereference(LabelDereference node, C context) return null; } + + @Override + protected Void visitJsonExists(JsonExists node, C context) + { + process(node.getJsonPathInvocation(), context); + + return null; + } + + @Override + protected Void visitJsonValue(JsonValue node, C context) + { + process(node.getJsonPathInvocation(), context); + node.getEmptyDefault().ifPresent(expression -> process(expression, context)); + node.getErrorDefault().ifPresent(expression -> process(expression, context)); + + return null; + } + + @Override + protected Void visitJsonQuery(JsonQuery node, C context) + { + process(node.getJsonPathInvocation(), context); + + return null; + } + + @Override + protected Void visitJsonPathInvocation(JsonPathInvocation node, C context) + { + process(node.getInputExpression(), context); + for (JsonPathParameter parameter : node.getPathParameters()) { + process(parameter.getParameter(), context); + } + + return null; + } } diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/ExpressionRewriter.java b/core/trino-parser/src/main/java/io/trino/sql/tree/ExpressionRewriter.java index 6864375b2b0a..39c692d6113d 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/tree/ExpressionRewriter.java +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/ExpressionRewriter.java @@ -269,4 +269,19 @@ public Expression rewriteLabelDereference(LabelDereference node, C context, Expr { return rewriteExpression(node, context, treeRewriter); } + + public Expression rewriteJsonExists(JsonExists node, C context, ExpressionTreeRewriter treeRewriter) + { + return rewriteExpression(node, context, treeRewriter); + } + + public Expression rewriteJsonValue(JsonValue node, C context, ExpressionTreeRewriter treeRewriter) + { + return rewriteExpression(node, context, treeRewriter); + } + + public Expression rewriteJsonQuery(JsonQuery node, C context, ExpressionTreeRewriter treeRewriter) + { + return rewriteExpression(node, context, treeRewriter); + } } diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/ExpressionTreeRewriter.java b/core/trino-parser/src/main/java/io/trino/sql/tree/ExpressionTreeRewriter.java index b5bd2051042a..846aac09ca11 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/tree/ExpressionTreeRewriter.java +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/ExpressionTreeRewriter.java @@ -1161,6 +1161,104 @@ protected Expression visitLabelDereference(LabelDereference node, Context con return node; } + + @Override + protected Expression visitJsonExists(JsonExists node, Context context) + { + if (!context.isDefaultRewrite()) { + Expression result = rewriter.rewriteJsonExists(node, context.get(), ExpressionTreeRewriter.this); + if (result != null) { + return result; + } + } + + JsonPathInvocation jsonPathInvocation = rewriteJsonPathInvocation(node.getJsonPathInvocation(), context); + + if (node.getJsonPathInvocation() != jsonPathInvocation) { + return new JsonExists(node.getLocation(), jsonPathInvocation, node.getErrorBehavior()); + } + + return node; + } + + @Override + protected Expression visitJsonValue(JsonValue node, Context context) + { + if (!context.isDefaultRewrite()) { + Expression result = rewriter.rewriteJsonValue(node, context.get(), ExpressionTreeRewriter.this); + if (result != null) { + return result; + } + } + + JsonPathInvocation jsonPathInvocation = rewriteJsonPathInvocation(node.getJsonPathInvocation(), context); + + Optional emptyDefault = node.getEmptyDefault().map(expression -> rewrite(expression, context.get())); + Optional errorDefault = node.getErrorDefault().map(expression -> rewrite(expression, context.get())); + + if (node.getJsonPathInvocation() != jsonPathInvocation || + !sameElements(node.getEmptyDefault(), emptyDefault) || + !sameElements(node.getErrorDefault(), errorDefault)) { + return new JsonValue( + node.getLocation(), + jsonPathInvocation, + node.getReturnedType(), + node.getEmptyBehavior(), + emptyDefault, + node.getErrorBehavior(), + errorDefault); + } + + return node; + } + + @Override + protected Expression visitJsonQuery(JsonQuery node, Context context) + { + if (!context.isDefaultRewrite()) { + Expression result = rewriter.rewriteJsonQuery(node, context.get(), ExpressionTreeRewriter.this); + if (result != null) { + return result; + } + } + + JsonPathInvocation jsonPathInvocation = rewriteJsonPathInvocation(node.getJsonPathInvocation(), context); + + if (node.getJsonPathInvocation() != jsonPathInvocation) { + return new JsonQuery( + node.getLocation(), + jsonPathInvocation, + node.getReturnedType(), + node.getOutputFormat(), + node.getWrapperBehavior(), + node.getQuotesBehavior(), + node.getEmptyBehavior(), + node.getErrorBehavior()); + } + + return node; + } + + private JsonPathInvocation rewriteJsonPathInvocation(JsonPathInvocation pathInvocation, Context context) + { + Expression inputExpression = rewrite(pathInvocation.getInputExpression(), context.get()); + + List pathParameters = pathInvocation.getPathParameters().stream() + .map(pathParameter -> { + Expression expression = rewrite(pathParameter.getParameter(), context.get()); + if (pathParameter.getParameter() != expression) { + return new JsonPathParameter(pathParameter.getLocation(), pathParameter.getName(), expression, pathParameter.getFormat()); + } + return pathParameter; + }) + .collect(toImmutableList()); + + if (pathInvocation.getInputExpression() != inputExpression || !sameElements(pathInvocation.getPathParameters(), pathParameters)) { + return new JsonPathInvocation(pathInvocation.getLocation(), inputExpression, pathInvocation.getInputFormat(), pathInvocation.getJsonPath(), pathParameters); + } + + return pathInvocation; + } } public static class Context diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/JsonExists.java b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonExists.java new file mode 100644 index 000000000000..ed6a2e889666 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonExists.java @@ -0,0 +1,103 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.tree; + +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class JsonExists + extends Expression +{ + private final JsonPathInvocation jsonPathInvocation; + private final ErrorBehavior errorBehavior; + + public JsonExists( + Optional location, + JsonPathInvocation jsonPathInvocation, + ErrorBehavior errorBehavior) + { + super(location); + requireNonNull(jsonPathInvocation, "jsonPathInvocation is null"); + requireNonNull(errorBehavior, "errorBehavior is null"); + + this.jsonPathInvocation = jsonPathInvocation; + this.errorBehavior = errorBehavior; + } + + public enum ErrorBehavior + { + FALSE, // default + TRUE, + UNKNOWN, + ERROR + } + + public JsonPathInvocation getJsonPathInvocation() + { + return jsonPathInvocation; + } + + public ErrorBehavior getErrorBehavior() + { + return errorBehavior; + } + + @Override + public R accept(AstVisitor visitor, C context) + { + return visitor.visitJsonExists(this, context); + } + + @Override + public List getChildren() + { + return ImmutableList.of(jsonPathInvocation); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + JsonExists that = (JsonExists) o; + return Objects.equals(jsonPathInvocation, that.jsonPathInvocation) && + errorBehavior == that.errorBehavior; + } + + @Override + public int hashCode() + { + return Objects.hash(jsonPathInvocation, errorBehavior); + } + + @Override + public boolean shallowEquals(Node other) + { + if (!sameClass(this, other)) { + return false; + } + + return errorBehavior == ((JsonExists) other).errorBehavior; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/JsonPathInvocation.java b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonPathInvocation.java new file mode 100644 index 000000000000..40afd539d063 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonPathInvocation.java @@ -0,0 +1,128 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.tree; + +import com.google.common.collect.ImmutableList; +import io.trino.sql.tree.JsonPathParameter.JsonFormat; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static io.trino.sql.ExpressionFormatter.formatJsonPathInvocation; +import static java.util.Objects.requireNonNull; + +public class JsonPathInvocation + extends Node +{ + private final Expression inputExpression; + private final JsonFormat inputFormat; + private final StringLiteral jsonPath; + private final List pathParameters; + + public JsonPathInvocation( + Optional location, + Expression inputExpression, + JsonFormat inputFormat, + StringLiteral jsonPath, + List pathParameters) + { + super(location); + requireNonNull(inputExpression, "inputExpression is null"); + requireNonNull(inputFormat, "inputFormat is null"); + requireNonNull(jsonPath, "jsonPath is null"); + requireNonNull(pathParameters, "pathParameters is null"); + + this.inputExpression = inputExpression; + this.inputFormat = inputFormat; + this.jsonPath = jsonPath; + this.pathParameters = ImmutableList.copyOf(pathParameters); + } + + public Expression getInputExpression() + { + return inputExpression; + } + + public JsonFormat getInputFormat() + { + return inputFormat; + } + + public StringLiteral getJsonPath() + { + return jsonPath; + } + + public List getPathParameters() + { + return pathParameters; + } + + @Override + public R accept(AstVisitor visitor, C context) + { + return visitor.visitJsonPathInvocation(this, context); + } + + @Override + public List getChildren() + { + ImmutableList.Builder children = ImmutableList.builder(); + children.add(inputExpression); + children.add(jsonPath); + pathParameters.stream() + .forEach(children::add); + return children.build(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + JsonPathInvocation that = (JsonPathInvocation) o; + return Objects.equals(inputExpression, that.inputExpression) && + inputFormat == that.inputFormat && + Objects.equals(jsonPath, that.jsonPath) && + Objects.equals(pathParameters, that.pathParameters); + } + + @Override + public int hashCode() + { + return Objects.hash(inputExpression, inputFormat, jsonPath, pathParameters); + } + + @Override + public boolean shallowEquals(Node other) + { + if (!sameClass(this, other)) { + return false; + } + + return inputFormat == ((JsonPathInvocation) other).inputFormat; + } + + @Override + public String toString() + { + return formatJsonPathInvocation(this); + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/JsonPathParameter.java b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonPathParameter.java new file mode 100644 index 000000000000..44042a414324 --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonPathParameter.java @@ -0,0 +1,130 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.tree; + +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static java.util.Objects.requireNonNull; + +public class JsonPathParameter + extends Node +{ + private final Identifier name; + private final Expression parameter; + private final Optional format; + + public JsonPathParameter(Optional location, Identifier name, Expression parameter, Optional format) + { + super(location); + + requireNonNull(name, "name is null"); + requireNonNull(parameter, "parameter is null"); + requireNonNull(format, "format is null"); + + this.name = name; + this.parameter = parameter; + this.format = format; + } + + public Identifier getName() + { + return name; + } + + public Expression getParameter() + { + return parameter; + } + + public Optional getFormat() + { + return format; + } + + @Override + public List getChildren() + { + return ImmutableList.of(parameter); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + JsonPathParameter that = (JsonPathParameter) o; + return Objects.equals(name, that.name) && + Objects.equals(parameter, that.parameter) && + Objects.equals(format, that.format); + } + + @Override + public int hashCode() + { + return Objects.hash(name, parameter, format); + } + + @Override + public String toString() + { + return toStringHelper(this) + .add("name", name) + .add("parameter", parameter) + .add("format", format) + .omitNullValues() + .toString(); + } + + @Override + public boolean shallowEquals(Node other) + { + if (!sameClass(this, other)) { + return false; + } + + JsonPathParameter otherRelation = (JsonPathParameter) other; + return name.equals(otherRelation.name) && format.equals(otherRelation.format); + } + + public enum JsonFormat + { + JSON("JSON"), + UTF8("JSON ENCODING UTF8"), + UTF16("JSON ENCODING UTF16"), + UTF32("JSON ENCODING UTF32"); + + private final String name; + + JsonFormat(String name) + { + this.name = name; + } + + @Override + public String toString() + { + return name; + } + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/JsonQuery.java b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonQuery.java new file mode 100644 index 000000000000..358acc0c535e --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonQuery.java @@ -0,0 +1,187 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.tree; + +import com.google.common.collect.ImmutableList; +import io.trino.sql.tree.JsonPathParameter.JsonFormat; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class JsonQuery + extends Expression +{ + private final JsonPathInvocation jsonPathInvocation; + private final Optional returnedType; + private final Optional outputFormat; + private final ArrayWrapperBehavior wrapperBehavior; + private final Optional quotesBehavior; + private final EmptyOrErrorBehavior emptyBehavior; + private final EmptyOrErrorBehavior errorBehavior; + + public JsonQuery( + Optional location, + JsonPathInvocation jsonPathInvocation, + Optional returnedType, + Optional outputFormat, + ArrayWrapperBehavior wrapperBehavior, + Optional quotesBehavior, + EmptyOrErrorBehavior emptyBehavior, + EmptyOrErrorBehavior errorBehavior) + { + super(location); + requireNonNull(jsonPathInvocation, "jsonPathInvocation is null"); + requireNonNull(returnedType, "returnedType is null"); + requireNonNull(outputFormat, "outputFormat is null"); + requireNonNull(wrapperBehavior, "wrapperBehavior is null"); + requireNonNull(quotesBehavior, "quotesBehavior is null"); + requireNonNull(emptyBehavior, "emptyBehavior is null"); + requireNonNull(errorBehavior, "errorBehavior is null"); + + this.jsonPathInvocation = jsonPathInvocation; + this.returnedType = returnedType; + this.outputFormat = outputFormat; + this.wrapperBehavior = wrapperBehavior; + this.quotesBehavior = quotesBehavior; + this.emptyBehavior = emptyBehavior; + this.errorBehavior = errorBehavior; + } + + public enum EmptyOrErrorBehavior + { + NULL("NULL"), // default behavior for ON ERROR and ON EMPTY conditions + ERROR("ERROR"), + EMPTY_ARRAY("EMPTY ARRAY"), + EMPTY_OBJECT("EMPTY OBJECT"); + + private final String label; + + EmptyOrErrorBehavior(String label) + { + this.label = label; + } + + @Override + public String toString() + { + return label; + } + } + + public enum ArrayWrapperBehavior + { + WITHOUT, // default + CONDITIONAL, + UNCONDITIONAL + } + + public enum QuotesBehavior + { + KEEP, // default + OMIT + } + + public JsonPathInvocation getJsonPathInvocation() + { + return jsonPathInvocation; + } + + public Optional getReturnedType() + { + return returnedType; + } + + public Optional getOutputFormat() + { + return outputFormat; + } + + public ArrayWrapperBehavior getWrapperBehavior() + { + return wrapperBehavior; + } + + public Optional getQuotesBehavior() + { + return quotesBehavior; + } + + public EmptyOrErrorBehavior getEmptyBehavior() + { + return emptyBehavior; + } + + public EmptyOrErrorBehavior getErrorBehavior() + { + return errorBehavior; + } + + @Override + public R accept(AstVisitor visitor, C context) + { + return visitor.visitJsonQuery(this, context); + } + + @Override + public List getChildren() + { + return ImmutableList.of(jsonPathInvocation); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + JsonQuery that = (JsonQuery) o; + return Objects.equals(jsonPathInvocation, that.jsonPathInvocation) && + Objects.equals(returnedType, that.returnedType) && + Objects.equals(outputFormat, that.outputFormat) && + wrapperBehavior == that.wrapperBehavior && + Objects.equals(quotesBehavior, that.quotesBehavior) && + emptyBehavior == that.emptyBehavior && + errorBehavior == that.errorBehavior; + } + + @Override + public int hashCode() + { + return Objects.hash(jsonPathInvocation, returnedType, outputFormat, wrapperBehavior, quotesBehavior, emptyBehavior, errorBehavior); + } + + @Override + public boolean shallowEquals(Node other) + { + if (!sameClass(this, other)) { + return false; + } + + JsonQuery otherJsonQuery = (JsonQuery) other; + + return returnedType.equals(otherJsonQuery.returnedType) && + outputFormat.equals(otherJsonQuery.outputFormat) && + wrapperBehavior == otherJsonQuery.wrapperBehavior && + Objects.equals(quotesBehavior, otherJsonQuery.quotesBehavior) && + emptyBehavior == otherJsonQuery.emptyBehavior && + errorBehavior == otherJsonQuery.errorBehavior; + } +} diff --git a/core/trino-parser/src/main/java/io/trino/sql/tree/JsonValue.java b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonValue.java new file mode 100644 index 000000000000..df147004cb6e --- /dev/null +++ b/core/trino-parser/src/main/java/io/trino/sql/tree/JsonValue.java @@ -0,0 +1,156 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.tree; + +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkArgument; +import static io.trino.sql.tree.JsonValue.EmptyOrErrorBehavior.DEFAULT; +import static java.util.Objects.requireNonNull; + +public class JsonValue + extends Expression +{ + private final JsonPathInvocation jsonPathInvocation; + private final Optional returnedType; + private final EmptyOrErrorBehavior emptyBehavior; + private final Optional emptyDefault; + private final EmptyOrErrorBehavior errorBehavior; + private final Optional errorDefault; + + public JsonValue( + Optional location, + JsonPathInvocation jsonPathInvocation, + Optional returnedType, + EmptyOrErrorBehavior emptyBehavior, + Optional emptyDefault, + EmptyOrErrorBehavior errorBehavior, + Optional errorDefault) + { + super(location); + requireNonNull(jsonPathInvocation, "jsonPathInvocation is null"); + requireNonNull(returnedType, "returnedType is null"); + requireNonNull(emptyBehavior, "emptyBehavior is null"); + requireNonNull(emptyDefault, "emptyDefault is null"); + checkArgument(emptyBehavior == DEFAULT || !emptyDefault.isPresent(), "default value can be specified only for DEFAULT ... ON EMPTY option"); + checkArgument(emptyBehavior != DEFAULT || emptyDefault.isPresent(), "DEFAULT ... ON EMPTY option requires default value"); + requireNonNull(errorBehavior, "errorBehavior is null"); + requireNonNull(errorDefault, "errorDefault is null"); + checkArgument(errorBehavior == DEFAULT || !errorDefault.isPresent(), "default value can be specified only for DEFAULT ... ON ERROR option"); + checkArgument(errorBehavior != DEFAULT || errorDefault.isPresent(), "DEFAULT ... ON ERROR option requires default value"); + + this.jsonPathInvocation = jsonPathInvocation; + this.returnedType = returnedType; + this.emptyBehavior = emptyBehavior; + this.emptyDefault = emptyDefault; + this.errorBehavior = errorBehavior; + this.errorDefault = errorDefault; + } + + public enum EmptyOrErrorBehavior + { + NULL, // default + ERROR, + DEFAULT + } + + public JsonPathInvocation getJsonPathInvocation() + { + return jsonPathInvocation; + } + + public Optional getReturnedType() + { + return returnedType; + } + + public EmptyOrErrorBehavior getEmptyBehavior() + { + return emptyBehavior; + } + + public Optional getEmptyDefault() + { + return emptyDefault; + } + + public EmptyOrErrorBehavior getErrorBehavior() + { + return errorBehavior; + } + + public Optional getErrorDefault() + { + return errorDefault; + } + + @Override + public R accept(AstVisitor visitor, C context) + { + return visitor.visitJsonValue(this, context); + } + + @Override + public List getChildren() + { + ImmutableList.Builder children = ImmutableList.builder(); + children.add(jsonPathInvocation); + emptyDefault.ifPresent(children::add); + errorDefault.ifPresent(children::add); + return children.build(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + JsonValue that = (JsonValue) o; + return Objects.equals(jsonPathInvocation, that.jsonPathInvocation) && + Objects.equals(returnedType, that.returnedType) && + emptyBehavior == that.emptyBehavior && + Objects.equals(emptyDefault, that.emptyDefault) && + errorBehavior == that.errorBehavior && + Objects.equals(errorDefault, that.errorDefault); + } + + @Override + public int hashCode() + { + return Objects.hash(jsonPathInvocation, returnedType, emptyBehavior, emptyDefault, errorBehavior, errorDefault); + } + + @Override + public boolean shallowEquals(Node other) + { + if (!sameClass(this, other)) { + return false; + } + + JsonValue otherJsonValue = (JsonValue) other; + + return returnedType.equals(otherJsonValue.returnedType) && + emptyBehavior == otherJsonValue.emptyBehavior && + errorBehavior == otherJsonValue.errorBehavior; + } +} diff --git a/core/trino-parser/src/test/java/io/trino/sql/jsonpath/TestPathParser.java b/core/trino-parser/src/test/java/io/trino/sql/jsonpath/TestPathParser.java new file mode 100644 index 000000000000..c4ab51219743 --- /dev/null +++ b/core/trino-parser/src/test/java/io/trino/sql/jsonpath/TestPathParser.java @@ -0,0 +1,482 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.sql.jsonpath; + +import com.google.common.collect.ImmutableList; +import io.trino.sql.jsonpath.PathParser.Location; +import io.trino.sql.jsonpath.tree.AbsMethod; +import io.trino.sql.jsonpath.tree.ArithmeticBinary; +import io.trino.sql.jsonpath.tree.ArithmeticUnary; +import io.trino.sql.jsonpath.tree.ArrayAccessor; +import io.trino.sql.jsonpath.tree.ArrayAccessor.Subscript; +import io.trino.sql.jsonpath.tree.CeilingMethod; +import io.trino.sql.jsonpath.tree.ComparisonPredicate; +import io.trino.sql.jsonpath.tree.ConjunctionPredicate; +import io.trino.sql.jsonpath.tree.ContextVariable; +import io.trino.sql.jsonpath.tree.DatetimeMethod; +import io.trino.sql.jsonpath.tree.DisjunctionPredicate; +import io.trino.sql.jsonpath.tree.DoubleMethod; +import io.trino.sql.jsonpath.tree.ExistsPredicate; +import io.trino.sql.jsonpath.tree.Filter; +import io.trino.sql.jsonpath.tree.FloorMethod; +import io.trino.sql.jsonpath.tree.IsUnknownPredicate; +import io.trino.sql.jsonpath.tree.JsonPath; +import io.trino.sql.jsonpath.tree.KeyValueMethod; +import io.trino.sql.jsonpath.tree.LastIndexVariable; +import io.trino.sql.jsonpath.tree.LikeRegexPredicate; +import io.trino.sql.jsonpath.tree.MemberAccessor; +import io.trino.sql.jsonpath.tree.NamedVariable; +import io.trino.sql.jsonpath.tree.NegationPredicate; +import io.trino.sql.jsonpath.tree.PredicateCurrentItemVariable; +import io.trino.sql.jsonpath.tree.SizeMethod; +import io.trino.sql.jsonpath.tree.SqlValueLiteral; +import io.trino.sql.jsonpath.tree.StartsWithPredicate; +import io.trino.sql.jsonpath.tree.TypeMethod; +import io.trino.sql.tree.BooleanLiteral; +import io.trino.sql.tree.DecimalLiteral; +import io.trino.sql.tree.DoubleLiteral; +import io.trino.sql.tree.LongLiteral; +import io.trino.sql.tree.StringLiteral; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.RecursiveComparisonAssert; +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static io.trino.sql.jsonpath.tree.ArithmeticBinary.Operator.ADD; +import static io.trino.sql.jsonpath.tree.ArithmeticBinary.Operator.DIVIDE; +import static io.trino.sql.jsonpath.tree.ArithmeticBinary.Operator.MODULUS; +import static io.trino.sql.jsonpath.tree.ArithmeticBinary.Operator.MULTIPLY; +import static io.trino.sql.jsonpath.tree.ArithmeticBinary.Operator.SUBTRACT; +import static io.trino.sql.jsonpath.tree.ArithmeticUnary.Sign.MINUS; +import static io.trino.sql.jsonpath.tree.ArithmeticUnary.Sign.PLUS; +import static io.trino.sql.jsonpath.tree.ComparisonPredicate.Operator.EQUAL; +import static io.trino.sql.jsonpath.tree.ComparisonPredicate.Operator.GREATER_THAN; +import static io.trino.sql.jsonpath.tree.ComparisonPredicate.Operator.GREATER_THAN_OR_EQUAL; +import static io.trino.sql.jsonpath.tree.ComparisonPredicate.Operator.LESS_THAN; +import static io.trino.sql.jsonpath.tree.ComparisonPredicate.Operator.LESS_THAN_OR_EQUAL; +import static io.trino.sql.jsonpath.tree.ComparisonPredicate.Operator.NOT_EQUAL; +import static io.trino.sql.jsonpath.tree.JsonNullLiteral.JSON_NULL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestPathParser +{ + private static final PathParser PATH_PARSER = new PathParser(new Location(1, 0)); + private static final RecursiveComparisonConfiguration COMPARISON_CONFIGURATION = RecursiveComparisonConfiguration.builder().withStrictTypeChecking(true).build(); + + @Test + public void testPathMode() + { + assertThat(path("lax null")) + .isEqualTo(new JsonPath(true, JSON_NULL)); + + assertThat(path("strict null")) + .isEqualTo(new JsonPath(false, JSON_NULL)); + } + + @Test + public void testNumericLiteral() + { + assertThat(path("lax 1")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new LongLiteral("1")))); + + assertThat(path("lax -2")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new LongLiteral("-2")))); + + assertThat(path("lax 1.2e3")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new DoubleLiteral("1.2e3")))); + + assertThat(path("lax -1.2e3")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new DoubleLiteral("-1.2e3")))); + + assertThat(path("lax 1.0")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new DecimalLiteral("1.0")))); + + assertThat(path("lax -.5")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new DecimalLiteral("-.5")))); + } + + @Test + public void testStringLiteral() + { + assertThat(path("lax \"aBcD\"")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new StringLiteral("aBcD")))); + + assertThat(path("lax \"x x\"")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new StringLiteral("x x")))); + + assertThat(path("lax \"x\"\"x\"")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new StringLiteral("x\"x")))); + } + + @Test + public void testBooleanLiteral() + { + assertThat(path("lax true")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new BooleanLiteral("true")))); + + assertThat(path("lax false")) + .isEqualTo(new JsonPath(true, new SqlValueLiteral(new BooleanLiteral("false")))); + } + + @Test + public void testVariable() + { + assertThat(path("lax $")) + .isEqualTo(new JsonPath(true, new ContextVariable())); + + assertThat(path("lax $Some_Name")) + .isEqualTo(new JsonPath(true, new NamedVariable("Some_Name"))); + + assertThat(path("lax last")) + .isEqualTo(new JsonPath(true, new LastIndexVariable())); + } + + @Test + public void testMethod() + { + assertThat(path("lax $.abs()")) + .isEqualTo(new JsonPath(true, new AbsMethod(new ContextVariable()))); + + assertThat(path("lax $.ceiling()")) + .isEqualTo(new JsonPath(true, new CeilingMethod(new ContextVariable()))); + + assertThat(path("lax $.datetime()")) + .isEqualTo(new JsonPath(true, new DatetimeMethod(new ContextVariable(), Optional.empty()))); + + assertThat(path("lax $.datetime(\"some datetime template\")")) + .isEqualTo(new JsonPath(true, new DatetimeMethod(new ContextVariable(), Optional.of("some datetime template")))); + + assertThat(path("lax $.double()")) + .isEqualTo(new JsonPath(true, new DoubleMethod(new ContextVariable()))); + + assertThat(path("lax $.floor()")) + .isEqualTo(new JsonPath(true, new FloorMethod(new ContextVariable()))); + + assertThat(path("lax $.keyvalue()")) + .isEqualTo(new JsonPath(true, new KeyValueMethod(new ContextVariable()))); + + assertThat(path("lax $.size()")) + .isEqualTo(new JsonPath(true, new SizeMethod(new ContextVariable()))); + + assertThat(path("lax $.type()")) + .isEqualTo(new JsonPath(true, new TypeMethod(new ContextVariable()))); + } + + @Test + public void testArithmeticBinary() + { + assertThat(path("lax $ + 2")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary(ADD, new ContextVariable(), new SqlValueLiteral(new LongLiteral("2"))))); + + assertThat(path("lax $ - 2")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary(SUBTRACT, new ContextVariable(), new SqlValueLiteral(new LongLiteral("2"))))); + + assertThat(path("lax $ * 2")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary(MULTIPLY, new ContextVariable(), new SqlValueLiteral(new LongLiteral("2"))))); + + assertThat(path("lax $ / 2")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary(DIVIDE, new ContextVariable(), new SqlValueLiteral(new LongLiteral("2"))))); + + assertThat(path("lax $ % 2")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary(MODULUS, new ContextVariable(), new SqlValueLiteral(new LongLiteral("2"))))); + } + + @Test + public void testArithmeticUnary() + { + assertThat(path("lax -$")) + .isEqualTo(new JsonPath(true, new ArithmeticUnary(MINUS, new ContextVariable()))); + + assertThat(path("lax +$")) + .isEqualTo(new JsonPath(true, new ArithmeticUnary(PLUS, new ContextVariable()))); + } + + @Test + public void testArrayAccessor() + { + assertThat(path("lax $[*]")) + .isEqualTo(new JsonPath( + true, + new ArrayAccessor(new ContextVariable(), ImmutableList.of()))); + + assertThat(path("lax $[5]")) + .isEqualTo(new JsonPath( + true, + new ArrayAccessor( + new ContextVariable(), + ImmutableList.of(new Subscript(new SqlValueLiteral(new LongLiteral("5"))))))); + + assertThat(path("lax $[5 to 10]")) + .isEqualTo(new JsonPath( + true, + new ArrayAccessor( + new ContextVariable(), + ImmutableList.of(new Subscript(new SqlValueLiteral(new LongLiteral("5")), new SqlValueLiteral(new LongLiteral("10"))))))); + + assertThat(path("lax $[3 to 5, 2, 0 to 1]")) + .isEqualTo(new JsonPath( + true, + new ArrayAccessor( + new ContextVariable(), + ImmutableList.of( + new Subscript(new SqlValueLiteral(new LongLiteral("3")), new SqlValueLiteral(new LongLiteral("5"))), + new Subscript(new SqlValueLiteral(new LongLiteral("2"))), + new Subscript(new SqlValueLiteral(new LongLiteral("0")), new SqlValueLiteral(new LongLiteral("1"))))))); + } + + @Test + public void testMemberAccessor() + { + assertThat(path("lax $.*")) + .isEqualTo(new JsonPath( + true, + new MemberAccessor(new ContextVariable(), Optional.empty()))); + + assertThat(path("lax $.Key_Identifier")) + .isEqualTo(new JsonPath( + true, + new MemberAccessor(new ContextVariable(), Optional.of("Key_Identifier")))); + + assertThat(path("lax $.\"Key Name\"")) + .isEqualTo(new JsonPath( + true, + new MemberAccessor(new ContextVariable(), Optional.of("Key Name")))); + } + + @Test + public void testPrecedenceAndGrouping() + { + assertThat(path("lax 1 + 2 + 3")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary( + ADD, + new ArithmeticBinary(ADD, new SqlValueLiteral(new LongLiteral("1")), new SqlValueLiteral(new LongLiteral("2"))), + new SqlValueLiteral(new LongLiteral("3"))))); + + assertThat(path("lax 1 * 2 + 3")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary( + ADD, + new ArithmeticBinary(MULTIPLY, new SqlValueLiteral(new LongLiteral("1")), new SqlValueLiteral(new LongLiteral("2"))), + new SqlValueLiteral(new LongLiteral("3"))))); + + assertThat(path("lax 1 + 2 * 3")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary( + ADD, + new SqlValueLiteral(new LongLiteral("1")), + new ArithmeticBinary(MULTIPLY, new SqlValueLiteral(new LongLiteral("2")), new SqlValueLiteral(new LongLiteral("3")))))); + + assertThat(path("lax (1 + 2) * 3")) + .isEqualTo(new JsonPath(true, new ArithmeticBinary( + MULTIPLY, + new ArithmeticBinary(ADD, new SqlValueLiteral(new LongLiteral("1")), new SqlValueLiteral(new LongLiteral("2"))), + new SqlValueLiteral(new LongLiteral("3"))))); + } + + @Test + public void testFilter() + { + assertThat(path("lax $ ? (exists($))")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ExistsPredicate(new ContextVariable())))); + + assertThat(path("lax $ ? ($x == $y)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ComparisonPredicate(EQUAL, new NamedVariable("x"), new NamedVariable("y"))))); + + assertThat(path("lax $ ? ($x <> $y)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ComparisonPredicate(NOT_EQUAL, new NamedVariable("x"), new NamedVariable("y"))))); + + assertThat(path("lax $ ? ($x != $y)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ComparisonPredicate(NOT_EQUAL, new NamedVariable("x"), new NamedVariable("y"))))); + + assertThat(path("lax $ ? ($x < $y)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ComparisonPredicate(LESS_THAN, new NamedVariable("x"), new NamedVariable("y"))))); + + assertThat(path("lax $ ? ($x > $y)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ComparisonPredicate(GREATER_THAN, new NamedVariable("x"), new NamedVariable("y"))))); + + assertThat(path("lax $ ? ($x <= $y)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ComparisonPredicate(LESS_THAN_OR_EQUAL, new NamedVariable("x"), new NamedVariable("y"))))); + + assertThat(path("lax $ ? ($x >= $y)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ComparisonPredicate(GREATER_THAN_OR_EQUAL, new NamedVariable("x"), new NamedVariable("y"))))); + + assertThat(path("lax $ ? ($ like_regex \"something*\")")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new LikeRegexPredicate(new ContextVariable(), "something*", Optional.empty())))); + + assertThat(path("lax $ ? ($ like_regex \"something*\" flag \"some_flag\")")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new LikeRegexPredicate(new ContextVariable(), "something*", Optional.of("some_flag"))))); + + assertThat(path("lax $ ? ($ starts with $some_variable)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new StartsWithPredicate(new ContextVariable(), new NamedVariable("some_variable"))))); + + assertThat(path("lax $ ? ($ starts with \"some_text\")")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new StartsWithPredicate(new ContextVariable(), new SqlValueLiteral(new StringLiteral("some_text")))))); + + assertThat(path("lax $ ? ((exists($)) is unknown)")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new IsUnknownPredicate(new ExistsPredicate(new ContextVariable()))))); + + assertThat(path("lax $ ? (! exists($))")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new NegationPredicate(new ExistsPredicate(new ContextVariable()))))); + + assertThat(path("lax $ ? (exists($x) && exists($y))")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ConjunctionPredicate(new ExistsPredicate(new NamedVariable("x")), new ExistsPredicate(new NamedVariable("y")))))); + + assertThat(path("lax $ ? (exists($x) || exists($y))")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new DisjunctionPredicate(new ExistsPredicate(new NamedVariable("x")), new ExistsPredicate(new NamedVariable("y")))))); + } + + @Test + public void testPrecedenceAndGroupingInFilter() + { + assertThat(path("lax $ ? (exists($x) && exists($y) && exists($z))")) + .isEqualTo(new JsonPath( + true, + new Filter( + new ContextVariable(), + new ConjunctionPredicate( + new ConjunctionPredicate(new ExistsPredicate(new NamedVariable("x")), new ExistsPredicate(new NamedVariable("y"))), + new ExistsPredicate(new NamedVariable("z")))))); + + assertThat(path("lax $ ? (exists($x) || exists($y) || exists($z))")) + .isEqualTo(new JsonPath( + true, + new Filter( + new ContextVariable(), + new DisjunctionPredicate( + new DisjunctionPredicate(new ExistsPredicate(new NamedVariable("x")), new ExistsPredicate(new NamedVariable("y"))), + new ExistsPredicate(new NamedVariable("z")))))); + + assertThat(path("lax $ ? (exists($x) || (exists($y) || exists($z)))")) + .isEqualTo(new JsonPath( + true, + new Filter( + new ContextVariable(), + new DisjunctionPredicate( + new ExistsPredicate(new NamedVariable("x")), + new DisjunctionPredicate(new ExistsPredicate(new NamedVariable("y")), new ExistsPredicate(new NamedVariable("z"))))))); + + assertThat(path("lax $ ? (exists($x) || exists($y) && exists($z))")) + .isEqualTo(new JsonPath( + true, + new Filter( + new ContextVariable(), + new DisjunctionPredicate( + new ExistsPredicate(new NamedVariable("x")), + new ConjunctionPredicate(new ExistsPredicate(new NamedVariable("y")), new ExistsPredicate(new NamedVariable("z"))))))); + } + + @Test + public void testPredicateCurrentItemVariable() + { + assertThat(path("lax $ ? (exists(@))")) + .isEqualTo(new JsonPath( + true, + new Filter(new ContextVariable(), new ExistsPredicate(new PredicateCurrentItemVariable())))); + } + + @Test + public void testCaseSensitiveKeywords() + { + assertThatThrownBy(() -> PATH_PARSER.parseJsonPath("LAX $")) + .hasMessage("line 1:1: mismatched input 'LAX' expecting {'lax', 'strict'}"); + + assertThatThrownBy(() -> PATH_PARSER.parseJsonPath("lax $[1 To 2]")) + .hasMessage("line 1:9: mismatched input 'To' expecting {',', ']'}"); + } + + @Test + public void testNonReservedKeywords() + { + // keyword "lax" as key in member accessor + assertThat(path("lax $.lax")) + .isEqualTo(new JsonPath( + true, + new MemberAccessor( + new ContextVariable(), + Optional.of("lax")))); + + // keyword "ceiling" as key in member accessor + assertThat(path("lax $.ceiling")) + .isEqualTo(new JsonPath( + true, + new MemberAccessor( + new ContextVariable(), + Optional.of("ceiling")))); + + // keyword "lax" as variable name + assertThat(path("lax $lax")) + .isEqualTo(new JsonPath( + true, + new NamedVariable("lax"))); + + // keyword "lax" as variable name in array subscript + assertThat(path("lax $[$lax]")) + .isEqualTo(new JsonPath( + true, + new ArrayAccessor(new ContextVariable(), ImmutableList.of(new Subscript(new NamedVariable("lax")))))); + } + + @Test + public void testNestedStructure() + { + assertThat(path("lax $multiplier[0].floor().abs() * ($.array.size() + $component ? (exists(@)))")) + .isEqualTo(new JsonPath( + true, + new ArithmeticBinary( + MULTIPLY, + new AbsMethod(new FloorMethod(new ArrayAccessor(new NamedVariable("multiplier"), ImmutableList.of(new Subscript(new SqlValueLiteral(new LongLiteral("0"))))))), + new ArithmeticBinary( + ADD, + new SizeMethod(new MemberAccessor(new ContextVariable(), Optional.of("array"))), + new Filter(new NamedVariable("component"), new ExistsPredicate(new PredicateCurrentItemVariable())))))); + } + + private static AssertProvider> path(String path) + { + return () -> new RecursiveComparisonAssert<>(PATH_PARSER.parseJsonPath(path), COMPARISON_CONFIGURATION); + } +} diff --git a/core/trino-parser/src/test/java/io/trino/sql/parser/TestSqlParser.java b/core/trino-parser/src/test/java/io/trino/sql/parser/TestSqlParser.java index a223283f6ac9..2779252d4176 100644 --- a/core/trino-parser/src/test/java/io/trino/sql/parser/TestSqlParser.java +++ b/core/trino-parser/src/test/java/io/trino/sql/parser/TestSqlParser.java @@ -95,6 +95,11 @@ import io.trino.sql.tree.Isolation; import io.trino.sql.tree.Join; import io.trino.sql.tree.JoinOn; +import io.trino.sql.tree.JsonExists; +import io.trino.sql.tree.JsonPathInvocation; +import io.trino.sql.tree.JsonPathParameter; +import io.trino.sql.tree.JsonQuery; +import io.trino.sql.tree.JsonValue; import io.trino.sql.tree.LambdaArgumentDeclaration; import io.trino.sql.tree.LambdaExpression; import io.trino.sql.tree.Lateral; @@ -251,6 +256,10 @@ import static io.trino.sql.tree.DescriptorArgument.nullDescriptorArgument; import static io.trino.sql.tree.FrameBound.Type.CURRENT_ROW; import static io.trino.sql.tree.FrameBound.Type.FOLLOWING; +import static io.trino.sql.tree.JsonPathParameter.JsonFormat.JSON; +import static io.trino.sql.tree.JsonPathParameter.JsonFormat.UTF16; +import static io.trino.sql.tree.JsonPathParameter.JsonFormat.UTF32; +import static io.trino.sql.tree.JsonPathParameter.JsonFormat.UTF8; import static io.trino.sql.tree.PatternSearchMode.Mode.SEEK; import static io.trino.sql.tree.ProcessingMode.Mode.FINAL; import static io.trino.sql.tree.ProcessingMode.Mode.RUNNING; @@ -3962,6 +3971,158 @@ private static Query selectAllFrom(Relation relation) Optional.empty()); } + public void testJsonExists() + { + // test defaults + assertThat(expression("JSON_EXISTS(json_column, 'lax $[5]')")) + .isEqualTo(new JsonExists( + Optional.of(location(1, 1)), + new JsonPathInvocation( + Optional.of(location(1, 13)), + new Identifier(location(1, 13), "json_column", false), + JSON, + new StringLiteral(location(1, 26), "lax $[5]"), + ImmutableList.of()), + JsonExists.ErrorBehavior.FALSE)); + + assertThat(expression("JSON_EXISTS(" + + " json_column FORMAT JSON ENCODING UTF8, " + + " 'lax $[start_parameter TO end_parameter.ceiling()]' " + + " PASSING " + + " start_column AS start_parameter, " + + " end_column FORMAT JSON ENCODING UTF16 AS end_parameter " + + " UNKNOWN ON ERROR)")) + .isEqualTo(new JsonExists( + Optional.of(location(1, 1)), + new JsonPathInvocation( + Optional.of(location(1, 44)), + new Identifier(location(1, 44), "json_column", false), + UTF8, + new StringLiteral(location(1, 114), "lax $[start_parameter TO end_parameter.ceiling()]"), + ImmutableList.of( + new JsonPathParameter( + Optional.of(location(1, 252)), + new Identifier(location(1, 268), "start_parameter", false), + new Identifier(location(1, 252), "start_column", false), + Optional.empty()), + new JsonPathParameter( + Optional.of(location(1, 328)), + new Identifier(location(1, 369), "end_parameter", false), + new Identifier(location(1, 328), "end_column", false), + Optional.of(UTF16)))), + JsonExists.ErrorBehavior.UNKNOWN)); + } + + @Test + public void testJsonValue() + { + // test defaults + assertThat(expression("JSON_VALUE(json_column, 'lax $[5]')")) + .isEqualTo(new JsonValue( + Optional.of(location(1, 1)), + new JsonPathInvocation( + Optional.of(location(1, 12)), + new Identifier(location(1, 12), "json_column", false), + JSON, + new StringLiteral(location(1, 25), "lax $[5]"), + ImmutableList.of()), + Optional.empty(), + JsonValue.EmptyOrErrorBehavior.NULL, + Optional.empty(), + JsonValue.EmptyOrErrorBehavior.NULL, + Optional.empty())); + + assertThat(expression("JSON_VALUE(" + + " json_column FORMAT JSON ENCODING UTF8, " + + " 'lax $[start_parameter TO end_parameter.ceiling()]' " + + " PASSING " + + " start_column AS start_parameter, " + + " end_column FORMAT JSON ENCODING UTF16 AS end_parameter " + + " RETURNING double " + + " DEFAULT 5e0 ON EMPTY " + + " ERROR ON ERROR)")) + .isEqualTo(new JsonValue( + Optional.of(location(1, 1)), + new JsonPathInvocation( + Optional.of(location(1, 43)), + new Identifier(location(1, 43), "json_column", false), + UTF8, + new StringLiteral(location(1, 113), "lax $[start_parameter TO end_parameter.ceiling()]"), + ImmutableList.of( + new JsonPathParameter( + Optional.of(location(1, 251)), + new Identifier(location(1, 267), "start_parameter", false), + new Identifier(location(1, 251), "start_column", false), + Optional.empty()), + new JsonPathParameter( + Optional.of(location(1, 327)), + new Identifier(location(1, 368), "end_parameter", false), + new Identifier(location(1, 327), "end_column", false), + Optional.of(UTF16)))), + Optional.of(new GenericDataType(location(1, 423), new Identifier(location(1, 423), "double", false), ImmutableList.of())), + JsonValue.EmptyOrErrorBehavior.DEFAULT, + Optional.of(new DoubleLiteral(location(1, 469), "5e0")), + JsonValue.EmptyOrErrorBehavior.ERROR, + Optional.empty())); + } + + @Test + public void testJsonQuery() + { + // test defaults + assertThat(expression("JSON_QUERY(json_column, 'lax $[5]')")) + .isEqualTo(new JsonQuery( + Optional.of(location(1, 1)), + new JsonPathInvocation( + Optional.of(location(1, 12)), + new Identifier(location(1, 12), "json_column", false), + JSON, + new StringLiteral(location(1, 25), "lax $[5]"), + ImmutableList.of()), + Optional.empty(), + Optional.empty(), + JsonQuery.ArrayWrapperBehavior.WITHOUT, + Optional.empty(), + JsonQuery.EmptyOrErrorBehavior.NULL, + JsonQuery.EmptyOrErrorBehavior.NULL)); + + assertThat(expression("JSON_QUERY(" + + " json_column FORMAT JSON ENCODING UTF8, " + + " 'lax $[start_parameter TO end_parameter.ceiling()]' " + + " PASSING " + + " start_column AS start_parameter, " + + " end_column FORMAT JSON ENCODING UTF16 AS end_parameter " + + " RETURNING varchar FORMAT JSON ENCODING UTF32 " + + " WITH ARRAY WRAPPER " + + " OMIT QUOTES " + + " EMPTY ARRAY ON EMPTY " + + " ERROR ON ERROR)")) + .isEqualTo(new JsonQuery( + Optional.of(location(1, 1)), + new JsonPathInvocation( + Optional.of(location(1, 43)), + new Identifier(location(1, 43), "json_column", false), + UTF8, + new StringLiteral(location(1, 113), "lax $[start_parameter TO end_parameter.ceiling()]"), + ImmutableList.of( + new JsonPathParameter( + Optional.of(location(1, 251)), + new Identifier(location(1, 267), "start_parameter", false), + new Identifier(location(1, 251), "start_column", false), + Optional.empty()), + new JsonPathParameter( + Optional.of(location(1, 327)), + new Identifier(location(1, 368), "end_parameter", false), + new Identifier(location(1, 327), "end_column", false), + Optional.of(UTF16)))), + Optional.of(new GenericDataType(location(1, 423), new Identifier(location(1, 423), "varchar", false), ImmutableList.of())), + Optional.of(UTF32), + JsonQuery.ArrayWrapperBehavior.UNCONDITIONAL, + Optional.of(JsonQuery.QuotesBehavior.OMIT), + JsonQuery.EmptyOrErrorBehavior.EMPTY_ARRAY, + JsonQuery.EmptyOrErrorBehavior.ERROR)); + } + private static QualifiedName makeQualifiedName(String tableName) { List parts = Splitter.on('.').splitToList(tableName).stream() diff --git a/core/trino-spi/src/main/java/io/trino/spi/StandardErrorCode.java b/core/trino-spi/src/main/java/io/trino/spi/StandardErrorCode.java index 845b96f9e346..c24cd137a880 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/StandardErrorCode.java +++ b/core/trino-spi/src/main/java/io/trino/spi/StandardErrorCode.java @@ -133,6 +133,13 @@ public enum StandardErrorCode MISSING_RETURN_TYPE(109, USER_ERROR), AMBIGUOUS_RETURN_TYPE(110, USER_ERROR), MISSING_ARGUMENT(111, USER_ERROR), + DUPLICATE_PARAMETER_NAME(112, USER_ERROR), + INVALID_PATH(113, USER_ERROR), + JSON_INPUT_CONVERSION_ERROR(114, USER_ERROR), + JSON_OUTPUT_CONVERSION_ERROR(115, USER_ERROR), + PATH_EVALUATION_ERROR(116, USER_ERROR), + INVALID_JSON_LITERAL(117, USER_ERROR), + JSON_VALUE_RESULT_ERROR(118, USER_ERROR), GENERIC_INTERNAL_ERROR(65536, INTERNAL_ERROR), TOO_MANY_REQUESTS_FAILED(65537, INTERNAL_ERROR), diff --git a/core/trino-spi/src/main/java/io/trino/spi/type/StandardTypes.java b/core/trino-spi/src/main/java/io/trino/spi/type/StandardTypes.java index eba59076cbd5..e39e7ba6bde9 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/type/StandardTypes.java +++ b/core/trino-spi/src/main/java/io/trino/spi/type/StandardTypes.java @@ -41,6 +41,7 @@ public final class StandardTypes public static final String ARRAY = "array"; public static final String MAP = "map"; public static final String JSON = "json"; + public static final String JSON_2016 = "json2016"; public static final String IPADDRESS = "ipaddress"; public static final String GEOMETRY = "Geometry"; public static final String UUID = "uuid"; diff --git a/docs/src/main/sphinx/language/reserved.rst b/docs/src/main/sphinx/language/reserved.rst index bdb5ff66e6ff..f94657c19e8f 100644 --- a/docs/src/main/sphinx/language/reserved.rst +++ b/docs/src/main/sphinx/language/reserved.rst @@ -1,3 +1,4 @@ + ================= Reserved keywords ================= @@ -54,6 +55,9 @@ Keyword SQL:2016 SQL-92 ``INTO`` reserved reserved ``IS`` reserved reserved ``JOIN`` reserved reserved +``JSON_EXISTS`` reserved +``JSON_QUERY`` reserved +``JSON_VALUE`` reserved ``LEFT`` reserved reserved ``LIKE`` reserved reserved ``LISTAGG`` reserved diff --git a/testing/trino-benchmark/src/main/java/io/trino/benchmark/JsonFunctionsBenchmark.java b/testing/trino-benchmark/src/main/java/io/trino/benchmark/JsonFunctionsBenchmark.java new file mode 100644 index 000000000000..5e7c05aaaaf5 --- /dev/null +++ b/testing/trino-benchmark/src/main/java/io/trino/benchmark/JsonFunctionsBenchmark.java @@ -0,0 +1,66 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.benchmark; + +import io.trino.testing.LocalQueryRunner; + +import static io.trino.benchmark.BenchmarkQueryRunner.createLocalQueryRunner; + +public abstract class JsonFunctionsBenchmark +{ + public static void main(String... args) + { + LocalQueryRunner localQueryRunner = createLocalQueryRunner(); + new JsonExtractBenchmark(localQueryRunner).runBenchmark(new JsonAvgBenchmarkResultWriter(System.out)); + new JsonQueryBenchmark(localQueryRunner).runBenchmark(new JsonAvgBenchmarkResultWriter(System.out)); + new JsonExtractScalarBenchmark(localQueryRunner).runBenchmark(new JsonAvgBenchmarkResultWriter(System.out)); + new JsonValueBenchmark(localQueryRunner).runBenchmark(new JsonAvgBenchmarkResultWriter(System.out)); + } + + public static class JsonExtractBenchmark + extends AbstractSqlBenchmark + { + public JsonExtractBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "json_extract", 5, 50, "SELECT json_extract(format('{name : %s, random_number : %s}', partkey, random()), '$.name') FROM lineitem"); + } + } + + public static class JsonQueryBenchmark + extends AbstractSqlBenchmark + { + public JsonQueryBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "json_query", 5, 50, "SELECT json_query(format('{name : %s, random_number : %s}', partkey, random()), 'strict $.name') FROM lineitem"); + } + } + + public static class JsonExtractScalarBenchmark + extends AbstractSqlBenchmark + { + public JsonExtractScalarBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "json_extract_scalar", 5, 50, "SELECT json_extract_scalar(format('{comment : %s, random_number : %s}', comment, random()), '$.comment') FROM lineitem"); + } + } + + public static class JsonValueBenchmark + extends AbstractSqlBenchmark + { + public JsonValueBenchmark(LocalQueryRunner localQueryRunner) + { + super(localQueryRunner, "json_value", 5, 50, "SELECT json_value(format('{comment : %s, random_number : %s}', comment, random()), 'strict $.comment') FROM lineitem"); + } + } +} diff --git a/testing/trino-testing/src/main/java/io/trino/testing/AbstractTestEngineOnlyQueries.java b/testing/trino-testing/src/main/java/io/trino/testing/AbstractTestEngineOnlyQueries.java index 7b8e3747fe5f..147aae13fe19 100644 --- a/testing/trino-testing/src/main/java/io/trino/testing/AbstractTestEngineOnlyQueries.java +++ b/testing/trino-testing/src/main/java/io/trino/testing/AbstractTestEngineOnlyQueries.java @@ -6256,6 +6256,224 @@ public void testPartialLimitWithPresortedConstantInputs() .matches("VALUES ('name', 'age')"); } + @Test + public void testJsonExistsFunction() + { + assertThat(query("SELECT json_exists(json_input, 'strict $?(@ < 3)') result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES true, true, true, false, false"); + + assertThat(query("SELECT json_exists(json_input, 'strict $?(@ < 3) / $' UNKNOWN ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES null, true, true, null, null"); + + // input conversion error + assertThat(query("SELECT json_exists(json_input, 'strict $?(@ < 3)' FALSE ON ERROR) result " + + " FROM (SELECT format('[%s...', regionkey) FROM region) t(json_input)")) // malformed JSON + .matches("VALUES false, false, false, false, false"); + } + + @Test + public void testJsonQueryFunction() + { + assertThat(query("SELECT json_query(json_input, 'strict $?(@ < 3)') result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES VARCHAR '0', '1', '2', null, null"); + + assertThat(query("SELECT json_query(json_input, 'strict $?(@ < 3)' EMPTY ARRAY ON EMPTY EMPTY OBJECT ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES VARCHAR '0', '1', '2', '[]', '[]'"); + + assertThat(query("SELECT json_query(json_input, 'strict $?(@ < 3) / $' EMPTY ARRAY ON EMPTY EMPTY OBJECT ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES VARCHAR '{}', '1', '1', '{}', '{}'"); + } + + @Test + public void testJsonValueFunctionReturnType() + { + // default returned type is varchar + assertThat(query("SELECT json_value(json_input, 'strict $?(@[0] starts with \"A\" || @[1] < 4)[2]') result " + + " FROM (SELECT format('[\"%s\", %s, %s]', name, regionkey, comment > 'k') FROM region) t(json_input)")) // JSON array[text, number, boolean] + .matches("VALUES VARCHAR 'true', 'false', 'false', 'true', null"); + + // returning char(6) (java type Slice) + assertThat(query("SELECT json_value(json_input, 'strict $?(@[1] > 1 || @[2] == true)[0]' RETURNING char(6)) result " + + " FROM (SELECT format('[\"%s\", %s, %s]', name, regionkey, comment > 'k') FROM region) t(json_input)")) // JSON array[text, number, boolean] + .matches("VALUES cast('AFRICA' AS char(6)), null, 'ASIA ', 'EUROPE', 'MIDDLE'"); + + // returning integer (java type long) + assertThat(query("SELECT json_value(json_input, 'strict $?(@[0] starts with \"A\" || @[1] < 4)[1]' RETURNING integer) result " + + " FROM (SELECT format('[\"%s\", %s, %s]', name, regionkey, comment > 'k') FROM region) t(json_input)")) // JSON array[text, number, boolean] + .matches("VALUES 0, 1, 2, 3, null"); + + // returning double (java type double) + assertThat(query("SELECT json_value(json_input, 'strict $?(@[0] starts with \"A\" || @[1] < 4)[1]' RETURNING double) result " + + " FROM (SELECT format('[\"%s\", %s, %s]', name, regionkey, comment > 'k') FROM region) t(json_input)")) // JSON array[text, number, boolean] + .matches("VALUES 0e0, 1e0, 2e0, 3e0, null"); + + // returning boolean (java type boolean) + assertThat(query("SELECT json_value(json_input, 'strict $?(@[0] starts with \"A\" || @[1] < 4)[2]' RETURNING boolean) result " + + " FROM (SELECT format('[\"%s\", %s, %s]', name, regionkey, comment > 'k') FROM region) t(json_input)")) // JSON array[text, number, boolean] + .matches("VALUES true, false, false, true, null"); + + // returning decimal(30, 20) (java type Object: Int128) + assertThat(query("SELECT json_value(json_input, 'strict $?(@[0] starts with \"A\" || @[1] < 4)[1]' RETURNING decimal(30, 20)) result " + + " FROM (SELECT format('[\"%s\", %s, %s]', name, regionkey, comment > 'k') FROM region) t(json_input)")) // JSON array[text, number, boolean] + .matches("VALUES cast(0 AS decimal(30, 20)), 1, 2, 3, null"); + } + + @Test + public void testJsonValueDefaults() + { + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3)' DEFAULT 'was empty' ON EMPTY DEFAULT 'was error' ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES VARCHAR '0', '1', '2', 'was empty', 'was empty'"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) + 10' DEFAULT 'was empty' ON EMPTY DEFAULT 'was error' ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES VARCHAR '10', '11', '12', 'was error', 'was error'"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) / 0' DEFAULT 'was empty' ON EMPTY DEFAULT 'was error' ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES VARCHAR 'was error', 'was error', 'was error', 'was error', 'was error'"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) + 10' RETURNING varchar(10) DEFAULT 'was empty' ON EMPTY DEFAULT 'was error' ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES cast('10' AS varchar(10)) , '11', '12', 'was error', 'was error'"); + + // returning bigint + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3)' RETURNING bigint DEFAULT -2 ON EMPTY DEFAULT -1 ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES BIGINT '0', 1, 2, -2, -2"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) + 10' RETURNING bigint DEFAULT -2 ON EMPTY DEFAULT -1 ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES BIGINT '10', 11, 12, -1, -1"); + + // returning double + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3)' RETURNING double DEFAULT -2 ON EMPTY DEFAULT -1 ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES 0e0, 1e0, 2e0, -2e0, -2e0"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) + 10' RETURNING double DEFAULT -2 ON EMPTY DEFAULT -1 ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES 10e0, 11e0, 12e0, -1e0, -1e0"); + + // returning boolean + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3)' RETURNING boolean DEFAULT false ON EMPTY DEFAULT false ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES false, true, true, false, false"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) + 10' RETURNING boolean DEFAULT false ON EMPTY DEFAULT false ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES true, true, true, false, false"); + + // returning decimal(30, 20) + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3)' RETURNING decimal(30, 20) DEFAULT -2 ON EMPTY DEFAULT -1 ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES cast(0 AS decimal(30, 20)), 1, 2, -2, -2"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) + 10' RETURNING decimal(30, 20) DEFAULT -2 ON EMPTY DEFAULT -1 ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES cast(10 AS decimal(30, 20)), 11, 12, -1, -1"); + } + + @Test + public void testJsonValueDefaultNull() + { + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3)' DEFAULT null ON EMPTY DEFAULT null ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES VARCHAR '0', '1', '2', null, null"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) + 10' RETURNING bigint DEFAULT null ON EMPTY DEFAULT null ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES BIGINT '10', 11, 12, null, null"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3)' RETURNING double DEFAULT null ON EMPTY DEFAULT null ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES 0e0, 1e0, 2e0, null, null"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3) + 10' RETURNING boolean DEFAULT null ON EMPTY DEFAULT null ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES true, true, true, null, null"); + + assertThat(query("SELECT json_value(json_input, 'strict $?(@ < 3)' RETURNING decimal(30, 20) DEFAULT null ON EMPTY DEFAULT null ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES cast(0 AS decimal(30, 20)), 1, 2, null, null"); + } + + @Test + public void testPassingClause() + { + assertThat(query("SELECT json_exists(json_input, 'strict $?(@ > $low && @ < $high)' PASSING 0e0 AS \"low\", 4.000 AS \"high\") result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES false, true, true, true, false"); + + assertThat(query("SELECT json_query(json_input, 'strict $?($bool == true || $name starts with \"A\")' PASSING comment > 'm' AS \"bool\", name AS \"name\") result " + + " FROM (SELECT format('%s', regionkey), comment, name FROM region) t(json_input, comment, name)")) + .matches("VALUES VARCHAR '0', '1', '2', null, '4'"); + + assertThat(query("SELECT json_value(json_input, 'strict $name' PASSING name AS \"name\") result " + + " FROM (SELECT format('%s', regionkey), name FROM region) t(json_input, name)")) + .matches("VALUES VARCHAR 'AFRICA', 'AMERICA', 'ASIA', 'EUROPE', 'MIDDLE EAST'"); + + // null as SQL value parameter -> the passed value is JSON null + assertThat(query("SELECT json_query(json_input, 'strict $var' PASSING null AS \"var\") result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) + .matches("VALUES VARCHAR 'null', 'null', 'null', 'null', 'null'"); + + // null as JSON parameter -> the passed value is empty sequence + assertThat(query("SELECT json_exists(json_input, 'strict $var' PASSING null FORMAT JSON AS \"var\") result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) + .matches("VALUES false, false, false, false, false"); + + assertThat(query("SELECT json_value(json_input, 'strict $var[$]' PASSING '[\"a\", \"b\", \"c\", \"d\", \"e\"]' FORMAT JSON AS \"var\") result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) + .matches("VALUES VARCHAR 'a', 'b', 'c', 'd', 'e'"); + } + + @Test + public void testNullInput() + { + assertThat(query("SELECT json_exists(json_input, 'strict $') result " + + " FROM (SELECT null FROM region) t(json_input)")) + .matches("VALUES cast(null AS boolean), null, null, null, null"); + + assertThat(query("SELECT json_query(json_input, 'strict $') result " + + " FROM (SELECT null FROM region) t(json_input)")) + .matches("VALUES cast(null AS varchar), null, null, null, null"); + + assertThat(query("SELECT json_value(json_input, 'strict $') result " + + " FROM (SELECT null FROM region) t(json_input)")) + .matches("VALUES cast(null AS varchar), null, null, null, null"); + } + + @Test + public void testJsonQueryAsInput() + { + // If the context item is output of a JSON-returning function (currently, the only JSON-returning function is json_query), + // it should inherit format JSON, if that is output format of the JSON-returning function + assertThat(query("SELECT json_value(json_query(json_input, 'strict $'), 'strict $[0]') result " + + " FROM (SELECT format('[\"%s\", %s, %s]', name, regionkey, comment > 'k') FROM region) t(json_input)")) + .matches("VALUES VARCHAR 'AFRICA', 'AMERICA', 'ASIA', 'EUROPE', 'MIDDLE EAST'"); + + // If a JSON path parameter is output of a JSON-returning function (currently, the only JSON-returning function is json_query), + // it should inherit format JSON, if that is output format of the JSON-returning function + assertThat(query("SELECT json_value('null', 'strict $array[0]' PASSING json_query(json_input, 'strict $') AS \"array\") result " + + " FROM (SELECT format('[\"%s\", %s, %s]', name, regionkey, comment > 'k') FROM region) t(json_input)")) + .matches("VALUES VARCHAR 'AFRICA', 'AMERICA', 'ASIA', 'EUROPE', 'MIDDLE EAST'"); + } + + @Test + public void testSubqueryInJsonFunctions() + { + // subqueries as: input item, passed parameter, empty default, error default + assertThat(query("SELECT json_value((SELECT json_input), 'strict $?(@ < $var)' PASSING (SELECT 3) AS \"var\" DEFAULT (SELECT 'x') ON EMPTY DEFAULT (SELECT 'y') ON ERROR) result " + + " FROM (SELECT format('%s', regionkey) FROM region) t(json_input)")) // JSON number + .matches("VALUES VARCHAR '0', '1', '2', 'x', 'x'"); + } + private static ZonedDateTime zonedDateTime(String value) { return ZONED_DATE_TIME_FORMAT.parse(value, ZonedDateTime::from);