Skip to content

Commit

Permalink
Merge #137251
Browse files Browse the repository at this point in the history
137251: sql: implement RETURNS TABLE syntax r=DrewKimball a=DrewKimball

#### sql: do not limit set-returning UDF when it has OUT parameters

This commit fixes a bug that caused the `SetOf` option for the UDF
`ReturnType` to be overwritten if the UDF had OUT parameters. The bug
caused a `LIMIT 1` to be imposed on the UDF's final body statement, so
that the UDF returned only a single row.

Fixes #128403

Release note (bug fix): Fixed a bug existing since v24.1 that would
cause a set-returning UDF with OUT parameters to return a single row.

#### sql: implement RETURNS TABLE syntax

This commit implements `RETURNS TABLE` for UDFs. `RETURNS TABLE` is
syntactic sugar for `RETURNS SETOF` with:
- RECORD if there are multiple TABLE parameters, or
- the type of the single TABLE parameter.
The TABLE parameters are added to the list of routine parameters.

Fixes #100226

Release note (sql change): Added support for `RETURNS TABLE` syntax when
creating a UDF.

Co-authored-by: Drew Kimball <[email protected]>
  • Loading branch information
craig[bot] and DrewKimball committed Dec 12, 2024
2 parents cccfc7c + 074371d commit 871b465
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 34 deletions.
1 change: 1 addition & 0 deletions docs/generated/sql/bnf/create_func.bnf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
create_func_stmt ::=
'CREATE' ( 'OR' 'REPLACE' | ) 'FUNCTION' routine_create_name '(' ( ( ( ( routine_param | routine_param | routine_param ) ) ( ( ',' ( routine_param | routine_param | routine_param ) ) )* ) | ) ')' 'RETURNS' ( 'SETOF' | ) routine_return_type ( ( ( ( 'AS' routine_body_str | 'LANGUAGE' ('SQL' | 'PLPGSQL') | ( 'CALLED' 'ON' 'NULL' 'INPUT' | 'RETURNS' 'NULL' 'ON' 'NULL' 'INPUT' | 'STRICT' | 'IMMUTABLE' | 'STABLE' | 'VOLATILE' | 'EXTERNAL' 'SECURITY' 'DEFINER' | 'EXTERNAL' 'SECURITY' 'INVOKER' | 'SECURITY' 'DEFINER' | 'SECURITY' 'INVOKER' | 'LEAKPROOF' | 'NOT' 'LEAKPROOF' ) ) ) ( ( ( 'AS' routine_body_str | 'LANGUAGE' ('SQL' | 'PLPGSQL') | ( 'CALLED' 'ON' 'NULL' 'INPUT' | 'RETURNS' 'NULL' 'ON' 'NULL' 'INPUT' | 'STRICT' | 'IMMUTABLE' | 'STABLE' | 'VOLATILE' | 'EXTERNAL' 'SECURITY' 'DEFINER' | 'EXTERNAL' 'SECURITY' 'INVOKER' | 'SECURITY' 'DEFINER' | 'SECURITY' 'INVOKER' | 'LEAKPROOF' | 'NOT' 'LEAKPROOF' ) ) ) )* ) | )
| 'CREATE' ( 'OR' 'REPLACE' | ) 'FUNCTION' routine_create_name '(' ( ( ( ( routine_param | routine_param | routine_param ) ) ( ( ',' ( routine_param | routine_param | routine_param ) ) )* ) | ) ')' 'RETURNS' 'TABLE' '(' table_func_column_list ')' ( ( ( ( 'AS' routine_body_str | 'LANGUAGE' ('SQL' | 'PLPGSQL') | ( 'CALLED' 'ON' 'NULL' 'INPUT' | 'RETURNS' 'NULL' 'ON' 'NULL' 'INPUT' | 'STRICT' | 'IMMUTABLE' | 'STABLE' | 'VOLATILE' | 'EXTERNAL' 'SECURITY' 'DEFINER' | 'EXTERNAL' 'SECURITY' 'INVOKER' | 'SECURITY' 'DEFINER' | 'SECURITY' 'INVOKER' | 'LEAKPROOF' | 'NOT' 'LEAKPROOF' ) ) ) ( ( ( 'AS' routine_body_str | 'LANGUAGE' ('SQL' | 'PLPGSQL') | ( 'CALLED' 'ON' 'NULL' 'INPUT' | 'RETURNS' 'NULL' 'ON' 'NULL' 'INPUT' | 'STRICT' | 'IMMUTABLE' | 'STABLE' | 'VOLATILE' | 'EXTERNAL' 'SECURITY' 'DEFINER' | 'EXTERNAL' 'SECURITY' 'INVOKER' | 'SECURITY' 'DEFINER' | 'SECURITY' 'INVOKER' | 'LEAKPROOF' | 'NOT' 'LEAKPROOF' ) ) ) )* ) | )
| 'CREATE' ( 'OR' 'REPLACE' | ) 'FUNCTION' routine_create_name '(' ( ( ( ( routine_param | routine_param | routine_param ) ) ( ( ',' ( routine_param | routine_param | routine_param ) ) )* ) | ) ')' ( ( ( ( 'AS' routine_body_str | 'LANGUAGE' ('SQL' | 'PLPGSQL') | ( 'CALLED' 'ON' 'NULL' 'INPUT' | 'RETURNS' 'NULL' 'ON' 'NULL' 'INPUT' | 'STRICT' | 'IMMUTABLE' | 'STABLE' | 'VOLATILE' | 'EXTERNAL' 'SECURITY' 'DEFINER' | 'EXTERNAL' 'SECURITY' 'INVOKER' | 'SECURITY' 'DEFINER' | 'SECURITY' 'INVOKER' | 'LEAKPROOF' | 'NOT' 'LEAKPROOF' ) ) ) ( ( ( 'AS' routine_body_str | 'LANGUAGE' ('SQL' | 'PLPGSQL') | ( 'CALLED' 'ON' 'NULL' 'INPUT' | 'RETURNS' 'NULL' 'ON' 'NULL' 'INPUT' | 'STRICT' | 'IMMUTABLE' | 'STABLE' | 'VOLATILE' | 'EXTERNAL' 'SECURITY' 'DEFINER' | 'EXTERNAL' 'SECURITY' 'INVOKER' | 'SECURITY' 'DEFINER' | 'SECURITY' 'INVOKER' | 'LEAKPROOF' | 'NOT' 'LEAKPROOF' ) ) ) )* ) | )
13 changes: 10 additions & 3 deletions docs/generated/sql/bnf/stmt_block.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -1823,6 +1823,7 @@ create_sequence_stmt ::=

create_func_stmt ::=
'CREATE' opt_or_replace 'FUNCTION' routine_create_name '(' opt_routine_param_with_default_list ')' 'RETURNS' opt_return_set routine_return_type opt_create_routine_opt_list opt_routine_body
| 'CREATE' opt_or_replace 'FUNCTION' routine_create_name '(' opt_routine_param_with_default_list ')' 'RETURNS' 'TABLE' '(' table_func_column_list ')' opt_create_routine_opt_list opt_routine_body
| 'CREATE' opt_or_replace 'FUNCTION' routine_create_name '(' opt_routine_param_with_default_list ')' opt_create_routine_opt_list opt_routine_body

create_proc_stmt ::=
Expand Down Expand Up @@ -2630,6 +2631,9 @@ opt_routine_body ::=
| 'BEGIN' 'ATOMIC' routine_body_stmt_list 'END'
|

table_func_column_list ::=
( table_func_column ) ( ( ',' table_func_column ) )*

trigger_action_time ::=
'BEFORE'
| 'AFTER'
Expand Down Expand Up @@ -3276,6 +3280,9 @@ routine_return_stmt ::=
routine_body_stmt_list ::=
( ) ( ( routine_body_stmt ';' ) )*

table_func_column ::=
param_name routine_param_type

trigger_event ::=
'INSERT'
| 'DELETE'
Expand Down Expand Up @@ -3698,6 +3705,9 @@ routine_body_stmt ::=
stmt_without_legacy_transaction
| routine_return_stmt

param_name ::=
type_function_name

trigger_transition ::=
transition_is_new transition_is_row opt_as table_alias_name

Expand Down Expand Up @@ -4658,9 +4668,6 @@ routine_param_class ::=
| 'INOUT'
| 'IN' 'OUT'

param_name ::=
type_function_name

opt_float ::=
'(' 'ICONST' ')'
|
Expand Down
6 changes: 6 additions & 0 deletions pkg/ccl/logictestccl/testdata/logic_test/plpgsql_unsupported
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ CREATE OR REPLACE PROCEDURE foo() AS $$
END
$$ LANGUAGE PLpgSQL;

statement error pq: unimplemented: set-returning PL/pgSQL functions
CREATE OR REPLACE FUNCTION bar() RETURNS SETOF INT LANGUAGE PLpgSQL AS $$ BEGIN RETURN NEXT 100; END $$;

statement error pq: unimplemented: set-returning PL/pgSQL functions
CREATE OR REPLACE FUNCTION bar() RETURNS TABLE (x INT) LANGUAGE PLpgSQL AS $$ BEGIN RETURN NEXT 100; END $$;

subtest error_detail

# Regression test for #123672 - annotate "unsupported" errors with the
Expand Down
108 changes: 108 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/udf_setof
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,111 @@ SELECT * FROM all_ab_tuple()
2 20
3 30
4 40

# OUT parameters should not cause a set-returning UDF to return a single row.
subtest regression_128403

statement ok
CREATE FUNCTION f128403(OUT x INT, OUT y TEXT) RETURNS SETOF RECORD AS $$
SELECT t, t::TEXT FROM generate_series(1, 10) g(t);
$$ LANGUAGE SQL;

query T rowsort
select f128403();
----
(1,1)
(2,2)
(3,3)
(4,4)
(5,5)
(6,6)
(7,7)
(8,8)
(9,9)
(10,10)

query IT rowsort
SELECT * FROM f128403();
----
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 10

subtest end

# RETURNS TABLE is syntactic sugar for RETURNS SETOF with:
# - RECORD if there are multiple TABLE parameters, or
# - the type of the single TABLE parameter.
subtest returns_table

statement error pgcode 42601 pq: OUT and INOUT arguments aren't allowed in TABLE functions
CREATE FUNCTION f_table1(OUT x INT, OUT y TEXT) RETURNS TABLE(x INT, y TEXT) AS $$
SELECT t, t::TEXT FROM generate_series(1, 10) g(t);
$$ LANGUAGE SQL;

statement ok
CREATE FUNCTION f_table1() RETURNS TABLE(x INT, y TEXT) AS $$
SELECT t, t::TEXT FROM generate_series(1, 10) g(t);
$$ LANGUAGE SQL;

query T rowsort
select f_table1();
----
(1,1)
(2,2)
(3,3)
(4,4)
(5,5)
(6,6)
(7,7)
(8,8)
(9,9)
(10,10)

query IT rowsort
SELECT * FROM f_table1();
----
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 10

# Case with a single TABLE parameter.
statement ok
CREATE FUNCTION f_table2() RETURNS TABLE(x INT) AS $$
SELECT t FROM generate_series(1, 10) g(t);
$$ LANGUAGE SQL;

query I rowsort
select f_table2();
----
1
2
3
4
5
6
7
8
9
10

statement error pgcode 42P13 return type mismatch in function declared to return int\nDETAIL: Actual return type is record
CREATE FUNCTION err() RETURNS TABLE (x INT) STRICT LANGUAGE SQL AS $$
SELECT a, b FROM ab ORDER BY a
$$

subtest end
9 changes: 5 additions & 4 deletions pkg/sql/opt/optbuilder/create_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,12 @@ func (b *Builder) buildCreateFunction(cf *tree.CreateRoutine, inScope *scope) (o
panic(pgerror.Newf(pgcode.InvalidFunctionDefinition, "function result type must be %s because of OUT parameters", outParamType.Name()))
}
// Override the return types so that we do return type validation and SHOW
// CREATE correctly.
funcReturnType = outParamType
cf.ReturnType = &tree.RoutineReturnType{
Type: outParamType,
// CREATE correctly. Take care not to override the SetOf value if it is set.
if cf.ReturnType == nil {
cf.ReturnType = &tree.RoutineReturnType{}
}
cf.ReturnType.Type = outParamType
funcReturnType = outParamType
} else if funcReturnType == nil {
if cf.IsProcedure {
// A procedure doesn't need a return type. Use a VOID return type to avoid
Expand Down
9 changes: 5 additions & 4 deletions pkg/sql/opt/testutils/testcat/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,12 @@ func (tc *Catalog) CreateRoutine(c *tree.CreateRoutine) {
panic(pgerror.Newf(pgcode.InvalidFunctionDefinition, "function result type must be %s because of OUT parameters", outParamType.Name()))
}
// Override the return types so that we do return type validation and SHOW
// CREATE correctly.
retType = outParamType
c.ReturnType = &tree.RoutineReturnType{
Type: outParamType,
// CREATE correctly. Make sure not to override the SetOf value if it is set.
if c.ReturnType == nil {
c.ReturnType = &tree.RoutineReturnType{}
}
c.ReturnType.Type = outParamType
retType = outParamType
} else if retType == nil {
if c.IsProcedure {
// A procedure doesn't need a return type. Use a VOID return type to avoid
Expand Down
7 changes: 7 additions & 0 deletions pkg/sql/parser/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,13 @@ func (l *lexer) setErr(err error) {
l.populateErrorDetails()
}

// setErrNoDetails is similar to setErr, but is used for an error that should
// not be further annotated with details.
func (l *lexer) setErrNoDetails(err error) {
err = pgerror.WithCandidateCode(err, pgcode.Syntax)
l.lastError = err
}

func (l *lexer) Error(e string) {
e = strings.TrimPrefix(e, "syntax error: ") // we'll add it again below.
l.lastError = pgerror.WithCandidateCode(errors.Newf("%s", e), pgcode.Syntax)
Expand Down
78 changes: 65 additions & 13 deletions pkg/sql/parser/sql.y
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ func setErr(sqllex sqlLexer, err error) int {
return 1
}

func setErrNoDetails(sqllex sqlLexer, err error) int {
sqllex.(*lexer).setErrNoDetails(err)
return 1
}

func unimplementedWithIssue(sqllex sqlLexer, issue int) int {
sqllex.(*lexer).UnimplementedWithIssue(issue)
return 1
Expand Down Expand Up @@ -1725,10 +1730,11 @@ func (u *sqlSymUnion) triggerForEach() tree.TriggerForEach {
%type <privilege.TargetObjectType> target_object_type

// Routine (UDF/SP) relevant components.
%type <bool> opt_or_replace opt_return_table opt_return_set opt_no
%type <bool> opt_or_replace opt_return_set opt_no
%type <str> param_name routine_as
%type <tree.RoutineParams> opt_routine_param_with_default_list routine_param_with_default_list func_params func_params_list
%type <tree.RoutineParam> routine_param_with_default routine_param
%type <tree.RoutineParams> opt_routine_param_with_default_list routine_param_with_default_list
%type <tree.RoutineParams> func_params func_params_list table_func_column_list
%type <tree.RoutineParam> routine_param_with_default routine_param table_func_column
%type <tree.ResolvableTypeReference> routine_return_type routine_param_type
%type <tree.RoutineOptions> opt_create_routine_opt_list create_routine_opt_list alter_func_opt_list
%type <tree.RoutineOption> create_routine_opt_item common_routine_opt_item
Expand Down Expand Up @@ -4869,8 +4875,7 @@ create_extension_stmt:
// %SeeAlso: WEBDOCS/create-function.html
create_func_stmt:
CREATE opt_or_replace FUNCTION routine_create_name '(' opt_routine_param_with_default_list ')'
RETURNS opt_return_table opt_return_set routine_return_type
opt_create_routine_opt_list opt_routine_body
RETURNS opt_return_set routine_return_type opt_create_routine_opt_list opt_routine_body
{
name := $4.unresolvedObjectName().ToRoutineName()
$$.val = &tree.CreateRoutine{
Expand All @@ -4879,11 +4884,43 @@ create_func_stmt:
Name: name,
Params: $6.routineParams(),
ReturnType: &tree.RoutineReturnType{
Type: $11.typeReference(),
SetOf: $10.bool(),
Type: $10.typeReference(),
SetOf: $9.bool(),
},
Options: $12.routineOptions(),
RoutineBody: $13.routineBody(),
Options: $11.routineOptions(),
RoutineBody: $12.routineBody(),
}
}
| CREATE opt_or_replace FUNCTION routine_create_name '(' opt_routine_param_with_default_list ')'
RETURNS TABLE '(' table_func_column_list ')' opt_create_routine_opt_list opt_routine_body
{
// RETURNS TABLE is syntactic sugar for RETURNS SETOF with:
// - RECORD if there are multiple TABLE parameters, or
// - the type of the single TABLE parameter.
// The TABLE parameters are added to the list of routine parameters.
tableParams := $11.routineParams()
returnType := tree.ResolvableTypeReference(types.AnyTuple)
if len(tableParams) == 1 {
returnType = tableParams[0].Type
}
routineParams := $6.routineParams()
for i := range routineParams {
// OUT parameters are not allowed in table functions.
if tree.IsOutParamClass(routineParams[i].Class) {
return setErrNoDetails(sqllex, errors.New("OUT and INOUT arguments aren't allowed in TABLE functions"))
}
}
$$.val = &tree.CreateRoutine{
IsProcedure: false,
Replace: $2.bool(),
Name: $4.unresolvedObjectName().ToRoutineName(),
Params: append(routineParams, tableParams...),
ReturnType: &tree.RoutineReturnType{
Type: returnType,
SetOf: true,
},
Options: $13.routineOptions(),
RoutineBody: $14.routineBody(),
}
}
| CREATE opt_or_replace FUNCTION routine_create_name '(' opt_routine_param_with_default_list ')'
Expand Down Expand Up @@ -4932,10 +4969,6 @@ opt_or_replace:
OR REPLACE { $$.val = true }
| /* EMPTY */ { $$.val = false }

opt_return_table:
TABLE { return unimplementedWithIssueDetail(sqllex, 100226, "UDF returning TABLE") }
| /* EMPTY */ { $$.val = false }

opt_return_set:
SETOF { $$.val = true}
| /* EMPTY */ { $$.val = false }
Expand Down Expand Up @@ -5022,6 +5055,25 @@ routine_param_type:
routine_return_type:
routine_param_type

table_func_column: param_name routine_param_type
{
$$.val = tree.RoutineParam{
Name: tree.Name($1),
Type: $2.typeReference(),
Class: tree.RoutineParamOut,
}
}

table_func_column_list:
table_func_column
{
$$.val = tree.RoutineParams{$1.routineParam()}
}
| table_func_column_list ',' table_func_column
{
$$.val = append($1.routineParams(), $3.routineParam())
}

opt_create_routine_opt_list:
create_routine_opt_list { $$.val = $1.routineOptions() }
| /* EMPTY */ { $$.val = tree.RoutineOptions{} }
Expand Down
Loading

0 comments on commit 871b465

Please sign in to comment.