/*
* 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 com.facebook.presto.operator.scalar;
import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.function.LiteralParameters;
import com.facebook.presto.spi.function.ScalarOperator;
import com.facebook.presto.spi.function.SqlNullable;
import com.facebook.presto.spi.function.SqlType;
import com.facebook.presto.type.BigintOperators;
import com.facebook.presto.type.BooleanOperators;
import com.facebook.presto.type.DoubleOperators;
import com.facebook.presto.type.VarcharOperators;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.airlift.slice.DynamicSliceOutput;
import io.airlift.slice.Slice;
import io.airlift.slice.SliceOutput;
import io.airlift.slice.Slices;
import java.io.IOException;
import static com.facebook.presto.spi.StandardErrorCode.INVALID_CAST_ARGUMENT;
import static com.facebook.presto.spi.function.OperatorType.CAST;
import static com.facebook.presto.spi.function.OperatorType.EQUAL;
import static com.facebook.presto.spi.function.OperatorType.HASH_CODE;
import static com.facebook.presto.spi.function.OperatorType.NOT_EQUAL;
import static com.facebook.presto.spi.type.StandardTypes.BIGINT;
import static com.facebook.presto.spi.type.StandardTypes.BOOLEAN;
import static com.facebook.presto.spi.type.StandardTypes.DATE;
import static com.facebook.presto.spi.type.StandardTypes.DOUBLE;
import static com.facebook.presto.spi.type.StandardTypes.INTEGER;
import static com.facebook.presto.spi.type.StandardTypes.JSON;
import static com.facebook.presto.spi.type.StandardTypes.REAL;
import static com.facebook.presto.spi.type.StandardTypes.TIMESTAMP;
import static com.facebook.presto.spi.type.StandardTypes.VARCHAR;
import static com.facebook.presto.util.DateTimeUtils.printDate;
import static com.facebook.presto.util.DateTimeUtils.printTimestampWithoutTimeZone;
import static com.facebook.presto.util.Failures.checkCondition;
import static com.facebook.presto.util.JsonUtil.createJsonGenerator;
import static com.facebook.presto.util.JsonUtil.createJsonParser;
import static com.fasterxml.jackson.core.JsonFactory.Feature.CANONICALIZE_FIELD_NAMES;
import static java.lang.Float.floatToRawIntBits;
import static java.lang.Math.toIntExact;
import static java.lang.String.format;
public final class JsonOperators
{
public static final JsonFactory JSON_FACTORY = new JsonFactory().disable(CANONICALIZE_FIELD_NAMES);
private JsonOperators()
{
}
@ScalarOperator(CAST)
@SqlNullable
@LiteralParameters("x")
@SqlType("varchar(x)")
public static Slice castToVarchar(@SqlType(JSON) Slice json)
{
try (JsonParser parser = createJsonParser(JSON_FACTORY, json)) {
JsonToken nextToken = parser.nextToken();
Slice result;
switch (nextToken) {
case VALUE_NULL:
result = null;
break;
case VALUE_STRING:
result = Slices.utf8Slice(parser.getText());
break;
case VALUE_NUMBER_FLOAT:
// Avoidance of loss of precision does not seem to be possible here because of Jackson implementation.
result = DoubleOperators.castToVarchar(parser.getDoubleValue());
break;
case VALUE_NUMBER_INT:
// An alternative is calling getLongValue and then BigintOperators.castToVarchar.
// It doesn't work as well because it can result in overflow and underflow exceptions for large integral numbers.
result = Slices.utf8Slice(parser.getText());
break;
case VALUE_TRUE:
result = BooleanOperators.castToVarchar(true);
break;
case VALUE_FALSE:
result = BooleanOperators.castToVarchar(false);
break;
default:
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), VARCHAR));
}
checkCondition(parser.nextToken() == null, INVALID_CAST_ARGUMENT, "Cannot cast input json to VARCHAR"); // check no trailing token
return result;
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), VARCHAR));
}
}
@ScalarOperator(CAST)
@SqlNullable
@SqlType(BIGINT)
public static Long castToBigint(@SqlType(JSON) Slice json)
{
try (JsonParser parser = createJsonParser(JSON_FACTORY, json)) {
parser.nextToken();
Long result;
switch (parser.getCurrentToken()) {
case VALUE_NULL:
result = null;
break;
case VALUE_STRING:
result = VarcharOperators.castToBigint(Slices.utf8Slice(parser.getText()));
break;
case VALUE_NUMBER_FLOAT:
result = DoubleOperators.castToLong(parser.getDoubleValue());
break;
case VALUE_NUMBER_INT:
result = parser.getLongValue();
break;
case VALUE_TRUE:
result = BooleanOperators.castToBigint(true);
break;
case VALUE_FALSE:
result = BooleanOperators.castToBigint(false);
break;
default:
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), BIGINT));
}
checkCondition(parser.nextToken() == null, INVALID_CAST_ARGUMENT, "Cannot cast input json to BIGINT"); // check no trailing token
return result;
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), BIGINT));
}
}
@ScalarOperator(CAST)
@SqlNullable
@SqlType(INTEGER)
public static Long castToInteger(@SqlType(JSON) Slice json)
{
try (JsonParser parser = createJsonParser(JSON_FACTORY, json)) {
parser.nextToken();
Long result;
switch (parser.getCurrentToken()) {
case VALUE_NULL:
result = null;
break;
case VALUE_STRING:
result = VarcharOperators.castToInteger(Slices.utf8Slice(parser.getText()));
break;
case VALUE_NUMBER_FLOAT:
result = DoubleOperators.castToInteger(parser.getDoubleValue());
break;
case VALUE_NUMBER_INT:
result = (long) toIntExact(parser.getLongValue());
break;
case VALUE_TRUE:
result = BooleanOperators.castToInteger(true);
break;
case VALUE_FALSE:
result = BooleanOperators.castToInteger(false);
break;
default:
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), INTEGER));
}
checkCondition(parser.nextToken() == null, INVALID_CAST_ARGUMENT, "Cannot cast input json to INTEGER"); // check no trailing token
return result;
}
catch (ArithmeticException | IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), INTEGER));
}
}
@ScalarOperator(CAST)
@SqlNullable
@SqlType(DOUBLE)
public static Double castToDouble(@SqlType(JSON) Slice json)
{
try (JsonParser parser = createJsonParser(JSON_FACTORY, json)) {
parser.nextToken();
Double result;
switch (parser.getCurrentToken()) {
case VALUE_NULL:
result = null;
break;
case VALUE_STRING:
result = VarcharOperators.castToDouble(Slices.utf8Slice(parser.getText()));
break;
case VALUE_NUMBER_FLOAT:
result = parser.getDoubleValue();
break;
case VALUE_NUMBER_INT:
// An alternative is calling getLongValue and then BigintOperators.castToDouble.
// It doesn't work as well because it can result in overflow and underflow exceptions for large integral numbers.
result = parser.getDoubleValue();
break;
case VALUE_TRUE:
result = BooleanOperators.castToDouble(true);
break;
case VALUE_FALSE:
result = BooleanOperators.castToDouble(false);
break;
default:
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), DOUBLE));
}
checkCondition(parser.nextToken() == null, INVALID_CAST_ARGUMENT, "Cannot cast input json to DOUBLE"); // check no trailing token
return result;
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), DOUBLE));
}
}
@ScalarOperator(CAST)
@SqlNullable
@SqlType(REAL)
public static Long castToFloat(@SqlType(JSON) Slice json)
{
try (JsonParser parser = createJsonParser(JSON_FACTORY, json)) {
parser.nextToken();
Long result;
switch (parser.getCurrentToken()) {
case VALUE_NULL:
result = null;
break;
case VALUE_STRING:
result = VarcharOperators.castToFloat(Slices.utf8Slice(parser.getText()));
break;
case VALUE_NUMBER_FLOAT:
result = (long) floatToRawIntBits(parser.getFloatValue());
break;
case VALUE_NUMBER_INT:
// An alternative is calling getLongValue and then BigintOperators.castToReal.
// It doesn't work as well because it can result in overflow and underflow exceptions for large integral numbers.
result = (long) floatToRawIntBits(parser.getFloatValue());
break;
case VALUE_TRUE:
result = BooleanOperators.castToReal(true);
break;
case VALUE_FALSE:
result = BooleanOperators.castToReal(false);
break;
default:
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), REAL));
}
checkCondition(parser.nextToken() == null, INVALID_CAST_ARGUMENT, "Cannot cast input json to REAL"); // check no trailing token
return result;
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), REAL));
}
}
@ScalarOperator(CAST)
@SqlNullable
@SqlType(BOOLEAN)
public static Boolean castToBoolean(@SqlType(JSON) Slice json)
{
try (JsonParser parser = createJsonParser(JSON_FACTORY, json)) {
parser.nextToken();
Boolean result;
switch (parser.getCurrentToken()) {
case VALUE_NULL:
result = null;
break;
case VALUE_STRING:
result = VarcharOperators.castToBoolean(Slices.utf8Slice(parser.getText()));
break;
case VALUE_NUMBER_FLOAT:
result = DoubleOperators.castToBoolean(parser.getDoubleValue());
break;
case VALUE_NUMBER_INT:
result = BigintOperators.castToBoolean(parser.getLongValue());
break;
case VALUE_TRUE:
result = true;
break;
case VALUE_FALSE:
result = false;
break;
default:
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), BOOLEAN));
}
checkCondition(parser.nextToken() == null, INVALID_CAST_ARGUMENT, "Cannot cast input json to BOOLEAN"); // check no trailing token
return result;
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), BOOLEAN));
}
}
@ScalarOperator(CAST)
@LiteralParameters("x")
@SqlType(JSON)
public static Slice castFromVarchar(@SqlType("varchar(x)") Slice value)
throws IOException
{
try {
SliceOutput output = new DynamicSliceOutput(value.length() + 2);
try (JsonGenerator jsonGenerator = createJsonGenerator(JSON_FACTORY, output)) {
jsonGenerator.writeString(value.toStringUtf8());
}
return output.slice();
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", value.toStringUtf8(), JSON));
}
}
@ScalarOperator(CAST)
@SqlType(JSON)
public static Slice castFromInteger(@SqlType(INTEGER) long value)
throws IOException
{
try {
SliceOutput output = new DynamicSliceOutput(20);
try (JsonGenerator jsonGenerator = createJsonGenerator(JSON_FACTORY, output)) {
jsonGenerator.writeNumber(value);
}
return output.slice();
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", value, JSON));
}
}
@ScalarOperator(CAST)
@SqlType(JSON)
public static Slice castFromBigint(@SqlType(BIGINT) long value)
throws IOException
{
try {
SliceOutput output = new DynamicSliceOutput(20);
try (JsonGenerator jsonGenerator = createJsonGenerator(JSON_FACTORY, output)) {
jsonGenerator.writeNumber(value);
}
return output.slice();
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", value, JSON));
}
}
@ScalarOperator(CAST)
@SqlType(JSON)
public static Slice castFromDouble(@SqlType(DOUBLE) double value)
throws IOException
{
try {
SliceOutput output = new DynamicSliceOutput(32);
try (JsonGenerator jsonGenerator = createJsonGenerator(JSON_FACTORY, output)) {
jsonGenerator.writeNumber(value);
}
return output.slice();
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", value, JSON));
}
}
@ScalarOperator(CAST)
@SqlType(JSON)
public static Slice castFromBoolean(@SqlType(BOOLEAN) boolean value)
throws IOException
{
try {
SliceOutput output = new DynamicSliceOutput(5);
try (JsonGenerator jsonGenerator = createJsonGenerator(JSON_FACTORY, output)) {
jsonGenerator.writeBoolean(value);
}
return output.slice();
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", value, JSON));
}
}
@ScalarOperator(CAST)
@SqlType(JSON)
public static Slice castFromTimestamp(ConnectorSession session, @SqlType(TIMESTAMP) long value)
throws IOException
{
try {
SliceOutput output = new DynamicSliceOutput(25);
try (JsonGenerator jsonGenerator = createJsonGenerator(JSON_FACTORY, output)) {
jsonGenerator.writeString(printTimestampWithoutTimeZone(session.getTimeZoneKey(), value));
}
return output.slice();
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", value, JSON));
}
}
@ScalarOperator(CAST)
@SqlType(JSON)
public static Slice castFromDate(ConnectorSession session, @SqlType(DATE) long value)
throws IOException
{
try {
SliceOutput output = new DynamicSliceOutput(12);
try (JsonGenerator jsonGenerator = createJsonGenerator(JSON_FACTORY, output)) {
jsonGenerator.writeString(printDate((int) value));
}
return output.slice();
}
catch (IOException e) {
throw new PrestoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", value, JSON));
}
}
@ScalarOperator(HASH_CODE)
@SqlType(BIGINT)
public static long hashCode(@SqlType(JSON) Slice value)
{
return value.hashCode();
}
@ScalarOperator(EQUAL)
@SqlType(BOOLEAN)
public static boolean equals(@SqlType(JSON) Slice leftJson, @SqlType(JSON) Slice rightJson)
{
return leftJson.equals(rightJson);
}
@ScalarOperator(NOT_EQUAL)
@SqlType(BOOLEAN)
public static boolean notEqual(@SqlType(JSON) Slice leftJson, @SqlType(JSON) Slice rightJson)
{
return !leftJson.equals(rightJson);
}
}