This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 340363ef27 [Feature][Transform-V2] Add GET_JSON_OBJECT SQL function 
(#12666)
340363ef27 is described below

commit 340363ef27b346c64519d9234107fd84a4288d9a
Author: chovygo <[email protected]>
AuthorDate: Fri Oct 9 05:26:21 2026 +0000

    [Feature][Transform-V2] Add GET_JSON_OBJECT SQL function (#12666)
    
    Co-authored-by: zhaowei <[email protected]>
---
 docs/en/transforms/sql-functions.md                |  37 +++
 docs/zh/transforms/sql-functions.md                |  37 +++
 .../calcite/udf/GetJsonObjectFunction.java         |  46 ++++
 .../transform/sql/zeta/ZetaSQLFunction.java        |   6 +
 .../seatunnel/transform/sql/zeta/ZetaSQLType.java  |   1 +
 .../transform/sql/zeta/functions/JsonFunction.java | 200 +++++++++++++++++
 .../calcite/udf/GetJsonObjectFunctionTest.java     | 163 ++++++++++++++
 .../transform/sql/SQLJsonFunctionsTest.java        | 250 +++++++++++++++++++++
 8 files changed, 740 insertions(+)

diff --git a/docs/en/transforms/sql-functions.md 
b/docs/en/transforms/sql-functions.md
index 44966f6820..c59d122ed5 100644
--- a/docs/en/transforms/sql-functions.md
+++ b/docs/en/transforms/sql-functions.md
@@ -1165,6 +1165,43 @@ AES_DECRYPT(AES_ENCRYPT(name, 'mySecretPass'), 
'mySecretPass')
 
 AES_DECRYPT(cipher, 'mySecretPass', '1234567890123456')
 
+## JSON Functions
+
+### GET_JSON_OBJECT
+
+```GET_JSON_OBJECT(json, path) -> STRING```
+
+Extracts a value from a JSON string using a JSON path and returns it as a 
string. Available in both the Zeta and Calcite SQL transforms.
+
+Supported path syntax:
+
+- `$` — the root document
+- `.field` — object field access
+- `[n]` — array index (non-negative integer)
+- `['field']` / `["field"]` — bracket field access (for keys containing `.` or 
`[`)
+
+Wildcards (`[*]`, `.*`), recursive descent (`..`) and filter expressions 
(`[?(...)]`) are not supported.
+
+Return semantics:
+
+- Any null input (`json` or `path`) → `null`
+- Invalid JSON → `null`
+- Path does not match / array index out of bounds → `null`
+- A JSON `null` value → `null`
+- A string value → the unquoted string content
+- A number or boolean → the value rendered as text
+- An object or array → the raw (compact) JSON text of that node
+
+Example:
+
+GET_JSON_OBJECT('{"a":"hello","b":[10,20]}', '$.a')
+
+GET_JSON_OBJECT('{"a":{"b":1}}', '$.a')
+
+GET_JSON_OBJECT('{"b":[10,20]}', '$.b[1]')
+
+SELECT CASE WHEN GET_JSON_OBJECT(name, '$.key1') = 'value1' THEN 'A' ELSE 'B' 
END AS r FROM dual
+
 ## System Functions
 
 ### CAST
diff --git a/docs/zh/transforms/sql-functions.md 
b/docs/zh/transforms/sql-functions.md
index ebdb15f6a8..3ac62d2a88 100644
--- a/docs/zh/transforms/sql-functions.md
+++ b/docs/zh/transforms/sql-functions.md
@@ -1170,6 +1170,43 @@ AES_DECRYPT(AES_ENCRYPT(name, 'mySecretPass'), 
'mySecretPass')
 
 AES_DECRYPT(cipher, 'mySecretPass', '1234567890123456')
 
+## JSON 函数
+
+### GET_JSON_OBJECT
+
+```GET_JSON_OBJECT(json, path) -> STRING```
+
+从 JSON 字符串中提取值,并以字符串形式返回。在 Zeta 与 Calcite 两种 SQL 引擎中均可用。
+
+支持的 path 语法:
+
+- `$` — 根文档
+- `.field` — 对象字段访问
+- `[n]` — 数组下标(非负整数)
+- `['field']` / `["field"]` — 括号字段访问(用于包含 `.` 或 `[` 的键)
+
+不支持通配符(`[*]`、`.*`)、递归下降(`..`)和过滤表达式(`[?(...)]`)。
+
+返回语义:
+
+- 任一输入为 null(`json` 或 `path`)→ `null`
+- 非法 JSON → `null`
+- path 未命中 / 数组下标越界 → `null`
+- JSON 的 `null` 值 → `null`
+- 字符串值 → 去掉外层引号后的字符串内容
+- 数字或布尔值 → 转为文本后的值
+- 对象或数组 → 该节点的原始(紧凑)JSON 文本
+
+示例:
+
+GET_JSON_OBJECT('{"a":"hello","b":[10,20]}', '$.a')
+
+GET_JSON_OBJECT('{"a":{"b":1}}', '$.a')
+
+GET_JSON_OBJECT('{"b":[10,20]}', '$.b[1]')
+
+SELECT CASE WHEN GET_JSON_OBJECT(name, '$.key1') = 'value1' THEN 'A' ELSE 'B' 
END AS r FROM dual
+
 ## System Functions
 
 ### CAST
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/GetJsonObjectFunction.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/GetJsonObjectFunction.java
new file mode 100644
index 0000000000..78f626bfa5
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/calcite/udf/GetJsonObjectFunction.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.transform.calcite.udf;
+
+import org.apache.seatunnel.transform.sql.zeta.functions.JsonFunction;
+
+import com.google.auto.service.AutoService;
+
+/**
+ * {@code GET_JSON_OBJECT(json, path)} UDF for the Calcite SQL transform.
+ *
+ * <p>Delegates to {@link JsonFunction#getJsonObject(String, String)} so the 
Zeta and Calcite
+ * engines share one implementation of the path and return semantics.
+ *
+ * <p>Usage: {@code GET_JSON_OBJECT(json, '$.field.subfield[0]')}
+ */
+@AutoService(CalciteUdf.class)
+public class GetJsonObjectFunction implements CalciteUdf {
+
+    @Override
+    public String functionName() {
+        return "GET_JSON_OBJECT";
+    }
+
+    public static String eval(String json, String path) {
+        if (json == null || path == null) {
+            return null;
+        }
+        return JsonFunction.getJsonObject(json, path);
+    }
+}
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java
index 233245d980..5a69ffd86b 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLFunction.java
@@ -34,6 +34,7 @@ import 
org.apache.seatunnel.transform.exception.TransformException;
 import org.apache.seatunnel.transform.sql.zeta.functions.ArrayFunction;
 import org.apache.seatunnel.transform.sql.zeta.functions.CryptoFunction;
 import org.apache.seatunnel.transform.sql.zeta.functions.DateTimeFunction;
+import org.apache.seatunnel.transform.sql.zeta.functions.JsonFunction;
 import org.apache.seatunnel.transform.sql.zeta.functions.MapFunction;
 import org.apache.seatunnel.transform.sql.zeta.functions.NumericFunction;
 import org.apache.seatunnel.transform.sql.zeta.functions.StringFunction;
@@ -135,6 +136,9 @@ public class ZetaSQLFunction {
     public static final String AES_ENCRYPT = "AES_ENCRYPT";
     public static final String AES_DECRYPT = "AES_DECRYPT";
 
+    // -------------------------json functions----------------------------
+    public static final String GET_JSON_OBJECT = "GET_JSON_OBJECT";
+
     // -------------------------numeric functions----------------------------
     public static final String ABS = "ABS";
     public static final String ACOS = "ACOS";
@@ -559,6 +563,8 @@ public class ZetaSQLFunction {
                 return CryptoFunction.aesEncrypt(args);
             case AES_DECRYPT:
                 return CryptoFunction.aesDecrypt(args);
+            case GET_JSON_OBJECT:
+                return JsonFunction.getJsonObject(args);
             case ABS:
                 return NumericFunction.abs(args);
             case ACOS:
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java
index b536a0746d..4d7adedf5a 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/ZetaSQLType.java
@@ -360,6 +360,7 @@ public class ZetaSQLType {
             case ZetaSQLFunction.TRIM_SCALE:
             case ZetaSQLFunction.AES_ENCRYPT:
             case ZetaSQLFunction.AES_DECRYPT:
+            case ZetaSQLFunction.GET_JSON_OBJECT:
                 return BasicType.STRING_TYPE;
             case ZetaSQLFunction.ASCII:
             case ZetaSQLFunction.LOCATE:
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/JsonFunction.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/JsonFunction.java
new file mode 100644
index 0000000000..a070131cca
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/zeta/functions/JsonFunction.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.transform.sql.zeta.functions;
+
+import 
org.apache.seatunnel.shade.com.fasterxml.jackson.core.JsonProcessingException;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.JsonNode;
+
+import org.apache.seatunnel.common.exception.CommonError;
+import org.apache.seatunnel.common.utils.JsonUtils;
+import org.apache.seatunnel.transform.sql.zeta.ZetaSQLFunction;
+
+import java.util.List;
+
+/**
+ * Built-in JSON functions for the Zeta SQL transform.
+ *
+ * <p>{@link #getJsonObject(List)} extracts a value from a JSON string using a 
JSON path. The
+ * supported path grammar is:
+ *
+ * <ul>
+ *   <li>{@code $} &mdash; the root document
+ *   <li>{@code .field} &mdash; object field access
+ *   <li>{@code [n]} &mdash; array index (non-negative integer)
+ *   <li>{@code ['field']} / {@code ["field"]} &mdash; bracket field access 
(allows keys that
+ *       contain {@code .} or {@code [})
+ * </ul>
+ *
+ * <p>Wildcards ({@code [*]}, {@code .*}), recursive descent ({@code ..}) and 
filter expressions
+ * ({@code [?(...)]}) are not supported.
+ *
+ * <p>Return semantics:
+ *
+ * <ul>
+ *   <li>any null input (json or path) &rarr; {@code null}
+ *   <li>invalid JSON &rarr; {@code null}
+ *   <li>path does not match / indexes out of bounds &rarr; {@code null}
+ *   <li>a JSON {@code null} value &rarr; {@code null}
+ *   <li>a string value &rarr; the unquoted string content
+ *   <li>a number or boolean &rarr; the value rendered as text
+ *   <li>an object or array &rarr; the raw (compact) JSON text of that node
+ * </ul>
+ *
+ * <p>JSON is parsed via the shaded Jackson runtime shared across SeaTunnel; 
the path walker is a
+ * minimal hand-rolled tokenizer so the grammar above is matched exactly 
without pulling in a full
+ * JSONPath engine.
+ */
+public class JsonFunction {
+
+    private JsonFunction() {}
+
+    /**
+     * Extracts a value via {@code GET_JSON_OBJECT(json, path)}.
+     *
+     * @param args [json, path]
+     * @return the extracted value as a string, or null on null input / 
invalid JSON / missing path
+     */
+    public static String getJsonObject(List<Object> args) {
+        String operation = ZetaSQLFunction.GET_JSON_OBJECT;
+        if (args.size() != 2) {
+            throw CommonError.illegalArgument(
+                    String.valueOf(args.size()), operation + " expects 2 
arguments: (json, path)");
+        }
+        Object jsonArg = args.get(0);
+        Object pathArg = args.get(1);
+        if (jsonArg == null || pathArg == null) {
+            return null;
+        }
+        return getJsonObject(jsonArg.toString(), pathArg.toString());
+    }
+
+    /** Core implementation, shared with the Calcite UDF so both engines 
behave identically. */
+    public static String getJsonObject(String json, String path) {
+        if (json == null || path == null) {
+            return null;
+        }
+        JsonNode node;
+        try {
+            node = JsonUtils.stringToJsonNode(json);
+        } catch (JsonProcessingException e) {
+            // Return null on invalid JSON rather than raising.
+            return null;
+        }
+        if (node == null) {
+            return null;
+        }
+        return render(resolvePath(node, path));
+    }
+
+    /**
+     * Walks the path against {@code root}, returning the matched node or null 
when the path is
+     * malformed or does not resolve.
+     */
+    private static JsonNode resolvePath(JsonNode root, String path) {
+        if (path.isEmpty() || path.charAt(0) != '$') {
+            // The path must start with the root marker.
+            return null;
+        }
+        JsonNode current = root;
+        int i = 1;
+        int len = path.length();
+        while (i < len && current != null) {
+            char c = path.charAt(i);
+            if (c == '.') {
+                int start = ++i;
+                while (i < len && path.charAt(i) != '.' && path.charAt(i) != 
'[') {
+                    i++;
+                }
+                String field = path.substring(start, i);
+                if (field.isEmpty() || !current.isObject()) {
+                    return null;
+                }
+                current = current.get(field);
+            } else if (c == '[') {
+                // Scan to the matching ']', skipping any ']' that appears 
inside a quoted field
+                // name so keys like ['a]b'] resolve correctly.
+                int close = -1;
+                char quoteChar = 0;
+                for (int j = i + 1; j < len; j++) {
+                    char cj = path.charAt(j);
+                    if (quoteChar != 0) {
+                        if (cj == quoteChar) {
+                            quoteChar = 0;
+                        }
+                    } else if (cj == '\'' || cj == '"') {
+                        quoteChar = cj;
+                    } else if (cj == ']') {
+                        close = j;
+                        break;
+                    }
+                }
+                if (close < 0) {
+                    return null;
+                }
+                String inside = path.substring(i + 1, close).trim();
+                i = close + 1;
+                if (inside.isEmpty()) {
+                    return null;
+                }
+                char quote = inside.charAt(0);
+                if (quote == '\'' || quote == '"') {
+                    if (inside.length() < 2 || inside.charAt(inside.length() - 
1) != quote) {
+                        return null;
+                    }
+                    if (!current.isObject()) {
+                        return null;
+                    }
+                    current = current.get(inside.substring(1, inside.length() 
- 1));
+                } else {
+                    int index;
+                    try {
+                        index = Integer.parseInt(inside);
+                    } catch (NumberFormatException e) {
+                        return null;
+                    }
+                    if (index < 0 || !current.isArray() || index >= 
current.size()) {
+                        return null;
+                    }
+                    current = current.get(index);
+                }
+            } else {
+                // Unexpected char right after '$' or a segment: not a valid 
path.
+                return null;
+            }
+        }
+        return current;
+    }
+
+    /** Renders the matched node per the return semantics (see class Javadoc). 
*/
+    private static String render(JsonNode node) {
+        if (node == null || node.isNull()) {
+            return null;
+        }
+        switch (node.getNodeType()) {
+            case OBJECT:
+            case ARRAY:
+                return node.toString();
+            case STRING:
+            case NUMBER:
+            case BOOLEAN:
+                return node.asText();
+            default:
+                return node.asText();
+        }
+    }
+}
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/calcite/udf/GetJsonObjectFunctionTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/calcite/udf/GetJsonObjectFunctionTest.java
new file mode 100644
index 0000000000..f88cf40e1b
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/calcite/udf/GetJsonObjectFunctionTest.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.transform.calcite.udf;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class GetJsonObjectFunctionTest {
+
+    @Test
+    void testExtractsStringValue() {
+        Assertions.assertEquals("hello", 
GetJsonObjectFunction.eval("{\"a\":\"hello\"}", "$.a"));
+    }
+
+    @Test
+    void testStringValueIsUnquoted() {
+        // Returns the decoded string content, without surrounding quotes.
+        Assertions.assertEquals(
+                "a \"b\" c", GetJsonObjectFunction.eval("{\"k\":\"a \\\"b\\\" 
c\"}", "$.k"));
+    }
+
+    @Test
+    void testExtractsNumberValue() {
+        Assertions.assertEquals("42", GetJsonObjectFunction.eval("{\"a\":42}", 
"$.a"));
+    }
+
+    @Test
+    void testExtractsDecimalNumber() {
+        Assertions.assertEquals("1.5", 
GetJsonObjectFunction.eval("{\"a\":1.5}", "$.a"));
+    }
+
+    @Test
+    void testExtractsBooleanValue() {
+        Assertions.assertEquals("true", 
GetJsonObjectFunction.eval("{\"a\":true}", "$.a"));
+        Assertions.assertEquals("false", 
GetJsonObjectFunction.eval("{\"a\":false}", "$.a"));
+    }
+
+    @Test
+    void testExtractsObjectAsRawJson() {
+        Assertions.assertEquals(
+                "{\"b\":1}", GetJsonObjectFunction.eval("{\"a\":{\"b\":1}}", 
"$.a"));
+    }
+
+    @Test
+    void testExtractsArrayAsRawJson() {
+        Assertions.assertEquals("[1,2,3]", 
GetJsonObjectFunction.eval("{\"a\":[1,2,3]}", "$.a"));
+    }
+
+    @Test
+    void testExtractsArrayElement() {
+        Assertions.assertEquals("20", 
GetJsonObjectFunction.eval("{\"a\":[10,20,30]}", "$.a[1]"));
+    }
+
+    @Test
+    void testNestedPath() {
+        Assertions.assertEquals(
+                "deep", 
GetJsonObjectFunction.eval("{\"a\":{\"b\":{\"c\":\"deep\"}}}", "$.a.b.c"));
+    }
+
+    @Test
+    void testRootPathReturnsWholeDocument() {
+        Assertions.assertEquals("{\"a\":1}", 
GetJsonObjectFunction.eval("{\"a\":1}", "$"));
+    }
+
+    @Test
+    void testNullJsonReturnsNull() {
+        Assertions.assertNull(GetJsonObjectFunction.eval(null, "$.a"));
+    }
+
+    @Test
+    void testNullPathReturnsNull() {
+        Assertions.assertNull(GetJsonObjectFunction.eval("{\"a\":1}", null));
+    }
+
+    @Test
+    void testInvalidJsonReturnsNull() {
+        Assertions.assertNull(GetJsonObjectFunction.eval("not a json", "$.a"));
+    }
+
+    @Test
+    void testMissingPathReturnsNull() {
+        Assertions.assertNull(GetJsonObjectFunction.eval("{\"a\":1}", 
"$.missing"));
+    }
+
+    @Test
+    void testJsonNullValueReturnsNull() {
+        Assertions.assertNull(GetJsonObjectFunction.eval("{\"a\":null}", 
"$.a"));
+    }
+
+    @Test
+    void testArrayIndexOutOfBoundsReturnsNull() {
+        Assertions.assertNull(GetJsonObjectFunction.eval("{\"a\":[1,2]}", 
"$.a[5]"));
+    }
+
+    @Test
+    void testBracketQuotedKeyWithDot() {
+        // Bracket-quoted field access lets keys contain '.' or '['.
+        Assertions.assertEquals("v", 
GetJsonObjectFunction.eval("{\"a.b\":\"v\"}", "$['a.b']"));
+    }
+
+    @Test
+    void testBracketQuotedKeyWithClosingBracket() {
+        // The bracket scanner is quote-aware, so ']' inside a quoted key does 
not close early.
+        Assertions.assertEquals("v", 
GetJsonObjectFunction.eval("{\"a]b\":\"v\"}", "$['a]b']"));
+    }
+
+    @Test
+    void testFieldAccessOnNonObjectReturnsNull() {
+        // .field on an array or scalar does not resolve.
+        Assertions.assertNull(GetJsonObjectFunction.eval("{\"a\":[1,2]}", 
"$.a.b"));
+        Assertions.assertNull(GetJsonObjectFunction.eval("{\"a\":1}", 
"$.a.b"));
+    }
+
+    @Test
+    void testArrayIndexOnNonArrayReturnsNull() {
+        // [n] on an object or scalar does not resolve.
+        Assertions.assertNull(GetJsonObjectFunction.eval("{\"a\":{\"b\":1}}", 
"$.a[0]"));
+        Assertions.assertNull(GetJsonObjectFunction.eval("{\"a\":\"str\"}", 
"$.a[0]"));
+    }
+
+    @Test
+    void testNegativeArrayIndexReturnsNull() {
+        Assertions.assertNull(GetJsonObjectFunction.eval("[1,2]", "$[-1]"));
+    }
+
+    @Test
+    void testConsecutiveArrayIndexes() {
+        Assertions.assertEquals("20", 
GetJsonObjectFunction.eval("[[10,20],[30,40]]", "$[0][1]"));
+    }
+
+    @Test
+    void testRootArrayAccess() {
+        Assertions.assertEquals("10", GetJsonObjectFunction.eval("[10,20]", 
"$[0]"));
+    }
+
+    @Test
+    void testScalarRootReturnsScalar() {
+        // A scalar root document is rendered as text, not wrapped.
+        Assertions.assertEquals("42", GetJsonObjectFunction.eval("42", "$"));
+        Assertions.assertEquals("hello", 
GetJsonObjectFunction.eval("\"hello\"", "$"));
+    }
+
+    @Test
+    void testFunctionName() {
+        GetJsonObjectFunction fn = new GetJsonObjectFunction();
+        Assertions.assertEquals("GET_JSON_OBJECT", fn.functionName());
+    }
+}
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLJsonFunctionsTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLJsonFunctionsTest.java
new file mode 100644
index 0000000000..7b101790aa
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/sql/SQLJsonFunctionsTest.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.transform.sql;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException;
+import org.apache.seatunnel.transform.sql.zeta.functions.JsonFunction;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class SQLJsonFunctionsTest {
+
+    private SeaTunnelRow runSql(String query, SeaTunnelRowType rowType, 
Object... values) {
+        CatalogTable table = CatalogTableUtil.getCatalogTable("test", rowType);
+        ReadonlyConfig config = 
ReadonlyConfig.fromMap(Collections.singletonMap("query", query));
+        SQLTransform transform = new SQLTransform(config, table);
+        List<SeaTunnelRow> out = transform.transformRow(new 
SeaTunnelRow(values));
+        Assertions.assertNotNull(out);
+        Assertions.assertFalse(out.isEmpty());
+        return out.get(0);
+    }
+
+    private SeaTunnelRowType stringRowType(String field) {
+        return new SeaTunnelRowType(
+                new String[] {field}, new SeaTunnelDataType[] 
{BasicType.STRING_TYPE});
+    }
+
+    @Test
+    public void testExtractsStringValue() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":\"hello\"}");
+        Assertions.assertEquals("hello", out.getField(0));
+    }
+
+    @Test
+    public void testExtractsNumberValue() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":42}");
+        Assertions.assertEquals("42", out.getField(0));
+    }
+
+    @Test
+    public void testExtractsDecimalNumber() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":1.5}");
+        Assertions.assertEquals("1.5", out.getField(0));
+    }
+
+    @Test
+    public void testExtractsBooleanValue() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":true}");
+        Assertions.assertEquals("true", out.getField(0));
+    }
+
+    @Test
+    public void testExtractsObjectAsRawJson() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":{\"b\":1}}");
+        Assertions.assertEquals("{\"b\":1}", out.getField(0));
+    }
+
+    @Test
+    public void testExtractsArrayAsRawJson() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":[1,2,3]}");
+        Assertions.assertEquals("[1,2,3]", out.getField(0));
+    }
+
+    @Test
+    public void testExtractsArrayElement() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a[1]') as r from 
dual",
+                        stringRowType("name"),
+                        "{\"a\":[10,20,30]}");
+        Assertions.assertEquals("20", out.getField(0));
+    }
+
+    @Test
+    public void testNestedPath() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a.b.c') as r from 
dual",
+                        stringRowType("name"),
+                        "{\"a\":{\"b\":{\"c\":\"deep\"}}}");
+        Assertions.assertEquals("deep", out.getField(0));
+    }
+
+    @Test
+    public void testNestedArrayAndObject() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a[0].b') as r from 
dual",
+                        stringRowType("name"),
+                        "{\"a\":[{\"b\":\"first\"},{\"b\":\"second\"}]}");
+        Assertions.assertEquals("first", out.getField(0));
+    }
+
+    @Test
+    public void testRootPathReturnsWholeDocument() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":1}");
+        Assertions.assertEquals("{\"a\":1}", out.getField(0));
+    }
+
+    @Test
+    public void testNullJsonReturnsNull() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        (Object) null);
+        Assertions.assertNull(out.getField(0));
+    }
+
+    @Test
+    public void testInvalidJsonReturnsNull() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "not a json");
+        Assertions.assertNull(out.getField(0));
+    }
+
+    @Test
+    public void testMissingPathReturnsNull() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.missing') as r from 
dual",
+                        stringRowType("name"),
+                        "{\"a\":1}");
+        Assertions.assertNull(out.getField(0));
+    }
+
+    @Test
+    public void testJsonNullValueReturnsNull() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":null}");
+        Assertions.assertNull(out.getField(0));
+    }
+
+    @Test
+    public void testArrayIndexOutOfBoundsReturnsNull() {
+        SeaTunnelRow out =
+                runSql(
+                        "select GET_JSON_OBJECT(name, '$.a[5]') as r from 
dual",
+                        stringRowType("name"),
+                        "{\"a\":[1,2]}");
+        Assertions.assertNull(out.getField(0));
+    }
+
+    @Test
+    public void testFunctionNameIsCaseInsensitive() {
+        // Function names are case-insensitive; the dispatcher upper-cases 
them.
+        SeaTunnelRow out =
+                runSql(
+                        "select get_json_object(name, '$.a') as r from dual",
+                        stringRowType("name"),
+                        "{\"a\":\"ok\"}");
+        Assertions.assertEquals("ok", out.getField(0));
+    }
+
+    @Test
+    public void testUsedInCaseWhen() {
+        // Reproduces the migration use case from the feature request.
+        SeaTunnelRow out =
+                runSql(
+                        "select case when GET_JSON_OBJECT(name, '$.key1') = 
'value1' then 'A'"
+                                + " else 'B' end as r from dual",
+                        stringRowType("name"),
+                        "{\"key1\":\"value1\"}");
+        Assertions.assertEquals("A", out.getField(0));
+    }
+
+    @Test
+    public void testMalformedPathReturnsNull() {
+        // Recursive descent, a trailing dot, and a path without the leading 
'$' are all rejected.
+        SeaTunnelRowType rowType = stringRowType("name");
+        Assertions.assertNull(
+                runSql("select GET_JSON_OBJECT(name, '$..a') as r from dual", 
rowType, "{\"a\":1}")
+                        .getField(0));
+        Assertions.assertNull(
+                runSql("select GET_JSON_OBJECT(name, '$.a.') as r from dual", 
rowType, "{\"a\":1}")
+                        .getField(0));
+        Assertions.assertNull(
+                runSql(
+                                "select GET_JSON_OBJECT(name, 'a.b') as r from 
dual",
+                                rowType,
+                                "{\"a\":{\"b\":1}}")
+                        .getField(0));
+    }
+
+    @Test
+    public void testArgumentCountGuard() {
+        Assertions.assertThrows(
+                SeaTunnelRuntimeException.class,
+                () -> 
JsonFunction.getJsonObject(Collections.<Object>singletonList("only-one")));
+    }
+}

Reply via email to