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

  1. Fix the UDF implementation to always return exactly one row for state and result
  2. Check UDF server behavior for empty/degenerate inputs and return a single null row instead of zero rows
  3. Inspect the UDF server logs to see why the batch length is not 1
  4. 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

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


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