{"record":{"id":"970ccf8cee05f264","repo":"risingwavelabs/risingwave","slug":"udf-returned-no-columns","errorCode":null,"errorMessage":"UDF returned no columns","messagePattern":"UDF returned no columns","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/expr/core/src/expr/expr_udf.rs","lineNumber":135,"sourceCode":"        self.metrics\n            .memory_usage_bytes\n            .set(self.runtime.memory_usage() as i64);\n\n        let arrow_output = arrow_output_result?;\n\n        if arrow_output.num_rows() != input.cardinality() {\n            bail!(\n                \"UDF returned {} rows, but expected {}\",\n                arrow_output.num_rows(),\n                input.cardinality(),\n            );\n        }\n\n        let output = self.arrow_convert.from_record_batch(&arrow_output)?;\n        let output = output.expand_vis(input.visibility().clone());\n\n        let Some(array) = output.columns().first() else {\n            bail!(\"UDF returned no columns\");\n        };\n        if !array.data_type().equals_datatype(&self.return_type) {\n            bail!(\n                \"UDF returned {:?}, but expected {:?}\",\n                array.data_type(),\n                self.return_type,\n            );\n        }\n\n        // handle optional error column\n        if let Some(errors) = output.columns().get(1) {\n            if errors.data_type() != DataType::Varchar {\n                bail!(\n                    \"UDF returned errors column with invalid type: {:?}\",\n                    errors.data_type()\n                );\n            }\n            let errors = errors","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/expr/core/src/expr/expr_udf.rs#L117-L153","documentation":"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.","triggerScenarios":"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.","commonSituations":"Custom UDFs returning only an error column or nothing; remote UDF services returning malformed payloads; SDK conversion bugs.","solutions":["Fix the UDF to always return at least the result column","Check the remote UDF service response and its serialization","Verify the Arrow->RisingWave conversion path isn't dropping columns","Test the UDF with a simple constant function to isolate where columns are lost"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// In UDF: ensure at least one output column\nassert!(!record_batch.columns().is_empty(), \"UDF must return a result column\");","typeGuard":"fn has_output_columns(b: &RecordBatch) -> bool { !b.columns().is_empty() }","tryCatchPattern":"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) }","preventionTips":["Always append the result column in UDF implementations","Smoke-test UDFs with a trivial constant function","Check remote UDF service responses are non-empty"],"tags":["rust","udf","arrow","empty-output"],"backgroundTag":"empty-api-response","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}