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