risingwavelabs/risingwave · error

insert should always be converted to batch plan

Error message

insert should always be converted to batch plan

What it means

LogicalInsert represents SQL INSERT into a table. In RisingWave, inserts are always planned as batch DML jobs, never converted to streaming operators, so to_stream panics via unreachable!() with this message. Reaching it means an INSERT node survived into the stream conversion phase, violating the planner's contract.

Source

Thrown at src/frontend/src/optimizer/plan_node/logical_insert.rs:169

    ) -> PlanRef {
        gen_filter_and_pushdown(self, predicate, Condition::true_cond(), ctx)
    }
}

impl ToBatch for LogicalInsert {
    fn to_batch(&self) -> Result<crate::optimizer::plan_node::BatchPlanRef> {
        let new_input = self.input().to_batch()?;
        let core = self.core.clone_with_input(new_input);
        Ok(BatchInsert::new(core).into())
    }
}

impl ToStream for LogicalInsert {
    fn to_stream(
        &self,
        _ctx: &mut ToStreamContext,
    ) -> Result<crate::optimizer::plan_node::StreamPlanRef> {
        unreachable!("insert should always be converted to batch plan");
    }

    fn logical_rewrite_for_stream(
        &self,
        _ctx: &mut RewriteStreamContext,
    ) -> Result<(PlanRef, ColIndexMapping)> {
        unreachable!("insert should always be converted to batch plan");
    }
}

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Ensure INSERT statements are executed as batch DML (plain INSERT), not embedded in streaming queries like CREATE MATERIALIZED VIEW.
  2. Check statement routing: the frontend should convert INSERT to a batch plan before any to_stream pass; upgrade RisingWave if this is a known regression.
  3. As a developer, replace unreachable!() with an explanatory bail! for better diagnostics.

Example fix

// before
fn to_stream(&self, _ctx: &mut ToStreamContext) -> Result<StreamPlanRef> {
    unreachable!("insert should always be converted to batch plan");
}
// after
fn to_stream(&self, _ctx: &mut ToStreamContext) -> Result<StreamPlanRef> {
    bail!("insert should always be converted to batch plan");
}
Defensive patterns

Strategy: validation

Validate before calling

// Reject INSERT inside streaming DDL before submitting
let upper = sql.to_uppercase();
if upper.trim_start().starts_with("INSERT") && upper.contains("CREATE MATERIALIZED VIEW") {
    return Err("INSERT must be a standalone batch DML statement".into());
}

Type guard

fn is_batch_dml(stmt: &Statement) -> bool {
    matches!(stmt, Statement::Insert { .. })
}

Try / catch

match client.run_sql(stmt) {
    Err(e) if format!("{e}").contains("converted to batch plan") => {
        client.run_sql(&strip_streaming_wrapper(stmt))?;
    }
    r => r?,
}

Prevention

When it happens

Trigger: to_stream is invoked on a LogicalInsert node — e.g. an INSERT statement mis-routed into the streaming plan path (such as an INSERT inside a materialized-view/streaming definition or an optimizer bug in statement routing).

Common situations: Attempting to use INSERT within streaming DDL; frontend regressions in statement-kind routing (batch DML vs streaming); tooling that rewrites SQL into streaming plans containing inserts.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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