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 8e99d01b9b7 Fix raw BYTES column min/max generation (#18952)
8e99d01b9b7 is described below

commit 8e99d01b9b725db351a09d8616020d9b61e286a7
Author: Xiang Fu <[email protected]>
AuthorDate: Fri Jul 10 09:09:08 2026 +0200

    Fix raw BYTES column min/max generation (#18952)
---
 .../ColumnMinMaxValueGenerator.java                |  12 +-
 .../ColumnMinMaxValueGeneratorTest.java            | 173 +++++++++++++++++++++
 2 files changed, 179 insertions(+), 6 deletions(-)

diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGenerator.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGenerator.java
index 0fe48d00499..318b7173dc8 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGenerator.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGenerator.java
@@ -365,10 +365,10 @@ public class ColumnMinMaxValueGenerator {
           if (isSingleValue) {
             for (int docId = 0; docId < numDocs; docId++) {
               byte[] value = rawIndexReader.getBytes(docId, readerContext);
-              if (min == null || ByteArray.compare(value, min) > 0) {
+              if (min == null || ByteArray.compare(value, min) < 0) {
                 min = value;
               }
-              if (max == null || ByteArray.compare(value, max) < 0) {
+              if (max == null || ByteArray.compare(value, max) > 0) {
                 max = value;
               }
             }
@@ -376,17 +376,17 @@ public class ColumnMinMaxValueGenerator {
             for (int docId = 0; docId < numDocs; docId++) {
               byte[][] values = rawIndexReader.getBytesMV(docId, 
readerContext);
               for (byte[] value : values) {
-                if (min == null || ByteArray.compare(value, min) > 0) {
+                if (min == null || ByteArray.compare(value, min) < 0) {
                   min = value;
                 }
-                if (max == null || ByteArray.compare(value, max) < 0) {
+                if (max == null || ByteArray.compare(value, max) > 0) {
                   max = value;
                 }
               }
             }
           }
-          minValue = new ByteArray(min);
-          maxValue = new ByteArray(max);
+          minValue = min != null ? new ByteArray(min) : null;
+          maxValue = max != null ? new ByteArray(max) : null;
           break;
         }
         default:
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGeneratorTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGeneratorTest.java
new file mode 100644
index 00000000000..c33aa0662a7
--- /dev/null
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/columnminmaxvalue/ColumnMinMaxValueGeneratorTest.java
@@ -0,0 +1,173 @@
+/**
+ * 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.pinot.segment.local.segment.index.loader.columnminmaxvalue;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.commons.io.FileUtils;
+import 
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
+import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
+import org.apache.pinot.segment.local.segment.store.SegmentLocalFSDirectory;
+import org.apache.pinot.segment.spi.V1Constants;
+import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
+import org.apache.pinot.segment.spi.index.metadata.SegmentMetadataImpl;
+import org.apache.pinot.segment.spi.store.SegmentDirectory;
+import org.apache.pinot.segment.spi.utils.SegmentMetadataUtils;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+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.ReadMode;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+
+/// Regression tests for [ColumnMinMaxValueGenerator] on raw BYTES columns.
+///
+/// The generator reads the forward index when column metadata does not 
contain min/max values. These tests exercise
+/// both single-value and multi-value forward index paths and verify their 
unsigned byte-wise ordering.
+public class ColumnMinMaxValueGeneratorTest {
+  private static final File TEMP_DIR =
+      new File(FileUtils.getTempDirectory(), 
ColumnMinMaxValueGeneratorTest.class.getSimpleName());
+  private static final String SEGMENT_NAME = "testSegment";
+  private static final String BYTES_COLUMN = "bytesCol";
+  private static final String BYTES_MV_COLUMN = "bytesMvCol";
+
+  private static final byte[] BYTES_SMALL = new byte[]{0x00, 0x01};
+  private static final byte[] BYTES_MID = new byte[]{0x10, (byte) 0xff};
+  private static final byte[] BYTES_LARGE = new byte[]{(byte) 0x80, 0x00};
+
+  @Test
+  public void testRawBytesMinMaxDirection()
+      throws Exception {
+    Schema schema = new Schema.SchemaBuilder().setSchemaName("testSchema")
+        .addSingleValueDimension(BYTES_COLUMN, DataType.BYTES)
+        .addMultiValueDimension(BYTES_MV_COLUMN, DataType.BYTES)
+        .build();
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable")
+        .setNoDictionaryColumns(List.of(BYTES_COLUMN, BYTES_MV_COLUMN))
+        .build();
+
+    File indexDir = buildSegment(tableConfig, schema, createRows());
+    removeMinMaxValuesFromMetadataFile(indexDir);
+
+    generateMinMaxValues(indexDir);
+
+    SegmentMetadataImpl reloaded = new SegmentMetadataImpl(indexDir);
+    assertMinMax(reloaded, BYTES_COLUMN);
+    assertMinMax(reloaded, BYTES_MV_COLUMN);
+  }
+
+  @Test
+  public void testEmptyRawBytesMarksMinMaxInvalid()
+      throws Exception {
+    Schema schema = new Schema.SchemaBuilder().setSchemaName("testSchema")
+        .addSingleValueDimension(BYTES_COLUMN, DataType.BYTES)
+        .build();
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName("testTable")
+        .setNoDictionaryColumns(List.of(BYTES_COLUMN))
+        .build();
+
+    File indexDir = buildSegment(tableConfig, schema, List.of());
+    removeMinMaxValuesFromMetadataFile(indexDir);
+    generateMinMaxValues(indexDir);
+
+    SegmentMetadataImpl reloaded = new SegmentMetadataImpl(indexDir);
+    assertNull(reloaded.getColumnMetadataFor(BYTES_COLUMN).getMinValue());
+    assertNull(reloaded.getColumnMetadataFor(BYTES_COLUMN).getMaxValue());
+    
assertTrue(reloaded.getColumnMetadataFor(BYTES_COLUMN).isMinMaxValueInvalid());
+  }
+
+  private static void assertMinMax(SegmentMetadataImpl segmentMetadata, String 
column) {
+    ByteArray min = (ByteArray) 
segmentMetadata.getColumnMetadataFor(column).getMinValue();
+    ByteArray max = (ByteArray) 
segmentMetadata.getColumnMetadataFor(column).getMaxValue();
+    assertEquals(min, new ByteArray(BYTES_SMALL));
+    assertEquals(max, new ByteArray(BYTES_LARGE));
+    assertTrue(min.compareTo(max) < 0);
+  }
+
+  private static void generateMinMaxValues(File indexDir)
+      throws Exception {
+    SegmentMetadataImpl segmentMetadata = new SegmentMetadataImpl(indexDir);
+    try (SegmentDirectory segmentDirectory = new 
SegmentLocalFSDirectory(indexDir, segmentMetadata, ReadMode.mmap);
+        SegmentDirectory.Writer writer = segmentDirectory.createWriter()) {
+      ColumnMinMaxValueGenerator generator =
+          new ColumnMinMaxValueGenerator(segmentMetadata, writer, 
ColumnMinMaxValueGeneratorMode.ALL);
+      generator.addColumnMinMaxValue();
+    }
+  }
+
+  private static List<GenericRow> createRows() {
+    List<GenericRow> rows = new ArrayList<>();
+    for (byte[] value : new byte[][]{BYTES_MID, BYTES_LARGE, BYTES_SMALL}) {
+      GenericRow row = new GenericRow();
+      row.putValue(BYTES_COLUMN, value);
+      row.putValue(BYTES_MV_COLUMN, new Object[]{value, BYTES_MID});
+      rows.add(row);
+    }
+    return rows;
+  }
+
+  private static File buildSegment(TableConfig tableConfig, Schema schema, 
List<GenericRow> rows)
+      throws Exception {
+    FileUtils.deleteQuietly(TEMP_DIR);
+    SegmentGeneratorConfig config = new SegmentGeneratorConfig(tableConfig, 
schema);
+    config.setOutDir(TEMP_DIR.getAbsolutePath());
+    config.setSegmentName(SEGMENT_NAME);
+
+    SegmentIndexCreationDriverImpl driver = new 
SegmentIndexCreationDriverImpl();
+    driver.init(config, new GenericRowRecordReader(rows));
+    driver.build();
+    return new File(TEMP_DIR, SEGMENT_NAME);
+  }
+
+  private static void removeMinMaxValuesFromMetadataFile(File indexDir)
+      throws Exception {
+    PropertiesConfiguration configuration = 
SegmentMetadataUtils.getPropertiesConfiguration(indexDir);
+    Iterator<String> keys = configuration.getKeys();
+    List<String> keysToClear = new ArrayList<>();
+    while (keys.hasNext()) {
+      String key = keys.next();
+      if (key.endsWith(V1Constants.MetadataKeys.Column.MIN_VALUE)
+          || key.endsWith(V1Constants.MetadataKeys.Column.MAX_VALUE)
+          || 
key.endsWith(V1Constants.MetadataKeys.Column.MIN_MAX_VALUE_INVALID)) {
+        keysToClear.add(key);
+      }
+    }
+    for (String key : keysToClear) {
+      configuration.clearProperty(key);
+    }
+    SegmentMetadataUtils.savePropertiesConfiguration(configuration, indexDir);
+  }
+
+  @AfterClass
+  public void tearDown() {
+    FileUtils.deleteQuietly(TEMP_DIR);
+  }
+}


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

Reply via email to