{"record":{"id":"fef3fe582dbb7c40","repo":"risingwavelabs/risingwave","slug":"tablefunction-should-be-converted-to-projectset","errorCode":null,"errorMessage":"TableFunction should be converted to ProjectSet","messagePattern":"TableFunction should be converted to ProjectSet","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/frontend/src/optimizer/plan_node/logical_table_function.rs","lineNumber":106,"sourceCode":"impl ExprVisitable for LogicalTableFunction {\n    fn visit_exprs(&self, v: &mut dyn ExprVisitor) {\n        self.core.visit_exprs(v);\n    }\n}\n\nimpl PredicatePushdown for LogicalTableFunction {\n    fn predicate_pushdown(\n        &self,\n        predicate: Condition,\n        _ctx: &mut PredicatePushdownContext,\n    ) -> PlanRef {\n        LogicalFilter::create(self.clone().into(), predicate)\n    }\n}\n\nimpl ToBatch for LogicalTableFunction {\n    fn to_batch(&self) -> Result<crate::optimizer::plan_node::BatchPlanRef> {\n        unreachable!(\"TableFunction should be converted to ProjectSet\")\n    }\n}\n\nimpl ToStream for LogicalTableFunction {\n    fn to_stream(\n        &self,\n        _ctx: &mut ToStreamContext,\n    ) -> Result<crate::optimizer::plan_node::StreamPlanRef> {\n        unreachable!(\"TableFunction should be converted to ProjectSet\")\n    }\n\n    fn logical_rewrite_for_stream(\n        &self,\n        _ctx: &mut RewriteStreamContext,\n    ) -> Result<(PlanRef, ColIndexMapping)> {\n        unreachable!(\"TableFunction should be converted to ProjectSet\")\n    }\n}","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/frontend/src/optimizer/plan_node/logical_table_function.rs#L88-L124","documentation":"`LogicalTableFunction::to_batch` unconditionally panics with `unreachable!()`. Table functions (set-returning functions like `generate_series` or unnest) are never converted directly to a batch physical node; they are first rewritten into a `LogicalProjectSet` (or `LogicalFilter`/`LogicalJoin` wrapper) by an earlier normalization rule, so this conversion should never run.","triggerScenarios":"The batch optimizer's ToBatch pass visits a `LogicalTableFunction` node that was not first rewritten to ProjectSet — i.e. the normalization rule inserting ProjectSet did not run or a new plan path bypassed it.","commonSituations":"Seen by RisingWave contributors adding new table functions or new plan entry points that skip the table-function-to-ProjectSet normalization; users see it only as an internal error when such a bug ships.","solutions":["Ensure the plan is normalized so table functions are wrapped in LogicalProjectSet before batch optimization (re-run the frontend rewrite rules).","Rewrite the query to avoid the raw set-returning function in the offending position (e.g. move it into a SELECT list or CROSS JOIN LATERAL form that normalization covers).","Report the query to RisingWave developers; this indicates a missing rewrite rule path."],"exampleFix":"// before\nimpl ToBatch for LogicalTableFunction {\n    fn to_batch(&self) -> Result<BatchPlanRef> { unreachable!(\"TableFunction should be converted to ProjectSet\") }\n}\n// after (caller side)\nlet normalized = plan_node.as_logical_table_function()\n    .map(|tf| LogicalProjectSet::create(tf.clone().into()))\n    .unwrap_or_else(|| plan_node.clone());\nlet batch = normalized.to_batch()?;\n","handlingStrategy":"validation","validationCode":"// Ensure table functions are normalized to ProjectSet before batch conversion.\nif let Some(tf) = plan.as_logical_table_function() {\n    plan = LogicalProjectSet::create(tf.clone().into()).into();\n}","typeGuard":"fn assert_no_raw_table_function(plan: &PlanRef) {\n    assert!(plan.as_logical_table_function().is_none(), \"wrap in LogicalProjectSet first\");\n}","tryCatchPattern":null,"preventionTips":["Always run the frontend normalization rewrites before ToBatch.","Never construct LogicalTableFunction plans manually; use the helpers that wrap in ProjectSet.","Add assertions/tests that post-rewrite plans contain no bare LogicalTableFunction.","Place set-returning functions in SELECT lists or LATERAL joins that normalization covers."],"tags":["rust","optimizer","unreachable","table-function","projectset"],"backgroundTag":"internal-invariant-violation","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"}