{"record":{"id":"b24f538fe11813a6","repo":"risingwavelabs/risingwave","slug":"query-epoch-not-set-in-distributed-lookup-join","errorCode":null,"errorMessage":"query_epoch not set in distributed lookup join","messagePattern":"query_epoch not set in distributed lookup join","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/batch/executors/src/executor/join/distributed_lookup_join.rs","lineNumber":196,"sourceCode":"            .map(ColumnId::from)\n            .collect();\n\n        // Use a full vnode bitmap so that the lookup can be performed on any worker.\n        // For a lookup table with hash distribution, the lookup keys always contain the\n        // distribution key; for a lookup table with singleton distribution, all lookups\n        // are gathered into a single task. In both cases the lookup is correct regardless\n        // of which worker the task is scheduled to.\n        let vnodes = Some(Bitmap::ones(table_desc.vnode_count()).into());\n\n        dispatch_state_store!(source.context().state_store(), state_store, {\n            let table = BatchTable::new_partial(state_store, column_ids, vnodes, table_desc);\n            let inner_side_builder = InnerSideExecutorBuilder::new(\n                outer_side_key_types,\n                inner_side_key_types.clone(),\n                lookup_prefix_len,\n                distributed_lookup_join_node\n                    .query_epoch\n                    .ok_or_else(|| anyhow!(\"query_epoch not set in distributed lookup join\"))?,\n                vec![],\n                table,\n                chunk_size,\n            );\n\n            let identity = source.plan_node().get_identity().clone();\n\n            Ok(DistributedLookupJoinExecutorArgs {\n                join_type,\n                condition,\n                outer_side_input,\n                outer_side_data_types,\n                outer_side_key_idxs,\n                inner_side_builder,\n                inner_side_key_types,\n                inner_side_key_idxs,\n                null_safe,\n                lookup_prefix_len,","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/batch/executors/src/executor/join/distributed_lookup_join.rs#L178-L214","documentation":"The distributed lookup join executor requires an epoch (query_epoch) from the protobuf plan node to snapshot the inner side table at a consistent MVCC version. If the proto field is None (unset), new_boxed_executor cannot build the inner-side executor and fails with this anyhow error.","triggerScenarios":"Executing a distributed lookup join plan whose DistributedLookupJoinNode protobuf was constructed without setting query_epoch — typically a plan serialization/deserialization gap or a frontend/meta node running older code that does not populate the field.","commonSituations":"Version-skew clusters where the frontend generates plans without query_epoch but the compute node requires it; hand-crafted or replayed protobuf plans; plan cache/format changes after an upgrade.","solutions":["Set query_epoch when constructing DistributedLookupJoinNode in the batch planner/frontend","Rebuild/redeploy frontend and compute nodes to the same version so the field is always populated","Check for stale plan fragments/caches from an older cluster version and invalidate them"],"exampleFix":"// before\nDistributedLookupJoinNode { lookup_prefix_len, ..Default::default() }\n// after\nDistributedLookupJoinNode { lookup_prefix_len, query_epoch: Some(epoch), ..Default::default() }","handlingStrategy":"validation","validationCode":"fn validate_lookup_join_plan(node: &DistributedLookupJoinNode) -> anyhow::Result<()> {\n    anyhow::ensure!(node.query_epoch.is_some(), \"distributed lookup join plan missing query_epoch\");\n    Ok(())\n}","typeGuard":"fn has_query_epoch(node: &DistributedLookupJoinNode) -> bool {\n    node.query_epoch.is_some()\n}","tryCatchPattern":"let epoch = distributed_lookup_join_node.query_epoch.ok_or_else(|| {\n    anyhow::anyhow!(\"query_epoch not set in distributed lookup join\")\n})?;","preventionTips":["Always populate query_epoch when constructing DistributedLookupJoinNode in the planner","Add a round-trip proto serialization test asserting query_epoch survives encode/decode","Keep frontend and compute node versions in lockstep to avoid proto field skew"],"tags":["rust","batch","lookup-join","protobuf","missing-field"],"backgroundTag":"missing-required-argument","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"}