risingwavelabs/risingwave · error

UDF returned no columns

Error message

UDF returned no columns

What it means

After converting the UDF's Arrow RecordBatch into RisingWave's column format, the expression takes the first column as the result. If the batch has zero columns there is no result to return, so it bails. A scalar/vectorized UDF must always produce at least one output column.

Source

Thrown at src/expr/core/src/expr/expr_udf.rs:135

        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,
            );
        }

        // handle optional error column
        if let Some(errors) = output.columns().get(1) {
            if errors.data_type() != DataType::Varchar {
                bail!(
                    "UDF returned errors column with invalid type: {:?}",
                    errors.data_type()
                );
            }
            let errors = errors

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Fix the UDF to always return at least the result column
  2. Check the remote UDF service response and its serialization
  3. Verify the Arrow->RisingWave conversion path isn't dropping columns
  4. Test the UDF with a simple constant function to isolate where columns are lost
Defensive patterns

Strategy: try-catch

Validate before calling

// In UDF: ensure at least one output column
assert!(!record_batch.columns().is_empty(), "UDF must return a result column");

Type guard

fn has_output_columns(b: &RecordBatch) -> bool { !b.columns().is_empty() }

Try / catch

match res { Err(e) if e.to_string().contains("UDF returned no columns") => log_and_report_udf_bug(&e), Ok(v) => v, Err(e) => return Err(e) }

Prevention

When it happens

Trigger: A UDF implementation or remote UDF service returns an empty RecordBatch (zero columns) — e.g. the function forgot to append its result column, or a conversion layer dropped all columns.

Common situations: Custom UDFs returning only an error column or nothing; remote UDF services returning malformed payloads; SDK conversion bugs.

Understand the failure class

Background: "empty response", "returned no data", "empty embeddings": what HTTP 200-with-empty-body errors mean across libraries — this error's family across 36 libraries.

Related errors


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