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

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


The following commit(s) were added to refs/heads/master by this push:
     new 69b9cbb14c7 [FLINK-40909][table-planner] Support filter push down for 
element access on ARRAY, MAP and VARIANT
69b9cbb14c7 is described below

commit 69b9cbb14c7243f95918e690c801e20d856bc015
Author: Arvid Heise <[email protected]>
AuthorDate: Mon Oct 5 13:56:19 2026 +0200

    [FLINK-40909][table-planner] Support filter push down for element access on 
ARRAY, MAP and VARIANT
    
    SQL element access (`a[1]`, `m['k']`, `v['k']`) is Calcite's ITEM operator. 
RexNodeToExpressionConverter resolves operators it does not map explicitly by 
name, finds no built-in function called ITEM, and classifies the whole conjunct 
as unconvertible, so such predicates never reach 
SupportsFilterPushDown#applyFilters.
    
    ITEM on an ARRAY, MAP or VARIANT operand now converts to 
BuiltInFunctionDefinitions.AT, the reverse of the existing AT -> ITEM mapping 
in DirectConvertRule. ROW and structured operands, which AT does not accept, 
stay unconvertible. The values test source learns to evaluate AT, and CAST or 
TRY_CAST around AT for VARIANT elements, so the push down on all three types is 
covered by plan and restore tests.
    
    Generated-by: Claude Code (Claude Opus 5.5)
---
 .../planner/plan/utils/RexNodeExtractor.scala      |   8 +-
 .../planner/factories/TestValuesTableFactory.java  |   9 +-
 .../exec/common/TableSourceScanTestPrograms.java   |  52 ++++++
 .../exec/stream/TableSourceScanRestoreTest.java    |   9 +
 .../PushFilterIntoTableSourceScanRuleTest.java     |  34 ++++
 .../flink/table/planner/utils/FilterUtils.java     | 202 ++++++++++++++++++++-
 .../PushFilterIntoTableSourceScanRuleTest.xml      |  73 ++++++++
 ...can-filter-pushdown-on-collection-elements.json | 163 +++++++++++++++++
 .../savepoint/_metadata                            | Bin 0 -> 5889 bytes
 .../planner/plan/utils/RexNodeExtractorTest.scala  |  68 ++++++-
 10 files changed, 603 insertions(+), 15 deletions(-)

diff --git 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/utils/RexNodeExtractor.scala
 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/utils/RexNodeExtractor.scala
index 8ebefbbdc95..24a4cf72a00 100644
--- 
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/utils/RexNodeExtractor.scala
+++ 
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/utils/RexNodeExtractor.scala
@@ -25,7 +25,7 @@ import 
org.apache.flink.table.data.util.DataFormatConverters.{LocalDateConverter
 import org.apache.flink.table.expressions._
 import org.apache.flink.table.expressions.ApiExpressionUtils._
 import org.apache.flink.table.functions.{BuiltInFunctionDefinition, 
FunctionIdentifier}
-import org.apache.flink.table.functions.BuiltInFunctionDefinitions.{AND, CAST, 
OR, TRY_CAST}
+import org.apache.flink.table.functions.BuiltInFunctionDefinitions.{AND, AT, 
CAST, OR, TRY_CAST}
 import org.apache.flink.table.planner.calcite.FlinkTypeFactory
 import org.apache.flink.table.planner.codegen.CodeGenException
 import org.apache.flink.table.planner.functions.sql.FlinkSqlOperatorTable
@@ -533,6 +533,12 @@ class RexNodeToExpressionConverter(
           Option(
             CallExpression
               .permanent(TRY_CAST, Seq(operands.head, 
typeLiteral(outputType)).asJava, outputType))
+        case FlinkSqlOperatorTable.ITEM =>
+          operands.head.getOutputDataType.getLogicalType.getTypeRoot match {
+            case ARRAY | MAP | VARIANT =>
+              Option(CallExpression.permanent(AT, operands.asJava, outputType))
+            case _ => None
+          }
         case _: SqlFunction | _: SqlPostfixOperator =>
           val names = new 
util.ArrayList[String](rexCall.getOperator.getNameAsId.names)
           names.set(names.size() - 1, replace(names.get(names.size() - 1)))
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesTableFactory.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesTableFactory.java
index 7049fc1c8c1..746f68ab9ca 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesTableFactory.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/factories/TestValuesTableFactory.java
@@ -1290,22 +1290,21 @@ public final class TestValuesTableFactory
             return Result.of(acceptedFilters, remainingFilters);
         }
 
-        private Function<String, Comparable<?>> getValueGetter(Row row) {
+        private Function<String, Object> getValueGetter(Row row) {
             final List<String> fieldNames = 
DataTypeUtils.flattenToNames(producedDataType);
             return fieldName -> {
                 int idx = fieldNames.indexOf(fieldName);
-                return (Comparable<?>) row.getField(idx);
+                return row.getField(idx);
             };
         }
 
-        private Function<int[], Comparable<?>> getNestedValueGetter(Row row) {
+        private Function<int[], Object> getNestedValueGetter(Row row) {
             return fieldIndices -> {
                 Object current = row;
                 for (int i = 0; i < fieldIndices.length - 1; i++) {
                     current = ((Row) current).getField(fieldIndices[i]);
                 }
-                return (Comparable<?>)
-                        ((Row) 
current).getField(fieldIndices[fieldIndices.length - 1]);
+                return ((Row) 
current).getField(fieldIndices[fieldIndices.length - 1]);
             };
         }
 
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/common/TableSourceScanTestPrograms.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/common/TableSourceScanTestPrograms.java
index 13e2816bb85..5ff4e1df74e 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/common/TableSourceScanTestPrograms.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/common/TableSourceScanTestPrograms.java
@@ -23,10 +23,16 @@ import org.apache.flink.table.test.program.SourceTestStep;
 import org.apache.flink.table.test.program.TableTestProgram;
 import org.apache.flink.table.utils.DateTimeUtils;
 import org.apache.flink.types.Row;
+import org.apache.flink.types.variant.Variant;
+import org.apache.flink.types.variant.VariantBuilder;
+
+import java.util.Map;
 
 /** {@link TableTestProgram} definitions for testing {@link 
StreamExecTableSourceScan}. */
 public class TableSourceScanTestPrograms {
 
+    private static final VariantBuilder VARIANT_BUILDER = Variant.newBuilder();
+
     static final Row[] BEFORE_DATA = {
         Row.of(1, 1L, "hi", DateTimeUtils.toLocalDateTime(1586937601000L)),
         Row.of(2, 2L, "hello", DateTimeUtils.toLocalDateTime(1586937602000L)),
@@ -105,6 +111,44 @@ public class TableSourceScanTestPrograms {
                     .runSql("INSERT INTO sink_t SELECT * FROM source_t WHERE a 
> 1")
                     .build();
 
+    public static final TableTestProgram 
FILTER_PUSHDOWN_ON_COLLECTION_ELEMENTS =
+            TableTestProgram.of(
+                            
"table-source-scan-filter-pushdown-on-collection-elements",
+                            "validates table source scan with pushed filters 
on array, map and"
+                                    + " variant elements")
+                    .setupTableSource(
+                            SourceTestStep.newBuilder("source_t")
+                                    .addSchema(
+                                            "a INT",
+                                            "arr ARRAY<INT>",
+                                            "m MAP<STRING, INT>",
+                                            "v VARIANT")
+                                    .addOption("filterable-fields", "arr;m;v")
+                                    .producedBeforeRestore(
+                                            collectionRow(1, 2, 2, 2),
+                                            collectionRow(2, 1, 2, 2),
+                                            collectionRow(3, 2, 1, 2),
+                                            collectionRow(4, 2, 2, 1))
+                                    .producedAfterRestore(
+                                            collectionRow(5, 3, 3, 3),
+                                            // empty containers: every element 
access is NULL
+                                            Row.of(
+                                                    6,
+                                                    new Integer[0],
+                                                    Map.of(),
+                                                    
VARIANT_BUILDER.object().build()))
+                                    .build())
+                    .setupTableSink(
+                            SinkTestStep.newBuilder("sink_t")
+                                    .addSchema("a INT")
+                                    .consumedBeforeRestore("+I[1]")
+                                    .consumedAfterRestore("+I[5]")
+                                    .build())
+                    .runSql(
+                            "INSERT INTO sink_t SELECT a FROM source_t"
+                                    + " WHERE arr[1] > 1 AND m['k'] > 1 AND 
CAST(v['k'] AS INT) > 1")
+                    .build();
+
     public static final TableTestProgram LIMIT_PUSHDOWN =
             TableTestProgram.of(
                             "table-source-scan-limit-pushdown",
@@ -260,4 +304,12 @@ public class TableSourceScanTestPrograms {
                             "INSERT INTO sink_one_t SELECT a, c FROM source_t",
                             "INSERT INTO sink_two_t SELECT a, b FROM source_t")
                     .build();
+
+    private static Row collectionRow(int a, int arrayElement, int mapValue, 
int variantField) {
+        return Row.of(
+                a,
+                new Integer[] {arrayElement},
+                Map.of("k", mapValue),
+                VARIANT_BUILDER.object().add("k", 
VARIANT_BUILDER.of(variantField)).build());
+    }
 }
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/TableSourceScanRestoreTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/TableSourceScanRestoreTest.java
index ccc5133cabb..858accafe10 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/TableSourceScanRestoreTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/nodes/exec/stream/TableSourceScanRestoreTest.java
@@ -24,6 +24,7 @@ import org.apache.flink.table.test.program.TableTestProgram;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 
 /** Restore tests for {@link StreamExecTableSourceScan}. */
 public class TableSourceScanRestoreTest extends RestoreTestBase {
@@ -38,6 +39,7 @@ public class TableSourceScanRestoreTest extends 
RestoreTestBase {
                 TableSourceScanTestPrograms.PROJECT_PUSHDOWN,
                 TableSourceScanTestPrograms.PROJECT_PUSHDOWN_DISABLED,
                 TableSourceScanTestPrograms.FILTER_PUSHDOWN,
+                
TableSourceScanTestPrograms.FILTER_PUSHDOWN_ON_COLLECTION_ELEMENTS,
                 TableSourceScanTestPrograms.LIMIT_PUSHDOWN,
                 TableSourceScanTestPrograms.PARTITION_PUSHDOWN,
                 TableSourceScanTestPrograms.READING_METADATA,
@@ -45,4 +47,11 @@ public class TableSourceScanRestoreTest extends 
RestoreTestBase {
                 TableSourceScanTestPrograms.SOURCE_WATERMARK,
                 TableSourceScanTestPrograms.REUSE_SOURCE);
     }
+
+    @Override
+    protected Map<Integer, List<TableTestProgram>> programsToIgnore() {
+        return Map.of(
+                // plans of this program exist only for version 2
+                1, 
List.of(TableSourceScanTestPrograms.FILTER_PUSHDOWN_ON_COLLECTION_ELEMENTS));
+    }
 }
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/rules/logical/PushFilterIntoTableSourceScanRuleTest.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/rules/logical/PushFilterIntoTableSourceScanRuleTest.java
index da21dbc5b33..b294db59e89 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/rules/logical/PushFilterIntoTableSourceScanRuleTest.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/plan/rules/logical/PushFilterIntoTableSourceScanRuleTest.java
@@ -120,6 +120,20 @@ class PushFilterIntoTableSourceScanRuleTest extends 
PushFilterIntoTableSourceSca
                         + " 'bounded' = 'true'\n"
                         + ")";
         util.tableEnv().executeSql(ddl4);
+
+        String ddl5 =
+                "CREATE TABLE ItemTable (\n"
+                        + "  id INT,\n"
+                        + "  arr ARRAY<INT>,\n"
+                        + "  m MAP<STRING, INT>,\n"
+                        + "  v VARIANT,\n"
+                        + "  nested ROW<arr ARRAY<INT>>\n"
+                        + ") WITH (\n"
+                        + " 'connector' = 'values',\n"
+                        + " 'filterable-fields' = 'arr;m;v;`nested.arr`',\n"
+                        + " 'bounded' = 'true'\n"
+                        + ")";
+        util.tableEnv().executeSql(ddl5);
     }
 
     @Test
@@ -204,4 +218,24 @@ class PushFilterIntoTableSourceScanRuleTest extends 
PushFilterIntoTableSourceSca
         util.verifyRelPlan(
                 "SELECT * FROM NestedItemTable WHERE 
`Result`.`Mid`.data_arr[2].`value` = 3");
     }
+
+    @Test
+    void testFilterOnArrayElement() {
+        util.verifyRelPlan("SELECT * FROM ItemTable WHERE arr[1] = 3");
+    }
+
+    @Test
+    void testFilterOnMapValue() {
+        util.verifyRelPlan("SELECT * FROM ItemTable WHERE m['k'] > 2 AND id > 
1");
+    }
+
+    @Test
+    void testFilterOnVariantField() {
+        util.verifyRelPlan("SELECT * FROM ItemTable WHERE CAST(v['k'] AS INT) 
> 2");
+    }
+
+    @Test
+    void testFilterOnNestedArrayElement() {
+        util.verifyRelPlan("SELECT * FROM ItemTable WHERE nested.arr[1] = 3");
+    }
 }
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/utils/FilterUtils.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/utils/FilterUtils.java
index 32d3afa233a..6965d55cef7 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/utils/FilterUtils.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/utils/FilterUtils.java
@@ -27,21 +27,89 @@ import 
org.apache.flink.table.expressions.ResolvedExpression;
 import org.apache.flink.table.expressions.ValueLiteralExpression;
 import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
 import org.apache.flink.table.functions.FunctionDefinition;
+import org.apache.flink.table.runtime.functions.VariantCastUtils;
+import org.apache.flink.table.types.logical.CharType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.LogicalTypeFamily;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
+import org.apache.flink.table.types.logical.MapType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.types.variant.Variant;
 import org.apache.flink.util.Preconditions;
 
 import javax.annotation.Nullable;
 
+import java.lang.reflect.Array;
+import java.util.EnumMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.TimeZone;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
+import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.AT;
+import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.CAST;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.LOWER;
+import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.TRY_CAST;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.UPPER;
 
 /** Utils for catalog and source to filter partition or row. */
 public class FilterUtils {
 
+    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
+
+    /**
+     * Casts of a VARIANT element that can be evaluated, by target type, using 
the same {@link
+     * VariantCastUtils} helpers as the generated code of {@code 
VariantToPrimitiveCastRule}.
+     */
+    private static final Map<LogicalTypeRoot, BiFunction<Variant, LogicalType, 
Object>>
+            VARIANT_CASTS = new EnumMap<>(LogicalTypeRoot.class);
+
+    static {
+        VARIANT_CASTS.put(LogicalTypeRoot.BOOLEAN, (variant, type) -> 
variant.getBoolean());
+        VARIANT_CASTS.put(
+                LogicalTypeRoot.TINYINT,
+                (variant, type) ->
+                        (byte)
+                                VariantCastUtils.toIntegral(
+                                        variant, Byte.MIN_VALUE, 
Byte.MAX_VALUE, "TINYINT"));
+        VARIANT_CASTS.put(
+                LogicalTypeRoot.SMALLINT,
+                (variant, type) ->
+                        (short)
+                                VariantCastUtils.toIntegral(
+                                        variant, Short.MIN_VALUE, 
Short.MAX_VALUE, "SMALLINT"));
+        VARIANT_CASTS.put(
+                LogicalTypeRoot.INTEGER,
+                (variant, type) ->
+                        (int)
+                                VariantCastUtils.toIntegral(
+                                        variant, Integer.MIN_VALUE, 
Integer.MAX_VALUE, "INT"));
+        VARIANT_CASTS.put(
+                LogicalTypeRoot.BIGINT,
+                (variant, type) ->
+                        VariantCastUtils.toIntegral(
+                                variant, Long.MIN_VALUE, Long.MAX_VALUE, 
"BIGINT"));
+        VARIANT_CASTS.put(
+                LogicalTypeRoot.FLOAT, (variant, type) -> 
VariantCastUtils.toFloat(variant));
+        VARIANT_CASTS.put(
+                LogicalTypeRoot.DOUBLE, (variant, type) -> 
VariantCastUtils.toDouble(variant));
+        VARIANT_CASTS.put(
+                LogicalTypeRoot.CHAR,
+                (variant, type) ->
+                        VariantCastUtils.toStringValue(
+                                        variant, UTC, ((CharType) 
type).getLength(), true)
+                                .toString());
+        VARIANT_CASTS.put(
+                LogicalTypeRoot.VARCHAR,
+                (variant, type) ->
+                        VariantCastUtils.toStringValue(
+                                        variant, UTC, ((VarCharType) 
type).getLength(), false)
+                                .toString());
+    }
+
     public static boolean shouldPushDown(ResolvedExpression expr, Set<String> 
filterableFields) {
         if (expr instanceof CallExpression && expr.getChildren().size() == 2) {
             return shouldPushDownUnaryExpression(
@@ -54,8 +122,8 @@ public class FilterUtils {
 
     public static boolean isRetainedAfterApplyingFilterPredicates(
             List<ResolvedExpression> predicates,
-            Function<String, Comparable<?>> getter,
-            @Nullable Function<int[], Comparable<?>> nestedFieldGetter) {
+            Function<String, ?> getter,
+            @Nullable Function<int[], ?> nestedFieldGetter) {
         for (ResolvedExpression predicate : predicates) {
             if (predicate instanceof CallExpression) {
                 FunctionDefinition definition =
@@ -115,6 +183,19 @@ public class FilterUtils {
             return true;
         }
 
+        if (isElementAccess(expr, filterableFields)) {
+            return true;
+        }
+
+        // A cast makes an element of a VARIANT comparable, e.g. CAST(v['k'] 
AS INT).
+        if (isCall(expr, CAST) || isCall(expr, TRY_CAST)) {
+            final ResolvedExpression element = 
expr.getResolvedChildren().get(0);
+            return VARIANT_CASTS.containsKey(
+                            
expr.getOutputDataType().getLogicalType().getTypeRoot())
+                    && 
element.getOutputDataType().getLogicalType().is(LogicalTypeRoot.VARIANT)
+                    && isElementAccess(element, filterableFields);
+        }
+
         if (expr instanceof CallExpression && expr.getChildren().size() == 1) {
             if (((CallExpression) expr).getFunctionDefinition().equals(UPPER)
                     || ((CallExpression) 
expr).getFunctionDefinition().equals(LOWER)) {
@@ -126,16 +207,60 @@ public class FilterUtils {
         return false;
     }
 
+    private static boolean isElementAccess(ResolvedExpression expr, 
Set<String> filterableFields) {
+        if (!isCall(expr, AT)) {
+            return false;
+        }
+        final ResolvedExpression container = expr.getResolvedChildren().get(0);
+        final ResolvedExpression key = expr.getResolvedChildren().get(1);
+        return isFilterableField(container, filterableFields)
+                && key instanceof ValueLiteralExpression
+                && isEvaluableKey(
+                        container.getOutputDataType().getLogicalType(),
+                        key.getOutputDataType().getLogicalType());
+    }
+
+    /** A map lookup only finds the key if the literal has the key's Java 
type. */
+    private static boolean isEvaluableKey(LogicalType containerType, 
LogicalType keyType) {
+        if (!containerType.is(LogicalTypeRoot.MAP)) {
+            return true;
+        }
+        final LogicalType mapKeyType = ((MapType) containerType).getKeyType();
+        return mapKeyType.is(keyType.getTypeRoot())
+                || (mapKeyType.is(LogicalTypeFamily.CHARACTER_STRING)
+                        && keyType.is(LogicalTypeFamily.CHARACTER_STRING));
+    }
+
+    private static boolean isCall(Expression expr, FunctionDefinition 
definition) {
+        return expr instanceof CallExpression
+                && ((CallExpression) 
expr).getFunctionDefinition().equals(definition);
+    }
+
+    private static boolean isFilterableField(
+            ResolvedExpression expr, Set<String> filterableFields) {
+        if (expr instanceof FieldReferenceExpression) {
+            return filterableFields.contains(((FieldReferenceExpression) 
expr).getName());
+        }
+        if (expr instanceof NestedFieldReferenceExpression) {
+            return filterableFields.contains(((NestedFieldReferenceExpression) 
expr).getName());
+        }
+        return false;
+    }
+
     @SuppressWarnings({"unchecked", "rawtypes"})
     private static boolean binaryFilterApplies(
             CallExpression binExpr,
-            Function<String, Comparable<?>> getter,
-            Function<int[], Comparable<?>> nestedFieldGetter) {
+            Function<String, ?> getter,
+            Function<int[], ?> nestedFieldGetter) {
         List<Expression> children = binExpr.getChildren();
         Preconditions.checkArgument(children.size() == 2);
 
         Comparable lhsValue = getValue(children.get(0), getter, 
nestedFieldGetter);
         Comparable rhsValue = getValue(children.get(1), getter, 
nestedFieldGetter);
+        if (lhsValue == null || rhsValue == null) {
+            // A comparison with NULL is never true, e.g. for an array index 
out of bounds.
+            return false;
+        }
         FunctionDefinition functionDefinition = 
binExpr.getFunctionDefinition();
         if 
(BuiltInFunctionDefinitions.GREATER_THAN.equals(functionDefinition)) {
             return lhsValue.compareTo(rhsValue) > 0;
@@ -154,14 +279,44 @@ public class FilterUtils {
         }
     }
 
+    /** Returns NULL for a missing field, an out-of-range index or a container 
of the wrong kind. */
+    private static @Nullable Variant getVariantElement(Variant variant, Object 
key) {
+        if (key instanceof String) {
+            return variant.isObject() ? variant.getField((String) key) : null;
+        }
+        final int index = ((Number) key).intValue();
+        return variant.isArray() && 1 <= index && index <= 
variant.getArraySize()
+                ? variant.getElement(index - 1)
+                : null;
+    }
+
+    /** Casts a VARIANT element like the generated code: an error yields NULL 
for TRY_CAST. */
+    private static @Nullable Object castVariant(
+            @Nullable Variant variant, LogicalType targetType, boolean 
tryCast) {
+        if (variant == null || variant.isNull()) {
+            return null;
+        }
+        try {
+            return VARIANT_CASTS.get(targetType.getTypeRoot()).apply(variant, 
targetType);
+        } catch (RuntimeException e) {
+            if (tryCast) {
+                return null;
+            }
+            throw e;
+        }
+    }
+
     private static boolean isComparable(Class<?> clazz) {
         return Comparable.class.isAssignableFrom(clazz);
     }
 
     private static Comparable<?> getValue(
-            Expression expr,
-            Function<String, Comparable<?>> getter,
-            Function<int[], Comparable<?>> nestedFieldGetter) {
+            Expression expr, Function<String, ?> getter, Function<int[], ?> 
nestedFieldGetter) {
+        return (Comparable<?>) getRawValue(expr, getter, nestedFieldGetter);
+    }
+
+    private static Object getRawValue(
+            Expression expr, Function<String, ?> getter, Function<int[], ?> 
nestedFieldGetter) {
         if (expr instanceof ValueLiteralExpression) {
             Optional<?> value =
                     ((ValueLiteralExpression) expr)
@@ -169,7 +324,7 @@ public class FilterUtils {
                                     ((ValueLiteralExpression) expr)
                                             .getOutputDataType()
                                             .getConversionClass());
-            return (Comparable<?>) value.orElse(null);
+            return value.orElse(null);
         }
 
         if (expr instanceof FieldReferenceExpression) {
@@ -185,6 +340,37 @@ public class FilterUtils {
             }
         }
 
+        if (isCall(expr, CAST) || isCall(expr, TRY_CAST)) {
+            return castVariant(
+                    (Variant) getRawValue(expr.getChildren().get(0), getter, 
nestedFieldGetter),
+                    ((CallExpression) 
expr).getOutputDataType().getLogicalType(),
+                    isCall(expr, TRY_CAST));
+        }
+
+        if (isCall(expr, AT)) {
+            final Object container =
+                    getRawValue(expr.getChildren().get(0), getter, 
nestedFieldGetter);
+            final Object key = getRawValue(expr.getChildren().get(1), getter, 
nestedFieldGetter);
+            if (container == null || key == null) {
+                return null;
+            }
+            if (container instanceof Map) {
+                return ((Map<?, ?>) container).get(key);
+            }
+            if (container.getClass().isArray()) {
+                // SQL array indices are 1-based.
+                final int index = ((Number) key).intValue();
+                return 1 <= index && index <= Array.getLength(container)
+                        ? Array.get(container, index - 1)
+                        : null;
+            }
+            if (container instanceof Variant) {
+                return getVariantElement((Variant) container, key);
+            }
+            throw new UnsupportedOperationException(
+                    String.format("Unsupported container for %s: %s.", expr, 
container.getClass()));
+        }
+
         if (expr instanceof CallExpression && expr.getChildren().size() == 1) {
             Object child = getValue(expr.getChildren().get(0), getter, 
nestedFieldGetter);
             FunctionDefinition functionDefinition = ((CallExpression) 
expr).getFunctionDefinition();
diff --git 
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/rules/logical/PushFilterIntoTableSourceScanRuleTest.xml
 
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/rules/logical/PushFilterIntoTableSourceScanRuleTest.xml
index c28a91adaa8..72f4d2ca52b 100644
--- 
a/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/rules/logical/PushFilterIntoTableSourceScanRuleTest.xml
+++ 
b/flink-table/flink-table-planner/src/test/resources/org/apache/flink/table/planner/plan/rules/logical/PushFilterIntoTableSourceScanRuleTest.xml
@@ -205,6 +205,79 @@ LogicalProject(name=[$0], id=[$1], amount=[$2], 
virtualField=[$3], price=[$4])
 +- LogicalProject(name=[$0], id=[$1], amount=[$2], virtualField=[+($2, 1)], 
price=[$3])
    +- LogicalFilter(condition=[>($3, 10)])
       +- LogicalTableScan(table=[[default_catalog, default_database, 
VirtualTable, filter=[]]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFilterOnArrayElement">
+    <Resource name="sql">
+      <![CDATA[SELECT * FROM ItemTable WHERE arr[1] = 3]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(id=[$0], arr=[$1], m=[$2], v=[$3], nested=[$4])
++- LogicalFilter(condition=[=(ITEM($1, 1), 3)])
+   +- LogicalTableScan(table=[[default_catalog, default_database, ItemTable]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+LogicalProject(id=[$0], arr=[$1], m=[$2], v=[$3], nested=[$4])
++- LogicalTableScan(table=[[default_catalog, default_database, ItemTable, 
filter=[=(ITEM(arr, 1), 3)]]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFilterOnMapValue">
+    <Resource name="sql">
+      <![CDATA[SELECT * FROM ItemTable WHERE m['k'] > 2 AND id > 1]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(id=[$0], arr=[$1], m=[$2], v=[$3], nested=[$4])
++- LogicalFilter(condition=[AND(>(ITEM($2, _UTF-16LE'k'), 2), >($0, 1))])
+   +- LogicalTableScan(table=[[default_catalog, default_database, ItemTable]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+LogicalProject(id=[$0], arr=[$1], m=[$2], v=[$3], nested=[$4])
++- LogicalFilter(condition=[>($0, 1)])
+   +- LogicalTableScan(table=[[default_catalog, default_database, ItemTable, 
filter=[>(ITEM(m, _UTF-16LE'k'), 2)]]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFilterOnNestedArrayElement">
+    <Resource name="sql">
+      <![CDATA[SELECT * FROM ItemTable WHERE nested.arr[1] = 3]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(id=[$0], arr=[$1], m=[$2], v=[$3], nested=[$4])
++- LogicalFilter(condition=[=(ITEM($4.arr, 1), 3)])
+   +- LogicalTableScan(table=[[default_catalog, default_database, ItemTable]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+LogicalProject(id=[$0], arr=[$1], m=[$2], v=[$3], nested=[$4])
++- LogicalTableScan(table=[[default_catalog, default_database, ItemTable, 
filter=[=(ITEM(nested.arr, 1), 3)]]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testFilterOnVariantField">
+    <Resource name="sql">
+      <![CDATA[SELECT * FROM ItemTable WHERE CAST(v['k'] AS INT) > 2]]>
+    </Resource>
+    <Resource name="ast">
+      <![CDATA[
+LogicalProject(id=[$0], arr=[$1], m=[$2], v=[$3], nested=[$4])
++- LogicalFilter(condition=[>(CAST(ITEM($3, _UTF-16LE'k')):INTEGER, 2)])
+   +- LogicalTableScan(table=[[default_catalog, default_database, ItemTable]])
+]]>
+    </Resource>
+    <Resource name="optimized rel plan">
+      <![CDATA[
+LogicalProject(id=[$0], arr=[$1], m=[$2], v=[$3], nested=[$4])
++- LogicalTableScan(table=[[default_catalog, default_database, ItemTable, 
filter=[>(CAST(ITEM(v, _UTF-16LE'k') AS INTEGER), 2)]]])
 ]]>
     </Resource>
   </TestCase>
diff --git 
a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-table-source-scan_2/table-source-scan-filter-pushdown-on-collection-elements/plan/table-source-scan-filter-pushdown-on-collection-elements.json
 
b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-table-source-scan_2/table-source-scan-filter-pushdown-on-collection-elements/plan/table-source-scan-filter-pushdown-on-collection-elements.json
new file mode 100644
index 00000000000..2adaa8164c4
--- /dev/null
+++ 
b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-table-source-scan_2/table-source-scan-filter-pushdown-on-collection-elements/plan/table-source-scan-filter-pushdown-on-collection-elements.json
@@ -0,0 +1,163 @@
+{
+  "flinkVersion" : "2.4",
+  "nodes" : [ {
+    "id" : 1,
+    "type" : "stream-exec-table-source-scan_2",
+    "scanTableSource" : {
+      "table" : {
+        "identifier" : "`default_catalog`.`default_database`.`source_t`",
+        "resolvedTable" : {
+          "schema" : {
+            "columns" : [ {
+              "name" : "a",
+              "dataType" : "INT"
+            }, {
+              "name" : "arr",
+              "dataType" : "ARRAY<INT>"
+            }, {
+              "name" : "m",
+              "dataType" : "MAP<VARCHAR(2147483647), INT>"
+            }, {
+              "name" : "v",
+              "dataType" : "VARIANT"
+            } ]
+          }
+        }
+      },
+      "abilities" : [ {
+        "type" : "FilterPushDown",
+        "predicates" : [ {
+          "kind" : "CALL",
+          "syntax" : "BINARY",
+          "internalName" : "$>$1",
+          "operands" : [ {
+            "kind" : "CALL",
+            "syntax" : "SPECIAL",
+            "internalName" : "$ITEM$1",
+            "operands" : [ {
+              "kind" : "INPUT_REF",
+              "inputIndex" : 1,
+              "type" : "ARRAY<INT>"
+            }, {
+              "kind" : "LITERAL",
+              "value" : 1,
+              "type" : "INT NOT NULL"
+            } ],
+            "type" : "INT"
+          }, {
+            "kind" : "LITERAL",
+            "value" : 1,
+            "type" : "INT NOT NULL"
+          } ],
+          "type" : "BOOLEAN"
+        }, {
+          "kind" : "CALL",
+          "syntax" : "BINARY",
+          "internalName" : "$>$1",
+          "operands" : [ {
+            "kind" : "CALL",
+            "syntax" : "SPECIAL",
+            "internalName" : "$ITEM$1",
+            "operands" : [ {
+              "kind" : "INPUT_REF",
+              "inputIndex" : 2,
+              "type" : "MAP<VARCHAR(2147483647), INT>"
+            }, {
+              "kind" : "LITERAL",
+              "value" : "k",
+              "type" : "CHAR(1) NOT NULL"
+            } ],
+            "type" : "INT"
+          }, {
+            "kind" : "LITERAL",
+            "value" : 1,
+            "type" : "INT NOT NULL"
+          } ],
+          "type" : "BOOLEAN"
+        }, {
+          "kind" : "CALL",
+          "syntax" : "BINARY",
+          "internalName" : "$>$1",
+          "operands" : [ {
+            "kind" : "CALL",
+            "syntax" : "SPECIAL",
+            "internalName" : "$CAST$1",
+            "operands" : [ {
+              "kind" : "CALL",
+              "syntax" : "SPECIAL",
+              "internalName" : "$ITEM$1",
+              "operands" : [ {
+                "kind" : "INPUT_REF",
+                "inputIndex" : 3,
+                "type" : "VARIANT"
+              }, {
+                "kind" : "LITERAL",
+                "value" : "k",
+                "type" : "CHAR(1) NOT NULL"
+              } ],
+              "type" : "VARIANT"
+            } ],
+            "type" : "INT"
+          }, {
+            "kind" : "LITERAL",
+            "value" : 1,
+            "type" : "INT NOT NULL"
+          } ],
+          "type" : "BOOLEAN"
+        } ]
+      }, {
+        "type" : "ProjectPushDown",
+        "projectedFields" : [ [ 0 ] ],
+        "producedType" : "ROW<`a` INT> NOT NULL"
+      }, {
+        "type" : "ReadingMetadata",
+        "metadataKeys" : [ ],
+        "producedType" : "ROW<`a` INT> NOT NULL"
+      } ]
+    },
+    "outputType" : "ROW<`a` INT>",
+    "description" : "TableSourceScan(table=[[default_catalog, 
default_database, source_t, filter=[AND(AND(>(ITEM(arr, 1), 1), >(ITEM(m, 
_UTF-16LE'k'), 1)), >(CAST(ITEM(v, _UTF-16LE'k') AS INTEGER), 1))], 
project=[a], metadata=[]]], fields=[a])"
+  }, {
+    "id" : 2,
+    "type" : "stream-exec-sink_2",
+    "configuration" : {
+      "table.exec.sink.keyed-shuffle" : "AUTO",
+      "table.exec.sink.not-null-enforcer" : "ERROR",
+      "table.exec.sink.rowtime-inserter" : "ENABLED",
+      "table.exec.sink.type-length-enforcer" : "IGNORE",
+      "table.exec.sink.upsert-materialize" : "AUTO"
+    },
+    "dynamicTableSink" : {
+      "table" : {
+        "identifier" : "`default_catalog`.`default_database`.`sink_t`",
+        "resolvedTable" : {
+          "schema" : {
+            "columns" : [ {
+              "name" : "a",
+              "dataType" : "INT"
+            } ]
+          }
+        }
+      }
+    },
+    "inputChangelogMode" : [ "INSERT" ],
+    "upsertMaterializeStrategy" : "ADAPTIVE",
+    "inputProperties" : [ {
+      "requiredDistribution" : {
+        "type" : "UNKNOWN"
+      },
+      "damBehavior" : "PIPELINED",
+      "priority" : 0
+    } ],
+    "outputType" : "ROW<`a` INT>",
+    "description" : "Sink(table=[default_catalog.default_database.sink_t], 
fields=[a])"
+  } ],
+  "edges" : [ {
+    "source" : 1,
+    "target" : 2,
+    "shuffle" : {
+      "type" : "FORWARD"
+    },
+    "shuffleMode" : "PIPELINED"
+  } ]
+}
\ No newline at end of file
diff --git 
a/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-table-source-scan_2/table-source-scan-filter-pushdown-on-collection-elements/savepoint/_metadata
 
b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-table-source-scan_2/table-source-scan-filter-pushdown-on-collection-elements/savepoint/_metadata
new file mode 100644
index 00000000000..080b403ca35
Binary files /dev/null and 
b/flink-table/flink-table-planner/src/test/resources/restore-tests/stream-exec-table-source-scan_2/table-source-scan-filter-pushdown-on-collection-elements/savepoint/_metadata
 differ
diff --git 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/utils/RexNodeExtractorTest.scala
 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/utils/RexNodeExtractorTest.scala
index 4af1f7be9a6..6792afc81b4 100644
--- 
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/utils/RexNodeExtractorTest.scala
+++ 
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/utils/RexNodeExtractorTest.scala
@@ -31,6 +31,8 @@ import 
org.apache.flink.table.planner.functions.sql.FlinkSqlOperatorTable
 import org.apache.flink.table.planner.functions.utils.ScalarSqlFunction
 import org.apache.flink.table.planner.utils.{DateTimeTestUtil, 
IntSumAggFunction}
 import org.apache.flink.table.resource.ResourceManager
+import org.apache.flink.table.runtime.types.LogicalTypeDataTypeConverter
+import org.apache.flink.table.types.logical.IntType
 import org.apache.flink.table.utils.CatalogManagerMocks
 
 import org.apache.calcite.avatica.util.ByteString
@@ -38,7 +40,7 @@ import org.apache.calcite.rel.`type`.RelDataType
 import org.apache.calcite.rex.{RexBuilder, RexNode}
 import org.apache.calcite.sql.`type`.SqlTypeName
 import org.apache.calcite.sql.SqlPostfixOperator
-import org.apache.calcite.sql.fun.{SqlStdOperatorTable, SqlTrimFunction}
+import org.apache.calcite.sql.fun.{SqlLibraryOperators, SqlStdOperatorTable, 
SqlTrimFunction}
 import org.apache.calcite.util.{DateString, TimestampString, TimeString}
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
@@ -498,6 +500,70 @@ class RexNodeExtractorTest extends RexNodeTestBase {
     assertExpressionArrayEquals(Array($"amount" <= $"id"), 
Array(convertedExpressions(1)))
   }
 
+  @Test
+  def testExtractItem(): Unit = {
+    val fieldNames = List("arr", "map", "variant", "row").asJava
+    val fieldTypes = List(
+      DataTypes.ARRAY(DataTypes.INT()),
+      DataTypes.MAP(DataTypes.STRING(), DataTypes.INT()),
+      DataTypes.VARIANT(),
+      DataTypes.ROW(DataTypes.FIELD("f", DataTypes.INT()))
+    )
+      .map(LogicalTypeDataTypeConverter.fromDataTypeToLogicalType)
+      .map(typeFactory.createFieldTypeFromLogicalType)
+    val Seq(arr, map, variant, row) =
+      fieldTypes.zipWithIndex.map { case (t, i) => rexBuilder.makeInputRef(t, 
i) }
+    val three = rexBuilder.makeExactLiteral(BigDecimal.valueOf(3))
+    val intType = typeFactory.createFieldTypeFromLogicalType(new IntType())
+
+    // arr[1] = 3
+    val onArray = rexBuilder.makeCall(
+      SqlStdOperatorTable.EQUALS,
+      rexBuilder.makeCall(
+        SqlStdOperatorTable.ITEM,
+        arr,
+        rexBuilder.makeExactLiteral(BigDecimal.ONE)),
+      three)
+    // map['k'] = 3
+    val onMap = rexBuilder.makeCall(
+      SqlStdOperatorTable.EQUALS,
+      rexBuilder.makeCall(SqlStdOperatorTable.ITEM, map, 
rexBuilder.makeLiteral("k")),
+      three)
+    // CAST(variant['k'] AS INT) = 3
+    val onVariant = rexBuilder.makeCall(
+      SqlStdOperatorTable.EQUALS,
+      rexBuilder.makeCast(
+        intType,
+        rexBuilder.makeCall(SqlStdOperatorTable.ITEM, variant, 
rexBuilder.makeLiteral("k"))),
+      three)
+    // arr[OFFSET(0)] = 3, a 0-based variant of ITEM that AT does not cover
+    val onOffset = rexBuilder.makeCall(
+      SqlStdOperatorTable.EQUALS,
+      rexBuilder.makeCall(
+        SqlLibraryOperators.OFFSET,
+        arr,
+        rexBuilder.makeExactLiteral(BigDecimal.ZERO)),
+      three)
+    // row['f'] = 3, which AT does not support
+    val onRow = rexBuilder.makeCall(
+      SqlStdOperatorTable.EQUALS,
+      rexBuilder.makeCall(SqlStdOperatorTable.ITEM, row, 
rexBuilder.makeLiteral("f")),
+      three)
+
+    val (convertedExpressions, unconvertedRexNodes) = 
extractConjunctiveConditions(
+      rexBuilder.makeCall(SqlStdOperatorTable.AND, onArray, onMap, onVariant, 
onOffset, onRow),
+      fieldNames,
+      rexBuilder,
+      functionCatalog)
+
+    assertThat(convertedExpressions.map(_.toString))
+      .containsExactly(
+        "equals(at(arr, 1), 3)",
+        "equals(at(map, 'k'), 3)",
+        "equals(cast(at(variant, 'k'), INT), 3)")
+    assertThat(unconvertedRexNodes).containsExactly(onOffset, onRow)
+  }
+
   @Test
   def testExtractWithTrim(): Unit = {
     // name

Reply via email to