risingwavelabs/risingwave · error

UDF returned {:?}, but expected {:?}

Error message

UDF returned {:?}, but expected {:?}

What it means

The UDF's first output column's Arrow data type is checked against the declared RisingWave return type of the expression. If the Arrow type does not equal the expected type, evaluation bails showing both. This guards against UDF implementations whose declared schema does not match what they actually produce.

Source

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

        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
                .as_utf8()
                .iter()
                .filter_map(|msg| msg.map(|s| ExprError::Custom(s.into())))

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Align the UDF's actual output type with its declared return type (cast inside the UDF)
  2. Update the CREATE FUNCTION return type to match the real output
  3. Check the UDF SDK's type mapping for the declared type and adjust
  4. If it's an Arrow variant mismatch (utf8 vs large_utf8), fix the UDF's array construction

Example fix

-- before
CREATE FUNCTION f(x int) RETURNS int AS ... -- UDF actually returns bigint
-- after
CREATE FUNCTION f(x int) RETURNS bigint AS ...
Defensive patterns

Strategy: type-guard

Validate before calling

// In UDF: verify output type before returning
assert!(array.data_type().equals_datatype(&expected_arrow_type), "UDF output type mismatch");

Type guard

fn matches_return_type(arr: &dyn Array, rt: &DataType) -> bool { arr.data_type().equals_datatype(rt) }

Try / catch

match res { Err(e) if e.to_string().contains("but expected") => reconcile_udf_signature(&e), Ok(v) => v, Err(e) => return Err(e) }

Prevention

When it happens

Trigger: A UDF returns data of a different type than its declared return type — e.g. declares Int32 but returns Int64, or declares a RisingWave type whose Arrow mapping differs from the produced array type.

Common situations: UDF signature changed but the CREATE FUNCTION return type wasn't updated; SDK/default type mapping mismatches (e.g. string vs large_string Arrow variants); remote UDF returning different types on certain inputs.

Related errors


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