{"record":{"id":"8409bedf507d40b1","repo":"vitessio/vitess","slug":"vt09002-8409be","errorCode":"VT09002","errorMessage":"%s statement with a replica target","messagePattern":"(.+?) statement with a replica target","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/vt/vtgate/planbuilder/operators/route_planning.go","lineNumber":124,"sourceCode":"\ttblName, ok := table.Alias.Expr.(sqlparser.TableName)\n\tif !ok {\n\t\tpanic(vterrors.VT12001(\"multi shard UPDATE with LIMIT\"))\n\t}\n\n\t_, _, _, typ, dest, err := ctx.VSchema.FindTableOrVindex(tblName)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif dest == nil {\n\t\trouting := &ShardedRouting{\n\t\t\tkeyspace:    vindexTable.Keyspace,\n\t\t\tRouteOpCode: engine.Scatter,\n\t\t}\n\t\treturn vindexTable, routing\n\t}\n\n\tif typ != topodatapb.TabletType_PRIMARY {\n\t\tpanic(vterrors.VT09002(dmlType))\n\t}\n\n\t// we are dealing with an explicitly targeted DML\n\trouting := &TargetedRouting{\n\t\tkeyspace:          vindexTable.Keyspace,\n\t\tTargetDestination: dest,\n\t}\n\treturn vindexTable, routing\n}\n\n/*\n\t\tThe greedy planner will plan a query by finding first finding the best route plan for every table.\n\t    Then, iteratively, it finds the cheapest join that can be produced between the remaining plans,\n\t\tand removes the two inputs to this cheapest plan and instead adds the join.\n\t\tAs an optimization, it first only considers joining tables that have predicates defined between them\n*/\nfunc greedySolve(ctx *plancontext.PlanningContext, qg *QueryGraph) Operator {\n\trouteOps := seedOperatorList(ctx, qg)","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/vtgate/planbuilder/operators/route_planning.go#L106-L142","documentation":"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.","triggerScenarios":"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.","commonSituations":"Session pinned to a replica target for reads, then reused for DML; routing misconfiguration sending writes to non-primary tablets.","solutions":["Run the DML with a primary target (`USE ks@primary`)","Remove the target qualifier so the DML routes to primary by default","Fix connection-pool/session target hygiene so writes never land on replica targets"],"exampleFix":"// before\nUSE ks@replica;\nUPDATE t SET a = 1 WHERE id = 5; -- VT09002: update statement with a replica target\n// after\nUSE ks@primary;\nUPDATE t SET a = 1 WHERE id = 5;","handlingStrategy":"validation","validationCode":"if !strings.HasSuffix(currentTarget, \"@primary\") {\n    conn.Execute(\"USE ks@primary\", nil)\n}\nconn.Execute(\"UPDATE t SET a=1 WHERE id=5\", nil)","typeGuard":null,"tryCatchPattern":"_, err := conn.Execute(\"UPDATE t SET a=1 WHERE id=5\", nil)\nif err != nil && strings.Contains(err.Error(), \"VT09002\") {\n    conn.Execute(\"USE ks@primary\", nil)\n    // retry the DML\n}","preventionTips":["Separate read and write connection pools with fixed targets","Reset targets before DML in shared sessions","Monitor for VT09002 as a signal of write-to-replica misrouting"],"tags":["vtgate","dml","routing","unsupported"],"backgroundTag":"dml-on-replica-target","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}