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

snuyanzin 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 4d43dde204e [FLINK-40355][table] Add new `MAP_CONTAINS_KEY` function
4d43dde204e is described below

commit 4d43dde204ef2fb8586c04adbbe3e7c5525e957a
Author: Vasudev Kelappassery <[email protected]>
AuthorDate: Tue Sep 15 20:54:18 2026 +0100

    [FLINK-40355][table] Add new `MAP_CONTAINS_KEY` function
---
 docs/data/sql_functions.yml                        |  19 ++++
 docs/data/sql_functions_zh.yml                     |  19 ++++
 .../docs/reference/pyflink.table/expressions.rst   |   1 +
 flink-python/pyflink/table/expression.py           |  18 ++++
 .../flink/table/api/internal/BaseExpressions.java  |  22 +++++
 .../functions/BuiltInFunctionDefinitions.java      |  16 ++++
 .../strategies/MapKeyArgumentTypeStrategy.java     |  71 ++++++++++++++
 .../strategies/SpecificInputTypeStrategies.java    |   3 +
 .../types/inference/InputTypeStrategiesTest.java   |  37 ++++++++
 .../table/planner/functions/MapFunctionITCase.java | 103 +++++++++++++++++++++
 .../functions/scalar/MapContainsKeyFunction.java   | 103 +++++++++++++++++++++
 11 files changed, 412 insertions(+)

diff --git a/docs/data/sql_functions.yml b/docs/data/sql_functions.yml
index ea8cc881368..f7517549d1f 100644
--- a/docs/data/sql_functions.yml
+++ b/docs/data/sql_functions.yml
@@ -931,6 +931,25 @@ collection:
   - sql: MAP_ENTRIES(map)
     table: MAP.mapEntries()
     description: Returns an array of all entries in the given map. No order 
guaranteed.
+  - sql: MAP_CONTAINS_KEY(map, key)
+    table: MAP.mapContainsKey(key)
+    description: |
+      Returns TRUE if the given key exists in the map, FALSE otherwise. 
Returns NULL if the map is
+      NULL. 
+      
+      If the search key is NULL, the function returns TRUE when the map 
contains a NULL key.
+      The given key is cast implicitly to the map's key type where Flink's 
implicit casting rules
+      allow it; otherwise the call fails validation.
+      
+      Examples
+        -- TRUE
+        MAP_CONTAINS_KEY(MAP['a', 1, 'b', 2], 'a')
+        -- FALSE
+        MAP_CONTAINS_KEY(MAP['a', 1, 'b', 2], 'z')
+        -- TRUE
+        MAP_CONTAINS_KEY(MAP[CAST(NULL AS STRING), 1], CAST(NULL AS STRING))
+        -- TRUE, the TINYINT key is cast to the map's INT key type
+        MAP_CONTAINS_KEY(MAP[1, 'a'], CAST(1 AS TINYINT))
   - sql: MAP_FROM_ARRAYS(array_of_keys, array_of_values)
     table: mapFromArrays(array_of_keys, array_of_values)
     description: Returns a map created from an arrays of keys and values. Note 
that the lengths of two arrays should be the same.
diff --git a/docs/data/sql_functions_zh.yml b/docs/data/sql_functions_zh.yml
index ab3cfa539c1..a7f9b7bb2f4 100644
--- a/docs/data/sql_functions_zh.yml
+++ b/docs/data/sql_functions_zh.yml
@@ -1064,6 +1064,25 @@ collection:
   - sql: MAP_ENTRIES(map)
     table: MAP.mapEntries()
     description: 以数组形式返回 map 中的所有 entry,不保证顺序。
+  - sql: MAP_CONTAINS_KEY(map, key)
+    table: MAP.mapContainsKey(key)
+    description: |
+      Returns TRUE if the given key exists in the map, FALSE otherwise. 
Returns NULL if the map is
+      NULL. 
+      
+      If the search key is NULL, the function returns TRUE when the map 
contains a NULL key.
+      The given key is cast implicitly to the map's key type where Flink's 
implicit casting rules
+      allow it; otherwise the call fails validation.
+      
+      Examples
+        -- TRUE
+        MAP_CONTAINS_KEY(MAP['a', 1, 'b', 2], 'a')
+        -- FALSE
+        MAP_CONTAINS_KEY(MAP['a', 1, 'b', 2], 'z')
+        -- TRUE
+        MAP_CONTAINS_KEY(MAP[CAST(NULL AS STRING), 1], CAST(NULL AS STRING))
+        -- TRUE, the TINYINT key is cast to the map's INT key type
+        MAP_CONTAINS_KEY(MAP[1, 'a'], CAST(1 AS TINYINT))
   - sql: MAP_FROM_ARRAYS(array_of_keys, array_of_values)
     table: mapFromArrays(array_of_keys, array_of_values)
     description: 返回由 key 的数组 keys 和 value 的数组 values 创建的 map。请注意两个数组的长度应该相等。
diff --git a/flink-python/docs/reference/pyflink.table/expressions.rst 
b/flink-python/docs/reference/pyflink.table/expressions.rst
index 2e2cee6cb93..a76320245a2 100644
--- a/flink-python/docs/reference/pyflink.table/expressions.rst
+++ b/flink-python/docs/reference/pyflink.table/expressions.rst
@@ -256,6 +256,7 @@ advanced type helper functions
     Expression.array_min
     Expression.array_sort
     Expression.array_union
+    Expression.map_contains_key
     Expression.map_entries
     Expression.map_from_entries
     Expression.map_keys
diff --git a/flink-python/pyflink/table/expression.py 
b/flink-python/pyflink/table/expression.py
index d695dbd4231..9c3fc1069dc 100644
--- a/flink-python/pyflink/table/expression.py
+++ b/flink-python/pyflink/table/expression.py
@@ -1986,6 +1986,24 @@ class Expression(Generic[T]):
         """
         return _unary_op("mapFromEntries")(self)
 
+    def map_contains_key(self, key) -> 'Expression':
+        """
+        Returns True if the given key exists in the map, False otherwise. 
Returns None if the map
+        is None.
+
+        If the search key is None, the function returns True when the map 
contains a None key.
+        The given key is cast implicitly to the map's key type where Flink's 
implicit casting
+        rules allow it; otherwise the call fails validation.
+
+        Examples:
+        ::
+
+            >>> map_("a", 1, "b", 2).map_contains_key("a") # True
+            >>> map_("a", 1, "b", 2).map_contains_key("z") # False
+            >>> map_(1, "a").map_contains_key(lit(1, DataTypes.TINYINT())) # 
True
+        """
+        return _binary_op("mapContainsKey")(self, key)
+
     # ---------------------------- time definition functions 
-----------------------------
 
     @property
diff --git 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
index 3aa6923461e..d4ba8815385 100644
--- 
a/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
+++ 
b/flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java
@@ -161,6 +161,7 @@ import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.LOWER;
 import static org.apache.flink.table.functions.BuiltInFunctionDefinitions.LPAD;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.LTRIM;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.MAKE_VALID_UTF8;
+import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.MAP_CONTAINS_KEY;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.MAP_ENTRIES;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.MAP_FROM_ENTRIES;
 import static 
org.apache.flink.table.functions.BuiltInFunctionDefinitions.MAP_KEYS;
@@ -2021,6 +2022,27 @@ public abstract class BaseExpressions<InType, OutType> {
         return toApiSpecificExpression(unresolvedCall(MAP_ENTRIES, toExpr()));
     }
 
+    /**
+     * Returns {@code TRUE} if the given key exists in the map, {@code FALSE} 
otherwise. Returns
+     * {@code NULL} if the map is {@code NULL}.
+     *
+     * <p>If the search key is {@code NULL}, the function returns {@code TRUE} 
when the map contains
+     * a {@code NULL} key. The given key is cast implicitly to the map's key 
type where Flink's
+     * implicit casting rules allow it; otherwise the call fails validation.
+     *
+     * <p>Examples:
+     *
+     * <pre>{@code
+     * map("a", 1, "b", 2).mapContainsKey("a") // TRUE
+     * map("a", 1, "b", 2).mapContainsKey("z") // FALSE
+     * map(1, "a").mapContainsKey(lit(1).cast(DataTypes.TINYINT())) // TRUE
+     * }</pre>
+     */
+    public OutType mapContainsKey(InType key) {
+        return toApiSpecificExpression(
+                unresolvedCall(MAP_CONTAINS_KEY, toExpr(), 
objectToExpression(key)));
+    }
+
     /**
      * Returns a map created from the given array of entries. Each entry must 
be a row with exactly
      * two fields, where the first field becomes the key and the second one 
the value.
diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/functions/BuiltInFunctionDefinitions.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/functions/BuiltInFunctionDefinitions.java
index 1b16c3f52bd..18f166d0ba6 100644
--- 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/functions/BuiltInFunctionDefinitions.java
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/functions/BuiltInFunctionDefinitions.java
@@ -112,6 +112,7 @@ import static 
org.apache.flink.table.types.inference.strategies.SpecificInputTyp
 import static 
org.apache.flink.table.types.inference.strategies.SpecificInputTypeStrategies.INDEX;
 import static 
org.apache.flink.table.types.inference.strategies.SpecificInputTypeStrategies.JSON_ARGUMENT;
 import static 
org.apache.flink.table.types.inference.strategies.SpecificInputTypeStrategies.LATERAL_SNAPSHOT_INPUT_TYPE_STRATEGY;
+import static 
org.apache.flink.table.types.inference.strategies.SpecificInputTypeStrategies.MAP_KEY_ARG;
 import static 
org.apache.flink.table.types.inference.strategies.SpecificInputTypeStrategies.ML_PREDICT_INPUT_TYPE_STRATEGY;
 import static 
org.apache.flink.table.types.inference.strategies.SpecificInputTypeStrategies.TO_CHANGELOG_INPUT_TYPE_STRATEGY;
 import static 
org.apache.flink.table.types.inference.strategies.SpecificInputTypeStrategies.TWO_EQUALS_COMPARABLE;
@@ -211,6 +212,21 @@ public final class BuiltInFunctionDefinitions {
                             
"org.apache.flink.table.runtime.functions.scalar.MapEntriesFunction")
                     .build();
 
+    public static final BuiltInFunctionDefinition MAP_CONTAINS_KEY =
+            BuiltInFunctionDefinition.newBuilder()
+                    .name("MAP_CONTAINS_KEY")
+                    .kind(SCALAR)
+                    .inputTypeStrategy(
+                            sequence(
+                                    List.of("map", "key"),
+                                    List.of(logical(LogicalTypeRoot.MAP), 
MAP_KEY_ARG)))
+                    .outputTypeStrategy(
+                            nullableIfArgs(
+                                    ConstantArgumentCount.of(0), 
explicit(DataTypes.BOOLEAN())))
+                    .runtimeClass(
+                            
"org.apache.flink.table.runtime.functions.scalar.MapContainsKeyFunction")
+                    .build();
+
     public static final BuiltInFunctionDefinition MAP_FROM_ARRAYS =
             BuiltInFunctionDefinition.newBuilder()
                     .name("MAP_FROM_ARRAYS")
diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/MapKeyArgumentTypeStrategy.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/MapKeyArgumentTypeStrategy.java
new file mode 100644
index 00000000000..da457b18db5
--- /dev/null
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/MapKeyArgumentTypeStrategy.java
@@ -0,0 +1,71 @@
+/*
+ * 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.flink.table.types.inference.strategies;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
+import org.apache.flink.table.functions.FunctionDefinition;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.inference.ArgumentTypeStrategy;
+import org.apache.flink.table.types.inference.CallContext;
+import org.apache.flink.table.types.inference.Signature.Argument;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.MapType;
+
+import java.util.List;
+import java.util.Optional;
+
+import static 
org.apache.flink.table.types.logical.utils.LogicalTypeCasts.supportsImplicitCast;
+
+/**
+ * Specific {@link ArgumentTypeStrategy} for the key argument of {@link
+ * BuiltInFunctionDefinitions#MAP_CONTAINS_KEY}.
+ */
+@Internal
+class MapKeyArgumentTypeStrategy implements ArgumentTypeStrategy {
+
+    @Override
+    public Optional<DataType> inferArgumentType(
+            CallContext callContext, int argumentPos, boolean throwOnFailure) {
+        List<DataType> argumentTypes = callContext.getArgumentDataTypes();
+        final int mapArgumentPos = 0;
+        final MapType mapType = (MapType) 
argumentTypes.get(mapArgumentPos).getLogicalType();
+        final LogicalType actualKeyType = 
argumentTypes.get(argumentPos).getLogicalType();
+        LogicalType expectedKeyType = mapType.getKeyType();
+
+        if (!expectedKeyType.isNullable() && actualKeyType.isNullable()) {
+            expectedKeyType = expectedKeyType.copy(true);
+        }
+
+        if (supportsImplicitCast(actualKeyType, expectedKeyType)) {
+            return Optional.of(DataTypes.of(expectedKeyType));
+        }
+        return callContext.fail(
+                throwOnFailure,
+                "Unsupported argument type. Expected type '%s' but actual type 
was '%s'.",
+                expectedKeyType,
+                actualKeyType);
+    }
+
+    @Override
+    public Argument getExpectedArgument(FunctionDefinition functionDefinition, 
int argumentPos) {
+        return Argument.of("<MAP KEY>");
+    }
+}
diff --git 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/SpecificInputTypeStrategies.java
 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/SpecificInputTypeStrategies.java
index 5f64808afa2..e012f310953 100644
--- 
a/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/SpecificInputTypeStrategies.java
+++ 
b/flink-table/flink-table-common/src/main/java/org/apache/flink/table/types/inference/strategies/SpecificInputTypeStrategies.java
@@ -113,6 +113,9 @@ public final class SpecificInputTypeStrategies {
     public static final ArgumentTypeStrategy ARRAY_OF_ENTRIES_ARG =
             new ArrayOfEntriesArgumentTypeStrategy();
 
+    /** Argument type derived from the map key type. */
+    public static final ArgumentTypeStrategy MAP_KEY_ARG = new 
MapKeyArgumentTypeStrategy();
+
     /**
      * Input strategy for {@link BuiltInFunctionDefinitions#JSON_OBJECT}.
      *
diff --git 
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/inference/InputTypeStrategiesTest.java
 
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/inference/InputTypeStrategiesTest.java
index e6973c3e0d0..38a2c3b86c8 100644
--- 
a/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/inference/InputTypeStrategiesTest.java
+++ 
b/flink-table/flink-table-common/src/test/java/org/apache/flink/table/types/inference/InputTypeStrategiesTest.java
@@ -644,6 +644,43 @@ class InputTypeStrategiesTest extends 
InputTypeStrategiesTestBase {
                         .expectArgumentTypes(
                                 
DataTypes.ARRAY(DataTypes.INT().notNull()).notNull(),
                                 DataTypes.INT()),
+                TestSpec.forStrategy(
+                                "MapKey argument type strategy implicitly 
casts the key",
+                                sequence(
+                                        logical(LogicalTypeRoot.MAP),
+                                        
SpecificInputTypeStrategies.MAP_KEY_ARG))
+                        .calledWithArgumentTypes(
+                                DataTypes.MAP(DataTypes.BIGINT().notNull(), 
DataTypes.STRING()),
+                                DataTypes.INT().notNull())
+                        .expectSignature("f(<MAP>, <MAP KEY>)")
+                        .expectArgumentTypes(
+                                DataTypes.MAP(DataTypes.BIGINT().notNull(), 
DataTypes.STRING()),
+                                DataTypes.BIGINT().notNull()),
+                TestSpec.forStrategy(
+                                "MapKey argument type strategy widens a NOT 
NULL key type "
+                                        + "for a nullable argument",
+                                sequence(
+                                        logical(LogicalTypeRoot.MAP),
+                                        
SpecificInputTypeStrategies.MAP_KEY_ARG))
+                        .calledWithArgumentTypes(
+                                DataTypes.MAP(DataTypes.BIGINT().notNull(), 
DataTypes.STRING())
+                                        .notNull(),
+                                DataTypes.BIGINT())
+                        .expectArgumentTypes(
+                                DataTypes.MAP(DataTypes.BIGINT().notNull(), 
DataTypes.STRING())
+                                        .notNull(),
+                                DataTypes.BIGINT()),
+                TestSpec.forStrategy(
+                                "MapKey argument type strategy rejects a key 
that cannot be cast",
+                                sequence(
+                                        logical(LogicalTypeRoot.MAP),
+                                        
SpecificInputTypeStrategies.MAP_KEY_ARG))
+                        .calledWithArgumentTypes(
+                                DataTypes.MAP(DataTypes.INT(), 
DataTypes.STRING()),
+                                DataTypes.BOOLEAN())
+                        .expectErrorMessage(
+                                "Unsupported argument type. Expected type 
'INT' but actual "
+                                        + "type was 'BOOLEAN'."),
                 
TestSpec.forStrategy(sequence(SpecificInputTypeStrategies.ARRAY_FULLY_COMPARABLE))
                         .expectSignature("f(<ARRAY<COMPARABLE>>)")
                         
.calledWithArgumentTypes(DataTypes.ARRAY(DataTypes.ROW()))
diff --git 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java
 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java
index 54b3b8cec05..7d24b7995fd 100644
--- 
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java
+++ 
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java
@@ -77,6 +77,7 @@ public class MapFunctionITCase extends 
BuiltInFunctionTestBase {
                         mapValuesTestCases(),
                         mapEntriesTestCases(),
                         mapFromArraysTestCases(),
+                        mapContainsKeyTestCases(),
                         mapFromEntriesTestCases(),
                         mapUnionTestCases())
                 .flatMap(s -> s);
@@ -591,6 +592,108 @@ public class MapFunctionITCase extends 
BuiltInFunctionTestBase {
                                         .notNull()));
     }
 
+    private Stream<TestSetSpec> mapContainsKeyTestCases() {
+        return Stream.of(
+                TestSetSpec.forFunction(
+                                BuiltInFunctionDefinitions.MAP_CONTAINS_KEY, 
"Invalid input")
+                        .onFieldsWithData(CollectionUtil.map(entry("a", 1)))
+                        .andDataTypes(DataTypes.MAP(DataTypes.STRING(), 
DataTypes.INT()))
+                        .testTableApiValidationError(
+                                $("f0").mapContainsKey(true),
+                                "Unsupported argument type. Expected type 
'STRING' "
+                                        + "but actual type was 'BOOLEAN NOT 
NULL'.")
+                        .testSqlValidationError(
+                                "MAP_CONTAINS_KEY(f0, TRUE)",
+                                "Unsupported argument type. Expected type 
'STRING' "
+                                        + "but actual type was 'BOOLEAN NOT 
NULL'."),
+                
TestSetSpec.forFunction(BuiltInFunctionDefinitions.MAP_CONTAINS_KEY)
+                        .onFieldsWithData(
+                                CollectionUtil.map(entry("a", 1), entry("b", 
2)),
+                                CollectionUtil.map(entry(1, 2), entry(null, 
3)),
+                                null,
+                                CollectionUtil.map(entry(new Integer[] {1, 2}, 
"x")))
+                        .andDataTypes(
+                                DataTypes.MAP(DataTypes.STRING(), 
DataTypes.INT()),
+                                DataTypes.MAP(DataTypes.INT(), 
DataTypes.INT()),
+                                DataTypes.MAP(DataTypes.STRING(), 
DataTypes.INT()),
+                                
DataTypes.MAP(DataTypes.ARRAY(DataTypes.INT()), DataTypes.STRING()))
+                        .testResult(
+                                $("f0").mapContainsKey("a"),
+                                "MAP_CONTAINS_KEY(f0, 'a')",
+                                true,
+                                DataTypes.BOOLEAN())
+                        // a miss is FALSE, not NULL
+                        .testResult(
+                                $("f0").mapContainsKey("z"),
+                                "MAP_CONTAINS_KEY(f0, 'z')",
+                                false,
+                                DataTypes.BOOLEAN())
+                        // only a NULL map yields NULL
+                        .testResult(
+                                $("f2").mapContainsKey("a"),
+                                "MAP_CONTAINS_KEY(f2, 'a')",
+                                null,
+                                DataTypes.BOOLEAN())
+                        // a NULL probe finds a NULL key
+                        .testResult(
+                                
$("f1").mapContainsKey(nullOf(DataTypes.INT())),
+                                "MAP_CONTAINS_KEY(f1, CAST(NULL AS INT))",
+                                true,
+                                DataTypes.BOOLEAN())
+                        // an absent NULL key is FALSE
+                        .testResult(
+                                
$("f0").mapContainsKey(nullOf(DataTypes.STRING())),
+                                "MAP_CONTAINS_KEY(f0, CAST(NULL AS STRING))",
+                                false,
+                                DataTypes.BOOLEAN())
+                        // a miss past a NULL key is FALSE
+                        .testResult(
+                                $("f1").mapContainsKey(9),
+                                "MAP_CONTAINS_KEY(f1, 9)",
+                                false,
+                                DataTypes.BOOLEAN())
+                        // complex keys compare structurally
+                        .testResult(
+                                $("f3").mapContainsKey(array(1, 2)),
+                                "MAP_CONTAINS_KEY(f3, ARRAY[1, 2])",
+                                true,
+                                DataTypes.BOOLEAN())
+                        // there is no VARIANT literal, so the key is built 
with PARSE_JSON
+                        .testResult(
+                                map(call("PARSE_JSON", "1"), lit(1))
+                                        .mapContainsKey(call("PARSE_JSON", 
"1")),
+                                "MAP_CONTAINS_KEY(MAP[PARSE_JSON('1'), 1], 
PARSE_JSON('1'))",
+                                true,
+                                DataTypes.BOOLEAN().notNull()),
+                TestSetSpec.forFunction(
+                                BuiltInFunctionDefinitions.MAP_CONTAINS_KEY, 
"Documented examples")
+                        .onFieldsWithData(1)
+                        .andDataTypes(DataTypes.INT().notNull())
+                        // a NOT NULL map yields a NOT NULL result
+                        .testResult(
+                                map(lit("a"), lit(1), lit("b"), 
lit(2)).mapContainsKey("a"),
+                                "MAP_CONTAINS_KEY(MAP['a', 1, 'b', 2], 'a')",
+                                true,
+                                DataTypes.BOOLEAN().notNull())
+                        .testResult(
+                                map(lit("a"), lit(1), lit("b"), 
lit(2)).mapContainsKey("z"),
+                                "MAP_CONTAINS_KEY(MAP['a', 1, 'b', 2], 'z')",
+                                false,
+                                DataTypes.BOOLEAN().notNull())
+                        .testResult(
+                                map(nullOf(DataTypes.STRING()), lit(1))
+                                        
.mapContainsKey(nullOf(DataTypes.STRING())),
+                                "MAP_CONTAINS_KEY(MAP[CAST(NULL AS STRING), 
1], CAST(NULL AS STRING))",
+                                true,
+                                DataTypes.BOOLEAN().notNull())
+                        .testResult(
+                                map(lit(1), lit("a"))
+                                        
.mapContainsKey(lit(1).cast(DataTypes.TINYINT())),
+                                "MAP_CONTAINS_KEY(MAP[1, 'a'], CAST(1 AS 
TINYINT))",
+                                true,
+                                DataTypes.BOOLEAN().notNull()));
+    }
+
     private Stream<TestSetSpec> mapUnionTestCases() {
         return Stream.of(
                 TestSetSpec.forFunction(BuiltInFunctionDefinitions.MAP_UNION)
diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/MapContainsKeyFunction.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/MapContainsKeyFunction.java
new file mode 100644
index 00000000000..4321c9a27ac
--- /dev/null
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/functions/scalar/MapContainsKeyFunction.java
@@ -0,0 +1,103 @@
+/*
+ * 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.flink.table.runtime.functions.scalar;
+
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.data.ArrayData;
+import org.apache.flink.table.data.MapData;
+import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
+import org.apache.flink.table.functions.FunctionContext;
+import 
org.apache.flink.table.functions.SpecializedFunction.ExpressionEvaluator;
+import org.apache.flink.table.functions.SpecializedFunction.SpecializedContext;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.KeyValueDataType;
+import org.apache.flink.util.FlinkRuntimeException;
+
+import javax.annotation.Nullable;
+
+import java.lang.invoke.MethodHandle;
+
+import static org.apache.flink.table.api.Expressions.$;
+
+/** Implementation of {@link BuiltInFunctionDefinitions#MAP_CONTAINS_KEY}. */
+@Internal
+public class MapContainsKeyFunction extends BuiltInScalarFunction {
+
+    private final ArrayData.ElementGetter keyElementGetter;
+    private final ExpressionEvaluator equalityEvaluator;
+    private transient MethodHandle equalityHandle;
+
+    public MapContainsKeyFunction(SpecializedContext context) {
+        super(BuiltInFunctionDefinitions.MAP_CONTAINS_KEY, context);
+        final DataType mapDataType = 
context.getCallContext().getArgumentDataTypes().get(0);
+        final DataType keyDataType = ((KeyValueDataType) 
mapDataType).getKeyDataType();
+
+        keyElementGetter = 
ArrayData.createElementGetter(keyDataType.getLogicalType());
+        equalityEvaluator =
+                context.createEvaluator(
+                        $("key").isEqual($("needle")),
+                        DataTypes.BOOLEAN(),
+                        DataTypes.FIELD("key", 
keyDataType.notNull().toInternal()),
+                        DataTypes.FIELD("needle", 
keyDataType.notNull().toInternal()));
+    }
+
+    @Override
+    public void open(FunctionContext context) throws Exception {
+        equalityHandle = equalityEvaluator.open(context);
+    }
+
+    public @Nullable Boolean eval(@Nullable MapData map, @Nullable Object 
needle) {
+        if (map == null) {
+            return null;
+        }
+        final ArrayData keys = map.keyArray();
+        final int size = map.size();
+        if (needle == null) {
+            // A NULL needle matches a NULL key, unlike SQL `NULL = NULL` 
which yields UNKNOWN.
+            for (int pos = 0; pos < size; pos++) {
+                if (keyElementGetter.getElementOrNull(keys, pos) == null) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        for (int pos = 0; pos < size; pos++) {
+            final Object key = keyElementGetter.getElementOrNull(keys, pos);
+            if (key != null && isEqual(key, needle)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean isEqual(final Object key, final Object needle) {
+        try {
+            return (boolean) equalityHandle.invoke(key, needle);
+        } catch (Throwable t) {
+            throw new FlinkRuntimeException(t);
+        }
+    }
+
+    @Override
+    public void close() throws Exception {
+        equalityEvaluator.close();
+    }
+}

Reply via email to