risingwavelabs/risingwave · error
UDF returned columns, but expected 2
Error message
UDF returned {} columns, but expected 2 What it means
`check_output` in the user-defined table function validates every output chunk of a generate-series-style UDF. Such UDFs must return exactly two columns: an Int32 row index and a value column; any other column count fails with this error.
Solutions
- Fix the UDF to return exactly two columns: (Int32 row index, value column).
- Check the UDF client/SDK wrapper that converts the UDF's return value into an Arrow RecordBatch for accidental column drops/additions.
- Pin/align the UDF service version with the RisingWave UDF protocol you coded against.
Example fix
// before (Python UDF)
def gen(n):
return [i for i in range(n)] # 1 column
// after
def gen(n):
return list(range(n)), [x * 2 for x in range(n)] # (index, value) => 2 columns Defensive patterns
Strategy: validation
Validate before calling
# caller-side pre-check on the UDF service output (pyarrow)
assert batch.num_columns == 2, f"UDF returned {batch.num_columns} columns"
assert batch.schema.field(0).type == pa.int32() Try / catch
match table_function.eval_inner(input).await {
Ok(chunk) => chunk,
Err(e) if e.to_string().contains("columns, but expected 2") => {
log::error!("UDF returned wrong column count; check UDF return shape");
bail!(ExecError::UdfOutput(e));
}
Err(e) => return Err(e.into()),
} Prevention
- Return exactly (index, value) two-column batches from generate-style UDFs
- Test UDF outputs against the expected Arrow schema in the UDF's own CI
- Avoid ad-hoc extra/debug columns in UDF return values
When it happens
Trigger: `eval_inner` invokes the external UDF, converts its Arrow RecordBatch to a `DataChunk`, and `check_output` finds `output.columns().len() != 2` — e.g. the UDF returns 1 or 3 columns.
Common situations: A Python UDF's return value is wrapped/unwrapped incorrectly (returning a single column instead of (index, value), or extra debug columns); a UDF library version change altered the returned batch shape.
Related errors
- UDF returned negative row index
- UDF returned at column 0, but expected
- UDF returned at column 1, but expected
- UDF aggregate has rows, but expected exactly 1
- UDF returned a value of type
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/b3597bdcccb7d4e1.
Report an issue: GitHub.
Appendix: source
Thrown at src/expr/core/src/table_function/user_defined.rs:93
.column_at(0)
.as_int32()
.raw_iter()
// we have checked all indices are non-negative
.map(|idx| visible_rows[idx as usize] as i32)
.collect::<I32Array>();
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)View on GitHub (pinned to 6469eb736d)