risingwavelabs/risingwave · error

Expected RisingWave plan in BatchPlanChoice, but got…

Error message

Expected RisingWave plan in BatchPlanChoice, but got DataFusion plan

What it means

BatchPlanChoice is an enum that wraps either a RisingWave-native batch plan (Rw) or a DataFusion plan (Df). unwrap_rw() refuses to extract a DataFusion variant, since the caller requires a RisingWave plan to proceed. Throwing this error prevents accidentally treating a DataFusion plan as a native one.

Solutions

  1. Disable the `datafusion` feature so queries are always planned natively by RisingWave.
  2. Check the variant with matches!(choice, BatchPlanChoice::Rw(..)) before calling unwrap_rw().
  3. Route DataFusion plans to a DataFusion-capable executor instead of the RisingWave batch path.
  4. Add a match arm handling BatchPlanChoice::Df in the caller if both engines must be supported.

Example fix

// before
let rw_plan = batch_plan.unwrap_rw()?;
// after
let rw_plan = match batch_plan {
    BatchPlanChoice::Rw(rw) => rw,
    BatchPlanChoice::Df { .. } => return Err(anyhow!("query requires the RisingWave planner; rebuild without the datafusion feature")),
};
Defensive patterns

Strategy: type-guard

Validate before calling

fn is_rw_plan(choice: &BatchPlanChoice) -> bool {
    matches!(choice, BatchPlanChoice::Rw(_))
}

Type guard

if let BatchPlanChoice::Rw(rw) = &choice { /* use rw */ }

Try / catch

match choice.unwrap_rw() {
    Ok(rw) => run(rw),
    Err(e) if e.to_string().contains("got DataFusion plan") => run_df_fallback(),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling unwrap_rw() on a BatchPlanChoice produced when the DataFusion feature is enabled and the query was planned by DataFusion (BatchPlanChoice::Df variant) instead of the native planner.

Common situations: Developers running with the `datafusion` feature flag enabled whose queries get routed to the DataFusion planner, then pass the plan to code paths (e.g. distributed batch execution) that only understand RisingWave plans.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

Thrown at src/frontend/src/handler/query.rs:65

    BatchPlanFragmenter, DistributedQueryStream, ExecutionContext, ExecutionContextRef,
    LocalQueryExecution, LocalQueryStream,
};
use crate::session::SessionImpl;

/// Choice between running RisingWave's own batch executor (Rw) or a `DataFusion` (DF) logical plan.
pub enum BatchPlanChoice {
    Rw(RwBatchQueryPlanResult),
    #[cfg(feature = "datafusion")]
    Df(DfBatchQueryPlanResult),
}

impl BatchPlanChoice {
    pub fn unwrap_rw(self) -> Result<RwBatchQueryPlanResult> {
        match self {
            BatchPlanChoice::Rw(result) => Ok(result),
            #[cfg(feature = "datafusion")]
            BatchPlanChoice::Df { .. } => {
                risingwave_common::bail!(
                    "Expected RisingWave plan in BatchPlanChoice, but got DataFusion plan"
                )
            }
        }
    }
}

pub async fn handle_query(
    handler_args: HandlerArgs,
    stmt: Statement,
    formats: Vec<Format>,
) -> Result<RwPgResponse> {
    let session = handler_args.session.clone();
    let context = OptimizerContext::from_handler_args(handler_args);

    #[cfg(feature = "datafusion")]
    {
        // We construct a future manually here to make sure this async function is `Send`.

View on GitHub (pinned to 6469eb736d)