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
- Ensure INSERT statements are executed as batch DML (plain INSERT), not embedded in streaming queries like CREATE MATERIALIZED VIEW.
- 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.
- 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
- Always issue INSERT as a standalone statement.
- Never embed INSERT in CREATE MATERIALIZED VIEW or stream definitions.
- Route DML through batch execution paths in clients/tools.
- Upgrade RisingWave if an INSERT unexpectedly hits the streaming planner.
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
- unreachable
- unimplemented
- Insert error: {:?}, error url: {:?}
- IcebergSinkWriter should be initialized before barrier
- BatchPosixFsReader should not hit this branch. refer to `bat
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/7e896bf9ba49f896.
Report an issue: GitHub.