risingwavelabs/risingwave · error

unimplemented

Error message

unimplemented

What it means

LogicalIntersect (SQL INTERSECT) does not implement a direct-to-batch conversion: its to_batch method calls unimplemented!(), which panics. INTERSECT must first be rewritten into equivalent EXCEPT/semijoin (or union-of-except) forms by an earlier logical rewrite rule; reaching to_batch on the raw node means that rewrite never happened.

Source

Thrown at src/frontend/src/optimizer/plan_node/logical_intersect.rs:100

impl PredicatePushdown for LogicalIntersect {
    fn predicate_pushdown(
        &self,
        predicate: Condition,
        ctx: &mut PredicatePushdownContext,
    ) -> PlanRef {
        let new_inputs = self
            .inputs()
            .iter()
            .map(|input| input.predicate_pushdown(predicate.clone(), ctx))
            .collect_vec();
        self.clone_with_inputs(&new_inputs)
    }
}

impl ToBatch for LogicalIntersect {
    fn to_batch(&self) -> Result<crate::optimizer::plan_node::BatchPlanRef> {
        unimplemented!()
    }
}

impl ToStream for LogicalIntersect {
    fn to_stream(
        &self,
        _ctx: &mut ToStreamContext,
    ) -> Result<crate::optimizer::plan_node::StreamPlanRef> {
        unimplemented!()
    }

    fn logical_rewrite_for_stream(
        &self,
        _ctx: &mut RewriteStreamContext,
    ) -> Result<(PlanRef, ColIndexMapping)> {
        unimplemented!()
    }
}

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Upgrade RisingWave: in released versions, INTERSECT queries are rewritten to EXCEPT before batch planning, so this panic indicates an optimizer bug worth reporting.
  2. Simplify or rewrite the query using EXCEPT or equivalent joins if you hit this on a patched/dev build.
  3. As a developer, ensure the rewrite rule converting LogicalIntersect to EXCEPT runs before to_batch, or implement to_batch properly.

Example fix

// before
impl ToBatch for LogicalIntersect {
    fn to_batch(&self) -> Result<BatchPlanRef> {
        unimplemented!()
    }
}
// after
impl ToBatch for LogicalIntersect {
    fn to_batch(&self) -> Result<BatchPlanRef> {
        bail!("LogicalIntersect should be rewritten to LogicalExcept before to_batch")
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// For dev tooling: ensure INTERSECT queries are rewritten before batch conversion
if plan_contains_intersect(batch_plan) {
    return Err("apply intersect-to-except rewrite first".into());
}

Type guard

fn contains_intersect(plan: &PlanRef) -> bool {
    plan.walk().any(|n| matches!(n.as_logical(), Some(LogicalNode::Intersect(_))))
}

Try / catch

let r = std::panic::catch_unwind(|| planner.to_batch(plan));
match r {
    Ok(v) => v?,
    Err(_) => return Err(anyhow!("unimplemented to_batch on intersect; apply the rewrite rule or report a bug")),
}

Prevention

When it happens

Trigger: The batch conversion pass (to_batch) is invoked on a LogicalIntersect node that was not rewritten into EXCEPT form first — e.g. a planner regression or a code path that skips the intersect-to-except rewrite.

Common situations: Running INTERSECT queries after changes to the logical rewrite rules; bugs where the rewrite from INTERSECT to EXCEPT/set-operations is skipped; interacting with optimizer internals in development.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/07368301bc2f26b3. Report an issue: GitHub.