Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- name: CreateTodoNullPlaceholder :exec
CALL create_todo(sqlc.arg(task)::text, null);

-- name: CreateTodoTypedNullPlaceholder :exec
CALL create_todo(sqlc.arg(task)::text, NULL::int);

-- name: CreateTodoNamedOut :exec
CALL create_todo(sqlc.arg(task)::text, sqlc.arg(id)::int);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE todos (
id serial PRIMARY KEY,
task text
);

CREATE PROCEDURE create_todo(IN p_task text, OUT p_id int)
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO todos (task) VALUES (p_task) RETURNING id INTO p_id;
END;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "1",
"packages": [
{
"path": "go",
"engine": "postgresql",
"sql_package": "pgx/v5",
"name": "querytest",
"schema": "schema.sql",
"queries": "query.sql"
}
]
}
16 changes: 16 additions & 0 deletions internal/sql/catalog/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ func (f *Function) InArgs() []*Argument {
return args
}

// CallArgs returns the arguments that must be supplied positionally for a
// PostgreSQL CALL statement. Unlike InArgs, OUT parameters are included, since
// CALL requires placeholder values for OUT parameters in their declared
// positions. TABLE parameters remain excluded as they describe return columns
// rather than callable arguments.
func (f *Function) CallArgs() []*Argument {
var args []*Argument
for _, a := range f.Args {
if a.Mode == ast.FuncParamTable {
continue
}
args = append(args, a)
}
return args
}

func (f *Function) OutArgs() []*Argument {
var args []*Argument
for _, a := range f.Args {
Expand Down
21 changes: 20 additions & 1 deletion internal/sql/catalog/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ func (c *Catalog) ListFuncsByName(rel *ast.FuncName) ([]Function, error) {
}

func (c *Catalog) ResolveFuncCall(call *ast.FuncCall) (*Function, error) {
return c.resolveFuncCall(call, false)
}

// ResolveCallStmt resolves the procedure referenced by a PostgreSQL CALL
// statement. Unlike ResolveFuncCall it includes OUT parameters when matching
// the supplied argument count, because CALL requires placeholder values for
// OUT parameters in their declared positions.
//
// See: https://www.postgresql.org/docs/current/sql-call.html
func (c *Catalog) ResolveCallStmt(call *ast.FuncCall) (*Function, error) {
return c.resolveFuncCall(call, true)
}

func (c *Catalog) resolveFuncCall(call *ast.FuncCall, isCallStmt bool) (*Function, error) {
// Do not validate unknown functions
funs, err := c.ListFuncsByName(call.Func)
if err != nil || len(funs) == 0 {
Expand Down Expand Up @@ -64,7 +78,12 @@ func (c *Catalog) ResolveFuncCall(call *ast.FuncCall) (*Function, error) {
}

for _, fun := range funs {
args := fun.InArgs()
var args []*Argument
if isCallStmt {
args = fun.CallArgs()
} else {
args = fun.InArgs()
}
var defaults int
var variadic bool
known := map[string]struct{}{}
Expand Down
24 changes: 24 additions & 0 deletions internal/sql/validate/func_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ func (v *funcCallVisitor) Visit(node ast.Node) astutils.Visitor {
return nil
}

// PostgreSQL CALL: resolve against all callable parameters (IN + OUT)
// because CALL requires placeholder values for OUT parameters in their
// declared positions. Returning nil here prevents Walk from descending
// into the inner FuncCall, which would otherwise be re-validated using
// the stricter IN-only matching path.
if cs, ok := node.(*ast.CallStmt); ok {
if cs.FuncCall == nil {
return nil
}
fn := cs.FuncCall.Func
if fn == nil || fn.Schema == "sqlc" {
return nil
}
fun, err := v.catalog.ResolveCallStmt(cs.FuncCall)
if fun != nil {
return nil
}
if errors.Is(err, sqlerr.NotFound) && !v.settings.Package.StrictFunctionChecks {
return nil
}
v.err = err
return nil
}

call, ok := node.(*ast.FuncCall)
if !ok {
return v
Expand Down
Loading