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

Jackie-Jiang 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 f054a9073fa Update sortedness in metadata if disagree with 
ColumnStatistics (#18998)
f054a9073fa is described below

commit f054a9073fa0f61b441f8e17e4ba8d7a22ca58eb
Author: Jhow <[email protected]>
AuthorDate: Thu Jul 16 12:34:56 2026 -0700

    Update sortedness in metadata if disagree with ColumnStatistics (#18998)
---
 .../segment/index/loader/ForwardIndexHandler.java  |  47 +++++----
 .../index/loader/ForwardIndexHandlerTest.java      | 117 +++++++++++++++++++++
 2 files changed, 142 insertions(+), 22 deletions(-)

diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandler.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandler.java
index 70dc3cb09c5..77fbcd2c60e 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandler.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandler.java
@@ -988,28 +988,7 @@ public class ForwardIndexHandler extends BaseIndexHandler {
 
     ColumnMetadata existingColMetadata = 
segmentMetadata.getColumnMetadataFor(column);
     FieldSpec fieldSpec = existingColMetadata.getFieldSpec();
-    String fwdIndexFileExtension;
-    if (fieldSpec.isSingleValueField()) {
-      if (existingColMetadata.isSorted()) {
-        fwdIndexFileExtension = 
V1Constants.Indexes.SORTED_SV_FORWARD_INDEX_FILE_EXTENSION;
-      } else {
-        fwdIndexFileExtension = 
V1Constants.Indexes.UNSORTED_SV_FORWARD_INDEX_FILE_EXTENSION;
-      }
-    } else {
-      fwdIndexFileExtension = 
V1Constants.Indexes.UNSORTED_MV_FORWARD_INDEX_FILE_EXTENSION;
-    }
-    File fwdIndexFile = new File(indexDir, column + fwdIndexFileExtension);
-
-    if (!inProgress.exists()) {
-      // Marker file does not exist, which means last run ended normally.
-      // Create a marker file.
-      FileUtils.touch(inProgress);
-    } else {
-      // Marker file exists, which means last run was interrupted.
-      // Remove forward index and dictionary files if they exist.
-      FileUtils.deleteQuietly(fwdIndexFile);
-      FileUtils.deleteQuietly(dictionaryFile);
-    }
+    File fwdIndexFile;
 
     AbstractColumnStatisticsCollector statsCollector;
     SegmentDictionaryCreator dictionaryCreator;
@@ -1032,6 +1011,29 @@ public class ForwardIndexHandler extends 
BaseIndexHandler {
         }
         statsCollector.seal();
       }
+
+      String fwdIndexFileExtension;
+      if (fieldSpec.isSingleValueField()) {
+        if (statsCollector.isSorted()) {
+          fwdIndexFileExtension = 
V1Constants.Indexes.SORTED_SV_FORWARD_INDEX_FILE_EXTENSION;
+        } else {
+          fwdIndexFileExtension = 
V1Constants.Indexes.UNSORTED_SV_FORWARD_INDEX_FILE_EXTENSION;
+        }
+      } else {
+        fwdIndexFileExtension = 
V1Constants.Indexes.UNSORTED_MV_FORWARD_INDEX_FILE_EXTENSION;
+      }
+      fwdIndexFile = new File(indexDir, column + fwdIndexFileExtension);
+
+      if (!inProgress.exists()) {
+        // Marker file does not exist, which means last run ended normally.
+        // Create a marker file.
+        FileUtils.touch(inProgress);
+      } else {
+        // Marker file exists, which means last run was interrupted.
+        // Remove forward index and dictionary files if they exist.
+        FileUtils.deleteQuietly(fwdIndexFile);
+        FileUtils.deleteQuietly(dictionaryFile);
+      }
       DictionaryIndexConfig dictConf = 
_fieldIndexConfigs.get(column).getConfig(StandardIndexes.dictionary());
       boolean optimizeDictionaryType = 
_tableConfig.getIndexingConfig().isOptimizeDictionaryType();
       boolean useVarLength = dictConf.isUseVarLengthDictionary() || 
DictionaryIndexType.shouldUseVarLengthDictionary(
@@ -1063,6 +1065,7 @@ public class ForwardIndexHandler extends BaseIndexHandler 
{
 
     LOGGER.info("Created forwardIndex. Updating metadata properties for 
segment={} and column={}", segmentName, column);
     Map<String, String> metadataProperties = new HashMap<>();
+    metadataProperties.put(getKeyFor(column, IS_SORTED), 
String.valueOf(statsCollector.isSorted()));
     metadataProperties.put(getKeyFor(column, HAS_DICTIONARY), 
String.valueOf(true));
     metadataProperties.put(getKeyFor(column, FORWARD_INDEX_ENCODING), 
EncodingType.DICTIONARY.name());
     metadataProperties.put(getKeyFor(column, DICTIONARY_ELEMENT_SIZE),
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandlerTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandlerTest.java
index cdb196387eb..8bc1faec4d6 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandlerTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandlerTest.java
@@ -33,6 +33,7 @@ import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 import javax.annotation.Nullable;
+import org.apache.commons.configuration2.PropertiesConfiguration;
 import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -75,8 +76,11 @@ import org.apache.pinot.spi.utils.ReadMode;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import static 
org.apache.pinot.segment.spi.V1Constants.MetadataKeys.Column.IS_SORTED;
+import static 
org.apache.pinot.segment.spi.V1Constants.MetadataKeys.Column.getKeyFor;
 import static org.testng.Assert.*;
 
 
@@ -1771,6 +1775,119 @@ public class ForwardIndexHandlerTest {
         metadata, false);
   }
 
+  @DataProvider(name = "coincidentallySortedRawTypes")
+  public Object[][] coincidentallySortedRawTypes() {
+    // Cover a fixed-width type and both variable-length types (STRING/BYTES 
take a different raw writer path).
+    return new Object[][]{
+        {DataType.LONG, 0L},
+        {DataType.STRING, "sameValue"},
+        {DataType.BYTES, new byte[]{0x1, 0x2, 0x3}},
+    };
+  }
+
+  /// Regression test for enabling a dictionary on a raw column whose 
persisted metadata says {@code isSorted=false}
+  /// but whose values happen to be monotonic (here, all identical). This is 
the shape produced by a realtime segment
+  /// committed while the column was raw: raw columns are persisted with 
{@code isSorted=false} regardless of the data.
+  /// On reload the fresh stats collector reads the identical values and 
reports the column as sorted, so before the
+  /// fix the forward-index creator emitted a `.sv.sorted.fwd` index while 
{@link ForwardIndexHandler} derived the temp
+  /// file name from the metadata ({@code .sv.unsorted.fwd}) and never updated 
{@code isSorted}, throwing
+  /// {@link java.io.FileNotFoundException} and leaving a format/metadata 
mismatch for the next read. The fix forces
+  /// the creator to honor the persisted {@code isSorted} flag, keeping the 
written format, the file name, and the
+  /// metadata consistent.
+  @Test(dataProvider = "coincidentallySortedRawTypes")
+  public void 
testEnableDictionaryForRawColumnWithCoincidentallySortedValues(DataType 
dataType, Object value)
+      throws Exception {
+    File tempDir = new File(FileUtils.getTempDirectory(), 
"ForwardIndexHandlerCoincidentalSortTest");
+    FileUtils.deleteQuietly(tempDir);
+    try {
+      String tableName = "coincidentalSortTable";
+      String column = "coincidentallySortedColumn";
+      String segmentName = "coincidentalSortSegment";
+      int numDocs = 48;
+
+      Schema schema = new Schema.SchemaBuilder().setSchemaName(tableName)
+          .addSingleValueDimension(column, dataType)
+          .build();
+
+      // Build the segment with the column as raw (no dictionary).
+      TableConfig rawTableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(tableName)
+          .setNoDictionaryColumns(List.of(column))
+          .setFieldConfigList(
+              List.of(new FieldConfig(column, FieldConfig.EncodingType.RAW, 
List.of(), CompressionCodec.PASS_THROUGH,
+                  null)))
+          .build();
+
+      // All-identical values -> trivially monotonic -> the stats collector 
treats the column as sorted.
+      List<GenericRow> rows = new ArrayList<>();
+      for (int i = 0; i < numDocs; i++) {
+        GenericRow row = new GenericRow();
+        row.putValue(column, value);
+        rows.add(row);
+      }
+      SegmentGeneratorConfig config = new 
SegmentGeneratorConfig(rawTableConfig, schema);
+      config.setOutDir(tempDir.getPath());
+      config.setSegmentName(segmentName);
+      SegmentIndexCreationDriverImpl driver = new 
SegmentIndexCreationDriverImpl();
+      driver.init(config, new GenericRowRecordReader(rows));
+      driver.build();
+      File segmentDir = new File(tempDir, segmentName);
+
+      // Offline generation records isSorted from the data, so it would set 
isSorted=true for these all-equal values.
+      // Overwrite it to false to reproduce the realtime raw-segment condition.
+      PropertiesConfiguration metadataConfig = 
SegmentMetadataUtils.getPropertiesConfiguration(segmentDir);
+      metadataConfig.setProperty(getKeyFor(column, IS_SORTED), 
String.valueOf(false));
+      SegmentMetadataUtils.savePropertiesConfiguration(metadataConfig, 
segmentDir);
+
+      ColumnMetadata beforeMetadata = new 
SegmentMetadataImpl(segmentDir).getColumnMetadataFor(column);
+      assertFalse(beforeMetadata.isSorted());
+      assertFalse(beforeMetadata.hasDictionary());
+
+      // Enable a dictionary on the column and run the forward-index handler 
(the reload path).
+      TableConfig dictTableConfig =
+          new 
TableConfigBuilder(TableType.OFFLINE).setTableName(tableName).setNoDictionaryColumns(List.of()).build();
+      IndexLoadingConfig indexLoadingConfig = new 
IndexLoadingConfig(dictTableConfig, schema);
+      try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(segmentDir, ReadMode.mmap);
+          SegmentDirectory.Writer writer = segmentDirectory.createWriter()) {
+        ForwardIndexHandler handler = new 
ForwardIndexHandler(segmentDirectory, indexLoadingConfig);
+        assertEquals(handler.computeOperations(writer),
+            Map.of(column, 
List.of(ForwardIndexHandler.Operation.ENABLE_DICTIONARY)));
+        // Before the fix this threw FileNotFoundException for the missing 
.sv.unsorted.fwd file.
+        handler.updateIndices(writer);
+        handler.postUpdateIndicesCleanup(writer);
+      }
+
+      // The column now has a dictionary; isSorted must stay false and the 
unsorted dict forward index must read back
+      // the original values without corruption.
+      ColumnMetadata afterMetadata = new 
SegmentMetadataImpl(segmentDir).getColumnMetadataFor(column);
+      assertTrue(afterMetadata.hasDictionary());
+      assertTrue(afterMetadata.isSorted());
+      try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(segmentDir, ReadMode.mmap);
+          SegmentDirectory.Reader reader = segmentDirectory.createReader()) {
+        assertTrue(reader.hasIndexFor(column, StandardIndexes.forward()));
+        assertTrue(reader.hasIndexFor(column, StandardIndexes.dictionary()));
+        IndexReaderFactory<ForwardIndexReader> readerFactory = 
StandardIndexes.forward().getReaderFactory();
+        FieldIndexConfigs fieldIndexConfigs = 
createFieldIndexConfigsFromMetadata(afterMetadata);
+        try (ForwardIndexReader<?> fwdReader =
+                readerFactory.createIndexReader(reader, fieldIndexConfigs, 
afterMetadata);
+            Dictionary dictionary = DictionaryIndexType.read(reader, 
afterMetadata)) {
+          PinotSegmentColumnReader columnReader =
+              new PinotSegmentColumnReader(column, fwdReader, dictionary, null,
+                  afterMetadata.getMaxNumberOfMultiValues());
+          for (int i = 0; i < numDocs; i++) {
+            Object actual = columnReader.getValue(i);
+            if (dataType == DataType.BYTES) {
+              assertEquals((byte[]) actual, (byte[]) value);
+            } else {
+              assertEquals(actual, value);
+            }
+          }
+        }
+      }
+    } finally {
+      FileUtils.deleteQuietly(tempDir);
+    }
+  }
+
   @Test
   public void testDisableDictionaryForMultipleColumns()
       throws Exception {


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

Reply via email to