Skip to content

Commit 7eb5979

Browse files
committed
Add JSON_OBJECT to ppl
Signed-off-by: Andrew Carbonetto <[email protected]>
1 parent a9721bf commit 7eb5979

File tree

8 files changed

+241
-3
lines changed

8 files changed

+241
-3
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,10 @@ public static FunctionExpression stringToJson(Expression value) {
691691
return compile(FunctionProperties.None, BuiltinFunctionName.JSON, value);
692692
}
693693

694+
public static FunctionExpression jsonObject(Expression... expressions) {
695+
return compile(FunctionProperties.None, BuiltinFunctionName.JSON_OBJECT, expressions);
696+
}
697+
694698
public static Aggregator avg(Expression... expressions) {
695699
return aggregate(BuiltinFunctionName.AVG, expressions);
696700
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ public enum BuiltinFunctionName {
207207
/** Json Functions. */
208208
JSON_VALID(FunctionName.of("json_valid")),
209209
JSON(FunctionName.of("json")),
210+
JSON_OBJECT(FunctionName.of("json_object")),
210211

211212
/** NULL Test. */
212213
IS_NULL(FunctionName.of("is null")),

core/src/main/java/org/opensearch/sql/expression/json/JsonFunctions.java

+76
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,41 @@
77

88
import static org.opensearch.sql.data.type.ExprCoreType.BOOLEAN;
99
import static org.opensearch.sql.data.type.ExprCoreType.STRING;
10+
import static org.opensearch.sql.data.type.ExprCoreType.STRUCT;
1011
import static org.opensearch.sql.data.type.ExprCoreType.UNDEFINED;
12+
import static org.opensearch.sql.expression.DSL.jsonObject;
1113
import static org.opensearch.sql.expression.function.FunctionDSL.define;
1214
import static org.opensearch.sql.expression.function.FunctionDSL.impl;
1315
import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling;
1416

17+
import java.util.Iterator;
18+
import java.util.LinkedHashMap;
19+
import java.util.List;
1520
import lombok.experimental.UtilityClass;
21+
import org.apache.commons.lang3.tuple.Pair;
22+
import org.opensearch.sql.data.model.ExprTupleValue;
23+
import org.opensearch.sql.data.model.ExprValue;
24+
import org.opensearch.sql.data.type.ExprCoreType;
25+
import org.opensearch.sql.data.type.ExprType;
26+
import org.opensearch.sql.exception.SemanticCheckException;
27+
import org.opensearch.sql.expression.Expression;
28+
import org.opensearch.sql.expression.FunctionExpression;
29+
import org.opensearch.sql.expression.env.Environment;
1630
import org.opensearch.sql.expression.function.BuiltinFunctionName;
1731
import org.opensearch.sql.expression.function.BuiltinFunctionRepository;
1832
import org.opensearch.sql.expression.function.DefaultFunctionResolver;
33+
import org.opensearch.sql.expression.function.FunctionBuilder;
34+
import org.opensearch.sql.expression.function.FunctionName;
35+
import org.opensearch.sql.expression.function.FunctionResolver;
36+
import org.opensearch.sql.expression.function.FunctionSignature;
1937
import org.opensearch.sql.utils.JsonUtils;
2038

2139
@UtilityClass
2240
public class JsonFunctions {
2341
public void register(BuiltinFunctionRepository repository) {
2442
repository.register(jsonValid());
2543
repository.register(jsonFunction());
44+
repository.register(jsonObject());
2645
}
2746

2847
private DefaultFunctionResolver jsonValid() {
@@ -35,4 +54,61 @@ private DefaultFunctionResolver jsonFunction() {
3554
BuiltinFunctionName.JSON.getName(),
3655
impl(nullMissingHandling(JsonUtils::castJson), UNDEFINED, STRING));
3756
}
57+
58+
/** Creates a JSON Object/tuple expr from a given list of kv pairs. */
59+
private static FunctionResolver jsonObject() {
60+
return new FunctionResolver() {
61+
@Override
62+
public FunctionName getFunctionName() {
63+
return BuiltinFunctionName.JSON_OBJECT.getName();
64+
}
65+
66+
@Override
67+
public Pair<FunctionSignature, FunctionBuilder> resolve(
68+
FunctionSignature unresolvedSignature) {
69+
List<ExprType> paramList = unresolvedSignature.getParamTypeList();
70+
// check that we got an even number of arguments
71+
if (paramList.size() % 2 != 0) {
72+
throw new SemanticCheckException(
73+
String.format(
74+
"Expected an even number of arguments but instead got %d arguments",
75+
paramList.size()));
76+
}
77+
78+
// check that each "key" argument (of key-value pair) is a string
79+
for (int i = 0; i < paramList.size(); i = i + 2) {
80+
ExprType paramType = paramList.get(i);
81+
if (!ExprCoreType.STRING.equals(paramType)) {
82+
throw new SemanticCheckException(
83+
String.format(
84+
"Expected type %s instead of %s for parameter #%d",
85+
ExprCoreType.STRING, paramType.typeName(), i + 1));
86+
}
87+
}
88+
89+
// return the unresolved signature and function builder
90+
return Pair.of(
91+
unresolvedSignature,
92+
(functionProperties, arguments) ->
93+
new FunctionExpression(getFunctionName(), arguments) {
94+
@Override
95+
public ExprValue valueOf(Environment<Expression, ExprValue> valueEnv) {
96+
LinkedHashMap<String, ExprValue> tupleValues = new LinkedHashMap<>();
97+
Iterator<Expression> iter = getArguments().iterator();
98+
while (iter.hasNext()) {
99+
tupleValues.put(
100+
iter.next().valueOf(valueEnv).stringValue(),
101+
iter.next().valueOf(valueEnv));
102+
}
103+
return ExprTupleValue.fromExprValueMap(tupleValues);
104+
}
105+
106+
@Override
107+
public ExprType type() {
108+
return STRUCT;
109+
}
110+
});
111+
}
112+
};
113+
}
38114
}

core/src/test/java/org/opensearch/sql/expression/json/JsonFunctionsTest.java

+76
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,80 @@ void json_returnsSemanticCheckException() {
188188
assertThrows(
189189
SemanticCheckException.class, () -> DSL.castJson(DSL.literal("\"missing quote")).valueOf());
190190
}
191+
192+
@Test
193+
public void json_object_returns_tuple() {
194+
FunctionExpression exp;
195+
196+
// Setup
197+
LinkedHashMap<String, ExprValue> objectMap = new LinkedHashMap<>();
198+
objectMap.put("foo", new ExprStringValue("foo"));
199+
objectMap.put("fuzz", ExprBooleanValue.of(true));
200+
objectMap.put("bar", new ExprLongValue(1234));
201+
objectMap.put("bar2", new ExprDoubleValue(12.34));
202+
objectMap.put("baz", ExprNullValue.of());
203+
objectMap.put(
204+
"obj", ExprTupleValue.fromExprValueMap(Map.of("internal", new ExprStringValue("value"))));
205+
// TODO: requires json_array()
206+
// objectMap.put(
207+
// "arr",
208+
// new ExprCollectionValue(
209+
// List.of(new ExprStringValue("string"), ExprBooleanValue.of(true),
210+
// ExprNullValue.of())));
211+
ExprValue expectedTupleExpr = ExprTupleValue.fromExprValueMap(objectMap);
212+
213+
// exercise
214+
exp =
215+
DSL.jsonObject(
216+
DSL.literal("foo"), DSL.literal("foo"),
217+
DSL.literal("fuzz"), DSL.literal(true),
218+
DSL.literal("bar"), DSL.literal(1234),
219+
DSL.literal("bar2"), DSL.literal(12.34),
220+
DSL.literal("baz"), DSL.literal(LITERAL_NULL),
221+
DSL.literal("obj"), DSL.jsonObject(DSL.literal("internal"), DSL.literal("value")));
222+
223+
// Verify
224+
var value = exp.valueOf();
225+
assertTrue(value instanceof ExprTupleValue);
226+
assertEquals(expectedTupleExpr, value);
227+
}
228+
229+
@Test
230+
public void json_object_returns_empty_tuple() {
231+
FunctionExpression exp;
232+
233+
// Setup
234+
LinkedHashMap<String, ExprValue> objectMap = new LinkedHashMap<>();
235+
ExprValue expectedTupleExpr = ExprTupleValue.fromExprValueMap(objectMap);
236+
237+
// exercise
238+
exp = DSL.jsonObject();
239+
240+
// Verify
241+
var value = exp.valueOf();
242+
assertTrue(value instanceof ExprTupleValue);
243+
assertEquals(expectedTupleExpr, value);
244+
}
245+
246+
@Test
247+
public void json_object_throws_SemanticCheckException() {
248+
// wrong number of arguments
249+
assertThrows(
250+
SemanticCheckException.class, () -> DSL.jsonObject(DSL.literal("only one")).valueOf());
251+
assertThrows(
252+
SemanticCheckException.class,
253+
() ->
254+
DSL.jsonObject(DSL.literal("one"), DSL.literal("two"), DSL.literal("three")).valueOf());
255+
256+
// key argument is not a string
257+
assertThrows(
258+
SemanticCheckException.class,
259+
() -> DSL.jsonObject(DSL.literal(1234), DSL.literal("two")).valueOf());
260+
assertThrows(
261+
SemanticCheckException.class,
262+
() ->
263+
DSL.jsonObject(
264+
DSL.literal("one"), DSL.literal(true), DSL.literal(true), DSL.literal("four"))
265+
.valueOf());
266+
}
191267
}

docs/user/ppl/functions/json.rst

+40
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,43 @@ Example::
5959
| json scalar string | "abc" | "abc" |
6060
| json empty string | | null |
6161
+---------------------+------------------------------+---------------+
62+
63+
JSON_OBJECT
64+
-----------
65+
66+
Description
67+
>>>>>>>>>>>
68+
69+
Usage: `json_object(<key>, <value>[, <key>, <value>]...)` returns a JSON object from key-value pairs.
70+
71+
Argument type:
72+
- A \<key\> must be STRING.
73+
- A \<value\> can be a scalar, another json object, or json array type. Note: scalar fields will be treated as single-value. Use `json_array` to construct an array value from a multi-value.
74+
75+
Return type: STRUCT
76+
77+
Example:
78+
79+
os> source=people | eval result = json_object('key', 123.45) | fields result
80+
fetched rows / total rows = 1/1
81+
+------------------+
82+
| result |
83+
+------------------+
84+
| {"key":123.45} |
85+
+------------------+
86+
87+
os> source=people | eval result = json_object('outer', json_object('inner', 123.45)) | fields result
88+
fetched rows / total rows = 1/1
89+
+------------------------------+
90+
| result |
91+
+------------------------------+
92+
| {"outer":{"inner":123.45}} |
93+
+------------------------------+
94+
95+
os> source=people | eval result = json_object('array_doc', json_array(123.45, "string", true, null)) | fields result
96+
fetched rows / total rows = 1/1
97+
+------------------------------------------------+
98+
| result |
99+
+------------------------------------------------+
100+
| {"array_doc":[123.45, "string", true, null]} |
101+
+------------------------------------------------+

integ-test/src/test/java/org/opensearch/sql/ppl/JsonFunctionsIT.java

+33-3
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,42 @@ public void test_cast_json_scalar_to_type() throws IOException {
166166
result =
167167
executeQuery(
168168
String.format(
169-
"source=%s | where test_name='json scalar boolean true' OR test_name='json scalar"
170-
+ " boolean false' | eval casted=cast(json(json_string) as boolean) | fields"
171-
+ " test_name, casted",
169+
"source=%s | " +
170+
"where test_name='json scalar boolean true' OR test_name='json scalar boolean false' | " +
171+
"eval casted=cast(json(json_string) as boolean) | " +
172+
"fields test_name, casted",
172173
TEST_INDEX_JSON_TEST));
173174
verifySchema(result, schema("test_name", null, "string"), schema("casted", null, "boolean"));
174175
verifyDataRows(
175176
result, rows("json scalar boolean true", true), rows("json scalar boolean false", false));
176177
}
178+
179+
@Test
180+
public void test_json_object() throws IOException {
181+
JSONObject result;
182+
183+
result =
184+
executeQuery(
185+
String.format(
186+
"source=%s | where json_valid(json_string) | " +
187+
"eval obj=json_object('key', json(json_string)) | " +
188+
"fields test_name, obj",
189+
TEST_INDEX_JSON_TEST));
190+
verifySchema(result, schema("test_name", null, "string"), schema("obj", null, "struct"));
191+
verifyDataRows(
192+
result,
193+
rows(
194+
"json nested object",
195+
new JSONObject(Map.of("key", Map.of("a", "1", "b", Map.of("c", "3"), "d", List.of(1, 2, 3))))),
196+
rows("json object", new JSONObject(Map.of("key", Map.of("a", "1", "b", "2")))),
197+
rows("json array", new JSONObject(Map.of("key", List.of(1, 2, 3, 4)))),
198+
rows("json scalar string", Map.of("key", "abc")),
199+
rows("json scalar int", Map.of("key", 1234)),
200+
rows("json scalar float", Map.of("key", 12.34)),
201+
rows("json scalar double", Map.of("key", 2.99792458e8)),
202+
rows("json scalar boolean true", Map.of("key", true)),
203+
rows("json scalar boolean false", Map.of("key", false)),
204+
rows("json empty string", Map.of())
205+
);
206+
}
177207
}

ppl/src/main/antlr/OpenSearchPPLLexer.g4

+1
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ CIDRMATCH: 'CIDRMATCH';
335335
// JSON FUNCTIONS
336336
JSON_VALID: 'JSON_VALID';
337337
JSON: 'JSON';
338+
JSON_OBJECT: 'JSON_OBJECT';
338339

339340
// FLOWCONTROL FUNCTIONS
340341
IFNULL: 'IFNULL';

ppl/src/main/antlr/OpenSearchPPLParser.g4

+10
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ valueExpression
310310
| extractFunction # extractFunctionCall
311311
| getFormatFunction # getFormatFunctionCall
312312
| timestampFunction # timestampFunctionCall
313+
| jsonObjectFunction # jsonObjectFunctionCall
313314
| LT_PRTHS valueExpression RT_PRTHS # parentheticValueExpr
314315
;
315316

@@ -324,6 +325,10 @@ positionFunction
324325
: positionFunctionName LT_PRTHS functionArg IN functionArg RT_PRTHS
325326
;
326327

328+
jsonObjectFunction
329+
: jsonObjectFunctionName LT_PRTHS (functionArg COMMA functionArg (COMMA functionArg COMMA functionArg)*)? RT_PRTHS
330+
;
331+
327332
booleanExpression
328333
: booleanFunctionCall
329334
;
@@ -420,6 +425,7 @@ evalFunctionName
420425
| flowControlFunctionName
421426
| systemFunctionName
422427
| positionFunctionName
428+
| jsonObjectFunctionName
423429
| jsonFunctionName
424430
;
425431

@@ -702,6 +708,10 @@ positionFunctionName
702708
: POSITION
703709
;
704710

711+
jsonObjectFunctionName
712+
: JSON_OBJECT
713+
;
714+
705715
jsonFunctionName
706716
: JSON
707717
;

0 commit comments

Comments
 (0)