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

Feature: PPL - json_set function #3271

Open
wants to merge 46 commits into
base: main
Choose a base branch
from

Conversation

andy-k-improving
Copy link
Contributor

@andy-k-improving andy-k-improving commented Jan 28, 2025

This PR depends on t he json_valid, json_object and json_extract:
#3262, #3242, #3230

Description

To introduce Json function json_set to perform insert || override with provided JsonPath String.

Related Issues

Resolves: #3214
Partially resolves: #3028

Check List

  • New functionality includes testing.
  • New functionality has been documented.
  • New functionality has javadoc added.
  • New functionality has a user manual doc added.
  • API changes companion pull request created.
  • Commits are signed per the DCO using --signoff.
  • Public documentation issue/PR created.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

14yapkc1 and others added 30 commits January 3, 2025 10:54
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Co-authored-by: Andrew Carbonetto <[email protected]>
Signed-off-by: kenrickyap <[email protected]>
….java

Co-authored-by: Andrew Carbonetto <[email protected]>
Signed-off-by: kenrickyap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Kenrick Yap <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
Signed-off-by: Andrew Carbonetto <[email protected]>
@YANG-DB
Copy link
Member

YANG-DB commented Jan 28, 2025

Hi @andy-k-improving
is this still in draft mode ? if not can u plz resolve the conflicts ?
thanks

@andy-k-improving
Copy link
Contributor Author

andy-k-improving commented Jan 28, 2025

Hi @andy-k-improving is this still in draft mode ? if not can u plz resolve the conflicts ? thanks

Functionality wide, this is ready.
however my code is based on some other json function as we share some util and Gradle deps.
#3262, #3242, #3230
Hence the conflict can only be fixed after above being merged I believe.

Signed-off-by: Andy Kwok <[email protected]>
Signed-off-by: Andy Kwok <[email protected]>
@andy-k-improving
Copy link
Contributor Author

@normanj-bitquill Fixed above comments, would you mind to have another look?

Copy link
Contributor

@currantw currantw left a comment

Choose a reason for hiding this comment

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

Reviewed to 2509fc8.

Comment on lines +58 to +69
BuiltinFunctionName.JSON_SET.getName(),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, STRING),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, LONG),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, SHORT),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, INTEGER),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, DOUBLE),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, BOOLEAN),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, ARRAY),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, STRUCT),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, BYTE),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, DATE),
impl(nullMissingHandling(JsonUtils::setJson), UNDEFINED, STRING, STRING, IP));
Copy link
Contributor

Choose a reason for hiding this comment

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

A few questions/thoughts:

  • Looks like you're missing FLOAT?
  • You're supporting DATE. What about TIME, TIMESTAMP, or INTERVAL types?
  • What about UNDEFINED? Is there anyway to set a JSON value to null?

Comment on lines +148 to +149
String jsonUnquoted = StringUtils.unquoteText(json.stringValue());
String pathUnquoted = StringUtils.unquoteText(path.stringValue());
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 this method need to use unquoteText but the other JSON methods above don't?

Comment on lines +151 to +161
Configuration conf =
Configuration.defaultConfiguration().addOptions(Option.SUPPRESS_EXCEPTIONS);
try {
JsonPath jsonPath = JsonPath.compile(pathUnquoted);
DocumentContext docContext = JsonPath.using(conf).parse(jsonUnquoted);
switch (docContext.read(jsonPath)) {
case null -> {
// Insert a new property
recursiveCreate(docContext, pathUnquoted, valueUnquoted);
return new ExprStringValue(docContext.jsonString());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are you using SUPPRESS_EXCEPTIONS here?

From the documentation:

Suppress all exceptions when evaluating path ... If an exception is thrown and the option ALWAYS_RETURN_LIST is not present null is returned.

Won't this mean that the first switch case (insert a new property) will be executed whenever an invalid path argument is given (i.e. if it isn't even valid JSON path syntax)? Why are we handling and throwing an exception if the path isn't a valid JSON path?

Copy link
Contributor

Choose a reason for hiding this comment

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

@kenrickyap current PR for json_expand handles InvalidPathException. Probably make sense for these two functions (and all other JSON functions) to handle this the same? Would it be possible to extract this logic to a shared method?

private static JsonPath getJsonPath(String jsonPathString) {

  try {
    return JsonPath.compile(jsonPathString);
  } catch () {
      final String errorMsgFormat = "JSON path '%s' is not valid. Error details: %s";
      throw new SemanticCheckException(String.format(errorMsgFormat, jsonPathString, e.getMessage()), e);
  }
}

}

/** Converts a JSON encoded string to an Expression object. */
public static ExprValue setJson(ExprValue json, ExprValue path, ExprValue valueToInsert) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What about the case where the JSON path points to multiple places? Does this method support inserting multiples values (it doesn't seem like it)? Would it just insert the first one, would it fail? Is this a case we should consider, and have an appropriate error message for it?

Comment on lines +167 to +171
case Iterable<?> ignored -> {
// New element in the array.
String updatedJson = docContext.add(pathUnquoted, valueUnquoted).jsonString();
return new ExprStringValue(updatedJson);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Am I understanding this correctly? If I call jsonSet with a JSON path that points to an array, this will insert the given value into the array, rather than set it equal to the given value?

Copy link
Contributor

Choose a reason for hiding this comment

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

What about the case where the JSON path points to a nested JSON object? Is a JSON object Iterable, and so would it fall into this case?

Comment on lines +185 to +201
/**
* Helper method to handle recursive scenario.
*
* @param docContext incoming Json in Java object form.
* @param path path in String to perform insertion.
* @param value value to be inserted with given path.
*/
private static void recursiveCreate(DocumentContext docContext, String path, Object value) {
final int pos = path.lastIndexOf('.');
final String parent = path.substring(0, pos);
final String current = path.substring(pos + 1);
// Attempt to read the current path as it is, trigger the recursive in case of deep insert.
if (docContext.read(parent) == null) {
recursiveCreate(docContext, parent, new LinkedHashMap<>());
}
docContext.put(parent, current, value);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

As mentioned in another comment, have you verified that this (recursive insertion) is not handled by WriteContext.set?

Comment on lines +168 to +172
void json_set_InsertString() {
FunctionExpression functionExpression =
DSL.jsonSet(DSL.literal("[]"), DSL.literal("$"), DSL.literal("test_value"));
assertEquals("[\"test_value\"]", functionExpression.valueOf().stringValue());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be worth extracting a helper function? Could help make this a big easier to read?

  void json_set_InsertString() {
    assertEquals("[\"test_value\"]", execute_set_json("[]", "$", "test_value"));
  }

static String execute_set_json(String jsonString, String jsonPathString, Object value) {
  ExprValue jsonExprValue = ExprValueUtils.stringValue(jsonString);
  ExprValue jsonPathExprValue = ExprValueUtils.stringValue(jsonPathString);
  ExprValue setExprValue = ExprValueUtils.fromObjectValue(value);

  FunctionExpression functionExpression = DSL.jsonSet(jsonExprValue, jsonPathExprValue, setExprValue);
  return functionExpression.valueOf().stringValue();
}


Usage: `json_set(json_string, json_path, value)` Perform value insertion or override with provided Json path and value. Returns the updated JSON object if valid, null otherwise.

Argument type: JSON_STRING, STRING, STRING/BOOLEAN/DOUBLE/INTEGER/NULL/STRUCT/ARRAY
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like you need to update this to include a few more supported types? Also, shouldn't this just be STRING instead of JSON_STRING?


Argument type: JSON_STRING, STRING, STRING/BOOLEAN/DOUBLE/INTEGER/NULL/STRUCT/ARRAY

Return type: JSON_STRING
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly, should this just be STRING?

Comment on lines +108 to +111
String.format(
"source=%s | eval updated=json_set(json_string, \\\"$.c.innerProperty\\\","
+ " \\\"test_value\\\") | fields test_name, updated",
TEST_INDEX_JSON_TEST));
Copy link
Contributor

Choose a reason for hiding this comment

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

I curious: why do we need so many escape characters here? Why doesn't one suffice?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEATURE] Add json_set function to PPL [RFC] Add PPL JSON extended functions support
7 participants