vitessio/vitess · error

VT09002

VT09002

Error message

%s statement with a replica target

What it means

VT09002 signals an unsupported feature. For an explicitly targeted DML (UPDATE/DELETE resolved via FindTableOrVindex), the destination tablet type must be PRIMARY; a replica/rdonly target makes the statement unsupported.

Source

Thrown at go/vt/vtgate/planbuilder/operators/route_planning.go:124

	tblName, ok := table.Alias.Expr.(sqlparser.TableName)
	if !ok {
		panic(vterrors.VT12001("multi shard UPDATE with LIMIT"))
	}

	_, _, _, typ, dest, err := ctx.VSchema.FindTableOrVindex(tblName)
	if err != nil {
		panic(err)
	}
	if dest == nil {
		routing := &ShardedRouting{
			keyspace:    vindexTable.Keyspace,
			RouteOpCode: engine.Scatter,
		}
		return vindexTable, routing
	}

	if typ != topodatapb.TabletType_PRIMARY {
		panic(vterrors.VT09002(dmlType))
	}

	// we are dealing with an explicitly targeted DML
	routing := &TargetedRouting{
		keyspace:          vindexTable.Keyspace,
		TargetDestination: dest,
	}
	return vindexTable, routing
}

/*
		The greedy planner will plan a query by finding first finding the best route plan for every table.
	    Then, iteratively, it finds the cheapest join that can be produced between the remaining plans,
		and removes the two inputs to this cheapest plan and instead adds the join.
		As an optimization, it first only considers joining tables that have predicates defined between them
*/
func greedySolve(ctx *plancontext.PlanningContext, qg *QueryGraph) Operator {
	routeOps := seedOperatorList(ctx, qg)

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Run the DML with a primary target (`USE ks@primary`)
  2. Remove the target qualifier so the DML routes to primary by default
  3. Fix connection-pool/session target hygiene so writes never land on replica targets

Example fix

// before
USE ks@replica;
UPDATE t SET a = 1 WHERE id = 5; -- VT09002: update statement with a replica target
// after
USE ks@primary;
UPDATE t SET a = 1 WHERE id = 5;
Defensive patterns

Strategy: validation

Validate before calling

if !strings.HasSuffix(currentTarget, "@primary") {
    conn.Execute("USE ks@primary", nil)
}
conn.Execute("UPDATE t SET a=1 WHERE id=5", nil)

Try / catch

_, err := conn.Execute("UPDATE t SET a=1 WHERE id=5", nil)
if err != nil && strings.Contains(err.Error(), "VT09002") {
    conn.Execute("USE ks@primary", nil)
    // retry the DML
}

Prevention

When it happens

Trigger: An UPDATE or DELETE with an explicit destination (e.g. `UPDATE ks.t ...` with session target `@replica` or a shard target resolving to a non-primary tablet) reaches the `typ != TabletType_PRIMARY` check in buildVindexTableForDML.

Common situations: Session pinned to a replica target for reads, then reused for DML; routing misconfiguration sending writes to non-primary tablets.

Related errors


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