vitessio/vitess · error

ErrUnpreparedQuery

ErrUnpreparedQuery

Error message

%w: call PlanQuery on a query planner first

What it means

FixedQueryPlan.Execute runs a pre-planned query on a tablet. If PlanQuery was never called, ParsedQuery is nil and Execute returns the sentinel ErrUnpreparedQuery wrapped via %w (so errors.Is works). It is an API-misuse guard: the query planner must produce a ParsedQuery before results can be executed against a target tablet.

Source

Thrown at go/vt/vtctl/workflow/vexec/query_plan.go:57

	Execute(ctx context.Context, target *topo.TabletInfo) (*querypb.QueryResult, error)
	// ExecuteScatter executes the planned query on the specified targets concurrently,
	// returning a mapping of the target tablet to a querypb.QueryResult.
	ExecuteScatter(ctx context.Context, targets ...*topo.TabletInfo) (map[*topo.TabletInfo]*querypb.QueryResult, error)
}

// FixedQueryPlan wraps a planned query produced by a QueryPlanner. It executes
// the same query with the same bind vals, regardless of the target.
type FixedQueryPlan struct {
	ParsedQuery *sqlparser.ParsedQuery

	workflow string
	tmc      tmclient.TabletManagerClient
}

// Execute is part of the QueryPlan interface.
func (qp *FixedQueryPlan) Execute(ctx context.Context, target *topo.TabletInfo) (qr *querypb.QueryResult, err error) {
	if qp.ParsedQuery == nil {
		return nil, fmt.Errorf("%w: call PlanQuery on a query planner first", ErrUnpreparedQuery)
	}

	targetAliasStr := target.AliasString()

	defer func() {
		if err != nil {
			log.Warn(fmt.Sprintf("Result on %v: %v", targetAliasStr, err))
			return
		}
	}()

	qr, err = qp.tmc.VReplicationExec(ctx, target.Tablet, qp.ParsedQuery.Query)
	if err != nil {
		return nil, err
	}
	return qr, nil
}

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Always call PlanQuery (via the vexec query planner) before Execute on the same plan.
  2. Check the error from PlanQuery and abort before calling Execute.
  3. If constructing vexec plans programmatically, set ParsedQuery explicitly or use the planner API rather than the struct literal.
  4. In code, match errors.Is(err, vexec.ErrUnpreparedQuery) to detect the missing-plan condition cleanly.

Example fix

// before
qp.Execute(ctx, target) // ParsedQuery nil
// after
if err := qp.PlanQuery(ctx); err != nil { return err }
qr, err := qp.Execute(ctx, target)
Defensive patterns

Strategy: try-catch

Validate before calling

if qp.ParsedQuery == nil {
	return fmt.Errorf("cannot execute: query plan not prepared; call PlanQuery first")
}

Type guard

func isPrepared(qp *vexec.FixedQueryPlan) bool { return qp != nil && qp.ParsedQuery != nil }

Try / catch

qr, err := qp.Execute(ctx, target)
if err != nil {
	if errors.Is(err, vexec.ErrUnpreparedQuery) {
		if perr := qp.PlanQuery(ctx); perr != nil { return perr }
		qr, err = qp.Execute(ctx, target)
	}
	if err != nil { return err }
}

Prevention

When it happens

Trigger: Calling Execute on a FixedQueryPlan returned by a planner whose PlanQuery method was never invoked, or after PlanQuery failed silently / stored nothing into ParsedQuery (e.g. nil query result path in vexec workflows).

Common situations: Custom tooling constructs a FixedQueryPlan directly instead of going through the planner; error handling skips a failed PlanQuery and proceeds to Execute; refactor changes call order so Execute runs before PlanQuery.

Related errors


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