diff --git a/src/SchemaSaurus.PostgreSql/PostgreSqlSchemaReader.Routines.cs b/src/SchemaSaurus.PostgreSql/PostgreSqlSchemaReader.Routines.cs index a99a226..5fd459e 100644 --- a/src/SchemaSaurus.PostgreSql/PostgreSqlSchemaReader.Routines.cs +++ b/src/SchemaSaurus.PostgreSql/PostgreSqlSchemaReader.Routines.cs @@ -206,7 +206,6 @@ CROSS JOIN LATERAL unnest( WHERE {routineWhere} AND ns.nspname NOT IN ('pg_catalog', 'information_schema'){schemaWhere} AND NOT EXISTS (SELECT 1 FROM pg_depend dep WHERE dep.objid = proc.oid AND dep.deptype IN ('e', 'x')) - AND param.parameter_name IS NOT NULL AND param.parameter_mode <> 't' ORDER BY proc.oid, param.ordinal_position """; @@ -231,7 +230,12 @@ AND param.parameter_mode <> 't' continue; var ordinal = (int)reader.GetInt64(positionOrdinal); - var parameterName = reader.GetString(nameOrdinal); + var parameterName = reader.GetStringNull(nameOrdinal); + + // PostgreSQL leaves unnamed arguments as NULL or an empty string in proargnames; name them positionally + if (string.IsNullOrEmpty(parameterName)) + parameterName = $"${ordinal}"; + var direction = MapParameterDirection(reader.GetString(directionOrdinal)); var typeName = reader.GetString(typeNameOrdinal); var formattedTypeName = AdjustFormattedTypeName(reader.GetString(formattedTypeOrdinal)); diff --git a/test/SchemaSaurus.PostgreSql.Tests/Scripts/Script008.UnnamedParameters.sql b/test/SchemaSaurus.PostgreSql.Tests/Scripts/Script008.UnnamedParameters.sql new file mode 100644 index 0000000..2e6973e --- /dev/null +++ b/test/SchemaSaurus.PostgreSql.Tests/Scripts/Script008.UnnamedParameters.sql @@ -0,0 +1,29 @@ +-- unnamed input combined with a named OUT parameter => proargnames = {'', 'result'} +CREATE OR REPLACE FUNCTION public."UnnamedInputWithOutput"( + integer, + OUT "Result" text +) +LANGUAGE sql +AS $$ + SELECT 'x'::text; +$$; + +-- unnamed input on a RETURNS TABLE function => proargnames = {''} +CREATE OR REPLACE FUNCTION public."UnnamedInputReturnsTable"( + integer +) +RETURNS TABLE ("Id" integer) +LANGUAGE sql +AS $$ + SELECT 1; +$$; + +-- no named parameters at all => proargnames IS NULL +CREATE OR REPLACE FUNCTION public."UnnamedInputScalar"( + integer +) +RETURNS integer +LANGUAGE sql +AS $$ + SELECT $1; +$$; diff --git a/test/SchemaSaurus.PostgreSql.Tests/UnnamedParameterTests.cs b/test/SchemaSaurus.PostgreSql.Tests/UnnamedParameterTests.cs new file mode 100644 index 0000000..8ca8783 --- /dev/null +++ b/test/SchemaSaurus.PostgreSql.Tests/UnnamedParameterTests.cs @@ -0,0 +1,47 @@ +using SchemaSaurus.Metadata; +using SchemaSaurus.PostgreSql.Tests.Fixtures; + +namespace SchemaSaurus.PostgreSql.Tests; + +public class UnnamedParameterTests(DatabaseFixture databaseFixture) + : SchemaReaderTestBase(databaseFixture) +{ + [Fact] + public async Task WhenFunctionHasUnnamedInputWithNamedOutputThenParametersArePositionallyNamed() + { + var model = await GetDatabaseModelAsync(); + var func = model.ScalarFunctions.First(f => f.QualifiedName.Name == "UnnamedInputWithOutput"); + + func.Parameters.Should().HaveCount(2); + + var input = func.Parameters.Single(p => p.Ordinal == 1); + input.Name.Should().Be("$1"); + input.Direction.Should().Be(ParameterDirection.Input); + input.SystemType.Should().Be(typeof(int)); + + var output = func.Parameters.Single(p => p.Ordinal == 2); + output.Name.Should().Be("Result"); + output.Direction.Should().Be(ParameterDirection.Output); + } + + [Fact] + public async Task WhenTableValuedFunctionHasUnnamedInputThenParameterIsPositionallyNamed() + { + var model = await GetDatabaseModelAsync(); + var func = model.TableValuedFunctions.First(f => f.QualifiedName.Name == "UnnamedInputReturnsTable"); + + func.Parameters.Should().HaveCount(1); + func.Parameters[0].Name.Should().Be("$1"); + func.Parameters[0].Ordinal.Should().Be(1); + } + + [Fact] + public async Task WhenFunctionHasNoNamedParametersThenParameterIsPositionallyNamed() + { + var model = await GetDatabaseModelAsync(); + var func = model.ScalarFunctions.First(f => f.QualifiedName.Name == "UnnamedInputScalar"); + + func.Parameters.Should().HaveCount(1); + func.Parameters[0].Name.Should().Be("$1"); + } +}