From a8ee4dde6d05118ecb279616b12e559b7aea3595 Mon Sep 17 00:00:00 2001 From: Ilya Soin Date: Sun, 16 Aug 2026 23:46:02 +0300 Subject: [PATCH 1/3] [FLINK-40399][state] Separate SavepointKeyFilter runtime contract from push-down planning SavepointKeyFilter carried two unrelated responsibilities: the contract the savepoint scan needs, and the algebra used only while combining predicates during filter push-down. The second group was public and @Experimental despite being unusable outside push-down translation, to the point that its javadoc had to disclaim it - "Used only while combining filters during push-down translation, not during the scan." Split the two: * org.apache.flink.state.api.filter.SavepointKeyFilter keeps only what the scan needs - test() and getExactKeys() - plus the public factories. * org.apache.flink.state.table.filter.SavepointKeyFilterPlan carries the planning algebra (isEmpty, bounds, intersect, filterKeys, empty), next to the connector that is its only user. SavepointKeyFilterPlan and BoundInfo are public only so the connector in org.apache.flink.state.table can reach them, and are marked @Internal. The unit tests are split the same way: the plan algebra cases move to SavepointKeyFilterPlanTest, leaving SavepointFilterTranslatorTest to cover only expression translation. No behaviour change. --- .../docs/libs/state_processor_api.md | 3 - docs/content/docs/libs/state_processor_api.md | 3 - .../state/api/filter/ExactKeyFilter.java | 15 -- .../state/api/filter/RangeKeyFilter.java | 86 ++------ .../state/api/filter/SavepointKeyFilter.java | 66 +------ .../table/SavepointFilterTranslator.java | 65 +++--- .../{api => table}/filter/BoundInfo.java | 6 +- .../filter/EmptyKeyFilterPlan.java} | 16 +- .../table/filter/ExactKeyFilterPlan.java | 63 ++++++ .../table/filter/RangeKeyFilterPlan.java | 126 ++++++++++++ .../table/filter/SavepointKeyFilterPlan.java | 106 ++++++++++ .../api/SavepointReaderKeyedStateITCase.java | 3 +- .../api/input/KeyedStateInputFormatTest.java | 23 --- .../table/SavepointFilterTranslatorTest.java | 187 ++++-------------- .../filter/SavepointKeyFilterPlanTest.java | 155 +++++++++++++++ 15 files changed, 549 insertions(+), 374 deletions(-) rename flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/{api => table}/filter/BoundInfo.java (92%) rename flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/{api/filter/EmptyKeyFilter.java => table/filter/EmptyKeyFilterPlan.java} (75%) create mode 100644 flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/ExactKeyFilterPlan.java create mode 100644 flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/RangeKeyFilterPlan.java create mode 100644 flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlan.java create mode 100644 flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlanTest.java diff --git a/docs/content.zh/docs/libs/state_processor_api.md b/docs/content.zh/docs/libs/state_processor_api.md index e0e20bfaf4a3b5..14b63aefa0edb6 100644 --- a/docs/content.zh/docs/libs/state_processor_api.md +++ b/docs/content.zh/docs/libs/state_processor_api.md @@ -254,7 +254,6 @@ DataStream keyRange = savepoint.readKeyedState( * `SavepointKeyFilter.exact(K key)` / `SavepointKeyFilter.exact(Set keys)` — match a single key or a finite set of keys. * `SavepointKeyFilter.range(K lower, boolean lowerInclusive, K upper, boolean upperInclusive)` — match a range; `K` must implement `Comparable`. Either bound may be `null` to leave that side unbounded. * `SavepointKeyFilter.range(K lower, boolean lowerInclusive, K upper, boolean upperInclusive, SerializableComparator comparator)` — same, but with an explicit comparator for key types that do not implement `Comparable`. The comparator must be serializable because the filter is shipped with the job; lambdas and method references assigned to `SerializableComparator` satisfy this automatically. -* `SavepointKeyFilter.empty()` — match no keys. Not intended for direct use — it only serves as an internal building block for the Table API filter pushdown. When the built-in filters are not enough, you can implement the `SavepointKeyFilter` interface yourself. For use with the DataStream API, only `test(K key)` has to be implemented; it is called for every key in each opened split and decides whether that key will be read. @@ -314,8 +313,6 @@ DataStream firstKeys = savepoint.readKeyedState( new UpToKeyFilter(100)); ``` -The remaining interface methods can be left at their defaults for DataStream API usage, as they are only used internally in the Table API during push-down handling. - ### 窗口状态 Window State State Processor API 支持读取[窗口算子]({{< ref "docs/dev/datastream/operators/windows" >}})的状态,当读取窗口状态时,需要指定算子 id,窗口分配器和聚合类型。 diff --git a/docs/content/docs/libs/state_processor_api.md b/docs/content/docs/libs/state_processor_api.md index 0b0472b7f39e40..6485e31cd5ce5a 100644 --- a/docs/content/docs/libs/state_processor_api.md +++ b/docs/content/docs/libs/state_processor_api.md @@ -268,7 +268,6 @@ DataStream keyRange = savepoint.readKeyedState( * `SavepointKeyFilter.exact(K key)` / `SavepointKeyFilter.exact(Set keys)` — match a single key or a finite set of keys. * `SavepointKeyFilter.range(K lower, boolean lowerInclusive, K upper, boolean upperInclusive)` — match a range; `K` must implement `Comparable`. Either bound may be `null` to leave that side unbounded. * `SavepointKeyFilter.range(K lower, boolean lowerInclusive, K upper, boolean upperInclusive, SerializableComparator comparator)` — same, but with an explicit comparator for key types that do not implement `Comparable`. The comparator must be serializable because the filter is shipped with the job; lambdas and method references assigned to `SerializableComparator` satisfy this automatically. -* `SavepointKeyFilter.empty()` — match no keys. Not intended for direct use — it only serves as an internal building block for the Table API filter pushdown. When the built-in filters are not enough, you can implement the `SavepointKeyFilter` interface yourself. For use with the DataStream API, only `test(K key)` has to be implemented; it is called for every key in each opened split and decides whether that key will be read. @@ -328,8 +327,6 @@ DataStream firstKeys = savepoint.readKeyedState( new UpToKeyFilter(100)); ``` -The remaining interface methods can be left at their defaults for DataStream API usage, as they are only used internally in the Table API during push-down handling. - #### Window State The state processor API supports reading state from a [window operator]({{< ref "docs/dev/datastream/operators/windows" >}}). diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/ExactKeyFilter.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/ExactKeyFilter.java index 13f37554d92679..2cdf677252194e 100644 --- a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/ExactKeyFilter.java +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/ExactKeyFilter.java @@ -18,7 +18,6 @@ package org.apache.flink.state.api.filter; -import java.util.HashSet; import java.util.Set; /** A filter that accepts a finite set of keys. */ @@ -42,20 +41,6 @@ public Set getExactKeys() { return keys; } - @Override - public SavepointKeyFilter intersect(SavepointKeyFilter other) { - if (other.isEmpty()) { - return other; - } - final Set otherKeys = other.getExactKeys(); - if (otherKeys != null) { - final Set intersection = new HashSet<>(keys); - intersection.retainAll(otherKeys); - return SavepointKeyFilter.exact(intersection); - } - return SavepointKeyFilter.filterKeys(keys, other); - } - @Override public String toString() { return "ExactKeyFilter" + keys; diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/RangeKeyFilter.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/RangeKeyFilter.java index 392a06fbf8766d..22f99c3e1b339e 100644 --- a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/RangeKeyFilter.java +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/RangeKeyFilter.java @@ -20,105 +20,51 @@ import javax.annotation.Nullable; -import java.util.Set; - /** A filter based on a range with an injected comparator. */ final class RangeKeyFilter implements SavepointKeyFilter { private static final long serialVersionUID = 3L; private final SerializableComparator comparator; - @Nullable private final BoundInfo lower; - @Nullable private final BoundInfo upper; + @Nullable private final K lower; + private final boolean isLowerInclusive; + @Nullable private final K upper; + private final boolean isUpperInclusive; RangeKeyFilter( SerializableComparator comparator, - @Nullable BoundInfo lower, - @Nullable BoundInfo upper) { + @Nullable K lower, + boolean isLowerInclusive, + @Nullable K upper, + boolean isUpperInclusive) { this.comparator = comparator; this.lower = lower; + this.isLowerInclusive = isLowerInclusive; this.upper = upper; + this.isUpperInclusive = isUpperInclusive; } @Override public boolean test(K key) { if (lower != null) { - int cmp = comparator.compare(lower.getValue(), key); - if (cmp > 0 || (cmp == 0 && !lower.isInclusive())) { + int cmp = comparator.compare(lower, key); + if (cmp > 0 || (cmp == 0 && !isLowerInclusive)) { return false; } } if (upper != null) { - int cmp = comparator.compare(upper.getValue(), key); - if (cmp < 0 || (cmp == 0 && !upper.isInclusive())) { + int cmp = comparator.compare(upper, key); + if (cmp < 0 || (cmp == 0 && !isUpperInclusive)) { return false; } } return true; } - @Override - public BoundInfo getLowerBound() { - return lower; - } - - @Override - public BoundInfo getUpperBound() { - return upper; - } - - @Override - public SavepointKeyFilter intersect(SavepointKeyFilter other) { - if (other.isEmpty()) { - return other; - } - final Set otherExactKeys = other.getExactKeys(); - if (otherExactKeys != null) { - return SavepointKeyFilter.filterKeys(otherExactKeys, this); - } - return intersectRange(other.getLowerBound(), other.getUpperBound()); - } - - private SavepointKeyFilter intersectRange( - @Nullable BoundInfo otherLower, @Nullable BoundInfo otherUpper) { - BoundInfo newLower = tighter(lower, otherLower, true); - BoundInfo newUpper = tighter(upper, otherUpper, false); - - if (newLower != null && newUpper != null) { - int cmp = comparator.compare(newLower.getValue(), newUpper.getValue()); - if (cmp > 0) { - return SavepointKeyFilter.empty(); - } - if (cmp == 0 && (!newLower.isInclusive() || !newUpper.isInclusive())) { - return SavepointKeyFilter.empty(); - } - } - return new RangeKeyFilter<>(comparator, newLower, newUpper); - } - - @Nullable - private BoundInfo tighter( - @Nullable BoundInfo a, @Nullable BoundInfo b, boolean preferHigher) { - if (a == null) { - return b; - } - if (b == null) { - return a; - } - int c = comparator.compare(a.getValue(), b.getValue()); - if (c == 0) { - return new BoundInfo<>(a.getValue(), a.isInclusive() && b.isInclusive()); - } - boolean aWins = preferHigher ? c > 0 : c < 0; - return aWins ? a : b; - } - @Override public String toString() { - String lowerStr = - lower == null ? "(-∞" : (lower.isInclusive() ? "[" : "(") + lower.getValue(); - String upperStr = - upper == null ? "+∞)" : upper.getValue() + (upper.isInclusive() ? "]" : ")"); + String lowerStr = lower == null ? "(-∞" : (isLowerInclusive ? "[" : "(") + lower; + String upperStr = upper == null ? "+∞)" : upper + (isUpperInclusive ? "]" : ")"); return "RangeKeyFilter" + lowerStr + ", " + upperStr; } } diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/SavepointKeyFilter.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/SavepointKeyFilter.java index c639c5c056d940..ebf059ec544207 100644 --- a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/SavepointKeyFilter.java +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/SavepointKeyFilter.java @@ -23,7 +23,6 @@ import javax.annotation.Nullable; import java.io.Serializable; -import java.util.HashSet; import java.util.Set; /** @@ -37,15 +36,6 @@ public interface SavepointKeyFilter extends Serializable { /** Returns {@code true} if the given key passes this filter. */ boolean test(K key); - /** - * Returns {@code true} if this filter rejects every key. - * - *

Used only while combining filters during push-down translation, not during the scan. - */ - default boolean isEmpty() { - return false; - } - /** * Returns the finite set of keys this filter matches, or {@code null} if the filter does not * resolve to a finite key set. @@ -55,58 +45,12 @@ default Set getExactKeys() { return null; } - /** - * Returns the lower bound of this filter's range, or {@code null} if the filter does not define - * a lower bound. - * - *

Used only while combining filters during push-down translation, not during the scan. - */ - @Nullable - default BoundInfo getLowerBound() { - return null; - } - - /** - * Returns the upper bound of this filter's range, or {@code null} if the filter does not define - * an upper bound. - * - *

Used only while combining filters during push-down translation, not during the scan. - */ - @Nullable - default BoundInfo getUpperBound() { - return null; - } - - /** - * Returns a filter that accepts a key if and only if both {@code this} and {@code other} accept - * it. - * - *

Used only while combining filters during push-down translation, not during the scan. - */ - default SavepointKeyFilter intersect(SavepointKeyFilter other) { - throw new UnsupportedOperationException( - getClass().getSimpleName() + " does not support intersect()"); - } - - static SavepointKeyFilter filterKeys(Set keys, SavepointKeyFilter predicate) { - final Set retained = new HashSet<>(); - for (K key : keys) { - if (predicate.test(key)) { - retained.add(key); - } - } - return exact(retained); - } - static SavepointKeyFilter exact(Set keys) { - if (keys.isEmpty()) { - return EmptyKeyFilter.instance(); - } return new ExactKeyFilter<>(keys); } static SavepointKeyFilter exact(K value) { - return new ExactKeyFilter<>(Set.of(value)); + return exact(Set.of(value)); } static > SavepointKeyFilter range( @@ -120,13 +64,7 @@ static SavepointKeyFilter range( @Nullable K upper, boolean upperInclusive, SerializableComparator comparator) { - BoundInfo lowerBoundInfo = lower != null ? new BoundInfo<>(lower, lowerInclusive) : null; - BoundInfo upperBoundInfo = upper != null ? new BoundInfo<>(upper, upperInclusive) : null; - return new RangeKeyFilter<>(comparator, lowerBoundInfo, upperBoundInfo); - } - - static SavepointKeyFilter empty() { - return EmptyKeyFilter.instance(); + return new RangeKeyFilter<>(comparator, lower, lowerInclusive, upper, upperInclusive); } final class NaturalOrderComparator> diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/SavepointFilterTranslator.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/SavepointFilterTranslator.java index f12cdf9a304e96..ec6af13c496320 100644 --- a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/SavepointFilterTranslator.java +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/SavepointFilterTranslator.java @@ -18,7 +18,7 @@ package org.apache.flink.state.table; -import org.apache.flink.state.api.filter.SavepointKeyFilter; +import org.apache.flink.state.table.filter.SavepointKeyFilterPlan; import org.apache.flink.table.expressions.CallExpression; import org.apache.flink.table.expressions.FieldReferenceExpression; import org.apache.flink.table.expressions.ResolvedExpression; @@ -40,7 +40,7 @@ import java.util.function.BiFunction; /** - * Converts {@link ResolvedExpression} key filter predicates into {@link SavepointKeyFilter} + * Converts {@link ResolvedExpression} key filter predicates into {@link SavepointKeyFilterPlan} * instances that can be used to prune key groups and key iterations during savepoint reads. */ @SuppressWarnings({"rawtypes", "unchecked"}) @@ -50,7 +50,7 @@ class SavepointFilterTranslator { private static final Map< FunctionDefinition, - BiFunction> + BiFunction> FILTERS = Map.of( BuiltInFunctionDefinitions.EQUALS, @@ -82,9 +82,9 @@ Result apply(List filters) { final List accepted = new ArrayList<>(); final List remaining = new ArrayList<>(); - SavepointKeyFilter keyFilter = null; + SavepointKeyFilterPlan keyFilter = null; for (ResolvedExpression filter : filters) { - SavepointKeyFilter extracted = extractFilter(filter); + SavepointKeyFilterPlan extracted = extractFilter(filter); if (extracted == null) { remaining.add(filter); continue; @@ -98,11 +98,12 @@ Result apply(List filters) { } @Nullable - private SavepointKeyFilter extractFilter(ResolvedExpression expr) { - final BiFunction extractor = - expr instanceof CallExpression - ? FILTERS.get(((CallExpression) expr).getFunctionDefinition()) - : null; + private SavepointKeyFilterPlan extractFilter(ResolvedExpression expr) { + final BiFunction + extractor = + expr instanceof CallExpression + ? FILTERS.get(((CallExpression) expr).getFunctionDefinition()) + : null; if (extractor == null) { LOG.debug( "Unsupported predicate [{}] cannot be pushed into savepoint key filter.", expr); @@ -116,7 +117,7 @@ private SavepointKeyFilter extractFilter(ResolvedExpression expr) { // ------------------------------------------------------------------------- @Nullable - private SavepointKeyFilter fromEquals(CallExpression call) { + private SavepointKeyFilterPlan fromEquals(CallExpression call) { if (!isBinaryValid(call)) { return null; } @@ -133,14 +134,14 @@ private SavepointKeyFilter fromEquals(CallExpression call) { if (value == null) { return null; } - return SavepointKeyFilter.exact(value); + return SavepointKeyFilterPlan.exact(value); } @Nullable - private SavepointKeyFilter fromOr(CallExpression call) { + private SavepointKeyFilterPlan fromOr(CallExpression call) { Set keys = new HashSet<>(); for (ResolvedExpression arg : call.getResolvedChildren()) { - SavepointKeyFilter sub = extractFilter(arg); + SavepointKeyFilterPlan sub = extractFilter(arg); if (sub == null) { return null; } @@ -151,7 +152,7 @@ private SavepointKeyFilter fromOr(CallExpression call) { } keys.addAll(subKeys); } - return SavepointKeyFilter.exact(keys); + return SavepointKeyFilterPlan.exact(keys); } // ------------------------------------------------------------------------- @@ -159,10 +160,10 @@ private SavepointKeyFilter fromOr(CallExpression call) { // ------------------------------------------------------------------------- @Nullable - private SavepointKeyFilter fromAnd(CallExpression call) { - SavepointKeyFilter merged = null; + private SavepointKeyFilterPlan fromAnd(CallExpression call) { + SavepointKeyFilterPlan merged = null; for (ResolvedExpression arg : call.getResolvedChildren()) { - SavepointKeyFilter sub = extractFilter(arg); + SavepointKeyFilterPlan sub = extractFilter(arg); // AND only absorbs range filters; exact (or null) children break pushdown. if (sub == null || sub.getExactKeys() != null) { return null; @@ -176,7 +177,7 @@ private SavepointKeyFilter fromAnd(CallExpression call) { } @Nullable - private SavepointKeyFilter fromBetween(CallExpression call) { + private SavepointKeyFilterPlan fromBetween(CallExpression call) { List args = call.getResolvedChildren(); if (args.size() != 3) { return null; @@ -200,33 +201,33 @@ private SavepointKeyFilter fromBetween(CallExpression call) { lower.getClass().getName()); return null; } - return SavepointKeyFilter.range( + return SavepointKeyFilterPlan.range( (Comparable) lower, true, (Comparable) upper, true); } @Nullable - private SavepointKeyFilter fromGreaterThan(CallExpression call) { + private SavepointKeyFilterPlan fromGreaterThan(CallExpression call) { return fromComparison(call, Comparison.GT); } @Nullable - private SavepointKeyFilter fromGreaterThanOrEqual(CallExpression call) { + private SavepointKeyFilterPlan fromGreaterThanOrEqual(CallExpression call) { return fromComparison(call, Comparison.GTE); } @Nullable - private SavepointKeyFilter fromLessThan(CallExpression call) { + private SavepointKeyFilterPlan fromLessThan(CallExpression call) { return fromComparison(call, Comparison.LT); } @Nullable - private SavepointKeyFilter fromLessThanOrEqual(CallExpression call) { + private SavepointKeyFilterPlan fromLessThanOrEqual(CallExpression call) { return fromComparison(call, Comparison.LTE); } @Nullable - private SavepointKeyFilter fromComparison(CallExpression call, Comparison cmp) { + private SavepointKeyFilterPlan fromComparison(CallExpression call, Comparison cmp) { if (!isBinaryValid(call)) { return null; } @@ -252,13 +253,13 @@ private SavepointKeyFilter fromComparison(CallExpression call, Comparison cmp) { Comparison keyLeftCmp = keyOnLeft ? cmp : cmp.flip(); switch (keyLeftCmp) { case GT: - return SavepointKeyFilter.range(b, false, null, true); + return SavepointKeyFilterPlan.range(b, false, null, true); case GTE: - return SavepointKeyFilter.range(b, true, null, true); + return SavepointKeyFilterPlan.range(b, true, null, true); case LT: - return SavepointKeyFilter.range(null, true, b, false); + return SavepointKeyFilterPlan.range(null, true, b, false); case LTE: - return SavepointKeyFilter.range(null, true, b, true); + return SavepointKeyFilterPlan.range(null, true, b, true); default: throw new IllegalStateException("Unknown Comparison: " + keyLeftCmp); } @@ -323,12 +324,12 @@ private Object widenToKeyType(Object value) { static final class Result { private final List accepted; private final List remaining; - @Nullable private final SavepointKeyFilter keyFilter; + @Nullable private final SavepointKeyFilterPlan keyFilter; private Result( List accepted, List remaining, - @Nullable SavepointKeyFilter keyFilter) { + @Nullable SavepointKeyFilterPlan keyFilter) { this.accepted = accepted; this.remaining = remaining; this.keyFilter = keyFilter; @@ -343,7 +344,7 @@ List remaining() { } @Nullable - SavepointKeyFilter keyFilter() { + SavepointKeyFilterPlan keyFilter() { return keyFilter; } } diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/BoundInfo.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/BoundInfo.java similarity index 92% rename from flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/BoundInfo.java rename to flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/BoundInfo.java index fe0b525cd1c7bf..dd1ea742bbeaa7 100644 --- a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/BoundInfo.java +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/BoundInfo.java @@ -16,14 +16,14 @@ * limitations under the License. */ -package org.apache.flink.state.api.filter; +package org.apache.flink.state.table.filter; -import org.apache.flink.annotation.Experimental; +import org.apache.flink.annotation.Internal; import java.io.Serializable; /** Information about a bound in a range filter. */ -@Experimental +@Internal public final class BoundInfo implements Serializable { private static final long serialVersionUID = 4L; diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/EmptyKeyFilter.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/EmptyKeyFilterPlan.java similarity index 75% rename from flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/EmptyKeyFilter.java rename to flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/EmptyKeyFilterPlan.java index 160cc17d4a519a..ae5502cac8c729 100644 --- a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/EmptyKeyFilter.java +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/EmptyKeyFilterPlan.java @@ -16,24 +16,24 @@ * limitations under the License. */ -package org.apache.flink.state.api.filter; +package org.apache.flink.state.table.filter; import java.util.Collections; import java.util.Set; /** A filter that rejects every key. */ -final class EmptyKeyFilter implements SavepointKeyFilter { +final class EmptyKeyFilterPlan implements SavepointKeyFilterPlan { private static final long serialVersionUID = 1L; @SuppressWarnings("rawtypes") - private static final EmptyKeyFilter INSTANCE = new EmptyKeyFilter<>(); + private static final EmptyKeyFilterPlan INSTANCE = new EmptyKeyFilterPlan<>(); - private EmptyKeyFilter() {} + private EmptyKeyFilterPlan() {} @SuppressWarnings("unchecked") - static EmptyKeyFilter instance() { - return (EmptyKeyFilter) INSTANCE; + static EmptyKeyFilterPlan instance() { + return (EmptyKeyFilterPlan) INSTANCE; } @Override @@ -52,7 +52,7 @@ public Set getExactKeys() { } @Override - public SavepointKeyFilter intersect(SavepointKeyFilter other) { + public SavepointKeyFilterPlan intersect(SavepointKeyFilterPlan other) { return this; } @@ -62,6 +62,6 @@ private Object readResolve() { @Override public String toString() { - return "EmptyKeyFilter"; + return "EmptyKeyFilterPlan"; } } diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/ExactKeyFilterPlan.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/ExactKeyFilterPlan.java new file mode 100644 index 00000000000000..23f870367d202b --- /dev/null +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/ExactKeyFilterPlan.java @@ -0,0 +1,63 @@ +/* + * 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.state.table.filter; + +import java.util.HashSet; +import java.util.Set; + +/** A filter that accepts a finite set of keys. */ +final class ExactKeyFilterPlan implements SavepointKeyFilterPlan { + + private static final long serialVersionUID = 2L; + + private final Set keys; + + ExactKeyFilterPlan(Set keys) { + this.keys = Set.copyOf(keys); + } + + @Override + public boolean test(K key) { + return keys.contains(key); + } + + @Override + public Set getExactKeys() { + return keys; + } + + @Override + public SavepointKeyFilterPlan intersect(SavepointKeyFilterPlan other) { + if (other.isEmpty()) { + return other; + } + final Set otherKeys = other.getExactKeys(); + if (otherKeys != null) { + final Set intersection = new HashSet<>(keys); + intersection.retainAll(otherKeys); + return SavepointKeyFilterPlan.exact(intersection); + } + return SavepointKeyFilterPlan.filterKeys(keys, other); + } + + @Override + public String toString() { + return "ExactKeyFilterPlan" + keys; + } +} diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/RangeKeyFilterPlan.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/RangeKeyFilterPlan.java new file mode 100644 index 00000000000000..8732f33a445b53 --- /dev/null +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/RangeKeyFilterPlan.java @@ -0,0 +1,126 @@ +/* + * 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.state.table.filter; + +import org.apache.flink.state.api.filter.SerializableComparator; + +import javax.annotation.Nullable; + +import java.util.Set; + +/** A filter based on a range with an injected comparator. */ +final class RangeKeyFilterPlan implements SavepointKeyFilterPlan { + + private static final long serialVersionUID = 3L; + + private final SerializableComparator comparator; + @Nullable private final BoundInfo lower; + @Nullable private final BoundInfo upper; + + RangeKeyFilterPlan( + SerializableComparator comparator, + @Nullable BoundInfo lower, + @Nullable BoundInfo upper) { + this.comparator = comparator; + this.lower = lower; + this.upper = upper; + } + + @Override + public boolean test(K key) { + if (lower != null) { + int cmp = comparator.compare(lower.getValue(), key); + if (cmp > 0 || (cmp == 0 && !lower.isInclusive())) { + return false; + } + } + if (upper != null) { + int cmp = comparator.compare(upper.getValue(), key); + if (cmp < 0 || (cmp == 0 && !upper.isInclusive())) { + return false; + } + } + return true; + } + + @Override + public BoundInfo getLowerBound() { + return lower; + } + + @Override + public BoundInfo getUpperBound() { + return upper; + } + + @Override + public SavepointKeyFilterPlan intersect(SavepointKeyFilterPlan other) { + if (other.isEmpty()) { + return other; + } + final Set otherExactKeys = other.getExactKeys(); + if (otherExactKeys != null) { + return SavepointKeyFilterPlan.filterKeys(otherExactKeys, this); + } + return intersectRange(other.getLowerBound(), other.getUpperBound()); + } + + private SavepointKeyFilterPlan intersectRange( + @Nullable BoundInfo otherLower, @Nullable BoundInfo otherUpper) { + BoundInfo newLower = tighter(lower, otherLower, true); + BoundInfo newUpper = tighter(upper, otherUpper, false); + + if (newLower != null && newUpper != null) { + int cmp = comparator.compare(newLower.getValue(), newUpper.getValue()); + if (cmp > 0) { + return SavepointKeyFilterPlan.empty(); + } + if (cmp == 0 && (!newLower.isInclusive() || !newUpper.isInclusive())) { + return SavepointKeyFilterPlan.empty(); + } + } + return new RangeKeyFilterPlan<>(comparator, newLower, newUpper); + } + + @Nullable + private BoundInfo tighter( + @Nullable BoundInfo a, @Nullable BoundInfo b, boolean preferHigher) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + int c = comparator.compare(a.getValue(), b.getValue()); + if (c == 0) { + return new BoundInfo<>(a.getValue(), a.isInclusive() && b.isInclusive()); + } + boolean aWins = preferHigher ? c > 0 : c < 0; + return aWins ? a : b; + } + + @Override + public String toString() { + String lowerStr = + lower == null ? "(-∞" : (lower.isInclusive() ? "[" : "(") + lower.getValue(); + String upperStr = + upper == null ? "+∞)" : upper.getValue() + (upper.isInclusive() ? "]" : ")"); + return "RangeKeyFilterPlan" + lowerStr + ", " + upperStr; + } +} diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlan.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlan.java new file mode 100644 index 00000000000000..21fe4f5c07898b --- /dev/null +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlan.java @@ -0,0 +1,106 @@ +/* + * 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.state.table.filter; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.state.api.filter.SavepointKeyFilter; +import org.apache.flink.state.api.filter.SerializableComparator; + +import javax.annotation.Nullable; + +import java.util.HashSet; +import java.util.Set; + +/** + * The internal, richer view of a {@link SavepointKeyFilter} used while combining and analyzing + * filters during push-down translation. + */ +@Internal +public interface SavepointKeyFilterPlan extends SavepointKeyFilter { + + /** Returns {@code true} if this filter rejects every key. */ + default boolean isEmpty() { + return false; + } + + /** Returns the lower bound of this filter's range, or {@code null} if there is none. */ + @Nullable + default BoundInfo getLowerBound() { + return null; + } + + /** Returns the upper bound of this filter's range, or {@code null} if there is none. */ + @Nullable + default BoundInfo getUpperBound() { + return null; + } + + /** + * Returns a filter that accepts a key if and only if both {@code this} and {@code other} accept + * it. + */ + SavepointKeyFilterPlan intersect(SavepointKeyFilterPlan other); + + /** Returns a filter matching the subset of {@code keys} that {@code predicate} accepts. */ + static SavepointKeyFilterPlan filterKeys(Set keys, SavepointKeyFilter predicate) { + final Set retained = new HashSet<>(); + for (K key : keys) { + if (predicate.test(key)) { + retained.add(key); + } + } + return exact(retained); + } + + /** Returns a filter accepting only the given keys, or the empty filter if there are none. */ + static SavepointKeyFilterPlan exact(Set keys) { + if (keys.isEmpty()) { + return EmptyKeyFilterPlan.instance(); + } + return new ExactKeyFilterPlan<>(keys); + } + + /** Returns a filter accepting only the given key. */ + static SavepointKeyFilterPlan exact(K value) { + return new ExactKeyFilterPlan<>(Set.of(value)); + } + + /** Returns a range filter over the natural ordering of the key type. */ + static > SavepointKeyFilterPlan range( + @Nullable K lower, boolean lowerInclusive, @Nullable K upper, boolean upperInclusive) { + return range(lower, lowerInclusive, upper, upperInclusive, new NaturalOrderComparator<>()); + } + + /** Returns a range filter ordered by {@code comparator}; a {@code null} bound is unbounded. */ + static SavepointKeyFilterPlan range( + @Nullable K lower, + boolean lowerInclusive, + @Nullable K upper, + boolean upperInclusive, + SerializableComparator comparator) { + BoundInfo lowerBoundInfo = lower != null ? new BoundInfo<>(lower, lowerInclusive) : null; + BoundInfo upperBoundInfo = upper != null ? new BoundInfo<>(upper, upperInclusive) : null; + return new RangeKeyFilterPlan<>(comparator, lowerBoundInfo, upperBoundInfo); + } + + /** Returns a filter that rejects every key. */ + static SavepointKeyFilterPlan empty() { + return EmptyKeyFilterPlan.instance(); + } +} diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/SavepointReaderKeyedStateITCase.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/SavepointReaderKeyedStateITCase.java index 00746a9285a94f..75dc9eb4a8f7ca 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/SavepointReaderKeyedStateITCase.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/SavepointReaderKeyedStateITCase.java @@ -31,6 +31,7 @@ import org.apache.flink.state.api.functions.KeyedStateReaderFunction; import org.apache.flink.state.api.utils.JobResultRetriever; import org.apache.flink.state.api.utils.SavepointTestBase; +import org.apache.flink.state.table.filter.SavepointKeyFilterPlan; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.api.functions.KeyedProcessFunction; @@ -219,7 +220,7 @@ void testReadKeyedStateWithEmptyFilter() throws Exception { SavepointReader savepoint = SavepointReader.read(env, savepointPath, backendTuple.f1); CountingReadResult result = - readKeyedStateWithCountingReader(savepoint, SavepointKeyFilter.empty()); + readKeyedStateWithCountingReader(savepoint, SavepointKeyFilterPlan.empty()); // No key reaches the reader, so no state is read. assertThat(result.values).isEmpty(); assertThat(result.counter).isZero(); diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/input/KeyedStateInputFormatTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/input/KeyedStateInputFormatTest.java index bde20fd0f579cc..2b9cb85126d785 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/input/KeyedStateInputFormatTest.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/input/KeyedStateInputFormatTest.java @@ -136,29 +136,6 @@ void testExactKeyFilterPrunesInputSplits(boolean asyncState) throws Exception { .hasSize(1); } - @ParameterizedTest(name = "Enable async state = {0}") - @ValueSource(booleans = {false, true}) - void testEmptyFilterProducesNoInputSplits(boolean asyncState) throws Exception { - OperatorID operatorID = OperatorIDGenerator.fromUid("uid"); - - OperatorSubtaskState state = - createOperatorSubtaskState(createFlatMap(asyncState), asyncState); - OperatorState operatorState = new OperatorState(null, null, operatorID, 1, 128); - operatorState.putState(0, state); - - KeyedStateInputFormat format = - new KeyedStateInputFormat<>( - operatorState, - new HashMapStateBackend(), - new Configuration(), - new KeyedStateReaderOperator<>(new ReaderFunction(), Types.INT), - new ExecutionConfig(), - SavepointKeyFilter.empty()); - KeyGroupRangeInputSplit[] splits = format.createInputSplits(10); - - assertThat(splits).isEmpty(); - } - @ParameterizedTest(name = "Enable async state = {0}") @ValueSource(booleans = {false, true}) void testRangeFilterDoesNotPruneInputSplits(boolean asyncState) throws Exception { diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java index 3e490b971602f2..3fcb2792d4ed5f 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java @@ -18,7 +18,7 @@ package org.apache.flink.state.table; -import org.apache.flink.state.api.filter.SavepointKeyFilter; +import org.apache.flink.state.table.filter.SavepointKeyFilterPlan; import org.apache.flink.table.api.DataTypes; import org.apache.flink.table.expressions.CallExpression; import org.apache.flink.table.expressions.FieldReferenceExpression; @@ -29,10 +29,6 @@ import org.junit.jupiter.api.Test; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; @@ -53,7 +49,7 @@ class SavepointFilterTranslatorTest { @Test void equalsKeyOnLeft() { - SavepointKeyFilter filter = keyFilterOf(eq(longKeyRef(), longLit(42L))); + SavepointKeyFilterPlan filter = keyFilterOf(eq(longKeyRef(), longLit(42L))); assertNotNull(filter); assertThat(filter.getExactKeys()).containsExactly(42L); assertThat(filter.test(42L)).isTrue(); @@ -62,7 +58,7 @@ void equalsKeyOnLeft() { @Test void equalsKeyOnRight() { - SavepointKeyFilter filter = keyFilterOf(eq(longLit(42L), longKeyRef())); + SavepointKeyFilterPlan filter = keyFilterOf(eq(longLit(42L), longKeyRef())); assertNotNull(filter); assertThat(filter.getExactKeys()).containsExactly(42L); assertThat(filter.test(42L)).isTrue(); @@ -71,13 +67,13 @@ void equalsKeyOnRight() { @Test void equalsNeitherSideIsKeyColumn_returnsNull() { - SavepointKeyFilter filter = keyFilterOf(eq(otherRef(), longLit(42L))); + SavepointKeyFilterPlan filter = keyFilterOf(eq(otherRef(), longLit(42L))); assertThat(filter).isNull(); } @Test void equalsNeitherSideIsLiteral_returnsNull() { - SavepointKeyFilter filter = keyFilterOf(eq(longKeyRef(), otherRef())); + SavepointKeyFilterPlan filter = keyFilterOf(eq(longKeyRef(), otherRef())); assertThat(filter).isNull(); } @@ -93,7 +89,7 @@ void orOfEqualsProducesMergedExactFilter() { eq(longKeyRef(), longLit(2L)), eq(longLit(3L), longKeyRef())); - SavepointKeyFilter filter = keyFilterOf(expr); + SavepointKeyFilterPlan filter = keyFilterOf(expr); assertNotNull(filter); assertThat(filter.getExactKeys()).containsExactlyInAnyOrder(1L, 2L, 3L); assertThat(filter.test(4L)).isFalse(); @@ -112,7 +108,7 @@ void orWithNonPushableChild_returnsNull() { @Test void betweenProducesInclusiveRange() { - SavepointKeyFilter filter = + SavepointKeyFilterPlan filter = keyFilterOf(between(longKeyRef(), longLit(10L), longLit(20L))); assertNotNull(filter); @@ -126,7 +122,7 @@ void betweenProducesInclusiveRange() { @Test void betweenWithNonKeyField_returnsNull() { - SavepointKeyFilter filter = + SavepointKeyFilterPlan filter = keyFilterOf(between(otherRef(), longLit(1L), longLit(10L))); assertThat(filter).isNull(); } @@ -137,7 +133,7 @@ void betweenWithNonKeyField_returnsNull() { @Test void greaterThanProducesExclusiveLowerBound() { - SavepointKeyFilter filter = keyFilterOf(gt(longKeyRef(), longLit(5L))); + SavepointKeyFilterPlan filter = keyFilterOf(gt(longKeyRef(), longLit(5L))); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); assertThat(filter.test(5L)).isFalse(); @@ -146,7 +142,7 @@ void greaterThanProducesExclusiveLowerBound() { @Test void greaterThanOrEqualProducesInclusiveLowerBound() { - SavepointKeyFilter filter = keyFilterOf(gte(longKeyRef(), longLit(5L))); + SavepointKeyFilterPlan filter = keyFilterOf(gte(longKeyRef(), longLit(5L))); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); assertThat(filter.test(4L)).isFalse(); @@ -156,7 +152,7 @@ void greaterThanOrEqualProducesInclusiveLowerBound() { @Test void lessThanProducesExclusiveUpperBound() { - SavepointKeyFilter filter = keyFilterOf(lt(longKeyRef(), longLit(10L))); + SavepointKeyFilterPlan filter = keyFilterOf(lt(longKeyRef(), longLit(10L))); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); assertThat(filter.test(9L)).isTrue(); @@ -165,7 +161,7 @@ void lessThanProducesExclusiveUpperBound() { @Test void lessThanOrEqualProducesInclusiveUpperBound() { - SavepointKeyFilter filter = keyFilterOf(lte(longKeyRef(), longLit(10L))); + SavepointKeyFilterPlan filter = keyFilterOf(lte(longKeyRef(), longLit(10L))); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); assertThat(filter.test(10L)).isTrue(); @@ -175,7 +171,7 @@ void lessThanOrEqualProducesInclusiveUpperBound() { @Test void comparisonWithLiteralOnLeft_flipsDirection() { // literal > key → key < literal → upper bound (exclusive) - SavepointKeyFilter filter = keyFilterOf(gt(longLit(10L), longKeyRef())); + SavepointKeyFilterPlan filter = keyFilterOf(gt(longLit(10L), longKeyRef())); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); assertThat(filter.test(9L)).isTrue(); @@ -185,7 +181,7 @@ void comparisonWithLiteralOnLeft_flipsDirection() { @Test void comparisonWithLiteralOnLeft_lte_flipsDirection() { // literal <= key → key >= literal → lower bound (inclusive) - SavepointKeyFilter filter = keyFilterOf(lte(longLit(5L), longKeyRef())); + SavepointKeyFilterPlan filter = keyFilterOf(lte(longLit(5L), longKeyRef())); assertThat(filter.getExactKeys()).isNull(); assertThat(filter.test(4L)).isFalse(); assertThat(filter.test(5L)).isTrue(); @@ -199,7 +195,7 @@ void comparisonWithLiteralOnLeft_lte_flipsDirection() { void andOfTwoRangesProducesIntersection() { // key >= 5 AND key <= 10 CallExpression expr = and(gte(longKeyRef(), longLit(5L)), lte(longKeyRef(), longLit(10L))); - SavepointKeyFilter filter = keyFilterOf(expr); + SavepointKeyFilterPlan filter = keyFilterOf(expr); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); @@ -213,7 +209,7 @@ void andOfTwoRangesProducesIntersection() { void andWithProvablyEmptyIntersection_matchesNothing() { // key > 10 AND key < 5 — disjoint CallExpression expr = and(gt(longKeyRef(), longLit(10L)), lt(longKeyRef(), longLit(5L))); - SavepointKeyFilter filter = keyFilterOf(expr); + SavepointKeyFilterPlan filter = keyFilterOf(expr); assertNotNull(filter); assertThat(filter.isEmpty()).isTrue(); @@ -251,7 +247,7 @@ void unrecognizedFunctionReturnsNull() { @Test void rangeFilterOnStringKey() { - SavepointKeyFilter filter = + SavepointKeyFilterPlan filter = keyFilterOf(between(stringKeyRef(), stringLit("beta"), stringLit("delta"))); assertNotNull(filter); @@ -271,7 +267,8 @@ void rangeFilterWithDoubleComparison() { FieldReferenceExpression floatKey = new FieldReferenceExpression("key", DataTypes.FLOAT().notNull(), 0, KEY_COL); - SavepointKeyFilter filter = keyFilterOf(between(floatKey, floatLower, floatUpper)); + SavepointKeyFilterPlan filter = + keyFilterOf(between(floatKey, floatLower, floatUpper)); assertNotNull(filter); assertThat(filter.test(1.5f)).isTrue(); @@ -281,74 +278,6 @@ void rangeFilterWithDoubleComparison() { assertThat(filter.test(4.0f)).isFalse(); } - // ------------------------------------------------------------------------- - // Range intersection - // ------------------------------------------------------------------------- - - @Test - void intersectNarrowsBounds() { - // [5, ∞) ∩ (-∞, 10] = [5, 10] - SavepointKeyFilter lower = SavepointKeyFilter.range(5L, true, null, true); - SavepointKeyFilter upper = SavepointKeyFilter.range(null, true, 10L, true); - SavepointKeyFilter result = lower.intersect(upper); - - assertThat(result.isEmpty()).isFalse(); - assertThat(result.getExactKeys()).isNull(); - assertThat(result.test(4L)).isFalse(); - assertThat(result.test(5L)).isTrue(); - assertThat(result.test(10L)).isTrue(); - assertThat(result.test(11L)).isFalse(); - } - - @Test - void intersectDisjointRangesReturnsEmpty() { - // [10, ∞) ∩ (-∞, 5] — disjoint - SavepointKeyFilter a = SavepointKeyFilter.range(10L, true, null, true); - SavepointKeyFilter b = SavepointKeyFilter.range(null, true, 5L, true); - assertThat(a.intersect(b).isEmpty()).isTrue(); - } - - @Test - void intersectEqualBoundsInclusiveIsNonEmpty() { - // [7, ∞) ∩ (-∞, 7] = [7, 7] - SavepointKeyFilter a = SavepointKeyFilter.range(7L, true, null, true); - SavepointKeyFilter b = SavepointKeyFilter.range(null, true, 7L, true); - SavepointKeyFilter result = a.intersect(b); - assertThat(result.isEmpty()).isFalse(); - assertThat(result.test(7L)).isTrue(); - assertThat(result.test(6L)).isFalse(); - assertThat(result.test(8L)).isFalse(); - } - - @Test - void intersectEqualBoundsOneExclusiveIsEmpty() { - // (7, ∞) ∩ (-∞, 7] — empty because lower is exclusive - SavepointKeyFilter a = SavepointKeyFilter.range(7L, false, null, true); - SavepointKeyFilter b = SavepointKeyFilter.range(null, true, 7L, true); - assertThat(a.intersect(b).isEmpty()).isTrue(); - } - - // ------------------------------------------------------------------------- - // Custom comparator - // ------------------------------------------------------------------------- - - @Test - void rangeWithCustomComparatorIsUsed() { - // Orders strings by length — clearly not the natural String order. - SavepointKeyFilter filter = - SavepointKeyFilter.range( - "aa", - true, - "cccc", - true, - (a, b) -> Integer.compare(a.length(), b.length())); - - // Length in [2, 4]: "abc" (3), passes; "a" (1) and "ccccc" (5), fail. - assertThat(filter.test("abc")).isTrue(); - assertThat(filter.test("a")).isFalse(); - assertThat(filter.test("ccccc")).isFalse(); - } - // ------------------------------------------------------------------------- // SavepointFilters.apply — intersection handling // ------------------------------------------------------------------------- @@ -359,7 +288,7 @@ void applyAccumulatesRangeAndRange() { apply( List.of(gte(longKeyRef(), longLit(3L)), lte(longKeyRef(), longLit(8L))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -384,7 +313,7 @@ void applyAccumulatesExactAndExact() { eq(longKeyRef(), longLit(3L)), eq(longKeyRef(), longLit(4L)))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -398,7 +327,7 @@ void applyAccumulatesExactAndExactEmptyResult_matchesNothing() { apply( List.of(eq(longKeyRef(), longLit(1L)), eq(longKeyRef(), longLit(2L))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -418,7 +347,7 @@ void applyAccumulatesExactAndRange_keepsOnlyKeysInRange() { eq(longKeyRef(), longLit(15L))), between(longKeyRef(), longLit(4L), longLit(12L))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -438,7 +367,7 @@ void applyAccumulatesRangeAndExact_keepsOnlyKeysInRange() { eq(longKeyRef(), longLit(10L)), eq(longKeyRef(), longLit(15L)))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -446,52 +375,6 @@ void applyAccumulatesRangeAndExact_keepsOnlyKeysInRange() { assertThat(result.getExactKeys()).containsExactlyInAnyOrder(5L, 10L); } - // ------------------------------------------------------------------------- - // Empty key filter - // ------------------------------------------------------------------------- - - @Test - void emptyKeyFilter_rejectsEverything() { - SavepointKeyFilter empty = SavepointKeyFilter.empty(); - assertThat(empty.isEmpty()).isTrue(); - assertThat(empty.getExactKeys()).isEmpty(); - assertThat(empty.test(42L)).isFalse(); - assertThat(empty.test("hello")).isFalse(); - } - - @Test - void exactWithEmptySetReturnsEmptyKeyFilter() { - SavepointKeyFilter filter = SavepointKeyFilter.exact(Collections.emptySet()); - assertThat(filter.isEmpty()).isTrue(); - } - - @Test - void emptyKeyFilterSingletonPreservedAcrossSerialization() throws Exception { - SavepointKeyFilter original = SavepointKeyFilter.empty(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { - oos.writeObject(original); - } - Object deserialized; - try (ObjectInputStream ois = - new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { - deserialized = ois.readObject(); - } - assertThat(deserialized).isSameAs(SavepointKeyFilter.empty()); - } - - // ------------------------------------------------------------------------- - // Exact key filter — single-value factory - // ------------------------------------------------------------------------- - - @Test - void exactSingleValueFactory() { - SavepointKeyFilter filter = SavepointKeyFilter.exact(42L); - assertThat(filter.getExactKeys()).containsExactly(42L); - assertThat(filter.test(42L)).isTrue(); - assertThat(filter.test(43L)).isFalse(); - } - // ------------------------------------------------------------------------- // AND with 3+ children // ------------------------------------------------------------------------- @@ -504,7 +387,7 @@ void andOfThreeRangesProducesIntersection() { gte(longKeyRef(), longLit(3L)), lte(longKeyRef(), longLit(20L)), lt(longKeyRef(), longLit(10L))); - SavepointKeyFilter filter = keyFilterOf(expr); + SavepointKeyFilterPlan filter = keyFilterOf(expr); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); @@ -522,7 +405,7 @@ void andOfThreeRangesProducesIntersection() { @Test void orWithSingleChild_returnsExactFilter() { CallExpression expr = or(eq(longKeyRef(), longLit(7L))); - SavepointKeyFilter filter = keyFilterOf(expr); + SavepointKeyFilterPlan filter = keyFilterOf(expr); assertNotNull(filter); assertThat(filter.getExactKeys()).containsExactly(7L); @@ -550,7 +433,7 @@ void applyWithEmptyThenRange_returnsEmpty() { and(gt(longKeyRef(), longLit(10L)), lt(longKeyRef(), longLit(5L))), lte(longKeyRef(), longLit(10L))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -566,7 +449,7 @@ void applyWithRangeThenEmpty_returnsEmpty() { lte(longKeyRef(), longLit(10L)), and(gt(longKeyRef(), longLit(10L)), lt(longKeyRef(), longLit(5L)))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -582,7 +465,7 @@ void applyWithEmptyThenExact_returnsEmpty() { and(gt(longKeyRef(), longLit(10L)), lt(longKeyRef(), longLit(5L))), eq(longKeyRef(), longLit(1L))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -596,7 +479,7 @@ void applyWithConflictingExactPredicates_returnsEmptyFilter() { apply( List.of(eq(longKeyRef(), longLit(1L)), eq(longKeyRef(), longLit(2L))), LONG_KEY_TYPE); - SavepointKeyFilter result = applied.keyFilter(); + SavepointKeyFilterPlan result = applied.keyFilter(); assertNotNull(result); assertThat(applied.accepted()).hasSize(2); @@ -662,7 +545,7 @@ void equalsWithWrongArgCount_returnsNull() { @Test void equalsWithIntLiteralIsWidenedToBigintKeyAndPushed() { ValueLiteralExpression intLit = new ValueLiteralExpression(5, DataTypes.INT().notNull()); - SavepointKeyFilter filter = keyFilterOf(eq(longKeyRef(), intLit)); + SavepointKeyFilterPlan filter = keyFilterOf(eq(longKeyRef(), intLit)); assertNotNull(filter); assertThat(filter.getExactKeys()).containsExactly(5L); @@ -674,7 +557,7 @@ void equalsWithIntLiteralIsWidenedToBigintKeyAndPushed() { void betweenWithIntLiteralBoundsIsWidenedToBigintKeyAndPushed() { ValueLiteralExpression lower = new ValueLiteralExpression(1, DataTypes.INT().notNull()); ValueLiteralExpression upper = new ValueLiteralExpression(10, DataTypes.INT().notNull()); - SavepointKeyFilter filter = keyFilterOf(between(longKeyRef(), lower, upper)); + SavepointKeyFilterPlan filter = keyFilterOf(between(longKeyRef(), lower, upper)); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); @@ -690,7 +573,7 @@ void equalsWithIntLiteralIsWidenedToDoubleKeyAndPushed() { new FieldReferenceExpression("key", DataTypes.DOUBLE().notNull(), 0, KEY_COL); ValueLiteralExpression intLit = new ValueLiteralExpression(5, DataTypes.INT().notNull()); - SavepointKeyFilter filter = keyFilterOf(eq(doubleKey, intLit)); + SavepointKeyFilterPlan filter = keyFilterOf(eq(doubleKey, intLit)); assertNotNull(filter); assertThat(filter.getExactKeys()).containsExactly(5.0d); @@ -718,7 +601,7 @@ void decimalKeyEqualityIsPushedDownPreservingLiteralScale() { new ValueLiteralExpression( new BigDecimal("5.00"), DataTypes.DECIMAL(10, 2).notNull()); - SavepointKeyFilter filter = keyFilterOf(eq(decKey, lit)); + SavepointKeyFilterPlan filter = keyFilterOf(eq(decKey, lit)); assertNotNull(filter); // Literal scale is preserved, so exact matching is scale sensitive: 5.00 matches, 5.0 not. @@ -731,7 +614,7 @@ void decimalKeyEqualityIsPushedDownPreservingLiteralScale() { // Expression helpers // ------------------------------------------------------------------------- - private static SavepointKeyFilter keyFilterOf(ResolvedExpression expr) { + private static SavepointKeyFilterPlan keyFilterOf(ResolvedExpression expr) { DataType keyType = findKeyType(expr); return apply(Collections.singletonList(expr), keyType).keyFilter(); } diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlanTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlanTest.java new file mode 100644 index 00000000000000..3e2105ad8fb664 --- /dev/null +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlanTest.java @@ -0,0 +1,155 @@ +/* + * 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.state.table.filter; + +import org.apache.flink.state.api.filter.SavepointKeyFilter; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Collections; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class SavepointKeyFilterPlanTest { + + // ------------------------------------------------------------------------- + // Range intersection + // ------------------------------------------------------------------------- + + @Test + void intersectNarrowsBounds() { + // [5, ∞) ∩ (-∞, 10] = [5, 10] + SavepointKeyFilterPlan lower = SavepointKeyFilterPlan.range(5L, true, null, true); + SavepointKeyFilterPlan upper = SavepointKeyFilterPlan.range(null, true, 10L, true); + SavepointKeyFilterPlan result = lower.intersect(upper); + + assertThat(result.isEmpty()).isFalse(); + assertThat(result.getExactKeys()).isNull(); + assertThat(result.test(4L)).isFalse(); + assertThat(result.test(5L)).isTrue(); + assertThat(result.test(10L)).isTrue(); + assertThat(result.test(11L)).isFalse(); + } + + @Test + void intersectDisjointRangesReturnsEmpty() { + // [10, ∞) ∩ (-∞, 5] — disjoint + SavepointKeyFilterPlan a = SavepointKeyFilterPlan.range(10L, true, null, true); + SavepointKeyFilterPlan b = SavepointKeyFilterPlan.range(null, true, 5L, true); + assertThat(a.intersect(b).isEmpty()).isTrue(); + } + + @Test + void intersectEqualBoundsInclusiveIsNonEmpty() { + // [7, ∞) ∩ (-∞, 7] = [7, 7] + SavepointKeyFilterPlan a = SavepointKeyFilterPlan.range(7L, true, null, true); + SavepointKeyFilterPlan b = SavepointKeyFilterPlan.range(null, true, 7L, true); + SavepointKeyFilterPlan result = a.intersect(b); + assertThat(result.isEmpty()).isFalse(); + assertThat(result.test(7L)).isTrue(); + assertThat(result.test(6L)).isFalse(); + assertThat(result.test(8L)).isFalse(); + } + + @Test + void intersectEqualBoundsOneExclusiveIsEmpty() { + // (7, ∞) ∩ (-∞, 7] — empty because lower is exclusive + SavepointKeyFilterPlan a = SavepointKeyFilterPlan.range(7L, false, null, true); + SavepointKeyFilterPlan b = SavepointKeyFilterPlan.range(null, true, 7L, true); + assertThat(a.intersect(b).isEmpty()).isTrue(); + } + + // ------------------------------------------------------------------------- + // Custom comparator + // ------------------------------------------------------------------------- + + @Test + void rangeWithCustomComparatorIsUsed() { + // Orders strings by length — clearly not the natural String order. + SavepointKeyFilter filter = + SavepointKeyFilterPlan.range( + "aa", + true, + "cccc", + true, + (a, b) -> Integer.compare(a.length(), b.length())); + + // Length in [2, 4]: "abc" (3), passes; "a" (1) and "ccccc" (5), fail. + assertThat(filter.test("abc")).isTrue(); + assertThat(filter.test("a")).isFalse(); + assertThat(filter.test("ccccc")).isFalse(); + } + + // ------------------------------------------------------------------------- + // Empty key filter + // ------------------------------------------------------------------------- + + @Test + void emptyKeyFilter_rejectsEverything() { + // empty -> matches nothing, of any key type + SavepointKeyFilterPlan empty = SavepointKeyFilterPlan.empty(); + assertThat(empty.isEmpty()).isTrue(); + assertThat(empty.getExactKeys()).isEmpty(); + assertThat(empty.test(42L)).isFalse(); + assertThat(empty.test("hello")).isFalse(); + } + + @Test + void exactWithEmptySetReturnsEmptyKeyFilter() { + // exact({}) -> collapses to empty + SavepointKeyFilterPlan filter = + SavepointKeyFilterPlan.exact(Collections.emptySet()); + assertThat(filter.isEmpty()).isTrue(); + } + + @Test + void emptyKeyFilterSingletonPreservedAcrossSerialization() throws Exception { + // empty is a singleton, so readResolve must return the same instance + SavepointKeyFilterPlan original = SavepointKeyFilterPlan.empty(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(original); + } + Object deserialized; + try (ObjectInputStream ois = + new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + deserialized = ois.readObject(); + } + assertThat(deserialized).isSameAs(SavepointKeyFilterPlan.empty()); + } + + // ------------------------------------------------------------------------- + // Exact key filter — single-value factory + // ------------------------------------------------------------------------- + + @Test + void exactSingleValueFactory() { + // exact(42) -> {42} + SavepointKeyFilter filter = SavepointKeyFilter.exact(42L); + assertThat(filter.getExactKeys()).containsExactly(42L); + assertThat(filter.test(42L)).isTrue(); + assertThat(filter.test(43L)).isFalse(); + } + +} From 27f72a2c28dbd7a4a79462b8a706050621d5e05e Mon Sep 17 00:00:00 2001 From: Ilya Soin Date: Sun, 16 Aug 2026 23:49:39 +0300 Subject: [PATCH 2/3] [FLINK-40399][tests] Cover remaining key filter push-down paths Adds cases for savepoint key filter push-down behaviour that had no test, most importantly that predicates which cannot be pushed are returned in remaining() so the runtime still evaluates them. Nothing covered that before: every apply() case asserted remaining() was empty, so a regression there would have silently dropped rows. Also covered: an untranslatable child aborting the whole AND/OR, "key = NULL" whose literal has no readable value, non-Comparable literals on a BYTES key, the two comparison flip directions that were missing, comparison arity, and at SQL level the upper-bound predicates, which had no end-to-end test at all. Finally, every test states the predicate under test as a leading comment, so the SQL shape being exercised is readable without decoding the expression builders. --- .../SavepointDynamicTableSourceTest.java | 77 ++++++++ .../table/SavepointFilterTranslatorTest.java | 166 +++++++++++++++++- 2 files changed, 242 insertions(+), 1 deletion(-) diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointDynamicTableSourceTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointDynamicTableSourceTest.java index e2a8ca574d0ce1..a257e516445e3f 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointDynamicTableSourceTest.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointDynamicTableSourceTest.java @@ -394,6 +394,32 @@ void testOrAcrossKeyAndNonKeyColumnIsNotPushedDownButReturnsCorrectResult() thro assertThat(result.get(1).getField("k")).isEqualTo(5L); } + @Test + void testOrOfExactAndRangeOnKeyIsNotPushedDownButReturnsCorrectResult() throws Exception { + // The planner hands this over intact as or(equals(k, 1), greaterThan(k, 5)), but OR only + // merges finite key sets, so a range branch makes the whole disjunction non-pushable. + StreamTableEnvironment tEnv = createBatchTableEnv(); + tEnv.executeSql(STATE_TABLE_DDL); + + String sql = "SELECT k FROM state_table WHERE k = 1 OR k > 5 ORDER BY k"; + + assertThat(hasPushedDownFilter(tEnv, sql)).isFalse(); + assertThat(collectKeys(tEnv, sql)).containsExactly(1L, 6L, 7L, 8L, 9L); + } + + @Test + void testOrOfTwoRangesOnKeyIsNotPushedDownButReturnsCorrectResult() throws Exception { + // Same limitation for "outside a range". This is also the shape the planner produces + // when it expands a Sarg, which is why a range combined with <> is not pushed either. + StreamTableEnvironment tEnv = createBatchTableEnv(); + tEnv.executeSql(STATE_TABLE_DDL); + + String sql = "SELECT k FROM state_table WHERE k < 2 OR k > 7 ORDER BY k"; + + assertThat(hasPushedDownFilter(tEnv, sql)).isFalse(); + assertThat(collectKeys(tEnv, sql)).containsExactly(0L, 1L, 8L, 9L); + } + @Test void testUnsupportedFilterIsNotPushedDownButReturnsCorrectResult() throws Exception { StreamTableEnvironment tEnv = createBatchTableEnv(); @@ -410,10 +436,61 @@ void testUnsupportedFilterIsNotPushedDownButReturnsCorrectResult() throws Except assertThat(keys).containsExactly(0L, 2L, 4L, 6L, 8L); } + @Test + void testFilterPushDownUpperBoundReturnsCorrectResult() throws Exception { + StreamTableEnvironment tEnv = createBatchTableEnv(); + tEnv.executeSql(STATE_TABLE_DDL); + + String sql = "SELECT k FROM state_table WHERE k < 3 ORDER BY k"; + + assertThat(hasPushedDownFilter(tEnv, sql)).isTrue(); + assertThat(collectKeys(tEnv, sql)).containsExactly(0L, 1L, 2L); + } + + @Test + void testFilterPushDownStrictLowerBoundReturnsCorrectResult() throws Exception { + StreamTableEnvironment tEnv = createBatchTableEnv(); + tEnv.executeSql(STATE_TABLE_DDL); + + String sql = "SELECT k FROM state_table WHERE k > 7 ORDER BY k"; + + assertThat(hasPushedDownFilter(tEnv, sql)).isTrue(); + assertThat(collectKeys(tEnv, sql)).containsExactly(8L, 9L); + } + + @Test + void testFilterPushDownIntersectingRangesReturnsCorrectResult() throws Exception { + StreamTableEnvironment tEnv = createBatchTableEnv(); + tEnv.executeSql(STATE_TABLE_DDL); + + String sql = "SELECT k FROM state_table WHERE k >= 3 AND k <= 6 ORDER BY k"; + + assertThat(hasPushedDownFilter(tEnv, sql)).isTrue(); + assertThat(collectKeys(tEnv, sql)).containsExactly(3L, 4L, 5L, 6L); + } + + @Test + void testFilterPushDownComparisonWithLiteralOnLeftSide() throws Exception { + // verify that "5 < k" (literal on the left) works the same as "k > 5". + StreamTableEnvironment tEnv = createBatchTableEnv(); + tEnv.executeSql(STATE_TABLE_DDL); + + String sql = "SELECT k FROM state_table WHERE 5 < k ORDER BY k"; + + assertThat(hasPushedDownFilter(tEnv, sql)).isTrue(); + assertThat(collectKeys(tEnv, sql)).containsExactly(6L, 7L, 8L, 9L); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- + private static List collectKeys(StreamTableEnvironment tEnv, String sql) throws Exception { + return tEnv.toDataStream(tEnv.sqlQuery(sql)).executeAndCollect(100).stream() + .map(r -> (Long) r.getField("k")) + .collect(Collectors.toList()); + } + private static StreamTableEnvironment createBatchTableEnv() { Configuration config = new Configuration(); config.set(RUNTIME_MODE, RuntimeExecutionMode.BATCH); diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java index 3fcb2792d4ed5f..09610146b4fcce 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java @@ -49,6 +49,7 @@ class SavepointFilterTranslatorTest { @Test void equalsKeyOnLeft() { + // key = 42 -> {42} SavepointKeyFilterPlan filter = keyFilterOf(eq(longKeyRef(), longLit(42L))); assertNotNull(filter); assertThat(filter.getExactKeys()).containsExactly(42L); @@ -58,6 +59,7 @@ void equalsKeyOnLeft() { @Test void equalsKeyOnRight() { + // 42 = key -> {42} SavepointKeyFilterPlan filter = keyFilterOf(eq(longLit(42L), longKeyRef())); assertNotNull(filter); assertThat(filter.getExactKeys()).containsExactly(42L); @@ -67,12 +69,14 @@ void equalsKeyOnRight() { @Test void equalsNeitherSideIsKeyColumn_returnsNull() { + // val = 42 -> not the key column, not pushed SavepointKeyFilterPlan filter = keyFilterOf(eq(otherRef(), longLit(42L))); assertThat(filter).isNull(); } @Test void equalsNeitherSideIsLiteral_returnsNull() { + // key = val -> no literal to match, not pushed SavepointKeyFilterPlan filter = keyFilterOf(eq(longKeyRef(), otherRef())); assertThat(filter).isNull(); } @@ -83,6 +87,7 @@ void equalsNeitherSideIsLiteral_returnsNull() { @Test void orOfEqualsProducesMergedExactFilter() { + // key = 1 OR key = 2 OR 3 = key -> {1, 2, 3} CallExpression expr = or( eq(longKeyRef(), longLit(1L)), @@ -108,6 +113,7 @@ void orWithNonPushableChild_returnsNull() { @Test void betweenProducesInclusiveRange() { + // key BETWEEN 10 AND 20 -> [10, 20] SavepointKeyFilterPlan filter = keyFilterOf(between(longKeyRef(), longLit(10L), longLit(20L))); @@ -122,6 +128,7 @@ void betweenProducesInclusiveRange() { @Test void betweenWithNonKeyField_returnsNull() { + // val BETWEEN 1 AND 10 -> not the key column, not pushed SavepointKeyFilterPlan filter = keyFilterOf(between(otherRef(), longLit(1L), longLit(10L))); assertThat(filter).isNull(); @@ -133,6 +140,7 @@ void betweenWithNonKeyField_returnsNull() { @Test void greaterThanProducesExclusiveLowerBound() { + // key > 5 -> (5, +∞) SavepointKeyFilterPlan filter = keyFilterOf(gt(longKeyRef(), longLit(5L))); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); @@ -142,6 +150,7 @@ void greaterThanProducesExclusiveLowerBound() { @Test void greaterThanOrEqualProducesInclusiveLowerBound() { + // key >= 5 -> [5, +∞) SavepointKeyFilterPlan filter = keyFilterOf(gte(longKeyRef(), longLit(5L))); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); @@ -152,6 +161,7 @@ void greaterThanOrEqualProducesInclusiveLowerBound() { @Test void lessThanProducesExclusiveUpperBound() { + // key < 10 -> (-∞, 10) SavepointKeyFilterPlan filter = keyFilterOf(lt(longKeyRef(), longLit(10L))); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); @@ -161,6 +171,7 @@ void lessThanProducesExclusiveUpperBound() { @Test void lessThanOrEqualProducesInclusiveUpperBound() { + // key <= 10 -> (-∞, 10] SavepointKeyFilterPlan filter = keyFilterOf(lte(longKeyRef(), longLit(10L))); assertNotNull(filter); assertThat(filter.getExactKeys()).isNull(); @@ -221,18 +232,19 @@ void andWithExactKeyChildIsNotPushable() { CallExpression expr = and(eq(longKeyRef(), longLit(5L)), gt(longKeyRef(), longLit(3L))); assertThat(keyFilterOf(expr)).isNull(); } - // ------------------------------------------------------------------------- // Unsupported predicates // ------------------------------------------------------------------------- @Test void nonCallExpressionReturnsNull() { + // A bare field reference is not a predicate, not pushed assertThat(keyFilterOf(longKeyRef())).isNull(); } @Test void unrecognizedFunctionReturnsNull() { + // key IS NULL -> unsupported function, not pushed CallExpression isNull = CallExpression.permanent( BuiltInFunctionDefinitions.IS_NULL, @@ -247,6 +259,7 @@ void unrecognizedFunctionReturnsNull() { @Test void rangeFilterOnStringKey() { + // key BETWEEN 'beta' AND 'delta' -> natural String order SavepointKeyFilterPlan filter = keyFilterOf(between(stringKeyRef(), stringLit("beta"), stringLit("delta"))); @@ -260,6 +273,7 @@ void rangeFilterOnStringKey() { @Test void rangeFilterWithDoubleComparison() { + // key BETWEEN 1.5 AND 3.5 on a FLOAT key -> [1.5, 3.5] ValueLiteralExpression floatLower = new ValueLiteralExpression(1.5f, DataTypes.FLOAT().notNull()); ValueLiteralExpression floatUpper = @@ -284,6 +298,7 @@ void rangeFilterWithDoubleComparison() { @Test void applyAccumulatesRangeAndRange() { + // key >= 3, key <= 8 -> [3, 8] SavepointFilterTranslator.Result applied = apply( List.of(gte(longKeyRef(), longLit(3L)), lte(longKeyRef(), longLit(8L))), @@ -301,6 +316,7 @@ void applyAccumulatesRangeAndRange() { @Test void applyAccumulatesExactAndExact() { + // key IN (1, 2, 3), key IN (2, 3, 4) -> {2, 3} SavepointFilterTranslator.Result applied = apply( List.of( @@ -323,6 +339,7 @@ void applyAccumulatesExactAndExact() { @Test void applyAccumulatesExactAndExactEmptyResult_matchesNothing() { + // key = 1, key = 2 -> disjoint, so nothing matches SavepointFilterTranslator.Result applied = apply( List.of(eq(longKeyRef(), longLit(1L)), eq(longKeyRef(), longLit(2L))), @@ -337,6 +354,7 @@ void applyAccumulatesExactAndExactEmptyResult_matchesNothing() { @Test void applyAccumulatesExactAndRange_keepsOnlyKeysInRange() { + // key IN (1, 5, 10, 15), key BETWEEN 4 AND 12 -> {5, 10} SavepointFilterTranslator.Result applied = apply( List.of( @@ -357,6 +375,7 @@ void applyAccumulatesExactAndRange_keepsOnlyKeysInRange() { @Test void applyAccumulatesRangeAndExact_keepsOnlyKeysInRange() { + // key BETWEEN 4 AND 12, key IN (1, 5, 10, 15) -> {5, 10}, operands swapped SavepointFilterTranslator.Result applied = apply( List.of( @@ -404,6 +423,7 @@ void andOfThreeRangesProducesIntersection() { @Test void orWithSingleChild_returnsExactFilter() { + // OR (key = 7) -> {7} CallExpression expr = or(eq(longKeyRef(), longLit(7L))); SavepointKeyFilterPlan filter = keyFilterOf(expr); @@ -417,6 +437,7 @@ void orWithSingleChild_returnsExactFilter() { @Test void comparisonWithNeitherSideBeingKeyColumn_returnsNull() { + // val > 5 and 5 < val -> not the key column, not pushed assertThat(keyFilterOf(gt(otherRef(), longLit(5L)))).isNull(); assertThat(keyFilterOf(lt(longLit(5L), otherRef()))).isNull(); } @@ -427,6 +448,7 @@ void comparisonWithNeitherSideBeingKeyColumn_returnsNull() { @Test void applyWithEmptyThenRange_returnsEmpty() { + // (key > 10 AND key < 5) -> empty, key <= 10 -> empty absorbs the range SavepointFilterTranslator.Result applied = apply( List.of( @@ -443,6 +465,7 @@ void applyWithEmptyThenRange_returnsEmpty() { @Test void applyWithRangeThenEmpty_returnsEmpty() { + // key <= 10, (key > 10 AND key < 5) -> empty, operands swapped SavepointFilterTranslator.Result applied = apply( List.of( @@ -459,6 +482,7 @@ void applyWithRangeThenEmpty_returnsEmpty() { @Test void applyWithEmptyThenExact_returnsEmpty() { + // (key > 10 AND key < 5) -> empty, key = 1 -> empty absorbs the exact set SavepointFilterTranslator.Result applied = apply( List.of( @@ -475,6 +499,7 @@ void applyWithEmptyThenExact_returnsEmpty() { @Test void applyWithConflictingExactPredicates_returnsEmptyFilter() { + // key = 1, key = 2 -> disjoint, so nothing matches SavepointFilterTranslator.Result applied = apply( List.of(eq(longKeyRef(), longLit(1L)), eq(longKeyRef(), longLit(2L))), @@ -493,6 +518,7 @@ void applyWithConflictingExactPredicates_returnsEmptyFilter() { @Test void betweenWithWrongArgCount_returnsNull() { + // BETWEEN with two children is malformed, not pushed CallExpression malformed = CallExpression.permanent( BuiltInFunctionDefinitions.BETWEEN, @@ -507,6 +533,7 @@ void betweenWithWrongArgCount_returnsNull() { @Test void betweenWithNonLiteralBound_returnsNull() { + // key BETWEEN val AND 10 -> non-literal bound, not pushed CallExpression expr = CallExpression.permanent( BuiltInFunctionDefinitions.BETWEEN, @@ -521,6 +548,7 @@ void betweenWithNonLiteralBound_returnsNull() { @Test void comparisonWithNonLiteralValue_returnsNull() { + // key > val -> no literal bound, not pushed assertThat(keyFilterOf(gt(longKeyRef(), otherRef()))).isNull(); } @@ -530,6 +558,7 @@ void comparisonWithNonLiteralValue_returnsNull() { @Test void equalsWithWrongArgCount_returnsNull() { + // EQUALS with one child is malformed, not pushed CallExpression malformed = CallExpression.permanent( BuiltInFunctionDefinitions.EQUALS, @@ -544,6 +573,7 @@ void equalsWithWrongArgCount_returnsNull() { @Test void equalsWithIntLiteralIsWidenedToBigintKeyAndPushed() { + // key = 5 (INT literal, BIGINT key) -> {5L} ValueLiteralExpression intLit = new ValueLiteralExpression(5, DataTypes.INT().notNull()); SavepointKeyFilterPlan filter = keyFilterOf(eq(longKeyRef(), intLit)); @@ -555,6 +585,7 @@ void equalsWithIntLiteralIsWidenedToBigintKeyAndPushed() { @Test void betweenWithIntLiteralBoundsIsWidenedToBigintKeyAndPushed() { + // key BETWEEN 1 AND 10 (INT literals, BIGINT key) -> [1L, 10L] ValueLiteralExpression lower = new ValueLiteralExpression(1, DataTypes.INT().notNull()); ValueLiteralExpression upper = new ValueLiteralExpression(10, DataTypes.INT().notNull()); SavepointKeyFilterPlan filter = keyFilterOf(between(longKeyRef(), lower, upper)); @@ -569,6 +600,7 @@ void betweenWithIntLiteralBoundsIsWidenedToBigintKeyAndPushed() { @Test void equalsWithIntLiteralIsWidenedToDoubleKeyAndPushed() { + // key = 5 (INT literal, DOUBLE key) -> {5.0} FieldReferenceExpression doubleKey = new FieldReferenceExpression("key", DataTypes.DOUBLE().notNull(), 0, KEY_COL); ValueLiteralExpression intLit = new ValueLiteralExpression(5, DataTypes.INT().notNull()); @@ -583,11 +615,13 @@ void equalsWithIntLiteralIsWidenedToDoubleKeyAndPushed() { @Test void nonNumericLiteralAgainstNumericKeyIsNotWidenedAndNotPushed() { + // key = '5' (STRING literal, BIGINT key) -> no widening, not pushed assertThat(keyFilterOf(eq(longKeyRef(), stringLit("5")))).isNull(); } @Test void numericLiteralWithNonWidenableKeyTypeIsNotPushed() { + // key = 5L (BIGINT literal, INT key) -> narrowing is unsafe, not pushed FieldReferenceExpression intKey = new FieldReferenceExpression("key", DataTypes.INT().notNull(), 0, KEY_COL); assertThat(keyFilterOf(eq(intKey, longLit(5L)))).isNull(); @@ -595,6 +629,7 @@ void numericLiteralWithNonWidenableKeyTypeIsNotPushed() { @Test void decimalKeyEqualityIsPushedDownPreservingLiteralScale() { + // key = 5.00 on a DECIMAL(10, 2) key -> {5.00} FieldReferenceExpression decKey = new FieldReferenceExpression("key", DataTypes.DECIMAL(10, 2).notNull(), 0, KEY_COL); ValueLiteralExpression lit = @@ -610,6 +645,116 @@ void decimalKeyEqualityIsPushedDownPreservingLiteralScale() { assertThat(filter.test(new BigDecimal("5.0"))).isFalse(); } + // ------------------------------------------------------------------------- + // apply — predicates that cannot be pushed stay in remaining() + // ------------------------------------------------------------------------- + + @Test + void applyKeepsNonPushablePredicatesInRemaining() { + // key = 5, key IS NULL -> only the first is pushed, the second must still be evaluated + SavepointFilterTranslator.Result applied = + apply(List.of(eq(longKeyRef(), longLit(5L)), isNull(longKeyRef())), LONG_KEY_TYPE); + SavepointKeyFilterPlan result = applied.keyFilter(); + + assertNotNull(result); + assertThat(applied.accepted()).hasSize(1); + assertThat(applied.remaining()).hasSize(1); + assertThat(result.getExactKeys()).containsExactly(5L); + } + + @Test + void applyWithOnlyNonPushablePredicateReturnsNoKeyFilter() { + // key IS NULL -> nothing to push, the predicate is handed back untouched + SavepointFilterTranslator.Result applied = + apply(List.of(isNull(longKeyRef())), LONG_KEY_TYPE); + + assertThat(applied.keyFilter()).isNull(); + assertThat(applied.accepted()).isEmpty(); + assertThat(applied.remaining()).hasSize(1); + } + + @Test + void applyWithNoPredicatesReturnsNoKeyFilter() { + // no predicates -> no filter, so the scan is not pruned at all + SavepointFilterTranslator.Result applied = apply(List.of(), LONG_KEY_TYPE); + + assertThat(applied.keyFilter()).isNull(); + assertThat(applied.accepted()).isEmpty(); + assertThat(applied.remaining()).isEmpty(); + } + + // ------------------------------------------------------------------------- + // Untranslatable children break their parent + // ------------------------------------------------------------------------- + + @Test + void andWithUntranslatableChild_returnsNull() { + // key > 5 AND key IS NULL -> the whole AND must not be pushed + assertThat(keyFilterOf(and(gt(longKeyRef(), longLit(5L)), isNull(longKeyRef())))).isNull(); + } + + @Test + void orWithUntranslatableChild_returnsNull() { + // key = 1 OR key IS NULL -> the whole OR must not be pushed + assertThat(keyFilterOf(or(eq(longKeyRef(), longLit(1L)), isNull(longKeyRef())))).isNull(); + } + + // ------------------------------------------------------------------------- + // Literals that cannot be read + // ------------------------------------------------------------------------- + + @Test + void equalsWithNullLiteral_returnsNull() { + // key = NULL -> the literal has no readable value, not pushed + assertThat(keyFilterOf(eq(longKeyRef(), nullLit()))).isNull(); + } + + @Test + void betweenWithNonComparableLiteral_returnsNull() { + // key BETWEEN x'01' AND x'02' on a BYTES key -> byte[] is not Comparable, not pushed + assertThat(keyFilterOf(between(bytesKeyRef(), bytesLit((byte) 1), bytesLit((byte) 2)))) + .isNull(); + } + + @Test + void comparisonWithNonComparableLiteral_returnsNull() { + // key > x'01' on a BYTES key -> byte[] is not Comparable, not pushed + assertThat(keyFilterOf(gt(bytesKeyRef(), bytesLit((byte) 1)))).isNull(); + } + + // ------------------------------------------------------------------------- + // Comparison — remaining flip directions and arity + // ------------------------------------------------------------------------- + + @Test + void comparisonWithLiteralOnLeft_gte_flipsDirection() { + // 10 >= key -> key <= 10 -> upper bound (inclusive) + SavepointKeyFilterPlan filter = keyFilterOf(gte(longLit(10L), longKeyRef())); + assertNotNull(filter); + assertThat(filter.test(10L)).isTrue(); + assertThat(filter.test(11L)).isFalse(); + } + + @Test + void comparisonWithLiteralOnLeft_lt_flipsDirection() { + // 5 < key -> key > 5 -> lower bound (exclusive) + SavepointKeyFilterPlan filter = keyFilterOf(lt(longLit(5L), longKeyRef())); + assertNotNull(filter); + assertThat(filter.test(5L)).isFalse(); + assertThat(filter.test(6L)).isTrue(); + } + + @Test + void comparisonWithWrongArgCount_returnsNull() { + // GREATER_THAN with one child is malformed, not pushed + CallExpression malformed = + CallExpression.permanent( + BuiltInFunctionDefinitions.GREATER_THAN, + Collections.singletonList(longKeyRef()), + DataTypes.BOOLEAN()); + assertThat(keyFilterOf(malformed)).isNull(); + } + // ------------------------------------------------------------------------- // Expression helpers // ------------------------------------------------------------------------- @@ -663,6 +808,25 @@ private static CallExpression eq(ResolvedExpression left, ResolvedExpression rig BuiltInFunctionDefinitions.EQUALS, Arrays.asList(left, right), DataTypes.BOOLEAN()); } + private static CallExpression isNull(ResolvedExpression arg) { + return CallExpression.permanent( + BuiltInFunctionDefinitions.IS_NULL, + Collections.singletonList(arg), + DataTypes.BOOLEAN()); + } + + private static ValueLiteralExpression nullLit() { + return new ValueLiteralExpression(null, DataTypes.BIGINT().nullable()); + } + + private static FieldReferenceExpression bytesKeyRef() { + return new FieldReferenceExpression("key", DataTypes.BYTES().notNull(), 0, KEY_COL); + } + + private static ValueLiteralExpression bytesLit(byte value) { + return new ValueLiteralExpression(new byte[] {value}, DataTypes.BYTES().notNull()); + } + private static CallExpression or(ResolvedExpression... args) { return CallExpression.permanent( BuiltInFunctionDefinitions.OR, Arrays.asList(args), DataTypes.BOOLEAN()); From a2713e951d8c2f1ff79ac3fbe6567257b687996e Mon Sep 17 00:00:00 2001 From: Ilya Soin Date: Mon, 17 Aug 2026 01:26:00 +0300 Subject: [PATCH 3/3] [FLINK-40399][state] spotless --- .../state/api/filter/RangeKeyFilter.java | 2 +- .../api/input/KeyedStateInputFormatTest.java | 24 +++++++++++++++++++ .../SavepointDynamicTableSourceTest.java | 3 ++- .../table/SavepointFilterTranslatorTest.java | 1 + .../filter/SavepointKeyFilterPlanTest.java | 4 +--- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/RangeKeyFilter.java b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/RangeKeyFilter.java index 22f99c3e1b339e..ccef5909fa2b7b 100644 --- a/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/RangeKeyFilter.java +++ b/flink-libraries/flink-state-processing-api/src/main/java/org/apache/flink/state/api/filter/RangeKeyFilter.java @@ -23,7 +23,7 @@ /** A filter based on a range with an injected comparator. */ final class RangeKeyFilter implements SavepointKeyFilter { - private static final long serialVersionUID = 3L; + private static final long serialVersionUID = 4L; private final SerializableComparator comparator; @Nullable private final K lower; diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/input/KeyedStateInputFormatTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/input/KeyedStateInputFormatTest.java index 2b9cb85126d785..b1183f6bb9116a 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/input/KeyedStateInputFormatTest.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/api/input/KeyedStateInputFormatTest.java @@ -55,6 +55,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; @@ -136,6 +137,29 @@ void testExactKeyFilterPrunesInputSplits(boolean asyncState) throws Exception { .hasSize(1); } + @ParameterizedTest(name = "Enable async state = {0}") + @ValueSource(booleans = {false, true}) + void testEmptyExactFilterProducesNoInputSplits(boolean asyncState) throws Exception { + OperatorID operatorID = OperatorIDGenerator.fromUid("uid"); + + OperatorSubtaskState state = + createOperatorSubtaskState(createFlatMap(asyncState), asyncState); + OperatorState operatorState = new OperatorState(null, null, operatorID, 1, 128); + operatorState.putState(0, state); + + KeyedStateInputFormat format = + new KeyedStateInputFormat<>( + operatorState, + new HashMapStateBackend(), + new Configuration(), + new KeyedStateReaderOperator<>(new ReaderFunction(), Types.INT), + new ExecutionConfig(), + SavepointKeyFilter.exact(Collections.emptySet())); + KeyGroupRangeInputSplit[] splits = format.createInputSplits(10); + + assertThat(splits).as("A filter matching no key leaves nothing to read").isEmpty(); + } + @ParameterizedTest(name = "Enable async state = {0}") @ValueSource(booleans = {false, true}) void testRangeFilterDoesNotPruneInputSplits(boolean asyncState) throws Exception { diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointDynamicTableSourceTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointDynamicTableSourceTest.java index a257e516445e3f..47277d5cd6e130 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointDynamicTableSourceTest.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointDynamicTableSourceTest.java @@ -485,7 +485,8 @@ void testFilterPushDownComparisonWithLiteralOnLeftSide() throws Exception { // Helpers // ------------------------------------------------------------------------- - private static List collectKeys(StreamTableEnvironment tEnv, String sql) throws Exception { + private static List collectKeys(StreamTableEnvironment tEnv, String sql) + throws Exception { return tEnv.toDataStream(tEnv.sqlQuery(sql)).executeAndCollect(100).stream() .map(r -> (Long) r.getField("k")) .collect(Collectors.toList()); diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java index 09610146b4fcce..6263d83ceb56f5 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/SavepointFilterTranslatorTest.java @@ -232,6 +232,7 @@ void andWithExactKeyChildIsNotPushable() { CallExpression expr = and(eq(longKeyRef(), longLit(5L)), gt(longKeyRef(), longLit(3L))); assertThat(keyFilterOf(expr)).isNull(); } + // ------------------------------------------------------------------------- // Unsupported predicates // ------------------------------------------------------------------------- diff --git a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlanTest.java b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlanTest.java index 3e2105ad8fb664..b57bf7ad31637e 100644 --- a/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlanTest.java +++ b/flink-libraries/flink-state-processing-api/src/test/java/org/apache/flink/state/table/filter/SavepointKeyFilterPlanTest.java @@ -27,7 +27,6 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Collections; -import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -133,7 +132,7 @@ void emptyKeyFilterSingletonPreservedAcrossSerialization() throws Exception { } Object deserialized; try (ObjectInputStream ois = - new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { deserialized = ois.readObject(); } assertThat(deserialized).isSameAs(SavepointKeyFilterPlan.empty()); @@ -151,5 +150,4 @@ void exactSingleValueFactory() { assertThat(filter.test(42L)).isTrue(); assertThat(filter.test(43L)).isFalse(); } - }