vitessio/vitess · error
VT09015
VT09015
Error message
VT09015
What it means
VT09015 is raised when the table targeted by a delete-with-input planned DELETE has no primary key defined in its vindex table metadata. Delete-with-input planning builds a `DELETE ... WHERE pk IN (list of values)` statement from the selected rows, which requires a primary key to uniquely identify rows to delete. Without one, the planner cannot construct the delete and aborts.
Source
Thrown at go/vt/vtgate/planbuilder/operators/delete.go:172
}
// getFirstVindex returns the first Vindex, if available
func getFirstVindex(vTbl *vindexes.BaseTable) vindexes.Vindex {
if len(vTbl.ColumnVindexes) > 0 {
return vTbl.ColumnVindexes[0].Vindex
}
return nil
}
func createDeleteOpWithTarget(ctx *plancontext.PlanningContext, target semantics.TableSet, ignore sqlparser.Ignore) dmlOp {
ti, err := ctx.SemTable.TableInfoFor(target)
if err != nil {
panic(vterrors.VT13001(err.Error()))
}
vTbl := ti.GetVindexTable()
if len(vTbl.PrimaryKey) == 0 {
panic(vterrors.VT09015())
}
tblName, err := ti.Name()
if err != nil {
panic(err)
}
leftComp := make(sqlparser.ValTuple, 0, len(vTbl.PrimaryKey))
cols := make([]*sqlparser.ColName, 0, len(vTbl.PrimaryKey))
for _, col := range vTbl.PrimaryKey {
colName := sqlparser.NewColNameWithQualifier(col.String(), tblName)
cols = append(cols, colName)
leftComp = append(leftComp, colName)
ctx.SemTable.Recursive[colName] = target
}
// optimize for case when there is only single column on left hand side.
var lhs sqlparser.Expr = leftComp
if len(leftComp) == 1 {
lhs = leftComp[0]View on GitHub (pinned to 01a25a7d17)
Solutions
- Define a primary key (PRIMARY KEY in DDL and a corresponding primary vindex in the vschema) on the target table.
- Ensure the primary sequence/vindex columns match the table's declared PRIMARY KEY so PrimaryKey is populated.
- Restructure the DELETE to avoid delete-with-input planning (drop LIMIT, split multi-table deletes) if the table legitimately has no PK.
- Review the vschema entry for the table and fix missing column_vindexes configuration.
Example fix
// before (vschema)
{"tables": {"logs": {}}}
// after
{"tables": {"logs": {"column_vindexes": [{"column": "id", "name": "hash"}], "column_list_authoritative": true}}} Defensive patterns
Strategy: validation
Validate before calling
-- run before enabling delete-with-input workloads SELECT table_name, constraint_name FROM information_schema.table_constraints WHERE table_schema = ? AND constraint_type = 'PRIMARY KEY' AND table_name = ?;
Type guard
func tableHasPrimaryKey(vTbl *vindexes.BaseTable) bool {
return vTbl != nil && len(vTbl.PrimaryKey) > 0
} Try / catch
if err := execDelete(sql); err != nil {
if strings.Contains(err.Error(), "VT09015") {
return fmt.Errorf("table needs a primary key for this DELETE: %w", err)
}
return err
} Prevention
- Require PRIMARY KEY on every table created in Vitess
- Keep vschema column_vindexes in sync with DDL primary keys
- Lint vschema in CI for tables lacking a primary vindex
- Avoid LIMIT deletes on tables you cannot guarantee have PKs
When it happens
Trigger: DELETE (with LIMIT, multi-table targets, or FK children) against a table whose vindexes/BaseTable metadata yields an empty PrimaryKey list — e.g. a table defined in the vschema without a primary vindex that also serves as a primary key, on a sharded keyspace.
Common situations: Vschema authoring mistakes: table declared without `column_vindexes` covering a primary key, or a non-MySQL-style table without PRIMARY KEY; deleting from such a table with a LIMIT clause forcing delete-with-input planning.
Related errors
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/7bd26baff5d445d0.
Report an issue: GitHub.