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

danny0405 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new d9e08f076370 feat(flink): add partitioned rli boostrap operator 
(#19805)
d9e08f076370 is described below

commit d9e08f0763708dd937f211a6a883ba0b7e1939be
Author: Peter Huang <[email protected]>
AuthorDate: Thu Sep 3 05:14:16 2026 -0700

    feat(flink): add partitioned rli boostrap operator (#19805)
    
    * feat(flink): add partitioned rli boostrap operator
---
 .../apache/hudi/configuration/FlinkOptions.java    |   2 +-
 .../apache/hudi/configuration/OptionsResolver.java |  13 ++
 .../bootstrap/AbstractRLIBootstrapOperator.java    |  86 +++++++++
 .../sink/bootstrap/BootstrapOperatorFactory.java   |  43 +++++
 .../hudi/sink/bootstrap/RLIBootstrapOperator.java  |  43 +----
 .../bootstrap/TimeBoundedRLIBootstrapOperator.java | 194 +++++++++++++++++++
 .../java/org/apache/hudi/sink/utils/Pipelines.java |   7 +-
 .../org/apache/hudi/table/HoodieTableFactory.java  |   1 -
 .../java/org/apache/hudi/util/StreamerUtil.java    |  20 ++
 .../hudi/configuration/TestOptionsResolver.java    |  31 +++
 .../TestTimeBoundedRLIBootstrapOperator.java       | 210 +++++++++++++++++++++
 .../org/apache/hudi/sink/utils/TestPipelines.java  |  47 +++++
 .../apache/hudi/table/TestHoodieTableFactory.java  |  61 ++++++
 .../org/apache/hudi/utils/TestStreamerUtil.java    |  22 +++
 14 files changed, 736 insertions(+), 44 deletions(-)

diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/FlinkOptions.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/FlinkOptions.java
index 88662af61cb3..6ec2adea7116 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/FlinkOptions.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/FlinkOptions.java
@@ -374,7 +374,7 @@ public class FlinkOptions extends HoodieConfig {
   public static final ConfigOption<Integer> 
INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS = ConfigOptions
       .key("index.rli.cache.rocksdb.bootstrap.days")
       .intType()
-      .defaultValue(7)
+      .defaultValue(-1)
       .withDescription("Number of days of Partitioned Record Index to load 
during bootstrap. Only partitions "
           + "within this window are pre-loaded; older partitions are loaded on 
demand when updates are observed.");
 
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/OptionsResolver.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/OptionsResolver.java
index 07f31daa8616..f848b3ae4343 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/OptionsResolver.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/configuration/OptionsResolver.java
@@ -81,6 +81,9 @@ public class OptionsResolver {
   // Value to override the default minimum file group count for global record 
level index.
   public static String GLOBAL_RECORD_LEVEL_INDEX_MIN_FILE_GROUP_COUNT_DEFAULT 
= "8";
 
+  // Value of FlinkOptions#INDEX_RLI_BACKEND_TYPE that selects the local 
RocksDB-based partitioned index cache.
+  private static final String ROCKSDB_INDEX_RLI_BACKEND_TYPE = "rocksdb";
+
   /**
    * Returns whether the current runtime mode is adaptive batch execution.
    */
@@ -262,6 +265,16 @@ public class OptionsResolver {
     return indexType == HoodieIndex.IndexType.GLOBAL_RECORD_LEVEL_INDEX;
   }
 
+  /**
+   * Returns whether the table uses partitioned record level index served by 
the local RocksDB-based
+   * partitioned index cache, i.e. {@link FlinkOptions#INDEX_RLI_BACKEND_TYPE} 
is configured as {@code rocksdb}.
+   */
+  public static boolean isTimeBoundedRLIBootstrapEnabled(Configuration conf) {
+    return conf.get(FlinkOptions.INDEX_BOOTSTRAP_ENABLED)
+            && 
ROCKSDB_INDEX_RLI_BACKEND_TYPE.equalsIgnoreCase(conf.get(FlinkOptions.INDEX_RLI_BACKEND_TYPE))
+            && conf.get(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS) > 
0;
+  }
+
   /**
    * Estimates the file group count to use for RLI partition of a new table.
    */
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/AbstractRLIBootstrapOperator.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/AbstractRLIBootstrapOperator.java
new file mode 100644
index 000000000000..1af2fbb6f251
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/AbstractRLIBootstrapOperator.java
@@ -0,0 +1,86 @@
+/*
+ * 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.hudi.sink.bootstrap;
+
+import org.apache.hudi.client.common.HoodieFlinkEngineContext;
+import org.apache.hudi.client.model.HoodieFlinkInternalRow;
+import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.metadata.HoodieBackedTableMetadata;
+import org.apache.hudi.util.StreamerUtil;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+
+/**
+ * Base class for bootstrap operators that load record level index (RLI) data 
from the metadata
+ * table, shared by {@link RLIBootstrapOperator} and {@link 
TimeBoundedRLIBootstrapOperator}.
+ */
+@Slf4j
+public abstract class AbstractRLIBootstrapOperator
+    extends AbstractBootstrapOperator {
+
+  protected transient HoodieBackedTableMetadata tableMetadata;
+  protected transient long loadedCnt;
+
+  protected AbstractRLIBootstrapOperator(Configuration conf) {
+    super(conf);
+  }
+
+  @Override
+  public void close() throws Exception {
+    closeMetadataTable();
+    super.close();
+  }
+
+  // -------------------------------------------------------------------------
+  //  Utilities
+  // -------------------------------------------------------------------------
+
+  protected HoodieBackedTableMetadata 
createTableMetadata(HoodieTableMetaClient metaClient) {
+    return new HoodieBackedTableMetadata(
+        HoodieFlinkEngineContext.DEFAULT,
+        metaClient.getStorage(),
+        StreamerUtil.metadataConfig(conf),
+        conf.get(FlinkOptions.PATH));
+  }
+
+  protected void emitIndexRecord(String partitionPath, String recordKey, 
HoodieRecordGlobalLocation location) {
+    output.collect(new StreamRecord<>(
+        new HoodieFlinkInternalRow(
+            recordKey,
+            partitionPath,
+            location.getFileId(),
+            String.valueOf(location.getInstantTime()))));
+    loadedCnt += 1;
+  }
+
+  protected void closeMetadataTable() {
+    if (tableMetadata != null) {
+      try {
+        tableMetadata.close();
+      } catch (Exception e) {
+        log.warn("Failed to close metadata table", e);
+      }
+      tableMetadata = null;
+    }
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/BootstrapOperatorFactory.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/BootstrapOperatorFactory.java
new file mode 100644
index 000000000000..34c82cf6c382
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/BootstrapOperatorFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.hudi.sink.bootstrap;
+
+import org.apache.hudi.configuration.OptionsResolver;
+
+import org.apache.flink.configuration.Configuration;
+
+/**
+ * Factory that resolves the concrete {@link AbstractBootstrapOperator} 
implementation to use for
+ * the index bootstrap pipeline, keeping the pipeline construction agnostic of 
the specific
+ * bootstrap operator selection logic.
+ */
+public final class BootstrapOperatorFactory {
+
+  private BootstrapOperatorFactory() {
+  }
+
+  public static AbstractBootstrapOperator createInstance(Configuration conf) {
+    if (OptionsResolver.isGlobalRecordLevelIndex(conf)) {
+      return new RLIBootstrapOperator(conf);
+    } else if (OptionsResolver.isTimeBoundedRLIBootstrapEnabled(conf)) {
+      return new TimeBoundedRLIBootstrapOperator(conf);
+    }
+    return new BootstrapOperator(conf);
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/RLIBootstrapOperator.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/RLIBootstrapOperator.java
index d0cb16a789e5..04f22478825a 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/RLIBootstrapOperator.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/RLIBootstrapOperator.java
@@ -18,23 +18,18 @@
 
 package org.apache.hudi.sink.bootstrap;
 
-import org.apache.hudi.client.common.HoodieFlinkEngineContext;
-import org.apache.hudi.client.model.HoodieFlinkInternalRow;
 import org.apache.hudi.common.data.HoodiePairData;
 import org.apache.hudi.common.function.SerializableFunctionUnchecked;
 import org.apache.hudi.common.model.FileSlice;
 import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
 import org.apache.hudi.common.table.HoodieTableConfig;
 import org.apache.hudi.common.table.HoodieTableMetaClient;
-import org.apache.hudi.configuration.FlinkOptions;
-import org.apache.hudi.metadata.HoodieBackedTableMetadata;
 import org.apache.hudi.util.StreamerUtil;
 import org.apache.hudi.utils.RuntimeContextUtils;
 
 import lombok.extern.slf4j.Slf4j;
 import org.apache.flink.configuration.Configuration;
 import org.apache.flink.runtime.state.StateInitializationContext;
-import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -51,10 +46,7 @@ import java.util.stream.Collectors;
  */
 @Slf4j
 public class RLIBootstrapOperator
-    extends AbstractBootstrapOperator {
-
-  private transient HoodieBackedTableMetadata tableMetadata;
-  private transient long loadedCnt;
+    extends AbstractRLIBootstrapOperator {
 
   public RLIBootstrapOperator(Configuration conf) {
     super(conf);
@@ -64,21 +56,11 @@ public class RLIBootstrapOperator
   public void initializeState(StateInitializationContext context) throws 
Exception {
     loadedCnt = 0;
     HoodieTableMetaClient metaClient = StreamerUtil.createMetaClient(conf);
-    this.tableMetadata = new HoodieBackedTableMetadata(
-        HoodieFlinkEngineContext.DEFAULT,
-        metaClient.getStorage(),
-        StreamerUtil.metadataConfig(conf),
-        conf.get(FlinkOptions.PATH));
+    this.tableMetadata = createTableMetadata(metaClient);
     // Load RLI records
     preLoadRLIRecords(metaClient.getTableConfig());
   }
 
-  @Override
-  public void close() throws Exception {
-    closeMetadataTable();
-    super.close();
-  }
-
   // -------------------------------------------------------------------------
   //  Utilities
   // -------------------------------------------------------------------------
@@ -133,24 +115,7 @@ public class RLIBootstrapOperator
     return fileGroupIdx % parallelism == taskID;
   }
 
-  private void emitIndexRecord(String recordKey, HoodieRecordGlobalLocation 
location) {
-    output.collect(new StreamRecord<>(
-        new HoodieFlinkInternalRow(
-            recordKey,
-            location.getPartitionPath(),
-            location.getFileId(),
-            String.valueOf(location.getInstantTime()))));
-    loadedCnt += 1;
-  }
-
-  private void closeMetadataTable() {
-    if (tableMetadata != null) {
-      try {
-        tableMetadata.close();
-      } catch (Exception e) {
-        log.warn("Failed to close metadata table", e);
-      }
-      tableMetadata = null;
-    }
+  protected void emitIndexRecord(String recordKey, HoodieRecordGlobalLocation 
location) {
+    emitIndexRecord(location.getPartitionPath(), recordKey, location);
   }
 }
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/TimeBoundedRLIBootstrapOperator.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/TimeBoundedRLIBootstrapOperator.java
new file mode 100644
index 000000000000..4d355e6e3e4d
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/bootstrap/TimeBoundedRLIBootstrapOperator.java
@@ -0,0 +1,194 @@
+/*
+ * 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.hudi.sink.bootstrap;
+
+import org.apache.hudi.common.data.HoodiePairData;
+import org.apache.hudi.common.function.SerializableFunctionUnchecked;
+import org.apache.hudi.common.model.FileSlice;
+import org.apache.hudi.common.model.HoodieRecordGlobalLocation;
+import org.apache.hudi.common.table.HoodieTableConfig;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.util.Functions;
+import org.apache.hudi.common.util.VisibleForTesting;
+import org.apache.hudi.common.util.hash.BucketIndexUtil;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.metadata.MetadataPartitionType;
+import org.apache.hudi.util.StreamerUtil;
+import org.apache.hudi.utils.RuntimeContextUtils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.runtime.state.StateInitializationContext;
+
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Bootstrap operator that preload of time bounded partitioned record level 
index (RLI) data
+ * from the metadata table.
+ *
+ * <p>Only data table partitions that fall within the last {@link 
FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS}
+ * days are eagerly preloaded; the partition path of each partition is parsed 
as a date using
+ * {@link FlinkOptions#PARTITION_FORMAT} (default {@link 
FlinkOptions#PARTITION_FORMAT_DAY}) to
+ * determine whether it falls inside the window. Partitions outside the 
window, and partitions whose
+ * path cannot be parsed as a date, are skipped here and are expected to be 
loaded on demand later.
+ *
+ * <p>Setting {@link FlinkOptions#INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS} to 
{@code 0} disables preloading
+ * entirely, which is the expected fallback for non-temporal (non 
date-partitioned) tables.
+ */
+@Slf4j
+public class TimeBoundedRLIBootstrapOperator
+    extends AbstractRLIBootstrapOperator {
+
+  private int parallelism;
+  private int taskID;
+  /**
+   * Functions for calculating the task partition to dispatch.
+   */
+  @VisibleForTesting
+  Functions.Function3<Integer, String, Integer, Integer> partitionIndexFunc;
+
+  public TimeBoundedRLIBootstrapOperator(Configuration conf) {
+    super(conf);
+  }
+
+  @Override
+  public void initializeState(StateInitializationContext context) throws 
Exception {
+    loadedCnt = 0;
+    this.taskID = 
RuntimeContextUtils.getIndexOfThisSubtask(getRuntimeContext());
+
+    int bootstrapDays = 
conf.get(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS);
+    if (bootstrapDays <= 0) {
+      log.info("Skip preloading partitioned RLI records because bootstrap days 
is configured as {}, taskId = {}",
+          bootstrapDays, taskID);
+      waitForBootstrapReady(taskID);
+      return;
+    }
+
+    HoodieTableMetaClient metaClient = StreamerUtil.createMetaClient(conf);
+    this.tableMetadata = createTableMetadata(metaClient);
+
+    this.parallelism = 
RuntimeContextUtils.getNumberOfParallelSubtasks(getRuntimeContext());
+    this.partitionIndexFunc = 
BucketIndexUtil.getPartitionIndexFunc(parallelism);
+    preLoadPartitionedRLIRecords(metaClient.getTableConfig(), bootstrapDays);
+  }
+
+  // -------------------------------------------------------------------------
+  //  Utilities
+  // -------------------------------------------------------------------------
+
+  private void preLoadPartitionedRLIRecords(HoodieTableConfig tableConfig, int 
bootstrapDays) {
+    if (!tableMetadata.enabled()) {
+      if (tableConfig.isMetadataTableAvailable()) {
+        throw new RuntimeException("Can not initialize the table metadata");
+      }
+      log.info("Skip preloading partitioned RLI records because table metadata 
is not initialized, taskId = {}", taskID);
+      waitForBootstrapReady(taskID);
+      closeMetadataTable();
+      return;
+    }
+
+    if 
(!tableConfig.isMetadataPartitionAvailable(MetadataPartitionType.RECORD_INDEX)) 
{
+      log.info("Skip preloading partitioned RLI records because record index 
is not available yet, taskId = {}", taskID);
+      waitForBootstrapReady(taskID);
+      closeMetadataTable();
+      return;
+    }
+
+    Map<String, List<FileSlice>> partitionedFileGroups =
+        
tableMetadata.getBucketizedFileGroupsForPartitionedRLI(MetadataPartitionType.RECORD_INDEX);
+    List<String> partitionsInWindow = 
filterPartitionsInWindow(partitionedFileGroups.keySet(), bootstrapDays);
+
+    log.info("Start preloading partitioned RLI records from metadata table for 
{}/{} partitions within the last {} days, "
+            + "taskId = {}, parallelism = {}",
+        partitionsInWindow.size(), partitionedFileGroups.size(), 
bootstrapDays, taskID, parallelism);
+
+    long startTime = System.currentTimeMillis();
+    for (String partitionPath : partitionsInWindow) {
+      preLoadPartition(partitionPath, 
partitionedFileGroups.get(partitionPath), taskID);
+    }
+    long costMs = System.currentTimeMillis() - startTime;
+    log.info("Finish preloading partitioned RLI records, total records: {}, 
cost: {} ms, taskId = {}", loadedCnt, costMs, taskID);
+
+    // Wait for other tasks to complete
+    waitForBootstrapReady(taskID);
+
+    // Cleanup resources
+    closeMetadataTable();
+  }
+
+  private void preLoadPartition(String partitionPath, List<FileSlice> 
fileSlices, int taskID) {
+    List<FileSlice> filteredFileSlices = new ArrayList<>();
+    for (int i = 0; i < fileSlices.size(); i++) {
+      if (shouldLoadBucket(partitionPath, fileSlices.size(), i, taskID)) {
+        filteredFileSlices.add(fileSlices.get(i));
+      }
+    }
+    if (filteredFileSlices.isEmpty()) {
+      return;
+    }
+    log.info("Subtask: {} will preload partition {} from file groups: {}, 
total file groups: {}.",
+        taskID, partitionPath, 
filteredFileSlices.stream().map(FileSlice::getFileId).collect(Collectors.joining(",")),
+        fileSlices.size());
+
+    // readRecordIndexLocations() discovers the full set of RLI file slices 
internally and passes it to
+    // the filter; the filter here ignores that argument and substitutes the 
file slices already scoped
+    // to this data partition, mirroring 
RecordLevelIndexBackend#bootstrapPartition.
+    SerializableFunctionUnchecked<List<FileSlice>, List<FileSlice>> 
fileSlicesFilter = fileSlicesToFilter -> filteredFileSlices;
+    HoodiePairData<String, HoodieRecordGlobalLocation> rliData = 
tableMetadata.readRecordIndexLocations(fileSlicesFilter);
+    rliData.forEach(locationPair -> emitIndexRecord(partitionPath, 
locationPair.getLeft(), locationPair.getRight()));
+  }
+
+  /**
+   * Determines if the given file group should be loaded by this task, using 
the same
+   * partition-aware assignment as the write path (see {@link 
BucketIndexUtil#getPartitionIndexFunc}),
+   * so that each file group is bootstrapped by the same task that owns it 
during writes.
+   */
+  @VisibleForTesting
+  boolean shouldLoadBucket(String partitionPath, int fileGroupCount, int 
fileGroupIdx, int taskID) {
+    return partitionIndexFunc.apply(fileGroupCount, partitionPath, 
fileGroupIdx) == taskID;
+  }
+
+  /**
+   * Filters the data table partitions whose partition path can be parsed as a 
date within the last
+   * {@code bootstrapDays} days, inclusive of today.
+   */
+  @VisibleForTesting
+  List<String> filterPartitionsInWindow(Iterable<String> partitionPaths, int 
bootstrapDays) {
+    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(
+        
conf.getOptional(FlinkOptions.PARTITION_FORMAT).orElse(FlinkOptions.PARTITION_FORMAT_DAY));
+    boolean hiveStylePartitioning = 
conf.get(FlinkOptions.HIVE_STYLE_PARTITIONING);
+    LocalDate today = conf.get(FlinkOptions.WRITE_UTC_TIMEZONE) ? 
LocalDate.now(ZoneOffset.UTC) : LocalDate.now();
+    LocalDate cutoff = today.minusDays(bootstrapDays);
+
+    List<String> partitionsInWindow = new ArrayList<>();
+    for (String partitionPath : partitionPaths) {
+      LocalDate partitionDate = StreamerUtil.parsePartitionDate(partitionPath, 
formatter, hiveStylePartitioning);
+      if (partitionDate != null && partitionDate.isAfter(cutoff) && 
!partitionDate.isAfter(today)) {
+        partitionsInWindow.add(partitionPath);
+      }
+    }
+    return partitionsInWindow;
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/Pipelines.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/Pipelines.java
index 7bf09e60234b..2410a3e18671 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/Pipelines.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/sink/utils/Pipelines.java
@@ -33,8 +33,8 @@ import org.apache.hudi.sink.CleanFunction;
 import org.apache.hudi.sink.StreamWriteOperator;
 import org.apache.hudi.sink.append.AppendWriteFunctions;
 import org.apache.hudi.sink.append.AppendWriteOperator;
-import org.apache.hudi.sink.bootstrap.BootstrapOperator;
-import org.apache.hudi.sink.bootstrap.RLIBootstrapOperator;
+import org.apache.hudi.sink.bootstrap.AbstractBootstrapOperator;
+import org.apache.hudi.sink.bootstrap.BootstrapOperatorFactory;
 import org.apache.hudi.sink.bootstrap.batch.BatchBootstrapOperator;
 import org.apache.hudi.sink.bucket.BucketBulkInsertWriterHelper;
 import org.apache.hudi.sink.bucket.BucketStreamWriteOperator;
@@ -427,11 +427,12 @@ public class Pipelines {
 
     boolean isGlobalRLI = OptionsResolver.isGlobalRecordLevelIndex(conf);
     if (conf.get(FlinkOptions.INDEX_BOOTSTRAP_ENABLED) || (bounded && 
!isGlobalRLI)) {
+      AbstractBootstrapOperator bootstrapOperator = 
BootstrapOperatorFactory.createInstance(conf);
       dataStream1 = dataStream1
           .transform(
               "index_bootstrap",
               new HoodieFlinkInternalRowTypeInfo(rowType),
-              isGlobalRLI ? new RLIBootstrapOperator(conf) : new 
BootstrapOperator(conf))
+              bootstrapOperator)
           
.setParallelism(conf.getOptional(FlinkOptions.INDEX_BOOTSTRAP_TASKS).orElse(dataStream1.getParallelism()))
           .uid(opUID("index_bootstrap", conf));
       ((OneInputTransformation<?, ?>) 
dataStream1.getTransformation()).setChainingStrategy(ChainingStrategy.ALWAYS);
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableFactory.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableFactory.java
index 0d1437c0216c..554e75d1f4eb 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableFactory.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/table/HoodieTableFactory.java
@@ -493,7 +493,6 @@ public class HoodieTableFactory implements 
DynamicTableSourceFactory, DynamicTab
       
conf.setString(HoodieMetadataConfig.RECORD_LEVEL_INDEX_ENABLE_PROP.key(), 
"true");
       conf.set(FlinkOptions.INDEX_GLOBAL_ENABLED, false);
       conf.setString(HoodieMetadataConfig.STREAMING_WRITE_ENABLED.key(), 
"true");
-      conf.set(FlinkOptions.INDEX_BOOTSTRAP_ENABLED, false);
       if (!conf.contains(FlinkOptions.INDEX_RLI_WRITE_BUFFER_SIZE)) {
         conf.set(FlinkOptions.INDEX_RLI_WRITE_BUFFER_SIZE, 
OptionsResolver.getWriteBufferSizeInBytes(conf) / 1024 / 1024 / 4);
       }
diff --git 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/StreamerUtil.java
 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/StreamerUtil.java
index 099da2c66eeb..4da31daddece 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/StreamerUtil.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/main/java/org/apache/hudi/util/StreamerUtil.java
@@ -92,6 +92,9 @@ import org.apache.parquet.hadoop.ParquetFileWriter;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.StringReader;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
@@ -1025,4 +1028,21 @@ public class StreamerUtil {
     }
     return Option.empty();
   }
+
+  public static LocalDate parsePartitionDate(String partitionPath, 
DateTimeFormatter formatter, boolean hiveStylePartitioning) {
+    String dateValue = partitionPath;
+    if (hiveStylePartitioning) {
+      int idx = partitionPath.indexOf('=');
+      if (idx >= 0) {
+        dateValue = partitionPath.substring(idx + 1);
+      }
+    }
+    try {
+      return LocalDate.parse(dateValue, formatter);
+    } catch (DateTimeParseException e) {
+      log.warn("Skip preloading partition {} because its path cannot be parsed 
as a date with format {}",
+              partitionPath, formatter, e);
+      return null;
+    }
+  }
 }
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/configuration/TestOptionsResolver.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/configuration/TestOptionsResolver.java
index d0ebf5fba637..15ff8110dcbb 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/configuration/TestOptionsResolver.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/configuration/TestOptionsResolver.java
@@ -407,6 +407,37 @@ public class TestOptionsResolver {
         OptionsResolver.getConflictResolutionStrategy(conf));
   }
 
+  @Test
+  void testPartitionedRLIWithRocksDBBackend() {
+    // isTimeBoundedRLIBootstrapEnabled requires all three of: 
INDEX_BOOTSTRAP_ENABLED explicitly
+    // turned on by the user (HoodieTableFactory no longer forces this for 
RECORD_LEVEL_INDEX),
+    // a rocksdb-backed RLI cache, and a positive bootstrap-days window.
+    Configuration conf = new Configuration();
+    conf.set(FlinkOptions.INDEX_TYPE, 
HoodieIndex.IndexType.RECORD_LEVEL_INDEX.name());
+    conf.set(FlinkOptions.INDEX_BOOTSTRAP_ENABLED, true);
+    conf.set(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS, 1);
+
+    // Wrong backend type.
+    conf.set(FlinkOptions.INDEX_RLI_BACKEND_TYPE, "mdt");
+    assertFalse(OptionsResolver.isTimeBoundedRLIBootstrapEnabled(conf));
+
+    // Backend type matches, case-insensitively.
+    conf.set(FlinkOptions.INDEX_RLI_BACKEND_TYPE, "rocksdb");
+    assertTrue(OptionsResolver.isTimeBoundedRLIBootstrapEnabled(conf));
+    conf.set(FlinkOptions.INDEX_RLI_BACKEND_TYPE, "RocksDB");
+    assertTrue(OptionsResolver.isTimeBoundedRLIBootstrapEnabled(conf));
+
+    // Bootstrap days must be positive.
+    conf.set(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS, 0);
+    assertFalse(OptionsResolver.isTimeBoundedRLIBootstrapEnabled(conf));
+    conf.set(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS, 1);
+
+    // INDEX_BOOTSTRAP_ENABLED must be explicitly turned on, even with the 
rest configured.
+    conf.set(FlinkOptions.INDEX_BOOTSTRAP_ENABLED, false);
+    assertFalse(OptionsResolver.isTimeBoundedRLIBootstrapEnabled(conf));
+    conf.set(FlinkOptions.INDEX_BOOTSTRAP_ENABLED, true);
+  }
+
   @Test
   void testWriteBufferSizingAndManagedMemory() {
     Configuration conf = new Configuration();
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bootstrap/TestTimeBoundedRLIBootstrapOperator.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bootstrap/TestTimeBoundedRLIBootstrapOperator.java
new file mode 100644
index 000000000000..32771bd80c09
--- /dev/null
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/bootstrap/TestTimeBoundedRLIBootstrapOperator.java
@@ -0,0 +1,210 @@
+/*
+ * 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.hudi.sink.bootstrap;
+
+import org.apache.hudi.client.model.HoodieFlinkInternalRow;
+import org.apache.hudi.common.table.HoodieTableMetaClient;
+import org.apache.hudi.common.util.Functions;
+import org.apache.hudi.common.util.hash.BucketIndexUtil;
+import org.apache.hudi.configuration.FlinkOptions;
+import org.apache.hudi.index.HoodieIndex;
+import org.apache.hudi.metadata.MetadataPartitionType;
+import org.apache.hudi.util.StreamerUtil;
+import org.apache.hudi.utils.TestConfigurations;
+
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link TimeBoundedRLIBootstrapOperator}.
+ */
+public class TestTimeBoundedRLIBootstrapOperator {
+
+  @TempDir
+  File tempFile;
+
+  @Test
+  void testSkipPreloadWhenBootstrapDaysIsNonPositive() throws Exception {
+    Configuration conf = getTimeBoundedRLIConf();
+    conf.set(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS, 0);
+    StreamerUtil.initTableIfNotExists(conf);
+
+    try (OneInputStreamOperatorTestHarness<HoodieFlinkInternalRow, 
HoodieFlinkInternalRow> harness =
+             new OneInputStreamOperatorTestHarness<>(new 
TimeBoundedRLIBootstrapOperator(conf), 1, 1, 0)) {
+      harness.open();
+
+      assertEquals(0, harness.getOutput().size());
+    }
+  }
+
+  @Test
+  void testSkipPreloadForFreshTableWithoutMetadataTable() throws Exception {
+    Configuration conf = getTimeBoundedRLIConf();
+    StreamerUtil.initTableIfNotExists(conf);
+
+    try (OneInputStreamOperatorTestHarness<HoodieFlinkInternalRow, 
HoodieFlinkInternalRow> harness =
+             new OneInputStreamOperatorTestHarness<>(new 
TimeBoundedRLIBootstrapOperator(conf), 1, 1, 0)) {
+      harness.open();
+
+      assertEquals(0, harness.getOutput().size());
+    }
+  }
+
+  @Test
+  void testFailFastWhenMetadataTableIsMarkedAvailableButCannotBeLoaded() 
throws Exception {
+    Configuration conf = getTimeBoundedRLIConf();
+    HoodieTableMetaClient metaClient = StreamerUtil.initTableIfNotExists(conf);
+    metaClient.getTableConfig().setMetadataPartitionState(metaClient, 
MetadataPartitionType.FILES.getPartitionPath(), true);
+
+    try (OneInputStreamOperatorTestHarness<HoodieFlinkInternalRow, 
HoodieFlinkInternalRow> harness =
+             new OneInputStreamOperatorTestHarness<>(new 
TimeBoundedRLIBootstrapOperator(conf), 1, 1, 0)) {
+      RuntimeException error = assertThrows(RuntimeException.class, 
harness::open);
+
+      assertEquals("Can not initialize the table metadata", 
error.getMessage());
+    }
+  }
+
+  @Test
+  void testFilterPartitionsInWindowWithDefaultDayFormat() {
+    Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
+    TimeBoundedRLIBootstrapOperator operator = new 
TimeBoundedRLIBootstrapOperator(conf);
+
+    DateTimeFormatter formatter = 
DateTimeFormatter.ofPattern(FlinkOptions.PARTITION_FORMAT_DAY);
+    LocalDate today = LocalDate.now(ZoneOffset.UTC);
+    int bootstrapDays = 3;
+
+    String todayPartition = today.format(formatter);
+    String oneDayAgoPartition = today.minusDays(1).format(formatter);
+    String cutoffPartition = today.minusDays(bootstrapDays).format(formatter);
+    String justInsideWindowPartition = today.minusDays(bootstrapDays - 
1).format(formatter);
+    String futurePartition = today.plusDays(1).format(formatter);
+    String unparsablePartition = "not-a-date";
+
+    List<String> partitionPaths = Arrays.asList(
+        todayPartition, oneDayAgoPartition, cutoffPartition, 
justInsideWindowPartition, futurePartition, unparsablePartition);
+
+    List<String> result = operator.filterPartitionsInWindow(partitionPaths, 
bootstrapDays);
+
+    assertEquals(3, result.size());
+    assertTrue(result.contains(todayPartition));
+    assertTrue(result.contains(oneDayAgoPartition));
+    assertTrue(result.contains(justInsideWindowPartition));
+    assertFalse(result.contains(cutoffPartition));
+    assertFalse(result.contains(futurePartition));
+    assertFalse(result.contains(unparsablePartition));
+  }
+
+  @Test
+  void testFilterPartitionsInWindowWithHiveStylePartitioning() {
+    Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
+    conf.set(FlinkOptions.HIVE_STYLE_PARTITIONING, true);
+    TimeBoundedRLIBootstrapOperator operator = new 
TimeBoundedRLIBootstrapOperator(conf);
+
+    DateTimeFormatter formatter = 
DateTimeFormatter.ofPattern(FlinkOptions.PARTITION_FORMAT_DAY);
+    LocalDate today = LocalDate.now(ZoneOffset.UTC);
+    int bootstrapDays = 3;
+
+    String todayPartition = "dt=" + today.format(formatter);
+    String outsideWindowPartition = "dt=" + 
today.minusDays(bootstrapDays).format(formatter);
+
+    List<String> result = 
operator.filterPartitionsInWindow(Arrays.asList(todayPartition, 
outsideWindowPartition), bootstrapDays);
+
+    assertEquals(1, result.size());
+    assertTrue(result.contains(todayPartition));
+  }
+
+  @Test
+  void testFilterPartitionsInWindowWithCustomPartitionFormat() {
+    Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
+    conf.set(FlinkOptions.PARTITION_FORMAT, 
FlinkOptions.PARTITION_FORMAT_DASHED_DAY);
+    TimeBoundedRLIBootstrapOperator operator = new 
TimeBoundedRLIBootstrapOperator(conf);
+
+    DateTimeFormatter formatter = 
DateTimeFormatter.ofPattern(FlinkOptions.PARTITION_FORMAT_DASHED_DAY);
+    LocalDate today = LocalDate.now(ZoneOffset.UTC);
+    int bootstrapDays = 1;
+
+    String todayPartition = today.format(formatter);
+    String outsideWindowPartition = today.minusDays(2).format(formatter);
+
+    List<String> result = 
operator.filterPartitionsInWindow(Arrays.asList(todayPartition, 
outsideWindowPartition), bootstrapDays);
+
+    assertEquals(1, result.size());
+    assertTrue(result.contains(todayPartition));
+  }
+
+  @Test
+  void testFilterPartitionsInWindowWithLocalTimezone() {
+    Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
+    conf.set(FlinkOptions.WRITE_UTC_TIMEZONE, false);
+    TimeBoundedRLIBootstrapOperator operator = new 
TimeBoundedRLIBootstrapOperator(conf);
+
+    DateTimeFormatter formatter = 
DateTimeFormatter.ofPattern(FlinkOptions.PARTITION_FORMAT_DAY);
+    LocalDate today = LocalDate.now();
+    int bootstrapDays = 1;
+
+    String todayPartition = today.format(formatter);
+
+    List<String> result = 
operator.filterPartitionsInWindow(Arrays.asList(todayPartition), bootstrapDays);
+
+    assertEquals(1, result.size());
+    assertTrue(result.contains(todayPartition));
+  }
+
+  @Test
+  void testShouldLoadBucketMatchesPartitionIndexFunc() {
+    Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
+    TimeBoundedRLIBootstrapOperator operator = new 
TimeBoundedRLIBootstrapOperator(conf);
+
+    int parallelism = 4;
+    Functions.Function3<Integer, String, Integer, Integer> partitionIndexFunc 
= BucketIndexUtil.getPartitionIndexFunc(parallelism);
+    operator.partitionIndexFunc = partitionIndexFunc;
+
+    String partitionPath = "20260101";
+    int fileGroupCount = 5;
+
+    for (int fileGroupIdx = 0; fileGroupIdx < fileGroupCount; fileGroupIdx++) {
+      int expectedTask = partitionIndexFunc.apply(fileGroupCount, 
partitionPath, fileGroupIdx);
+      for (int taskID = 0; taskID < parallelism; taskID++) {
+        assertEquals(expectedTask == taskID, 
operator.shouldLoadBucket(partitionPath, fileGroupCount, fileGroupIdx, taskID));
+      }
+    }
+  }
+
+  private Configuration getTimeBoundedRLIConf() {
+    Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());
+    conf.set(FlinkOptions.METADATA_ENABLED, true);
+    conf.set(FlinkOptions.INDEX_TYPE, 
HoodieIndex.IndexType.GLOBAL_RECORD_LEVEL_INDEX.name());
+    conf.set(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS, 3);
+    return conf;
+  }
+}
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestPipelines.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestPipelines.java
index 09340cd270ec..3b0244b5d343 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestPipelines.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/sink/utils/TestPipelines.java
@@ -25,6 +25,7 @@ import org.apache.hudi.configuration.FlinkOptions;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.exception.HoodieNotSupportedException;
 import org.apache.hudi.index.HoodieIndex;
+import org.apache.hudi.sink.bootstrap.TimeBoundedRLIBootstrapOperator;
 import org.apache.hudi.sink.partitioner.GlobalRecordIndexPartitioner;
 import org.apache.hudi.utils.TestConfigurations;
 
@@ -34,6 +35,8 @@ import org.apache.flink.streaming.api.datastream.DataStream;
 import org.apache.flink.streaming.api.datastream.DataStreamSink;
 import org.apache.flink.streaming.api.datastream.DataStreamSource;
 import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
+import org.apache.flink.streaming.api.operators.SimpleOperatorFactory;
+import org.apache.flink.streaming.api.transformations.OneInputTransformation;
 import org.apache.flink.streaming.api.transformations.PartitionTransformation;
 import org.apache.flink.streaming.runtime.partitioner.CustomPartitionerWrapper;
 import org.apache.flink.streaming.runtime.partitioner.StreamPartitioner;
@@ -48,6 +51,7 @@ import java.util.List;
 import java.util.stream.Collectors;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -101,6 +105,49 @@ public class TestPipelines {
     assertEquals(3, streaming.getParallelism());
   }
 
+  @Test
+  void 
testPartitionedRLIWithRocksDBBackendUsesPartitionedRLIBootstrapOperator() {
+    // HoodieTableFactory no longer forces INDEX_BOOTSTRAP_ENABLED to false 
for RECORD_LEVEL_INDEX,
+    // so a user that wants time-bounded RLI bootstrap sets the flag 
explicitly alongside the
+    // rocksdb backend config; set it here to exercise the same gate a 
factory-built sink goes through.
+    Configuration conf = defaultConf();
+    conf.set(FlinkOptions.INDEX_GLOBAL_ENABLED, false);
+    conf.set(FlinkOptions.INDEX_TYPE, 
HoodieIndex.IndexType.RECORD_LEVEL_INDEX.name());
+    conf.set(FlinkOptions.INDEX_RLI_BACKEND_TYPE, "rocksdb");
+    conf.set(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS, 1);
+    conf.set(FlinkOptions.INDEX_BOOTSTRAP_ENABLED, true);
+    DataStream<RowData> input = rowDataInput();
+
+    DataStream<HoodieFlinkInternalRow> streaming =
+        Pipelines.bootstrap(conf, TestConfigurations.ROW_TYPE, input, false, 
false);
+
+    assertEquals("index_bootstrap", streaming.getTransformation().getName());
+    assertInstanceOf(TimeBoundedRLIBootstrapOperator.class, 
bootstrapOperator(streaming));
+  }
+
+  @Test
+  void 
testPartitionedRLIWithRocksDBBackendSkipsBootstrapWhenNotExplicitlyEnabled() {
+    // With the forced false removed from HoodieTableFactory, 
INDEX_BOOTSTRAP_ENABLED is now the
+    // single source of truth in Pipelines: rocksdb backend + bootstrap-days 
config alone must not
+    // wire in a bootstrap operator unless the flag itself is turned on.
+    Configuration conf = defaultConf();
+    conf.set(FlinkOptions.INDEX_GLOBAL_ENABLED, false);
+    conf.set(FlinkOptions.INDEX_TYPE, 
HoodieIndex.IndexType.RECORD_LEVEL_INDEX.name());
+    conf.set(FlinkOptions.INDEX_RLI_BACKEND_TYPE, "rocksdb");
+    conf.set(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS, 1);
+    DataStream<RowData> input = rowDataInput();
+
+    DataStream<HoodieFlinkInternalRow> streaming =
+        Pipelines.bootstrap(conf, TestConfigurations.ROW_TYPE, input, false, 
false);
+
+    assertNotEquals("index_bootstrap", 
streaming.getTransformation().getName());
+  }
+
+  private Object bootstrapOperator(DataStream<HoodieFlinkInternalRow> stream) {
+    OneInputTransformation<?, ?> transformation = (OneInputTransformation<?, 
?>) stream.getTransformation();
+    return ((SimpleOperatorFactory<?>) 
transformation.getOperatorFactory()).getOperator();
+  }
+
   @Test
   void testWritePipelineOperatorGraphs() {
     Configuration conf = defaultConf();
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableFactory.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableFactory.java
index 1bbfbb65418f..4400e580530b 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableFactory.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/table/TestHoodieTableFactory.java
@@ -68,8 +68,10 @@ import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Test cases for {@link HoodieTableFactory}.
@@ -849,6 +851,65 @@ public class TestHoodieTableFactory {
     
assertThat(globalRLIWithBootstrapResolvedConf.get(FlinkOptions.INDEX_BOOTSTRAP_ENABLED),
 is(true));
   }
 
+  @Test
+  void testRecordLevelIndexBootstrapEnabledDefaultsToFalseWhenUnset() {
+    // HoodieTableFactory no longer forces INDEX_BOOTSTRAP_ENABLED for 
RECORD_LEVEL_INDEX; when the
+    // user never sets it, the option's own default (false) should simply pass 
through untouched.
+    Configuration rliConf = new Configuration(this.conf);
+    rliConf.set(FlinkOptions.OPERATION, "upsert");
+    rliConf.set(FlinkOptions.INDEX_TYPE, 
HoodieIndex.IndexType.RECORD_LEVEL_INDEX.name());
+    rliConf.set(FlinkOptions.METADATA_ENABLED, true);
+    rliConf.set(FlinkOptions.INDEX_GLOBAL_ENABLED, false);
+
+    HoodieTableSink noBootstrapSink =
+        (HoodieTableSink) new 
HoodieTableFactory().createDynamicTableSink(MockContext.getInstance(rliConf));
+    
assertThat(noBootstrapSink.getConf().get(FlinkOptions.INDEX_BOOTSTRAP_ENABLED), 
is(false));
+  }
+
+  @Test
+  void testFactoryBuiltSinkKeepsTimeBoundedRLIBootstrapEnabled() {
+    // The user is responsible for turning INDEX_BOOTSTRAP_ENABLED on for 
time-bounded RLI
+    // bootstrap; HoodieTableFactory must pass that choice through unchanged 
rather than
+    // overriding it, so the bootstrap pipeline stays reachable.
+    Configuration rliConf = new Configuration(this.conf);
+    rliConf.set(FlinkOptions.OPERATION, "upsert");
+    rliConf.set(FlinkOptions.INDEX_TYPE, 
HoodieIndex.IndexType.RECORD_LEVEL_INDEX.name());
+    rliConf.set(FlinkOptions.METADATA_ENABLED, true);
+    rliConf.set(FlinkOptions.INDEX_GLOBAL_ENABLED, false);
+    rliConf.set(FlinkOptions.INDEX_RLI_BACKEND_TYPE, "rocksdb");
+    rliConf.set(FlinkOptions.INDEX_RLI_CACHE_ROCKSDB_BOOTSTRAP_DAYS, 1);
+    rliConf.set(FlinkOptions.INDEX_BOOTSTRAP_ENABLED, true);
+
+    HoodieTableSink rliSink =
+        (HoodieTableSink) new 
HoodieTableFactory().createDynamicTableSink(MockContext.getInstance(rliConf));
+    Configuration rliResolvedConf = rliSink.getConf();
+
+    assertThat(rliResolvedConf.get(FlinkOptions.INDEX_BOOTSTRAP_ENABLED), 
is(true));
+    
assertTrue(OptionsResolver.isTimeBoundedRLIBootstrapEnabled(rliResolvedConf));
+  }
+
+  @Test
+  void 
testFactoryDoesNotForceDisableExplicitBootstrapEnabledForRecordLevelIndex() {
+    // Documents the behavior change from this revision: previously the 
factory always reset
+    // INDEX_BOOTSTRAP_ENABLED to false for RECORD_LEVEL_INDEX. Now a user-set 
true survives even
+    // when the rocksdb time-bounded config isn't present, though 
isTimeBoundedRLIBootstrapEnabled
+    // still requires the full rocksdb + bootstrap-days configuration to 
select the time-bounded
+    // bootstrap operator.
+    Configuration rliConf = new Configuration(this.conf);
+    rliConf.set(FlinkOptions.OPERATION, "upsert");
+    rliConf.set(FlinkOptions.INDEX_TYPE, 
HoodieIndex.IndexType.RECORD_LEVEL_INDEX.name());
+    rliConf.set(FlinkOptions.METADATA_ENABLED, true);
+    rliConf.set(FlinkOptions.INDEX_GLOBAL_ENABLED, false);
+    rliConf.set(FlinkOptions.INDEX_BOOTSTRAP_ENABLED, true);
+
+    HoodieTableSink rliSink =
+        (HoodieTableSink) new 
HoodieTableFactory().createDynamicTableSink(MockContext.getInstance(rliConf));
+    Configuration rliResolvedConf = rliSink.getConf();
+
+    assertThat(rliResolvedConf.get(FlinkOptions.INDEX_BOOTSTRAP_ENABLED), 
is(true));
+    
assertFalse(OptionsResolver.isTimeBoundedRLIBootstrapEnabled(rliResolvedConf));
+  }
+
   @Test
   void testLanceFormatSupportedForFlinkTables() {
     Configuration lanceConf = new Configuration();
diff --git 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/utils/TestStreamerUtil.java
 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/utils/TestStreamerUtil.java
index 3c77c51e5d73..becc48b64786 100644
--- 
a/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/utils/TestStreamerUtil.java
+++ 
b/hudi-flink-datasource/hudi-flink/src/test/java/org/apache/hudi/utils/TestStreamerUtil.java
@@ -66,6 +66,8 @@ import org.mockito.Mockito;
 
 import java.io.File;
 import java.io.IOException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -360,6 +362,26 @@ class TestStreamerUtil {
     assertTrue(StreamerUtil.partitionExists(tempFile.getAbsolutePath(), 
"dt=2026-08-06", hadoopConf));
   }
 
+  @Test
+  void testParsePartitionDate() {
+    DateTimeFormatter dayFormatter = 
DateTimeFormatter.ofPattern(FlinkOptions.PARTITION_FORMAT_DAY);
+    assertEquals(LocalDate.of(2026, 8, 6), 
StreamerUtil.parsePartitionDate("20260806", dayFormatter, false));
+
+    DateTimeFormatter dashedDayFormatter = 
DateTimeFormatter.ofPattern(FlinkOptions.PARTITION_FORMAT_DASHED_DAY);
+    assertEquals(LocalDate.of(2026, 8, 6), 
StreamerUtil.parsePartitionDate("2026-08-06", dashedDayFormatter, false));
+
+    assertEquals(LocalDate.of(2026, 8, 6), 
StreamerUtil.parsePartitionDate("dt=20260806", dayFormatter, true));
+
+    // hiveStylePartitioning=false must not strip the "dt=" prefix, so parsing 
fails.
+    assertNull(StreamerUtil.parsePartitionDate("dt=20260806", dayFormatter, 
false));
+
+    // no '=' present, hive-style parsing falls back to the raw path.
+    assertEquals(LocalDate.of(2026, 8, 6), 
StreamerUtil.parsePartitionDate("20260806", dayFormatter, true));
+
+    assertNull(StreamerUtil.parsePartitionDate("not-a-date", dayFormatter, 
false));
+    assertNull(StreamerUtil.parsePartitionDate("2026-08-06", dayFormatter, 
false));
+  }
+
   @Test
   void testOrderingFieldAndKeyGeneratorValidation() {
     Configuration conf = 
TestConfigurations.getDefaultConf(tempFile.getAbsolutePath());

Reply via email to