This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new fd1e6edb09 [flink] Preserve query authorization when narrowing splits
in the Flink source (#9949)
fd1e6edb09 is described below
commit fd1e6edb093d8aa12c8b2d0c68e6aad2fd4dbc4f
Author: Jiajia Li <[email protected]>
AuthorDate: Sun Sep 20 13:48:38 2026 +0800
[flink] Preserve query authorization when narrowing splits in the Flink
source (#9949)
---
.../apache/paimon/table/ChainGroupReadTable.java | 5 +-
.../paimon/table/ChainTableFileStoreTable.java | 4 +-
.../apache/paimon/table/source/QueryAuthSplit.java | 10 -
.../org/apache/paimon/table/source/Splits.java | 77 ++++++
.../org/apache/paimon/table/source/SplitsTest.java | 167 +++++++++++++
.../source/ContinuousFileSplitEnumerator.java | 14 +-
.../source/DynamicPartitionFilteringInfo.java | 29 +++
.../flink/source/FileStoreSourceSplitReader.java | 6 +-
.../source/StaticFileStoreSplitEnumerator.java | 4 +-
.../AlignedContinuousFileSplitEnumerator.java | 3 +-
.../source/assigners/AlignedSplitAssigner.java | 18 +-
.../assigners/DynamicPartitionPruningAssigner.java | 20 +-
.../source/assigners/PreAssignSplitAssigner.java | 31 +--
.../flink/source/operator/MonitorSource.java | 11 +-
.../paimon/flink/source/operator/ReadOperator.java | 16 +-
.../apache/paimon/flink/utils/TableScanUtils.java | 8 +-
.../source/ContinuousFileSplitEnumeratorTest.java | 53 +++++
.../source/FileStoreSourceSplitReaderTest.java | 48 +++-
.../flink/source/FlinkSourceBuilderTest.java | 221 +++++++++++++++++
...ContinuousFileSplitEnumeratorQueryAuthTest.java | 145 ++++++++++++
.../source/assigners/AlignedSplitAssignerTest.java | 245 +++++++++++++++++++
.../DynamicPartitionPruningAssignerTest.java | 262 +++++++++++++++++++++
.../assigners/PreAssignSplitAssignerTest.java | 219 +++++++++++++++++
.../operator/ReadOperatorSplitShapeTest.java | 230 ++++++++++++++++++
.../paimon/flink/utils/TableScanUtilsTest.java | 116 +++++++++
25 files changed, 1887 insertions(+), 75 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/ChainGroupReadTable.java
b/paimon-core/src/main/java/org/apache/paimon/table/ChainGroupReadTable.java
index 5bfeec5129..5564260d8e 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/ChainGroupReadTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/ChainGroupReadTable.java
@@ -39,6 +39,7 @@ import org.apache.paimon.table.source.DataTableScan;
import org.apache.paimon.table.source.InnerTableRead;
import org.apache.paimon.table.source.QueryAuthSplit;
import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.table.source.TableRead;
import org.apache.paimon.types.RowType;
import org.apache.paimon.utils.ChainPartitionProjector;
@@ -623,7 +624,7 @@ public class ChainGroupReadTable extends
FallbackReadFileStoreTable {
@Override
public RecordReader<InternalRow> createReader(Split split) throws
IOException {
// fallbackRead unwraps and applies the rules itself, so it gets
the wrapper untouched.
- Split inner = QueryAuthSplit.unwrap(split);
+ Split inner = Splits.underlying(split);
if (inner instanceof ChainSplit || inner instanceof DataSplit) {
return fallbackRead.createReader(split);
}
@@ -653,7 +654,7 @@ public class ChainGroupReadTable extends
FallbackReadFileStoreTable {
"Branch scans of the same chain table returned
different authorization rules.");
authResult = next;
}
- dataSplits.add((DataSplit) QueryAuthSplit.unwrap(split));
+ dataSplits.add((DataSplit) Splits.underlying(split));
}
return dataSplits;
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/ChainTableFileStoreTable.java
b/paimon-core/src/main/java/org/apache/paimon/table/ChainTableFileStoreTable.java
index 8daed52edc..dec16c5504 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/ChainTableFileStoreTable.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/ChainTableFileStoreTable.java
@@ -29,8 +29,8 @@ import org.apache.paimon.schema.TableSchema;
import org.apache.paimon.table.source.ChainSplit;
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.InnerTableRead;
-import org.apache.paimon.table.source.QueryAuthSplit;
import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.table.source.StreamDataTableScan;
import org.apache.paimon.table.source.TableRead;
import org.apache.paimon.types.RowType;
@@ -219,7 +219,7 @@ public class ChainTableFileStoreTable extends
FallbackReadFileStoreTable {
return fallbackRead.createReader(split);
}
// Route on what the wrapper carries, so the wrapper still reaches
the read.
- Split inner = QueryAuthSplit.unwrap(split);
+ Split inner = Splits.underlying(split);
if (inner instanceof ChainSplit || inner instanceof DataSplit) {
return chainGroupRead.createReader(split);
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/QueryAuthSplit.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/QueryAuthSplit.java
index f38fbc500f..3f6106d015 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/source/QueryAuthSplit.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/source/QueryAuthSplit.java
@@ -54,16 +54,6 @@ public class QueryAuthSplit implements Split {
return split;
}
- /**
- * The split this wrapper carries, or {@code split} itself when it is not
one. Narrowing a
- * planned split to a concrete type must go through this, or the cast
fails once the table
- * carries a rule. A caller handing the split to a read passes the wrapper
on instead, or the
- * authorization never reaches the reader.
- */
- public static Split unwrap(Split split) {
- return split instanceof QueryAuthSplit ? ((QueryAuthSplit)
split).split() : split;
- }
-
@Nullable
public TableQueryAuthResult authResult() {
return authResult;
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/source/Splits.java
b/paimon-core/src/main/java/org/apache/paimon/table/source/Splits.java
new file mode 100644
index 0000000000..5a11c481ae
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/table/source/Splits.java
@@ -0,0 +1,77 @@
+/*
+ * 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.paimon.table.source;
+
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.table.FallbackReadFileStoreTable.FallbackSplit;
+
+import javax.annotation.Nullable;
+
+/** Utilities for looking at what a planned {@link Split} is built from. */
+public class Splits {
+
+ private Splits() {}
+
+ /**
+ * The split a planned one is built from, looked at through every wrapper.
One layer is not
+ * enough: an authorized fallback table hands out a {@link FallbackSplit}
holding a {@link
+ * QueryAuthSplit}.
+ *
+ * <p>Stops at a {@link DataSplit}. {@code FallbackDataSplit} is one and
wraps itself, so that
+ * condition ends the loop as much as it preserves what a plan carrying no
rules resolved to.
+ *
+ * <p>Only for reading what the split is made of. A caller handing it to a
read passes the
+ * planned split on as it is, or the authorization never reaches the
reader.
+ */
+ public static Split underlying(Split split) {
+ Split current = split;
+ while (!(current instanceof DataSplit)) {
+ if (current instanceof QueryAuthSplit) {
+ current = ((QueryAuthSplit) current).split();
+ } else if (current instanceof FallbackSplit) {
+ current = ((FallbackSplit) current).wrapped();
+ } else {
+ return current;
+ }
+ }
+ return current;
+ }
+
+ /**
+ * The authorization a planned split carries, or null when it carries
none. The companion of
+ * {@link #underlying(Split)}, which drops the wrapper the rules live on.
+ *
+ * <p>A {@code FallbackDataSplit} wraps itself, which ends the walk: it
carries no rules either.
+ */
+ @Nullable
+ public static TableQueryAuthResult authResult(Split split) {
+ Split current = split;
+ while (!(current instanceof QueryAuthSplit)) {
+ if (!(current instanceof FallbackSplit)) {
+ return null;
+ }
+ Split wrapped = ((FallbackSplit) current).wrapped();
+ if (wrapped == current) {
+ return null;
+ }
+ current = wrapped;
+ }
+ return ((QueryAuthSplit) current).authResult();
+ }
+}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/source/SplitsTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/source/SplitsTest.java
new file mode 100644
index 0000000000..3f403f5909
--- /dev/null
+++ b/paimon-core/src/test/java/org/apache/paimon/table/source/SplitsTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.paimon.table.source;
+
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FallbackReadFileStoreTable.FallbackDataSplit;
+import org.apache.paimon.table.FallbackReadFileStoreTable.FallbackSplit;
+import org.apache.paimon.table.FallbackReadFileStoreTable.FallbackSplitImpl;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+import static org.apache.paimon.io.DataFileTestUtils.newFile;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link Splits}. */
+public class SplitsTest {
+
+ @Test
+ public void testDataSplitIsItsOwnUnderlyingSplit() {
+ DataSplit split = dataSplit();
+
+ assertThat(Splits.underlying(split)).isSameAs(split);
+ }
+
+ @Test
+ public void testAuthorizationWrapperIsLookedThrough() {
+ DataSplit split = dataSplit();
+
+ assertThat(Splits.underlying(withQueryAuth(split))).isSameAs(split);
+ }
+
+ @Test
+ public void testFallbackWrapperAroundAuthorizationWrapperIsLookedThrough()
{
+ DataSplit split = dataSplit();
+ Split nested = toFallbackSplit(withQueryAuth(split));
+
+ assertThat(nested).isInstanceOf(FallbackSplitImpl.class);
+ assertThat(((FallbackSplit)
nested).wrapped()).isInstanceOf(QueryAuthSplit.class);
+ assertThat(Splits.underlying(nested)).isSameAs(split);
+ }
+
+ @Test
+ public void testFallbackDataSplitIsItsOwnUnderlyingSplit() {
+ DataSplit split = dataSplit();
+ Split fallback = toFallbackSplit(split);
+
+ assertThat(fallback).isInstanceOf(FallbackDataSplit.class);
+ assertThat(Splits.underlying(fallback)).isSameAs(fallback);
+ }
+
+ @Test
+ public void
testSplitThatIsNeitherADataSplitNorAWrapperIsItsOwnUnderlyingSplit() {
+ ChainSplit split = chainSplit();
+
+ assertThat(Splits.underlying(split)).isSameAs(split);
+ }
+
+ @Test
+ public void
testFallbackWrapperAroundASplitThatIsNotADataSplitIsLookedThrough() {
+ ChainSplit split = chainSplit();
+
+ assertThat(Splits.underlying(toFallbackSplit(split))).isSameAs(split);
+ }
+
+ @Test
+ public void
testAuthorizationWrapperAroundASplitThatIsNotADataSplitIsLookedThrough() {
+ ChainSplit split = chainSplit();
+
+ assertThat(Splits.underlying(withQueryAuth(split))).isSameAs(split);
+ }
+
+ @Test
+ public void
testAuthorizationWrapperAroundAFallbackDataSplitStopsAtTheFallbackDataSplit() {
+ Split fallback = toFallbackSplit(dataSplit());
+
+
assertThat(Splits.underlying(withQueryAuth(fallback))).isSameAs(fallback);
+ }
+
+ @Test
+ public void testSplitCarryingNoAuthorizationHasNoAuthResult() {
+ assertThat(Splits.authResult(dataSplit())).isNull();
+ assertThat(Splits.authResult(chainSplit())).isNull();
+ assertThat(Splits.authResult(toFallbackSplit(dataSplit()))).isNull();
+ assertThat(Splits.authResult(toFallbackSplit(chainSplit()))).isNull();
+ }
+
+ @Test
+ public void testAuthResultIsReadThroughTheAuthorizationWrapper() {
+ TableQueryAuthResult authResult = authResult();
+
+ assertThat(Splits.authResult(new QueryAuthSplit(dataSplit(),
authResult)))
+ .isSameAs(authResult);
+ }
+
+ @Test
+ public void testAuthResultIsReadThroughAFallbackWrapper() {
+ TableQueryAuthResult authResult = authResult();
+ Split nested = toFallbackSplit(new QueryAuthSplit(dataSplit(),
authResult));
+
+ assertThat(Splits.authResult(nested)).isSameAs(authResult);
+ }
+
+ @Test
+ public void testAuthResultIsReadFromAWrapperAroundAFallbackDataSplit() {
+ TableQueryAuthResult authResult = authResult();
+ Split split = new QueryAuthSplit(toFallbackSplit(dataSplit()),
authResult);
+
+ assertThat(Splits.authResult(split)).isSameAs(authResult);
+ }
+
+ @Test
+ public void testAuthorizationWrapperWithoutRulesHasNoAuthResult() {
+ assertThat(Splits.authResult(new QueryAuthSplit(dataSplit(),
null))).isNull();
+ }
+
+ private static TableQueryAuthResult authResult() {
+ return new TableQueryAuthResult(
+ null, Collections.singletonMap("name", "{\"name\":\"NULL\"}"));
+ }
+
+ private static DataSplit dataSplit() {
+ return DataSplit.builder()
+ .withSnapshot(1)
+ .withPartition(BinaryRow.EMPTY_ROW)
+ .withBucket(0)
+ .withBucketPath("bucket-0")
+ .withDataFiles(Collections.singletonList(newFile(0L, 1L)))
+ .build();
+ }
+
+ private static ChainSplit chainSplit() {
+ return new ChainSplit(
+ BinaryRow.EMPTY_ROW,
+ Collections.singletonList(newFile(0L, 1L)),
+ Collections.emptyMap(),
+ Collections.emptyMap(),
+ null);
+ }
+
+ private static Split withQueryAuth(Split split) {
+ return new QueryAuthSplit(split, new TableQueryAuthResult(null, null));
+ }
+
+ private static Split toFallbackSplit(Split split) {
+ return FallbackReadFileStoreTable.toFallbackSplit(split, true);
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/ContinuousFileSplitEnumerator.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/ContinuousFileSplitEnumerator.java
index 75c75cfddb..abbafd2f65 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/ContinuousFileSplitEnumerator.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/ContinuousFileSplitEnumerator.java
@@ -31,6 +31,8 @@ import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.EndOfScanException;
import org.apache.paimon.table.source.IncrementalSplit;
import org.apache.paimon.table.source.SnapshotNotExistPlan;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.table.source.StreamTableScan;
import org.apache.paimon.table.source.TableScan;
@@ -329,12 +331,14 @@ public class ContinuousFileSplitEnumerator
protected int assignSuggestedTask(FileStoreSourceSplit split) {
int task;
- if (split.split() instanceof DataSplit) {
- task = assignSuggestedTask((DataSplit) split.split());
- } else if (split.split() instanceof ChainSplit) {
- task = assignSuggestedTask((ChainSplit) split.split());
+ // Reads the underlying split, assigns the original.
+ Split inner = Splits.underlying(split.split());
+ if (inner instanceof DataSplit) {
+ task = assignSuggestedTask((DataSplit) inner);
+ } else if (inner instanceof ChainSplit) {
+ task = assignSuggestedTask((ChainSplit) inner);
} else {
- task = assignSuggestedTask((IncrementalSplit) split.split());
+ task = assignSuggestedTask((IncrementalSplit) inner);
}
// Split assigners keep splits in a map keyed by task, but only ever
hand out splits for
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/DynamicPartitionFilteringInfo.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/DynamicPartitionFilteringInfo.java
index f70739e950..aa5374a27c 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/DynamicPartitionFilteringInfo.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/DynamicPartitionFilteringInfo.java
@@ -18,11 +18,20 @@
package org.apache.paimon.flink.source;
+import org.apache.paimon.catalog.TableQueryAuthResult;
import org.apache.paimon.codegen.CodeGenUtils;
import org.apache.paimon.codegen.Projection;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.flink.FlinkRowData;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.types.RowType;
+import org.apache.flink.table.connector.source.DynamicFilteringData;
+
import java.io.Serializable;
+import java.util.Collections;
import java.util.List;
/** Manage dynamic partition filtering fields and table partition row. */
@@ -49,4 +58,24 @@ public class DynamicPartitionFilteringInfo implements
Serializable {
return partitionRowProjection;
}
+
+ /**
+ * Whether {@code split} may still hold a row {@code dynamicFilteringData}
asks for, answered
+ * from the partition the plan recorded rather than by opening the split.
+ *
+ * <p>A mask on a filtering field breaks that shortcut: the reader hands
out a value other than
+ * the recorded one, so such a split is kept for the join to filter on the
masked value.
+ */
+ public boolean mayMatch(DynamicFilteringData dynamicFilteringData, Split
split) {
+ TableQueryAuthResult authResult = Splits.authResult(split);
+ if (authResult != null
+ && !Collections.disjoint(
+ authResult.extractColumnMasking().keySet(),
+ dynamicPartitionFilteringFields)) {
+ return true;
+ }
+ BinaryRow partition = ((DataSplit)
Splits.underlying(split)).partition();
+ return dynamicFilteringData.contains(
+ new
FlinkRowData(getPartitionRowProjection().apply(partition)));
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FileStoreSourceSplitReader.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FileStoreSourceSplitReader.java
index 8568e2a9dd..1c230ab59b 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FileStoreSourceSplitReader.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FileStoreSourceSplitReader.java
@@ -26,6 +26,7 @@ import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.reader.RecordReader.RecordIterator;
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.table.source.TableRead;
import org.apache.paimon.types.BlobType;
import org.apache.paimon.types.RowType;
@@ -219,9 +220,10 @@ public class FileStoreSourceSplitReader
}
// update metric when split changes
- if (nextSplit.split() instanceof DataSplit) {
+ Split inner = Splits.underlying(nextSplit.split());
+ if (inner instanceof DataSplit) {
long eventTime =
- ((DataSplit) nextSplit.split())
+ ((DataSplit) inner)
.earliestFileCreationEpochMillis()
.orElse(FileStoreSourceReaderMetrics.UNDEFINED);
metrics.recordSnapshotUpdate(eventTime);
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/StaticFileStoreSplitEnumerator.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/StaticFileStoreSplitEnumerator.java
index 7bd8ff990f..c0ecc363ac 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/StaticFileStoreSplitEnumerator.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/StaticFileStoreSplitEnumerator.java
@@ -148,7 +148,7 @@ public class StaticFileStoreSplitEnumerator
this.splitAssigner =
((PreAssignSplitAssigner) splitAssigner)
.ofDynamicPartitionPruning(
-
dynamicPartitionFilteringInfo.getPartitionRowProjection(),
+ dynamicPartitionFilteringInfo,
((DynamicFilteringEvent)
sourceEvent).getData());
} else {
this.splitAssigner =
@@ -156,7 +156,7 @@ public class StaticFileStoreSplitEnumerator
.createDynamicPartitionPruningAssignerIfNeeded(
subtaskId,
splitAssigner,
-
dynamicPartitionFilteringInfo.getPartitionRowProjection(),
+ dynamicPartitionFilteringInfo,
sourceEvent,
LOG);
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/align/AlignedContinuousFileSplitEnumerator.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/align/AlignedContinuousFileSplitEnumerator.java
index c728134c07..90164b5d36 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/align/AlignedContinuousFileSplitEnumerator.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/align/AlignedContinuousFileSplitEnumerator.java
@@ -26,6 +26,7 @@ import org.apache.paimon.flink.source.assigners.SplitAssigner;
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.EndOfScanException;
import org.apache.paimon.table.source.SnapshotNotExistPlan;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.table.source.StreamTableScan;
import org.apache.paimon.table.source.TableScan;
import org.apache.paimon.utils.Preconditions;
@@ -123,7 +124,7 @@ public class AlignedContinuousFileSplitEnumerator extends
ContinuousFileSplitEnu
Map<Long, List<FileStoreSourceSplit>> splitsBySnapshot = new
TreeMap<>();
for (FileStoreSourceSplit split : splits) {
- long snapshotId = ((DataSplit) split.split()).snapshotId();
+ long snapshotId = ((DataSplit)
Splits.underlying(split.split())).snapshotId();
splitsBySnapshot.computeIfAbsent(snapshotId, snapshot -> new
ArrayList<>()).add(split);
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/AlignedSplitAssigner.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/AlignedSplitAssigner.java
index 648758f838..5f5686093b 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/AlignedSplitAssigner.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/AlignedSplitAssigner.java
@@ -21,6 +21,7 @@ package org.apache.paimon.flink.source.assigners;
import org.apache.paimon.flink.source.FileStoreSourceSplit;
import org.apache.paimon.flink.source.align.PlaceholderSplit;
import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.utils.Preconditions;
import javax.annotation.Nullable;
@@ -66,9 +67,9 @@ public class AlignedSplitAssigner implements SplitAssigner {
@Override
public void addSplit(int subtask, FileStoreSourceSplit splits) {
- long snapshotId = ((DataSplit) splits.split()).snapshotId();
+ long snapshotId = dataSplit(splits).snapshotId();
PendingSnapshot last = pendingSplitAssignment.peekLast();
- boolean isPlaceholder = splits.split() instanceof PlaceholderSplit;
+ boolean isPlaceholder = dataSplit(splits) instanceof PlaceholderSplit;
if (last == null || last.snapshotId != snapshotId) {
last = new PendingSnapshot(snapshotId, isPlaceholder, new
HashMap<>());
last.add(subtask, splits);
@@ -85,8 +86,8 @@ public class AlignedSplitAssigner implements SplitAssigner {
return;
}
- long snapshotId = ((DataSplit) splits.get(0).split()).snapshotId();
- boolean isPlaceholder = splits.get(0).split() instanceof
PlaceholderSplit;
+ long snapshotId = dataSplit(splits.get(0)).snapshotId();
+ boolean isPlaceholder = dataSplit(splits.get(0)) instanceof
PlaceholderSplit;
PendingSnapshot head = pendingSplitAssignment.peek();
if (head == null || snapshotId != head.snapshotId) {
head = new PendingSnapshot(snapshotId, isPlaceholder, new
HashMap<>());
@@ -154,7 +155,7 @@ public class AlignedSplitAssigner implements SplitAssigner {
public void add(int subtask, FileStoreSourceSplit split) {
Preconditions.checkArgument(
- ((DataSplit) split.split()).snapshotId() == snapshotId,
+ dataSplit(split).snapshotId() == snapshotId,
"SnapshotId not equal. This is a bug, please file an
issue.");
subtaskSplits.computeIfAbsent(subtask, id -> new
ArrayList<>()).add(split);
}
@@ -166,7 +167,7 @@ public class AlignedSplitAssigner implements SplitAssigner {
splits.forEach(
split ->
Preconditions.checkArgument(
- ((DataSplit) split.split()).snapshotId()
== snapshotId,
+ dataSplit(split).snapshotId() ==
snapshotId,
"SnapshotId not equal"));
subtaskSplits.put(subtask, splits);
}
@@ -175,4 +176,9 @@ public class AlignedSplitAssigner implements SplitAssigner {
return subtaskSplits.isEmpty() || isPlaceHolder;
}
}
+
+ /** Reads the underlying split, assigns the original. */
+ private static DataSplit dataSplit(FileStoreSourceSplit split) {
+ return (DataSplit) Splits.underlying(split.split());
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/DynamicPartitionPruningAssigner.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/DynamicPartitionPruningAssigner.java
index 9221f1f27e..5e4475c8b1 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/DynamicPartitionPruningAssigner.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/DynamicPartitionPruningAssigner.java
@@ -18,11 +18,8 @@
package org.apache.paimon.flink.source.assigners;
-import org.apache.paimon.codegen.Projection;
-import org.apache.paimon.data.BinaryRow;
-import org.apache.paimon.flink.FlinkRowData;
+import org.apache.paimon.flink.source.DynamicPartitionFilteringInfo;
import org.apache.paimon.flink.source.FileStoreSourceSplit;
-import org.apache.paimon.table.source.DataSplit;
import org.apache.flink.api.connector.source.SourceEvent;
import org.apache.flink.table.connector.source.DynamicFilteringData;
@@ -41,15 +38,15 @@ import java.util.stream.Collectors;
public class DynamicPartitionPruningAssigner implements SplitAssigner {
private final SplitAssigner innerAssigner;
- private final Projection partitionRowProjection;
+ private final DynamicPartitionFilteringInfo dynamicPartitionFilteringInfo;
private final DynamicFilteringData dynamicFilteringData;
public DynamicPartitionPruningAssigner(
SplitAssigner innerAssigner,
- Projection partitionRowProjection,
+ DynamicPartitionFilteringInfo dynamicPartitionFilteringInfo,
DynamicFilteringData dynamicFilteringData) {
this.innerAssigner = innerAssigner;
- this.partitionRowProjection = partitionRowProjection;
+ this.dynamicPartitionFilteringInfo = dynamicPartitionFilteringInfo;
this.dynamicFilteringData = dynamicFilteringData;
}
@@ -90,7 +87,7 @@ public class DynamicPartitionPruningAssigner implements
SplitAssigner {
public static SplitAssigner createDynamicPartitionPruningAssignerIfNeeded(
int subtaskId,
SplitAssigner oriAssigner,
- Projection partitionRowProjection,
+ DynamicPartitionFilteringInfo dynamicPartitionFilteringInfo,
SourceEvent sourceEvent,
Logger logger) {
DynamicFilteringData dynamicFilteringData = ((DynamicFilteringEvent)
sourceEvent).getData();
@@ -100,7 +97,7 @@ public class DynamicPartitionPruningAssigner implements
SplitAssigner {
dynamicFilteringData.isFiltering());
return dynamicFilteringData.isFiltering()
? new DynamicPartitionPruningAssigner(
- oriAssigner, partitionRowProjection,
dynamicFilteringData)
+ oriAssigner, dynamicPartitionFilteringInfo,
dynamicFilteringData)
: oriAssigner;
}
@@ -115,9 +112,6 @@ public class DynamicPartitionPruningAssigner implements
SplitAssigner {
}
private boolean filter(FileStoreSourceSplit sourceSplit) {
- DataSplit dataSplit = (DataSplit) sourceSplit.split();
- BinaryRow partition = dataSplit.partition();
- FlinkRowData projected = new
FlinkRowData(partitionRowProjection.apply(partition));
- return dynamicFilteringData.contains(projected);
+ return dynamicPartitionFilteringInfo.mayMatch(dynamicFilteringData,
sourceSplit.split());
}
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/PreAssignSplitAssigner.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/PreAssignSplitAssigner.java
index 24ac4a2911..c0077e89ee 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/PreAssignSplitAssigner.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/assigners/PreAssignSplitAssigner.java
@@ -18,11 +18,8 @@
package org.apache.paimon.flink.source.assigners;
-import org.apache.paimon.codegen.Projection;
-import org.apache.paimon.data.BinaryRow;
-import org.apache.paimon.flink.FlinkRowData;
+import org.apache.paimon.flink.source.DynamicPartitionFilteringInfo;
import org.apache.paimon.flink.source.FileStoreSourceSplit;
-import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.utils.BinPacking;
import org.apache.paimon.utils.SerializableFunction;
@@ -92,13 +89,13 @@ public class PreAssignSplitAssigner implements
SplitAssigner {
int splitBatchSize,
int parallelism,
Collection<FileStoreSourceSplit> splits,
- Projection partitionRowProjection,
+ DynamicPartitionFilteringInfo dynamicPartitionFilteringInfo,
DynamicFilteringData dynamicFilteringData) {
this(
splitBatchSize,
parallelism,
splits,
- partitionRowProjection,
+ dynamicPartitionFilteringInfo,
dynamicFilteringData,
split -> split.split().rowCount());
}
@@ -107,14 +104,17 @@ public class PreAssignSplitAssigner implements
SplitAssigner {
int splitBatchSize,
int parallelism,
Collection<FileStoreSourceSplit> splits,
- Projection partitionRowProjection,
+ DynamicPartitionFilteringInfo dynamicPartitionFilteringInfo,
DynamicFilteringData dynamicFilteringData,
SerializableFunction<FileStoreSourceSplit, Long> weightFunc) {
this(
splitBatchSize,
parallelism,
splits.stream()
- .filter(s -> filter(partitionRowProjection,
dynamicFilteringData, s))
+ .filter(
+ s ->
+ dynamicPartitionFilteringInfo.mayMatch(
+ dynamicFilteringData,
s.split()))
.collect(Collectors.toList()),
weightFunc);
}
@@ -279,23 +279,14 @@ public class PreAssignSplitAssigner implements
SplitAssigner {
}
public SplitAssigner ofDynamicPartitionPruning(
- Projection partitionRowProjection, DynamicFilteringData
dynamicFilteringData) {
+ DynamicPartitionFilteringInfo dynamicPartitionFilteringInfo,
+ DynamicFilteringData dynamicFilteringData) {
return new PreAssignSplitAssigner(
splitBatchSize,
parallelism,
splits,
- partitionRowProjection,
+ dynamicPartitionFilteringInfo,
dynamicFilteringData,
weightFunc);
}
-
- private static boolean filter(
- Projection partitionRowProjection,
- DynamicFilteringData dynamicFilteringData,
- FileStoreSourceSplit sourceSplit) {
- DataSplit dataSplit = (DataSplit) sourceSplit.split();
- BinaryRow partition = dataSplit.partition();
- FlinkRowData projected = new
FlinkRowData(partitionRowProjection.apply(partition));
- return dynamicFilteringData.contains(projected);
- }
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MonitorSource.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MonitorSource.java
index 740cb59311..1b1ad2392f 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MonitorSource.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/MonitorSource.java
@@ -32,8 +32,10 @@ import org.apache.paimon.table.Table;
import org.apache.paimon.table.sink.ChannelComputer;
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.EndOfScanException;
+import org.apache.paimon.table.source.IncrementalSplit;
import org.apache.paimon.table.source.ReadBuilder;
import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.table.source.StreamTableScan;
import org.apache.paimon.table.source.TableScan;
import org.apache.paimon.types.RowType;
@@ -417,7 +419,14 @@ public class MonitorSource extends
AbstractNonCoordinatedSource<Split> {
return ChannelComputer.select(key.f1, numPartitions);
},
split -> {
- DataSplit dataSplit = (DataSplit) split;
+ // Keys on the underlying split, routes the original.
Either shape orders
+ // by partition and bucket.
+ Split inner = Splits.underlying(split);
+ if (inner instanceof IncrementalSplit) {
+ IncrementalSplit incrementalSplit = (IncrementalSplit)
inner;
+ return Tuple2.of(incrementalSplit.partition(),
incrementalSplit.bucket());
+ }
+ DataSplit dataSplit = (DataSplit) inner;
return Tuple2.of(dataSplit.partition(),
dataSplit.bucket());
});
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/ReadOperator.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/ReadOperator.java
index 95bf5606e7..5004058561 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/ReadOperator.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/operator/ReadOperator.java
@@ -27,6 +27,7 @@ import org.apache.paimon.flink.source.RecordLimiter;
import org.apache.paimon.flink.source.metrics.FileStoreSourceReaderMetrics;
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.table.source.TableRead;
import org.apache.paimon.types.BlobType;
import org.apache.paimon.types.RowType;
@@ -144,10 +145,14 @@ public class ReadOperator extends
AbstractStreamOperator<RowData>
Split split = record.getValue();
// update metric when reading a new split
+ // A shape this metric cannot read must not fail the read.
+ Split inner = Splits.underlying(split);
long eventTime =
- ((DataSplit) split)
- .earliestFileCreationEpochMillis()
- .orElse(FileStoreSourceReaderMetrics.UNDEFINED);
+ inner instanceof DataSplit
+ ? ((DataSplit) inner)
+ .earliestFileCreationEpochMillis()
+ .orElse(FileStoreSourceReaderMetrics.UNDEFINED)
+ : FileStoreSourceReaderMetrics.UNDEFINED;
sourceReaderMetrics.recordSnapshotUpdate(eventTime);
// update idleStartTime when reading a new split
idleStartTime = FileStoreSourceReaderMetrics.ACTIVE;
@@ -156,7 +161,10 @@ public class ReadOperator extends
AbstractStreamOperator<RowData>
try (CloseableIterator<InternalRow> iterator =
read.createReader(split).toCloseableIterator()) {
while (!reachLimit() && iterator.hasNext()) {
- emitEventTimeLag = System.currentTimeMillis() - eventTime;
+ emitEventTimeLag =
+ eventTime == FileStoreSourceReaderMetrics.UNDEFINED
+ ? FileStoreSourceReaderMetrics.UNDEFINED
+ : System.currentTimeMillis() - eventTime;
// each Split is already counted as one input record,
// so we don't need to count the first record
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/utils/TableScanUtils.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/utils/TableScanUtils.java
index ca901ad1bf..d756ab8bd2 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/utils/TableScanUtils.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/utils/TableScanUtils.java
@@ -23,6 +23,8 @@ import org.apache.paimon.flink.source.FileStoreSourceSplit;
import org.apache.paimon.options.Options;
import org.apache.paimon.table.Table;
import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.Splits;
import org.apache.paimon.table.source.TableScan;
import java.util.HashMap;
@@ -67,8 +69,10 @@ public class TableScanUtils {
/** Get snapshot id from {@link FileStoreSourceSplit}. */
public static Optional<Long> getSnapshotId(FileStoreSourceSplit split) {
- if (split.split() instanceof DataSplit) {
- return Optional.of(((DataSplit) split.split()).snapshotId());
+ // An empty answer does not fail here, it stalls consumer progress and
watermarks.
+ Split inner = Splits.underlying(split.split());
+ if (inner instanceof DataSplit) {
+ return Optional.of(((DataSplit) inner).snapshotId());
}
return Optional.empty();
}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/ContinuousFileSplitEnumeratorTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/ContinuousFileSplitEnumeratorTest.java
index 0a50038bb4..103ae68683 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/ContinuousFileSplitEnumeratorTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/ContinuousFileSplitEnumeratorTest.java
@@ -18,11 +18,14 @@
package org.apache.paimon.flink.source;
+import org.apache.paimon.catalog.TableQueryAuthResult;
import org.apache.paimon.io.DataFileMeta;
import org.apache.paimon.table.BucketMode;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
import org.apache.paimon.table.source.DataFilePlan;
import org.apache.paimon.table.source.DataSplit;
import org.apache.paimon.table.source.IncrementalSplit;
+import org.apache.paimon.table.source.QueryAuthSplit;
import org.apache.paimon.table.source.StreamTableScan;
import org.apache.paimon.table.source.TableScan;
@@ -858,6 +861,56 @@ public class ContinuousFileSplitEnumeratorTest
Assertions.assertThat(enumerator.nextSnapshotId).isEqualTo(3);
}
+ @Test
+ public void testQueryAuthSplitAssignedLikeTheSplitItWraps() {
+ int parallelism = 3;
+ ContinuousFileSplitEnumerator enumerator =
buildEnumerator(parallelism);
+
+ FileStoreSourceSplit plain = createSnapshotSplit(1, 2,
Collections.emptyList());
+ FileStoreSourceSplit wrapped =
+ new FileStoreSourceSplit(
+ plain.splitId(),
+ new QueryAuthSplit(plain.split(), new
TableQueryAuthResult(null, null)));
+
+ assertThat(enumerator.assignSuggestedTask(wrapped))
+ .isEqualTo(enumerator.assignSuggestedTask(plain));
+ }
+
+ @Test
+ public void testFallbackWrappedQueryAuthSplitAssignedLikeTheSplitItWraps()
{
+ int parallelism = 3;
+ ContinuousFileSplitEnumerator enumerator =
buildEnumerator(parallelism);
+
+ FileStoreSourceSplit plain = createSnapshotSplit(1, 2,
Collections.emptyList());
+ FileStoreSourceSplit nested =
+ new FileStoreSourceSplit(
+ plain.splitId(),
+ FallbackReadFileStoreTable.toFallbackSplit(
+ new QueryAuthSplit(
+ plain.split(), new
TableQueryAuthResult(null, null)),
+ true));
+
+ assertThat(nested.split()).isNotInstanceOf(DataSplit.class);
+ assertThat(enumerator.assignSuggestedTask(nested))
+ .isEqualTo(enumerator.assignSuggestedTask(plain));
+ }
+
+ @Test
+ public void testFallbackDataSplitAssignedLikeTheSplitItCopies() {
+ int parallelism = 3;
+ ContinuousFileSplitEnumerator enumerator =
buildEnumerator(parallelism);
+
+ FileStoreSourceSplit plain = createSnapshotSplit(1, 2,
Collections.emptyList());
+ FileStoreSourceSplit fallback =
+ new FileStoreSourceSplit(
+ plain.splitId(),
+
FallbackReadFileStoreTable.toFallbackSplit(plain.split(), true));
+
+ assertThat(fallback.split()).isInstanceOf(DataSplit.class);
+ assertThat(enumerator.assignSuggestedTask(fallback))
+ .isEqualTo(enumerator.assignSuggestedTask(plain));
+ }
+
@Test
public void testPostponeBucketIncrementalSplitAssignedByWriteId() {
int parallelism = 3;
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FileStoreSourceSplitReaderTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FileStoreSourceSplitReaderTest.java
index b3a9414093..42e3c1406e 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FileStoreSourceSplitReaderTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FileStoreSourceSplitReaderTest.java
@@ -19,6 +19,7 @@
package org.apache.paimon.flink.source;
import org.apache.paimon.KeyValue;
+import org.apache.paimon.catalog.TableQueryAuthResult;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.disk.IOManager;
@@ -32,6 +33,7 @@ import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.schema.FileSystemSchemaManager;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.table.source.QueryAuthSplit;
import org.apache.paimon.table.source.Split;
import org.apache.paimon.table.source.TableRead;
import org.apache.paimon.utils.RecordWriter;
@@ -138,13 +140,49 @@ public class FileStoreSourceSplitReaderTest {
assertThat(thread.isAlive()).isFalse();
}
+ @Test
+ public void testWrappedSplitReportsTheFileCreationTimeOfTheSplitItWraps()
throws Exception {
+ TestChangelogDataReadWrite rw = new
TestChangelogDataReadWrite(tempDir.toString());
+ List<DataFileMeta> files = rw.writeFiles(row(1), 0, kvs());
+
+ FileStoreSourceSplit split = newSourceSplit("id1", row(1), 0, files);
+ FileStoreSourceSplit wrapped =
+ new FileStoreSourceSplit(
+ "id1",
+ new QueryAuthSplit(split.split(), new
TableQueryAuthResult(null, null)));
+
+ long plain = readAndReportFileCreationTime(rw.createReadWithKey(),
split);
+ long authorized =
readAndReportFileCreationTime(rw.createReadWithKey(), wrapped);
+
+ assertThat(plain).isNotEqualTo(FileStoreSourceReaderMetrics.UNDEFINED);
+ assertThat(authorized).isEqualTo(plain);
+ }
+
+ private long readAndReportFileCreationTime(TableRead tableRead,
FileStoreSourceSplit split)
+ throws Exception {
+ FileStoreSourceReaderMetrics metrics =
+ new FileStoreSourceReaderMetrics(new DummyMetricGroup());
+ FileStoreSourceSplitReader reader = createReader(tableRead, null,
metrics);
+ try {
+ assignSplit(reader, split);
+ readRecords(reader.fetch(), split.splitId(), 0);
+ assertThat(reader.fetch().finishedSplits())
+ .isEqualTo(Collections.singleton(split.splitId()));
+ } finally {
+ reader.close();
+ }
+ return metrics.getLatestFileCreationTime();
+ }
+
private FileStoreSourceSplitReader createReader(TableRead tableRead,
@Nullable Long limit) {
+ return createReader(
+ tableRead, limit, new FileStoreSourceReaderMetrics(new
DummyMetricGroup()));
+ }
+
+ private FileStoreSourceSplitReader createReader(
+ TableRead tableRead, @Nullable Long limit,
FileStoreSourceReaderMetrics metrics) {
return new FileStoreSourceSplitReader(
- tableRead,
- limit == null ? null : new RecordLimiter(limit),
- new FileStoreSourceReaderMetrics(new DummyMetricGroup()),
- null,
- false);
+ tableRead, limit == null ? null : new RecordLimiter(limit),
metrics, null, false);
}
private void innerTestOnce(int skip) throws Exception {
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkSourceBuilderTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkSourceBuilderTest.java
index 4f8a097e47..709b0e733b 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkSourceBuilderTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkSourceBuilderTest.java
@@ -22,22 +22,36 @@ import org.apache.paimon.catalog.Catalog;
import org.apache.paimon.catalog.CatalogContext;
import org.apache.paimon.catalog.CatalogFactory;
import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.data.BinaryRow;
import org.apache.paimon.flink.source.operator.MonitorSource;
import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
import org.apache.paimon.table.Table;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.IncrementalSplit;
+import org.apache.paimon.table.source.QueryAuthSplit;
+import org.apache.paimon.table.source.Split;
import org.apache.paimon.types.DataTypes;
import org.apache.flink.api.dag.Transformation;
+import org.apache.flink.runtime.plugable.SerializationDelegate;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.transformations.PartitionTransformation;
import org.apache.flink.streaming.api.transformations.SourceTransformation;
+import org.apache.flink.streaming.runtime.partitioner.StreamPartitioner;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.runtime.typeutils.InternalTypeInfo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import java.nio.file.Path;
+import java.util.Collections;
import static org.apache.paimon.flink.LogicalTypeConversion.toLogicalType;
import static org.assertj.core.api.Assertions.assertThat;
@@ -204,4 +218,211 @@ public class FlinkSourceBuilderTest {
.getSource())
.isInstanceOf(PaimonDataStreamSource.class));
}
+
+ @ValueSource(booleans = {false, true})
+ @ParameterizedTest
+ public void testOrderedShuffleRoutesQueryAuthSplitLikeTheSplitItWraps(
+ boolean shuffleBucketWithPartition) throws Exception {
+ Table table = createTable("ordered_shuffle_" +
shuffleBucketWithPartition, false, 2, true);
+ StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
+
+ DataStream<RowData> dataStream =
+ MonitorSource.buildSource(
+ env,
+ "source",
+ InternalTypeInfo.of(toLogicalType(table.rowType())),
+ table.newReadBuilder(),
+ 10,
+ false,
+ shuffleBucketWithPartition,
+ false,
+ null,
+ true,
+ null);
+
+ Transformation<?> input =
dataStream.getTransformation().getInputs().get(0);
+ assertThat(input).isInstanceOf(PartitionTransformation.class);
+ @SuppressWarnings("unchecked")
+ StreamPartitioner<Split> partitioner =
+ ((PartitionTransformation<Split>) input).getPartitioner();
+ partitioner.setup(4);
+
+ DataSplit bucketOne = dataSplit(1);
+ DataSplit bucketThree = dataSplit(3);
+
+ assertThat(selectChannel(partitioner, withQueryAuth(bucketOne)))
+ .isEqualTo(selectChannel(partitioner, bucketOne));
+ assertThat(selectChannel(partitioner, withQueryAuth(bucketThree)))
+ .isEqualTo(selectChannel(partitioner, bucketThree));
+
+ assertThat(selectChannel(partitioner, withQueryAuth(bucketOne)))
+ .isNotEqualTo(selectChannel(partitioner,
withQueryAuth(bucketThree)));
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testOrderedShuffleRoutesIncrementalSplitByPartitionAndBucket(
+ boolean shuffleBucketWithPartition) throws Exception {
+ Table table =
+ createTable("ordered_incremental_" +
shuffleBucketWithPartition, false, 2, true);
+ StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
+
+ DataStream<RowData> dataStream =
+ MonitorSource.buildSource(
+ env,
+ "source",
+ InternalTypeInfo.of(toLogicalType(table.rowType())),
+ table.newReadBuilder(),
+ 10,
+ false,
+ shuffleBucketWithPartition,
+ false,
+ null,
+ true,
+ null);
+
+ Transformation<?> input =
dataStream.getTransformation().getInputs().get(0);
+ @SuppressWarnings("unchecked")
+ StreamPartitioner<Split> partitioner =
+ ((PartitionTransformation<Split>) input).getPartitioner();
+ partitioner.setup(4);
+
+ Split bucketOne = incrementalSplit(1);
+ Split bucketThree = incrementalSplit(3);
+
+ assertThat(selectChannel(partitioner, bucketOne))
+ .isEqualTo(selectChannel(partitioner, dataSplit(1)));
+ assertThat(selectChannel(partitioner, withQueryAuth(bucketOne)))
+ .isEqualTo(selectChannel(partitioner, bucketOne));
+ assertThat(selectChannel(partitioner, withQueryAuth(bucketThree)))
+ .isEqualTo(selectChannel(partitioner, bucketThree));
+
+ assertThat(selectChannel(partitioner, withQueryAuth(bucketOne)))
+ .isNotEqualTo(selectChannel(partitioner,
withQueryAuth(bucketThree)));
+ }
+
+ @ValueSource(booleans = {false, true})
+ @ParameterizedTest
+ public void
testOrderedShuffleRoutesFallbackWrappedQueryAuthSplitLikeTheSplitItWraps(
+ boolean shuffleBucketWithPartition) throws Exception {
+ Table table = createTable("ordered_nested_" +
shuffleBucketWithPartition, false, 2, true);
+ StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
+
+ DataStream<RowData> dataStream =
+ MonitorSource.buildSource(
+ env,
+ "source",
+ InternalTypeInfo.of(toLogicalType(table.rowType())),
+ table.newReadBuilder(),
+ 10,
+ false,
+ shuffleBucketWithPartition,
+ false,
+ null,
+ true,
+ null);
+
+ Transformation<?> input =
dataStream.getTransformation().getInputs().get(0);
+ assertThat(input).isInstanceOf(PartitionTransformation.class);
+ @SuppressWarnings("unchecked")
+ StreamPartitioner<Split> partitioner =
+ ((PartitionTransformation<Split>) input).getPartitioner();
+ partitioner.setup(4);
+
+ DataSplit bucketOne = dataSplit(1);
+ DataSplit bucketThree = dataSplit(3);
+ Split nestedOne = withFallback(withQueryAuth(bucketOne));
+ Split nestedThree = withFallback(withQueryAuth(bucketThree));
+
+ assertThat(nestedOne).isNotInstanceOf(DataSplit.class);
+ assertThat(selectChannel(partitioner, nestedOne))
+ .isEqualTo(selectChannel(partitioner, bucketOne));
+ assertThat(selectChannel(partitioner, nestedThree))
+ .isEqualTo(selectChannel(partitioner, bucketThree));
+ assertThat(selectChannel(partitioner, nestedOne))
+ .isNotEqualTo(selectChannel(partitioner, nestedThree));
+
+ Split nestedIncrementalOne =
withFallback(withQueryAuth(incrementalSplit(1)));
+ assertThat(selectChannel(partitioner, nestedIncrementalOne))
+ .isEqualTo(selectChannel(partitioner, bucketOne));
+ }
+
+ @ValueSource(booleans = {false, true})
+ @ParameterizedTest
+ public void testOrderedShuffleRoutesFallbackDataSplitLikeTheSplitItCopies(
+ boolean shuffleBucketWithPartition) throws Exception {
+ Table table = createTable("ordered_fallback_" +
shuffleBucketWithPartition, false, 2, true);
+ StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
+
+ DataStream<RowData> dataStream =
+ MonitorSource.buildSource(
+ env,
+ "source",
+ InternalTypeInfo.of(toLogicalType(table.rowType())),
+ table.newReadBuilder(),
+ 10,
+ false,
+ shuffleBucketWithPartition,
+ false,
+ null,
+ true,
+ null);
+
+ Transformation<?> input =
dataStream.getTransformation().getInputs().get(0);
+ @SuppressWarnings("unchecked")
+ StreamPartitioner<Split> partitioner =
+ ((PartitionTransformation<Split>) input).getPartitioner();
+ partitioner.setup(4);
+
+ DataSplit bucketOne = dataSplit(1);
+ DataSplit bucketThree = dataSplit(3);
+ Split fallbackOne = withFallback(bucketOne);
+ Split fallbackThree = withFallback(bucketThree);
+
+ assertThat(fallbackOne).isInstanceOf(DataSplit.class);
+ assertThat(selectChannel(partitioner, fallbackOne))
+ .isEqualTo(selectChannel(partitioner, bucketOne));
+ assertThat(selectChannel(partitioner, fallbackThree))
+ .isEqualTo(selectChannel(partitioner, bucketThree));
+ assertThat(selectChannel(partitioner, fallbackOne))
+ .isNotEqualTo(selectChannel(partitioner, fallbackThree));
+ }
+
+ private static int selectChannel(StreamPartitioner<Split> partitioner,
Split split) {
+ SerializationDelegate<StreamRecord<Split>> delegate = new
SerializationDelegate<>(null);
+ delegate.setInstance(new StreamRecord<>(split));
+ return partitioner.selectChannel(delegate);
+ }
+
+ private static DataSplit dataSplit(int bucket) {
+ return DataSplit.builder()
+ .withSnapshot(1)
+ .withPartition(BinaryRow.EMPTY_ROW)
+ .withBucket(bucket)
+ .withDataFiles(Collections.emptyList())
+ .isStreaming(true)
+ .withBucketPath("/temp/xxx")
+ .build();
+ }
+
+ private static Split incrementalSplit(int bucket) {
+ return new IncrementalSplit(
+ 1L,
+ BinaryRow.EMPTY_ROW,
+ bucket,
+ 1,
+ Collections.emptyList(),
+ null,
+ Collections.emptyList(),
+ null,
+ true);
+ }
+
+ private static Split withQueryAuth(Split split) {
+ return new QueryAuthSplit(split, new TableQueryAuthResult(null, null));
+ }
+
+ private static Split withFallback(Split split) {
+ return FallbackReadFileStoreTable.toFallbackSplit(split, true);
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/align/AlignedContinuousFileSplitEnumeratorQueryAuthTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/align/AlignedContinuousFileSplitEnumeratorQueryAuthTest.java
new file mode 100644
index 0000000000..e05fe061f4
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/align/AlignedContinuousFileSplitEnumeratorQueryAuthTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.paimon.flink.source.align;
+
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.flink.source.FileSplitEnumeratorTestBase;
+import org.apache.paimon.flink.source.FileStoreSourceSplit;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.QueryAuthSplit;
+import org.apache.paimon.table.source.Split;
+
+import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import
org.apache.flink.connector.testutils.source.reader.TestingSplitEnumeratorContext;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.apache.paimon.io.DataFileTestUtils.row;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests {@link AlignedContinuousFileSplitEnumerator} over {@link
QueryAuthSplit}. */
+public class AlignedContinuousFileSplitEnumeratorQueryAuthTest
+ extends FileSplitEnumeratorTestBase<FileStoreSourceSplit> {
+
+ @Test
+ public void testWrappedSplitsGroupedBySnapshotLikeTheSplitsTheyWrap()
throws Exception {
+ List<FileStoreSourceSplit> splits =
+ Arrays.asList(snapshotSplit(2, 0), snapshotSplit(1, 1),
snapshotSplit(1, 0));
+
+ List<FileStoreSourceSplit> plain = assignFirstSnapshot(splits);
+ List<FileStoreSourceSplit> authorized =
assignFirstSnapshot(withQueryAuth(splits));
+
+ assertThat(underlying(plain)).containsExactly(splits.get(2).split(),
splits.get(1).split());
+ assertThat(underlying(authorized)).isEqualTo(underlying(plain));
+ }
+
+ @Test
+ public void testWrappedSplitIsAssignedStillWrapped() throws Exception {
+ List<FileStoreSourceSplit> wrapped =
+ withQueryAuth(Collections.singletonList(snapshotSplit(1, 0)));
+
+ List<FileStoreSourceSplit> assigned = assignFirstSnapshot(wrapped);
+
+ assertThat(assigned).hasSize(1);
+ assertThat(assigned.get(0)).isSameAs(wrapped.get(0));
+ assertThat(assigned.get(0).split()).isInstanceOf(QueryAuthSplit.class);
+ }
+
+ private List<FileStoreSourceSplit>
assignFirstSnapshot(List<FileStoreSourceSplit> splits) {
+ TestingSplitEnumeratorContext<FileStoreSourceSplit> context =
getSplitEnumeratorContext(2);
+ AlignedContinuousFileSplitEnumerator enumerator = enumerator(context);
+ enumerator.addSplits(splits);
+ enumerator.handleSplitRequest(0, "test-host");
+ enumerator.handleSplitRequest(1, "test-host");
+
+ List<FileStoreSourceSplit> assigned = new ArrayList<>();
+ new TreeMap<>(context.getSplitAssignments())
+ .values()
+ .forEach(state -> assigned.addAll(state.getAssignedSplits()));
+ return assigned;
+ }
+
+ private static AlignedContinuousFileSplitEnumerator enumerator(
+ SplitEnumeratorContext<FileStoreSourceSplit> context) {
+ return new AlignedContinuousFileSplitEnumerator(
+ context,
+ Collections.emptyList(),
+ null,
+ Long.MAX_VALUE,
+ null,
+ false,
+ 30000L,
+ 10,
+ false,
+ -1,
+ 10);
+ }
+
+ private FileStoreSourceSplit snapshotSplit(int snapshotId, int bucket) {
+ return createSnapshotSplit(snapshotId, bucket,
Collections.emptyList());
+ }
+
+ private static FileStoreSourceSplit withQueryAuth(FileStoreSourceSplit
split) {
+ return new FileStoreSourceSplit(
+ split.splitId(),
+ new QueryAuthSplit(split.split(), new
TableQueryAuthResult(null, null)),
+ split.recordsToSkip());
+ }
+
+ private static List<FileStoreSourceSplit>
withQueryAuth(List<FileStoreSourceSplit> splits) {
+ return splits.stream()
+
.map(AlignedContinuousFileSplitEnumeratorQueryAuthTest::withQueryAuth)
+ .collect(Collectors.toList());
+ }
+
+ private static List<Split> underlying(Collection<FileStoreSourceSplit>
splits) {
+ List<Split> underlying = new ArrayList<>();
+ for (FileStoreSourceSplit split : splits) {
+ Split inner = split.split();
+ underlying.add(
+ inner instanceof QueryAuthSplit ? ((QueryAuthSplit)
inner).split() : inner);
+ }
+ return underlying;
+ }
+
+ @Override
+ protected FileStoreSourceSplit createSnapshotSplit(
+ int snapshotId, int bucket, List<DataFileMeta> files, int...
partitions) {
+ return new FileStoreSourceSplit(
+ UUID.randomUUID().toString(),
+ DataSplit.builder()
+ .withSnapshot(snapshotId)
+ .withPartition(row(partitions))
+ .withBucket(bucket)
+ .withDataFiles(files)
+ .isStreaming(true)
+ .withBucketPath("/temp/xxx")
+ .build(),
+ 0);
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/AlignedSplitAssignerTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/AlignedSplitAssignerTest.java
new file mode 100644
index 0000000000..a327451fba
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/AlignedSplitAssignerTest.java
@@ -0,0 +1,245 @@
+/*
+ * 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.paimon.flink.source.assigners;
+
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.flink.source.FileStoreSourceSplit;
+import org.apache.paimon.flink.source.align.PlaceholderSplit;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FallbackReadFileStoreTable.FallbackSplitImpl;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.QueryAuthSplit;
+import org.apache.paimon.table.source.Split;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.apache.paimon.io.DataFileTestUtils.row;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests {@link AlignedSplitAssigner} over {@link QueryAuthSplit}. */
+public class AlignedSplitAssignerTest {
+
+ @Test
+ public void testWrappedSplitsAssignedLikeTheSplitsTheyWrap() {
+ List<FileStoreSourceSplit> splits =
+ Arrays.asList(snapshotSplit(1, 0), snapshotSplit(1, 1),
snapshotSplit(2, 0));
+
+ AlignedSplitAssigner plain = new AlignedSplitAssigner();
+ AlignedSplitAssigner authorized = new AlignedSplitAssigner();
+ for (int i = 0; i < splits.size(); i++) {
+ plain.addSplit(i % 2, splits.get(i));
+ authorized.addSplit(i % 2, withQueryAuth(splits.get(i)));
+ }
+
+
assertThat(authorized.remainingSnapshots()).isEqualTo(plain.remainingSnapshots());
+
assertThat(authorized.numberOfRemainingSplits()).isEqualTo(plain.numberOfRemainingSplits());
+
assertThat(authorized.getNextSnapshotId(0)).isEqualTo(plain.getNextSnapshotId(0));
+ assertThat(authorized.isAligned()).isEqualTo(plain.isAligned());
+ assertThat(underlying(authorized.remainingSplits()))
+ .isEqualTo(underlying(plain.remainingSplits()));
+
+ assertThat(underlying(authorized.getNext(0, null)))
+ .isEqualTo(underlying(plain.getNext(0, null)));
+ assertThat(underlying(authorized.getNext(1, null)))
+ .isEqualTo(underlying(plain.getNext(1, null)));
+ assertThat(authorized.isAligned()).isEqualTo(plain.isAligned());
+
assertThat(authorized.numberOfRemainingSplits()).isEqualTo(plain.numberOfRemainingSplits());
+ }
+
+ @Test
+ public void testWrappedSplitIsAssignedStillWrapped() {
+ FileStoreSourceSplit wrapped = withQueryAuth(snapshotSplit(1, 0));
+
+ AlignedSplitAssigner assigner = new AlignedSplitAssigner();
+ assigner.addSplit(0, wrapped);
+
+ assertThat(assigner.remainingSplits()).containsExactly(wrapped);
+ List<FileStoreSourceSplit> assigned = assigner.getNext(0, null);
+ assertThat(assigned).hasSize(1);
+ assertThat(assigned.get(0)).isSameAs(wrapped);
+ assertThat(assigned.get(0).split()).isInstanceOf(QueryAuthSplit.class);
+ }
+
+ @Test
+ public void testWrappedPlaceholderStillCountsAsAligned() {
+ FileStoreSourceSplit placeholder = placeholderSplit(1);
+
+ AlignedSplitAssigner plain = new AlignedSplitAssigner();
+ plain.addSplit(0, placeholder);
+ AlignedSplitAssigner authorized = new AlignedSplitAssigner();
+ authorized.addSplit(0, withQueryAuth(placeholder));
+
+ assertThat(plain.isAligned()).isTrue();
+ assertThat(authorized.isAligned()).isEqualTo(plain.isAligned());
+ assertThat(authorized.getNext(0, null)).isEqualTo(plain.getNext(0,
null));
+ }
+
+ @Test
+ public void testWrappedSplitsAddedBackIntoTheSnapshotAlreadyPending() {
+ FileStoreSourceSplit pending = snapshotSplit(4, 0);
+ List<FileStoreSourceSplit> back = Arrays.asList(snapshotSplit(4, 1),
snapshotSplit(4, 2));
+
+ AlignedSplitAssigner plain = new AlignedSplitAssigner();
+ plain.addSplit(0, pending);
+ plain.addSplitsBack(1, back);
+ AlignedSplitAssigner authorized = new AlignedSplitAssigner();
+ authorized.addSplit(0, withQueryAuth(pending));
+ authorized.addSplitsBack(1, withQueryAuth(back));
+
+ assertThat(plain.remainingSnapshots()).isEqualTo(1);
+
assertThat(authorized.remainingSnapshots()).isEqualTo(plain.remainingSnapshots());
+
assertThat(authorized.numberOfRemainingSplits()).isEqualTo(plain.numberOfRemainingSplits());
+ assertThat(underlying(authorized.getNext(1, null)))
+ .isEqualTo(underlying(plain.getNext(1, null)));
+ }
+
+ @Test
+ public void testWrappedSplitsFromDifferentSnapshotsAreStillRejected() {
+ List<FileStoreSourceSplit> mixed =
+ withQueryAuth(Arrays.asList(snapshotSplit(1, 0),
snapshotSplit(2, 0)));
+
+ assertThatThrownBy(() -> new AlignedSplitAssigner().addSplitsBack(0,
mixed))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("SnapshotId not equal");
+ }
+
+ @Test
+ public void testWrappedPlaceholderAddedBackStillCountsAsAligned() {
+ FileStoreSourceSplit placeholder = placeholderSplit(5);
+
+ AlignedSplitAssigner plain = new AlignedSplitAssigner();
+ plain.addSplitsBack(0, Collections.singletonList(placeholder));
+ AlignedSplitAssigner authorized = new AlignedSplitAssigner();
+ authorized.addSplitsBack(0,
Collections.singletonList(withQueryAuth(placeholder)));
+
+ assertThat(plain.isAligned()).isTrue();
+ assertThat(authorized.isAligned()).isEqualTo(plain.isAligned());
+ }
+
+ @Test
+ public void testFallbackWrappedSplitsAssignedLikeTheSplitsTheyWrap() {
+ List<FileStoreSourceSplit> splits =
+ Arrays.asList(snapshotSplit(1, 0), snapshotSplit(1, 1),
snapshotSplit(2, 0));
+
+ AlignedSplitAssigner plain = new AlignedSplitAssigner();
+ AlignedSplitAssigner nested = new AlignedSplitAssigner();
+ for (int i = 0; i < splits.size(); i++) {
+ plain.addSplit(i % 2, splits.get(i));
+ nested.addSplit(i % 2, withFallback(withQueryAuth(splits.get(i))));
+ }
+
+
assertThat(nested.remainingSnapshots()).isEqualTo(plain.remainingSnapshots());
+
assertThat(nested.numberOfRemainingSplits()).isEqualTo(plain.numberOfRemainingSplits());
+
assertThat(nested.getNextSnapshotId(0)).isEqualTo(plain.getNextSnapshotId(0));
+ assertThat(underlying(nested.remainingSplits()))
+ .isEqualTo(underlying(plain.remainingSplits()));
+
+ assertThat(underlying(nested.getNext(0, null)))
+ .isEqualTo(underlying(plain.getNext(0, null)));
+ assertThat(underlying(nested.getNext(1, null)))
+ .isEqualTo(underlying(plain.getNext(1, null)));
+ }
+
+ @Test
+ public void testFallbackWrappedSplitIsAssignedStillFallbackWrapped() {
+ FileStoreSourceSplit nested =
withFallback(withQueryAuth(snapshotSplit(1, 0)));
+
+ AlignedSplitAssigner assigner = new AlignedSplitAssigner();
+ assigner.addSplit(0, nested);
+
+ assertThat(assigner.remainingSplits()).containsExactly(nested);
+ List<FileStoreSourceSplit> assigned = assigner.getNext(0, null);
+ assertThat(assigned).hasSize(1);
+ assertThat(assigned.get(0)).isSameAs(nested);
+
assertThat(assigned.get(0).split()).isInstanceOf(FallbackSplitImpl.class);
+ }
+
+ @Test
+ public void testFallbackWrappedPlaceholderStillCountsAsAligned() {
+ FileStoreSourceSplit placeholder = placeholderSplit(1);
+
+ AlignedSplitAssigner plain = new AlignedSplitAssigner();
+ plain.addSplit(0, placeholder);
+ AlignedSplitAssigner nested = new AlignedSplitAssigner();
+ nested.addSplit(0, withFallback(withQueryAuth(placeholder)));
+
+ assertThat(plain.isAligned()).isTrue();
+ assertThat(nested.isAligned()).isEqualTo(plain.isAligned());
+ }
+
+ private static FileStoreSourceSplit snapshotSplit(long snapshotId, int
bucket) {
+ return new FileStoreSourceSplit(
+ UUID.randomUUID().toString(),
+ DataSplit.builder()
+ .withSnapshot(snapshotId)
+ .withPartition(row(1))
+ .withBucket(bucket)
+ .withDataFiles(Collections.emptyList())
+ .isStreaming(true)
+ .withBucketPath("/temp/xxx") // not used
+ .build());
+ }
+
+ private static FileStoreSourceSplit placeholderSplit(long snapshotId) {
+ return new FileStoreSourceSplit(
+ UUID.randomUUID().toString(), new
PlaceholderSplit(snapshotId));
+ }
+
+ private static FileStoreSourceSplit withQueryAuth(FileStoreSourceSplit
split) {
+ return new FileStoreSourceSplit(
+ split.splitId(),
+ new QueryAuthSplit(split.split(), new
TableQueryAuthResult(null, null)),
+ split.recordsToSkip());
+ }
+
+ private static List<FileStoreSourceSplit>
withQueryAuth(List<FileStoreSourceSplit> splits) {
+ return splits.stream()
+ .map(AlignedSplitAssignerTest::withQueryAuth)
+ .collect(Collectors.toList());
+ }
+
+ private static FileStoreSourceSplit withFallback(FileStoreSourceSplit
split) {
+ return new FileStoreSourceSplit(
+ split.splitId(),
+ FallbackReadFileStoreTable.toFallbackSplit(split.split(),
true),
+ split.recordsToSkip());
+ }
+
+ private static List<Split> underlying(Collection<FileStoreSourceSplit>
splits) {
+ List<Split> underlying = new ArrayList<>();
+ for (FileStoreSourceSplit split : splits) {
+ Split inner = split.split();
+ if (inner instanceof FallbackSplitImpl) {
+ inner = ((FallbackSplitImpl) inner).wrapped();
+ }
+ underlying.add(
+ inner instanceof QueryAuthSplit ? ((QueryAuthSplit)
inner).split() : inner);
+ }
+ return underlying;
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/DynamicPartitionPruningAssignerTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/DynamicPartitionPruningAssignerTest.java
new file mode 100644
index 0000000000..00ebc9b986
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/DynamicPartitionPruningAssignerTest.java
@@ -0,0 +1,262 @@
+/*
+ * 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.paimon.flink.source.assigners;
+
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.flink.source.DynamicPartitionFilteringInfo;
+import org.apache.paimon.flink.source.FileStoreSourceSplit;
+import org.apache.paimon.predicate.NullTransform;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FallbackReadFileStoreTable.FallbackSplitImpl;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.QueryAuthSplit;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.JsonSerdeUtil;
+
+import org.apache.flink.api.java.typeutils.GenericTypeInfo;
+import org.apache.flink.table.connector.source.DynamicFilteringData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.IntType;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.apache.paimon.io.DataFileTestUtils.row;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests {@link DynamicPartitionPruningAssigner} over {@link QueryAuthSplit}.
*/
+public class DynamicPartitionPruningAssignerTest {
+
+ @Test
+ public void testWrappedSplitsPrunedLikeTheSplitsTheyWrap() {
+ FileStoreSourceSplit kept = partitionSplit(1);
+ FileStoreSourceSplit pruned = partitionSplit(2);
+ List<FileStoreSourceSplit> splits = Arrays.asList(kept, pruned);
+
+ SplitAssigner plain = pruningAssigner(splits);
+ SplitAssigner authorized = pruningAssigner(withQueryAuth(splits));
+
+
assertThat(underlying(plain.remainingSplits())).containsExactly(kept.split());
+ assertThat(underlying(authorized.remainingSplits()))
+ .isEqualTo(underlying(plain.remainingSplits()));
+
+ List<FileStoreSourceSplit> authorizedNext = authorized.getNext(0,
null);
+
assertThat(underlying(authorizedNext)).isEqualTo(underlying(plain.getNext(0,
null)));
+ assertThat(underlying(authorizedNext)).containsExactly(kept.split());
+
+ assertThat(authorized.getNext(0, null)).isEqualTo(plain.getNext(0,
null)).isEmpty();
+ }
+
+ @Test
+ public void testWrappedSplitIsKeptStillWrapped() {
+ FileStoreSourceSplit wrapped = withQueryAuth(partitionSplit(1));
+
+ SplitAssigner assigner = pruningAssigner(Collections.emptyList());
+ assigner.addSplit(0, wrapped);
+
+ assertThat(assigner.remainingSplits()).containsExactly(wrapped);
+ List<FileStoreSourceSplit> assigned = assigner.getNext(0, null);
+ assertThat(assigned).hasSize(1);
+ assertThat(assigned.get(0)).isSameAs(wrapped);
+ assertThat(assigned.get(0).split()).isInstanceOf(QueryAuthSplit.class);
+ }
+
+ @Test
+ public void testFallbackWrappedSplitsPrunedLikeTheSplitsTheyWrap() {
+ FileStoreSourceSplit kept = partitionSplit(1);
+ FileStoreSourceSplit pruned = partitionSplit(2);
+ List<FileStoreSourceSplit> splits = Arrays.asList(kept, pruned);
+
+ SplitAssigner plain = pruningAssigner(splits);
+ SplitAssigner nested =
pruningAssigner(withFallback(withQueryAuth(splits)));
+
+ assertThat(underlying(nested.remainingSplits()))
+ .isEqualTo(underlying(plain.remainingSplits()));
+
+ List<FileStoreSourceSplit> nestedNext = nested.getNext(0, null);
+
assertThat(underlying(nestedNext)).isEqualTo(underlying(plain.getNext(0,
null)));
+ assertThat(underlying(nestedNext)).containsExactly(kept.split());
+
+ assertThat(nested.getNext(0, null)).isEqualTo(plain.getNext(0,
null)).isEmpty();
+ }
+
+ @Test
+ public void testFallbackWrappedSplitIsKeptStillFallbackWrapped() {
+ FileStoreSourceSplit nested =
withFallback(withQueryAuth(partitionSplit(1)));
+
+ SplitAssigner assigner = pruningAssigner(Collections.emptyList());
+ assigner.addSplit(0, nested);
+
+ assertThat(assigner.remainingSplits()).containsExactly(nested);
+ List<FileStoreSourceSplit> assigned = assigner.getNext(0, null);
+ assertThat(assigned).hasSize(1);
+ assertThat(assigned.get(0)).isSameAs(nested);
+
assertThat(assigned.get(0).split()).isInstanceOf(FallbackSplitImpl.class);
+ assertThat(((FallbackSplitImpl) assigned.get(0).split()).wrapped())
+ .isInstanceOf(QueryAuthSplit.class);
+ }
+
+ @Test
+ public void testSplitIsKeptWhenAFilteringFieldIsMasked() {
+ FileStoreSourceSplit masked = withColumnMasking(partitionSplit(2),
"f0");
+
+ SplitAssigner assigner =
pruningAssigner(Collections.singletonList(masked));
+
+ assertThat(assigner.remainingSplits()).containsExactly(masked);
+ assertThat(assigner.getNext(0, null)).containsExactly(masked);
+ }
+
+ @Test
+ public void testSplitIsPrunedWhenTheMaskedColumnIsNotAFilteringField() {
+ FileStoreSourceSplit masked = withColumnMasking(partitionSplit(2),
"other");
+
+ SplitAssigner assigner =
pruningAssigner(Collections.singletonList(masked));
+
+ assertThat(assigner.remainingSplits()).isEmpty();
+ assertThat(assigner.getNext(0, null)).isEmpty();
+ }
+
+ @Test
+ public void testMaskedFilteringFieldKeepsTheSplitThroughAFallbackWrapper()
{
+ FileStoreSourceSplit masked =
withFallback(withColumnMasking(partitionSplit(2), "f0"));
+
+ SplitAssigner assigner =
pruningAssigner(Collections.singletonList(masked));
+
+ assertThat(assigner.remainingSplits()).containsExactly(masked);
+ }
+
+ private static SplitAssigner
pruningAssigner(Collection<FileStoreSourceSplit> splits) {
+ DynamicPartitionFilteringInfo filteringInfo =
+ new DynamicPartitionFilteringInfo(
+ RowType.of(DataTypes.INT()),
Collections.singletonList("f0"));
+ DynamicFilteringData filteringData =
+ new MockDynamicFilteringData(
+ org.apache.flink.table.types.logical.RowType.of(new
IntType()),
+ new RowData[] {GenericRowData.of(1)});
+ return new DynamicPartitionPruningAssigner(
+ new FIFOSplitAssigner(splits), filteringInfo, filteringData);
+ }
+
+ private static FileStoreSourceSplit partitionSplit(int partition) {
+ return new FileStoreSourceSplit(
+ UUID.randomUUID().toString(),
+ DataSplit.builder()
+ .withSnapshot(1)
+ .withPartition(row(partition))
+ .withBucket(0)
+ .withDataFiles(Collections.emptyList())
+ .isStreaming(true)
+ .withBucketPath("/temp/xxx") // not used
+ .build());
+ }
+
+ private static FileStoreSourceSplit withQueryAuth(FileStoreSourceSplit
split) {
+ return new FileStoreSourceSplit(
+ split.splitId(),
+ new QueryAuthSplit(split.split(), new
TableQueryAuthResult(null, null)),
+ split.recordsToSkip());
+ }
+
+ private static FileStoreSourceSplit withColumnMasking(
+ FileStoreSourceSplit split, String column) {
+ return new FileStoreSourceSplit(
+ split.splitId(),
+ new QueryAuthSplit(
+ split.split(),
+ new TableQueryAuthResult(
+ null,
+ Collections.singletonMap(
+ column,
JsonSerdeUtil.toJson(NullTransform.INSTANCE)))),
+ split.recordsToSkip());
+ }
+
+ private static List<FileStoreSourceSplit>
withQueryAuth(List<FileStoreSourceSplit> splits) {
+ return splits.stream()
+ .map(DynamicPartitionPruningAssignerTest::withQueryAuth)
+ .collect(Collectors.toList());
+ }
+
+ private static FileStoreSourceSplit withFallback(FileStoreSourceSplit
split) {
+ return new FileStoreSourceSplit(
+ split.splitId(),
+ FallbackReadFileStoreTable.toFallbackSplit(split.split(),
true),
+ split.recordsToSkip());
+ }
+
+ private static List<FileStoreSourceSplit>
withFallback(List<FileStoreSourceSplit> splits) {
+ return splits.stream()
+ .map(DynamicPartitionPruningAssignerTest::withFallback)
+ .collect(Collectors.toList());
+ }
+
+ private static List<Split> underlying(Collection<FileStoreSourceSplit>
splits) {
+ List<Split> underlying = new ArrayList<>();
+ for (FileStoreSourceSplit split : splits) {
+ Split inner = split.split();
+ if (inner instanceof FallbackSplitImpl) {
+ inner = ((FallbackSplitImpl) inner).wrapped();
+ }
+ underlying.add(
+ inner instanceof QueryAuthSplit ? ((QueryAuthSplit)
inner).split() : inner);
+ }
+ return underlying;
+ }
+
+ private static class MockDynamicFilteringData extends DynamicFilteringData
{
+
+ private final org.apache.flink.table.types.logical.RowType rowType;
+ private final RowData[] neededPartitions;
+
+ public MockDynamicFilteringData(
+ org.apache.flink.table.types.logical.RowType rowType,
RowData[] neededPartitions) {
+ super(new GenericTypeInfo<>(RowData.class), rowType,
Collections.emptyList(), true);
+ this.rowType = rowType;
+ this.neededPartitions = neededPartitions;
+ }
+
+ public boolean contains(RowData row) {
+ checkArgument(rowType.getFieldCount() == row.getArity());
+ for (RowData mayMatch : neededPartitions) {
+ if (matchRow(row, mayMatch)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean matchRow(RowData row, RowData mayMatch) {
+ for (int i = 0; i < rowType.getFieldCount(); i++) {
+ if (row.getInt(i) != mayMatch.getInt(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/PreAssignSplitAssignerTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/PreAssignSplitAssignerTest.java
new file mode 100644
index 0000000000..ea1e54873b
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/assigners/PreAssignSplitAssignerTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.paimon.flink.source.assigners;
+
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.flink.source.DynamicPartitionFilteringInfo;
+import org.apache.paimon.flink.source.FileStoreSourceSplit;
+import org.apache.paimon.predicate.NullTransform;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.QueryAuthSplit;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.JsonSerdeUtil;
+
+import org.apache.flink.api.java.typeutils.GenericTypeInfo;
+import org.apache.flink.table.connector.source.DynamicFilteringData;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.types.logical.IntType;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.apache.paimon.io.DataFileTestUtils.row;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests {@link PreAssignSplitAssigner} pruning over {@link QueryAuthSplit}.
*/
+public class PreAssignSplitAssignerTest {
+
+ @Test
+ public void testWrappedSplitsPrunedLikeTheSplitsTheyWrap() {
+ FileStoreSourceSplit keptOne = partitionSplit(1);
+ FileStoreSourceSplit pruned = partitionSplit(2);
+ FileStoreSourceSplit keptThree = partitionSplit(3);
+ List<FileStoreSourceSplit> splits = Arrays.asList(keptOne, pruned,
keptThree);
+
+ SplitAssigner plain = pruningAssigner(splits);
+ SplitAssigner authorized = pruningAssigner(withQueryAuth(splits));
+
+ assertThat(underlying(plain.remainingSplits()))
+ .containsExactly(keptOne.split(), keptThree.split());
+ assertThat(underlying(authorized.remainingSplits()))
+ .isEqualTo(underlying(plain.remainingSplits()));
+
assertThat(authorized.numberOfRemainingSplits()).isEqualTo(plain.numberOfRemainingSplits());
+
+ List<FileStoreSourceSplit> authorizedNext = authorized.getNext(0,
null);
+
assertThat(underlying(authorizedNext)).isEqualTo(underlying(plain.getNext(0,
null)));
+
assertThat(underlying(authorizedNext)).containsExactly(keptOne.split(),
keptThree.split());
+
+ assertThat(authorized.getNext(0, null)).isEqualTo(plain.getNext(0,
null)).isEmpty();
+
assertThat(authorized.numberOfRemainingSplits()).isEqualTo(plain.numberOfRemainingSplits());
+ }
+
+ @Test
+ public void testWrappedSplitIsKeptStillWrapped() {
+ FileStoreSourceSplit wrapped = withQueryAuth(partitionSplit(1));
+
+ SplitAssigner assigner =
pruningAssigner(Collections.singletonList(wrapped));
+
+ assertThat(assigner.remainingSplits()).containsExactly(wrapped);
+ List<FileStoreSourceSplit> assigned = assigner.getNext(0, null);
+ assertThat(assigned).hasSize(1);
+ assertThat(assigned.get(0)).isSameAs(wrapped);
+ assertThat(assigned.get(0).split()).isInstanceOf(QueryAuthSplit.class);
+ }
+
+ @Test
+ public void testWrappedSplitOutsideTheFilterIsNotKept() {
+ FileStoreSourceSplit pruned = partitionSplit(2);
+
+ SplitAssigner plain =
pruningAssigner(Collections.singletonList(pruned));
+ SplitAssigner authorized =
+
pruningAssigner(Collections.singletonList(withQueryAuth(pruned)));
+
+ assertThat(plain.remainingSplits()).isEmpty();
+ assertThat(authorized.remainingSplits()).isEmpty();
+
assertThat(authorized.numberOfRemainingSplits()).isEqualTo(plain.numberOfRemainingSplits());
+ assertThat(authorized.getNext(0, null)).isEqualTo(plain.getNext(0,
null)).isEmpty();
+ }
+
+ @Test
+ public void testSplitIsKeptWhenAFilteringFieldIsMasked() {
+ FileStoreSourceSplit masked = withColumnMasking(partitionSplit(2),
"f0");
+
+ SplitAssigner assigner =
pruningAssigner(Collections.singletonList(masked));
+
+ assertThat(assigner.remainingSplits()).containsExactly(masked);
+ assertThat(assigner.numberOfRemainingSplits()).isEqualTo(1);
+ assertThat(assigner.getNext(0, null)).containsExactly(masked);
+ }
+
+ @Test
+ public void testSplitIsPrunedWhenTheMaskedColumnIsNotAFilteringField() {
+ FileStoreSourceSplit masked = withColumnMasking(partitionSplit(2),
"other");
+
+ SplitAssigner assigner =
pruningAssigner(Collections.singletonList(masked));
+
+ assertThat(assigner.remainingSplits()).isEmpty();
+ assertThat(assigner.numberOfRemainingSplits()).isZero();
+ assertThat(assigner.getNext(0, null)).isEmpty();
+ }
+
+ private static SplitAssigner
pruningAssigner(Collection<FileStoreSourceSplit> splits) {
+ DynamicPartitionFilteringInfo filteringInfo =
+ new DynamicPartitionFilteringInfo(
+ RowType.of(DataTypes.INT()),
Collections.singletonList("f0"));
+ DynamicFilteringData filteringData =
+ new MockDynamicFilteringData(
+ org.apache.flink.table.types.logical.RowType.of(new
IntType()),
+ new RowData[] {GenericRowData.of(1),
GenericRowData.of(3)});
+ return new PreAssignSplitAssigner(10, 1, splits)
+ .ofDynamicPartitionPruning(filteringInfo, filteringData);
+ }
+
+ private static FileStoreSourceSplit partitionSplit(int partition) {
+ return new FileStoreSourceSplit(
+ UUID.randomUUID().toString(),
+ DataSplit.builder()
+ .withSnapshot(1)
+ .withPartition(row(partition))
+ .withBucket(0)
+ .withDataFiles(Collections.emptyList())
+ .isStreaming(false)
+ .withBucketPath("/temp/xxx")
+ .build());
+ }
+
+ private static FileStoreSourceSplit withQueryAuth(FileStoreSourceSplit
split) {
+ return new FileStoreSourceSplit(
+ split.splitId(),
+ new QueryAuthSplit(split.split(), new
TableQueryAuthResult(null, null)),
+ split.recordsToSkip());
+ }
+
+ private static FileStoreSourceSplit withColumnMasking(
+ FileStoreSourceSplit split, String column) {
+ return new FileStoreSourceSplit(
+ split.splitId(),
+ new QueryAuthSplit(
+ split.split(),
+ new TableQueryAuthResult(
+ null,
+ Collections.singletonMap(
+ column,
JsonSerdeUtil.toJson(NullTransform.INSTANCE)))),
+ split.recordsToSkip());
+ }
+
+ private static List<FileStoreSourceSplit>
withQueryAuth(List<FileStoreSourceSplit> splits) {
+ return splits.stream()
+ .map(PreAssignSplitAssignerTest::withQueryAuth)
+ .collect(Collectors.toList());
+ }
+
+ private static List<Split> underlying(Collection<FileStoreSourceSplit>
splits) {
+ List<Split> underlying = new ArrayList<>();
+ for (FileStoreSourceSplit split : splits) {
+ Split inner = split.split();
+ underlying.add(
+ inner instanceof QueryAuthSplit ? ((QueryAuthSplit)
inner).split() : inner);
+ }
+ return underlying;
+ }
+
+ private static class MockDynamicFilteringData extends DynamicFilteringData
{
+
+ private final org.apache.flink.table.types.logical.RowType rowType;
+ private final RowData[] neededPartitions;
+
+ public MockDynamicFilteringData(
+ org.apache.flink.table.types.logical.RowType rowType,
RowData[] neededPartitions) {
+ super(new GenericTypeInfo<>(RowData.class), rowType,
Collections.emptyList(), true);
+ this.rowType = rowType;
+ this.neededPartitions = neededPartitions;
+ }
+
+ public boolean contains(RowData row) {
+ checkArgument(rowType.getFieldCount() == row.getArity());
+ for (RowData mayMatch : neededPartitions) {
+ if (matchRow(row, mayMatch)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean matchRow(RowData row, RowData mayMatch) {
+ for (int i = 0; i < rowType.getFieldCount(); i++) {
+ if (row.getInt(i) != mayMatch.getInt(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/operator/ReadOperatorSplitShapeTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/operator/ReadOperatorSplitShapeTest.java
new file mode 100644
index 0000000000..b2c6574748
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/operator/ReadOperatorSplitShapeTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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.paimon.flink.source.operator;
+
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManager;
+import org.apache.paimon.flink.utils.TestingMetricUtils;
+import org.apache.paimon.metrics.MetricRegistry;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FallbackReadFileStoreTable.FallbackSplitImpl;
+import org.apache.paimon.table.source.ChainSplit;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.QueryAuthSplit;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.TableRead;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.IteratorRecordReader;
+
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+import org.apache.flink.table.runtime.typeutils.InternalSerializers;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.apache.paimon.flink.LogicalTypeConversion.toLogicalType;
+import static org.apache.paimon.io.DataFileTestUtils.newFile;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests {@link ReadOperator} over every shape of {@link Split} it can be
handed. */
+public class ReadOperatorSplitShapeTest {
+
+ private static final RowType READ_TYPE = RowType.of(DataTypes.INT());
+
+ private static final int VALUE = 42;
+
+ @Test
+ public void testWrappedDataSplitReadsTheRowsOfTheSplitItWraps() throws
Exception {
+ Split split = dataSplit();
+
+ ReadResult plain = read(split);
+ ReadResult authorized = read(withQueryAuth(split));
+
+ assertThat(plain.values).containsExactly(VALUE);
+ assertThat(authorized.values).isEqualTo(plain.values);
+ }
+
+ @Test
+ public void testWrappedDataSplitIsHandedToTheReadStillWrapped() throws
Exception {
+ Split wrapped = withQueryAuth(dataSplit());
+
+ ReadResult authorized = read(wrapped);
+
+ assertThat(authorized.readSplit).isSameAs(wrapped);
+ assertThat(authorized.readSplit).isInstanceOf(QueryAuthSplit.class);
+ }
+
+ @Test
+ public void testSplitThatIsNotADataSplitIsReadWrappedOrNot() throws
Exception {
+ ReadResult plain = read(chainSplit());
+ ReadResult authorized = read(withQueryAuth(chainSplit()));
+
+ assertThat(plain.values).containsExactly(VALUE);
+ assertThat(authorized.values).isEqualTo(plain.values);
+ }
+
+ @Test
+ public void
testFallbackWrappedQueryAuthSplitReadsTheRowsOfTheSplitItWraps() throws
Exception {
+ Split split = dataSplit();
+
+ ReadResult plain = read(split);
+ ReadResult nested = read(withFallback(withQueryAuth(split)));
+
+ assertThat(plain.values).containsExactly(VALUE);
+ assertThat(nested.values).isEqualTo(plain.values);
+ }
+
+ @Test
+ public void
testFallbackWrappedQueryAuthSplitIsHandedToTheReadStillWrapped() throws
Exception {
+ Split nested = withFallback(withQueryAuth(dataSplit()));
+
+ ReadResult result = read(nested);
+
+ assertThat(result.readSplit).isSameAs(nested);
+ assertThat(result.readSplit).isInstanceOf(FallbackSplitImpl.class);
+ }
+
+ @Test
+ public void
testFallbackWrappedQueryAuthSplitKeepsTheEventTimeOfTheSplitItWraps()
+ throws Exception {
+ Split split = dataSplit();
+
+ ReadResult plain = read(split);
+ ReadResult authorized = read(withQueryAuth(split));
+ ReadResult nested = read(withFallback(withQueryAuth(split)));
+ ReadResult fallback = read(withFallback(split));
+
+ assertThat(plain.fetchEventTimeLag).isGreaterThan(0L);
+ assertThat(authorized.fetchEventTimeLag).isGreaterThan(0L);
+ assertThat(nested.fetchEventTimeLag).isGreaterThan(0L);
+ assertThat(fallback.fetchEventTimeLag).isGreaterThan(0L);
+ }
+
+ @Test
+ public void testSplitWithoutAnEventTimeLeavesTheMetricUndefined() throws
Exception {
+ assertThat(read(chainSplit()).fetchEventTimeLag).isEqualTo(-1L);
+
assertThat(read(withFallback(withQueryAuth(chainSplit()))).fetchEventTimeLag)
+ .isEqualTo(-1L);
+ }
+
+ private static ReadResult read(Split split) throws Exception {
+ RecordingTableRead tableRead = new RecordingTableRead();
+ ReadOperator operator = new ReadOperator(() -> tableRead, null, null,
READ_TYPE, false);
+
+ OneInputStreamOperatorTestHarness<Split, RowData> harness =
+ new OneInputStreamOperatorTestHarness<>(operator);
+ harness.setup(InternalSerializers.create(toLogicalType(READ_TYPE)));
+ harness.open();
+ try {
+ harness.processElement(new StreamRecord<>(split));
+
+ List<Integer> values = new ArrayList<>();
+ for (Object record : harness.getOutput()) {
+ @SuppressWarnings("unchecked")
+ StreamRecord<RowData> streamRecord = (StreamRecord<RowData>)
record;
+ values.add(streamRecord.getValue().getInt(0));
+ }
+ long fetchEventTimeLag =
+ (Long)
+ TestingMetricUtils.getGauge(
+ operator.getMetricGroup(),
"currentFetchEventTimeLag")
+ .getValue();
+ return new ReadResult(values, tableRead.split, fetchEventTimeLag);
+ } finally {
+ harness.close();
+ }
+ }
+
+ private static Split dataSplit() {
+ return DataSplit.builder()
+ .withSnapshot(1)
+ .withPartition(BinaryRow.EMPTY_ROW)
+ .withBucket(0)
+ .withBucketPath("bucket-0")
+ .withDataFiles(Collections.singletonList(newFile(0L, 1L)))
+ .build();
+ }
+
+ private static Split chainSplit() {
+ return new ChainSplit(
+ BinaryRow.EMPTY_ROW,
+ Collections.singletonList(newFile(0L, 1L)),
+ Collections.emptyMap(),
+ Collections.emptyMap(),
+ null);
+ }
+
+ private static Split withQueryAuth(Split split) {
+ return new QueryAuthSplit(split, new TableQueryAuthResult(null, null));
+ }
+
+ private static Split withFallback(Split split) {
+ return FallbackReadFileStoreTable.toFallbackSplit(split, true);
+ }
+
+ private static class ReadResult {
+
+ private final List<Integer> values;
+ private final Split readSplit;
+ private final long fetchEventTimeLag;
+
+ private ReadResult(List<Integer> values, Split readSplit, long
fetchEventTimeLag) {
+ this.values = values;
+ this.readSplit = readSplit;
+ this.fetchEventTimeLag = fetchEventTimeLag;
+ }
+ }
+
+ private static class RecordingTableRead implements TableRead {
+
+ private Split split;
+
+ @Override
+ public TableRead withMetricRegistry(MetricRegistry registry) {
+ return this;
+ }
+
+ @Override
+ public TableRead executeFilter() {
+ return this;
+ }
+
+ @Override
+ public TableRead withIOManager(IOManager ioManager) {
+ return this;
+ }
+
+ @Override
+ public RecordReader<InternalRow> createReader(Split split) throws
IOException {
+ this.split = split;
+ InternalRow row = GenericRow.of(VALUE);
+ return new
IteratorRecordReader<>(Collections.singleton(row).iterator());
+ }
+ }
+}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/utils/TableScanUtilsTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/utils/TableScanUtilsTest.java
new file mode 100644
index 0000000000..c32d531263
--- /dev/null
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/utils/TableScanUtilsTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.paimon.flink.utils;
+
+import org.apache.paimon.catalog.TableQueryAuthResult;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.flink.source.FileStoreSourceSplit;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.QueryAuthSplit;
+import org.apache.paimon.table.source.Split;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.OptionalLong;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link TableScanUtils}. */
+public class TableScanUtilsTest {
+
+ @Test
+ public void testSnapshotIdReadThroughTheAuthorizationWrapper() {
+ FileStoreSourceSplit plain = sourceSplit(dataSplit(7L));
+ FileStoreSourceSplit wrapped = sourceSplit(new
QueryAuthSplit(plain.split(), authResult()));
+
+ assertThat(TableScanUtils.getSnapshotId(plain)).hasValue(7L);
+ assertThat(TableScanUtils.getSnapshotId(wrapped))
+ .isEqualTo(TableScanUtils.getSnapshotId(plain));
+ }
+
+ @Test
+ public void testSnapshotIdOfASplitWithoutOneIsEmpty() {
+ Split noSnapshotId =
+ new Split() {
+ @Override
+ public long rowCount() {
+ return 0;
+ }
+
+ @Override
+ public OptionalLong mergedRowCount() {
+ return OptionalLong.empty();
+ }
+ };
+
+ assertThat(TableScanUtils.getSnapshotId(sourceSplit(noSnapshotId)))
+ .isEqualTo(Optional.empty());
+ assertThat(
+ TableScanUtils.getSnapshotId(
+ sourceSplit(new QueryAuthSplit(noSnapshotId,
authResult()))))
+ .isEqualTo(Optional.empty());
+ }
+
+ @Test
+ public void testSnapshotIdReadThroughTheFallbackAndAuthorizationWrappers()
{
+ FileStoreSourceSplit plain = sourceSplit(dataSplit(7L));
+ FileStoreSourceSplit nested =
+ sourceSplit(withFallback(new QueryAuthSplit(plain.split(),
authResult())));
+
+ assertThat(nested.split()).isNotInstanceOf(DataSplit.class);
+ assertThat(TableScanUtils.getSnapshotId(nested))
+ .isEqualTo(TableScanUtils.getSnapshotId(plain))
+ .hasValue(7L);
+ }
+
+ @Test
+ public void testSnapshotIdOfAFallbackDataSplitIsTheSnapshotItCopies() {
+ FileStoreSourceSplit fallback =
sourceSplit(withFallback(dataSplit(7L)));
+
+ assertThat(fallback.split()).isInstanceOf(DataSplit.class);
+ assertThat(TableScanUtils.getSnapshotId(fallback)).hasValue(7L);
+ }
+
+ private static TableQueryAuthResult authResult() {
+ return new TableQueryAuthResult(null, null);
+ }
+
+ private static Split withFallback(Split split) {
+ return FallbackReadFileStoreTable.toFallbackSplit(split, true);
+ }
+
+ private static DataSplit dataSplit(long snapshotId) {
+ return DataSplit.builder()
+ .withSnapshot(snapshotId)
+ .withPartition(BinaryRow.EMPTY_ROW)
+ .withBucket(0)
+ .withDataFiles(Collections.emptyList())
+ .isStreaming(true)
+ .withBucketPath("/temp/xxx")
+ .build();
+ }
+
+ private static FileStoreSourceSplit sourceSplit(Split split) {
+ return new FileStoreSourceSplit(UUID.randomUUID().toString(), split,
0);
+ }
+}