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]

Reply via email to