Repository navigation
[SPARK-59854][SQL] Report the column in decoding errors of the vectorized Parquet reader #59542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
20ce444
68a4b4e
862d115
3568ed4
20fe1ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,6 +98,12 @@ public class VectorizedColumnReader { | |
| */ | ||
| private long pageFirstRowIndex; | ||
|
|
||
| /** | ||
| * The data page the values are being decoded from, for error messages. Only its description is | ||
| * kept: for codecs that decompress lazily, the page holds the decompressor stream. | ||
| */ | ||
| private String currentPage; | ||
|
|
||
| private final PageReader pageReader; | ||
| private final ColumnDescriptor descriptor; | ||
| private final LogicalTypeAnnotation logicalTypeAnnotation; | ||
|
|
@@ -146,23 +152,33 @@ public VectorizedColumnReader( | |
| int96RebaseMode, | ||
| int96RebaseTz); | ||
|
|
||
| DictionaryPage dictionaryPage = pageReader.readDictionaryPage(); | ||
| if (dictionaryPage != null) { | ||
| try { | ||
| this.dictionary = dictionaryPage.getEncoding().initDictionary(descriptor, dictionaryPage); | ||
| this.isCurrentPageDictionaryEncoded = true; | ||
| } catch (IOException e) { | ||
| throw new IOException("could not decode the dictionary for " + descriptor, e); | ||
| } catch (ParquetDecodingException e) { | ||
| throw new ParquetDecodingException( | ||
| "could not decode the dictionary for " + descriptor + ": " + e.getMessage(), e); | ||
| Dictionary dict = null; | ||
| try { | ||
| // This decompresses the page eagerly for some codecs, e.g. ZSTD. | ||
| DictionaryPage dictionaryPage = pageReader.readDictionaryPage(); | ||
| if (dictionaryPage != null) { | ||
| dict = dictionaryPage.getEncoding().initDictionary(descriptor, dictionaryPage); | ||
| } | ||
| } else { | ||
| this.dictionary = null; | ||
| this.isCurrentPageDictionaryEncoded = false; | ||
| } catch (IOException e) { | ||
| throw new IOException("could not decode the dictionary for " + descriptor, e); | ||
| } catch (ParquetDecodingException e) { | ||
| throw new ParquetDecodingException( | ||
| "could not decode the dictionary for " + descriptor + ": " + e.getMessage(), e); | ||
| } catch (IndexOutOfBoundsException | NegativeArraySizeException | | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the third bullet of item 5 of my second review (on L373) is still open, and it applies to data pages too. The description says that LZ4_RAW differs "because parquet-java does not turn it into an
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed: with |
||
| UnsupportedOperationException e) { | ||
| // Reading the dictionary page decompresses it, for some codecs only when initDictionary | ||
| // reads its bytes, and initDictionary parses it. A corrupted page or page header makes | ||
| // them throw these: an IndexOutOfBoundsException when a BINARY dictionary ends within the | ||
| // length of an entry, a NegativeArraySizeException for a negative size, and an | ||
| // UnsupportedOperationException for an encoding that has no dictionary. An entry that | ||
| // claims more bytes than the page has is not detected here (SPARK-60316). | ||
| throw new ParquetDecodingException( | ||
| "could not decode the dictionary for " + descriptor + ": " + e, e); | ||
| } | ||
| this.dictionary = dict; | ||
| this.isCurrentPageDictionaryEncoded = dict != null; | ||
| if (pageReader.getTotalValueCount() == 0) { | ||
| throw new IOException("totalValueCount == 0"); | ||
| throw new IOException("totalValueCount == 0 in col " + descriptor); | ||
| } | ||
| assert "LEGACY".equals(datetimeRebaseMode) || "EXCEPTION".equals(datetimeRebaseMode) || | ||
| "CORRECTED".equals(datetimeRebaseMode); | ||
|
|
@@ -272,79 +288,96 @@ void readBatch( | |
| } | ||
| readState.resetForNewPage(pageValueCount, pageFirstRowIndex); | ||
| } | ||
| PrimitiveType.PrimitiveTypeName typeName = | ||
| descriptor.getPrimitiveType().getPrimitiveTypeName(); | ||
| if (isCurrentPageDictionaryEncoded) { | ||
| // Save starting offset in case we need to decode dictionary IDs. | ||
| int startOffset = readState.valueOffset; | ||
| // Save starting row index so we can check if we need to eagerly decode dict ids later | ||
| long startRowId = readState.rowId; | ||
|
|
||
| // Read and decode dictionary ids. | ||
| if (readState.maxRepetitionLevel == 0) { | ||
| defColumn.readIntegers(readState, dictionaryIds, column, definitionLevels, | ||
| (VectorizedValuesReader) dataColumn); | ||
| } else { | ||
| repColumn.readIntegersRepeated(readState, repetitionLevels, defColumn, definitionLevels, | ||
| dictionaryIds, column, (VectorizedValuesReader) dataColumn); | ||
| } | ||
| // A corrupted page makes the readers below throw a ParquetDecodingException without the | ||
| // column. Only that is wrapped, so that other exceptions, e.g. SparkUpgradeException, | ||
| // keep their types. | ||
| try { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: wrapping the block in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A private method would re-indent the same lines too: the block is at 6 spaces inside the |
||
| PrimitiveType.PrimitiveTypeName typeName = | ||
| descriptor.getPrimitiveType().getPrimitiveTypeName(); | ||
| if (isCurrentPageDictionaryEncoded) { | ||
| // Save starting offset in case we need to decode dictionary IDs. | ||
| int startOffset = readState.valueOffset; | ||
| // Save starting row index so we can check if we need to eagerly decode dict ids later | ||
| long startRowId = readState.rowId; | ||
|
|
||
| // Read and decode dictionary ids. | ||
| if (readState.maxRepetitionLevel == 0) { | ||
| defColumn.readIntegers(readState, dictionaryIds, column, definitionLevels, | ||
| (VectorizedValuesReader) dataColumn); | ||
| } else { | ||
| repColumn.readIntegersRepeated(readState, repetitionLevels, defColumn, definitionLevels, | ||
| dictionaryIds, column, (VectorizedValuesReader) dataColumn); | ||
| } | ||
|
|
||
| // TIMESTAMP_MILLIS encoded as INT64 can't be lazily decoded as we need to post process | ||
| // the values to add microseconds precision. | ||
| if (column.hasDictionary() || (startRowId == pageFirstRowIndex && | ||
| isLazyDecodingSupported(typeName, column.dataType()))) { | ||
| // Column vector supports lazy decoding of dictionary values so just set the dictionary. | ||
| // We can't do this if startRowId is not the first row index in the page AND the column | ||
| // doesn't have a dictionary (i.e. some non-dictionary encoded values have already been | ||
| // added). | ||
| PrimitiveType primitiveType = descriptor.getPrimitiveType(); | ||
|
|
||
| // We need to make sure that we initialize the right type for the dictionary otherwise | ||
| // WritableColumnVector will throw an exception when trying to decode to an Int when the | ||
| // dictionary is in fact initialized as Long | ||
| LogicalTypeAnnotation typeAnnotation = primitiveType.getLogicalTypeAnnotation(); | ||
| boolean castLongToInt = | ||
| typeAnnotation instanceof DecimalLogicalTypeAnnotation annotation && | ||
| annotation.getPrecision() <= Decimal.MAX_INT_DIGITS() && | ||
| primitiveType.getPrimitiveTypeName() == INT64; | ||
|
|
||
| // We require a long value, but we need to use dictionary to decode the original | ||
| // signed int first | ||
| boolean isUnsignedInt32 = updaterFactory.isUnsignedIntTypeMatched(32); | ||
|
|
||
| // We require a decimal value, but we need to use dictionary to decode the original | ||
| // signed long first | ||
| boolean isUnsignedInt64 = updaterFactory.isUnsignedIntTypeMatched(64); | ||
|
|
||
| boolean needTransform = castLongToInt || isUnsignedInt32 || isUnsignedInt64; | ||
| column.setDictionary(new ParquetDictionary(dictionary, needTransform)); | ||
| } else { | ||
| updater.decodeDictionaryIds(readState.valueOffset - startOffset, startOffset, column, | ||
| dictionaryIds, dictionary); | ||
| } | ||
| } else { | ||
| if (column.hasDictionary() && readState.valueOffset != 0) { | ||
| // This batch already has dictionary encoded values but this new page is not. The batch | ||
| // does not support a mix of dictionary and not so we will decode the dictionary. | ||
| updater.decodeDictionaryIds(readState.valueOffset, 0, column, dictionaryIds, dictionary); | ||
| } | ||
| column.setDictionary(null); | ||
| VectorizedValuesReader valuesReader = (VectorizedValuesReader) dataColumn; | ||
| if (readState.maxRepetitionLevel == 0) { | ||
| defColumn.readBatch(readState, column, definitionLevels, valuesReader, updater); | ||
| // TIMESTAMP_MILLIS encoded as INT64 can't be lazily decoded as we need to post process | ||
| // the values to add microseconds precision. | ||
| if (column.hasDictionary() || (startRowId == pageFirstRowIndex && | ||
| isLazyDecodingSupported(typeName, column.dataType()))) { | ||
| // Column vector supports lazy decoding of dictionary values so just set the dictionary. | ||
| // We can't do this if startRowId is not the first row index in the page AND the column | ||
| // doesn't have a dictionary (i.e. some non-dictionary encoded values have already been | ||
| // added). | ||
| PrimitiveType primitiveType = descriptor.getPrimitiveType(); | ||
|
|
||
| // We need to make sure that we initialize the right type for the dictionary otherwise | ||
| // WritableColumnVector will throw an exception when trying to decode to an Int when the | ||
| // dictionary is in fact initialized as Long | ||
| LogicalTypeAnnotation typeAnnotation = primitiveType.getLogicalTypeAnnotation(); | ||
| boolean castLongToInt = | ||
| typeAnnotation instanceof DecimalLogicalTypeAnnotation annotation && | ||
| annotation.getPrecision() <= Decimal.MAX_INT_DIGITS() && | ||
| primitiveType.getPrimitiveTypeName() == INT64; | ||
|
|
||
| // We require a long value, but we need to use dictionary to decode the original | ||
| // signed int first | ||
| boolean isUnsignedInt32 = updaterFactory.isUnsignedIntTypeMatched(32); | ||
|
|
||
| // We require a decimal value, but we need to use dictionary to decode the original | ||
| // signed long first | ||
| boolean isUnsignedInt64 = updaterFactory.isUnsignedIntTypeMatched(64); | ||
|
|
||
| boolean needTransform = castLongToInt || isUnsignedInt32 || isUnsignedInt64; | ||
| column.setDictionary(new ParquetDictionary(dictionary, needTransform)); | ||
| } else { | ||
| updater.decodeDictionaryIds(readState.valueOffset - startOffset, startOffset, column, | ||
| dictionaryIds, dictionary); | ||
| } | ||
| } else { | ||
| repColumn.readBatchRepeated(readState, repetitionLevels, defColumn, definitionLevels, | ||
| column, valuesReader, updater); | ||
| if (column.hasDictionary() && readState.valueOffset != 0) { | ||
| // This batch already has dictionary encoded values but this new page is not. The batch | ||
| // does not support a mix of dictionary and not so we will decode the dictionary. | ||
| updater.decodeDictionaryIds(readState.valueOffset, 0, column, dictionaryIds, | ||
| dictionary); | ||
| } | ||
| column.setDictionary(null); | ||
| VectorizedValuesReader valuesReader = (VectorizedValuesReader) dataColumn; | ||
| if (readState.maxRepetitionLevel == 0) { | ||
| defColumn.readBatch(readState, column, definitionLevels, valuesReader, updater); | ||
| } else { | ||
| repColumn.readBatchRepeated(readState, repetitionLevels, defColumn, definitionLevels, | ||
| column, valuesReader, updater); | ||
| } | ||
| } | ||
| } catch (ParquetDecodingException e) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wrapper catches only
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At 68a4b4e this is unchanged. With a 2-entry INT32 dictionary and an RLE run of id 5:
I understand the description says only
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, an out-of-range id fails with an
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filed SPARK-60317 for it, and the description lists it as not covered. I verified both paths: read as |
||
| throw new ParquetDecodingException("could not decode page " + currentPage + " in col " + | ||
| descriptor + ": " + e.getMessage(), e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private int readPage() { | ||
| DataPage page = pageReader.readPage(); | ||
| DataPage page; | ||
| try { | ||
| // This decompresses the page eagerly for some codecs, e.g. ZSTD. | ||
| page = pageReader.readPage(); | ||
| } catch (ParquetDecodingException e) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: this catches only
Could we also wrap these here and at L159, except
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both confirmed: LZ4_RAW's |
||
| throw new ParquetDecodingException( | ||
| "could not read the next page in col " + descriptor + ": " + e.getMessage(), e); | ||
| } | ||
| if (page == null) { | ||
| return -1; | ||
| } | ||
| this.currentPage = page.toString(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the second bullet of item 5 of my second review was incomplete. A negative Could we reword the "not covered" item, e.g. "A negative uncompressed size in the header of a compressed data page fails with a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed: with a SNAPPY data page whose |
||
| this.pageFirstRowIndex = page.getFirstRowIndex().orElse(0L); | ||
|
|
||
| return page.accept(new DataPage.Visitor<Integer>() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,6 @@ | |
| import org.apache.parquet.column.values.bitpacking.Packer; | ||
| import org.apache.parquet.io.ParquetDecodingException; | ||
|
|
||
| import org.apache.spark.network.util.JavaUtils; | ||
| import org.apache.spark.sql.catalyst.util.RebaseDateTime; | ||
| import org.apache.spark.sql.execution.datasources.DataSourceUtils; | ||
| import org.apache.spark.sql.execution.vectorized.WritableColumnVector; | ||
|
|
@@ -89,8 +88,10 @@ public class VectorizedDeltaBinaryPackedReader extends VectorizedReaderBase { | |
|
|
||
| @Override | ||
| public void initFromPage(int valueCount, ByteBufferInputStream in) throws IOException { | ||
| JavaUtils.checkArgument(valueCount >= 1, | ||
| "Page must have at least one value, but it has " + valueCount); | ||
| if (valueCount < 1) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: like the header fields checked here, the mini block bit widths come from the file, but they are never range-checked, so a corrupt width still fails without the page or the column. The spec allows arbitrary widths for the unused mini blocks of the last block, so the check has to be where a width is used:
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| throw new ParquetDecodingException("Invalid DELTA_BINARY_PACKED page: it must have at " | ||
| + "least one value, but it has " + valueCount); | ||
| } | ||
| this.in = in; | ||
| // Read the header | ||
| this.blockSizeInValues = BytesUtils.readUnsignedVarInt(in); | ||
|
|
@@ -100,14 +101,17 @@ public void initFromPage(int valueCount, ByteBufferInputStream in) throws IOExce | |
| throw new ParquetDecodingException("Invalid DELTA_BINARY_PACKED block size in values: " | ||
| + blockSizeInValues + " (must be in [1, " + MAX_BLOCK_SIZE_IN_VALUES + "])"); | ||
| } | ||
| if (miniBlockNumInABlock <= 0 || miniBlockNumInABlock > blockSizeInValues) { | ||
| if (miniBlockNumInABlock <= 0) { | ||
| throw new ParquetDecodingException("Invalid DELTA_BINARY_PACKED mini block count: " | ||
| + miniBlockNumInABlock + " (block size in values: " + blockSizeInValues + ")"); | ||
| } | ||
| double miniSize = (double) blockSizeInValues / miniBlockNumInABlock; | ||
| JavaUtils.checkArgument(miniSize % 8 == 0, | ||
| "miniBlockSize must be multiple of 8, but it's " + miniSize); | ||
| this.miniBlockSizeInValues = (int) miniSize; | ||
| if (blockSizeInValues % miniBlockNumInABlock != 0 || | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this check makes the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, the |
||
| (blockSizeInValues / miniBlockNumInABlock) % 8 != 0) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: this half of the condition has no test. The only new test uses a block of 128 values in 3 mini blocks, which already fails
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| throw new ParquetDecodingException("Invalid DELTA_BINARY_PACKED mini block count: " | ||
| + miniBlockNumInABlock + " (block size in values: " + blockSizeInValues | ||
| + ", the mini block size must be a multiple of 8)"); | ||
| } | ||
| this.miniBlockSizeInValues = blockSizeInValues / miniBlockNumInABlock; | ||
| // True value count. May be less than valueCount because of nulls | ||
| this.totalValueCount = BytesUtils.readUnsignedVarInt(in); | ||
| if (totalValueCount < 0) { | ||
|
|
@@ -472,9 +476,15 @@ private void readBlockHeader() { | |
| * see org.apache.parquet.column.values.delta.DeltaBinaryPackingValuesReader#unpackMiniBlock | ||
| */ | ||
| private void unpackMiniBlock() throws IOException { | ||
| int bitWidth = bitWidths[currentMiniBlock]; | ||
| // The bit widths of the unused mini blocks of the last block can be anything, so a width is | ||
| // checked only when its mini block is unpacked. | ||
| if (bitWidth > 64) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the user-facing section of the description says that the original Since the description becomes the commit message, could we say so, e.g. "The dictionary exceptions are kept as the cause, and the DELTA_BINARY_PACKED checks now throw a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, thanks. The user-facing section now says that the dictionary exceptions are kept as the cause, and that the DELTA_BINARY_PACKED checks throw a |
||
| throw new ParquetDecodingException("Invalid DELTA_BINARY_PACKED mini block bit width: " | ||
| + bitWidth + " (must be at most 64)"); | ||
| } | ||
| Arrays.fill(this.unpackedValuesBuffer, 0); | ||
| BytePackerForLong packer = Packer.LITTLE_ENDIAN.newBytePackerForLong( | ||
| bitWidths[currentMiniBlock]); | ||
| BytePackerForLong packer = Packer.LITTLE_ENDIAN.newBytePackerForLong(bitWidth); | ||
| for (int j = 0; j < miniBlockSizeInValues; j += 8) { | ||
| ByteBuffer buffer = in.slice(packer.getBitWidth()); | ||
| if (buffer.hasArray()) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: because this
tryis separate from theinitDictionaryone below, a dictionary page that cannot be decompressed fails differently depending on the codec. With ZSTD it is aParquetDecodingException, "could not read the dictionary page for ...". With SNAPPY or GZIP, which decompress insideinitDictionary, it is anIOException, "could not decode the dictionary for ..." (L167-L168). A log search for one misses the other. MovingpageReader.readDictionaryPage()into thetryat L164 would give one message shape, and theNegativeArraySizeExceptionarm would then also cover the eager case.Item 7 of my first review is still open as well. If the decode block of
readBatch(L293-L358) moved into a private method and only that call were wrapped,git blamewould keep its history: today about 61 of the changed lines differ only in indentation (git diff -wshows 42+/3- instead of 103+/64-). Either way the wrapper should keep catching onlyParquetDecodingException, sinceParquetStorageFilter.rethrowIfMustPropagatedispatches on the exactRuntimeExceptionclass.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done for the first part:
readDictionaryPage()is now in the sametryasinitDictionary, so a dictionary page that cannot be read fails withcould not decode the dictionary for ...for every codec.For the helper method, I tried it and measured
git blameon the decode block. Plaingit blameattributes 64 of its lines to this PR in both versions,-w8 in both, and-w -M7 with thetryand 6 with the method. Moving the block into a method body re-indents it as well (to 4 spaces, not back to the original 6), so it does not keep the history. So I kept thetry. Agreed that the wrapper must keep catching onlyParquetDecodingException, so that a vector growth failure, whichrethrowIfMustPropagatechecks by its exact class, is not wrapped.