databendlabs/databend · error
logic error: expected CreateTable plan
Error message
logic error: expected CreateTable plan
What it means
This panic fires in get_schemas (src/query/service/src/history_tables/alter_table.rs:53) after planning a rewritten CREATE TABLE statement. The code assumes plan_sql on a CREATE TABLE statement always yields Plan::CreateTable; any other Plan variant hits the unreachable!("logic error: expected CreateTable plan") branch. It is an internal invariant violation, meaning the SQL string passed to the planner was not a CREATE TABLE statement as expected.
Solutions
- Upgrade to a release where the ALTER TABLE schema-diff code plans a verified CREATE TABLE statement (or where the plan match handles more variants)
- Inspect the generated new_create_sql (enable query logging) and confirm the table's CREATE statement is well-formed; recreate the table if its metadata is corrupted
- Avoid ALTER TABLE on system/history tables; file a bug with the exact ALTER TABLE statement and Databend version
- As a code fix, replace unreachable! with a returned ErrorCode::Internal error describing the unexpected plan variant
Example fix
// before
let new_table_schema = match create_plan {
Plan::CreateTable(plan) => plan.schema,
_ => unreachable!("logic error: expected CreateTable plan"),
};
// after
let new_table_schema = match create_plan {
Plan::CreateTable(plan) => plan.schema,
other => {
return Err(ErrorCode::Internal(format!(
"expected CreateTable plan, got {:?}", other
)));
}
}; Defensive patterns
Strategy: validation
Validate before calling
// ensure the target is a user table before ALTER TABLE
let table = ctx.get_table(catalog, database, table_name).await?;
if table.engine().is_empty() {
return Err("cannot determine table engine; ALTER may be unsupported".into());
} Type guard
if let Plan::CreateTable(plan) = &create_plan { /* use plan.schema */ } else { /* handle error */ } Try / catch
match planner.plan_sql(sql).await {
Ok((Plan::CreateTable(p), _)) => p.schema,
Ok((other, _)) => return Err(ErrorCode::Internal(format!("unexpected plan {other:?}"))),
Err(e) => return Err(e),
} Prevention
- Do not ALTER TABLE system/history tables; use supported management commands
- Pin Databend versions and check ALTER TABLE release notes before upgrading
- Report panics with the exact statement so maintainers can convert unreachable! into proper errors
When it happens
Trigger: get_alter_table_sql reconstructs a `CREATE TABLE` statement from the table's create option and plans it via Planner::plan_sql; the panic occurs if the planned result is not Plan::CreateTable — e.g. the generated SQL was mutated into another statement kind, or the planner binding changed and returned a different plan variant.
Common situations: Running ALTER TABLE on history/system tables after an internal refactor of plan_sql output; users hit it as an abrupt query-node panic ('logic error: expected CreateTable plan') when altering a table whose generated create SQL no longer plans as CreateTable, typically after a version upgrade or with unusual table options.
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
- table name is provided
- Input plan must be Query, but it's
- Input plan must be Query, but it's
- 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/ffcf4f180a14437a.
Report an issue: GitHub.
Appendix: source
Thrown at src/query/service/src/history_tables/alter_table.rs:53
pub async fn get_schemas(
ctx: Arc<QueryContext>,
new_create_sql: &str,
table_name: &str,
) -> Result<(TableSchemaRef, TableSchemaRef)> {
let old_table_schema = ThreadTracker::tracking_future(ctx.get_table(
CATALOG_DEFAULT,
"system_history",
table_name,
))
.await?
.schema();
let mut planner = Planner::new(ctx.clone());
let (create_plan, _) = ThreadTracker::tracking_future(planner.plan_sql(new_create_sql)).await?;
let new_table_schema = match create_plan {
Plan::CreateTable(plan) => plan.schema,
_ => {
unreachable!("logic error: expected CreateTable plan")
}
};
Ok((old_table_schema, new_table_schema))
}
pub async fn get_alter_table_sql(
ctx: Arc<QueryContext>,
new_create_sql: &str,
table_name: &str,
) -> Result<Vec<String>> {
let mut tracking_payload = ThreadTracker::new_tracking_payload();
tracking_payload.capture_log_settings = Some(CaptureLogSettings::capture_off());
let (old_table_schema, new_table_schema) = tracking_payload
.tracking(get_schemas(ctx, new_create_sql, table_name))
.await?;
// The table schema change follow "open-closed principle", only accept adding new fields.
// If the new table schema has less or equal fields than the old one, means older versionView on GitHub (pinned to 288d84d76e)