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.

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.

Appendix: 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)