risingwavelabs/risingwave · error

UDF returned at column 0, but expected

Error message

UDF returned {:?} at column 0, but expected {:?}

What it means

Guard in check_output for table-function UDFs: the UDF's output column 0 (the index column) has a type other than the required Int32. The generic generator protocol mandates an i32 index as column 0; a wrongly declared UDF return type triggers this error during evaluation.

Solutions

  1. Change the UDF to emit the row-index column as Arrow Int32 (e.g. cast to `pa.int32()` in pyarrow).
  2. If the UDF cannot change, cast the index column in the UDF wrapper/service before returning the batch.
  3. Update any SDK examples/docs where the index was produced as Int64.

Example fix

// before (Python)
return pa.record_batch([pa.array(range(n)), values], names=["i", "v"])  # i is int64
// after
return pa.record_batch([pa.array(range(n), pa.int32()), values], names=["i", "v"])
Defensive patterns

Strategy: validation

Validate before calling

# Python UDF: force index column to int32 before returning
indices = pa.array(range(n), type=pa.int32())

Try / catch

match chunk_result {
    Ok(c) => c,
    Err(e) if e.to_string().contains("at column 0, but expected") => {
        bail!("UDF index column must be Arrow Int32; fix UDF return types");
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: The UDF returns a two-column batch but column 0 is Int64/UInt32/Utf8 etc., so `output.column_at(0).data_type() != DataType::Int32` holds.

Common situations: A Python UDF emits 64-bit integers (Python ints default to int64 in pyarrow); a generated index column typed as Int64 in the UDF implementation.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

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

            let output = DataChunk::new(
                vec![origin_indices.into_ref(), output.column_at(1).clone()],
                output.visibility().clone(),
            );
            yield output;
        }
    }

    /// 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,
            );

View on GitHub (pinned to 6469eb736d)