Skip to content

Commit e7f8c21

Browse files
committed
fix(postgresql): include OUT params when matching CALL signatures
Resolves #4216. PostgreSQL CALL requires positional placeholders for OUT parameters, but ResolveFuncCall used InArgs() which strips them, so the supplied arg count never matched the procedure's IN arity and analysis failed with "function ... does not exist". Add Function.CallArgs() and route ast.CallStmt through a new ResolveCallStmt path that matches against IN+OUT arguments.
1 parent b84b1d6 commit e7f8c21

9 files changed

Lines changed: 180 additions & 1 deletion

File tree

internal/endtoend/testdata/stored_procedures_pg_out_args/postgresql/pgx/v5/go/db.go

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/stored_procedures_pg_out_args/postgresql/pgx/v5/go/models.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/stored_procedures_pg_out_args/postgresql/pgx/v5/go/query.sql.go

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- name: CreateTodoNullPlaceholder :exec
2+
CALL create_todo(sqlc.arg(task)::text, null);
3+
4+
-- name: CreateTodoTypedNullPlaceholder :exec
5+
CALL create_todo(sqlc.arg(task)::text, NULL::int);
6+
7+
-- name: CreateTodoNamedOut :exec
8+
CALL create_todo(sqlc.arg(task)::text, sqlc.arg(id)::int);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CREATE TABLE todos (
2+
id serial PRIMARY KEY,
3+
task text
4+
);
5+
6+
CREATE PROCEDURE create_todo(IN p_task text, OUT p_id int)
7+
LANGUAGE plpgsql AS $$
8+
BEGIN
9+
INSERT INTO todos (task) VALUES (p_task) RETURNING id INTO p_id;
10+
END;
11+
$$;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"version": "1",
3+
"packages": [
4+
{
5+
"path": "go",
6+
"engine": "postgresql",
7+
"sql_package": "pgx/v5",
8+
"name": "querytest",
9+
"schema": "schema.sql",
10+
"queries": "query.sql"
11+
}
12+
]
13+
}

internal/sql/catalog/func.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ func (f *Function) InArgs() []*Argument {
3939
return args
4040
}
4141

42+
// CallArgs returns the arguments that must be supplied positionally for a
43+
// PostgreSQL CALL statement. Unlike InArgs, OUT parameters are included, since
44+
// CALL requires placeholder values for OUT parameters in their declared
45+
// positions. TABLE parameters remain excluded as they describe return columns
46+
// rather than callable arguments.
47+
func (f *Function) CallArgs() []*Argument {
48+
var args []*Argument
49+
for _, a := range f.Args {
50+
if a.Mode == ast.FuncParamTable {
51+
continue
52+
}
53+
args = append(args, a)
54+
}
55+
return args
56+
}
57+
4258
func (f *Function) OutArgs() []*Argument {
4359
var args []*Argument
4460
for _, a := range f.Args {

internal/sql/catalog/public.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ func (c *Catalog) ListFuncsByName(rel *ast.FuncName) ([]Function, error) {
3333
}
3434

3535
func (c *Catalog) ResolveFuncCall(call *ast.FuncCall) (*Function, error) {
36+
return c.resolveFuncCall(call, false)
37+
}
38+
39+
// ResolveCallStmt resolves the procedure referenced by a PostgreSQL CALL
40+
// statement. Unlike ResolveFuncCall it includes OUT parameters when matching
41+
// the supplied argument count, because CALL requires placeholder values for
42+
// OUT parameters in their declared positions.
43+
//
44+
// See: https://www.postgresql.org/docs/current/sql-call.html
45+
func (c *Catalog) ResolveCallStmt(call *ast.FuncCall) (*Function, error) {
46+
return c.resolveFuncCall(call, true)
47+
}
48+
49+
func (c *Catalog) resolveFuncCall(call *ast.FuncCall, isCallStmt bool) (*Function, error) {
3650
// Do not validate unknown functions
3751
funs, err := c.ListFuncsByName(call.Func)
3852
if err != nil || len(funs) == 0 {
@@ -64,7 +78,12 @@ func (c *Catalog) ResolveFuncCall(call *ast.FuncCall) (*Function, error) {
6478
}
6579

6680
for _, fun := range funs {
67-
args := fun.InArgs()
81+
var args []*Argument
82+
if isCallStmt {
83+
args = fun.CallArgs()
84+
} else {
85+
args = fun.InArgs()
86+
}
6887
var defaults int
6988
var variadic bool
7089
known := map[string]struct{}{}

internal/sql/validate/func_call.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,30 @@ func (v *funcCallVisitor) Visit(node ast.Node) astutils.Visitor {
2121
return nil
2222
}
2323

24+
// PostgreSQL CALL: resolve against all callable parameters (IN + OUT)
25+
// because CALL requires placeholder values for OUT parameters in their
26+
// declared positions. Returning nil here prevents Walk from descending
27+
// into the inner FuncCall, which would otherwise be re-validated using
28+
// the stricter IN-only matching path.
29+
if cs, ok := node.(*ast.CallStmt); ok {
30+
if cs.FuncCall == nil {
31+
return nil
32+
}
33+
fn := cs.FuncCall.Func
34+
if fn == nil || fn.Schema == "sqlc" {
35+
return nil
36+
}
37+
fun, err := v.catalog.ResolveCallStmt(cs.FuncCall)
38+
if fun != nil {
39+
return nil
40+
}
41+
if errors.Is(err, sqlerr.NotFound) && !v.settings.Package.StrictFunctionChecks {
42+
return nil
43+
}
44+
v.err = err
45+
return nil
46+
}
47+
2448
call, ok := node.(*ast.FuncCall)
2549
if !ok {
2650
return v

0 commit comments

Comments
 (0)