// Copyright 2025 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, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package bindinfo import ( "context" "fmt" "strings" "github.com/pingcap/errors" "github.com/pingcap/tidb/pkg/kv" "github.com/pingcap/tidb/pkg/parser/ast" "github.com/pingcap/tidb/pkg/parser/format" "github.com/pingcap/tidb/pkg/parser/terror" "github.com/pingcap/tidb/pkg/planner/core/resolve" "github.com/pingcap/tidb/pkg/sessionctx" "github.com/pingcap/tidb/pkg/util" "github.com/pingcap/tidb/pkg/util/chunk" "github.com/pingcap/tidb/pkg/util/hint" "github.com/pingcap/tidb/pkg/util/logutil" utilparser "github.com/pingcap/tidb/pkg/util/parser" "github.com/pingcap/tidb/pkg/util/sqlexec" "go.uber.org/zap" ) func callWithSCtx(sPool util.DestroyableSessionPool, wrapTxn bool, f func(sctx sessionctx.Context) error) (err error) { resource, err := sPool.Get() if err != nil { return err } defer func() { if err == nil { // only recycle when no error sPool.Put(resource) } else { // Note: Otherwise, the session will be leaked. sPool.Destroy(resource) } }() sctx := resource.(sessionctx.Context) if wrapTxn { if _, err = exec(sctx, "BEGIN PESSIMISTIC"); err != nil { return } defer func() { if err == nil { _, err = exec(sctx, "COMMIT") } else { _, err1 := exec(sctx, "ROLLBACK") terror.Log(errors.Trace(err1)) } }() } err = f(sctx) return } // exec is a helper function to execute sql and return RecordSet. func exec(sctx sessionctx.Context, sql string, args ...any) (sqlexec.RecordSet, error) { sqlExec := sctx.GetSQLExecutor() return sqlExec.ExecuteInternal(kv.WithInternalSourceType(context.Background(), kv.InternalTxnBindInfo), sql, args...) } // execRows is a helper function to execute sql and return rows and fields. func execRows(sctx sessionctx.Context, sql string, args ...any) (rows []chunk.Row, fields []*resolve.ResultField, err error) { sqlExec := sctx.GetRestrictedSQLExecutor() return sqlExec.ExecRestrictedSQL(kv.WithInternalSourceType(context.Background(), kv.InternalTxnBindInfo), []sqlexec.OptionFuncAlias{sqlexec.ExecOptionUseCurSession}, sql, args...) } // bindingLogger with category "sql-bind" is used to log statistic related messages. func bindingLogger() *zap.Logger { return logutil.BgLogger().With(zap.String("category", "sql-bind")) } // GenerateBindingSQL generates binding sqls from stmt node and plan hints. func GenerateBindingSQL(stmtNode ast.StmtNode, planHint string, defaultDB string) string { // We need to evolve plan based on the current sql, not the original sql which may have different parameters. // So here we would remove the hint and inject the current best plan hint. hint.BindHint(stmtNode, &hint.HintsSet{}) bindSQL := RestoreDBForBinding(stmtNode, defaultDB) if bindSQL == "" { return "" } switch n := stmtNode.(type) { case *ast.DeleteStmt: deleteIdx := strings.Index(bindSQL, "DELETE") // Remove possible `explain` prefix. bindSQL = bindSQL[deleteIdx:] return strings.Replace(bindSQL, "DELETE", fmt.Sprintf("DELETE /*+ %s*/", planHint), 1) case *ast.UpdateStmt: updateIdx := strings.Index(bindSQL, "UPDATE") // Remove possible `explain` prefix. bindSQL = bindSQL[updateIdx:] return strings.Replace(bindSQL, "UPDATE", fmt.Sprintf("UPDATE /*+ %s*/", planHint), 1) case *ast.SelectStmt: var selectIdx int if n.With != nil { var withSb strings.Builder withIdx := strings.Index(bindSQL, "WITH") restoreCtx := format.NewRestoreCtx(format.RestoreStringSingleQuotes|format.RestoreSpacesAroundBinaryOperation|format.RestoreStringWithoutCharset|format.RestoreNameBackQuotes, &withSb) restoreCtx.DefaultDB = defaultDB if err := n.With.Restore(restoreCtx); err != nil { bindingLogger().Debug("restore SQL failed", zap.Error(err)) return "" } withEnd := withIdx + len(withSb.String()) tmp := strings.Replace(bindSQL[withEnd:], "SELECT", fmt.Sprintf("SELECT /*+ %s*/", planHint), 1) return strings.Join([]string{bindSQL[withIdx:withEnd], tmp}, "") } selectIdx = strings.Index(bindSQL, "SELECT") // Remove possible `explain` prefix. bindSQL = bindSQL[selectIdx:] return strings.Replace(bindSQL, "SELECT", fmt.Sprintf("SELECT /*+ %s*/", planHint), 1) case *ast.InsertStmt: insertIdx := int(0) if n.IsReplace { insertIdx = strings.Index(bindSQL, "REPLACE") } else { insertIdx = strings.Index(bindSQL, "INSERT") } // Remove possible `explain` prefix. bindSQL = bindSQL[insertIdx:] return strings.Replace(bindSQL, "SELECT", fmt.Sprintf("SELECT /*+ %s*/", planHint), 1) } bindingLogger().Debug("unexpected statement type when generating bind SQL", zap.Any("statement", stmtNode)) return "" } func readBindingsFromStorage(sPool util.DestroyableSessionPool, condition string, args ...any) (bindings []*Binding, err error) { selectStmt := fmt.Sprintf(`SELECT original_sql, bind_sql, default_db, status, create_time, update_time, charset, collation, source, sql_digest, plan_digest FROM mysql.bind_info %s`, condition) err = callWithSCtx(sPool, false, func(sctx sessionctx.Context) error { rows, _, err := execRows(sctx, selectStmt, args...) if err != nil { return err } bindings = make([]*Binding, 0, len(rows)) for _, row := range rows { // Skip the builtin record which is designed for binding synchronization. if row.GetString(0) == BuiltinPseudoSQL4BindLock { continue } binding := newBindingFromStorage(row) if hErr := prepareHints(sctx, binding); hErr != nil { bindingLogger().Warn("failed to generate bind record from data row", zap.Error(hErr)) continue } bindings = append(bindings, binding) } return nil }) return } // newBindingFromStorage builds Bindings from a tuple in storage. func newBindingFromStorage(row chunk.Row) *Binding { status := row.GetString(3) // For compatibility, the 'Using' status binding will be converted to the 'Enabled' status binding. if status == StatusUsing { status = StatusEnabled } return &Binding{ OriginalSQL: row.GetString(0), Db: strings.ToLower(row.GetString(2)), BindSQL: row.GetString(1), Status: status, CreateTime: row.GetTime(4), UpdateTime: row.GetTime(5), Charset: row.GetString(6), Collation: row.GetString(7), Source: row.GetString(8), SQLDigest: row.GetString(9), PlanDigest: row.GetString(10), } } // getBindingPlanDigest does the best efforts to fill binding's plan_digest. func getBindingPlanDigest(sctx sessionctx.Context, schema, bindingSQL string) (planDigest string) { defer func() { if r := recover(); r != nil { bindingLogger().Error("panic when filling plan digest for binding", zap.String("binding_sql", bindingSQL), zap.Reflect("panic", r)) } }() vars := sctx.GetSessionVars() defer func(originalBaseline bool, originalDB string) { vars.UsePlanBaselines = originalBaseline vars.CurrentDB = originalDB }(vars.UsePlanBaselines, vars.CurrentDB) vars.UsePlanBaselines = false vars.CurrentDB = schema p := utilparser.GetParser() defer utilparser.DestroyParser(p) p.SetSQLMode(vars.SQLMode) p.SetParserConfig(vars.BuildParserConfig()) charset, collation := vars.GetCharsetInfo() if stmt, err := p.ParseOneStmt(bindingSQL, charset, collation); err == nil { if !hasParam(stmt) { // if there is '?' from `create binding using select a from t where a=?`, // the final plan digest might be incorrect. planDigest, _ = CalculatePlanDigest(sctx, stmt) } } return }