224 lines
7.8 KiB
Go
224 lines
7.8 KiB
Go
// 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
|
|
}
|