Jackie-Jiang commented on code in PR #16306:
URL: https://github.com/apache/pinot/pull/16306#discussion_r2211031503
##########
pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java:
##########
@@ -321,17 +326,296 @@ private static void setValuesToMap(String keyColumnName,
String valueColumnName,
Map<String, String> objMap = (Map) obj;
result.put(objMap.get(keyColumnName), objMap.get(valueColumnName));
} else {
- ObjectMapper mapper = new ObjectMapper();
JsonNode mapNode;
try {
- mapNode = mapper.readTree(obj.toString());
- } catch (JsonProcessingException e) {
+ mapNode = JsonUtils.stringToJsonNode(obj.toString());
+ } catch (IOException e) {
throw new RuntimeException(e);
}
result.put(mapNode.get(keyColumnName).asText(),
mapNode.get(valueColumnName).asText());
}
}
+ /**
+ * Extract keys from JSON object using JsonPath.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*') returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 2) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1) returns ["$['a']"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @return List of key paths matching the JsonPath, empty list if input is
null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath)
+ throws IOException {
+ return jsonExtractKey(jsonObj, jsonPath, Integer.MAX_VALUE, false);
+ }
+
+ /**
+ * Extract keys from JSON object using JsonPath with depth limit.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 1) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 2) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1) returns ["$['a']"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @param maxDepth Maximum depth to recurse (must be positive)
+ * @return List of key paths matching the JsonPath up to maxDepth, empty
list if input is null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath, int
maxDepth)
+ throws IOException {
+ return jsonExtractKey(jsonObj, jsonPath, maxDepth, false);
+ }
+
+ /**
+ * Extract keys from JSON object using JsonPath with depth limit and output
format option.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 1, false) returns
["$['a']", "$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$..**', 2, true) returns
["a", "b", "b.c"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1, true) returns ["a"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @param maxDepth Maximum depth to recurse (must be positive)
+ * @param dotNotation If true, return keys in dot notation (e.g., "a.b.c"),
+ * if false, return JsonPath format (e.g.,
"$['a']['b']['c']")
+ * @return List of key paths matching the JsonPath up to maxDepth, empty
list if input is null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath, int
maxDepth, boolean dotNotation)
+ throws IOException {
+ if (maxDepth <= 0) {
+ return java.util.Collections.emptyList();
+ }
+
+ // Special handling for $.** and $.. recursive key extraction
+ if ("$..**".equals(jsonPath) || "$..".equals(jsonPath)) {
+ return extractAllKeysRecursively(jsonObj, maxDepth, dotNotation);
+ }
+
+ // For other expressions, try to get keys using AS_PATH_LIST
+ List<String> keys = null;
+ try {
+ keys = KEY_PARSE_CONTEXT.parse(
+ jsonObj instanceof String ? (String) jsonObj :
jsonObj).read(jsonPath);
+ } catch (Exception e) {
+ // AS_PATH_LIST might not work for all expressions
+ }
+
+ // If AS_PATH_LIST doesn't work, fall back to manual path construction
+ if (keys == null || keys.isEmpty()) {
+ return extractKeysForNonRecursiveExpression(jsonObj, jsonPath, maxDepth,
dotNotation);
+ }
+
+ // Filter keys by depth if maxDepth is specified
+ if (maxDepth != Integer.MAX_VALUE) {
+ keys = keys.stream()
+ .filter(key -> getKeyDepth(key) <= maxDepth)
+ .collect(java.util.stream.Collectors.toList());
+ }
+
+ // Convert to dot notation if requested
+ if (dotNotation) {
+ keys = keys.stream()
+ .map(JsonFunctions::convertToDotNotation)
+ .collect(java.util.stream.Collectors.toList());
+ }
+
+ return keys;
+ }
+
+ /**
+ * Extract keys for non-recursive expressions by manually constructing paths
+ */
+ private static List<String> extractKeysForNonRecursiveExpression(Object
jsonObj, String jsonPath,
+ int maxDepth, boolean dotNotation)
+ throws IOException {
+ JsonNode node;
+ if (jsonObj instanceof String) {
+ node = JsonUtils.stringToJsonNode((String) jsonObj);
+ } else {
+ node = JsonUtils.stringToJsonNode(JsonUtils.objectToString(jsonObj));
+ }
+
+ List<String> keys = new java.util.ArrayList<>();
+
+ // Handle common patterns
+ if ("$.*".equals(jsonPath)) {
+ // Top level keys
+ if (node.isObject()) {
+ node.fieldNames().forEachRemaining(fieldName -> {
+ String path = "$['" + fieldName + "']";
+ if (dotNotation) {
+ keys.add(fieldName);
+ } else {
+ keys.add(path);
+ }
+ });
+ } else if (node.isArray()) {
+ for (int i = 0; i < node.size(); i++) {
+ String path = "$[" + i + "]";
+ if (dotNotation) {
+ keys.add(String.valueOf(i));
+ } else {
+ keys.add(path);
+ }
+ }
+ }
+ } else if (jsonPath.matches("\\$\\.[^.]+\\.\\*")) {
+ // Pattern like $.field.*
+ String fieldPath = jsonPath.substring(2, jsonPath.length() - 2); //
Remove $. and .*
+ JsonNode targetNode = node.get(fieldPath);
+ if (targetNode != null) {
+ if (targetNode.isObject()) {
+ targetNode.fieldNames().forEachRemaining(fieldName -> {
+ String path = "$['" + fieldPath + "']['" + fieldName + "']";
+ if (dotNotation) {
+ keys.add(fieldPath + "." + fieldName);
+ } else {
+ keys.add(path);
+ }
+ });
+ } else if (targetNode.isArray()) {
+ for (int i = 0; i < targetNode.size(); i++) {
+ String path = "$['" + fieldPath + "'][" + i + "]";
+ if (dotNotation) {
+ keys.add(fieldPath + "." + i);
+ } else {
+ keys.add(path);
+ }
+ }
+ }
+ }
+ }
+ return keys;
+ }
+
+ /**
+ * Extract all keys recursively from a JSON object up to maxDepth
+ */
+ private static List<String> extractAllKeysRecursively(Object jsonObj, int
maxDepth) {
+ return extractAllKeysRecursively(jsonObj, maxDepth, false);
+ }
+
+ /**
+ * Extract all keys recursively from a JSON object up to maxDepth with
output format option
+ */
+ private static List<String> extractAllKeysRecursively(Object jsonObj, int
maxDepth, boolean dotNotation) {
+ List<String> allKeys = new java.util.ArrayList<>();
+ try {
+ JsonNode node;
+ if (jsonObj instanceof String) {
+ node = JsonUtils.stringToJsonNode((String) jsonObj);
+ } else {
+ node = JsonUtils.stringToJsonNode(JsonUtils.objectToString(jsonObj));
+ }
+
+ extractKeysFromNode(node, "$", allKeys, maxDepth, 1, dotNotation);
+ } catch (Exception e) {
+ // Return empty list on error
+ }
+ return allKeys;
+ }
+
+ /**
+ * Recursively extract keys from a JsonNode
+ */
+ private static void extractKeysFromNode(JsonNode node, String currentPath,
List<String> keys,
+ int maxDepth, int currentDepth) {
+ extractKeysFromNode(node, currentPath, keys, maxDepth, currentDepth,
false);
+ }
+
+ /**
+ * Recursively extract keys from a JsonNode with output format option
+ */
+ private static void extractKeysFromNode(JsonNode node, String currentPath,
List<String> keys,
+ int maxDepth, int currentDepth, boolean dotNotation) {
+ if (currentDepth > maxDepth) {
+ return;
+ }
+
+ if (node.isObject()) {
+ node.fieldNames().forEachRemaining(fieldName -> {
+ String newPath = currentPath + "['" + fieldName + "']";
+ String keyToAdd = dotNotation ? convertToDotNotation(newPath) :
newPath;
+ keys.add(keyToAdd);
+
+ JsonNode childNode = node.get(fieldName);
+ if (currentDepth < maxDepth && (childNode.isObject() ||
childNode.isArray())) {
+ extractKeysFromNode(childNode, newPath, keys, maxDepth, currentDepth
+ 1, dotNotation);
+ }
+ });
+ } else if (node.isArray()) {
+ for (int i = 0; i < node.size(); i++) {
+ String newPath = currentPath + "[" + i + "]";
+ String keyToAdd = dotNotation ? convertToDotNotation(newPath) :
newPath;
+ keys.add(keyToAdd);
+
+ JsonNode childNode = node.get(i);
+ if (currentDepth < maxDepth && (childNode.isObject() ||
childNode.isArray())) {
+ extractKeysFromNode(childNode, newPath, keys, maxDepth, currentDepth
+ 1, dotNotation);
+ }
+ }
+ }
+ }
+
+ /**
+ * Convert JsonPath format to dot notation
+ * Example: $['a']['b']['c'] -> a.b.c
+ * $[0]['name'] -> 0.name
+ */
+ private static String convertToDotNotation(String jsonPath) {
Review Comment:
This is super expensive, especially if we do it on a per row basis. We
should avoid regexp replacement
##########
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractKeyTransformFunction.java:
##########
@@ -76,7 +84,41 @@ public void init(List<TransformFunction> arguments,
Map<String, ColumnContext> c
+ "function");
}
_jsonFieldTransformFunction = firstArgument;
- _jsonPath =
JsonPathCache.INSTANCE.getOrCompute(((LiteralTransformFunction)
arguments.get(1)).getStringLiteral());
+ _jsonPath = ((LiteralTransformFunction)
arguments.get(1)).getStringLiteral();
+
+ // Handle optional third argument (maxDepth)
+ if (arguments.size() >= 3) {
+ TransformFunction depthArgument = arguments.get(2);
+ if (!(depthArgument instanceof LiteralTransformFunction)) {
+ throw new IllegalArgumentException("The third argument (maxDepth) must
be a literal integer");
+ }
+ try {
+ _maxDepth = Integer.parseInt(((LiteralTransformFunction)
depthArgument).getStringLiteral());
+ if (_maxDepth <= 0) {
+ throw new IllegalArgumentException("maxDepth must be a positive
integer");
+ }
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("The third argument (maxDepth) must
be a valid integer");
+ }
+ }
+
+ // Handle optional fourth argument (dotNotation)
+ if (arguments.size() == 4) {
+ TransformFunction dotNotationArgument = arguments.get(3);
+ if (!(dotNotationArgument instanceof LiteralTransformFunction)) {
+ throw new IllegalArgumentException("The fourth argument (dotNotation)
must be a literal boolean");
+ }
+ try {
+ String dotNotationStr = ((LiteralTransformFunction)
dotNotationArgument).getStringLiteral();
Review Comment:
You can use `getBooleanLiteral()`
##########
pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java:
##########
@@ -321,17 +326,296 @@ private static void setValuesToMap(String keyColumnName,
String valueColumnName,
Map<String, String> objMap = (Map) obj;
result.put(objMap.get(keyColumnName), objMap.get(valueColumnName));
} else {
- ObjectMapper mapper = new ObjectMapper();
JsonNode mapNode;
try {
- mapNode = mapper.readTree(obj.toString());
- } catch (JsonProcessingException e) {
+ mapNode = JsonUtils.stringToJsonNode(obj.toString());
+ } catch (IOException e) {
throw new RuntimeException(e);
}
result.put(mapNode.get(keyColumnName).asText(),
mapNode.get(valueColumnName).asText());
}
}
+ /**
+ * Extract keys from JSON object using JsonPath.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*') returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 2) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1) returns ["$['a']"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @return List of key paths matching the JsonPath, empty list if input is
null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath)
+ throws IOException {
+ return jsonExtractKey(jsonObj, jsonPath, Integer.MAX_VALUE, false);
+ }
+
+ /**
+ * Extract keys from JSON object using JsonPath with depth limit.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 1) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 2) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1) returns ["$['a']"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @param maxDepth Maximum depth to recurse (must be positive)
+ * @return List of key paths matching the JsonPath up to maxDepth, empty
list if input is null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath, int
maxDepth)
+ throws IOException {
+ return jsonExtractKey(jsonObj, jsonPath, maxDepth, false);
+ }
+
+ /**
+ * Extract keys from JSON object using JsonPath with depth limit and output
format option.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 1, false) returns
["$['a']", "$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$..**', 2, true) returns
["a", "b", "b.c"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1, true) returns ["a"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @param maxDepth Maximum depth to recurse (must be positive)
+ * @param dotNotation If true, return keys in dot notation (e.g., "a.b.c"),
+ * if false, return JsonPath format (e.g.,
"$['a']['b']['c']")
+ * @return List of key paths matching the JsonPath up to maxDepth, empty
list if input is null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath, int
maxDepth, boolean dotNotation)
+ throws IOException {
+ if (maxDepth <= 0) {
+ return java.util.Collections.emptyList();
+ }
+
+ // Special handling for $.** and $.. recursive key extraction
+ if ("$..**".equals(jsonPath) || "$..".equals(jsonPath)) {
+ return extractAllKeysRecursively(jsonObj, maxDepth, dotNotation);
+ }
+
+ // For other expressions, try to get keys using AS_PATH_LIST
+ List<String> keys = null;
+ try {
+ keys = KEY_PARSE_CONTEXT.parse(
+ jsonObj instanceof String ? (String) jsonObj :
jsonObj).read(jsonPath);
+ } catch (Exception e) {
+ // AS_PATH_LIST might not work for all expressions
+ }
+
+ // If AS_PATH_LIST doesn't work, fall back to manual path construction
+ if (keys == null || keys.isEmpty()) {
+ return extractKeysForNonRecursiveExpression(jsonObj, jsonPath, maxDepth,
dotNotation);
+ }
+
+ // Filter keys by depth if maxDepth is specified
+ if (maxDepth != Integer.MAX_VALUE) {
+ keys = keys.stream()
+ .filter(key -> getKeyDepth(key) <= maxDepth)
+ .collect(java.util.stream.Collectors.toList());
+ }
+
+ // Convert to dot notation if requested
+ if (dotNotation) {
+ keys = keys.stream()
+ .map(JsonFunctions::convertToDotNotation)
+ .collect(java.util.stream.Collectors.toList());
+ }
+
+ return keys;
+ }
+
+ /**
+ * Extract keys for non-recursive expressions by manually constructing paths
+ */
+ private static List<String> extractKeysForNonRecursiveExpression(Object
jsonObj, String jsonPath,
+ int maxDepth, boolean dotNotation)
+ throws IOException {
+ JsonNode node;
+ if (jsonObj instanceof String) {
+ node = JsonUtils.stringToJsonNode((String) jsonObj);
+ } else {
+ node = JsonUtils.stringToJsonNode(JsonUtils.objectToString(jsonObj));
+ }
+
+ List<String> keys = new java.util.ArrayList<>();
+
+ // Handle common patterns
+ if ("$.*".equals(jsonPath)) {
+ // Top level keys
+ if (node.isObject()) {
+ node.fieldNames().forEachRemaining(fieldName -> {
+ String path = "$['" + fieldName + "']";
+ if (dotNotation) {
+ keys.add(fieldName);
+ } else {
+ keys.add(path);
+ }
+ });
+ } else if (node.isArray()) {
+ for (int i = 0; i < node.size(); i++) {
+ String path = "$[" + i + "]";
+ if (dotNotation) {
+ keys.add(String.valueOf(i));
+ } else {
+ keys.add(path);
+ }
+ }
+ }
+ } else if (jsonPath.matches("\\$\\.[^.]+\\.\\*")) {
+ // Pattern like $.field.*
+ String fieldPath = jsonPath.substring(2, jsonPath.length() - 2); //
Remove $. and .*
+ JsonNode targetNode = node.get(fieldPath);
+ if (targetNode != null) {
+ if (targetNode.isObject()) {
+ targetNode.fieldNames().forEachRemaining(fieldName -> {
+ String path = "$['" + fieldPath + "']['" + fieldName + "']";
+ if (dotNotation) {
+ keys.add(fieldPath + "." + fieldName);
+ } else {
+ keys.add(path);
+ }
+ });
+ } else if (targetNode.isArray()) {
+ for (int i = 0; i < targetNode.size(); i++) {
+ String path = "$['" + fieldPath + "'][" + i + "]";
+ if (dotNotation) {
+ keys.add(fieldPath + "." + i);
+ } else {
+ keys.add(path);
+ }
+ }
+ }
+ }
+ }
+ return keys;
+ }
+
+ /**
+ * Extract all keys recursively from a JSON object up to maxDepth
+ */
+ private static List<String> extractAllKeysRecursively(Object jsonObj, int
maxDepth) {
+ return extractAllKeysRecursively(jsonObj, maxDepth, false);
+ }
+
+ /**
+ * Extract all keys recursively from a JSON object up to maxDepth with
output format option
+ */
+ private static List<String> extractAllKeysRecursively(Object jsonObj, int
maxDepth, boolean dotNotation) {
+ List<String> allKeys = new java.util.ArrayList<>();
+ try {
+ JsonNode node;
+ if (jsonObj instanceof String) {
+ node = JsonUtils.stringToJsonNode((String) jsonObj);
+ } else {
+ node = JsonUtils.stringToJsonNode(JsonUtils.objectToString(jsonObj));
+ }
+
+ extractKeysFromNode(node, "$", allKeys, maxDepth, 1, dotNotation);
+ } catch (Exception e) {
+ // Return empty list on error
+ }
+ return allKeys;
+ }
+
+ /**
+ * Recursively extract keys from a JsonNode
+ */
+ private static void extractKeysFromNode(JsonNode node, String currentPath,
List<String> keys,
+ int maxDepth, int currentDepth) {
+ extractKeysFromNode(node, currentPath, keys, maxDepth, currentDepth,
false);
+ }
+
+ /**
+ * Recursively extract keys from a JsonNode with output format option
+ */
+ private static void extractKeysFromNode(JsonNode node, String currentPath,
List<String> keys,
+ int maxDepth, int currentDepth, boolean dotNotation) {
+ if (currentDepth > maxDepth) {
+ return;
+ }
+
+ if (node.isObject()) {
+ node.fieldNames().forEachRemaining(fieldName -> {
+ String newPath = currentPath + "['" + fieldName + "']";
+ String keyToAdd = dotNotation ? convertToDotNotation(newPath) :
newPath;
Review Comment:
When dot notation is enabled, can we also pass the key so that we don't need
to do regexp replace? We can directly construct the dot notation key
##########
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractKeyTransformFunction.java:
##########
@@ -76,7 +84,41 @@ public void init(List<TransformFunction> arguments,
Map<String, ColumnContext> c
+ "function");
}
_jsonFieldTransformFunction = firstArgument;
- _jsonPath =
JsonPathCache.INSTANCE.getOrCompute(((LiteralTransformFunction)
arguments.get(1)).getStringLiteral());
+ _jsonPath = ((LiteralTransformFunction)
arguments.get(1)).getStringLiteral();
+
+ // Handle optional third argument (maxDepth)
+ if (arguments.size() >= 3) {
+ TransformFunction depthArgument = arguments.get(2);
+ if (!(depthArgument instanceof LiteralTransformFunction)) {
+ throw new IllegalArgumentException("The third argument (maxDepth) must
be a literal integer");
+ }
+ try {
+ _maxDepth = Integer.parseInt(((LiteralTransformFunction)
depthArgument).getStringLiteral());
+ if (_maxDepth <= 0) {
Review Comment:
Should we treat non-positive max depth as unlimited depth?
##########
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractKeyTransformFunction.java:
##########
@@ -40,8 +41,12 @@
*
* Usage:
* jsonExtractKey(jsonFieldName, 'jsonPath')
+ * jsonExtractKey(jsonFieldName, 'jsonPath', maxDepth)
+ * jsonExtractKey(jsonFieldName, 'jsonPath', maxDepth, dotNotation)
Review Comment:
For the optional parameters, it is not easy to use if we force the order,
e.g. when user only want to set `dotNotation` but not `maxDepth`
One way to handle it is to make it one single argument keyed by the
parameter name. See `DistinctCountSmartHLLAggregationFunction.Parameters` as an
example
##########
pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java:
##########
@@ -321,17 +326,296 @@ private static void setValuesToMap(String keyColumnName,
String valueColumnName,
Map<String, String> objMap = (Map) obj;
result.put(objMap.get(keyColumnName), objMap.get(valueColumnName));
} else {
- ObjectMapper mapper = new ObjectMapper();
JsonNode mapNode;
try {
- mapNode = mapper.readTree(obj.toString());
- } catch (JsonProcessingException e) {
+ mapNode = JsonUtils.stringToJsonNode(obj.toString());
+ } catch (IOException e) {
throw new RuntimeException(e);
}
result.put(mapNode.get(keyColumnName).asText(),
mapNode.get(valueColumnName).asText());
}
}
+ /**
+ * Extract keys from JSON object using JsonPath.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*') returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 2) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1) returns ["$['a']"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @return List of key paths matching the JsonPath, empty list if input is
null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath)
+ throws IOException {
+ return jsonExtractKey(jsonObj, jsonPath, Integer.MAX_VALUE, false);
+ }
+
+ /**
+ * Extract keys from JSON object using JsonPath with depth limit.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 1) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 2) returns ["$['a']",
"$['b']"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1) returns ["$['a']"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @param maxDepth Maximum depth to recurse (must be positive)
+ * @return List of key paths matching the JsonPath up to maxDepth, empty
list if input is null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath, int
maxDepth)
+ throws IOException {
+ return jsonExtractKey(jsonObj, jsonPath, maxDepth, false);
+ }
+
+ /**
+ * Extract keys from JSON object using JsonPath with depth limit and output
format option.
+ * <p>
+ * Examples:
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$.*', 1, false) returns
["$['a']", "$['b']"]
+ * - jsonExtractKey('{"a": 1, "b": {"c": 2}}', '$..**', 2, true) returns
["a", "b", "b.c"]
+ * - jsonExtractKey('{"a": [1, 2]}', '$.*', 1, true) returns ["a"]
+ *
+ * @param jsonObj JSON object or string
+ * @param jsonPath JsonPath expression to extract keys
+ * @param maxDepth Maximum depth to recurse (must be positive)
+ * @param dotNotation If true, return keys in dot notation (e.g., "a.b.c"),
+ * if false, return JsonPath format (e.g.,
"$['a']['b']['c']")
+ * @return List of key paths matching the JsonPath up to maxDepth, empty
list if input is null or invalid
+ */
+ @ScalarFunction
+ public static List jsonExtractKey(Object jsonObj, String jsonPath, int
maxDepth, boolean dotNotation)
+ throws IOException {
+ if (maxDepth <= 0) {
+ return java.util.Collections.emptyList();
+ }
+
+ // Special handling for $.** and $.. recursive key extraction
+ if ("$..**".equals(jsonPath) || "$..".equals(jsonPath)) {
+ return extractAllKeysRecursively(jsonObj, maxDepth, dotNotation);
+ }
+
+ // For other expressions, try to get keys using AS_PATH_LIST
+ List<String> keys = null;
+ try {
+ keys = KEY_PARSE_CONTEXT.parse(
+ jsonObj instanceof String ? (String) jsonObj :
jsonObj).read(jsonPath);
+ } catch (Exception e) {
+ // AS_PATH_LIST might not work for all expressions
+ }
+
+ // If AS_PATH_LIST doesn't work, fall back to manual path construction
+ if (keys == null || keys.isEmpty()) {
+ return extractKeysForNonRecursiveExpression(jsonObj, jsonPath, maxDepth,
dotNotation);
+ }
+
+ // Filter keys by depth if maxDepth is specified
+ if (maxDepth != Integer.MAX_VALUE) {
+ keys = keys.stream()
+ .filter(key -> getKeyDepth(key) <= maxDepth)
+ .collect(java.util.stream.Collectors.toList());
+ }
+
+ // Convert to dot notation if requested
+ if (dotNotation) {
+ keys = keys.stream()
+ .map(JsonFunctions::convertToDotNotation)
Review Comment:
Are we converting it twice? Seems it is already converted in
`extractKeysFromNode()`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]