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

yuxiqian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink-cdc.git


The following commit(s) were added to refs/heads/master by this push:
     new cc384a0e0 [FLINK-40313][runtime] Support nested collection functions 
in Transforms (#4496)
cc384a0e0 is described below

commit cc384a0e01628d9be49cc2b9417931d238a996fc
Author: haruki <[email protected]>
AuthorDate: Fri Aug 14 16:03:56 2026 +0800

    [FLINK-40313][runtime] Support nested collection functions in Transforms 
(#4496)
---
 docs/content.zh/docs/core-concept/transform.md     |   7 +
 docs/content/docs/core-concept/transform.md        |   7 +
 .../src/test/resources/specs/nested.yaml           |  48 ++++++-
 .../runtime/functions/impl/StructFunctions.java    | 155 +++++++++++++++++++++
 .../flink/cdc/runtime/parser/JaninoCompiler.java   | 140 ++++++++++++++++++-
 .../flink/cdc/runtime/parser/TransformParser.java  |  47 ++++++-
 .../parser/metadata/TransformSqlOperatorTable.java |  22 +++
 .../typeutils/CalciteDataTypeConverter.java        |  19 +++
 .../functions/impl/StructFunctionsTest.java        |  65 +++++++++
 .../cdc/runtime/parser/TransformParserTest.java    | 110 +++++++++++++++
 10 files changed, 616 insertions(+), 4 deletions(-)

diff --git a/docs/content.zh/docs/core-concept/transform.md 
b/docs/content.zh/docs/core-concept/transform.md
index 545f069de..8636567cf 100644
--- a/docs/content.zh/docs/core-concept/transform.md
+++ b/docs/content.zh/docs/core-concept/transform.md
@@ -281,6 +281,13 @@ Flink CDC 使用 [Calcite](https://calcite.apache.org/) 
来解析表达式并且
 
 | 函数 | Janino 代码 | 描述 |
 | -------- | ----------- | ----------- |
+| ARRAY[value, ...] | array(value, ...) | 使用给定值构造数组。所有值必须具有公共类型。 |
+| MAP[key, value, ...] | map(key, value, ...) | 使用键值对构造 Map。所有键、所有值必须分别具有公共类型。 
|
+| ROW(value, ...) | row(value, ...) | 使用给定值构造 Row。字段名依次生成为 `f0`、`f1` 等。 |
+| CARDINALITY(arrayOrMap) | cardinality(arrayOrMap) | 返回数组或 Map 的元素数量。输入为 NULL 
时返回 NULL。 |
+| ARRAY_CONTAINS(array, value) | arrayContains(array, value) | 返回数组是否包含给定值。 |
+| ARRAY_POSITION(array, value) | arrayPosition(array, value) | 返回首个匹配值的位置,位置从 
1 开始;未找到时返回 0。 |
+| ELEMENT(array) | element(array) | 返回单元素数组中的元素。空数组返回 NULL;数组包含多个元素时抛出异常。 |
 | array[index] | itemAccess(array, index) | 返回数组中位置 `index` 的元素。索引从 1 开始(SQL 
标准)。如果索引超出范围或数组为 NULL,则返回 NULL。 |
 | map[key] | itemAccess(map, key) | 返回 map 中与 `key` 关联的值。如果 key 不存在或 map 为 
NULL,则返回 NULL。 |
 | row[index] | itemAccess(row, index) | 返回 row 中位置 `index` 的字段。索引从 1 
开始。索引必须是常量(不能是计算表达式),因为返回类型必须在静态阶段确定。 |
diff --git a/docs/content/docs/core-concept/transform.md 
b/docs/content/docs/core-concept/transform.md
index 5e623b277..a9d217ae7 100644
--- a/docs/content/docs/core-concept/transform.md
+++ b/docs/content/docs/core-concept/transform.md
@@ -282,6 +282,13 @@ Struct functions are used to access elements in ARRAY, 
MAP, ROW, and VARIANT typ
 
 | Function | Janino Code | Description |
 | -------- | ----------- | ----------- |
+| ARRAY[value, ...] | array(value, ...) | Creates an array from the given 
values. The values must have a common type. |
+| MAP[key, value, ...] | map(key, value, ...) | Creates a map from key-value 
pairs. Keys and values must have common types respectively. |
+| ROW(value, ...) | row(value, ...) | Creates a row from the given values. 
Field names are generated as `f0`, `f1`, and so on. |
+| CARDINALITY(arrayOrMap) | cardinality(arrayOrMap) | Returns the number of 
elements in an array or map. Returns NULL if the input is NULL. |
+| ARRAY_CONTAINS(array, value) | arrayContains(array, value) | Returns whether 
the array contains the given value. |
+| ARRAY_POSITION(array, value) | arrayPosition(array, value) | Returns the 
1-based position of the first matching value, or 0 if it is not found. |
+| ELEMENT(array) | element(array) | Returns the element of a single-element 
array. Returns NULL for an empty array and throws an exception if the array has 
more than one element. |
 | array[index] | itemAccess(array, index) | Returns the element at position 
`index` in the array. Index is 1-based (SQL standard). Returns NULL if the 
index is out of bounds or if the array is NULL. |
 | map[key] | itemAccess(map, key) | Returns the value associated with `key` in 
the map. Returns NULL if the key does not exist or if the map is NULL. |
 | row[index] | itemAccess(row, index) | Returns the field at position `index` 
in the row. Index is 1-based. The index must be a constant (not a computed 
expression) since the return type must be statically determined. |
diff --git a/flink-cdc-composer/src/test/resources/specs/nested.yaml 
b/flink-cdc-composer/src/test/resources/specs/nested.yaml
index 3ac67facd..08bbaecb7 100644
--- a/flink-cdc-composer/src/test/resources/specs/nested.yaml
+++ b/flink-cdc-composer/src/test/resources/specs/nested.yaml
@@ -30,6 +30,52 @@
     DataChangeEvent{tableId=foo.bar.baz, before=[-1, [2, 3, 5, 7, 11, 13, 17, 
19], [二, san, 五, qi, 十一], {1 -> yi, 2 -> er, 3 -> san}, {二 -> [E, R], 三 -> [S, 
A, N], 一 -> [Y, I]}, {name: STRING -> Derrida, length: INT -> 7}, 
[{"k":1},"hello",{"k":2}]], after=[], op=DELETE, meta=()}
     DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, null, null, 
null, null, null, null], op=INSERT, meta=()}
     DataChangeEvent{tableId=foo.bar.baz, before=[0, null, null, null, null, 
null, null], after=[], op=DELETE, meta=()}
+- do: Construct Nested Objects
+  projection: |-
+    id_
+    ARRAY[1, id_] AS constructed_array
+    MAP['id', id_, 'size', CARDINALITY(array_int_)] AS constructed_map
+    ROW(id_, ARRAY[string_, varchar_]) AS constructed_row
+    MAP['nested', ARRAY[id_, bigint_]] AS nested_map
+    ARRAY[ARRAY[1], ARRAY[id_]] AS nested_array
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`constructed_array` ARRAY<BIGINT>,`constructed_map` 
MAP<STRING, BIGINT>,`constructed_row` ROW<`f0` BIGINT, `f1` 
ARRAY<STRING>>,`nested_map` MAP<STRING, ARRAY<BIGINT>>,`nested_array` 
ARRAY<ARRAY<BIGINT>>}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, [1, 1], {id -> 
1, size -> 7}, {f0: BIGINT -> 1, f1: ARRAY<STRING> -> [From A to Z is Lie, 
Zorro]}, {nested -> [1, 5]}, [[1], [1]]], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, [1, 1], {id -> 1, size -> 
7}, {f0: BIGINT -> 1, f1: ARRAY<STRING> -> [From A to Z is Lie, Zorro]}, 
{nested -> [1, 5]}, [[1], [1]]], after=[-1, [1, -1], {id -> -1, size -> 8}, 
{f0: BIGINT -> -1, f1: ARRAY<STRING> -> [天地玄黄宇宙洪荒, 疯帽子]}, {nested -> [-1, -5]}, 
[[1], [-1]]], op=UPDATE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, [1, -1], {id -> -1, size 
-> 8}, {f0: BIGINT -> -1, f1: ARRAY<STRING> -> [天地玄黄宇宙洪荒, 疯帽子]}, {nested -> 
[-1, -5]}, [[1], [-1]]], after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, [1, 0], {id -> 
0, size -> null}, {f0: BIGINT -> 0, f1: ARRAY<STRING> -> [null, null]}, {nested 
-> [0, null]}, [[1], [0]]], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, [1, 0], {id -> 0, size -> 
null}, {f0: BIGINT -> 0, f1: ARRAY<STRING> -> [null, null]}, {nested -> [0, 
null]}, [[1], [0]]], after=[], op=DELETE, meta=()}
+- do: Collection Functions
+  projection: |-
+    id_
+    CARDINALITY(array_int_) AS array_size
+    CARDINALITY(map_int_string_) AS map_size
+    ARRAY_CONTAINS(array_int_, 5) AS contains_five
+    ARRAY_POSITION(array_int_, 5) AS position_five
+    ELEMENT(ARRAY[id_]) AS only_element
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`array_size` INT,`map_size` INT,`contains_five` 
BOOLEAN,`position_five` INT,`only_element` BIGINT}, primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[1, 7, 3, true, 5, 
1], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[1, 7, 3, true, 5, 1], 
after=[-1, 8, 3, true, 3, -1], op=UPDATE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, 8, 3, true, 3, -1], 
after=[], op=DELETE, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, null, null, 
null, null, 0], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, null, null, null, null, 
0], after=[], op=DELETE, meta=()}
+- do: Filter by Collection Functions
+  projection: id_, array_int_, array_string_
+  filter: ARRAY_CONTAINS(array_string_, '五') AND ARRAY_POSITION(array_int_, 5) 
= 3
+  primary-key: id_
+  expect: |-
+    CreateTableEvent{tableId=foo.bar.baz, schema=columns={`id_` BIGINT NOT 
NULL 'Identifier',`array_int_` ARRAY<INT>,`array_string_` ARRAY<STRING>}, 
primaryKeys=id_, options=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[], after=[-1, [2, 3, 5, 7, 
11, 13, 17, 19], [二, san, 五, qi, 十一]], op=INSERT, meta=()}
+    DataChangeEvent{tableId=foo.bar.baz, before=[-1, [2, 3, 5, 7, 11, 13, 17, 
19], [二, san, 五, qi, 十一]], after=[], op=DELETE, meta=()}
+- do: Element with Multiple Values
+  projection: |-
+    id_
+    ELEMENT(array_int_) AS invalid_element
+  primary-key: id_
+  expect-error: 'Array has more than one element.'
 - do: Parse Variant from JSON
   projection: |-
     id_
@@ -363,4 +409,4 @@
     DataChangeEvent{tableId=foo.bar.baz, before=[1, "value", 42], after=[-1, 
"value", 42], op=UPDATE, meta=()}
     DataChangeEvent{tableId=foo.bar.baz, before=[-1, "value", 42], after=[], 
op=DELETE, meta=()}
     DataChangeEvent{tableId=foo.bar.baz, before=[], after=[0, "value", 42], 
op=INSERT, meta=()}
-    DataChangeEvent{tableId=foo.bar.baz, before=[0, "value", 42], after=[], 
op=DELETE, meta=()}
\ No newline at end of file
+    DataChangeEvent{tableId=foo.bar.baz, before=[0, "value", 42], after=[], 
op=DELETE, meta=()}
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/StructFunctions.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/StructFunctions.java
index 9ce343782..32da35d38 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/StructFunctions.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/functions/impl/StructFunctions.java
@@ -19,6 +19,10 @@ package org.apache.flink.cdc.runtime.functions.impl;
 
 import org.apache.flink.cdc.common.types.variant.Variant;
 
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -30,6 +34,157 @@ import java.util.Map;
  */
 public class StructFunctions {
 
+    /** Creates an ARRAY value. */
+    public static List<Object> array(Object... elements) {
+        List<Object> result = new ArrayList<>(elements.length);
+        for (Object element : elements) {
+            result.add(element);
+        }
+        return result;
+    }
+
+    /** Creates a MAP value from alternating keys and values. */
+    public static Map<Object, Object> map(Object... keyValues) {
+        if (keyValues.length == 0 || keyValues.length % 2 != 0) {
+            throw new IllegalArgumentException(
+                    "MAP requires at least one key-value pair and an even 
number of arguments.");
+        }
+        Map<Object, Object> result = new LinkedHashMap<>();
+        for (int i = 0; i < keyValues.length; i += 2) {
+            result.put(keyValues[i], keyValues[i + 1]);
+        }
+        return result;
+    }
+
+    /** Creates a ROW value. */
+    public static List<Object> row(Object... fields) {
+        List<Object> result = new ArrayList<>(fields.length);
+        for (Object field : fields) {
+            result.add(field);
+        }
+        return result;
+    }
+
+    /** Returns the number of elements in an ARRAY. */
+    public static Integer cardinality(List<?> array) {
+        return array == null ? null : array.size();
+    }
+
+    /** Returns the number of entries in a MAP. */
+    public static Integer cardinality(Map<?, ?> map) {
+        return map == null ? null : map.size();
+    }
+
+    /** Returns whether an ARRAY contains the given value. */
+    public static Boolean arrayContains(List<?> array, Object value) {
+        if (array == null) {
+            return null;
+        }
+        for (Object element : array) {
+            if (valueEquals(element, value)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Returns the 1-based position of a value in an ARRAY, or 0 if it is not 
found. */
+    public static Integer arrayPosition(List<?> array, Object value) {
+        if (array == null || value == null) {
+            return null;
+        }
+        for (int i = 0; i < array.size(); i++) {
+            if (valueEquals(array.get(i), value)) {
+                return i + 1;
+            }
+        }
+        return 0;
+    }
+
+    /** Returns the only element of an ARRAY. */
+    public static <T> T element(List<T> array) {
+        if (array == null || array.isEmpty()) {
+            return null;
+        }
+        if (array.size() > 1) {
+            throw new IllegalArgumentException("Array has more than one 
element.");
+        }
+        return array.get(0);
+    }
+
+    private static boolean valueEquals(Object left, Object right) {
+        if (left == right) {
+            return true;
+        }
+        if (left == null || right == null) {
+            return false;
+        }
+        if (left instanceof Number && right instanceof Number) {
+            return numberEquals((Number) left, (Number) right);
+        }
+        if (left instanceof List<?> && right instanceof List<?>) {
+            List<?> leftList = (List<?>) left;
+            List<?> rightList = (List<?>) right;
+            if (leftList.size() != rightList.size()) {
+                return false;
+            }
+            for (int i = 0; i < leftList.size(); i++) {
+                if (!valueEquals(leftList.get(i), rightList.get(i))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        if (left instanceof Map<?, ?> && right instanceof Map<?, ?>) {
+            return mapEquals((Map<?, ?>) left, (Map<?, ?>) right);
+        }
+        if (left.getClass().isArray() && right.getClass().isArray()) {
+            int length = Array.getLength(left);
+            if (length != Array.getLength(right)) {
+                return false;
+            }
+            for (int i = 0; i < length; i++) {
+                if (!valueEquals(Array.get(left, i), Array.get(right, i))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return left.equals(right);
+    }
+
+    private static boolean numberEquals(Number left, Number right) {
+        if (left instanceof Float
+                || left instanceof Double
+                || right instanceof Float
+                || right instanceof Double) {
+            return Double.compare(left.doubleValue(), right.doubleValue()) == 
0;
+        }
+        return new BigDecimal(left.toString()).compareTo(new 
BigDecimal(right.toString())) == 0;
+    }
+
+    private static boolean mapEquals(Map<?, ?> left, Map<?, ?> right) {
+        if (left.size() != right.size()) {
+            return false;
+        }
+        for (Map.Entry<?, ?> leftEntry : left.entrySet()) {
+            boolean found = false;
+            for (Map.Entry<?, ?> rightEntry : right.entrySet()) {
+                if (valueEquals(leftEntry.getKey(), rightEntry.getKey())) {
+                    if (!valueEquals(leftEntry.getValue(), 
rightEntry.getValue())) {
+                        return false;
+                    }
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Accesses an element from an ARRAY by index (1-based, SQL standard).
      *
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
index abe2fb8c2..4caef2768 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/JaninoCompiler.java
@@ -234,6 +234,10 @@ public class JaninoCompiler {
                     udfFunction.get(),
                     translateOperands(context, sqlBasicCall).toArray(new 
Java.Rvalue[0]));
         }
+        if (isCollectionConstructor(sqlBasicCall)) {
+            return generateCollectionConstructorOperation(
+                    context, sqlBasicCall, 
getCollectionConstructorFunctionName(sqlBasicCall));
+        }
         if (isIntervalArithmetic(sqlBasicCall)) {
             return generateIntervalArithmeticOperation(context, sqlBasicCall);
         }
@@ -491,6 +495,131 @@ public class JaninoCompiler {
         return new Java.MethodInvocation(Location.NOWHERE, null, functionName, 
atoms);
     }
 
+    private static Java.Rvalue generateCollectionConstructorOperation(
+            Context context, SqlBasicCall sqlBasicCall, String functionName) {
+        DataType resultType =
+                TransformParser.deduceSubExpressionType(
+                        context.columns,
+                        sqlBasicCall,
+                        context.udfDescriptors,
+                        context.supportedMetadataColumns);
+        return generateCollectionConstructorOperation(
+                context, sqlBasicCall, functionName, resultType);
+    }
+
+    private static Java.Rvalue generateCollectionConstructorOperation(
+            Context context, SqlBasicCall sqlBasicCall, String functionName, 
DataType resultType) {
+        List<DataType> targetTypes = new ArrayList<>();
+        switch (sqlBasicCall.getKind()) {
+            case ARRAY_VALUE_CONSTRUCTOR:
+                for (int i = 0; i < sqlBasicCall.operandCount(); i++) {
+                    targetTypes.add(resultType.getChildren().get(0));
+                }
+                break;
+            case MAP_VALUE_CONSTRUCTOR:
+                for (int i = 0; i < sqlBasicCall.operandCount(); i++) {
+                    targetTypes.add(resultType.getChildren().get(i % 2));
+                }
+                break;
+            case ROW:
+                targetTypes.addAll(resultType.getChildren());
+                break;
+            default:
+                throw new ParseException("Unrecognized collection constructor: 
" + sqlBasicCall);
+        }
+
+        List<SqlNode> operands = sqlBasicCall.getOperandList();
+        Java.Rvalue[] atoms = new Java.Rvalue[operands.size()];
+        for (int i = 0; i < operands.size(); i++) {
+            SqlNode operand = operands.get(i);
+            DataType targetType = targetTypes.get(i);
+            if (operand instanceof SqlBasicCall
+                    && isCollectionConstructor((SqlBasicCall) operand)) {
+                SqlBasicCall nestedConstructor = (SqlBasicCall) operand;
+                atoms[i] =
+                        generateCollectionConstructorOperation(
+                                context,
+                                nestedConstructor,
+                                
getCollectionConstructorFunctionName(nestedConstructor),
+                                targetType);
+            } else {
+                atoms[i] = translateSqlNodeToJaninoRvalue(context, operand);
+                if (!(operand instanceof SqlLiteral) || ((SqlLiteral) 
operand).getValue() != null) {
+                    DataType operandType =
+                            TransformParser.deduceSubExpressionType(
+                                    context.columns,
+                                    operand,
+                                    context.udfDescriptors,
+                                    context.supportedMetadataColumns);
+                    atoms[i] = generateImplicitTypeConvertMethod(operandType, 
targetType, atoms[i]);
+                }
+            }
+        }
+        return generateFunctionOperation(functionName, atoms);
+    }
+
+    private static boolean isCollectionConstructor(SqlBasicCall sqlBasicCall) {
+        return sqlBasicCall.getKind() == SqlKind.ARRAY_VALUE_CONSTRUCTOR
+                || sqlBasicCall.getKind() == SqlKind.MAP_VALUE_CONSTRUCTOR
+                || sqlBasicCall.getKind() == SqlKind.ROW;
+    }
+
+    private static String getCollectionConstructorFunctionName(SqlBasicCall 
sqlBasicCall) {
+        switch (sqlBasicCall.getKind()) {
+            case ARRAY_VALUE_CONSTRUCTOR:
+                return "array";
+            case MAP_VALUE_CONSTRUCTOR:
+                return "map";
+            case ROW:
+                return "row";
+            default:
+                throw new ParseException("Unrecognized collection constructor: 
" + sqlBasicCall);
+        }
+    }
+
+    private static Java.Rvalue generateImplicitTypeConvertMethod(
+            DataType sourceType, DataType targetType, Java.Rvalue atom) {
+        if (sourceType.getTypeRoot() == targetType.getTypeRoot()
+                && (!(sourceType instanceof DecimalType)
+                        || 
sourceType.copy(true).equals(targetType.copy(true)))) {
+            return atom;
+        }
+        switch (targetType.getTypeRoot()) {
+            case BOOLEAN:
+                return generateFunctionOperation("castToBoolean", new 
Java.Rvalue[] {atom});
+            case TINYINT:
+                return generateFunctionOperation("castToByte", new 
Java.Rvalue[] {atom});
+            case SMALLINT:
+                return generateFunctionOperation("castToShort", new 
Java.Rvalue[] {atom});
+            case INTEGER:
+                return generateFunctionOperation("castToInteger", new 
Java.Rvalue[] {atom});
+            case BIGINT:
+                return generateFunctionOperation("castToLong", new 
Java.Rvalue[] {atom});
+            case FLOAT:
+                return generateFunctionOperation("castToFloat", new 
Java.Rvalue[] {atom});
+            case DOUBLE:
+                return generateFunctionOperation("castToDouble", new 
Java.Rvalue[] {atom});
+            case DECIMAL:
+                DecimalType decimalType = (DecimalType) targetType;
+                return generateFunctionOperation(
+                        "castToBigDecimal",
+                        new Java.Rvalue[] {
+                            atom,
+                            new Java.AmbiguousName(
+                                    Location.NOWHERE,
+                                    new String[] 
{String.valueOf(decimalType.getPrecision())}),
+                            new Java.AmbiguousName(
+                                    Location.NOWHERE,
+                                    new String[] 
{String.valueOf(decimalType.getScale())})
+                        });
+            case CHAR:
+            case VARCHAR:
+                return generateFunctionOperation("castToString", new 
Java.Rvalue[] {atom});
+            default:
+                return atom;
+        }
+    }
+
     private static Java.Rvalue generateLazyBinaryFunctionOperation(
             Context context, SqlBasicCall sqlBasicCall, String functionName, 
Java.Rvalue[] atoms) {
         if (atoms.length != 2) {
@@ -811,7 +940,11 @@ public class JaninoCompiler {
         Java.Rvalue methodInvocation =
                 new Java.MethodInvocation(Location.NOWHERE, null, 
"itemAccess", atoms);
 
-        // Deduce the return type and add a cast to ensure proper type 
conversion
+        return castExpressionToInferredType(context, sqlBasicCall, 
methodInvocation);
+    }
+
+    private static Java.Rvalue castExpressionToInferredType(
+            Context context, SqlBasicCall sqlBasicCall, Java.Rvalue 
expression) {
         DataType resultType =
                 TransformParser.deduceSubExpressionType(
                         context.columns,
@@ -821,7 +954,7 @@ public class JaninoCompiler {
 
         // Get the Java class for the result type and add a cast
         // Use getCanonicalName() to correctly handle array types (e.g., 
byte[] instead of "[B")
-        return castToJavaType(resultType, methodInvocation);
+        return castToJavaType(resultType, expression);
     }
 
     private static Java.Rvalue castToJavaType(DataType resultType, Java.Rvalue 
expression) {
@@ -859,6 +992,9 @@ public class JaninoCompiler {
             return generateIfNullOperation(context, sqlBasicCall, atoms);
         } else if (operationName.equals("NULLIF")) {
             return generateNullIfOperation(context, sqlBasicCall, atoms);
+        } else if (operationName.equals("ELEMENT")) {
+            return castExpressionToInferredType(
+                    context, sqlBasicCall, 
generateFunctionOperation("element", atoms));
         } else {
             if (isAiFunction(operationName) && atoms.length >= 1) {
                 if (!(sqlBasicCall.operand(0) instanceof 
SqlCharStringLiteral)) {
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
index 8276d323c..5fcf1b608 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/TransformParser.java
@@ -66,7 +66,9 @@ import org.apache.calcite.sql.type.SqlReturnTypeInference;
 import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.util.SqlOperatorTables;
+import org.apache.calcite.sql.validate.SqlConformance;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
+import org.apache.calcite.sql.validate.SqlDelegatingConformance;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.sql2rel.SqlToRelConverter;
@@ -99,12 +101,55 @@ public class TransformParser {
     private static final String DEFAULT_TABLE = "TB";
     private static final String MAPPED_COLUMN_NAME_PREFIX = "$";
     private static final String MAPPED_SINGLE_COLUMN_NAME = 
MAPPED_COLUMN_NAME_PREFIX + "0";
+    private static final SqlConformance TRANSFORM_SQL_CONFORMANCE = new 
TransformSqlConformance();
+
+    private static class TransformSqlConformance extends 
SqlDelegatingConformance {
+
+        private TransformSqlConformance() {
+            super(SqlConformanceEnum.MYSQL_5);
+        }
+
+        @Override
+        public boolean allowCharLiteralAlias() {
+            return SqlConformanceEnum.MYSQL_5.allowCharLiteralAlias();
+        }
+
+        @Override
+        public boolean allowExplicitRowValueConstructor() {
+            return true;
+        }
+
+        @Override
+        public boolean isLimitStartCountAllowed() {
+            return SqlConformanceEnum.MYSQL_5.isLimitStartCountAllowed();
+        }
+
+        @Override
+        public boolean isPercentRemainderAllowed() {
+            return SqlConformanceEnum.MYSQL_5.isPercentRemainderAllowed();
+        }
+
+        @Override
+        public boolean allowGeometry() {
+            return SqlConformanceEnum.MYSQL_5.allowGeometry();
+        }
+
+        @Override
+        public boolean shouldConvertRaggedUnionTypesToVarying() {
+            return 
SqlConformanceEnum.MYSQL_5.shouldConvertRaggedUnionTypesToVarying();
+        }
+
+        @Override
+        public boolean allowExtendedTrim() {
+            return SqlConformanceEnum.MYSQL_5.allowExtendedTrim();
+        }
+    }
 
     private static SqlParser getCalciteParser(String sql) {
         return SqlParser.create(
                 TransformSqlSyntaxRewriter.rewriteTryCast(sql),
                 SqlParser.Config.DEFAULT
-                        .withConformance(SqlConformanceEnum.MYSQL_5)
+                        .withConformance(TRANSFORM_SQL_CONFORMANCE)
                         .withCaseSensitive(true)
                         .withLex(Lex.JAVA));
     }
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/TransformSqlOperatorTable.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/TransformSqlOperatorTable.java
index ff6ca6803..0c37a3d0d 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/TransformSqlOperatorTable.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/parser/metadata/TransformSqlOperatorTable.java
@@ -685,8 +685,30 @@ public class TransformSqlOperatorTable extends 
ReflectiveSqlOperatorTable {
     // ---------------------
     // Struct Functions
     // ---------------------
+    public static final SqlOperator ARRAY = 
SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR;
+    public static final SqlOperator MAP = 
SqlStdOperatorTable.MAP_VALUE_CONSTRUCTOR;
+    public static final SqlOperator ROW = SqlStdOperatorTable.ROW;
+
     // Supports accessing elements of ARRAY[index], ROW[index], MAP[key], and 
VARIANT[index/key]
     public static final SqlOperator ITEM = new VariantAwareItemOperator();
+    public static final SqlFunction CARDINALITY = 
SqlStdOperatorTable.CARDINALITY;
+    public static final SqlFunction ELEMENT = SqlStdOperatorTable.ELEMENT;
+    public static final SqlFunction ARRAY_CONTAINS =
+            new SqlFunction(
+                    "ARRAY_CONTAINS",
+                    SqlKind.OTHER_FUNCTION,
+                    ReturnTypes.BOOLEAN_NULLABLE,
+                    null,
+                    OperandTypes.family(SqlTypeFamily.ARRAY, 
SqlTypeFamily.ANY),
+                    SqlFunctionCategory.SYSTEM);
+    public static final SqlFunction ARRAY_POSITION =
+            new SqlFunction(
+                    "ARRAY_POSITION",
+                    SqlKind.OTHER_FUNCTION,
+                    ReturnTypes.INTEGER_NULLABLE,
+                    null,
+                    OperandTypes.family(SqlTypeFamily.ARRAY, 
SqlTypeFamily.ANY),
+                    SqlFunctionCategory.SYSTEM);
 
     public static final SqlFunction AI_CHAT_PREDICT =
             new SqlFunction(
diff --git 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/typeutils/CalciteDataTypeConverter.java
 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/typeutils/CalciteDataTypeConverter.java
index 012cfa704..4c2dde3f5 100644
--- 
a/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/typeutils/CalciteDataTypeConverter.java
+++ 
b/flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/typeutils/CalciteDataTypeConverter.java
@@ -23,6 +23,7 @@ import org.apache.flink.cdc.common.types.BigIntType;
 import org.apache.flink.cdc.common.types.BinaryType;
 import org.apache.flink.cdc.common.types.BooleanType;
 import org.apache.flink.cdc.common.types.CharType;
+import org.apache.flink.cdc.common.types.DataField;
 import org.apache.flink.cdc.common.types.DataType;
 import org.apache.flink.cdc.common.types.DataTypes;
 import org.apache.flink.cdc.common.types.DateType;
@@ -44,6 +45,8 @@ import org.apache.flink.cdc.common.types.ZonedTimestampType;
 
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.type.SqlTypeName;
 
 import java.util.List;
@@ -339,9 +342,25 @@ public class CalciteDataTypeConverter {
             case VARIANT:
                 return DataTypes.VARIANT();
             case ROW:
+                return DataTypes.ROW(
+                        relDataType.getFieldList().stream()
+                                .map(
+                                        field ->
+                                                DataTypes.FIELD(
+                                                        
normalizeRowFieldName(field),
+                                                        
convertCalciteRelDataTypeToDataType(
+                                                                
field.getType())))
+                                .toArray(DataField[]::new));
             default:
                 throw new UnsupportedOperationException(
                         "Unsupported type: " + relDataType.getSqlTypeName());
         }
     }
+
+    private static String normalizeRowFieldName(RelDataTypeField field) {
+        if 
(field.getName().equals(SqlUtil.deriveAliasFromOrdinal(field.getIndex()))) {
+            return "f" + field.getIndex();
+        }
+        return field.getName();
+    }
 }
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/StructFunctionsTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/StructFunctionsTest.java
index 4d9510451..84ec9e3e9 100644
--- 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/StructFunctionsTest.java
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/functions/impl/StructFunctionsTest.java
@@ -31,10 +31,75 @@ import java.util.List;
 import java.util.Map;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Unit tests for {@link StructFunctions}. */
 class StructFunctionsTest {
 
+    @Nested
+    class CollectionFunctionTests {
+
+        @Test
+        void testCollectionConstructors() {
+            assertThat(StructFunctions.array(1, "two", 
null)).containsExactly(1, "two", null);
+            assertThat(StructFunctions.row(1, "two", null)).containsExactly(1, 
"two", null);
+            assertThat(StructFunctions.map("one", 1, "two", 2, "one", 3))
+                    .containsExactlyInAnyOrderEntriesOf(Map.of("one", 3, 
"two", 2));
+        }
+
+        @Test
+        void testInvalidMapConstructor() {
+            assertThatThrownBy(() -> StructFunctions.map())
+                    .isInstanceOf(IllegalArgumentException.class);
+            assertThatThrownBy(() -> StructFunctions.map("one", 1, "two"))
+                    .isInstanceOf(IllegalArgumentException.class);
+        }
+
+        @Test
+        void testCardinality() {
+            assertThat(StructFunctions.cardinality(Arrays.asList(1, 2, 
3))).isEqualTo(3);
+            assertThat(StructFunctions.cardinality(Map.of("one", 1, "two", 
2))).isEqualTo(2);
+            assertThat(StructFunctions.cardinality((List<?>) null)).isNull();
+            assertThat(StructFunctions.cardinality((Map<?, ?>) null)).isNull();
+        }
+
+        @Test
+        void testArrayContains() {
+            List<Object> array =
+                    Arrays.asList(
+                            1, null, new byte[] {1, 2}, Arrays.asList(2, 
Map.of("three", 3L)));
+
+            assertThat(StructFunctions.arrayContains(array, 1L)).isTrue();
+            assertThat(StructFunctions.arrayContains(array, null)).isTrue();
+            assertThat(StructFunctions.arrayContains(array, new byte[] {1, 
2})).isTrue();
+            assertThat(StructFunctions.arrayContains(array, Arrays.asList(2L, 
Map.of("three", 3))))
+                    .isTrue();
+            assertThat(StructFunctions.arrayContains(array, 4)).isFalse();
+            assertThat(StructFunctions.arrayContains(null, 1)).isNull();
+        }
+
+        @Test
+        void testArrayPosition() {
+            List<Object> array = Arrays.asList(1, null, 2L, 1);
+
+            assertThat(StructFunctions.arrayPosition(array, 1L)).isEqualTo(1);
+            assertThat(StructFunctions.arrayPosition(array, 2)).isEqualTo(3);
+            assertThat(StructFunctions.arrayPosition(array, 3)).isZero();
+            assertThat(StructFunctions.arrayPosition(array, null)).isNull();
+            assertThat(StructFunctions.arrayPosition(null, 1)).isNull();
+        }
+
+        @Test
+        void testElement() {
+            assertThat((Object) StructFunctions.element(null)).isNull();
+            assertThat((Object) 
StructFunctions.element(Collections.emptyList())).isNull();
+            
assertThat(StructFunctions.element(Collections.singletonList("one"))).isEqualTo("one");
+            assertThatThrownBy(() -> 
StructFunctions.element(Arrays.asList("one", "two")))
+                    .isInstanceOf(IllegalArgumentException.class)
+                    .hasMessage("Array has more than one element.");
+        }
+    }
+
     // ========================================
     // List (ARRAY) Access Tests
     // ========================================
diff --git 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/TransformParserTest.java
 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/TransformParserTest.java
index 8efde2e38..621188bbb 100644
--- 
a/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/TransformParserTest.java
+++ 
b/flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/parser/TransformParserTest.java
@@ -136,6 +136,116 @@ class TransformParserTest {
         Assertions.assertThat(parse.getWhere()).hasToString("`uniq_id` > 10 
AND `id` IS NOT NULL");
     }
 
+    @Test
+    void testCollectionConstructorsAndFunctions() {
+        List<Column> columns =
+                List.of(
+                        Column.physicalColumn("id", DataTypes.BIGINT()),
+                        Column.physicalColumn("arr", 
DataTypes.ARRAY(DataTypes.INT())),
+                        Column.physicalColumn(
+                                "m", DataTypes.MAP(DataTypes.STRING(), 
DataTypes.INT())));
+
+        List<ProjectionColumn> projectionColumns =
+                TransformParser.generateProjectionColumns(
+                        "ARRAY[1, id] AS array_col, "
+                                + "MAP['one', 1, 'id', id] AS map_col, "
+                                + "ROW(id, arr) AS row_col, "
+                                + "CARDINALITY(arr) AS array_size, "
+                                + "CARDINALITY(m) AS map_size, "
+                                + "ARRAY_CONTAINS(arr, 2) AS contains_two, "
+                                + "ARRAY_POSITION(arr, 2) AS position_two, "
+                                + "ELEMENT(ARRAY[id]) AS only_element, "
+                                + "ARRAY[ARRAY[1], ARRAY[id]] AS nested_array",
+                        columns,
+                        Collections.emptyList(),
+                        new SupportedMetadataColumn[0]);
+
+        Assertions.assertThat(projectionColumns)
+                .extracting(ProjectionColumn::getDataType)
+                .containsExactly(
+                        DataTypes.ARRAY(DataTypes.BIGINT()),
+                        DataTypes.MAP(DataTypes.STRING(), DataTypes.BIGINT()),
+                        DataTypes.ROW(DataTypes.BIGINT(), 
DataTypes.ARRAY(DataTypes.INT())),
+                        DataTypes.INT(),
+                        DataTypes.INT(),
+                        DataTypes.BOOLEAN(),
+                        DataTypes.INT(),
+                        DataTypes.BIGINT(),
+                        DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.BIGINT())));
+        Assertions.assertThat(projectionColumns)
+                .extracting(ProjectionColumn::getScriptExpression)
+                .containsExactly(
+                        "array(castToLong(1), $0)",
+                        "map(\"one\", castToLong(1), \"id\", $0)",
+                        "row($0, $1)",
+                        "cardinality($0)",
+                        "cardinality($0)",
+                        "arrayContains($0, 2)",
+                        "arrayPosition($0, 2)",
+                        "(java.lang.Long) element(array($0))",
+                        "array(array(castToLong(1)), array($0))");
+    }
+
+    @Test
+    void testCollectionConstructorWithIncompatibleElementTypes() {
+        Assertions.assertThatThrownBy(
+                        () ->
+                                TransformParser.generateProjectionColumns(
+                                        "ARRAY[1, TRUE] AS invalid_array",
+                                        Collections.emptyList(),
+                                        Collections.emptyList(),
+                                        new SupportedMetadataColumn[0]))
+                .isExactlyInstanceOf(CalciteContextException.class)
+                .hasMessageContaining("Parameters must be of the same type");
+    }
+
+    @Test
+    void testPreservesNamedRowFieldsInCollectionTypes() {
+        DataType namedRowType =
+                DataTypes.ROW(
+                        DataTypes.FIELD("name", DataTypes.STRING()),
+                        DataTypes.FIELD("length", DataTypes.INT()));
+        List<Column> columns = List.of(Column.physicalColumn("complex_row_", 
namedRowType));
+
+        List<ProjectionColumn> projectionColumns =
+                TransformParser.generateProjectionColumns(
+                        "ELEMENT(ARRAY[complex_row_]) AS row_element, "
+                                + "ARRAY[complex_row_] AS row_array, "
+                                + "MAP['row', complex_row_] AS row_map",
+                        columns,
+                        Collections.emptyList(),
+                        new SupportedMetadataColumn[0]);
+
+        Assertions.assertThat(projectionColumns)
+                .extracting(ProjectionColumn::getDataType)
+                .containsExactly(
+                        namedRowType,
+                        DataTypes.ARRAY(namedRowType),
+                        DataTypes.MAP(DataTypes.STRING(), namedRowType));
+    }
+
+    @Test
+    void testTranslateCollectionFunctionsToJaninoExpression() {
+        List<Column> columns =
+                List.of(
+                        Column.physicalColumn("id", DataTypes.BIGINT()),
+                        Column.physicalColumn("arr", 
DataTypes.ARRAY(DataTypes.INT())),
+                        Column.physicalColumn(
+                                "m", DataTypes.MAP(DataTypes.STRING(), 
DataTypes.INT())));
+
+        testFilterExpressionWithColumns("CARDINALITY(arr)", 
"cardinality(arr)", columns);
+        testFilterExpressionWithColumns("ARRAY_CONTAINS(arr, 2)", 
"arrayContains(arr, 2)", columns);
+        testFilterExpressionWithColumns("ARRAY_POSITION(arr, 2)", 
"arrayPosition(arr, 2)", columns);
+        testFilterExpressionWithColumns(
+                "ELEMENT(ARRAY[id])", "(java.lang.Long) element(array(id))", 
columns);
+        testFilterExpressionWithColumns(
+                "MAP['one', 1]['one']",
+                "(java.lang.Integer) itemAccess(map(\"one\", 1), \"one\")",
+                columns);
+        testFilterExpressionWithColumns(
+                "ROW(id, arr)[2]", "(java.util.List) itemAccess(row(id, arr), 
2)", columns);
+    }
+
     @Test
     void testTransformCalciteValidate() {
         SqlSelect parse =

Reply via email to