diff --git a/Makefile b/Makefile index b8745e57dc..4617651286 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-endtoend test test-ci test-examples test-endtoend start psql mysqlsh proto +.PHONY: build build-endtoend test test-ci test-examples test-endtoend start psql mysqlsh proto sqlc build: go build ./... @@ -23,17 +23,20 @@ build-endtoend: test-ci: test-examples build-endtoend vet +sqlc: + go build -o ./bin/sqlc ./cmd/sqlc/ + sqlc-dev: - go build -o ~/bin/sqlc-dev ./cmd/sqlc/ + go build -o ./bin/sqlc-dev ./cmd/sqlc/ sqlc-pg-gen: - go build -o ~/bin/sqlc-pg-gen ./internal/tools/sqlc-pg-gen + go build -o ./bin/sqlc-pg-gen ./internal/tools/sqlc-pg-gen sqlc-gen-json: - go build -o ~/bin/sqlc-gen-json ./cmd/sqlc-gen-json + go build -o ./bin/sqlc-gen-json ./cmd/sqlc-gen-json test-json-process-plugin: - go build -o ~/bin/test-json-process-plugin ./scripts/test-json-process-plugin/ + go build -o ./bin/test-json-process-plugin ./scripts/test-json-process-plugin/ start: docker compose up -d diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/internal/compiler/output_columns.go b/internal/compiler/output_columns.go index dbd486359a..65e7badd33 100644 --- a/internal/compiler/output_columns.go +++ b/internal/compiler/output_columns.go @@ -59,6 +59,25 @@ func (c *Compiler) outputColumns(qc *QueryCatalog, node ast.Node) ([]*Column, er targets := &ast.List{} switch n := node.(type) { + case *ast.CallStmt: + fun, err := qc.catalog.ResolveFuncCall(n.FuncCall) + if err != nil { + return nil, err + } + var cols []*Column + for _, arg := range fun.Args { + if arg.Mode == ast.FuncParamOut || arg.Mode == ast.FuncParamInOut || arg.Mode == ast.FuncParamTable { + name := arg.Name + typeName := arg.Type.Name + if arg.Type.Names != nil && len(arg.Type.Names.Items) > 0 { + typeName = astutils.Join(arg.Type.Names, ".") + } else if arg.Type.Schema != "" { + typeName = arg.Type.Schema + "." + arg.Type.Name + } + cols = append(cols, &Column{Name: name, DataType: typeName, NotNull: false}) + } + } + return cols, nil case *ast.DeleteStmt: targets = n.ReturningList case *ast.InsertStmt: @@ -580,8 +599,7 @@ func (c *Compiler) sourceTables(qc *QueryCatalog, node ast.Node) ([]*Table, erro DataType: arg.Type.Name, }) } - } - if fn.ReturnType != nil { + } else if fn.ReturnType != nil { table.Columns = []*Column{ { Name: colName, diff --git a/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..1e00549714 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..05eac6134e --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/models.go @@ -0,0 +1,18 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type TestDatum struct { + TestID pgtype.Int4 + TestDate pgtype.Date + TestTime pgtype.Timestamptz + TestString pgtype.Text + TestVarchar pgtype.Text + TestDouble pgtype.Float8 +} diff --git a/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..b9de382bb7 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,92 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const selectAll = `-- name: SelectAll :many +SELECT test_id, test_date, test_time, test_string, test_varchar, test_double FROM public.get_test() +` + +type SelectAllRow struct { + TestID pgtype.Int4 + TestDate pgtype.Date + TestTime pgtype.Timestamptz + TestString pgtype.Text + TestVarchar interface{} + TestDouble pgtype.Float8 +} + +func (q *Queries) SelectAll(ctx context.Context) ([]SelectAllRow, error) { + rows, err := q.db.Query(ctx, selectAll) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SelectAllRow + for rows.Next() { + var i SelectAllRow + if err := rows.Scan( + &i.TestID, + &i.TestDate, + &i.TestTime, + &i.TestString, + &i.TestVarchar, + &i.TestDouble, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const selectWithTime = `-- name: SelectWithTime :many +SELECT test_id, test_date, test_time, test_string, test_varchar, test_double FROM public.get_test($1::timestamp) +` + +type SelectWithTimeRow struct { + TestID pgtype.Int4 + TestDate pgtype.Date + TestTime pgtype.Timestamptz + TestString pgtype.Text + TestVarchar interface{} + TestDouble pgtype.Float8 +} + +func (q *Queries) SelectWithTime(ctx context.Context, dollar_1 pgtype.Timestamp) ([]SelectWithTimeRow, error) { + rows, err := q.db.Query(ctx, selectWithTime, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SelectWithTimeRow + for rows.Next() { + var i SelectWithTimeRow + if err := rows.Scan( + &i.TestID, + &i.TestDate, + &i.TestTime, + &i.TestString, + &i.TestVarchar, + &i.TestDouble, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..a72fe0b364 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/query.sql @@ -0,0 +1,5 @@ +-- name: SelectAll :many +SELECT * FROM public.get_test(); + +-- name: SelectWithTime :many +SELECT * FROM public.get_test($1::timestamp); diff --git a/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..d382cc63b4 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/schema.sql @@ -0,0 +1,34 @@ +-- Create mock test table +create table if not exists public.test_data +( + test_id integer, + test_date date, + test_time timestamp with time zone, + test_string text, + test_varchar character varying, + test_double double precision +); + +create function public.get_test(input_time timestamp without time zone DEFAULT now()) + returns TABLE + ( + test_id integer, + test_date date, + test_time timestamp with time zone, + test_string text, + test_varchar character varying, + test_double double precision + ) + stable + language sql +as +$$ +SELECT test_id, + test_date, + test_time, + test_string, + test_varchar, + test_double +FROM public.test_data +WHERE test_time <= input_time +$$; diff --git a/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_function_table/postgresql/pgx/v5/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v5", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/db.go new file mode 100644 index 0000000000..1e00549714 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/models.go new file mode 100644 index 0000000000..aeb0ffd1ff --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/models.go @@ -0,0 +1,13 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Tbl struct { + Value pgtype.Int4 +} diff --git a/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/query.sql.go new file mode 100644 index 0000000000..7eeea6c0a4 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/go/query.sql.go @@ -0,0 +1,189 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const callInsertData = `-- name: CallInsertData :one +CALL insert_data($1, $2, null, null, null, null, null, null, null, null, null, null, null) +` + +type CallInsertDataParams struct { + A int32 + B int32 +} + +type CallInsertDataRow struct { + C pgtype.Int4 + I pgtype.Float8 + J pgtype.Numeric + K pgtype.Float4 + D pgtype.Text + H pgtype.Text + E pgtype.Timestamp + M pgtype.Interval + F []byte + G []byte + L pgtype.Bool +} + +func (q *Queries) CallInsertData(ctx context.Context, arg CallInsertDataParams) (CallInsertDataRow, error) { + row := q.db.QueryRow(ctx, callInsertData, arg.A, arg.B) + var i CallInsertDataRow + err := row.Scan( + &i.C, + &i.I, + &i.J, + &i.K, + &i.D, + &i.H, + &i.E, + &i.M, + &i.F, + &i.G, + &i.L, + ) + return i, err +} + +const callInsertDataNamed = `-- name: CallInsertDataNamed :one +CALL insert_data( + b => $1, + a => $2, + c => null, + i => null, + j => null, + k => null, + d => null, + h => null, + e => null, + m => null, + f => null, + g => null, + l => null + ) +` + +type CallInsertDataNamedParams struct { + B int32 + A int32 +} + +type CallInsertDataNamedRow struct { + C pgtype.Int4 + I pgtype.Float8 + J pgtype.Numeric + K pgtype.Float4 + D pgtype.Text + H pgtype.Text + E pgtype.Timestamp + M pgtype.Interval + F []byte + G []byte + L pgtype.Bool +} + +func (q *Queries) CallInsertDataNamed(ctx context.Context, arg CallInsertDataNamedParams) (CallInsertDataNamedRow, error) { + row := q.db.QueryRow(ctx, callInsertDataNamed, arg.B, arg.A) + var i CallInsertDataNamedRow + err := row.Scan( + &i.C, + &i.I, + &i.J, + &i.K, + &i.D, + &i.H, + &i.E, + &i.M, + &i.F, + &i.G, + &i.L, + ) + return i, err +} + +const callInsertDataNoArgs = `-- name: CallInsertDataNoArgs :one +CALL insert_data(1, 2, null, null, null, null, null, null, null, null, null, null, null) +` + +type CallInsertDataNoArgsRow struct { + C pgtype.Int4 + I pgtype.Float8 + J pgtype.Numeric + K pgtype.Float4 + D pgtype.Text + H pgtype.Text + E pgtype.Timestamp + M pgtype.Interval + F []byte + G []byte + L pgtype.Bool +} + +func (q *Queries) CallInsertDataNoArgs(ctx context.Context) (CallInsertDataNoArgsRow, error) { + row := q.db.QueryRow(ctx, callInsertDataNoArgs) + var i CallInsertDataNoArgsRow + err := row.Scan( + &i.C, + &i.I, + &i.J, + &i.K, + &i.D, + &i.H, + &i.E, + &i.M, + &i.F, + &i.G, + &i.L, + ) + return i, err +} + +const callInsertDataSqlcArgs = `-- name: CallInsertDataSqlcArgs :one +CALL insert_data($1, $2, null, null, null, null, null, null, null, null, null, null, null) +` + +type CallInsertDataSqlcArgsParams struct { + Foo int32 + Bar int32 +} + +type CallInsertDataSqlcArgsRow struct { + C pgtype.Int4 + I pgtype.Float8 + J pgtype.Numeric + K pgtype.Float4 + D pgtype.Text + H pgtype.Text + E pgtype.Timestamp + M pgtype.Interval + F []byte + G []byte + L pgtype.Bool +} + +func (q *Queries) CallInsertDataSqlcArgs(ctx context.Context, arg CallInsertDataSqlcArgsParams) (CallInsertDataSqlcArgsRow, error) { + row := q.db.QueryRow(ctx, callInsertDataSqlcArgs, arg.Foo, arg.Bar) + var i CallInsertDataSqlcArgsRow + err := row.Scan( + &i.C, + &i.I, + &i.J, + &i.K, + &i.D, + &i.H, + &i.E, + &i.M, + &i.F, + &i.G, + &i.L, + ) + return i, err +} diff --git a/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/pg_procedure_instruction.md b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/pg_procedure_instruction.md new file mode 100644 index 0000000000..2f535a6679 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/pg_procedure_instruction.md @@ -0,0 +1,50 @@ +# PostgreSQL Procedure OUT Arguments Support + +I have implemented support for capturing `OUT` and `INOUT` arguments from PostgreSQL `CALL` statements in `sqlc`. + +## Implementation Details + +The `sqlc` analyzer/compiler (`internal/compiler/output_columns.go`) was updated to handle `CALL` statements. Previously, `CALL` statements were treated as having no output columns. The updated logic now: +1. Resolves the procedure definition from the catalog. +2. Identifies arguments with `OUT`, `INOUT`, or `TABLE` modes. +3. Maps these arguments to output columns in the generated code. + +## How to get OUT params + +To retrieve `OUT` arguments in your Go code: + +1. **Annotate your query with `:one`**: Use `:one` (or `:many` if the procedure returns multiple rows) instead of `:exec`. `:exec` tells `sqlc` to disregard any result rows, so it won't capture the `OUT` values. + + ```sql + -- name: CallInsertData :one + CALL insert_data($1, $2, null); + ``` + +2. **Provide placeholders**: You still need to provide placeholders for the `OUT` arguments in the SQL call (e.g., `null`), as required by PostgreSQL's `CALL` syntax when not using named arguments for everything, or simply to satisfy `sqlc`'s signature matching. + +3. **Use the return value**: The generated Go method will now return the `OUT` parameters. + - If there is a single `OUT` parameter, it will be returned directly. + - If there are multiple `OUT` parameters, a struct will be returned containing all of them. + +### Example + +**Schema:** +```sql +CREATE PROCEDURE insert_data(IN a integer, IN b integer, OUT c integer) ... +``` + +**Query:** +```sql +-- name: CallInsertData :one +CALL insert_data($1, $2, null); +``` + +**Generated Go:** +```go +func (q *Queries) CallInsertData(ctx context.Context, arg CallInsertDataParams) (pgtype.Int4, error) { + // ... + var c pgtype.Int4 + err := row.Scan(&c) + return c, err +} +``` diff --git a/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/query.sql new file mode 100644 index 0000000000..e286101479 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/query.sql @@ -0,0 +1,25 @@ +-- name: CallInsertData :one +CALL insert_data($1, $2, null, null, null, null, null, null, null, null, null, null, null); + +-- name: CallInsertDataNoArgs :one +CALL insert_data(1, 2, null, null, null, null, null, null, null, null, null, null, null); + +-- name: CallInsertDataNamed :one +CALL insert_data( + b => $1, + a => $2, + c => null, + i => null, + j => null, + k => null, + d => null, + h => null, + e => null, + m => null, + f => null, + g => null, + l => null + ); + +-- name: CallInsertDataSqlcArgs :one +CALL insert_data(sqlc.arg('foo'), sqlc.arg('bar'), null, null, null, null, null, null, null, null, null, null, null); diff --git a/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/schema.sql new file mode 100644 index 0000000000..25e073a673 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/schema.sql @@ -0,0 +1,55 @@ +CREATE TABLE tbl +( + value integer +); + +-- https://www.postgresql.org/docs/current/sql-createprocedure.html +CREATE PROCEDURE insert_data( + IN a integer, + IN b integer, + -- Numbers + OUT c integer, + OUT i float, + OUT j numeric, + OUT k real, + -- Text + OUT d varchar, + OUT h text, + -- Time + OUT e timestamp, + OUT m interval, + -- Other + OUT f jsonb, + OUT g bytea, + OUT l boolean +) + LANGUAGE plpgsql +AS +$$ +BEGIN + INSERT INTO tbl VALUES (a); + INSERT INTO tbl VALUES (b); + + c := 777; + + -- Numbers assignments + i := random() * 100; + j := (random() * 500)::numeric(10, 2); + k := (random() * 50)::real; + + -- Text assignments + d := 'Varchar val ' || floor(random() * 100); + h := 'Text val ' || md5(random()::text); + + -- Time assignments + e := now(); + m := make_interval(hours => floor(random() * 24)::int); + + -- Other assignments + f := '{ + "key": "random" + }'::jsonb; + g := '\xDEADBEEF'::bytea; + l := (random() > 0.5); +END; +$$; diff --git a/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/sqlc.json b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/sqlc.json new file mode 100644 index 0000000000..32ede07158 --- /dev/null +++ b/internal/endtoend/testdata/ddl_create_procedure_with_out_args/postgresql/pgx/v5/sqlc.json @@ -0,0 +1,13 @@ +{ + "version": "1", + "packages": [ + { + "path": "go", + "engine": "postgresql", + "sql_package": "pgx/v5", + "name": "querytest", + "schema": "schema.sql", + "queries": "query.sql" + } + ] +} diff --git a/internal/sql/catalog/func.go b/internal/sql/catalog/func.go index e170777311..d09a0d3f0e 100644 --- a/internal/sql/catalog/func.go +++ b/internal/sql/catalog/func.go @@ -43,7 +43,7 @@ func (f *Function) OutArgs() []*Argument { var args []*Argument for _, a := range f.Args { switch a.Mode { - case ast.FuncParamOut: + case ast.FuncParamOut, ast.FuncParamTable: args = append(args, a) } } diff --git a/internal/sql/catalog/public.go b/internal/sql/catalog/public.go index 19fd50722f..7cb51d3566 100644 --- a/internal/sql/catalog/public.go +++ b/internal/sql/catalog/public.go @@ -64,49 +64,107 @@ func (c *Catalog) ResolveFuncCall(call *ast.FuncCall) (*Function, error) { } for _, fun := range funs { - args := fun.InArgs() - var defaults int - var variadic bool + // Separate input and output args from the function signature + inArgs := fun.InArgs() + outArgs := fun.OutArgs() + + // Build known argument names from all parameters (IN/OUT/INOUT/etc.) known := map[string]struct{}{} - for _, arg := range args { + for _, a := range fun.Args { + if a.Name != "" { + known[a.Name] = struct{}{} + } + } + + // Count defaults and whether the last IN arg is variadic + var defaultsIn int + var variadic bool + for _, arg := range inArgs { if arg.HasDefault { - defaults += 1 + defaultsIn += 1 } if arg.Mode == ast.FuncParamVariadic { variadic = true - defaults += 1 + // Treat the variadic parameter like having a default for count checks + defaultsIn += 1 } - if arg.Name != "" { - known[arg.Name] = struct{}{} + } + + // Tally named arguments provided by the call: which refer to IN vs OUT names + var namedIn, namedOut int + var unknownArgName bool + for _, expr := range named { + if expr.Name != nil { + name := *expr.Name + if _, ok := known[name]; !ok { + unknownArgName = true + continue + } + // Classify whether the provided named arg matches an IN or OUT param + var isIn bool + for _, a := range inArgs { + if a.Name == name { + isIn = true + break + } + } + if isIn { + namedIn += 1 + } else { + // If not IN, treat it as an OUT placeholder/name + namedOut += 1 + } } } + if unknownArgName { + // Provided a named argument that doesn't exist in the signature + continue + } + // Positional arguments always come first (we validated above that + // positional cannot follow named). They fill IN parameters first; any + // excess positional arguments are treated as placeholders for OUT params + var posFillIn = len(positional) + if posFillIn > len(inArgs) { + posFillIn = len(inArgs) + } + // Count how many IN arguments are provided (positional for IN + named for IN) + inProvided := posFillIn + namedIn + + // Validate IN argument counts against the signature considering defaults/variadic if variadic { - if (len(named) + len(positional)) < (len(args) - defaults) { + if inProvided < (len(inArgs) - defaultsIn) { continue } } else { - if (len(named) + len(positional)) > len(args) { + if inProvided > len(inArgs) { continue } - if (len(named) + len(positional)) < (len(args) - defaults) { + if inProvided < (len(inArgs) - defaultsIn) { continue } } - // Validate that the provided named arguments exist in the function - var unknownArgName bool - for _, expr := range named { - if expr.Name != nil { - if _, found := known[*expr.Name]; !found { - unknownArgName = true - } - } + // Validate OUT placeholders. These are only valid in procedure calls. + // For normal function invocation, callers cannot pass values for OUT params. + posOut := 0 + if len(positional) > len(inArgs) { + posOut = len(positional) - len(inArgs) } - if unknownArgName { - continue + outProvided := posOut + namedOut + if fun.ReturnType == nil { + // Procedure: allow passing placeholders for OUT params, but not more than available + if outProvided > len(outArgs) { + continue + } + } else { + // Function: do not allow any OUT placeholders + if outProvided > 0 { + continue + } } + // All checks passed for this candidate return &fun, nil } @@ -117,7 +175,7 @@ func (c *Catalog) ResolveFuncCall(call *ast.FuncCall) (*Function, error) { return nil, &sqlerr.Error{ Code: "42883", - Message: fmt.Sprintf("function %s(%s) does not exist", call.Func.Name, strings.Join(sig, ", ")), + Message: fmt.Sprintf("CODE 42883: function %s(%s) does not exist", call.Func.Name, strings.Join(sig, ", ")), Location: call.Pos(), // Hint: "No function matches the given name and argument types. You might need to add explicit type casts.", }