{"record":{"id":"b3597bdcccb7d4e1","repo":"risingwavelabs/risingwave","slug":"udf-returned-columns-but-expected-2","errorCode":null,"errorMessage":"UDF returned {} columns, but expected 2","messagePattern":"UDF returned (.+?) columns, but expected 2","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/expr/core/src/table_function/user_defined.rs","lineNumber":93,"sourceCode":"                .column_at(0)\n                .as_int32()\n                .raw_iter()\n                // we have checked all indices are non-negative\n                .map(|idx| visible_rows[idx as usize] as i32)\n                .collect::<I32Array>();\n\n            let output = DataChunk::new(\n                vec![origin_indices.into_ref(), output.column_at(1).clone()],\n                output.visibility().clone(),\n            );\n            yield output;\n        }\n    }\n\n    /// Check if the output chunk is valid.\n    fn check_output(&self, output: &DataChunk) -> Result<()> {\n        if output.columns().len() != 2 {\n            bail!(\n                \"UDF returned {} columns, but expected 2\",\n                output.columns().len()\n            );\n        }\n        if output.column_at(0).data_type() != DataType::Int32 {\n            bail!(\n                \"UDF returned {:?} at column 0, but expected {:?}\",\n                output.column_at(0).data_type(),\n                DataType::Int32,\n            );\n        }\n        if output.column_at(0).as_int32().raw_iter().any(|i| i < 0) {\n            bail!(\"UDF returned negative row index\");\n        }\n        if !output\n            .column_at(1)\n            .data_type()\n            .equals_datatype(&self.return_type)","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/expr/core/src/table_function/user_defined.rs#L75-L111","documentation":"`check_output` in the user-defined table function validates every output chunk of a generate-series-style UDF. Such UDFs must return exactly two columns: an Int32 row index and a value column; any other column count fails with this error.","triggerScenarios":"`eval_inner` invokes the external UDF, converts its Arrow RecordBatch to a `DataChunk`, and `check_output` finds `output.columns().len() != 2` — e.g. the UDF returns 1 or 3 columns.","commonSituations":"A Python UDF's return value is wrapped/unwrapped incorrectly (returning a single column instead of (index, value), or extra debug columns); a UDF library version change altered the returned batch shape.","solutions":["Fix the UDF to return exactly two columns: (Int32 row index, value column).","Check the UDF client/SDK wrapper that converts the UDF's return value into an Arrow RecordBatch for accidental column drops/additions.","Pin/align the UDF service version with the RisingWave UDF protocol you coded against."],"exampleFix":"// before (Python UDF)\ndef gen(n):\n    return [i for i in range(n)]  # 1 column\n// after\ndef gen(n):\n    return list(range(n)), [x * 2 for x in range(n)]  # (index, value) => 2 columns","handlingStrategy":"validation","validationCode":"# caller-side pre-check on the UDF service output (pyarrow)\nassert batch.num_columns == 2, f\"UDF returned {batch.num_columns} columns\"\nassert batch.schema.field(0).type == pa.int32()","typeGuard":null,"tryCatchPattern":"match table_function.eval_inner(input).await {\n    Ok(chunk) => chunk,\n    Err(e) if e.to_string().contains(\"columns, but expected 2\") => {\n        log::error!(\"UDF returned wrong column count; check UDF return shape\");\n        bail!(ExecError::UdfOutput(e));\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Return exactly (index, value) two-column batches from generate-style UDFs","Test UDF outputs against the expected Arrow schema in the UDF's own CI","Avoid ad-hoc extra/debug columns in UDF return values"],"tags":["udf","table-function","validation","arrow"],"backgroundTag":"unexpected-response-shape","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}