All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.trino.operator.scalar.json.JsonObjectFunction Maven / Gradle / Ivy

/*
 * 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.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
import io.airlift.slice.Slice;
import io.trino.annotation.UsedByGeneratedCode;
import io.trino.json.ir.TypedValue;
import io.trino.metadata.SqlScalarFunction;
import io.trino.operator.scalar.ChoicesSpecializedSqlScalarFunction;
import io.trino.operator.scalar.SpecializedSqlScalarFunction;
import io.trino.spi.TrinoException;
import io.trino.spi.block.SqlRow;
import io.trino.spi.function.BoundSignature;
import io.trino.spi.function.FunctionMetadata;
import io.trino.spi.function.Signature;
import io.trino.spi.type.RowType;
import io.trino.spi.type.Type;
import io.trino.spi.type.TypeSignature;
import io.trino.type.Json2016Type;

import java.lang.invoke.MethodHandle;
import java.util.HashMap;
import java.util.Map;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static io.trino.json.JsonInputErrorNode.JSON_ERROR;
import static io.trino.json.ir.SqlJsonLiteralConverter.getJsonNode;
import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT;
import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED;
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.FAIL_ON_NULL;
import static io.trino.spi.type.StandardTypes.BOOLEAN;
import static io.trino.spi.type.StandardTypes.JSON_2016;
import static io.trino.spi.type.TypeUtils.readNativeValue;
import static io.trino.sql.analyzer.ExpressionAnalyzer.JSON_NO_PARAMETERS_ROW_TYPE;
import static io.trino.util.Reflection.methodHandle;

public class JsonObjectFunction
        extends SqlScalarFunction
{
    public static final JsonObjectFunction JSON_OBJECT_FUNCTION = new JsonObjectFunction();
    public static final String JSON_OBJECT_FUNCTION_NAME = "$json_object";
    private static final MethodHandle METHOD_HANDLE = methodHandle(JsonObjectFunction.class, "jsonObject", RowType.class, RowType.class, SqlRow.class, SqlRow.class, boolean.class, boolean.class);
    private static final JsonNode EMPTY_OBJECT = new ObjectNode(JsonNodeFactory.instance);

    private JsonObjectFunction()
    {
        super(FunctionMetadata.scalarBuilder(JSON_OBJECT_FUNCTION_NAME)
                .signature(Signature.builder()
                        .typeVariable("K")
                        .typeVariable("V")
                        .returnType(new TypeSignature(JSON_2016))
                        .argumentTypes(ImmutableList.of(new TypeSignature("K"), new TypeSignature("V"), new TypeSignature(BOOLEAN), new TypeSignature(BOOLEAN)))
                        .build())
                .argumentNullability(true, true, false, false)
                .hidden()
                .description("Creates a JSON object from key-value pairs")
                .build());
    }

    @Override
    protected SpecializedSqlScalarFunction specialize(BoundSignature boundSignature)
    {
        RowType keysRowType = (RowType) boundSignature.getArgumentType(0);
        RowType valuesRowType = (RowType) boundSignature.getArgumentType(1);
        checkArgument(keysRowType.getFields().size() == valuesRowType.getFields().size(), "keys and values do not match");
        MethodHandle methodHandle = METHOD_HANDLE
                .bindTo(keysRowType)
                .bindTo(valuesRowType);
        return new ChoicesSpecializedSqlScalarFunction(
                boundSignature,
                FAIL_ON_NULL,
                ImmutableList.of(BOXED_NULLABLE, BOXED_NULLABLE, NEVER_NULL, NEVER_NULL),
                methodHandle);
    }

    @UsedByGeneratedCode
    public static JsonNode jsonObject(RowType keysRowType, RowType valuesRowType, SqlRow keysRow, SqlRow valuesRow, boolean nullOnNull, boolean uniqueKeys)
    {
        if (JSON_NO_PARAMETERS_ROW_TYPE.equals(keysRowType)) {
            return EMPTY_OBJECT;
        }

        Map members = new HashMap<>();
        int keysRawIndex = keysRow.getRawIndex();
        int valuesRawIndex = valuesRow.getRawIndex();

        for (int i = 0; i < keysRowType.getFields().size(); i++) {
            Type keyType = keysRowType.getFields().get(i).getType();
            Object key = readNativeValue(keyType, keysRow.getRawFieldBlock(i), keysRawIndex);
            if (key == null) {
                throw new TrinoException(INVALID_FUNCTION_ARGUMENT, "null value passed for JSON object key to JSON_OBJECT function");
            }
            String keyName = ((Slice) key).toStringUtf8();

            Type valueType = valuesRowType.getFields().get(i).getType();
            Object value = readNativeValue(valueType, valuesRow.getRawFieldBlock(i), valuesRawIndex);
            checkState(!JSON_ERROR.equals(value), "malformed JSON error suppressed in the input function");

            JsonNode valueNode;
            if (value == null) {
                if (nullOnNull) {
                    valueNode = NullNode.getInstance();
                }
                else { // absent on null
                    continue;
                }
            }
            else if (valueType.equals(Json2016Type.JSON_2016)) {
                valueNode = (JsonNode) value;
            }
            else {
                valueNode = getJsonNode(TypedValue.fromValueAsObject(valueType, value))
                        .orElseThrow(() -> new TrinoException(INVALID_FUNCTION_ARGUMENT, "value passed to JSON_OBJECT function cannot be converted to JSON"));
            }

            if (members.put(keyName, valueNode) != null) {
                if (uniqueKeys) {
                    // failure is the expected behavior when a duplicate key is found in the WITH UNIQUE KEYS option
                    throw new TrinoException(INVALID_FUNCTION_ARGUMENT, "duplicate key passed to JSON_OBJECT function");
                }
                // in the WITHOUT UNIQUE KEYS option, if a duplicate key is found, both entries should be present in the resulting JSON object (per SQL standard p. 359-360).
                // the chosen library does not support JSON objects with duplicate keys.
                // we try to support the WITHOUT UNIQUE KEYS option, which is the default, but we have to fail if a duplicate key appears.
                throw new TrinoException(NOT_SUPPORTED, "cannot construct a JSON object with duplicate key");
            }
        }

        return new ObjectNode(JsonNodeFactory.instance, members);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy