risingwavelabs/risingwave · error

expect PbNodeBody::Project but got: {:?}

Error message

expect PbNodeBody::Project but got: {:?}

What it means

When rewriting the Project node inserted for auto schema change (`rewrite_project_node`), the code requires the node's body to be `PbNodeBody::Project` so its expressions can be adjusted for added/removed columns. Any other variant returns this error, failing the schema refresh rewrite.

Source

Thrown at src/meta/src/stream/stream_graph/fragment.rs:685

    merge.upstream_fragment_id = upstream_table_fragment_id;

    Ok(ScanRewriteResult {
        old_output_index_to_new_output_index,
        new_output_index_by_column_id,
        output_fields: stream_scan_node.fields.clone(),
    })
}

/// Rewrite Project node input refs and extend with newly added columns.
fn rewrite_project_node(
    project_node: &mut StreamNode,
    scan_rewrite: &ScanRewriteResult,
    newly_added_columns: &[ColumnCatalog],
    removed_column_ids: &HashSet<ColumnId>,
    upstream_table_name: &str,
) -> MetaResult<()> {
    let PbNodeBody::Project(project_node_body) = project_node.node_body.as_mut().unwrap() else {
        return Err(anyhow!(
            "expect PbNodeBody::Project but got: {:?}",
            project_node.node_body
        )
        .into());
    };
    let has_non_input_ref = project_node_body
        .select_list
        .iter()
        .any(|expr| !matches!(expr.rex_node, Some(expr_node::RexNode::InputRef(_))));
    if has_non_input_ref && !removed_column_ids.is_empty() {
        return Err(anyhow!(
            "auto schema change with drop column only supports Project with InputRef"
        )
        .into());
    }

    let mut new_select_list = Vec::with_capacity(project_node_body.select_list.len());
    let mut new_project_fields = Vec::with_capacity(project_node.fields.len());

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Restart meta and retry the schema-changing operation to rebuild consistent graph state.
  2. Verify all RisingWave components are on the same version.
  3. Drop and recreate the sink to regenerate a canonical graph with a proper Project node.
  4. File a RisingWave bug with fragment graph dumps — the check pass should have matched this node as Project.

Example fix

// before
let PbNodeBody::Project(project_node_body) = project_node.node_body.as_mut().unwrap() else { ... };
// after: skip projection rewrite when shape is unexpected
let Some(PbNodeBody::Project(project_node_body)) = project_node.node_body.as_mut() else {
    tracing::warn!("expected Project node for schema refresh, got {:?}; skipping", project_node.node_body);
    return Ok(());
};
Defensive patterns

Strategy: type-guard

Validate before calling

if !matches!(project_node.node_body.as_ref(), Some(PbNodeBody::Project(_))) {
    return Err("schema refresh expects a Project node here".into());
}

Type guard

fn as_project_mut(node: &mut StreamNode) -> Option<&mut PbProjectNode> {
    match node.node_body.as_mut() {
        Some(PbNodeBody::Project(p)) => Some(p),
        _ => None,
    }
}

Try / catch

match rewrite_project_node(&mut project_node, ...).await {
    Ok(()) => {},
    Err(e) if e.to_string().contains("expect PbNodeBody::Project") => {
        // invariant breach: rebuild the sink graph
        rebuild_sink_graph()?;
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: `rewrite_refresh_schema_sink_fragment` -> `rewrite_project_node` called with a node whose `node_body` is not `PbNodeBody::Project` (e.g. it was resolved via a Project branch but the body is actually something else).

Common situations: Upstream table schema change (ALTER TABLE ADD/DROP COLUMN, schema registry evolution) triggering a rewrite against inconsistent fragment metadata; frontend/meta version drift; internal invariant breach between the check and rewrite passes.

Related errors


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