{"record":{"id":"a4ce92abd7f6acc4","repo":"risingwavelabs/risingwave","slug":"batch-refresh-materialized-views-must-not-depend-o","errorCode":null,"errorMessage":"batch refresh materialized views must not depend on sources directly; fragment {} has source/source-backfill nodes","messagePattern":"batch refresh materialized views must not depend on sources directly; fragment (.+?) has source/source-backfill nodes","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/meta/src/stream/stream_manager.rs","lineNumber":685,"sourceCode":"        };\n\n        let job_type = if let Some(refresh_interval_sec) = refresh_interval_sec {\n            if since_timestamp_epoch.is_some() {\n                bail!(\"since_timestamp should not be specified when no snapshot backfill\");\n            }\n            let snapshot_backfill_info = snapshot_backfill_info.ok_or_else(|| {\n                anyhow::anyhow!(\n                    \"batch refresh materialized view must have snapshot backfill upstream\"\n                )\n            })?;\n            // Batch refresh jobs must not contain source or source-backfill nodes,\n            // because we skip split assignment resolution for them.\n            for fragment in info.stream_job_fragments.inner.fragments.values() {\n                let mask = fragment.fragment_type_mask;\n                if mask.contains(FragmentTypeFlag::Source)\n                    || mask.contains(FragmentTypeFlag::SourceScan)\n                {\n                    bail!(\n                        \"batch refresh materialized views must not depend on sources directly; \\\n                         fragment {} has source/source-backfill nodes\",\n                        fragment.fragment_id\n                    );\n                }\n            }\n            tracing::debug!(\n                ?snapshot_backfill_info,\n                refresh_interval_sec,\n                \"sending Command::CreateBatchRefreshStreamingJob\"\n            );\n            CreateStreamingJobType::BatchRefresh(BatchRefreshInfo {\n                snapshot_backfill_info,\n                refresh_interval_sec,\n            })\n        } else if let Some(snapshot_backfill_info) = snapshot_backfill_info {\n            tracing::debug!(\n                ?snapshot_backfill_info,","sourceCodeStart":667,"sourceCodeEnd":703,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/meta/src/stream/stream_manager.rs#L667-L703","documentation":"Batch refresh materialized views must not read from sources directly. The meta node scans each fragment's FragmentTypeFlag and, if any fragment contains Source or SourceScan nodes, it bails, because split assignment resolution is skipped for batch refresh jobs and direct source reads would be left unresolved.","triggerScenarios":"Creating a batch refresh MV (refresh_interval_sec set, snapshot_backfill_info present) whose plan contains a fragment with fragment_type_mask containing FragmentTypeFlag::Source or SourceScan — i.e. the MV reads a source (or a source-backfill node) directly in its dependency chain.","commonSituations":"`CREATE MATERIALIZED VIEW ... WITH (refresh=...) AS SELECT ... FROM <source>`; building a refresh MV on top of a chain of MVs where one still reads a source directly; users migrating pipelines that previously used regular streaming MVs on sources.","solutions":["Insert an intermediate regular (non-batch-refresh) MV between the source and the batch refresh MV, then build the refresh MV on top of that MV.","Replace the direct source read with a table/MV as the upstream of the batch refresh job.","If reading the source directly is required, use a normal streaming MV instead of batch refresh."],"exampleFix":"// before\nCREATE MATERIALIZED VIEW mv WITH (refresh = '10 minutes') AS SELECT * FROM src;\n// after\nCREATE MATERIALIZED VIEW base_mv AS SELECT * FROM src;\nCREATE MATERIALIZED VIEW mv WITH (refresh = '10 minutes') AS SELECT * FROM base_mv;","handlingStrategy":"validation","validationCode":"-- ensure the batch refresh MV does not read a source directly\n-- build on MVs/tables only:\nCREATE MATERIALIZED VIEW base_mv AS SELECT * FROM src;\nCREATE MATERIALIZED VIEW mv WITH (refresh = '10 minutes') AS SELECT * FROM base_mv;","typeGuard":null,"tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"must not depend on sources directly\") =>\n        return Err(\"insert an intermediate MV between the source and the batch refresh MV\".into()),\n    other => other,\n}","preventionTips":["Never put a source directly in the FROM chain of a batch refresh MV.","Keep an architecture rule: sources -> regular MVs -> batch refresh MVs.","Review fragment type masks in EXPLAIN/fragment graphs when designing refresh pipelines."],"tags":["sql","validation","fragment","source","materialized-view"],"backgroundTag":"incompatible-source-type","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"}