Skip to content

Commit 3dfc47f

Browse files
authored
executor: fix mysql_insert_id() for "INSERT .. ON DUPLICATE KEY" statement (#56514)
close #55965
1 parent 1946f92 commit 3dfc47f

File tree

2 files changed

+86
-5
lines changed

2 files changed

+86
-5
lines changed

pkg/executor/insert.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ func (e *InsertValues) prefetchDataCache(ctx context.Context, txn kv.Transaction
194194
}
195195

196196
// updateDupRow updates a duplicate row to a new row.
197-
func (e *InsertExec) updateDupRow(ctx context.Context, idxInBatch int, txn kv.Transaction, row toBeCheckedRow, handle kv.Handle, _ []*expression.Assignment, dupKeyCheck table.DupKeyCheckMode) error {
197+
func (e *InsertExec) updateDupRow(ctx context.Context, idxInBatch int, txn kv.Transaction, row toBeCheckedRow, handle kv.Handle, _ []*expression.Assignment, dupKeyCheck table.DupKeyCheckMode, autoColIdx int) error {
198198
oldRow, err := getOldRow(ctx, e.Ctx(), txn, row.t, handle, e.GenExprs)
199199
if err != nil {
200200
return err
@@ -205,7 +205,7 @@ func (e *InsertExec) updateDupRow(ctx context.Context, idxInBatch int, txn kv.Tr
205205
extraCols = e.Ctx().GetSessionVars().CurrInsertBatchExtraCols[idxInBatch]
206206
}
207207

208-
err = e.doDupRowUpdate(ctx, handle, oldRow, row.row, extraCols, e.OnDuplicate, idxInBatch, dupKeyCheck)
208+
err = e.doDupRowUpdate(ctx, handle, oldRow, row.row, extraCols, e.OnDuplicate, idxInBatch, dupKeyCheck, autoColIdx)
209209
if kv.ErrKeyExists.Equal(err) || table.ErrCheckConstraintViolated.Equal(err) {
210210
ec := e.Ctx().GetSessionVars().StmtCtx.ErrCtx()
211211
return ec.HandleErrorWithAlias(kv.ErrKeyExists, err, err)
@@ -249,14 +249,19 @@ func (e *InsertExec) batchUpdateDupRows(ctx context.Context, newRows [][]types.D
249249
// TODO: just use `DupKeyCheckSkip` here.
250250
addRecordDupKeyCheck := optimizeDupKeyCheckForNormalInsert(e.Ctx().GetSessionVars(), txn)
251251

252+
_, autoColIdx, found := findAutoIncrementColumn(e.Table)
253+
if !found {
254+
autoColIdx = -1
255+
}
256+
252257
for i, r := range toBeCheckedRows {
253258
if r.handleKey != nil {
254259
handle, err := tablecodec.DecodeRowKey(r.handleKey.newKey)
255260
if err != nil {
256261
return err
257262
}
258263

259-
err = e.updateDupRow(ctx, i, txn, r, handle, e.OnDuplicate, updateDupKeyCheck)
264+
err = e.updateDupRow(ctx, i, txn, r, handle, e.OnDuplicate, updateDupKeyCheck, autoColIdx)
260265
if err == nil {
261266
continue
262267
}
@@ -273,7 +278,7 @@ func (e *InsertExec) batchUpdateDupRows(ctx context.Context, newRows [][]types.D
273278
if handle == nil {
274279
continue
275280
}
276-
err = e.updateDupRow(ctx, i, txn, r, handle, e.OnDuplicate, updateDupKeyCheck)
281+
err = e.updateDupRow(ctx, i, txn, r, handle, e.OnDuplicate, updateDupKeyCheck, autoColIdx)
277282
if err != nil {
278283
if kv.IsErrNotFound(err) {
279284
// Data index inconsistent? A unique key provide the handle information, but the
@@ -427,7 +432,7 @@ func (e *InsertExec) initEvalBuffer4Dup() {
427432

428433
// doDupRowUpdate updates the duplicate row.
429434
func (e *InsertExec) doDupRowUpdate(ctx context.Context, handle kv.Handle, oldRow []types.Datum, newRow []types.Datum,
430-
extraCols []types.Datum, cols []*expression.Assignment, idxInBatch int, dupKeyMode table.DupKeyCheckMode) error {
435+
extraCols []types.Datum, cols []*expression.Assignment, idxInBatch int, dupKeyMode table.DupKeyCheckMode, autoColIdx int) error {
431436
assignFlag := make([]bool, len(e.Table.WritableCols()))
432437
// See http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_values
433438
e.curInsertVals.SetDatums(newRow...)
@@ -478,6 +483,17 @@ func (e *InsertExec) doDupRowUpdate(ctx context.Context, handle kv.Handle, oldRo
478483
if err != nil {
479484
return err
480485
}
486+
487+
if autoColIdx >= 0 {
488+
if e.Ctx().GetSessionVars().StmtCtx.AffectedRows() > 0 {
489+
// If "INSERT ... ON DUPLICATE KEY UPDATE" duplicate and update a row,
490+
// auto increment value should be set correctly for mysql_insert_id()
491+
// See https://github.com/pingcap/tidb/issues/55965
492+
e.Ctx().GetSessionVars().StmtCtx.InsertID = newData[autoColIdx].GetUint64()
493+
} else {
494+
e.Ctx().GetSessionVars().StmtCtx.InsertID = 0
495+
}
496+
}
481497
return nil
482498
}
483499

pkg/executor/insert_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,68 @@ func TestInsertLockUnchangedKeys(t *testing.T) {
592592
}
593593
}
594594
}
595+
596+
func TestMySQLInsertID(t *testing.T) {
597+
// mysql_insert_id() differs from LAST_INSERT_ID()
598+
// See https://github.com/pingcap/tidb/issues/55965
599+
// mysql_insert_id() is got from tk.Session().LastInsertID()
600+
store := testkit.CreateMockStore(t)
601+
tk := testkit.NewTestKit(t, store)
602+
tk.MustExec(`use test`)
603+
tk.MustExec("drop table if exists tb")
604+
tk.MustExec("create table tb(pk int primary key auto_increment, a int, b int, unique(a))")
605+
defer tk.MustExec("drop table if exists tb")
606+
607+
tk.MustExec("insert into tb (a, b) values (1, 1) on duplicate key update b = values(b)")
608+
require.Equal(t, tk.Session().LastInsertID(), uint64(1))
609+
610+
tk.MustExec("insert into tb (a, b) values (2, 2) on duplicate key update b = values(b)")
611+
require.Equal(t, tk.Session().LastInsertID(), uint64(2))
612+
613+
// If there is an AUTO_INCREMENT column in the table and there were some explicit successfully
614+
// inserted values or some updated values, return the last of the inserted or updated values.
615+
// Ref https://dev.mysql.com/doc/c-api/5.7/en/mysql-insert-id.html#:~:text=When%20called%20after%20an%20INSERT%20...%20ON,of%20the%20inserted%20or%20updated%20values
616+
tk.MustExec("insert into tb (a, b) values (1, 2) on duplicate key update b = values(b)")
617+
require.Equal(t, tk.Session().LastInsertID(), uint64(1))
618+
tk.MustQuery("select LAST_INSERT_ID()").Check(testkit.Rows("2"))
619+
620+
tk.MustQuery("select * from tb").Sort().Check(testkit.Rows("1 1 2", "2 2 2"))
621+
622+
// When the new row and the old row are exactly the same (no inserted or updated values), mysql_insert_id() is 0
623+
tk.MustExec("insert into tb (a, b) values (1, 2) on duplicate key update b = 2")
624+
require.Equal(t, tk.Session().LastInsertID(), uint64(0))
625+
tk.MustQuery("select LAST_INSERT_ID()").Check(testkit.Rows("2"))
626+
627+
// When the value of auto increment column is assigned explicitly, LAST_INSERT_ID() is unchanged.
628+
// mysql_insert_id() is set to the explicit assigned value.
629+
tk.MustExec("insert into tb values (6, 6, 6)")
630+
require.Equal(t, tk.Session().LastInsertID(), uint64(6))
631+
tk.MustQuery("select LAST_INSERT_ID()").Check(testkit.Rows("2"))
632+
633+
// Update statement touches neigher mysql_insert_id() nor LAST_INSERT_ID()
634+
tk.MustExec("update tb set b = 7, pk = pk + 1 where b = 6")
635+
require.Equal(t, tk.Session().LastInsertID(), uint64(0))
636+
tk.MustQuery("select LAST_INSERT_ID()").Check(testkit.Rows("2"))
637+
638+
// How to distinguish LAST_INSERT_ID() and mysql_insert_id()?
639+
// In a word, LAST_INSERT_ID() is always get from auto allocated value, while mysql_insert_id() can be
640+
// auto allocated or explicited specified.
641+
642+
// Another scenario mentioned by @lcwangcao
643+
// What's the behaviour when transaction conflict involved?
644+
tk.MustExec("truncate table tb")
645+
tk.MustExec("insert into tb (a, b) values (1, 1), (2, 2)")
646+
647+
tk1 := testkit.NewTestKit(t, store)
648+
tk1.MustExec("use test")
649+
tk1.MustExec("begin")
650+
tk1.MustExec("update tb set b = 2 where a = 1")
651+
go func() {
652+
time.Sleep(100 * time.Millisecond)
653+
tk1.MustExec("commit")
654+
}()
655+
// The first time this will update one row.
656+
// Then transaction conflict and retry, in the second time it modify nothing.
657+
tk.MustExec("insert into tb(a, b) values(1,2) on duplicate key update b = 2;")
658+
require.Equal(t, tk.Session().LastInsertID(), uint64(0))
659+
}

0 commit comments

Comments
 (0)