risingwavelabs/risingwave · error
update should always be converted to batch plan
Error message
update should always be converted to batch plan
What it means
`LogicalUpdate::to_stream` unconditionally panics with `unreachable!()`. DML statements like UPDATE are never converted into streaming plans: they are executed by the batch engine (DML is lowered to a batch pipeline against the table), so the ToStream path for LogicalUpdate is intentionally unreachable. Reaching it means the optimizer attempted to stream a DML statement.
Source
Thrown at src/frontend/src/optimizer/plan_node/logical_update.rs:128
let core = generic::Update {
table_name: self.core.table_name.clone(),
table_id: self.core.table_id,
table_version_id: self.core.table_version_id,
input: new_input,
old_exprs: self.core.old_exprs.clone(),
new_exprs: self.core.new_exprs.clone(),
returning: self.core.returning,
};
Ok(BatchUpdate::new(core, self.schema().clone()).into())
}
}
impl ToStream for LogicalUpdate {
fn to_stream(
&self,
_ctx: &mut ToStreamContext,
) -> Result<crate::optimizer::plan_node::StreamPlanRef> {
unreachable!("update should always be converted to batch plan");
}
fn logical_rewrite_for_stream(
&self,
_ctx: &mut RewriteStreamContext,
) -> Result<(PlanRef, ColIndexMapping)> {
unreachable!("update should always be converted to batch plan");
}
}
View on GitHub (pinned to 6469eb736d)
Solutions
- Execute the UPDATE through the normal SQL execution path (batch DML), not via streaming query/`CREATE MATERIALIZED VIEW`.
- If you need incremental maintenance of derived data, create a materialized view over the table and let UPDATE go to the base table separately.
- Check your SQL routing/explain tooling: use `EXPLAIN` (batch) rather than stream explain for DML statements.
- If stock RisingWave triggers it, file a bug with the statement and frontend version.
Example fix
// before CREATE MATERIALIZED VIEW mv AS UPDATE t SET a = 1 WHERE id = 2; -- invalid: DML in streaming plan // after UPDATE t SET a = 1 WHERE id = 2; -- executed as batch DML
Defensive patterns
Strategy: validation
Validate before calling
// Route DML to batch execution; never embed UPDATE in streaming DDL.
let stmt = parse(sql);
if matches!(stmt, Statement::Update { .. }) && ctx.is_streaming {
return Err("UPDATE must run as batch DML, not in a streaming plan");
} Type guard
fn is_batch_only(stmt: &Statement) -> bool {
matches!(stmt, Statement::Update { .. } | Statement::Insert { .. } | Statement::Delete { .. })
} Try / catch
if sql.starts_with("UPDATE") {
execute_batch(sql) // never send through stream/explain-stream paths
} else {
execute_streaming(sql)
} Prevention
- Never include UPDATE/INSERT/DELETE inside CREATE MATERIALIZED VIEW or streaming queries.
- Send DML statements only through the batch execution path.
- Use batch EXPLAIN for DML; streaming EXPLAIN is for queries only.
- Validate statement type before choosing the optimizer entry point in tooling.
When it happens
Trigger: An UPDATE statement routed through the streaming optimizer — e.g. an UPDATE used in a context the frontend treats as a streaming query (such as inside a `CREATE MATERIALIZED VIEW` or a streaming-explain path) instead of the batch DML executor path.
Common situations: Seen by users running DML through streaming-specific entry points or by contributors wiring new statement types into the wrong optimizer branch; normal `UPDATE ...` statements go to batch and never hit this.
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
- LogicalIcebergIntermediateScan is only for batch queries
- internal error: entered unreachable code
- shared node should be handled specially in PlanRef::clone_wi
- TableFunction should be converted to ProjectSet
- should always have a stream key in the stream plan but not,
AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11).
Data as JSON: /api/errors/e6db04a051359f47.
Report an issue: GitHub.