|
33 | 33 | import java.util.Random; |
34 | 34 | import java.util.Set; |
35 | 35 | import javax.annotation.Nullable; |
| 36 | +import org.apache.commons.configuration2.PropertiesConfiguration; |
36 | 37 | import org.apache.commons.configuration2.ex.ConfigurationException; |
37 | 38 | import org.apache.commons.io.FileUtils; |
38 | 39 | import org.apache.commons.lang3.StringUtils; |
|
75 | 76 | import org.apache.pinot.spi.utils.builder.TableConfigBuilder; |
76 | 77 | import org.testng.annotations.AfterMethod; |
77 | 78 | import org.testng.annotations.BeforeMethod; |
| 79 | +import org.testng.annotations.DataProvider; |
78 | 80 | import org.testng.annotations.Test; |
79 | 81 |
|
| 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; |
80 | 84 | import static org.testng.Assert.*; |
81 | 85 |
|
82 | 86 |
|
@@ -1771,6 +1775,119 @@ public void testDisableDictionaryForSortedColumn() |
1771 | 1775 | metadata, false); |
1772 | 1776 | } |
1773 | 1777 |
|
| 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 | + |
1774 | 1891 | @Test |
1775 | 1892 | public void testDisableDictionaryForMultipleColumns() |
1776 | 1893 | throws Exception { |
|
0 commit comments