Repository navigation
Conversation
gustavodemorais
left a comment
There was a problem hiding this comment.
Thanks for the PR containing the full implementation, @VasShabu! This is definitely a very nice feature.
Before going into the detailed review: I wanted to suggest you to extend testing. We already have keep-first deduplication in operators/deduplicate. Could you take a look at ProcTimeDeduplicateKeepFirstRowFunctionTest (TTL expiry), RowTimeDeduplicateFunctionTest (rowtime + updating input) and DeduplicateITCase, and check which tests are still missing for the cases we support so we don't miss on coverage?
| } else { // watermarkless mode | ||
| if (seen.getValue() == null) { | ||
| projectedOutput.replaceRow(input); | ||
| projectedOutput.setRowKind(RowKind.INSERT); |
There was a problem hiding this comment.
You're unconditionally accepting the first change for a key as the first appearance. Edge case: the first change for a key can be a deletion. Imagine we start reading a kafka topic a later offset and only see the deletion but not the original event.
-U and -D should never count as "first" because they only retract a row. Returning early on them means only +I or +U can become the kept row.
| import java.util.Set; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class DeduplicateKeepFirstTypeStrategy { |
There was a problem hiding this comment.
seems
@Internal and javadoc are still missing
|
|
||
| import java.util.stream.IntStream; | ||
|
|
||
| public class DeduplicateKeepFirst extends BuiltInProcessTableFunction<RowData> { |
64c8834 to
2b557ef
Compare
| -- +I[user_name:'Vas', action:'login'] | ||
| ``` | ||
|
|
||
| The input may be an updating changelog. `DEDUPLICATE_KEEP_FIRST` keeps the first `+I` or `+U` observed per key and swallows every later change to that key (duplicate, `-U`, `+U`, `-D`), so the output stays insert-only. A `-U` or `-D` for a key that has not been seen yet is ignored and does not mark the key as seen, so the next `+I` or `+U` for that key is still emitted. Event-time mode (`on_time`) is not supported with updating input. |
There was a problem hiding this comment.
link "updating changelog" to a page, currently PTF page until Gustavos PR is in
There was a problem hiding this comment.
people might not know +I or -U
| ```sql | ||
| SELECT * FROM DEDUPLICATE_KEEP_FIRST( | ||
| input => TABLE source_table [PARTITION BY key_col [, key_col2 ...]], | ||
| [on_time => DESCRIPTOR(rowtime_column),] |
There was a problem hiding this comment.
we should also document "uid" argument that is required when more than one dedup is used. btw system args are at the end of the signature, this is important if positional args instead f named args are used.
| .map(Field::getName) | ||
| .collect(Collectors.toSet()); | ||
| String timestampField = "event_time"; | ||
| for (int i = 0; takenNames.contains(timestampField); i++) { |
There was a problem hiding this comment.
can we just solve duplicates by x$0, x$1, etc?
There was a problem hiding this comment.
or even better: just nest one more time
| timestampField = "event_time_" + i; | ||
| } | ||
| candidateFields.add(DataTypes.FIELD(timestampField, DataTypes.BIGINT())); | ||
| return Optional.of( |
There was a problem hiding this comment.
We don't always need this state right?
There was a problem hiding this comment.
We don't always need it no, however I changed its from a row to a boolean in the case when we are dealing with watermarkless mode as I dont think that we can completely remove it outright.
9844ab3 to
f70cd89
Compare
| return callContext.fail( | ||
| throwOnFailure, | ||
| "The 'state_ttl' argument must not be negative, but was '%s'. " | ||
| + "Use a positive INTERVAL to bound state retention, INTERVAL '0' to " | ||
| + "retain state indefinitely without a TTL, or omit the argument to " | ||
| + "fall back to 'table.exec.state.ttl'.", | ||
| stateTtl.get()); |
There was a problem hiding this comment.
| return callContext.fail( | |
| throwOnFailure, | |
| "The 'state_ttl' argument must not be negative, but was '%s'. " | |
| + "Use a positive INTERVAL to bound state retention, INTERVAL '0' to " | |
| + "retain state indefinitely without a TTL, or omit the argument to " | |
| + "fall back to 'table.exec.state.ttl'.", | |
| stateTtl.get()); | |
| return callContext.fail( | |
| throwOnFailure, | |
| "The 'state_ttl' argument must not be negative, but was '%s'. " | |
| + "Use a positive INTERVAL to bound state retention, INTERVAL '0' to " | |
| + "retain state indefinitely without a TTL, or omit the argument to " | |
| + "fall back to session defaults.", | |
| stateTtl.get()); |
| // Only event-time mode buffers a candidate. Without on_time a fixed type keeps | ||
| // the unused state independent of the input schema across query evolution. | ||
| if (tableSemantics.timeColumn() == -1) { | ||
| return Optional.of(ValueView.newValueViewDataType(DataTypes.BOOLEAN())); |
There was a problem hiding this comment.
let's not use boolean but a correct type, allows for switching the mode in the future. it doesn't add any additional state.
There was a problem hiding this comment.
sounds good will change accordingly
| candidate.setValue( | ||
| candidateSerializer.copy(materializeCandidate(input, rowtime))); |
There was a problem hiding this comment.
this should not be necessary, are there better ways? we have copied the row already. why do we need to serialize again, this would be expensive.
| } | ||
| } else { // already emitted → drop, optionally refresh the TTL | ||
| if (resetTtlOnDuplicate == null || resetTtlOnDuplicate) { | ||
| seen.setValue(true); |
There was a problem hiding this comment.
did you verify that this indeed causes resetTtlOnDuplicate?
There was a problem hiding this comment.
Yeah I have confirmed that this work with claude + with the flink sqlclient
f70cd89 to
8a382cd
Compare
…addressed -u -d issue, @internal etc
…didate state - Drop DEDUPLICATE_KEEP_FIRST rows with ts <= watermark, use one candidate state type in both modes, and derive the event-time mode once in the constructor - Trim the tests (drop Table API, multi-key and TTL-expiry cases, add restore rows) and polish the docs
cb5ab2a to
38fe567
Compare
…go into different functions given the input args
38fe567 to
b328f53
Compare
What is the purpose of the change
Implement a new PTF for deduplication of a stream, currently Flink does not support such method, however people are able to get similar functionality with long and verbose SQL statements, this flip is to make this a mainline function and
Brief change log
Verifying this change
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 4.8]