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 b1a68a57d2 [core] Wait for global index in visibility callback (#8265)
b1a68a57d2 is described below
commit b1a68a57d2e28374f84fc0f52e3b7be86f19c7ff
Author: Jingsong Lee <[email protected]>
AuthorDate: Wed Jun 17 20:09:44 2026 +0800
[core] Wait for global index in visibility callback (#8265)
Support waiting for newly committed row-tracking data to be covered by
existing global indexes when `visibility-callback.enabled` is enabled.
This extends the visibility callback beyond compaction so batch writes
do not return before global index visibility catches up.
---
docs/generated/core_configuration.html | 4 +-
.../main/java/org/apache/paimon/CoreOptions.java | 12 +-
.../java/org/apache/paimon/AbstractFileStore.java | 19 +-
.../metastore/GlobalIndexVisibilityChecker.java | 222 +++++++++++++++++++++
.../paimon/metastore/VisibilityWaitCallback.java | 44 +++-
.../metastore/VisibilityWaitCallbackTest.java | 214 ++++++++++++++++++++
6 files changed, 494 insertions(+), 21 deletions(-)
diff --git a/docs/generated/core_configuration.html
b/docs/generated/core_configuration.html
index 45e47fdf23..c17b6a5007 100644
--- a/docs/generated/core_configuration.html
+++ b/docs/generated/core_configuration.html
@@ -1678,13 +1678,13 @@ If the data size allocated for the sorting task is
uneven,which may lead to perf
<td><h5>visibility-callback.enabled</h5></td>
<td style="word-wrap: break-word;">false</td>
<td>Boolean</td>
- <td>Whether to enable the visibility wait callback that waits for
compaction to complete after commit. This is useful for primary key tables with
deletion vectors or postpone bucket mode to ensure data visibility, only used
for batch mode or bounded stream.</td>
+ <td>Whether to enable the visibility wait callback that waits for
compaction or global index build to complete after commit. This is useful for
primary key tables with deletion vectors or postpone bucket mode and
row-tracking tables with global indexes to ensure data visibility, only used
for batch mode or bounded stream.</td>
</tr>
<tr>
<td><h5>visibility-callback.timeout</h5></td>
<td style="word-wrap: break-word;">30 min</td>
<td>Duration</td>
- <td>The maximum time to wait for compaction to complete when
visibility callback is enabled. If the timeout is reached, an exception will be
thrown.</td>
+ <td>The maximum time to wait for compaction or global index build
to complete when visibility callback is enabled. If the timeout is reached, an
exception will be thrown.</td>
</tr>
<tr>
<td><h5>write-buffer-for-append</h5></td>
diff --git a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
index 4a4b7b5f81..81bf1fe0e0 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -2554,17 +2554,19 @@ public class CoreOptions implements Serializable {
.booleanType()
.defaultValue(false)
.withDescription(
- "Whether to enable the visibility wait callback
that waits for compaction to complete "
- + "after commit. This is useful for
primary key tables with deletion vectors or "
- + "postpone bucket mode to ensure data
visibility, only used for batch mode or bounded stream.");
+ "Whether to enable the visibility wait callback
that waits for compaction or global "
+ + "index build to complete after commit.
This is useful for primary key tables "
+ + "with deletion vectors or postpone
bucket mode and row-tracking tables with "
+ + "global indexes to ensure data
visibility, only used for batch mode or bounded stream.");
public static final ConfigOption<Duration> VISIBILITY_CALLBACK_TIMEOUT =
key("visibility-callback.timeout")
.durationType()
.defaultValue(Duration.ofMinutes(30))
.withDescription(
- "The maximum time to wait for compaction to
complete when visibility callback is enabled. "
- + "If the timeout is reached, an exception
will be thrown.");
+ "The maximum time to wait for compaction or global
index build to complete when "
+ + "visibility callback is enabled. If the
timeout is reached, an exception will "
+ + "be thrown.");
public static final ConfigOption<Duration>
VISIBILITY_CALLBACK_CHECK_INTERVAL =
key("visibility-callback.check-interval")
diff --git a/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
b/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
index cc298823ff..524dd4962c 100644
--- a/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
+++ b/paimon-core/src/main/java/org/apache/paimon/AbstractFileStore.java
@@ -434,17 +434,26 @@ abstract class AbstractFileStore<T> implements
FileStore<T> {
callbacks.add(new ChainTableOverwriteCommitCallback(table));
}
- if (options.visibilityCallbackEnabled() &&
!schema.primaryKeys().isEmpty()) {
- if (table.bucketMode() == BucketMode.POSTPONE_MODE
- || options.deletionVectorsEnabled()) {
- callbacks.add(new VisibilityWaitCallback(table));
- }
+ if (options.visibilityCallbackEnabled() &&
shouldWaitForVisibility(table)) {
+ callbacks.add(new VisibilityWaitCallback(table));
}
callbacks.addAll(CallbackUtils.loadCommitCallbacks(options, table));
return callbacks;
}
+ private boolean shouldWaitForVisibility(FileStoreTable table) {
+ if (options.rowTrackingEnabled()) {
+ return true;
+ }
+
+ if (schema.primaryKeys().isEmpty()) {
+ return false;
+ }
+
+ return table.bucketMode() == BucketMode.POSTPONE_MODE ||
options.deletionVectorsEnabled();
+ }
+
@Override
@Nullable
public PartitionExpire newPartitionExpire(String commitUser,
FileStoreTable table) {
diff --git
a/paimon-core/src/main/java/org/apache/paimon/metastore/GlobalIndexVisibilityChecker.java
b/paimon-core/src/main/java/org/apache/paimon/metastore/GlobalIndexVisibilityChecker.java
new file mode 100644
index 0000000000..596bf15e91
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/metastore/GlobalIndexVisibilityChecker.java
@@ -0,0 +1,222 @@
+/*
+ * 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.metastore;
+
+import org.apache.paimon.Snapshot;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.index.GlobalIndexMeta;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.FileSource;
+import org.apache.paimon.manifest.IndexManifestEntry;
+import org.apache.paimon.manifest.ManifestEntry;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.utils.Range;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.apache.paimon.format.blob.BlobFileFormat.isBlobFile;
+
+/** Checks whether newly added data files are covered by existing global
indexes. */
+class GlobalIndexVisibilityChecker {
+
+ private final FileStoreTable table;
+ private final Map<BinaryRow, List<Range>> rowIdRangesByPartition;
+ private final Map<BinaryRow, Set<GlobalIndexIdentifier>>
globalIndexesByPartition;
+
+ private GlobalIndexVisibilityChecker(
+ FileStoreTable table,
+ Map<BinaryRow, List<Range>> rowIdRangesByPartition,
+ Map<BinaryRow, Set<GlobalIndexIdentifier>>
globalIndexesByPartition) {
+ this.table = table;
+ this.rowIdRangesByPartition = rowIdRangesByPartition;
+ this.globalIndexesByPartition = globalIndexesByPartition;
+ }
+
+ static GlobalIndexVisibilityChecker create(
+ FileStoreTable table, Snapshot snapshot, List<ManifestEntry>
deltaFiles) {
+ Map<BinaryRow, List<Range>> rowIdRangesByPartition =
+ collectRowIdRangesByPartition(deltaFiles);
+ Map<BinaryRow, Set<GlobalIndexIdentifier>> globalIndexesByPartition =
+ rowIdRangesByPartition.isEmpty()
+ ? new HashMap<>()
+ : collectGlobalIndexesByPartition(
+ table, snapshot,
rowIdRangesByPartition.keySet());
+ return new GlobalIndexVisibilityChecker(
+ table, rowIdRangesByPartition, globalIndexesByPartition);
+ }
+
+ boolean noNeedToWait() {
+ return globalIndexesByPartition.isEmpty();
+ }
+
+ boolean visibleIn(Snapshot snapshot) {
+ Map<BinaryRow, Map<GlobalIndexIdentifier, List<Range>>>
indexedRangesByPartition =
+ new HashMap<>();
+ for (IndexManifestEntry entry : scanGlobalIndexes(table, snapshot)) {
+ GlobalIndexMeta globalIndex = entry.indexFile().globalIndexMeta();
+ if (globalIndex == null) {
+ continue;
+ }
+
+ Set<GlobalIndexIdentifier> identifiers =
+ globalIndexesByPartition.get(entry.partition());
+ if (identifiers == null) {
+ continue;
+ }
+
+ GlobalIndexIdentifier identifier = identifierOf(entry);
+ if (identifiers.contains(identifier)) {
+ indexedRangesByPartition
+ .computeIfAbsent(entry.partition().copy(), k -> new
HashMap<>())
+ .computeIfAbsent(identifier, k -> new ArrayList<>())
+ .add(globalIndex.rowRange());
+ }
+ }
+
+ for (Map.Entry<BinaryRow, Set<GlobalIndexIdentifier>> partitionIndexes
:
+ globalIndexesByPartition.entrySet()) {
+ BinaryRow partition = partitionIndexes.getKey();
+ List<Range> rowIdRanges = rowIdRangesByPartition.get(partition);
+ Map<GlobalIndexIdentifier, List<Range>> indexedRanges =
+ indexedRangesByPartition.get(partition);
+ for (GlobalIndexIdentifier identifier :
partitionIndexes.getValue()) {
+ List<Range> ranges =
+ Range.sortAndMergeOverlap(
+ indexedRanges == null ? null :
indexedRanges.get(identifier), true);
+ for (Range rowIdRange : rowIdRanges) {
+ if (!rowIdRange.exclude(ranges).isEmpty()) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static Map<BinaryRow, List<Range>> collectRowIdRangesByPartition(
+ List<ManifestEntry> deltaFiles) {
+ Map<BinaryRow, List<Range>> rangesByPartition = new HashMap<>();
+ for (ManifestEntry entry : deltaFiles) {
+ if (shouldTrackGlobalIndex(entry)) {
+ rangesByPartition
+ .computeIfAbsent(entry.partition().copy(), k -> new
ArrayList<>())
+ .add(entry.file().nonNullRowIdRange());
+ }
+ }
+ for (Map.Entry<BinaryRow, List<Range>> entry :
rangesByPartition.entrySet()) {
+ entry.setValue(Range.sortAndMergeOverlap(entry.getValue(), true));
+ }
+ return rangesByPartition;
+ }
+
+ private static boolean shouldTrackGlobalIndex(ManifestEntry entry) {
+ if (!FileKind.ADD.equals(entry.kind())) {
+ return false;
+ }
+
+ DataFileMeta file = entry.file();
+ if (file.firstRowId() == null || file.rowCount() <= 0) {
+ return false;
+ }
+
+ Optional<FileSource> fileSource = file.fileSource();
+ if (!fileSource.isPresent() ||
!FileSource.APPEND.equals(fileSource.get())) {
+ return false;
+ }
+
+ return !isBlobFile(file.fileName());
+ }
+
+ private static Map<BinaryRow, Set<GlobalIndexIdentifier>>
collectGlobalIndexesByPartition(
+ FileStoreTable table, Snapshot snapshot, Set<BinaryRow>
partitionsToTrack) {
+ Map<BinaryRow, Set<GlobalIndexIdentifier>> indexesByPartition = new
HashMap<>();
+ for (IndexManifestEntry entry : scanGlobalIndexes(table, snapshot)) {
+ GlobalIndexMeta globalIndex = entry.indexFile().globalIndexMeta();
+ if (globalIndex != null &&
partitionsToTrack.contains(entry.partition())) {
+ indexesByPartition
+ .computeIfAbsent(entry.partition().copy(), k -> new
HashSet<>())
+ .add(identifierOf(entry));
+ }
+ }
+ return indexesByPartition;
+ }
+
+ private static List<IndexManifestEntry> scanGlobalIndexes(
+ FileStoreTable table, Snapshot snapshot) {
+ return table.store()
+ .newIndexFileHandler()
+ .scan(snapshot, entry -> entry.indexFile().globalIndexMeta()
!= null);
+ }
+
+ private static GlobalIndexIdentifier identifierOf(IndexManifestEntry
entry) {
+ GlobalIndexMeta globalIndex = entry.indexFile().globalIndexMeta();
+ return new GlobalIndexIdentifier(
+ entry.indexFile().indexType(),
+ globalIndex.indexFieldId(),
+ globalIndex.extraFieldIds());
+ }
+
+ private static class GlobalIndexIdentifier {
+
+ private final String indexType;
+ private final int indexFieldId;
+ private final int[] extraFieldIds;
+
+ private GlobalIndexIdentifier(String indexType, int indexFieldId,
int[] extraFieldIds) {
+ this.indexType = indexType;
+ this.indexFieldId = indexFieldId;
+ this.extraFieldIds =
+ extraFieldIds == null
+ ? null
+ : Arrays.copyOf(extraFieldIds,
extraFieldIds.length);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof GlobalIndexIdentifier)) {
+ return false;
+ }
+
+ GlobalIndexIdentifier that = (GlobalIndexIdentifier) o;
+ return indexFieldId == that.indexFieldId
+ && Objects.equals(indexType, that.indexType)
+ && Arrays.equals(extraFieldIds, that.extraFieldIds);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(indexType, indexFieldId);
+ result = 31 * result + Arrays.hashCode(extraFieldIds);
+ return result;
+ }
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/metastore/VisibilityWaitCallback.java
b/paimon-core/src/main/java/org/apache/paimon/metastore/VisibilityWaitCallback.java
index 824c232367..562cd55a40 100644
---
a/paimon-core/src/main/java/org/apache/paimon/metastore/VisibilityWaitCallback.java
+++
b/paimon-core/src/main/java/org/apache/paimon/metastore/VisibilityWaitCallback.java
@@ -41,7 +41,7 @@ import java.util.concurrent.TimeoutException;
import static org.apache.paimon.utils.Preconditions.checkNotNull;
-/** A {@link CommitCallback} to wait for compaction for visibility. */
+/** A {@link CommitCallback} to wait for compaction and global indexes for
visibility. */
public class VisibilityWaitCallback implements CommitCallback {
private static final Logger LOG =
LoggerFactory.getLogger(VisibilityWaitCallback.class);
@@ -68,6 +68,8 @@ public class VisibilityWaitCallback implements CommitCallback
{
Set<String> namesToTrack = new HashSet<>();
Set<BinaryRow> partitionsToTrack = new HashSet<>();
+ GlobalIndexVisibilityChecker globalIndexVisibilityChecker =
+ GlobalIndexVisibilityChecker.create(table, context.snapshot,
context.deltaFiles);
for (ManifestEntry entry : context.deltaFiles) {
if (shouldBeTracked(entry)) {
namesToTrack.add(entry.fileName());
@@ -75,12 +77,16 @@ public class VisibilityWaitCallback implements
CommitCallback {
}
}
- if (namesToTrack.isEmpty()) {
+ if (namesToTrack.isEmpty() &&
globalIndexVisibilityChecker.noNeedToWait()) {
return;
}
try {
- waitForCompaction(context.snapshot, namesToTrack,
partitionsToTrack);
+ waitForVisibility(
+ context.snapshot,
+ namesToTrack,
+ partitionsToTrack,
+ globalIndexVisibilityChecker);
} catch (InterruptedException | TimeoutException e) {
throw new RuntimeException(e);
}
@@ -91,25 +97,45 @@ public class VisibilityWaitCallback implements
CommitCallback {
// No-op for retry as the callback is idempotent
}
- private void waitForCompaction(
- Snapshot fromSnapshot, Set<String> namesToTrack, Set<BinaryRow>
partitionsToTrack)
+ private void waitForVisibility(
+ Snapshot fromSnapshot,
+ Set<String> namesToTrack,
+ Set<BinaryRow> partitionsToTrack,
+ GlobalIndexVisibilityChecker globalIndexVisibilityChecker)
throws InterruptedException, TimeoutException {
long startTime = System.currentTimeMillis();
+ boolean compactionDone = namesToTrack.isEmpty();
+ boolean globalIndexDone = globalIndexVisibilityChecker.noNeedToWait();
+ Snapshot checkedSnapshot = fromSnapshot;
while (System.currentTimeMillis() - startTime < timeout.toMillis()) {
Snapshot latest = table.snapshotManager().latestSnapshot();
checkNotNull(latest, "No latest snapshot");
- if (latest.id() > fromSnapshot.id()
+
+ if (!compactionDone
+ && latest.id() > checkedSnapshot.id()
&& !stillInLatest(latest, namesToTrack,
partitionsToTrack)) {
+ compactionDone = true;
+ }
+ if (!globalIndexDone &&
globalIndexVisibilityChecker.visibleIn(latest)) {
+ globalIndexDone = true;
+ }
+ if (compactionDone && globalIndexDone) {
return;
}
- fromSnapshot = latest;
+ checkedSnapshot = latest;
- LOG.info("Waiting for files of table {} to be compacted...",
table.fullName());
+ LOG.info(
+ "Waiting for visibility of table {}. Compaction done: {},
global index done: {}.",
+ table.fullName(),
+ compactionDone,
+ globalIndexDone);
//noinspection BusyWait
Thread.sleep(checkInterval.toMillis());
}
- throw new TimeoutException("Timeout waiting for files to be compacted
after " + timeout);
+ throw new TimeoutException(
+ "Timeout waiting for files to be compacted or global indexes
to be built after "
+ + timeout);
}
private boolean stillInLatest(
diff --git
a/paimon-core/src/test/java/org/apache/paimon/metastore/VisibilityWaitCallbackTest.java
b/paimon-core/src/test/java/org/apache/paimon/metastore/VisibilityWaitCallbackTest.java
new file mode 100644
index 0000000000..030b2af41c
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/metastore/VisibilityWaitCallbackTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.metastore;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.globalindex.btree.BTreeGlobalIndexBuilder;
+import org.apache.paimon.partition.PartitionPredicate;
+import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.TableTestBase;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.Pair;
+import org.apache.paimon.utils.RowRangeIndex;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link VisibilityWaitCallback}. */
+public class VisibilityWaitCallbackTest extends TableTestBase {
+
+ @Override
+ protected Schema schemaDefault() {
+ return Schema.newBuilder()
+ .column("f0", DataTypes.INT())
+ .column("f1", DataTypes.STRING())
+ .option(CoreOptions.ROW_TRACKING_ENABLED.key(), "true")
+ .option(CoreOptions.DATA_EVOLUTION_ENABLED.key(), "true")
+ .option(CoreOptions.VISIBILITY_CALLBACK_ENABLED.key(), "true")
+ .option(CoreOptions.VISIBILITY_CALLBACK_CHECK_INTERVAL.key(),
"100 ms")
+ .option(CoreOptions.VISIBILITY_CALLBACK_TIMEOUT.key(), "30 s")
+ .build();
+ }
+
+ @Test
+ public void testWaitForGlobalIndexBuildOfNewData() throws Exception {
+ createTableDefault();
+ FileStoreTable table = getTableDefault();
+ writeRows(table, 0, 3);
+ buildIndex(table, false);
+
+ long indexedSnapshotId = table.snapshotManager().latestSnapshot().id();
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ CompletableFuture<Void> writeFuture =
+ CompletableFuture.runAsync(
+ () -> {
+ try {
+ writeRows(getTableDefault(), 3, 2);
+ } catch (Exception e) {
+ throw new CompletionException(e);
+ }
+ },
+ executor);
+
+ try {
+ waitUntil(() -> table.snapshotManager().latestSnapshot().id() >
indexedSnapshotId);
+ Thread.sleep(300L);
+ assertThat(writeFuture.isDone()).isFalse();
+
+ buildIndex(getTableDefault(), true);
+ writeFuture.get(10, TimeUnit.SECONDS);
+
+ BTreeGlobalIndexBuilder builder =
+ new
BTreeGlobalIndexBuilder(getTableDefault()).withIndexField("f1");
+ assertThat(builder.incrementalScan()).isNotPresent();
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testDoNotWaitForGlobalIndexBuildOfUnindexedPartition() throws
Exception {
+ Identifier identifier = identifier("PartitionedTable");
+ catalog.createTable(identifier, partitionedSchema(), false);
+ FileStoreTable table = getTable(identifier);
+
+ writePartitionRows(table, "a", 0, 3);
+ buildPartitionIndex(table, "a");
+
+ writePartitionRows(getTable(identifier), "b", 3, 2);
+ }
+
+ private Schema partitionedSchema() {
+ return Schema.newBuilder()
+ .column("pt", DataTypes.STRING())
+ .column("f0", DataTypes.INT())
+ .column("f1", DataTypes.STRING())
+ .partitionKeys("pt")
+ .option(CoreOptions.ROW_TRACKING_ENABLED.key(), "true")
+ .option(CoreOptions.DATA_EVOLUTION_ENABLED.key(), "true")
+ .option(CoreOptions.VISIBILITY_CALLBACK_ENABLED.key(), "true")
+ .option(CoreOptions.VISIBILITY_CALLBACK_CHECK_INTERVAL.key(),
"100 ms")
+ .option(CoreOptions.VISIBILITY_CALLBACK_TIMEOUT.key(), "1 s")
+ .build();
+ }
+
+ private void writeRows(FileStoreTable table, int start, int count) throws
Exception {
+ BatchWriteBuilder builder = table.newBatchWriteBuilder();
+ try (BatchTableWrite write = builder.newWrite();
+ BatchTableCommit commit = builder.newCommit()) {
+ for (int i = start; i < start + count; i++) {
+ write.write(GenericRow.of(i, BinaryString.fromString("a" +
i)));
+ }
+ commit.commit(write.prepareCommit());
+ }
+ }
+
+ private void writePartitionRows(FileStoreTable table, String partition,
int start, int count)
+ throws Exception {
+ BatchWriteBuilder builder = table.newBatchWriteBuilder();
+ try (BatchTableWrite write = builder.newWrite();
+ BatchTableCommit commit = builder.newCommit()) {
+ for (int i = start; i < start + count; i++) {
+ write.write(
+ GenericRow.of(
+ BinaryString.fromString(partition),
+ i,
+ BinaryString.fromString("a" + i)));
+ }
+ commit.commit(write.prepareCommit());
+ }
+ }
+
+ private void buildIndex(FileStoreTable table, boolean incremental) throws
Exception {
+ BTreeGlobalIndexBuilder builder = new
BTreeGlobalIndexBuilder(table).withIndexField("f1");
+ Optional<Pair<RowRangeIndex, List<DataSplit>>> scan =
+ incremental ? builder.incrementalScan() : builder.scan();
+ assertThat(scan).isPresent();
+
+ List<CommitMessage> commitMessages = new ArrayList<>();
+ for (DataSplit dataSplit : scan.get().getRight()) {
+ commitMessages.addAll(builder.build(dataSplit, ioManager));
+ }
+
+ try (BatchTableCommit commit =
table.newBatchWriteBuilder().newCommit()) {
+ commit.commit(commitMessages);
+ }
+ }
+
+ private void buildPartitionIndex(FileStoreTable table, String partition)
throws Exception {
+ BTreeGlobalIndexBuilder builder =
+ new BTreeGlobalIndexBuilder(table)
+ .withIndexField("f1")
+ .withPartitionPredicate(partitionPredicate(table,
partition));
+ Optional<Pair<RowRangeIndex, List<DataSplit>>> scan = builder.scan();
+ assertThat(scan).isPresent();
+
+ List<CommitMessage> commitMessages = new ArrayList<>();
+ for (DataSplit dataSplit : scan.get().getRight()) {
+ commitMessages.addAll(builder.build(dataSplit, ioManager));
+ }
+
+ try (BatchTableCommit commit =
table.newBatchWriteBuilder().newCommit()) {
+ commit.commit(commitMessages);
+ }
+ }
+
+ private PartitionPredicate partitionPredicate(FileStoreTable table, String
partition) {
+ RowType partType = table.rowType().project("pt");
+ Predicate predicate =
+ PartitionPredicate.createPartitionPredicate(
+ partType,
+ Collections.singletonMap("pt",
BinaryString.fromString(partition)));
+ return PartitionPredicate.fromPredicate(partType, predicate);
+ }
+
+ private void waitUntil(BooleanSupplier condition) throws Exception {
+ long deadline = System.currentTimeMillis() +
TimeUnit.SECONDS.toMillis(10);
+ while (System.currentTimeMillis() < deadline) {
+ if (condition.getAsBoolean()) {
+ return;
+ }
+ Thread.sleep(50L);
+ }
+ throw new AssertionError("Condition was not met before timeout.");
+ }
+}