vitessio/vitess · error · ErrUnpreparedQuery

attempted to execute unprepared query

Error message

attempted to execute unprepared query

What it means

ErrUnpreparedQuery is returned when a QueryPlan is executed without first being prepared/bound. The VExec execution path (Execute, ExecuteScatter) expects a plan that went through preparation; executing a nil or unbound plan is a programming error and surfaces as this sentinel.

Source

Thrown at go/vt/vtctl/workflow/vexec/query_planner.go:46

var ( // Query planning errors.
	// ErrCannotUpdateImmutableColumn is returned when attempting to plan a
	// query that updates a column that should be treated as immutable.
	ErrCannotUpdateImmutableColumn = errors.New("cannot update immutable column")
	// ErrUnsupportedQueryConstruct is returned when a particular query
	// construct is unsupported by a QueryPlanner, despite the more general kind
	// of query being supported.
	//
	// For example, VReplication supports DELETEs, but does not support DELETEs
	// with LIMIT clauses, so planning a "DELETE ... LIMIT" will return
	// ErrUnsupportedQueryConstruct rather than a "CREATE TABLE", which would
	// return an ErrUnsupportedQuery.
	ErrUnsupportedQueryConstruct = errors.New("unsupported query construct")
)

// Query execution errors.
// ErrUnpreparedQuery is returned when attempting to execute an unprepared
// QueryPlan.
var ErrUnpreparedQuery = errors.New("attempted to execute unprepared query")

// QueryPlanner defines the interface that VExec uses to build QueryPlans for
// various vexec workflows. A given vexec table, which is to say a table in the
// "_vt" database, will have at most one QueryPlanner implementation, which is
// responsible for defining both what queries are supported for that table, as
// well as how to build plans for those queries.
//
// VReplicationQueryPlanner is a good example implementation to refer to.
type QueryPlanner interface {
	// (NOTE:@ajm188) I don't think this method fits on the query planner. To
	// me, especially given that it's only implemented by the vrep query planner
	// in the old implementation (the schema migration query planner no-ops this
	// method), this fits better on our workflow.Manager struct, probably as a
	// method called something like "VReplicationExec(ctx, query, Options{DryRun: true})"
	// DryRun(ctx context.Context) error

	// PlanQuery constructs and returns a QueryPlan for a given statement. The
	// resulting QueryPlan is suitable for repeated, concurrent use.

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Check and handle the error from the planning step before calling Execute/ExecuteScatter
  2. Ensure the QueryPlan was fully prepared (non-nil bound query/fields) before execution
  3. Fix the planner implementation so it either returns a valid plan or an explicit unsupported-query error

Example fix

// before
plan, _ := planner.PlanDelete(stmt)
qp.Execute(ctx, plan) // ErrUnpreparedQuery
// after
plan, err := planner.PlanDelete(stmt)
if err != nil {
    return err
}
return qp.Execute(ctx, plan)
Defensive patterns

Strategy: validation

Validate before calling

if plan == nil || plan.BoundQuery == nil {
    return ErrUnpreparedQuery
}

Type guard

func isPrepared(p *QueryPlan) bool {
    return p != nil && p.BoundQuery != nil && p.BoundQuery.Query != ""
}

Try / catch

plan, err := planner.PlanUpdate(stmt)
if err != nil { return err }
if !isPrepared(plan) { return ErrUnpreparedQuery }
return qp.Execute(ctx, plan)

Prevention

When it happens

Trigger: Calling QueryPlan.Execute or ExecuteScatter before the plan was prepared (e.g. planner returned an error that was ignored, or a plan struct with a nil bound query is passed in).

Common situations: Bugs in new QueryPlanner implementations, ignoring the error from planDelete/planUpdate and proceeding to execute, or race conditions clearing prepared plans before execution.

Related errors


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/9ae8a7741e04cb5e. Report an issue: GitHub.