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 function and cast(x as json) function #3243

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6dbf37b
added implementation
14yapkc1 Jan 3, 2025
b8c6d68
added doctest, integ-tests, and unit tests
14yapkc1 Jan 6, 2025
afb668c
addressed pr comments
kenrickyap Jan 7, 2025
54ef183
addressed PR comments
kenrickyap Jan 7, 2025
d841394
removed unused dependencies
kenrickyap Jan 7, 2025
25fb527
linting
kenrickyap Jan 7, 2025
4a20d08
addressed pr comment and rolling back disabled test case
kenrickyap Jan 8, 2025
fdc4729
removed disabled import
kenrickyap Jan 9, 2025
707a0b9
nit
kenrickyap Jan 9, 2025
4f28211
Update integ-test/src/test/java/org/opensearch/sql/ppl/JsonFunctionIT…
kenrickyap Jan 9, 2025
9ec6335
fixed integ test
kenrickyap Jan 9, 2025
3324e66
SQL: adding error case unit tests for json_valid
acarbonetto Jan 15, 2025
7123c35
json_valid: null and missing should return false
acarbonetto Jan 15, 2025
dbca991
PPL: Add json and cast to json functions
acarbonetto Jan 8, 2025
7df87cb
PPL: Update json cast for review
acarbonetto Jan 8, 2025
cd45fcc
Fix testes
acarbonetto Jan 9, 2025
6f5dc07
spotless
acarbonetto Jan 9, 2025
0aae36e
Fix tests
acarbonetto Jan 14, 2025
b225f28
SPOTLESS
acarbonetto Jan 14, 2025
78af4f8
Clean up for merge
acarbonetto Jan 15, 2025
b84282a
clean up unit tests
acarbonetto Jan 15, 2025
1e23286
Add casting from undefined
acarbonetto Jan 15, 2025
343f5a2
Add cast to scalar from undefined expression
acarbonetto Jan 16, 2025
e8b6df3
Add test for missing/null
acarbonetto Jan 16, 2025
ab9be75
Clean up merge conflicts
acarbonetto Jan 17, 2025
788be9d
Fix jacoco coverage
acarbonetto Jan 17, 2025
a9721bf
Move to Switch by json type
acarbonetto Jan 17, 2025
daa95ff
Merge branch 'main' into feature/acarbo_json_cast_ppl
acarbonetto Jan 20, 2025
c6c6cc1
Remove conflicted files
acarbonetto Jan 21, 2025
a5652ea
Add doctext row
acarbonetto Jan 21, 2025
65faa90
Adding more json tests
acarbonetto Jan 29, 2025
25b0f90
adding more json IT tests
acarbonetto Jan 29, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_FLOAT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_INT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_IP;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_JSON;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_LONG;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_SHORT;
import static org.opensearch.sql.expression.function.BuiltinFunctionName.CAST_TO_STRING;
Expand Down Expand Up @@ -56,6 +57,7 @@ public class Cast extends UnresolvedExpression {
.put("timestamp", CAST_TO_TIMESTAMP.getName())
.put("datetime", CAST_TO_DATETIME.getName())
.put("ip", CAST_TO_IP.getName())
.put("json", CAST_TO_JSON.getName())
.build();

/** The source expression cast from. */
Expand Down
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 @@ -687,6 +687,10 @@ public static FunctionExpression jsonValid(Expression... expressions) {
return compile(FunctionProperties.None, BuiltinFunctionName.JSON_VALID, expressions);
}

public static FunctionExpression stringToJson(Expression value) {
return compile(FunctionProperties.None, BuiltinFunctionName.JSON, value);
}

public static Aggregator avg(Expression... expressions) {
return aggregate(BuiltinFunctionName.AVG, expressions);
}
Expand Down Expand Up @@ -843,6 +847,10 @@ public static FunctionExpression castIp(Expression value) {
return compile(FunctionProperties.None, BuiltinFunctionName.CAST_TO_IP, value);
}

public static FunctionExpression castJson(Expression value) {
return compile(FunctionProperties.None, BuiltinFunctionName.CAST_TO_JSON, value);
}

public static FunctionExpression typeof(Expression value) {
return compile(FunctionProperties.None, BuiltinFunctionName.TYPEOF, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ public enum BuiltinFunctionName {

/** Json Functions. */
JSON_VALID(FunctionName.of("json_valid")),
JSON(FunctionName.of("json")),

/** NULL Test. */
IS_NULL(FunctionName.of("is null")),
Expand Down Expand Up @@ -235,6 +236,7 @@ public enum BuiltinFunctionName {
CAST_TO_TIMESTAMP(FunctionName.of("cast_to_timestamp")),
CAST_TO_DATETIME(FunctionName.of("cast_to_datetime")),
CAST_TO_IP(FunctionName.of("cast_to_ip")),
CAST_TO_JSON(FunctionName.of("cast_to_json")),
TYPEOF(FunctionName.of("typeof")),

/** Relevance Function. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

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.UNDEFINED;
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 lombok.experimental.UtilityClass;
import org.opensearch.sql.expression.function.BuiltinFunctionName;
Expand All @@ -20,10 +22,17 @@
public class JsonFunctions {
public void register(BuiltinFunctionRepository repository) {
repository.register(jsonValid());
repository.register(jsonFunction());
}

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

private DefaultFunctionResolver jsonFunction() {
return define(
BuiltinFunctionName.JSON.getName(),
impl(nullMissingHandling(JsonUtils::castJson), UNDEFINED, STRING));
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import static org.opensearch.sql.data.type.ExprCoreType.STRING;
import static org.opensearch.sql.data.type.ExprCoreType.TIME;
import static org.opensearch.sql.data.type.ExprCoreType.TIMESTAMP;
import static org.opensearch.sql.data.type.ExprCoreType.UNDEFINED;
import static org.opensearch.sql.expression.function.FunctionDSL.impl;
import static org.opensearch.sql.expression.function.FunctionDSL.implWithProperties;
import static org.opensearch.sql.expression.function.FunctionDSL.nullMissingHandling;
Expand All @@ -42,6 +43,7 @@
import org.opensearch.sql.expression.function.BuiltinFunctionRepository;
import org.opensearch.sql.expression.function.DefaultFunctionResolver;
import org.opensearch.sql.expression.function.FunctionDSL;
import org.opensearch.sql.utils.JsonUtils;

@UtilityClass
public class TypeCastOperators {
Expand All @@ -57,6 +59,7 @@ public static void register(BuiltinFunctionRepository repository) {
repository.register(castToDouble());
repository.register(castToBoolean());
repository.register(castToIp());
repository.register(castToJson());
repository.register(castToDate());
repository.register(castToTime());
repository.register(castToTimestamp());
Expand Down Expand Up @@ -105,7 +108,8 @@ private static DefaultFunctionResolver castToShort() {
impl(
nullMissingHandling((v) -> new ExprShortValue(v.booleanValue() ? 1 : 0)),
SHORT,
BOOLEAN));
BOOLEAN),
impl(nullMissingHandling((v) -> v), SHORT, UNDEFINED));
}

private static DefaultFunctionResolver castToInt() {
Expand All @@ -119,7 +123,8 @@ private static DefaultFunctionResolver castToInt() {
impl(
nullMissingHandling((v) -> new ExprIntegerValue(v.booleanValue() ? 1 : 0)),
INTEGER,
BOOLEAN));
BOOLEAN),
impl(nullMissingHandling((v) -> v), INTEGER, UNDEFINED));
}

private static DefaultFunctionResolver castToLong() {
Expand All @@ -133,7 +138,8 @@ private static DefaultFunctionResolver castToLong() {
impl(
nullMissingHandling((v) -> new ExprLongValue(v.booleanValue() ? 1L : 0L)),
LONG,
BOOLEAN));
BOOLEAN),
impl(nullMissingHandling((v) -> v), LONG, UNDEFINED));
}

private static DefaultFunctionResolver castToFloat() {
Expand All @@ -147,7 +153,8 @@ private static DefaultFunctionResolver castToFloat() {
impl(
nullMissingHandling((v) -> new ExprFloatValue(v.booleanValue() ? 1f : 0f)),
FLOAT,
BOOLEAN));
BOOLEAN),
impl(nullMissingHandling((v) -> v), FLOAT, UNDEFINED));
}

private static DefaultFunctionResolver castToDouble() {
Expand All @@ -161,7 +168,8 @@ private static DefaultFunctionResolver castToDouble() {
impl(
nullMissingHandling((v) -> new ExprDoubleValue(v.booleanValue() ? 1D : 0D)),
DOUBLE,
BOOLEAN));
BOOLEAN),
impl(nullMissingHandling((v) -> v), DOUBLE, UNDEFINED));
}

private static DefaultFunctionResolver castToBoolean() {
Expand All @@ -173,7 +181,8 @@ private static DefaultFunctionResolver castToBoolean() {
STRING),
impl(
nullMissingHandling((v) -> ExprBooleanValue.of(v.doubleValue() != 0)), BOOLEAN, DOUBLE),
impl(nullMissingHandling((v) -> v), BOOLEAN, BOOLEAN));
impl(nullMissingHandling((v) -> v), BOOLEAN, BOOLEAN),
impl(nullMissingHandling((v) -> v), BOOLEAN, UNDEFINED));
}

private static DefaultFunctionResolver castToIp() {
Expand All @@ -183,6 +192,12 @@ private static DefaultFunctionResolver castToIp() {
impl(nullMissingHandling((v) -> v), IP, IP));
}

private static DefaultFunctionResolver castToJson() {
return FunctionDSL.define(
BuiltinFunctionName.CAST_TO_JSON.getName(),
impl(nullMissingHandling(JsonUtils::castJson), UNDEFINED, STRING));
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
}

private static DefaultFunctionResolver castToDate() {
return FunctionDSL.define(
BuiltinFunctionName.CAST_TO_DATE.getName(),
Expand Down
82 changes: 79 additions & 3 deletions core/src/main/java/org/opensearch/sql/utils/JsonUtils.java
Copy link
Member

Choose a reason for hiding this comment

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

I think a license header is missing...

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 add.
I'm surprised that it isn't added automagically by spotless.

Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
package org.opensearch.sql.utils;

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 com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import lombok.experimental.UtilityClass;
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.ExprIntegerValue;
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;

@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.
* @return true if the string can be parsed as valid JSON, else false (including null or missing).
*/
public static ExprValue isValidJson(ExprValue jsonExprValue) {
ObjectMapper objectMapper = new ObjectMapper();
Expand All @@ -23,9 +40,68 @@ public static ExprValue isValidJson(ExprValue jsonExprValue) {

try {
objectMapper.readTree(jsonExprValue.stringValue());
return ExprValueUtils.LITERAL_TRUE;
return LITERAL_TRUE;
} catch (JsonProcessingException e) {
return ExprValueUtils.LITERAL_FALSE;
return LITERAL_FALSE;
}
}

/**
* Converts a JSON encoded string to a {@link ExprValue}. Expression type will be UNDEFINED.
*
* @param json JSON string (e.g. "{\"hello\": \"world\"}").
* @return ExprValue returns an expression that best represents the provided JSON-encoded string.
* <ol>
* <li>{@link ExprTupleValue} if the JSON is an object
* <li>{@link ExprCollectionValue} if the JSON is an array
* <li>{@link ExprDoubleValue} if the JSON is a floating-point number scalar
* <li>{@link ExprIntegerValue} if the JSON is an integral number scalar
* <li>{@link ExprStringValue} if the JSON is a string scalar
* <li>{@link ExprBooleanValue} if the JSON is a boolean scalar
* <li>{@link ExprNullValue} if the JSON is null, empty, or invalid
* </ol>
*/
public static ExprValue castJson(ExprValue json) {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode;
try {
jsonNode = objectMapper.readTree(json.stringValue());
} catch (JsonProcessingException e) {
final String errorFormat = "JSON string '%s' is not valid. Error details: %s";
throw new SemanticCheckException(String.format(errorFormat, json, e.getMessage()), e);
}

return processJsonNode(jsonNode);
}

private static ExprValue processJsonNode(JsonNode jsonNode) {
switch (jsonNode.getNodeType()) {
case ARRAY:
List<ExprValue> elements = new LinkedList<>();
for (var iter = jsonNode.iterator(); iter.hasNext(); ) {
jsonNode = iter.next();
elements.add(processJsonNode(jsonNode));
}
return new ExprCollectionValue(elements);
case OBJECT:
Map<String, ExprValue> values = new LinkedHashMap<>();
for (var iter = jsonNode.fields(); iter.hasNext(); ) {
Map.Entry<String, JsonNode> entry = iter.next();
values.put(entry.getKey(), processJsonNode(entry.getValue()));
}
return ExprTupleValue.fromExprValueMap(values);
case STRING:
return new ExprStringValue(jsonNode.asText());
case NUMBER:
if (jsonNode.isFloatingPointNumber()) {
return new ExprDoubleValue(jsonNode.asDouble());
}
return new ExprIntegerValue(jsonNode.asLong());
case BOOLEAN:
return jsonNode.asBoolean() ? LITERAL_TRUE : LITERAL_FALSE;
default:
// in all other cases, return null
return LITERAL_NULL;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,8 @@ public void testWeekFormats(
expectedInteger);
}

// subtracting 1 as a temporary fix for year 2024.
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
// Issue: https://github.com/opensearch-project/sql/issues/2477
@Test
public void testWeekOfYearWithTimeType() {
LocalDate today = LocalDate.now(functionProperties.getQueryStartClock());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ public void testYearweekWithoutMode() {
assertEquals(eval(expression), eval(expressionWithoutMode));
}

// subtracting 1 as a temporary fix for year 2024.
acarbonetto marked this conversation as resolved.
Show resolved Hide resolved
// Issue: https://github.com/opensearch-project/sql/issues/2477
@Test
public void testYearweekWithTimeType() {
int expected = getYearWeekBeforeSunday(LocalDate.now(functionProperties.getQueryStartClock()));
Expand Down
Loading
Loading