risingwavelabs/risingwave · error
UDF aggregate {what} has {} rows, but expected exactly 1
Error message
UDF aggregate {what} has {} rows, but expected exactly 1 What it means
`ensure_single_row` guards UDF aggregate state/result arrays: both must contain exactly one row because call sites read via `datum_at(0)`. If the external UDF runtime returns an array with 0 or >1 rows, this error prevents an out-of-bounds read.
Source
Thrown at src/expr/core/src/aggregate/user_defined.rs:128
}
/// Decode the state from a datum in state table.
fn decode_state(&self, datum: Datum) -> Result<AggregateState> {
let array = {
let mut builder = DataType::Bytea.create_array_builder(1);
builder.append(datum);
builder.finish()
};
let state = UdfArrowConvert::default().to_array(self.state_field.data_type(), &array)?;
Ok(AggregateState::Any(Box::new(State(state))))
}
}
/// The runtime is external input: reject an array of the wrong length so that `datum_at(0)` at
/// the call sites is in bounds.
fn ensure_single_row(array: &ArrayRef, what: &str) -> Result<()> {
if array.len() != 1 {
return Err(anyhow::anyhow!(
"UDF aggregate {what} has {} rows, but expected exactly 1",
array.len()
)
.into());
}
Ok(())
}
// In arrow-udf, aggregate state is represented as an `ArrayRef`.
// To avoid unnecessary conversion between `ArrayRef` and `Datum`,
// we store `ArrayRef` directly in our `AggregateState`.
#[derive(Debug)]
struct State(ArrayRef);
impl EstimateSize for State {
fn estimated_heap_size(&self) -> usize {
self.0.get_array_memory_size()
}View on GitHub (pinned to 6469eb736d)
Solutions
- Fix the UDF implementation to always return exactly one row for state and result
- Check UDF server behavior for empty/degenerate inputs and return a single null row instead of zero rows
- Inspect the UDF server logs to see why the batch length is not 1
- Upgrade/sync RisingWave and UDF service versions
Example fix
// before (UDF returns empty array for empty input) Int64Array::from(Vec::<i64>::new()) // after Int64Array::from(vec![None]) // single null row preserves arity
Defensive patterns
Strategy: validation
Validate before calling
if result_array.len() != 1 {
// reject before calling encode_state/get_result-dependent code
} Type guard
fn is_single_row(a: &arrow::array::ArrayRef) -> bool { a.len() == 1 } Try / catch
match agg.get_result().await {
Ok(v) => v,
Err(e) if e.to_string().contains("expected exactly 1") => {
// treat as UDF server contract violation; surface to user
}
Err(e) => return Err(e),
} Prevention
- Ensure UDF state/merge functions always return exactly one row
- Test UDF servers against empty and multi-batch inputs
- Document the single-row contract in UDF SDKs
When it happens
Trigger: `get_result` or `encode_state` on a UserDefinedAggregate receives an ArrayRef whose len() != 1 — typically a UDF server returning an empty batch or multiple rows for the merged state/result.
Common situations: UDF implementation of `merge`/`state` accumulates multiple rows; UDF server misbehaves on empty input; protocol drift where the server streams partial batches.
Related errors
- UDF returned a value of type {} while the declared return ty
- UDF returned {} rows, but expected {}
- 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/74bc0046f9ffd87b.
Report an issue: GitHub.