From eb0a8f2ff77cc21dfd0f8c940aafbb1af26b9156 Mon Sep 17 00:00:00 2001 From: David Boone Date: Thu, 3 Sep 2026 13:50:03 -0700 Subject: [PATCH 1/2] Add PostgreSQL test functions with unnamed parameters An unnamed input paired with a named OUT parameter, an unnamed input on a RETURNS TABLE function, and a function with no named parameters at all. Any one of these aborts the whole schema read, so every PostgreSQL schema reader test fails until the reader is fixed. --- .../Scripts/Script008.UnnamedParameters.sql | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/SchemaSaurus.PostgreSql.Tests/Scripts/Script008.UnnamedParameters.sql 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; +$$; From 64171c0af265b72cf4acc6ef6569c2ca772ef65d Mon Sep 17 00:00:00 2001 From: David Boone Date: Thu, 3 Sep 2026 13:50:03 -0700 Subject: [PATCH 2/2] Fix PostgreSQL routine reader for unnamed parameters PostgreSQL stores unnamed arguments as empty strings in proargnames whenever any argument is named, which happens for every function with an OUT parameter or a RETURNS TABLE clause. Those empty names passed the IS NOT NULL filter and made ParameterBuilder.Build throw, aborting the entire schema read. Unnamed arguments are now named positionally as $n, and the new tests assert that naming. --- .../PostgreSqlSchemaReader.Routines.cs | 8 +++- .../UnnamedParameterTests.cs | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 test/SchemaSaurus.PostgreSql.Tests/UnnamedParameterTests.cs 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/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"); + } +}