Skip to content

Commit d5806cc

Browse files
kenrickyap14yapkc1acarbonetto
authored
json-valid PPL function (#3230)
Add json-valid PPL function (#3230) --------- Signed-off-by: Kenrick Yap <[email protected]> Signed-off-by: Kenrick Yap <[email protected]> Signed-off-by: kenrickyap <[email protected]> Signed-off-by: Andrew Carbonetto <[email protected]> Co-authored-by: Kenrick Yap <[email protected]> Co-authored-by: Andrew Carbonetto <[email protected]>
1 parent e7be8ca commit d5806cc

File tree

19 files changed

+279
-3
lines changed

19 files changed

+279
-3
lines changed

core/src/main/java/org/opensearch/sql/expression/DSL.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,10 @@ public static FunctionExpression notLike(Expression... expressions) {
683683
return compile(FunctionProperties.None, BuiltinFunctionName.NOT_LIKE, expressions);
684684
}
685685

686+
public static FunctionExpression jsonValid(Expression... expressions) {
687+
return compile(FunctionProperties.None, BuiltinFunctionName.JSON_VALID, expressions);
688+
}
689+
686690
public static Aggregator avg(Expression... expressions) {
687691
return aggregate(BuiltinFunctionName.AVG, expressions);
688692
}

core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionName.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ public enum BuiltinFunctionName {
204204
TRIM(FunctionName.of("trim")),
205205
UPPER(FunctionName.of("upper")),
206206

207+
/** Json Functions. */
208+
JSON_VALID(FunctionName.of("json_valid")),
209+
207210
/** NULL Test. */
208211
IS_NULL(FunctionName.of("is null")),
209212
IS_NOT_NULL(FunctionName.of("is not null")),

core/src/main/java/org/opensearch/sql/expression/function/BuiltinFunctionRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.opensearch.sql.expression.datetime.DateTimeFunctions;
2929
import org.opensearch.sql.expression.datetime.IntervalClause;
3030
import org.opensearch.sql.expression.ip.IPFunctions;
31+
import org.opensearch.sql.expression.json.JsonFunctions;
3132
import org.opensearch.sql.expression.operator.arthmetic.ArithmeticFunctions;
3233
import org.opensearch.sql.expression.operator.arthmetic.MathematicalFunctions;
3334
import org.opensearch.sql.expression.operator.convert.TypeCastOperators;
@@ -83,6 +84,7 @@ public static synchronized BuiltinFunctionRepository getInstance() {
8384
SystemFunctions.register(instance);
8485
OpenSearchFunctions.register(instance);
8586
IPFunctions.register(instance);
87+
JsonFunctions.register(instance);
8688
}
8789
return instance;
8890
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.json;
7+
8+
import static org.opensearch.sql.data.type.ExprCoreType.BOOLEAN;
9+
import static org.opensearch.sql.data.type.ExprCoreType.STRING;
10+
import static org.opensearch.sql.expression.function.FunctionDSL.define;
11+
import static org.opensearch.sql.expression.function.FunctionDSL.impl;
12+
13+
import lombok.experimental.UtilityClass;
14+
import org.opensearch.sql.expression.function.BuiltinFunctionName;
15+
import org.opensearch.sql.expression.function.BuiltinFunctionRepository;
16+
import org.opensearch.sql.expression.function.DefaultFunctionResolver;
17+
import org.opensearch.sql.utils.JsonUtils;
18+
19+
@UtilityClass
20+
public class JsonFunctions {
21+
public void register(BuiltinFunctionRepository repository) {
22+
repository.register(jsonValid());
23+
}
24+
25+
private DefaultFunctionResolver jsonValid() {
26+
return define(
27+
BuiltinFunctionName.JSON_VALID.getName(), impl(JsonUtils::isValidJson, BOOLEAN, STRING));
28+
}
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.opensearch.sql.utils;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import lombok.experimental.UtilityClass;
6+
import org.opensearch.sql.data.model.ExprValue;
7+
import org.opensearch.sql.data.model.ExprValueUtils;
8+
9+
@UtilityClass
10+
public class JsonUtils {
11+
/**
12+
* Checks if given JSON string can be parsed as valid JSON.
13+
*
14+
* @param jsonExprValue JSON string (e.g. "{\"hello\": \"world\"}").
15+
* @return true if the string can be parsed as valid JSON, else false.
16+
*/
17+
public static ExprValue isValidJson(ExprValue jsonExprValue) {
18+
ObjectMapper objectMapper = new ObjectMapper();
19+
20+
if (jsonExprValue.isNull() || jsonExprValue.isMissing()) {
21+
return ExprValueUtils.LITERAL_FALSE;
22+
}
23+
24+
try {
25+
objectMapper.readTree(jsonExprValue.stringValue());
26+
return ExprValueUtils.LITERAL_TRUE;
27+
} catch (JsonProcessingException e) {
28+
return ExprValueUtils.LITERAL_FALSE;
29+
}
30+
}
31+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.expression.json;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertThrows;
10+
import static org.opensearch.sql.data.model.ExprValueUtils.LITERAL_FALSE;
11+
import static org.opensearch.sql.data.model.ExprValueUtils.LITERAL_MISSING;
12+
import static org.opensearch.sql.data.model.ExprValueUtils.LITERAL_NULL;
13+
import static org.opensearch.sql.data.model.ExprValueUtils.LITERAL_TRUE;
14+
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.ExtendWith;
17+
import org.mockito.junit.jupiter.MockitoExtension;
18+
import org.opensearch.sql.data.model.ExprValue;
19+
import org.opensearch.sql.data.model.ExprValueUtils;
20+
import org.opensearch.sql.exception.ExpressionEvaluationException;
21+
import org.opensearch.sql.expression.DSL;
22+
import org.opensearch.sql.expression.FunctionExpression;
23+
24+
@ExtendWith(MockitoExtension.class)
25+
public class JsonFunctionsTest {
26+
private static final ExprValue JsonNestedObject =
27+
ExprValueUtils.stringValue("{\"a\":\"1\",\"b\":{\"c\":\"2\",\"d\":\"3\"}}");
28+
private static final ExprValue JsonObject =
29+
ExprValueUtils.stringValue("{\"a\":\"1\",\"b\":\"2\"}");
30+
private static final ExprValue JsonArray = ExprValueUtils.stringValue("[1, 2, 3, 4]");
31+
private static final ExprValue JsonScalarString = ExprValueUtils.stringValue("\"abc\"");
32+
private static final ExprValue JsonEmptyString = ExprValueUtils.stringValue("");
33+
private static final ExprValue JsonInvalidObject =
34+
ExprValueUtils.stringValue("{\"invalid\":\"json\", \"string\"}");
35+
private static final ExprValue JsonInvalidScalar = ExprValueUtils.stringValue("abc");
36+
37+
@Test
38+
public void json_valid_returns_false() {
39+
assertEquals(LITERAL_FALSE, execute(JsonInvalidObject));
40+
assertEquals(LITERAL_FALSE, execute(JsonInvalidScalar));
41+
assertEquals(LITERAL_FALSE, execute(LITERAL_NULL));
42+
assertEquals(LITERAL_FALSE, execute(LITERAL_MISSING));
43+
}
44+
45+
@Test
46+
public void json_valid_throws_ExpressionEvaluationException() {
47+
assertThrows(
48+
ExpressionEvaluationException.class, () -> execute(ExprValueUtils.booleanValue(true)));
49+
}
50+
51+
@Test
52+
public void json_valid_returns_true() {
53+
assertEquals(LITERAL_TRUE, execute(JsonNestedObject));
54+
assertEquals(LITERAL_TRUE, execute(JsonObject));
55+
assertEquals(LITERAL_TRUE, execute(JsonArray));
56+
assertEquals(LITERAL_TRUE, execute(JsonScalarString));
57+
assertEquals(LITERAL_TRUE, execute(JsonEmptyString));
58+
}
59+
60+
private ExprValue execute(ExprValue jsonString) {
61+
FunctionExpression exp = DSL.jsonValid(DSL.literal(jsonString));
62+
return exp.valueOf();
63+
}
64+
}

docs/category.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"user/ppl/functions/datetime.rst",
3535
"user/ppl/functions/expressions.rst",
3636
"user/ppl/functions/ip.rst",
37+
"user/ppl/functions/json.rst",
3738
"user/ppl/functions/math.rst",
3839
"user/ppl/functions/relevance.rst",
3940
"user/ppl/functions/string.rst"

docs/user/dql/metadata.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Example 1: Show All Indices Information
3535
SQL query::
3636

3737
os> SHOW TABLES LIKE '%'
38-
fetched rows / total rows = 10/10
38+
fetched rows / total rows = 11/11
3939
+----------------+-------------+-----------------+------------+---------+----------+------------+-----------+---------------------------+----------------+
4040
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS | TYPE_CAT | TYPE_SCHEM | TYPE_NAME | SELF_REFERENCING_COL_NAME | REF_GENERATION |
4141
|----------------+-------------+-----------------+------------+---------+----------+------------+-----------+---------------------------+----------------|
@@ -44,6 +44,7 @@ SQL query::
4444
| docTestCluster | null | accounts | BASE TABLE | null | null | null | null | null | null |
4545
| docTestCluster | null | apache | BASE TABLE | null | null | null | null | null | null |
4646
| docTestCluster | null | books | BASE TABLE | null | null | null | null | null | null |
47+
| docTestCluster | null | json_test | BASE TABLE | null | null | null | null | null | null |
4748
| docTestCluster | null | nested | BASE TABLE | null | null | null | null | null | null |
4849
| docTestCluster | null | nyc_taxi | BASE TABLE | null | null | null | null | null | null |
4950
| docTestCluster | null | people | BASE TABLE | null | null | null | null | null | null |

docs/user/ppl/functions/json.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
====================
2+
JSON Functions
3+
====================
4+
5+
.. rubric:: Table of contents
6+
7+
.. contents::
8+
:local:
9+
:depth: 1
10+
11+
JSON_VALID
12+
----------
13+
14+
Description
15+
>>>>>>>>>>>
16+
17+
Usage: `json_valid(json_string)` checks if `json_string` is a valid JSON-encoded string.
18+
19+
Argument type: STRING
20+
21+
Return type: BOOLEAN
22+
23+
Example::
24+
25+
> source=json_test | eval is_valid = json_valid(json_string) | fields test_name, json_string, is_valid
26+
fetched rows / total rows = 6/6
27+
+---------------------+---------------------------------+----------+
28+
| test_name | json_string | is_valid |
29+
|---------------------|---------------------------------|----------|
30+
| json nested object | {"a":"1","b":{"c":"2","d":"3"}} | True |
31+
| json object | {"a":"1","b":"2"} | True |
32+
| json array | [1, 2, 3, 4] | True |
33+
| json scalar string | "abc" | True |
34+
| json empty string | | True |
35+
| json invalid object | {"invalid":"json", "string"} | False |
36+
+---------------------+---------------------------------+----------+

doctest/test_data/json_test.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{"test_name":"json nested object", "json_string":"{\"a\":\"1\",\"b\":{\"c\":\"2\",\"d\":\"3\"}}"}
2+
{"test_name":"json object", "json_string":"{\"a\":\"1\",\"b\":\"2\"}"}
3+
{"test_name":"json array", "json_string":"[1, 2, 3, 4]"}
4+
{"test_name":"json scalar string", "json_string":"\"abc\""}
5+
{"test_name":"json empty string","json_string":""}
6+
{"test_name":"json invalid object", "json_string":"{\"invalid\":\"json\", \"string\"}"}

0 commit comments

Comments
 (0)