databendlabs/databend · error
plan in InsertInputSource::Stag must be CopyIntoTable
Error message
plan in InsertInputSource::Stag must be CopyIntoTable
What it means
During EXPLAIN of an INSERT, format_insert_source expects the plan stored in InsertInputSource::Stag to be a CopyIntoTable plan (it renders the stage child tree); any other plan shape hits `unreachable!("plan in InsertInputSource::Stag must be CopyIntoTable")`. The binder is the only producer of this variant, so the panic indicates the binder attached a non-copy-into plan to a stage input.
Solutions
- Inspect the binder code that constructs InsertInputSource::Stag and confirm it always wraps a CopyIntoTable plan.
- Add the actual plan debug output to the unreachable! message for faster diagnosis.
- If other plan shapes are legitimate, match them explicitly and format accordingly instead of panicking.
- Check recent changes to insert/stage binding; use git blame on the producer of the variant.
Example fix
// before
_ => unreachable!("plan in InsertInputSource::Stag must be CopyIntoTable"),
// after
other => FormatTreeNode::with_children(format!("{plan_name} ({other:?}):"), vec![]).format_pretty(), Defensive patterns
Strategy: try-catch
Try / catch
// Skip EXPLAIN of stage inserts if the version is affected; execute directly
match try_explain(stmt) {
Err(e) if e.contains("Stag must be CopyIntoTable") => exec_insert(stmt),
r => r,
} Prevention
- Only EXPLAIN the INSERT ... FROM @stage statement on versions where the binder is verified.
- Use COPY INTO instead of INSERT-from-stage for stage loading if the EXPLAIN path panics.
- Keep stage-file format options simple to avoid alternate binder paths.
- Report the statement text with the panic to pinpoint the binder change.
When it happens
Trigger: Running EXPLAIN (or EXPLAIN VERBOSE) on an INSERT ... INTO ... FROM @stage statement whose bound stage input plan is not CopyIntoTable — e.g. binder changes produce a different plan wrapper for stage sources.
Common situations: Hit by users explaining data-loading statements (COPY-style INSERT from a named stage or files) after binder/planner refactors; shows up only on the EXPLAIN path, not execution.
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
- internal error: entered unreachable code
- internal error: entered unreachable code
- internal error: entered unreachable code
- internal error: entered unreachable code
- {}
AI-assisted analysis of databendlabs/databend@288d84d76e (2026-09-11).
Data as JSON: /api/errors/b832762d86121bdd.
Report an issue: GitHub.
Appendix: source
Thrown at src/query/sql/src/planner/plans/insert.rs:269
FormatTreeNode::new(format!("from_stage_attachment: {from_stage_attachment}")),
FormatTreeNode::new(format!(
"required_values_schema: [{required_values_schema}]"
)),
FormatTreeNode::new(format!(
"required_source_schema: [{required_source_schema}]"
)),
FormatTreeNode::new(format!("write_mode: {write_mode}")),
FormatTreeNode::new(format!("validation_mode: {validation_mode}")),
FormatTreeNode::new(format!("stage_table_info: {stage_table_info}")),
FormatTreeNode::new(format!("enable_distributed: {enable_distributed}")),
];
children.extend(stage_node);
Ok(
FormatTreeNode::with_children(format!("{plan_name} (stage):"), children)
.format_pretty()?,
)
}
_ => unreachable!("plan in InsertInputSource::Stag must be CopyIntoTable"),
},
}
}
impl std::fmt::Debug for Insert {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("Insert")
.field("catalog", &self.catalog)
.field("database", &self.database)
.field("table", &self.table)
.field("branch", &self.branch)
.field("schema", &self.schema)
.field("overwrite", &self.overwrite)
.finish()
}
}
View on GitHub (pinned to 288d84d76e)