diff --git a/pkg/sql/logictest/testdata/logic_test/udf_plpgsql b/pkg/sql/logictest/testdata/logic_test/udf_plpgsql index ea4d8de982e3..bc265dbada58 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf_plpgsql +++ b/pkg/sql/logictest/testdata/logic_test/udf_plpgsql @@ -325,172 +325,16 @@ SELECT f(1, 5), f(-5, 5), f(0, 1) ---- 10 10 0 -# Dijkstra's Algorithm -# -# ┌─┬────8──┬─┬──7────┬─┐ -# ┌─────┤1│ │2│ │3├─────┐ -# │ └┬┘ └┬┴───┐ └┬┘ │ -# │ │ │ │ │ │ -# 4 │ 2 │ │ 9 -# │ │ │ │ │ │ -# ┌┴┐ │ ┌┴┐ │ │ ┌┴┐ -# │0│ 11 ┌───┤8│ │ 14 │4│ -# └┬┘ │ │ └┬┘ │ │ └┬┘ -# │ │ 7 │ │ │ │ -# 8 │ │ 6 │ │ 10 -# │ │ │ │ │ │ │ -# │ ┌┴┬───┘ ┌┴┐ └───┬┴┐ │ -# └─────┤7│ │6│ │5├─────┘ -# └─┴────1──┴─┴──4────┴─┘ -# -# Encode the graph as a series of undirected edges, where "a" and "b" are the -# "to" and "from" nodes and "weight" is the weight of the edge. -statement ok -CREATE TABLE edges (a INT, b INT, weight INT); -INSERT INTO edges VALUES -(0, 1, 4), -(0, 7, 8), -(1, 7, 11), -(1, 2, 8), -(2, 8, 2), -(7, 8, 7), -(7, 6, 1), -(6, 8, 6), -(2, 5, 4), -(5, 6, 2), -(2, 3, 7), -(3, 5, 14), -(3, 4, 9), -(4, 5, 10); - -# Get the number of vertexes in the graph. -statement ok -CREATE FUNCTION vertexes() RETURNS INT AS $$ SELECT max(greatest(a, b)) + 1 FROM edges $$ LANGUAGE SQL; - -# Get the maximum int32 value. -statement ok -CREATE FUNCTION max_int() RETURNS INT AS $$ SELECT 2147483647 $$ LANGUAGE SQL; - -# Get the weight of the edge between the two given nodes, if any. -statement ok -CREATE FUNCTION graph(x INT, y INT) RETURNS INT AS $$ - SELECT coalesce((SELECT weight FROM edges WHERE (a = x AND b = y) OR (a = y AND b = x) LIMIT 1), 0); -$$ LANGUAGE SQL; - -# Replace the element at the given index of the array with the given value. -statement ok -CREATE FUNCTION replace(arr INT[], idx INT, val INT) RETURNS INT[] AS $$ - DECLARE - i INT; - n INT := array_length(arr, 1); - res INT[] := ARRAY[]::INT[]; - BEGIN - i := 0; - LOOP - IF i = idx THEN - res := res || val; - ELSE - res := res || arr[i+1]; - END IF; - i := i + 1; - IF i >= n THEN EXIT; END IF; - END LOOP; - RETURN res; - END -$$ LANGUAGE PLpgSQL; - -# Return the node with the minimum distance from the source node known so far -# out of the nodes that don't already have a shortest path calculated. -statement ok -CREATE FUNCTION min_distance(dist INT[], spt_set INT[]) RETURNS INT AS $$ - DECLARE - n INT := vertexes(); - i INT; - min INT := max_int(); - min_index INT := 0; - BEGIN - i := 0; - LOOP - IF spt_set[i+1] = 0 AND dist[i+1] <= min THEN - min := dist[i+1]; - min_index := i; - END IF; - i := i + 1; - IF i >= n THEN EXIT; END IF; - END LOOP; - RETURN min_index; - END -$$ LANGUAGE PLPGSQL; - -# Implement dijkstra's algorithm using the "edges" table. -statement ok -CREATE FUNCTION dijkstra(src INT) RETURNS INT[] AS $$ - DECLARE - n INT := vertexes(); - i INT; - count INT; - dist INT[] := ARRAY[]::INT[]; - spt_set INT[] := ARRAY[]::INT[]; - u INT; - BEGIN - i := 0; - LOOP - dist := dist || max_int(); - spt_set := spt_set || 0; - i := i + 1; - IF i >= n THEN EXIT; END IF; - END LOOP; - dist := replace(dist, src, 0); - count := 0; - LOOP - u := min_distance(dist, spt_set); - spt_set := replace(spt_set, u, 1); - i := 0; - LOOP - IF - spt_set[i+1] = 0 AND - graph(u, i) > 0 AND - dist[u+1] <> max_int() AND - dist[u+1] + graph(u, i) < dist[i+1] - THEN - dist := replace(dist, i, dist[u+1] + graph(u, i)); - END IF; - i := i + 1; - IF i >= n THEN EXIT; END IF; - END LOOP; - count := count + 1; - IF count >= n THEN EXIT; END IF; - END LOOP; - RETURN dist; - END -$$ LANGUAGE PLPGSQL; +# TODO(drewk): add back the dijkstra test once UDFs calling other UDFs is +# allowed. -# Run dijkstra's algorithm using node 0 as the source. -query II nosort,colnames -SELECT i AS "Vertex", dist[i+1] AS "Distance From Source" -FROM generate_series(0, vertexes() - 1) f(i), dijkstra(0) g(dist); ----- -Vertex Distance From Source -0 0 -1 4 -2 12 -3 19 -4 21 -5 11 -6 9 -7 8 -8 14 - -statement ok +statement error pgcode 2F005 control reached end of function without RETURN CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ BEGIN END $$ LANGUAGE PLpgSQL; statement error pgcode 2F005 control reached end of function without RETURN -SELECT f(1, 2); - -statement ok CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ DECLARE i INT; @@ -500,9 +344,6 @@ CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ $$ LANGUAGE PLpgSQL; statement error pgcode 2F005 control reached end of function without RETURN -SELECT f(1, 2); - -statement ok CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ BEGIN IF a < b THEN @@ -512,9 +353,6 @@ CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ $$ LANGUAGE PLpgSQL; statement error pgcode 2F005 control reached end of function without RETURN -SELECT f(1, 2); - -statement ok CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ DECLARE i INT; @@ -528,9 +366,6 @@ CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ $$ LANGUAGE PLpgSQL; statement error pgcode 2F005 control reached end of function without RETURN -SELECT f(1, 2); - -statement ok CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ BEGIN LOOP @@ -540,9 +375,6 @@ CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ $$ LANGUAGE PLpgSQL; statement error pgcode 2F005 control reached end of function without RETURN -SELECT f(1, 2); - -statement ok CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ BEGIN LOOP @@ -554,9 +386,6 @@ CREATE OR REPLACE FUNCTION f(a INT, b INT) RETURNS INT AS $$ END $$ LANGUAGE PLpgSQL; -statement error pgcode 2F005 control reached end of function without RETURN -SELECT f(1, 2); - statement error pgcode 0A000 PL/pgSQL functions with RECORD input arguments are not yet supported CREATE FUNCTION f_err(p1 RECORD) RETURNS RECORD AS $$ BEGIN @@ -831,3 +660,151 @@ $$ LANGUAGE PLpgSQL; query error pgcode P0001 pq: foo SELECT f(); + +statement error pgcode 42601 pq: too few parameters specified for RAISE +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE 'foo% % %', 1, 2; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42601 pq: too many parameters specified for RAISE +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE 'foo%', 1, 2; + RETURN 0; + END +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42601 pq: RAISE option already specified: ERRCODE +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + RAISE EXCEPTION USING ERRCODE = '22012', ERRCODE = '22013'; + return 0; + END +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42601 pq: \"i\" is not a known variable +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + i := 0; + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42601 CONTINUE cannot be used outside a loop +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + CONTINUE; + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +statement error pgcode 42601 EXIT cannot be used outside a loop, unless it has a label +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + BEGIN + EXIT; + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i CONSTANT INT; + BEGIN + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +query I +SELECT f(); +---- +NULL + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i CONSTANT INT := 0; + BEGIN + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +query I +SELECT f(); +---- +0 + +statement ok +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i CONSTANT INT := (SELECT x FROM xy ORDER BY x LIMIT 1); + BEGIN + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +query I +SELECT f(); +---- +1 + +statement ok +CREATE OR REPLACE FUNCTION f(n INT) RETURNS INT AS $$ + DECLARE + i CONSTANT INT := n; + BEGIN + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +query IIIIII +SELECT f(-100), f(-1), f(0), f(1), f(100), f(NULL); +---- +-100 -1 0 1 100 NULL + +statement error pgcode 22005 pq: variable \"i\" is declared CONSTANT +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i CONSTANT INT; + BEGIN + i := i + 1; + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +statement error pgcode 22005 pq: variable \"i\" is declared CONSTANT +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i CONSTANT INT := 0; + BEGIN + i := i + 1; + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +statement error pgcode 22005 pq: variable \"i\" is declared CONSTANT +CREATE OR REPLACE FUNCTION f(n INT) RETURNS INT AS $$ + DECLARE + i CONSTANT INT := 0; + BEGIN + IF n > 0 THEN + i := i + 1; + END IF; + RETURN i; + END +$$ LANGUAGE PLpgSQL; + +statement error pgcode 22005 pq: variable \"i\" is declared CONSTANT +CREATE OR REPLACE FUNCTION f() RETURNS INT AS $$ + DECLARE + i CONSTANT INT := 0; + BEGIN + LOOP IF i >= 10 THEN EXIT; END IF; + i := i + 1; + END LOOP; + RETURN i; + END +$$ LANGUAGE PLpgSQL; diff --git a/pkg/sql/logictest/testdata/logic_test/udf_volatility_check b/pkg/sql/logictest/testdata/logic_test/udf_volatility_check index 8e7c719efa54..a3bce3258887 100644 --- a/pkg/sql/logictest/testdata/logic_test/udf_volatility_check +++ b/pkg/sql/logictest/testdata/logic_test/udf_volatility_check @@ -102,3 +102,36 @@ statement error pgcode 22023 pq: volatile statement not allowed in immutable fun ALTER FUNCTION f IMMUTABLE subtest end + +subtest plpgsql_volatility + +statement error pgcode 22023 pq: referencing relations is not allowed in immutable function +CREATE FUNCTION f() RETURNS FLOAT LANGUAGE PLpgSQL IMMUTABLE AS $$ BEGIN RETURN (SELECT a FROM t1); END $$; + +statement error pgcode 22023 pq: volatile statement not allowed in immutable function: BEGIN +CREATE FUNCTION f() RETURNS FLOAT LANGUAGE PLpgSQL IMMUTABLE AS $$ BEGIN RETURN (SELECT random()); END $$; + +statement error pgcode 22023 pq: stable statement not allowed in immutable function: BEGIN +CREATE FUNCTION f() RETURNS TIMESTAMP LANGUAGE PLpgSQL IMMUTABLE AS $$ BEGIN RETURN (SELECT statement_timestamp()); END $$; + +statement error pgcode 22023 pq: volatile statement not allowed in stable function: BEGIN +CREATE FUNCTION f() RETURNS FLOAT LANGUAGE PLpgSQL STABLE AS $$ BEGIN RETURN (SELECT random()); END $$; + +statement error pgcode 22023 pq: volatile statement not allowed in immutable function: BEGIN +CREATE FUNCTION f() RETURNS FLOAT LANGUAGE PLpgSQL IMMUTABLE AS $$ BEGIN RETURN (SELECT @1 FROM random()); END $$; + +statement error pgcode 22023 pq: volatile statement not allowed in immutable function: BEGIN +CREATE FUNCTION f() RETURNS INT LANGUAGE PLpgSQL IMMUTABLE AS $$ + BEGIN + RETURN (SELECT t1.a FROM t1 JOIN t2 ON t1.a = t2.a + random()::INT); + END +$$; + +statement error pgcode 22023 pq: volatile statement not allowed in immutable function: BEGIN +CREATE FUNCTION f() RETURNS INT LANGUAGE PLpgSQL IMMUTABLE AS $$ + BEGIN + RETURN (SELECT a FROM t1 WHERE b = 1 + random()); + END +$$; + +subtest end diff --git a/pkg/sql/opt/optbuilder/create_function.go b/pkg/sql/opt/optbuilder/create_function.go index 893b00c9c146..ee9550642f0e 100644 --- a/pkg/sql/opt/optbuilder/create_function.go +++ b/pkg/sql/opt/optbuilder/create_function.go @@ -143,6 +143,7 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o // named parameters to the scope so that references to them in the body can // be resolved. bodyScope := b.allocScope() + var paramTypes tree.ParamTypes for i := range cf.Params { param := &cf.Params[i] typ, err := tree.ResolveType(b.ctx, param.Type, b.semaCtx.TypeResolver) @@ -170,6 +171,14 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o typedesc.GetTypeDescriptorClosure(typ).ForEach(func(id descpb.ID) { typeDeps.Add(int(id)) }) + + // Collect the parameters for PLpgSQL routines. + if language == tree.RoutineLangPLpgSQL { + paramTypes = append(paramTypes, tree.ParamType{ + Name: param.Name.String(), + Typ: typ, + }) + } } // Collect the user defined type dependency of the return type. @@ -205,8 +214,8 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o b.evalCtx.Annotations = &ann // We need to disable stable function folding because we want to catch the - // volatility of stable functions. If folded, we only get a scalar and lose - // the volatility. + // volatility of stable functions. If folded, we only get a scalar and + // lose the volatility. b.factory.FoldingControl().TemporarilyDisallowStableFolds(func() { stmtScope = b.buildStmtAtRootWithScope(stmts[i].AST, nil /* desiredTypes */, bodyScope) }) @@ -230,8 +239,16 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o panic(err) } - // TODO(drewk): build and check volatility. We will need to remove the hack - // to disable UDFs calling other UDFs before doing this. + // We need to disable stable function folding because we want to catch the + // volatility of stable functions. If folded, we only get a scalar and lose + // the volatility. + b.factory.FoldingControl().TemporarilyDisallowStableFolds(func() { + var plBuilder plpgsqlBuilder + plBuilder.init(b, nil /* colRefs */, paramTypes, stmt.AST, funcReturnType) + stmtScope = plBuilder.build(stmt.AST, bodyScope) + }) + checkStmtVolatility(targetVolatility, stmtScope, stmt) + // Format the statements with qualified datasource names. formatFuncBodyStmt(fmtCtx, stmt.AST, false /* newLine */) afterBuildStmt() diff --git a/pkg/sql/opt/optbuilder/plpgsql.go b/pkg/sql/opt/optbuilder/plpgsql.go index e42efb6af0d9..f027f00fccba 100644 --- a/pkg/sql/opt/optbuilder/plpgsql.go +++ b/pkg/sql/opt/optbuilder/plpgsql.go @@ -121,6 +121,9 @@ type plpgsqlBuilder struct { // varTypes maps from the name of each variable to its type. varTypes map[tree.Name]*types.T + // constants tracks the variables that were declared as constant. + constants map[tree.Name]struct{} + // returnType is the return type of the PL/pgSQL function. returnType *types.T @@ -164,12 +167,6 @@ func (b *plpgsqlBuilder) init( "not-null PL/pgSQL variables are not yet supported", )) } - if dec.Constant { - panic(unimplemented.NewWithIssueDetail(105241, - "constant variable", - "constant PL/pgSQL variables are not yet supported", - )) - } if dec.Collate != "" { panic(unimplemented.NewWithIssueDetail(105245, "variable collation", @@ -185,14 +182,20 @@ func (b *plpgsqlBuilder) build(block *plpgsqltree.PLpgSQLStmtBlock, s *scope) *s s = s.push() b.ensureScopeHasExpr(s) - // Some variable declarations initialize the variable. + b.constants = make(map[tree.Name]struct{}) for _, dec := range b.decls { if dec.Expr != nil { + // Some variable declarations initialize the variable. s = b.addPLpgSQLAssign(s, dec.Var, dec.Expr) } else { // Uninitialized variables are null. s = b.addPLpgSQLAssign(s, dec.Var, &tree.CastExpr{Expr: tree.DNull, Type: dec.Typ}) } + if dec.Constant { + // Add to the constants map after initializing the variable, since + // constant variables only prevent assignment, not initialization. + b.constants[dec.Var] = struct{}{} + } } if s = b.buildPLpgSQLStatements(block.Body, s); s != nil { return s @@ -399,9 +402,14 @@ func (b *plpgsqlBuilder) buildPLpgSQLStatements( func (b *plpgsqlBuilder) addPLpgSQLAssign( inScope *scope, ident plpgsqltree.PLpgSQLVariable, val plpgsqltree.PLpgSQLExpr, ) *scope { + if b.constants != nil { + if _, ok := b.constants[ident]; ok { + panic(pgerror.Newf(pgcode.ErrorInAssignment, "variable \"%s\" is declared CONSTANT", ident)) + } + } typ, ok := b.varTypes[ident] if !ok { - panic(errors.AssertionFailedf("failed to find type for variable %s", ident)) + panic(pgerror.Newf(pgcode.Syntax, "\"%s\" is not a known variable", ident)) } assignScope := inScope.push() for i := range inScope.cols { @@ -448,9 +456,7 @@ func (b *plpgsqlBuilder) getRaiseArgs( // DEBUG log-level maps to severity DEBUG1. severity = makeConstStr("DEBUG1") default: - panic(unimplemented.Newf( - "unimplemented log level", "RAISE log level %s is not yet supported", raise.LogLevel, - )) + panic(errors.AssertionFailedf("unexpected log level %s", raise.LogLevel)) } // Retrieve the message, if it was set with the format syntax. if raise.Message != "" { @@ -537,7 +543,7 @@ func (b *plpgsqlBuilder) makeRaiseFormatMessage( if j > 0 { // Add the next argument at the location of this parameter. if argIdx >= len(args) { - panic(pgerror.Newf(pgcode.PLpgSQL, "too few parameters specified for RAISE")) + panic(pgerror.Newf(pgcode.Syntax, "too few parameters specified for RAISE")) } addToResult(b.buildPLpgSQLExpr(args[argIdx], types.String, s)) argIdx++ @@ -546,7 +552,7 @@ func (b *plpgsqlBuilder) makeRaiseFormatMessage( } } if argIdx < len(args) { - panic(pgerror.Newf(pgcode.PLpgSQL, "too many parameters specified for RAISE")) + panic(pgerror.Newf(pgcode.Syntax, "too many parameters specified for RAISE")) } return result } diff --git a/pkg/sql/sem/plpgsqltree/statements.go b/pkg/sql/sem/plpgsqltree/statements.go index 04584cc8418a..83d04a2a5fd7 100644 --- a/pkg/sql/sem/plpgsqltree/statements.go +++ b/pkg/sql/sem/plpgsqltree/statements.go @@ -60,7 +60,6 @@ type PLpgSQLStmtBlock struct { Decls []PLpgSQLDecl Body []PLpgSQLStatement Exceptions *PLpgSQLExceptionBlock - Scope VariableScope } // TODO(drewk): format Label and Exceptions fields. diff --git a/pkg/sql/sem/plpgsqltree/variable.go b/pkg/sql/sem/plpgsqltree/variable.go index e83b416fde97..a19c74ca4fe8 100644 --- a/pkg/sql/sem/plpgsqltree/variable.go +++ b/pkg/sql/sem/plpgsqltree/variable.go @@ -13,9 +13,3 @@ package plpgsqltree import "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" type PLpgSQLVariable = tree.Name - -// Scope contains all the variables defined in the DECLARE section of current statement block. -type VariableScope struct { - Variables []*PLpgSQLVariable - VarNameToIdx map[string]int // mapping from variable -}