{"record":{"id":"e6db04a051359f47","repo":"risingwavelabs/risingwave","slug":"update-should-always-be-converted-to-batch-plan","errorCode":null,"errorMessage":"update should always be converted to batch plan","messagePattern":"update should always be converted to batch plan","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/frontend/src/optimizer/plan_node/logical_update.rs","lineNumber":128,"sourceCode":"        let core = generic::Update {\n            table_name: self.core.table_name.clone(),\n            table_id: self.core.table_id,\n            table_version_id: self.core.table_version_id,\n            input: new_input,\n            old_exprs: self.core.old_exprs.clone(),\n            new_exprs: self.core.new_exprs.clone(),\n            returning: self.core.returning,\n        };\n        Ok(BatchUpdate::new(core, self.schema().clone()).into())\n    }\n}\n\nimpl ToStream for LogicalUpdate {\n    fn to_stream(\n        &self,\n        _ctx: &mut ToStreamContext,\n    ) -> Result<crate::optimizer::plan_node::StreamPlanRef> {\n        unreachable!(\"update should always be converted to batch plan\");\n    }\n\n    fn logical_rewrite_for_stream(\n        &self,\n        _ctx: &mut RewriteStreamContext,\n    ) -> Result<(PlanRef, ColIndexMapping)> {\n        unreachable!(\"update should always be converted to batch plan\");\n    }\n}\n","sourceCodeStart":110,"sourceCodeEnd":138,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/frontend/src/optimizer/plan_node/logical_update.rs#L110-L138","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nCREATE MATERIALIZED VIEW mv AS UPDATE t SET a = 1 WHERE id = 2; -- invalid: DML in streaming plan\n// after\nUPDATE t SET a = 1 WHERE id = 2; -- executed as batch DML\n","handlingStrategy":"validation","validationCode":"// Route DML to batch execution; never embed UPDATE in streaming DDL.\nlet stmt = parse(sql);\nif matches!(stmt, Statement::Update { .. }) && ctx.is_streaming {\n    return Err(\"UPDATE must run as batch DML, not in a streaming plan\");\n}","typeGuard":"fn is_batch_only(stmt: &Statement) -> bool {\n    matches!(stmt, Statement::Update { .. } | Statement::Insert { .. } | Statement::Delete { .. })\n}","tryCatchPattern":"if sql.starts_with(\"UPDATE\") {\n    execute_batch(sql) // never send through stream/explain-stream paths\n} else {\n    execute_streaming(sql)\n}","preventionTips":["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."],"tags":["rust","optimizer","unreachable","update","dml","streaming"],"backgroundTag":"internal-invariant-violation","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}