-
Notifications
You must be signed in to change notification settings - Fork 141
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
acarbonetto
wants to merge
11
commits into
opensearch-project:main
Choose a base branch
from
Bit-Quill:feature/acarbo_json_object_ppl
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
70152eb
added implementation
14yapkc1 76c3995
added doctest, integ-tests, and unit tests
14yapkc1 ce2c551
addressed PR comments
kenrickyap ad1bde3
fixed unit tests
kenrickyap ccf47a2
addressed pr comments
kenrickyap acc76a0
addressed PR comments
kenrickyap 519c6f2
removed unused dependencies
kenrickyap 2e319fe
linting
kenrickyap ee0820d
addressed pr comment and rolling back disabled test case
kenrickyap d44fc5a
Merge branch 'main' into feature/json-valid
kenrickyap 27299f2
PPL: Add JSON_OBJECT function
acarbonetto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
core/src/main/java/org/opensearch/sql/expression/json/JsonFunctions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
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
26
core/src/main/java/org/opensearch/sql/utils/JsonUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
134 changes: 134 additions & 0 deletions
134
core/src/test/java/org/opensearch/sql/expression/json/JsonFunctionsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 likeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will remove