From b4d17064aab4cbac8886588a2c66928fc38744c9 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 13:36:52 -0400 Subject: [PATCH 01/12] sql/opt: extract buildSQLRoutineBodyStmts from buildRoutine Extract the body-building loop from `buildRoutine` into a standalone method `buildSQLRoutineBodyStmts`. No behavioral change. A follow-up commit will add a deferred-build path that skips this call and instead captures the ASTs for later building at execution time. Release note: None --- pkg/sql/opt/optbuilder/routine.go | 74 ++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/pkg/sql/opt/optbuilder/routine.go b/pkg/sql/opt/optbuilder/routine.go index c5961db10cb3..042e0d028edd 100644 --- a/pkg/sql/opt/optbuilder/routine.go +++ b/pkg/sql/opt/optbuilder/routine.go @@ -463,32 +463,9 @@ func (b *Builder) buildRoutine( }) appendedNullForVoidReturn = true } - body = make([]memo.RelExpr, len(stmts)) - bodyProps = make([]*physical.Required, len(stmts)) - bodyTags = make([]string, len(stmts)) - bodyASTs = make([]tree.Statement, len(stmts)) - for i := range stmts { - // TODO(michae2): We should be checking the statement hints cache here to - // find any external statement hints that could apply to this statement. - stmtScope := b.buildStmtAtRootWithScope(stmts[i].AST, nil /* desiredTypes */, bodyScope) - - // The last statement produces the output of the UDF. - if i == len(stmts)-1 { - rTyp := b.finalizeRoutineReturnType(f, stmtScope, inScope, oldInsideDataSource) - stmtScope = b.finishRoutineReturnStmt(stmtScope, isSetReturning, oldInsideDataSource, rTyp) - } - body[i] = stmtScope.expr - bodyProps[i] = stmtScope.makePhysicalProps() - bodyASTs[i] = stmts[i].AST - // We don't need a statement tag for the artificial appended `SELECT NULL` - // statement. - if appendedNullForVoidReturn && i == len(stmts)-1 { - bodyTags[i] = "" - } else { - bodyTags[i] = stmts[i].AST.StatementTag() - } - - } + body, bodyProps, bodyTags, bodyASTs = b.buildSQLRoutineBodyStmts( + stmts, bodyScope, f, inScope, isSetReturning, oldInsideDataSource, appendedNullForVoidReturn, + ) if b.verboseTracing { bodyStmts = make([]string, len(stmts)) @@ -569,6 +546,51 @@ func (b *Builder) buildRoutine( return routine } +// buildSQLRoutineBodyStmts eagerly builds the body statements of a SQL routine +// into RelExprs. Each statement is built and type-checked against the routine's +// return type. +func (b *Builder) buildSQLRoutineBodyStmts( + stmts statements.Statements, + bodyScope *scope, + f *tree.FuncExpr, + inScope *scope, + isSetReturning bool, + insideDataSource bool, + appendedNullForVoidReturn bool, +) ( + body []memo.RelExpr, + bodyProps []*physical.Required, + bodyTags []string, + bodyASTs []tree.Statement, +) { + body = make([]memo.RelExpr, len(stmts)) + bodyProps = make([]*physical.Required, len(stmts)) + bodyTags = make([]string, len(stmts)) + bodyASTs = make([]tree.Statement, len(stmts)) + for i := range stmts { + // TODO(michae2): We should be checking the statement hints cache here to + // find any external statement hints that could apply to this statement. + stmtScope := b.buildStmtAtRootWithScope(stmts[i].AST, nil /* desiredTypes */, bodyScope) + + // The last statement produces the output of the UDF. + if i == len(stmts)-1 { + rTyp := b.finalizeRoutineReturnType(f, stmtScope, inScope, insideDataSource) + stmtScope = b.finishRoutineReturnStmt(stmtScope, isSetReturning, insideDataSource, rTyp) + } + body[i] = stmtScope.expr + bodyProps[i] = stmtScope.makePhysicalProps() + bodyASTs[i] = stmts[i].AST + // We don't need a statement tag for the artificial appended `SELECT NULL` + // statement. + if appendedNullForVoidReturn && i == len(stmts)-1 { + bodyTags[i] = "" + } else { + bodyTags[i] = stmts[i].AST.StatementTag() + } + } + return body, bodyProps, bodyTags, bodyASTs +} + // finishRoutineReturnStmt manages the output columns for a statement that will // be added to the result set of a routine. Depending on the context and return // type of the routine, this may mean expanding a tuple into multiple columns, From cd372b7456da6395b03976ac5ff1d940b3f33c75 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 13:37:00 -0400 Subject: [PATCH 02/12] sql/opt: add RoutineBodyBuilder interface and sqlRoutineBodyBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `RoutineBodyBuilder` interface in `memo`, modeled after `PostQueryBuilder`, to defer building of SQL routine body statements to execution time. Add a `BodyBuilder` field to `UDFDefinition`. Implement `sqlRoutineBodyBuilder` in `optbuilder/routine.go`, which captures metadata at plan time (parameter types, privilege context, statement tree snapshot, ASTs) and builds body RelExprs in a fresh Builder at execution time, following the `buildTriggerCascadeHelper` pattern used for FK cascades and AFTER triggers. Add `GetInitFnForDeferredRoutine` to `statementTree`. Unlike `GetInitFnForPostQuery` which excludes the current stack level, this captures ALL levels. The difference is that post-queries are children of the current-level mutation (e.g. a cascade triggered by a DELETE), while deferred routines are siblings. For example: UPDATE t SET x = my_udf(); -- my_udf() body: INSERT INTO t VALUES (1) Both the outer UPDATE and the UDF body mutate `t`. Without capturing the current level, the deferred body would see an empty statement tree and miss the conflict. Add nil-Body guards across execbuilder, memo formatter, and norm factory so existing code tolerates a deferred-build `UDFDefinition`. Nothing uses these yet — no behavioral change. Release note: None --- pkg/sql/opt/exec/execbuilder/relational.go | 2 +- pkg/sql/opt/exec/execbuilder/scalar.go | 33 +- pkg/sql/opt/memo/expr.go | 28 +- pkg/sql/opt/memo/expr_format.go | 52 +-- pkg/sql/opt/norm/factory.go | 11 +- pkg/sql/opt/optbuilder/routine.go | 207 ++++++++--- pkg/sql/opt/optbuilder/statement_tree.go | 64 +++- pkg/sql/opt/optbuilder/statement_tree_test.go | 323 +++++++++++++++++- 8 files changed, 625 insertions(+), 95 deletions(-) diff --git a/pkg/sql/opt/exec/execbuilder/relational.go b/pkg/sql/opt/exec/execbuilder/relational.go index 483a0fccc084..0c7cfa3d848a 100644 --- a/pkg/sql/opt/exec/execbuilder/relational.go +++ b/pkg/sql/opt/exec/execbuilder/relational.go @@ -3716,7 +3716,6 @@ func (b *Builder) buildCall(c *memo.CallExpr) (_ execPlan, outputCols colOrdMap, b.setMutationFlags(s) } } - // Create a tree.RoutinePlanFn that can plan the statements in the UDF body. planGen := b.buildRoutinePlanGenerator( udf.Def.Params, @@ -3728,6 +3727,7 @@ func (b *Builder) buildCall(c *memo.CallExpr) (_ execPlan, outputCols colOrdMap, false, /* allowOuterWithRefs */ nil, /* wrapRootExpr */ 0, /* resultBufferID */ + nil, /* bodyBuilder */ ) r := tree.NewTypedRoutineExpr( diff --git a/pkg/sql/opt/exec/execbuilder/scalar.go b/pkg/sql/opt/exec/execbuilder/scalar.go index 8e93fa88a086..a2ae30126ea1 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -701,7 +701,8 @@ func (b *Builder) buildExistsSubquery( nil, /* stmtASTs */ true, /* allowOuterWithRefs */ wrapRootExpr, - 0, /* resultBufferID */ + 0, /* resultBufferID */ + nil, /* bodyBuilder */ ) return tree.NewTypedCoalesceExpr(tree.TypedExprs{ tree.NewTypedRoutineExpr( @@ -830,6 +831,7 @@ func (b *Builder) buildSubquery( true, /* allowOuterWithRefs */ nil, /* wrapRootExpr */ 0, /* resultBufferID */ + nil, /* bodyBuilder */ ) _, tailCall := b.tailCalls[subquery] return tree.NewTypedRoutineExpr( @@ -1001,7 +1003,7 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ // Execution expects there to be more than one body statement if a cursor is // opened or the result of the first statement is directed to a buffer. firstStmtOut := udf.Def.FirstStmtOutput - if len(udf.Def.Body) <= 1 && + if udf.Def.Body != nil && len(udf.Def.Body) <= 1 && (firstStmtOut.CursorDeclaration != nil || firstStmtOut.TargetBufferID != 0) { panic(errors.AssertionFailedf( "expected more than one body statement for a routine that " + @@ -1026,6 +1028,7 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ false, /* allowOuterWithRefs */ nil, /* wrapRootExpr */ udf.Def.ResultBufferID, + nil, /* bodyBuilder */ ) // Enable stepping for volatile functions so that statements within the UDF @@ -1101,6 +1104,7 @@ func (b *Builder) initRoutineExceptionHandler( false, /* allowOuterWithRefs */ nil, /* wrapRootExpr */ 0, /* resultBufferID */ + nil, /* bodyBuilder */ ) // Build a routine with no arguments for the exception handler. The actual // arguments will be supplied when (if) the handler is invoked. @@ -1152,6 +1156,7 @@ func (b *Builder) buildRoutinePlanGenerator( allowOuterWithRefs bool, wrapRootExpr wrapRootExprFn, resultBufferID memo.RoutineResultBufferID, + bodyBuilder memo.RoutineBodyBuilder, ) tree.RoutinePlanGenerator { // argOrd returns the ordinal of the argument within the arguments list that // can be substituted for each reference to the given function parameter @@ -1199,6 +1204,30 @@ func (b *Builder) buildRoutinePlanGenerator( log.VEventf(ctx, 1, "%v", caughtErr) }) + // If a BodyBuilder is set, build the routine body now (deferred from + // plan time to execution time). + if bodyBuilder != nil { + var tmpO xform.Optimizer + tmpO.Init(ctx, b.evalCtx, b.catalog) + builtBody, builtProps, newParams, err := bodyBuilder.Build( + ctx, b.semaCtx, b.evalCtx, b.catalog, tmpO.Factory(), + ) + if err != nil { + return err + } + stmts = builtBody + stmtProps = builtProps + originalMemo = tmpO.Factory().Memo() + argOrd = func(col opt.ColumnID) (ord int, ok bool) { + for i, param := range newParams { + if col == param { + return i, true + } + } + return 0, false + } + } + dbName := b.evalCtx.SessionData().Database appName := b.evalCtx.SessionData().ApplicationName // TODO(yuzefovich): look into computing fingerprintFormat lazily. diff --git a/pkg/sql/opt/memo/expr.go b/pkg/sql/opt/memo/expr.go index d9fa95166e4e..9c7dbcc6780e 100644 --- a/pkg/sql/opt/memo/expr.go +++ b/pkg/sql/opt/memo/expr.go @@ -734,7 +734,8 @@ type UDFDefinition struct { Params opt.ColList // Body contains a relational expression for each statement in the function - // body. It is unset during construction of a recursive UDF. + // body. It is nil during construction of a recursive UDF, and when body + // building is deferred to execution time (see BodyBuilder). Body []RelExpr // BodyProps contains the physical properties with which each body statement @@ -777,6 +778,11 @@ type UDFDefinition struct { // results to the same buffer. This is used to implement the PL/pgsql // RETURN NEXT and RETURN QUERY statements. ResultBufferID RoutineResultBufferID + + // BodyBuilder, when non-nil, defers body building to execution time. + // When set, Body and BodyProps are nil at plan time; they will be + // populated by calling BodyBuilder.Build() at execution time. + BodyBuilder RoutineBodyBuilder } // ExceptionBlock contains the information needed to match and handle errors in @@ -1442,6 +1448,26 @@ type PostQueryBuilder interface { ) (RelExpr, error) } +// RoutineBodyBuilder defers building of SQL routine body statements to +// execution time. At plan time, the builder captures metadata (parameter +// types, privilege context, statement tree state). At execution time, +// Build constructs the body RelExprs in a fresh memo. +// +// Like PostQueryBuilder, Build does not mutate captured state; it is +// safe to call concurrently if the plan is cached and reused. +// +// Note: factory is always *norm.Factory; declared as interface{} to +// avoid circular package dependencies. +type RoutineBodyBuilder interface { + Build( + ctx context.Context, + semaCtx *tree.SemaContext, + evalCtx *eval.Context, + catalog cat.Catalog, + factory interface{}, + ) (body []RelExpr, bodyProps []*physical.Required, params opt.ColList, err error) +} + // GroupingOrderType is the grouping column order type for group by and distinct // operations in the memo. type GroupingOrderType int diff --git a/pkg/sql/opt/memo/expr_format.go b/pkg/sql/opt/memo/expr_format.go index 1c952122f316..e77e1800f8b2 100644 --- a/pkg/sql/opt/memo/expr_format.go +++ b/pkg/sql/opt/memo/expr_format.go @@ -1082,30 +1082,40 @@ func (f *ExprFmtCtx) formatScalarWithLabel( if len(def.Params) > 0 { f.formatColList(tp, "params:", def.Params, opt.ColSet{} /* notNullCols */) } - n := tp.Child("body") - for i := range def.Body { - stmtNode := n - if i == 0 { - if def.FirstStmtOutput.CursorDeclaration != nil { - // The first statement is opening a cursor. - stmtNode = n.Child("open-cursor") - } else if def.FirstStmtOutput.TargetBufferID != 0 { - // The first statement is writing to a target buffer. - stmtNode = n.Child("add-to-srf-result") + if def.Body != nil { + n := tp.Child("body") + for i := range def.Body { + stmtNode := n + if i == 0 { + if def.FirstStmtOutput.CursorDeclaration != nil { + // The first statement is opening a cursor. + stmtNode = n.Child("open-cursor") + } else if def.FirstStmtOutput.TargetBufferID != 0 { + // The first statement is writing to a target buffer. + stmtNode = n.Child("add-to-srf-result") + } + } + prevTailCalls := f.tailCalls + + // Routine calls in the last body statement may be tail calls if + // ResultBufferID is unset. If it is set, the result of the last + // body statement is not directly used as the result of the UDF + // call, so it cannot contain tail calls. + if i == len(def.Body)-1 && def.ResultBufferID == 0 { + f.tailCalls = make(map[opt.ScalarExpr]struct{}) + ExtractTailCalls(def.Body[i], f.tailCalls) } + f.formatExpr(def.Body[i], stmtNode) + f.tailCalls = prevTailCalls } - prevTailCalls := f.tailCalls - - // Routine calls in the last body statement may be tail calls if - // ResultBufferID is unset. If it is set, the result of the last body - // statement is not directly used as the result of the UDF call, so it - // cannot contain tail calls. - if i == len(def.Body)-1 && def.ResultBufferID == 0 { - f.tailCalls = make(map[opt.ScalarExpr]struct{}) - ExtractTailCalls(def.Body[i], f.tailCalls) + } else { + // Deferred-build routine: body is not yet built. Show ASTs. + n := tp.Child("body (deferred)") + for i, ast := range def.BodyASTs { + if ast != nil { + n.Childf("stmt%d: %s", i+1, tree.AsString(ast)) + } } - f.formatExpr(def.Body[i], stmtNode) - f.tailCalls = prevTailCalls } delete(f.withinUDFs, def) } else if _, recursive := f.withinUDFs[def]; recursive { diff --git a/pkg/sql/opt/norm/factory.go b/pkg/sql/opt/norm/factory.go index b6e134919b26..c098b8ae96cf 100644 --- a/pkg/sql/opt/norm/factory.go +++ b/pkg/sql/opt/norm/factory.go @@ -379,10 +379,13 @@ func (f *Factory) AssignPlaceholders(from *memo.Memo) (retErr error) { newDef = &defCopy newRoutineDefs[t.Def] = newDef // Make sure to copy the slice that stores the body statements, rather - // than mutating the original. - newDef.Body = make([]memo.RelExpr, len(t.Def.Body)) - for i := range t.Def.Body { - newDef.Body[i] = f.CopyAndReplaceDefault(t.Def.Body[i], replaceFn).(memo.RelExpr) + // than mutating the original. BodyBuilder is immutable and + // memo-independent; the struct copy above suffices. + if t.Def.Body != nil { + newDef.Body = make([]memo.RelExpr, len(t.Def.Body)) + for i := range t.Def.Body { + newDef.Body[i] = f.CopyAndReplaceDefault(t.Def.Body[i], replaceFn).(memo.RelExpr) + } } } return f.ConstructUDFCall(newArgs, &memo.UDFCallPrivate{Def: newDef}) diff --git a/pkg/sql/opt/optbuilder/routine.go b/pkg/sql/opt/optbuilder/routine.go index 042e0d028edd..5087ef0ef6e1 100644 --- a/pkg/sql/opt/optbuilder/routine.go +++ b/pkg/sql/opt/optbuilder/routine.go @@ -6,24 +6,28 @@ package optbuilder import ( + "context" "strings" "github.com/cockroachdb/cockroach/pkg/security/username" "github.com/cockroachdb/cockroach/pkg/sql/catalog/funcdesc" "github.com/cockroachdb/cockroach/pkg/sql/opt" + "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" + "github.com/cockroachdb/cockroach/pkg/sql/opt/norm" "github.com/cockroachdb/cockroach/pkg/sql/opt/props/physical" "github.com/cockroachdb/cockroach/pkg/sql/parser" - "github.com/cockroachdb/cockroach/pkg/sql/parser/statements" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" plpgsql "github.com/cockroachdb/cockroach/pkg/sql/plpgsql/parser" "github.com/cockroachdb/cockroach/pkg/sql/sem/cast" + "github.com/cockroachdb/cockroach/pkg/sql/sem/eval" "github.com/cockroachdb/cockroach/pkg/sql/sem/plpgsqltree" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/sem/volatility" "github.com/cockroachdb/cockroach/pkg/sql/types" "github.com/cockroachdb/cockroach/pkg/util/buildutil" + "github.com/cockroachdb/cockroach/pkg/util/errorutil" "github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented" "github.com/cockroachdb/errors" ) @@ -446,27 +450,19 @@ func (b *Builder) buildRoutine( panic(err) } - var appendedNullForVoidReturn bool - // Add a VALUES (NULL) statement if the return type of the function is - // VOID. We cannot simply project NULL from the last statement because - // all columns would be pruned and the contents of last statement would - // not be executed. - // TODO(mgartner): This will add some planning overhead for every - // invocation of the function. Is there a more efficient way to do this? - if f.ResolvedType().Family() == types.VoidFamily { - stmts = append(stmts, statements.Statement[tree.Statement]{ - AST: &tree.Select{ - Select: &tree.ValuesClause{ - Rows: []tree.Exprs{{tree.DNull}}, - }, - }, - }) - appendedNullForVoidReturn = true + // Extract the ASTs for buildSQLRoutineBodyStmts. + stmtASTs := make([]tree.Statement, len(stmts)) + for i := range stmts { + stmtASTs[i] = stmts[i].AST } - body, bodyProps, bodyTags, bodyASTs = b.buildSQLRoutineBodyStmts( - stmts, bodyScope, f, inScope, isSetReturning, oldInsideDataSource, appendedNullForVoidReturn, + body, bodyProps, bodyTags = b.buildSQLRoutineBodyStmts( + stmtASTs, bodyScope, f.ResolvedType(), f, inScope, isSetReturning, + oldInsideDataSource, ) + // Collect the original (pre-VOID-append) ASTs for the UDFDefinition. + bodyASTs = stmtASTs + if b.verboseTracing { bodyStmts = make([]string, len(stmts)) for i := range stmts { @@ -546,49 +542,91 @@ func (b *Builder) buildRoutine( return routine } -// buildSQLRoutineBodyStmts eagerly builds the body statements of a SQL routine -// into RelExprs. Each statement is built and type-checked against the routine's -// return type. +// buildSQLRoutineBodyStmts builds the body statements of a SQL routine into +// RelExprs. It is used on both the eager path (plan-time) and the deferred path +// (execution-time), distinguished by funcExpr: +// +// - Eager path (funcExpr != nil): called at plan time from +// buildRoutine. The return type may still need finalization (e.g. resolving +// AnyTuple for RETURNS RECORD routines), so finalizeRoutineReturnType is +// called. funcExpr and inScope are required for this +// finalization. +// +// - Deferred path (funcExpr == nil): called at execution time from +// sqlRoutineBodyBuilder.Build. The return type (rTyp) was already resolved +// and persisted at plan time — AnyTuple and ReturnsRecordType are always +// resolved before reaching this path. Only validateReturnType is needed to +// confirm that the rebuilt body columns are still compatible. +// +// rTyp is the routine's return type on both paths: on the eager path it is +// funcExpr.ResolvedType(); on the deferred path it is the type captured +// at plan time. It is always used for the VOID-return check (appending +// VALUES (NULL)). func (b *Builder) buildSQLRoutineBodyStmts( - stmts statements.Statements, + stmtASTs []tree.Statement, bodyScope *scope, - f *tree.FuncExpr, + rTyp *types.T, + funcExpr *tree.FuncExpr, inScope *scope, isSetReturning bool, insideDataSource bool, - appendedNullForVoidReturn bool, -) ( - body []memo.RelExpr, - bodyProps []*physical.Required, - bodyTags []string, - bodyASTs []tree.Statement, -) { - body = make([]memo.RelExpr, len(stmts)) - bodyProps = make([]*physical.Required, len(stmts)) - bodyTags = make([]string, len(stmts)) - bodyASTs = make([]tree.Statement, len(stmts)) - for i := range stmts { +) (body []memo.RelExpr, bodyProps []*physical.Required, bodyTags []string) { + // Add a VALUES (NULL) statement if the return type of the function is + // VOID. We cannot simply project NULL from the last statement because + // all columns would be pruned and the contents of last statement would + // not be executed. + // TODO(mgartner): This will add some planning overhead for every + // invocation of the function. Is there a more efficient way to do this? + var appendedNullForVoidReturn bool + if rTyp.Family() == types.VoidFamily { + stmtASTs = append(append([]tree.Statement(nil), stmtASTs...), &tree.Select{ + Select: &tree.ValuesClause{ + Rows: []tree.Exprs{{tree.DNull}}, + }, + }) + appendedNullForVoidReturn = true + } + + body = make([]memo.RelExpr, len(stmtASTs)) + bodyProps = make([]*physical.Required, len(stmtASTs)) + bodyTags = make([]string, len(stmtASTs)) + for i, ast := range stmtASTs { // TODO(michae2): We should be checking the statement hints cache here to // find any external statement hints that could apply to this statement. - stmtScope := b.buildStmtAtRootWithScope(stmts[i].AST, nil /* desiredTypes */, bodyScope) + stmtScope := b.buildStmtAtRootWithScope(ast, nil /* desiredTypes */, bodyScope) - // The last statement produces the output of the UDF. - if i == len(stmts)-1 { - rTyp := b.finalizeRoutineReturnType(f, stmtScope, inScope, insideDataSource) + // The last statement produces the output of the routine. + if i == len(stmtASTs)-1 { + if funcExpr != nil { + // Eager path: finalize the return type. This handles AnyTuple + // resolution for RETURNS RECORD routines, column-definition-list + // validation for data-source usage, and type annotation on the + // FuncExpr. See finalizeRoutineReturnType for details. + rTyp = b.finalizeRoutineReturnType( + funcExpr, stmtScope, inScope, insideDataSource, + ) + } else { + // Deferred path: the return type is already resolved. Just + // validate that the rebuilt body columns are compatible. + if err := validateReturnType( + b.ctx, b.semaCtx, rTyp, stmtScope.cols, + ); err != nil { + panic(err) + } + } stmtScope = b.finishRoutineReturnStmt(stmtScope, isSetReturning, insideDataSource, rTyp) } body[i] = stmtScope.expr bodyProps[i] = stmtScope.makePhysicalProps() - bodyASTs[i] = stmts[i].AST // We don't need a statement tag for the artificial appended `SELECT NULL` // statement. - if appendedNullForVoidReturn && i == len(stmts)-1 { + if appendedNullForVoidReturn && i == len(stmtASTs)-1 { bodyTags[i] = "" } else { - bodyTags[i] = stmts[i].AST.StatementTag() + bodyTags[i] = ast.StatementTag() } } - return body, bodyProps, bodyTags, bodyASTs + return body, bodyProps, bodyTags } // finishRoutineReturnStmt manages the output columns for a statement that will @@ -957,6 +995,87 @@ func (b *Builder) buildDo(do *tree.DoBlock, inScope *scope) *scope { return outScope } +// sqlRoutineBodyBuilder implements memo.RoutineBodyBuilder for SQL routines. +// It captures all metadata needed to build the routine body at execution time +// instead of plan time. +type sqlRoutineBodyBuilder struct { + stmtASTs []tree.Statement + paramTypes []*types.T + paramNames []tree.Name + rTyp *types.T + isSetReturning bool + insideDataSource bool + privilegeUser string + routineType tree.RoutineType + stmtTreeInitFn func() statementTree +} + +var _ memo.RoutineBodyBuilder = &sqlRoutineBodyBuilder{} + +// Build constructs the body RelExprs for a SQL routine in a fresh memo. +// It is called at execution time, after plan-time metadata has been captured. +func (rb *sqlRoutineBodyBuilder) Build( + ctx context.Context, + semaCtx *tree.SemaContext, + evalCtx *eval.Context, + catalog cat.Catalog, + factoryI interface{}, +) (body []memo.RelExpr, bodyProps []*physical.Required, params opt.ColList, retErr error) { + // Enact panic handling similar to Builder.Build(). + defer errorutil.MaybeCatchPanic(&retErr, nil /* errCallback */) + + factory := factoryI.(*norm.Factory) + b := New(ctx, semaCtx, evalCtx, catalog, factory, nil /* stmt */) + + // Initialize the statement tree from the captured init function. + if rb.stmtTreeInitFn != nil { + b.stmtTree = rb.stmtTreeInitFn() + } + + // Configure the builder for routine body building. + b.insideUDF = true + b.insideSQLRoutine = true + b.trackSchemaDeps = false + + // Builtin routines need access to internal tables. + if rb.routineType == tree.BuiltinRoutine { + defer b.DisableUnsafeInternalCheck()() + } + + // Restore the effective privilege user for SECURITY DEFINER contexts. + if rb.privilegeUser != "" { + privUser := username.MakeSQLUsernameFromPreNormalizedString(rb.privilegeUser) + b.dataSourcePrivilegeUserOverride = privUser + b.executePrivilegeUserOverride = privUser + } + + // Create body scope with parameter columns. + bodyScope := b.allocScope() + params = make(opt.ColList, len(rb.paramTypes)) + for i := range rb.paramTypes { + var name tree.Name + if i < len(rb.paramNames) { + name = rb.paramNames[i] + } + argColName := funcParamColName(name, i) + col := b.synthesizeColumn( + bodyScope, argColName, rb.paramTypes[i], nil /* expr */, nil, /* scalar */ + ) + col.setParamOrd(i) + params[i] = col.id + } + + // Build the body statements using the shared helper. Pass + // funcExpr=nil to indicate the deferred path (return type is already + // resolved). + body, bodyProps, _ = b.buildSQLRoutineBodyStmts( + rb.stmtASTs, bodyScope, rb.rTyp, + nil /* funcExpr */, nil, /* inScope */ + rb.isSetReturning, rb.insideDataSource, + ) + return body, bodyProps, params, nil +} + // buildDoBody builds the body of the anonymous routine for a DO statement. func (b *Builder) buildPLpgSQLDoBody(do *plpgsqltree.DoBlock) *scope { // Build an expression for each statement in the function body. diff --git a/pkg/sql/opt/optbuilder/statement_tree.go b/pkg/sql/opt/optbuilder/statement_tree.go index b30b61a792da..88ef4e1f0539 100644 --- a/pkg/sql/opt/optbuilder/statement_tree.go +++ b/pkg/sql/opt/optbuilder/statement_tree.go @@ -61,6 +61,17 @@ import ( // the initialized statement tree. Note that some care must be taken to ensure // that the statementTreeNode references remain valid and up-to-date (see the // stmts comment below). +// +// +--------------------+ +// | Deferred Routines | +// +--------------------+ +// Deferred routines are similar to AFTER triggers: their body statements are +// not built into RelExprs at plan time but deferred to execution time (see +// RoutineBodyBuilder). Like AFTER triggers, their mutation checks must also +// be deferred. We use the same mechanism: GetInitFnForDeferredRoutine captures +// references to the ancestor statementTreeNodes so that the deferred builder +// can initialize a statement tree that includes all ancestor mutations at +// build time. type statementTree struct { // stmts is a stack of statement nodes, as described in the struct comment. // It is a slice of pointers to ensure that slice appends don't invalidate @@ -209,12 +220,25 @@ func (st *statementTree) CanMutateTable( return true } +// GetInitFnForDeferredRoutine returns a function that can be used to initialize +// the statement tree for a deferred-build SQL routine. Unlike +// GetInitFnForPostQuery, this method captures ALL stack levels including the +// current one, because CTE mutations register at the current level and must be +// visible to the deferred routine's conflict checks. +func (st *statementTree) GetInitFnForDeferredRoutine() func() statementTree { + return st.getInitFn(st.stmts) +} + // GetInitFnForPostQuery returns a function that can be used to initialize the // statement tree for a post-query that is a child of the current statement. // This is necessary because the post-query is not built until after the main // statement has finished executing. The returned function may be nil if the // statement tree does not need to be initialized for the post-query. // +// Unlike GetInitFnForDeferredRoutine, this excludes the current level because +// post-queries are children of the current statement — only ancestor mutations +// can conflict. +// // NOTE: cascades are checked up-front by Builder.checkMultipleMutationsCascade, // but the statement tree still must be propagated to them via this function in // case the cascade causes a trigger to fire. @@ -222,26 +246,30 @@ func (st *statementTree) GetInitFnForPostQuery() func() statementTree { if len(st.stmts) <= 1 { return nil } - // Save references to the ancestor statementTreeNodes. Modifications to them - // after this point should be reflected in the references, ensuring that all - // ancestor mutations are visible by the time the post-query plan is built. - // - // This is necessary because the full set of mutations in the current - // statement may not be known at the time the statement tree is saved. For - // example, a CTE in which the first branch triggers a post-query, and the - // second is a mutation. - ancestorStatements := make([]*statementTreeNode, len(st.stmts)-1) - copy(ancestorStatements, st.stmts[:len(st.stmts)-1]) + return st.getInitFn(st.stmts[:len(st.stmts)-1]) +} + +// getInitFn returns a function that initializes a new statementTree by +// flattening the given stack levels into a single ancestor node. The +// returned function may be nil if levels is empty. +// +// References to the statementTreeNodes are captured by pointer, so mutations +// registered after this call (e.g. by sibling CTEs) are visible when the +// returned function is invoked. +// +// Child mutations are omitted because they can only conflict with ancestor +// nodes, and those conflicts have already been checked. +func (st *statementTree) getInitFn(levels []*statementTreeNode) func() statementTree { + if len(levels) == 0 { + return nil + } + saved := make([]*statementTreeNode, len(levels)) + copy(saved, levels) return func() statementTree { - // Combine the non-child mutated tables for all ancestor nodes into a single - // ancestor node. This provides all the information needed to check for - // conflicts in a trigger run as a post-query. We can omit the child - // mutation tables because they can only conflict with the ancestor nodes, - // and that case has already been checked. var node statementTreeNode - for i := range ancestorStatements { - node.simpleInsertTables.UnionWith(ancestorStatements[i].simpleInsertTables) - node.generalMutationTables.UnionWith(ancestorStatements[i].generalMutationTables) + for i := range saved { + node.simpleInsertTables.UnionWith(saved[i].simpleInsertTables) + node.generalMutationTables.UnionWith(saved[i].generalMutationTables) } return statementTree{stmts: []*statementTreeNode{&node}} } diff --git a/pkg/sql/opt/optbuilder/statement_tree_test.go b/pkg/sql/opt/optbuilder/statement_tree_test.go index b92331e6e055..1de158072e6f 100644 --- a/pkg/sql/opt/optbuilder/statement_tree_test.go +++ b/pkg/sql/opt/optbuilder/statement_tree_test.go @@ -21,6 +21,8 @@ func TestStatementTree(t *testing.T) { post getInit init + getInitDeferred + initDeferred t1 t2 fail @@ -473,7 +475,7 @@ func TestStatementTree(t *testing.T) { pop, }, }, - // 25. + // 26. // Original: // Push // Push @@ -505,7 +507,7 @@ func TestStatementTree(t *testing.T) { pop, }, }, - // 26. + // 27. // Original: // Push // CanMutateTable(t1, default) @@ -531,7 +533,7 @@ func TestStatementTree(t *testing.T) { mut | t1 | fail, }, }, - // 27. + // 28. // Original: // Push // Push @@ -557,7 +559,7 @@ func TestStatementTree(t *testing.T) { mut | t1 | fail, }, }, - // 28. + // 29. // Original: // Push // CanMutateTable(t1, default) @@ -587,11 +589,308 @@ func TestStatementTree(t *testing.T) { mut | t1 | fail, }, }, + // 30. + // Pointer-based capture sees late-arriving mutations. + // Original: + // Push + // CanMutateTable(t1, default) + // GetInitFnForDeferredRoutine() + // CanMutateTable(t2, default) <-- added after capture + // Pop + // + // Deferred Routine: + // initFn() + // Push + // CanMutateTable(t2, default) FAIL <-- visible via pointer + { + cmds: []cmd{ + push, + mut | t1, + getInitDeferred, + mut | t2, + pop, + initDeferred, + push, + mut | t2 | fail, + }, + }, + // 31. + // Current-level mutations visible in deferred routine. + // e.g. UPDATE t SET x = my_udf() where my_udf body also mutates t. + // Original: + // Push + // CanMutateTable(t1, default) + // GetInitFnForDeferredRoutine() + // Pop + // + // Deferred Routine: + // initFn() + // Push + // CanMutateTable(t1, default) FAIL + { + cmds: []cmd{ + push, + mut | t1, + getInitDeferred, + pop, + initDeferred, + push, + mut | t1 | fail, + }, + }, + // 32. + // Different-table mutations allowed. + // Original: + // Push + // CanMutateTable(t1, default) + // GetInitFnForDeferredRoutine() + // Pop + // + // Deferred Routine: + // initFn() + // Push + // CanMutateTable(t2, default) + // Pop + { + cmds: []cmd{ + push, + mut | t1, + getInitDeferred, + pop, + initDeferred, + push, + mut | t2, + pop, + }, + }, + // 33. + // Nested ancestor mutations visible. + // Original: + // Push + // CanMutateTable(t1, default) + // Push + // GetInitFnForDeferredRoutine() + // Pop + // Pop + // + // Deferred Routine: + // initFn() + // Push + // CanMutateTable(t1, default) FAIL + { + cmds: []cmd{ + push, + mut | t1, + push, + getInitDeferred, + pop, + pop, + initDeferred, + push, + mut | t1 | fail, + }, + }, + // 34. + // Sibling CTE mutations visible via pointer capture. + // Original: + // Push + // Push + // GetInitFnForDeferredRoutine() + // Pop + // CanMutateTable(t1, default) <-- sibling mutation after capture + // Pop + // + // Deferred Routine: + // initFn() + // Push + // CanMutateTable(t1, default) FAIL + { + cmds: []cmd{ + push, + push, + getInitDeferred, + pop, + mut | t1, + pop, + initDeferred, + push, + mut | t1 | fail, + }, + }, + // 35. + // Child mutations merged via Pop are NOT visible to deferred routine. + // The child's mutation is a sibling of the deferred routine (e.g. two + // CTEs), so it should not conflict. + // e.g. WITH cte1 AS (SELECT my_udf()), cte2 AS (UPDATE t ...) ... + // Original: + // Push + // GetInitFnForDeferredRoutine() + // Push + // CanMutateTable(t1, default) + // Pop <-- t1 merges into parent's children*, not simpleInsert/generalMutation + // Pop + // + // Deferred Routine: + // initFn() + // Push + // CanMutateTable(t1, default) <-- OK, sibling mutation + // Pop + { + cmds: []cmd{ + push, + getInitDeferred, + push, + mut | t1, + pop, + pop, + initDeferred, + push, + mut | t1, + pop, + }, + }, + // 36. + // simpleInsert+simpleInsert on same table OK. + // Original: + // Push + // CanMutateTable(t1, simpleInsert) + // GetInitFnForDeferredRoutine() + // Pop + // + // Deferred Routine: + // initFn() + // Push + // CanMutateTable(t1, simpleInsert) + // Pop + { + cmds: []cmd{ + push, + mut | t1 | simple, + getInitDeferred, + pop, + initDeferred, + push, + mut | t1 | simple, + pop, + }, + }, + // 37. + // generalMutation+simpleInsert on same table FAIL. + // Original: + // Push + // CanMutateTable(t1, default) + // GetInitFnForDeferredRoutine() + // Pop + // + // Deferred Routine: + // initFn() + // Push + // CanMutateTable(t1, simpleInsert) FAIL + { + cmds: []cmd{ + push, + mut | t1, + getInitDeferred, + pop, + initDeferred, + push, + mut | t1 | simple | fail, + }, + }, + // 38. + // Empty stack returns nil initFn. + // + // Deferred Routine: + // initFn() == nil => empty statementTree + // Push + // CanMutateTable(t1, default) + // Pop + { + cmds: []cmd{ + getInitDeferred, + initDeferred, + push, + mut | t1, + pop, + }, + }, + // 39. + // Sibling body statements within deferred routine don't conflict. + // Original: + // Push + // CanMutateTable(t1, default) + // GetInitFnForDeferredRoutine() + // Pop + // + // Deferred Routine: + // initFn() + // Push + // Push + // CanMutateTable(t2, default) + // Pop + // Push + // CanMutateTable(t2, default) + // Pop + // Pop + { + cmds: []cmd{ + push, + mut | t1, + getInitDeferred, + pop, + initDeferred, + push, + push, + mut | t2, + pop, + push, + mut | t2, + pop, + pop, + }, + }, + // 40. + // Two separate deferred routine invocations mutating the same table. + // The deferred routines are siblings, so they should not conflict. + // Original: + // Push + // GetInitFnForDeferredRoutine() -- routine 1 + // GetInitFnForDeferredRoutine() -- routine 2 + // Pop + // + // Deferred Routine 1: + // initFn() + // Push + // CanMutateTable(t1, default) + // Pop + // + // Deferred Routine 2: + // initFn() + // Push + // CanMutateTable(t1, default) + // Pop + { + cmds: []cmd{ + push, + getInitDeferred, + getInitDeferred, + pop, + initDeferred, + push, + mut | t1, + pop, + initDeferred, + push, + mut | t1, + pop, + }, + }, } for i, tc := range testCases { var mt statementTree var pqTreeFn func() statementTree + var deferredTreeFns []func() statementTree for j, c := range tc.cmds { switch { case c&push == push: @@ -613,6 +912,22 @@ func TestStatementTree(t *testing.T) { mt = pqTreeFn() } + case c&getInitDeferred == getInitDeferred: + deferredTreeFns = append(deferredTreeFns, mt.GetInitFnForDeferredRoutine()) + + case c&initDeferred == initDeferred: + if len(deferredTreeFns) == 0 { + mt = statementTree{} + } else { + fn := deferredTreeFns[0] + deferredTreeFns = deferredTreeFns[1:] + if fn == nil { + mt = statementTree{} + } else { + mt = fn() + } + } + case c&mut == mut: var tabID cat.StableID switch { From 5431ffb247f8e92a2981f4a28f0255c27debeccb Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Wed, 13 May 2026 15:11:53 -0400 Subject: [PATCH 03/12] sql: persist CanMutate on function descriptors With deferred UDF body optimization, body statements are not built into RelExprs at plan time. The execution layer needs to know whether a routine can mutate before execution to choose between LeafTxn and RootTxn (via PlanFlagContainsMutation). Fix this by computing the CanMutate property at CREATE FUNCTION time from the optimizer's transitive Relational().CanMutate logical property (which covers direct DML, mutations in CTEs/subqueries, and nested mutating UDF calls) and persisting it on the function descriptor. At query time, the persisted value is read through the Overload and UDFDefinition and used to set PlanFlagContainsMutation without needing to build the body. The descriptor field uses a three-way enum (UNKNOWN_CAN_MUTATE, CAN_MUTATE, CANNOT_MUTATE) rather than a bool. The zero value UNKNOWN_CAN_MUTATE means "not yet determined" and causes consumers to fall back to inspecting the eagerly-built body RelExprs. This handles pre-existing function descriptors created before this field was introduced without requiring a migration: they naturally have the zero value, which triggers the correct fallback behavior. Functions created or replaced after the version gate is active get CAN_MUTATE or CANNOT_MUTATE, allowing consumers to skip the body inspection. For anonymous routines (DO blocks and trigger functions), CanMutate is derived directly from the body expression at build time, since these have no descriptor. The version gate on writing CanMutate is needed for rollback safety: if the field were written before finalization and the cluster rolled back, old binaries would not reset it during CREATE OR REPLACE, leaving stale values that could cause correctness issues after re-upgrade. Release note: None --- .../settings/settings-for-tenants.txt | 2 +- docs/generated/settings/settings.html | 2 +- .../backup_base_generated_test.go | 56 +++ .../create_trigger.side_effects | 3 + .../drop_table_trigger.side_effects | 3 + .../drop_trigger/drop_trigger.side_effects | 2 + pkg/clusterversion/cockroach_versions.go | 7 + pkg/sql/catalog/bootstrap/testdata/testdata | 8 +- pkg/sql/catalog/catpb/function.proto | 6 + pkg/sql/catalog/descpb/structured.proto | 17 +- pkg/sql/catalog/descriptor.go | 3 + pkg/sql/catalog/funcdesc/func_desc.go | 32 ++ pkg/sql/catalog/systemschema/system.go | 2 +- .../testdata/bootstrap_system | 6 +- .../testdata/bootstrap_tenant | 6 +- pkg/sql/catalog/tabledesc/validate_test.go | 1 + pkg/sql/create_function.go | 27 ++ .../testdata/logic_test/crdb_internal_catalog | 2 +- .../logic_test/mixed_version_udf_can_mutate | 86 +++++ .../cockroach-go-testserver-25.4/BUILD.bazel | 2 +- .../generated_test.go | 7 + .../cockroach-go-testserver-26.1/BUILD.bazel | 2 +- .../generated_test.go | 7 + pkg/sql/opt/exec/execbuilder/relational.go | 13 +- pkg/sql/opt/exec/execbuilder/scalar.go | 13 +- pkg/sql/opt/memo/expr.go | 10 + pkg/sql/opt/memo/logical_props_builder.go | 18 +- pkg/sql/opt/optbuilder/create_function.go | 9 + pkg/sql/opt/optbuilder/routine.go | 21 +- pkg/sql/opt/optbuilder/trigger.go | 1 + .../internal/scbuildstmt/create_function.go | 9 +- .../scbuild/testdata/create_function | 4 +- .../testdata/create_or_replace_function | 4 +- .../scbuild/testdata/create_procedure | 4 +- pkg/sql/schemachanger/scdecomp/decomp.go | 1 + .../schemachanger/scdecomp/testdata/function | 1 + .../scexec/scmutationexec/function.go | 1 + pkg/sql/schemachanger/scpb/elements.proto | 1 + pkg/sql/schemachanger/scpb/uml/table.puml | 1 + .../scplan/testdata/create_function | 4 + .../testdata/create_or_replace_function | 4 + .../scplan/testdata/create_procedure | 4 + .../schemachanger/sctest_generated_test.go | 84 +++++ .../alter_table_add_check_udf.side_effects | 3 + .../create_complex.side_effects | 2 + .../create_complex__statement_3_of_4.explain | 4 +- .../create_complex__statement_4_of_4.explain | 2 +- .../create_function/create_function.explain | 4 +- .../create_function.side_effects | 2 + .../create_function_calling_function.explain | 4 +- ...ate_function_calling_function.side_effects | 4 + ...reate_function_calling_mutating.definition | 17 + .../create_function_calling_mutating.explain | 78 ++++ ...te_function_calling_mutating.explain_shape | 16 + ...ate_function_calling_mutating.side_effects | 353 ++++++++++++++++++ ...calling_mutating__statement_1_of_2.explain | 80 ++++ ...g_mutating__statement_1_of_2.explain_shape | 18 + ...calling_mutating__statement_2_of_2.explain | 116 ++++++ ...g_mutating__statement_2_of_2.explain_shape | 24 ++ .../create_function_in_txn.side_effects | 3 + ..._function_in_txn__statement_1_of_2.explain | 4 +- ..._function_in_txn__statement_2_of_2.explain | 2 +- .../create_function_mutating.definition | 11 + .../create_function_mutating.explain | 77 ++++ .../create_function_mutating.explain_shape | 15 + .../create_function_mutating.side_effects | 208 +++++++++++ .../drop_column_with_trigger_dep.side_effects | 3 + .../drop_column_with_udf_default.side_effects | 2 + .../drop_function/drop_function.side_effects | 1 + .../drop_index_vanilla_index.side_effects | 3 + ...rop_table_cross_table_trigger.side_effects | 6 + .../drop_table_udf_default.side_effects | 3 + pkg/sql/sem/tree/create_routine.go | 32 ++ pkg/sql/sem/tree/overload.go | 6 + pkg/upgrade/upgrades/upgrades.go | 12 + 75 files changed, 1565 insertions(+), 46 deletions(-) create mode 100644 pkg/sql/logictest/testdata/logic_test/mixed_version_udf_can_mutate create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.definition create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.explain create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.explain_shape create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.side_effects create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_1_of_2.explain create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_1_of_2.explain_shape create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_2_of_2.explain create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_2_of_2.explain_shape create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.definition create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.explain create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.explain_shape create mode 100644 pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.side_effects diff --git a/docs/generated/settings/settings-for-tenants.txt b/docs/generated/settings/settings-for-tenants.txt index 35e6ed587e34..dcd0d17f2858 100644 --- a/docs/generated/settings/settings-for-tenants.txt +++ b/docs/generated/settings/settings-for-tenants.txt @@ -446,4 +446,4 @@ trace.zipkin.collector string the address of a Zipkin instance to receive trace ui.database_locality_metadata.enabled boolean true if enabled shows extended locality data about databases and tables in DB Console which can be expensive to compute application ui.default_timezone string the default timezone used to format timestamps in the ui application ui.display_timezone enumeration etc/utc the timezone used to format timestamps in the ui. This setting is deprecatedand will be removed in a future version. Use the 'ui.default_timezone' setting instead. 'ui.default_timezone' takes precedence over this setting. [etc/utc = 0, america/new_york = 1] application -version version 1000026.2-upgrading-to-1000026.3-step-010 set the active cluster version in the format '.' application +version version 1000026.2-upgrading-to-1000026.3-step-012 set the active cluster version in the format '.' application diff --git a/docs/generated/settings/settings.html b/docs/generated/settings/settings.html index fe7040b23445..2514864cf7d5 100644 --- a/docs/generated/settings/settings.html +++ b/docs/generated/settings/settings.html @@ -407,6 +407,6 @@
ui.database_locality_metadata.enabled
booleantrueif enabled shows extended locality data about databases and tables in DB Console which can be expensive to computeBasic/Standard/Advanced/Self-Hosted
ui.default_timezone
stringthe default timezone used to format timestamps in the uiBasic/Standard/Advanced/Self-Hosted
ui.display_timezone
enumerationetc/utcthe timezone used to format timestamps in the ui. This setting is deprecatedand will be removed in a future version. Use the 'ui.default_timezone' setting instead. 'ui.default_timezone' takes precedence over this setting. [etc/utc = 0, america/new_york = 1]Basic/Standard/Advanced/Self-Hosted -
version
version1000026.2-upgrading-to-1000026.3-step-010set the active cluster version in the format '<major>.<minor>'Basic/Standard/Advanced/Self-Hosted +
version
version1000026.2-upgrading-to-1000026.3-step-012set the active cluster version in the format '<major>.<minor>'Basic/Standard/Advanced/Self-Hosted diff --git a/pkg/ccl/schemachangerccl/sctestbackupccl/backup_base_generated_test.go b/pkg/ccl/schemachangerccl/sctestbackupccl/backup_base_generated_test.go index acb0b2b10153..1c2883d4f48e 100644 --- a/pkg/ccl/schemachangerccl/sctestbackupccl/backup_base_generated_test.go +++ b/pkg/ccl/schemachangerccl/sctestbackupccl/backup_base_generated_test.go @@ -645,6 +645,13 @@ func TestBackupRollbacks_base_create_function_calling_function(t *testing.T) { sctest.BackupRollbacks(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestBackupRollbacks_base_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.BackupRollbacks(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestBackupRollbacks_base_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -652,6 +659,13 @@ func TestBackupRollbacks_base_create_function_in_txn(t *testing.T) { sctest.BackupRollbacks(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestBackupRollbacks_base_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.BackupRollbacks(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestBackupRollbacks_base_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -1555,6 +1569,13 @@ func TestBackupRollbacksMixedVersion_base_create_function_calling_function(t *te sctest.BackupRollbacksMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestBackupRollbacksMixedVersion_base_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.BackupRollbacksMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestBackupRollbacksMixedVersion_base_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -1562,6 +1583,13 @@ func TestBackupRollbacksMixedVersion_base_create_function_in_txn(t *testing.T) { sctest.BackupRollbacksMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestBackupRollbacksMixedVersion_base_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.BackupRollbacksMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestBackupRollbacksMixedVersion_base_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -2465,6 +2493,13 @@ func TestBackupSuccess_base_create_function_calling_function(t *testing.T) { sctest.BackupSuccess(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestBackupSuccess_base_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.BackupSuccess(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestBackupSuccess_base_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -2472,6 +2507,13 @@ func TestBackupSuccess_base_create_function_in_txn(t *testing.T) { sctest.BackupSuccess(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestBackupSuccess_base_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.BackupSuccess(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestBackupSuccess_base_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -3375,6 +3417,13 @@ func TestBackupSuccessMixedVersion_base_create_function_calling_function(t *test sctest.BackupSuccessMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestBackupSuccessMixedVersion_base_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.BackupSuccessMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestBackupSuccessMixedVersion_base_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -3382,6 +3431,13 @@ func TestBackupSuccessMixedVersion_base_create_function_in_txn(t *testing.T) { sctest.BackupSuccessMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestBackupSuccessMixedVersion_base_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.BackupSuccessMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestBackupSuccessMixedVersion_base_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.side_effects b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.side_effects index 9c55cfe7571f..f39b93a142e8 100644 --- a/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.side_effects +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/create_trigger/create_trigger.side_effects @@ -74,6 +74,7 @@ upsert descriptor #104 + version: "2" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE + dependedOnBy: + - id: 104 + triggerIds: @@ -168,6 +169,7 @@ upsert descriptor #104 + version: "2" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE + declarativeSchemaChangerState: + authorization: + userName: root @@ -246,6 +248,7 @@ upsert descriptor #104 + version: "3" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.side_effects b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.side_effects index 316775f5b671..c4dae767f674 100644 --- a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.side_effects +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_table_trigger/drop_table_trigger.side_effects @@ -61,6 +61,7 @@ upsert descriptor #104 + version: "4" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: @@ -137,6 +138,7 @@ upsert descriptor #104 + version: "4" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: @@ -195,6 +197,7 @@ upsert descriptor #104 + version: "5" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.side_effects b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.side_effects index c8d0a03745c4..1cc35b542978 100644 --- a/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.side_effects +++ b/pkg/ccl/schemachangerccl/testdata/end_to_end/drop_trigger/drop_trigger.side_effects @@ -57,6 +57,7 @@ upsert descriptor #104 + version: "4" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: @@ -114,6 +115,7 @@ upsert descriptor #104 + version: "4" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: diff --git a/pkg/clusterversion/cockroach_versions.go b/pkg/clusterversion/cockroach_versions.go index 65f4bc24dcf3..9c95387360f3 100644 --- a/pkg/clusterversion/cockroach_versions.go +++ b/pkg/clusterversion/cockroach_versions.go @@ -290,6 +290,11 @@ const ( // to persist per-tenant resource group configurations. V26_3_AddResourceGroupsTable + // V26_3_FunctionDescCanMutate adds the can_mutate field to function + // descriptors, computed at CREATE FUNCTION time to indicate whether the + // routine body contains mutation statements. + V26_3_FunctionDescCanMutate + // ************************************************* // Step (1) Add new versions above this comment. // Do not add new versions to a patch release. @@ -380,6 +385,8 @@ var versionTable = [numKeys]roachpb.Version{ V26_3_AlterStatementsTablePK: {Major: 26, Minor: 2, Internal: 8}, V26_3_AddResourceGroupsTable: {Major: 26, Minor: 2, Internal: 10}, + + V26_3_FunctionDescCanMutate: {Major: 26, Minor: 2, Internal: 12}, // ************************************************* // Step (2): Add new versions above this comment. // ************************************************* diff --git a/pkg/sql/catalog/bootstrap/testdata/testdata b/pkg/sql/catalog/bootstrap/testdata/testdata index 0a4910017c2b..7fccbbb73399 100644 --- a/pkg/sql/catalog/bootstrap/testdata/testdata +++ b/pkg/sql/catalog/bootstrap/testdata/testdata @@ -5,10 +5,10 @@ # The --rewrite flag only updates output blocks, not command arguments, so # the hash must be corrected manually first. -system hash=3ac34768496a5bfbccdfbd20fdc6e5b6560d55a3ab807403e9c6f01059e971e2 +system hash=bb983c5bcb8b3142c5a9b22e89e1466a17623f569c7a4b42f154970112091904 ---- [{"key":"8b"} -,{"key":"8b89898a89","value":"0312470a0673797374656d10011a250a0d0a0561646d696e1080101880100a0c0a04726f6f7410801018801012046e6f646518032200280140004a006a0a08da843d10021800200a7000"} +,{"key":"8b89898a89","value":"0312470a0673797374656d10011a250a0d0a0561646d696e1080101880100a0c0a04726f6f7410801018801012046e6f646518032200280140004a006a0a08da843d10021800200c7000"} ,{"key":"8b898b8a89","value":"030aac030a0a64657363726970746f721803200128013a0042270a02696410011a0c0801104018002a005014600020003000680070007800800100880100980100422f0a0a64657363726970746f7210021a0c0808100018002a0050116000200130006800700078008001008801009801004803527a0a077072696d61727910011801220269642a0a64657363726970746f72300140004a10080010001a00200028003000380040005a0070027a0408002000800100880100900104980101a20106080012001800a80100b20100ba0100c00100c80100d00101e00100e9010000000000000000f20100f8010080020060026a210a0b0a0561646d696e102018200a0a0a04726f6f741020182012046e6f64651803800101880103980100b201130a077072696d61727910001a02696420012800b201240a1066616d5f325f64657363726970746f7210021a0a64657363726970746f7220022802b80103c20100e80100f2010408001200f801008002009202009a0200b20200b80200c0021dc80200e00200800300880302a80300b00300d00300d80300e00300f80300880400980400a00400a80400b00400b80400"} ,{"key":"8b898c8a89","value":"030a91070a0575736572731804200128013a00422d0a08757365726e616d6510011a0c0807100018002a00501960002000300068007000780080010088010098010042330a0e68617368656450617373776f726410021a0c0808100018002a00501160002001300068007000780080010088010098010042320a066973526f6c6510031a0c0800100018002a005010600020002a0566616c73653000680070007800800100880100980100422c0a07757365725f696410041a0c080c100018002a00501a600020003000680070007800800100880100980100423f0a19657374696d617465645f6c6173745f6c6f67696e5f74696d6510051a0d0809100018002a0050a009600020013000680070007800800100880100980100480652b6010a077072696d617279100118012208757365726e616d652a0e68617368656450617373776f72642a066973526f6c652a07757365725f69642a19657374696d617465645f6c6173745f6c6f67696e5f74696d65300140004a10080010001a00200028003000380040005a0070027003700470057a0408002000800100880100900104980101a20106080012001800a80100b20100ba0100c00100c80100d00102e00100e9010000000000000000f20100f801008002005a7d0a1175736572735f757365725f69645f696478100218012207757365725f69643004380140004a10080010001a00200028003000380040005a007a0408002000800100880100900103980100a20106080012001800a80100b20100ba0100c00100c80100d00101e00100e9010000000000000000f20100f8010080020060036a250a0d0a0561646d696e10e00318e0030a0c0a04726f6f7410e00318e00312046e6f64651803800101880103980100b201240a077072696d61727910001a08757365726e616d651a07757365725f6964200120042804b2012c0a1466616d5f325f68617368656450617373776f726410021a0e68617368656450617373776f726420022802b2011c0a0c66616d5f335f6973526f6c6510031a066973526f6c6520032803b201420a1f66616d5f355f657374696d617465645f6c6173745f6c6f67696e5f74696d6510051a19657374696d617465645f6c6173745f6c6f67696e5f74696d6520052805b80106c20100e80100f2010408001200f801008002009202009a0200b20200b80200c0021dc80200e00200800300880303a80300b00300d00300d80300e00300f80300880400980400a00400a80400b00400b80400"} ,{"key":"8b898d8a89","value":"030a9b030a057a6f6e65731805200128013a0042270a02696410011a0c0801104018002a005014600020003000680070007800800100880100980100422b0a06636f6e66696710021a0c0808100018002a005011600020013000680070007800800100880100980100480352760a077072696d61727910011801220269642a06636f6e666967300140004a10080010001a00200028003000380040005a0070027a0408002000800100880100900104980101a20106080012001800a80100b20100ba0100c00100c80100d00101e00100e9010000000000000000f20100f8010080020060026a250a0d0a0561646d696e10e00318e0030a0c0a04726f6f7410e00318e00312046e6f64651803800101880103980100b201130a077072696d61727910001a02696420012800b2011c0a0c66616d5f325f636f6e66696710021a06636f6e66696720022802b80103c20100e80100f2010408001200f801008002009202009a0200b20200b80200c0021dc80200e00200800300880302a80300b00300d00300d80300e00300f80300880400980400a00400a80400b00400b80400"} @@ -251,10 +251,10 @@ system hash=3ac34768496a5bfbccdfbd20fdc6e5b6560d55a3ab807403e9c6f01059e971e2 ,{"key":"da898888","value":"0120"} ] -tenant hash=4e47b77f3ef655a61c1cc468ba6eb30b9da4cc0c2539b2e9bf0f625316390ebd +tenant hash=6af446ad5d05545c395b80c7764b40c0a5a75928f22b43441aff42ab960ac301 ---- [{"key":""} -,{"key":"8b89898a89","value":"0312470a0673797374656d10011a250a0d0a0561646d696e1080101880100a0c0a04726f6f7410801018801012046e6f646518032200280140004a006a0a08da843d10021800200a7000"} +,{"key":"8b89898a89","value":"0312470a0673797374656d10011a250a0d0a0561646d696e1080101880100a0c0a04726f6f7410801018801012046e6f646518032200280140004a006a0a08da843d10021800200c7000"} ,{"key":"8b898b8a89","value":"030aac030a0a64657363726970746f721803200128013a0042270a02696410011a0c0801104018002a005014600020003000680070007800800100880100980100422f0a0a64657363726970746f7210021a0c0808100018002a0050116000200130006800700078008001008801009801004803527a0a077072696d61727910011801220269642a0a64657363726970746f72300140004a10080010001a00200028003000380040005a0070027a0408002000800100880100900104980101a20106080012001800a80100b20100ba0100c00100c80100d00101e00100e9010000000000000000f20100f8010080020060026a210a0b0a0561646d696e102018200a0a0a04726f6f741020182012046e6f64651803800101880103980100b201130a077072696d61727910001a02696420012800b201240a1066616d5f325f64657363726970746f7210021a0a64657363726970746f7220022802b80103c20100e80100f2010408001200f801008002009202009a0200b20200b80200c0021dc80200e00200800300880302a80300b00300d00300d80300e00300f80300880400980400a00400a80400b00400b80400"} ,{"key":"8b898c8a89","value":"030a91070a0575736572731804200128013a00422d0a08757365726e616d6510011a0c0807100018002a00501960002000300068007000780080010088010098010042330a0e68617368656450617373776f726410021a0c0808100018002a00501160002001300068007000780080010088010098010042320a066973526f6c6510031a0c0800100018002a005010600020002a0566616c73653000680070007800800100880100980100422c0a07757365725f696410041a0c080c100018002a00501a600020003000680070007800800100880100980100423f0a19657374696d617465645f6c6173745f6c6f67696e5f74696d6510051a0d0809100018002a0050a009600020013000680070007800800100880100980100480652b6010a077072696d617279100118012208757365726e616d652a0e68617368656450617373776f72642a066973526f6c652a07757365725f69642a19657374696d617465645f6c6173745f6c6f67696e5f74696d65300140004a10080010001a00200028003000380040005a0070027003700470057a0408002000800100880100900104980101a20106080012001800a80100b20100ba0100c00100c80100d00102e00100e9010000000000000000f20100f801008002005a7d0a1175736572735f757365725f69645f696478100218012207757365725f69643004380140004a10080010001a00200028003000380040005a007a0408002000800100880100900103980100a20106080012001800a80100b20100ba0100c00100c80100d00101e00100e9010000000000000000f20100f8010080020060036a250a0d0a0561646d696e10e00318e0030a0c0a04726f6f7410e00318e00312046e6f64651803800101880103980100b201240a077072696d61727910001a08757365726e616d651a07757365725f6964200120042804b2012c0a1466616d5f325f68617368656450617373776f726410021a0e68617368656450617373776f726420022802b2011c0a0c66616d5f335f6973526f6c6510031a066973526f6c6520032803b201420a1f66616d5f355f657374696d617465645f6c6173745f6c6f67696e5f74696d6510051a19657374696d617465645f6c6173745f6c6f67696e5f74696d6520052805b80106c20100e80100f2010408001200f801008002009202009a0200b20200b80200c0021dc80200e00200800300880303a80300b00300d00300d80300e00300f80300880400980400a00400a80400b00400b80400"} ,{"key":"8b898d8a89","value":"030a9b030a057a6f6e65731805200128013a0042270a02696410011a0c0801104018002a005014600020003000680070007800800100880100980100422b0a06636f6e66696710021a0c0808100018002a005011600020013000680070007800800100880100980100480352760a077072696d61727910011801220269642a06636f6e666967300140004a10080010001a00200028003000380040005a0070027a0408002000800100880100900104980101a20106080012001800a80100b20100ba0100c00100c80100d00101e00100e9010000000000000000f20100f8010080020060026a250a0d0a0561646d696e10e00318e0030a0c0a04726f6f7410e00318e00312046e6f64651803800101880103980100b201130a077072696d61727910001a02696420012800b2011c0a0c66616d5f325f636f6e66696710021a06636f6e66696720022802b80103c20100e80100f2010408001200f801008002009202009a0200b20200b80200c0021dc80200e00200800300880302a80300b00300d00300d80300e00300f80300880400980400a00400a80400b00400b80400"} diff --git a/pkg/sql/catalog/catpb/function.proto b/pkg/sql/catalog/catpb/function.proto index f69ac0c61a84..3c6ac7ae05ce 100644 --- a/pkg/sql/catalog/catpb/function.proto +++ b/pkg/sql/catalog/catpb/function.proto @@ -46,6 +46,12 @@ message Function { INVOKER = 0; DEFINER = 1; } + + enum CanMutate { + UNKNOWN_CAN_MUTATE = 0; + CAN_MUTATE = 1; + CANNOT_MUTATE = 2; + } } // These wrappers are for the convenience of referencing the enum types from a diff --git a/pkg/sql/catalog/descpb/structured.proto b/pkg/sql/catalog/descpb/structured.proto index b9b2cfad513d..ab3146abe46d 100644 --- a/pkg/sql/catalog/descpb/structured.proto +++ b/pkg/sql/catalog/descpb/structured.proto @@ -2017,7 +2017,22 @@ message FunctionDescriptor { optional uint32 replicated_pcr_version = 24 [(gogoproto.nullable) = false, (gogoproto.customname) = "ReplicatedPCRVersion", (gogoproto.casttype) = "DescriptorVersion"]; - // Next field id is 25 + // CanMutate indicates whether the routine body can perform mutations. + // This includes direct DML statements (INSERT, UPDATE, DELETE, UPSERT), + // mutations inside CTEs or subqueries, and calls to other routines that + // themselves can mutate. It is computed at CREATE FUNCTION time by + // inspecting the CanMutate logical property of the built body + // statements, and persisted so that it is available at execution time + // without needing to build the routine body. + // + // UNKNOWN_CAN_MUTATE (the zero value) indicates that the mutation status + // has not been determined. This is the case for function descriptors + // created before this field was introduced. When unknown, consumers must + // fall back to inspecting the body RelExprs to determine whether the + // routine can mutate. + optional cockroach.sql.catalog.catpb.Function.CanMutate can_mutate = 25 [(gogoproto.nullable) = false]; + + // Next field id is 26 } // Descriptor is a union type for descriptors for tables, schemas, databases, diff --git a/pkg/sql/catalog/descriptor.go b/pkg/sql/catalog/descriptor.go index 020fbc6e3494..e83df739e531 100644 --- a/pkg/sql/catalog/descriptor.go +++ b/pkg/sql/catalog/descriptor.go @@ -1169,6 +1169,9 @@ type FunctionDescriptor interface { // GetSecurity returns the security specification of this function. GetSecurity() catpb.Function_Security + + // GetCanMutate returns the CanMutate field of this function descriptor. + GetCanMutate() catpb.Function_CanMutate } // FilterDroppedDescriptor returns an error if the descriptor state is DROP. diff --git a/pkg/sql/catalog/funcdesc/func_desc.go b/pkg/sql/catalog/funcdesc/func_desc.go index 26c4e8e6c0f1..5ff4ed1f7ada 100644 --- a/pkg/sql/catalog/funcdesc/func_desc.go +++ b/pkg/sql/catalog/funcdesc/func_desc.go @@ -643,6 +643,11 @@ func (desc *Mutable) SetSecurity(v catpb.Function_Security) { desc.Security = v } +// SetCanMutate sets the CanMutate field on the function descriptor. +func (desc *Mutable) SetCanMutate(v catpb.Function_CanMutate) { + desc.CanMutate = v +} + // SetName sets the function name. func (desc *Mutable) SetName(n string) { desc.Name = n @@ -1027,6 +1032,11 @@ func (desc *immutable) GetSecurity() catpb.Function_Security { return desc.Security } +// GetCanMutate implements the FunctionDescriptor interface. +func (desc *immutable) GetCanMutate() catpb.Function_CanMutate { + return desc.CanMutate +} + func (desc *immutable) ToOverload() (ret *tree.Overload, err error) { routineType := tree.UDFRoutine if desc.IsProcedure() { @@ -1075,10 +1085,32 @@ func (desc *immutable) ToOverload() (ret *tree.Overload, err error) { ret.Class = tree.GeneratorClass } ret.SecurityMode = desc.getCreateExprSecurity() + ret.CanMutate = canMutateToOverload(desc.FunctionDescriptor.CanMutate) return ret, nil } +// canMutateToOverload converts the proto CanMutate enum to the tree type. +func canMutateToOverload(cm catpb.Function_CanMutate) tree.RoutineCanMutate { + switch cm { + case catpb.Function_CAN_MUTATE: + return tree.RoutineMutates + case catpb.Function_CANNOT_MUTATE: + return tree.RoutineDoesNotMutate + default: + return tree.RoutineCanMutateUnknown + } +} + +// CanMutateBoolToProto converts a definitively-known bool to the proto enum. +// Used at CREATE FUNCTION time when the body has been fully analyzed. +func CanMutateBoolToProto(canMutate bool) catpb.Function_CanMutate { + if canMutate { + return catpb.Function_CAN_MUTATE + } + return catpb.Function_CANNOT_MUTATE +} + func (desc *immutable) getOverloadVolatility() (volatility.V, error) { var ret volatility.V switch desc.Volatility { diff --git a/pkg/sql/catalog/systemschema/system.go b/pkg/sql/catalog/systemschema/system.go index 614f8b62bad1..d3194a65c13b 100644 --- a/pkg/sql/catalog/systemschema/system.go +++ b/pkg/sql/catalog/systemschema/system.go @@ -1482,7 +1482,7 @@ const SystemDatabaseName = catconstants.SystemDatabaseName // release version). // // NB: Don't set this to clusterversion.Latest; use a specific version instead. -var SystemDatabaseSchemaBootstrapVersion = clusterversion.V26_3_AddResourceGroupsTable.Version() +var SystemDatabaseSchemaBootstrapVersion = clusterversion.V26_3_FunctionDescCanMutate.Version() // MakeSystemDatabaseDesc constructs a copy of the system database // descriptor. diff --git a/pkg/sql/catalog/systemschema_test/testdata/bootstrap_system b/pkg/sql/catalog/systemschema_test/testdata/bootstrap_system index 6683899cd07c..23e2823ad842 100644 --- a/pkg/sql/catalog/systemschema_test/testdata/bootstrap_system +++ b/pkg/sql/catalog/systemschema_test/testdata/bootstrap_system @@ -850,7 +850,7 @@ schema_telemetry ---- {"database":{"name":"defaultdb","id":100,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2","withGrantOption":"2"},{"userProto":"public","privileges":"17592186046464"},{"userProto":"root","privileges":"2","withGrantOption":"2"}],"ownerProto":"root","version":3},"schemas":{"public":{"id":101}},"defaultPrivileges":{}}} {"database":{"name":"postgres","id":102,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2","withGrantOption":"2"},{"userProto":"public","privileges":"17592186046464"},{"userProto":"root","privileges":"2","withGrantOption":"2"}],"ownerProto":"root","version":3},"schemas":{"public":{"id":103}},"defaultPrivileges":{}}} -{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":10}}} +{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":12}}} {"table":{"name":"advisory_locks","id":80,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"database_id","id":1,"type":{"family":"IntFamily","width":32,"oid":23}},{"name":"lock_type","id":2,"type":{"family":"IntFamily","width":32,"oid":23}},{"name":"lock_key","id":3,"type":{"family":"IntFamily","width":64,"oid":20}}],"nextColumnId":4,"families":[{"name":"primary","columnNames":["database_id","lock_type","lock_key"],"columnIds":[1,2,3]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["database_id","lock_type","lock_key"],"keyColumnDirections":["ASC","ASC","ASC"],"keyColumnIds":[1,2,3],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} {"table":{"name":"cluster_metrics","id":78,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"id","id":1,"type":{"family":"IntFamily","width":64,"oid":20},"defaultExpr":"unique_rowid()"},{"name":"name","id":2,"type":{"family":"StringFamily","oid":25}},{"name":"labels","id":3,"type":{"family":"JsonFamily","oid":3802},"defaultExpr":"'_':::JSONB"},{"name":"type","id":4,"type":{"family":"StringFamily","oid":25}},{"name":"value","id":5,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"node_id","id":6,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"last_updated","id":7,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"crdb_internal_last_updated_shard_8","id":8,"type":{"family":"IntFamily","width":32,"oid":23},"hidden":true,"computeExpr":"mod(fnv32(md5(crdb_internal.datums_to_bytes(last_updated))), _:::INT8)","virtual":true}],"nextColumnId":9,"families":[{"name":"primary","columnNames":["id","name","labels","type","value","node_id","last_updated"],"columnIds":[1,2,3,4,5,6,7]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["id"],"keyColumnDirections":["ASC"],"storeColumnNames":["name","labels","type","value","node_id","last_updated"],"keyColumnIds":[1],"storeColumnIds":[2,3,4,5,6,7],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":2,"vecConfig":{}},"indexes":[{"name":"name_labels_idx","id":2,"unique":true,"version":3,"keyColumnNames":["name","labels"],"keyColumnDirections":["ASC","ASC"],"keyColumnIds":[2,3],"keySuffixColumnIds":[1],"compositeColumnIds":[3],"foreignKey":{},"interleave":{},"partitioning":{},"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},{"name":"last_updated_idx","id":3,"version":3,"keyColumnNames":["crdb_internal_last_updated_shard_8","last_updated"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["name","labels","type","value","node_id"],"keyColumnIds":[8,7],"keySuffixColumnIds":[1],"storeColumnIds":[2,3,4,5,6],"foreignKey":{},"interleave":{},"partitioning":{},"sharded":{"isSharded":true,"name":"crdb_internal_last_updated_shard_8","shardBuckets":8,"columnNames":["last_updated"]},"geoConfig":{},"vecConfig":{}}],"nextIndexId":4,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"checks":[{"expr":"crdb_internal_last_updated_shard_8 IN (_:::INT8, _:::INT8, _:::INT8, _:::INT8, _:::INT8, _:::INT8, _:::INT8, _:::INT8)","name":"check_crdb_internal_last_updated_shard_8","columnIds":[8],"fromHashShardedColumn":true,"constraintId":3}],"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":4}} {"table":{"name":"comments","id":24,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"type","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"object_id","id":2,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"sub_id","id":3,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"comment","id":4,"type":{"family":"StringFamily","oid":25}}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["type","object_id","sub_id"],"columnIds":[1,2,3]},{"name":"fam_4_comment","id":4,"columnNames":["comment"],"columnIds":[4],"defaultColumnId":4}],"nextFamilyId":5,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["type","object_id","sub_id"],"keyColumnDirections":["ASC","ASC","ASC"],"storeColumnNames":["comment"],"keyColumnIds":[1,2,3],"storeColumnIds":[4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"public","privileges":"32"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} @@ -928,7 +928,7 @@ schema_telemetry schema_telemetry snapshot_id=7cd8a9ae-f35c-4cd2-970a-757174600874 max_records=10 ---- -{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":10}}} +{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":12}}} {"table":{"name":"descriptor_id_seq","id":7,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"value","id":1,"type":{"family":"IntFamily","width":64,"oid":20}}],"families":[{"name":"primary","columnNames":["value"],"columnIds":[1],"defaultColumnId":1}],"primaryIndex":{"name":"primary","id":1,"version":4,"keyColumnNames":["value"],"keyColumnDirections":["ASC"],"keyColumnIds":[1],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"vecConfig":{}},"privileges":{"users":[{"userProto":"admin","privileges":"32","withGrantOption":"32"},{"userProto":"root","privileges":"32","withGrantOption":"32"}],"ownerProto":"node","version":3},"formatVersion":3,"sequenceOpts":{"increment":"1","minValue":"1","maxValue":"9223372036854775807","start":"1","sequenceOwner":{},"sessionCacheSize":"1"},"replacementOf":{"time":{}},"createAsOfTime":{}}} {"table":{"name":"job_progress","id":68,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"job_id","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"written","id":2,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"fraction","id":3,"type":{"family":"FloatFamily","width":64,"oid":701},"nullable":true},{"name":"resolved","id":4,"type":{"family":"DecimalFamily","oid":1700},"nullable":true}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["job_id","written","fraction","resolved"],"columnIds":[1,2,3,4]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["job_id","written"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["fraction","resolved"],"keyColumnIds":[1,2],"storeColumnIds":[3,4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} {"table":{"name":"job_progress_history","id":69,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"job_id","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"written","id":2,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"fraction","id":3,"type":{"family":"FloatFamily","width":64,"oid":701},"nullable":true},{"name":"resolved","id":4,"type":{"family":"DecimalFamily","oid":1700},"nullable":true}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["job_id","written","fraction","resolved"],"columnIds":[1,2,3,4]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["job_id","written"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["fraction","resolved"],"keyColumnIds":[1,2],"storeColumnIds":[3,4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} @@ -941,7 +941,7 @@ schema_telemetry snapshot_id=7cd8a9ae-f35c-4cd2-970a-757174600874 max_records=10 schema_telemetry snapshot_id=7cd8a9ae-f35c-4cd2-970a-757174600874 max_records=10 ---- -{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":10}}} +{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":12}}} {"table":{"name":"descriptor_id_seq","id":7,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"value","id":1,"type":{"family":"IntFamily","width":64,"oid":20}}],"families":[{"name":"primary","columnNames":["value"],"columnIds":[1],"defaultColumnId":1}],"primaryIndex":{"name":"primary","id":1,"version":4,"keyColumnNames":["value"],"keyColumnDirections":["ASC"],"keyColumnIds":[1],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"vecConfig":{}},"privileges":{"users":[{"userProto":"admin","privileges":"32","withGrantOption":"32"},{"userProto":"root","privileges":"32","withGrantOption":"32"}],"ownerProto":"node","version":3},"formatVersion":3,"sequenceOpts":{"increment":"1","minValue":"1","maxValue":"9223372036854775807","start":"1","sequenceOwner":{},"sessionCacheSize":"1"},"replacementOf":{"time":{}},"createAsOfTime":{}}} {"table":{"name":"job_progress","id":68,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"job_id","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"written","id":2,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"fraction","id":3,"type":{"family":"FloatFamily","width":64,"oid":701},"nullable":true},{"name":"resolved","id":4,"type":{"family":"DecimalFamily","oid":1700},"nullable":true}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["job_id","written","fraction","resolved"],"columnIds":[1,2,3,4]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["job_id","written"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["fraction","resolved"],"keyColumnIds":[1,2],"storeColumnIds":[3,4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} {"table":{"name":"job_progress_history","id":69,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"job_id","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"written","id":2,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"fraction","id":3,"type":{"family":"FloatFamily","width":64,"oid":701},"nullable":true},{"name":"resolved","id":4,"type":{"family":"DecimalFamily","oid":1700},"nullable":true}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["job_id","written","fraction","resolved"],"columnIds":[1,2,3,4]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["job_id","written"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["fraction","resolved"],"keyColumnIds":[1,2],"storeColumnIds":[3,4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} diff --git a/pkg/sql/catalog/systemschema_test/testdata/bootstrap_tenant b/pkg/sql/catalog/systemschema_test/testdata/bootstrap_tenant index 6683899cd07c..23e2823ad842 100644 --- a/pkg/sql/catalog/systemschema_test/testdata/bootstrap_tenant +++ b/pkg/sql/catalog/systemschema_test/testdata/bootstrap_tenant @@ -850,7 +850,7 @@ schema_telemetry ---- {"database":{"name":"defaultdb","id":100,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2","withGrantOption":"2"},{"userProto":"public","privileges":"17592186046464"},{"userProto":"root","privileges":"2","withGrantOption":"2"}],"ownerProto":"root","version":3},"schemas":{"public":{"id":101}},"defaultPrivileges":{}}} {"database":{"name":"postgres","id":102,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2","withGrantOption":"2"},{"userProto":"public","privileges":"17592186046464"},{"userProto":"root","privileges":"2","withGrantOption":"2"}],"ownerProto":"root","version":3},"schemas":{"public":{"id":103}},"defaultPrivileges":{}}} -{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":10}}} +{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":12}}} {"table":{"name":"advisory_locks","id":80,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"database_id","id":1,"type":{"family":"IntFamily","width":32,"oid":23}},{"name":"lock_type","id":2,"type":{"family":"IntFamily","width":32,"oid":23}},{"name":"lock_key","id":3,"type":{"family":"IntFamily","width":64,"oid":20}}],"nextColumnId":4,"families":[{"name":"primary","columnNames":["database_id","lock_type","lock_key"],"columnIds":[1,2,3]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["database_id","lock_type","lock_key"],"keyColumnDirections":["ASC","ASC","ASC"],"keyColumnIds":[1,2,3],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} {"table":{"name":"cluster_metrics","id":78,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"id","id":1,"type":{"family":"IntFamily","width":64,"oid":20},"defaultExpr":"unique_rowid()"},{"name":"name","id":2,"type":{"family":"StringFamily","oid":25}},{"name":"labels","id":3,"type":{"family":"JsonFamily","oid":3802},"defaultExpr":"'_':::JSONB"},{"name":"type","id":4,"type":{"family":"StringFamily","oid":25}},{"name":"value","id":5,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"node_id","id":6,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"last_updated","id":7,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"crdb_internal_last_updated_shard_8","id":8,"type":{"family":"IntFamily","width":32,"oid":23},"hidden":true,"computeExpr":"mod(fnv32(md5(crdb_internal.datums_to_bytes(last_updated))), _:::INT8)","virtual":true}],"nextColumnId":9,"families":[{"name":"primary","columnNames":["id","name","labels","type","value","node_id","last_updated"],"columnIds":[1,2,3,4,5,6,7]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["id"],"keyColumnDirections":["ASC"],"storeColumnNames":["name","labels","type","value","node_id","last_updated"],"keyColumnIds":[1],"storeColumnIds":[2,3,4,5,6,7],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":2,"vecConfig":{}},"indexes":[{"name":"name_labels_idx","id":2,"unique":true,"version":3,"keyColumnNames":["name","labels"],"keyColumnDirections":["ASC","ASC"],"keyColumnIds":[2,3],"keySuffixColumnIds":[1],"compositeColumnIds":[3],"foreignKey":{},"interleave":{},"partitioning":{},"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},{"name":"last_updated_idx","id":3,"version":3,"keyColumnNames":["crdb_internal_last_updated_shard_8","last_updated"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["name","labels","type","value","node_id"],"keyColumnIds":[8,7],"keySuffixColumnIds":[1],"storeColumnIds":[2,3,4,5,6],"foreignKey":{},"interleave":{},"partitioning":{},"sharded":{"isSharded":true,"name":"crdb_internal_last_updated_shard_8","shardBuckets":8,"columnNames":["last_updated"]},"geoConfig":{},"vecConfig":{}}],"nextIndexId":4,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"checks":[{"expr":"crdb_internal_last_updated_shard_8 IN (_:::INT8, _:::INT8, _:::INT8, _:::INT8, _:::INT8, _:::INT8, _:::INT8, _:::INT8)","name":"check_crdb_internal_last_updated_shard_8","columnIds":[8],"fromHashShardedColumn":true,"constraintId":3}],"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":4}} {"table":{"name":"comments","id":24,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"type","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"object_id","id":2,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"sub_id","id":3,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"comment","id":4,"type":{"family":"StringFamily","oid":25}}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["type","object_id","sub_id"],"columnIds":[1,2,3]},{"name":"fam_4_comment","id":4,"columnNames":["comment"],"columnIds":[4],"defaultColumnId":4}],"nextFamilyId":5,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["type","object_id","sub_id"],"keyColumnDirections":["ASC","ASC","ASC"],"storeColumnNames":["comment"],"keyColumnIds":[1,2,3],"storeColumnIds":[4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"public","privileges":"32"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} @@ -928,7 +928,7 @@ schema_telemetry schema_telemetry snapshot_id=7cd8a9ae-f35c-4cd2-970a-757174600874 max_records=10 ---- -{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":10}}} +{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":12}}} {"table":{"name":"descriptor_id_seq","id":7,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"value","id":1,"type":{"family":"IntFamily","width":64,"oid":20}}],"families":[{"name":"primary","columnNames":["value"],"columnIds":[1],"defaultColumnId":1}],"primaryIndex":{"name":"primary","id":1,"version":4,"keyColumnNames":["value"],"keyColumnDirections":["ASC"],"keyColumnIds":[1],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"vecConfig":{}},"privileges":{"users":[{"userProto":"admin","privileges":"32","withGrantOption":"32"},{"userProto":"root","privileges":"32","withGrantOption":"32"}],"ownerProto":"node","version":3},"formatVersion":3,"sequenceOpts":{"increment":"1","minValue":"1","maxValue":"9223372036854775807","start":"1","sequenceOwner":{},"sessionCacheSize":"1"},"replacementOf":{"time":{}},"createAsOfTime":{}}} {"table":{"name":"job_progress","id":68,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"job_id","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"written","id":2,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"fraction","id":3,"type":{"family":"FloatFamily","width":64,"oid":701},"nullable":true},{"name":"resolved","id":4,"type":{"family":"DecimalFamily","oid":1700},"nullable":true}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["job_id","written","fraction","resolved"],"columnIds":[1,2,3,4]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["job_id","written"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["fraction","resolved"],"keyColumnIds":[1,2],"storeColumnIds":[3,4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} {"table":{"name":"job_progress_history","id":69,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"job_id","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"written","id":2,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"fraction","id":3,"type":{"family":"FloatFamily","width":64,"oid":701},"nullable":true},{"name":"resolved","id":4,"type":{"family":"DecimalFamily","oid":1700},"nullable":true}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["job_id","written","fraction","resolved"],"columnIds":[1,2,3,4]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["job_id","written"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["fraction","resolved"],"keyColumnIds":[1,2],"storeColumnIds":[3,4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} @@ -941,7 +941,7 @@ schema_telemetry snapshot_id=7cd8a9ae-f35c-4cd2-970a-757174600874 max_records=10 schema_telemetry snapshot_id=7cd8a9ae-f35c-4cd2-970a-757174600874 max_records=10 ---- -{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":10}}} +{"database":{"name":"system","id":1,"modificationTime":{"wallTime":"0"},"version":"1","privileges":{"users":[{"userProto":"admin","privileges":"2048","withGrantOption":"2048"},{"userProto":"root","privileges":"2048","withGrantOption":"2048"}],"ownerProto":"node","version":3},"systemDatabaseSchemaVersion":{"majorVal":1000026,"minorVal":2,"internal":12}}} {"table":{"name":"descriptor_id_seq","id":7,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"value","id":1,"type":{"family":"IntFamily","width":64,"oid":20}}],"families":[{"name":"primary","columnNames":["value"],"columnIds":[1],"defaultColumnId":1}],"primaryIndex":{"name":"primary","id":1,"version":4,"keyColumnNames":["value"],"keyColumnDirections":["ASC"],"keyColumnIds":[1],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"vecConfig":{}},"privileges":{"users":[{"userProto":"admin","privileges":"32","withGrantOption":"32"},{"userProto":"root","privileges":"32","withGrantOption":"32"}],"ownerProto":"node","version":3},"formatVersion":3,"sequenceOpts":{"increment":"1","minValue":"1","maxValue":"9223372036854775807","start":"1","sequenceOwner":{},"sessionCacheSize":"1"},"replacementOf":{"time":{}},"createAsOfTime":{}}} {"table":{"name":"job_progress","id":68,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"job_id","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"written","id":2,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"fraction","id":3,"type":{"family":"FloatFamily","width":64,"oid":701},"nullable":true},{"name":"resolved","id":4,"type":{"family":"DecimalFamily","oid":1700},"nullable":true}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["job_id","written","fraction","resolved"],"columnIds":[1,2,3,4]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["job_id","written"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["fraction","resolved"],"keyColumnIds":[1,2],"storeColumnIds":[3,4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} {"table":{"name":"job_progress_history","id":69,"version":"1","modificationTime":{},"parentId":1,"unexposedParentSchemaId":29,"columns":[{"name":"job_id","id":1,"type":{"family":"IntFamily","width":64,"oid":20}},{"name":"written","id":2,"type":{"family":"TimestampTZFamily","oid":1184},"defaultExpr":"now():::TIMESTAMPTZ"},{"name":"fraction","id":3,"type":{"family":"FloatFamily","width":64,"oid":701},"nullable":true},{"name":"resolved","id":4,"type":{"family":"DecimalFamily","oid":1700},"nullable":true}],"nextColumnId":5,"families":[{"name":"primary","columnNames":["job_id","written","fraction","resolved"],"columnIds":[1,2,3,4]}],"nextFamilyId":1,"primaryIndex":{"name":"primary","id":1,"unique":true,"version":4,"keyColumnNames":["job_id","written"],"keyColumnDirections":["ASC","DESC"],"storeColumnNames":["fraction","resolved"],"keyColumnIds":[1,2],"storeColumnIds":[3,4],"foreignKey":{},"interleave":{},"partitioning":{},"encodingType":1,"sharded":{},"geoConfig":{},"constraintId":1,"vecConfig":{}},"nextIndexId":2,"privileges":{"users":[{"userProto":"admin","privileges":"480","withGrantOption":"480"},{"userProto":"root","privileges":"480","withGrantOption":"480"}],"ownerProto":"node","version":3},"nextMutationId":1,"formatVersion":3,"replacementOf":{"time":{}},"createAsOfTime":{},"nextConstraintId":2}} diff --git a/pkg/sql/catalog/tabledesc/validate_test.go b/pkg/sql/catalog/tabledesc/validate_test.go index ad7546638e89..e15a1fed707b 100644 --- a/pkg/sql/catalog/tabledesc/validate_test.go +++ b/pkg/sql/catalog/tabledesc/validate_test.go @@ -350,6 +350,7 @@ var validationMap = []struct { "IsProcedure": {status: thisFieldReferencesNoObjects}, "Security": {status: thisFieldReferencesNoObjects}, "ReplicatedPCRVersion": {status: thisFieldReferencesNoObjects}, + "CanMutate": {status: thisFieldReferencesNoObjects}, }, }, { diff --git a/pkg/sql/create_function.go b/pkg/sql/create_function.go index 4c39cd72832e..bad7aea8ca08 100644 --- a/pkg/sql/create_function.go +++ b/pkg/sql/create_function.go @@ -9,6 +9,7 @@ import ( "context" "fmt" + "github.com/cockroachdb/cockroach/pkg/clusterversion" "github.com/cockroachdb/cockroach/pkg/keys" "github.com/cockroachdb/cockroach/pkg/server/telemetry" "github.com/cockroachdb/cockroach/pkg/sql/catalog" @@ -134,6 +135,17 @@ func (n *createFunctionNode) createNewFunction( return err } + // Only persist CanMutate after the version gate is active. During a + // rolling upgrade, old nodes don't know about the can_mutate field and + // won't reset it in resetFuncOption during CREATE OR REPLACE. If we + // wrote the field before finalization and then rolled back, a subsequent + // CREATE OR REPLACE on the old binary could leave a stale value (e.g. + // CANNOT_MUTATE on a body that now contains DML), causing new nodes + // after re-upgrade to incorrectly skip the body-loop fallback. + if params.p.EvalContext().Settings.Version.IsActive(params.ctx, clusterversion.V26_3_FunctionDescCanMutate) { + udfDesc.SetCanMutate(funcdesc.CanMutateBoolToProto(n.cf.CanMutate)) + } + if err := n.addUDFReferences(udfDesc, params); err != nil { return err } @@ -273,6 +285,17 @@ func (n *createFunctionNode) replaceFunction( return err } + // Only persist CanMutate after the version gate is active. During a + // rolling upgrade, old nodes don't know about the can_mutate field and + // won't reset it in resetFuncOption during CREATE OR REPLACE. If we + // wrote the field before finalization and then rolled back, a subsequent + // CREATE OR REPLACE on the old binary could leave a stale value (e.g. + // CANNOT_MUTATE on a body that now contains DML), causing new nodes + // after re-upgrade to incorrectly skip the body-loop fallback. + if params.p.EvalContext().Settings.Version.IsActive(params.ctx, clusterversion.V26_3_FunctionDescCanMutate) { + udfDesc.SetCanMutate(funcdesc.CanMutateBoolToProto(n.cf.CanMutate)) + } + // Removing all existing references before adding new references. for _, id := range udfDesc.DependsOn { backRefMutable, err := params.p.Descriptors().MutableByID(params.p.txn).Table(params.ctx, id) @@ -615,6 +638,10 @@ func resetFuncOption(udfDesc *funcdesc.Mutable) { udfDesc.SetVolatility(catpb.Function_VOLATILE) udfDesc.SetNullInputBehavior(catpb.Function_CALLED_ON_NULL_INPUT) udfDesc.SetLeakProof(false) + // Reset CanMutate so that a stale value from a previous version of the + // descriptor does not persist after CREATE OR REPLACE. The correct value + // is re-set below, gated on V26_3_FunctionDescCanMutate. + udfDesc.SetCanMutate(catpb.Function_UNKNOWN_CAN_MUTATE) } func makeFunctionParam( diff --git a/pkg/sql/logictest/testdata/logic_test/crdb_internal_catalog b/pkg/sql/logictest/testdata/logic_test/crdb_internal_catalog index 92de169deaa0..c1343e76f6c7 100644 --- a/pkg/sql/logictest/testdata/logic_test/crdb_internal_catalog +++ b/pkg/sql/logictest/testdata/logic_test/crdb_internal_catalog @@ -138,7 +138,7 @@ skipif config schema-locked-disabled local-mixed-25.4 local-mixed-26.1 local-mix query IT SELECT least(id, 999), strip_volatile(descriptor) FROM crdb_internal.kv_catalog_descriptor WHERE id IN (1, 2, 3, 29, 'geometry_columns'::regclass::int) OR (id > 100 and id < 200) ORDER BY id ---- -1 {"database": {"id": 1, "name": "system", "privileges": {"ownerProto": "node", "users": [{"privileges": "2048", "userProto": "admin", "withGrantOption": "2048"}, {"privileges": "2048", "userProto": "root", "withGrantOption": "2048"}], "version": 3}, "systemDatabaseSchemaVersion": {"internal": 10, "majorVal": 1000026, "minorVal": 2}, "version": "1"}} +1 {"database": {"id": 1, "name": "system", "privileges": {"ownerProto": "node", "users": [{"privileges": "2048", "userProto": "admin", "withGrantOption": "2048"}, {"privileges": "2048", "userProto": "root", "withGrantOption": "2048"}], "version": 3}, "systemDatabaseSchemaVersion": {"internal": 12, "majorVal": 1000026, "minorVal": 2}, "version": "1"}} 3 {"table": {"columns": [{"id": 1, "name": "id", "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 2, "name": "descriptor", "nullable": true, "type": {"family": "BytesFamily", "oid": 17}}], "formatVersion": 3, "name": "descriptor", "nextColumnId": 3, "nextConstraintId": 2, "nextIndexId": 2, "nextMutationId": 1, "parentId": 1, "primaryIndex": {"constraintId": 1, "encodingType": 1, "foreignKey": {}, "geoConfig": {}, "id": 1, "interleave": {}, "keyColumnDirections": ["ASC"], "keyColumnIds": [1], "keyColumnNames": ["id"], "name": "primary", "partitioning": {}, "sharded": {}, "storeColumnIds": [2], "storeColumnNames": ["descriptor"], "unique": true, "vecConfig": {}, "version": 4}, "privileges": {"ownerProto": "node", "users": [{"privileges": "32", "userProto": "admin", "withGrantOption": "32"}, {"privileges": "32", "userProto": "root", "withGrantOption": "32"}], "version": 3}, "replacementOf": {"time": {}}, "version": "1"}} 29 {"schema": {"defaultPrivileges": {"type": "SCHEMA"}, "id": 29, "name": "public", "privileges": {"ownerProto": "node", "users": [{"privileges": "2", "userProto": "admin", "withGrantOption": "2"}, {"privileges": "516", "userProto": "public"}, {"privileges": "2", "userProto": "root", "withGrantOption": "2"}], "version": 3}, "version": "1"}} 101 {"schema": {"id": 101, "name": "public", "parentId": 100, "privileges": {"ownerProto": "root", "users": [{"privileges": "2", "userProto": "admin", "withGrantOption": "2"}, {"privileges": "516", "userProto": "public"}, {"privileges": "2", "userProto": "root", "withGrantOption": "2"}], "version": 3}, "version": "1"}} diff --git a/pkg/sql/logictest/testdata/logic_test/mixed_version_udf_can_mutate b/pkg/sql/logictest/testdata/logic_test/mixed_version_udf_can_mutate new file mode 100644 index 000000000000..ceb4a885105e --- /dev/null +++ b/pkg/sql/logictest/testdata/logic_test/mixed_version_udf_can_mutate @@ -0,0 +1,86 @@ +# LogicTest: cockroach-go-testserver-configs + +# Test that the CanMutate field on function descriptors is handled correctly +# across an upgrade. Functions created before V26_3_FunctionDescCanMutate have +# CanMutate = UNKNOWN (zero value, omitted from JSON). After upgrade, new +# functions get the field set, and CREATE FUNCTION correctly determines +# CanMutate by eagerly building callee bodies even when the callee has an +# UNKNOWN descriptor. + +statement ok +CREATE TABLE ab (a INT PRIMARY KEY, b INT) + +# t0: Create f1 (mutating) on old binary. CanMutate won't be persisted. +statement ok +CREATE FUNCTION f1_mutating() RETURNS VOID LANGUAGE SQL AS 'INSERT INTO ab VALUES (1, 2)' + +# Verify canMutate is absent on the old-binary descriptor. +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_mutating()'::regprocedure::oid::int - 100000 +---- +NULL + +# t1: Upgrade all nodes and finalize. +upgrade all + +statement ok +SET CLUSTER SETTING version = crdb_internal.node_executable_version() + +# t2: Create f2 (non-mutating) on new binary. Should get CANNOT_MUTATE. +statement ok +CREATE FUNCTION f2_readonly() RETURNS INT LANGUAGE SQL AS 'SELECT 1' + +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_readonly()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +# t3: Create f3 that calls both f1 (UNKNOWN) and f2 (CANNOT_MUTATE). +# During CREATE FUNCTION, b.insideFuncDef forces eager build of f1's body, +# so f1's mutation is detected and f3 correctly gets CAN_MUTATE. +statement ok +CREATE FUNCTION f3_calls_both() RETURNS INT LANGUAGE SQL AS $$ + SELECT f1_mutating(); + SELECT f2_readonly(); +$$ + +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f3_calls_both()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +# Verify the "mutations" property propagates in query plans. +# f3_calls_both should show mutations because it transitively mutates. +query B +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' = 'CAN_MUTATE' +FROM system.descriptor +WHERE id = 'f3_calls_both()'::regprocedure::oid::int - 100000 +---- +true + +# CREATE OR REPLACE f1 on the new binary should now set CanMutate. +statement ok +CREATE OR REPLACE FUNCTION f1_mutating() RETURNS VOID LANGUAGE SQL AS 'INSERT INTO ab VALUES (1, 2)' + +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_mutating()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE diff --git a/pkg/sql/logictest/tests/cockroach-go-testserver-25.4/BUILD.bazel b/pkg/sql/logictest/tests/cockroach-go-testserver-25.4/BUILD.bazel index cfa49078ac24..1ca72dc27260 100644 --- a/pkg/sql/logictest/tests/cockroach-go-testserver-25.4/BUILD.bazel +++ b/pkg/sql/logictest/tests/cockroach-go-testserver-25.4/BUILD.bazel @@ -11,7 +11,7 @@ go_test( "//pkg/sql/logictest:testdata", # keep ], exec_properties = {"test.Pool": "heavy"}, - shard_count = 11, + shard_count = 12, tags = ["cpu:3"], deps = [ "//pkg/base", diff --git a/pkg/sql/logictest/tests/cockroach-go-testserver-25.4/generated_test.go b/pkg/sql/logictest/tests/cockroach-go-testserver-25.4/generated_test.go index d9ea6c6711d4..d7ad502aa36b 100644 --- a/pkg/sql/logictest/tests/cockroach-go-testserver-25.4/generated_test.go +++ b/pkg/sql/logictest/tests/cockroach-go-testserver-25.4/generated_test.go @@ -134,6 +134,13 @@ func TestLogic_mixed_version_trigger_backref( runLogicTest(t, "mixed_version_trigger_backref") } +func TestLogic_mixed_version_udf_can_mutate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "mixed_version_udf_can_mutate") +} + func TestLogic_mixed_version_upgrade_preserve_ttl( t *testing.T, ) { diff --git a/pkg/sql/logictest/tests/cockroach-go-testserver-26.1/BUILD.bazel b/pkg/sql/logictest/tests/cockroach-go-testserver-26.1/BUILD.bazel index d9691cba8451..a86448939850 100644 --- a/pkg/sql/logictest/tests/cockroach-go-testserver-26.1/BUILD.bazel +++ b/pkg/sql/logictest/tests/cockroach-go-testserver-26.1/BUILD.bazel @@ -11,7 +11,7 @@ go_test( "//pkg/sql/logictest:testdata", # keep ], exec_properties = {"test.Pool": "heavy"}, - shard_count = 10, + shard_count = 11, tags = ["cpu:3"], deps = [ "//pkg/base", diff --git a/pkg/sql/logictest/tests/cockroach-go-testserver-26.1/generated_test.go b/pkg/sql/logictest/tests/cockroach-go-testserver-26.1/generated_test.go index a3ca1214b264..9213eb4db6b8 100644 --- a/pkg/sql/logictest/tests/cockroach-go-testserver-26.1/generated_test.go +++ b/pkg/sql/logictest/tests/cockroach-go-testserver-26.1/generated_test.go @@ -134,6 +134,13 @@ func TestLogic_mixed_version_trigger_backref( runLogicTest(t, "mixed_version_trigger_backref") } +func TestLogic_mixed_version_udf_can_mutate( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runLogicTest(t, "mixed_version_udf_can_mutate") +} + func TestLogic_upgrade( t *testing.T, ) { diff --git a/pkg/sql/opt/exec/execbuilder/relational.go b/pkg/sql/opt/exec/execbuilder/relational.go index 0c7cfa3d848a..7f7afce7369a 100644 --- a/pkg/sql/opt/exec/execbuilder/relational.go +++ b/pkg/sql/opt/exec/execbuilder/relational.go @@ -3711,9 +3711,16 @@ func (b *Builder) buildCall(c *memo.CallExpr) (_ execPlan, outputCols colOrdMap, } } - for _, s := range udf.Def.Body { - if s.Relational().CanMutate { - b.setMutationFlags(s) + switch udf.Def.CanMutate { + case tree.RoutineMutates: + b.flags.Set(exec.PlanFlagContainsMutation) + case tree.RoutineCanMutateUnknown: + // The descriptor predates the can_mutate field. Fall back to + // inspecting the eagerly-built body expressions. + for _, s := range udf.Def.Body { + if s.Relational().CanMutate { + b.setMutationFlags(s) + } } } // Create a tree.RoutinePlanFn that can plan the statements in the UDF body. diff --git a/pkg/sql/opt/exec/execbuilder/scalar.go b/pkg/sql/opt/exec/execbuilder/scalar.go index a2ae30126ea1..f4ed9a420734 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -988,9 +988,16 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ return nil, err } - for _, s := range udf.Def.Body { - if s.Relational().CanMutate { - b.setMutationFlags(s) + switch udf.Def.CanMutate { + case tree.RoutineMutates: + b.flags.Set(exec.PlanFlagContainsMutation) + case tree.RoutineCanMutateUnknown: + // The descriptor predates the can_mutate field. Fall back to + // inspecting the eagerly-built body expressions. + for _, s := range udf.Def.Body { + if s.Relational().CanMutate { + b.setMutationFlags(s) + } } } diff --git a/pkg/sql/opt/memo/expr.go b/pkg/sql/opt/memo/expr.go index 9c7dbcc6780e..542cf996c5e8 100644 --- a/pkg/sql/opt/memo/expr.go +++ b/pkg/sql/opt/memo/expr.go @@ -783,6 +783,16 @@ type UDFDefinition struct { // When set, Body and BodyProps are nil at plan time; they will be // populated by calling BodyBuilder.Build() at execution time. BodyBuilder RoutineBodyBuilder + + // CanMutate indicates whether the routine body can perform mutations. + // For descriptor-backed routines this is sourced from the persisted + // descriptor field via the Overload. When RoutineCanMutateUnknown + // (for descriptors predating the field), consumers fall back to + // inspecting Body RelExprs to determine mutation behavior. For + // anonymous routines (DO blocks, triggers) this is always + // RoutineMutates or RoutineDoesNotMutate, derived from the body + // expression at build time. + CanMutate tree.RoutineCanMutate } // ExceptionBlock contains the information needed to match and handle errors in diff --git a/pkg/sql/opt/memo/logical_props_builder.go b/pkg/sql/opt/memo/logical_props_builder.go index f6a14ff0844d..942bd2aad9d0 100644 --- a/pkg/sql/opt/memo/logical_props_builder.go +++ b/pkg/sql/opt/memo/logical_props_builder.go @@ -1919,11 +1919,19 @@ func BuildSharedProps(e opt.Expr, shared *props.Shared, evalCtx *eval.Context) { case *UDFCallExpr: shared.HasUDF = true shared.VolatilitySet.Add(t.Def.Volatility) - for _, s := range t.Def.Body { - if s != nil { - if relExpr := s.Relational(); relExpr != nil && relExpr.CanMutate { - shared.CanMutate = true - break + switch t.Def.CanMutate { + case tree.RoutineMutates: + shared.CanMutate = true + case tree.RoutineCanMutateUnknown: + // The descriptor predates the can_mutate field. Fall back to + // inspecting the eagerly-built body expressions. (Deferred-build + // routines always have a known CanMutate value.) + for _, s := range t.Def.Body { + if s != nil { + if relExpr := s.Relational(); relExpr != nil && relExpr.CanMutate { + shared.CanMutate = true + break + } } } } diff --git a/pkg/sql/opt/optbuilder/create_function.go b/pkg/sql/opt/optbuilder/create_function.go index faa64e4218f4..82ba33f5ad73 100644 --- a/pkg/sql/opt/optbuilder/create_function.go +++ b/pkg/sql/opt/optbuilder/create_function.go @@ -377,6 +377,7 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o // Validate each statement and collect the dependencies. var stmtScope *scope + var canMutate bool switch language { case tree.RoutineLangSQL: // Parse the function body. @@ -402,6 +403,9 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o stmtScope = b.buildStmtAtRootWithScope(stmts[i].AST, nil /* desiredTypes */, bodyScope) }) checkStmtVolatility(targetVolatility, stmtScope, stmt.AST) + if !canMutate && stmtScope.expr != nil && stmtScope.expr.Relational().CanMutate { + canMutate = true + } // Format the statements with qualified datasource names. formatFuncBodyStmt(fmtCtx, stmt.AST, language, i > 0 /* newLine */) @@ -468,6 +472,9 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o stmtScope = plBuilder.buildRootBlock(stmt.AST, bodyScope, routineParams) }) checkStmtVolatility(targetVolatility, stmtScope, stmt) + if stmtScope.expr != nil && stmtScope.expr.Relational().CanMutate { + canMutate = true + } // Format the statements with qualified datasource names. formatFuncBodyStmt(fmtCtx, stmt.AST, language, false /* newLine */) @@ -476,6 +483,8 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o panic(errors.AssertionFailedf("unexpected language: %v", language)) } + cf.CanMutate = canMutate + if stmtScope != nil && (language != tree.RoutineLangPLpgSQL || !isSetReturning) { // Validate that the result type of the last statement matches the // return type of the function. We skip this validation for PL/pgSQL SRFs diff --git a/pkg/sql/opt/optbuilder/routine.go b/pkg/sql/opt/optbuilder/routine.go index 5087ef0ef6e1..b69dcbfefd5a 100644 --- a/pkg/sql/opt/optbuilder/routine.go +++ b/pkg/sql/opt/optbuilder/routine.go @@ -516,6 +516,22 @@ func (b *Builder) buildRoutine( panic(errors.AssertionFailedf("unexpected language: %v", o.Language)) } + // Derive canMutate from the descriptor (via the Overload). When the + // descriptor's CanMutate is unknown (for descriptors predating the + // field), fall back to inspecting the eagerly-built body expressions. + canMutate := o.CanMutate + if canMutate == tree.RoutineCanMutateUnknown { + for _, s := range body { + if s.Relational().CanMutate { + canMutate = tree.RoutineMutates + break + } + } + if canMutate == tree.RoutineCanMutateUnknown { + canMutate = tree.RoutineDoesNotMutate + } + } + multiColDataSource := len(f.ResolvedType().TupleContents()) > 0 && oldInsideDataSource routine := b.factory.ConstructUDFCall( args, @@ -536,6 +552,7 @@ func (b *Builder) buildRoutine( BodyASTs: bodyASTs, Params: params, ResultBufferID: resultBufferID, + CanMutate: canMutate, }, }, ) @@ -972,6 +989,7 @@ func (b *Builder) buildDo(do *tree.DoBlock, inScope *scope) *scope { // Build a CALL expression that invokes the routine. outScope := inScope.push() + bodyExpr := bodyScope.expr routine := b.factory.ConstructUDFCall( memo.ScalarListExpr{}, &memo.UDFCallPrivate{ @@ -981,10 +999,11 @@ func (b *Builder) buildDo(do *tree.DoBlock, inScope *scope) *scope { Volatility: volatility.Volatile, RoutineType: tree.ProcedureRoutine, RoutineLang: tree.RoutineLangPLpgSQL, - Body: []memo.RelExpr{bodyScope.expr}, + Body: []memo.RelExpr{bodyExpr}, BodyProps: []*physical.Required{bodyScope.makePhysicalProps()}, BodyStmts: bodyStmts, BodyASTs: []tree.Statement{nil}, + CanMutate: tree.RoutineCanMutateFromBool(bodyExpr.Relational().CanMutate), }, }, ) diff --git a/pkg/sql/opt/optbuilder/trigger.go b/pkg/sql/opt/optbuilder/trigger.go index d0665afc803f..d18f753ad51a 100644 --- a/pkg/sql/opt/optbuilder/trigger.go +++ b/pkg/sql/opt/optbuilder/trigger.go @@ -856,6 +856,7 @@ func (b *Builder) buildTriggerFunction( stmtScope := plBuilder.buildRootBlock(stmt.AST, triggerFuncScope, params) udfDef.Body = []memo.RelExpr{stmtScope.expr} udfDef.BodyProps = []*physical.Required{stmtScope.makePhysicalProps()} + udfDef.CanMutate = tree.RoutineCanMutateFromBool(stmtScope.expr.Relational().CanMutate) // Placeholder that ensure the length of BodyTags is the same as Body. udfDef.BodyTags = make([]string, 1) diff --git a/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go b/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go index 9a9bdcf70168..b7984d35a34c 100644 --- a/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go +++ b/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go @@ -210,7 +210,11 @@ func CreateFunction(b BuildCtx, n *tree.CreateRoutine) { validateTypeReferences(b, refProvider, db.DatabaseID) validateFunctionRelationReferences(b, refProvider, db.DatabaseID) validateFunctionToFunctionReferences(b, refProvider, db.DatabaseID) - b.Add(b.WrapFunctionBody(fnID, fnBodyStr, lang, typ, refProvider)) + fnBody := b.WrapFunctionBody(fnID, fnBodyStr, lang, typ, refProvider) + if b.EvalCtx().Settings.Version.ActiveVersion(b).IsActive(clusterversion.V26_3_FunctionDescCanMutate) { + fnBody.CanMutate = funcdesc.CanMutateBoolToProto(n.CanMutate) + } + b.Add(fnBody) if b.EvalCtx().Settings.Version.ActiveVersion(b).IsActive(clusterversion.V26_2) { b.Add(&scpb.FunctionParams{ FunctionID: fnID, @@ -410,6 +414,9 @@ func replaceFunction( // Build the FunctionBody element with the new body and references. fnBody := b.WrapFunctionBody(fnID, fnBodyStr, lang, typ, refProvider) + if b.EvalCtx().Settings.Version.ActiveVersion(b).IsActive(clusterversion.V26_3_FunctionDescCanMutate) { + fnBody.CanMutate = funcdesc.CanMutateBoolToProto(n.CanMutate) + } b.Replace(fnBody) // Replace the FunctionParams element with the updated params. diff --git a/pkg/sql/schemachanger/scbuild/testdata/create_function b/pkg/sql/schemachanger/scbuild/testdata/create_function index 1ce79e84aa76..4a1312934573 100644 --- a/pkg/sql/schemachanger/scbuild/testdata/create_function +++ b/pkg/sql/schemachanger/scbuild/testdata/create_function @@ -39,7 +39,7 @@ $$; - [[UserPrivileges:{DescID: 110, Name: root}, PUBLIC], ABSENT] {descriptorId: 110, privileges: "2", userName: root, withGrantOption: "2"} - [[FunctionBody:{DescID: 110}, PUBLIC], ABSENT] - {body: "SELECT a FROM t;\nSELECT b FROM t@t_idx_b;\nSELECT c FROM t@t_idx_c;\nSELECT a FROM v;\nSELECT b'@':::@100108;\nSELECT nextval(105:::REGCLASS);", functionId: 110, lang: {lang: SQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [108, 109], usesViews: [{columnIds: [1], viewId: 107}]} + {body: "SELECT a FROM t;\nSELECT b FROM t@t_idx_b;\nSELECT c FROM t@t_idx_c;\nSELECT a FROM v;\nSELECT b'@':::@100108;\nSELECT nextval(105:::REGCLASS);", canMutate: 2, functionId: 110, lang: {lang: SQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [108, 109], usesViews: [{columnIds: [1], viewId: 107}]} - [[FunctionParams:{DescID: 110}, PUBLIC], ABSENT] {functionId: 110, params: [{class: {}, name: a, type: {closedTypeIds: [108, 109], type: {family: EnumFamily, oid: 100108, udtMetadata: {arrayTypeOid: 100109}}, typeName: public.notmyworkday}}]} @@ -72,6 +72,6 @@ $$; - [[UserPrivileges:{DescID: 111, Name: root}, PUBLIC], ABSENT] {descriptorId: 111, privileges: "2", userName: root, withGrantOption: "2"} - [[FunctionBody:{DescID: 111}, PUBLIC], ABSENT] - {body: "BEGIN\nSELECT a FROM t;\nSELECT b FROM t@t_idx_b;\nSELECT c FROM t@t_idx_c;\nSELECT a FROM v;\nSELECT b'@':::@100108;\nRETURN nextval(105:::REGCLASS);\nEND;\n", functionId: 111, lang: {lang: PLPGSQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [108, 109], usesViews: [{columnIds: [1], viewId: 107}]} + {body: "BEGIN\nSELECT a FROM t;\nSELECT b FROM t@t_idx_b;\nSELECT c FROM t@t_idx_c;\nSELECT a FROM v;\nSELECT b'@':::@100108;\nRETURN nextval(105:::REGCLASS);\nEND;\n", canMutate: 2, functionId: 111, lang: {lang: PLPGSQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [108, 109], usesViews: [{columnIds: [1], viewId: 107}]} - [[FunctionParams:{DescID: 111}, PUBLIC], ABSENT] {functionId: 111, params: [{class: {}, name: a, type: {closedTypeIds: [108, 109], type: {family: EnumFamily, oid: 100108, udtMetadata: {arrayTypeOid: 100109}}, typeName: public.notmyworkday}}]} diff --git a/pkg/sql/schemachanger/scbuild/testdata/create_or_replace_function b/pkg/sql/schemachanger/scbuild/testdata/create_or_replace_function index 1908c1b3a647..d737ab52510b 100644 --- a/pkg/sql/schemachanger/scbuild/testdata/create_or_replace_function +++ b/pkg/sql/schemachanger/scbuild/testdata/create_or_replace_function @@ -34,7 +34,7 @@ $$; - [[FunctionParams:{DescID: 109}, PUBLIC], ABSENT] {functionId: 109, params: [{class: {}, name: a, type: {closedTypeIds: [107, 108], type: {family: EnumFamily, oid: 100107, udtMetadata: {arrayTypeOid: 100108}}, typeName: public.notmyworkday}}]} - [[FunctionBody:{DescID: 109}, PUBLIC], ABSENT] - {body: SELECT 1;, functionId: 109, lang: {lang: SQL}, usesTypeIds: [107, 108]} + {body: SELECT 1;, canMutate: 2, functionId: 109, lang: {lang: SQL}, usesTypeIds: [107, 108]} # Replace a trigger function that has an active trigger. The trigger's # TriggerDeps and TriggerFunctionCall should be updated via Drop+Add. @@ -70,7 +70,7 @@ $$; - [[FunctionParams:{DescID: 113}, PUBLIC], ABSENT] {functionId: 113, params: []} - [[FunctionBody:{DescID: 113}, PUBLIC], ABSENT] - {body: "\n BEGIN\n INSERT INTO log2 VALUES ('replaced');\n RETURN NEW;\n END\n", functionId: 113, lang: {lang: PLPGSQL}} + {body: "\n BEGIN\n INSERT INTO log2 VALUES ('replaced');\n RETURN NEW;\n END\n", canMutate: 2, functionId: 113, lang: {lang: PLPGSQL}} - [[IndexData:{DescID: 110, IndexID: 1}, PUBLIC], PUBLIC] {indexId: 1, tableId: 110} - [[TriggerFunctionCall:{DescID: 110, TriggerID: 1}, PUBLIC], ABSENT] diff --git a/pkg/sql/schemachanger/scbuild/testdata/create_procedure b/pkg/sql/schemachanger/scbuild/testdata/create_procedure index a97f90d83518..21798f88330d 100644 --- a/pkg/sql/schemachanger/scbuild/testdata/create_procedure +++ b/pkg/sql/schemachanger/scbuild/testdata/create_procedure @@ -37,7 +37,7 @@ $$; - [[UserPrivileges:{DescID: 110, Name: root}, PUBLIC], ABSENT] {descriptorId: 110, privileges: "2", userName: root, withGrantOption: "2"} - [[FunctionBody:{DescID: 110}, PUBLIC], ABSENT] - {body: "SELECT a FROM t;\nSELECT b FROM t@t_idx_b;\nSELECT c FROM t@t_idx_c;\nSELECT a FROM v;\nSELECT b'@':::@100108;\nSELECT nextval(105:::REGCLASS);", functionId: 110, lang: {lang: SQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [108, 109], usesViews: [{columnIds: [1], viewId: 107}]} + {body: "SELECT a FROM t;\nSELECT b FROM t@t_idx_b;\nSELECT c FROM t@t_idx_c;\nSELECT a FROM v;\nSELECT b'@':::@100108;\nSELECT nextval(105:::REGCLASS);", canMutate: 2, functionId: 110, lang: {lang: SQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [108, 109], usesViews: [{columnIds: [1], viewId: 107}]} - [[FunctionParams:{DescID: 110}, PUBLIC], ABSENT] {functionId: 110, params: [{class: {}, name: a, type: {closedTypeIds: [108, 109], type: {family: EnumFamily, oid: 100108, udtMetadata: {arrayTypeOid: 100109}}, typeName: public.notmyworkday}}]} @@ -68,6 +68,6 @@ $$; - [[UserPrivileges:{DescID: 111, Name: root}, PUBLIC], ABSENT] {descriptorId: 111, privileges: "2", userName: root, withGrantOption: "2"} - [[FunctionBody:{DescID: 111}, PUBLIC], ABSENT] - {body: "BEGIN\nSELECT a FROM t;\nSELECT b FROM t@t_idx_b;\nSELECT c FROM t@t_idx_c;\nSELECT a FROM v;\nSELECT b'@':::@100108;\nSELECT nextval(105:::REGCLASS);\nEND;\n", functionId: 111, lang: {lang: PLPGSQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [108, 109], usesViews: [{columnIds: [1], viewId: 107}]} + {body: "BEGIN\nSELECT a FROM t;\nSELECT b FROM t@t_idx_b;\nSELECT c FROM t@t_idx_c;\nSELECT a FROM v;\nSELECT b'@':::@100108;\nSELECT nextval(105:::REGCLASS);\nEND;\n", canMutate: 2, functionId: 111, lang: {lang: PLPGSQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [108, 109], usesViews: [{columnIds: [1], viewId: 107}]} - [[FunctionParams:{DescID: 111}, PUBLIC], ABSENT] {functionId: 111, params: [{class: {}, name: a, type: {closedTypeIds: [108, 109], type: {family: EnumFamily, oid: 100108, udtMetadata: {arrayTypeOid: 100109}}, typeName: public.notmyworkday}}]} diff --git a/pkg/sql/schemachanger/scdecomp/decomp.go b/pkg/sql/schemachanger/scdecomp/decomp.go index e10cedf4d09e..b090c89f759d 100644 --- a/pkg/sql/schemachanger/scdecomp/decomp.go +++ b/pkg/sql/schemachanger/scdecomp/decomp.go @@ -1202,6 +1202,7 @@ func (w *walkCtx) walkFunction(fnDesc catalog.FunctionDescriptor) { Body: string(fnDesc.GetFunctionBody()), Lang: catpb.FunctionLanguage{Lang: fnDesc.GetLanguage()}, UsesTypeIDs: fnDesc.GetDependsOnTypes(), + CanMutate: fnDesc.GetCanMutate(), } dedupeColIDs := func(colIDs []catid.ColumnID) []catid.ColumnID { ret := catalog.MakeTableColSet() diff --git a/pkg/sql/schemachanger/scdecomp/testdata/function b/pkg/sql/schemachanger/scdecomp/testdata/function index 881967f1fcb8..2758a0959a7f 100644 --- a/pkg/sql/schemachanger/scdecomp/testdata/function +++ b/pkg/sql/schemachanger/scdecomp/testdata/function @@ -80,6 +80,7 @@ ElementState: SELECT c FROM defaultdb.public.t@t_idx_c; SELECT a FROM defaultdb.public.v; SELECT nextval(105:::REGCLASS); + canMutate: 2 functionId: 110 lang: lang: SQL diff --git a/pkg/sql/schemachanger/scexec/scmutationexec/function.go b/pkg/sql/schemachanger/scexec/scmutationexec/function.go index 2346ec6172b0..bd07e52639aa 100644 --- a/pkg/sql/schemachanger/scexec/scmutationexec/function.go +++ b/pkg/sql/schemachanger/scexec/scmutationexec/function.go @@ -97,6 +97,7 @@ func (i *immediateVisitor) SetFunctionBody(ctx context.Context, op scop.SetFunct } fn.SetFuncBody(descpb.RoutineBody(op.Body.Body)) fn.SetLang(op.Body.Lang.Lang) + fn.SetCanMutate(op.Body.CanMutate) return nil } diff --git a/pkg/sql/schemachanger/scpb/elements.proto b/pkg/sql/schemachanger/scpb/elements.proto index af328b2c9656..aaaa123c5a4e 100644 --- a/pkg/sql/schemachanger/scpb/elements.proto +++ b/pkg/sql/schemachanger/scpb/elements.proto @@ -1094,6 +1094,7 @@ message FunctionBody { repeated uint32 uses_sequence_ids = 6 [(gogoproto.customname) = "UsesSequenceIDs", (gogoproto.casttype) = "github.com/cockroachdb/cockroach/pkg/sql/sem/catid.DescID"]; repeated uint32 uses_type_ids = 7 [(gogoproto.customname) = "UsesTypeIDs", (gogoproto.casttype) = "github.com/cockroachdb/cockroach/pkg/sql/sem/catid.DescID"]; repeated uint32 uses_function_ids = 8 [(gogoproto.customname) = "UsesFunctionIDs", (gogoproto.casttype) = "github.com/cockroachdb/cockroach/pkg/sql/sem/catid.DescID"]; + int32 can_mutate = 9 [(gogoproto.casttype) = "github.com/cockroachdb/cockroach/pkg/sql/catalog/catpb.Function_CanMutate"]; } message FunctionSecurity { diff --git a/pkg/sql/schemachanger/scpb/uml/table.puml b/pkg/sql/schemachanger/scpb/uml/table.puml index 3d7d4e0711f4..9214559a56d6 100644 --- a/pkg/sql/schemachanger/scpb/uml/table.puml +++ b/pkg/sql/schemachanger/scpb/uml/table.puml @@ -240,6 +240,7 @@ FunctionBody : []UsesViews FunctionBody : []UsesSequenceIDs FunctionBody : []UsesTypeIDs FunctionBody : []UsesFunctionIDs +FunctionBody : CanMutate object FunctionLeakProof diff --git a/pkg/sql/schemachanger/scplan/testdata/create_function b/pkg/sql/schemachanger/scplan/testdata/create_function index 5c1beaefdddc..7c24b45a6544 100644 --- a/pkg/sql/schemachanger/scplan/testdata/create_function +++ b/pkg/sql/schemachanger/scplan/testdata/create_function @@ -99,6 +99,7 @@ StatementPhase stage 1 of 1 with 13 MutationType ops SELECT a FROM v; SELECT b'@':::@100107; SELECT nextval(105:::REGCLASS); + CanMutate: 2 FunctionID: 109 Lang: Lang: 1 @@ -272,6 +273,7 @@ PreCommitPhase stage 2 of 2 with 13 MutationType ops SELECT a FROM v; SELECT b'@':::@100107; SELECT nextval(105:::REGCLASS); + CanMutate: 2 FunctionID: 109 Lang: Lang: 1 @@ -532,6 +534,7 @@ StatementPhase stage 1 of 1 with 13 MutationType ops SELECT b'@':::@100107; RETURN nextval(105:::REGCLASS); END; + CanMutate: 2 FunctionID: 111 Lang: Lang: 2 @@ -707,6 +710,7 @@ PreCommitPhase stage 2 of 2 with 13 MutationType ops SELECT b'@':::@100107; RETURN nextval(105:::REGCLASS); END; + CanMutate: 2 FunctionID: 111 Lang: Lang: 2 diff --git a/pkg/sql/schemachanger/scplan/testdata/create_or_replace_function b/pkg/sql/schemachanger/scplan/testdata/create_or_replace_function index 1020f3363b43..7f19fac18fcb 100644 --- a/pkg/sql/schemachanger/scplan/testdata/create_or_replace_function +++ b/pkg/sql/schemachanger/scplan/testdata/create_or_replace_function @@ -68,6 +68,7 @@ StatementPhase stage 1 of 1 with 8 MutationType ops *scop.SetFunctionBody Body: Body: SELECT 1; + CanMutate: 2 FunctionID: 109 Lang: Lang: 1 @@ -137,6 +138,7 @@ PreCommitPhase stage 2 of 2 with 8 MutationType ops *scop.SetFunctionBody Body: Body: SELECT 1; + CanMutate: 2 FunctionID: 109 Lang: Lang: 1 @@ -208,6 +210,7 @@ StatementPhase stage 1 of 1 with 12 MutationType ops INSERT INTO log2 VALUES ('replaced'); RETURN NEW; END + CanMutate: 2 FunctionID: 113 Lang: Lang: 2 @@ -295,6 +298,7 @@ PreCommitPhase stage 2 of 2 with 12 MutationType ops INSERT INTO log2 VALUES ('replaced'); RETURN NEW; END + CanMutate: 2 FunctionID: 113 Lang: Lang: 2 diff --git a/pkg/sql/schemachanger/scplan/testdata/create_procedure b/pkg/sql/schemachanger/scplan/testdata/create_procedure index 36f4a5dbd4b2..711b7d1bc246 100644 --- a/pkg/sql/schemachanger/scplan/testdata/create_procedure +++ b/pkg/sql/schemachanger/scplan/testdata/create_procedure @@ -95,6 +95,7 @@ StatementPhase stage 1 of 1 with 12 MutationType ops SELECT a FROM v; SELECT b'@':::@100107; SELECT nextval(105:::REGCLASS); + CanMutate: 2 FunctionID: 109 Lang: Lang: 1 @@ -263,6 +264,7 @@ PreCommitPhase stage 2 of 2 with 12 MutationType ops SELECT a FROM v; SELECT b'@':::@100107; SELECT nextval(105:::REGCLASS); + CanMutate: 2 FunctionID: 109 Lang: Lang: 1 @@ -511,6 +513,7 @@ StatementPhase stage 1 of 1 with 12 MutationType ops SELECT b'@':::@100107; SELECT nextval(105:::REGCLASS); END; + CanMutate: 2 FunctionID: 111 Lang: Lang: 2 @@ -681,6 +684,7 @@ PreCommitPhase stage 2 of 2 with 12 MutationType ops SELECT b'@':::@100107; SELECT nextval(105:::REGCLASS); END; + CanMutate: 2 FunctionID: 111 Lang: Lang: 2 diff --git a/pkg/sql/schemachanger/sctest_generated_test.go b/pkg/sql/schemachanger/sctest_generated_test.go index fcbbe54c7576..a786b54a48a2 100644 --- a/pkg/sql/schemachanger/sctest_generated_test.go +++ b/pkg/sql/schemachanger/sctest_generated_test.go @@ -645,6 +645,13 @@ func TestEndToEndSideEffects_create_function_calling_function(t *testing.T) { sctest.EndToEndSideEffects(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestEndToEndSideEffects_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.EndToEndSideEffects(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestEndToEndSideEffects_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -652,6 +659,13 @@ func TestEndToEndSideEffects_create_function_in_txn(t *testing.T) { sctest.EndToEndSideEffects(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestEndToEndSideEffects_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.EndToEndSideEffects(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestEndToEndSideEffects_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -1555,6 +1569,13 @@ func TestExecuteWithDMLInjection_create_function_calling_function(t *testing.T) sctest.ExecuteWithDMLInjection(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestExecuteWithDMLInjection_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.ExecuteWithDMLInjection(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestExecuteWithDMLInjection_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -1562,6 +1583,13 @@ func TestExecuteWithDMLInjection_create_function_in_txn(t *testing.T) { sctest.ExecuteWithDMLInjection(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestExecuteWithDMLInjection_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.ExecuteWithDMLInjection(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestExecuteWithDMLInjection_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -2465,6 +2493,13 @@ func TestGenerateSchemaChangeCorpus_create_function_calling_function(t *testing. sctest.GenerateSchemaChangeCorpus(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestGenerateSchemaChangeCorpus_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.GenerateSchemaChangeCorpus(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestGenerateSchemaChangeCorpus_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -2472,6 +2507,13 @@ func TestGenerateSchemaChangeCorpus_create_function_in_txn(t *testing.T) { sctest.GenerateSchemaChangeCorpus(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestGenerateSchemaChangeCorpus_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.GenerateSchemaChangeCorpus(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestGenerateSchemaChangeCorpus_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -3375,6 +3417,13 @@ func TestPause_create_function_calling_function(t *testing.T) { sctest.Pause(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestPause_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.Pause(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestPause_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -3382,6 +3431,13 @@ func TestPause_create_function_in_txn(t *testing.T) { sctest.Pause(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestPause_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.Pause(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestPause_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -4285,6 +4341,13 @@ func TestPauseMixedVersion_create_function_calling_function(t *testing.T) { sctest.PauseMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestPauseMixedVersion_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.PauseMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestPauseMixedVersion_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -4292,6 +4355,13 @@ func TestPauseMixedVersion_create_function_in_txn(t *testing.T) { sctest.PauseMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestPauseMixedVersion_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.PauseMixedVersion(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestPauseMixedVersion_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -5195,6 +5265,13 @@ func TestRollback_create_function_calling_function(t *testing.T) { sctest.Rollback(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestRollback_create_function_calling_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating" + sctest.Rollback(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestRollback_create_function_in_txn(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) @@ -5202,6 +5279,13 @@ func TestRollback_create_function_in_txn(t *testing.T) { sctest.Rollback(t, path, sctest.SingleNodeTestClusterFactory{}) } +func TestRollback_create_function_mutating(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + const path = "pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating" + sctest.Rollback(t, path, sctest.SingleNodeTestClusterFactory{}) +} + func TestRollback_create_index(t *testing.T) { defer leaktest.AfterTest(t)() defer log.Scope(t).Close(t) diff --git a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.side_effects index bd28fc6fe03b..a20f40ea8a01 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/alter_table_add_check_udf/alter_table_add_check_udf.side_effects @@ -66,6 +66,7 @@ upsert descriptor #104 + version: "2" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE + dependedOnBy: + - constraintIds: + - 2 @@ -162,6 +163,7 @@ upsert descriptor #104 + version: "2" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE + declarativeSchemaChangerState: + authorization: + userName: root @@ -285,6 +287,7 @@ upsert descriptor #104 + version: "4" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex.side_effects index e44f0138e8c3..aea3bd253d3a 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex.side_effects @@ -119,6 +119,7 @@ write *eventpb.CreateFunction to event log: upsert descriptor #107 - +function: + + canMutate: CANNOT_MUTATE + functionBody: SELECT 1; + id: 107 + lang: SQL @@ -315,6 +316,7 @@ upsert descriptor #106 upsert descriptor #107 - +function: + + canMutate: CANNOT_MUTATE + functionBody: SELECT 1; + id: 107 + lang: SQL diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex__statement_3_of_4.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex__statement_3_of_4.explain index 60f47cba874e..8ec1a42dad3d 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex__statement_3_of_4.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex__statement_3_of_4.explain @@ -29,7 +29,7 @@ Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹t›() │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":2,"UserName":"admin","WithGrantOption":2}} │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":1048576,"UserName":"public"}} │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":2,"UserName":"root","WithGrantOption":2}} - │ ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","FunctionID":107}} + │ ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","CanMutate":2,"FunctionID":107}} │ ├── UpdateFunctionTypeReferences {"FunctionID":107} │ ├── UpdateFunctionRelationReferences {"FunctionID":107} │ ├── SetFunctionParams {"Params":{"FunctionID":107}} @@ -110,7 +110,7 @@ Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹t›() ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":2,"UserName":"admin","WithGrantOption":2}} ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":1048576,"UserName":"public"}} ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":2,"UserName":"root","WithGrantOption":2}} - ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","FunctionID":107}} + ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","CanMutate":2,"FunctionID":107}} ├── UpdateFunctionTypeReferences {"FunctionID":107} ├── UpdateFunctionRelationReferences {"FunctionID":107} ├── SetFunctionParams {"Params":{"FunctionID":107}} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex__statement_4_of_4.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex__statement_4_of_4.explain index dd6708f7aef5..8bb0d96dd36b 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex__statement_4_of_4.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_complex/create_complex__statement_4_of_4.explain @@ -156,7 +156,7 @@ Schema change plan for CREATE SEQUENCE ‹db›.‹sc›.‹sq1› MINVALUE 1 MA ├── CreateFunctionDescriptor {"Function":{"FunctionID":107}} ├── SetFunctionName {"FunctionID":107,"Name":"t"} ├── SetFunctionParams {"Params":{"FunctionID":107}} - ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","FunctionID":107}} + ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","CanMutate":2,"FunctionID":107}} ├── UpdateFunctionTypeReferences {"FunctionID":107} ├── UpdateFunctionRelationReferences {"FunctionID":107} ├── CreateSequenceDescriptor {"SequenceID":108} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function/create_function.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_function/create_function.explain index 67858a7bf02b..5a41540ca81f 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_function/create_function.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function/create_function.explain @@ -46,7 +46,7 @@ Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f›(‹a │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":110,"Privileges":2,"UserName":"admin","WithGrantOption":2}} │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":110,"Privileges":1048576,"UserName":"public"}} │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":110,"Privileges":2,"UserName":"root","WithGrantOption":2}} - │ ├── SetFunctionBody {"Body":{"Body":"SELECT a FROM t;...","FunctionID":110}} + │ ├── SetFunctionBody {"Body":{"Body":"SELECT a FROM t;...","CanMutate":2,"FunctionID":110}} │ ├── UpdateFunctionTypeReferences {"FunctionID":110} │ ├── UpdateFunctionRelationReferences {"FunctionID":110} │ ├── SetFunctionParams {"Params":{"FunctionID":110}} @@ -87,7 +87,7 @@ Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f›(‹a ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":110,"Privileges":2,"UserName":"admin","WithGrantOption":2}} ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":110,"Privileges":1048576,"UserName":"public"}} ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":110,"Privileges":2,"UserName":"root","WithGrantOption":2}} - ├── SetFunctionBody {"Body":{"Body":"SELECT a FROM t;...","FunctionID":110}} + ├── SetFunctionBody {"Body":{"Body":"SELECT a FROM t;...","CanMutate":2,"FunctionID":110}} ├── UpdateFunctionTypeReferences {"FunctionID":110} ├── UpdateFunctionRelationReferences {"FunctionID":110} ├── SetFunctionParams {"Params":{"FunctionID":110}} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function/create_function.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/create_function/create_function.side_effects index e53ca9472461..640130fbf808 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_function/create_function.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function/create_function.side_effects @@ -43,6 +43,7 @@ write *eventpb.CreateFunction to event log: upsert descriptor #110 - +function: + + canMutate: CANNOT_MUTATE + dependsOn: + - 104 + - 105 @@ -186,6 +187,7 @@ persist all catalog changes to storage upsert descriptor #110 - +function: + + canMutate: CANNOT_MUTATE + dependsOn: + - 104 + - 105 diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_function/create_function_calling_function.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_function/create_function_calling_function.explain index 20fa9ff2dcdd..9e11292db9c4 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_function/create_function_calling_function.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_function/create_function_calling_function.explain @@ -52,7 +52,7 @@ Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f3›(‹ │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":112,"Privileges":2,"UserName":"admin","WithGrantOption":2}} │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":112,"Privileges":1048576,"UserName":"public"}} │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":112,"Privileges":2,"UserName":"root","WithGrantOption":2}} - │ ├── SetFunctionBody {"Body":{"Body":"SELECT f2(a) + f...","FunctionID":112}} + │ ├── SetFunctionBody {"Body":{"Body":"SELECT f2(a) + f...","CanMutate":2,"FunctionID":112}} │ ├── UpdateFunctionTypeReferences {"FunctionID":112} │ ├── UpdateFunctionRelationReferences {"FunctionID":112} │ ├── SetFunctionParams {"Params":{"FunctionID":112}} @@ -93,7 +93,7 @@ Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f3›(‹ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":112,"Privileges":2,"UserName":"admin","WithGrantOption":2}} ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":112,"Privileges":1048576,"UserName":"public"}} ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":112,"Privileges":2,"UserName":"root","WithGrantOption":2}} - ├── SetFunctionBody {"Body":{"Body":"SELECT f2(a) + f...","FunctionID":112}} + ├── SetFunctionBody {"Body":{"Body":"SELECT f2(a) + f...","CanMutate":2,"FunctionID":112}} ├── UpdateFunctionTypeReferences {"FunctionID":112} ├── UpdateFunctionRelationReferences {"FunctionID":112} ├── SetFunctionParams {"Params":{"FunctionID":112}} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_function/create_function_calling_function.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_function/create_function_calling_function.side_effects index de6e0f10683d..d4373750ea9a 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_function/create_function_calling_function.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_function/create_function_calling_function.side_effects @@ -49,6 +49,7 @@ write *eventpb.CreateFunction to event log: upsert descriptor #112 - +function: + + canMutate: CANNOT_MUTATE + dependsOnFunctions: + - 110 + - 111 @@ -141,6 +142,7 @@ upsert descriptor #110 volatility: VOLATILE upsert descriptor #111 function: + canMutate: CANNOT_MUTATE + dependedOnBy: + - id: 112 dependsOnFunctions: @@ -160,6 +162,7 @@ persist all catalog changes to storage upsert descriptor #112 - +function: + + canMutate: CANNOT_MUTATE + dependsOnFunctions: + - 110 + - 111 @@ -252,6 +255,7 @@ upsert descriptor #110 volatility: VOLATILE upsert descriptor #111 function: + canMutate: CANNOT_MUTATE + dependedOnBy: + - id: 112 dependsOnFunctions: diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.definition b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.definition new file mode 100644 index 000000000000..9cf318569418 --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.definition @@ -0,0 +1,17 @@ +setup +CREATE TABLE t(a INT PRIMARY KEY, b INT); +CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; +---- + +test +CREATE FUNCTION f_caller(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT f_insert(a, b); +$$; +CREATE FUNCTION f_caller_via_cte(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + WITH cte AS (SELECT f_insert(a, b) AS result) SELECT result FROM cte; +$$; +---- diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.explain new file mode 100644 index 000000000000..d379e956580f --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.explain @@ -0,0 +1,78 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); +CREATE FUNCTION f_insert(a INT, b INT) RETURNS VOID LANGUAGE SQL AS $$ + INSERT INTO t VALUES(a, b); +$$; + +/* test */ +EXPLAIN (DDL) CREATE FUNCTION f_caller(a INT, b INT) RETURNS VOID LANGUAGE SQL AS $$ + SELECT f_insert(a, b); +$$; +---- +Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller›(‹a› INT8, ‹b› INT8) + RETURNS VOID + LANGUAGE SQL + AS $$SELECT ‹public›.‹f_insert›(‹a›, ‹b›);$$; + ├── StatementPhase + │ └── Stage 1 of 1 in StatementPhase + │ ├── 9 elements transitioning toward PUBLIC + │ │ ├── ABSENT → PUBLIC Function:{DescID: 106 (f_caller+)} + │ │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 106 (f_caller+), ReferencedDescID: 101 (public)} + │ │ ├── ABSENT → PUBLIC FunctionName:{DescID: 106 (f_caller+)} + │ │ ├── ABSENT → PUBLIC Owner:{DescID: 106 (f_caller+)} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "admin"} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "public"} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "root"} + │ │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 106 (f_caller+)} + │ │ └── ABSENT → PUBLIC FunctionParams:{DescID: 106 (f_caller+)} + │ └── 12 Mutation operations + │ ├── CreateFunctionDescriptor {"Function":{"FunctionID":106}} + │ ├── SetFunctionName {"FunctionID":106,"Name":"f_caller"} + │ ├── UpdateOwner {"Owner":{"DescriptorID":106,"Owner":"root"}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":1048576,"UserName":"public"}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"root","WithGrantOption":2}} + │ ├── SetFunctionBody {"Body":{"Body":"SELECT f_insert(...","CanMutate":1,"FunctionID":106}} + │ ├── UpdateFunctionTypeReferences {"FunctionID":106} + │ ├── UpdateFunctionRelationReferences {"FunctionID":106} + │ ├── SetFunctionParams {"Params":{"FunctionID":106}} + │ ├── SetObjectParentID {"ObjParent":{"ChildObjectID":106,"SchemaID":101}} + │ └── MarkDescriptorAsPublic {"DescriptorID":106} + └── PreCommitPhase + ├── Stage 1 of 2 in PreCommitPhase + │ ├── 9 elements transitioning toward PUBLIC + │ │ ├── PUBLIC → ABSENT Function:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT SchemaChild:{DescID: 106 (f_caller+), ReferencedDescID: 101 (public)} + │ │ ├── PUBLIC → ABSENT FunctionName:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT Owner:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "admin"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "public"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "root"} + │ │ ├── PUBLIC → ABSENT FunctionBody:{DescID: 106 (f_caller+)} + │ │ └── PUBLIC → ABSENT FunctionParams:{DescID: 106 (f_caller+)} + │ └── 1 Mutation operation + │ └── UndoAllInTxnImmediateMutationOpSideEffects + └── Stage 2 of 2 in PreCommitPhase + ├── 9 elements transitioning toward PUBLIC + │ ├── ABSENT → PUBLIC Function:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 106 (f_caller+), ReferencedDescID: 101 (public)} + │ ├── ABSENT → PUBLIC FunctionName:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC Owner:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "admin"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "public"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "root"} + │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 106 (f_caller+)} + │ └── ABSENT → PUBLIC FunctionParams:{DescID: 106 (f_caller+)} + └── 12 Mutation operations + ├── CreateFunctionDescriptor {"Function":{"FunctionID":106}} + ├── SetFunctionName {"FunctionID":106,"Name":"f_caller"} + ├── UpdateOwner {"Owner":{"DescriptorID":106,"Owner":"root"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":1048576,"UserName":"public"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"root","WithGrantOption":2}} + ├── SetFunctionBody {"Body":{"Body":"SELECT f_insert(...","CanMutate":1,"FunctionID":106}} + ├── UpdateFunctionTypeReferences {"FunctionID":106} + ├── UpdateFunctionRelationReferences {"FunctionID":106} + ├── SetFunctionParams {"Params":{"FunctionID":106}} + ├── SetObjectParentID {"ObjParent":{"ChildObjectID":106,"SchemaID":101}} + └── MarkDescriptorAsPublic {"DescriptorID":106} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.explain_shape b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.explain_shape new file mode 100644 index 000000000000..567c0da672b0 --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.explain_shape @@ -0,0 +1,16 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); +CREATE FUNCTION f_insert(a INT, b INT) RETURNS VOID LANGUAGE SQL AS $$ + INSERT INTO t VALUES(a, b); +$$; + +/* test */ +EXPLAIN (DDL, SHAPE) CREATE FUNCTION f_caller(a INT, b INT) RETURNS VOID LANGUAGE SQL AS $$ + SELECT f_insert(a, b); +$$; +---- +Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller›(‹a› INT8, ‹b› INT8) + RETURNS VOID + LANGUAGE SQL + AS $$SELECT ‹public›.‹f_insert›(‹a›, ‹b›);$$; + └── execute 1 system table mutations transaction diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.side_effects new file mode 100644 index 000000000000..54c44f73ec5e --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating.side_effects @@ -0,0 +1,353 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); +CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; +---- +... ++object {100 101 t} -> 104 + +/* test */ +CREATE FUNCTION f_caller(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT f_insert(a, b); +$$; +CREATE FUNCTION f_caller_via_cte(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + WITH cte AS (SELECT f_insert(a, b) AS result) SELECT result FROM cte; +$$; +---- +begin transaction #1 +# begin StatementPhase +checking for feature: CREATE FUNCTION +increment telemetry for sql.schema.create_function +write *eventpb.CreateFunction to event log: + functionName: defaultdb.public.f_caller + sql: + descriptorId: 106 + statement: "CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller›(‹a› INT8, ‹b› INT8)\n\tRETURNS INT8\n\tLANGUAGE SQL\n\tAS $$SELECT ‹public›.‹f_insert›(‹a›, ‹b›);$$" + tag: CREATE FUNCTION + user: root +## StatementPhase stage 1 of 1 with 12 MutationType ops +upsert descriptor #106 + - + +function: + + canMutate: CAN_MUTATE + + dependsOnFunctions: + + - 105 + + functionBody: SELECT f_insert(a, b); + + id: 106 + + lang: SQL + + modificationTime: {} + + name: f_caller + + nullInputBehavior: CALLED_ON_NULL_INPUT + + params: + + - name: a + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + - name: b + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + parentId: 100 + + parentSchemaId: 101 + + privileges: + + ownerProto: root + + users: + + - privileges: "2" + + userProto: admin + + withGrantOption: "2" + + - privileges: "1048576" + + userProto: public + + - privileges: "2" + + userProto: root + + withGrantOption: "2" + + version: 3 + + returnType: + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + version: "1" + + volatility: VOLATILE +upsert descriptor #101 + schema: + functions: + + f_caller: + + signatures: + + - argTypes: + + - family: IntFamily + + oid: 20 + + width: 64 + + - family: IntFamily + + oid: 20 + + width: 64 + + id: 106 + + returnType: + + family: IntFamily + + oid: 20 + + width: 64 + f_insert: + signatures: + ... + withGrantOption: "2" + version: 3 + - version: "2" + + version: "3" +upsert descriptor #105 + function: + canMutate: CAN_MUTATE + + dependedOnBy: + + - id: 106 + dependsOn: + - 104 + ... + oid: 20 + width: 64 + - version: "1" + + version: "2" + volatility: VOLATILE +checking for feature: CREATE FUNCTION +increment telemetry for sql.schema.create_function +write *eventpb.CreateFunction to event log: + functionName: defaultdb.public.f_caller_via_cte + sql: + descriptorId: 107 + statement: "CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller_via_cte›(‹a› INT8, ‹b› INT8)\n\tRETURNS INT8\n\tLANGUAGE SQL\n\tAS $$WITH ‹cte› AS (SELECT ‹public›.‹f_insert›(‹a›, ‹b›) AS ‹result›) SELECT ‹result› FROM ‹\"\"›.‹\"\"›.‹cte›;$$" + tag: CREATE FUNCTION + user: root +## StatementPhase stage 1 of 1 with 12 MutationType ops +upsert descriptor #107 + - + +function: + + canMutate: CAN_MUTATE + + dependsOnFunctions: + + - 105 + + functionBody: WITH cte AS (SELECT f_insert(a, b) AS result) SELECT result FROM cte; + + id: 107 + + lang: SQL + + modificationTime: {} + + name: f_caller_via_cte + + nullInputBehavior: CALLED_ON_NULL_INPUT + + params: + + - name: a + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + - name: b + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + parentId: 100 + + parentSchemaId: 101 + + privileges: + + ownerProto: root + + users: + + - privileges: "2" + + userProto: admin + + withGrantOption: "2" + + - privileges: "1048576" + + userProto: public + + - privileges: "2" + + userProto: root + + withGrantOption: "2" + + version: 3 + + returnType: + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + version: "1" + + volatility: VOLATILE +upsert descriptor #101 + ... + oid: 20 + width: 64 + + f_caller_via_cte: + + signatures: + + - argTypes: + + - family: IntFamily + + oid: 20 + + width: 64 + + - family: IntFamily + + oid: 20 + + width: 64 + + id: 107 + + returnType: + + family: IntFamily + + oid: 20 + + width: 64 + f_insert: + signatures: + ... + withGrantOption: "2" + version: 3 + - version: "2" + + version: "3" +upsert descriptor #105 + ... + dependedOnBy: + - id: 106 + + - id: 107 + dependsOn: + - 104 + ... + oid: 20 + width: 64 + - version: "1" + + version: "2" + volatility: VOLATILE +# end StatementPhase +# begin PreCommitPhase +## PreCommitPhase stage 1 of 2 with 1 MutationType op +undo all catalog changes within txn #1 +persist all catalog changes to storage +## PreCommitPhase stage 2 of 2 with 24 MutationType ops +upsert descriptor #106 + - + +function: + + canMutate: CAN_MUTATE + + dependsOnFunctions: + + - 105 + + functionBody: SELECT f_insert(a, b); + + id: 106 + + lang: SQL + + modificationTime: {} + + name: f_caller + + nullInputBehavior: CALLED_ON_NULL_INPUT + + params: + + - name: a + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + - name: b + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + parentId: 100 + + parentSchemaId: 101 + + privileges: + + ownerProto: root + + users: + + - privileges: "2" + + userProto: admin + + withGrantOption: "2" + + - privileges: "1048576" + + userProto: public + + - privileges: "2" + + userProto: root + + withGrantOption: "2" + + version: 3 + + returnType: + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + version: "1" + + volatility: VOLATILE +upsert descriptor #107 + - + +function: + + canMutate: CAN_MUTATE + + dependsOnFunctions: + + - 105 + + functionBody: WITH cte AS (SELECT f_insert(a, b) AS result) SELECT result FROM cte; + + id: 107 + + lang: SQL + + modificationTime: {} + + name: f_caller_via_cte + + nullInputBehavior: CALLED_ON_NULL_INPUT + + params: + + - name: a + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + - name: b + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + parentId: 100 + + parentSchemaId: 101 + + privileges: + + ownerProto: root + + users: + + - privileges: "2" + + userProto: admin + + withGrantOption: "2" + + - privileges: "1048576" + + userProto: public + + - privileges: "2" + + userProto: root + + withGrantOption: "2" + + version: 3 + + returnType: + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + version: "1" + + volatility: VOLATILE +upsert descriptor #101 + schema: + functions: + + f_caller: + + signatures: + + - argTypes: + + - family: IntFamily + + oid: 20 + + width: 64 + + - family: IntFamily + + oid: 20 + + width: 64 + + id: 106 + + returnType: + + family: IntFamily + + oid: 20 + + width: 64 + + f_caller_via_cte: + + signatures: + + - argTypes: + + - family: IntFamily + + oid: 20 + + width: 64 + + - family: IntFamily + + oid: 20 + + width: 64 + + id: 107 + + returnType: + + family: IntFamily + + oid: 20 + + width: 64 + f_insert: + signatures: + ... + withGrantOption: "2" + version: 3 + - version: "2" + + version: "3" +upsert descriptor #105 + function: + canMutate: CAN_MUTATE + + dependedOnBy: + + - id: 106 + + - id: 107 + dependsOn: + - 104 + ... + oid: 20 + width: 64 + - version: "1" + + version: "2" + volatility: VOLATILE +persist all catalog changes to storage +# end PreCommitPhase +commit transaction #1 diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_1_of_2.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_1_of_2.explain new file mode 100644 index 000000000000..2e4131b82d35 --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_1_of_2.explain @@ -0,0 +1,80 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); +CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; + +/* test */ +EXPLAIN (DDL) CREATE FUNCTION f_caller(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT f_insert(a, b); +$$; +---- +Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller›(‹a› INT8, ‹b› INT8) + RETURNS INT8 + LANGUAGE SQL + AS $$SELECT ‹public›.‹f_insert›(‹a›, ‹b›);$$; + ├── StatementPhase + │ └── Stage 1 of 1 in StatementPhase + │ ├── 9 elements transitioning toward PUBLIC + │ │ ├── ABSENT → PUBLIC Function:{DescID: 106 (f_caller+)} + │ │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 106 (f_caller+), ReferencedDescID: 101 (public)} + │ │ ├── ABSENT → PUBLIC FunctionName:{DescID: 106 (f_caller+)} + │ │ ├── ABSENT → PUBLIC Owner:{DescID: 106 (f_caller+)} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "admin"} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "public"} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "root"} + │ │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 106 (f_caller+)} + │ │ └── ABSENT → PUBLIC FunctionParams:{DescID: 106 (f_caller+)} + │ └── 12 Mutation operations + │ ├── CreateFunctionDescriptor {"Function":{"FunctionID":106}} + │ ├── SetFunctionName {"FunctionID":106,"Name":"f_caller"} + │ ├── UpdateOwner {"Owner":{"DescriptorID":106,"Owner":"root"}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":1048576,"UserName":"public"}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"root","WithGrantOption":2}} + │ ├── SetFunctionBody {"Body":{"Body":"SELECT f_insert(...","CanMutate":1,"FunctionID":106}} + │ ├── UpdateFunctionTypeReferences {"FunctionID":106} + │ ├── UpdateFunctionRelationReferences {"FunctionID":106} + │ ├── SetFunctionParams {"Params":{"FunctionID":106}} + │ ├── SetObjectParentID {"ObjParent":{"ChildObjectID":106,"SchemaID":101}} + │ └── MarkDescriptorAsPublic {"DescriptorID":106} + └── PreCommitPhase + ├── Stage 1 of 2 in PreCommitPhase + │ ├── 9 elements transitioning toward PUBLIC + │ │ ├── PUBLIC → ABSENT Function:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT SchemaChild:{DescID: 106 (f_caller+), ReferencedDescID: 101 (public)} + │ │ ├── PUBLIC → ABSENT FunctionName:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT Owner:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "admin"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "public"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "root"} + │ │ ├── PUBLIC → ABSENT FunctionBody:{DescID: 106 (f_caller+)} + │ │ └── PUBLIC → ABSENT FunctionParams:{DescID: 106 (f_caller+)} + │ └── 1 Mutation operation + │ └── UndoAllInTxnImmediateMutationOpSideEffects + └── Stage 2 of 2 in PreCommitPhase + ├── 9 elements transitioning toward PUBLIC + │ ├── ABSENT → PUBLIC Function:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 106 (f_caller+), ReferencedDescID: 101 (public)} + │ ├── ABSENT → PUBLIC FunctionName:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC Owner:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "admin"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "public"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "root"} + │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 106 (f_caller+)} + │ └── ABSENT → PUBLIC FunctionParams:{DescID: 106 (f_caller+)} + └── 12 Mutation operations + ├── CreateFunctionDescriptor {"Function":{"FunctionID":106}} + ├── SetFunctionName {"FunctionID":106,"Name":"f_caller"} + ├── UpdateOwner {"Owner":{"DescriptorID":106,"Owner":"root"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":1048576,"UserName":"public"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"root","WithGrantOption":2}} + ├── SetFunctionBody {"Body":{"Body":"SELECT f_insert(...","CanMutate":1,"FunctionID":106}} + ├── UpdateFunctionTypeReferences {"FunctionID":106} + ├── UpdateFunctionRelationReferences {"FunctionID":106} + ├── SetFunctionParams {"Params":{"FunctionID":106}} + ├── SetObjectParentID {"ObjParent":{"ChildObjectID":106,"SchemaID":101}} + └── MarkDescriptorAsPublic {"DescriptorID":106} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_1_of_2.explain_shape b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_1_of_2.explain_shape new file mode 100644 index 000000000000..e451554f1cb1 --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_1_of_2.explain_shape @@ -0,0 +1,18 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); +CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; + +/* test */ +EXPLAIN (DDL, SHAPE) CREATE FUNCTION f_caller(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT f_insert(a, b); +$$; +---- +Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller›(‹a› INT8, ‹b› INT8) + RETURNS INT8 + LANGUAGE SQL + AS $$SELECT ‹public›.‹f_insert›(‹a›, ‹b›);$$; + └── execute 1 system table mutations transaction diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_2_of_2.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_2_of_2.explain new file mode 100644 index 000000000000..b86e18064c09 --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_2_of_2.explain @@ -0,0 +1,116 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); +CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; + +/* test */ +CREATE FUNCTION f_caller(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT f_insert(a, b); +$$; +EXPLAIN (DDL) CREATE FUNCTION f_caller_via_cte(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + WITH cte AS (SELECT f_insert(a, b) AS result) SELECT result FROM cte; +$$; +---- +Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller_via_cte›(‹a› INT8, ‹b› INT8) + RETURNS INT8 + LANGUAGE SQL + AS $$WITH ‹cte› AS (SELECT ‹public›.‹f_insert›(‹a›, ‹b›) AS ‹result›) SELECT ‹result› FROM ‹""›.‹""›.‹cte›;$$; following CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller›(‹a› INT8, ‹b› INT8) + RETURNS INT8 + LANGUAGE SQL + AS $$SELECT ‹public›.‹f_insert›(‹a›, ‹b›);$$; + ├── StatementPhase + │ └── Stage 1 of 1 in StatementPhase + │ ├── 9 elements transitioning toward PUBLIC + │ │ ├── ABSENT → PUBLIC Function:{DescID: 107 (f_caller_via_cte+)} + │ │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 107 (f_caller_via_cte+), ReferencedDescID: 101 (public)} + │ │ ├── ABSENT → PUBLIC FunctionName:{DescID: 107 (f_caller_via_cte+)} + │ │ ├── ABSENT → PUBLIC Owner:{DescID: 107 (f_caller_via_cte+)} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "admin"} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "public"} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "root"} + │ │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 107 (f_caller_via_cte+)} + │ │ └── ABSENT → PUBLIC FunctionParams:{DescID: 107 (f_caller_via_cte+)} + │ └── 12 Mutation operations + │ ├── CreateFunctionDescriptor {"Function":{"FunctionID":107}} + │ ├── SetFunctionName {"FunctionID":107,"Name":"f_caller_via_cte"} + │ ├── UpdateOwner {"Owner":{"DescriptorID":107,"Owner":"root"}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":1048576,"UserName":"public"}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":2,"UserName":"root","WithGrantOption":2}} + │ ├── SetFunctionBody {"Body":{"Body":"WITH cte AS (SEL...","CanMutate":1,"FunctionID":107}} + │ ├── UpdateFunctionTypeReferences {"FunctionID":107} + │ ├── UpdateFunctionRelationReferences {"FunctionID":107} + │ ├── SetFunctionParams {"Params":{"FunctionID":107}} + │ ├── SetObjectParentID {"ObjParent":{"ChildObjectID":107,"SchemaID":101}} + │ └── MarkDescriptorAsPublic {"DescriptorID":107} + └── PreCommitPhase + ├── Stage 1 of 2 in PreCommitPhase + │ ├── 18 elements transitioning toward PUBLIC + │ │ ├── PUBLIC → ABSENT Owner:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "admin"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "public"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 106 (f_caller+), Name: "root"} + │ │ ├── PUBLIC → ABSENT Function:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT SchemaChild:{DescID: 106 (f_caller+), ReferencedDescID: 101 (public)} + │ │ ├── PUBLIC → ABSENT FunctionName:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT FunctionParams:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT FunctionBody:{DescID: 106 (f_caller+)} + │ │ ├── PUBLIC → ABSENT Function:{DescID: 107 (f_caller_via_cte+)} + │ │ ├── PUBLIC → ABSENT SchemaChild:{DescID: 107 (f_caller_via_cte+), ReferencedDescID: 101 (public)} + │ │ ├── PUBLIC → ABSENT FunctionName:{DescID: 107 (f_caller_via_cte+)} + │ │ ├── PUBLIC → ABSENT Owner:{DescID: 107 (f_caller_via_cte+)} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "admin"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "public"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "root"} + │ │ ├── PUBLIC → ABSENT FunctionBody:{DescID: 107 (f_caller_via_cte+)} + │ │ └── PUBLIC → ABSENT FunctionParams:{DescID: 107 (f_caller_via_cte+)} + │ └── 1 Mutation operation + │ └── UndoAllInTxnImmediateMutationOpSideEffects + └── Stage 2 of 2 in PreCommitPhase + ├── 18 elements transitioning toward PUBLIC + │ ├── ABSENT → PUBLIC Owner:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "admin"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "public"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 106 (f_caller+), Name: "root"} + │ ├── ABSENT → PUBLIC Function:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 106 (f_caller+), ReferencedDescID: 101 (public)} + │ ├── ABSENT → PUBLIC FunctionName:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC FunctionParams:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 106 (f_caller+)} + │ ├── ABSENT → PUBLIC Function:{DescID: 107 (f_caller_via_cte+)} + │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 107 (f_caller_via_cte+), ReferencedDescID: 101 (public)} + │ ├── ABSENT → PUBLIC FunctionName:{DescID: 107 (f_caller_via_cte+)} + │ ├── ABSENT → PUBLIC Owner:{DescID: 107 (f_caller_via_cte+)} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "admin"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "public"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 107 (f_caller_via_cte+), Name: "root"} + │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 107 (f_caller_via_cte+)} + │ └── ABSENT → PUBLIC FunctionParams:{DescID: 107 (f_caller_via_cte+)} + └── 24 Mutation operations + ├── CreateFunctionDescriptor {"Function":{"FunctionID":106}} + ├── SetFunctionName {"FunctionID":106,"Name":"f_caller"} + ├── SetFunctionParams {"Params":{"FunctionID":106}} + ├── SetFunctionBody {"Body":{"Body":"SELECT f_insert(...","CanMutate":1,"FunctionID":106}} + ├── UpdateFunctionTypeReferences {"FunctionID":106} + ├── UpdateFunctionRelationReferences {"FunctionID":106} + ├── CreateFunctionDescriptor {"Function":{"FunctionID":107}} + ├── SetFunctionName {"FunctionID":107,"Name":"f_caller_via_cte"} + ├── UpdateOwner {"Owner":{"DescriptorID":107,"Owner":"root"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":1048576,"UserName":"public"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":107,"Privileges":2,"UserName":"root","WithGrantOption":2}} + ├── SetFunctionBody {"Body":{"Body":"WITH cte AS (SEL...","CanMutate":1,"FunctionID":107}} + ├── UpdateFunctionTypeReferences {"FunctionID":107} + ├── UpdateFunctionRelationReferences {"FunctionID":107} + ├── SetFunctionParams {"Params":{"FunctionID":107}} + ├── UpdateOwner {"Owner":{"DescriptorID":106,"Owner":"root"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":1048576,"UserName":"public"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":106,"Privileges":2,"UserName":"root","WithGrantOption":2}} + ├── SetObjectParentID {"ObjParent":{"ChildObjectID":106,"SchemaID":101}} + ├── SetObjectParentID {"ObjParent":{"ChildObjectID":107,"SchemaID":101}} + ├── MarkDescriptorAsPublic {"DescriptorID":106} + └── MarkDescriptorAsPublic {"DescriptorID":107} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_2_of_2.explain_shape b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_2_of_2.explain_shape new file mode 100644 index 000000000000..19f1aa3e481d --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_calling_mutating/create_function_calling_mutating__statement_2_of_2.explain_shape @@ -0,0 +1,24 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); +CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; + +/* test */ +CREATE FUNCTION f_caller(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT f_insert(a, b); +$$; +EXPLAIN (DDL, SHAPE) CREATE FUNCTION f_caller_via_cte(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + WITH cte AS (SELECT f_insert(a, b) AS result) SELECT result FROM cte; +$$; +---- +Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller_via_cte›(‹a› INT8, ‹b› INT8) + RETURNS INT8 + LANGUAGE SQL + AS $$WITH ‹cte› AS (SELECT ‹public›.‹f_insert›(‹a›, ‹b›) AS ‹result›) SELECT ‹result› FROM ‹""›.‹""›.‹cte›;$$; following CREATE FUNCTION ‹defaultdb›.‹public›.‹f_caller›(‹a› INT8, ‹b› INT8) + RETURNS INT8 + LANGUAGE SQL + AS $$SELECT ‹public›.‹f_insert›(‹a›, ‹b›);$$; + └── execute 1 system table mutations transaction diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn.side_effects index 51dcae5eb634..b3b8c5615078 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn.side_effects @@ -23,6 +23,7 @@ write *eventpb.CreateFunction to event log: upsert descriptor #105 - +function: + + canMutate: CANNOT_MUTATE + functionBody: SELECT 1; + id: 105 + lang: SQL @@ -163,6 +164,7 @@ persist all catalog changes to storage upsert descriptor #105 - +function: + + canMutate: CANNOT_MUTATE + declarativeSchemaChangerState: + authorization: + userName: root @@ -635,6 +637,7 @@ upsert descriptor #104 + version: "8" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn__statement_1_of_2.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn__statement_1_of_2.explain index a1d536ddc31a..94933f6a5c8e 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn__statement_1_of_2.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn__statement_1_of_2.explain @@ -27,7 +27,7 @@ Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹t›() │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":2,"UserName":"admin","WithGrantOption":2}} │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":1048576,"UserName":"public"}} │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":2,"UserName":"root","WithGrantOption":2}} - │ ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","FunctionID":105}} + │ ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","CanMutate":2,"FunctionID":105}} │ ├── UpdateFunctionTypeReferences {"FunctionID":105} │ ├── UpdateFunctionRelationReferences {"FunctionID":105} │ ├── SetFunctionParams {"Params":{"FunctionID":105}} @@ -65,7 +65,7 @@ Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹t›() ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":2,"UserName":"admin","WithGrantOption":2}} ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":1048576,"UserName":"public"}} ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":2,"UserName":"root","WithGrantOption":2}} - ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","FunctionID":105}} + ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","CanMutate":2,"FunctionID":105}} ├── UpdateFunctionTypeReferences {"FunctionID":105} ├── UpdateFunctionRelationReferences {"FunctionID":105} ├── SetFunctionParams {"Params":{"FunctionID":105}} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn__statement_2_of_2.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn__statement_2_of_2.explain index cf35ca140547..9a03634ea4ac 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn__statement_2_of_2.explain +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_in_txn/create_function_in_txn__statement_2_of_2.explain @@ -83,7 +83,7 @@ Schema change plan for CREATE UNIQUE INDEX ‹idx› ON ‹defaultdb›.‹publi │ ├── CreateFunctionDescriptor {"Function":{"FunctionID":105}} │ ├── SetFunctionName {"FunctionID":105,"Name":"t"} │ ├── SetFunctionParams {"Params":{"FunctionID":105}} - │ ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","FunctionID":105}} + │ ├── SetFunctionBody {"Body":{"Body":"SELECT 1;","CanMutate":2,"FunctionID":105}} │ ├── UpdateFunctionTypeReferences {"FunctionID":105} │ ├── UpdateFunctionRelationReferences {"FunctionID":105} │ ├── SetTableSchemaLocked {"TableID":104} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.definition b/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.definition new file mode 100644 index 000000000000..e6e4c84fc339 --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.definition @@ -0,0 +1,11 @@ +setup +CREATE TABLE t(a INT PRIMARY KEY, b INT); +---- + +test +CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; +---- diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.explain b/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.explain new file mode 100644 index 000000000000..91c702187b03 --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.explain @@ -0,0 +1,77 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); + +/* test */ +EXPLAIN (DDL) CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; +---- +Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f_insert›(‹a› INT8, ‹b› INT8) + RETURNS INT8 + LANGUAGE SQL + AS $$SELECT ‹b›; INSERT INTO ‹defaultdb›.‹public›.‹t› VALUES (‹a›, ‹b›); SELECT ‹a›;$$; + ├── StatementPhase + │ └── Stage 1 of 1 in StatementPhase + │ ├── 9 elements transitioning toward PUBLIC + │ │ ├── ABSENT → PUBLIC Function:{DescID: 105 (f_insert+)} + │ │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 105 (f_insert+), ReferencedDescID: 101 (public)} + │ │ ├── ABSENT → PUBLIC FunctionName:{DescID: 105 (f_insert+)} + │ │ ├── ABSENT → PUBLIC Owner:{DescID: 105 (f_insert+)} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 105 (f_insert+), Name: "admin"} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 105 (f_insert+), Name: "public"} + │ │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 105 (f_insert+), Name: "root"} + │ │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 105 (f_insert+)} + │ │ └── ABSENT → PUBLIC FunctionParams:{DescID: 105 (f_insert+)} + │ └── 12 Mutation operations + │ ├── CreateFunctionDescriptor {"Function":{"FunctionID":105}} + │ ├── SetFunctionName {"FunctionID":105,"Name":"f_insert"} + │ ├── UpdateOwner {"Owner":{"DescriptorID":105,"Owner":"root"}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":1048576,"UserName":"public"}} + │ ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":2,"UserName":"root","WithGrantOption":2}} + │ ├── SetFunctionBody {"Body":{"Body":"SELECT b;\nINSERT...","CanMutate":1,"FunctionID":105}} + │ ├── UpdateFunctionTypeReferences {"FunctionID":105} + │ ├── UpdateFunctionRelationReferences {"FunctionID":105} + │ ├── SetFunctionParams {"Params":{"FunctionID":105}} + │ ├── SetObjectParentID {"ObjParent":{"ChildObjectID":105,"SchemaID":101}} + │ └── MarkDescriptorAsPublic {"DescriptorID":105} + └── PreCommitPhase + ├── Stage 1 of 2 in PreCommitPhase + │ ├── 9 elements transitioning toward PUBLIC + │ │ ├── PUBLIC → ABSENT Function:{DescID: 105 (f_insert+)} + │ │ ├── PUBLIC → ABSENT SchemaChild:{DescID: 105 (f_insert+), ReferencedDescID: 101 (public)} + │ │ ├── PUBLIC → ABSENT FunctionName:{DescID: 105 (f_insert+)} + │ │ ├── PUBLIC → ABSENT Owner:{DescID: 105 (f_insert+)} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 105 (f_insert+), Name: "admin"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 105 (f_insert+), Name: "public"} + │ │ ├── PUBLIC → ABSENT UserPrivileges:{DescID: 105 (f_insert+), Name: "root"} + │ │ ├── PUBLIC → ABSENT FunctionBody:{DescID: 105 (f_insert+)} + │ │ └── PUBLIC → ABSENT FunctionParams:{DescID: 105 (f_insert+)} + │ └── 1 Mutation operation + │ └── UndoAllInTxnImmediateMutationOpSideEffects + └── Stage 2 of 2 in PreCommitPhase + ├── 9 elements transitioning toward PUBLIC + │ ├── ABSENT → PUBLIC Function:{DescID: 105 (f_insert+)} + │ ├── ABSENT → PUBLIC SchemaChild:{DescID: 105 (f_insert+), ReferencedDescID: 101 (public)} + │ ├── ABSENT → PUBLIC FunctionName:{DescID: 105 (f_insert+)} + │ ├── ABSENT → PUBLIC Owner:{DescID: 105 (f_insert+)} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 105 (f_insert+), Name: "admin"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 105 (f_insert+), Name: "public"} + │ ├── ABSENT → PUBLIC UserPrivileges:{DescID: 105 (f_insert+), Name: "root"} + │ ├── ABSENT → PUBLIC FunctionBody:{DescID: 105 (f_insert+)} + │ └── ABSENT → PUBLIC FunctionParams:{DescID: 105 (f_insert+)} + └── 12 Mutation operations + ├── CreateFunctionDescriptor {"Function":{"FunctionID":105}} + ├── SetFunctionName {"FunctionID":105,"Name":"f_insert"} + ├── UpdateOwner {"Owner":{"DescriptorID":105,"Owner":"root"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":2,"UserName":"admin","WithGrantOption":2}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":1048576,"UserName":"public"}} + ├── UpdateUserPrivileges {"Privileges":{"DescriptorID":105,"Privileges":2,"UserName":"root","WithGrantOption":2}} + ├── SetFunctionBody {"Body":{"Body":"SELECT b;\nINSERT...","CanMutate":1,"FunctionID":105}} + ├── UpdateFunctionTypeReferences {"FunctionID":105} + ├── UpdateFunctionRelationReferences {"FunctionID":105} + ├── SetFunctionParams {"Params":{"FunctionID":105}} + ├── SetObjectParentID {"ObjParent":{"ChildObjectID":105,"SchemaID":101}} + └── MarkDescriptorAsPublic {"DescriptorID":105} diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.explain_shape b/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.explain_shape new file mode 100644 index 000000000000..52dce36ff6dc --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.explain_shape @@ -0,0 +1,15 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); + +/* test */ +EXPLAIN (DDL, SHAPE) CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; +---- +Schema change plan for CREATE FUNCTION ‹defaultdb›.‹public›.‹f_insert›(‹a› INT8, ‹b› INT8) + RETURNS INT8 + LANGUAGE SQL + AS $$SELECT ‹b›; INSERT INTO ‹defaultdb›.‹public›.‹t› VALUES (‹a›, ‹b›); SELECT ‹a›;$$; + └── execute 1 system table mutations transaction diff --git a/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.side_effects new file mode 100644 index 000000000000..e0a3bd323fd1 --- /dev/null +++ b/pkg/sql/schemachanger/testdata/end_to_end/create_function_mutating/create_function_mutating.side_effects @@ -0,0 +1,208 @@ +/* setup */ +CREATE TABLE t(a INT PRIMARY KEY, b INT); +---- +... ++object {100 101 t} -> 104 + +/* test */ +CREATE FUNCTION f_insert(a INT, b INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT b; + INSERT INTO t VALUES(a, b); + SELECT a; +$$; +---- +begin transaction #1 +# begin StatementPhase +checking for feature: CREATE FUNCTION +increment telemetry for sql.schema.create_function +write *eventpb.CreateFunction to event log: + functionName: defaultdb.public.f_insert + sql: + descriptorId: 105 + statement: "CREATE FUNCTION ‹defaultdb›.‹public›.‹f_insert›(‹a› INT8, ‹b› INT8)\n\tRETURNS INT8\n\tLANGUAGE SQL\n\tAS $$SELECT ‹b›; INSERT INTO ‹defaultdb›.‹public›.‹t› VALUES (‹a›, ‹b›); SELECT ‹a›;$$" + tag: CREATE FUNCTION + user: root +## StatementPhase stage 1 of 1 with 12 MutationType ops +upsert descriptor #105 + - + +function: + + canMutate: CAN_MUTATE + + dependsOn: + + - 104 + + functionBody: |- + + SELECT b; + + INSERT INTO t VALUES (a, b); + + SELECT a; + + id: 105 + + lang: SQL + + modificationTime: {} + + name: f_insert + + nullInputBehavior: CALLED_ON_NULL_INPUT + + params: + + - name: a + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + - name: b + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + parentId: 100 + + parentSchemaId: 101 + + privileges: + + ownerProto: root + + users: + + - privileges: "2" + + userProto: admin + + withGrantOption: "2" + + - privileges: "1048576" + + userProto: public + + - privileges: "2" + + userProto: root + + withGrantOption: "2" + + version: 3 + + returnType: + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + version: "1" + + volatility: VOLATILE +upsert descriptor #101 + schema: + + functions: + + f_insert: + + signatures: + + - argTypes: + + - family: IntFamily + + oid: 20 + + width: 64 + + - family: IntFamily + + oid: 20 + + width: 64 + + id: 105 + + returnType: + + family: IntFamily + + oid: 20 + + width: 64 + id: 101 + modificationTime: {} + ... + withGrantOption: "2" + version: 3 + - version: "1" + + version: "2" +upsert descriptor #104 + ... + createAsOfTime: + wallTime: "1640995200000000000" + + dependedOnBy: + + - columnIds: + + - 1 + + - 2 + + id: 105 + families: + - columnIds: + ... + schemaLocked: true + unexposedParentSchemaId: 101 + - version: "1" + + version: "2" +# end StatementPhase +# begin PreCommitPhase +## PreCommitPhase stage 1 of 2 with 1 MutationType op +undo all catalog changes within txn #1 +persist all catalog changes to storage +## PreCommitPhase stage 2 of 2 with 12 MutationType ops +upsert descriptor #105 + - + +function: + + canMutate: CAN_MUTATE + + dependsOn: + + - 104 + + functionBody: |- + + SELECT b; + + INSERT INTO t VALUES (a, b); + + SELECT a; + + id: 105 + + lang: SQL + + modificationTime: {} + + name: f_insert + + nullInputBehavior: CALLED_ON_NULL_INPUT + + params: + + - name: a + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + - name: b + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + parentId: 100 + + parentSchemaId: 101 + + privileges: + + ownerProto: root + + users: + + - privileges: "2" + + userProto: admin + + withGrantOption: "2" + + - privileges: "1048576" + + userProto: public + + - privileges: "2" + + userProto: root + + withGrantOption: "2" + + version: 3 + + returnType: + + type: + + family: IntFamily + + oid: 20 + + width: 64 + + version: "1" + + volatility: VOLATILE +upsert descriptor #101 + schema: + + functions: + + f_insert: + + signatures: + + - argTypes: + + - family: IntFamily + + oid: 20 + + width: 64 + + - family: IntFamily + + oid: 20 + + width: 64 + + id: 105 + + returnType: + + family: IntFamily + + oid: 20 + + width: 64 + id: 101 + modificationTime: {} + ... + withGrantOption: "2" + version: 3 + - version: "1" + + version: "2" +upsert descriptor #104 + ... + createAsOfTime: + wallTime: "1640995200000000000" + + dependedOnBy: + + - columnIds: + + - 1 + + - 2 + + id: 105 + families: + - columnIds: + ... + schemaLocked: true + unexposedParentSchemaId: 101 + - version: "1" + + version: "2" +persist all catalog changes to storage +# end PreCommitPhase +commit transaction #1 diff --git a/pkg/sql/schemachanger/testdata/end_to_end/drop_column_with_trigger_dep/drop_column_with_trigger_dep.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/drop_column_with_trigger_dep/drop_column_with_trigger_dep.side_effects index 6aeabca3cace..d8aae779704e 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/drop_column_with_trigger_dep/drop_column_with_trigger_dep.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/drop_column_with_trigger_dep/drop_column_with_trigger_dep.side_effects @@ -176,6 +176,7 @@ upsert descriptor #104 + version: "4" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: @@ -308,6 +309,7 @@ upsert descriptor #104 + version: "4" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE + declarativeSchemaChangerState: + authorization: + userName: root @@ -787,6 +789,7 @@ upsert descriptor #104 + version: "12" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/sql/schemachanger/testdata/end_to_end/drop_column_with_udf_default/drop_column_with_udf_default.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/drop_column_with_udf_default/drop_column_with_udf_default.side_effects index 59b15bd38eb1..157787c0441f 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/drop_column_with_udf_default/drop_column_with_udf_default.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/drop_column_with_udf_default/drop_column_with_udf_default.side_effects @@ -143,6 +143,7 @@ persist all catalog changes to storage ## PreCommitPhase stage 2 of 2 with 12 MutationType ops upsert descriptor #104 function: + canMutate: CANNOT_MUTATE + declarativeSchemaChangerState: + authorization: + userName: root @@ -686,6 +687,7 @@ begin transaction #13 ## PostCommitNonRevertiblePhase stage 4 of 4 with 4 MutationType ops upsert descriptor #104 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/sql/schemachanger/testdata/end_to_end/drop_function/drop_function.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/drop_function/drop_function.side_effects index f8716c1122d4..9103f6484e38 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/drop_function/drop_function.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/drop_function/drop_function.side_effects @@ -305,6 +305,7 @@ upsert descriptor #108 + version: "4" upsert descriptor #109 function: + canMutate: CANNOT_MUTATE + declarativeSchemaChangerState: + authorization: + userName: root diff --git a/pkg/sql/schemachanger/testdata/end_to_end/drop_index_vanilla_index/drop_index_vanilla_index.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/drop_index_vanilla_index/drop_index_vanilla_index.side_effects index ab77de34705d..89efaf4c93e9 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/drop_index_vanilla_index/drop_index_vanilla_index.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/drop_index_vanilla_index/drop_index_vanilla_index.side_effects @@ -122,6 +122,7 @@ upsert descriptor #104 + version: "11" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: @@ -258,6 +259,7 @@ upsert descriptor #104 + version: "11" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: @@ -413,6 +415,7 @@ upsert descriptor #104 + version: "14" upsert descriptor #105 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/sql/schemachanger/testdata/end_to_end/drop_table_cross_table_trigger/drop_table_cross_table_trigger.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/drop_table_cross_table_trigger/drop_table_cross_table_trigger.side_effects index 5a323abc04d5..a07c6669f0ec 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/drop_table_cross_table_trigger/drop_table_cross_table_trigger.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/drop_table_cross_table_trigger/drop_table_cross_table_trigger.side_effects @@ -177,6 +177,7 @@ upsert descriptor #107 + version: "6" upsert descriptor #108 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: @@ -191,6 +192,7 @@ upsert descriptor #108 volatility: VOLATILE upsert descriptor #109 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 105 - triggerIds: @@ -381,6 +383,7 @@ upsert descriptor #107 + version: "6" upsert descriptor #108 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 104 - triggerIds: @@ -402,6 +405,7 @@ upsert descriptor #108 volatility: VOLATILE upsert descriptor #109 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - id: 105 - triggerIds: @@ -522,6 +526,7 @@ upsert descriptor #107 + version: "7" upsert descriptor #108 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root @@ -539,6 +544,7 @@ upsert descriptor #108 volatility: VOLATILE upsert descriptor #109 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/sql/schemachanger/testdata/end_to_end/drop_table_udf_default/drop_table_udf_default.side_effects b/pkg/sql/schemachanger/testdata/end_to_end/drop_table_udf_default/drop_table_udf_default.side_effects index d461747b59ed..d3fc18a1ff6e 100644 --- a/pkg/sql/schemachanger/testdata/end_to_end/drop_table_udf_default/drop_table_udf_default.side_effects +++ b/pkg/sql/schemachanger/testdata/end_to_end/drop_table_udf_default/drop_table_udf_default.side_effects @@ -23,6 +23,7 @@ write *eventpb.DropTable to event log: delete object namespace entry {100 101 t} -> 105 upsert descriptor #104 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - columnIds: - - 2 @@ -59,6 +60,7 @@ persist all catalog changes to storage delete object namespace entry {100 101 t} -> 105 upsert descriptor #104 function: + canMutate: CANNOT_MUTATE - dependedOnBy: - - columnIds: - - 2 @@ -121,6 +123,7 @@ begin transaction #3 ## PostCommitNonRevertiblePhase stage 1 of 1 with 5 MutationType ops upsert descriptor #104 function: + canMutate: CANNOT_MUTATE - declarativeSchemaChangerState: - authorization: - userName: root diff --git a/pkg/sql/sem/tree/create_routine.go b/pkg/sql/sem/tree/create_routine.go index d32d4c26a2fc..9c32c1225d64 100644 --- a/pkg/sql/sem/tree/create_routine.go +++ b/pkg/sql/sem/tree/create_routine.go @@ -95,6 +95,10 @@ type CreateRoutine struct { // BodyAnnotations is not assigned during initial parsing of user input. It's // assigned by the opt builder when the optimizer parses the body statements. BodyAnnotations []*Annotations + // CanMutate is set by the opt builder to indicate whether the routine body + // can perform mutations, including direct DML, mutations inside CTEs or + // subqueries, and calls to other mutating routines. + CanMutate bool } // Format implements the NodeFormatter interface. @@ -446,6 +450,34 @@ func (node RoutineSecurity) Format(ctx *FmtCtx) { } } +// RoutineCanMutate describes whether a routine body can perform mutations +// (INSERT, UPDATE, DELETE, UPSERT, or calls to other mutating routines). +type RoutineCanMutate int + +const ( + // RoutineCanMutateUnknown indicates the mutation status has not been + // determined. This is the case for function descriptors created before + // the can_mutate field was introduced. Consumers must fall back to + // inspecting the body RelExprs when this value is encountered, and + // deferred optbuild must not be used. + RoutineCanMutateUnknown RoutineCanMutate = iota + // RoutineMutates indicates the routine body contains mutations. + RoutineMutates + // RoutineDoesNotMutate indicates the routine body does not contain + // mutations. + RoutineDoesNotMutate +) + +// RoutineCanMutateFromBool converts a boolean CanMutate value to the +// corresponding RoutineCanMutate enum value. This is used when the mutation +// status is definitively known (e.g., after analyzing body RelExprs). +func RoutineCanMutateFromBool(canMutate bool) RoutineCanMutate { + if canMutate { + return RoutineMutates + } + return RoutineDoesNotMutate +} + // RoutineBodyStr is a string containing all statements in a UDF body. type RoutineBodyStr string diff --git a/pkg/sql/sem/tree/overload.go b/pkg/sql/sem/tree/overload.go index 8ca22b4c15e9..e7f7b37a2592 100644 --- a/pkg/sql/sem/tree/overload.go +++ b/pkg/sql/sem/tree/overload.go @@ -301,6 +301,12 @@ type Overload struct { // should be performed against the function owner rather than the invoking // user. SecurityMode RoutineSecurity + + // CanMutate indicates whether the routine body can perform mutations. + // This includes direct DML, mutations inside CTEs or subqueries, and + // calls to other mutating routines. RoutineCanMutateUnknown means the + // descriptor predates the field and consumers must check body RelExprs. + CanMutate RoutineCanMutate } // params implements the overloadImpl interface. diff --git a/pkg/upgrade/upgrades/upgrades.go b/pkg/upgrade/upgrades/upgrades.go index 11179310d8f9..ec3a5e7e8ae0 100644 --- a/pkg/upgrade/upgrades/upgrades.go +++ b/pkg/upgrade/upgrades/upgrades.go @@ -220,6 +220,18 @@ var upgrades = []upgradebase.Upgrade{ "restore for a cluster predating this table can leave it empty", ), ), + + upgrade.NewTenantUpgrade( + "add can_mutate field to function descriptors", + clusterversion.V26_3_FunctionDescCanMutate.Version(), + upgrade.NoPrecondition, + // No-op: pre-existing function descriptors have the zero value + // UNKNOWN_CAN_MUTATE, which causes consumers to fall back to + // inspecting eagerly-built body RelExprs. CREATE OR REPLACE + // upgrades the descriptor to CAN_MUTATE or CANNOT_MUTATE. + NoTenantUpgradeFunc, + upgrade.RestoreActionNotRequired("function descriptors are restored with correct can_mutate values"), + ), // Note: when starting a new release version, the first upgrade (for // Vxy_zStart) must be a newFirstUpgrade. Keep this comment at the bottom. } From 5ec174c60716e88897f9563ddf0ae2a382644ea2 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 15:39:00 -0400 Subject: [PATCH 04/12] sql/opt: enable deferred SQL routine body building MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable deferred body building for SQL routines: body RelExprs are now built at execution time rather than plan time. Two cases still require eager build: - AnyTuple return type (RECORD without OUT params), because the actual return type must be inferred from the body. - Inlineable UDFs (single-statement, non-volatile, non-set-returning), because expression indexes and partial index predicates depend on the inlined body at plan time. Without this, CREATE INDEX on an IMMUTABLE UDF expression would fail. This restriction is overly conservative for regular DML queries — #169459 tracks loosening it to only force eager build in contexts that actually require plan-time inlining. EXPLAIN respects the deferred execution flow: rather than forcing eager build, a BuildDeferredBody callback on ExprFmtCtx builds deferred bodies during formatting, showing the full plan structure inline. For EXPLAIN (OPT, ENV), table refs from deferred body memos are unioned into the outer metadata so schemas and stats are collected. A side effect of deferred build is that privilege checks now match PostgreSQL: EXECUTE on the function is checked before SELECT on tables referenced in the body (previously reversed because eager build resolved table refs first). Release note (performance improvement): SQL routine (UDF/procedure) body statements are now built at execution time rather than plan time. --- pkg/sql/logictest/testdata/logic_test/udf | 32 ++----- pkg/sql/logictest/testdata/logic_test/udf_fk | 87 ++++++++++++++++++- pkg/sql/opt/exec/execbuilder/relational.go | 2 +- pkg/sql/opt/exec/execbuilder/scalar.go | 2 +- pkg/sql/opt/exec/execbuilder/statement.go | 33 +++++++ pkg/sql/opt/memo/expr_format.go | 90 ++++++++++++++------ pkg/sql/opt/optbuilder/routine.go | 73 +++++++++++++++- 7 files changed, 256 insertions(+), 63 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/udf b/pkg/sql/logictest/testdata/logic_test/udf index 28470d344430..e00d81aa5373 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf +++ b/pkg/sql/logictest/testdata/logic_test/udf @@ -19,7 +19,6 @@ CREATE FUNCTION f() RETURNS INT IMMUTABLE AS $$ SELECT 1 $$; statement error pgcode 42P13 pq: no function body specified CREATE FUNCTION f() RETURNS INT IMMUTABLE LANGUAGE SQL; - statement ok CREATE FUNCTION a(i INT) RETURNS INT LANGUAGE SQL AS 'SELECT i' @@ -87,7 +86,6 @@ INDEX t_idx_b(b), INDEX t_idx_c(c) ); - statement ok CREATE FUNCTION f(a notmyworkday) RETURNS INT VOLATILE LANGUAGE SQL AS $$ SELECT a FROM t; @@ -160,7 +158,6 @@ statement ok USE test; subtest end - subtest udf_regproc query T @@ -201,7 +198,6 @@ SELECT 999999::regproc; subtest end - subtest execute_dropped_function statement ok @@ -220,7 +216,6 @@ SELECT f_test_exec_dropped(321); subtest end - subtest create_or_replace_function statement ok @@ -296,7 +291,6 @@ $$ subtest end - subtest seq_qualified_name statement ok @@ -324,7 +318,6 @@ SELECT f_seq_qualified_name_quoted() subtest end - subtest execution statement ok @@ -503,7 +496,6 @@ SELECT int_identity((1/0)::INT) subtest end - subtest args statement error pgcode 42P13 pq: SQL functions cannot have arguments of type record @@ -630,7 +622,6 @@ FROM generate_series(1, 100) AS g(i) subtest end - subtest return_type_assignment_casts # Do not allow functions with return type mismatches that cannot be cast in an @@ -659,7 +650,6 @@ SELECT stoc('abc') subtest end - subtest tagged_dollar_quotes statement ok @@ -693,7 +683,6 @@ $inner$ subtest end - subtest array_flatten statement ok @@ -715,7 +704,6 @@ SELECT arr(i) FROM generate_series(1, 3) g(i) subtest end - subtest lowercase_hint_error_implicit_schema statement ok @@ -1143,13 +1131,9 @@ SET ROLE bob; statement error pgcode 42501 pq: user bob does not have EXECUTE privilege on function f_scalar SELECT f_scalar(); -# Note: Postgres fails with a different error message: "user bob does not have EXECUTE privilege on function f_scalar". -# This is because psql will first validate access to f_scalar, but crdb first validates access to xy. -skipif config local-mixed-25.4 local-mixed-26.1 -statement error pgcode 42501 pq: user bob does not have SELECT privilege on relation xy -SELECT * FROM v; - -onlyif config local-mixed-25.4 local-mixed-26.1 +# With deferred routine body building, CockroachDB now checks EXECUTE privilege +# on the function before checking SELECT on the table referenced in the body, +# matching PostgreSQL behavior. statement error pgcode 42501 pq: user bob does not have EXECUTE privilege on function f_scalar SELECT * FROM v; @@ -1493,15 +1477,13 @@ statement error pgcode 42501 user alice does not have SELECT privilege on relati SELECT * FROM priv_f() # Same result through the view: priv_f runs as alice (INVOKER), who -# cannot read priv_t. -skipif config local-mixed-25.4 local-mixed-26.1 +# cannot read priv_t. Previously, eager body building during view +# expansion checked privileges as the view owner (root), masking the +# missing SELECT privilege. Deferred optbuild builds the body at +# execution time in the invoker's context, which correctly fails. statement error pgcode 42501 user alice does not have SELECT privilege on relation priv_t SELECT * FROM priv_v -onlyif config local-mixed-25.4 local-mixed-26.1 -statement ok -SELECT * FROM priv_v - statement ok RESET ROLE diff --git a/pkg/sql/logictest/testdata/logic_test/udf_fk b/pkg/sql/logictest/testdata/logic_test/udf_fk index 4c3964ce5490..6584ca050c26 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf_fk +++ b/pkg/sql/logictest/testdata/logic_test/udf_fk @@ -11,7 +11,6 @@ CREATE TABLE parent (p INT PRIMARY KEY); statement ok CREATE TABLE child (c INT PRIMARY KEY, p INT NOT NULL REFERENCES parent(p)); - subtest insert statement ok @@ -126,7 +125,6 @@ subtest end subtest delete - statement ok ALTER TABLE parent SET (schema_locked=false) @@ -636,7 +634,6 @@ SELECT * FROM selfref; subtest end - subtest corruption_check statement ok @@ -704,7 +701,6 @@ SELECT i, j FROM child@child_j_idx; statement ok DROP TABLE IF EXISTS child CASCADE; - statement ok ALTER TABLE parent SET (schema_locked=false) @@ -759,5 +755,88 @@ SELECT i, j FROM child@child_j_idx; ---- 0 2 +subtest end + +subtest deferred_build_mutation_conflicts + +# Test mutation conflict detection between deferred-build UDFs and sibling CTEs. +# Use dedicated table names to avoid conflicts with other subtests. + +statement ok +CREATE TABLE deferred_parent (i INT PRIMARY KEY); + +statement ok +CREATE TABLE deferred_child ( + i INT PRIMARY KEY, + j INT REFERENCES deferred_parent(i) ON DELETE CASCADE, + k INT, + INDEX deferred_child_j_idx (j) +); + +statement ok +INSERT INTO deferred_parent VALUES (0), (1), (2); + +statement ok +INSERT INTO deferred_child VALUES (0, 0, 0); + +# Two sibling deferred UDFs doing simple INSERTs on different tables are allowed. +statement ok +CREATE FUNCTION insert_deferred_parent(v INT) RETURNS INT LANGUAGE SQL AS $$ + INSERT INTO deferred_parent VALUES (v) RETURNING i; +$$; + +statement ok +CREATE FUNCTION insert_deferred_child(v INT, p INT) RETURNS INT LANGUAGE SQL AS $$ + INSERT INTO deferred_child VALUES (v, p, 0) RETURNING i; +$$; + +query II +WITH x AS (SELECT insert_deferred_parent(10) AS i), y AS (SELECT insert_deferred_child(10, 1) AS i) SELECT * FROM x, y; +---- +10 10 + +# Verify data integrity after the above. +query I rowsort +SELECT i FROM deferred_parent WHERE i >= 10; +---- +10 + +query III rowsort +SELECT i, j, k FROM deferred_child WHERE i >= 10; +---- +10 1 0 + +# Clean up test functions. +statement ok +DROP FUNCTION insert_deferred_parent; +DROP FUNCTION insert_deferred_child; + +# Two sibling deferred UDFs mutating different tables are allowed. +statement ok +CREATE FUNCTION update_deferred_parent(v INT) RETURNS INT LANGUAGE SQL AS $$ + UPDATE deferred_parent SET i = i WHERE i = v RETURNING i; +$$; + +statement ok +CREATE FUNCTION update_deferred_child_k(v INT, new_k INT) RETURNS INT LANGUAGE SQL AS $$ + UPDATE deferred_child SET k = new_k WHERE i = v RETURNING i; +$$; + +query II +WITH x AS (SELECT update_deferred_parent(0) AS i), y AS (SELECT update_deferred_child_k(0, 5) AS i) SELECT * FROM x, y; +---- +0 0 + +# Verify data integrity. +query III +SELECT i, j, k FROM deferred_child WHERE i = 0; +---- +0 0 5 + +statement ok +DROP FUNCTION update_deferred_parent; +DROP FUNCTION update_deferred_child_k; +DROP TABLE deferred_child; +DROP TABLE deferred_parent; subtest end diff --git a/pkg/sql/opt/exec/execbuilder/relational.go b/pkg/sql/opt/exec/execbuilder/relational.go index 7f7afce7369a..1efb18bb0de7 100644 --- a/pkg/sql/opt/exec/execbuilder/relational.go +++ b/pkg/sql/opt/exec/execbuilder/relational.go @@ -3734,7 +3734,7 @@ func (b *Builder) buildCall(c *memo.CallExpr) (_ execPlan, outputCols colOrdMap, false, /* allowOuterWithRefs */ nil, /* wrapRootExpr */ 0, /* resultBufferID */ - nil, /* bodyBuilder */ + udf.Def.BodyBuilder, ) r := tree.NewTypedRoutineExpr( diff --git a/pkg/sql/opt/exec/execbuilder/scalar.go b/pkg/sql/opt/exec/execbuilder/scalar.go index f4ed9a420734..d66dcf4dd068 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -1035,7 +1035,7 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ false, /* allowOuterWithRefs */ nil, /* wrapRootExpr */ udf.Def.ResultBufferID, - nil, /* bodyBuilder */ + udf.Def.BodyBuilder, ) // Enable stepping for volatile functions so that statements within the UDF diff --git a/pkg/sql/opt/exec/execbuilder/statement.go b/pkg/sql/opt/exec/execbuilder/statement.go index 9837caed04be..172a6a679c15 100644 --- a/pkg/sql/opt/exec/execbuilder/statement.go +++ b/pkg/sql/opt/exec/execbuilder/statement.go @@ -134,6 +134,29 @@ func (b *Builder) buildExplainOpt( } f := memo.MakeExprFmtCtx(b.ctx, fmtFlags, redactValues, b.mem, b.catalog) + + // Set up a callback to build deferred routine bodies during formatting. + // This ensures EXPLAIN output shows the full plan structure for deferred + // bodies, matching the actual execution behavior. Each body is built in + // a fresh optimizer so it gets its own memo (the outer memo is frozen + // after optimization). + var deferredBodyMemos []*memo.Memo + f.BuildDeferredBody = func( + def *memo.UDFDefinition, + ) ([]memo.RelExpr, opt.ColList, *memo.Memo, error) { + var o xform.Optimizer + o.Init(b.ctx, b.evalCtx, b.catalog) + body, _, params, err := def.BodyBuilder.Build( + b.ctx, b.semaCtx, b.evalCtx, b.catalog, o.Factory(), + ) + if err != nil { + return nil, nil, nil, err + } + bodyMemo := o.Factory().Memo() + deferredBodyMemos = append(deferredBodyMemos, bodyMemo) + return body, params, bodyMemo, nil + } + f.FormatExpr(explain.Input) planStr := f.Buffer.String() if redactValues { @@ -146,6 +169,16 @@ func (b *Builder) buildExplainOpt( // tell the exec factory what information it needs to fetch. var envOpts exec.ExplainEnvData if explain.Options.Flags[tree.ExplainFlagEnv] { + // Add table references from deferred body memos to the outer memo's + // metadata so that EXPLAIN (OPT, ENV) includes their schemas and stats. + for _, bodyMemo := range deferredBodyMemos { + bodyMeta := bodyMemo.Metadata() + for i := 0; i < bodyMeta.NumTables(); i++ { + tabID := opt.TableID(i + 1) + tab := bodyMeta.Table(tabID) + b.mem.Metadata().AddTable(tab, &tree.TableName{}) + } + } var err error envOpts, err = b.getEnvData() if err != nil { diff --git a/pkg/sql/opt/memo/expr_format.go b/pkg/sql/opt/memo/expr_format.go index e77e1800f8b2..4fcd3acf855f 100644 --- a/pkg/sql/opt/memo/expr_format.go +++ b/pkg/sql/opt/memo/expr_format.go @@ -177,6 +177,12 @@ type ExprFmtCtx struct { // tailCalls allows for quick lookup of all the routines in tail-call position // when the last body statement of a routine is formatted. tailCalls map[opt.ScalarExpr]struct{} + + // BuildDeferredBody, when set, is called during formatting to build + // deferred UDF bodies and show their plan structure inline. The callback + // returns body RelExprs, params, and the memo they belong to (needed so + // column metadata resolves correctly for the body's column IDs). + BuildDeferredBody func(def *UDFDefinition) (body []RelExpr, params opt.ColList, bodyMemo *Memo, err error) } // makeExprFmtCtxForString creates an expression formatting context from a new @@ -1073,47 +1079,75 @@ func (f *ExprFmtCtx) formatScalar(scalar opt.ScalarExpr, tp treeprinter.Node) { func (f *ExprFmtCtx) formatScalarWithLabel( label string, scalar opt.ScalarExpr, tp treeprinter.Node, ) { + formatUDFBody := func(def *UDFDefinition, body []RelExpr, tp treeprinter.Node) { + n := tp.Child("body") + for i := range body { + stmtNode := n + if i == 0 { + if def.FirstStmtOutput.CursorDeclaration != nil { + stmtNode = n.Child("open-cursor") + } else if def.FirstStmtOutput.TargetBufferID != 0 { + stmtNode = n.Child("add-to-srf-result") + } + } + prevTailCalls := f.tailCalls + + // Routine calls in the last body statement may be tail calls if + // ResultBufferID is unset. If it is set, the result of the last + // body statement is not directly used as the result of the UDF + // call, so it cannot contain tail calls. + if i == len(body)-1 && def.ResultBufferID == 0 { + f.tailCalls = make(map[opt.ScalarExpr]struct{}) + ExtractTailCalls(body[i], f.tailCalls) + } + f.formatExpr(body[i], stmtNode) + f.tailCalls = prevTailCalls + } + } + formatUDFDefinition := func(def *UDFDefinition, tp treeprinter.Node) { if _, seen := f.seenUDFs[def]; !seen { // Ensure that the definition of the UDF is not printed out again if // it has already been seen. f.seenUDFs[def] = struct{}{} f.withinUDFs[def] = struct{}{} - if len(def.Params) > 0 { - f.formatColList(tp, "params:", def.Params, opt.ColSet{} /* notNullCols */) - } if def.Body != nil { - n := tp.Child("body") - for i := range def.Body { - stmtNode := n - if i == 0 { - if def.FirstStmtOutput.CursorDeclaration != nil { - // The first statement is opening a cursor. - stmtNode = n.Child("open-cursor") - } else if def.FirstStmtOutput.TargetBufferID != 0 { - // The first statement is writing to a target buffer. - stmtNode = n.Child("add-to-srf-result") - } - } - prevTailCalls := f.tailCalls - - // Routine calls in the last body statement may be tail calls if - // ResultBufferID is unset. If it is set, the result of the last - // body statement is not directly used as the result of the UDF - // call, so it cannot contain tail calls. - if i == len(def.Body)-1 && def.ResultBufferID == 0 { - f.tailCalls = make(map[opt.ScalarExpr]struct{}) - ExtractTailCalls(def.Body[i], f.tailCalls) + // Eager path: body was built at plan time. + if len(def.Params) > 0 { + f.formatColList(tp, "params:", def.Params, opt.ColSet{} /* notNullCols */) + } + formatUDFBody(def, def.Body, tp) + } else if f.BuildDeferredBody != nil && def.BodyBuilder != nil { + // Deferred + callback: build the body now for display. + body, params, bodyMemo, err := f.BuildDeferredBody(def) + if err != nil { + tp.Childf("error building deferred body: %v", err) + } else { + // Swap to body memo so column IDs resolve correctly. + prevMemo := f.Memo + f.Memo = bodyMemo + if len(params) > 0 { + f.formatColList(tp, "params:", params, opt.ColSet{} /* notNullCols */) } - f.formatExpr(def.Body[i], stmtNode) - f.tailCalls = prevTailCalls + formatUDFBody(def, body, tp) + f.Memo = prevMemo } } else { - // Deferred-build routine: body is not yet built. Show ASTs. + // Deferred fallback: no callback, show AST text. + if len(def.Params) > 0 { + f.formatColList(tp, "params:", def.Params, opt.ColSet{} /* notNullCols */) + } n := tp.Child("body (deferred)") + fmtFlags := tree.FmtSimple + if f.RedactableValues { + fmtFlags = tree.FmtMarkRedactionNode | tree.FmtOmitNameRedaction + } + fmtCtx := tree.NewFmtCtx(fmtFlags) for i, ast := range def.BodyASTs { if ast != nil { - n.Childf("stmt%d: %s", i+1, tree.AsString(ast)) + fmtCtx.Reset() + fmtCtx.FormatNode(ast) + n.Childf("stmt%d: %s", i+1, fmtCtx.String()) } } } diff --git a/pkg/sql/opt/optbuilder/routine.go b/pkg/sql/opt/optbuilder/routine.go index b69dcbfefd5a..1fc5f3975c77 100644 --- a/pkg/sql/opt/optbuilder/routine.go +++ b/pkg/sql/opt/optbuilder/routine.go @@ -442,6 +442,7 @@ func (b *Builder) buildRoutine( var bodyStmts []string var bodyTags []string var bodyASTs []tree.Statement + var bodyBuilder memo.RoutineBodyBuilder switch o.Language { case tree.RoutineLangSQL: // Parse the function body. @@ -455,10 +456,73 @@ func (b *Builder) buildRoutine( for i := range stmts { stmtASTs[i] = stmts[i].AST } - body, bodyProps, bodyTags = b.buildSQLRoutineBodyStmts( - stmtASTs, bodyScope, f.ResolvedType(), f, inScope, isSetReturning, - oldInsideDataSource, - ) + + // Validate column definition list before the eager/deferred branch. + // Skip for AnyTuple since that always takes the eager path where + // validation runs inside finalizeRoutineReturnType. + if oldInsideDataSource && !f.ResolvedType().Identical(types.AnyTuple) { + b.validateGeneratorFunctionReturnType(f.ResolvedOverload(), f.ResolvedType(), inScope) + } + + // Force eager build when the UDF could be inlined. Inlining requires + // len(Body) == 1 and non-volatile, which means the body RelExpr must + // be available at plan time. UDFs used in expression indexes and + // partial index predicates depend on inlining for correctness. + // + // TODO(#169459): this is overly conservative — most inlineable UDFs + // in regular DML queries don't need eager build. Only force it in + // contexts that require plan-time inlining (expression indexes, + // partial index predicates, computed columns). + couldBeInlined := len(stmts) == 1 && !isSetReturning && + o.Volatility != volatility.Volatile + + if f.ResolvedType().Identical(types.AnyTuple) || couldBeInlined || b.insideFuncDef { + // Eager path: build body RelExprs now. + body, bodyProps, bodyTags = b.buildSQLRoutineBodyStmts( + stmtASTs, bodyScope, f.ResolvedType(), f, inScope, isSetReturning, + oldInsideDataSource, + ) + } else { + // Deferred path: store ASTs and tags only; body and bodyProps stay nil. + bodyTags = make([]string, len(stmtASTs)) + for i, ast := range stmtASTs { + bodyTags[i] = ast.StatementTag() + } + + // Capture resolved parameter types and names for the deferred builder. + var resolvedParamTypes []*types.T + var resolvedParamNames []tree.Name + if paramTypes, ok := o.Types.(tree.ParamTypes); ok { + resolvedParamTypes = make([]*types.T, len(paramTypes)) + resolvedParamNames = make([]tree.Name, len(paramTypes)) + for i := range paramTypes { + resolvedParamTypes[i] = maybeReplacePolymorphicType(paramTypes[i].Typ, polyArgTyp) + if resolvedParamTypes[i].Identical(types.AnyTuple) { + // RECORD-typed parameter: use the actual argument type. + resolvedParamTypes[i] = argTypes[i] + } + resolvedParamNames[i] = tree.Name(paramTypes[i].Name) + } + } + + // Capture the effective privilege user for SECURITY DEFINER. + var privUser string + if !b.dataSourcePrivilegeUserOverride.Undefined() { + privUser = b.dataSourcePrivilegeUserOverride.Normalized() + } + + bodyBuilder = &sqlRoutineBodyBuilder{ + stmtASTs: stmtASTs, + paramTypes: resolvedParamTypes, + paramNames: resolvedParamNames, + rTyp: f.ResolvedType(), + isSetReturning: isSetReturning, + insideDataSource: oldInsideDataSource, + privilegeUser: privUser, + routineType: o.Type, + stmtTreeInitFn: b.stmtTree.GetInitFnForDeferredRoutine(), + } + } // Collect the original (pre-VOID-append) ASTs for the UDFDefinition. bodyASTs = stmtASTs @@ -553,6 +617,7 @@ func (b *Builder) buildRoutine( Params: params, ResultBufferID: resultBufferID, CanMutate: canMutate, + BodyBuilder: bodyBuilder, }, }, ) From bff8ef24a98f7c3c84770d0c832757dc38906cc6 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 16:05:51 -0400 Subject: [PATCH 05/12] sql: add EXPLAIN ANALYZE (DEBUG) bundle support for deferred routine bodies When SQL routine body building is deferred to execution time, the plan-time memo lacks body RelExprs and table references. This causes EXPLAIN ANALYZE (DEBUG) bundles to miss optimizer detail and table stats/schema for tables referenced inside deferred routines. This commit propagates execution-time metadata back to the bundle collector: - Add DeferredRoutineOptPlans and DeferredRoutineTableRefs fields to eval.Context, initialized when bundle collection is active. - After deferred body building in buildRoutinePlanGenerator, capture the formatted optimizer plan (opt-vv level with redaction markers) and all table references from the execution-time memo. - In the bundle collector, emit opt-vv-deferred-.txt files and union deferred table refs with plan-time metadata for stats/schema collection. Note: EXPLAIN ANALYZE already uses deferred build with no special handling needed. The conn_executor intercepts the ExplainAnalyze AST before the optbuilder runs, strips the EXPLAIN ANALYZE wrapper, and passes the inner statement through the normal build path where deferred build is active. Output is generated after execution by walking the explain.Plan tree (not the memo), so deferred bodies are transparent. Release note: None --- pkg/sql/explain_bundle.go | 35 ++++++++++++++++++ pkg/sql/explain_bundle_test.go | 10 +++--- pkg/sql/instrumentation.go | 8 +++++ pkg/sql/opt/exec/execbuilder/relational.go | 1 + pkg/sql/opt/exec/execbuilder/scalar.go | 41 ++++++++++++++++++++++ pkg/sql/sem/eval/BUILD.bazel | 1 + pkg/sql/sem/eval/context.go | 23 ++++++++++++ 7 files changed, 114 insertions(+), 5 deletions(-) diff --git a/pkg/sql/explain_bundle.go b/pkg/sql/explain_bundle.go index 3937b289d406..491273d06937 100644 --- a/pkg/sql/explain_bundle.go +++ b/pkg/sql/explain_bundle.go @@ -414,6 +414,24 @@ func (b *stmtBundleBuilder) addOptPlans(ctx context.Context) { memo.ExprFmtHideQualifications|memo.ExprFmtHideScalars|memo.ExprFmtHideTypes|memo.ExprFmtHideNotVisibleIndexInfo|memo.ExprFmtHideFastPathChecks, )) b.z.AddFile("opt-vv.txt", formatOptPlan(memo.ExprFmtHideQualifications|memo.ExprFmtHideNotVisibleIndexInfo)) + + // Add supplementary opt-vv files for deferred routine bodies that were + // built during execution. The captured plans always contain redaction + // markers, so we either redact or strip them depending on the mode. + if b.p != nil { + evalCtx := b.p.EvalContext() + for _, dp := range evalCtx.DeferredRoutineOptPlans { + fileName := fmt.Sprintf("opt-vv-deferred-%s.txt", dp.Name) + rs := redact.RedactableString(dp.Plan) + var contents string + if b.flags.RedactValues { + contents = string(rs.Redact()) + } else { + contents = rs.StripMarkers() + } + b.z.AddFile(fileName, contents) + } + } } // addExecPlan adds the EXPLAIN (VERBOSE) plan as file plan.txt. @@ -719,6 +737,13 @@ func (b *stmtBundleBuilder) addEnv(ctx context.Context) { for _, table := range mem.Metadata().AllTables() { referencedByMetadata.Add(int(table.Table.ID())) } + // Include tables discovered during execution-time deferred routine + // body building. + if b.p != nil { + for _, tab := range b.p.EvalContext().DeferredRoutineTableRefs { + referencedByMetadata.Add(int(tab.ID())) + } + } var refTables []cat.Table var refTableIncluded intsets.Fast opt.VisitFKReferenceTables( @@ -861,6 +886,16 @@ func (b *stmtBundleBuilder) addEnv(ctx context.Context) { include = hasDelete || hasUpdate || hasUpsert }, ) + // Add tables discovered during deferred routine body building that + // weren't visited by VisitFKReferenceTables. + if b.p != nil { + for _, tab := range b.p.EvalContext().DeferredRoutineTableRefs { + if !tab.IsVirtualTable() && !refTableIncluded.Contains(int(tab.ID())) { + refTables = append(refTables, tab) + refTableIncluded.Add(int(tab.ID())) + } + } + } // Collect trigger information from all referenced tables. For each // trigger, we record its name (for CREATE TRIGGER output later), // the trigger function, and any tables/types/routines it depends on. diff --git a/pkg/sql/explain_bundle_test.go b/pkg/sql/explain_bundle_test.go index 57545d71be07..51538465eedd 100644 --- a/pkg/sql/explain_bundle_test.go +++ b/pkg/sql/explain_bundle_test.go @@ -718,7 +718,7 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R } return nil }, false, /* expectErrors */ - plans, "statement.sql stats-defaultdb.public.pterosaur.sql env.sql vec.txt vec-v.txt", + plans, "statement.sql stats-defaultdb.public.pterosaur.sql env.sql vec.txt vec-v.txt opt-vv-deferred-test_redact.txt", ) }) } @@ -808,7 +808,7 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R } return nil }, false /* expectErrors */, base, plans, - "distsql.html vec-v.txt vec.txt") + "distsql.html vec-v.txt vec.txt opt-vv-deferred-add_proc.txt") }) // Regression test for #142041: triggers, their functions, and tables they @@ -971,7 +971,7 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R return nil }, false /* expectErrors */, base, plans, - "stats-defaultdb.public.abc.sql stats-defaultdb.s.a.sql distsql.html vec-v.txt vec.txt", + "stats-defaultdb.public.abc.sql stats-defaultdb.s.a.sql distsql.html vec-v.txt vec.txt opt-vv-deferred-foo.txt", ) }) @@ -1002,7 +1002,7 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R return nil }, false /* expectErrors */, base, plans, - "stats-defaultdb.public.abc.sql stats-defaultdb.s.a.sql distsql.html vec-v.txt vec.txt", + "stats-defaultdb.public.abc.sql stats-defaultdb.s.a.sql distsql.html vec-v.txt vec.txt opt-vv-deferred-bar.txt", ) }) @@ -1210,7 +1210,7 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R } return nil }, false, /* expectErrors */ - base, plans, "distsql.html vec.txt vec-v.txt", + base, plans, "distsql.html vec.txt vec-v.txt opt-vv-deferred-r1.txt", ) }) diff --git a/pkg/sql/instrumentation.go b/pkg/sql/instrumentation.go index cb9c39a31aa1..c9bfd484bcd5 100644 --- a/pkg/sql/instrumentation.go +++ b/pkg/sql/instrumentation.go @@ -25,6 +25,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/hints" "github.com/cockroachdb/cockroach/pkg/sql/idxrecommendations" "github.com/cockroachdb/cockroach/pkg/sql/isql" + "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/opt/exec" "github.com/cockroachdb/cockroach/pkg/sql/opt/exec/execbuilder" "github.com/cockroachdb/cockroach/pkg/sql/opt/exec/explain" @@ -422,6 +423,13 @@ func (ih *instrumentationHelper) finalizeSetup(ctx context.Context, cfg *Executo if pollInterval := inFlightTraceCollectorPollInterval.Get(cfg.SV()); pollInterval > 0 { ih.startInFlightTraceCollector(ctx, cfg.InternalDB.Executor(), pollInterval) } + // Initialize the slices so that execution-time deferred routine body + // builds will capture their optimizer plans and table references for + // the bundle. + if ih.evalCtx != nil { + ih.evalCtx.DeferredRoutineOptPlans = make([]eval.DeferredRoutineOptPlan, 0) + ih.evalCtx.DeferredRoutineTableRefs = make([]cat.Table, 0) + } } } diff --git a/pkg/sql/opt/exec/execbuilder/relational.go b/pkg/sql/opt/exec/execbuilder/relational.go index 1efb18bb0de7..7aa8ef3fa6b1 100644 --- a/pkg/sql/opt/exec/execbuilder/relational.go +++ b/pkg/sql/opt/exec/execbuilder/relational.go @@ -3735,6 +3735,7 @@ func (b *Builder) buildCall(c *memo.CallExpr) (_ execPlan, outputCols colOrdMap, nil, /* wrapRootExpr */ 0, /* resultBufferID */ udf.Def.BodyBuilder, + udf.Def.Name, ) r := tree.NewTypedRoutineExpr( diff --git a/pkg/sql/opt/exec/execbuilder/scalar.go b/pkg/sql/opt/exec/execbuilder/scalar.go index d66dcf4dd068..3be2a97a9630 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -6,6 +6,7 @@ package execbuilder import ( + "bytes" "context" "github.com/cockroachdb/cockroach/pkg/sql/appstatspb" @@ -703,6 +704,7 @@ func (b *Builder) buildExistsSubquery( wrapRootExpr, 0, /* resultBufferID */ nil, /* bodyBuilder */ + "", /* routineName */ ) return tree.NewTypedCoalesceExpr(tree.TypedExprs{ tree.NewTypedRoutineExpr( @@ -832,6 +834,7 @@ func (b *Builder) buildSubquery( nil, /* wrapRootExpr */ 0, /* resultBufferID */ nil, /* bodyBuilder */ + "", /* routineName */ ) _, tailCall := b.tailCalls[subquery] return tree.NewTypedRoutineExpr( @@ -1036,6 +1039,7 @@ func (b *Builder) buildUDF(ctx *buildScalarCtx, scalar opt.ScalarExpr) (tree.Typ nil, /* wrapRootExpr */ udf.Def.ResultBufferID, udf.Def.BodyBuilder, + udf.Def.Name, ) // Enable stepping for volatile functions so that statements within the UDF @@ -1112,6 +1116,7 @@ func (b *Builder) initRoutineExceptionHandler( nil, /* wrapRootExpr */ 0, /* resultBufferID */ nil, /* bodyBuilder */ + "", /* routineName */ ) // Build a routine with no arguments for the exception handler. The actual // arguments will be supplied when (if) the handler is invoked. @@ -1164,6 +1169,7 @@ func (b *Builder) buildRoutinePlanGenerator( wrapRootExpr wrapRootExprFn, resultBufferID memo.RoutineResultBufferID, bodyBuilder memo.RoutineBodyBuilder, + routineName string, ) tree.RoutinePlanGenerator { // argOrd returns the ordinal of the argument within the arguments list that // can be substituted for each reference to the given function parameter @@ -1196,6 +1202,7 @@ func (b *Builder) buildRoutinePlanGenerator( var o xform.Optimizer var gistFactory explain.PlanGistFactory var latencyRecorder = sqlstats.NewStatementLatencyRecorder() + var deferredPlanCaptured bool originalMemo := b.mem planGen := func( ctx context.Context, @@ -1233,6 +1240,40 @@ func (b *Builder) buildRoutinePlanGenerator( } return 0, false } + + // Capture the deferred body's optimizer plan and table + // references for EXPLAIN ANALYZE (DEBUG) bundles. Only done on + // the first invocation to avoid duplicates from set-returning + // or multi-row routines. + if !deferredPlanCaptured && + b.evalCtx.DeferredRoutineOptPlans != nil && routineName != "" { + deferredPlanCaptured = true + flags := memo.ExprFmtHideQualifications | memo.ExprFmtHideNotVisibleIndexInfo + var buf bytes.Buffer + for i, stmt := range stmts { + if i > 0 { + buf.WriteString("\n") + } + // Always format with redaction markers so the bundle + // code can apply redaction when needed. + f := memo.MakeExprFmtCtxBuffer( + ctx, &buf, flags, + true /* redactableValues */, originalMemo, b.catalog, + ) + f.FormatExpr(stmt) + } + b.evalCtx.DeferredRoutineOptPlans = append( + b.evalCtx.DeferredRoutineOptPlans, + eval.DeferredRoutineOptPlan{Name: routineName, Plan: buf.String()}, + ) + // Capture table references from the deferred body's + // metadata so the bundle includes their stats and schema. + for _, tm := range originalMemo.Metadata().AllTables() { + b.evalCtx.DeferredRoutineTableRefs = append( + b.evalCtx.DeferredRoutineTableRefs, tm.Table, + ) + } + } } dbName := b.evalCtx.SessionData().Database diff --git a/pkg/sql/sem/eval/BUILD.bazel b/pkg/sql/sem/eval/BUILD.bazel index 9dd3ba9308ab..89981c70931c 100644 --- a/pkg/sql/sem/eval/BUILD.bazel +++ b/pkg/sql/sem/eval/BUILD.bazel @@ -60,6 +60,7 @@ go_library( "//pkg/sql/hintpb", "//pkg/sql/lex", "//pkg/sql/oidext", + "//pkg/sql/opt/cat", "//pkg/sql/parserutils", "//pkg/sql/pgrepl/lsn", "//pkg/sql/pgwire/pgcode", diff --git a/pkg/sql/sem/eval/context.go b/pkg/sql/sem/eval/context.go index 8d17d4ac2f37..4b79bfe25fe6 100644 --- a/pkg/sql/sem/eval/context.go +++ b/pkg/sql/sem/eval/context.go @@ -26,6 +26,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/server/telemetry" "github.com/cockroachdb/cockroach/pkg/settings/cluster" "github.com/cockroachdb/cockroach/pkg/sql/catalog/catpb" + "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgwirecancel" @@ -402,6 +403,28 @@ type Context struct { // WorkloadType distinguishes the kind of workload that WorkloadID // represents (statement fingerprint, job ID, system task). WorkloadType workloadid.WorkloadType + + // DeferredRoutineOptPlans accumulates formatted optimizer plans for SQL + // routine bodies that were built at execution time (deferred from plan + // time). These are used by EXPLAIN ANALYZE (DEBUG) to include + // supplementary opt-vv-deferred-.txt files in statement bundles, + // providing full optimizer detail for deferred routine bodies. + DeferredRoutineOptPlans []DeferredRoutineOptPlan + + // DeferredRoutineTableRefs accumulates table references discovered + // during execution-time building of deferred SQL routine bodies. The + // bundle collector unions these with the plan-time metadata tables to + // ensure stats and schema are collected for all referenced tables. + DeferredRoutineTableRefs []cat.Table +} + +// DeferredRoutineOptPlan holds the formatted optimizer plan output for a +// single deferred SQL routine body, captured during execution. +type DeferredRoutineOptPlan struct { + // Name is the function or procedure name. + Name string + // Plan is the formatted optimizer plan output (opt-vv level). + Plan string } // RoutineStatementCounters encapsulates metrics for tracking the execution From 71cfc1472a65760c6846dd1ddaf65bf16f6885e1 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 16:07:02 -0400 Subject: [PATCH 06/12] sql/opt: show deferred UDF body plans in test output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With deferred routine body building, volatile UDF bodies are not built at plan time — test output previously showed `body (deferred)` with raw AST text instead of the full RelExpr plan. This created a test coverage gap for deferred routine body plans. Set the `BuildDeferredBody` callback in `OptTester.FormatExpr` so that deferred bodies are built during formatting and tests show full plan structure inline. The callback builds the body into the outer memo's factory so column IDs are globally unique across the outer query and all UDF bodies. This follows the same pattern used for post-query (cascade/trigger) test formatting in `OptTester.PostQueries`, which also passes the outer factory to `Build()` for the same reason. Note that production code (both EXPLAIN and normal execution) correctly uses a fresh memo since the outer memo may be cached or shared. Also move `checkExpectedRules` from `postProcess` into a new `FormatAndCheck` method that runs after formatting, so that rules fired during deferred body building (e.g. `NormalizeArrayFlattenToAgg`) are tracked in `appliedRules` before `expect=`/`expect-not=` are checked. Test data changes fall into two categories: 1. Column ID renumbering: deferred UDF bodies previously showed body- memo column IDs starting from :1 (which could collide with outer query columns). Now that bodies are built into the outer memo, body column IDs continue from where the outer memo left off, producing globally unique IDs. 2. Outer query column renumbering: with eager build, body columns were allocated before some outer query columns, affecting the outer column numbering. With deferred build, the outer query columns are allocated first (body isn't built yet), so outer columns may get lower IDs than before. Release note: None --- pkg/sql/opt/exec/execbuilder/testdata/udf | 6 +- pkg/sql/opt/memo/expr_format.go | 2 + pkg/sql/opt/memo/testdata/logprops/udf | 16 +- pkg/sql/opt/norm/testdata/rules/inline | 12 +- pkg/sql/opt/norm/testdata/rules/scalar | 150 +-- pkg/sql/opt/norm/testdata/rules/udf | 110 +-- pkg/sql/opt/optbuilder/testdata/create_view | 8 +- .../optbuilder/testdata/fk-on-delete-cascade | 12 +- pkg/sql/opt/optbuilder/testdata/orderby | 14 +- pkg/sql/opt/optbuilder/testdata/procedure | 26 +- .../optbuilder/testdata/row_level_security | 48 +- pkg/sql/opt/optbuilder/testdata/udf | 866 +++++++++--------- pkg/sql/opt/optbuilder/testdata/view | 24 +- pkg/sql/opt/testutils/opttester/opt_tester.go | 61 +- 14 files changed, 707 insertions(+), 648 deletions(-) diff --git a/pkg/sql/opt/exec/execbuilder/testdata/udf b/pkg/sql/opt/exec/execbuilder/testdata/udf index f05ffcae315c..664cfcc180f1 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/udf +++ b/pkg/sql/opt/exec/execbuilder/testdata/udf @@ -320,15 +320,15 @@ EXPLAIN (OPT, TYPES) SELECT fn93210() ---- values - ├── columns: fn93210:6(int) + ├── columns: fn93210:1(int) ├── cardinality: [1 - 1] ├── volatile ├── stats: [rows=1] ├── cost: 0.02 ├── key: () - ├── fd: ()-->(6) + ├── fd: ()-->(1) ├── distribution: test - ├── prune: (6) + ├── prune: (1) └── tuple [type=tuple{int}] └── udf: fn93210 [type=int] └── body diff --git a/pkg/sql/opt/memo/expr_format.go b/pkg/sql/opt/memo/expr_format.go index 4fcd3acf855f..6a44b3458c4a 100644 --- a/pkg/sql/opt/memo/expr_format.go +++ b/pkg/sql/opt/memo/expr_format.go @@ -1085,8 +1085,10 @@ func (f *ExprFmtCtx) formatScalarWithLabel( stmtNode := n if i == 0 { if def.FirstStmtOutput.CursorDeclaration != nil { + // The first statement is opening a cursor. stmtNode = n.Child("open-cursor") } else if def.FirstStmtOutput.TargetBufferID != 0 { + // The first statement is writing to a target buffer. stmtNode = n.Child("add-to-srf-result") } } diff --git a/pkg/sql/opt/memo/testdata/logprops/udf b/pkg/sql/opt/memo/testdata/logprops/udf index 4c6054888ec0..d66102fb5a05 100644 --- a/pkg/sql/opt/memo/testdata/logprops/udf +++ b/pkg/sql/opt/memo/testdata/logprops/udf @@ -25,9 +25,9 @@ build SELECT a + fn_volatile() FROM ab ---- project - ├── columns: "?column?":6(int) + ├── columns: "?column?":5(int) ├── volatile - ├── prune: (6) + ├── prune: (5) ├── scan ab │ ├── columns: a:1(int!null) b:2(int) crdb_internal_mvcc_timestamp:3(decimal) tableoid:4(oid) │ ├── key: (1) @@ -35,26 +35,26 @@ project │ ├── prune: (1-4) │ └── interesting orderings: (+1) └── projections - └── plus [as="?column?":6, type=int, outer=(1), volatile, udf] + └── plus [as="?column?":5, type=int, outer=(1), volatile, udf] ├── variable: a:1 [type=int] └── udf: fn_volatile [type=int] └── body └── limit - ├── columns: "?column?":5(int!null) + ├── columns: "?column?":6(int!null) ├── cardinality: [1 - 1] ├── key: () - ├── fd: ()-->(5) + ├── fd: ()-->(6) ├── project - │ ├── columns: "?column?":5(int!null) + │ ├── columns: "?column?":6(int!null) │ ├── cardinality: [1 - 1] │ ├── key: () - │ ├── fd: ()-->(5) + │ ├── fd: ()-->(6) │ ├── values │ │ ├── cardinality: [1 - 1] │ │ ├── key: () │ │ └── tuple [type=tuple] │ └── projections - │ └── const: 1 [as="?column?":5, type=int] + │ └── const: 1 [as="?column?":6, type=int] └── const: 1 [type=int] build diff --git a/pkg/sql/opt/norm/testdata/rules/inline b/pkg/sql/opt/norm/testdata/rules/inline index 9f64a4313aaa..273ef2aa81ba 100644 --- a/pkg/sql/opt/norm/testdata/rules/inline +++ b/pkg/sql/opt/norm/testdata/rules/inline @@ -1768,11 +1768,11 @@ norm expect-not=InlineUDF SELECT vol() FROM a ---- project - ├── columns: vol:9 + ├── columns: vol:8 ├── volatile ├── scan a └── projections - └── vol() [as=vol:9, volatile, udf] + └── vol() [as=vol:8, volatile, udf] exec-ddl CREATE FUNCTION set_fn(x INT) RETURNS SETOF INT IMMUTABLE LANGUAGE SQL AS $$ @@ -1785,7 +1785,7 @@ norm expect-not=InlineUDF SELECT * FROM set_fn(0) ---- project-set - ├── columns: set_fn:9 + ├── columns: set_fn:2 ├── immutable ├── values │ ├── cardinality: [1 - 1] @@ -1806,12 +1806,12 @@ norm expect-not=InlineUDF SELECT multi_stmt() FROM a ---- project - ├── columns: multi_stmt:10 + ├── columns: multi_stmt:8 ├── immutable - ├── fd: ()-->(10) + ├── fd: ()-->(8) ├── scan a └── projections - └── multi_stmt() [as=multi_stmt:10, immutable, udf] + └── multi_stmt() [as=multi_stmt:8, immutable, udf] exec-ddl CREATE FUNCTION empty() RETURNS BOOL STABLE LANGUAGE SQL AS '' diff --git a/pkg/sql/opt/norm/testdata/rules/scalar b/pkg/sql/opt/norm/testdata/rules/scalar index a6bea921cd82..38f7e5dd0a5d 100644 --- a/pkg/sql/opt/norm/testdata/rules/scalar +++ b/pkg/sql/opt/norm/testdata/rules/scalar @@ -1062,74 +1062,74 @@ opt expect=EliminateUDFCallSubquery format=show-scalars SELECT (VALUES (f1(0))) ---- values - ├── columns: column1:4 + ├── columns: column1:3 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(4) + ├── fd: ()-->(3) └── tuple └── udf: f1 ├── args │ └── const: 0 - ├── params: i:1 + ├── params: i:4 └── body └── values - ├── columns: i:2 - ├── outer: (1) + ├── columns: i:5 + ├── outer: (4) ├── cardinality: [1 - 1] ├── key: () - ├── fd: ()-->(2) + ├── fd: ()-->(5) └── tuple - └── variable: i:1 + └── variable: i:4 opt expect=EliminateUDFCallSubquery format=show-scalars SELECT (SELECT f1(0)) ---- values - ├── columns: f1:4 + ├── columns: f1:3 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(4) + ├── fd: ()-->(3) └── tuple └── udf: f1 ├── args │ └── const: 0 - ├── params: i:1 + ├── params: i:4 └── body └── values - ├── columns: i:2 - ├── outer: (1) + ├── columns: i:5 + ├── outer: (4) ├── cardinality: [1 - 1] ├── key: () - ├── fd: ()-->(2) + ├── fd: ()-->(5) └── tuple - └── variable: i:1 + └── variable: i:4 opt expect=EliminateUDFCallSubquery format=show-scalars SELECT (SELECT v FROM (VALUES (f1(0))) AS v(v)) ---- values - ├── columns: v:4 + ├── columns: v:3 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(4) + ├── fd: ()-->(3) └── tuple └── udf: f1 ├── args │ └── const: 0 - ├── params: i:1 + ├── params: i:4 └── body └── values - ├── columns: i:2 - ├── outer: (1) + ├── columns: i:5 + ├── outer: (4) ├── cardinality: [1 - 1] ├── key: () - ├── fd: ()-->(2) + ├── fd: ()-->(5) └── tuple - └── variable: i:1 + └── variable: i:4 # Cannot eliminate multi-row values subquery. @@ -1137,136 +1137,136 @@ opt expect-not=EliminateUDFCallSubquery format=show-scalars SELECT (VALUES (f1(0)), (f1(1))) ---- values - ├── columns: column1:6 + ├── columns: column1:4 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(6) + ├── fd: ()-->(4) └── tuple └── subquery └── max1-row - ├── columns: column1:5 + ├── columns: column1:3 ├── error: "more than one row returned by a subquery used as an expression" ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(5) + ├── fd: ()-->(3) └── values - ├── columns: column1:5 + ├── columns: column1:3 ├── cardinality: [2 - 2] ├── volatile ├── tuple │ └── udf: f1 │ ├── args │ │ └── const: 0 - │ ├── params: i:1 + │ ├── params: i:5 │ └── body │ └── values - │ ├── columns: i:2 - │ ├── outer: (1) + │ ├── columns: i:6 + │ ├── outer: (5) │ ├── cardinality: [1 - 1] │ ├── key: () - │ ├── fd: ()-->(2) + │ ├── fd: ()-->(6) │ └── tuple - │ └── variable: i:1 + │ └── variable: i:5 └── tuple └── udf: f1 ├── args │ └── const: 1 - ├── params: i:3 + ├── params: i:7 └── body └── values - ├── columns: i:4 - ├── outer: (3) + ├── columns: i:8 + ├── outer: (7) ├── cardinality: [1 - 1] ├── key: () - ├── fd: ()-->(4) + ├── fd: ()-->(8) └── tuple - └── variable: i:3 + └── variable: i:7 # Cannot eliminate multi-column values subquery. opt expect-not=EliminateUDFCallSubquery format=show-scalars SELECT (f1(0), f1(1)) = (SELECT f1(0), f1(1)) ---- values - ├── columns: "?column?":12 + ├── columns: "?column?":8 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(12) + ├── fd: ()-->(8) └── tuple └── eq ├── tuple │ ├── udf: f1 │ │ ├── args │ │ │ └── const: 0 - │ │ ├── params: i:7 + │ │ ├── params: i:9 │ │ └── body │ │ └── values - │ │ ├── columns: i:8 - │ │ ├── outer: (7) + │ │ ├── columns: i:10 + │ │ ├── outer: (9) │ │ ├── cardinality: [1 - 1] │ │ ├── key: () - │ │ ├── fd: ()-->(8) + │ │ ├── fd: ()-->(10) │ │ └── tuple - │ │ └── variable: i:7 + │ │ └── variable: i:9 │ └── udf: f1 │ ├── args │ │ └── const: 1 - │ ├── params: i:9 + │ ├── params: i:11 │ └── body │ └── values - │ ├── columns: i:10 - │ ├── outer: (9) + │ ├── columns: i:12 + │ ├── outer: (11) │ ├── cardinality: [1 - 1] │ ├── key: () - │ ├── fd: ()-->(10) + │ ├── fd: ()-->(12) │ └── tuple - │ └── variable: i:9 + │ └── variable: i:11 └── subquery └── project - ├── columns: column11:11 + ├── columns: column7:7 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(11) + ├── fd: ()-->(7) ├── values - │ ├── columns: f1:3 f1:6 + │ ├── columns: f1:2 f1:4 │ ├── cardinality: [1 - 1] │ ├── volatile │ ├── key: () - │ ├── fd: ()-->(3,6) + │ ├── fd: ()-->(2,4) │ └── tuple │ ├── udf: f1 │ │ ├── args │ │ │ └── const: 0 - │ │ ├── params: i:1 + │ │ ├── params: i:13 │ │ └── body │ │ └── values - │ │ ├── columns: i:2 - │ │ ├── outer: (1) + │ │ ├── columns: i:14 + │ │ ├── outer: (13) │ │ ├── cardinality: [1 - 1] │ │ ├── key: () - │ │ ├── fd: ()-->(2) + │ │ ├── fd: ()-->(14) │ │ └── tuple - │ │ └── variable: i:1 + │ │ └── variable: i:13 │ └── udf: f1 │ ├── args │ │ └── const: 1 - │ ├── params: i:4 + │ ├── params: i:15 │ └── body │ └── values - │ ├── columns: i:5 - │ ├── outer: (4) + │ ├── columns: i:16 + │ ├── outer: (15) │ ├── cardinality: [1 - 1] │ ├── key: () - │ ├── fd: ()-->(5) + │ ├── fd: ()-->(16) │ └── tuple - │ └── variable: i:4 + │ └── variable: i:15 └── projections - └── tuple [as=column11:11, outer=(3,6)] - ├── variable: f1:3 - └── variable: f1:6 + └── tuple [as=column7:7, outer=(2,4)] + ├── variable: f1:2 + └── variable: f1:4 # Cannot eliminate non-udf call subquery. @@ -2563,42 +2563,42 @@ CREATE FUNCTION arr() RETURNS INT[] LANGUAGE SQL AS $$ $$ ---- -# Should trigger for uncorrelated ArrayFlatten subqueries within a UDF +# Should trigger for uncorrelated ArrayFlatten subqueries within a UDF. norm expect=NormalizeArrayFlattenToAgg format=show-scalars SELECT arr() ---- values - ├── columns: arr:4 + ├── columns: arr:1 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(4) + ├── fd: ()-->(1) └── tuple └── udf: arr └── body └── values - ├── columns: array:3 + ├── columns: array:4 ├── cardinality: [1 - 1] ├── key: () - ├── fd: ()-->(3) + ├── fd: ()-->(4) └── tuple └── coalesce ├── subquery │ └── scalar-group-by - │ ├── columns: array_agg:2 + │ ├── columns: array_agg:3 │ ├── cardinality: [1 - 1] │ ├── key: () - │ ├── fd: ()-->(2) + │ ├── fd: ()-->(3) │ ├── values - │ │ ├── columns: column1:1!null + │ │ ├── columns: column1:2!null │ │ ├── cardinality: [2 - 2] │ │ ├── tuple │ │ │ └── const: 1 │ │ └── tuple │ │ └── const: 2 │ └── aggregations - │ └── array-agg [as=array_agg:2, outer=(1)] - │ └── variable: column1:1 + │ └── array-agg [as=array_agg:3, outer=(2)] + │ └── variable: column1:2 └── const: ARRAY[] exec-ddl diff --git a/pkg/sql/opt/norm/testdata/rules/udf b/pkg/sql/opt/norm/testdata/rules/udf index d50666338d36..9b30a0a2c0d1 100644 --- a/pkg/sql/opt/norm/testdata/rules/udf +++ b/pkg/sql/opt/norm/testdata/rules/udf @@ -7,19 +7,19 @@ norm format=show-scalars SELECT one() ---- values - ├── columns: one:2 + ├── columns: one:1 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(2) + ├── fd: ()-->(1) └── tuple └── udf: one └── body └── values - ├── columns: "?column?":1!null + ├── columns: "?column?":2!null ├── cardinality: [1 - 1] ├── key: () - ├── fd: ()-->(1) + ├── fd: ()-->(2) └── tuple └── const: 1 @@ -76,98 +76,98 @@ norm format=show-scalars SELECT f160475(33, ARRAY[44, 55]::INT[]) ---- values - ├── columns: f160475:13 + ├── columns: f160475:3 ├── cardinality: [1 - 1] ├── volatile ├── key: () - ├── fd: ()-->(13) + ├── fd: ()-->(3) └── tuple └── udf: f160475 ├── args │ ├── const: 33 │ └── const: ARRAY[44,55] - ├── params: x:1 y:2 + ├── params: x:4 y:5 └── body └── project - ├── columns: c:5 - ├── outer: (1,2) + ├── columns: c:8 + ├── outer: (4,5) ├── cardinality: [0 - 1] ├── immutable ├── key: () - ├── fd: ()-->(5) + ├── fd: ()-->(8) └── limit - ├── columns: a:3!null c:5 rowid:6!null bool_or:11!null - ├── outer: (1,2) + ├── columns: a:6!null c:8 rowid:9!null bool_or:14!null + ├── outer: (4,5) ├── cardinality: [0 - 1] ├── immutable ├── key: () - ├── fd: ()-->(3,5,6,11) + ├── fd: ()-->(6,8,9,14) ├── select - │ ├── columns: a:3!null c:5 rowid:6!null bool_or:11!null - │ ├── outer: (1,2) + │ ├── columns: a:6!null c:8 rowid:9!null bool_or:14!null + │ ├── outer: (4,5) │ ├── immutable - │ ├── key: (6) - │ ├── fd: ()-->(3,11), (6)-->(5) + │ ├── key: (9) + │ ├── fd: ()-->(6,14), (9)-->(8) │ ├── group-by (hash) - │ │ ├── columns: a:3 c:5 rowid:6!null bool_or:11!null - │ │ ├── grouping columns: rowid:6!null - │ │ ├── outer: (2) + │ │ ├── columns: a:6 c:8 rowid:9!null bool_or:14!null + │ │ ├── grouping columns: rowid:9!null + │ │ ├── outer: (5) │ │ ├── immutable - │ │ ├── key: (6) - │ │ ├── fd: (6)-->(3,5,11) + │ │ ├── key: (9) + │ │ ├── fd: (9)-->(6,8,14) │ │ ├── project - │ │ │ ├── columns: notnull:10!null a:3 c:5 rowid:6!null - │ │ │ ├── outer: (2) + │ │ │ ├── columns: notnull:13!null a:6 c:8 rowid:9!null + │ │ │ ├── outer: (5) │ │ │ ├── immutable - │ │ │ ├── fd: (6)-->(3,5) + │ │ │ ├── fd: (9)-->(6,8) │ │ │ ├── inner-join (cross) - │ │ │ │ ├── columns: a:3 b:4!null c:5 rowid:6!null unnest:9 - │ │ │ │ ├── outer: (2) + │ │ │ │ ├── columns: a:6 b:7!null c:8 rowid:9!null unnest:12 + │ │ │ │ ├── outer: (5) │ │ │ │ ├── immutable - │ │ │ │ ├── fd: (6)-->(3-5) + │ │ │ │ ├── fd: (9)-->(6-8) │ │ │ │ ├── select - │ │ │ │ │ ├── columns: a:3 b:4!null c:5 rowid:6!null - │ │ │ │ │ ├── key: (6) - │ │ │ │ │ ├── fd: (6)-->(3-5) + │ │ │ │ │ ├── columns: a:6 b:7!null c:8 rowid:9!null + │ │ │ │ │ ├── key: (9) + │ │ │ │ │ ├── fd: (9)-->(6-8) │ │ │ │ │ ├── scan t160475 - │ │ │ │ │ │ ├── columns: a:3 b:4 c:5 rowid:6!null - │ │ │ │ │ │ ├── key: (6) - │ │ │ │ │ │ └── fd: (6)-->(3-5) + │ │ │ │ │ │ ├── columns: a:6 b:7 c:8 rowid:9!null + │ │ │ │ │ │ ├── key: (9) + │ │ │ │ │ │ └── fd: (9)-->(6-8) │ │ │ │ │ └── filters - │ │ │ │ │ └── is-not [outer=(4), constraints=(/4: (/NULL - ]; tight)] - │ │ │ │ │ ├── variable: b:4 + │ │ │ │ │ └── is-not [outer=(7), constraints=(/7: (/NULL - ]; tight)] + │ │ │ │ │ ├── variable: b:7 │ │ │ │ │ └── null │ │ │ │ ├── project-set - │ │ │ │ │ ├── columns: unnest:9 - │ │ │ │ │ ├── outer: (2) + │ │ │ │ │ ├── columns: unnest:12 + │ │ │ │ │ ├── outer: (5) │ │ │ │ │ ├── immutable │ │ │ │ │ ├── values │ │ │ │ │ │ ├── cardinality: [1 - 1] │ │ │ │ │ │ ├── key: () │ │ │ │ │ │ └── tuple │ │ │ │ │ └── zip - │ │ │ │ │ └── function: unnest [outer=(2), immutable] - │ │ │ │ │ └── variable: y:2 + │ │ │ │ │ └── function: unnest [outer=(5), immutable] + │ │ │ │ │ └── variable: y:5 │ │ │ │ └── filters - │ │ │ │ └── is-not [outer=(4,9)] + │ │ │ │ └── is-not [outer=(7,12)] │ │ │ │ ├── eq - │ │ │ │ │ ├── variable: b:4 - │ │ │ │ │ └── variable: unnest:9 + │ │ │ │ │ ├── variable: b:7 + │ │ │ │ │ └── variable: unnest:12 │ │ │ │ └── false │ │ │ └── projections - │ │ │ └── is-not [as=notnull:10, outer=(9)] - │ │ │ ├── variable: unnest:9 + │ │ │ └── is-not [as=notnull:13, outer=(12)] + │ │ │ ├── variable: unnest:12 │ │ │ └── null │ │ └── aggregations - │ │ ├── bool-or [as=bool_or:11, outer=(10)] - │ │ │ └── variable: notnull:10 - │ │ ├── const-agg [as=a:3, outer=(3)] - │ │ │ └── variable: a:3 - │ │ └── const-agg [as=c:5, outer=(5)] - │ │ └── variable: c:5 + │ │ ├── bool-or [as=bool_or:14, outer=(13)] + │ │ │ └── variable: notnull:13 + │ │ ├── const-agg [as=a:6, outer=(6)] + │ │ │ └── variable: a:6 + │ │ └── const-agg [as=c:8, outer=(8)] + │ │ └── variable: c:8 │ └── filters - │ ├── eq [outer=(1,3), constraints=(/1: (/NULL - ]; /3: (/NULL - ]), fd=(1)==(3), (3)==(1)] - │ │ ├── variable: a:3 - │ │ └── variable: x:1 - │ └── variable: bool_or:11 [outer=(11), constraints=(/11: [/true - /true]; tight), fd=()-->(11)] + │ ├── eq [outer=(4,6), constraints=(/4: (/NULL - ]; /6: (/NULL - ]), fd=(4)==(6), (6)==(4)] + │ │ ├── variable: a:6 + │ │ └── variable: x:4 + │ └── variable: bool_or:14 [outer=(14), constraints=(/14: [/true - /true]; tight), fd=()-->(14)] └── const: 1 diff --git a/pkg/sql/opt/optbuilder/testdata/create_view b/pkg/sql/opt/optbuilder/testdata/create_view index cced47ab68e1..a3ccb19fefec 100644 --- a/pkg/sql/opt/optbuilder/testdata/create_view +++ b/pkg/sql/opt/optbuilder/testdata/create_view @@ -422,7 +422,7 @@ CREATE VIEW v27 AS SELECT foo(); ---- create-view t.public.v27 ├── SELECT public.foo() - ├── columns: foo:2 + ├── columns: foo:1 └── dependencies └── [FUNCTION 100059] @@ -431,7 +431,7 @@ CREATE VIEW v28 AS SELECT bar(1, 2); ---- create-view t.public.v28 ├── SELECT public.bar(1:::INT8, 2:::INT8) - ├── columns: bar:4 + ├── columns: bar:3 └── dependencies └── [FUNCTION 100060] @@ -450,7 +450,7 @@ CREATE VIEW v30 AS SELECT foo() AS x, bar(1, 2) AS y, baz() AS z ---- create-view t.public.v30 ├── SELECT public.foo() AS x, public.bar(1:::INT8, 2:::INT8) AS y, public.baz() AS z - ├── columns: x:2 y:6 z:24 + ├── columns: x:1 y:4 z:22 └── dependencies ├── [FUNCTION 100059] ├── [FUNCTION 100060] @@ -461,7 +461,7 @@ CREATE VIEW v31 AS SELECT bar(a, b) FROM ab ---- create-view t.public.v31 ├── SELECT public.bar(a, b) FROM t.public.ab - ├── columns: bar:8 + ├── columns: bar:7 └── dependencies ├── ab [columns: a b] └── [FUNCTION 100060] diff --git a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade index ad372be7859f..ff6dd827556c 100644 --- a/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade +++ b/pkg/sql/opt/optbuilder/testdata/fk-on-delete-cascade @@ -136,18 +136,18 @@ root └── cascade └── delete child ├── columns: - ├── fetch columns: c:12 child.p:13 + ├── fetch columns: c:11 child.p:12 └── semi-join (hash) - ├── columns: c:12!null child.p:13!null + ├── columns: c:11!null child.p:12!null ├── scan child - │ ├── columns: c:12!null child.p:13!null + │ ├── columns: c:11!null child.p:12!null │ └── flags: avoid-full-scan disabled not visible index feature ├── with-scan &1 - │ ├── columns: p:16!null + │ ├── columns: p:15!null │ └── mapping: - │ └── parent.p:4 => p:16 + │ └── parent.p:4 => p:15 └── filters - └── child.p:13 = p:16 + └── child.p:12 = p:15 # Delete with immutable UDF; fast path. build-post-queries diff --git a/pkg/sql/opt/optbuilder/testdata/orderby b/pkg/sql/opt/optbuilder/testdata/orderby index 74439f97aeef..d2fd44103254 100644 --- a/pkg/sql/opt/optbuilder/testdata/orderby +++ b/pkg/sql/opt/optbuilder/testdata/orderby @@ -1528,19 +1528,19 @@ build SELECT id FROM t_sub1 ORDER BY volatile_fn(id) NULLS LAST ---- sort - ├── columns: id:1 [hidden: nulls_ordering_column8:9!null column10:10] - ├── ordering: +9,+10 + ├── columns: id:1 [hidden: nulls_ordering_column7:8!null column9:9] + ├── ordering: +8,+9 └── project - ├── columns: nulls_ordering_column8:9!null column10:10 id:1 + ├── columns: nulls_ordering_column7:8!null column9:9 id:1 ├── project - │ ├── columns: column8:8 id:1 + │ ├── columns: column7:7 id:1 │ ├── scan t_sub1 │ │ └── columns: id:1 val:2 rowid:3!null crdb_internal_mvcc_timestamp:4 tableoid:5 │ └── projections - │ └── volatile_fn(id:1) [as=column8:8] + │ └── volatile_fn(id:1) [as=column7:7] └── projections - ├── column8:8 IS NULL [as=nulls_ordering_column8:9] - └── column8:8 [as=column10:10] + ├── column7:7 IS NULL [as=nulls_ordering_column7:8] + └── column7:7 [as=column9:9] # When ORDER BY expression matches a SELECT list expression, reuse the # existing projection column (SQL99 rule 3) instead of building a duplicate. diff --git a/pkg/sql/opt/optbuilder/testdata/procedure b/pkg/sql/opt/optbuilder/testdata/procedure index 5bac96a1131e..7560bf85f69d 100644 --- a/pkg/sql/opt/optbuilder/testdata/procedure +++ b/pkg/sql/opt/optbuilder/testdata/procedure @@ -111,31 +111,31 @@ call │ ├── const: 10 │ ├── const: 20 │ └── const: 30.3 - ├── params: a:1 b:2 c:3 + ├── params: a:4 b:5 c:6 └── body ├── insert abc │ ├── columns: │ ├── insert-mapping: - │ │ ├── column1:9 => abc.a:4 - │ │ ├── column2:10 => abc.b:5 - │ │ └── c_cast:12 => abc.c:6 + │ │ ├── column1:12 => abc.a:7 + │ │ ├── column2:13 => abc.b:8 + │ │ └── c_cast:15 => abc.c:9 │ └── project - │ ├── columns: c_cast:12 column1:9 column2:10 + │ ├── columns: c_cast:15 column1:12 column2:13 │ ├── values - │ │ ├── columns: column1:9 column2:10 column3:11 + │ │ ├── columns: column1:12 column2:13 column3:14 │ │ └── tuple │ │ ├── plus - │ │ │ ├── variable: a:1 + │ │ │ ├── variable: a:4 │ │ │ └── const: 10 - │ │ ├── variable: b:2 - │ │ └── variable: c:3 + │ │ ├── variable: b:5 + │ │ └── variable: c:6 │ └── projections - │ └── assignment-cast: INT8 [as=c_cast:12] - │ └── variable: column3:11 + │ └── assignment-cast: INT8 [as=c_cast:15] + │ └── variable: column3:14 └── limit - ├── columns: column1:13 + ├── columns: column1:16 ├── values - │ ├── columns: column1:13 + │ ├── columns: column1:16 │ └── tuple │ └── null └── const: 1 diff --git a/pkg/sql/opt/optbuilder/testdata/row_level_security b/pkg/sql/opt/optbuilder/testdata/row_level_security index b3c2e36c0f34..2335ea01eb23 100644 --- a/pkg/sql/opt/optbuilder/testdata/row_level_security +++ b/pkg/sql/opt/optbuilder/testdata/row_level_security @@ -987,53 +987,53 @@ build format=show-scalars SELECT insert_zero_into_t1(10) ---- project - ├── columns: insert_zero_into_t1:14 + ├── columns: insert_zero_into_t1:2 ├── values │ └── tuple └── projections - └── udf: insert_zero_into_t1 [as=insert_zero_into_t1:14] + └── udf: insert_zero_into_t1 [as=insert_zero_into_t1:2] ├── args │ └── const: 10 - ├── params: n:1 + ├── params: n:3 └── body └── limit - ├── columns: n:12 + ├── columns: n:14 ├── project - │ ├── columns: n:12 + │ ├── columns: n:14 │ ├── insert t1 - │ │ ├── columns: c1:2!null c2:3 c3:4 rowid:5!null + │ │ ├── columns: c1:4!null c2:5 c3:6 rowid:7!null │ │ ├── insert-mapping: - │ │ │ ├── column1:8 => c1:2 - │ │ │ ├── c2_default:9 => c2:3 - │ │ │ ├── c3_default:10 => c3:4 - │ │ │ └── rowid_default:11 => rowid:5 + │ │ │ ├── column1:10 => c1:4 + │ │ │ ├── c2_default:11 => c2:5 + │ │ │ ├── c3_default:12 => c3:6 + │ │ │ └── rowid_default:13 => rowid:7 │ │ ├── return-mapping: - │ │ │ ├── column1:8 => c1:2 - │ │ │ ├── c2_default:9 => c2:3 - │ │ │ ├── c3_default:10 => c3:4 - │ │ │ └── rowid_default:11 => rowid:5 - │ │ ├── check columns: rls:13 + │ │ │ ├── column1:10 => c1:4 + │ │ │ ├── c2_default:11 => c2:5 + │ │ │ ├── c3_default:12 => c3:6 + │ │ │ └── rowid_default:13 => rowid:7 + │ │ ├── check columns: rls:15 │ │ └── project - │ │ ├── columns: rls:13 column1:8!null c2_default:9 c3_default:10 rowid_default:11 + │ │ ├── columns: rls:15 column1:10!null c2_default:11 c3_default:12 rowid_default:13 │ │ ├── project - │ │ │ ├── columns: c2_default:9 c3_default:10 rowid_default:11 column1:8!null + │ │ │ ├── columns: c2_default:11 c3_default:12 rowid_default:13 column1:10!null │ │ │ ├── values - │ │ │ │ ├── columns: column1:8!null + │ │ │ │ ├── columns: column1:10!null │ │ │ │ └── tuple │ │ │ │ └── const: 0 │ │ │ └── projections - │ │ │ ├── cast: STRING [as=c2_default:9] + │ │ │ ├── cast: STRING [as=c2_default:11] │ │ │ │ └── null - │ │ │ ├── cast: DATE [as=c3_default:10] + │ │ │ ├── cast: DATE [as=c3_default:12] │ │ │ │ └── null - │ │ │ └── function: unique_rowid [as=rowid_default:11] + │ │ │ └── function: unique_rowid [as=rowid_default:13] │ │ └── projections - │ │ └── lt [as=rls:13] + │ │ └── lt [as=rls:15] │ │ ├── function: char_length - │ │ │ └── variable: c2_default:9 + │ │ │ └── variable: c2_default:11 │ │ └── const: 10 │ └── projections - │ └── variable: n:1 [as=n:12] + │ └── variable: n:3 [as=n:14] └── const: 1 exec-ddl diff --git a/pkg/sql/opt/optbuilder/testdata/udf b/pkg/sql/opt/optbuilder/testdata/udf index 0a789788e2a8..d76d35a94cda 100644 --- a/pkg/sql/opt/optbuilder/testdata/udf +++ b/pkg/sql/opt/optbuilder/testdata/udf @@ -30,40 +30,40 @@ build format=show-scalars SELECT one() ---- project - ├── columns: one:2 + ├── columns: one:1 ├── values │ └── tuple └── projections - └── udf: one [as=one:2] + └── udf: one [as=one:1] └── body └── limit - ├── columns: "?column?":1!null + ├── columns: "?column?":2!null ├── project - │ ├── columns: "?column?":1!null + │ ├── columns: "?column?":2!null │ ├── values │ │ └── tuple │ └── projections - │ └── const: 1 [as="?column?":1] + │ └── const: 1 [as="?column?":2] └── const: 1 build format=show-scalars SELECT *, one() FROM abc ---- project - ├── columns: a:1!null b:2 c:3 one:7 + ├── columns: a:1!null b:2 c:3 one:6 ├── scan abc │ └── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 └── projections - └── udf: one [as=one:7] + └── udf: one [as=one:6] └── body └── limit - ├── columns: "?column?":6!null + ├── columns: "?column?":7!null ├── project - │ ├── columns: "?column?":6!null + │ ├── columns: "?column?":7!null │ ├── values │ │ └── tuple │ └── projections - │ └── const: 1 [as="?column?":6] + │ └── const: 1 [as="?column?":7] └── const: 1 build format=show-scalars @@ -94,7 +94,7 @@ build format=show-scalars SELECT a + one(), b + two() FROM abc WHERE c = two() ---- project - ├── columns: "?column?":9 "?column?":12 + ├── columns: "?column?":6 "?column?":7 ├── select │ ├── columns: a:1!null b:2 c:3!null crdb_internal_mvcc_timestamp:4 tableoid:5 │ ├── scan abc @@ -105,52 +105,52 @@ project │ └── udf: two │ └── body │ ├── project - │ │ ├── columns: "?column?":6!null + │ │ ├── columns: "?column?":8!null │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── const: 1 [as="?column?":6] + │ │ └── const: 1 [as="?column?":8] │ └── limit - │ ├── columns: "?column?":7!null + │ ├── columns: "?column?":9!null │ ├── project - │ │ ├── columns: "?column?":7!null + │ │ ├── columns: "?column?":9!null │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── const: 2 [as="?column?":7] + │ │ └── const: 2 [as="?column?":9] │ └── const: 1 └── projections - ├── plus [as="?column?":9] + ├── plus [as="?column?":6] │ ├── variable: a:1 │ └── udf: one │ └── body │ └── limit - │ ├── columns: "?column?":8!null + │ ├── columns: "?column?":10!null │ ├── project - │ │ ├── columns: "?column?":8!null + │ │ ├── columns: "?column?":10!null │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── const: 1 [as="?column?":8] + │ │ └── const: 1 [as="?column?":10] │ └── const: 1 - └── plus [as="?column?":12] + └── plus [as="?column?":7] ├── variable: b:2 └── udf: two └── body ├── project - │ ├── columns: "?column?":10!null + │ ├── columns: "?column?":11!null │ ├── values │ │ └── tuple │ └── projections - │ └── const: 1 [as="?column?":10] + │ └── const: 1 [as="?column?":11] └── limit - ├── columns: "?column?":11!null + ├── columns: "?column?":12!null ├── project - │ ├── columns: "?column?":11!null + │ ├── columns: "?column?":12!null │ ├── values │ │ └── tuple │ └── projections - │ └── const: 2 [as="?column?":11] + │ └── const: 2 [as="?column?":12] └── const: 1 exec-ddl @@ -163,19 +163,19 @@ build format=show-scalars SELECT ordered() ---- project - ├── columns: ordered:6 + ├── columns: ordered:1 ├── values │ └── tuple └── projections - └── udf: ordered [as=ordered:6] + └── udf: ordered [as=ordered:1] └── body └── limit - ├── columns: a:1!null b:2 - ├── internal-ordering: -2 + ├── columns: a:2!null b:3 + ├── internal-ordering: -3 ├── project - │ ├── columns: a:1!null b:2 + │ ├── columns: a:2!null b:3 │ └── scan abc - │ └── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 + │ └── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 └── const: 1 @@ -193,94 +193,94 @@ build format=show-scalars SELECT add(1, 2) ---- project - ├── columns: add:4 + ├── columns: add:3 ├── values │ └── tuple └── projections - └── udf: add [as=add:4] + └── udf: add [as=add:3] ├── args │ ├── const: 1 │ └── const: 2 - ├── params: x:1 y:2 + ├── params: x:4 y:5 └── body └── limit - ├── columns: "?column?":3 + ├── columns: "?column?":6 ├── project - │ ├── columns: "?column?":3 + │ ├── columns: "?column?":6 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":3] - │ ├── variable: x:1 - │ └── variable: y:2 + │ └── plus [as="?column?":6] + │ ├── variable: x:4 + │ └── variable: y:5 └── const: 1 build format=show-scalars SELECT add(add(1, 2), 3) ---- project - ├── columns: add:7 + ├── columns: add:5 ├── values │ └── tuple └── projections - └── udf: add [as=add:7] + └── udf: add [as=add:5] ├── args │ ├── udf: add │ │ ├── args │ │ │ ├── const: 1 │ │ │ └── const: 2 - │ │ ├── params: x:1 y:2 + │ │ ├── params: x:6 y:7 │ │ └── body │ │ └── limit - │ │ ├── columns: "?column?":3 + │ │ ├── columns: "?column?":8 │ │ ├── project - │ │ │ ├── columns: "?column?":3 + │ │ │ ├── columns: "?column?":8 │ │ │ ├── values │ │ │ │ └── tuple │ │ │ └── projections - │ │ │ └── plus [as="?column?":3] - │ │ │ ├── variable: x:1 - │ │ │ └── variable: y:2 + │ │ │ └── plus [as="?column?":8] + │ │ │ ├── variable: x:6 + │ │ │ └── variable: y:7 │ │ └── const: 1 │ └── const: 3 - ├── params: x:4 y:5 + ├── params: x:9 y:10 └── body └── limit - ├── columns: "?column?":6 + ├── columns: "?column?":11 ├── project - │ ├── columns: "?column?":6 + │ ├── columns: "?column?":11 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":6] - │ ├── variable: x:4 - │ └── variable: y:5 + │ └── plus [as="?column?":11] + │ ├── variable: x:9 + │ └── variable: y:10 └── const: 1 build format=show-scalars SELECT add(a, b) FROM abc ---- project - ├── columns: add:9 + ├── columns: add:8 ├── scan abc │ └── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 └── projections - └── udf: add [as=add:9] + └── udf: add [as=add:8] ├── args │ ├── variable: a:1 │ └── variable: b:2 - ├── params: x:6 y:7 + ├── params: x:9 y:10 └── body └── limit - ├── columns: "?column?":8 + ├── columns: "?column?":11 ├── project - │ ├── columns: "?column?":8 + │ ├── columns: "?column?":11 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":8] - │ ├── variable: x:6 - │ └── variable: y:7 + │ └── plus [as="?column?":11] + │ ├── variable: x:9 + │ └── variable: y:10 └── const: 1 build format=show-scalars @@ -299,18 +299,18 @@ project ├── args │ ├── variable: b:2 │ └── variable: c:3 - ├── params: x:6 y:7 + ├── params: x:8 y:9 └── body └── limit - ├── columns: "?column?":8 + ├── columns: "?column?":10 ├── project - │ ├── columns: "?column?":8 + │ ├── columns: "?column?":10 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":8] - │ ├── variable: x:6 - │ └── variable: y:7 + │ └── plus [as="?column?":10] + │ ├── variable: x:8 + │ └── variable: y:9 └── const: 1 build format=show-scalars @@ -331,32 +331,32 @@ project │ │ ├── args │ │ │ ├── variable: b:2 │ │ │ └── variable: c:3 - │ │ ├── params: x:6 y:7 + │ │ ├── params: x:10 y:11 │ │ └── body │ │ └── limit - │ │ ├── columns: "?column?":8 + │ │ ├── columns: "?column?":12 │ │ ├── project - │ │ │ ├── columns: "?column?":8 + │ │ │ ├── columns: "?column?":12 │ │ │ ├── values │ │ │ │ └── tuple │ │ │ └── projections - │ │ │ └── plus [as="?column?":8] - │ │ │ ├── variable: x:6 - │ │ │ └── variable: y:7 + │ │ │ └── plus [as="?column?":12] + │ │ │ ├── variable: x:10 + │ │ │ └── variable: y:11 │ │ └── const: 1 │ └── const: 3 - ├── params: x:9 y:10 + ├── params: x:13 y:14 └── body └── limit - ├── columns: "?column?":11 + ├── columns: "?column?":15 ├── project - │ ├── columns: "?column?":11 + │ ├── columns: "?column?":15 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":11] - │ ├── variable: x:9 - │ └── variable: y:10 + │ └── plus [as="?column?":15] + │ ├── variable: x:13 + │ └── variable: y:14 └── const: 1 exec-ddl @@ -369,70 +369,70 @@ build format=show-scalars SELECT fetch_b(1) ---- project - ├── columns: fetch_b:7 + ├── columns: fetch_b:2 ├── values │ └── tuple └── projections - └── udf: fetch_b [as=fetch_b:7] + └── udf: fetch_b [as=fetch_b:2] ├── args │ └── const: 1 - ├── params: a_arg:1 + ├── params: a_arg:3 └── body └── limit - ├── columns: b:3 + ├── columns: b:5 ├── project - │ ├── columns: b:3 + │ ├── columns: b:5 │ └── select - │ ├── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ ├── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ ├── scan abc - │ │ └── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ └── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ └── filters │ └── eq - │ ├── variable: a:2 - │ └── variable: a_arg:1 + │ ├── variable: a:4 + │ └── variable: a_arg:3 └── const: 1 build format=show-scalars SELECT fetch_b(add(1, 2)) ---- project - ├── columns: fetch_b:10 + ├── columns: fetch_b:4 ├── values │ └── tuple └── projections - └── udf: fetch_b [as=fetch_b:10] + └── udf: fetch_b [as=fetch_b:4] ├── args │ └── udf: add │ ├── args │ │ ├── const: 1 │ │ └── const: 2 - │ ├── params: x:1 y:2 + │ ├── params: x:5 y:6 │ └── body │ └── limit - │ ├── columns: "?column?":3 + │ ├── columns: "?column?":7 │ ├── project - │ │ ├── columns: "?column?":3 + │ │ ├── columns: "?column?":7 │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── plus [as="?column?":3] - │ │ ├── variable: x:1 - │ │ └── variable: y:2 + │ │ └── plus [as="?column?":7] + │ │ ├── variable: x:5 + │ │ └── variable: y:6 │ └── const: 1 - ├── params: a_arg:4 + ├── params: a_arg:8 └── body └── limit - ├── columns: b:6 + ├── columns: b:10 ├── project - │ ├── columns: b:6 + │ ├── columns: b:10 │ └── select - │ ├── columns: a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 tableoid:9 + │ ├── columns: a:9!null b:10 c:11 crdb_internal_mvcc_timestamp:12 tableoid:13 │ ├── scan abc - │ │ └── columns: a:5!null b:6 c:7 crdb_internal_mvcc_timestamp:8 tableoid:9 + │ │ └── columns: a:9!null b:10 c:11 crdb_internal_mvcc_timestamp:12 tableoid:13 │ └── filters │ └── eq - │ ├── variable: a:5 - │ └── variable: a_arg:4 + │ ├── variable: a:9 + │ └── variable: a_arg:8 └── const: 1 build format=show-scalars @@ -450,20 +450,20 @@ project └── udf: fetch_b ├── args │ └── variable: a:1 - ├── params: a_arg:6 + ├── params: a_arg:7 └── body └── limit - ├── columns: b:8 + ├── columns: b:9 ├── project - │ ├── columns: b:8 + │ ├── columns: b:9 │ └── select - │ ├── columns: a:7!null b:8 c:9 crdb_internal_mvcc_timestamp:10 tableoid:11 + │ ├── columns: a:8!null b:9 c:10 crdb_internal_mvcc_timestamp:11 tableoid:12 │ ├── scan abc - │ │ └── columns: a:7!null b:8 c:9 crdb_internal_mvcc_timestamp:10 tableoid:11 + │ │ └── columns: a:8!null b:9 c:10 crdb_internal_mvcc_timestamp:11 tableoid:12 │ └── filters │ └── eq - │ ├── variable: a:7 - │ └── variable: a_arg:6 + │ ├── variable: a:8 + │ └── variable: a_arg:7 └── const: 1 exec-ddl @@ -477,27 +477,27 @@ build format=show-scalars SELECT shadowed_a(1) ---- project - ├── columns: shadowed_a:7 + ├── columns: shadowed_a:2 ├── values │ └── tuple └── projections - └── udf: shadowed_a [as=shadowed_a:7] + └── udf: shadowed_a [as=shadowed_a:2] ├── args │ └── const: 1 - ├── params: a:1 + ├── params: a:3 └── body └── limit - ├── columns: c:4 + ├── columns: c:6 ├── project - │ ├── columns: c:4 + │ ├── columns: c:6 │ └── select - │ ├── columns: abc.a:2!null b:3!null c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ ├── columns: abc.a:4!null b:5!null c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ ├── scan abc - │ │ └── columns: abc.a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ └── columns: abc.a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ └── filters │ └── eq - │ ├── variable: b:3 - │ └── variable: abc.a:2 + │ ├── variable: b:5 + │ └── variable: abc.a:4 └── const: 1 exec-ddl @@ -510,68 +510,68 @@ build format=show-scalars SELECT add_num_args(1, 2) ---- project - ├── columns: add_num_args:4 + ├── columns: add_num_args:3 ├── values │ └── tuple └── projections - └── udf: add_num_args [as=add_num_args:4] + └── udf: add_num_args [as=add_num_args:3] ├── args │ ├── const: 1 │ └── const: 2 - ├── params: x:1 y:2 + ├── params: x:4 y:5 └── body └── limit - ├── columns: "?column?":3 + ├── columns: "?column?":6 ├── project - │ ├── columns: "?column?":3 + │ ├── columns: "?column?":6 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":3] - │ ├── variable: x:1 - │ └── variable: y:2 + │ └── plus [as="?column?":6] + │ ├── variable: x:4 + │ └── variable: y:5 └── const: 1 build format=show-scalars SELECT add_num_args(add_num_args(1, 2), 3) ---- project - ├── columns: add_num_args:7 + ├── columns: add_num_args:5 ├── values │ └── tuple └── projections - └── udf: add_num_args [as=add_num_args:7] + └── udf: add_num_args [as=add_num_args:5] ├── args │ ├── udf: add_num_args │ │ ├── args │ │ │ ├── const: 1 │ │ │ └── const: 2 - │ │ ├── params: x:1 y:2 + │ │ ├── params: x:6 y:7 │ │ └── body │ │ └── limit - │ │ ├── columns: "?column?":3 + │ │ ├── columns: "?column?":8 │ │ ├── project - │ │ │ ├── columns: "?column?":3 + │ │ │ ├── columns: "?column?":8 │ │ │ ├── values │ │ │ │ └── tuple │ │ │ └── projections - │ │ │ └── plus [as="?column?":3] - │ │ │ ├── variable: x:1 - │ │ │ └── variable: y:2 + │ │ │ └── plus [as="?column?":8] + │ │ │ ├── variable: x:6 + │ │ │ └── variable: y:7 │ │ └── const: 1 │ └── const: 3 - ├── params: x:4 y:5 + ├── params: x:9 y:10 └── body └── limit - ├── columns: "?column?":6 + ├── columns: "?column?":11 ├── project - │ ├── columns: "?column?":6 + │ ├── columns: "?column?":11 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":6] - │ ├── variable: x:4 - │ └── variable: y:5 + │ └── plus [as="?column?":11] + │ ├── variable: x:9 + │ └── variable: y:10 └── const: 1 build format=show-scalars @@ -592,39 +592,39 @@ project │ │ ├── args │ │ │ ├── variable: b:2 │ │ │ └── variable: c:3 - │ │ ├── params: x:6 y:7 + │ │ ├── params: x:10 y:11 │ │ └── body │ │ └── limit - │ │ ├── columns: "?column?":8 + │ │ ├── columns: "?column?":12 │ │ ├── project - │ │ │ ├── columns: "?column?":8 + │ │ │ ├── columns: "?column?":12 │ │ │ ├── values │ │ │ │ └── tuple │ │ │ └── projections - │ │ │ └── plus [as="?column?":8] - │ │ │ ├── variable: x:6 - │ │ │ └── variable: y:7 + │ │ │ └── plus [as="?column?":12] + │ │ │ ├── variable: x:10 + │ │ │ └── variable: y:11 │ │ └── const: 1 │ └── const: 3 - ├── params: x:9 y:10 + ├── params: x:13 y:14 └── body └── limit - ├── columns: "?column?":11 + ├── columns: "?column?":15 ├── project - │ ├── columns: "?column?":11 + │ ├── columns: "?column?":15 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":11] - │ ├── variable: x:9 - │ └── variable: y:10 + │ └── plus [as="?column?":15] + │ ├── variable: x:13 + │ └── variable: y:14 └── const: 1 assign-placeholders-build query-args=(33) format=show-scalars SELECT add_num_args(1, $1) FROM abc WHERE a = add_num_args($1, 2) ---- project - ├── columns: add_num_args:12 + ├── columns: add_num_args:10 ├── select │ ├── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 │ ├── scan abc @@ -636,36 +636,36 @@ project │ ├── args │ │ ├── const: 33 │ │ └── const: 2 - │ ├── params: x:6 y:7 + │ ├── params: x:11 y:12 │ └── body │ └── limit - │ ├── columns: "?column?":8 + │ ├── columns: "?column?":13 │ ├── project - │ │ ├── columns: "?column?":8 + │ │ ├── columns: "?column?":13 │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── plus [as="?column?":8] - │ │ ├── variable: x:6 - │ │ └── variable: y:7 + │ │ └── plus [as="?column?":13] + │ │ ├── variable: x:11 + │ │ └── variable: y:12 │ └── const: 1 └── projections - └── udf: add_num_args [as=add_num_args:12] + └── udf: add_num_args [as=add_num_args:10] ├── args │ ├── const: 1 │ └── const: 33 - ├── params: x:9 y:10 + ├── params: x:14 y:15 └── body └── limit - ├── columns: "?column?":11 + ├── columns: "?column?":16 ├── project - │ ├── columns: "?column?":11 + │ ├── columns: "?column?":16 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":11] - │ ├── variable: x:9 - │ └── variable: y:10 + │ └── plus [as="?column?":16] + │ ├── variable: x:14 + │ └── variable: y:15 └── const: 1 # -------------------------------------------------- @@ -682,68 +682,68 @@ build format=show-scalars SELECT add_anon(1, 2) ---- project - ├── columns: add_anon:4 + ├── columns: add_anon:3 ├── values │ └── tuple └── projections - └── udf: add_anon [as=add_anon:4] + └── udf: add_anon [as=add_anon:3] ├── args │ ├── const: 1 │ └── const: 2 - ├── params: arg1:1 arg2:2 + ├── params: arg1:4 arg2:5 └── body └── limit - ├── columns: "?column?":3 + ├── columns: "?column?":6 ├── project - │ ├── columns: "?column?":3 + │ ├── columns: "?column?":6 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":3] - │ ├── variable: arg1:1 - │ └── variable: arg2:2 + │ └── plus [as="?column?":6] + │ ├── variable: arg1:4 + │ └── variable: arg2:5 └── const: 1 build format=show-scalars SELECT add_anon(add_anon(1, 2), 3) ---- project - ├── columns: add_anon:7 + ├── columns: add_anon:5 ├── values │ └── tuple └── projections - └── udf: add_anon [as=add_anon:7] + └── udf: add_anon [as=add_anon:5] ├── args │ ├── udf: add_anon │ │ ├── args │ │ │ ├── const: 1 │ │ │ └── const: 2 - │ │ ├── params: arg1:1 arg2:2 + │ │ ├── params: arg1:6 arg2:7 │ │ └── body │ │ └── limit - │ │ ├── columns: "?column?":3 + │ │ ├── columns: "?column?":8 │ │ ├── project - │ │ │ ├── columns: "?column?":3 + │ │ │ ├── columns: "?column?":8 │ │ │ ├── values │ │ │ │ └── tuple │ │ │ └── projections - │ │ │ └── plus [as="?column?":3] - │ │ │ ├── variable: arg1:1 - │ │ │ └── variable: arg2:2 + │ │ │ └── plus [as="?column?":8] + │ │ │ ├── variable: arg1:6 + │ │ │ └── variable: arg2:7 │ │ └── const: 1 │ └── const: 3 - ├── params: arg1:4 arg2:5 + ├── params: arg1:9 arg2:10 └── body └── limit - ├── columns: "?column?":6 + ├── columns: "?column?":11 ├── project - │ ├── columns: "?column?":6 + │ ├── columns: "?column?":11 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":6] - │ ├── variable: arg1:4 - │ └── variable: arg2:5 + │ └── plus [as="?column?":11] + │ ├── variable: arg1:9 + │ └── variable: arg2:10 └── const: 1 build format=show-scalars @@ -764,39 +764,39 @@ project │ │ ├── args │ │ │ ├── variable: b:2 │ │ │ └── variable: c:3 - │ │ ├── params: arg1:6 arg2:7 + │ │ ├── params: arg1:10 arg2:11 │ │ └── body │ │ └── limit - │ │ ├── columns: "?column?":8 + │ │ ├── columns: "?column?":12 │ │ ├── project - │ │ │ ├── columns: "?column?":8 + │ │ │ ├── columns: "?column?":12 │ │ │ ├── values │ │ │ │ └── tuple │ │ │ └── projections - │ │ │ └── plus [as="?column?":8] - │ │ │ ├── variable: arg1:6 - │ │ │ └── variable: arg2:7 + │ │ │ └── plus [as="?column?":12] + │ │ │ ├── variable: arg1:10 + │ │ │ └── variable: arg2:11 │ │ └── const: 1 │ └── const: 3 - ├── params: arg1:9 arg2:10 + ├── params: arg1:13 arg2:14 └── body └── limit - ├── columns: "?column?":11 + ├── columns: "?column?":15 ├── project - │ ├── columns: "?column?":11 + │ ├── columns: "?column?":15 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":11] - │ ├── variable: arg1:9 - │ └── variable: arg2:10 + │ └── plus [as="?column?":15] + │ ├── variable: arg1:13 + │ └── variable: arg2:14 └── const: 1 assign-placeholders-build query-args=(33) format=show-scalars SELECT add_anon(1, $1) FROM abc WHERE a = add_anon($1, 2) ---- project - ├── columns: add_anon:12 + ├── columns: add_anon:10 ├── select │ ├── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 │ ├── scan abc @@ -808,36 +808,36 @@ project │ ├── args │ │ ├── const: 33 │ │ └── const: 2 - │ ├── params: arg1:6 arg2:7 + │ ├── params: arg1:11 arg2:12 │ └── body │ └── limit - │ ├── columns: "?column?":8 + │ ├── columns: "?column?":13 │ ├── project - │ │ ├── columns: "?column?":8 + │ │ ├── columns: "?column?":13 │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── plus [as="?column?":8] - │ │ ├── variable: arg1:6 - │ │ └── variable: arg2:7 + │ │ └── plus [as="?column?":13] + │ │ ├── variable: arg1:11 + │ │ └── variable: arg2:12 │ └── const: 1 └── projections - └── udf: add_anon [as=add_anon:12] + └── udf: add_anon [as=add_anon:10] ├── args │ ├── const: 1 │ └── const: 33 - ├── params: arg1:9 arg2:10 + ├── params: arg1:14 arg2:15 └── body └── limit - ├── columns: "?column?":11 + ├── columns: "?column?":16 ├── project - │ ├── columns: "?column?":11 + │ ├── columns: "?column?":16 │ ├── values │ │ └── tuple │ └── projections - │ └── plus [as="?column?":11] - │ ├── variable: arg1:9 - │ └── variable: arg2:10 + │ └── plus [as="?column?":16] + │ ├── variable: arg1:14 + │ └── variable: arg2:15 └── const: 1 @@ -855,41 +855,41 @@ build format=show-scalars SELECT get_abc(3) ---- project - ├── columns: get_abc:9 + ├── columns: get_abc:2 ├── values │ └── tuple └── projections - └── udf: get_abc [as=get_abc:9] + └── udf: get_abc [as=get_abc:2] ├── args │ └── const: 3 - ├── params: i:1 + ├── params: i:3 └── body └── project - ├── columns: column8:8 b:3 + ├── columns: column10:10 b:5 ├── project - │ ├── columns: column7:7 b:3 + │ ├── columns: column9:9 b:5 │ ├── limit - │ │ ├── columns: a:2!null b:3 c:4!null - │ │ ├── internal-ordering: -3 + │ │ ├── columns: a:4!null b:5 c:6!null + │ │ ├── internal-ordering: -5 │ │ ├── project - │ │ │ ├── columns: a:2!null b:3 c:4!null + │ │ │ ├── columns: a:4!null b:5 c:6!null │ │ │ └── select - │ │ │ ├── columns: a:2!null b:3 c:4!null crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ │ ├── columns: a:4!null b:5 c:6!null crdb_internal_mvcc_timestamp:7 tableoid:8 │ │ │ ├── scan abc - │ │ │ │ └── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ │ │ └── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ │ │ └── filters │ │ │ └── gt - │ │ │ ├── variable: c:4 - │ │ │ └── variable: i:1 + │ │ │ ├── variable: c:6 + │ │ │ └── variable: i:3 │ │ └── const: 1 │ └── projections - │ └── tuple [as=column7:7] - │ ├── variable: a:2 - │ ├── variable: b:3 - │ └── variable: c:4 + │ └── tuple [as=column9:9] + │ ├── variable: a:4 + │ ├── variable: b:5 + │ └── variable: c:6 └── projections - └── assignment-cast: RECORD [as=column8:8] - └── variable: column7:7 + └── assignment-cast: RECORD [as=column10:10] + └── variable: column9:9 exec-ddl CREATE FUNCTION get_abc_star(i INT) RETURNS abc LANGUAGE SQL AS $$ @@ -901,41 +901,41 @@ build format=show-scalars SELECT get_abc_star(3) ---- project - ├── columns: get_abc_star:9 + ├── columns: get_abc_star:2 ├── values │ └── tuple └── projections - └── udf: get_abc_star [as=get_abc_star:9] + └── udf: get_abc_star [as=get_abc_star:2] ├── args │ └── const: 3 - ├── params: i:1 + ├── params: i:3 └── body └── project - ├── columns: column8:8 b:3 + ├── columns: column10:10 b:5 ├── project - │ ├── columns: column7:7 b:3 + │ ├── columns: column9:9 b:5 │ ├── limit - │ │ ├── columns: a:2!null b:3 c:4!null - │ │ ├── internal-ordering: -3 + │ │ ├── columns: a:4!null b:5 c:6!null + │ │ ├── internal-ordering: -5 │ │ ├── project - │ │ │ ├── columns: a:2!null b:3 c:4!null + │ │ │ ├── columns: a:4!null b:5 c:6!null │ │ │ └── select - │ │ │ ├── columns: a:2!null b:3 c:4!null crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ │ ├── columns: a:4!null b:5 c:6!null crdb_internal_mvcc_timestamp:7 tableoid:8 │ │ │ ├── scan abc - │ │ │ │ └── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ │ │ └── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ │ │ └── filters │ │ │ └── gt - │ │ │ ├── variable: c:4 - │ │ │ └── variable: i:1 + │ │ │ ├── variable: c:6 + │ │ │ └── variable: i:3 │ │ └── const: 1 │ └── projections - │ └── tuple [as=column7:7] - │ ├── variable: a:2 - │ ├── variable: b:3 - │ └── variable: c:4 + │ └── tuple [as=column9:9] + │ ├── variable: a:4 + │ ├── variable: b:5 + │ └── variable: c:6 └── projections - └── assignment-cast: RECORD [as=column8:8] - └── variable: column7:7 + └── assignment-cast: RECORD [as=column10:10] + └── variable: column9:9 exec-ddl CREATE FUNCTION abc_b(i abc) RETURNS INT LANGUAGE SQL AS $$ @@ -947,28 +947,28 @@ build format=show-scalars SELECT abc_b((1,2,3)::abc) ---- project - ├── columns: abc_b:3 + ├── columns: abc_b:2 ├── values │ └── tuple └── projections - └── udf: abc_b [as=abc_b:3] + └── udf: abc_b [as=abc_b:2] ├── args │ └── cast: RECORD │ └── tuple │ ├── const: 1 │ ├── const: 2 │ └── const: 3 - ├── params: i:1 + ├── params: i:3 └── body └── limit - ├── columns: b:2 + ├── columns: b:4 ├── project - │ ├── columns: b:2 + │ ├── columns: b:4 │ ├── values │ │ └── tuple │ └── projections - │ └── column-access: 1 [as=b:2] - │ └── variable: i:1 + │ └── column-access: 1 [as=b:4] + │ └── variable: i:3 └── const: 1 @@ -984,29 +984,29 @@ build format=show-scalars SELECT itof(123) ---- project - ├── columns: itof:4 + ├── columns: itof:2 ├── values │ └── tuple └── projections - └── udf: itof [as=itof:4] + └── udf: itof [as=itof:2] ├── args │ └── const: 123 - ├── params: i:1 + ├── params: i:3 └── body └── project - ├── columns: column3:3 + ├── columns: column5:5 ├── limit - │ ├── columns: i:2 + │ ├── columns: i:4 │ ├── project - │ │ ├── columns: i:2 + │ │ ├── columns: i:4 │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── variable: i:1 [as=i:2] + │ │ └── variable: i:3 [as=i:4] │ └── const: 1 └── projections - └── assignment-cast: FLOAT8 [as=column3:3] - └── variable: i:2 + └── assignment-cast: FLOAT8 [as=column5:5] + └── variable: i:4 exec-ddl CREATE FUNCTION stoc(s STRING) RETURNS CHAR LANGUAGE SQL AS 'SELECT s' @@ -1016,29 +1016,29 @@ build format=show-scalars SELECT stoc('a') ---- project - ├── columns: stoc:4 + ├── columns: stoc:2 ├── values │ └── tuple └── projections - └── udf: stoc [as=stoc:4] + └── udf: stoc [as=stoc:2] ├── args │ └── const: 'a' - ├── params: s:1 + ├── params: s:3 └── body └── project - ├── columns: column3:3 + ├── columns: column5:5 ├── limit - │ ├── columns: s:2 + │ ├── columns: s:4 │ ├── project - │ │ ├── columns: s:2 + │ │ ├── columns: s:4 │ │ ├── values │ │ │ └── tuple │ │ └── projections - │ │ └── variable: s:1 [as=s:2] + │ │ └── variable: s:3 [as=s:4] │ └── const: 1 └── projections - └── assignment-cast: CHAR [as=column3:3] - └── variable: s:2 + └── assignment-cast: CHAR [as=column5:5] + └── variable: s:4 # -------------------------------------------------- @@ -1053,25 +1053,25 @@ build format=show-scalars SELECT retvoid(1) ---- project - ├── columns: retvoid:4 + ├── columns: retvoid:2 ├── values │ └── tuple └── projections - └── udf: retvoid [as=retvoid:4] + └── udf: retvoid [as=retvoid:2] ├── args │ └── const: 1 - ├── params: i:1 + ├── params: i:3 └── body ├── project - │ ├── columns: i:2 + │ ├── columns: i:4 │ ├── values │ │ └── tuple │ └── projections - │ └── variable: i:1 [as=i:2] + │ └── variable: i:3 [as=i:4] └── limit - ├── columns: column1:3 + ├── columns: column1:5 ├── values - │ ├── columns: column1:3 + │ ├── columns: column1:5 │ └── tuple │ └── null └── const: 1 @@ -1089,33 +1089,33 @@ build format=show-scalars SELECT strict_fn(1, 'foo', false) ---- project - ├── columns: strict_fn:5 + ├── columns: strict_fn:4 ├── values │ └── tuple └── projections - └── udf: strict_fn [as=strict_fn:5] + └── udf: strict_fn [as=strict_fn:4] ├── strict ├── args │ ├── const: 1 │ ├── const: 'foo' │ └── false - ├── params: i:1 t:2 b:3 + ├── params: i:5 t:6 b:7 └── body └── limit - ├── columns: i:4 + ├── columns: i:8 ├── project - │ ├── columns: i:4 + │ ├── columns: i:8 │ ├── values │ │ └── tuple │ └── projections - │ └── variable: i:1 [as=i:4] + │ └── variable: i:5 [as=i:8] └── const: 1 build format=show-scalars SELECT strict_fn(a, b::TEXT, false) FROM abc WHERE strict_fn(a+1+2, b::TEXT, false) = 10 ---- project - ├── columns: strict_fn:14 + ├── columns: strict_fn:12 ├── select │ ├── columns: a:1!null abc.b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 │ ├── scan abc @@ -1133,36 +1133,36 @@ project │ │ │ ├── cast: STRING │ │ │ │ └── variable: abc.b:2 │ │ │ └── false - │ │ ├── params: i:6 t:7 b:8 + │ │ ├── params: i:13 t:14 b:15 │ │ └── body │ │ └── limit - │ │ ├── columns: i:9 + │ │ ├── columns: i:16 │ │ ├── project - │ │ │ ├── columns: i:9 + │ │ │ ├── columns: i:16 │ │ │ ├── values │ │ │ │ └── tuple │ │ │ └── projections - │ │ │ └── variable: i:6 [as=i:9] + │ │ │ └── variable: i:13 [as=i:16] │ │ └── const: 1 │ └── const: 10 └── projections - └── udf: strict_fn [as=strict_fn:14] + └── udf: strict_fn [as=strict_fn:12] ├── strict ├── args │ ├── variable: a:1 │ ├── cast: STRING │ │ └── variable: abc.b:2 │ └── false - ├── params: i:10 t:11 b:12 + ├── params: i:17 t:18 b:19 └── body └── limit - ├── columns: i:13 + ├── columns: i:20 ├── project - │ ├── columns: i:13 + │ ├── columns: i:20 │ ├── values │ │ └── tuple │ └── projections - │ └── variable: i:10 [as=i:13] + │ └── variable: i:17 [as=i:20] └── const: 1 exec-ddl @@ -1181,11 +1181,11 @@ norm format=show-scalars SELECT f101516((SELECT t1.i FROM t101516 t2 JOIN t101516 t3 ON t2.i = t3.i)) FROM t101516 t1 ---- project - ├── columns: f101516:16 + ├── columns: f101516:15 ├── scan t101516 [as=t1] │ └── columns: t1.i:1 └── projections - └── udf: f101516 [as=f101516:16] + └── udf: f101516 [as=f101516:15] ├── strict ├── args │ └── subquery @@ -1205,10 +1205,10 @@ project │ │ └── variable: t3.i:9 │ └── projections │ └── variable: t1.i:1 [as=i:13] - ├── params: x:14 + ├── params: x:16 └── body └── values - ├── columns: "?column?":15!null + ├── columns: "?column?":17!null └── tuple └── const: 1 @@ -1303,18 +1303,18 @@ build format=show-scalars SELECT fn_star() ---- project - ├── columns: fn_star:5 + ├── columns: fn_star:1 ├── values │ └── tuple └── projections - └── udf: fn_star [as=fn_star:5] + └── udf: fn_star [as=fn_star:1] └── body └── limit - ├── columns: a:1 + ├── columns: a:2 ├── project - │ ├── columns: a:1 + │ ├── columns: a:2 │ └── scan tstar - │ └── columns: a:1 rowid:2!null crdb_internal_mvcc_timestamp:3 tableoid:4 + │ └── columns: a:2 rowid:3!null crdb_internal_mvcc_timestamp:4 tableoid:5 └── const: 1 exec-ddl @@ -1382,75 +1382,75 @@ build format=show-scalars SELECT set_fn(1) ---- project-set - ├── columns: set_fn:7 + ├── columns: set_fn:2 ├── values │ └── tuple └── zip └── udf: set_fn ├── args │ └── const: 1 - ├── params: i:1 + ├── params: i:3 └── body └── project - ├── columns: b:3 + ├── columns: b:5 └── select - ├── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + ├── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 ├── scan abc - │ └── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ └── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 └── filters └── gt - ├── variable: a:2 - └── variable: i:1 + ├── variable: a:4 + └── variable: i:3 build format=show-scalars SELECT * FROM set_fn(1) ---- project-set - ├── columns: set_fn:7 + ├── columns: set_fn:2 ├── values │ └── tuple └── zip └── udf: set_fn ├── args │ └── const: 1 - ├── params: i:1 + ├── params: i:3 └── body └── project - ├── columns: b:3 + ├── columns: b:5 └── select - ├── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + ├── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 ├── scan abc - │ └── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ └── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 └── filters └── gt - ├── variable: a:2 - └── variable: i:1 + ├── variable: a:4 + └── variable: i:3 build format=show-scalars SELECT a, set_fn(a) FROM abc ---- project - ├── columns: a:1!null set_fn:12 + ├── columns: a:1!null set_fn:7 └── project-set - ├── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 set_fn:12 + ├── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 set_fn:7 ├── scan abc │ └── columns: a:1!null b:2 c:3 crdb_internal_mvcc_timestamp:4 tableoid:5 └── zip └── udf: set_fn ├── args │ └── variable: a:1 - ├── params: i:6 + ├── params: i:8 └── body └── project - ├── columns: b:8 + ├── columns: b:10 └── select - ├── columns: a:7!null b:8 c:9 crdb_internal_mvcc_timestamp:10 tableoid:11 + ├── columns: a:9!null b:10 c:11 crdb_internal_mvcc_timestamp:12 tableoid:13 ├── scan abc - │ └── columns: a:7!null b:8 c:9 crdb_internal_mvcc_timestamp:10 tableoid:11 + │ └── columns: a:9!null b:10 c:11 crdb_internal_mvcc_timestamp:12 tableoid:13 └── filters └── gt - ├── variable: a:7 - └── variable: i:6 + ├── variable: a:9 + └── variable: i:8 # -------------------------------------------------- @@ -1465,61 +1465,61 @@ build format=show-scalars SELECT any_fn(10) ---- project - ├── columns: any_fn:11 + ├── columns: any_fn:2 ├── values │ └── tuple └── projections - └── udf: any_fn [as=any_fn:11] + └── udf: any_fn [as=any_fn:2] ├── args │ └── const: 10 - ├── params: i:1 + ├── params: i:3 └── body └── limit - ├── columns: "?column?":10 + ├── columns: "?column?":12 ├── project - │ ├── columns: "?column?":10 + │ ├── columns: "?column?":12 │ ├── values │ │ └── tuple │ └── projections - │ └── subquery [as="?column?":10] + │ └── subquery [as="?column?":12] │ └── project - │ ├── columns: case:9 + │ ├── columns: case:11 │ ├── scalar-group-by - │ │ ├── columns: bool_or:8 + │ │ ├── columns: bool_or:10 │ │ ├── project - │ │ │ ├── columns: notnull:7!null + │ │ │ ├── columns: notnull:9!null │ │ │ ├── select - │ │ │ │ ├── columns: a:2!null + │ │ │ │ ├── columns: a:4!null │ │ │ │ ├── project - │ │ │ │ │ ├── columns: a:2!null + │ │ │ │ │ ├── columns: a:4!null │ │ │ │ │ └── scan abc - │ │ │ │ │ └── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ │ │ │ └── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ │ │ │ └── filters │ │ │ │ └── is-not │ │ │ │ ├── eq - │ │ │ │ │ ├── variable: i:1 - │ │ │ │ │ └── variable: a:2 + │ │ │ │ │ ├── variable: i:3 + │ │ │ │ │ └── variable: a:4 │ │ │ │ └── false │ │ │ └── projections - │ │ │ └── is-not [as=notnull:7] - │ │ │ ├── variable: a:2 + │ │ │ └── is-not [as=notnull:9] + │ │ │ ├── variable: a:4 │ │ │ └── null │ │ └── aggregations - │ │ └── bool-or [as=bool_or:8] - │ │ └── variable: notnull:7 + │ │ └── bool-or [as=bool_or:10] + │ │ └── variable: notnull:9 │ └── projections - │ └── case [as=case:9] + │ └── case [as=case:11] │ ├── true │ ├── when │ │ ├── and - │ │ │ ├── variable: bool_or:8 + │ │ │ ├── variable: bool_or:10 │ │ │ └── is-not - │ │ │ ├── variable: i:1 + │ │ │ ├── variable: i:3 │ │ │ └── null │ │ └── true │ ├── when │ │ ├── is - │ │ │ ├── variable: bool_or:8 + │ │ │ ├── variable: bool_or:10 │ │ │ └── null │ │ └── false │ └── null @@ -1533,68 +1533,68 @@ build format=show-scalars SELECT all_fn(10) ---- project - ├── columns: all_fn:11 + ├── columns: all_fn:2 ├── values │ └── tuple └── projections - └── udf: all_fn [as=all_fn:11] + └── udf: all_fn [as=all_fn:2] ├── args │ └── const: 10 - ├── params: i:1 + ├── params: i:3 └── body └── limit - ├── columns: "?column?":10 + ├── columns: "?column?":12 ├── project - │ ├── columns: "?column?":10 + │ ├── columns: "?column?":12 │ ├── values │ │ └── tuple │ └── projections - │ └── not [as="?column?":10] + │ └── not [as="?column?":12] │ └── subquery │ └── project - │ ├── columns: case:9 + │ ├── columns: case:11 │ ├── scalar-group-by - │ │ ├── columns: bool_or:8 + │ │ ├── columns: bool_or:10 │ │ ├── project - │ │ │ ├── columns: notnull:7!null + │ │ │ ├── columns: notnull:9!null │ │ │ ├── select - │ │ │ │ ├── columns: a:2!null + │ │ │ │ ├── columns: a:4!null │ │ │ │ ├── project - │ │ │ │ │ ├── columns: a:2!null + │ │ │ │ │ ├── columns: a:4!null │ │ │ │ │ └── select - │ │ │ │ │ ├── columns: a:2!null b:3!null c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ │ │ │ ├── columns: a:4!null b:5!null c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ │ │ │ │ ├── scan abc - │ │ │ │ │ │ └── columns: a:2!null b:3 c:4 crdb_internal_mvcc_timestamp:5 tableoid:6 + │ │ │ │ │ │ └── columns: a:4!null b:5 c:6 crdb_internal_mvcc_timestamp:7 tableoid:8 │ │ │ │ │ └── filters │ │ │ │ │ └── gt - │ │ │ │ │ ├── variable: b:3 - │ │ │ │ │ └── variable: i:1 + │ │ │ │ │ ├── variable: b:5 + │ │ │ │ │ └── variable: i:3 │ │ │ │ └── filters │ │ │ │ └── is-not │ │ │ │ ├── ge - │ │ │ │ │ ├── variable: i:1 - │ │ │ │ │ └── variable: a:2 + │ │ │ │ │ ├── variable: i:3 + │ │ │ │ │ └── variable: a:4 │ │ │ │ └── false │ │ │ └── projections - │ │ │ └── is-not [as=notnull:7] - │ │ │ ├── variable: a:2 + │ │ │ └── is-not [as=notnull:9] + │ │ │ ├── variable: a:4 │ │ │ └── null │ │ └── aggregations - │ │ └── bool-or [as=bool_or:8] - │ │ └── variable: notnull:7 + │ │ └── bool-or [as=bool_or:10] + │ │ └── variable: notnull:9 │ └── projections - │ └── case [as=case:9] + │ └── case [as=case:11] │ ├── true │ ├── when │ │ ├── and - │ │ │ ├── variable: bool_or:8 + │ │ │ ├── variable: bool_or:10 │ │ │ └── is-not - │ │ │ ├── variable: i:1 + │ │ │ ├── variable: i:3 │ │ │ └── null │ │ └── true │ ├── when │ │ ├── is - │ │ │ ├── variable: bool_or:8 + │ │ │ ├── variable: bool_or:10 │ │ │ └── null │ │ └── false │ └── null @@ -1608,70 +1608,70 @@ build format=show-scalars SELECT any_fn_tuple(10, 20) ---- project - ├── columns: any_fn_tuple:13 + ├── columns: any_fn_tuple:3 ├── values │ └── tuple └── projections - └── udf: any_fn_tuple [as=any_fn_tuple:13] + └── udf: any_fn_tuple [as=any_fn_tuple:3] ├── args │ ├── const: 10 │ └── const: 20 - ├── params: i:1 j:2 + ├── params: i:4 j:5 └── body └── limit - ├── columns: "?column?":12 + ├── columns: "?column?":15 ├── project - │ ├── columns: "?column?":12 + │ ├── columns: "?column?":15 │ ├── values │ │ └── tuple │ └── projections - │ └── subquery [as="?column?":12] + │ └── subquery [as="?column?":15] │ └── project - │ ├── columns: case:11 + │ ├── columns: case:14 │ ├── scalar-group-by - │ │ ├── columns: bool_or:10 + │ │ ├── columns: bool_or:13 │ │ ├── project - │ │ │ ├── columns: notnull:9!null + │ │ │ ├── columns: notnull:12!null │ │ │ ├── select - │ │ │ │ ├── columns: column8:8 + │ │ │ │ ├── columns: column11:11 │ │ │ │ ├── project - │ │ │ │ │ ├── columns: column8:8 + │ │ │ │ │ ├── columns: column11:11 │ │ │ │ │ ├── project - │ │ │ │ │ │ ├── columns: a:3!null b:4 + │ │ │ │ │ │ ├── columns: a:6!null b:7 │ │ │ │ │ │ └── scan abc - │ │ │ │ │ │ └── columns: a:3!null b:4 c:5 crdb_internal_mvcc_timestamp:6 tableoid:7 + │ │ │ │ │ │ └── columns: a:6!null b:7 c:8 crdb_internal_mvcc_timestamp:9 tableoid:10 │ │ │ │ │ └── projections - │ │ │ │ │ └── tuple [as=column8:8] - │ │ │ │ │ ├── variable: a:3 - │ │ │ │ │ └── variable: b:4 + │ │ │ │ │ └── tuple [as=column11:11] + │ │ │ │ │ ├── variable: a:6 + │ │ │ │ │ └── variable: b:7 │ │ │ │ └── filters │ │ │ │ └── is-not │ │ │ │ ├── eq │ │ │ │ │ ├── tuple - │ │ │ │ │ │ ├── variable: i:1 - │ │ │ │ │ │ └── variable: j:2 - │ │ │ │ │ └── variable: column8:8 + │ │ │ │ │ │ ├── variable: i:4 + │ │ │ │ │ │ └── variable: j:5 + │ │ │ │ │ └── variable: column11:11 │ │ │ │ └── false │ │ │ └── projections - │ │ │ └── is-tuple-not-null [as=notnull:9] - │ │ │ └── variable: column8:8 + │ │ │ └── is-tuple-not-null [as=notnull:12] + │ │ │ └── variable: column11:11 │ │ └── aggregations - │ │ └── bool-or [as=bool_or:10] - │ │ └── variable: notnull:9 + │ │ └── bool-or [as=bool_or:13] + │ │ └── variable: notnull:12 │ └── projections - │ └── case [as=case:11] + │ └── case [as=case:14] │ ├── true │ ├── when │ │ ├── and - │ │ │ ├── variable: bool_or:10 + │ │ │ ├── variable: bool_or:13 │ │ │ └── is-tuple-not-null │ │ │ └── tuple - │ │ │ ├── variable: i:1 - │ │ │ └── variable: j:2 + │ │ │ ├── variable: i:4 + │ │ │ └── variable: j:5 │ │ └── true │ ├── when │ │ ├── is - │ │ │ ├── variable: bool_or:10 + │ │ │ ├── variable: bool_or:13 │ │ │ └── null │ │ └── false │ └── null @@ -1703,7 +1703,22 @@ project build UPDATE abc SET c = 3 WHERE ups(1, 2, 3) ---- -error (0A000): multiple mutations of the same table "abc" are not supported unless they all use INSERT without ON CONFLICT; this is to prevent data corruption, see documentation of sql.multiple_modifications_of_table.enabled +update abc + ├── columns: + ├── fetch columns: a:6 b:7 c:8 + ├── update-mapping: + │ └── c_new:14 => c:3 + └── project + ├── columns: c_new:14!null a:6!null b:7 c:8 crdb_internal_mvcc_timestamp:9 tableoid:10 + ├── select + │ ├── columns: a:6!null b:7 c:8 crdb_internal_mvcc_timestamp:9 tableoid:10 + │ ├── scan abc + │ │ ├── columns: a:6!null b:7 c:8 crdb_internal_mvcc_timestamp:9 tableoid:10 + │ │ └── flags: avoid-full-scan + │ └── filters + │ └── ups(1, 2, 3) + └── projections + └── 3 [as=c_new:14] # Multiple mutations to abc by the root statement and the UDF is allowed with # enable_multiple_modifications_of_table enabled. @@ -1714,9 +1729,9 @@ update abc ├── columns: ├── fetch columns: a:6 b:7 c:8 ├── update-mapping: - │ └── c_new:23 => c:3 + │ └── c_new:14 => c:3 └── project - ├── columns: c_new:23!null a:6!null b:7 c:8 crdb_internal_mvcc_timestamp:9 tableoid:10 + ├── columns: c_new:14!null a:6!null b:7 c:8 crdb_internal_mvcc_timestamp:9 tableoid:10 ├── select │ ├── columns: a:6!null b:7 c:8 crdb_internal_mvcc_timestamp:9 tableoid:10 │ ├── scan abc @@ -1725,7 +1740,7 @@ update abc │ └── filters │ └── ups(1, 2, 3) └── projections - └── 3 [as=c_new:23] + └── 3 [as=c_new:14] # Multiple mutations to abc by the root statement and the UDF. build @@ -1736,7 +1751,7 @@ WITH x AS ( ) SELECT * FROM x ---- -error (0A000): multiple mutations of the same table "abc" are not supported unless they all use INSERT without ON CONFLICT; this is to prevent data corruption, see documentation of sql.multiple_modifications_of_table.enabled +error (42703): column "j" does not exist exec-ddl CREATE FUNCTION upd_ups(x INT, y INT, z INT) RETURNS VOID LANGUAGE SQL AS $$ @@ -1748,7 +1763,12 @@ $$ build SELECT upd_ups(1, 2, 3) ---- -error (0A000): multiple mutations of the same table "abc" are not supported unless they all use INSERT without ON CONFLICT; this is to prevent data corruption, see documentation of sql.multiple_modifications_of_table.enabled +project + ├── columns: upd_ups:4 + ├── values + │ └── () + └── projections + └── upd_ups(1, 2, 3) [as=upd_ups:4] # Multiple mutations to abc by the two UDFs invoked is allowed with # enabled_multiple_modifications_of_table enabled. @@ -1756,11 +1776,11 @@ build set=enable_multiple_modifications_of_table=true SELECT upd_ups(1, 2, 3) ---- project - ├── columns: upd_ups:28 + ├── columns: upd_ups:4 ├── values │ └── () └── projections - └── upd_ups(1, 2, 3) [as=upd_ups:28] + └── upd_ups(1, 2, 3) [as=upd_ups:4] exec-ddl CREATE FUNCTION ups2(a1 INT, b1 INT, c1 INT, a2 INT, b2 INT, c2 INT) RETURNS BOOL LANGUAGE SQL AS $$ @@ -1930,24 +1950,24 @@ norm format=show-scalars SELECT f160473() ---- values - ├── columns: f160473:8 + ├── columns: f160473:1 └── tuple └── udf: f160473 └── body └── project - ├── columns: c:3 + ├── columns: c:4 └── limit - ├── columns: a:1!null b:2!null c:3 + ├── columns: a:2!null b:3!null c:4 ├── select - │ ├── columns: a:1!null b:2!null c:3 + │ ├── columns: a:2!null b:3!null c:4 │ ├── scan t160473 - │ │ └── columns: a:1 b:2 c:3 + │ │ └── columns: a:2 b:3 c:4 │ └── filters │ ├── eq - │ │ ├── variable: a:1 + │ │ ├── variable: a:2 │ │ └── const: 33 │ └── in - │ ├── variable: b:2 + │ ├── variable: b:3 │ └── tuple │ ├── const: 44 │ └── const: 55 @@ -1963,9 +1983,9 @@ norm format=show-scalars SELECT f160473() ---- values - ├── columns: f160473:8 + ├── columns: f160473:1 └── tuple └── udf: f160473 └── body └── values - └── columns: c:3!null + └── columns: c:4!null diff --git a/pkg/sql/opt/optbuilder/testdata/view b/pkg/sql/opt/optbuilder/testdata/view index 0d9603863ed1..9f90ea378200 100644 --- a/pkg/sql/opt/optbuilder/testdata/view +++ b/pkg/sql/opt/optbuilder/testdata/view @@ -161,11 +161,11 @@ build SELECT * FROM v1 ---- project - ├── columns: foo:2 + ├── columns: foo:1 ├── values │ └── () └── projections - └── foo() [as=foo:2] + └── foo() [as=foo:1] exec-ddl CREATE VIEW v2 AS SELECT bar(1, 2); @@ -175,11 +175,11 @@ build SELECT * FROM v2 ---- project - ├── columns: bar:4 + ├── columns: bar:3 ├── values │ └── () └── projections - └── bar(1, 2) [as=bar:4] + └── bar(1, 2) [as=bar:3] exec-ddl CREATE VIEW v3 AS SELECT baz(); @@ -203,13 +203,13 @@ build SELECT * FROM v4 ---- project - ├── columns: x:2 y:6 z:17 + ├── columns: x:1 y:4 z:15 ├── values │ └── () └── projections - ├── foo() [as=x:2] - ├── bar(1, 2) [as=y:6] - └── baz() [as=z:17] + ├── foo() [as=x:1] + ├── bar(1, 2) [as=y:4] + └── baz() [as=z:15] exec-ddl CREATE VIEW v5 AS SELECT bar(k, i) FROM a @@ -219,11 +219,11 @@ build SELECT * FROM v5 ---- project - ├── columns: bar:11 + ├── columns: bar:10 ├── scan a │ └── columns: k:1!null i:2 f:3 s:4 j:5 crdb_internal_mvcc_timestamp:6 tableoid:7 └── projections - └── bar(k:1, i:2) [as=bar:11] + └── bar(k:1, i:2) [as=bar:10] exec-ddl CREATE VIEW v6 AS SELECT * FROM a INNER JOIN cd ON bar(k, i) = c @@ -251,8 +251,8 @@ build SELECT * FROM v7 ---- project - ├── columns: f_view:9 + ├── columns: f_view:1 ├── values │ └── () └── projections - └── f_view() [as=f_view:9] + └── f_view() [as=f_view:1] diff --git a/pkg/sql/opt/testutils/opttester/opt_tester.go b/pkg/sql/opt/testutils/opttester/opt_tester.go index 92af5c99860c..ac2526665074 100644 --- a/pkg/sql/opt/testutils/opttester/opt_tester.go +++ b/pkg/sql/opt/testutils/opttester/opt_tester.go @@ -655,7 +655,7 @@ func (ot *OptTester) runCommandInternal(tb testing.TB, d *datadriven.TestData) s return fmt.Sprintf("error: %s\n", text) } ot.postProcess(tb, d, e) - return ot.FormatExpr(e) + return ot.FormatAndCheck(tb, d, e) case "norm": e, err := ot.OptNorm() @@ -672,7 +672,7 @@ func (ot *OptTester) runCommandInternal(tb testing.TB, d *datadriven.TestData) s return fmt.Sprintf("error: %s\n", text) } ot.postProcess(tb, d, e) - return ot.FormatExpr(e) + return ot.FormatAndCheck(tb, d, e) case "opt": e, err := ot.Optimize() @@ -681,9 +681,9 @@ func (ot *OptTester) runCommandInternal(tb testing.TB, d *datadriven.TestData) s } ot.postProcess(tb, d, e) if ot.Flags.RoundFloatsInStringsSigFigs > 0 { - return floatcmp.RoundFloatsInString(ot.FormatExpr(e), ot.Flags.RoundFloatsInStringsSigFigs) + return floatcmp.RoundFloatsInString(ot.FormatAndCheck(tb, d, e), ot.Flags.RoundFloatsInStringsSigFigs) } - return ot.FormatExpr(e) + return ot.FormatAndCheck(tb, d, e) case "assign-placeholders-build", "assign-placeholders-norm", "assign-placeholders-opt": explore := d.Cmd == "assign-placeholders-opt" @@ -693,7 +693,7 @@ func (ot *OptTester) runCommandInternal(tb testing.TB, d *datadriven.TestData) s d.Fatalf(tb, "%+v", err) } ot.postProcess(tb, d, e) - return ot.FormatExpr(e) + return ot.FormatAndCheck(tb, d, e) case "placeholder-fast-path": e, ok, err := ot.PlaceholderFastPath() @@ -775,7 +775,7 @@ func (ot *OptTester) runCommandInternal(tb testing.TB, d *datadriven.TestData) s d.Fatalf(tb, "%+v", err) } ot.postProcess(tb, d, e) - return ot.FormatExpr(e) + return ot.FormatAndCheck(tb, d, e) case "exprnorm": e, err := ot.ExprNorm() @@ -783,7 +783,7 @@ func (ot *OptTester) runCommandInternal(tb testing.TB, d *datadriven.TestData) s return fmt.Sprintf("error: %s\n", err) } ot.postProcess(tb, d, e) - return ot.FormatExpr(e) + return ot.FormatAndCheck(tb, d, e) case "expropt": e, err := ot.ExprOpt() @@ -794,7 +794,7 @@ func (ot *OptTester) runCommandInternal(tb testing.TB, d *datadriven.TestData) s return fmt.Sprintf("error: %s\n", err) } ot.postProcess(tb, d, e) - return ot.FormatExpr(e) + return ot.FormatAndCheck(tb, d, e) case "stats-quality": result, err := ot.StatsQuality(tb, d) @@ -859,12 +859,41 @@ func (ot *OptTester) GetMemo() *memo.Memo { return ot.f.Memo() } -// FormatExpr is a convenience wrapper for memo.FormatExpr. +// FormatExpr is a convenience wrapper for memo.FormatExpr. It sets the +// BuildDeferredBody callback so that deferred UDF bodies are shown inline. func (ot *OptTester) FormatExpr(e opt.Expr) string { mem := ot.f.Memo() - return memo.FormatExpr( - ot.ctx, e, ot.Flags.ExprFormat, false /* redactableValues */, mem, ot.catalog, + f := memo.MakeExprFmtCtx( + ot.ctx, ot.Flags.ExprFormat, false /* redactableValues */, mem, ot.catalog, ) + f.BuildDeferredBody = ot.buildDeferredBodyForTest + f.FormatExpr(e) + return f.Buffer.String() +} + +// buildDeferredBodyForTest builds a deferred UDF body using the outer memo's +// factory so that column IDs are globally unique (no overlap with the outer +// query). This follows the same pattern used for post-query (cascade/trigger) +// test formatting in OptTester.PostQueries, which also passes the outer +// factory to Build() for the same reason (see "We use the same memo to build +// the cascade" in the buildPostQueries closure). Note that production code +// uses a fresh memo for both post-queries (see post_queries.go) and deferred +// routine bodies (see scalar.go) since the outer memo may be cached or shared. +// +// Rule tracking and optimization settings are inherited from the outer +// factory (configured in makeOptimizer), so expect=/expect-not= directives +// work correctly. +func (ot *OptTester) buildDeferredBodyForTest( + def *memo.UDFDefinition, +) ([]memo.RelExpr, opt.ColList, *memo.Memo, error) { + body, _, params, err := def.BodyBuilder.Build( + ot.ctx, &ot.semaCtx, &ot.evalCtx, ot.catalog, ot.f, + ) + if err != nil { + return nil, nil, nil, err + } + // Return the outer memo — body columns are allocated in the same namespace. + return body, params, ot.f.Memo(), nil } func formatRuleSet(r RuleSet) string { @@ -902,7 +931,15 @@ func (ot *OptTester) postProcess(tb testing.TB, d *datadriven.TestData, e opt.Ex memo.RequestColStat(ot.ctx, &ot.evalCtx, mem, rel, cols) } } +} + +// FormatAndCheck formats the expression and then checks expected rules. Rules +// are checked after formatting because FormatExpr may build deferred UDF +// bodies, which can trigger additional normalization rules. +func (ot *OptTester) FormatAndCheck(tb testing.TB, d *datadriven.TestData, e opt.Expr) string { + result := ot.FormatExpr(e) ot.checkExpectedRules(tb, d) + return result } // Fills in lazily-derived properties (for display). @@ -2032,7 +2069,7 @@ func (ot *OptTester) StatsQuality(tb testing.TB, d *datadriven.TestData) (string buf := bytes.Buffer{} ot.postProcess(tb, d, expr) - buf.WriteString(ot.FormatExpr(expr)) + buf.WriteString(ot.FormatAndCheck(tb, d, expr)) // Split the previous test output into blocks containing the stats for each // expression. The first element will contain the expression tree itself, so From 3e8a250d82997c00afc670150e153f50d799b944 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Sat, 16 May 2026 20:19:37 -0400 Subject: [PATCH 07/12] sql/opt: move udf_mutations logprops test to logic test The udf_mutations subtest in logprops/udf relied on the opt test catalog providing a correct CanMutate value on function overloads. With deferred UDF body building, the test catalog can no longer derive this from the body RelExprs, and the test catalog doesn't persist CanMutate (it bypasses the optbuilder's buildCreateFunction). Move the test to a logic test where the production pipeline (DSC with CanMutate on the descriptor) handles it correctly. Epic: none Release note: None Co-Authored-By: Claude Opus 4.6 --- .../testdata/logic_test/udf_calling_udf | 193 +++++ pkg/sql/opt/memo/testdata/logprops/udf | 664 ------------------ 2 files changed, 193 insertions(+), 664 deletions(-) diff --git a/pkg/sql/logictest/testdata/logic_test/udf_calling_udf b/pkg/sql/logictest/testdata/logic_test/udf_calling_udf index 2386e1f01b02..d33baa3616a3 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf_calling_udf +++ b/pkg/sql/logictest/testdata/logic_test/udf_calling_udf @@ -261,3 +261,196 @@ CREATE PROCEDURE public.p131354() $$ subtest end + +# Regression test: the "mutations" property should propagate from a nested UDF +# that contains a mutation to the outer expression. This was previously tested +# in pkg/sql/opt/memo/testdata/logprops/udf but moved here because the opt test +# catalog does not persist CanMutate on function overloads, causing incorrect +# results with deferred UDF body building. + +subtest udf_mutations + +statement ok +CREATE TABLE ab (a INT PRIMARY KEY, b INT); + +statement ok +CREATE FUNCTION fn_mutating() RETURNS VOID LANGUAGE SQL AS 'INSERT INTO ab VALUES (1, 2)'; + +statement ok +CREATE FUNCTION fn_calls_mutating(a INT) RETURNS INT LANGUAGE SQL AS $$ + SELECT fn_mutating(); + SELECT a; +$$; + +# The "mutations" property should propagate through a direct UDF call. +query T +EXPLAIN (OPT, VERBOSE) SELECT fn_calls_mutating(a) FROM ab +---- +project + ├── columns: fn_calls_mutating:8 + ├── volatile, mutations + ├── stats: [rows=1000] + ├── cost: 1108.64 + ├── cost-flags: unbounded-cardinality + ├── distribution: test + ├── prune: (8) + ├── scan ab + │ ├── columns: ab.a:1 + │ ├── stats: [rows=1000] + │ ├── cost: 1088.62 + │ ├── cost-flags: unbounded-cardinality + │ ├── key: (1) + │ ├── distribution: test + │ └── prune: (1) + └── projections + └── fn_calls_mutating(ab.a:1) [as=fn_calls_mutating:8, outer=(1), volatile, udf] + +# The "mutations" property should propagate through CTEs that call mutating +# functions. +query T +EXPLAIN (OPT, VERBOSE) WITH cte AS (SELECT fn_mutating()) SELECT * FROM cte +---- +with &1 (cte) + ├── columns: fn_mutating:2 + ├── cardinality: [1 - 1] + ├── volatile, mutations + ├── stats: [rows=1] + ├── cost: 0.04 + ├── key: () + ├── fd: ()-->(2) + ├── distribution: test + ├── prune: (2) + ├── values + │ ├── columns: fn_mutating:1 + │ ├── cardinality: [1 - 1] + │ ├── volatile, mutations + │ ├── stats: [rows=1] + │ ├── cost: 0.02 + │ ├── key: () + │ ├── fd: ()-->(1) + │ ├── distribution: test + │ └── (fn_mutating(),) + └── with-scan &1 (cte) + ├── columns: fn_mutating:2 + ├── mapping: + │ └── fn_mutating:1 => fn_mutating:2 + ├── cardinality: [1 - 1] + ├── stats: [rows=1] + ├── cost: 0.01 + ├── key: () + ├── fd: ()-->(2) + ├── distribution: test + └── prune: (2) + +# The "mutations" property should propagate when a UDF body contains a mutation +# inside a CTE. +statement ok +CREATE FUNCTION fn_mutating_via_cte() RETURNS INT LANGUAGE SQL AS $$ + WITH cte AS (INSERT INTO ab VALUES (1, 2) RETURNING a) SELECT a FROM cte +$$; + +query T +EXPLAIN (OPT, VERBOSE) SELECT fn_mutating_via_cte() FROM ab +---- +project + ├── columns: fn_mutating_via_cte:7 + ├── volatile, mutations + ├── stats: [rows=1000] + ├── cost: 1088.44 + ├── cost-flags: unbounded-cardinality + ├── distribution: test + ├── prune: (7) + ├── scan ab + │ ├── stats: [rows=1000] + │ ├── cost: 1068.42 + │ ├── cost-flags: unbounded-cardinality + │ └── distribution: test + └── projections + └── fn_mutating_via_cte() [as=fn_mutating_via_cte:7, volatile, udf] + +# The "mutations" property should propagate through a nested function that calls +# a mutating-via-CTE function inside its own CTE. +statement ok +CREATE FUNCTION fn_calls_mutating_via_cte() RETURNS INT LANGUAGE SQL AS $$ + WITH cte AS (SELECT fn_mutating_via_cte()) SELECT * FROM cte +$$; + +query T +EXPLAIN (OPT, VERBOSE) SELECT fn_calls_mutating_via_cte() FROM ab +---- +project + ├── columns: fn_calls_mutating_via_cte:7 + ├── volatile, mutations + ├── stats: [rows=1000] + ├── cost: 1088.44 + ├── cost-flags: unbounded-cardinality + ├── distribution: test + ├── prune: (7) + ├── scan ab + │ ├── stats: [rows=1000] + │ ├── cost: 1068.42 + │ ├── cost-flags: unbounded-cardinality + │ └── distribution: test + └── projections + └── fn_calls_mutating_via_cte() [as=fn_calls_mutating_via_cte:7, volatile, udf] + +# The "mutations" property should propagate from a PL/pgSQL routine that +# contains a mutation. +statement ok +CREATE FUNCTION fn_plpgsql_mutating() RETURNS VOID LANGUAGE PLpgSQL AS $$ + BEGIN + INSERT INTO ab VALUES (1, 2); + END +$$; + +query T +EXPLAIN (OPT, VERBOSE) SELECT fn_plpgsql_mutating() FROM ab +---- +project + ├── columns: fn_plpgsql_mutating:17 + ├── volatile, mutations + ├── stats: [rows=1000] + ├── cost: 1088.44 + ├── cost-flags: unbounded-cardinality + ├── distribution: test + ├── prune: (17) + ├── scan ab + │ ├── stats: [rows=1000] + │ ├── cost: 1068.42 + │ ├── cost-flags: unbounded-cardinality + │ └── distribution: test + └── projections + └── fn_plpgsql_mutating() [as=fn_plpgsql_mutating:17, volatile, udf] + +# The "mutations" property should propagate from a PL/pgSQL routine that +# transitively calls a mutating function. +statement ok +CREATE FUNCTION fn_plpgsql_calls_mutating() RETURNS INT LANGUAGE PLpgSQL AS $$ + DECLARE + v VOID; + BEGIN + SELECT fn_mutating() INTO v; + RETURN 1; + END +$$; + +query T +EXPLAIN (OPT, VERBOSE) SELECT fn_plpgsql_calls_mutating() FROM ab +---- +project + ├── columns: fn_plpgsql_calls_mutating:15 + ├── volatile, mutations + ├── stats: [rows=1000] + ├── cost: 1088.44 + ├── cost-flags: unbounded-cardinality + ├── distribution: test + ├── prune: (15) + ├── scan ab + │ ├── stats: [rows=1000] + │ ├── cost: 1068.42 + │ ├── cost-flags: unbounded-cardinality + │ └── distribution: test + └── projections + └── fn_plpgsql_calls_mutating() [as=fn_plpgsql_calls_mutating:15, volatile, udf] + +subtest end diff --git a/pkg/sql/opt/memo/testdata/logprops/udf b/pkg/sql/opt/memo/testdata/logprops/udf index d66102fb5a05..8fe00af8dd0d 100644 --- a/pkg/sql/opt/memo/testdata/logprops/udf +++ b/pkg/sql/opt/memo/testdata/logprops/udf @@ -275,667 +275,3 @@ select │ └── const: 1 [as="?column?":6, type=int] └── const: 1 [type=int] -subtest udf_mutations - -# Regression test: the "mutations" property should propagate from a nested UDF -# that contains a mutation to the outer expression. -exec-ddl -CREATE FUNCTION fn_mutating() RETURNS VOID LANGUAGE SQL AS 'INSERT INTO ab VALUES (1, 2)' ----- - -exec-ddl -CREATE FUNCTION fn_calls_mutating(a INT) RETURNS INT LANGUAGE SQL AS $$ - SELECT fn_mutating(); - SELECT a; -$$ ----- - -build -SELECT fn_calls_mutating(a) FROM ab ----- -project - ├── columns: fn_calls_mutating:15(int) - ├── volatile, mutations - ├── prune: (15) - ├── scan ab - │ ├── columns: ab.a:1(int!null) b:2(int) crdb_internal_mvcc_timestamp:3(decimal) tableoid:4(oid) - │ ├── key: (1) - │ ├── fd: (1)-->(2-4) - │ ├── prune: (1-4) - │ └── interesting orderings: (+1) - └── projections - └── udf: fn_calls_mutating [as=fn_calls_mutating:15, type=int, outer=(1), volatile, udf] - ├── args - │ └── variable: ab.a:1 [type=int] - ├── params: a:5(int) - └── body - ├── project - │ ├── columns: fn_mutating:13(void) - │ ├── cardinality: [1 - 1] - │ ├── volatile, mutations - │ ├── key: () - │ ├── fd: ()-->(13) - │ ├── values - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ └── tuple [type=tuple] - │ └── projections - │ └── udf: fn_mutating [as=fn_mutating:13, type=void, volatile, udf] - │ └── body - │ ├── insert ab - │ │ ├── columns: - │ │ ├── insert-mapping: - │ │ │ ├── column1:10 => ab.a:6 - │ │ │ └── column2:11 => b:7 - │ │ ├── cardinality: [0 - 0] - │ │ ├── volatile, mutations - │ │ └── values - │ │ ├── columns: column1:10(int!null) column2:11(int!null) - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ ├── fd: ()-->(10,11) - │ │ └── tuple [type=tuple{int, int}] - │ │ ├── const: 1 [type=int] - │ │ └── const: 2 [type=int] - │ └── limit - │ ├── columns: column1:12(unknown) - │ ├── cardinality: [1 - 1] - │ ├── key: () - │ ├── fd: ()-->(12) - │ ├── values - │ │ ├── columns: column1:12(unknown) - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ ├── fd: ()-->(12) - │ │ └── tuple [type=tuple{unknown}] - │ │ └── null [type=unknown] - │ └── const: 1 [type=int] - └── limit - ├── columns: a:14(int) - ├── outer: (5) - ├── cardinality: [1 - 1] - ├── key: () - ├── fd: ()-->(14) - ├── project - │ ├── columns: a:14(int) - │ ├── outer: (5) - │ ├── cardinality: [1 - 1] - │ ├── key: () - │ ├── fd: ()-->(14) - │ ├── values - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ └── tuple [type=tuple] - │ └── projections - │ └── variable: a:5 [as=a:14, type=int, outer=(5)] - └── const: 1 [type=int] - -# The "mutations" property should propagate through CTEs that call mutating -# functions. -build -WITH cte AS (SELECT fn_mutating()) SELECT * FROM cte ----- -with &1 (cte) - ├── columns: fn_mutating:9(void) - ├── cardinality: [1 - 1] - ├── volatile, mutations - ├── key: () - ├── fd: ()-->(9) - ├── prune: (9) - ├── project - │ ├── columns: fn_mutating:8(void) - │ ├── cardinality: [1 - 1] - │ ├── volatile, mutations - │ ├── key: () - │ ├── fd: ()-->(8) - │ ├── prune: (8) - │ ├── values - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ └── tuple [type=tuple] - │ └── projections - │ └── udf: fn_mutating [as=fn_mutating:8, type=void, volatile, udf] - │ └── body - │ ├── insert ab - │ │ ├── columns: - │ │ ├── insert-mapping: - │ │ │ ├── column1:5 => a:1 - │ │ │ └── column2:6 => b:2 - │ │ ├── cardinality: [0 - 0] - │ │ ├── volatile, mutations - │ │ └── values - │ │ ├── columns: column1:5(int!null) column2:6(int!null) - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ ├── fd: ()-->(5,6) - │ │ └── tuple [type=tuple{int, int}] - │ │ ├── const: 1 [type=int] - │ │ └── const: 2 [type=int] - │ └── limit - │ ├── columns: column1:7(unknown) - │ ├── cardinality: [1 - 1] - │ ├── key: () - │ ├── fd: ()-->(7) - │ ├── values - │ │ ├── columns: column1:7(unknown) - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ ├── fd: ()-->(7) - │ │ └── tuple [type=tuple{unknown}] - │ │ └── null [type=unknown] - │ └── const: 1 [type=int] - └── with-scan &1 (cte) - ├── columns: fn_mutating:9(void) - ├── mapping: - │ └── fn_mutating:8(void) => fn_mutating:9(void) - ├── cardinality: [1 - 1] - ├── key: () - ├── fd: ()-->(9) - └── prune: (9) - -build -WITH cte AS (SELECT fn_calls_mutating(a) FROM ab) SELECT * FROM cte ----- -with &1 (cte) - ├── columns: fn_calls_mutating:16(int) - ├── volatile, mutations - ├── prune: (16) - ├── project - │ ├── columns: fn_calls_mutating:15(int) - │ ├── volatile, mutations - │ ├── prune: (15) - │ ├── scan ab - │ │ ├── columns: ab.a:1(int!null) b:2(int) crdb_internal_mvcc_timestamp:3(decimal) tableoid:4(oid) - │ │ ├── key: (1) - │ │ ├── fd: (1)-->(2-4) - │ │ ├── prune: (1-4) - │ │ └── interesting orderings: (+1) - │ └── projections - │ └── udf: fn_calls_mutating [as=fn_calls_mutating:15, type=int, outer=(1), volatile, udf] - │ ├── args - │ │ └── variable: ab.a:1 [type=int] - │ ├── params: a:5(int) - │ └── body - │ ├── project - │ │ ├── columns: fn_mutating:13(void) - │ │ ├── cardinality: [1 - 1] - │ │ ├── volatile, mutations - │ │ ├── key: () - │ │ ├── fd: ()-->(13) - │ │ ├── values - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── key: () - │ │ │ └── tuple [type=tuple] - │ │ └── projections - │ │ └── udf: fn_mutating [as=fn_mutating:13, type=void, volatile, udf] - │ │ └── body - │ │ ├── insert ab - │ │ │ ├── columns: - │ │ │ ├── insert-mapping: - │ │ │ │ ├── column1:10 => ab.a:6 - │ │ │ │ └── column2:11 => b:7 - │ │ │ ├── cardinality: [0 - 0] - │ │ │ ├── volatile, mutations - │ │ │ └── values - │ │ │ ├── columns: column1:10(int!null) column2:11(int!null) - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── key: () - │ │ │ ├── fd: ()-->(10,11) - │ │ │ └── tuple [type=tuple{int, int}] - │ │ │ ├── const: 1 [type=int] - │ │ │ └── const: 2 [type=int] - │ │ └── limit - │ │ ├── columns: column1:12(unknown) - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ ├── fd: ()-->(12) - │ │ ├── values - │ │ │ ├── columns: column1:12(unknown) - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── key: () - │ │ │ ├── fd: ()-->(12) - │ │ │ └── tuple [type=tuple{unknown}] - │ │ │ └── null [type=unknown] - │ │ └── const: 1 [type=int] - │ └── limit - │ ├── columns: a:14(int) - │ ├── outer: (5) - │ ├── cardinality: [1 - 1] - │ ├── key: () - │ ├── fd: ()-->(14) - │ ├── project - │ │ ├── columns: a:14(int) - │ │ ├── outer: (5) - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ ├── fd: ()-->(14) - │ │ ├── values - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── key: () - │ │ │ └── tuple [type=tuple] - │ │ └── projections - │ │ └── variable: a:5 [as=a:14, type=int, outer=(5)] - │ └── const: 1 [type=int] - └── with-scan &1 (cte) - ├── columns: fn_calls_mutating:16(int) - ├── mapping: - │ └── fn_calls_mutating:15(int) => fn_calls_mutating:16(int) - └── prune: (16) - -# The "mutations" property should propagate when a UDF body contains a mutation -# inside a CTE. -exec-ddl -CREATE FUNCTION fn_mutating_via_cte() RETURNS INT LANGUAGE SQL AS $$ - WITH cte AS (INSERT INTO ab VALUES (1, 2) RETURNING a) SELECT a FROM cte -$$ ----- - -build -SELECT fn_mutating_via_cte() FROM ab ----- -project - ├── columns: fn_mutating_via_cte:12(int) - ├── volatile, mutations - ├── prune: (12) - ├── scan ab - │ ├── columns: ab.a:1(int!null) b:2(int) crdb_internal_mvcc_timestamp:3(decimal) tableoid:4(oid) - │ ├── key: (1) - │ ├── fd: (1)-->(2-4) - │ ├── prune: (1-4) - │ └── interesting orderings: (+1) - └── projections - └── udf: fn_mutating_via_cte [as=fn_mutating_via_cte:12, type=int, volatile, udf] - └── body - └── limit - ├── columns: a:11(int!null) - ├── cardinality: [1 - 1] - ├── volatile, mutations - ├── key: () - ├── fd: ()-->(11) - ├── with &1 (cte) - │ ├── columns: a:11(int!null) - │ ├── cardinality: [1 - 1] - │ ├── volatile, mutations - │ ├── key: () - │ ├── fd: ()-->(11) - │ ├── project - │ │ ├── columns: ab.a:5(int!null) - │ │ ├── cardinality: [1 - 1] - │ │ ├── volatile, mutations - │ │ ├── key: () - │ │ ├── fd: ()-->(5) - │ │ └── insert ab - │ │ ├── columns: ab.a:5(int!null) b:6(int!null) - │ │ ├── insert-mapping: - │ │ │ ├── column1:9 => ab.a:5 - │ │ │ └── column2:10 => b:6 - │ │ ├── return-mapping: - │ │ │ ├── column1:9 => ab.a:5 - │ │ │ └── column2:10 => b:6 - │ │ ├── cardinality: [1 - 1] - │ │ ├── volatile, mutations - │ │ ├── key: () - │ │ ├── fd: ()-->(5,6) - │ │ └── values - │ │ ├── columns: column1:9(int!null) column2:10(int!null) - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ ├── fd: ()-->(9,10) - │ │ └── tuple [type=tuple{int, int}] - │ │ ├── const: 1 [type=int] - │ │ └── const: 2 [type=int] - │ └── with-scan &1 (cte) - │ ├── columns: a:11(int!null) - │ ├── mapping: - │ │ └── ab.a:5(int) => a:11(int) - │ ├── cardinality: [1 - 1] - │ ├── key: () - │ └── fd: ()-->(11) - └── const: 1 [type=int] - -# The "mutations" property should propagate through a nested function that calls -# a mutating-via-CTE function inside its own CTE. -exec-ddl -CREATE FUNCTION fn_calls_mutating_via_cte() RETURNS INT LANGUAGE SQL AS $$ - WITH cte AS (SELECT fn_mutating_via_cte()) SELECT * FROM cte -$$ ----- - -build -SELECT fn_calls_mutating_via_cte() FROM ab ----- -project - ├── columns: fn_calls_mutating_via_cte:14(int) - ├── volatile, mutations - ├── prune: (14) - ├── scan ab - │ ├── columns: ab.a:1(int!null) b:2(int) crdb_internal_mvcc_timestamp:3(decimal) tableoid:4(oid) - │ ├── key: (1) - │ ├── fd: (1)-->(2-4) - │ ├── prune: (1-4) - │ └── interesting orderings: (+1) - └── projections - └── udf: fn_calls_mutating_via_cte [as=fn_calls_mutating_via_cte:14, type=int, volatile, udf] - └── body - └── limit - ├── columns: fn_mutating_via_cte:13(int) - ├── cardinality: [1 - 1] - ├── volatile, mutations - ├── key: () - ├── fd: ()-->(13) - ├── with &2 (cte) - │ ├── columns: fn_mutating_via_cte:13(int) - │ ├── cardinality: [1 - 1] - │ ├── volatile, mutations - │ ├── key: () - │ ├── fd: ()-->(13) - │ ├── project - │ │ ├── columns: fn_mutating_via_cte:12(int) - │ │ ├── cardinality: [1 - 1] - │ │ ├── volatile, mutations - │ │ ├── key: () - │ │ ├── fd: ()-->(12) - │ │ ├── values - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── key: () - │ │ │ └── tuple [type=tuple] - │ │ └── projections - │ │ └── udf: fn_mutating_via_cte [as=fn_mutating_via_cte:12, type=int, volatile, udf] - │ │ └── body - │ │ └── limit - │ │ ├── columns: a:11(int!null) - │ │ ├── cardinality: [1 - 1] - │ │ ├── volatile, mutations - │ │ ├── key: () - │ │ ├── fd: ()-->(11) - │ │ ├── with &1 (cte) - │ │ │ ├── columns: a:11(int!null) - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── volatile, mutations - │ │ │ ├── key: () - │ │ │ ├── fd: ()-->(11) - │ │ │ ├── project - │ │ │ │ ├── columns: ab.a:5(int!null) - │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ ├── volatile, mutations - │ │ │ │ ├── key: () - │ │ │ │ ├── fd: ()-->(5) - │ │ │ │ └── insert ab - │ │ │ │ ├── columns: ab.a:5(int!null) b:6(int!null) - │ │ │ │ ├── insert-mapping: - │ │ │ │ │ ├── column1:9 => ab.a:5 - │ │ │ │ │ └── column2:10 => b:6 - │ │ │ │ ├── return-mapping: - │ │ │ │ │ ├── column1:9 => ab.a:5 - │ │ │ │ │ └── column2:10 => b:6 - │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ ├── volatile, mutations - │ │ │ │ ├── key: () - │ │ │ │ ├── fd: ()-->(5,6) - │ │ │ │ └── values - │ │ │ │ ├── columns: column1:9(int!null) column2:10(int!null) - │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ ├── key: () - │ │ │ │ ├── fd: ()-->(9,10) - │ │ │ │ └── tuple [type=tuple{int, int}] - │ │ │ │ ├── const: 1 [type=int] - │ │ │ │ └── const: 2 [type=int] - │ │ │ └── with-scan &1 (cte) - │ │ │ ├── columns: a:11(int!null) - │ │ │ ├── mapping: - │ │ │ │ └── ab.a:5(int) => a:11(int) - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── key: () - │ │ │ └── fd: ()-->(11) - │ │ └── const: 1 [type=int] - │ └── with-scan &2 (cte) - │ ├── columns: fn_mutating_via_cte:13(int) - │ ├── mapping: - │ │ └── fn_mutating_via_cte:12(int) => fn_mutating_via_cte:13(int) - │ ├── cardinality: [1 - 1] - │ ├── key: () - │ └── fd: ()-->(13) - └── const: 1 [type=int] - -# The "mutations" property should propagate from a PL/pgSQL routine that -# contains a mutation. -exec-ddl -CREATE FUNCTION fn_plpgsql_mutating() RETURNS VOID LANGUAGE PLpgSQL AS $$ - BEGIN - INSERT INTO ab VALUES (1, 2); - END -$$ ----- - -build -SELECT fn_plpgsql_mutating() FROM ab ----- -project - ├── columns: fn_plpgsql_mutating:13(void) - ├── volatile, mutations - ├── prune: (13) - ├── scan ab - │ ├── columns: a:1(int!null) b:2(int) crdb_internal_mvcc_timestamp:3(decimal) tableoid:4(oid) - │ ├── key: (1) - │ ├── fd: (1)-->(2-4) - │ ├── prune: (1-4) - │ └── interesting orderings: (+1) - └── projections - └── udf: fn_plpgsql_mutating [as=fn_plpgsql_mutating:13, type=void, volatile, udf] - └── body - └── limit - ├── columns: "_stmt_exec_1":12(void) - ├── cardinality: [1 - 1] - ├── volatile, mutations - ├── key: () - ├── fd: ()-->(12) - ├── project - │ ├── columns: "_stmt_exec_1":12(void) - │ ├── cardinality: [1 - 1] - │ ├── volatile, mutations - │ ├── key: () - │ ├── fd: ()-->(12) - │ ├── values - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ └── tuple [type=tuple] - │ └── projections - │ └── udf: _stmt_exec_1 [as="_stmt_exec_1":12, type=void, volatile, udf] - │ └── body - │ ├── insert ab - │ │ ├── columns: - │ │ ├── insert-mapping: - │ │ │ ├── column1:9 => a:5 - │ │ │ └── column2:10 => b:6 - │ │ ├── cardinality: [0 - 0] - │ │ ├── volatile, mutations - │ │ └── values - │ │ ├── columns: column1:9(int!null) column2:10(int!null) - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ ├── fd: ()-->(9,10) - │ │ └── tuple [type=tuple{int, int}] - │ │ ├── const: 1 [type=int] - │ │ └── const: 2 [type=int] - │ └── project - │ ├── columns: "_implicit_return":11(void) - │ ├── cardinality: [1 - 1] - │ ├── immutable - │ ├── key: () - │ ├── fd: ()-->(11) - │ ├── values - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ └── tuple [type=tuple] - │ └── projections - │ └── cast: VOID [as="_implicit_return":11, type=void, immutable] - │ └── null [type=unknown] - └── const: 1 [type=int] - -# The "mutations" property should propagate from a PL/pgSQL routine that -# transitively calls a mutating function. -exec-ddl -CREATE FUNCTION fn_plpgsql_calls_mutating() RETURNS INT LANGUAGE PLpgSQL AS $$ - DECLARE - v VOID; - BEGIN - SELECT fn_mutating() INTO v; - RETURN 1; - END -$$ ----- - -build -SELECT fn_plpgsql_calls_mutating() FROM ab ----- -project - ├── columns: fn_plpgsql_calls_mutating:20(int) - ├── volatile, mutations - ├── prune: (20) - ├── scan ab - │ ├── columns: a:1(int!null) b:2(int) crdb_internal_mvcc_timestamp:3(decimal) tableoid:4(oid) - │ ├── key: (1) - │ ├── fd: (1)-->(2-4) - │ ├── prune: (1-4) - │ └── interesting orderings: (+1) - └── projections - └── udf: fn_plpgsql_calls_mutating [as=fn_plpgsql_calls_mutating:20, type=int, volatile, udf] - └── body - └── limit - ├── columns: "_stmt_exec_1":19(int) - ├── cardinality: [1 - 1] - ├── volatile, mutations - ├── key: () - ├── fd: ()-->(19) - ├── project - │ ├── columns: "_stmt_exec_1":19(int) - │ ├── cardinality: [1 - 1] - │ ├── volatile, mutations - │ ├── key: () - │ ├── fd: ()-->(19) - │ ├── barrier - │ │ ├── columns: v:5(void) - │ │ ├── cardinality: [1 - 1] - │ │ ├── immutable - │ │ ├── key: () - │ │ ├── fd: ()-->(5) - │ │ └── project - │ │ ├── columns: v:5(void) - │ │ ├── cardinality: [1 - 1] - │ │ ├── immutable - │ │ ├── key: () - │ │ ├── fd: ()-->(5) - │ │ ├── values - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── key: () - │ │ │ └── tuple [type=tuple] - │ │ └── projections - │ │ └── cast: VOID [as=v:5, type=void, immutable] - │ │ └── null [type=unknown] - │ └── projections - │ └── udf: _stmt_exec_1 [as="_stmt_exec_1":19, type=int, outer=(5), volatile, udf] - │ ├── args - │ │ └── variable: v:5 [type=void] - │ ├── params: v:6(void) - │ └── body - │ └── project - │ ├── columns: "_stmt_exec_ret_2":18(int) - │ ├── cardinality: [1 - 1] - │ ├── volatile, mutations - │ ├── key: () - │ ├── fd: ()-->(18) - │ ├── project - │ │ ├── columns: v:17(void) - │ │ ├── cardinality: [1 - 1] - │ │ ├── volatile, mutations - │ │ ├── key: () - │ │ ├── fd: ()-->(17) - │ │ ├── prune: (17) - │ │ ├── barrier - │ │ │ ├── columns: fn_mutating:14(void) - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── volatile, mutations - │ │ │ ├── key: () - │ │ │ ├── fd: ()-->(14) - │ │ │ └── right-join (cross) - │ │ │ ├── columns: fn_mutating:14(void) - │ │ │ ├── cardinality: [1 - 1] - │ │ │ ├── volatile, mutations - │ │ │ ├── key: () - │ │ │ ├── fd: ()-->(14) - │ │ │ ├── limit - │ │ │ │ ├── columns: fn_mutating:14(void) - │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ ├── volatile, mutations - │ │ │ │ ├── key: () - │ │ │ │ ├── fd: ()-->(14) - │ │ │ │ ├── project - │ │ │ │ │ ├── columns: fn_mutating:14(void) - │ │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ │ ├── volatile, mutations - │ │ │ │ │ ├── key: () - │ │ │ │ │ ├── fd: ()-->(14) - │ │ │ │ │ ├── values - │ │ │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ │ │ ├── key: () - │ │ │ │ │ │ └── tuple [type=tuple] - │ │ │ │ │ └── projections - │ │ │ │ │ └── udf: fn_mutating [as=fn_mutating:14, type=void, volatile, udf] - │ │ │ │ │ └── body - │ │ │ │ │ ├── insert ab - │ │ │ │ │ │ ├── columns: - │ │ │ │ │ │ ├── insert-mapping: - │ │ │ │ │ │ │ ├── column1:11 => a:7 - │ │ │ │ │ │ │ └── column2:12 => b:8 - │ │ │ │ │ │ ├── cardinality: [0 - 0] - │ │ │ │ │ │ ├── volatile, mutations - │ │ │ │ │ │ └── values - │ │ │ │ │ │ ├── columns: column1:11(int!null) column2:12(int!null) - │ │ │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ │ │ ├── key: () - │ │ │ │ │ │ ├── fd: ()-->(11,12) - │ │ │ │ │ │ └── tuple [type=tuple{int, int}] - │ │ │ │ │ │ ├── const: 1 [type=int] - │ │ │ │ │ │ └── const: 2 [type=int] - │ │ │ │ │ └── limit - │ │ │ │ │ ├── columns: column1:13(unknown) - │ │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ │ ├── key: () - │ │ │ │ │ ├── fd: ()-->(13) - │ │ │ │ │ ├── values - │ │ │ │ │ │ ├── columns: column1:13(unknown) - │ │ │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ │ │ ├── key: () - │ │ │ │ │ │ ├── fd: ()-->(13) - │ │ │ │ │ │ └── tuple [type=tuple{unknown}] - │ │ │ │ │ │ └── null [type=unknown] - │ │ │ │ │ └── const: 1 [type=int] - │ │ │ │ └── const: 1 [type=int] - │ │ │ ├── values - │ │ │ │ ├── cardinality: [1 - 1] - │ │ │ │ ├── key: () - │ │ │ │ └── tuple [type=tuple] - │ │ │ └── filters (true) - │ │ └── projections - │ │ └── variable: fn_mutating:14 [as=v:17, type=void, outer=(14)] - │ └── projections - │ └── udf: _stmt_exec_ret_2 [as="_stmt_exec_ret_2":18, type=int, outer=(17), udf] - │ ├── tail-call - │ ├── args - │ │ └── variable: v:17 [type=void] - │ ├── params: v:15(void) - │ └── body - │ └── project - │ ├── columns: stmt_return_3:16(int!null) - │ ├── cardinality: [1 - 1] - │ ├── key: () - │ ├── fd: ()-->(16) - │ ├── values - │ │ ├── cardinality: [1 - 1] - │ │ ├── key: () - │ │ └── tuple [type=tuple] - │ └── projections - │ └── const: 1 [as=stmt_return_3:16, type=int] - └── const: 1 [type=int] - -subtest end From 9451cc01194821e435a43533db96eda057241ae6 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Sat, 16 May 2026 23:38:47 -0400 Subject: [PATCH 08/12] sql: include routine KV reads in EXPLAIN ANALYZE top-level stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, EXPLAIN ANALYZE's "rows decoded from KV" line only reflected KV reads from the outer plan's trace spans. Reads from SQL routine body executions (UDFs, stored procedures) were silently dropped because inner plans run through runPlanInsidePlan with separate flow metadata that is never added to the outer plan's distSQLFlowInfos. Fix this by tracking routine KV stats (rows and bytes read) separately on the planner. After each routine body statement executes, its stats are accumulated and added to the trace-derived queryLevelStats before rendering the EXPLAIN ANALYZE output. Note that this fix only updates the top-level "rows decoded from KV" aggregate. Per-node stats in the expanded plan tree still reflect only the outer plan's reads — individual routine invocations are *not* yet surfaced in the plan output (will be covered in follow-up PR). Additionally, `KVPairsRead` and `BatchRequestsIssued` for inner routines remain untracked because the `ProducerMetadata.Metrics` proto does not carry those fields. Fixes: #170398 Release note (bug fix): Fixed a bug where EXPLAIN ANALYZE's "rows decoded from KV" line did not include KV reads performed inside UDF and stored procedure bodies, causing the reported count to be lower than actual. --- pkg/sql/conn_executor_exec.go | 10 + .../testdata/explain_analyze_routine | 328 ++++++++++++++++++ .../execbuilder/tests/local/generated_test.go | 7 + pkg/sql/planner.go | 11 + pkg/sql/routine.go | 2 + 5 files changed, 358 insertions(+) create mode 100644 pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine diff --git a/pkg/sql/conn_executor_exec.go b/pkg/sql/conn_executor_exec.go index 3852b92d7ccb..32ad24bb04f0 100644 --- a/pkg/sql/conn_executor_exec.go +++ b/pkg/sql/conn_executor_exec.go @@ -3301,6 +3301,16 @@ func populateQueryLevelStats( queryLevelStats, err := execstats.GetQueryLevelStats( trace, cfg.TestingKnobs.DeterministicExplain, flowsMetadata, ) + + // The queryLevelStats computed from trace spans only accounts for the + // outer plan's KV reads. SQL routine body executions (UDFs, stored + // procedures) run as inner plans whose KV reads are not captured by + // the outer plan's trace spans. Add the separately tracked routine + // KV stats so that EXPLAIN ANALYZE's "rows decoded from KV" line + // reflects all KV work, including routine body reads. + queryLevelStats.KVRowsRead += p.routineKVStats.rowsRead + queryLevelStats.KVBytesRead += p.routineKVStats.bytesRead + queryLevelStatsWithErr := execstats.MakeQueryLevelStatsWithErr(queryLevelStats, err) ih.queryLevelStatsWithErr = &queryLevelStatsWithErr if err != nil { diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine new file mode 100644 index 000000000000..c4d1a87b4c15 --- /dev/null +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine @@ -0,0 +1,328 @@ +# LogicTest: local + +# Regression test for #170398: KV reads from inside routine bodies were +# silently dropped from the EXPLAIN ANALYZE "rows decoded from KV" line. + +statement ok +CREATE TABLE t_ea (k INT PRIMARY KEY, v INT, FAMILY (k, v)) + +statement ok +INSERT INTO t_ea SELECT i, i*10 FROM generate_series(1, 10) AS g(i) + +# Baseline: show the plan for the UDF body query executed directly. +# This full-scans t_ea (10 rows). When the same query runs inside a +# routine body, these 10 rows should appear in the outer statement's +# "rows decoded from KV" total. +query T +EXPLAIN ANALYZE SELECT count(*)::INT FROM t_ea WHERE v > 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 10 (80 B, 20 KVs, 10 gRPC calls) +regions: +· +• group (scalar) +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 1 +│ +└── • filter + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 10 + │ filter: v > 3 + │ + └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 10 + actual row count: 10 + missing stats + table: t_ea@t_ea_pkey + spans: FULL SCAN + +# ------------------------------------------------------------------ +# Deferred-build path: VOLATILE single-statement SQL UDF. +# VOLATILE prevents inlining and triggers deferred body building. +# ------------------------------------------------------------------ +subtest volatile_sql_udf + +statement ok +CREATE FUNCTION count_above_volatile(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT count(*)::INT FROM t_ea WHERE v > x +$$ + +# 3 outer rows (k <= 3) + 3 invocations × 10 inner rows = 33 total rows. +query T +EXPLAIN ANALYZE SELECT count_above_volatile(k) FROM t_ea WHERE k <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 33 (906 B, 6 KVs, 3 gRPC calls) +regions: +· +• render +│ execution time: 0µs +│ actual row count: 3 +│ +└── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 3 + missing stats + table: t_ea@t_ea_pkey + spans: [ - /3] + +subtest end + +# ------------------------------------------------------------------ +# Eager-build path: PL/pgSQL UDF with a table read. +# PL/pgSQL routines always build their body eagerly and are never +# inlined. +# ------------------------------------------------------------------ +subtest plpgsql_udf + +statement ok +CREATE FUNCTION count_above_plpgsql(x INT) RETURNS INT LANGUAGE PLpgSQL AS $$ +DECLARE + result INT; +BEGIN + SELECT count(*)::INT INTO result FROM t_ea WHERE v > x; + RETURN result; +END +$$ + +# 3 outer rows (k <= 3) + 3 invocations × 10 inner rows = 33 total rows. +query T +EXPLAIN ANALYZE SELECT count_above_plpgsql(k) FROM t_ea WHERE k <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 33 (906 B, 6 KVs, 3 gRPC calls) +regions: +· +• render +│ execution time: 0µs +│ actual row count: 3 +│ +└── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 3 + missing stats + table: t_ea@t_ea_pkey + spans: [ - /3] + +subtest end + +# ------------------------------------------------------------------ +# Multi-statement UDF (deferred build, never inlined). +# ------------------------------------------------------------------ +subtest multi_stmt_udf + +statement ok +CREATE FUNCTION count_above_multi(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT 1; + SELECT count(*)::INT FROM t_ea WHERE v > x +$$ + +# 3 outer rows (k <= 3) + 3 invocations × 10 inner rows = 33 total rows. +query T +EXPLAIN ANALYZE SELECT count_above_multi(k) FROM t_ea WHERE k <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 33 (906 B, 6 KVs, 3 gRPC calls) +regions: +· +• render +│ execution time: 0µs +│ actual row count: 3 +│ +└── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 3 + missing stats + table: t_ea@t_ea_pkey + spans: [ - /3] + +subtest end + +# ------------------------------------------------------------------ +# Multiple scans within a single routine body. Each body statement's +# KV reads should be accumulated. +# ------------------------------------------------------------------ +subtest multi_scan_routine + +statement ok +CREATE TABLE t_ea2 (k INT PRIMARY KEY, v INT, FAMILY (k, v)) + +statement ok +INSERT INTO t_ea2 SELECT i, i FROM generate_series(1, 5) AS g(i) + +statement ok +CREATE FUNCTION multi_scan(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT count(*)::INT FROM t_ea WHERE v > x; + SELECT count(*)::INT FROM t_ea2 WHERE v > x +$$ + +# 3 outer rows (k <= 3) + 3 invocations × (10 rows from t_ea + 5 rows from t_ea2) = 48 total. +query T +EXPLAIN ANALYZE SELECT multi_scan(k) FROM t_ea WHERE k <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 48 (1.3 KiB, 6 KVs, 3 gRPC calls) +regions: +· +• render +│ execution time: 0µs +│ actual row count: 3 +│ +└── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 3 + missing stats + table: t_ea@t_ea_pkey + spans: [ - /3] + +subtest end + +# ------------------------------------------------------------------ +# Nested UDF calls: one UDF in the main query body, another in a CTE. +# ------------------------------------------------------------------ +subtest nested_udf_with_cte + +# 3 outer rows from the CTE (k <= 3), each invoking count_above_volatile +# → 3 × 10 = 30 inner rows from CTE UDF calls. +# Then the main query scans the CTE result (3 rows) and invokes +# count_above_volatile on each → 3 × 10 = 30 more inner rows. +# Plus 3 rows from the CTE scan of t_ea and 3 from the main query +# reading the CTE (materialized). +# Total outer KV reads: 3 (CTE scan of t_ea WHERE k <= 3). +# Total inner KV reads: 30 (CTE UDF) + 30 (main query UDF) = 60. +# Grand total: 3 + 60 = 63 rows. +query T +EXPLAIN ANALYZE + WITH cte AS ( + SELECT k, count_above_volatile(k) AS cnt FROM t_ea WHERE k <= 3 + ) + SELECT count_above_volatile(k) FROM cte +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 63 (1.7 KiB, 6 KVs, 3 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan buffer +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 3 +│ label: buffer 1 (cte) +│ +└── • subquery + │ id: @S1 + │ original sql: SELECT k, public.count_above_volatile(k) AS cnt FROM t_ea WHERE k <= 3 + │ exec mode: discard all rows + │ + └── • buffer + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 3 + │ label: buffer 1 (cte) + │ + └── • render + │ execution time: 0µs + │ actual row count: 3 + │ + └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 3 + missing stats + table: t_ea@t_ea_pkey + spans: [ - /3] + +subtest end + +# ------------------------------------------------------------------ +# Verify that queries without routines are unaffected by the fix. +# ------------------------------------------------------------------ +subtest no_routine + +query T +EXPLAIN ANALYZE SELECT count(*) FROM t_ea WHERE v > 5 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 10 (80 B, 20 KVs, 10 gRPC calls) +regions: +· +• group (scalar) +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 1 +│ +└── • filter + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 10 + │ filter: v > 5 + │ + └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 10 + actual row count: 10 + missing stats + table: t_ea@t_ea_pkey + spans: FULL SCAN + +subtest end diff --git a/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go b/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go index 933860411bac..fbe822f60152 100644 --- a/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go +++ b/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go @@ -216,6 +216,13 @@ func TestExecBuild_explain_analyze( runExecBuildLogicTest(t, "explain_analyze") } +func TestExecBuild_explain_analyze_routine( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runExecBuildLogicTest(t, "explain_analyze_routine") +} + func TestExecBuild_explain_env( t *testing.T, ) { diff --git a/pkg/sql/planner.go b/pkg/sql/planner.go index 7c0831445fb8..7d300c3d8abf 100644 --- a/pkg/sql/planner.go +++ b/pkg/sql/planner.go @@ -261,6 +261,15 @@ type planner struct { // concurrency. routineMetadataForwarder metadataForwarder + // routineKVStats accumulates KV read statistics from SQL routine + // (UDF/procedure) body executions. These stats are tracked separately + // from the outer plan's stats so that EXPLAIN ANALYZE can include + // inner routine reads in the top-level "rows decoded from KV" line. + routineKVStats struct { + rowsRead int64 + bytesRead int64 + } + storedProcTxnState storedProcTxnStateAccessor createdSequences createdSequences @@ -1080,6 +1089,8 @@ func (p *planner) resetPlanner( p.typeResolutionDbID = descpb.InvalidID p.pausablePortal = nil p.routineMetadataForwarder = nil + p.routineKVStats.rowsRead = 0 + p.routineKVStats.bytesRead = 0 p.autoRetryCounter = 0 p.autoRetryStmtReason = nil p.autoRetryStmtCounter = 0 diff --git a/pkg/sql/routine.go b/pkg/sql/routine.go index 0f42b77889cc..8a39b0736802 100644 --- a/pkg/sql/routine.go +++ b/pkg/sql/routine.go @@ -420,6 +420,8 @@ func (g *routineGenerator) startInternal(ctx context.Context, txn *kv.Txn) (err } statsBuilder.QueryLevelStats(queryStats.bytesRead, queryStats.rowsRead, queryStats.rowsWritten, queryStats.kvCPUTimeNanos.Nanoseconds()) forwardInnerQueryStats(g.p.routineMetadataForwarder, queryStats) + g.p.routineKVStats.rowsRead += queryStats.rowsRead + g.p.routineKVStats.bytesRead += queryStats.bytesRead if openCursor { return cursorHelper.createCursor(g.p) } From 0ce8b3878f4b4320838cba0dadedfba9245dfd92 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Tue, 19 May 2026 12:56:10 -0400 Subject: [PATCH 09/12] sql: show routine body plans in EXPLAIN ANALYZE output Previously, EXPLAIN ANALYZE only showed the top-level query plan with no visibility into the plans used by non-inlined SQL routine bodies (volatile UDFs, PL/pgSQL functions, stored procedures). This made it difficult to understand performance characteristics of queries that invoke routines, since the routine body plans are built at execution time (deferred building) and were invisible to the explain infrastructure. This commit captures routine body explain plans during execution and renders them as additional "routine" sections beneath the main plan tree in EXPLAIN ANALYZE output. Each routine section shows the routine name and the plan tree for each body statement, including the SQL text. The implementation wraps exec.Factory with explain.Factory during routine body building in buildRoutinePlanGenerator, captures the explain nodes, and stores them on eval.Context. After execution completes, instrumentationHelper.populateRoutinePlans() transfers the captured plans into the explain.Plan for rendering. A dedup mechanism using {routineName, planGistVector} keys ensures each unique plan variant is shown only once, while genuinely different plans (e.g., from NULL vs non-NULL arguments) each get their own section. Resolves: #170448 Epic: CRDB-42655 Release note (sql change): EXPLAIN ANALYZE now shows the execution plans of non-inlined SQL routine bodies (volatile UDFs, PL/pgSQL functions, stored procedures) as additional "routine" sections beneath the main query plan. Each section displays the routine name and the plan tree for each body statement, making it easier to diagnose performance issues in queries that invoke routines. Co-Authored-By: Claude Opus 4.6 --- pkg/sql/instrumentation.go | 42 ++ pkg/sql/opt/exec/execbuilder/scalar.go | 78 ++- .../testdata/explain_analyze_routine | 262 +++++++--- .../testdata/explain_analyze_routine_plans | 483 ++++++++++++++++++ .../execbuilder/tests/local/generated_test.go | 7 + pkg/sql/opt/exec/explain/emit.go | 21 +- pkg/sql/opt/exec/explain/explain_factory.go | 15 + pkg/sql/sem/eval/context.go | 32 +- 8 files changed, 860 insertions(+), 80 deletions(-) create mode 100644 pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans diff --git a/pkg/sql/instrumentation.go b/pkg/sql/instrumentation.go index c9bfd484bcd5..7f15aee1147b 100644 --- a/pkg/sql/instrumentation.go +++ b/pkg/sql/instrumentation.go @@ -480,9 +480,22 @@ func (ih *instrumentationHelper) Setup( // TODO(radu): maybe capture some of the rows and include them in the // bundle. ih.discardRows = true + // Initialize capture state for routine body plans. Clear stale + // entries from previous statements so each EXPLAIN ANALYZE + // starts fresh; both the bundle code and populateRoutinePlans + // read DeferredRoutineOptPlans during Finish of this statement. + if ih.evalCtx != nil { + ih.evalCtx.CapturedRoutineGists = make(map[string]struct{}) + ih.evalCtx.DeferredRoutineOptPlans = nil + } case explainAnalyzePlanOutput, explainAnalyzeDistSQLOutput: ih.discardRows = true + // Initialize capture state for routine body plans. See above. + if ih.evalCtx != nil { + ih.evalCtx.CapturedRoutineGists = make(map[string]struct{}) + ih.evalCtx.DeferredRoutineOptPlans = nil + } default: // Handle transaction-level diagnostics @@ -804,6 +817,10 @@ func (ih *instrumentationHelper) Finish( return retErr } + // Transfer any captured routine body explain plans into the explain.Plan + // so they're available for rendering. + ih.populateRoutinePlans() + switch ih.outputMode { case explainAnalyzeDebugOutput: return setExplainBundleResult(ctx, res, bundle, cfg, warnings) @@ -872,6 +889,31 @@ func (ih *instrumentationHelper) RecordExplainPlan(explainPlan *explain.Plan) { ih.explainPlan = explainPlan } +// populateRoutinePlans transfers captured routine body explain plans from +// eval.Context into the explain.Plan so they're available for rendering. +// This must be called after execution completes (so all routine invocations +// have had a chance to capture) and before emitExplain. +func (ih *instrumentationHelper) populateRoutinePlans() { + if ih.explainPlan == nil || ih.evalCtx == nil { + return + } + for _, dp := range ih.evalCtx.DeferredRoutineOptPlans { + if len(dp.ExplainPlan) == 0 { + continue + } + // Type-assert the []any elements back to *explain.Node. + nodes := make([]*explain.Node, len(dp.ExplainPlan)) + for i, n := range dp.ExplainPlan { + nodes[i] = n.(*explain.Node) + } + ih.explainPlan.RoutinePlans = append(ih.explainPlan.RoutinePlans, explain.RoutinePlanInfo{ + Name: dp.Name, + ExplainPlan: nodes, + BodyStmts: dp.BodyStmts, + }) + } +} + // RecordPlanInfo records top-level information about the plan. func (ih *instrumentationHelper) RecordPlanInfo( distribution physicalplan.PlanDistribution, vectorized, containsMutation, generic, optimized bool, diff --git a/pkg/sql/opt/exec/execbuilder/scalar.go b/pkg/sql/opt/exec/execbuilder/scalar.go index 3be2a97a9630..53fce750748f 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -8,6 +8,7 @@ package execbuilder import ( "bytes" "context" + "strings" "github.com/cockroachdb/cockroach/pkg/sql/appstatspb" "github.com/cockroachdb/cockroach/pkg/sql/opt" @@ -1280,6 +1281,14 @@ func (b *Builder) buildRoutinePlanGenerator( appName := b.evalCtx.SessionData().ApplicationName // TODO(yuzefovich): look into computing fingerprintFormat lazily. fingerprintFormat := tree.FmtHideConstants | tree.FmtFlags(tree.QueryFormattingForFingerprintsMask.Get(&b.evalCtx.Settings.SV)) + + // captureExplain is true when we are inside an EXPLAIN ANALYZE + // and should capture routine body plans. We only capture for + // named routines (not subquery/exists wrappers). + captureExplain := b.evalCtx.CapturedRoutineGists != nil && routineName != "" + var explainNodes []*explain.Node + var gistStrs []string + for i := range stmts { latencyRecorder.Reset() var builder *sqlstats.RecordedStatementStatsBuilder @@ -1389,6 +1398,15 @@ func (b *Builder) buildRoutinePlanGenerator( ef = &gistFactory } + // Wrap with explain.Factory for EXPLAIN ANALYZE routine + // plan capture. The explain.Factory must be outermost so it + // captures the full plan structure, with gistFactory inside. + var explainFactory *explain.Factory + if captureExplain { + explainFactory = explain.NewFactory(ef, b.semaCtx, b.evalCtx) + ef = explainFactory + } + eb := New(ctx, ef, &o, f.Memo(), b.catalog, optimizedExpr, b.semaCtx, b.evalCtx, false /* allowAutoCommit */, b.IsANSIDML) eb.withExprs = withExprs eb.disableTelemetry = true @@ -1402,10 +1420,34 @@ func (b *Builder) buildRoutinePlanGenerator( eb.addRoutineResultBuffer(resultBufferID, resultWriter) } plan, err := eb.Build() + + // Extract explain tree and unwrap the plan for execution. + // When explain.Factory is used, eb.Build() returns + // *explain.Plan wrapping the real exec plan in WrappedPlan. + // We capture the explain root for EXPLAIN ANALYZE output and + // pass the unwrapped plan to fn() for execution. + var execPlan exec.Plan = plan + if explainFactory != nil && plan != nil { + ep, ok := plan.(*explain.Plan) + if ok { + explainNodes = append(explainNodes, ep.Root) + execPlan = ep.WrappedPlan + } + } + if gistFactory.Initialized() { planGist := gistFactory.PlanGist() - builder.PlanGist(planGist.String(), planGist.Hash()) + gistStr := planGist.String() + builder.PlanGist(gistStr, planGist.Hash()) + if captureExplain { + gistStrs = append(gistStrs, gistStr) + } + } else if captureExplain { + // No gist available (e.g., PL/pgSQL with nil AST or + // gists disabled). Use empty string for the dedup key. + gistStrs = append(gistStrs, "") } + if err != nil { if errors.IsAssertionFailure(err) { // Enhance the error with the EXPLAIN (OPT, VERBOSE) of the @@ -1428,12 +1470,44 @@ func (b *Builder) buildRoutinePlanGenerator( } incrementRoutineStmtCounter(b.evalCtx.StartedRoutineStatementCounters, dbName, appName, tag) sqlstats.RecordStatementPhase(latencyRecorder, sqlstats.StatementEndPlanning) - err = fn(plan, statsBuilderWithLatencies, stmtForDistSQLDiagram, isFinalPlan) + err = fn(execPlan, statsBuilderWithLatencies, stmtForDistSQLDiagram, isFinalPlan) if err != nil { return err } incrementRoutineStmtCounter(b.evalCtx.ExecutedRoutineStatementCounters, dbName, appName, tag) } + + // Store captured routine body explain plans for EXPLAIN + // ANALYZE if this is a new {name, gist} variant. The dedup + // key is "routineName:gist1,gist2,..." where each gist is + // the per-body-statement plan gist string. + if captureExplain && len(explainNodes) > 0 { + gistKey := strings.Join(gistStrs, ",") + dedupKey := routineName + ":" + gistKey + if _, ok := b.evalCtx.CapturedRoutineGists[dedupKey]; !ok { + b.evalCtx.CapturedRoutineGists[dedupKey] = struct{}{} + bodyStmtTexts := make([]string, len(explainNodes)) + for j := range bodyStmtTexts { + if j < len(stmtASTs) && stmtASTs[j] != nil { + bodyStmtTexts[j] = tree.AsString(stmtASTs[j]) + } + } + explainAny := make([]any, len(explainNodes)) + for j, n := range explainNodes { + explainAny[j] = n + } + b.evalCtx.DeferredRoutineOptPlans = append( + b.evalCtx.DeferredRoutineOptPlans, + eval.DeferredRoutineOptPlan{ + Name: routineName, + ExplainPlan: explainAny, + BodyStmts: bodyStmtTexts, + GistKey: gistKey, + }, + ) + } + } + return nil } return planGen diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine index c4d1a87b4c15..7b14b3d90881 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine @@ -69,20 +69,38 @@ plan type: custom rows decoded from KV: 33 (906 B, 6 KVs, 3 gRPC calls) regions: · -• render -│ execution time: 0µs -│ actual row count: 3 +• root │ -└── • scan - sql nodes: - kv nodes: - regions: - KV time: 0µs - KV rows decoded: 3 - actual row count: 3 - missing stats - table: t_ea@t_ea_pkey - spans: [ - /3] +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t_ea@t_ea_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: count_above_volatile + │ + └── • body stmt + │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x + │ + └── • group (scalar) + │ + └── • filter + │ filter: v > 1 + │ + └── • scan + missing stats + table: t_ea@t_ea_pkey + spans: FULL SCAN subtest end @@ -114,20 +132,43 @@ plan type: custom rows decoded from KV: 33 (906 B, 6 KVs, 3 gRPC calls) regions: · -• render -│ execution time: 0µs -│ actual row count: 3 +• root │ -└── • scan - sql nodes: - kv nodes: - regions: - KV time: 0µs - KV rows decoded: 3 - actual row count: 3 - missing stats - table: t_ea@t_ea_pkey - spans: [ - /3] +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t_ea@t_ea_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: count_above_plpgsql + │ + └── • body stmt + │ + └── • render + │ + └── • group (streaming) + │ + └── • cross join (right outer) + │ + ├── • filter + │ │ filter: v > 1 + │ │ + │ └── • scan + │ missing stats + │ table: t_ea@t_ea_pkey + │ spans: FULL SCAN + │ + └── • emptyrow subtest end @@ -153,20 +194,44 @@ plan type: custom rows decoded from KV: 33 (906 B, 6 KVs, 3 gRPC calls) regions: · -• render -│ execution time: 0µs -│ actual row count: 3 +• root │ -└── • scan - sql nodes: - kv nodes: - regions: - KV time: 0µs - KV rows decoded: 3 - actual row count: 3 - missing stats - table: t_ea@t_ea_pkey - spans: [ - /3] +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t_ea@t_ea_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: count_above_multi + │ + ├── • body stmt + │ │ sql: SELECT 1 + │ │ + │ └── • values + │ size: 1 column, 1 row + │ + └── • body stmt + │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x + │ + └── • group (scalar) + │ + └── • filter + │ filter: v > 1 + │ + └── • scan + missing stats + table: t_ea@t_ea_pkey + spans: FULL SCAN subtest end @@ -199,20 +264,51 @@ plan type: custom rows decoded from KV: 48 (1.3 KiB, 6 KVs, 3 gRPC calls) regions: · -• render -│ execution time: 0µs -│ actual row count: 3 +• root │ -└── • scan - sql nodes: - kv nodes: - regions: - KV time: 0µs - KV rows decoded: 3 - actual row count: 3 - missing stats - table: t_ea@t_ea_pkey - spans: [ - /3] +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t_ea@t_ea_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: multi_scan + │ + ├── • body stmt + │ │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x + │ │ + │ └── • group (scalar) + │ │ + │ └── • filter + │ │ filter: v > 1 + │ │ + │ └── • scan + │ missing stats + │ table: t_ea@t_ea_pkey + │ spans: FULL SCAN + │ + └── • body stmt + │ sql: SELECT count(*)::INT8 FROM test.public.t_ea2 WHERE v > x + │ + └── • group (scalar) + │ + └── • filter + │ filter: v > 1 + │ + └── • scan + missing stats + table: t_ea2@t_ea2_pkey + spans: FULL SCAN subtest end @@ -257,32 +353,48 @@ regions: │ actual row count: 3 │ label: buffer 1 (cte) │ -└── • subquery - │ id: @S1 - │ original sql: SELECT k, public.count_above_volatile(k) AS cnt FROM t_ea WHERE k <= 3 - │ exec mode: discard all rows +├── • subquery +│ │ id: @S1 +│ │ original sql: SELECT k, public.count_above_volatile(k) AS cnt FROM t_ea WHERE k <= 3 +│ │ exec mode: discard all rows +│ │ +│ └── • buffer +│ │ sql nodes: +│ │ regions: +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ label: buffer 1 (cte) +│ │ +│ └── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t_ea@t_ea_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: count_above_volatile │ - └── • buffer - │ sql nodes: - │ regions: - │ execution time: 0µs - │ actual row count: 3 - │ label: buffer 1 (cte) + └── • body stmt + │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x │ - └── • render - │ execution time: 0µs - │ actual row count: 3 + └── • group (scalar) │ - └── • scan - sql nodes: - kv nodes: - regions: - KV time: 0µs - KV rows decoded: 3 - actual row count: 3 - missing stats - table: t_ea@t_ea_pkey - spans: [ - /3] + └── • filter + │ filter: v > 1 + │ + └── • scan + missing stats + table: t_ea@t_ea_pkey + spans: FULL SCAN subtest end diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans new file mode 100644 index 000000000000..50b2158681a0 --- /dev/null +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans @@ -0,0 +1,483 @@ +# LogicTest: local + +# Tests for surfacing routine body plans in EXPLAIN ANALYZE output. +# Issue: #170448 + +statement ok +CREATE TABLE t (a INT PRIMARY KEY, b INT, FAMILY (a, b)) + +statement ok +INSERT INTO t SELECT i, i*10 FROM generate_series(1, 10) AS g(i) + +# ------------------------------------------------------------------ +# 1. Single-statement volatile SQL UDF. +# ------------------------------------------------------------------ +subtest single_stmt_volatile_udf + +statement ok +CREATE FUNCTION lookup_volatile(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT b FROM t WHERE a = x +$$ + +# The routine body plan should appear in a "routine" section below +# the main plan. +query T +EXPLAIN ANALYZE SELECT lookup_volatile(a) FROM t WHERE a <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 6 (57 B, 3 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t@t_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: lookup_volatile + │ + └── • body stmt + │ sql: SELECT b FROM test.public.t WHERE a = x + │ + └── • scan + missing stats + table: t@t_pkey + spans: [/1 - /1] + +subtest end + +# ------------------------------------------------------------------ +# 2. Multi-statement volatile SQL UDF. +# ------------------------------------------------------------------ +subtest multi_stmt_udf + +statement ok +CREATE FUNCTION multi_stmt(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT 1; + SELECT b FROM t WHERE a = x +$$ + +query T +EXPLAIN ANALYZE SELECT multi_stmt(a) FROM t WHERE a <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 6 (57 B, 3 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t@t_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: multi_stmt + │ + ├── • body stmt + │ │ sql: SELECT 1 + │ │ + │ └── • values + │ size: 1 column, 1 row + │ + └── • body stmt + │ sql: SELECT b FROM test.public.t WHERE a = x + │ + └── • scan + missing stats + table: t@t_pkey + spans: [/1 - /1] + +subtest end + +# ------------------------------------------------------------------ +# 3. Same UDF in multiple positions (deduplication). +# Only one routine section should appear. +# ------------------------------------------------------------------ +subtest dedup_same_udf + +query T +EXPLAIN ANALYZE SELECT lookup_volatile(a), lookup_volatile(b) FROM t WHERE a <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 7 (69 B, 6 KVs, 3 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t@t_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: lookup_volatile + │ + └── • body stmt + │ sql: SELECT b FROM test.public.t WHERE a = x + │ + └── • scan + missing stats + table: t@t_pkey + spans: [/1 - /1] + +subtest end + +# ------------------------------------------------------------------ +# 4. Multiple distinct UDFs. +# Both routine sections should appear. +# ------------------------------------------------------------------ +subtest multiple_distinct_udfs + +statement ok +CREATE FUNCTION other_udf(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT x + 1 +$$ + +query T +EXPLAIN ANALYZE SELECT lookup_volatile(a), other_udf(a) FROM t WHERE a <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 6 (57 B, 3 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t@t_pkey +│ spans: [ - /3] +│ +├── • routine +│ │ name: lookup_volatile +│ │ +│ └── • body stmt +│ │ sql: SELECT b FROM test.public.t WHERE a = x +│ │ +│ └── • scan +│ missing stats +│ table: t@t_pkey +│ spans: [/1 - /1] +│ +└── • routine + │ name: other_udf + │ + └── • body stmt + │ sql: SELECT x + 1 + │ + └── • values + size: 1 column, 1 row + +subtest end + +# ------------------------------------------------------------------ +# 5. Nested UDF calls. +# Both outer and inner routines should get sections. +# ------------------------------------------------------------------ +subtest nested_udfs + +statement ok +CREATE FUNCTION inner_fn(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT x * 10 +$$ + +statement ok +CREATE FUNCTION outer_fn(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT inner_fn(x) + 1 +$$ + +query T +EXPLAIN ANALYZE SELECT outer_fn(a) FROM t WHERE a <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 3 (24 B, 6 KVs, 3 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t@t_pkey +│ spans: [ - /3] +│ +├── • routine +│ │ name: inner_fn +│ │ +│ └── • body stmt +│ │ sql: SELECT x * 10 +│ │ +│ └── • values +│ size: 1 column, 1 row +│ +└── • routine + │ name: outer_fn + │ + └── • body stmt + │ sql: SELECT public.inner_fn(x) + 1 + │ + └── • values + size: 1 column, 1 row + +subtest end + +# ------------------------------------------------------------------ +# 6. No-routine baseline (regression guard). +# No routine section should appear. +# ------------------------------------------------------------------ +subtest no_routine_baseline + +query T +EXPLAIN ANALYZE SELECT * FROM t WHERE a <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 3 (24 B, 6 KVs, 3 gRPC calls) +regions: +· +• scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 3 + missing stats + table: t@t_pkey + spans: [ - /3] + +subtest end + +# ------------------------------------------------------------------ +# 7. Inlined (STABLE) UDF — no routine section should appear. +# ------------------------------------------------------------------ +subtest inlined_udf + +statement ok +CREATE FUNCTION lookup_stable(x INT) RETURNS INT STABLE LANGUAGE SQL AS $$ + SELECT b FROM t WHERE a = x +$$ + +query T +EXPLAIN ANALYZE SELECT lookup_stable(a) FROM t WHERE a <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 10 (80 B, 20 KVs, 10 gRPC calls) +regions: +· +• render +│ +└── • merge join + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 3 + │ equality: (a) = (a) + │ left cols are key + │ right cols are key + │ + ├── • scan + │ sql nodes: + │ kv nodes: + │ regions: + │ KV time: 0µs + │ KV rows decoded: 7 + │ actual row count: 7 + │ missing stats + │ table: t@t_pkey + │ spans: FULL SCAN + │ + └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 3 + missing stats + table: t@t_pkey + spans: [ - /3] + +subtest end + +# ------------------------------------------------------------------ +# 8. PL/pgSQL UDF. +# ------------------------------------------------------------------ +subtest plpgsql_udf + +statement ok +CREATE FUNCTION lookup_plpgsql(x INT) RETURNS INT LANGUAGE PLpgSQL AS $$ +DECLARE result INT; +BEGIN + SELECT b INTO result FROM t WHERE a = x; + RETURN result; +END +$$ + +query T +EXPLAIN ANALYZE SELECT lookup_plpgsql(a) FROM t WHERE a <= 3 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 6 (57 B, 3 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 3 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 3 +│ missing stats +│ table: t@t_pkey +│ spans: [ - /3] +│ +└── • routine + │ name: lookup_plpgsql + │ + └── • body stmt + │ + └── • render + │ + └── • cross join (left outer) + │ + ├── • emptyrow + │ + └── • scan + missing stats + table: t@t_pkey + spans: [/1 - /1] + +subtest end + +# ------------------------------------------------------------------ +# 9. Plan variants: different argument values produce different plans. +# Non-NULL values produce a point lookup; NULL produces a +# contradiction (a = NULL is always false). Both variants should +# appear as separate routine sections. +# ------------------------------------------------------------------ +subtest plan_variants + +query T +EXPLAIN ANALYZE SELECT lookup_volatile(a) FROM (VALUES (1), (NULL::INT)) AS v(a) +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 1 (11 B, 0 KVs, 0 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 2 +│ │ +│ └── • values +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 2 +│ size: 1 column, 2 rows +│ +├── • routine +│ │ name: lookup_volatile +│ │ +│ └── • body stmt +│ │ sql: SELECT b FROM test.public.t WHERE a = x +│ │ +│ └── • scan +│ missing stats +│ table: t@t_pkey +│ spans: [/1 - /1] +│ +└── • routine + │ name: lookup_volatile + │ + └── • body stmt + │ sql: SELECT b FROM test.public.t WHERE a = x + │ + └── • norows + +subtest end diff --git a/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go b/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go index fbe822f60152..b42d9b28c2b4 100644 --- a/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go +++ b/pkg/sql/opt/exec/execbuilder/tests/local/generated_test.go @@ -223,6 +223,13 @@ func TestExecBuild_explain_analyze_routine( runExecBuildLogicTest(t, "explain_analyze_routine") } +func TestExecBuild_explain_analyze_routine_plans( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runExecBuildLogicTest(t, "explain_analyze_routine_plans") +} + func TestExecBuild_explain_env( t *testing.T, ) { diff --git a/pkg/sql/opt/exec/explain/emit.go b/pkg/sql/opt/exec/explain/emit.go index c3833eeaf139..0ce3977b8456 100644 --- a/pkg/sql/opt/exec/explain/emit.go +++ b/pkg/sql/opt/exec/explain/emit.go @@ -111,7 +111,8 @@ func emitInternal( } if len(plan.Subqueries) == 0 && len(plan.Cascades) == 0 && - len(plan.Checks) == 0 && len(plan.Triggers) == 0 { + len(plan.Checks) == 0 && len(plan.Triggers) == 0 && + len(plan.RoutinePlans) == 0 { return walk(plan.Root) } ob.EnterNode("root", plan.Root.Columns(), plan.Root.Ordering()) @@ -216,6 +217,24 @@ func emitInternal( } ob.LeaveNode() } + // Emit routine body plans captured during EXPLAIN ANALYZE. + // Each body statement is wrapped in its own sub-node to + // keep the SQL text paired with its plan tree. + for _, rp := range plan.RoutinePlans { + ob.EnterMetaNode("routine") + ob.Attr("name", rp.Name) + for j, bodyNode := range rp.ExplainPlan { + ob.EnterMetaNode("body stmt") + if j < len(rp.BodyStmts) && rp.BodyStmts[j] != "" { + ob.Attr("sql", rp.BodyStmts[j]) + } + if err := walk(bodyNode); err != nil { + return err + } + ob.LeaveNode() + } + ob.LeaveNode() + } ob.LeaveNode() return nil } diff --git a/pkg/sql/opt/exec/explain/explain_factory.go b/pkg/sql/opt/exec/explain/explain_factory.go index c14b58fede42..3447de231b4d 100644 --- a/pkg/sql/opt/exec/explain/explain_factory.go +++ b/pkg/sql/opt/exec/explain/explain_factory.go @@ -127,6 +127,21 @@ type Plan struct { Checks []*Node WrappedPlan exec.Plan Gist PlanGist + // RoutinePlans holds captured explain plans for non-inlined SQL + // routine bodies (UDFs, stored procedures), populated during + // EXPLAIN ANALYZE execution. + RoutinePlans []RoutinePlanInfo +} + +// RoutinePlanInfo holds the captured explain plan for a single routine +// body variant (identified by routine name + plan gist vector). +type RoutinePlanInfo struct { + // Name is the function or procedure name. + Name string + // ExplainPlan holds the captured explain tree for each body statement. + ExplainPlan []*Node + // BodyStmts holds the SQL text of each body statement. + BodyStmts []string } var _ exec.Plan = &Plan{} diff --git a/pkg/sql/sem/eval/context.go b/pkg/sql/sem/eval/context.go index 4b79bfe25fe6..9cd4c6866470 100644 --- a/pkg/sql/sem/eval/context.go +++ b/pkg/sql/sem/eval/context.go @@ -416,15 +416,43 @@ type Context struct { // bundle collector unions these with the plan-time metadata tables to // ensure stats and schema are collected for all referenced tables. DeferredRoutineTableRefs []cat.Table + + // CapturedRoutineGists tracks which routine plan variants have already + // been captured during EXPLAIN ANALYZE execution. It serves dual + // purpose: + // - Capture signal: non-nil means we are inside an EXPLAIN ANALYZE + // and should capture routine body plans. Initialized by + // instrumentationHelper.Setup() for EXPLAIN ANALYZE output modes. + // - Dedup key: prevents duplicate captures for the same routine + + // plan shape across multiple call sites or invocations. + // The map key is "routineName:gistKey" where gistKey is the + // concatenation of per-body-statement plan gist strings. + // + // Thread safety: accessed only from the planGen closure on the + // connExecutor goroutine. No locking needed. + CapturedRoutineGists map[string]struct{} } -// DeferredRoutineOptPlan holds the formatted optimizer plan output for a -// single deferred SQL routine body, captured during execution. +// DeferredRoutineOptPlan holds plan information for a single deferred SQL +// routine body, captured during execution. type DeferredRoutineOptPlan struct { // Name is the function or procedure name. Name string // Plan is the formatted optimizer plan output (opt-vv level). Plan string + // ExplainPlan holds captured explain trees for each body statement, + // used to render routine sub-plans in EXPLAIN ANALYZE output. Each + // element is an *explain.Node but typed as any to avoid an import + // cycle (eval -> explain). + ExplainPlan []any + // BodyStmts holds the SQL text of each body statement, shown before + // its sub-plan in EXPLAIN ANALYZE output. + BodyStmts []string + // GistKey is the dedup key: the concatenation of per-body-statement + // plan gist strings (e.g., "AgFSAQAHAA==,AgHSBgEHAA=="). Two + // invocations are the same variant only if all per-statement gists + // match. + GistKey string } // RoutineStatementCounters encapsulates metrics for tracking the execution From b2038b37cf386259555eecedb0b8f43e059be8d1 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Tue, 19 May 2026 13:52:25 -0400 Subject: [PATCH 10/12] sql: add per-node execution stats to routine body plans in EXPLAIN ANALYZE Wire up execution stats (KV time, rows decoded, actual row count, etc.) for routine body plan nodes in EXPLAIN ANALYZE output. Two fixes: 1. Set associateNodeWithComponents on the inner PlanningCtx in runPlanInsidePlan so routine body exec.Nodes get mapped to component IDs in the shared trace metadata. 2. Call populateRoutinePlans before annotateExplain (and walk the routine plans during annotation) so stats are attached to the already-populated plan nodes. This is WIP: currently only the first execution's stats are shown for each {name, gist} combo. A follow-up commit will aggregate stats across all invocations. Epic: CRDB-46498 Release note: None Co-Authored-By: Claude Opus 4.6 --- pkg/sql/apply_join.go | 1 + pkg/sql/conn_executor_exec.go | 1 + pkg/sql/instrumentation.go | 6 + .../testdata/explain_analyze_routine | 74 +++++++ .../testdata/explain_analyze_routine_plans | 208 +++++++++++++++++- 5 files changed, 289 insertions(+), 1 deletion(-) diff --git a/pkg/sql/apply_join.go b/pkg/sql/apply_join.go index 92d20e10dd4c..81f0d8e9a7a9 100644 --- a/pkg/sql/apply_join.go +++ b/pkg/sql/apply_join.go @@ -342,6 +342,7 @@ func runPlanInsidePlan( planCtx := execCfg.DistSQLPlanner.NewPlanningCtx(ctx, evalCtx, &plannerCopy, plannerCopy.txn, distributeType) planCtx.distSQLBlockers = blockers planCtx.stmtType = recv.stmtType + planCtx.associateNodeWithComponents = plannerCopy.instrumentation.getAssociateNodeWithComponentsFn() if sqlStatsBuilder != nil && plannerCopy.instrumentation.ShouldSaveFlows() { planCtx.collectExecStats = true planCtx.saveFlows = getDefaultSaveFlowsFunc(ctx, &plannerCopy, planComponentTypeInner) diff --git a/pkg/sql/conn_executor_exec.go b/pkg/sql/conn_executor_exec.go index 32ad24bb04f0..323940f86c63 100644 --- a/pkg/sql/conn_executor_exec.go +++ b/pkg/sql/conn_executor_exec.go @@ -3346,6 +3346,7 @@ func populateQueryLevelStats( ih.queryLevelStatsWithErr.Stats.MaxMemUsage = p.execMon.MaximumBytes() } } + ih.populateRoutinePlans() if ih.traceMetadata != nil && ih.explainPlan != nil { ih.traceMetadata.annotateExplain( ctx, diff --git a/pkg/sql/instrumentation.go b/pkg/sql/instrumentation.go index 7f15aee1147b..7ef5fea18b63 100644 --- a/pkg/sql/instrumentation.go +++ b/pkg/sql/instrumentation.go @@ -912,6 +912,7 @@ func (ih *instrumentationHelper) populateRoutinePlans() { BodyStmts: dp.BodyStmts, }) } + ih.evalCtx.DeferredRoutineOptPlans = nil } // RecordPlanInfo records top-level information about the plan. @@ -1299,6 +1300,11 @@ func (m execNodeTraceMetadata) annotateExplain( m.annotateExplain(ctx, tp.(*explain.Plan), spans, makeDeterministic, p) } } + for _, rp := range plan.RoutinePlans { + for _, n := range rp.ExplainPlan { + walk(n) + } + } } // SetIndexRecommendations checks if we should generate a new index recommendation. diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine index 7b14b3d90881..ebd55703f467 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine @@ -93,11 +93,25 @@ regions: │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x │ └── • group (scalar) + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 1 │ └── • filter + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 10 │ filter: v > 1 │ └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 30 + actual row count: 10 missing stats table: t_ea@t_ea_pkey spans: FULL SCAN @@ -218,17 +232,35 @@ regions: │ │ sql: SELECT 1 │ │ │ └── • values + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 1 │ size: 1 column, 1 row │ └── • body stmt │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x │ └── • group (scalar) + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 1 │ └── • filter + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 10 │ filter: v > 1 │ └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 30 + actual row count: 10 missing stats table: t_ea@t_ea_pkey spans: FULL SCAN @@ -288,11 +320,25 @@ regions: │ │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x │ │ │ └── • group (scalar) + │ │ sql nodes: + │ │ regions: + │ │ execution time: 0µs + │ │ actual row count: 1 │ │ │ └── • filter + │ │ sql nodes: + │ │ regions: + │ │ execution time: 0µs + │ │ actual row count: 10 │ │ filter: v > 1 │ │ │ └── • scan + │ sql nodes: + │ kv nodes: + │ regions: + │ KV time: 0µs + │ KV rows decoded: 45 + │ actual row count: 5 │ missing stats │ table: t_ea@t_ea_pkey │ spans: FULL SCAN @@ -301,11 +347,25 @@ regions: │ sql: SELECT count(*)::INT8 FROM test.public.t_ea2 WHERE v > x │ └── • group (scalar) + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 1 │ └── • filter + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 4 │ filter: v > 1 │ └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 45 + actual row count: 5 missing stats table: t_ea2@t_ea2_pkey spans: FULL SCAN @@ -387,11 +447,25 @@ regions: │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x │ └── • group (scalar) + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 1 │ └── • filter + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 10 │ filter: v > 1 │ └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 60 + actual row count: 10 missing stats table: t_ea@t_ea_pkey spans: FULL SCAN diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans index 50b2158681a0..4d688eba91b0 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans @@ -55,6 +55,12 @@ regions: │ sql: SELECT b FROM test.public.t WHERE a = x │ └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 1 missing stats table: t@t_pkey spans: [/1 - /1] @@ -106,12 +112,22 @@ regions: │ │ sql: SELECT 1 │ │ │ └── • values + │ sql nodes: + │ regions: + │ execution time: 0µs + │ actual row count: 1 │ size: 1 column, 1 row │ └── • body stmt │ sql: SELECT b FROM test.public.t WHERE a = x │ └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 3 + actual row count: 1 missing stats table: t@t_pkey spans: [/1 - /1] @@ -158,6 +174,12 @@ regions: │ sql: SELECT b FROM test.public.t WHERE a = x │ └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 4 + actual row count: 0 missing stats table: t@t_pkey spans: [/1 - /1] @@ -209,6 +231,12 @@ regions: │ │ sql: SELECT b FROM test.public.t WHERE a = x │ │ │ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 1 │ missing stats │ table: t@t_pkey │ spans: [/1 - /1] @@ -220,6 +248,10 @@ regions: │ sql: SELECT x + 1 │ └── • values + sql nodes: + regions: + execution time: 0µs + actual row count: 1 size: 1 column, 1 row subtest end @@ -274,6 +306,10 @@ regions: │ │ sql: SELECT x * 10 │ │ │ └── • values +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 1 │ size: 1 column, 1 row │ └── • routine @@ -283,6 +319,10 @@ regions: │ sql: SELECT public.inner_fn(x) + 1 │ └── • values + sql nodes: + regions: + execution time: 0µs + actual row count: 1 size: 1 column, 1 row subtest end @@ -431,7 +471,163 @@ regions: subtest end # ------------------------------------------------------------------ -# 9. Plan variants: different argument values produce different plans. +# 9. SQL stored procedure. +# ------------------------------------------------------------------ +subtest sql_stored_procedure + +statement ok +CREATE PROCEDURE read_proc(x INT) LANGUAGE SQL AS $$ + SELECT b FROM t WHERE a = x; +$$ + +query T +EXPLAIN ANALYZE CALL read_proc(1) +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 1 (11 B, 0 KVs, 0 gRPC calls) +regions: +· +• root +│ +├── • call +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 0 +│ estimated row count: 0 +│ procedure: read_proc(1) +│ +└── • routine + │ name: read_proc + │ + ├── • body stmt + │ │ sql: SELECT b FROM test.public.t WHERE a = x + │ │ + │ └── • scan + │ sql nodes: + │ kv nodes: + │ regions: + │ KV time: 0µs + │ KV rows decoded: 1 + │ actual row count: 1 + │ missing stats + │ table: t@t_pkey + │ spans: [/1 - /1] + │ + └── • body stmt + │ + └── • values + size: 1 column, 1 row + +subtest end + +# ------------------------------------------------------------------ +# 10. PL/pgSQL stored procedure with SELECT INTO. +# ------------------------------------------------------------------ +subtest plpgsql_stored_procedure + +statement ok +CREATE PROCEDURE read_proc_plpgsql(x INT) LANGUAGE PLpgSQL AS $$ +DECLARE result INT; +BEGIN + SELECT b INTO result FROM t WHERE a = x; + RAISE NOTICE 'result: %', result; +END +$$ + +query T +EXPLAIN ANALYZE CALL read_proc_plpgsql(1) +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 1 (11 B, 0 KVs, 0 gRPC calls) +regions: +· +• root +│ +├── • call +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 0 +│ estimated row count: 0 +│ procedure: read_proc_plpgsql(1) +│ +├── • routine +│ │ name: _stmt_raise_3 +│ │ +│ ├── • body stmt +│ │ │ +│ │ └── • values +│ │ size: 1 column, 1 row +│ │ +│ └── • body stmt +│ │ +│ └── • values +│ size: 1 column, 1 row +│ +├── • routine +│ │ name: _stmt_exec_ret_2 +│ │ +│ └── • body stmt +│ │ +│ └── • values +│ size: 1 column, 1 row +│ +├── • routine +│ │ name: _stmt_exec_1 +│ │ +│ └── • body stmt +│ │ sql: SELECT b FROM test.public.t WHERE a = x +│ │ +│ └── • render +│ │ execution time: 0µs +│ │ actual row count: 1 +│ │ +│ └── • render +│ │ +│ └── • cross join (left outer) +│ │ sql nodes: +│ │ regions: +│ │ execution time: 0µs +│ │ actual row count: 1 +│ │ +│ ├── • emptyrow +│ │ sql nodes: +│ │ regions: +│ │ execution time: 0µs +│ │ actual row count: 1 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 1 +│ actual row count: 1 +│ missing stats +│ table: t@t_pkey +│ spans: [/1 - /1] +│ +└── • routine + │ name: read_proc_plpgsql + │ + └── • body stmt + │ + └── • render + │ + └── • values + size: 1 column, 1 row + +subtest end + +# ------------------------------------------------------------------ +# 11. Plan variants: different argument values produce different plans. # Non-NULL values produce a point lookup; NULL produces a # contradiction (a = NULL is always false). Both variants should # appear as separate routine sections. @@ -468,6 +664,12 @@ regions: │ │ sql: SELECT b FROM test.public.t WHERE a = x │ │ │ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 1 +│ actual row count: 1 │ missing stats │ table: t@t_pkey │ spans: [/1 - /1] @@ -479,5 +681,9 @@ regions: │ sql: SELECT b FROM test.public.t WHERE a = x │ └── • norows + sql nodes: + regions: + execution time: 0µs + actual row count: 0 subtest end From 83c39ca4990fd837100085db9a455d1deaf43043 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Tue, 26 May 2026 14:54:24 -0400 Subject: [PATCH 11/12] sql: add invocation counts and variant labels to routine plans in EXPLAIN ANALYZE Routine body plans in EXPLAIN ANALYZE now show how many times each routine variant was invoked (`invocations: N`), and when a routine has multiple distinct plan shapes (e.g. point lookup vs norows due to NULL args), each is labeled with `plan variant: X of Y`. The implementation changes CapturedRoutineGists from a set to a counter map, increments on every invocation, and populates InvocationCount and variant numbering in populateRoutinePlans. Routine plans are also sorted by name for deterministic output ordering. Epic: none Release note (sql change): EXPLAIN ANALYZE now shows invocation counts and plan variant labels for routine body plans. Co-Authored-By: Claude Opus 4.6 --- pkg/sql/instrumentation.go | 39 +- pkg/sql/opt/exec/execbuilder/scalar.go | 4 +- .../testdata/explain_analyze_routine | 5 + .../testdata/explain_analyze_routine_plans | 338 ++++++++++++++++-- pkg/sql/opt/exec/explain/emit.go | 4 + pkg/sql/opt/exec/explain/explain_factory.go | 8 + pkg/sql/sem/eval/context.go | 5 +- 7 files changed, 373 insertions(+), 30 deletions(-) diff --git a/pkg/sql/instrumentation.go b/pkg/sql/instrumentation.go index 7ef5fea18b63..c3958676834c 100644 --- a/pkg/sql/instrumentation.go +++ b/pkg/sql/instrumentation.go @@ -7,6 +7,7 @@ package sql import ( "bytes" + "cmp" "context" "fmt" "slices" @@ -485,7 +486,7 @@ func (ih *instrumentationHelper) Setup( // starts fresh; both the bundle code and populateRoutinePlans // read DeferredRoutineOptPlans during Finish of this statement. if ih.evalCtx != nil { - ih.evalCtx.CapturedRoutineGists = make(map[string]struct{}) + ih.evalCtx.CapturedRoutineGists = make(map[string]int) ih.evalCtx.DeferredRoutineOptPlans = nil } @@ -493,7 +494,7 @@ func (ih *instrumentationHelper) Setup( ih.discardRows = true // Initialize capture state for routine body plans. See above. if ih.evalCtx != nil { - ih.evalCtx.CapturedRoutineGists = make(map[string]struct{}) + ih.evalCtx.CapturedRoutineGists = make(map[string]int) ih.evalCtx.DeferredRoutineOptPlans = nil } @@ -906,13 +907,41 @@ func (ih *instrumentationHelper) populateRoutinePlans() { for i, n := range dp.ExplainPlan { nodes[i] = n.(*explain.Node) } + // Look up the invocation count for this variant. + dedupKey := dp.Name + ":" + dp.GistKey + invocations := ih.evalCtx.CapturedRoutineGists[dedupKey] ih.explainPlan.RoutinePlans = append(ih.explainPlan.RoutinePlans, explain.RoutinePlanInfo{ - Name: dp.Name, - ExplainPlan: nodes, - BodyStmts: dp.BodyStmts, + Name: dp.Name, + ExplainPlan: nodes, + BodyStmts: dp.BodyStmts, + InvocationCount: invocations, }) } ih.evalCtx.DeferredRoutineOptPlans = nil + + // Sort routine plans by name for deterministic output. Within the + // same name, preserve capture order (stable sort). + slices.SortStableFunc(ih.explainPlan.RoutinePlans, func(a, b explain.RoutinePlanInfo) int { + return cmp.Compare(a.Name, b.Name) + }) + + // Compute variant numbering: count plans per routine name and + // assign 1-based VariantIdx and TotalVariants for names with + // more than one distinct plan shape. + nameCount := make(map[string]int) + for i := range ih.explainPlan.RoutinePlans { + nameCount[ih.explainPlan.RoutinePlans[i].Name]++ + } + nameIdx := make(map[string]int) + for i := range ih.explainPlan.RoutinePlans { + rp := &ih.explainPlan.RoutinePlans[i] + total := nameCount[rp.Name] + rp.TotalVariants = total + if total > 1 { + nameIdx[rp.Name]++ + rp.VariantIdx = nameIdx[rp.Name] + } + } } // RecordPlanInfo records top-level information about the plan. diff --git a/pkg/sql/opt/exec/execbuilder/scalar.go b/pkg/sql/opt/exec/execbuilder/scalar.go index 53fce750748f..f9ccdfff2d2c 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -1484,8 +1484,8 @@ func (b *Builder) buildRoutinePlanGenerator( if captureExplain && len(explainNodes) > 0 { gistKey := strings.Join(gistStrs, ",") dedupKey := routineName + ":" + gistKey - if _, ok := b.evalCtx.CapturedRoutineGists[dedupKey]; !ok { - b.evalCtx.CapturedRoutineGists[dedupKey] = struct{}{} + b.evalCtx.CapturedRoutineGists[dedupKey]++ + if b.evalCtx.CapturedRoutineGists[dedupKey] == 1 { bodyStmtTexts := make([]string, len(explainNodes)) for j := range bodyStmtTexts { if j < len(stmtASTs) && stmtASTs[j] != nil { diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine index ebd55703f467..c178e1b29c0c 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine @@ -88,6 +88,7 @@ regions: │ └── • routine │ name: count_above_volatile + │ invocations: 3 │ └── • body stmt │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x @@ -165,6 +166,7 @@ regions: │ └── • routine │ name: count_above_plpgsql + │ invocations: 3 │ └── • body stmt │ @@ -227,6 +229,7 @@ regions: │ └── • routine │ name: count_above_multi + │ invocations: 3 │ ├── • body stmt │ │ sql: SELECT 1 @@ -315,6 +318,7 @@ regions: │ └── • routine │ name: multi_scan + │ invocations: 3 │ ├── • body stmt │ │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x @@ -442,6 +446,7 @@ regions: │ └── • routine │ name: count_above_volatile + │ invocations: 6 │ └── • body stmt │ sql: SELECT count(*)::INT8 FROM test.public.t_ea WHERE v > x diff --git a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans index 4d688eba91b0..ff2e20255171 100644 --- a/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans +++ b/pkg/sql/opt/exec/execbuilder/testdata/explain_analyze_routine_plans @@ -50,6 +50,7 @@ regions: │ └── • routine │ name: lookup_volatile + │ invocations: 3 │ └── • body stmt │ sql: SELECT b FROM test.public.t WHERE a = x @@ -107,6 +108,7 @@ regions: │ └── • routine │ name: multi_stmt + │ invocations: 3 │ ├── • body stmt │ │ sql: SELECT 1 @@ -169,6 +171,7 @@ regions: │ └── • routine │ name: lookup_volatile + │ invocations: 6 │ └── • body stmt │ sql: SELECT b FROM test.public.t WHERE a = x @@ -226,6 +229,7 @@ regions: │ ├── • routine │ │ name: lookup_volatile +│ │ invocations: 3 │ │ │ └── • body stmt │ │ sql: SELECT b FROM test.public.t WHERE a = x @@ -243,6 +247,7 @@ regions: │ └── • routine │ name: other_udf + │ invocations: 3 │ └── • body stmt │ sql: SELECT x + 1 @@ -301,6 +306,7 @@ regions: │ ├── • routine │ │ name: inner_fn +│ │ invocations: 3 │ │ │ └── • body stmt │ │ sql: SELECT x * 10 @@ -314,6 +320,7 @@ regions: │ └── • routine │ name: outer_fn + │ invocations: 3 │ └── • body stmt │ sql: SELECT public.inner_fn(x) + 1 @@ -454,6 +461,7 @@ regions: │ └── • routine │ name: lookup_plpgsql + │ invocations: 3 │ └── • body stmt │ @@ -502,6 +510,7 @@ regions: │ └── • routine │ name: read_proc + │ invocations: 1 │ ├── • body stmt │ │ sql: SELECT b FROM test.public.t WHERE a = x @@ -559,28 +568,8 @@ regions: │ procedure: read_proc_plpgsql(1) │ ├── • routine -│ │ name: _stmt_raise_3 -│ │ -│ ├── • body stmt -│ │ │ -│ │ └── • values -│ │ size: 1 column, 1 row -│ │ -│ └── • body stmt -│ │ -│ └── • values -│ size: 1 column, 1 row -│ -├── • routine -│ │ name: _stmt_exec_ret_2 -│ │ -│ └── • body stmt -│ │ -│ └── • values -│ size: 1 column, 1 row -│ -├── • routine │ │ name: _stmt_exec_1 +│ │ invocations: 1 │ │ │ └── • body stmt │ │ sql: SELECT b FROM test.public.t WHERE a = x @@ -614,8 +603,32 @@ regions: │ table: t@t_pkey │ spans: [/1 - /1] │ +├── • routine +│ │ name: _stmt_exec_ret_2 +│ │ invocations: 1 +│ │ +│ └── • body stmt +│ │ +│ └── • values +│ size: 1 column, 1 row +│ +├── • routine +│ │ name: _stmt_raise_3 +│ │ invocations: 1 +│ │ +│ ├── • body stmt +│ │ │ +│ │ └── • values +│ │ size: 1 column, 1 row +│ │ +│ └── • body stmt +│ │ +│ └── • values +│ size: 1 column, 1 row +│ └── • routine │ name: read_proc_plpgsql + │ invocations: 1 │ └── • body stmt │ @@ -659,6 +672,8 @@ regions: │ ├── • routine │ │ name: lookup_volatile +│ │ plan variant: 1 of 2 +│ │ invocations: 1 │ │ │ └── • body stmt │ │ sql: SELECT b FROM test.public.t WHERE a = x @@ -676,6 +691,287 @@ regions: │ └── • routine │ name: lookup_volatile + │ plan variant: 2 of 2 + │ invocations: 1 + │ + └── • body stmt + │ sql: SELECT b FROM test.public.t WHERE a = x + │ + └── • norows + sql nodes: + regions: + execution time: 0µs + actual row count: 0 + +subtest end + +# ------------------------------------------------------------------ +# 12. Many invocations of a single-variant UDF (invocation count). +# ------------------------------------------------------------------ +subtest many_invocations + +query T +EXPLAIN ANALYZE SELECT lookup_volatile(a) FROM t WHERE a <= 5 +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 10 (95 B, 5 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 5 +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 5 +│ actual row count: 5 +│ missing stats +│ table: t@t_pkey +│ spans: [ - /5] +│ +└── • routine + │ name: lookup_volatile + │ invocations: 5 + │ + └── • body stmt + │ sql: SELECT b FROM test.public.t WHERE a = x + │ + └── • scan + sql nodes: + kv nodes: + regions: + KV time: 0µs + KV rows decoded: 5 + actual row count: 1 + missing stats + table: t@t_pkey + spans: [/1 - /1] + +subtest end + +# ------------------------------------------------------------------ +# 13. Multi-variant with unequal invocation counts. +# Non-NULL values produce one plan variant (point lookup), NULL +# produces a different variant (norows). We invoke with 3 non-NULLs +# and 2 NULLs. +# ------------------------------------------------------------------ +subtest multi_variant_unequal_counts + +query T +EXPLAIN ANALYZE + SELECT lookup_volatile(a) + FROM (VALUES (1), (2), (3), (NULL::INT), (NULL::INT)) AS v(a) +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 3 (33 B, 0 KVs, 0 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 5 +│ │ +│ └── • values +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 5 +│ size: 1 column, 5 rows +│ +├── • routine +│ │ name: lookup_volatile +│ │ plan variant: 1 of 2 +│ │ invocations: 3 +│ │ +│ └── • body stmt +│ │ sql: SELECT b FROM test.public.t WHERE a = x +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 3 +│ actual row count: 1 +│ missing stats +│ table: t@t_pkey +│ spans: [/1 - /1] +│ +└── • routine + │ name: lookup_volatile + │ plan variant: 2 of 2 + │ invocations: 2 + │ + └── • body stmt + │ sql: SELECT b FROM test.public.t WHERE a = x + │ + └── • norows + sql nodes: + regions: + execution time: 0µs + actual row count: 0 + +subtest end + +# ------------------------------------------------------------------ +# 14. UDF used in WHERE clause. +# ------------------------------------------------------------------ +subtest udf_in_where + +statement ok +CREATE FUNCTION is_positive(x INT) RETURNS BOOL VOLATILE LANGUAGE SQL AS $$ + SELECT x > 0 +$$ + +query T +EXPLAIN ANALYZE SELECT * FROM t WHERE is_positive(b) +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 10 (80 B, 20 KVs, 10 gRPC calls) +regions: +· +• root +│ +├── • filter +│ │ sql nodes: +│ │ regions: +│ │ execution time: 0µs +│ │ actual row count: 10 +│ │ filter: is_positive(b) +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 10 +│ actual row count: 10 +│ missing stats +│ table: t@t_pkey +│ spans: FULL SCAN +│ +└── • routine + │ name: is_positive + │ invocations: 10 + │ + └── • body stmt + │ sql: SELECT x > 0 + │ + └── • values + sql nodes: + regions: + execution time: 0µs + actual row count: 1 + size: 1 column, 1 row + +subtest end + +# ------------------------------------------------------------------ +# 15. Multiple distinct routines, each with variants. +# ------------------------------------------------------------------ +subtest multiple_routines_with_variants + +statement ok +CREATE FUNCTION other_lookup(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ + SELECT b FROM t WHERE a = x +$$ + +query T +EXPLAIN ANALYZE + SELECT lookup_volatile(a), other_lookup(a) + FROM (VALUES (1), (NULL::INT)) AS v(a) +---- +planning time: 10µs +execution time: 100µs +distribution: +plan type: custom +rows decoded from KV: 2 (22 B, 0 KVs, 0 gRPC calls) +regions: +· +• root +│ +├── • render +│ │ execution time: 0µs +│ │ actual row count: 2 +│ │ +│ └── • values +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 2 +│ size: 1 column, 2 rows +│ +├── • routine +│ │ name: lookup_volatile +│ │ plan variant: 1 of 2 +│ │ invocations: 1 +│ │ +│ └── • body stmt +│ │ sql: SELECT b FROM test.public.t WHERE a = x +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 2 +│ actual row count: 1 +│ missing stats +│ table: t@t_pkey +│ spans: [/1 - /1] +│ +├── • routine +│ │ name: lookup_volatile +│ │ plan variant: 2 of 2 +│ │ invocations: 1 +│ │ +│ └── • body stmt +│ │ sql: SELECT b FROM test.public.t WHERE a = x +│ │ +│ └── • norows +│ sql nodes: +│ regions: +│ execution time: 0µs +│ actual row count: 0 +│ +├── • routine +│ │ name: other_lookup +│ │ plan variant: 1 of 2 +│ │ invocations: 1 +│ │ +│ └── • body stmt +│ │ sql: SELECT b FROM test.public.t WHERE a = x +│ │ +│ └── • scan +│ sql nodes: +│ kv nodes: +│ regions: +│ KV time: 0µs +│ KV rows decoded: 2 +│ actual row count: 1 +│ missing stats +│ table: t@t_pkey +│ spans: [/1 - /1] +│ +└── • routine + │ name: other_lookup + │ plan variant: 2 of 2 + │ invocations: 1 │ └── • body stmt │ sql: SELECT b FROM test.public.t WHERE a = x diff --git a/pkg/sql/opt/exec/explain/emit.go b/pkg/sql/opt/exec/explain/emit.go index 0ce3977b8456..c309d98c92cd 100644 --- a/pkg/sql/opt/exec/explain/emit.go +++ b/pkg/sql/opt/exec/explain/emit.go @@ -223,6 +223,10 @@ func emitInternal( for _, rp := range plan.RoutinePlans { ob.EnterMetaNode("routine") ob.Attr("name", rp.Name) + if rp.TotalVariants > 1 { + ob.Attrf("plan variant", "%d of %d", rp.VariantIdx, rp.TotalVariants) + } + ob.Attrf("invocations", "%d", rp.InvocationCount) for j, bodyNode := range rp.ExplainPlan { ob.EnterMetaNode("body stmt") if j < len(rp.BodyStmts) && rp.BodyStmts[j] != "" { diff --git a/pkg/sql/opt/exec/explain/explain_factory.go b/pkg/sql/opt/exec/explain/explain_factory.go index 3447de231b4d..4c71b83655a3 100644 --- a/pkg/sql/opt/exec/explain/explain_factory.go +++ b/pkg/sql/opt/exec/explain/explain_factory.go @@ -142,6 +142,14 @@ type RoutinePlanInfo struct { ExplainPlan []*Node // BodyStmts holds the SQL text of each body statement. BodyStmts []string + // InvocationCount is the number of times this variant was invoked. + InvocationCount int + // VariantIdx is the 1-based index of this variant among all variants + // for the same routine name. Only meaningful when TotalVariants > 1. + VariantIdx int + // TotalVariants is the total number of distinct plan variants for + // this routine name. When 1, variant labeling is omitted. + TotalVariants int } var _ exec.Plan = &Plan{} diff --git a/pkg/sql/sem/eval/context.go b/pkg/sql/sem/eval/context.go index 9cd4c6866470..b7c2b81dd7d5 100644 --- a/pkg/sql/sem/eval/context.go +++ b/pkg/sql/sem/eval/context.go @@ -426,11 +426,12 @@ type Context struct { // - Dedup key: prevents duplicate captures for the same routine + // plan shape across multiple call sites or invocations. // The map key is "routineName:gistKey" where gistKey is the - // concatenation of per-body-statement plan gist strings. + // concatenation of per-body-statement plan gist strings. The value + // tracks the number of times each variant was invoked. // // Thread safety: accessed only from the planGen closure on the // connExecutor goroutine. No locking needed. - CapturedRoutineGists map[string]struct{} + CapturedRoutineGists map[string]int } // DeferredRoutineOptPlan holds plan information for a single deferred SQL From d3812c0fd2b6db27414f15c60c9ebb825abcdf19 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Tue, 2 Jun 2026 12:25:03 -0400 Subject: [PATCH 12/12] some fix --- demo.sql | 269 +++++++++++++++++++++++++ pkg/sql/opt/exec/execbuilder/scalar.go | 10 +- 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 demo.sql diff --git a/demo.sql b/demo.sql new file mode 100644 index 000000000000..9b60a7e64210 --- /dev/null +++ b/demo.sql @@ -0,0 +1,269 @@ +CREATE PROCEDURE foo(x INT) LANGUAGE SQL AS $$ +SELECT 1; +SELECT 2; +$$; + +EXPLAIN ANALYZE CALL foo(3); + + +-------------------------------------------------------------------------------- +-- EXPLAIN ANALYZE with routines (functions, procedures, triggers). +-- +-- Demonstrates how routine body plans, invocation counts, and per-node +-- execution stats surface in EXPLAIN ANALYZE output. The emphasis is on +-- stored procedures (CALL), with UDF and trigger coverage for contrast. +-- +-- Run with: +-- ./cockroach demo --empty --no-line-editor < demo.sql +-------------------------------------------------------------------------------- + +CREATE TABLE t (a INT PRIMARY KEY, b INT, FAMILY (a, b)); +INSERT INTO t SELECT i, i*10 FROM generate_series(1, 10) AS g(i); + + +-------------------------------------------------------------------------------- +-- PART 1: STORED PROCEDURES (CALL) +-- +-- EXPLAIN ANALYZE CALL shows a `call` node above the procedure's routine +-- body. Each body statement becomes a routine node named `_stmt__` +-- (or the callee's name for nested CALLs / UDF references). +-------------------------------------------------------------------------------- + +-- 1. SQL procedure. +CREATE PROCEDURE bump_sql(x INT) LANGUAGE SQL AS $$ + UPDATE t SET b = b + 1 WHERE a = x; +$$; + +EXPLAIN ANALYZE CALL bump_sql(1); + + +-- 2. SQL procedure with an OUT parameter (returns a row). +CREATE PROCEDURE get_b(IN x INT, OUT res INT) LANGUAGE SQL AS $$ + SELECT b FROM t WHERE a = x +$$; + +EXPLAIN ANALYZE CALL get_b(3, NULL); + + +-- 3. SQL procedure running a multi-table join. The routine body captures the +-- full plan of a non-trivial statement — the join operator and both input +-- scans appear under the `body stmt`, each with its own execution stats. +CREATE TABLE u (a INT PRIMARY KEY, c INT); +INSERT INTO u SELECT i, i*100 FROM generate_series(1, 10) AS g(i); + +CREATE PROCEDURE join_summary(lim INT) LANGUAGE SQL AS $$ + SELECT t.a, t.b, u.c + FROM t JOIN u ON t.a = u.a + WHERE t.b > lim + ORDER BY t.a; +$$; + +EXPLAIN ANALYZE CALL join_summary(30); + + +-- 4. PL/pgSQL procedure (no control flow). +CREATE PROCEDURE bump_plpgsql(x INT) LANGUAGE PLpgSQL AS $$ +BEGIN + UPDATE t SET b = b + 1 WHERE a = x; +END +$$; + +EXPLAIN ANALYZE CALL bump_plpgsql(2); + + +-- 5. PL/pgSQL procedure with an INOUT parameter. +CREATE PROCEDURE inc(INOUT v INT) LANGUAGE PLpgSQL AS $$ +BEGIN + v := v + 1; +END +$$; + +EXPLAIN ANALYZE CALL inc(41); + + +-- 6. Multi-statement DML procedure (INSERT / UPDATE / DELETE). Each body +-- statement becomes its own routine node (`_stmt_exec_1/2/3`), and the +-- mutation operator reports its `actual row count` (3 inserted, 5 +-- updated, 3 deleted). +CREATE PROCEDURE dml_proc() LANGUAGE PLpgSQL AS $$ +BEGIN + INSERT INTO t VALUES (100, 1000), (101, 1010), (102, 1020); + UPDATE t SET b = b + 1 WHERE a <= 5; + DELETE FROM t WHERE a >= 100; +END +$$; + +EXPLAIN ANALYZE CALL dml_proc(); + + +-- 7. Procedure calling another procedure (nested CALL). The callee appears +-- as its own routine node beneath the caller's body. +CREATE PROCEDURE inner_proc(x INT) LANGUAGE SQL AS $$ + UPDATE t SET b = b + 1 WHERE a = x; +$$; + +CREATE PROCEDURE outer_proc(x INT) LANGUAGE PLpgSQL AS $$ +BEGIN + CALL inner_proc(x); +END +$$; + +EXPLAIN ANALYZE CALL outer_proc(1); + + +-- 8. Procedure calling a UDF: the function shows up as a nested routine. +CREATE FUNCTION dbl(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ +SELECT x * 2 +$$; + +CREATE PROCEDURE set_dbl(x INT) LANGUAGE PLpgSQL AS $$ +BEGIN + UPDATE t SET b = dbl(x) WHERE a = x; +END +$$; + +EXPLAIN ANALYZE CALL set_dbl(1); + + +-- 9. Procedure with in-body transaction control (COMMIT) — a +-- procedure-only capability with no UDF equivalent. +CREATE PROCEDURE txn_proc() LANGUAGE PLpgSQL AS $$ +BEGIN + UPDATE t SET b = b + 1 WHERE a = 1; + COMMIT; +END +$$; + +EXPLAIN ANALYZE CALL txn_proc(); + + +-- 10. PL/pgSQL procedure whose body uses control flow (a FOR loop), invoked +-- via CALL. Each control-flow block compiles into a nested sub-routine +-- planned lazily at execution time; under EXPLAIN ANALYZE these appear as +-- their own routine nodes (e.g. `stmt_loop_*`) with per-iteration +-- invocation counts. +CREATE PROCEDURE bump_loop(n INT) LANGUAGE PLpgSQL AS $$ +BEGIN + FOR i IN 1..n LOOP + UPDATE t SET b = b + 1 WHERE a = i; + END LOOP; +END +$$; + +EXPLAIN ANALYZE CALL bump_loop(3); + + +-- 11. PL/pgSQL procedure with an IF/ELSE branch, invoked via CALL. The taken +-- branch's body is captured as a nested routine node. +CREATE PROCEDURE classify(x INT) LANGUAGE PLpgSQL AS $$ +BEGIN + IF x > 5 THEN UPDATE t SET b = 1 WHERE a = x; + ELSE UPDATE t SET b = 0 WHERE a = x; + END IF; +END +$$; + +EXPLAIN ANALYZE CALL classify(7); + + +-------------------------------------------------------------------------------- +-- PART 2: USER-DEFINED FUNCTIONS +-------------------------------------------------------------------------------- + +-- 12. Scalar SQL UDF in a projection. +-- A single routine node with `invocations` and a `body stmt` subplan. +CREATE FUNCTION lookup_volatile(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ +SELECT b FROM t WHERE a = x +$$; + +EXPLAIN ANALYZE (VERBOSE) SELECT lookup_volatile(a) FROM t WHERE a <= 3; + + +-- 13. Nested UDF calls (outer_fn calls inner_fn): body plans nest. +CREATE FUNCTION inner_fn(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ +SELECT x * 10 +$$; + +CREATE FUNCTION outer_fn(x INT) RETURNS INT VOLATILE LANGUAGE SQL AS $$ +SELECT inner_fn(x) + 1 +$$; + +EXPLAIN ANALYZE SELECT outer_fn(a) FROM t WHERE a <= 3; + + +-- 14. Multiple plan variants for the same routine. NULL vs non-NULL inputs +-- produce different body plans; each is labeled "plan variant: N of M" +-- with its own invocation count. +EXPLAIN ANALYZE +SELECT lookup_volatile(a) +FROM (VALUES (1), (2), (3), (NULL::INT), (NULL::INT)) AS v(a); + + +-- 15. Set-returning UDF (SETOF) in the FROM clause. +CREATE FUNCTION top_b(lim INT) RETURNS SETOF INT VOLATILE LANGUAGE SQL AS $$ +SELECT b FROM t ORDER BY b DESC LIMIT lim +$$; + +EXPLAIN ANALYZE SELECT * FROM top_b(3); + + +-- 16. Volatile UDF in a WHERE filter (invoked once per scanned row). +CREATE FUNCTION is_big(x INT) RETURNS BOOL VOLATILE LANGUAGE SQL AS $$ +SELECT x > 50 +$$; + +EXPLAIN ANALYZE SELECT a FROM t WHERE is_big(b); + + +-- 17. PL/pgSQL *function* with control flow (a loop) invoked via SELECT. The +-- same control-flow loop works both here (function via SELECT) and in a +-- CALL'd procedure (see examples 10-11): the loop expands into nested +-- routine bodies in the plan. +CREATE FUNCTION sum_to(n INT) RETURNS INT LANGUAGE PLpgSQL AS $$ +DECLARE + s INT := 0; +BEGIN + FOR i IN 1..n LOOP + s := s + i; + END LOOP; + RETURN s; +END +$$; + +EXPLAIN ANALYZE SELECT sum_to(a) FROM t WHERE a <= 3; + + +-------------------------------------------------------------------------------- +-- PART 3: TRIGGERS +-------------------------------------------------------------------------------- + +-- 18. AFTER INSERT trigger: the fired trigger function appears under an +-- `after-triggers` node. +CREATE TABLE audit (id INT PRIMARY KEY DEFAULT unique_rowid(), a INT, b INT); + +CREATE FUNCTION audit_insert() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ +BEGIN + INSERT INTO audit (a, b) VALUES ((NEW).a, (NEW).b); + RETURN NEW; +END +$$; + +CREATE TRIGGER trg_audit AFTER INSERT ON t + FOR EACH ROW EXECUTE FUNCTION audit_insert(); + +EXPLAIN ANALYZE INSERT INTO t VALUES (100, 1000); + + +-- 19. BEFORE INSERT trigger that mutates the incoming row: appears under a +-- `before-triggers` node. +CREATE FUNCTION bump_before() RETURNS TRIGGER LANGUAGE PLpgSQL AS $$ +BEGIN + NEW.b := (NEW).b + 1; + RETURN NEW; +END +$$; + +CREATE TRIGGER trg_bump BEFORE INSERT ON t + FOR EACH ROW EXECUTE FUNCTION bump_before(); + +EXPLAIN ANALYZE INSERT INTO t VALUES (200, 2000); diff --git a/pkg/sql/opt/exec/execbuilder/scalar.go b/pkg/sql/opt/exec/execbuilder/scalar.go index f9ccdfff2d2c..5b17d6a903ca 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -905,7 +905,15 @@ func (b *Builder) buildSubquery( if len(eb.checks) > 0 { return expectedLazyRoutineError("check") } - plan, err := b.factory.ConstructPlan( + // Construct the plan with the same factory that built the nodes + // above (ef), not the enclosing builder's factory (b.factory). + // Under EXPLAIN ANALYZE, b.factory may be an explain.Factory + // captured when the enclosing routine body was built, while the + // nodes here were built lazily at execution time with the plain + // runtime factory ef. Using b.factory would both panic on the + // root.(*Node) assertion and yield a non-executable *explain.Plan. + // This mirrors the recursive-CTE builder (see relational.go). + plan, err := ef.ConstructPlan( ePlan.root, eb.subqueries, eb.cascades, eb.triggers, eb.checks, inputRowCount, eb.flags, ) if err != nil {