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
- Disable the `datafusion` feature so queries are always planned natively by RisingWave.
- Check the variant with matches!(choice, BatchPlanChoice::Rw(..)) before calling unwrap_rw().
- Route DataFusion plans to a DataFusion-capable executor instead of the RisingWave batch path.
- 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
- Gate unwrap_rw() calls behind non-datafusion builds or assert the variant first.
- Centralize plan dispatch in one place instead of scattering unwrap_rw calls.
- Run CI with the datafusion feature enabled to catch mixed-engine paths early.
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
- {0}
- {0}
- Actor exited unexpectedly
- aggregate function is not supported
- argument `--license-key-path` (or env var…
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)