Skip to content

Commit f054a90

Browse files
authored
Update sortedness in metadata if disagree with ColumnStatistics (#18998)
1 parent 11eb00c commit f054a90

2 files changed

Lines changed: 142 additions & 22 deletions

File tree

pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandler.java

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -988,28 +988,7 @@ private void createDictBasedForwardIndex(String column, SegmentDirectory.Writer
988988

989989
ColumnMetadata existingColMetadata = segmentMetadata.getColumnMetadataFor(column);
990990
FieldSpec fieldSpec = existingColMetadata.getFieldSpec();
991-
String fwdIndexFileExtension;
992-
if (fieldSpec.isSingleValueField()) {
993-
if (existingColMetadata.isSorted()) {
994-
fwdIndexFileExtension = V1Constants.Indexes.SORTED_SV_FORWARD_INDEX_FILE_EXTENSION;
995-
} else {
996-
fwdIndexFileExtension = V1Constants.Indexes.UNSORTED_SV_FORWARD_INDEX_FILE_EXTENSION;
997-
}
998-
} else {
999-
fwdIndexFileExtension = V1Constants.Indexes.UNSORTED_MV_FORWARD_INDEX_FILE_EXTENSION;
1000-
}
1001-
File fwdIndexFile = new File(indexDir, column + fwdIndexFileExtension);
1002-
1003-
if (!inProgress.exists()) {
1004-
// Marker file does not exist, which means last run ended normally.
1005-
// Create a marker file.
1006-
FileUtils.touch(inProgress);
1007-
} else {
1008-
// Marker file exists, which means last run was interrupted.
1009-
// Remove forward index and dictionary files if they exist.
1010-
FileUtils.deleteQuietly(fwdIndexFile);
1011-
FileUtils.deleteQuietly(dictionaryFile);
1012-
}
991+
File fwdIndexFile;
1013992

1014993
AbstractColumnStatisticsCollector statsCollector;
1015994
SegmentDictionaryCreator dictionaryCreator;
@@ -1032,6 +1011,29 @@ private void createDictBasedForwardIndex(String column, SegmentDirectory.Writer
10321011
}
10331012
statsCollector.seal();
10341013
}
1014+
1015+
String fwdIndexFileExtension;
1016+
if (fieldSpec.isSingleValueField()) {
1017+
if (statsCollector.isSorted()) {
1018+
fwdIndexFileExtension = V1Constants.Indexes.SORTED_SV_FORWARD_INDEX_FILE_EXTENSION;
1019+
} else {
1020+
fwdIndexFileExtension = V1Constants.Indexes.UNSORTED_SV_FORWARD_INDEX_FILE_EXTENSION;
1021+
}
1022+
} else {
1023+
fwdIndexFileExtension = V1Constants.Indexes.UNSORTED_MV_FORWARD_INDEX_FILE_EXTENSION;
1024+
}
1025+
fwdIndexFile = new File(indexDir, column + fwdIndexFileExtension);
1026+
1027+
if (!inProgress.exists()) {
1028+
// Marker file does not exist, which means last run ended normally.
1029+
// Create a marker file.
1030+
FileUtils.touch(inProgress);
1031+
} else {
1032+
// Marker file exists, which means last run was interrupted.
1033+
// Remove forward index and dictionary files if they exist.
1034+
FileUtils.deleteQuietly(fwdIndexFile);
1035+
FileUtils.deleteQuietly(dictionaryFile);
1036+
}
10351037
DictionaryIndexConfig dictConf = _fieldIndexConfigs.get(column).getConfig(StandardIndexes.dictionary());
10361038
boolean optimizeDictionaryType = _tableConfig.getIndexingConfig().isOptimizeDictionaryType();
10371039
boolean useVarLength = dictConf.isUseVarLengthDictionary() || DictionaryIndexType.shouldUseVarLengthDictionary(
@@ -1063,6 +1065,7 @@ private void createDictBasedForwardIndex(String column, SegmentDirectory.Writer
10631065

10641066
LOGGER.info("Created forwardIndex. Updating metadata properties for segment={} and column={}", segmentName, column);
10651067
Map<String, String> metadataProperties = new HashMap<>();
1068+
metadataProperties.put(getKeyFor(column, IS_SORTED), String.valueOf(statsCollector.isSorted()));
10661069
metadataProperties.put(getKeyFor(column, HAS_DICTIONARY), String.valueOf(true));
10671070
metadataProperties.put(getKeyFor(column, FORWARD_INDEX_ENCODING), EncodingType.DICTIONARY.name());
10681071
metadataProperties.put(getKeyFor(column, DICTIONARY_ELEMENT_SIZE),

pinot-segment-local/src/test/java/org/apache/pinot/segment/local/segment/index/loader/ForwardIndexHandlerTest.java

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Random;
3434
import java.util.Set;
3535
import javax.annotation.Nullable;
36+
import org.apache.commons.configuration2.PropertiesConfiguration;
3637
import org.apache.commons.configuration2.ex.ConfigurationException;
3738
import org.apache.commons.io.FileUtils;
3839
import org.apache.commons.lang3.StringUtils;
@@ -75,8 +76,11 @@
7576
import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
7677
import org.testng.annotations.AfterMethod;
7778
import org.testng.annotations.BeforeMethod;
79+
import org.testng.annotations.DataProvider;
7880
import org.testng.annotations.Test;
7981

82+
import static org.apache.pinot.segment.spi.V1Constants.MetadataKeys.Column.IS_SORTED;
83+
import static org.apache.pinot.segment.spi.V1Constants.MetadataKeys.Column.getKeyFor;
8084
import static org.testng.Assert.*;
8185

8286

@@ -1771,6 +1775,119 @@ public void testDisableDictionaryForSortedColumn()
17711775
metadata, false);
17721776
}
17731777

1778+
@DataProvider(name = "coincidentallySortedRawTypes")
1779+
public Object[][] coincidentallySortedRawTypes() {
1780+
// Cover a fixed-width type and both variable-length types (STRING/BYTES take a different raw writer path).
1781+
return new Object[][]{
1782+
{DataType.LONG, 0L},
1783+
{DataType.STRING, "sameValue"},
1784+
{DataType.BYTES, new byte[]{0x1, 0x2, 0x3}},
1785+
};
1786+
}
1787+
1788+
/// Regression test for enabling a dictionary on a raw column whose persisted metadata says {@code isSorted=false}
1789+
/// but whose values happen to be monotonic (here, all identical). This is the shape produced by a realtime segment
1790+
/// committed while the column was raw: raw columns are persisted with {@code isSorted=false} regardless of the data.
1791+
/// On reload the fresh stats collector reads the identical values and reports the column as sorted, so before the
1792+
/// fix the forward-index creator emitted a `.sv.sorted.fwd` index while {@link ForwardIndexHandler} derived the temp
1793+
/// file name from the metadata ({@code .sv.unsorted.fwd}) and never updated {@code isSorted}, throwing
1794+
/// {@link java.io.FileNotFoundException} and leaving a format/metadata mismatch for the next read. The fix forces
1795+
/// the creator to honor the persisted {@code isSorted} flag, keeping the written format, the file name, and the
1796+
/// metadata consistent.
1797+
@Test(dataProvider = "coincidentallySortedRawTypes")
1798+
public void testEnableDictionaryForRawColumnWithCoincidentallySortedValues(DataType dataType, Object value)
1799+
throws Exception {
1800+
File tempDir = new File(FileUtils.getTempDirectory(), "ForwardIndexHandlerCoincidentalSortTest");
1801+
FileUtils.deleteQuietly(tempDir);
1802+
try {
1803+
String tableName = "coincidentalSortTable";
1804+
String column = "coincidentallySortedColumn";
1805+
String segmentName = "coincidentalSortSegment";
1806+
int numDocs = 48;
1807+
1808+
Schema schema = new Schema.SchemaBuilder().setSchemaName(tableName)
1809+
.addSingleValueDimension(column, dataType)
1810+
.build();
1811+
1812+
// Build the segment with the column as raw (no dictionary).
1813+
TableConfig rawTableConfig = new TableConfigBuilder(TableType.OFFLINE).setTableName(tableName)
1814+
.setNoDictionaryColumns(List.of(column))
1815+
.setFieldConfigList(
1816+
List.of(new FieldConfig(column, FieldConfig.EncodingType.RAW, List.of(), CompressionCodec.PASS_THROUGH,
1817+
null)))
1818+
.build();
1819+
1820+
// All-identical values -> trivially monotonic -> the stats collector treats the column as sorted.
1821+
List<GenericRow> rows = new ArrayList<>();
1822+
for (int i = 0; i < numDocs; i++) {
1823+
GenericRow row = new GenericRow();
1824+
row.putValue(column, value);
1825+
rows.add(row);
1826+
}
1827+
SegmentGeneratorConfig config = new SegmentGeneratorConfig(rawTableConfig, schema);
1828+
config.setOutDir(tempDir.getPath());
1829+
config.setSegmentName(segmentName);
1830+
SegmentIndexCreationDriverImpl driver = new SegmentIndexCreationDriverImpl();
1831+
driver.init(config, new GenericRowRecordReader(rows));
1832+
driver.build();
1833+
File segmentDir = new File(tempDir, segmentName);
1834+
1835+
// Offline generation records isSorted from the data, so it would set isSorted=true for these all-equal values.
1836+
// Overwrite it to false to reproduce the realtime raw-segment condition.
1837+
PropertiesConfiguration metadataConfig = SegmentMetadataUtils.getPropertiesConfiguration(segmentDir);
1838+
metadataConfig.setProperty(getKeyFor(column, IS_SORTED), String.valueOf(false));
1839+
SegmentMetadataUtils.savePropertiesConfiguration(metadataConfig, segmentDir);
1840+
1841+
ColumnMetadata beforeMetadata = new SegmentMetadataImpl(segmentDir).getColumnMetadataFor(column);
1842+
assertFalse(beforeMetadata.isSorted());
1843+
assertFalse(beforeMetadata.hasDictionary());
1844+
1845+
// Enable a dictionary on the column and run the forward-index handler (the reload path).
1846+
TableConfig dictTableConfig =
1847+
new TableConfigBuilder(TableType.OFFLINE).setTableName(tableName).setNoDictionaryColumns(List.of()).build();
1848+
IndexLoadingConfig indexLoadingConfig = new IndexLoadingConfig(dictTableConfig, schema);
1849+
try (SegmentDirectory segmentDirectory = new SegmentLocalFSDirectory(segmentDir, ReadMode.mmap);
1850+
SegmentDirectory.Writer writer = segmentDirectory.createWriter()) {
1851+
ForwardIndexHandler handler = new ForwardIndexHandler(segmentDirectory, indexLoadingConfig);
1852+
assertEquals(handler.computeOperations(writer),
1853+
Map.of(column, List.of(ForwardIndexHandler.Operation.ENABLE_DICTIONARY)));
1854+
// Before the fix this threw FileNotFoundException for the missing .sv.unsorted.fwd file.
1855+
handler.updateIndices(writer);
1856+
handler.postUpdateIndicesCleanup(writer);
1857+
}
1858+
1859+
// The column now has a dictionary; isSorted must stay false and the unsorted dict forward index must read back
1860+
// the original values without corruption.
1861+
ColumnMetadata afterMetadata = new SegmentMetadataImpl(segmentDir).getColumnMetadataFor(column);
1862+
assertTrue(afterMetadata.hasDictionary());
1863+
assertTrue(afterMetadata.isSorted());
1864+
try (SegmentDirectory segmentDirectory = new SegmentLocalFSDirectory(segmentDir, ReadMode.mmap);
1865+
SegmentDirectory.Reader reader = segmentDirectory.createReader()) {
1866+
assertTrue(reader.hasIndexFor(column, StandardIndexes.forward()));
1867+
assertTrue(reader.hasIndexFor(column, StandardIndexes.dictionary()));
1868+
IndexReaderFactory<ForwardIndexReader> readerFactory = StandardIndexes.forward().getReaderFactory();
1869+
FieldIndexConfigs fieldIndexConfigs = createFieldIndexConfigsFromMetadata(afterMetadata);
1870+
try (ForwardIndexReader<?> fwdReader =
1871+
readerFactory.createIndexReader(reader, fieldIndexConfigs, afterMetadata);
1872+
Dictionary dictionary = DictionaryIndexType.read(reader, afterMetadata)) {
1873+
PinotSegmentColumnReader columnReader =
1874+
new PinotSegmentColumnReader(column, fwdReader, dictionary, null,
1875+
afterMetadata.getMaxNumberOfMultiValues());
1876+
for (int i = 0; i < numDocs; i++) {
1877+
Object actual = columnReader.getValue(i);
1878+
if (dataType == DataType.BYTES) {
1879+
assertEquals((byte[]) actual, (byte[]) value);
1880+
} else {
1881+
assertEquals(actual, value);
1882+
}
1883+
}
1884+
}
1885+
}
1886+
} finally {
1887+
FileUtils.deleteQuietly(tempDir);
1888+
}
1889+
}
1890+
17741891
@Test
17751892
public void testDisableDictionaryForMultipleColumns()
17761893
throws Exception {

0 commit comments

Comments
 (0)