Skip to content

Commit 7e8c5d8

Browse files
author
hongli.wwj
committed
[flink] Expose scan.bucket for single-bucket manifest pruning
1 parent 4a5462d commit 7e8c5d8

8 files changed

Lines changed: 277 additions & 9 deletions

File tree

docs/generated/flink_connector_configuration.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@
152152
<td>Boolean</td>
153153
<td>Bounded mode for Paimon consumer. By default, Paimon automatically selects bounded mode based on the mode of the Flink job.</td>
154154
</tr>
155+
<tr>
156+
<td><h5>scan.bucket</h5></td>
157+
<td style="word-wrap: break-word;">(none)</td>
158+
<td>Integer</td>
159+
<td>Specify a single bucket to scan. This option filters manifest entries and only plans splits for the given bucket. It is only supported for fixed-bucket primary key tables (bucket &gt; 0). It cannot be used with postpone bucket tables.</td>
160+
</tr>
155161
<tr>
156162
<td><h5>scan.dedicated-split-generation</h5></td>
157163
<td style="word-wrap: break-word;">false</td>

paimon-core/src/main/java/org/apache/paimon/table/source/ReadBuilderImpl.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.apache.paimon.predicate.Predicate;
2424
import org.apache.paimon.predicate.PredicateBuilder;
2525
import org.apache.paimon.predicate.TopN;
26+
import org.apache.paimon.table.BucketMode;
27+
import org.apache.paimon.table.FileStoreTable;
2628
import org.apache.paimon.table.InnerTable;
2729
import org.apache.paimon.types.RowType;
2830
import org.apache.paimon.utils.Filter;
@@ -37,6 +39,7 @@
3739

3840
import static org.apache.paimon.partition.PartitionPredicate.createPartitionPredicate;
3941
import static org.apache.paimon.partition.PartitionPredicate.fromPredicate;
42+
import static org.apache.paimon.utils.Preconditions.checkArgument;
4043
import static org.apache.paimon.utils.Preconditions.checkState;
4144

4245
/** Implementation for {@link ReadBuilder}. */
@@ -161,10 +164,36 @@ public ReadBuilder withRowRangeIndex(RowRangeIndex rowRangeIndex) {
161164

162165
@Override
163166
public ReadBuilder withBucket(int bucket) {
167+
validateSpecifiedBucket(table, bucket);
164168
this.specifiedBucket = bucket;
165169
return this;
166170
}
167171

172+
/**
173+
* Validates bucket id before manifest pruning ({@link InnerTableScan#withBucket(int)}). Callers
174+
* such as Flink {@code scan.bucket} should route through {@link #withBucket(int)}.
175+
*/
176+
static void validateSpecifiedBucket(InnerTable table, int bucket) {
177+
checkArgument(bucket >= 0, "Bucket id must be non-negative, but is %s.", bucket);
178+
if (!(table instanceof FileStoreTable)) {
179+
throw new IllegalArgumentException(
180+
"Bucket scan is only supported for FileStoreTable, but got "
181+
+ table.getClass().getName());
182+
}
183+
FileStoreTable fileStoreTable = (FileStoreTable) table;
184+
checkArgument(
185+
fileStoreTable.bucketMode() != BucketMode.POSTPONE_MODE,
186+
"Bucket scan is not supported for postpone bucket tables.");
187+
int numBuckets = CoreOptions.fromMap(fileStoreTable.options()).bucket();
188+
if (numBuckets > 0) {
189+
checkArgument(
190+
bucket < numBuckets,
191+
"Bucket id %s must be less than table bucket number %s.",
192+
bucket,
193+
numBuckets);
194+
}
195+
}
196+
168197
@Override
169198
public ReadBuilder withBucketFilter(Filter<Integer> bucketFilter) {
170199
this.bucketFilter = bucketFilter;

paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkConnectorOptions.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,16 @@ public class FlinkConnectorOptions {
274274
+ "normal source, the max partition(s) will be determined before job running "
275275
+ "without refreshing even for streaming jobs.");
276276

277+
public static final ConfigOption<Integer> SCAN_BUCKET =
278+
ConfigOptions.key("scan.bucket")
279+
.intType()
280+
.noDefaultValue()
281+
.withDescription(
282+
"Specify a single bucket to scan. This option filters manifest entries "
283+
+ "and only plans splits for the given bucket. It is only supported "
284+
+ "for fixed-bucket primary key tables (bucket > 0). It cannot be used "
285+
+ "with postpone bucket tables.");
286+
277287
public static final ConfigOption<Duration> LOOKUP_DYNAMIC_PARTITION_REFRESH_INTERVAL =
278288
ConfigOptions.key("lookup.dynamic-partition.refresh-interval")
279289
.durationType()

paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkSourceBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.paimon.flink.sink.FlinkSink;
2727
import org.apache.paimon.flink.source.align.AlignedContinuousFileStoreSource;
2828
import org.apache.paimon.flink.source.operator.MonitorSource;
29+
import org.apache.paimon.flink.utils.ScanBucketUtils;
2930
import org.apache.paimon.flink.utils.TableScanUtils;
3031
import org.apache.paimon.options.Options;
3132
import org.apache.paimon.partition.PartitionPredicate;
@@ -200,6 +201,7 @@ private ReadBuilder createReadBuilder(@Nullable org.apache.paimon.types.RowType
200201
if (limit != null) {
201202
readBuilder.withLimit(limit.intValue());
202203
}
204+
ScanBucketUtils.applyScanBucket(table, readBuilder, conf);
203205
return readBuilder.dropStats();
204206
}
205207

paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/source/FlinkTableSource.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.paimon.flink.lookup.DynamicPartitionLoader;
2727
import org.apache.paimon.flink.lookup.PartitionLoader;
2828
import org.apache.paimon.flink.lookup.StaticPartitionLoader;
29+
import org.apache.paimon.flink.utils.ScanBucketUtils;
2930
import org.apache.paimon.manifest.PartitionEntry;
3031
import org.apache.paimon.options.Options;
3132
import org.apache.paimon.partition.PartitionPredicate;
@@ -35,6 +36,7 @@
3536
import org.apache.paimon.predicate.PredicateVisitor;
3637
import org.apache.paimon.table.DataTable;
3738
import org.apache.paimon.table.Table;
39+
import org.apache.paimon.table.source.ReadBuilder;
3840
import org.apache.paimon.table.source.Split;
3941
import org.apache.paimon.utils.RowDataToObjectArrayConverter;
4042

@@ -245,13 +247,14 @@ protected Integer inferSourceParallelism(StreamExecutionEnvironment env) {
245247
protected void scanSplitsForInference() {
246248
if (splitStatistics == null) {
247249
if (table instanceof DataTable) {
248-
List<PartitionEntry> partitionEntries =
250+
ReadBuilder readBuilder =
249251
table.newReadBuilder()
250252
.dropStats()
251253
.withFilter(predicate)
252-
.withPartitionFilter(partitionPredicate)
253-
.newScan()
254-
.listPartitionEntries();
254+
.withPartitionFilter(partitionPredicate);
255+
ScanBucketUtils.applyScanBucket(table, readBuilder, options);
256+
List<PartitionEntry> partitionEntries =
257+
readBuilder.newScan().listPartitionEntries();
255258
long totalSize = 0;
256259
long rowCount = 0;
257260
for (PartitionEntry entry : partitionEntries) {
@@ -262,15 +265,14 @@ protected void scanSplitsForInference() {
262265
splitStatistics =
263266
new SplitStatistics((int) (totalSize / splitTargetSize + 1), rowCount);
264267
} else {
265-
List<Split> splits =
268+
ReadBuilder readBuilder =
266269
table.newReadBuilder()
267270
.dropStats()
268271
.withFilter(predicate)
269272
.withPartitionFilter(partitionPredicate)
270-
.withProjection(new int[0])
271-
.newScan()
272-
.plan()
273-
.splits();
273+
.withProjection(new int[0]);
274+
ScanBucketUtils.applyScanBucket(table, readBuilder, options);
275+
List<Split> splits = readBuilder.newScan().plan().splits();
274276
splitStatistics =
275277
new SplitStatistics(
276278
splits.size(), splits.stream().mapToLong(Split::rowCount).sum());
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.paimon.flink.utils;
20+
21+
import org.apache.paimon.flink.FlinkConnectorOptions;
22+
import org.apache.paimon.options.Options;
23+
import org.apache.paimon.table.Table;
24+
import org.apache.paimon.table.source.ReadBuilder;
25+
26+
/** Utilities for {@link FlinkConnectorOptions#SCAN_BUCKET}. */
27+
public class ScanBucketUtils {
28+
29+
private ScanBucketUtils() {}
30+
31+
/** Apply {@link FlinkConnectorOptions#SCAN_BUCKET} to the given {@link ReadBuilder}. */
32+
public static ReadBuilder applyScanBucket(
33+
Table table, ReadBuilder readBuilder, Options options) {
34+
Integer scanBucket = options.get(FlinkConnectorOptions.SCAN_BUCKET);
35+
if (scanBucket == null) {
36+
return readBuilder;
37+
}
38+
return readBuilder.withBucket(scanBucket);
39+
}
40+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.paimon.flink;
20+
21+
import org.apache.paimon.data.InternalRow;
22+
import org.apache.paimon.manifest.ManifestEntry;
23+
import org.apache.paimon.reader.RecordReader;
24+
import org.apache.paimon.reader.RecordReaderIterator;
25+
import org.apache.paimon.table.FileStoreTable;
26+
import org.apache.paimon.table.source.DataSplit;
27+
28+
import org.apache.flink.types.Row;
29+
import org.junit.jupiter.api.Test;
30+
31+
import javax.annotation.Nullable;
32+
33+
import java.util.ArrayList;
34+
import java.util.Collections;
35+
import java.util.List;
36+
37+
import static java.util.Collections.singletonList;
38+
import static org.assertj.core.api.Assertions.assertThat;
39+
40+
/** ITCase for {@link org.apache.paimon.flink.FlinkConnectorOptions#SCAN_BUCKET}. */
41+
public class ScanBucketITCase extends CatalogITCaseBase {
42+
43+
@Override
44+
protected List<String> ddl() {
45+
return singletonList(
46+
"CREATE TABLE IF NOT EXISTS T (id INT, v INT, PRIMARY KEY (id) NOT ENFORCED) "
47+
+ "WITH ('bucket' = '4')");
48+
}
49+
50+
@Nullable
51+
@Override
52+
protected Boolean sqlSyncMode() {
53+
return true;
54+
}
55+
56+
@Test
57+
public void testScanBucketFilter() throws Exception {
58+
sql(
59+
"INSERT INTO T VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60), (7, 70), (8, 80)");
60+
61+
FileStoreTable table = paimonTable("T");
62+
63+
int targetBucket = 0;
64+
for (int bucket = 0; bucket < 4; bucket++) {
65+
List<ManifestEntry> files = table.store().newScan().withBucket(bucket).plan().files();
66+
if (!files.isEmpty()) {
67+
targetBucket = bucket;
68+
break;
69+
}
70+
}
71+
72+
List<Row> expected = readRowsFromBucket(table, targetBucket);
73+
74+
List<Row> actual =
75+
batchSql(
76+
String.format(
77+
"SELECT * FROM T /*+ OPTIONS('scan.bucket' = '%s') */",
78+
targetBucket));
79+
80+
assertThat(actual).containsExactlyInAnyOrderElementsOf(expected);
81+
}
82+
83+
private List<Row> readRowsFromBucket(FileStoreTable table, int bucket) throws Exception {
84+
List<ManifestEntry> files = table.store().newScan().withBucket(bucket).plan().files();
85+
List<Row> rows = new ArrayList<>();
86+
for (ManifestEntry file : files) {
87+
DataSplit split =
88+
DataSplit.builder()
89+
.withPartition(file.partition())
90+
.withBucket(file.bucket())
91+
.withDataFiles(Collections.singletonList(file.file()))
92+
.withBucketPath("not used")
93+
.build();
94+
RecordReader<InternalRow> reader = table.newReadBuilder().newRead().createReader(split);
95+
RecordReaderIterator<InternalRow> iterator = new RecordReaderIterator<>(reader);
96+
while (iterator.hasNext()) {
97+
InternalRow row = iterator.next();
98+
rows.add(Row.of(row.getInt(0), row.getInt(1)));
99+
}
100+
iterator.close();
101+
}
102+
return rows;
103+
}
104+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.paimon.flink.utils;
20+
21+
import org.apache.paimon.CoreOptions;
22+
import org.apache.paimon.catalog.Catalog;
23+
import org.apache.paimon.catalog.FileSystemCatalog;
24+
import org.apache.paimon.catalog.Identifier;
25+
import org.apache.paimon.flink.FlinkConnectorOptions;
26+
import org.apache.paimon.flink.util.AbstractTestBase;
27+
import org.apache.paimon.fs.Path;
28+
import org.apache.paimon.fs.local.LocalFileIO;
29+
import org.apache.paimon.options.Options;
30+
import org.apache.paimon.schema.Schema;
31+
import org.apache.paimon.table.FileStoreTable;
32+
import org.apache.paimon.table.source.ReadBuilder;
33+
import org.apache.paimon.types.DataTypes;
34+
35+
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.api.io.TempDir;
37+
38+
import java.util.HashMap;
39+
import java.util.Map;
40+
41+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
42+
43+
/** Tests for {@link ScanBucketUtils}. */
44+
public class ScanBucketUtilsTest extends AbstractTestBase {
45+
46+
@TempDir java.nio.file.Path tempDir;
47+
48+
@Test
49+
public void testInvalidBucket() throws Exception {
50+
FileStoreTable table = createTable(4);
51+
Options options = new Options();
52+
options.set(FlinkConnectorOptions.SCAN_BUCKET, 5);
53+
ReadBuilder readBuilder = table.newReadBuilder();
54+
assertThatThrownBy(() -> ScanBucketUtils.applyScanBucket(table, readBuilder, options))
55+
.isInstanceOf(IllegalArgumentException.class)
56+
.hasMessageContaining("Bucket id 5 must be less than table bucket number 4");
57+
}
58+
59+
private FileStoreTable createTable(int numBuckets) throws Exception {
60+
Map<String, String> options = new HashMap<>();
61+
options.put(CoreOptions.BUCKET.key(), String.valueOf(numBuckets));
62+
Schema schema =
63+
Schema.newBuilder()
64+
.column("id", DataTypes.INT())
65+
.column("v", DataTypes.INT())
66+
.primaryKey("id")
67+
.options(options)
68+
.build();
69+
Catalog catalog = new FileSystemCatalog(LocalFileIO.create(), new Path(tempDir.toString()));
70+
catalog.createDatabase("default", true);
71+
Identifier identifier = Identifier.create("default", "test_bucket");
72+
catalog.createTable(identifier, schema, false);
73+
return (FileStoreTable) catalog.getTable(identifier);
74+
}
75+
}

0 commit comments

Comments
 (0)