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

  1. Fix the UDF to return exactly one output row per input row
  2. Check whether the UDF accidentally filters or aggregates; move that logic to a separate query construct
  3. If the UDF is remote, inspect its response batch size and SDK conversion code
  4. 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

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


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