// Copyright 2018 PingCAP, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // See the License for the specific language governing permissions and // limitations under the License. package executor import ( "github.com/juju/errors" "github.com/pingcap/tidb/kv" "github.com/pingcap/tidb/types" "github.com/pingcap/tidb/util/chunk" "golang.org/x/net/context" ) // ReplaceExec represents a replace executor. type ReplaceExec struct { *InsertValues Priority int finished bool } // Close implements the Executor Close interface. func (e *ReplaceExec) Close() error { if e.SelectExec != nil { return e.SelectExec.Close() } return nil } // Open implements the Executor Open interface. func (e *ReplaceExec) Open(ctx context.Context) error { if e.SelectExec != nil { return e.SelectExec.Open(ctx) } return nil } func (e *ReplaceExec) exec(rows []types.DatumRow) error { /* * MySQL uses the following algorithm for REPLACE (and LOAD DATA ... REPLACE): * 1. Try to insert the new row into the table * 2. While the insertion fails because a duplicate-key error occurs for a primary key or unique index: * 3. Delete from the table the conflicting row that has the duplicate key value * 4. Try again to insert the new row into the table * See http://dev.mysql.com/doc/refman/5.7/en/replace.html * * For REPLACE statements, the affected-rows value is 2 if the new row replaced an old row, * because in this case, one row was inserted after the duplicate was deleted. * See http://dev.mysql.com/doc/refman/5.7/en/mysql-affected-rows.html */ idx := 0 rowsLen := len(rows) sc := e.ctx.GetSessionVars().StmtCtx for { if idx >= rowsLen { break } row := rows[idx] h, err1 := e.Table.AddRecord(e.ctx, row, false) if err1 == nil { e.ctx.StmtAddDirtyTableOP(DirtyTableAddRow, e.Table.Meta().ID, h, row) idx++ continue } if err1 != nil && !kv.ErrKeyExists.Equal(err1) { return errors.Trace(err1) } oldRow, err1 := e.Table.Row(e.ctx, h) if err1 != nil { return errors.Trace(err1) } rowUnchanged, err1 := types.EqualDatums(sc, oldRow, row) if err1 != nil { return errors.Trace(err1) } if rowUnchanged { // If row unchanged, we do not need to do insert. e.ctx.GetSessionVars().StmtCtx.AddAffectedRows(1) idx++ continue } // Remove current row and try replace again. err1 = e.Table.RemoveRecord(e.ctx, h, oldRow) if err1 != nil { return errors.Trace(err1) } e.ctx.StmtAddDirtyTableOP(DirtyTableDeleteRow, e.Table.Meta().ID, h, nil) e.ctx.GetSessionVars().StmtCtx.AddAffectedRows(1) } if e.lastInsertID != 0 { e.ctx.GetSessionVars().SetLastInsertID(e.lastInsertID) } e.finished = true return nil } // Next implements the Executor Next interface. func (e *ReplaceExec) Next(ctx context.Context, chk *chunk.Chunk) error { chk.Reset() if e.finished { return nil } cols, err := e.getColumns(e.Table.Cols()) if err != nil { return errors.Trace(err) } if len(e.children) > 0 && e.children[0] != nil { return errors.Trace(e.insertRowsFromSelect(ctx, cols, e.exec)) } return errors.Trace(e.insertRows(cols, e.exec)) }