Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PPL: Add json_object command #3242

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions core/src/main/java/org/opensearch/sql/expression/DSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,14 @@ public static FunctionExpression notLike(Expression... expressions) {
return compile(FunctionProperties.None, BuiltinFunctionName.NOT_LIKE, expressions);
}

public static FunctionExpression jsonValid(Expression... expressions) {
return compile(FunctionProperties.None, BuiltinFunctionName.JSON_VALID, expressions);
}

public static FunctionExpression jsonObject(Expression... expressions) {
return compile(FunctionProperties.None, BuiltinFunctionName.JSON_OBJECT, expressions);
}

public static Aggregator avg(Expression... expressions) {
return aggregate(BuiltinFunctionName.AVG, expressions);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ public enum BuiltinFunctionName {
TRIM(FunctionName.of("trim")),
UPPER(FunctionName.of("upper")),

/** Json Functions. */
JSON_VALID(FunctionName.of("json_valid")),
JSON_OBJECT(FunctionName.of("json_object")),

/** NULL Test. */
IS_NULL(FunctionName.of("is null")),
IS_NOT_NULL(FunctionName.of("is not null")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.opensearch.sql.expression.datetime.DateTimeFunctions;
import org.opensearch.sql.expression.datetime.IntervalClause;
import org.opensearch.sql.expression.ip.IPFunctions;
import org.opensearch.sql.expression.json.JsonFunctions;
import org.opensearch.sql.expression.operator.arthmetic.ArithmeticFunctions;
import org.opensearch.sql.expression.operator.arthmetic.MathematicalFunctions;
import org.opensearch.sql.expression.operator.convert.TypeCastOperators;
Expand Down Expand Up @@ -83,6 +84,7 @@ public static synchronized BuiltinFunctionRepository getInstance() {
SystemFunctions.register(instance);
OpenSearchFunctions.register(instance);
IPFunctions.register(instance);
JsonFunctions.register(instance);
}
return instance;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.json;

import static org.opensearch.sql.data.type.ExprCoreType.BOOLEAN;
import static org.opensearch.sql.data.type.ExprCoreType.STRING;
import static org.opensearch.sql.data.type.ExprCoreType.STRUCT;
import static org.opensearch.sql.expression.function.FunctionDSL.define;
import static org.opensearch.sql.expression.function.FunctionDSL.impl;
import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.tuple.Pair;
import org.opensearch.sql.data.model.ExprTupleValue;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.data.type.ExprCoreType;
import org.opensearch.sql.data.type.ExprType;
import org.opensearch.sql.exception.SemanticCheckException;
import org.opensearch.sql.expression.Expression;
import org.opensearch.sql.expression.FunctionExpression;
import org.opensearch.sql.expression.env.Environment;
import org.opensearch.sql.expression.function.BuiltinFunctionName;
import org.opensearch.sql.expression.function.BuiltinFunctionRepository;
import org.opensearch.sql.expression.function.DefaultFunctionResolver;
import org.opensearch.sql.expression.function.FunctionBuilder;
import org.opensearch.sql.expression.function.FunctionName;
import org.opensearch.sql.expression.function.FunctionResolver;
import org.opensearch.sql.expression.function.FunctionSignature;
import org.opensearch.sql.utils.JsonUtils;

@UtilityClass
public class JsonFunctions {
public void register(BuiltinFunctionRepository repository) {
repository.register(jsonValid());
repository.register(jsonObject());
}

private DefaultFunctionResolver jsonValid() {
return define(
BuiltinFunctionName.JSON_VALID.getName(),
impl(nullMissingHandling(JsonUtils::isValidJson), BOOLEAN, STRING));
}

/** Creates a JSON Object/tuple expr from a given list of kv pairs. */
private static FunctionResolver jsonObject() {
return new FunctionResolver() {
@Override
public FunctionName getFunctionName() {
return BuiltinFunctionName.JSON_OBJECT.getName();
}

@Override
public Pair<FunctionSignature, FunctionBuilder> resolve(
FunctionSignature unresolvedSignature) {
List<ExprType> paramList = unresolvedSignature.getParamTypeList();
// check that we got an even number of arguments
if (paramList.size() % 2 != 0) {
throw new SemanticCheckException(
String.format(
"Expected an even number of arguments but instead got #%d arguments",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the # do here..? Does this return something like

Expected an even number of arguments but instead got #3 arguments

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove

paramList.size()));
}

// check that each "key" argument (of key-value pair) is a string
for (int i = 0; i < paramList.size(); i = i + 2) {
ExprType paramType = paramList.get(i);
if (!ExprCoreType.STRING.equals(paramType)) {
throw new SemanticCheckException(
String.format(
"Expected type %s instead of %s for parameter #%d",
ExprCoreType.STRING, paramType.typeName(), i + 1));
}
}

// return the unresolved signature and function builder
return Pair.of(
unresolvedSignature,
(functionProperties, arguments) ->
new FunctionExpression(getFunctionName(), arguments) {
@Override
public ExprValue valueOf(Environment<Expression, ExprValue> valueEnv) {
LinkedHashMap<String, ExprValue> tupleValues = new LinkedHashMap<>();
Iterator<Expression> iter = getArguments().iterator();
while (iter.hasNext()) {
tupleValues.put(
iter.next().valueOf(valueEnv).stringValue(),
iter.next().valueOf(valueEnv));
}
return ExprTupleValue.fromExprValueMap(tupleValues);
}

@Override
public ExprType type() {
return STRUCT;
}
});
}
};
}
}
26 changes: 26 additions & 0 deletions core/src/main/java/org/opensearch/sql/utils/JsonUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.opensearch.sql.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.experimental.UtilityClass;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.data.model.ExprValueUtils;

@UtilityClass
public class JsonUtils {
/**
* Checks if given JSON string can be parsed as valid JSON.
*
* @param jsonExprValue JSON string (e.g. "{\"hello\": \"world\"}").
* @return true if the string can be parsed as valid JSON, else false.
*/
public static ExprValue isValidJson(ExprValue jsonExprValue) {
ObjectMapper objectMapper = new ObjectMapper();
try {
objectMapper.readTree(jsonExprValue.stringValue());
return ExprValueUtils.LITERAL_TRUE;
} catch (JsonProcessingException e) {
return ExprValueUtils.LITERAL_FALSE;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import java.time.LocalDate;
import java.util.stream.Stream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.time.LocalDate;
import java.util.stream.Stream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.expression.json;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.opensearch.sql.data.model.ExprValueUtils.LITERAL_FALSE;
import static org.opensearch.sql.data.model.ExprValueUtils.LITERAL_NULL;
import static org.opensearch.sql.data.model.ExprValueUtils.LITERAL_TRUE;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.sql.data.model.ExprBooleanValue;
import org.opensearch.sql.data.model.ExprCollectionValue;
import org.opensearch.sql.data.model.ExprDoubleValue;
import org.opensearch.sql.data.model.ExprLongValue;
import org.opensearch.sql.data.model.ExprNullValue;
import org.opensearch.sql.data.model.ExprStringValue;
import org.opensearch.sql.data.model.ExprTupleValue;
import org.opensearch.sql.data.model.ExprValue;
import org.opensearch.sql.data.model.ExprValueUtils;
import org.opensearch.sql.exception.SemanticCheckException;
import org.opensearch.sql.expression.DSL;
import org.opensearch.sql.expression.FunctionExpression;
import org.opensearch.sql.expression.LiteralExpression;

@ExtendWith(MockitoExtension.class)
public class JsonFunctionsTest {
private static final ExprValue JsonObject =
ExprValueUtils.stringValue("{\"a\":\"1\",\"b\":\"2\"}");
private static final ExprValue JsonArray = ExprValueUtils.stringValue("[1, 2, 3, 4]");
private static final ExprValue JsonScalarString = ExprValueUtils.stringValue("\"abc\"");
private static final ExprValue JsonEmptyString = ExprValueUtils.stringValue("");
private static final ExprValue JsonInvalidObject =
ExprValueUtils.stringValue("{\"invalid\":\"json\", \"string\"}");
private static final ExprValue JsonInvalidScalar = ExprValueUtils.stringValue("abc");

@Test
public void json_valid_returns_false() {
assertEquals(LITERAL_FALSE, execute(JsonInvalidObject));
assertEquals(LITERAL_FALSE, execute(JsonInvalidScalar));
}

@Test
public void json_valid_returns_true() {
assertEquals(LITERAL_TRUE, execute(JsonObject));
assertEquals(LITERAL_TRUE, execute(JsonArray));
assertEquals(LITERAL_TRUE, execute(JsonScalarString));
assertEquals(LITERAL_TRUE, execute(JsonEmptyString));
}

private ExprValue execute(ExprValue jsonString) {
FunctionExpression exp = DSL.jsonValid(DSL.literal(jsonString));
return exp.valueOf();
}

@Test
public void json_object_returns_tuple() {
FunctionExpression exp;

// Setup
LinkedHashMap<String, ExprValue> objectMap = new LinkedHashMap<>();
objectMap.put("foo", new ExprStringValue("foo"));
objectMap.put("fuzz", ExprBooleanValue.of(true));
objectMap.put("bar", new ExprLongValue(1234));
objectMap.put("bar2", new ExprDoubleValue(12.34));
objectMap.put("baz", ExprNullValue.of());
objectMap.put(
"obj", ExprTupleValue.fromExprValueMap(Map.of("internal", new ExprStringValue("value"))));
// TODO: requires json_array()
// objectMap.put(
// "arr",
// new ExprCollectionValue(
// List.of(new ExprStringValue("string"), ExprBooleanValue.of(true), ExprNullValue.of())));
ExprValue expectedTupleExpr = ExprTupleValue.fromExprValueMap(objectMap);

// exercise
exp = DSL.jsonObject(
DSL.literal("foo"), DSL.literal("foo"),
DSL.literal("fuzz"), DSL.literal(true),
DSL.literal("bar"), DSL.literal(1234),
DSL.literal("bar2"), DSL.literal(12.34),
DSL.literal("baz"), new LiteralExpression(ExprValueUtils.nullValue()),
DSL.literal("obj"), DSL.jsonObject(
DSL.literal("internal"), DSL.literal("value")
)
);

// Verify
var value = exp.valueOf();
assertTrue(value instanceof ExprTupleValue);
assertEquals(expectedTupleExpr, value);
}

@Test
public void json_object_returns_empty_tuple() {
FunctionExpression exp;

// Setup
LinkedHashMap<String, ExprValue> objectMap = new LinkedHashMap<>();
ExprValue expectedTupleExpr = ExprTupleValue.fromExprValueMap(objectMap);

// exercise
exp = DSL.jsonObject();

// Verify
var value = exp.valueOf();
assertTrue(value instanceof ExprTupleValue);
assertEquals(expectedTupleExpr, value);
}

@Test
public void json_object_throws_SemanticCheckException() {
// wrong number of arguments
assertThrows(
SemanticCheckException.class, () -> DSL.jsonObject(DSL.literal("only one")).valueOf());
assertThrows(
SemanticCheckException.class, () -> DSL.jsonObject(DSL.literal("one"), DSL.literal("two"), DSL.literal("three")).valueOf());

// key argument is not a string
assertThrows(
SemanticCheckException.class, () -> DSL.jsonObject(DSL.literal(1234), DSL.literal("two")).valueOf());
assertThrows(
SemanticCheckException.class, () -> DSL.jsonObject(DSL.literal("one"), DSL.literal(true), DSL.literal(true), DSL.literal("four")).valueOf());
}
}
1 change: 1 addition & 0 deletions docs/category.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"user/ppl/functions/datetime.rst",
"user/ppl/functions/expressions.rst",
"user/ppl/functions/ip.rst",
"user/ppl/functions/json.rst",
"user/ppl/functions/math.rst",
"user/ppl/functions/relevance.rst",
"user/ppl/functions/string.rst"
Expand Down
3 changes: 2 additions & 1 deletion docs/user/dql/metadata.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Example 1: Show All Indices Information
SQL query::

os> SHOW TABLES LIKE '%'
fetched rows / total rows = 10/10
fetched rows / total rows = 11/11
+----------------+-------------+-----------------+------------+---------+----------+------------+-----------+---------------------------+----------------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS | TYPE_CAT | TYPE_SCHEM | TYPE_NAME | SELF_REFERENCING_COL_NAME | REF_GENERATION |
|----------------+-------------+-----------------+------------+---------+----------+------------+-----------+---------------------------+----------------|
Expand All @@ -44,6 +44,7 @@ SQL query::
| docTestCluster | null | accounts | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | apache | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | books | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | json_test | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | nested | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | nyc_taxi | BASE TABLE | null | null | null | null | null | null |
| docTestCluster | null | people | BASE TABLE | null | null | null | null | null | null |
Expand Down
Loading
Loading