prestodb/presto · error · IllegalArgumentException

Invalid row block

Error message

Invalid row block

What it means

When converting a RunLengthEncodedBlock containing a single row value into a columnar row, a NULL row must have every null-suppressed field block empty (0 positions). If any field of a null RLE row has a non-zero position count, the block is structurally inconsistent and IllegalArgumentException("Invalid row block") is thrown. This indicates corrupted or incorrectly constructed nested blocks.

Source

Thrown at presto-common/src/main/java/com/facebook/presto/common/block/ColumnarRow.java:116

                // 3) the estimated serialized size for the fields Blocks which were just constructed as new DictionaryBlocks:
                //     the average row size: averageRowSize * the number of rows: nonNullPositionCount
                (Integer.BYTES + Byte.BYTES) * positionCount + averageRowSize * nonNullPositionCount);
    }

    private static ColumnarRow toColumnarRow(RunLengthEncodedBlock rleBlock)
    {
        Block rleValue = rleBlock.getValue();
        int positionCount = rleBlock.getPositionCount();
        ColumnarRow columnarRow = toColumnarRow(rleValue);

        Block[] fields = new Block[columnarRow.getFieldCount()];
        long averageRowSize = 0;
        for (int i = 0; i < columnarRow.getFieldCount(); i++) {
            Block nullSuppressedField = columnarRow.getField(i);
            if (rleValue.isNull(0)) {
                // the rle value is a null row so, all null-suppressed fields should empty
                if (nullSuppressedField.getPositionCount() != 0) {
                    throw new IllegalArgumentException("Invalid row block");
                }
                fields[i] = nullSuppressedField;
            }
            else {
                fields[i] = new RunLengthEncodedBlock(nullSuppressedField, positionCount);
                averageRowSize += nullSuppressedField.getSizeInBytes() / nullSuppressedField.getPositionCount();
            }
        }
        return new ColumnarRow(
                rleBlock,
                fields,
                INSTANCE_SIZE + rleBlock.getRetainedSizeInBytes(),
                // The estimated serialized size is the sum of the following:
                // 1) the offsets size: Integer.BYTES * positionCount. Note that even though ColumnarRow doesn't have the offsets array, the serialized RowBlock still has it. Please see RowBlockEncodingBuffer.
                // 2) nulls array size: Byte.BYTES * positionCount
                // 3) the estimated serialized size for the fields Blocks which were just constructed as new RunLengthEncodedBlocks:
                //     the average row size: averageRowSize * the number of rows: positionCount
                (Integer.BYTES + Byte.BYTES) * positionCount + averageRowSize * positionCount);

View on GitHub (pinned to 55bb57d202)

Solutions

  1. Fix the producer so a NULL row's RLE block wraps field blocks with positionCount 0
  2. Rebuild the RLE block using RunLengthEncodedBlock(valueBlock, positionCount) from a properly formed row block instead of hand-assembling fields
  3. Validate the source data/page upstream to catch malformed blocks before conversion
  4. If the RLE value is non-null, ensure each field block has exactly 1 position (the loop divides by positionCount later)
  5. Report/inspect the connector or codec that emitted the block, since this is data corruption, not user input

Example fix

// before — building RLE row block from a null row with leftover fields
Block[] nonEmptyFields = existingFields; // fields still hold positions
RunLengthEncodedBlock rle = new RunLengthEncodedBlock(new RowBlockBuilder(...).build(), 1);
// after — empty field blocks for a null row
RowBlockBuilder builder = new RowBlockBuilder(fieldTypes, null, 1);
builder.buildEntry(b -> { for (Type t : fieldTypes) { b.appendNull(); } });
RunLengthEncodedBlock rle = new RunLengthEncodedBlock(builder.build(), positionCount);
Defensive patterns

Strategy: validation

Validate before calling

static void validateRleRowBlock(RunLengthEncodedBlock rleValue, ColumnarRow columnarRow) {
    for (int i = 0; i < columnarRow.getFieldCount(); i++) {
        Block field = columnarRow.getField(i);
        if (rleValue.isNull(0) && field.getPositionCount() != 0) {
            throw new IllegalArgumentException("Field " + i + " of null RLE row must be empty");
        }
    }
}

Try / catch

try {
    ColumnarRow row = ColumnarRow.toColumnarRow(rleBlock);
} catch (IllegalArgumentException e) {
    if (e.getMessage().equals("Invalid row block")) {
        throw new corruptPageException("Malformed RLE row block: null row with non-empty fields");
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling ColumnarRow.toColumnarRow / columnarRow on a RunLengthEncodedBlock whose single value is a NULL row but whose internal field blocks contain a non-zero number of positions — i.e. a malformed RLE-wrapped row block produced by a writer or deserializer.

Common situations: Custom serializers building RLE row blocks with mismatched field lengths; corrupted intermediate data from a connector; bugs in code that wraps a null row into RunLengthEncodedBlock without emptying field blocks.

Related errors


AI-assisted analysis of prestodb/presto@55bb57d202 (2026-09-04). Data as JSON: /api/errors/12ee6528c2279da0. Report an issue: GitHub.