From 7e80ad51623b87dff4ad810124dd485684b6ef3f Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Mon, 16 Feb 2026 11:57:27 -0500
Subject: [PATCH 1/2] Add infrastructure for proargdflts in pg_proc.dat

Add support for specifying function argument defaults directly in
pg_proc.dat using a new proargdflts field for IN arguments and
provariadicdflt for VARIADIC arguments. The genbki.pl script
generates function_defaults.sql which is processed by initdb after
postgres.bki.
---
 src/backend/catalog/genbki.pl   | 218 +++++++++++++++++++++++++++++++-
 src/bin/initdb/initdb.c         |   5 +
 src/include/catalog/Makefile    |   4 +-
 src/include/catalog/meson.build |   2 +
 4 files changed, 225 insertions(+), 4 deletions(-)

diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index b2c1b1c5733..113cb0e4996 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -2,9 +2,12 @@
 #----------------------------------------------------------------------
 #
 # genbki.pl
-#    Perl script that generates postgres.bki and symbol definition
-#    headers from specially formatted header files and data files.
-#    postgres.bki is used to initialize the postgres template database.
+#    Perl script that generates postgres.bki, symbol definition headers,
+#    and function_defaults.sql from specially formatted header files and
+#    data files.  postgres.bki is used to initialize the postgres template
+#    database.  function_defaults.sql provides default argument values for
+#    built-in functions specified via proargdflts (for IN arguments)
+#    and provariadicdflt (for VARIADIC arguments) in pg_proc.dat.
 #
 # Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
 # Portions Copyright (c) 1994, Regents of the University of California
@@ -452,6 +455,9 @@ open my $syscache_ids_fh, '>', $syscache_ids_file . $tmpext
 my $syscache_info_file = $output_path . 'syscache_info.h';
 open my $syscache_info_fh, '>', $syscache_info_file . $tmpext
   or die "can't open $syscache_info_file$tmpext: $!";
+my $fmgr_defaults_file = $output_path . 'function_defaults.sql';
+open my $fmgr_defaults, '>', $fmgr_defaults_file . $tmpext
+  or die "can't open $fmgr_defaults_file$tmpext: $!";
 
 # Generate postgres.bki and pg_*_d.h headers.
 
@@ -597,6 +603,8 @@ EOM
 			  if $key eq "oid_symbol"
 			  || $key eq "array_type_oid"
 			  || $key eq "descr"
+			  || $key eq "proargdflts"
+			  || $key eq "provariadicdflt"
 			  || $key eq "autogenerated"
 			  || $key eq "line_number";
 			die sprintf "unrecognized field name \"%s\" in %s.dat line %s\n",
@@ -726,6 +734,171 @@ foreach my $c (@system_constraints)
 	print $constraints $c, "\n\n";
 }
 
+# Now generate function_defaults.sql
+# This file contains CREATE OR REPLACE FUNCTION statements to set default argument values
+# for functions that have proargdflts specified in pg_proc.dat.
+
+print $fmgr_defaults <<EOM;
+--
+-- PostgreSQL System Function Defaults
+--
+-- Auto-generated from pg_proc.dat proargdflts entries.
+-- Do not edit manually.
+--
+-- This file sets default argument values for built-in functions.
+-- It is processed after postgres.bki during initdb.
+--
+
+EOM
+
+# Maps for converting catalog codes to SQL keywords
+my %volatility_map = ('i' => 'IMMUTABLE', 's' => 'STABLE', 'v' => 'VOLATILE');
+my %parallel_map = ('s' => 'PARALLEL SAFE', 'r' => 'PARALLEL RESTRICTED', 'u' => 'PARALLEL UNSAFE');
+
+foreach my $row (@{ $catalog_data{pg_proc} })
+{
+	next unless defined $row->{proargdflts} || defined $row->{provariadicdflt};
+
+	my $proname = $row->{proname};
+
+	# Get all argument types (use proallargtypes if present and not NULL, else proargtypes)
+	my @allargtypes;
+	if (defined $row->{proallargtypes} && $row->{proallargtypes} ne '' && $row->{proallargtypes} ne '_null_')
+	{
+		my $alltypes = $row->{proallargtypes};
+		$alltypes =~ s/^\{|\}$//g;
+		@allargtypes = split /,/, $alltypes;
+	}
+	else
+	{
+		@allargtypes = split /\s+/, $row->{proargtypes};
+	}
+
+	# Get argument modes (i=IN, o=OUT, b=INOUT, v=VARIADIC, t=TABLE)
+	my @argmodes;
+	if (defined $row->{proargmodes} && $row->{proargmodes} ne '' && $row->{proargmodes} ne '_null_')
+	{
+		my $modes = $row->{proargmodes};
+		$modes =~ s/^\{|\}$//g;
+		@argmodes = split /,/, $modes;
+	}
+	else
+	{
+		# All arguments are IN if no modes specified
+		@argmodes = ('i') x scalar(@allargtypes);
+	}
+
+	# Parse proargnames: '{name1,name2}' -> ['name1', 'name2']
+	my @argnames;
+	if (defined $row->{proargnames})
+	{
+		my $names = $row->{proargnames};
+		$names =~ s/^\{|\}$//g;
+		@argnames = split /,/, $names;
+	}
+
+	# Count IN arguments (these are the ones that can have defaults)
+	my $n_in_args = 0;
+	for my $mode (@argmodes)
+	{
+		$n_in_args++ if $mode eq 'i';
+	}
+
+	# Parse defaults: '{val1,val2}' or 'val1,val2' -> ['val1', 'val2']
+	my @defaults;
+	my $ndefaults = 0;
+	if (defined $row->{proargdflts})
+	{
+		my $defaults_str = $row->{proargdflts};
+		$defaults_str =~ s/^\{|\}$//g;
+		# Split on comma, but respect parentheses for function calls like foo(1,2)
+		@defaults = parse_defaults_list($defaults_str);
+		$ndefaults = scalar @defaults;
+	}
+
+	if ($ndefaults > $n_in_args)
+	{
+		die sprintf "too many defaults (%d) for function %s with %d IN args at line %s\n",
+		  $ndefaults, $proname, $n_in_args, $row->{line_number};
+	}
+
+	# Build the argument list with names, types, modes, and defaults
+	my $first_default_in_arg = $n_in_args - $ndefaults;
+	my @arg_defs;
+	my $in_arg_idx = 0;
+	for (my $i = 0; $i < scalar(@allargtypes); $i++)
+	{
+		my $argname = $argnames[$i] // '';
+		my $argtype = $allargtypes[$i];
+		my $argmode = $argmodes[$i];
+		my $arg_def;
+
+		if ($argmode eq 'o')
+		{
+			$arg_def = "OUT $argname $argtype";
+		}
+		elsif ($argmode eq 'b')
+		{
+			$arg_def = "INOUT $argname $argtype";
+		}
+		elsif ($argmode eq 'v')
+		{
+			$arg_def = "VARIADIC $argname $argtype";
+			if (defined $row->{provariadicdflt})
+			{
+				my $vardefault = $row->{provariadicdflt};
+				$vardefault =~ s/^\s+|\s+$//g;  # trim whitespace
+				$arg_def .= " DEFAULT $vardefault";
+			}
+		}
+		else
+		{
+			# IN argument (mode 'i' or default)
+			$arg_def = "$argname $argtype";
+
+			# Add default if this IN arg has one
+			if ($in_arg_idx >= $first_default_in_arg)
+			{
+				my $default_idx = $in_arg_idx - $first_default_in_arg;
+				my $default_val = $defaults[$default_idx];
+				$default_val =~ s/^\s+|\s+$//g;  # trim whitespace
+				$arg_def .= " DEFAULT $default_val";
+			}
+			$in_arg_idx++;
+		}
+		push @arg_defs, $arg_def;
+	}
+
+	my $volatility = $volatility_map{$row->{provolatile} // 'v'} // 'VOLATILE';
+	my $parallel = $parallel_map{$row->{proparallel} // 'u'} // 'PARALLEL UNSAFE';
+
+	# Check if function is strict
+	my $strict = ($row->{proisstrict} // 't') eq 't' ? 'STRICT' : '';
+
+	# Get cost (default is 1 for internal functions)
+	my $cost = "COST " . ($row->{procost} // 1);
+
+	# Get return type and prosrc
+	my $rettype = $row->{prorettype};
+	# Add SETOF if proretset is true
+	if (($row->{proretset} // 'f') eq 't')
+	{
+		$rettype = "SETOF $rettype";
+	}
+	my $prosrc = $row->{prosrc};
+
+	# Build the CREATE OR REPLACE FUNCTION statement
+	my $args_str = join(",\n    ", @arg_defs);
+	my @options = grep { $_ ne '' } ($volatility, $parallel, $strict, $cost);
+	my $options_str = join(' ', @options);
+
+	print $fmgr_defaults "CREATE OR REPLACE FUNCTION \"$proname\"(\n    $args_str)\n" .
+		" RETURNS $rettype\n" .
+		" LANGUAGE internal\n" .
+		" $options_str\n" .
+		"AS '$prosrc';\n\n";
+}
+
 # Now generate schemapg.h
 
 print_boilerplate($schemapg, "schemapg.h",
@@ -837,6 +1010,7 @@ close $fk_info;
 close $constraints;
 close $syscache_ids_fh;
 close $syscache_info_fh;
+close $fmgr_defaults;
 
 # Finally, rename the completed files into place.
 Catalog::RenameTempFile($bkifile, $tmpext);
@@ -845,6 +1019,7 @@ Catalog::RenameTempFile($fk_info_file, $tmpext);
 Catalog::RenameTempFile($constraints_file, $tmpext);
 Catalog::RenameTempFile($syscache_ids_file, $tmpext);
 Catalog::RenameTempFile($syscache_info_file, $tmpext);
+Catalog::RenameTempFile($fmgr_defaults_file, $tmpext);
 
 exit($num_errors != 0 ? 1 : 0);
 
@@ -1176,6 +1351,43 @@ sub print_boilerplate
 EOM
 }
 
+# Parse a comma-separated list of default values, respecting parentheses.
+# This handles expressions like "foo(1,2), bar" correctly.
+sub parse_defaults_list
+{
+	my $str = shift;
+	my @result;
+	my $current = '';
+	my $depth = 0;
+
+	for my $char (split //, $str)
+	{
+		if ($char eq '(' || $char eq '[')
+		{
+			$depth++;
+			$current .= $char;
+		}
+		elsif ($char eq ')' || $char eq ']')
+		{
+			$depth--;
+			$current .= $char;
+		}
+		elsif ($char eq ',' && $depth == 0)
+		{
+			push @result, $current;
+			$current = '';
+		}
+		else
+		{
+			$current .= $char;
+		}
+	}
+	# Don't forget the last element
+	push @result, $current if $current ne '' || @result > 0;
+
+	return @result;
+}
+
 sub usage
 {
 	die <<EOM;
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index a3980e5535f..811988bb590 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -183,6 +183,7 @@ static char *info_schema_file;
 static char *features_file;
 static char *system_constraints_file;
 static char *system_functions_file;
+static char *function_defaults_file;
 static char *system_views_file;
 static bool success = false;
 static bool made_new_pgdata = false;
@@ -2799,6 +2800,7 @@ setup_data_file_paths(void)
 	set_input(&features_file, "sql_features.txt");
 	set_input(&system_constraints_file, "system_constraints.sql");
 	set_input(&system_functions_file, "system_functions.sql");
+	set_input(&function_defaults_file, "function_defaults.sql");
 	set_input(&system_views_file, "system_views.sql");
 
 	if (show_setting || debug)
@@ -2827,6 +2829,7 @@ setup_data_file_paths(void)
 	check_input(features_file);
 	check_input(system_constraints_file);
 	check_input(system_functions_file);
+	check_input(function_defaults_file);
 	check_input(system_views_file);
 }
 
@@ -3119,6 +3122,8 @@ initialize_data_directory(void)
 
 	setup_run_file(cmdfd, system_functions_file);
 
+	setup_run_file(cmdfd, function_defaults_file);
+
 	setup_depend(cmdfd);
 
 	/*
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index c90022f7c57..dbf67eb44dc 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -118,6 +118,7 @@ GENBKI_OUTPUT_FILES = \
 	$(GENERATED_HEADERS) \
 	postgres.bki \
 	system_constraints.sql \
+	function_defaults.sql \
 	schemapg.h \
 	syscache_ids.h \
 	syscache_info.h \
@@ -145,6 +146,7 @@ bki-stamp: $(top_srcdir)/src/backend/catalog/genbki.pl $(top_srcdir)/src/backend
 install: all installdirs
 	$(INSTALL_DATA) postgres.bki '$(DESTDIR)$(datadir)/postgres.bki'
 	$(INSTALL_DATA) system_constraints.sql '$(DESTDIR)$(datadir)/system_constraints.sql'
+	$(INSTALL_DATA) function_defaults.sql '$(DESTDIR)$(datadir)/function_defaults.sql'
 # In non-vpath builds, src/include/Makefile already installs all headers.
 ifeq ($(vpath_build),yes)
 	$(INSTALL_DATA) schemapg.h '$(DESTDIR)$(includedir_server)'/catalog/schemapg.h
@@ -159,7 +161,7 @@ installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(datadir)' '$(DESTDIR)$(includedir_server)'
 
 uninstall:
-	rm -f $(addprefix '$(DESTDIR)$(datadir)'/, postgres.bki system_constraints.sql)
+	rm -f $(addprefix '$(DESTDIR)$(datadir)'/, postgres.bki system_constraints.sql function_defaults.sql)
 	rm -f $(addprefix '$(DESTDIR)$(includedir_server)'/catalog/, schemapg.h syscache_ids.h system_fk_info.h $(GENERATED_HEADERS))
 
 clean:
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index b63cd584068..e9968438a0d 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -105,12 +105,14 @@ input = []
 output_files = [
   'postgres.bki',
   'system_constraints.sql',
+  'function_defaults.sql',
   'schemapg.h',
   'syscache_ids.h',
   'syscache_info.h',
   'system_fk_info.h',
 ]
 output_install = [
+  dir_data,
   dir_data,
   dir_data,
   dir_include_server / 'catalog',
-- 
2.43.0

