JingsongLi commented on code in PR #10001:
URL: https://github.com/apache/paimon/pull/10001#discussion_r4056471363


##########
docs/docs/concepts/spec/rowformat.md:
##########
@@ -182,11 +182,12 @@ To read a row by its zero-based row number within the 
file:
 
 1. **Read Footer**: Seek to file end - 32 bytes, read the 32-byte footer. 
Validate magic number.
 2. **Read Block Index**: Seek to `indexOffset`, read `indexLength` bytes, 
decode the three arrays. Compute block offsets by prefix sum of 
`blockCompressedSizes[]`.
-3. **Select Block**: Find block `b` where `blockRowStarts[b] <= rowNum < 
blockEnd`. For the last block, `blockEnd` is `totalRowCount`; otherwise it is 
`blockRowStarts[b + 1]`.
-4. **Read Block**: Seek to `blockOffset(b)`, read `blockCompressedSizes[b]` 
bytes.
-5. **Decompress**: ZSTD decompress into a buffer of size 
`blockUncompressedSizes[b]`.
-6. **Locate Row**: Compute `localIdx = rowNum - blockRowStarts[b]`. Read 
`offsets[localIdx]` from the offset array at the end of the decompressed block.
-7. **Deserialize**: Read the row starting at the computed offset using the row 
serialization format.
+3. **Check Consistency**: The three arrays must have the same length, that 
length must equal `blockCount`, and `blockCompressedSizes[]` must sum to 
`indexOffset`, because the blocks are written contiguously from position 0 and 
the index follows the last one. A reader that bounds its block loop by one of 
the two — the footer's `blockCount` or the index array length — must reject a 
file where they disagree rather than silently reading fewer blocks.

Review Comment:
   [P1] Apply the new consistency contract to the Python row reader too
   
   This now defines rejection as a format-reader requirement, and the PR 
description specifically calls out that Python bounds iteration by the footer 
count, but `pypaimon/read/reader/format_row_reader.py::_read_metadata` still 
trusts `block_count`, `index_offset`, and `index_length` and never compares the 
three decoded array lengths or their compressed-size sum. For example, changing 
`blockCount` to 0 still makes Python return an empty result for a non-empty 
file rather than reject it. Please implement the same validation and regression 
cases in Python so Java and Python do not retain the cross-language behavior 
this change is meant to remove.
   



##########
paimon-format/src/main/java/org/apache/paimon/format/row/RowBlockIndex.java:
##########
@@ -36,12 +38,62 @@ class RowBlockIndex {
 
     RowBlockIndex(
             long[] blockCompressedSizes, long[] blockUncompressedSizes, long[] 
blockRowStarts) {
+        checkArgument(
+                blockCompressedSizes.length == blockUncompressedSizes.length
+                        && blockCompressedSizes.length == 
blockRowStarts.length,
+                "Row file block index arrays disagree on the block count: %s 
compressed sizes, %s uncompressed sizes, %s row starts.",
+                blockCompressedSizes.length,
+                blockUncompressedSizes.length,
+                blockRowStarts.length);
         this.blockCompressedSizes = blockCompressedSizes;
         this.blockUncompressedSizes = blockUncompressedSizes;
         this.blockRowStarts = blockRowStarts;
         this.blockOffsets = computeOffsets(blockCompressedSizes);
     }
 
+    /**
+     * Checks the index against the footer, which is the only place both are 
in hand. Blocks are
+     * written contiguously from position 0 and the index follows the last 
one, so the compressed
+     * sizes must sum to exactly {@code indexOffset} — see the row format 
spec. Row starts index the
+     * arrays of every later lookup and size the per-block selection array.
+     */
+    void validate(RowFileFooter footer) throws IOException {
+        if (blockCount() != footer.blockCount) {
+            throw new IOException(
+                    String.format(
+                            "Row file block index holds %d blocks, but the 
footer declares %d.",
+                            blockCount(), footer.blockCount));
+        }
+
+        long blocksEnd =
+                blockCount() == 0
+                        ? 0
+                        : blockOffset(blockCount() - 1) + 
blockCompressedSize(blockCount() - 1);
+        if (blocksEnd != footer.indexOffset) {
+            throw new IOException(
+                    String.format(
+                            "Row file blocks end at %d, but the footer puts 
the block index at %d.",
+                            blocksEnd, footer.indexOffset));
+        }
+
+        for (int i = 1; i < blockCount(); i++) {
+            if (blockRowStarts[i] < blockRowStarts[i - 1]) {

Review Comment:
   [P1] Reject row-start gaps and duplicates before selection uses them
   
   Checking only for a decrease still accepts `[10]` for a one-block file and 
`[0, 0]` for two blocks. `RowFormatReader.computeBlocksToRead` then treats 
those values as block ranges: selected rows 0-9 are omitted in the first case, 
and the first block has the empty range `[0, 0)` in the second, so 
selection-backed reads can silently drop valid rows even though this validation 
succeeds. Please require the first start to be 0, subsequent starts to increase 
strictly, and an empty index only when `totalRowCount` is 0; a selection 
regression would pin the behavior.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to