risingwavelabs/risingwave · error

UDF returned negative row index

Error message

UDF returned negative row index

What it means

Guard in check_output for table-function UDFs: the Int32 index column returned by the UDF contains a negative row index. Indices must be non-negative to reference valid rows of the input chunk, so evaluation fails when a UDF emits a negative index.

Solutions

  1. Fix the UDF so the index column starts at 0 and strictly increases without negatives.
  2. If -1 was used as a sentinel, use nulls in the value column instead of negative indices.
  3. Add a self-check inside the UDF that clamps/asserts index validity before returning.

Example fix

// before (Python UDF)
return list(range(-1, n))  # starts at -1
// after
return list(range(0, n))
Defensive patterns

Strategy: validation

Validate before calling

# Python UDF: validate indices before returning
assert all(i >= 0 for i in indices), "negative row index generated"

Try / catch

match chunk_result {
    Ok(c) => c,
    Err(e) if e.to_string().contains("negative row index") => {
        bail!("UDF produced negative indices; check index generation logic");
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: The UDF returns Int32 column 0 containing negative values (e.g. a computed index like `i - offset` or an overflowing computation), detected by `raw_iter().any(|i| i < 0)`.

Common situations: A buggy UDF generating indices with an off-by-offset computation; UDF logic that emits sentinel -1 for missing rows.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/041dc9ebd8c26dbf. Report an issue: GitHub.

Appendix: source

Thrown at src/expr/core/src/table_function/user_defined.rs:106

    }

    /// Check if the output chunk is valid.
    fn check_output(&self, output: &DataChunk) -> Result<()> {
        if output.columns().len() != 2 {
            bail!(
                "UDF returned {} columns, but expected 2",
                output.columns().len()
            );
        }
        if output.column_at(0).data_type() != DataType::Int32 {
            bail!(
                "UDF returned {:?} at column 0, but expected {:?}",
                output.column_at(0).data_type(),
                DataType::Int32,
            );
        }
        if output.column_at(0).as_int32().raw_iter().any(|i| i < 0) {
            bail!("UDF returned negative row index");
        }
        if !output
            .column_at(1)
            .data_type()
            .equals_datatype(&self.return_type)
        {
            bail!(
                "UDF returned {:?} at column 1, but expected {:?}",
                output.column_at(1).data_type(),
                &self.return_type,
            );
        }
        Ok(())
    }
}

pub fn new_user_defined(prost: &PbTableFunction, chunk_size: usize) -> Result<BoxedTableFunction> {
    let udf = prost.get_udf()?;

View on GitHub (pinned to 6469eb736d)