risingwavelabs/risingwave · error
UDF returned {} rows, but expected {}
Error message
UDF returned {} rows, but expected {} What it means
After a UDF's Arrow computation runs, the expression validates that the output batch has exactly as many rows as the input batch (one output row per input row, respecting visibility). A row-count mismatch means the UDF implementation violated the vectorized UDF contract, so evaluation bails.
Source
Thrown at src/expr/core/src/expr/expr_udf.rs:124
.instrument_await(self.span.clone())
.await;
timer.stop_and_record();
if arrow_output_result.is_ok() {
&self.metrics.success_count
} else {
&self.metrics.failure_count
}
.inc();
// update memory usage
self.metrics
.memory_usage_bytes
.set(self.runtime.memory_usage() as i64);
let arrow_output = arrow_output_result?;
if arrow_output.num_rows() != input.cardinality() {
bail!(
"UDF returned {} rows, but expected {}",
arrow_output.num_rows(),
input.cardinality(),
);
}
let output = self.arrow_convert.from_record_batch(&arrow_output)?;
let output = output.expand_vis(input.visibility().clone());
let Some(array) = output.columns().first() else {
bail!("UDF returned no columns");
};
if !array.data_type().equals_datatype(&self.return_type) {
bail!(
"UDF returned {:?}, but expected {:?}",
array.data_type(),
self.return_type,
);View on GitHub (pinned to 6469eb736d)
Solutions
- Fix the UDF to return exactly one output row per input row
- Check whether the UDF accidentally filters or aggregates; move that logic to a separate query construct
- If the UDF is remote, inspect its response batch size and SDK conversion code
- Add unit tests for the UDF with edge-case inputs (empty, null-containing batches)
Example fix
// before (UDF filters rows internally) let filtered = batch.filter(...)?; // after let filtered = batch; // return one row per input row
Defensive patterns
Strategy: try-catch
Validate before calling
// In UDF implementation: assert output rows match input rows assert_eq!(output.num_rows(), input.num_rows(), "UDF must be row-preserving");
Try / catch
match res { Err(e) if e.to_string().contains("UDF returned") => { log_udf_contract_violation(&e); fallback_to_error_row() }, Ok(v) => v, Err(e) => return Err(e) } Prevention
- Write UDF unit tests asserting 1:1 input/output row counts
- Avoid filtering/aggregating inside scalar UDFs
- Test UDFs against empty and visibility-masked batches
When it happens
Trigger: A UDF implementation (e.g. Python/JS/remote UDF) returns an Arrow RecordBatch whose num_rows differs from input cardinality — e.g. filtering rows, aggregation inside a scalar UDF, or wrong-length output arrays.
Common situations: Custom UDFs doing unintended filtering or group-bys; external UDF services returning malformed batches; bugs in UDF SDKs generating output of the wrong length.
Related errors
- UDF returned a value of type {} while the declared return ty
- UDF aggregate {what} has {} rows, but expected exactly 1
- UDF returned no columns
- UDF returned {:?}, but expected {:?}
- UDF returned errors column with invalid type: {:?}
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/df1e810116e41c3c.
Report an issue: GitHub.