[FLINK-40355][table] Add new MAP_CONTAINS_KEY function - #28970
Conversation
b7b48ba to
fb849a3
Compare
| try { | ||
| return (boolean) equalityHandle.invoke(key, needle); | ||
| } catch (Throwable t) { | ||
| throw new FlinkRuntimeException(t); |
There was a problem hiding this comment.
In the past we have not wanted to issue errors in SQL as it would end the job. I suggest we return false and log in this error case. In the error case they are not equal.
There was a problem hiding this comment.
The catch Throwable here mirrors other builtin function's implementation, functions like :
- ARRAY_CONTAINS
- ARRAY_POSITION
- ARRAY_REMOVE
I was also talking to my mentor about logging and how it is not great in streaming as we are processing millions of records and logs can get full really quickly and how you dont know easily what error causes a specific entry in a log.
Please let me know your thoughts, intrested to hear more, I see that there are two options, either keep this implementation or alter other functions aswell to ensure that we are returning False as you say and logging. However if we decide to do this to the naked eye some invalid input that would cause an error would be hidden by a silent failure only to be discovered in a log.
Would love to hear you thoughts regarding this.
Thanks
Vas.
There was a problem hiding this comment.
Hi there,
As this is a function is a test to say does the map contain a key function - if it fails I assume that it doesn't contain the key function, so false would seem reasonable. What sort of errors can we get here, that we would reasonably want to surface?
At the same time I am keen we should follow the convention of the other functions. Though we could argue ARRAY_CONTAINS is a test also, so should not error, but ARRAY_REMOVE is a mutation , which if it fails should reasonably be an error.
There was a problem hiding this comment.
Hey you two, that's a valid discussion.
I'm +1 for keeping the throw. What actually causes this exception - a representation bug, a broken custom RAW comparator, pathological nesting - are edge cases and isn't "key not present," so returning false would just produce a wrong answer indistinguishable from a real negative. That's worse than crashing.
Also, ARRAY_CONTAINS is a test-predicate too and still throws today - changing it only here adds a new inconsistency instead of fixing one. Even if we wanted to change the behavior for all functions, which I don't think we should, this would require a larger discussion. For this function I'd just do as the other ones
There was a problem hiding this comment.
In practice it almost never fires. The caller only calls isEqual when both key and needle are non-null (the needle != null && elementKey != null guard), and generated equality on two internal values of the same type is exception-free. So it's mostly defensive. It would realistically only trigger on an internal codegen or runtime bug while comparing two keys, or a JVM Error bubbling up through invoke.
| description: | | ||
| Returns TRUE if the given key exists in the map, FALSE otherwise. Returns NULL if the map is | ||
| NULL. A NULL key matches a NULL key in the map. 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. |
There was a problem hiding this comment.
when we say fails validation - I assume we should return false as per my other comment
There was a problem hiding this comment.
This validation error is called during the planning stage i think when a map and a key is given with different types on the keys
gustavodemorais
left a comment
There was a problem hiding this comment.
Thanks for working on this, @VasShabu. I've added some comments
| $("f0").mapContainsKey(true), | ||
| "Invalid input arguments. Expected signatures are:\n" | ||
| + "MAP_CONTAINS_KEY(map <MAP>, key <MAP KEY>)") |
There was a problem hiding this comment.
Can we throw a better error here?
If I understand it correctly, we're throwing because the boolean type is not valid here?
There was a problem hiding this comment.
Not a blocker but can't we tell the type and be more specific as to why it failed
| try { | ||
| return (boolean) equalityHandle.invoke(key, needle); | ||
| } catch (Throwable t) { | ||
| throw new FlinkRuntimeException(t); |
There was a problem hiding this comment.
Hey you two, that's a valid discussion.
I'm +1 for keeping the throw. What actually causes this exception - a representation bug, a broken custom RAW comparator, pathological nesting - are edge cases and isn't "key not present," so returning false would just produce a wrong answer indistinguishable from a real negative. That's worse than crashing.
Also, ARRAY_CONTAINS is a test-predicate too and still throws today - changing it only here adds a new inconsistency instead of fixing one. Even if we wanted to change the behavior for all functions, which I don't think we should, this would require a larger discussion. For this function I'd just do as the other ones
| 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; | ||
| } | ||
| } | ||
| } else { | ||
| for (int pos = 0; pos < size; pos++) { | ||
| final Object key = keyElementGetter.getElementOrNull(keys, pos); | ||
| if (key != null && isEqual(key, needle)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Can we simplify this? Something like
| 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; | |
| } | |
| } | |
| } else { | |
| for (int pos = 0; pos < size; pos++) { | |
| final Object key = keyElementGetter.getElementOrNull(keys, pos); | |
| if (key != null && isEqual(key, needle)) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| 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(); | |
| for (int pos = 0; pos < size; pos++) { | |
| final Object elementKey = keyElementGetter.getElementOrNull(keys, pos); | |
| // A NULL needle matches a NULL key, unlike SQL `NULL = NULL` which yields UNKNOWN. | |
| if (needle == null && elementKey == null) { | |
| return true; | |
| } else if (needle != null && elementKey != null && isEqual(elementKey, needle)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } |
There was a problem hiding this comment.
@gustavodemorais it was before, however it is less performant
since it is runtime I suggested to change it
#28970 (comment)
There was a problem hiding this comment.
We have an additional if condition for each element in the smaller version. I think this is small performance diff since we also do things like "isEqual(elementKey, needle)" here. However, I agree it's a performance diff and Ramin seems to also be +1 to the previous version. I'd say @VasShabu can rollback to your suggestion and maybe only add a very short comment to explain why. Useful since there are other functions like ArrayContainsFunction that use the shorter version and one might wonder why we went with the lengthy version
| /** | ||
| * Specific {@link ArgumentTypeStrategy} for {@link BuiltInFunctionDefinitions#MAP_CONTAINS_KEY}. | ||
| */ |
There was a problem hiding this comment.
Maybe mention for which specific argument you are inferring
| try { | ||
| return (boolean) equalityHandle.invoke(key, needle); | ||
| } catch (Throwable t) { | ||
| throw new FlinkRuntimeException(t); |
There was a problem hiding this comment.
In practice it almost never fires. The caller only calls isEqual when both key and needle are non-null (the needle != null && elementKey != null guard), and generated equality on two internal values of the same type is exception-free. So it's mostly defensive. It would realistically only trigger on an internal codegen or runtime bug while comparing two keys, or a JVM Error bubbling up through invoke.
# Conflicts: # flink-python/pyflink/table/expression.py # flink-table/flink-table-api-java/src/main/java/org/apache/flink/table/api/internal/BaseExpressions.java # flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/MapFunctionITCase.java
…changes + simplify hotpath
8912224 to
f1dab7e
Compare
gustavodemorais
left a comment
There was a problem hiding this comment.
Hey @VasShabu, thanks for the update! I think we're almost there. Added two more suggestions
| DataTypes.MAP(DataTypes.INT(), DataTypes.STRING()), | ||
| DataTypes.BOOLEAN()) | ||
| .expectErrorMessage( | ||
| "Invalid input arguments. Expected signatures are:\n" |
There was a problem hiding this comment.
Test should be failing since you updated the msg to "Unsupported argument type.."
There was a problem hiding this comment.
ah yes, I forgot to run the inputTypeStrategiesTest, before commiting I ran mapFunctionITCases, will make sure I run all tests relating to a feature prior to commit.
| 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; | ||
| } | ||
| } | ||
| } else { | ||
| for (int pos = 0; pos < size; pos++) { | ||
| final Object key = keyElementGetter.getElementOrNull(keys, pos); | ||
| if (key != null && isEqual(key, needle)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } |
There was a problem hiding this comment.
We have an additional if condition for each element in the smaller version. I think this is small performance diff since we also do things like "isEqual(elementKey, needle)" here. However, I agree it's a performance diff and Ramin seems to also be +1 to the previous version. I'd say @VasShabu can rollback to your suggestion and maybe only add a very short comment to explain why. Useful since there are other functions like ArrayContainsFunction that use the shorter version and one might wonder why we went with the lengthy version
| } | ||
|
|
||
| /** | ||
| /* * |
There was a problem hiding this comment.
why do we need this change?
|
@flinkbot run azure |
64edf98 to
119ef1b
Compare
What is the purpose of the change
This pull request add a new function maoContainsKey function, which which given a key which check if it is present within a map.
Brief change log
Verifying this change
Please make sure both new and modified tests in this PR follow the conventions for tests defined in our code quality guide.
This change added tests and can be verified as follows:
/mvnw -o -pl flink-table/flink-table-planner -Dtest='JsonFunctionsITCase' -Dsurefire.failIfNoSpecifiedTests=false -Dcheckstyle.skip=true -Dspotless.check.skip=true -Drat.skip=true -e -Denforcer.skip=true test
(example:)
Does this pull request potentially affect one of the following parts:
@Public(Evolving): (no)Documentation
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Opus 5