This is an automated email from the ASF dual-hosted git repository.

vinishjail97 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-xtable.git


The following commit(s) were added to refs/heads/main by this push:
     new b3a07cc8 fix(hudi): Emit REMOVE in HudiDataFileExtractor when CLEAN 
deletes the previous file version before incremental sync (#824)
b3a07cc8 is described below

commit b3a07cc85d491e1f383667d373427e7082f0baa4
Author: Vinish Reddy <[email protected]>
AuthorDate: Sun Oct 11 11:15:30 2026 +0530

    fix(hudi): Emit REMOVE in HudiDataFileExtractor when CLEAN deletes the 
previous file version before incremental sync (#824)
    
    * fix(hudi): emit REMOVE when CLEAN deletes previous file version before 
incremental sync
    
    When Hudi CLEAN physically removes an old base file from storage before 
XTable
    processes the corresponding UPSERT commit incrementally, getAllBaseFiles() 
only
    returns the new file. The else-if branch in getUpdatesToPartition that 
emits a
    REMOVE for the previous version never fires, leaving a stale ADD in the 
downstream
    Delta log that causes FILE_NOT_FOUND errors on read.
    
    Fix: after the base-file loop, if an ADD was emitted but no REMOVE was 
emitted
    and HoodieWriteStat.prevCommit indicates a prior version exists, recover 
the old
    file path by reading the previous commit's metadata from the visible Hudi 
timeline
    and emit a REMOVE. Gracefully degrades (logs warning, skips) if the previous
    commit has been archived.
    
    * Adapt the CLEAN REMOVE recovery to the Hudi 1.x timeline API
    
    * Cache previous-commit metadata per sync when recovering cleaned files
    
    * Add IT for CLEAN deleting replaced file groups of a pending clustering 
commit
    
    Async clustering that completes after a sync and is cleaned before the next
    sync must not be processed incrementally. The controller falls back to
    snapshot sync because the pending replace commit is older than the clean's
    earliest commit to retain.
    
    * Simplify readCommitMetadata with Optional.flatMap
---
 .../apache/xtable/hudi/HudiDataFileExtractor.java  |  91 ++++++++++-
 .../org/apache/xtable/ITConversionController.java  |  94 ++++++++++++
 .../java/org/apache/xtable/TestJavaHudiTable.java  |   9 +-
 .../xtable/hudi/TestHudiDataFileExtractor.java     | 170 +++++++++++++++++++++
 4 files changed, 357 insertions(+), 7 deletions(-)

diff --git 
a/xtable-core/src/main/java/org/apache/xtable/hudi/HudiDataFileExtractor.java 
b/xtable-core/src/main/java/org/apache/xtable/hudi/HudiDataFileExtractor.java
index 2a45ae79..c94e3dbc 100644
--- 
a/xtable-core/src/main/java/org/apache/xtable/hudi/HudiDataFileExtractor.java
+++ 
b/xtable-core/src/main/java/org/apache/xtable/hudi/HudiDataFileExtractor.java
@@ -23,14 +23,18 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import lombok.Builder;
 import lombok.Value;
+import lombok.extern.log4j.Log4j2;
 
 import org.apache.hadoop.fs.Path;
 
@@ -71,6 +75,7 @@ import org.apache.xtable.model.storage.InternalFilesDiff;
 import org.apache.xtable.model.storage.PartitionFileGroup;
 
 /** Extracts all the files for Hudi table represented by {@link 
InternalTable}. */
+@Log4j2
 public class HudiDataFileExtractor implements AutoCloseable {
   private final HoodieTableMetadata tableMetadata;
   private final HoodieTableMetaClient metaClient;
@@ -80,6 +85,10 @@ public class HudiDataFileExtractor implements AutoCloseable {
   private final HoodieMetadataConfig metadataConfig;
   private final FileSystemViewManager fileSystemViewManager;
   private final Path basePath;
+  // Previous-commit metadata read to recover cleaned files, keyed by instant 
time. The extractor
+  // lives for a single sync, so each instant is read at most once per sync.
+  private final Map<String, Optional<HoodieCommitMetadata>> 
prevCommitMetadataCache =
+      new HashMap<>();
 
   public HudiDataFileExtractor(
       HoodieTableMetaClient metaClient,
@@ -165,16 +174,13 @@ public class HudiDataFileExtractor implements 
AutoCloseable {
               .getPartitionToWriteStats()
               .forEach(
                   (partitionPath, writeStats) -> {
-                    Set<String> affectedFileIds =
-                        writeStats.stream()
-                            .map(HoodieWriteStat::getFileId)
-                            .collect(Collectors.toSet());
                     AddedAndRemovedFiles addedAndRemovedFiles =
                         getUpdatesToPartition(
                             fsView,
+                            timeline,
                             instantToConsider,
                             partitionPath,
-                            affectedFileIds,
+                            writeStats,
                             partitioningFields);
                     addedFiles.addAll(addedAndRemovedFiles.getAdded());
                     removedFiles.addAll(addedAndRemovedFiles.getRemoved());
@@ -282,10 +288,26 @@ public class HudiDataFileExtractor implements 
AutoCloseable {
 
   private AddedAndRemovedFiles getUpdatesToPartition(
       TableFileSystemView fsView,
+      HoodieTimeline timeline,
       HoodieInstant instantToConsider,
       String partitionPath,
-      Set<String> affectedFileIds,
+      List<HoodieWriteStat> writeStats,
       List<InternalPartitionField> partitioningFields) {
+    Set<String> affectedFileIds =
+        
writeStats.stream().map(HoodieWriteStat::getFileId).collect(Collectors.toSet());
+    // Build a map from fileId → prevCommit for file groups that are UPSERTs 
(not brand-new).
+    // Used below to recover the old file path when CLEAN has already deleted 
it from the FS.
+    Map<String, String> fileIdToPrevCommit =
+        writeStats.stream()
+            .filter(
+                ws ->
+                    ws.getPrevCommit() != null
+                        && 
!HoodieTimeline.INIT_INSTANT_TS.equals(ws.getPrevCommit()))
+            .collect(
+                Collectors.toMap(
+                    HoodieWriteStat::getFileId,
+                    HoodieWriteStat::getPrevCommit,
+                    (existing, replacement) -> existing));
     List<InternalDataFile> filesToAdd = new 
ArrayList<>(affectedFileIds.size());
     List<InternalDataFile> filesToRemove = new 
ArrayList<>(affectedFileIds.size());
     List<PartitionValue> partitionValues =
@@ -300,6 +322,7 @@ public class HudiDataFileExtractor implements AutoCloseable 
{
               List<HoodieBaseFile> baseFiles =
                   fileGroup.getAllBaseFiles().collect(Collectors.toList());
               boolean newBaseFileAdded = false;
+              boolean removeEmitted = false;
               for (HoodieBaseFile baseFile : baseFiles) {
                 if 
(baseFile.getCommitTime().equals(instantToConsider.requestedTime())) {
                   newBaseFileAdded = true;
@@ -308,13 +331,69 @@ public class HudiDataFileExtractor implements 
AutoCloseable {
                   // if a new base file was added, then the previous base file 
for the group needs
                   // to be removed
                   filesToRemove.add(buildFileWithoutStats(partitionValues, 
baseFile));
+                  removeEmitted = true;
                   break;
                 }
               }
+              // Recover old file path from timeline when CLEAN deleted it 
before this sync.
+              if (newBaseFileAdded && !removeEmitted) {
+                String fileId = fileGroup.getFileGroupId().getFileId();
+                String prevCommit = fileIdToPrevCommit.get(fileId);
+                if (prevCommit != null) {
+                  recoverRemovedFile(timeline, partitionPath, fileId, 
prevCommit, partitionValues)
+                      .ifPresent(filesToRemove::add);
+                }
+              }
             });
     return 
AddedAndRemovedFiles.builder().added(filesToAdd).removed(filesToRemove).build();
   }
 
+  /**
+   * Looks up the previous base file path from the timeline when CLEAN has 
already deleted it from
+   * storage, so the downstream format can emit a proper REMOVE for the stale 
file.
+   *
+   * @return the old {@link InternalDataFile}, or empty if the previous commit 
is no longer in the
+   *     visible timeline (e.g. archived before this sync ran)
+   */
+  Optional<InternalDataFile> recoverRemovedFile(
+      HoodieTimeline timeline,
+      String partitionPath,
+      String fileId,
+      String prevCommitTime,
+      List<PartitionValue> partitionValues) {
+    return prevCommitMetadataCache
+        .computeIfAbsent(prevCommitTime, commitTime -> 
readCommitMetadata(timeline, commitTime))
+        .flatMap(
+            prevMeta ->
+                prevMeta
+                    .getPartitionToWriteStats()
+                    .getOrDefault(partitionPath, Collections.emptyList())
+                    .stream()
+                    .filter(ws -> fileId.equals(ws.getFileId()))
+                    .findFirst()
+                    .map(
+                        ws ->
+                            buildFileWithoutStats(
+                                partitionValues, new 
HoodieBaseFile(ws.getPath()))));
+  }
+
+  private Optional<HoodieCommitMetadata> readCommitMetadata(
+      HoodieTimeline timeline, String commitTime) {
+    return timeline.getInstants().stream()
+        .filter(hoodieInstant -> 
commitTime.equals(hoodieInstant.requestedTime()))
+        .findFirst()
+        .flatMap(
+            instant -> {
+              try {
+                return Optional.of(timeline.readCommitMetadata(instant));
+              } catch (IOException e) {
+                log.warn(
+                    "Unable to read previous commit {} to recover removed 
files", commitTime, e);
+                return Optional.empty();
+              }
+            });
+  }
+
   private AddedAndRemovedFiles getUpdatesToPartitionForReplaceCommit(
       TableFileSystemView fsView,
       HoodieInstant instantToConsider,
diff --git 
a/xtable-core/src/test/java/org/apache/xtable/ITConversionController.java 
b/xtable-core/src/test/java/org/apache/xtable/ITConversionController.java
index ee6c5c98..d282cd0a 100644
--- a/xtable-core/src/test/java/org/apache/xtable/ITConversionController.java
+++ b/xtable-core/src/test/java/org/apache/xtable/ITConversionController.java
@@ -729,6 +729,100 @@ public class ITConversionController {
     }
   }
 
+  /**
+   * Verifies that incremental sync emits a REMOVE when Hudi CLEAN deletes the 
previous base file
+   * version from storage before XTable processes the UPSERT commit.
+   */
+  @Test
+  public void testIncrementalSyncEmitsRemoveWhenHudiCleanRunsBeforeSync() {
+    String tableName = getTableName();
+    ConversionSourceProvider<?> conversionSourceProvider = 
getConversionSourceProvider(HUDI);
+    try (TestJavaHudiTable table =
+        TestJavaHudiTable.forStandardSchema(
+            tableName, tempDir, null, HoodieTableType.COPY_ON_WRITE)) {
+      ConversionConfig conversionConfig =
+          getTableSyncConfig(
+              HUDI, SyncMode.INCREMENTAL, tableName, table, 
ImmutableList.of(DELTA), null, null);
+      ConversionController conversionController =
+          new ConversionController(jsc.hadoopConfiguration());
+
+      // Commit A: insert 100 records; sync so Delta has ADD actions for 
commitA files.
+      List<HoodieRecord<HoodieAvroPayload>> insertedRecords = 
table.insertRecords(100, true);
+      conversionController.sync(conversionConfig, conversionSourceProvider);
+      checkDatasetEquivalence(HUDI, table, Collections.singletonList(DELTA), 
100);
+
+      // Commit B: upsert 50 records, creating new file versions for those 
file groups.
+      // XTable does NOT sync here — simulating the lag between Hudi writes 
and XTable runs.
+      table.upsertRecords(insertedRecords.subList(0, 50), true);
+
+      // Commit C: insert 20 brand-new records. A third commit is required 
before CLEAN so
+      // that Hudi's clean plan is non-empty (retainCommits=1 needs N+1 
commits to purge N).
+      table.insertRecords(20, true);
+
+      // CLEAN physically deletes commitA's file versions from disk — this is 
the race condition.
+      // After this point getAllBaseFiles() for the upserted file groups 
returns only commitB files.
+      table.clean();
+
+      // Incremental sync picks up commits B and C. For the upserted file 
groups in commit B, the
+      // fix recovers the commitA file path from prevCommit metadata and emits 
a REMOVE, preventing
+      // Delta from retaining a stale ADD that would cause 
FileNotFoundException on read.
+      conversionController.sync(conversionConfig, conversionSourceProvider);
+
+      // 100 original + 20 new inserts = 120 records (50 upserted records are 
updated, not added).
+      // Reading via Spark would throw FileNotFoundException on stale commitA 
ADDs without the fix.
+      checkDatasetEquivalence(HUDI, table, Collections.singletonList(DELTA), 
120);
+    }
+  }
+
+  @Test
+  public void 
testSyncFallsBackToSnapshotWhenCleanDeletesReplacedFileGroupsOfPendingCommit() {
+    String tableName = getTableName();
+    ConversionSourceProvider<?> conversionSourceProvider = 
getConversionSourceProvider(HUDI);
+    try (TestJavaHudiTable table =
+        TestJavaHudiTable.forStandardSchema(
+            tableName, tempDir, null, HoodieTableType.COPY_ON_WRITE)) {
+      ConversionConfig conversionConfig =
+          getTableSyncConfig(
+              HUDI,
+              SyncMode.INCREMENTAL,
+              tableName,
+              table,
+              ImmutableList.of(DELTA, ICEBERG),
+              null,
+              null);
+      ConversionController conversionController =
+          new ConversionController(jsc.hadoopConfiguration());
+
+      // Commit A: insert 100 records; sync so the targets reference commitA 
files.
+      table.insertRecords(100, true);
+      conversionController.sync(conversionConfig, conversionSourceProvider);
+      checkDatasetEquivalence(HUDI, table, ImmutableList.of(DELTA, ICEBERG), 
100);
+
+      // Replace commit B: schedule async clustering over the commitA file 
groups, but do not run
+      // it yet.
+      String clusteringInstant = table.onlyScheduleClustering();
+
+      // Commit C: insert 20 records and sync. XTable syncs C and records B as 
a pending commit.
+      table.insertRecords(20, true);
+      conversionController.sync(conversionConfig, conversionSourceProvider);
+      checkDatasetEquivalence(HUDI, table, ImmutableList.of(DELTA, ICEBERG), 
120);
+
+      // B completes after the sync and marks the commitA file groups as 
replaced.
+      table.completeScheduledClustering(clusteringInstant);
+
+      // CLEAN retains C, so the earliest commit to retain equals the last 
synced commit and the
+      // next sync stays incremental. B is older than C, so CLEAN physically 
deletes the replaced
+      // commitA file groups before XTable processes B.
+      table.clean();
+
+      // B is still a pending commit for the targets and is older than the 
earliest commit to
+      // retain, so the controller falls back to snapshot sync instead of 
processing B
+      // incrementally against a file system view that no longer has the 
commitA file groups.
+      conversionController.sync(conversionConfig, conversionSourceProvider);
+      checkDatasetEquivalence(HUDI, table, ImmutableList.of(DELTA, ICEBERG), 
120);
+    }
+  }
+
   @ParameterizedTest
   @EnumSource(value = SyncMode.class)
   public void testSyncWithSingleFormat(SyncMode syncMode) {
diff --git a/xtable-core/src/test/java/org/apache/xtable/TestJavaHudiTable.java 
b/xtable-core/src/test/java/org/apache/xtable/TestJavaHudiTable.java
index 8e8d05c0..99b362c4 100644
--- a/xtable-core/src/test/java/org/apache/xtable/TestJavaHudiTable.java
+++ b/xtable-core/src/test/java/org/apache/xtable/TestJavaHudiTable.java
@@ -292,7 +292,14 @@ public class TestJavaHudiTable extends 
TestAbstractHudiTable {
   }
 
   public void cluster() {
-    String instant = writeClient.scheduleClustering(Option.empty()).get();
+    completeScheduledClustering(onlyScheduleClustering());
+  }
+
+  public String onlyScheduleClustering() {
+    return writeClient.scheduleClustering(Option.empty()).get();
+  }
+
+  public void completeScheduledClustering(String instant) {
     writeClient.cluster(instant, true);
     // Reinitializing as clustering disables auto commit and we want to enable 
it back.
     writeClient = initJavaWriteClient(schema, typedProperties, null);
diff --git 
a/xtable-core/src/test/java/org/apache/xtable/hudi/TestHudiDataFileExtractor.java
 
b/xtable-core/src/test/java/org/apache/xtable/hudi/TestHudiDataFileExtractor.java
new file mode 100644
index 00000000..883bc0b4
--- /dev/null
+++ 
b/xtable-core/src/test/java/org/apache/xtable/hudi/TestHudiDataFileExtractor.java
@@ -0,0 +1,170 @@
+/*
+ * 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.xtable.hudi;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.Optional;
+
+import org.apache.hadoop.conf.Configuration;
+import org.junit.jupiter.api.Test;
+
+import org.apache.hudi.common.model.HoodieCommitMetadata;
+import org.apache.hudi.common.model.HoodieWriteStat;
+import org.apache.hudi.common.table.HoodieTableConfig;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.table.timeline.HoodieInstant;
+import org.apache.hudi.common.table.timeline.HoodieTimeline;
+import org.apache.hudi.common.table.timeline.versioning.v2.InstantComparatorV2;
+import org.apache.hudi.storage.StoragePath;
+import org.apache.hudi.storage.hadoop.HadoopStorageConfiguration;
+
+import org.apache.xtable.model.storage.InternalDataFile;
+
+class TestHudiDataFileExtractor {
+
+  @Test
+  void recoverRemovedFile_returnsFileWhenPreviousCommitInTimeline() throws 
Exception {
+    HudiDataFileExtractor extractor = 
buildExtractorWithBasePath("file:///tmp/test-table");
+    String partition = "year=2023";
+    String fileId = "fg-1";
+    String prevCommitTime = "20230101000000000";
+    String oldPath = partition + "/" + fileId + "_0-0-0_" + prevCommitTime + 
".parquet";
+
+    HoodieInstant prevInstant =
+        new HoodieInstant(
+            HoodieInstant.State.COMPLETED,
+            HoodieTimeline.COMMIT_ACTION,
+            prevCommitTime,
+            InstantComparatorV2.REQUESTED_TIME_BASED_COMPARATOR);
+    HoodieTimeline timeline = mock(HoodieTimeline.class);
+    
when(timeline.getInstants()).thenReturn(Collections.singletonList(prevInstant));
+    when(timeline.readCommitMetadata(prevInstant))
+        .thenReturn(singleStatCommit(partition, fileId, oldPath));
+
+    Optional<InternalDataFile> result =
+        extractor.recoverRemovedFile(
+            timeline, partition, fileId, prevCommitTime, 
Collections.emptyList());
+
+    assertTrue(result.isPresent());
+    assertEquals(oldPath, result.get().getPhysicalPath());
+  }
+
+  @Test
+  void recoverRemovedFile_returnsEmptyWhenPreviousCommitArchived() {
+    HudiDataFileExtractor extractor = 
buildExtractorWithBasePath("file:///tmp/test-table");
+
+    HoodieTimeline timeline = mock(HoodieTimeline.class);
+    when(timeline.getInstants()).thenReturn(Collections.emptyList());
+
+    Optional<InternalDataFile> result =
+        extractor.recoverRemovedFile(
+            timeline, "year=2023", "fg-1", "20230101000000000", 
Collections.emptyList());
+
+    assertFalse(result.isPresent());
+  }
+
+  @Test
+  void recoverRemovedFile_returnsEmptyWhenFileIdNotInPrevCommit() throws 
Exception {
+    HudiDataFileExtractor extractor = 
buildExtractorWithBasePath("file:///tmp/test-table");
+    String partition = "year=2023";
+    String prevCommitTime = "20230101000000000";
+
+    HoodieInstant prevInstant =
+        new HoodieInstant(
+            HoodieInstant.State.COMPLETED,
+            HoodieTimeline.COMMIT_ACTION,
+            prevCommitTime,
+            InstantComparatorV2.REQUESTED_TIME_BASED_COMPARATOR);
+    HoodieTimeline timeline = mock(HoodieTimeline.class);
+    
when(timeline.getInstants()).thenReturn(Collections.singletonList(prevInstant));
+    when(timeline.readCommitMetadata(prevInstant))
+        .thenReturn(
+            singleStatCommit(partition, "different-fg", partition + 
"/different-fg.parquet"));
+
+    Optional<InternalDataFile> result =
+        extractor.recoverRemovedFile(
+            timeline, partition, "fg-1", prevCommitTime, 
Collections.emptyList());
+
+    assertFalse(result.isPresent());
+  }
+
+  @Test
+  void recoverRemovedFile_readsPreviousCommitMetadataOncePerInstant() throws 
Exception {
+    HudiDataFileExtractor extractor = 
buildExtractorWithBasePath("file:///tmp/test-table");
+    String partition = "year=2023";
+    String prevCommitTime = "20230101000000000";
+    String oldPath1 = partition + "/fg-1_0-0-0_" + prevCommitTime + ".parquet";
+    String oldPath2 = partition + "/fg-2_0-0-0_" + prevCommitTime + ".parquet";
+
+    HoodieInstant prevInstant =
+        new HoodieInstant(
+            HoodieInstant.State.COMPLETED,
+            HoodieTimeline.COMMIT_ACTION,
+            prevCommitTime,
+            InstantComparatorV2.REQUESTED_TIME_BASED_COMPARATOR);
+    HoodieCommitMetadata prevMetadata = singleStatCommit(partition, "fg-1", 
oldPath1);
+    HoodieWriteStat secondStat = new HoodieWriteStat();
+    secondStat.setFileId("fg-2");
+    secondStat.setPath(oldPath2);
+    prevMetadata.addWriteStat(partition, secondStat);
+    HoodieTimeline timeline = mock(HoodieTimeline.class);
+    
when(timeline.getInstants()).thenReturn(Collections.singletonList(prevInstant));
+    when(timeline.readCommitMetadata(prevInstant)).thenReturn(prevMetadata);
+
+    Optional<InternalDataFile> result1 =
+        extractor.recoverRemovedFile(
+            timeline, partition, "fg-1", prevCommitTime, 
Collections.emptyList());
+    Optional<InternalDataFile> result2 =
+        extractor.recoverRemovedFile(
+            timeline, partition, "fg-2", prevCommitTime, 
Collections.emptyList());
+
+    assertEquals(oldPath1, result1.get().getPhysicalPath());
+    assertEquals(oldPath2, result2.get().getPhysicalPath());
+    verify(timeline, times(1)).readCommitMetadata(prevInstant);
+  }
+
+  private static HoodieCommitMetadata singleStatCommit(
+      String partition, String fileId, String path) {
+    HoodieWriteStat stat = new HoodieWriteStat();
+    stat.setFileId(fileId);
+    stat.setPath(path);
+    HoodieCommitMetadata metadata = new HoodieCommitMetadata();
+    metadata.addWriteStat(partition, stat);
+    return metadata;
+  }
+
+  private static HudiDataFileExtractor buildExtractorWithBasePath(String 
basePathStr) {
+    HoodieTableMetaClient metaClient = mock(HoodieTableMetaClient.class);
+    HoodieTableConfig tableConfig = mock(HoodieTableConfig.class);
+    doReturn(new HadoopStorageConfiguration(new 
Configuration())).when(metaClient).getStorageConf();
+    when(metaClient.getTableConfig()).thenReturn(tableConfig);
+    when(tableConfig.isMetadataTableAvailable()).thenReturn(false);
+    when(metaClient.getBasePath()).thenReturn(new StoragePath(basePathStr));
+    return new HudiDataFileExtractor(metaClient, null, null);
+  }
+}

Reply via email to