diff --git a/pkg/planner/core/BUILD.bazel b/pkg/planner/core/BUILD.bazel index df7c8ec4808a0..aa7e9692318a1 100644 --- a/pkg/planner/core/BUILD.bazel +++ b/pkg/planner/core/BUILD.bazel @@ -297,6 +297,7 @@ go_test( "//pkg/testkit/testsetup", "//pkg/testkit/testutil", "//pkg/types", + "//pkg/types/parser_driver", "//pkg/util", "//pkg/util/chunk", "//pkg/util/collate", diff --git a/pkg/planner/core/expression_rewriter.go b/pkg/planner/core/expression_rewriter.go index ef02666c31001..0b044fad94612 100644 --- a/pkg/planner/core/expression_rewriter.go +++ b/pkg/planner/core/expression_rewriter.go @@ -172,7 +172,7 @@ func buildSimpleExpr(ctx expression.BuildContext, node ast.ExprNode, opts ...exp } if ft := options.TargetFieldType; ft != nil { - expr = expression.BuildCastFunction(ctx, expr, ft) + expr = expression.BuildCastFunction(ctx, expr, ft.DeepCopy()) } return expr, err @@ -1612,7 +1612,8 @@ func (er *expressionRewriter) Leave(originInNode ast.Node) (retNode ast.Node, ok return retNode, false } - castFunction, err := expression.BuildCastFunctionWithCheck(er.sctx, arg, v.Tp, false, v.ExplicitCharSet) + targetTp := v.Tp.DeepCopy() + castFunction, err := expression.BuildCastFunctionWithCheck(er.sctx, arg, targetTp, false, v.ExplicitCharSet) if err != nil { er.err = err return retNode, false @@ -1633,7 +1634,8 @@ func (er *expressionRewriter) Leave(originInNode ast.Node) (retNode ast.Node, ok er.ctxNameStk[len(er.ctxNameStk)-1] = types.EmptyName case *ast.JSONSumCrc32Expr: arg := er.ctxStack[len(er.ctxStack)-1] - jsonSumFunction, err := expression.BuildJSONSumCrc32FunctionWithCheck(er.sctx, arg, v.Tp) + targetTp := v.Tp.DeepCopy() + jsonSumFunction, err := expression.BuildJSONSumCrc32FunctionWithCheck(er.sctx, arg, targetTp) if err != nil { er.err = err return retNode, false @@ -1651,7 +1653,7 @@ func (er *expressionRewriter) Leave(originInNode ast.Node) (retNode ast.Node, ok er.rowToScalarFunc(v) case *ast.PatternInExpr: if v.Sel == nil { - er.inToExpression(len(v.List), v.Not, &v.Type) + er.inToExpression(len(v.List), v.Not, v.Type.DeepCopy()) } case *ast.PositionExpr: withPlanCtx(func(planCtx *exprRewriterPlanCtx) { @@ -1913,7 +1915,7 @@ func (er *expressionRewriter) unaryOpToExpression(v *ast.UnaryOperationExpr) { er.err = expression.ErrOperandColumns.GenWithStackByArgs(1) return } - er.ctxStack[stkLen-1], er.err = er.newFunction(op, &v.Type, er.ctxStack[stkLen-1]) + er.ctxStack[stkLen-1], er.err = er.newFunction(op, v.Type.DeepCopy(), er.ctxStack[stkLen-1]) er.ctxNameStk[stkLen-1] = types.EmptyName } @@ -1965,7 +1967,7 @@ func (er *expressionRewriter) isNullToExpression(v *ast.IsNullExpr) { er.err = expression.ErrOperandColumns.GenWithStackByArgs(1) return } - function := er.notToExpression(v.Not, ast.IsNull, &v.Type, er.ctxStack[stkLen-1]) + function := er.notToExpression(v.Not, ast.IsNull, v.Type.DeepCopy(), er.ctxStack[stkLen-1]) er.ctxStackPop(1) er.ctxStackAppend(function, types.EmptyName) } @@ -2005,7 +2007,7 @@ func (er *expressionRewriter) isTrueToScalarFunc(v *ast.IsTruthExpr) { er.err = expression.ErrOperandColumns.GenWithStackByArgs(1) return } - function := er.notToExpression(v.Not, op, &v.Type, er.ctxStack[stkLen-1]) + function := er.notToExpression(v.Not, op, v.Type.DeepCopy(), er.ctxStack[stkLen-1]) er.ctxStackPop(1) er.ctxStackAppend(function, types.EmptyName) } @@ -2196,7 +2198,7 @@ func (er *expressionRewriter) caseToExpression(v *ast.CaseExpr) { // else clause args = er.ctxStack[stkLen-argsLen:] } - function, err := er.newFunction(ast.Case, &v.Type, args...) + function, err := er.newFunction(ast.Case, v.Type.DeepCopy(), args...) if err != nil { er.err = err return @@ -2244,7 +2246,7 @@ func (er *expressionRewriter) patternLikeOrIlikeToExpression(v *ast.PatternLikeO funcName = ast.Ilike } types.DefaultTypeForValue(int(v.Escape), fieldType, char, col) - function = er.notToExpression(v.Not, funcName, &v.Type, + function = er.notToExpression(v.Not, funcName, v.Type.DeepCopy(), er.ctxStack[l-2], er.ctxStack[l-1], &expression.Constant{Value: types.NewIntDatum(int64(v.Escape)), RetType: fieldType}) } @@ -2258,7 +2260,7 @@ func (er *expressionRewriter) regexpToScalarFunc(v *ast.PatternRegexpExpr) { if er.err != nil { return } - function := er.notToExpression(v.Not, ast.Regexp, &v.Type, er.ctxStack[l-2], er.ctxStack[l-1]) + function := er.notToExpression(v.Not, ast.Regexp, v.Type.DeepCopy(), er.ctxStack[l-2], er.ctxStack[l-1]) er.ctxStackPop(2) er.ctxStackAppend(function, types.EmptyName) } @@ -2346,21 +2348,21 @@ func (er *expressionRewriter) betweenToExpression(v *ast.BetweenExpr) { rexp = expression.BuildCastCollationFunction(er.sctx, rexp, coll, enumOrSetRealTypeIsStr) var l, r expression.Expression - l, er.err = expression.NewFunction(er.sctx, ast.GE, &v.Type, expr, lexp) + l, er.err = expression.NewFunction(er.sctx, ast.GE, v.Type.DeepCopy(), expr, lexp) if er.err != nil { return } - r, er.err = expression.NewFunction(er.sctx, ast.LE, &v.Type, expr, rexp) + r, er.err = expression.NewFunction(er.sctx, ast.LE, v.Type.DeepCopy(), expr, rexp) if er.err != nil { return } - function, err := er.newFunction(ast.LogicAnd, &v.Type, l, r) + function, err := er.newFunction(ast.LogicAnd, v.Type.DeepCopy(), l, r) if err != nil { er.err = err return } if v.Not { - function, err = er.newFunction(ast.UnaryNot, &v.Type, function) + function, err = er.newFunction(ast.UnaryNot, v.Type.DeepCopy(), function) if err != nil { er.err = err return @@ -2434,7 +2436,7 @@ func (er *expressionRewriter) rewriteFuncCall(v *ast.FuncCallExpr) bool { RetType: nullTp, } // if(param1 = param2, NULL, param1) - funcIf, err := er.newFunction(ast.If, &v.Type, funcCompare, paramNull, param1) + funcIf, err := er.newFunction(ast.If, v.Type.DeepCopy(), funcCompare, paramNull, param1) if err != nil { er.err = err return true @@ -2495,7 +2497,7 @@ func (er *expressionRewriter) funcCallToExpressionWithPlanCtx(planCtx *exprRewri err = groupingFunc.Function.(*expression.BuiltinGroupingImplSig).SetMetadata(planCtx.rollExpand.GroupingMode, planCtx.rollExpand.GenerateGroupingMarks(resolvedCols)) return groupingFunc, err } - function, er.err = er.newFunctionWithInit(v.FnName.L, &v.Type, init, newArg) + function, er.err = er.newFunctionWithInit(v.FnName.L, v.Type.DeepCopy(), init, newArg) er.ctxStackAppend(function, types.EmptyName) } default: @@ -2522,15 +2524,15 @@ func (er *expressionRewriter) funcCallToExpression(v *ast.FuncCallExpr) { // When the expression is unix_timestamp and the number of argument is not zero, // we deal with it as normal expression. if v.FnName.L == ast.UnixTimestamp && len(v.Args) != 0 { - function, er.err = er.newFunction(v.FnName.L, &v.Type, args...) + function, er.err = er.newFunction(v.FnName.L, v.Type.DeepCopy(), args...) er.ctxStackAppend(function, types.EmptyName) } else { - function, er.err = expression.NewFunctionBase(er.sctx, v.FnName.L, &v.Type, args...) + function, er.err = expression.NewFunctionBase(er.sctx, v.FnName.L, v.Type.DeepCopy(), args...) c := &expression.Constant{Value: types.NewDatum(nil), RetType: function.GetType(er.sctx.GetEvalCtx()).Clone(), DeferredExpr: function} er.ctxStackAppend(c, types.EmptyName) } } else { - function, er.err = er.newFunction(v.FnName.L, &v.Type, args...) + function, er.err = er.newFunction(v.FnName.L, v.Type.DeepCopy(), args...) er.ctxStackAppend(function, types.EmptyName) } } diff --git a/pkg/planner/core/expression_test.go b/pkg/planner/core/expression_test.go index 3da5b7a37704c..766e7c6f1b3e5 100644 --- a/pkg/planner/core/expression_test.go +++ b/pkg/planner/core/expression_test.go @@ -202,6 +202,52 @@ func TestCast(t *testing.T) { require.Equal(t, types.KindNull, v.Kind()) } +func TestCastRetTypeDoesNotShareASTFieldType(t *testing.T) { + targetTp := types.NewFieldType(mysql.TypeLonglong) + targetTp.AddFlag(mysql.NotNullFlag) + + ctx := coretestsdk.MockContext() + defer func() { + do := domain.GetDomain(ctx) + do.StatsHandle().Close() + }() + + tbl := &model.TableInfo{ + Name: ast.NewCIStr("t"), + Columns: []*model.ColumnInfo{ + { + Name: ast.NewCIStr("a"), + Offset: 0, + State: model.StatePublic, + FieldType: *types.NewFieldType(mysql.TypeLonglong), + }, + }, + } + expr := parseExpr(t, "cast(a as signed)").(*ast.FuncCastExpr) + expr.Tp = targetTp + + built1, err := buildExpr(t, ctx, expr, expression.WithTableInfo("", tbl)) + require.NoError(t, err) + sf1, ok := built1.(*expression.ScalarFunction) + require.True(t, ok) + require.NotSame(t, targetTp, sf1.RetType) + require.True(t, mysql.HasNotNullFlag(targetTp.GetFlag())) + + sf1.RetType.SetType(mysql.TypeString) + sf1.RetType.AddFlag(mysql.UnsignedFlag) + + built2, err := buildExpr(t, ctx, expr, expression.WithTableInfo("", tbl)) + require.NoError(t, err) + sf2, ok := built2.(*expression.ScalarFunction) + require.True(t, ok) + require.NotSame(t, sf1.RetType, sf2.RetType) + require.Equal(t, mysql.TypeLonglong, targetTp.GetType()) + require.True(t, mysql.HasNotNullFlag(targetTp.GetFlag())) + require.Equal(t, mysql.TypeLonglong, sf2.RetType.GetType()) + require.False(t, mysql.HasNotNullFlag(sf2.RetType.GetFlag())) + require.False(t, mysql.HasUnsignedFlag(sf2.RetType.GetFlag())) +} + func TestPatternIn(t *testing.T) { tests := []testCase{ { diff --git a/pkg/planner/core/logical_plan_builder.go b/pkg/planner/core/logical_plan_builder.go index f1221e42f12e7..586442c891043 100644 --- a/pkg/planner/core/logical_plan_builder.go +++ b/pkg/planner/core/logical_plan_builder.go @@ -3685,7 +3685,7 @@ func (b *PlanBuilder) addAliasName(ctx context.Context, selectStmt *ast.SelectSt } func (b *PlanBuilder) pushTableHints(hints []*ast.TableOptimizerHint, currentLevel int) { - hints = b.hintProcessor.GetCurrentStmtHints(hints, currentLevel) + hints = b.hintProcessor.GetCurrentStmtHints(hints, currentLevel, b.hintState) sessionVars := b.ctx.GetSessionVars() currentDB := sessionVars.CurrentDB warnHandler := sessionVars.StmtCtx @@ -4495,7 +4495,7 @@ func (b *PlanBuilder) buildDataSource(ctx context.Context, tn *ast.TableName, as // Because of the nested views, so we should check the left table list in hint when build the data source from the view inside the current view. currentQBNameMap4View[qbName] = viewQBNameHintTable[1:] currentViewHints[qbName] = b.hintProcessor.ViewQBNameToHints[qbName] - b.hintProcessor.ViewQBNameUsed[qbName] = struct{}{} + b.hintProcessor.MarkViewQBNameUsed(qbName, b.hintState) } } return b.BuildDataSourceFromView(ctx, dbName, tableInfo, currentQBNameMap4View, currentViewHints) @@ -4995,18 +4995,21 @@ func (b *PlanBuilder) BuildDataSourceFromView(ctx context.Context, dbName ast.CI hintProcessor.ViewQBNameToTable = qbNameMap4View hintProcessor.ViewQBNameToHints = viewHints - hintProcessor.ViewQBNameUsed = make(map[string]struct{}) - hintProcessor.QBOffsetToHints = currentQbHints hintProcessor.QBNameToSelOffset = currentQbNameMap + hintState := hintProcessor.NewBuildState() + hintState.QBOffsetToHints = currentQbHints originHintProcessor := b.hintProcessor + originHintState := b.hintState originPlannerSelectBlockAsName := b.ctx.GetSessionVars().PlannerSelectBlockAsName.Load() b.hintProcessor = hintProcessor + b.hintState = hintState newPlannerSelectBlockAsName := make([]ast.HintTable, hintProcessor.MaxSelectStmtOffset()+1) b.ctx.GetSessionVars().PlannerSelectBlockAsName.Store(&newPlannerSelectBlockAsName) defer func() { - b.hintProcessor.HandleUnusedViewHints() + b.hintProcessor.SetWarns(b.hintProcessor.HandleUnusedViewHints(b.hintState, nil)) b.hintProcessor = originHintProcessor + b.hintState = originHintState b.ctx.GetSessionVars().PlannerSelectBlockAsName.Store(originPlannerSelectBlockAsName) }() nodeW := resolve.NewNodeWWithCtx(selectNode, b.resolveCtx) diff --git a/pkg/planner/core/operator/logicalop/logical_datasource.go b/pkg/planner/core/operator/logicalop/logical_datasource.go index ea010f0b2ea4f..c7899396b3aeb 100644 --- a/pkg/planner/core/operator/logicalop/logical_datasource.go +++ b/pkg/planner/core/operator/logicalop/logical_datasource.go @@ -49,6 +49,8 @@ import ( type DataSource struct { LogicalSchemaProducer `hash64-equals:"true"` + // AstIndexHints keeps the original AST hints for later access-path selection. + // It is treated as read-only after DataSource build and may be shared by plans rebuilt from the same AST. AstIndexHints []*ast.IndexHint IndexHints []h.HintedIndex Table table.Table @@ -56,6 +58,7 @@ type DataSource struct { Columns []*model.ColumnInfo DBName ast.CIStr + // TableAsName points to the AST alias and is only used as read-only metadata after plan build. TableAsName *ast.CIStr `hash64-equals:"true"` // IndexMergeHints are the hint for indexmerge. IndexMergeHints []h.HintedIndex @@ -84,7 +87,8 @@ type DataSource struct { // The data source may be a partition, rather than a real table. PartitionDefIdx *int PhysicalTableID int64 `hash64-equals:"true"` - PartitionNames []ast.CIStr + // PartitionNames records the explicit partition list from AST and is treated as read-only after plan build. + PartitionNames []ast.CIStr // handleCol represents the handle column for the datasource, either the // int primary key column or extra handle column. diff --git a/pkg/planner/core/operator/logicalop/logical_lock.go b/pkg/planner/core/operator/logicalop/logical_lock.go index f6b0dda0c93cc..2a3221e4f7d2c 100644 --- a/pkg/planner/core/operator/logicalop/logical_lock.go +++ b/pkg/planner/core/operator/logicalop/logical_lock.go @@ -26,6 +26,8 @@ import ( type LogicalLock struct { BaseLogicalPlan `hash64-equals:"true"` + // Lock points to AST lock metadata. Preprocess may normalize the AST before planning, + // but after LogicalLock is built this field is treated as read-only. Lock *ast.SelectLockInfo `hash64-equals:"true"` TblID2Handle map[int64][]util.HandleCols diff --git a/pkg/planner/core/operator/logicalop/logical_show.go b/pkg/planner/core/operator/logicalop/logical_show.go index c59f57b26043a..61322616b77b4 100644 --- a/pkg/planner/core/operator/logicalop/logical_show.go +++ b/pkg/planner/core/operator/logicalop/logical_show.go @@ -37,11 +37,12 @@ type LogicalShow struct { // ShowContents stores the contents for the `SHOW` statement. type ShowContents struct { - Tp ast.ShowStmtType // Databases/Tables/Columns/.... - DBName string - Table *resolve.TableNameW // Used for showing columns. - Partition ast.CIStr // Use for showing partition - Column *ast.ColumnName // Used for `desc table column`. + Tp ast.ShowStmtType // Databases/Tables/Columns/.... + DBName string + Table *resolve.TableNameW // Used for showing columns. + Partition ast.CIStr // Use for showing partition + // Column points to the AST selector for `desc table column` and is treated as read-only after plan build. + Column *ast.ColumnName IndexName ast.CIStr ResourceGroupName string // Used for showing resource group Flag int // Some flag parsed from sql, such as FULL. @@ -51,10 +52,11 @@ type ShowContents struct { CountWarningsOrErrors bool // Used for showing count(*) warnings | errors Full bool - IfNotExists bool // Used for `show create database if not exists`. - GlobalScope bool // Used by show variables. - Extended bool // Used for `show extended columns from ...` - Limit *ast.Limit // Used for limit Result Set row number. + IfNotExists bool // Used for `show create database if not exists`. + GlobalScope bool // Used by show variables. + Extended bool // Used for `show extended columns from ...` + // Limit points to the AST SHOW limit clause. It is only read during planner build and should stay immutable. + Limit *ast.Limit ImportJobID *int64 // Used for SHOW LOAD DATA JOB ImportGroupKey string // Used for SHOW IMPORT GROUP diff --git a/pkg/planner/core/operator/physicalop/physical_lock.go b/pkg/planner/core/operator/physicalop/physical_lock.go index bd2d37bcffad1..8558d00105163 100644 --- a/pkg/planner/core/operator/physicalop/physical_lock.go +++ b/pkg/planner/core/operator/physicalop/physical_lock.go @@ -33,6 +33,7 @@ import ( type PhysicalLock struct { BasePhysicalPlan + // Lock shares the read-only AST lock metadata forwarded from LogicalLock. Lock *ast.SelectLockInfo `plan-cache-clone:"shallow"` TblID2Handle map[int64][]util.HandleCols diff --git a/pkg/planner/core/planbuilder.go b/pkg/planner/core/planbuilder.go index 7b6ed624f3a7f..15d01fbbb1289 100644 --- a/pkg/planner/core/planbuilder.go +++ b/pkg/planner/core/planbuilder.go @@ -250,7 +250,10 @@ type PlanBuilder struct { // finish building the subquery or CTE. handleHelper *handleColHelper + // read-only meta derived from ast node. hintProcessor *hint.QBHintHandler + // mutable state of QBHint when building. + hintState *hint.QBHintBuildState // qbOffset is the offsets of current processing select stmts. qbOffset []int @@ -389,6 +392,11 @@ func GetDBTableInfo(visitInfo []visitInfo) []stmtctx.TableEntry { return tables } +// GetHintState gets the HintState from the PlanBuilder. +func (b *PlanBuilder) GetHintState() *hint.QBHintBuildState { + return b.hintState +} + // GetOptFlag gets the OptFlag of the PlanBuilder. func (b *PlanBuilder) GetOptFlag() uint64 { if b.isSampling { @@ -466,6 +474,9 @@ func (b *PlanBuilder) Init(sctx base.PlanContext, is infoschema.InfoSchema, proc b.ctx = sctx b.is = is b.hintProcessor = processor + if processor != nil { + b.hintState = processor.NewBuildState() + } b.isForUpdateRead = sctx.GetSessionVars().IsPessimisticReadConsistency() b.noDecorrelate = sctx.GetSessionVars().EnableNoDecorrelateInSelect if savedBlockNames == nil { @@ -507,6 +518,14 @@ func (b *PlanBuilder) ResetForReuse() *PlanBuilder { return b } +// HandleUnusedViewHints appends warnings for unused view hints in the current build. +func (b *PlanBuilder) HandleUnusedViewHints() { + if b.hintProcessor == nil { + return + } + b.hintProcessor.SetWarns(b.hintProcessor.HandleUnusedViewHints(b.hintState, nil)) +} + // Build builds the ast node to a Plan. func (b *PlanBuilder) Build(ctx context.Context, node *resolve.NodeW) (base.Plan, error) { err := b.checkSEMStmt(node.Node) @@ -750,7 +769,7 @@ func (b *PlanBuilder) buildSet(ctx context.Context, v *ast.SetStmt) (base.Plan, if vars.ExtendValue != nil { assign.ExtendValue = &expression.Constant{ Value: vars.ExtendValue.(*driver.ValueExpr).Datum, - RetType: &vars.ExtendValue.(*driver.ValueExpr).Type, + RetType: vars.ExtendValue.(*driver.ValueExpr).Type.DeepCopy(), } } p.VarAssigns = append(p.VarAssigns, assign) @@ -4351,7 +4370,7 @@ func (b PlanBuilder) getInsertColExpr(ctx context.Context, insertPlan *physicalo case *driver.ValueExpr: outExpr = &expression.Constant{ Value: x.Datum, - RetType: &x.Type, + RetType: x.Type.DeepCopy(), } case *driver.ParamMarkerExpr: outExpr, err = expression.ParamMarkerExpression(b.ctx.GetExprCtx(), x, false) @@ -5185,7 +5204,7 @@ func (b *PlanBuilder) convertValue(valueItem ast.ExprNode, mockTablePlan base.Lo case *driver.ValueExpr: expr = &expression.Constant{ Value: x.Datum, - RetType: &x.Type, + RetType: x.Type.DeepCopy(), } default: expr, _, err = b.rewrite(context.TODO(), valueItem, mockTablePlan, nil, true) diff --git a/pkg/planner/core/planbuilder_test.go b/pkg/planner/core/planbuilder_test.go index f2820282be327..741845f82fabc 100644 --- a/pkg/planner/core/planbuilder_test.go +++ b/pkg/planner/core/planbuilder_test.go @@ -42,7 +42,9 @@ import ( "github.com/pingcap/tidb/pkg/planner/property" "github.com/pingcap/tidb/pkg/planner/util" "github.com/pingcap/tidb/pkg/planner/util/coretestsdk" + "github.com/pingcap/tidb/pkg/table" "github.com/pingcap/tidb/pkg/types" + driver "github.com/pingcap/tidb/pkg/types/parser_driver" "github.com/pingcap/tidb/pkg/util/dbterror/plannererrors" "github.com/pingcap/tidb/pkg/util/hint" "github.com/pingcap/tidb/pkg/util/mock" @@ -161,6 +163,39 @@ func TestRewriterPool(t *testing.T) { builder.rewriterCounter-- } +func TestGetInsertColExprDeepCopiesValueExprFieldType(t *testing.T) { + ctx := coretestsdk.MockContext() + defer func() { + domain.GetDomain(ctx).StatsHandle().Close() + }() + builder, _ := NewPlanBuilder().Init(ctx, nil, hint.NewQBHintHandler(nil)) + + valueExpr, ok := ast.NewValueExpr(1, "", "").(*driver.ValueExpr) + require.True(t, ok) + valueExpr.Type.AddFlag(mysql.NotNullFlag) + + col := &table.Column{ + ColumnInfo: &model.ColumnInfo{ + Name: ast.NewCIStr("a"), + FieldType: *types.NewFieldType(mysql.TypeLonglong), + }, + } + expr, err := builder.getInsertColExpr(context.TODO(), &physicalop.Insert{}, nil, col, valueExpr, nil) + require.NoError(t, err) + + constExpr, ok := expr.(*expression.Constant) + require.True(t, ok) + require.NotSame(t, valueExpr.GetType(), constExpr.RetType) + require.Equal(t, mysql.TypeLonglong, valueExpr.Type.GetType()) + require.True(t, mysql.HasNotNullFlag(valueExpr.Type.GetFlag())) + + constExpr.RetType.SetType(mysql.TypeString) + constExpr.RetType.DelFlag(mysql.NotNullFlag) + + require.Equal(t, mysql.TypeLonglong, valueExpr.Type.GetType()) + require.True(t, mysql.HasNotNullFlag(valueExpr.Type.GetFlag())) +} + func TestDisableFold(t *testing.T) { // Functions like BENCHMARK() shall not be folded into result 0, // but normal outer function with constant args should be folded. diff --git a/pkg/planner/optimize.go b/pkg/planner/optimize.go index 405efe1b525e2..4200aa0698e9c 100644 --- a/pkg/planner/optimize.go +++ b/pkg/planner/optimize.go @@ -460,10 +460,10 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW // build logical plan hintProcessor := hint.NewQBHintHandler(sctx.GetSessionVars().StmtCtx) node.Node.Accept(hintProcessor) - defer hintProcessor.HandleUnusedViewHints() builder := planBuilderPool.Get().(*core.PlanBuilder) defer planBuilderPool.Put(builder.ResetForReuse()) builder.Init(sctx, is, hintProcessor) + defer builder.HandleUnusedViewHints() p, err := buildLogicalPlan(ctx, sctx, node, builder) if err != nil { return nil, nil, 0, err diff --git a/pkg/sessionctx/stmtctx/BUILD.bazel b/pkg/sessionctx/stmtctx/BUILD.bazel index f9513e8187b6d..58c2bbd7a4110 100644 --- a/pkg/sessionctx/stmtctx/BUILD.bazel +++ b/pkg/sessionctx/stmtctx/BUILD.bazel @@ -44,10 +44,12 @@ go_test( ], embed = [":stmtctx"], flaky = True, - shard_count = 15, + shard_count = 17, deps = [ "//pkg/errctx", "//pkg/kv", + "//pkg/meta/model", + "//pkg/parser/ast", "//pkg/sessionctx/variable", "//pkg/testkit", "//pkg/testkit/testfailpoint", diff --git a/pkg/sessionctx/stmtctx/stmtctx.go b/pkg/sessionctx/stmtctx/stmtctx.go index a8ef4e288c58d..80c0600b786bb 100644 --- a/pkg/sessionctx/stmtctx/stmtctx.go +++ b/pkg/sessionctx/stmtctx/stmtctx.go @@ -69,6 +69,26 @@ func AllocateTaskID() uint64 { // SQLWarn relates a sql warning and it's level. type SQLWarn = contextutil.SQLWarn +// LogicalPlanBuildState stores the statement-scoped planner state that is mutated while +// building a logical plan from AST. +type LogicalPlanBuildState struct { + warnings []SQLWarn + extraWarnings []SQLWarn + tables []TableEntry + tableStats map[int64]any + lockTableIDs map[int64]struct{} + tblInfo2UnionScan map[*model.TableInfo]bool + useDynamicPruneMode bool + viewDepth int32 + colRefFromUpdatePlan intset.FastIntSet + // plan cache related stuff + planCacheUseCache bool + planCacheType contextutil.PlanCacheType + planCacheUnqualified string + planCacheForce bool + planCacheAlwaysWarn bool +} + // ReferenceCount indicates the reference count of StmtCtx. type ReferenceCount int32 @@ -583,6 +603,44 @@ func (sc *StatementContext) Reset() bool { return true } +// SaveLogicalPlanBuildState captures the statement-scoped planner state before building +// another logical plan candidate from the same AST. +func (sc *StatementContext) SaveLogicalPlanBuildState() LogicalPlanBuildState { + planCacheUseCache, planCacheType, planCacheUnqualified, planCacheForce, planCacheAlwaysWarn := sc.PlanCacheTracker.Save() + return LogicalPlanBuildState{ + warnings: slices.Clone(sc.GetWarnings()), + extraWarnings: slices.Clone(sc.GetExtraWarnings()), + tables: slices.Clone(sc.Tables), + tableStats: maps.Clone(sc.TableStats), + lockTableIDs: maps.Clone(sc.LockTableIDs), + tblInfo2UnionScan: maps.Clone(sc.TblInfo2UnionScan), + useDynamicPruneMode: sc.UseDynamicPruneMode, + viewDepth: sc.ViewDepth, + colRefFromUpdatePlan: sc.ColRefFromUpdatePlan.Copy(), + planCacheUseCache: planCacheUseCache, + planCacheType: planCacheType, + planCacheUnqualified: planCacheUnqualified, + planCacheForce: planCacheForce, + planCacheAlwaysWarn: planCacheAlwaysWarn, + } +} + +// RestoreLogicalPlanBuildState restores the statement-scoped planner state after a +// discarded logical plan build attempt. +func (sc *StatementContext) RestoreLogicalPlanBuildState(state LogicalPlanBuildState) { + sc.SetWarnings(slices.Clone(state.warnings)) + sc.SetExtraWarnings(slices.Clone(state.extraWarnings)) + sc.Tables = slices.Clone(state.tables) + sc.TableStats = maps.Clone(state.tableStats) + sc.LockTableIDs = maps.Clone(state.lockTableIDs) + sc.TblInfo2UnionScan = maps.Clone(state.tblInfo2UnionScan) + sc.UseDynamicPruneMode = state.useDynamicPruneMode + sc.ViewDepth = state.viewDepth + sc.ColRefFromUpdatePlan.CopyFrom(state.colRefFromUpdatePlan) + sc.PlanCacheTracker.Restore(state.planCacheUseCache, state.planCacheType, state.planCacheUnqualified, state.planCacheForce, state.planCacheAlwaysWarn) + sc.RangeFallbackHandler = contextutil.NewRangeFallbackHandler(&sc.PlanCacheTracker, sc) +} + // CtxID returns the context id of the statement func (sc *StatementContext) CtxID() uint64 { return sc.ctxID diff --git a/pkg/sessionctx/stmtctx/stmtctx_test.go b/pkg/sessionctx/stmtctx/stmtctx_test.go index b7494004a4ae7..e6115eaa339c5 100644 --- a/pkg/sessionctx/stmtctx/stmtctx_test.go +++ b/pkg/sessionctx/stmtctx/stmtctx_test.go @@ -28,6 +28,8 @@ import ( "github.com/pingcap/errors" "github.com/pingcap/tidb/pkg/errctx" "github.com/pingcap/tidb/pkg/kv" + "github.com/pingcap/tidb/pkg/meta/model" + "github.com/pingcap/tidb/pkg/parser/ast" "github.com/pingcap/tidb/pkg/sessionctx/stmtctx" "github.com/pingcap/tidb/pkg/sessionctx/variable" "github.com/pingcap/tidb/pkg/testkit" @@ -219,6 +221,90 @@ func TestMarshalSQLWarn(t *testing.T) { tk.MustQuery("show warnings").Check(rows) } +func TestLogicalPlanBuildStateRestore(t *testing.T) { + sc := stmtctx.NewStmtCtx() + sc.AppendWarning(errors.New("baseline warning")) + sc.AppendExtraWarning(errors.New("baseline extra warning")) + sc.Tables = []stmtctx.TableEntry{{DB: "test", Table: "t"}} + sc.TableStats = map[int64]any{42: "baseline stats"} + sc.LockTableIDs = map[int64]struct{}{1: {}} + tblInfo := &model.TableInfo{ID: 42} + sc.TblInfo2UnionScan = map[*model.TableInfo]bool{tblInfo: true} + sc.UseDynamicPruneMode = true + sc.ViewDepth = 2 + sc.ColRefFromUpdatePlan.Insert(7) + sc.SetCacheType(contextutil.SessionNonPrepared) + sc.EnablePlanCache() + + state := sc.SaveLogicalPlanBuildState() + + sc.AppendWarning(errors.New("candidate warning")) + sc.AppendExtraWarning(errors.New("candidate extra warning")) + sc.Tables = []stmtctx.TableEntry{{DB: "candidate", Table: "t2"}} + sc.TableStats = map[int64]any{99: "candidate stats"} + sc.LockTableIDs[2] = struct{}{} + sc.TblInfo2UnionScan = map[*model.TableInfo]bool{{ID: 99}: false} + sc.UseDynamicPruneMode = false + sc.ViewDepth = 9 + sc.ColRefFromUpdatePlan.Insert(9) + sc.SetSkipPlanCache("candidate reason") + + sc.RestoreLogicalPlanBuildState(state) + + warnings := sc.GetWarnings() + require.Len(t, warnings, 1) + require.Equal(t, "baseline warning", warnings[0].Err.Error()) + + extraWarnings := sc.GetExtraWarnings() + require.Len(t, extraWarnings, 1) + require.Equal(t, "baseline extra warning", extraWarnings[0].Err.Error()) + + require.Equal(t, []stmtctx.TableEntry{{DB: "test", Table: "t"}}, sc.Tables) + require.Equal(t, map[int64]any{42: "baseline stats"}, sc.TableStats) + require.Equal(t, map[int64]struct{}{1: {}}, sc.LockTableIDs) + require.Equal(t, map[*model.TableInfo]bool{tblInfo: true}, sc.TblInfo2UnionScan) + require.True(t, sc.UseDynamicPartitionPrune()) + require.Equal(t, int32(2), sc.ViewDepth) + require.True(t, sc.ColRefFromUpdatePlan.Has(7)) + require.False(t, sc.ColRefFromUpdatePlan.Has(9)) + require.True(t, sc.UseCache()) + require.Empty(t, sc.PlanCacheUnqualified()) +} + +func TestQBHintHandlerBuildState(t *testing.T) { + handler := hint.NewQBHintHandler(nil) + handler.QBNameToSelOffset = map[string]int{"qb_1": 1} + handler.ViewQBNameToTable = map[string][]ast.HintTable{ + "view_qb": {{TableName: ast.NewCIStr("t")}}, + } + handler.ViewQBNameToHints = map[string][]*ast.TableOptimizerHint{ + "view_qb": {{HintName: ast.NewCIStr("merge_join")}}, + } + handler.Enter(&ast.SelectStmt{}) + handler.Enter(&ast.SelectStmt{}) + state := handler.NewBuildState() + hints := handler.GetCurrentStmtHints([]*ast.TableOptimizerHint{ + {HintName: ast.NewCIStr("use_index"), QBName: ast.NewCIStr("qb_1")}, + }, 1, state) + handler.MarkViewQBNameUsed("view_qb", state) + + require.Len(t, hints, 1) + require.Equal(t, "use_index", hints[0].HintName.L) + + require.Equal(t, 2, handler.MaxSelectStmtOffset()) + require.Equal(t, map[string]int{"qb_1": 1}, handler.QBNameToSelOffset) + require.Equal(t, map[string][]*ast.TableOptimizerHint{ + "view_qb": {{HintName: ast.NewCIStr("merge_join")}}, + }, handler.ViewQBNameToHints) + require.Equal(t, map[string][]ast.HintTable{ + "view_qb": {{TableName: ast.NewCIStr("t")}}, + }, handler.ViewQBNameToTable) + require.Equal(t, map[int][]*ast.TableOptimizerHint{ + 1: {{HintName: ast.NewCIStr("use_index"), QBName: ast.NewCIStr("qb_1")}}, + }, state.QBOffsetToHints) + require.Equal(t, map[string]struct{}{"view_qb": {}}, state.ViewQBNameUsed) +} + func TestApproxRuntimeInfo(t *testing.T) { var n = rand.Intn(19000) + 1000 var valRange = rand.Int31n(10000) + 1000 diff --git a/pkg/util/context/plancache.go b/pkg/util/context/plancache.go index 9cac5ec81012e..d6c46cd4db81e 100644 --- a/pkg/util/context/plancache.go +++ b/pkg/util/context/plancache.go @@ -122,6 +122,26 @@ func (h *PlanCacheTracker) EnablePlanCache() { h.useCache = true } +// Save captures the mutable planning-time state of the tracker. +func (h *PlanCacheTracker) Save() (useCache bool, cacheType PlanCacheType, planCacheUnqualified string, forcePlanCache bool, alwaysWarnSkipCache bool) { + h.mu.Lock() + defer h.mu.Unlock() + + return h.useCache, h.cacheType, h.planCacheUnqualified, h.forcePlanCache, h.alwaysWarnSkipCache +} + +// Restore restores the mutable planning-time state of the tracker. +func (h *PlanCacheTracker) Restore(useCache bool, cacheType PlanCacheType, planCacheUnqualified string, forcePlanCache bool, alwaysWarnSkipCache bool) { + h.mu.Lock() + defer h.mu.Unlock() + + h.useCache = useCache + h.cacheType = cacheType + h.planCacheUnqualified = planCacheUnqualified + h.forcePlanCache = forcePlanCache + h.alwaysWarnSkipCache = alwaysWarnSkipCache +} + // UseCache returns whether to use plan cache. func (h *PlanCacheTracker) UseCache() bool { h.mu.Lock() diff --git a/pkg/util/hint/hint_query_block.go b/pkg/util/hint/hint_query_block.go index eb85910b59827..0262e1b53e6a3 100644 --- a/pkg/util/hint/hint_query_block.go +++ b/pkg/util/hint/hint_query_block.go @@ -30,18 +30,23 @@ import ( // In both cases, the `use_index` hint doesn't take effect directly, since a specific qb_name is specified, and this // QBHintHandler is used to handle this cases. type QBHintHandler struct { - QBNameToSelOffset map[string]int // map[QBName]SelectOffset - QBOffsetToHints map[int][]*ast.TableOptimizerHint // map[QueryBlockOffset]Hints + QBNameToSelOffset map[string]int // map[QBName]SelectOffset // Used for the view's hint ViewQBNameToTable map[string][]ast.HintTable // map[QBName]HintedTable ViewQBNameToHints map[string][]*ast.TableOptimizerHint // map[QBName]Hints - ViewQBNameUsed map[string]struct{} // map[QBName]Used warnHandler hintWarnHandler selectStmtOffset int } +// QBHintBuildState stores the per-build runtime state for a QBHintHandler. +// The handler itself only keeps AST-derived metadata and can be shared safely. +type QBHintBuildState struct { + QBOffsetToHints map[int][]*ast.TableOptimizerHint // map[QueryBlockOffset]Hints + ViewQBNameUsed map[string]struct{} // map[QBName]Used +} + // hintWarnHandler is used to handle the warning when parsing hints. type hintWarnHandler interface { SetHintWarning(warn string) @@ -55,6 +60,20 @@ func NewQBHintHandler(warnHandler hintWarnHandler) *QBHintHandler { } } +// NewBuildState creates the per-build runtime state for the handler. +func (p *QBHintHandler) NewBuildState() *QBHintBuildState { + if p == nil { + return nil + } + + state := &QBHintBuildState{} + if len(p.ViewQBNameToTable) == 0 { + return state + } + state.ViewQBNameUsed = make(map[string]struct{}, len(p.ViewQBNameToTable)) + return state +} + // MaxSelectStmtOffset returns the current stmt offset. func (p *QBHintHandler) MaxSelectStmtOffset() int { return p.selectStmtOffset @@ -132,7 +151,6 @@ func (p *QBHintHandler) handleViewHints(hints []*ast.TableOptimizerHint, offset usedHints[i] = true if p.ViewQBNameToTable == nil { p.ViewQBNameToTable = make(map[string][]ast.HintTable) - p.ViewQBNameUsed = make(map[string]struct{}) } qbName := hint.QBName.L if qbName == "" { @@ -201,15 +219,21 @@ func (p *QBHintHandler) handleViewHints(hints []*ast.TableOptimizerHint, offset } // HandleUnusedViewHints handle the unused view hints. -func (p *QBHintHandler) HandleUnusedViewHints() { +func (p *QBHintHandler) HandleUnusedViewHints(state *QBHintBuildState, warn []string) []string { + if state == nil { + return warn + } + + warn = warn[:0] if p.ViewQBNameToTable != nil { for qbName := range p.ViewQBNameToTable { - _, ok := p.ViewQBNameUsed[qbName] + _, ok := state.ViewQBNameUsed[qbName] if !ok && p.warnHandler != nil { - p.warnHandler.SetHintWarning(fmt.Sprintf("The qb_name hint %s is unused, please check whether the table list in the qb_name hint %s is correct", qbName, qbName)) + warn = append(warn, fmt.Sprintf("The qb_name hint %s is unused, please check whether the table list in the qb_name hint %s is correct", qbName, qbName)) } } } + return warn } const ( @@ -242,6 +266,16 @@ func (p *QBHintHandler) getBlockOffset(blockName ast.CIStr) int { return -1 } +// SetWarns set the warning from a list of strings. +func (p *QBHintHandler) SetWarns(warns []string) { + if p == nil || p.warnHandler == nil || len(warns) == 0 { + return + } + for _, one := range warns { + p.warnHandler.SetHintWarning(one) + } +} + // GetHintOffset gets the offset of stmt that the hints take effects. func (p *QBHintHandler) GetHintOffset(qbName ast.CIStr, currentOffset int) int { if qbName.L != "" { @@ -279,9 +313,12 @@ func (p *QBHintHandler) isHint4View(hint *ast.TableOptimizerHint) bool { } // GetCurrentStmtHints extracts all hints that take effects at current stmt. -func (p *QBHintHandler) GetCurrentStmtHints(hints []*ast.TableOptimizerHint, currentOffset int) []*ast.TableOptimizerHint { - if p.QBOffsetToHints == nil { - p.QBOffsetToHints = make(map[int][]*ast.TableOptimizerHint) +func (p *QBHintHandler) GetCurrentStmtHints(hints []*ast.TableOptimizerHint, currentOffset int, state *QBHintBuildState) []*ast.TableOptimizerHint { + if state == nil { + state = &QBHintBuildState{} + } + if state.QBOffsetToHints == nil { + state.QBOffsetToHints = make(map[int][]*ast.TableOptimizerHint) } for _, hint := range hints { if hint.HintName.L == hintQBName { @@ -295,11 +332,19 @@ func (p *QBHintHandler) GetCurrentStmtHints(hints []*ast.TableOptimizerHint, cur } continue } - if !slices.Contains(p.QBOffsetToHints[offset], hint) { - p.QBOffsetToHints[offset] = append(p.QBOffsetToHints[offset], hint) + if !slices.Contains(state.QBOffsetToHints[offset], hint) { + state.QBOffsetToHints[offset] = append(state.QBOffsetToHints[offset], hint) } } - return p.QBOffsetToHints[currentOffset] + return state.QBOffsetToHints[currentOffset] +} + +// MarkViewQBNameUsed records that the named view qb hint was used in the current build. +func (*QBHintHandler) MarkViewQBNameUsed(qbName string, state *QBHintBuildState) { + if state == nil || state.ViewQBNameUsed == nil { + return + } + state.ViewQBNameUsed[qbName] = struct{}{} } // GenerateQBName builds QBName from offset.