{"record":{"id":"787394e175f5da6b","repo":"risingwavelabs/risingwave","slug":"index-scan-failed-to-batch","errorCode":null,"errorMessage":"index scan failed to batch","messagePattern":"index scan failed to batch","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/frontend/src/optimizer/plan_node/logical_join.rs","lineNumber":312,"sourceCode":"        {\n            result_plan = Some(lookup_join);\n        }\n\n        if self\n            .core\n            .ctx()\n            .session_ctx()\n            .config()\n            .enable_index_selection()\n        {\n            let indexes = logical_scan.table_indexes();\n            for index in indexes {\n                if let Some(index_scan) = logical_scan.to_index_scan_if_index_covered(index) {\n                    let index_scan: PlanRef = index_scan.into();\n                    let that = self.clone_with_left_right(self.left(), index_scan.clone());\n                    let mut new_batch_join = batch_join.clone();\n                    new_batch_join.right =\n                        index_scan.to_batch().expect(\"index scan failed to batch\");\n\n                    // Lookup covered index.\n                    if let Some(lookup_join) =\n                        that.to_batch_lookup_join(predicate.clone(), new_batch_join)?\n                    {\n                        match &result_plan {\n                            None => result_plan = Some(lookup_join),\n                            Some(prev_lookup_join) => {\n                                // Prefer to choose lookup join with longer lookup prefix len.\n                                if prev_lookup_join.lookup_prefix_len()\n                                    < lookup_join.lookup_prefix_len()\n                                {\n                                    result_plan = Some(lookup_join)\n                                }\n                            }\n                        }\n                    }\n                }","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/frontend/src/optimizer/plan_node/logical_join.rs#L294-L330","documentation":"During index-lookup-join conversion, the right-side index scan is lowered to a batch plan with to_batch() and the result is expect()-ed. If the index scan cannot be converted to a batch node (no valid batch implementation), the optimizer panics instead of skipping the index.","triggerScenarios":"to_batch_lookup_join_with_index_selection encounters an index whose LogicalScan cannot produce a batch plan via to_index_scan_if_index_covered + to_batch, e.g. due to unsupported scan features or missing table/catalog info.","commonSituations":"Queries triggering batch index lookup joins where the index's primary/table structure changed, or a new scan feature (e.g. unsupported filter pushdown) makes the index scan unconvertible.","solutions":["Check the index selection logic: ensure only indexes that fully cover the required columns are selected","Verify the table and index catalogs are consistent (recreate index if stale)","Fall back to a regular hash join instead of forcing the lookup-join path when to_batch returns None","Report with the query plan; this indicates an optimizer rule missing a feasibility check"],"exampleFix":"// before\nnew_batch_join.right = index_scan.to_batch().expect(\"index scan failed to batch\");\n// after\nmatch index_scan.to_batch() {\n    Some(right) => new_batch_join.right = right,\n    None => continue, // try next index or fall back to non-lookup join\n}","handlingStrategy":"fallback","validationCode":"// before lookup-join conversion, verify the index covers required columns and its scan converts:\n// if logical_scan.to_index_scan_if_index_covered(index).and_then(|s| s.to_batch()).is_none() { skip index }","typeGuard":null,"tryCatchPattern":"match index_scan.to_batch() { Some(p) => p, None => fallback_to_hash_join() }","preventionTips":["Only select fully covering indexes for lookup joins","Keep table/index catalogs in sync","Treat to_batch()==None as skip, not panic, in optimizer rules"],"tags":["optimizer","batch","index","lookup-join"],"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-23T08:17:48.524Z"}