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

  1. 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)
  2. 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
  3. Avoid ALTER TABLE on system/history tables; file a bug with the exact ALTER TABLE statement and Databend version
  4. 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

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


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 version

View on GitHub (pinned to 288d84d76e)