From c21c3b4196b706a191041435970e2f04cc98e45f Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 13:36:52 -0400 Subject: [PATCH 1/9] 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 9d612e44db11c7ae8ff31174f25fc2b96d43983f Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 13:37:00 -0400 Subject: [PATCH 2/9] 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 81ea70a68cba..25c28dd67c71 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 08b7a733a581..365f44c70e8f 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -714,7 +714,8 @@ func (b *Builder) buildExistsSubquery( nil, /* stmtASTs */ true, /* allowOuterWithRefs */ wrapRootExpr, - 0, /* resultBufferID */ + 0, /* resultBufferID */ + nil, /* bodyBuilder */ ) return tree.NewTypedCoalesceExpr(tree.TypedExprs{ tree.NewTypedRoutineExpr( @@ -843,6 +844,7 @@ func (b *Builder) buildSubquery( true, /* allowOuterWithRefs */ nil, /* wrapRootExpr */ 0, /* resultBufferID */ + nil, /* bodyBuilder */ ) _, tailCall := b.tailCalls[subquery] return tree.NewTypedRoutineExpr( @@ -1014,7 +1016,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 " + @@ -1039,6 +1041,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 @@ -1114,6 +1117,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. @@ -1165,6 +1169,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 @@ -1212,6 +1217,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 2e2edb83eef41316b91eb4ec9620c3b9e6ce91c7 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Wed, 13 May 2026 15:11:53 -0400 Subject: [PATCH 3/9] 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 | 4 +- .../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 +- .../scbuild/testdata/drop_function | 2 +- 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 + 76 files changed, 1567 insertions(+), 48 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 eb2fe46fc9bc..9cccb5b858f2 100644 --- a/docs/generated/settings/settings-for-tenants.txt +++ b/docs/generated/settings/settings-for-tenants.txt @@ -447,4 +447,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 1928833caa49..267376e5bffa 100644 --- a/docs/generated/settings/settings.html +++ b/docs/generated/settings/settings.html @@ -408,6 +408,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 21993fd65d73..a7ce14483138 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" @@ -135,6 +136,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 } @@ -274,6 +286,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) @@ -620,6 +643,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..a011826f59b6 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"}} @@ -153,5 +153,5 @@ SELECT least(id, 999), strip_volatile(descriptor) FROM crdb_internal.kv_catalog_ 110 {"type": {"alias": {"arrayContents": {"family": "EnumFamily", "oid": 100109, "udtMetadata": {"arrayTypeOid": 100110}}, "family": "ArrayFamily", "oid": 100110}, "id": 110, "kind": "ALIAS", "name": "_greeting", "parentId": 106, "parentSchemaId": 108, "privileges": {"ownerProto": "root", "users": [{"privileges": "2", "userProto": "admin", "withGrantOption": "2"}, {"privileges": "512", "userProto": "public"}, {"privileges": "2", "userProto": "root", "withGrantOption": "2"}], "version": 3}, "version": "1"}} 111 {"table": {"checks": [{"columnIds": [1], "constraintId": 2, "expr": "k > 0:::INT8", "name": "ck"}], "columns": [{"id": 1, "name": "k", "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 2, "name": "v", "nullable": true, "type": {"family": "StringFamily", "oid": 25}}], "dependedOnBy": [{"columnIds": [1, 2], "id": 112}], "formatVersion": 3, "name": "kv", "nextColumnId": 3, "nextConstraintId": 3, "nextIndexId": 2, "nextMutationId": 1, "parentId": 106, "primaryIndex": {"constraintId": 1, "encodingType": 1, "foreignKey": {}, "geoConfig": {}, "id": 1, "interleave": {}, "keyColumnDirections": ["ASC"], "keyColumnIds": [1], "keyColumnNames": ["k"], "name": "kv_pkey", "partitioning": {}, "sharded": {}, "storeColumnIds": [2], "storeColumnNames": ["v"], "unique": true, "vecConfig": {}, "version": 4}, "privileges": {"ownerProto": "root", "users": [{"privileges": "2", "userProto": "admin", "withGrantOption": "2"}, {"privileges": "2", "userProto": "root", "withGrantOption": "2"}], "version": 3}, "replacementOf": {"time": {}}, "schemaLocked": true, "version": "7"}} 112 {"table": {"columns": [{"id": 1, "name": "k", "nullable": true, "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 2, "name": "v", "nullable": true, "type": {"family": "StringFamily", "oid": 25}}, {"defaultExpr": "unique_rowid()", "hidden": true, "id": 3, "name": "rowid", "type": {"family": "IntFamily", "oid": 20, "width": 64}}], "dependsOn": [111], "formatVersion": 3, "indexes": [{"createdExplicitly": true, "foreignKey": {}, "geoConfig": {}, "id": 2, "interleave": {}, "keyColumnDirections": ["ASC"], "keyColumnIds": [2], "keyColumnNames": ["v"], "keySuffixColumnIds": [3], "name": "idx", "partitioning": {}, "sharded": {}, "vecConfig": {}, "version": 4}], "isMaterializedView": true, "name": "mv", "nextColumnId": 4, "nextConstraintId": 2, "nextIndexId": 4, "nextMutationId": 1, "parentId": 106, "primaryIndex": {"constraintId": 1, "encodingType": 1, "foreignKey": {}, "geoConfig": {}, "id": 1, "interleave": {}, "keyColumnDirections": ["ASC"], "keyColumnIds": [3], "keyColumnNames": ["rowid"], "name": "mv_pkey", "partitioning": {}, "sharded": {}, "storeColumnIds": [1, 2], "storeColumnNames": ["k", "v"], "unique": true, "vecConfig": {}, "version": 4}, "privileges": {"ownerProto": "root", "users": [{"privileges": "2", "userProto": "admin", "withGrantOption": "2"}, {"privileges": "2", "userProto": "root", "withGrantOption": "2"}], "version": 3}, "replacementOf": {"time": {}}, "version": "8", "viewQuery": "SELECT k, v FROM db.public.kv"}} -113 {"function": {"functionBody": "SELECT json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(d, ARRAY['table':::STRING, 'families':::STRING]:::STRING[]), ARRAY['table':::STRING, 'nextFamilyId':::STRING]:::STRING[]), ARRAY['table':::STRING, 'id':::STRING]:::STRING[]), ARRAY['table':::STRING, 'unexposedParentSchemaId':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '0':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '1':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '2':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'primaryIndex':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'createAsOfTime':::STRING]:::STRING[]), ARRAY['table':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['function':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['type':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['schema':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['database':::STRING, 'modificationTime':::STRING]:::STRING[]);", "id": 113, "lang": "SQL", "name": "strip_volatile", "nullInputBehavior": "CALLED_ON_NULL_INPUT", "params": [{"class": "IN", "name": "d", "type": {"family": "JsonFamily", "oid": 3802}}], "parentId": 104, "parentSchemaId": 105, "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": "JsonFamily", "oid": 3802}}, "version": "1", "volatility": "STABLE"}} +113 {"function": {"canMutate": "CANNOT_MUTATE", "functionBody": "SELECT json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(d, ARRAY['table':::STRING, 'families':::STRING]:::STRING[]), ARRAY['table':::STRING, 'nextFamilyId':::STRING]:::STRING[]), ARRAY['table':::STRING, 'id':::STRING]:::STRING[]), ARRAY['table':::STRING, 'unexposedParentSchemaId':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '0':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '1':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '2':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'primaryIndex':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'createAsOfTime':::STRING]:::STRING[]), ARRAY['table':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['function':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['type':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['schema':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['database':::STRING, 'modificationTime':::STRING]:::STRING[]);", "id": 113, "lang": "SQL", "name": "strip_volatile", "nullInputBehavior": "CALLED_ON_NULL_INPUT", "params": [{"class": "IN", "name": "d", "type": {"family": "JsonFamily", "oid": 3802}}], "parentId": 104, "parentSchemaId": 105, "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": "JsonFamily", "oid": 3802}}, "version": "1", "volatility": "STABLE"}} 999 {"table": {"columns": [{"id": 1, "name": "f_table_catalog", "nullable": true, "type": {"family": "StringFamily", "oid": 19}}, {"id": 2, "name": "f_table_schema", "nullable": true, "type": {"family": "StringFamily", "oid": 19}}, {"id": 3, "name": "f_table_name", "nullable": true, "type": {"family": "StringFamily", "oid": 19}}, {"id": 4, "name": "f_geometry_column", "nullable": true, "type": {"family": "StringFamily", "oid": 19}}, {"id": 5, "name": "coord_dimension", "nullable": true, "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 6, "name": "srid", "nullable": true, "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 7, "name": "type", "nullable": true, "type": {"family": "StringFamily", "oid": 25}}], "formatVersion": 3, "name": "geometry_columns", "nextColumnId": 8, "nextConstraintId": 2, "nextIndexId": 2, "nextMutationId": 1, "primaryIndex": {"constraintId": 1, "foreignKey": {}, "geoConfig": {}, "id": 1, "interleave": {}, "partitioning": {}, "sharded": {}, "vecConfig": {}}, "privileges": {"ownerProto": "node", "users": [{"privileges": "32", "userProto": "public"}], "version": 3}, "replacementOf": {"time": {}}, "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 25c28dd67c71..0b8c96c62b39 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 365f44c70e8f..627a87a21188 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -1001,9 +1001,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 04e99cad5b20..e2c0a7325fee 100644 --- a/pkg/sql/opt/optbuilder/create_function.go +++ b/pkg/sql/opt/optbuilder/create_function.go @@ -385,6 +385,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. lateBinding cannot be true here: it requires @@ -411,6 +412,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 */) @@ -489,6 +493,9 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o }) if !lateBinding { 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 */) @@ -498,6 +505,8 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o panic(errors.AssertionFailedf("unexpected language: %v", language)) } + cf.CanMutate = canMutate + if !lateBinding { if stmtScope != nil && (language != tree.RoutineLangPLpgSQL || !isSetReturning) { // Validate that the result type of the last statement matches the 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 2e2deec1534a..6d4a0bdc4d65 100644 --- a/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go +++ b/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go @@ -211,7 +211,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, routineLazilyEvaluatesSQL(b, n, lang, typ), refProvider)) + fnBody := b.WrapFunctionBody(fnID, fnBodyStr, lang, routineLazilyEvaluatesSQL(b, n, 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, @@ -411,6 +415,9 @@ func replaceFunction( // Build the FunctionBody element with the new body and references. fnBody := b.WrapFunctionBody(fnID, fnBodyStr, lang, routineLazilyEvaluatesSQL(b, n, 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/scbuild/testdata/drop_function b/pkg/sql/schemachanger/scbuild/testdata/drop_function index f7c7f6bfbb85..8a8fb42fde43 100644 --- a/pkg/sql/schemachanger/scbuild/testdata/drop_function +++ b/pkg/sql/schemachanger/scbuild/testdata/drop_function @@ -46,4 +46,4 @@ DROP FUNCTION f; - [[FunctionParams:{DescID: 109}, ABSENT], PUBLIC] {functionId: 109, params: [{class: {}, name: a, type: {closedTypeIds: [107, 108], type: {family: EnumFamily, oid: 100107, udtMetadata: {arrayTypeOid: 100108}}, typeName: public.notmyworkday}}]} - [[FunctionBody:{DescID: 109}, ABSENT], PUBLIC] - {body: "SELECT a FROM defaultdb.public.t;\nSELECT b FROM defaultdb.public.t@t_idx_b;\nSELECT c FROM defaultdb.public.t@t_idx_c;\nSELECT a FROM defaultdb.public.v;\nSELECT nextval(105:::REGCLASS);", functionId: 109, lang: {lang: SQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [107, 108], usesViews: [{columnIds: [1], viewId: 106}]} + {body: "SELECT a FROM defaultdb.public.t;\nSELECT b FROM defaultdb.public.t@t_idx_b;\nSELECT c FROM defaultdb.public.t@t_idx_c;\nSELECT a FROM defaultdb.public.v;\nSELECT nextval(105:::REGCLASS);", canMutate: 2, functionId: 109, lang: {lang: SQL}, usesSequenceIds: [105], usesTables: [{columnIds: [1], tableId: 104}, {columnIds: [2], indexId: 2, tableId: 104}, {columnIds: [3], indexId: 3, tableId: 104}], usesTypeIds: [107, 108], usesViews: [{columnIds: [1], viewId: 106}]} 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 43591ce2c7ba61d7f0d895257101d3f40b187646 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 21 May 2026 16:05:37 +0000 Subject: [PATCH 4/9] sql: propagate CanMutate to callers on CREATE OR REPLACE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a function is replaced via CREATE OR REPLACE and becomes mutating, propagate CAN_MUTATE transitively to all caller functions via the DependedOnBy back-references. Only the non-mutating → mutating direction is propagated; the reverse is left conservative (callers keep CAN_MUTATE) because determining true non-mutating status would require re-analyzing each caller's entire body to ensure no child routine mutates at all. Release note: None Co-Authored-By: Claude Opus 4.6 --- pkg/sql/create_function.go | 61 +++ .../testdata/logic_test/udf_calling_udf | 477 ++++++++++++++++++ .../internal/scbuildstmt/create_function.go | 54 ++ 3 files changed, 592 insertions(+) diff --git a/pkg/sql/create_function.go b/pkg/sql/create_function.go index a7ce14483138..40e2d2bac980 100644 --- a/pkg/sql/create_function.go +++ b/pkg/sql/create_function.go @@ -278,6 +278,8 @@ func (n *createFunctionNode) replaceFunction( } } + oldCanMutate := udfDesc.GetCanMutate() + resetFuncOption(udfDesc) if err := validateVolatilityInOptions(n.cf.Options, udfDesc); err != nil { return err @@ -295,6 +297,15 @@ func (n *createFunctionNode) replaceFunction( // 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 the function became mutating, propagate CAN_MUTATE to all + // transitive callers so their descriptors stay accurate for + // deferred opt-building and leaf/root txn selection. + if udfDesc.GetCanMutate() == catpb.Function_CAN_MUTATE && + oldCanMutate != catpb.Function_CAN_MUTATE { + if err := propagateCanMutateToCallers(params, udfDesc); err != nil { + return err + } + } } // Removing all existing references before adding new references. @@ -807,3 +818,53 @@ func (n *createFunctionNode) validateParameters(udfDesc *funcdesc.Mutable) error } return nil } + +// propagateCanMutateToCallers transitively updates CAN_MUTATE on all +// functions that (directly or indirectly) call the given function. +// This is needed because a caller's persisted CanMutate is a snapshot +// from its creation time and becomes stale when a callee is replaced +// with a mutating body. Deferred opt-building relies on the descriptor +// value, so it must be kept current. +func propagateCanMutateToCallers(params runParams, fnDesc *funcdesc.Mutable) error { + visited := make(map[descpb.ID]struct{}) + visited[fnDesc.GetID()] = struct{}{} + return propagateCanMutateImpl(params, fnDesc.DependedOnBy, visited) +} + +func propagateCanMutateImpl( + params runParams, refs []descpb.FunctionDescriptor_Reference, visited map[descpb.ID]struct{}, +) error { + for i := range refs { + callerID := refs[i].ID + if _, ok := visited[callerID]; ok { + continue + } + visited[callerID] = struct{}{} + + desc, err := params.p.Descriptors().MutableByID(params.p.txn).Desc( + params.ctx, callerID, + ) + if err != nil { + return err + } + if desc.DescriptorType() != catalog.Function { + continue + } + callerFnDesc := desc.(*funcdesc.Mutable) + if callerFnDesc.GetCanMutate() == catpb.Function_CAN_MUTATE { + continue + } + callerFnDesc.SetCanMutate(catpb.Function_CAN_MUTATE) + if err := params.p.writeFuncSchemaChange( + params.ctx, callerFnDesc, + ); err != nil { + return err + } + if err := propagateCanMutateImpl( + params, callerFnDesc.DependedOnBy, visited, + ); err != nil { + return err + } + } + return nil +} diff --git a/pkg/sql/logictest/testdata/logic_test/udf_calling_udf b/pkg/sql/logictest/testdata/logic_test/udf_calling_udf index 2386e1f01b02..d571c527f560 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf_calling_udf +++ b/pkg/sql/logictest/testdata/logic_test/udf_calling_udf @@ -261,3 +261,480 @@ CREATE PROCEDURE public.p131354() $$ subtest end + +# Test that CREATE OR REPLACE propagates CanMutate to callers when a function's +# body changes from non-mutating to mutating. Deferred opt-building relies on +# the descriptor's CanMutate being accurate (no body to inspect as fallback), +# and CanMutate affects leaf/root txn selection. + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE TABLE can_mutate_t (a INT PRIMARY KEY) + +subtest can_mutate_direct_propagation + +# f2 is non-mutating, f1 calls f2. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f2_direct() RETURNS INT LANGUAGE SQL AS 'SELECT 1' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f1_direct() RETURNS INT LANGUAGE SQL AS 'SELECT f2_direct()' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_direct()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_direct()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +# Replace f2 with a mutating body. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE OR REPLACE FUNCTION f2_direct() RETURNS INT LANGUAGE SQL AS $$ + INSERT INTO can_mutate_t VALUES (1) ON CONFLICT DO NOTHING; + SELECT 1; +$$ + +# f2 should now be CAN_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_direct()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +# f1 should have been propagated to CAN_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_direct()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +DROP FUNCTION f1_direct; +DROP FUNCTION f2_direct; + +subtest end + +subtest can_mutate_transitive_chain + +# f1 -> f2 -> f3 -> f4, all non-mutating initially. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f4_chain() RETURNS INT LANGUAGE SQL AS 'SELECT 1' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f3_chain() RETURNS INT LANGUAGE SQL AS 'SELECT f4_chain()' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f2_chain() RETURNS INT LANGUAGE SQL AS 'SELECT f3_chain()' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f1_chain() RETURNS INT LANGUAGE SQL AS 'SELECT f2_chain()' + +# Verify all start as CANNOT_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_chain()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_chain()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f3_chain()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f4_chain()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +# Replace f4 with a mutating body. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE OR REPLACE FUNCTION f4_chain() RETURNS INT LANGUAGE SQL AS $$ + INSERT INTO can_mutate_t VALUES (2) ON CONFLICT DO NOTHING; + SELECT 1; +$$ + +# All four should now be CAN_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f4_chain()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f3_chain()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_chain()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_chain()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +DROP FUNCTION f1_chain; +DROP FUNCTION f2_chain; +DROP FUNCTION f3_chain; +DROP FUNCTION f4_chain; + +subtest end + +subtest can_mutate_short_circuit + +# f1 is already mutating (has its own INSERT), and also calls f2. +# Replacing f2 to be mutating should not error or redundantly update f1. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f2_sc() RETURNS INT LANGUAGE SQL AS 'SELECT 1' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f1_sc() RETURNS INT LANGUAGE SQL AS $$ + INSERT INTO can_mutate_t VALUES (3) ON CONFLICT DO NOTHING; + SELECT f2_sc(); +$$ + +# f1 is already CAN_MUTATE, f2 is CANNOT_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_sc()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_sc()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +# Replace f2 to be mutating. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE OR REPLACE FUNCTION f2_sc() RETURNS INT LANGUAGE SQL AS $$ + INSERT INTO can_mutate_t VALUES (4) ON CONFLICT DO NOTHING; + SELECT 1; +$$ + +# f2 now CAN_MUTATE; f1 was already CAN_MUTATE (short-circuited). +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_sc()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_sc()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +DROP FUNCTION f1_sc; +DROP FUNCTION f2_sc; + +subtest end + +subtest can_mutate_no_propagation_mutating_to_nonmutating + +# When a function goes from mutating to non-mutating, callers should +# conservatively keep CAN_MUTATE (we don't downgrade). +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f2_m2nm() RETURNS INT LANGUAGE SQL AS $$ + INSERT INTO can_mutate_t VALUES (5) ON CONFLICT DO NOTHING; + SELECT 1; +$$ + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f1_m2nm() RETURNS INT LANGUAGE SQL AS 'SELECT f2_m2nm()' + +# Both should be CAN_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_m2nm()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +# Replace f2 with a non-mutating body. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE OR REPLACE FUNCTION f2_m2nm() RETURNS INT LANGUAGE SQL AS 'SELECT 42' + +# f2 is now CANNOT_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_m2nm()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +# f1 keeps CAN_MUTATE (conservative; not downgraded). +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_m2nm()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +DROP FUNCTION f1_m2nm; +DROP FUNCTION f2_m2nm; + +subtest end + +subtest can_mutate_no_change_nonmutating_to_nonmutating + +# Replacing a non-mutating function with another non-mutating body +# should not change anything. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f2_nm2nm() RETURNS INT LANGUAGE SQL AS 'SELECT 1' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f1_nm2nm() RETURNS INT LANGUAGE SQL AS 'SELECT f2_nm2nm()' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_nm2nm()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE OR REPLACE FUNCTION f2_nm2nm() RETURNS INT LANGUAGE SQL AS 'SELECT 2' + +# Both should remain CANNOT_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_nm2nm()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_nm2nm()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +DROP FUNCTION f1_nm2nm; +DROP FUNCTION f2_nm2nm; + +subtest end + +subtest can_mutate_diamond_dependency + +# Diamond: f1 -> f2, f1 -> f3, f2 -> f4, f3 -> f4. +# Replacing f4 should propagate to f2, f3, and f1. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f4_diamond() RETURNS INT LANGUAGE SQL AS 'SELECT 1' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f3_diamond() RETURNS INT LANGUAGE SQL AS 'SELECT f4_diamond()' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f2_diamond() RETURNS INT LANGUAGE SQL AS 'SELECT f4_diamond()' + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE FUNCTION f1_diamond() RETURNS INT LANGUAGE SQL AS $$ + SELECT f2_diamond(); + SELECT f3_diamond(); +$$ + +# Verify all start as CANNOT_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_diamond()'::regprocedure::oid::int - 100000 +---- +CANNOT_MUTATE + +# Replace f4 with a mutating body. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +CREATE OR REPLACE FUNCTION f4_diamond() RETURNS INT LANGUAGE SQL AS $$ + INSERT INTO can_mutate_t VALUES (10) ON CONFLICT DO NOTHING; + SELECT 1; +$$ + +# All four should now be CAN_MUTATE. +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f4_diamond()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f3_diamond()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f2_diamond()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +query T +SELECT crdb_internal.pb_to_json( + 'cockroach.sql.sqlbase.Descriptor', descriptor, false +)->'function'->>'canMutate' +FROM system.descriptor +WHERE id = 'f1_diamond()'::regprocedure::oid::int - 100000 +---- +CAN_MUTATE + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +DROP FUNCTION f1_diamond; +DROP FUNCTION f2_diamond; +DROP FUNCTION f3_diamond; +DROP FUNCTION f4_diamond; + +subtest end + +skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 +statement ok +DROP TABLE can_mutate_t diff --git a/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go b/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go index 6d4a0bdc4d65..8bf2fb3ea289 100644 --- a/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go +++ b/pkg/sql/schemachanger/scbuild/internal/scbuildstmt/create_function.go @@ -239,6 +239,8 @@ func replaceFunction( ) { _, _, existingFnElem := scpb.FindFunction(existingFnElts) fnID := existingFnElem.FunctionID + _, _, existingFnBody := scpb.FindFunctionBody(existingFnElts) + oldCanMutate := existingFnBody.CanMutate // Validate compatibility: routine kind must match. if n.IsProcedure != existingFnElem.IsProcedure { @@ -420,6 +422,16 @@ func replaceFunction( } b.Replace(fnBody) + // If the function became mutating, propagate CAN_MUTATE to all + // transitive callers so their descriptors stay accurate for + // deferred opt-building and leaf/root txn selection. + if b.EvalCtx().Settings.Version.ActiveVersion(b).IsActive(clusterversion.V26_3_FunctionDescCanMutate) { + if fnBody.CanMutate == catpb.Function_CAN_MUTATE && + oldCanMutate != catpb.Function_CAN_MUTATE { + propagateCanMutateToCallersDSC(b, fnID) + } + } + // Replace the FunctionParams element with the updated params. funcParams := &scpb.FunctionParams{ FunctionID: fnID, @@ -714,3 +726,45 @@ func routineLazilyEvaluatesSQL( return n.IsProcedure && lang == catpb.Function_PLPGSQL && sqlclustersettings.PLpgSQLProcedureLateBindingEnabled(b, b.EvalCtx().Settings) } + +// propagateCanMutateToCallersDSC transitively updates CAN_MUTATE on all +// functions that (directly or indirectly) call the given function. See +// propagateCanMutateToCallers in pkg/sql/create_function.go for the +// legacy-path equivalent. +func propagateCanMutateToCallersDSC(b BuildCtx, fnID descpb.ID) { + visited := make(map[descpb.ID]struct{}) + visited[fnID] = struct{}{} + propagateCanMutateDSCImpl(b, fnID, visited) +} + +func propagateCanMutateDSCImpl(b BuildCtx, fnID descpb.ID, visited map[descpb.ID]struct{}) { + type callerInfo struct { + fnID descpb.ID + body *scpb.FunctionBody + } + var callers []callerInfo + + b.BackReferences(fnID).FilterFunctionBody().ForEach( + func(_ scpb.Status, target scpb.TargetStatus, e *scpb.FunctionBody) { + if target != scpb.ToPublic { + return + } + if _, ok := visited[e.FunctionID]; ok { + return + } + if e.CanMutate == catpb.Function_CAN_MUTATE { + visited[e.FunctionID] = struct{}{} + return + } + callers = append(callers, callerInfo{fnID: e.FunctionID, body: e}) + }, + ) + + for _, c := range callers { + visited[c.fnID] = struct{}{} + updated := *c.body + updated.CanMutate = catpb.Function_CAN_MUTATE + b.Replace(&updated) + propagateCanMutateDSCImpl(b, c.fnID, visited) + } +} From 5d2c38239804a52ccac714bbfea9cd02c3602b7c Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 21 May 2026 16:02:57 -0400 Subject: [PATCH 5/9] ---REBASED FROM #169911--- From e5759cc73bb6b86d376b5b7d1c3c261f08633eea Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 15:39:00 -0400 Subject: [PATCH 6/9] 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 | 86 +++++++++++++++++-- 7 files changed, 268 insertions(+), 64 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 0b8c96c62b39..74adacbf6abf 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 627a87a21188..08c0abbb9537 100644 --- a/pkg/sql/opt/exec/execbuilder/scalar.go +++ b/pkg/sql/opt/exec/execbuilder/scalar.go @@ -1048,7 +1048,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..fb2f32ecab45 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,80 @@ 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 + + // Force eager build when the descriptor's CanMutate status is + // unknown (pre-V26_3_FunctionDescCanMutate). Deferred body building + // relies on the persisted CanMutate field to propagate mutation + // flags without inspecting the body; without it, the executor may + // incorrectly use a LeafTxn for mutating routines. + canMutateUnknown := o.CanMutate == tree.RoutineCanMutateUnknown + + if f.ResolvedType().Identical(types.AnyTuple) || couldBeInlined || b.insideFuncDef || canMutateUnknown { + // 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 @@ -518,7 +589,11 @@ func (b *Builder) buildRoutine( // 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. + // V26_3_FunctionDescCanMutate version gate), fall back to inspecting + // the eagerly-built body expressions. The fallback is safe because + // unknown-status descriptors always have eagerly-built body + // expressions available for inspection (we force eager building above + // when canMutateUnknown is true). canMutate := o.CanMutate if canMutate == tree.RoutineCanMutateUnknown { for _, s := range body { @@ -553,6 +628,7 @@ func (b *Builder) buildRoutine( Params: params, ResultBufferID: resultBufferID, CanMutate: canMutate, + BodyBuilder: bodyBuilder, }, }, ) From f570e321941dd10946f6c52125db0d4d1498d671 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 16:05:51 -0400 Subject: [PATCH 7/9] 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 a1d95b783695..db1f34600af3 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 78ad2ae533d8..ce29ffc3b1bc 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 74adacbf6abf..ff9caf53e4ea 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 08c0abbb9537..611dc1aa5afc 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" @@ -716,6 +717,7 @@ func (b *Builder) buildExistsSubquery( wrapRootExpr, 0, /* resultBufferID */ nil, /* bodyBuilder */ + "", /* routineName */ ) return tree.NewTypedCoalesceExpr(tree.TypedExprs{ tree.NewTypedRoutineExpr( @@ -845,6 +847,7 @@ func (b *Builder) buildSubquery( nil, /* wrapRootExpr */ 0, /* resultBufferID */ nil, /* bodyBuilder */ + "", /* routineName */ ) _, tailCall := b.tailCalls[subquery] return tree.NewTypedRoutineExpr( @@ -1049,6 +1052,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 @@ -1125,6 +1129,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. @@ -1177,6 +1182,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 @@ -1209,6 +1215,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, @@ -1246,6 +1253,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 f512a04fa377..776e9d36ded7 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 9cbd1da10549f364b30ffd68401c226f8d66c9ae Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Thu, 7 May 2026 16:07:02 -0400 Subject: [PATCH 8/9] 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 be0f9a2f920c..c57afe896091 100644 --- a/pkg/sql/opt/norm/testdata/rules/inline +++ b/pkg/sql/opt/norm/testdata/rules/inline @@ -1817,11 +1817,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 $$ @@ -1834,7 +1834,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] @@ -1855,12 +1855,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 5efd51e83627761022ae88932585887705a9bba7 Mon Sep 17 00:00:00 2001 From: ZhouXing19 Date: Sat, 16 May 2026 20:19:37 -0400 Subject: [PATCH 9/9] 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 | 108 +++ pkg/sql/opt/memo/testdata/logprops/udf | 664 ------------------ 2 files changed, 108 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 d571c527f560..df78f6a35f51 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf_calling_udf +++ b/pkg/sql/logictest/testdata/logic_test/udf_calling_udf @@ -738,3 +738,111 @@ subtest end skipif config local-mixed-25.4 local-mixed-26.1 local-mixed-26.2 statement ok DROP TABLE can_mutate_t + +# 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 B +SELECT EXISTS( + SELECT 1 FROM [EXPLAIN (OPT, VERBOSE) SELECT fn_calls_mutating(a) FROM ab] + WHERE info LIKE '%volatile, mutations%' +) +---- +true + +# The "mutations" property should propagate through CTEs that call mutating +# functions. +query B +SELECT EXISTS( + SELECT 1 FROM [EXPLAIN (OPT, VERBOSE) WITH cte AS (SELECT fn_mutating()) SELECT * FROM cte] + WHERE info LIKE '%volatile, mutations%' +) +---- +true + +# 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 B +SELECT EXISTS( + SELECT 1 FROM [EXPLAIN (OPT, VERBOSE) SELECT fn_mutating_via_cte() FROM ab] + WHERE info LIKE '%volatile, mutations%' +) +---- +true + +# 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 B +SELECT EXISTS( + SELECT 1 FROM [EXPLAIN (OPT, VERBOSE) SELECT fn_calls_mutating_via_cte() FROM ab] + WHERE info LIKE '%volatile, mutations%' +) +---- +true + +# 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 B +SELECT EXISTS( + SELECT 1 FROM [EXPLAIN (OPT, VERBOSE) SELECT fn_plpgsql_mutating() FROM ab] + WHERE info LIKE '%volatile, mutations%' +) +---- +true + +# 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 B +SELECT EXISTS( + SELECT 1 FROM [EXPLAIN (OPT, VERBOSE) SELECT fn_plpgsql_calls_mutating() FROM ab] + WHERE info LIKE '%volatile, mutations%' +) +---- +true + +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