vitessio/vitess · error · VitessError

VT12001

VT12001

Error message

VT12001: unsupported: REPLACE INTO using select statement

What it means

VT12001: REPLACE INTO that takes its rows from a SELECT statement is not supported when the table requires delete-before-insert handling. createOperatorFromInsert must build a plan that deletes existing rows before inserting, and that construction only works with literal VALUES rows; a select-statement source is rejected.

Source

Thrown at go/vt/vtgate/planbuilder/operators/insert.go:134

	deleteBeforeInsert := false
	if ins.Action == sqlparser.ReplaceAct &&
		(ctx.SemTable.ForeignKeysPresent() || vTbl.Keyspace.Sharded) &&
		(len(vTbl.PrimaryKey) > 0 || len(vTbl.UniqueKeys) > 0) {
		// this needs a delete before insert as there can be row clash which needs to be deleted first.
		ins.Action = sqlparser.InsertAct
		deleteBeforeInsert = true
	}

	insOp := checkAndCreateInsertOperator(ctx, ins, vTbl, routing)

	if !deleteBeforeInsert {
		return insOp
	}

	rows, isRows := ins.Rows.(sqlparser.Values)
	if !isRows {
		panic(vterrors.VT12001("REPLACE INTO using select statement"))
	}

	pkCompExpr := pkCompExpression(vTbl, ins, rows)
	uniqKeyCompExprs := uniqKeyCompExpressions(vTbl, ins, rows)
	whereExpr := getWhereCondExpr(append(uniqKeyCompExprs, pkCompExpr))

	delStmt := &sqlparser.Delete{
		Comments:   ins.Comments,
		TableExprs: sqlparser.TableExprs{sqlparser.Clone(ins.Table)},
		Where:      sqlparser.NewWhere(sqlparser.WhereClause, whereExpr),
	}
	delOp := createOpFromStmt(ctx, delStmt, false, "")
	return &Sequential{Sources: []Operator{delOp, insOp}}
}

func checkAndCreateInsertOperator(ctx *plancontext.PlanningContext, ins *sqlparser.Insert, vTbl *vindexes.BaseTable, routing Routing) Operator {
	insOp := createInsertOperator(ctx, ins, vTbl, routing)

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Rewrite as two statements: INSERT INTO ... SELECT, preceded by explicit DELETEs for conflicting keys
  2. Load the SELECT results into the application and issue REPLACE INTO with literal VALUES
  3. Use INSERT ... ON DUPLICATE KEY UPDATE instead of REPLACE if rows should update rather than delete+insert
  4. If the table does not need delete-before-insert behavior, verify table/keyspace configuration (this path is reached only when deleteBeforeInsert is set)

Example fix

-- before
REPLACE INTO t SELECT * FROM t2;
-- after
INSERT INTO t (a,b) SELECT a,b FROM t2
  ON DUPLICATE KEY UPDATE b = VALUES(b);
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check: REPLACE INTO must use VALUES, not SELECT, on sharded tables
func replaceUsesSelect(stmt sqlparser.Statement) bool {
  ins, ok := stmt.(*sqlparser.Insert)
  if !ok || ins.Action != sqlparser.ReplaceAct { return false }
  _, isValues := ins.Rows.(sqlparser.Values)
  return !isValues
}

Type guard

rows, isValues := ins.Rows.(sqlparser.Values) // isValues must be true for REPLACE INTO plans

Try / catch

_, err := db.ExecContext(ctx, q)
if err != nil && strings.Contains(err.Error(), "VT12001") && strings.Contains(err.Error(), "REPLACE INTO") {
  // fall back to DELETE + INSERT ... SELECT strategy
}

Prevention

When it happens

Trigger: `REPLACE INTO tbl SELECT ...` (ins.Rows is not sqlparser.Values) on a sharded/managed table where deleteBeforeInsert is true — i.e. the table has primary key or unique key handling requiring the delete-before-insert plan.

Common situations: Migrating MySQL applications to Vitess that use REPLACE INTO ... SELECT for data copies; scripts that dedupe rows via REPLACE INTO with a SELECT source on sharded keyspaces.

Related errors


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