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 feecdfe064 [core] Add global index search mode (#8296)
feecdfe064 is described below

commit feecdfe064e7a79946467ec3e37cabdae3d912fd
Author: Jingsong Lee <[email protected]>
AuthorDate: Sat Jun 20 18:41:11 2026 +0800

    [core] Add global index search mode (#8296)
    
    Adds `global-index.search-mode` so global index scans can choose between 
indexed-only search and scanning unindexed raw rows when needed.
---
 docs/docs/multimodal-table/global-index.mdx        |  17 ++-
 docs/generated/core_configuration.html             |   6 +
 .../main/java/org/apache/paimon/CoreOptions.java   |  43 ++++++
 .../apache/paimon/predicate/PredicateVisitor.java  |  15 +++
 .../paimon/predicate/PredicateVisitorTest.java     |  53 ++++++++
 .../paimon/globalindex/DataEvolutionBatchScan.java |   7 +-
 .../paimon/globalindex/GlobalIndexScanner.java     | 147 ++++++++++++++++++---
 .../paimon/table/BtreeGlobalIndexTableTest.java    | 106 +++++++++++++++
 8 files changed, 375 insertions(+), 19 deletions(-)

diff --git a/docs/docs/multimodal-table/global-index.mdx 
b/docs/docs/multimodal-table/global-index.mdx
index 90a8ac33bc..1cecce7993 100644
--- a/docs/docs/multimodal-table/global-index.mdx
+++ b/docs/docs/multimodal-table/global-index.mdx
@@ -129,8 +129,20 @@ ALTER TABLE my_table SET (
 
 Global index files cover row-id ranges. If more rows are appended after an 
index is built, those
 new rows are not automatically covered by the existing index files. Run 
`create_global_index` again
-to build index files for newly uncovered data. A query that can be answered by 
a matching global
-index reads indexed row ranges; rows in uncovered ranges are not returned for 
that indexed query.
+to build index files for newly uncovered data. By default, queries use fast 
search and only read
+indexed row ranges; rows in uncovered ranges are not returned for that indexed 
query.
+
+To improve freshness for query types that support raw-data search, set:
+
+```sql
+ALTER TABLE my_table SET ('global-index.search-mode' = 'full');
+```
+
+With `full` search, supported global-index queries first use the snapshot 
`nextRowId` and global
+index row-id coverage to detect whether any row range is missing from the 
index. Raw data is scanned
+only when such a gap exists. Use `detail` search when data files may have been 
rewritten or updated
+after index creation; it scans data file metadata to find the exact unindexed 
row ranges and can
+handle index invalidation caused by updates or rewrites.
 
 To temporarily disable global-index scan acceleration while keeping the index 
files, set:
 
@@ -147,6 +159,7 @@ These table options affect global index build and read 
behavior:
 | Option | Default | Description |
 |---|---|---|
 | `global-index.enabled` | `true` | Whether scans can use global indexes. |
+| `global-index.search-mode` | `fast` | Search mode for global-index queries. 
`fast` searches indexed data only. `full` checks snapshot `nextRowId` against 
global index row-id coverage and scans raw data only if a gap exists. `detail` 
scans data file metadata to find exact unindexed rows and can handle index 
invalidation caused by updates or rewrites. |
 | `global-index.external-path` | Not set | Root directory for global index 
files. If not set, files are stored under the table index directory. |
 | `sorted-index.records-per-range` | `10000000` | Expected number of records 
per sorted global index file for BTree and Bitmap builds. |
 | `sorted-index.build.max-parallelism` | `4096` | Maximum Flink or Spark 
parallelism for building sorted global indexes. |
diff --git a/docs/generated/core_configuration.html 
b/docs/generated/core_configuration.html
index 575c40e21f..9d1a962463 100644
--- a/docs/generated/core_configuration.html
+++ b/docs/generated/core_configuration.html
@@ -746,6 +746,12 @@ under the License.
             <td>Long</td>
             <td>Row count per shard for global index.</td>
         </tr>
+        <tr>
+            <td><h5>global-index.search-mode</h5></td>
+            <td style="word-wrap: break-word;">fast</td>
+            <td><p>Enum</p></td>
+            <td>Search mode for global index queries. Supported values are 
'fast', 'full', and 'detail'.<br /><br />Possible values:<ul><li>"fast": Only 
search indexed data.</li><li>"full": Use snapshot next row id and global index 
coverage to detect missing row ids, and scan raw data only when a gap 
exists.</li><li>"detail": Scan data files to find exact unindexed rows. This 
can handle index invalidation caused by updates or rewrites.</li></ul></td>
+        </tr>
         <tr>
             <td><h5>global-index.thread-num</h5></td>
             <td style="word-wrap: break-word;">32</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 f2a0d32a4d..a884752434 100644
--- a/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
+++ b/paimon-api/src/main/java/org/apache/paimon/CoreOptions.java
@@ -2543,6 +2543,14 @@ public class CoreOptions implements Serializable {
                     .defaultValue(true)
                     .withDescription("Whether to enable global index for 
scan.");
 
+    public static final ConfigOption<GlobalIndexSearchMode> 
GLOBAL_INDEX_SEARCH_MODE =
+            key("global-index.search-mode")
+                    .enumType(GlobalIndexSearchMode.class)
+                    .defaultValue(GlobalIndexSearchMode.FAST)
+                    .withDescription(
+                            "Search mode for global index queries. "
+                                    + "Supported values are 'fast', 'full', 
and 'detail'.");
+
     public static final ConfigOption<Integer> GLOBAL_INDEX_THREAD_NUM =
             key("global-index.thread-num")
                     .intType()
@@ -4049,6 +4057,10 @@ public class CoreOptions implements Serializable {
         return options.get(GLOBAL_INDEX_ENABLED);
     }
 
+    public GlobalIndexSearchMode globalIndexSearchMode() {
+        return options.get(GLOBAL_INDEX_SEARCH_MODE);
+    }
+
     public Integer globalIndexThreadNum() {
         return options.get(GLOBAL_INDEX_THREAD_NUM);
     }
@@ -4846,4 +4858,35 @@ public class CoreOptions implements Serializable {
         /** Drop all global index entries for the whole partitions affected by 
the update. */
         DROP_PARTITION_INDEX
     }
+
+    /** Search mode for global index queries. */
+    public enum GlobalIndexSearchMode implements DescribedEnum {
+        FAST("fast", "Only search indexed data."),
+        FULL(
+                "full",
+                "Use snapshot next row id and global index coverage to detect 
missing row ids, "
+                        + "and scan raw data only when a gap exists."),
+        DETAIL(
+                "detail",
+                "Scan data files to find exact unindexed rows. "
+                        + "This can handle index invalidation caused by 
updates or rewrites.");
+
+        private final String value;
+        private final String description;
+
+        GlobalIndexSearchMode(String value, String description) {
+            this.value = value;
+            this.description = description;
+        }
+
+        @Override
+        public String toString() {
+            return value;
+        }
+
+        @Override
+        public InlineElement getDescription() {
+            return text(description);
+        }
+    }
 }
diff --git 
a/paimon-common/src/main/java/org/apache/paimon/predicate/PredicateVisitor.java 
b/paimon-common/src/main/java/org/apache/paimon/predicate/PredicateVisitor.java
index f14cb32886..9f4047b8cb 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/predicate/PredicateVisitor.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/predicate/PredicateVisitor.java
@@ -18,6 +18,8 @@
 
 package org.apache.paimon.predicate;
 
+import org.apache.paimon.types.RowType;
+
 import javax.annotation.Nullable;
 
 import java.util.Collections;
@@ -38,6 +40,19 @@ public interface PredicateVisitor<T> {
         return predicate.visit(new FieldNameCollector());
     }
 
+    static Set<Integer> collectFieldIds(RowType rowType, @Nullable Predicate 
predicate) {
+        if (predicate == null) {
+            return Collections.emptySet();
+        }
+        Set<Integer> fieldIds = new HashSet<>();
+        for (String name : collectFieldNames(predicate)) {
+            if (rowType.containsField(name)) {
+                fieldIds.add(rowType.getField(name).id());
+            }
+        }
+        return fieldIds;
+    }
+
     /** A visitor that collects all field names referenced by a predicate. */
     class FieldNameCollector implements PredicateVisitor<Set<String>> {
 
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateVisitorTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateVisitorTest.java
new file mode 100644
index 0000000000..393e05f3ea
--- /dev/null
+++ 
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateVisitorTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.predicate;
+
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link PredicateVisitor}. */
+public class PredicateVisitorTest {
+
+    @Test
+    public void testCollectFieldIdsUsesFieldIdInsteadOfFieldPosition() {
+        RowType rowType =
+                new RowType(
+                        Arrays.asList(
+                                new DataField(10, "a", DataTypes.INT()),
+                                new DataField(20, "b", DataTypes.INT()),
+                                new DataField(30, "c", DataTypes.INT())));
+        PredicateBuilder builder = new PredicateBuilder(rowType);
+
+        Predicate predicate =
+                PredicateBuilder.or(
+                        builder.equal(0, 1),
+                        PredicateBuilder.and(builder.equal(1, 2), 
builder.equal(0, 3)));
+
+        assertThat(PredicateVisitor.collectFieldIds(rowType, predicate))
+                .containsExactlyInAnyOrder(10, 20);
+        assertThat(PredicateVisitor.collectFieldIds(rowType, null)).isEmpty();
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/globalindex/DataEvolutionBatchScan.java
 
b/paimon-core/src/main/java/org/apache/paimon/globalindex/DataEvolutionBatchScan.java
index 8d6038ccc4..b03f27f3d3 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/globalindex/DataEvolutionBatchScan.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/globalindex/DataEvolutionBatchScan.java
@@ -282,8 +282,9 @@ public class DataEvolutionBatchScan implements 
DataTableScan {
             Optional<GlobalIndexResult> result = scanner.scan(filter);
             if (result.isPresent()) {
                 LOG.info("Scan table '{}' with global index.", table.name());
+                return result;
             }
-            return result;
+            return Optional.empty();
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -296,7 +297,9 @@ public class DataEvolutionBatchScan implements 
DataTableScan {
         Function<Split, List<IndexedSplit>> process =
                 split ->
                         Collections.singletonList(
-                                wrap((DataSplit) split, rowRangeIndex, 
scoreGetter));
+                                split instanceof IndexedSplit
+                                        ? (IndexedSplit) split
+                                        : wrap((DataSplit) split, 
rowRangeIndex, scoreGetter));
         randomlyExecuteSequentialReturn(process, splits, 
null).forEachRemaining(indexedSplits::add);
         return () -> indexedSplits;
     }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexScanner.java
 
b/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexScanner.java
index 1c8bbd3dd7..93504d317c 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexScanner.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/globalindex/GlobalIndexScanner.java
@@ -18,21 +18,29 @@
 
 package org.apache.paimon.globalindex;
 
+import org.apache.paimon.CoreOptions.GlobalIndexSearchMode;
+import org.apache.paimon.Snapshot;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.globalindex.io.GlobalIndexFileReader;
 import org.apache.paimon.index.GlobalIndexMeta;
 import org.apache.paimon.index.IndexFileMeta;
 import org.apache.paimon.index.IndexPathFactory;
+import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.manifest.IndexManifestEntry;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.partition.PartitionPredicate;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.source.DataSplit;
+import org.apache.paimon.table.source.ScanMode;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.snapshot.SnapshotReader;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.Filter;
 import org.apache.paimon.utils.Range;
+import org.apache.paimon.utils.RoaringNavigableMap64;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -51,7 +59,7 @@ import java.util.function.IntFunction;
 import java.util.stream.Collectors;
 
 import static org.apache.paimon.CoreOptions.GLOBAL_INDEX_THREAD_NUM;
-import static org.apache.paimon.predicate.PredicateVisitor.collectFieldNames;
+import static org.apache.paimon.predicate.PredicateVisitor.collectFieldIds;
 import static 
org.apache.paimon.table.source.snapshot.TimeTravelUtil.tryTravelOrLatest;
 import static org.apache.paimon.utils.Preconditions.checkArgument;
 import static org.apache.paimon.utils.Preconditions.checkNotNull;
@@ -60,28 +68,47 @@ import static 
org.apache.paimon.utils.Preconditions.checkNotNull;
 public class GlobalIndexScanner implements Closeable {
 
     private final Options options;
+    private final RowType rowType;
     private final ExecutorService executor;
     private final GlobalIndexEvaluator globalIndexEvaluator;
     private final IndexPathFactory indexPathFactory;
+    private final Map<Integer, List<Range>> coverageByField;
+    private final FileStoreTable table;
+    private final Snapshot snapshot;
+    private final PartitionPredicate partitionFilter;
 
-    public GlobalIndexScanner(
+    private GlobalIndexScanner(
+            FileStoreTable table,
+            Snapshot snapshot,
+            PartitionPredicate partitionFilter,
             Options options,
             RowType rowType,
             FileIO fileIO,
             IndexPathFactory indexPathFactory,
             Collection<IndexFileMeta> indexFiles) {
+        this.table = table;
+        this.snapshot = snapshot;
+        this.partitionFilter = partitionFilter;
         this.options = options;
+        this.rowType = rowType;
         this.executor =
                 
GlobalIndexReadThreadPool.getExecutorService(options.get(GLOBAL_INDEX_THREAD_NUM));
         this.indexPathFactory = indexPathFactory;
         GlobalIndexFileReader indexFileReader = meta -> 
fileIO.newInputStream(meta.filePath());
         Map<Integer, IndexMetaFileGroup> indexMetas = new HashMap<>();
         Map<Integer, List<IndexMetaFileGroup>> extraIndexMetas = new 
HashMap<>();
+        this.coverageByField = new HashMap<>();
         for (IndexFileMeta indexFile : indexFiles) {
             GlobalIndexMeta meta = checkNotNull(indexFile.globalIndexMeta());
             String indexType = indexFile.indexType();
             Range range = new Range(meta.rowRangeStart(), meta.rowRangeEnd());
             int indexFieldId = meta.indexFieldId();
+            coverageByField.computeIfAbsent(indexFieldId, k -> new 
ArrayList<>()).add(range);
+            if (meta.extraFieldIds() != null) {
+                for (int extra : meta.extraFieldIds()) {
+                    coverageByField.computeIfAbsent(extra, k -> new 
ArrayList<>()).add(range);
+                }
+            }
             List<Integer> fieldIds = meta.getIndexedFieldIds();
             IndexMetaFileGroup group = indexMetas.get(indexFieldId);
             if (group == null) {
@@ -172,6 +199,9 @@ public class GlobalIndexScanner implements Closeable {
         }
         return Optional.of(
                 new GlobalIndexScanner(
+                        null,
+                        null,
+                        null,
                         table.coreOptions().toConfiguration(),
                         table.rowType(),
                         table.fileIO(),
@@ -181,11 +211,33 @@ public class GlobalIndexScanner implements Closeable {
 
     public static Optional<GlobalIndexScanner> create(
             FileStoreTable table, PartitionPredicate partitionFilter, 
Predicate filter) {
-        Set<Integer> filterFieldIds =
-                collectFieldNames(filter).stream()
-                        .filter(name -> table.rowType().containsField(name))
-                        .map(name -> table.rowType().getField(name).id())
-                        .collect(Collectors.toSet());
+        Snapshot snapshot = tryTravelOrLatest(table);
+        List<IndexFileMeta> indexFiles =
+                table.store().newIndexFileHandler()
+                        .scan(snapshot, indexFileFilter(table, 
partitionFilter, filter)).stream()
+                        .map(IndexManifestEntry::indexFile)
+                        .collect(Collectors.toList());
+        if (indexFiles.isEmpty()) {
+            return Optional.empty();
+        }
+        return Optional.of(
+                new GlobalIndexScanner(
+                        table,
+                        snapshot,
+                        partitionFilter,
+                        table.coreOptions().toConfiguration(),
+                        table.rowType(),
+                        table.fileIO(),
+                        table.store().pathFactory().globalIndexFileFactory(),
+                        indexFiles));
+    }
+
+    private static Filter<IndexManifestEntry> indexFileFilter(
+            FileStoreTable table, PartitionPredicate partitionFilter, 
Predicate filter) {
+        if (filter == null) {
+            return entry -> false;
+        }
+        Set<Integer> filterFieldIds = collectFieldIds(table.rowType(), filter);
         Filter<IndexManifestEntry> indexFileFilter =
                 entry -> {
                     if (partitionFilter != null && 
!partitionFilter.test(entry.partition())) {
@@ -209,17 +261,82 @@ public class GlobalIndexScanner implements Closeable {
                     }
                     return false;
                 };
-
-        List<IndexFileMeta> indexFiles =
-                
table.store().newIndexFileHandler().scan(tryTravelOrLatest(table), 
indexFileFilter)
-                        .stream()
-                        .map(IndexManifestEntry::indexFile)
-                        .collect(Collectors.toList());
-        return create(table, indexFiles);
+        return indexFileFilter;
     }
 
     public Optional<GlobalIndexResult> scan(Predicate predicate) {
-        return globalIndexEvaluator.evaluate(predicate);
+        Optional<GlobalIndexResult> result = 
globalIndexEvaluator.evaluate(predicate);
+        return result.map(indexedResultRows -> withUnindexedRows(predicate, 
indexedResultRows));
+    }
+
+    private GlobalIndexResult withUnindexedRows(
+            Predicate predicate, GlobalIndexResult indexedResultRows) {
+        if (indexedResultRows instanceof ScoredGlobalIndexResult
+                || table == null
+                || table.coreOptions().globalIndexSearchMode() == 
GlobalIndexSearchMode.FAST) {
+            return indexedResultRows;
+        }
+
+        RoaringNavigableMap64 rows = new RoaringNavigableMap64();
+        rows.or(indexedResultRows.results());
+        for (Range range : unindexedRanges(predicate)) {
+            rows.addRange(range);
+        }
+        return GlobalIndexResult.create(rows);
+    }
+
+    private List<Range> indexedRanges(Predicate predicate) {
+        List<Range> ranges = null;
+        for (Integer fieldId : collectFieldIds(rowType, predicate)) {
+            List<Range> fieldRanges = coverageByField.get(fieldId);
+            if (fieldRanges == null || fieldRanges.isEmpty()) {
+                return Collections.emptyList();
+            }
+            fieldRanges = Range.sortAndMergeOverlap(fieldRanges, true);
+            ranges = ranges == null ? fieldRanges : Range.and(ranges, 
fieldRanges);
+        }
+        return ranges == null ? Collections.emptyList() : 
Range.sortAndMergeOverlap(ranges, true);
+    }
+
+    private List<Range> unindexedRanges(Predicate predicate) {
+        if (snapshot == null || snapshot.nextRowId() == null || 
snapshot.nextRowId() <= 0) {
+            return Collections.emptyList();
+        }
+
+        List<Range> dataRanges;
+        if (table.coreOptions().globalIndexSearchMode() == 
GlobalIndexSearchMode.DETAIL) {
+            dataRanges = dataRangesByDataFiles();
+        } else {
+            dataRanges = Collections.singletonList(new Range(0, 
snapshot.nextRowId() - 1));
+        }
+
+        List<Range> predicateIndexedRanges =
+                Range.sortAndMergeOverlap(indexedRanges(predicate), true);
+        List<Range> unindexedRanges = new ArrayList<>();
+        for (Range dataRange : Range.sortAndMergeOverlap(dataRanges, true)) {
+            unindexedRanges.addAll(dataRange.exclude(predicateIndexedRanges));
+        }
+        return Range.sortAndMergeOverlap(unindexedRanges, true);
+    }
+
+    private List<Range> dataRangesByDataFiles() {
+        SnapshotReader snapshotReader =
+                table.newSnapshotReader()
+                        .withPartitionFilter(partitionFilter)
+                        .withMode(ScanMode.ALL)
+                        .withSnapshot(snapshot);
+        List<Range> dataRanges = new ArrayList<>();
+        for (Split split : snapshotReader.read().splits()) {
+            if (!(split instanceof DataSplit)) {
+                continue;
+            }
+            for (DataFileMeta file : ((DataSplit) split).dataFiles()) {
+                if (file.firstRowId() != null) {
+                    dataRanges.add(file.nonNullRowIdRange());
+                }
+            }
+        }
+        return dataRanges;
     }
 
     private Collection<GlobalIndexReader> createReaders(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
index 3fcd8f2ae4..8d963b11e9 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/table/BtreeGlobalIndexTableTest.java
@@ -18,6 +18,7 @@
 
 package org.apache.paimon.table;
 
+import org.apache.paimon.CoreOptions;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.globalindex.DataEvolutionBatchScan;
@@ -36,6 +37,7 @@ import org.apache.paimon.table.sink.CommitMessage;
 import org.apache.paimon.table.source.DataSplit;
 import org.apache.paimon.table.source.ReadBuilder;
 import org.apache.paimon.table.source.Split;
+import org.apache.paimon.table.source.TableScan;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.utils.Range;
 import org.apache.paimon.utils.RoaringNavigableMap64;
@@ -44,6 +46,7 @@ import org.junit.jupiter.api.Test;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -130,6 +133,73 @@ public class BtreeGlobalIndexTableTest extends 
DataEvolutionTestBase {
         assertThat(readF1).containsExactly("a200", "a300", "a400", "a56789");
     }
 
+    @Test
+    public void testBTreeGlobalIndexSearchModeControlsUnindexedData() throws 
Exception {
+        write(500L);
+        createIndex("f1");
+        appendRows(500, 1000);
+
+        FileStoreTable table = (FileStoreTable) catalog.getTable(identifier());
+        Predicate predicate =
+                new PredicateBuilder(table.rowType())
+                        .in(
+                                1,
+                                Arrays.asList(
+                                        BinaryString.fromString("a100"),
+                                        BinaryString.fromString("a700")));
+
+        assertThat(readF1(table, predicate)).containsExactly("a100");
+
+        assertThat(readF1(tableWithSearchMode(table, "full"), predicate))
+                .containsExactly("a100", "a700");
+        assertThat(readF1(tableWithSearchMode(table, "detail"), predicate))
+                .containsExactly("a100", "a700");
+
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        Predicate andWithUnindexedField =
+                PredicateBuilder.and(
+                        builder.equal(1, BinaryString.fromString("a700")),
+                        builder.equal(2, BinaryString.fromString("b700")));
+
+        assertThat(readF1(table, andWithUnindexedField)).isEmpty();
+        assertThat(readF1(tableWithSearchMode(table, "full"), 
andWithUnindexedField))
+                .containsExactly("a700");
+        assertThat(readF1(tableWithSearchMode(table, "detail"), 
andWithUnindexedField))
+                .containsExactly("a700");
+    }
+
+    @Test
+    public void testBTreeGlobalIndexSearchModeUsesAllPredicateFieldCoverage() 
throws Exception {
+        write(500L);
+        createIndex("f1");
+        appendRows(500, 1000);
+        createIndex("f2");
+
+        FileStoreTable table = (FileStoreTable) catalog.getTable(identifier());
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+        Predicate andPredicate =
+                PredicateBuilder.and(
+                        builder.equal(1, BinaryString.fromString("a700")),
+                        builder.equal(2, BinaryString.fromString("b700")));
+
+        assertThat(readF1(table, andPredicate)).isEmpty();
+        assertThat(readF1(tableWithSearchMode(table, "full"), andPredicate))
+                .containsExactly("a700");
+        assertThat(readF1(tableWithSearchMode(table, "detail"), andPredicate))
+                .containsExactly("a700");
+
+        Predicate orPredicate =
+                PredicateBuilder.or(
+                        builder.equal(1, BinaryString.fromString("a700")),
+                        builder.equal(2, BinaryString.fromString("b701")));
+
+        assertThat(readF1(table, orPredicate)).containsExactly("a701");
+        assertThat(readF1(tableWithSearchMode(table, "full"), orPredicate))
+                .containsExactly("a700", "a701");
+        assertThat(readF1(tableWithSearchMode(table, "detail"), orPredicate))
+                .containsExactly("a700", "a701");
+    }
+
     @Test
     public void testMultipleBTreeIndices() throws Exception {
         write(100000L);
@@ -237,6 +307,42 @@ public class BtreeGlobalIndexTableTest extends 
DataEvolutionTestBase {
                 .collect(Collectors.toList());
     }
 
+    private List<String> readF1(ReadBuilder readBuilder, TableScan.Plan plan) 
throws Exception {
+        List<String> readF1 = new ArrayList<>();
+        readBuilder
+                .newRead()
+                .executeFilter()
+                .createReader(plan)
+                .forEachRemaining(row -> 
readF1.add(row.getString(1).toString()));
+        return readF1;
+    }
+
+    private List<String> readF1(FileStoreTable table, Predicate predicate) 
throws Exception {
+        ReadBuilder readBuilder = table.newReadBuilder().withFilter(predicate);
+        return readF1(readBuilder, readBuilder.newScan().plan());
+    }
+
+    private FileStoreTable tableWithSearchMode(FileStoreTable table, String 
searchMode) {
+        return table.copy(
+                
Collections.singletonMap(CoreOptions.GLOBAL_INDEX_SEARCH_MODE.key(), 
searchMode));
+    }
+
+    private void appendRows(int fromInclusive, int toExclusive) throws 
Exception {
+        FileStoreTable table = (FileStoreTable) catalog.getTable(identifier());
+        BatchWriteBuilder writeBuilder = table.newBatchWriteBuilder();
+        try (BatchTableWrite write = writeBuilder.newWrite();
+                BatchTableCommit commit = writeBuilder.newCommit()) {
+            for (int i = fromInclusive; i < toExclusive; i++) {
+                write.write(
+                        GenericRow.of(
+                                i,
+                                BinaryString.fromString("a" + i),
+                                BinaryString.fromString("b" + i)));
+            }
+            commit.commit(write.prepareCommit());
+        }
+    }
+
     private RoaringNavigableMap64 globalIndexScan(FileStoreTable table, 
Predicate predicate)
             throws Exception {
         try (GlobalIndexScanner scanner =

Reply via email to