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

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


The following commit(s) were added to refs/heads/master by this push:
     new 3764aea534e Drop star-tree indexes left unreadable by a dimension 
encoding change (#19349)
3764aea534e is described below

commit 3764aea534e9b2cc0dffe0ecd0c50f31d5d35b7e
Author: Gonzalo Ortiz Jaureguizar <[email protected]>
AuthorDate: Thu Sep 17 03:08:00 2026 +0200

    Drop star-tree indexes left unreadable by a dimension encoding change 
(#19349)
    
    * Drop star-tree indexes left unreadable by a dimension encoding change
    
    A star-tree stores its dimension values as dictionary ids in a fixed-bit
    forward index whose bit width is read from the main column metadata at load
    time. Moving a star-tree dimension column to `noDictionaryColumns` makes
    `ForwardIndexHandler` re-encode that column to raw, but with
    `enableDynamicStarTreeCreation` disabled `SegmentPreProcessor` leaves the
    star-tree untouched. The segment then becomes permanently unloadable:
    `StarTreeLoaderUtils` asks `FixedBitIntReader` for a reader over the now
    unavailable bit width and gets a bare `IllegalStateException`, so the 
segment
    goes to ERROR on every load, e.g. on any restart that reloads it.
    
    The re-encoding is persisted, so an affected segment is already 
inconsistent on
    disk and `needProcess()` no longer reports anything to do, which makes a 
plain
    reload fail in exactly the same way.
    
    - `SegmentPreProcessor` now removes star-trees whose dimension column lost 
its
      dictionary even when `enableDynamicStarTreeCreation` is disabled. Removing
      star-trees only deletes files, unlike the rebuild that flag guards 
against.
      When the flag is enabled the regular flow already rebuilds them, since the
      split order no longer matches the builder configs.
    - `StarTreeLoaderUtils` skips an unreadable star-tree with a warning 
instead of
      failing the whole segment load, covering segments whose pre-processing is
      skipped.
    - `FixedBitIntReader` now reports the offending number of bits.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
    
    * Verify star-tree survives a RAW forward index with separated dictionary
    
    Address review feedback on #19349:
    - Reflow the unloadableDimensions ternary.
    - Add testStarTreeDimensionConvertedToRawWithSeparatedDictionary, covering 
the
      RAW forward index with separated dictionary configuration added in 
#19153. The
      dimension keeps its dictionary, so the star-tree stays loadable and must 
not be
      dropped by the new repair path.
    
    * Assert the pre-process gates in the star-tree encoding-drift tests
    
    Address review feedback on the new tests:
    
    - testStarTreeDimensionConvertedToRawWithSeparatedDictionary now asserts
      needProcess() before and after processing. The star-tree must not be
      treated as stale, and a second round must be a no-op across every
      handler, not just the star-tree one.
    - testStarTreeDimensionConvertedToNoDictionaryWithoutPreprocess now goes
      through ImmutableSegmentLoader#needPreprocess with skipSegmentPreprocess
      set, which is the gate that actually skips the repair, instead of just
      passing needPreprocess = false to the loader. It also asserts the
      star-tree metadata is still on disk, so the test distinguishes a
      star-tree skipped at load from one that was removed.
    
    * Adapt the skip-preprocess test to the current loader semantics
    
    Two changes on master land on this test:
    
    - #19391 made IndexLoadingConfig snapshot skipSegmentPreprocess instead of
      reading it live from the table config, so setting the flag now needs a
      fresh IndexLoadingConfig rather than an in-place edit.
    - #19394 gated ImmutableSegmentLoader#load on needPreprocess(), so the
      loader can be asked to pre-process and will still honour the flag.
    
    The test now builds a separate config for the flag, asserts needPreprocess()
    flips from true to false across it, and lets the loader decide, which is the
    path the server takes.
    
    ---------
    
    Co-authored-by: Gonzalo Ortiz <[email protected]>
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .../local/io/reader/impl/FixedBitIntReader.java    |   3 +-
 .../segment/index/loader/SegmentPreProcessor.java  |  32 ++-
 .../local/startree/StarTreeBuilderUtils.java       |  34 +++
 .../startree/v2/store/StarTreeLoaderUtils.java     |  19 +-
 .../index/loader/SegmentPreProcessorTest.java      | 272 +++++++++++++++++++++
 5 files changed, 351 insertions(+), 9 deletions(-)

diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/io/reader/impl/FixedBitIntReader.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/io/reader/impl/FixedBitIntReader.java
index 3308566bd71..12091b6b13e 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/io/reader/impl/FixedBitIntReader.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/io/reader/impl/FixedBitIntReader.java
@@ -106,7 +106,8 @@ public abstract class FixedBitIntReader {
       case 31:
         return new Bit31Reader(dataBuffer);
       default:
-        throw new IllegalStateException();
+        throw new IllegalStateException("Illegal number of bits per value: " + 
numBitsPerValue + ", must be within 1 "
+            + "and 31");
     }
   }
 
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessor.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessor.java
index e4d74d336b2..f8dbc0dddce 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessor.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessor.java
@@ -295,16 +295,18 @@ public class SegmentPreProcessor implements AutoCloseable 
{
   }
 
   private boolean needProcessStarTrees() {
+    SegmentMetadataImpl segmentMetadata = 
_segmentDirectory.getSegmentMetadata();
+    List<StarTreeV2Metadata> starTreeMetadataList = 
segmentMetadata.getStarTreeV2MetadataList();
     // Check if there is need to create/modify/remove star-trees.
     if (!_indexLoadingConfig.isEnableDynamicStarTreeCreation()) {
-      return false;
+      // Star-trees left unreadable by a column encoding change are still 
removed, see processStarTrees().
+      return starTreeMetadataList != null && 
!StarTreeBuilderUtils.findUnloadableDimensions(starTreeMetadataList,
+          segmentMetadata).isEmpty();
     }
 
-    SegmentMetadataImpl segmentMetadata = 
_segmentDirectory.getSegmentMetadata();
     List<StarTreeV2BuilderConfig> starTreeBuilderConfigs =
         
StarTreeBuilderUtils.generateBuilderConfigs(_indexLoadingConfig.getStarTreeIndexConfigs(),
             _indexLoadingConfig.isEnableDefaultStarTree(), segmentMetadata);
-    List<StarTreeV2Metadata> starTreeMetadataList = 
segmentMetadata.getStarTreeV2MetadataList();
     // There are existing star-trees, but if they match the builder configs 
exactly,
     // then there is no need to generate the star-trees
 
@@ -397,19 +399,35 @@ public class SegmentPreProcessor implements AutoCloseable 
{
   private boolean processStarTrees(File indexDir,
       @Nullable SegmentOperationsThrottlerSet segmentOperationsThrottlerSet)
       throws Exception {
+    SegmentMetadataImpl segmentMetadata = 
_segmentDirectory.getSegmentMetadata();
+    String segmentName = segmentMetadata.getName();
+    List<StarTreeV2Metadata> starTreeMetadataList = 
segmentMetadata.getStarTreeV2MetadataList();
+
     if (!_indexLoadingConfig.isEnableDynamicStarTreeCreation()) {
-      return false;
+      // A star-tree whose dimension column is no longer dictionary-encoded 
(e.g. because the column was added to
+      // 'noDictionaryColumns' and re-encoded by the forward index handler 
above) cannot be read, and fails the whole
+      // segment load. Drop it even here: removing star-trees only deletes 
files, so unlike rebuilding them it is
+      // cheap enough to do with dynamic star-tree creation disabled. When it 
is enabled the star-trees are rebuilt by
+      // the regular flow below, because their split order no longer matches 
the builder configs.
+      Set<String> unloadableDimensions = starTreeMetadataList != null
+          ? 
StarTreeBuilderUtils.findUnloadableDimensions(starTreeMetadataList, 
segmentMetadata)
+          : Set.of();
+      if (unloadableDimensions.isEmpty()) {
+        return false;
+      }
+      LOGGER.warn("Removing star-trees from segment: {} because dimension 
columns: {} are no longer "
+              + "dictionary-encoded. Enable dynamic star-tree creation to have 
them rebuilt", segmentName,
+          unloadableDimensions);
+      StarTreeBuilderUtils.removeStarTrees(indexDir);
+      return true;
     }
 
-    SegmentMetadataImpl segmentMetadata = 
_segmentDirectory.getSegmentMetadata();
-    String segmentName = segmentMetadata.getName();
     List<StarTreeV2BuilderConfig> starTreeBuilderConfigs =
         
StarTreeBuilderUtils.generateBuilderConfigs(_indexLoadingConfig.getStarTreeIndexConfigs(),
             _indexLoadingConfig.isEnableDefaultStarTree(), segmentMetadata);
 
     boolean shouldGenerateStarTree = !starTreeBuilderConfigs.isEmpty();
     boolean shouldRemoveStarTree = false;
-    List<StarTreeV2Metadata> starTreeMetadataList = 
segmentMetadata.getStarTreeV2MetadataList();
     if (starTreeMetadataList != null) {
       // There are existing star-trees
       if (!shouldGenerateStarTree) {
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/startree/StarTreeBuilderUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/startree/StarTreeBuilderUtils.java
index 72e73b4cfd8..465a5737f40 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/startree/StarTreeBuilderUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/startree/StarTreeBuilderUtils.java
@@ -28,7 +28,9 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Queue;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import javax.annotation.Nullable;
 import org.apache.commons.configuration2.PropertiesConfiguration;
 import org.apache.commons.io.FileUtils;
@@ -36,6 +38,7 @@ import org.apache.pinot.common.request.Literal;
 import org.apache.pinot.common.request.context.ExpressionContext;
 import 
org.apache.pinot.segment.local.startree.v2.builder.StarTreeV2BuilderConfig;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.segment.spi.ColumnMetadata;
 import org.apache.pinot.segment.spi.Constants;
 import org.apache.pinot.segment.spi.SegmentMetadata;
 import 
org.apache.pinot.segment.spi.index.startree.AggregationFunctionColumnPair;
@@ -283,6 +286,37 @@ public class StarTreeBuilderUtils {
     return false;
   }
 
+  /// Returns the first dimension of the given star-tree that the segment can 
no longer back with a dictionary
+  /// encoded forward index, or `null` if the star-tree is loadable.
+  ///
+  /// A star-tree stores its dimension values as dictionary ids in a fixed-bit 
forward index whose bit width is read
+  /// from the *main* column metadata at load time. Re-encoding a dimension 
column to raw (e.g. after adding it to
+  /// `noDictionaryColumns`) therefore leaves the star-tree unreadable, and 
loading the segment fails.
+  @Nullable
+  public static String findUnloadableDimension(StarTreeV2Metadata 
starTreeMetadata, SegmentMetadata segmentMetadata) {
+    for (String dimension : starTreeMetadata.getDimensionsSplitOrder()) {
+      ColumnMetadata columnMetadata = 
segmentMetadata.getColumnMetadataFor(dimension);
+      if (columnMetadata == null || !columnMetadata.hasDictionary()) {
+        return dimension;
+      }
+    }
+    return null;
+  }
+
+  /// Returns the dimensions that make the given star-trees unloadable, or an 
empty set if they are all loadable.
+  /// See [#findUnloadableDimension(StarTreeV2Metadata, SegmentMetadata)].
+  public static Set<String> findUnloadableDimensions(List<StarTreeV2Metadata> 
metadataList,
+      SegmentMetadata segmentMetadata) {
+    Set<String> dimensions = new TreeSet<>();
+    for (StarTreeV2Metadata starTreeMetadata : metadataList) {
+      String dimension = findUnloadableDimension(starTreeMetadata, 
segmentMetadata);
+      if (dimension != null) {
+        dimensions.add(dimension);
+      }
+    }
+    return dimensions;
+  }
+
   /// Returns `true` if the given star-tree builder configs are equal, `false` 
otherwise.
   public static boolean 
areStarTreeBuilderConfigListsEqual(List<StarTreeV2BuilderConfig> builderConfig1,
       List<StarTreeV2BuilderConfig> builderConfig2) {
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/startree/v2/store/StarTreeLoaderUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/startree/v2/store/StarTreeLoaderUtils.java
index 4e6ab15b49b..07e1a8c7d1f 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/startree/v2/store/StarTreeLoaderUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/startree/v2/store/StarTreeLoaderUtils.java
@@ -27,6 +27,7 @@ import 
org.apache.pinot.segment.local.aggregator.ValueAggregatorFactory;
 import 
org.apache.pinot.segment.local.segment.index.forward.ForwardIndexReaderFactory;
 import 
org.apache.pinot.segment.local.segment.index.readers.forward.FixedBitSVForwardIndexReaderV2;
 import org.apache.pinot.segment.local.startree.OffHeapStarTree;
+import org.apache.pinot.segment.local.startree.StarTreeBuilderUtils;
 import org.apache.pinot.segment.spi.ColumnMetadata;
 import org.apache.pinot.segment.spi.datasource.DataSource;
 import org.apache.pinot.segment.spi.index.StandardIndexes;
@@ -42,10 +43,14 @@ import org.apache.pinot.segment.spi.store.SegmentDirectory;
 import org.apache.pinot.spi.data.FieldSpec;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.apache.pinot.spi.data.MetricFieldSpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 
 /// The `StarTreeLoaderUtils` class provides utility methods to load star-tree 
indexes.
 public class StarTreeLoaderUtils {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(StarTreeLoaderUtils.class);
+
   private StarTreeLoaderUtils() {
   }
 
@@ -57,11 +62,23 @@ public class StarTreeLoaderUtils {
     int numStarTrees = starTreeMetadataList.size();
     List<StarTreeV2> starTrees = new ArrayList<>(numStarTrees);
     for (int i = 0; i < numStarTrees; i++) {
+      StarTreeV2Metadata starTreeMetadata = starTreeMetadataList.get(i);
+
+      // A star-tree is unreadable once one of its dimension columns loses its 
dictionary, which happens when the
+      // column is moved to 'noDictionaryColumns' without the star-tree being 
rebuilt. Skip it instead of failing the
+      // whole segment load. SegmentPreProcessor normally removes such 
star-trees, so reaching this point means the
+      // pre-processing was skipped for this segment.
+      String unloadableDimension = 
StarTreeBuilderUtils.findUnloadableDimension(starTreeMetadata, segmentMetadata);
+      if (unloadableDimension != null) {
+        LOGGER.warn("Skipping star-tree: {} in segment: {} because dimension 
column: {} is no longer "
+            + "dictionary-encoded", i, segmentMetadata.getName(), 
unloadableDimension);
+        continue;
+      }
+
       SegmentDirectory.Reader indexReader = 
segmentReader.getStarTreeIndexReader(i);
       // Load star-tree index
       StarTree starTree = new 
OffHeapStarTree(indexReader.getIndexFor(String.valueOf(i), 
StandardIndexes.inverted()));
 
-      StarTreeV2Metadata starTreeMetadata = starTreeMetadataList.get(i);
       int numDocs = starTreeMetadata.getNumDocs();
       Map<String, DataSource> dataSourceMap = new HashMap<>();
 
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessorTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessorTest.java
index c7272504a37..12105b176e6 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessorTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessorTest.java
@@ -39,6 +39,7 @@ import 
org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.RandomStringUtils;
 import org.apache.pinot.segment.local.PinotBuffersAfterClassCheckRule;
+import 
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
 import org.apache.pinot.segment.local.io.util.PinotDataBitSet;
 import org.apache.pinot.segment.local.segment.creator.SegmentTestUtils;
 import 
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
@@ -46,9 +47,11 @@ import 
org.apache.pinot.segment.local.segment.index.converter.SegmentV1V2ToV3For
 import 
org.apache.pinot.segment.local.segment.index.loader.columnminmaxvalue.ColumnMinMaxValueGeneratorMode;
 import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
 import org.apache.pinot.segment.local.segment.store.SegmentLocalFSDirectory;
+import org.apache.pinot.segment.local.startree.StarTreeBuilderUtils;
 import org.apache.pinot.segment.local.utils.SegmentOperationsThrottler;
 import org.apache.pinot.segment.local.utils.SegmentOperationsThrottlerSet;
 import org.apache.pinot.segment.spi.ColumnMetadata;
+import org.apache.pinot.segment.spi.ImmutableSegment;
 import org.apache.pinot.segment.spi.V1Constants;
 import org.apache.pinot.segment.spi.compression.ChunkCompressionType;
 import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
@@ -61,6 +64,7 @@ import org.apache.pinot.segment.spi.index.StandardIndexes;
 import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
 import org.apache.pinot.segment.spi.index.reader.ForwardIndexReader;
 import 
org.apache.pinot.segment.spi.index.startree.AggregationFunctionColumnPair;
+import org.apache.pinot.segment.spi.index.startree.StarTreeV2;
 import org.apache.pinot.segment.spi.index.startree.StarTreeV2Metadata;
 import org.apache.pinot.segment.spi.store.SegmentDirectory;
 import org.apache.pinot.segment.spi.store.SegmentDirectoryPaths;
@@ -81,6 +85,7 @@ import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.data.readers.GenericRow;
 import org.apache.pinot.spi.utils.ByteArray;
+import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.ReadMode;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.annotations.AfterMethod;
@@ -2025,6 +2030,273 @@ public class SegmentPreProcessorTest implements 
PinotBuffersAfterClassCheckRule
     }
   }
 
+  /// A star-tree dimension column that is moved to 'noDictionaryColumns' 
without the star-tree being rebuilt leaves
+  /// the star-tree unreadable: its dimension forward index stores dictionary 
ids in a fixed-bit encoding whose width
+  /// is read from the main column metadata, which is now raw. The stale 
star-tree must be dropped so the segment
+  /// stays loadable, even when dynamic star-tree creation is disabled.
+  @Test
+  public void testStarTreeDimensionConvertedToNoDictionary()
+      throws Exception {
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable").build();
+    Schema schema = new 
Schema.SchemaBuilder().addSingleValueDimension("stringCol", DataType.STRING)
+        .addMetric("longCol", DataType.LONG)
+        .build();
+    IndexingConfig indexingConfig = tableConfig.getIndexingConfig();
+    indexingConfig.setStarTreeIndexConfigs(
+        List.of(new StarTreeIndexConfig(List.of("stringCol"), null, 
List.of("SUM__longCol"), null, 1000)));
+    buildStarTreeTestSegment(tableConfig, schema);
+
+    // Drift the config: the star-tree dimension is moved to 
noDictionaryColumns and the star-tree config is dropped,
+    // while dynamic star-tree creation stays disabled.
+    indexingConfig.setNoDictionaryColumns(List.of("stringCol"));
+    indexingConfig.setStarTreeIndexConfigs(null);
+    indexingConfig.setEnableDynamicStarTreeCreation(false);
+    IndexLoadingConfig indexLoadingConfig = new 
IndexLoadingConfig(tableConfig, schema);
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap);
+        SegmentPreProcessor processor = new 
SegmentPreProcessor(segmentDirectory, indexLoadingConfig)) {
+      assertTrue(processor.needProcess());
+      processor.process(SEGMENT_OPERATIONS_THROTTLER);
+    }
+    assertSegmentLoadsWithoutStarTree(indexLoadingConfig);
+
+    // The stale star-tree is gone, so there is nothing left to process
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap);
+        SegmentPreProcessor processor = new 
SegmentPreProcessor(segmentDirectory, indexLoadingConfig)) {
+      assertFalse(processor.needProcess());
+    }
+  }
+
+  /// Same drift as [#testStarTreeDimensionConvertedToNoDictionary()], but the 
dict-to-raw conversion has already been
+  /// persisted by an earlier pre-processing round, so the segment on disk is 
already inconsistent and nothing else
+  /// needs updating. Pre-processing must still detect and repair it.
+  @Test
+  public void testStarTreeDimensionAlreadyConvertedToNoDictionary()
+      throws Exception {
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable").build();
+    Schema schema = new 
Schema.SchemaBuilder().addSingleValueDimension("stringCol", DataType.STRING)
+        .addMetric("longCol", DataType.LONG)
+        .build();
+    IndexingConfig indexingConfig = tableConfig.getIndexingConfig();
+    indexingConfig.setStarTreeIndexConfigs(
+        List.of(new StarTreeIndexConfig(List.of("stringCol"), null, 
List.of("SUM__longCol"), null, 1000)));
+    buildStarTreeTestSegment(tableConfig, schema);
+
+    // Convert the dimension column to raw while leaving the star-tree in 
place, reproducing the state an earlier
+    // pre-processing round leaves behind.
+    indexingConfig.setNoDictionaryColumns(List.of("stringCol"));
+    IndexLoadingConfig indexLoadingConfig = new 
IndexLoadingConfig(tableConfig, schema);
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap)) {
+      new ForwardIndexHandler(segmentDirectory, 
indexLoadingConfig).updateIndices(segmentDirectory.createWriter());
+    }
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap)) {
+      
assertFalse(segmentDirectory.getSegmentMetadata().getColumnMetadataFor("stringCol").hasDictionary());
+      
assertNotNull(segmentDirectory.getSegmentMetadata().getStarTreeV2MetadataList());
+    }
+
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap);
+        SegmentPreProcessor processor = new 
SegmentPreProcessor(segmentDirectory, indexLoadingConfig)) {
+      assertTrue(processor.needProcess());
+      processor.process(SEGMENT_OPERATIONS_THROTTLER);
+    }
+    assertSegmentLoadsWithoutStarTree(indexLoadingConfig);
+  }
+
+  /// The loader must not fail the whole segment over a stale star-tree even 
when pre-processing never gets a chance to
+  /// repair it, e.g. because it is skipped for the table.
+  @Test
+  public void testStarTreeDimensionConvertedToNoDictionaryWithoutPreprocess()
+      throws Exception {
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable").build();
+    Schema schema = new 
Schema.SchemaBuilder().addSingleValueDimension("stringCol", DataType.STRING)
+        .addMetric("longCol", DataType.LONG)
+        .build();
+    IndexingConfig indexingConfig = tableConfig.getIndexingConfig();
+    indexingConfig.setStarTreeIndexConfigs(
+        List.of(new StarTreeIndexConfig(List.of("stringCol"), null, 
List.of("SUM__longCol"), null, 1000)));
+    buildStarTreeTestSegment(tableConfig, schema);
+
+    indexingConfig.setNoDictionaryColumns(List.of("stringCol"));
+    IndexLoadingConfig indexLoadingConfig = new 
IndexLoadingConfig(tableConfig, schema);
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap)) {
+      new ForwardIndexHandler(segmentDirectory, 
indexLoadingConfig).updateIndices(segmentDirectory.createWriter());
+    }
+
+    // Pre-processing would repair the segment, so it has to be off for the 
loader to ever see the stale star-tree.
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap)) {
+      assertTrue(ImmutableSegmentLoader.needPreprocess(segmentDirectory, 
indexLoadingConfig));
+    }
+
+    // 'skipSegmentPreprocess' is the knob that turns it off, and it takes 
effect through
+    // ImmutableSegmentLoader#needPreprocess, not 
SegmentPreProcessor#needProcess. IndexLoadingConfig snapshots it,
+    // so it needs a fresh config rather than an in-place edit of the table 
config.
+    indexingConfig.setSkipSegmentPreprocess(true);
+    IndexLoadingConfig skipPreprocessLoadingConfig = new 
IndexLoadingConfig(tableConfig, schema);
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap)) {
+      assertFalse(ImmutableSegmentLoader.needPreprocess(segmentDirectory, 
skipPreprocessLoadingConfig));
+    }
+
+    // Ask the loader to pre-process, as the server does: it gates on 
needPreprocess() itself, so the flag keeps the
+    // stale star-tree in place and the star-tree loader has to cope with it 
rather than fail the load.
+    ImmutableSegment segment = ImmutableSegmentLoader.load(INDEX_DIR, 
skipPreprocessLoadingConfig, true);
+    try {
+      assertEquals(segment.getSegmentMetadata().getTotalDocs(), 5);
+      // The stale star-tree is still on disk; it is skipped at load time, not 
removed
+      assertNotNull(segment.getSegmentMetadata().getStarTreeV2MetadataList());
+      List<StarTreeV2> starTrees = segment.getStarTrees();
+      assertNotNull(starTrees);
+      assertTrue(starTrees.isEmpty());
+    } finally {
+      segment.destroy();
+    }
+  }
+
+  /// With dynamic star-tree creation enabled, the stale star-tree is not just 
dropped but rebuilt from the current
+  /// config, which no longer splits on the re-encoded column.
+  @Test
+  public void testStarTreeDimensionConvertedToNoDictionaryWithDynamicCreation()
+      throws Exception {
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable").build();
+    Schema schema = new 
Schema.SchemaBuilder().addSingleValueDimension("stringCol", DataType.STRING)
+        .addSingleValueDimension("intCol", DataType.INT)
+        .addMetric("longCol", DataType.LONG)
+        .build();
+    IndexingConfig indexingConfig = tableConfig.getIndexingConfig();
+    indexingConfig.setStarTreeIndexConfigs(
+        List.of(new StarTreeIndexConfig(List.of("stringCol", "intCol"), null, 
List.of("SUM__longCol"), null, 1000)));
+    buildStarTreeTestSegment(tableConfig, schema);
+
+    // 'stringCol' becomes raw and drops out of the split order, and the 
star-tree is rebuilt on 'intCol' alone
+    indexingConfig.setNoDictionaryColumns(List.of("stringCol"));
+    indexingConfig.setStarTreeIndexConfigs(
+        List.of(new StarTreeIndexConfig(List.of("intCol"), null, 
List.of("SUM__longCol"), null, 1000)));
+    indexingConfig.setEnableDynamicStarTreeCreation(true);
+    IndexLoadingConfig indexLoadingConfig = new 
IndexLoadingConfig(tableConfig, schema);
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap);
+        SegmentPreProcessor processor = new 
SegmentPreProcessor(segmentDirectory, indexLoadingConfig)) {
+      assertTrue(processor.needProcess());
+      processor.process(SEGMENT_OPERATIONS_THROTTLER);
+    }
+
+    ImmutableSegment segment = ImmutableSegmentLoader.load(INDEX_DIR, 
indexLoadingConfig, false);
+    try {
+      List<StarTreeV2> starTrees = segment.getStarTrees();
+      assertNotNull(starTrees);
+      assertEquals(starTrees.size(), 1);
+      assertEquals(starTrees.get(0).getMetadata().getDimensionsSplitOrder(), 
List.of("intCol"));
+    } finally {
+      segment.destroy();
+    }
+  }
+
+  /// Apache Pinot PR #19153 added star-tree support for dimensions stored as 
a `RAW` forward index with a separated
+  /// dictionary. Such a column still has a dictionary, so its star-tree stays 
readable and must NOT be treated as
+  /// stale: pre-processing has to flip the forward index to raw, keep the 
dictionary, and leave the star-tree alone.
+  @Test
+  public void testStarTreeDimensionConvertedToRawWithSeparatedDictionary()
+      throws Exception {
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable").build();
+    Schema schema = new 
Schema.SchemaBuilder().addSingleValueDimension("stringCol", DataType.STRING)
+        .addMetric("longCol", DataType.LONG)
+        .build();
+    IndexingConfig indexingConfig = tableConfig.getIndexingConfig();
+    indexingConfig.setStarTreeIndexConfigs(
+        List.of(new StarTreeIndexConfig(List.of("stringCol"), null, 
List.of("SUM__longCol"), null, 1000)));
+    buildStarTreeTestSegment(tableConfig, schema);
+
+    // Keep the star-tree config, but store the dimension as RAW forward index 
with the dictionary kept alongside
+    ObjectNode indexes = JsonUtils.newObjectNode();
+    ObjectNode forwardConfig = JsonUtils.newObjectNode();
+    forwardConfig.put("encodingType", "RAW");
+    indexes.set("forward", forwardConfig);
+    ObjectNode dictionaryConfig = JsonUtils.newObjectNode();
+    dictionaryConfig.put("disabled", false);
+    indexes.set("dictionary", dictionaryConfig);
+    tableConfig.setFieldConfigList(List.of(
+        new 
FieldConfig.Builder("stringCol").withEncodingType(FieldConfig.EncodingType.RAW)
+            .withIndexes(indexes)
+            .build()));
+    indexingConfig.setEnableDynamicStarTreeCreation(false);
+    IndexLoadingConfig indexLoadingConfig = new 
IndexLoadingConfig(tableConfig, schema);
+
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap);
+        SegmentPreProcessor processor = new 
SegmentPreProcessor(segmentDirectory, indexLoadingConfig)) {
+      // The forward index still has to be flipped to RAW, so there is work to 
do
+      assertTrue(processor.needProcess());
+      processor.process(SEGMENT_OPERATIONS_THROTTLER);
+    }
+
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap)) {
+      SegmentMetadataImpl segmentMetadata = 
segmentDirectory.getSegmentMetadata();
+      ColumnMetadata columnMetadata = 
segmentMetadata.getColumnMetadataFor("stringCol");
+      assertEquals(columnMetadata.getForwardIndexEncoding(), 
FieldConfig.EncodingType.RAW);
+      assertTrue(columnMetadata.hasDictionary());
+      // The star-tree is still loadable, so it must be left in place
+      assertNotNull(segmentMetadata.getStarTreeV2MetadataList());
+      
assertTrue(StarTreeBuilderUtils.findUnloadableDimensions(segmentMetadata.getStarTreeV2MetadataList(),
+          segmentMetadata).isEmpty());
+    }
+
+    // Pre-processing must be idempotent here: neither the star-tree nor the 
forward index and dictionary handlers
+    // may ask for more work on a second round.
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap);
+        SegmentPreProcessor processor = new 
SegmentPreProcessor(segmentDirectory, indexLoadingConfig)) {
+      assertFalse(processor.needProcess());
+    }
+
+    ImmutableSegment segment = ImmutableSegmentLoader.load(INDEX_DIR, 
indexLoadingConfig, false);
+    try {
+      List<StarTreeV2> starTrees = segment.getStarTrees();
+      assertNotNull(starTrees);
+      assertEquals(starTrees.size(), 1);
+      assertEquals(starTrees.get(0).getMetadata().getDimensionsSplitOrder(), 
List.of("stringCol"));
+      assertNotNull(segment.getDataSource("stringCol").getDictionary());
+    } finally {
+      segment.destroy();
+    }
+  }
+
+  private void buildStarTreeTestSegment(TableConfig tableConfig, Schema schema)
+      throws Exception {
+    FileUtils.deleteQuietly(TEMP_DIR);
+    SegmentGeneratorConfig config = new SegmentGeneratorConfig(tableConfig, 
schema);
+    config.setInstanceType(InstanceType.SERVER);
+    config.setOutDir(TEMP_DIR.getAbsolutePath());
+    config.setSegmentName(SEGMENT_NAME);
+
+    String[] stringValues = {"A", "C", "B", "C", "D"};
+    long[] longValues = {2, 1, 2, 3, 4};
+    List<GenericRow> rows = new ArrayList<>(stringValues.length);
+    for (int i = 0; i < stringValues.length; i++) {
+      GenericRow row = new GenericRow();
+      row.putValue("stringCol", stringValues[i]);
+      row.putValue("intCol", i % 3);
+      row.putValue("longCol", longValues[i]);
+      rows.add(row);
+    }
+
+    SegmentIndexCreationDriverImpl driver = new 
SegmentIndexCreationDriverImpl();
+    driver.init(config, new GenericRowRecordReader(rows));
+    driver.build();
+
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap)) {
+      
assertNotNull(segmentDirectory.getSegmentMetadata().getStarTreeV2MetadataList());
+    }
+  }
+
+  private void assertSegmentLoadsWithoutStarTree(IndexLoadingConfig 
indexLoadingConfig)
+      throws Exception {
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(INDEX_DIR, ReadMode.mmap)) {
+      
assertNull(segmentDirectory.getSegmentMetadata().getStarTreeV2MetadataList());
+    }
+    ImmutableSegment segment = ImmutableSegmentLoader.load(INDEX_DIR, 
indexLoadingConfig, false);
+    try {
+      assertEquals(segment.getSegmentMetadata().getTotalDocs(), 5);
+      assertTrue(segment.getStarTrees() == null || 
segment.getStarTrees().isEmpty());
+    } finally {
+      segment.destroy();
+    }
+  }
+
   @Test
   public void testStarTreeCreationWithInvalidFunctionColumnPair()
       throws Exception {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to