Repository: pingcap/tidb
Pull request #60296: planner: add an unique key into mysql.bind_info
Author: login: qw4990
Labels: name: sig/planner
description: SIG: Planner
name: size/XL
description: Denotes a PR that changes 500-999 lines, ignoring generated files.
name: release-note-none
description: Denotes a PR that doesn't merit a release note.
name: ok-to-test
description: Indicates a PR is ready to be tested.
name: approved
name: lgtm
Created: 2025-03-27T08:18:48Z | Merged: 2025-04-02T09:09:06Z

<!--

Thank you for contributing to TiDB!

PR Title Format:
1. pkg [, pkg2, pkg3]: what's changed
2. *: what's changed

-->

### What problem does this PR solve?
<!--

Please create an issue first to describe the problem.

There MUST be one line starting with "Issue Number:  " and
linking the relevant issues via the "close" or "ref".

For more info, check https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/contribute-code.html#referring-to-an-issue.

-->

Issue Number: ref #60148

Problem Summary: planner: add an unique key into mysql.bind_info

### What changed and how does it work?

add an unique key into mysql.bind_info, later we'll use this unique index to avoid duplicated data when automatically recording bindings.

### Check List

Tests <!-- At least one of them must be included. -->

- [x] Unit test
- [ ] Integration test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test
  > - [ ] I checked and no code files have been changed.
  > <!-- Or your custom  "No need to test" reasons -->

Side effects

- [ ] Performance regression: Consumes more CPU
- [ ] Performance regression: Consumes more Memory
- [ ] Breaking backward compatibility

Documentation

- [ ] Affects user behaviors
- [ ] Contains syntax changes
- [ ] Contains variable changes
- [ ] Contains experimental features
- [ ] Changes MySQL compatibility

### Release note

<!-- compatibility change, improvement, bugfix, and new feature need a release note -->

Please refer to [Release Notes Language Style Guide](https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/release-notes-style-guide.html) to write a quality release note.

```release-note
None
```



Diff:
diff --git a/br/pkg/restore/snap_client/systable_restore_test.go b/br/pkg/restore/snap_client/systable_restore_test.go
index 4797d6c7fd8ea..a1e8371c64162 100644
--- a/br/pkg/restore/snap_client/systable_restore_test.go
+++ b/br/pkg/restore/snap_client/systable_restore_test.go
@@ -117,5 +117,5 @@ func TestCheckSysTableCompatibility(t *testing.T) {
 
 // The above variables are in the file br/pkg/restore/systable_restore.go
 func TestMonitorTheSystemTableIncremental(t *testing.T) {
-	require.Equal(t, int64(245), session.CurrentBootstrapVersion)
+	require.Equal(t, int64(246), session.CurrentBootstrapVersion)
 }
diff --git a/pkg/bindinfo/binding_operator.go b/pkg/bindinfo/binding_operator.go
index feaf12f6073f9..f5cbe4b8b1dc7 100644
--- a/pkg/bindinfo/binding_operator.go
+++ b/pkg/bindinfo/binding_operator.go
@@ -97,6 +97,13 @@ func (op *bindingOperator) CreateBinding(sctx sessionctx.Context, bindings []*Bi
 			// overflow directly.
 
 			// Insert the Bindings to the storage.
+			var sqlDigest, planDigest any // null by default
+			if binding.SQLDigest != "" {
+				sqlDigest = binding.SQLDigest
+			}
+			if binding.PlanDigest != "" {
+				planDigest = binding.PlanDigest
+			}
 			_, err = exec(
 				sctx,
 				`INSERT INTO mysql.bind_info VALUES (%?,%?, %?, %?, %?, %?, %?, %?, %?, %?, %?)`,
@@ -109,8 +116,8 @@ func (op *bindingOperator) CreateBinding(sctx sessionctx.Context, bindings []*Bi
 				binding.Charset,
 				binding.Collation,
 				binding.Source,
-				binding.SQLDigest,
-				binding.PlanDigest,
+				sqlDigest,
+				planDigest,
 			)
 			failpoint.Inject("CreateGlobalBindingNthFail", func(val failpoint.Value) {
 				n := val.(int)
diff --git a/pkg/bindinfo/binding_operator_test.go b/pkg/bindinfo/binding_operator_test.go
index 6f9ce784ce678..d53e8e8ae0615 100644
--- a/pkg/bindinfo/binding_operator_test.go
+++ b/pkg/bindinfo/binding_operator_test.go
@@ -260,8 +260,6 @@ func TestSetBindingStatusWithoutBindingInCache(t *testing.T) {
 
 	// Simulate creating bindings on other machines
 	_, sqlDigest := parser.NormalizeDigestForBinding("select * from `test` . `t` where `a` > ?")
-	tk.MustExec("insert into mysql.bind_info values('select * from `test` . `t` where `a` > ?', 'SELECT /*+ USE_INDEX(`t` `idx_a`)*/ * FROM `test`.`t` WHERE `a` > 10', 'test', 'deleted', '2000-01-01 09:00:00', '2000-01-01 09:00:00', '', '','" +
-		bindinfo.SourceManual + "', '" + sqlDigest.String() + "', '')")
 	tk.MustExec("insert into mysql.bind_info values('select * from `test` . `t` where `a` > ?', 'SELECT /*+ USE_INDEX(`t` `idx_a`)*/ * FROM `test`.`t` WHERE `a` > 10', 'test', 'enabled', '2000-01-02 09:00:00', '2000-01-02 09:00:00', '', '','" +
 		bindinfo.SourceManual + "', '" + sqlDigest.String() + "', '')")
 	tk.MustExec("set binding disabled for select * from t where a > 10")
@@ -274,8 +272,6 @@ func TestSetBindingStatusWithoutBindingInCache(t *testing.T) {
 	utilCleanBindingEnv(tk)
 
 	// Simulate creating bindings on other machines
-	tk.MustExec("insert into mysql.bind_info values('select * from `test` . `t` where `a` > ?', 'SELECT * FROM `test`.`t` WHERE `a` > 10', 'test', 'deleted', '2000-01-01 09:00:00', '2000-01-01 09:00:00', '', '','" +
-		bindinfo.SourceManual + "', '" + sqlDigest.String() + "', '')")
 	tk.MustExec("insert into mysql.bind_info values('select * from `test` . `t` where `a` > ?', 'SELECT * FROM `test`.`t` WHERE `a` > 10', 'test', 'disabled', '2000-01-02 09:00:00', '2000-01-02 09:00:00', '', '','" +
 		bindinfo.SourceManual + "', '" + sqlDigest.String() + "', '')")
 	tk.MustExec("set binding enabled for select * from t where a > 10")
diff --git a/pkg/session/BUILD.bazel b/pkg/session/BUILD.bazel
index b684099d16ef0..11d003abe366c 100644
--- a/pkg/session/BUILD.bazel
+++ b/pkg/session/BUILD.bazel
@@ -155,7 +155,6 @@ go_test(
     shard_count = 50,
     deps = [
         "//pkg/autoid_service",
-        "//pkg/bindinfo",
         "//pkg/config",
         "//pkg/ddl",
         "//pkg/domain",
@@ -165,7 +164,6 @@ go_test(
         "//pkg/keyspace",
         "//pkg/kv",
         "//pkg/meta",
-        "//pkg/parser",
         "//pkg/parser/ast",
         "//pkg/parser/auth",
         "//pkg/session/types",
diff --git a/pkg/session/bootstrap.go b/pkg/session/bootstrap.go
index a0f4971310bfb..63223c7bf7e9f 100644
--- a/pkg/session/bootstrap.go
+++ b/pkg/session/bootstrap.go
@@ -304,10 +304,11 @@ const (
 		charset TEXT NOT NULL,
 		collation TEXT NOT NULL,
 		source VARCHAR(10) NOT NULL DEFAULT 'unknown',
-		sql_digest varchar(64),
-		plan_digest varchar(64),
+		sql_digest varchar(64) DEFAULT NULL,
+		plan_digest varchar(64) DEFAULT NULL,
 		INDEX sql_index(original_sql(700),default_db(68)) COMMENT "accelerate the speed when add global binding query",
-		INDEX time_index(update_time) COMMENT "accelerate the speed when querying with last update time"
+		INDEX time_index(update_time) COMMENT "accelerate the speed when querying with last update time",
+		UNIQUE INDEX digest_index(plan_digest, sql_digest) COMMENT "avoid duplicated records"
 	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;`
 
 	// CreateRoleEdgesTable stores the role and user relationship information.
@@ -1275,11 +1276,14 @@ const (
 
 	// version245 updates column types of mysql.bind_info.
 	version245 = 245
+
+	// version246 adds new unique index for mysql.bind_info.
+	version246 = 246
 )
 
 // currentBootstrapVersion is defined as a variable, so we can modify its value for testing.
 // please make sure this is the largest version
-var currentBootstrapVersion int64 = version245
+var currentBootstrapVersion int64 = version246
 
 // DDL owner key's expired time is ManagerSessionTTL seconds, we should wait the time and give more time to have a chance to finish it.
 var internalSQLTimeout = owner.ManagerSessionTTL + 15
@@ -1460,6 +1464,7 @@ var (
 		upgradeToVer243,
 		upgradeToVer244,
 		upgradeToVer245,
+		upgradeToVer246,
 	}
 )
 
@@ -3406,6 +3411,60 @@ func upgradeToVer245(s sessiontypes.Session, ver int64) {
 	doReentrantDDL(s, "ALTER TABLE mysql.bind_info MODIFY COLUMN bind_sql LONGTEXT NOT NULL")
 }
 
+func upgradeToVer246(s sessiontypes.Session, ver int64) {
+	if ver >= version246 {
+		return
+	}
+
+	// log duplicated digests that will be set to null.
+	ctx := kv.WithInternalSourceType(context.Background(), kv.InternalTxnBootstrap)
+	rs, err := s.ExecuteInternal(ctx,
+		`select plan_digest, sql_digest from mysql.bind_info group by plan_digest, sql_digest having count(1) > 1`)
+	if err != nil {
+		logutil.BgLogger().Fatal("failed to get duplicated plan and sql digests", zap.Error(err))
+		return
+	}
+	req := rs.NewChunk(nil)
+	duplicatedDigests := make(map[string]struct{})
+	for {
+		err = rs.Next(ctx, req)
+		if err != nil {
+			logutil.BgLogger().Fatal("failed to get duplicated plan and sql digests", zap.Error(err))
+			return
+		}
+		if req.NumRows() == 0 {
+			break
+		}
+		for i := 0; i < req.NumRows(); i++ {
+			planDigest, sqlDigest := req.GetRow(i).GetString(0), req.GetRow(i).GetString(1)
+			duplicatedDigests[sqlDigest+", "+planDigest] = struct{}{}
+		}
+		req.Reset()
+	}
+	if err := rs.Close(); err != nil {
+		logutil.BgLogger().Warn("failed to close record set", zap.Error(err))
+	}
+	if len(duplicatedDigests) > 0 {
+		digestList := make([]string, 0, len(duplicatedDigests))
+		for k := range duplicatedDigests {
+			digestList = append(digestList, "("+k+")")
+		}
+		logutil.BgLogger().Warn("set the following (plan digest, sql digest) in mysql.bind_info to null " +
+			"for adding new unique index: " + strings.Join(digestList, ", "))
+	}
+
+	// to avoid the failure of adding the unique index, remove duplicated rows on these 2 digest columns first.
+	// in most cases, there should be no duplicated rows, since now we only store one binding for each sql_digest.
+	// compared with upgrading failure, it's OK to set these 2 columns to null.
+	doReentrantDDL(s, `UPDATE mysql.bind_info SET plan_digest=null, sql_digest=null
+                       WHERE (plan_digest, sql_digest) in (
+                         select plan_digest, sql_digest from mysql.bind_info
+                         group by plan_digest, sql_digest having count(1) > 1)`)
+	doReentrantDDL(s, "ALTER TABLE mysql.bind_info MODIFY COLUMN sql_digest VARCHAR(64) DEFAULT NULL")
+	doReentrantDDL(s, "ALTER TABLE mysql.bind_info MODIFY COLUMN plan_digest VARCHAR(64) DEFAULT NULL")
+	doReentrantDDL(s, "ALTER TABLE mysql.bind_info ADD UNIQUE INDEX digest_index(plan_digest, sql_digest)", dbterror.ErrDupKeyName)
+}
+
 // initGlobalVariableIfNotExists initialize a global variable with specific val if it does not exist.
 func initGlobalVariableIfNotExists(s sessiontypes.Session, name string, val any) {
 	ctx := kv.WithInternalSourceType(context.Background(), kv.InternalTxnBootstrap)
diff --git a/pkg/session/bootstrap_test.go b/pkg/session/bootstrap_test.go
index 8bb4ecb157c7d..503afc70c065d 100644
--- a/pkg/session/bootstrap_test.go
+++ b/pkg/session/bootstrap_test.go
@@ -18,7 +18,6 @@ import (
 	"context"
 	"crypto/tls"
 	"fmt"
-	"sort"
 	"strings"
 	"testing"
 	"time"
@@ -26,14 +25,12 @@ import (
 	"github.com/pingcap/failpoint"
 	"github.com/pingcap/kvproto/pkg/keyspacepb"
 	"github.com/pingcap/log"
-	"github.com/pingcap/tidb/pkg/bindinfo"
 	"github.com/pingcap/tidb/pkg/ddl"
 	"github.com/pingcap/tidb/pkg/domain"
 	"github.com/pingcap/tidb/pkg/expression/sessionexpr"
 	"github.com/pingcap/tidb/pkg/keyspace"
 	"github.com/pingcap/tidb/pkg/kv"
 	"github.com/pingcap/tidb/pkg/meta"
-	"github.com/pingcap/tidb/pkg/parser"
 	"github.com/pingcap/tidb/pkg/parser/auth"
 	sessiontypes "github.com/pingcap/tidb/pkg/session/types"
 	"github.com/pingcap/tidb/pkg/sessionctx"
@@ -588,53 +585,6 @@ func TestStmtSummary(t *testing.T) {
 	require.NoError(t, r.Close())
 }
 
-func TestUpdateDuplicateBindInfo(t *testing.T) {
-	ctx := context.Background()
-	store, dom := CreateStoreAndBootstrap(t)
-	defer func() { require.NoError(t, store.Close()) }()
-	defer dom.Close()
-	se := CreateSessionAndSetID(t, store)
-	MustExec(t, se, "alter table mysql.bind_info drop column if exists plan_digest")
-	MustExec(t, se, "alter table mysql.bind_info drop column if exists sql_digest")
-
-	MustExec(t, se, `insert into mysql.bind_info values('select * from t', 'select /*+ use_index(t, idx_a)*/ * from t', 'test', 'enabled', '2021-01-04 14:50:58.257', '2021-01-04 14:50:58.257', 'utf8', 'utf8_general_ci', 'manual')`)
-	// The latest one.
-	MustExec(t, se, `insert into mysql.bind_info values('select * from test . t', 'select /*+ use_index(t, idx_b)*/ * from test.t', 'test', 'enabled', '2021-01-04 14:50:58.257', '2021-01-09 14:50:58.257', 'utf8', 'utf8_general_ci', 'manual')`)
-
-	MustExec(t, se, `insert into mysql.bind_info values('select * from t where a < ?', 'select * from t use index(idx) where a < 1', 'test', 'deleted', '2021-06-04 17:04:43.333', '2021-06-04 17:04:43.335', 'utf8', 'utf8_general_ci', 'manual')`)
-	MustExec(t, se, `insert into mysql.bind_info values('select * from t where a < ?', 'select * from t ignore index(idx) where a < 1', 'test', 'enabled', '2021-06-04 17:04:43.335', '2021-06-04 17:04:43.335', 'utf8', 'utf8_general_ci', 'manual')`)
-	MustExec(t, se, `insert into mysql.bind_info values('select * from test . t where a <= ?', 'select * from test.t use index(idx) where a <= 1', '', 'deleted', '2021-06-04 17:04:43.345', '2021-06-04 17:04:45.334', 'utf8', 'utf8_general_ci', 'manual')`)
-	MustExec(t, se, `insert into mysql.bind_info values('select * from test . t where a <= ?', 'select * from test.t ignore index(idx) where a <= 1', '', 'enabled', '2021-06-04 17:04:45.334', '2021-06-04 17:04:45.334', 'utf8', 'utf8_general_ci', 'manual')`)
-
-	upgradeToVer67(se, version66)
-
-	r := MustExecToRecodeSet(t, se, `select original_sql, bind_sql, default_db, status, create_time from mysql.bind_info where source != 'builtin' order by create_time`)
-	req := r.NewChunk(nil)
-	require.NoError(t, r.Next(ctx, req))
-	require.Equal(t, 3, req.NumRows())
-	row := req.GetRow(0)
-	require.Equal(t, "select * from `test` . `t`", row.GetString(0))
-	require.Equal(t, "SELECT /*+ use_index(`t` `idx_b`)*/ * FROM `test`.`t`", row.GetString(1))
-	require.Equal(t, "", row.GetString(2))
-	require.Equal(t, bindinfo.StatusEnabled, row.GetString(3))
-	require.Equal(t, "2021-01-04 14:50:58.257", row.GetTime(4).String())
-	row = req.GetRow(1)
-	require.Equal(t, "select * from `test` . `t` where `a` < ?", row.GetString(0))
-	require.Equal(t, "SELECT * FROM `test`.`t` IGNORE INDEX (`idx`) WHERE `a` < 1", row.GetString(1))
-	require.Equal(t, "", row.GetString(2))
-	require.Equal(t, bindinfo.StatusEnabled, row.GetString(3))
-	require.Equal(t, "2021-06-04 17:04:43.335", row.GetTime(4).String())
-	row = req.GetRow(2)
-	require.Equal(t, "select * from `test` . `t` where `a` <= ?", row.GetString(0))
-	require.Equal(t, "SELECT * FROM `test`.`t` IGNORE INDEX (`idx`) WHERE `a` <= 1", row.GetString(1))
-	require.Equal(t, "", row.GetString(2))
-	require.Equal(t, bindinfo.StatusEnabled, row.GetString(3))
-	require.Equal(t, "2021-06-04 17:04:45.334", row.GetTime(4).String())
-
-	require.NoError(t, r.Close())
-	MustExec(t, se, "delete from mysql.bind_info where original_sql = 'select * from test . t'")
-}
-
 func TestUpgradeClusteredIndexDefaultValue(t *testing.T) {
 	store, dom := CreateStoreAndBootstrap(t)
 	defer func() { require.NoError(t, store.Close()) }()
@@ -922,33 +872,6 @@ func testIndexMergeUpgradeFrom400To540(t *testing.T, enable bool) {
 	}
 }
 
-func TestUpgradeToVer85(t *testing.T) {
-	ctx := context.Background()
-	store, dom := CreateStoreAndBootstrap(t)
-	defer func() { require.NoError(t, store.Close()) }()
-	defer dom.Close()
-	se := CreateSessionAndSetID(t, store)
-	MustExec(t, se, "alter table mysql.bind_info drop column if exists plan_digest")
-	MustExec(t, se, "alter table mysql.bind_info drop column if exists sql_digest")
-
-	MustExec(t, se, `insert into mysql.bind_info values('select * from t', 'select /*+ use_index(t, idx_a)*/ * from t', 'test', 'using', '2021-01-04 14:50:58.257', '2021-01-04 14:50:58.257', 'utf8', 'utf8_general_ci', 'manual')`)
-	MustExec(t, se, `insert into mysql.bind_info values('select * from t1', 'select /*+ use_index(t1, idx_a)*/ * from t1', 'test', 'enabled', '2021-01-05 14:50:58.257', '2021-01-05 14:50:58.257', 'utf8', 'utf8_general_ci', 'manual')`)
-	MustExec(t, se, `insert into mysql.bind_info values('select * from t2', 'select /*+ use_index(t2, idx_a)*/ * from t2', 'test', 'disabled', '2021-01-06 14:50:58.257', '2021-01-06 14:50:58.257', 'utf8', 'utf8_general_ci', 'manual')`)
-	MustExec(t, se, `insert into mysql.bind_info values('select * from t3', 'select /*+ use_index(t3, idx_a)*/ * from t3', 'test', 'deleted', '2021-01-07 14:50:58.257', '2021-01-07 14:50:58.257', 'utf8', 'utf8_general_ci', 'manual')`)
-	MustExec(t, se, `insert into mysql.bind_info values('select * from t4', 'select /*+ use_index(t4, idx_a)*/ * from t4', 'test', 'invalid', '2021-01-08 14:50:58.257', '2021-01-08 14:50:58.257', 'utf8', 'utf8_general_ci', 'manual')`)
-	upgradeToVer85(se, version84)
-
-	r := MustExecToRecodeSet(t, se, `select count(*) from mysql.bind_info where status = 'enabled'`)
-	req := r.NewChunk(nil)
-	require.NoError(t, r.Next(ctx, req))
-	require.Equal(t, 1, req.NumRows())
-	row := req.GetRow(0)
-	require.Equal(t, int64(2), row.GetInt64(0))
-
-	require.NoError(t, r.Close())
-	MustExec(t, se, "delete from mysql.bind_info where default_db = 'test'")
-}
-
 func TestTiDBEnablePagingVariable(t *testing.T) {
 	store, dom := CreateStoreAndBootstrap(t)
 	se := CreateSessionAndSetID(t, store)
@@ -2005,89 +1928,6 @@ func TestTiDBUpgradeToVer170(t *testing.T) {
 	dom.Close()
 }
 
-func TestTiDBBindingInListToVer175(t *testing.T) {
-	ctx := context.Background()
-	store, dom := CreateStoreAndBootstrap(t)
-	defer func() { require.NoError(t, store.Close()) }()
-
-	// bootstrap as version174
-	ver174 := version174
-	seV174 := CreateSessionAndSetID(t, store)
-	txn, err := store.Begin()
-	require.NoError(t, err)
-	m := meta.NewMutator(txn)
-	err = m.FinishBootstrap(int64(ver174))
-	require.NoError(t, err)
-	revertVersionAndVariables(t, seV174, ver174)
-	err = txn.Commit(context.Background())
-	require.NoError(t, err)
-	store.SetOption(StoreBootstrappedKey, nil)
-
-	// create some bindings at version174
-	MustExec(t, seV174, "use test")
-	MustExec(t, seV174, "create table t (a int, b int, c int, key(c))")
-	_, digest := parser.NormalizeDigestForBinding("SELECT * FROM `test`.`t` WHERE `a` IN (1,2,3)")
-	MustExec(t, seV174, fmt.Sprintf("insert into mysql.bind_info values ('select * from `test` . `t` where `a` in ( ... )', 'SELECT /*+ use_index(`t` `c`)*/ * FROM `test`.`t` WHERE `a` IN (1,2,3)', 'test', 'enabled', '2023-09-13 14:41:38.319', '2023-09-13 14:41:35.319', 'utf8', 'utf8_general_ci', 'manual', '%s', '')", digest.String()))
-	_, digest = parser.NormalizeDigestForBinding("SELECT * FROM `test`.`t` WHERE `a` IN (1)")
-	MustExec(t, seV174, fmt.Sprintf("insert into mysql.bind_info values ('select * from `test` . `t` where `a` in ( ? )', 'SELECT /*+ use_index(`t` `c`)*/ * FROM `test`.`t` WHERE `a` IN (1)', 'test', 'enabled', '2023-09-13 14:41:38.319', '2023-09-13 14:41:36.319', 'utf8', 'utf8_general_ci', 'manual', '%s', '')", digest.String()))
-	_, digest = parser.NormalizeDigestForBinding("SELECT * FROM `test`.`t` WHERE `a` IN (1) AND `b` IN (1,2,3)")
-	MustExec(t, seV174, fmt.Sprintf("insert into mysql.bind_info values ('select * from `test` . `t` where `a` in ( ? ) and `b` in ( ... )', 'SELECT /*+ use_index(`t` `c`)*/ * FROM `test`.`t` WHERE `a` IN (1) AND `b` IN (1,2,3)', 'test', 'enabled', '2023-09-13 14:41:37.319', '2023-09-13 14:41:38.319', 'utf8', 'utf8_general_ci', 'manual', '%s', '')", digest.String()))
-
-	showBindings := func(s sessiontypes.Session) (records []string) {
-		MustExec(t, s, "admin reload bindings")
-		res := MustExecToRecodeSet(t, s, "show global bindings")
-		chk := res.NewChunk(nil)
-		for {
-			require.NoError(t, res.Next(ctx, chk))
-			if chk.NumRows() == 0 {
-				break
-			}
-			for i := 0; i < chk.NumRows(); i++ {
-				originalSQL := chk.GetRow(i).GetString(0)
-				bindSQL := chk.GetRow(i).GetString(1)
-				records = append(records, fmt.Sprintf("%s:%s", bindSQL, originalSQL))
-			}
-		}
-		require.NoError(t, res.Close())
-		sort.Strings(records)
-		return
-	}
-	bindings := showBindings(seV174)
-	// on ver174, `in (1)` and `in (1,2,3)` have different normalized results: `in (?)` and `in (...)`
-	require.Equal(t, []string{"SELECT /*+ use_index(`t` `c`)*/ * FROM `test`.`t` WHERE `a` IN (1) AND `b` IN (1,2,3):select * from `test` . `t` where `a` in ( ? ) and `b` in ( ... )",
-		"SELECT /*+ use_index(`t` `c`)*/ * FROM `test`.`t` WHERE `a` IN (1):select * from `test` . `t` where `a` in ( ? )"}, bindings)
-
-	// upgrade to ver175
-	dom.Close()
-	domCurVer, err := BootstrapSession(store)
-	require.NoError(t, err)
-	defer domCurVer.Close()
-	seCurVer := CreateSessionAndSetID(t, store)
-	ver, err := getBootstrapVersion(seCurVer)
-	require.NoError(t, err)
-	require.Equal(t, currentBootstrapVersion, ver)
-
-	// `in (?)` becomes to `in ( ... )`
-	bindings = showBindings(seCurVer)
-	require.Equal(t, []string{"SELECT /*+ use_index(`t` `c`)*/ * FROM `test`.`t` WHERE `a` IN (1) AND `b` IN (1,2,3):select * from `test` . `t` where `a` in ( ... ) and `b` in ( ... )",
-		"SELECT /*+ use_index(`t` `c`)*/ * FROM `test`.`t` WHERE `a` IN (1):select * from `test` . `t` where `a` in ( ... )"}, bindings)
-
-	planFromBinding := func(s sessiontypes.Session, q string) {
-		MustExec(t, s, q)
-		res := MustExecToRecodeSet(t, s, "select @@last_plan_from_binding")
-		chk := res.NewChunk(nil)
-		require.NoError(t, res.Next(ctx, chk))
-		require.Equal(t, int64(1), chk.GetRow(0).GetInt64(0))
-		require.NoError(t, res.Close())
-	}
-	planFromBinding(seCurVer, "select * from test.t where a in (1)")
-	planFromBinding(seCurVer, "select * from test.t where a in (1,2,3)")
-	planFromBinding(seCurVer, "select * from test.t where a in (1,2,3,4,5,6,7)")
-	planFromBinding(seCurVer, "select * from test.t where a in (1,2,3,4,5,6,7) and b in(1)")
-	planFromBinding(seCurVer, "select * from test.t where a in (1,2,3,4,5,6,7) and b in(1,2,3,4)")
-	planFromBinding(seCurVer, "select * from test.t where a in (7) and b in(1,2,3,4)")
-}
-
 func TestTiDBUpgradeToVer176(t *testing.T) {
 	store, do := CreateStoreAndBootstrap(t)
 	defer func() {
@@ -2761,3 +2601,46 @@ func TestWriteClusterIDToMySQLTiDBWhenUpgradingTo242(t *testing.T) {
 	require.NoError(t, r.Close())
 	seCurVer.Close()
 }
+
+func TestBindInfoUniqueIndex(t *testing.T) {
+	ctx := context.Background()
+	store, dom := CreateStoreAndBootstrap(t)
+	defer func() { require.NoError(t, store.Close()) }()
+
+	// bootstrap as version245
+	ver245 := version245
+	seV245 := CreateSessionAndSetID(t, store)
+	txn, err := store.Begin()
+	require.NoError(t, err)
+	m := meta.NewMutator(txn)
+	err = m.FinishBootstrap(int64(ver245))
+	require.NoError(t, err)
+	revertVersionAndVariables(t, seV245, ver245)
+	err = txn.Commit(ctx)
+	require.NoError(t, err)
+	store.SetOption(StoreBootstrappedKey, nil)
+
+	// remove the unique index on mysql.bind_info for testing
+	MustExec(t, seV245, "alter table mysql.bind_info drop index digest_index")
+
+	// insert duplicated values into mysql.bind_info
+	for _, sqlDigest := range []string{"null", "'x'", "'y'"} {
+		for _, planDigest := range []string{"null", "'x'", "'y'"} {
+			insertStmt := fmt.Sprintf(`insert into mysql.bind_info values (
+             "sql", "bind_sql", "db", "disabled", NOW(), NOW(), "", "", "", %s, %s)`,
+				sqlDigest, planDigest)
+			MustExec(t, seV245, insertStmt)
+			MustExec(t, seV245, insertStmt)
+		}
+	}
+
+	// upgrade to current version
+	dom.Close()
+	domCurVer, err := BootstrapSession(store)
+	require.NoError(t, err)
+	defer domCurVer.Close()
+	seCurVer := CreateSessionAndSetID(t, store)
+	ver, err := getBootstrapVersion(seCurVer)
+	require.NoError(t, err)
+	require.Equal(t, currentBootstrapVersion, ver)
+}


Reviews:
user: login: AilinKid
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T01:38:50Z
user: login: henrybw
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T03:36:53Z
user: login: henrybw
body: The changes and tests look good to me, but for the upgrader I think we should probably note (via logging probably) any duplicate rows that were found.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T03:37:59Z
user: login: qw4990
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T09:40:25Z
user: login: qw4990
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T09:41:07Z
user: login: AilinKid
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T10:27:11Z
user: login: henrybw
body: LGTM. Thanks!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T00:21:45Z
user: login: 3pointer
body: After restore bind_info, br should do similar work like upgradeToVer246 too. 

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T02:36:04Z
user: login: yudongusa
state: APPROVED
author_association: NONE
submitted_at: 2025-04-02T02:42:19Z

Review comments:
diff_hunk: @@ -3406,6 +3411,23 @@ func upgradeToVer245(s sessiontypes.Session, ver int64) {
 	doReentrantDDL(s, "ALTER TABLE mysql.bind_info MODIFY COLUMN bind_sql LONGTEXT NOT NULL")
 }
 
+func upgradeToVer246(s sessiontypes.Session, ver int64) {
+	if ver >= version246 {
+		return
+	}
+
+	// to avoid the failure of adding the unique index, remove duplicated rows on these 2 digest columns first.
+	// in most cases, there should be no duplicated rows, since now we only store one binding for each sql_digest.
+	// compared with upgrading failure, it's OK to set these 2 columns to null, the
+	doReentrantDDL(s, `UPDATE mysql.bind_info SET plan_digest=null, sql_digest=null
path: pkg/session/bootstrap.go
user: login: AilinKid
body: 1. should we have some msg out like log or warnings.
2. this rows with plan_digest=null, sql_digest=null can only be deleted by scan or range delete or what? user delete their one binding with SQL in, in the internal, we may fetch the binding with specific where condition like sql_digest=xxx, this may not work for this kind of "dirty rows" ever?

created_at: 2025-04-01T01:38:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3406,6 +3411,23 @@ func upgradeToVer245(s sessiontypes.Session, ver int64) {
 	doReentrantDDL(s, "ALTER TABLE mysql.bind_info MODIFY COLUMN bind_sql LONGTEXT NOT NULL")
 }
 
+func upgradeToVer246(s sessiontypes.Session, ver int64) {
+	if ver >= version246 {
+		return
+	}
+
+	// to avoid the failure of adding the unique index, remove duplicated rows on these 2 digest columns first.
+	// in most cases, there should be no duplicated rows, since now we only store one binding for each sql_digest.
+	// compared with upgrading failure, it's OK to set these 2 columns to null, the
+	doReentrantDDL(s, `UPDATE mysql.bind_info SET plan_digest=null, sql_digest=null
path: pkg/session/bootstrap.go
user: login: henrybw
body: I agree that it would be a good idea to log or otherwise indicate that duplicate bindings were found during the upgrade (and their digests were set to null). It's probably not a big deal if this is ignored, but I would think that a DBA would like to know about these updates.
created_at: 2025-04-01T03:36:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3406,6 +3411,23 @@ func upgradeToVer245(s sessiontypes.Session, ver int64) {
 	doReentrantDDL(s, "ALTER TABLE mysql.bind_info MODIFY COLUMN bind_sql LONGTEXT NOT NULL")
 }
 
+func upgradeToVer246(s sessiontypes.Session, ver int64) {
+	if ver >= version246 {
+		return
+	}
+
+	// to avoid the failure of adding the unique index, remove duplicated rows on these 2 digest columns first.
+	// in most cases, there should be no duplicated rows, since now we only store one binding for each sql_digest.
+	// compared with upgrading failure, it's OK to set these 2 columns to null, the
+	doReentrantDDL(s, `UPDATE mysql.bind_info SET plan_digest=null, sql_digest=null
path: pkg/session/bootstrap.go
user: login: qw4990
body: For the second comment, YES, if we set `sql_digest` to null, we are not able to delete them via SQLs like `delete ... where sql_digest = ?` and then they'll be some kind of `dirty rows` forever.
But compared with upgrading failure, some `dirty rows` seems more acceptable. And in most cases, `sql_digest` should not be duplicated, so this problem is less likely to happen.
created_at: 2025-04-01T09:40:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3406,6 +3411,23 @@ func upgradeToVer245(s sessiontypes.Session, ver int64) {
 	doReentrantDDL(s, "ALTER TABLE mysql.bind_info MODIFY COLUMN bind_sql LONGTEXT NOT NULL")
 }
 
+func upgradeToVer246(s sessiontypes.Session, ver int64) {
+	if ver >= version246 {
+		return
+	}
+
+	// to avoid the failure of adding the unique index, remove duplicated rows on these 2 digest columns first.
+	// in most cases, there should be no duplicated rows, since now we only store one binding for each sql_digest.
+	// compared with upgrading failure, it's OK to set these 2 columns to null, the
+	doReentrantDDL(s, `UPDATE mysql.bind_info SET plan_digest=null, sql_digest=null
path: pkg/session/bootstrap.go
user: login: qw4990
body: And I added some log for this, and tested it locally:
```
[2025/04/01 17:35:08.272 +08:00] [WARN] [bootstrap.go:3449] ["set the following (plan digest, sql digest) in mysql.bind_info to null for adding new unique index: (y, ), (, y), (x, ), (x, x), (y, y), (y, x), (, ), (x, y), (, x)"]
```

Please help review it again: @AilinKid @henrybw 
created_at: 2025-04-01T09:41:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: tiprow[bot]
created_at: 2025-03-27T08:19:03Z
body: Hi @qw4990. Thanks for your PR.

PRs from untrusted users cannot be marked as trusted with `/ok-to-test` in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using `/test all`.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=pingcap%2Ftidb).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>

author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-27T10:03:02Z
body: ## [Codecov](https://app.codecov.io/gh/pingcap/tidb/pull/60296?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) Report
Attention: Patch coverage is `73.07692%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 74.9273%. Comparing base [(`d916614`)](https://app.codecov.io/gh/pingcap/tidb/commit/d9166140268741cb0179d33f9716bd912fc6796e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) to head [(`f019e98`)](https://app.codecov.io/gh/pingcap/tidb/commit/f019e989087fed42d929dc352951acfdbea4d8a8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap).
> Report is 10 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff                @@
##             master     #60296        +/-   ##
================================================
+ Coverage   73.1301%   74.9273%   +1.7971%     
================================================
  Files          1710       1726        +16     
  Lines        472671     475015      +2344     
================================================
+ Hits         345665     355916     +10251     
+ Misses       105751      97255      -8496     
- Partials      21255      21844       +589     
```

| [Flag](https://app.codecov.io/gh/pingcap/tidb/pull/60296/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | Coverage Δ | |
|---|---|---|
| [integration](https://app.codecov.io/gh/pingcap/tidb/pull/60296/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `46.3918% <11.5384%> (?)` | |
| [unit](https://app.codecov.io/gh/pingcap/tidb/pull/60296/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `72.5793% <73.0769%> (+0.2508%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Components](https://app.codecov.io/gh/pingcap/tidb/pull/60296/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | Coverage Δ | |
|---|---|---|
| [dumpling](https://app.codecov.io/gh/pingcap/tidb/pull/60296/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `52.6910% <ø> (ø)` | |
| [parser](https://app.codecov.io/gh/pingcap/tidb/pull/60296/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `∅ <ø> (∅)` | |
| [br](https://app.codecov.io/gh/pingcap/tidb/pull/60296/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pingcap) | `62.4199% <ø> (+15.2276%)` | :arrow_up: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: hawkingrei
created_at: 2025-03-27T10:09:15Z
body: /ok-to-test
author_association: MEMBER
user: login: qw4990
created_at: 2025-03-28T11:40:40Z
body: /test unit-test
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-03-28T11:41:02Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2761123241):

>/test unit-test


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-01T01:08:42Z
body: These 3 UTs `TestUpdateDuplicateBindInfo`, `TestUpgradeToVer85`, `TestTiDBBindingInListToVer175` are not standard, too old and conflicting with the current PR (they insert duplicated rows into `mysql.bind_info` via insert-stmt directly), so we delete them.
author_association: CONTRIBUTOR
user: login: qw4990
created_at: 2025-04-01T09:37:27Z
body: Tested the log locally, it can work well when upgrading:
```
[2025/04/01 17:35:08.272 +08:00] [WARN] [bootstrap.go:3449] ["set the following (plan digest, sql digest) in mysql.bind_info to null for adding new unique index: (y, ), (, y), (x, ), (x, x), (y, y), (y, x), (, ), (x, y), (, x)"]
```
author_association: CONTRIBUTOR
user: login: ti-chi-bot[bot]
created_at: 2025-04-02T00:21:52Z
body: @henrybw: adding LGTM is restricted to approvers and reviewers in OWNERS files.

<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#pullrequestreview-2734285786):

>LGTM. Thanks!


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T01:17:52Z
body: /retest
author_association: CONTRIBUTOR
user: login: ti-chi-bot[bot]
created_at: 2025-04-02T02:36:15Z
body: [LGTM Timeline notifier]
---
Timeline:

- `2025-04-01 10:27:18.866344446 +0000 UTC m=+1560932.550580537`: :ballot_box_with_check: agreed by [AilinKid](https://github.com/AilinKid).
- `2025-04-02 02:36:14.570189804 +0000 UTC m=+1619068.254425915`: :ballot_box_with_check: agreed by [3pointer](https://github.com/3pointer).
author_association: NONE
user: login: ti-chi-bot[bot]
created_at: 2025-04-02T02:42:26Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/pingcap/tidb/pull/60296#pullrequestreview-2734485878" title="Approved">3pointer</a>*, *<a href="https://github.com/pingcap/tidb/pull/60296#pullrequestreview-2732228769" title="Approved">AilinKid</a>*, *<a href="https://github.com/pingcap/tidb/pull/60296#pullrequestreview-2734285786" title="Approved">henrybw</a>*, *<a href="https://github.com/pingcap/tidb/pull/60296#pullrequestreview-2734504983" title="Approved">yudongusa</a>*

The full list of commands accepted by this bot can be found [here](https://prow.tidb.net/command-help?repo=pingcap%2Ftidb).

The pull request process is described [here](https://book.prow.tidb.net/#/workflows/pr)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/pingcap/tidb/blob/master/OWNERS)~~ [3pointer,AilinKid]
- ~~[br/OWNERS](https://github.com/pingcap/tidb/blob/master/br/OWNERS)~~ [3pointer]
- ~~[pkg/bindinfo/OWNERS](https://github.com/pingcap/tidb/blob/master/pkg/bindinfo/OWNERS)~~ [AilinKid]
- ~~[pkg/session/OWNERS](https://github.com/pingcap/tidb/blob/master/pkg/session/OWNERS)~~ [yudongusa]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T03:13:48Z
body: /test build
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-02T03:14:10Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2771236841):

>/test build


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T03:35:35Z
body: /test build
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-02T03:35:58Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2771257739):

>/test build


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T04:04:50Z
body: /test build
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-02T04:05:12Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2771285277):

>/test build


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T06:21:16Z
body: /test build
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-02T06:21:38Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2771449082):

>/test build


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T07:37:52Z
body: /test check-dev2
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-02T07:38:14Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2771600049):

>/test check-dev2


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T07:39:28Z
body: /retest
author_association: CONTRIBUTOR
user: login: qw4990
created_at: 2025-04-02T08:04:04Z
body: /test check-dev2
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-02T08:04:26Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2771701532):

>/test check-dev2


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T08:25:13Z
body: /test check-dev2
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-02T08:25:36Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2771787680):

>/test check-dev2


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: qw4990
created_at: 2025-04-02T08:46:20Z
body: /test check-dev2
author_association: CONTRIBUTOR
user: login: tiprow[bot]
created_at: 2025-04-02T08:46:41Z
body: @qw4990: The specified target(s) for `/test` were not found.
The following commands are available to trigger required jobs:
* `/test fast_test_tiprow`
* `/test tidb_parser_test`

Use `/test all` to run all jobs.


<details>

In response to [this](https://github.com/pingcap/tidb/pull/60296#issuecomment-2771854499):

>/test check-dev2


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE