From d843d6508dac46a4fe3df85100593baf4187837a Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Thu, 21 Aug 2025 16:43:50 -0400
Subject: [PATCH v3 1/4] Provide more-specific error hints for function lookup
 failures.

Up to now we've contented ourselves with a one-size-fits-all error
hint when we fail to find any match to a function or procedure call.
That was mostly okay in the beginning, but in the presence of named
arguments it's really not great.  We at least ought to distinguish
"function name doesn't exist" from "function name exists, but not with
those argument names".  And the rules for named-argument matching are
arcane enough that some more detail seems warranted if we match the
argument names but the call still doesn't work.

This patch proposes a framework for dealing with these problems:
FuncnameGetCandidates and related code should pass back a bitmask of
flags showing how far the match succeeded.  This allows a considerable
amount of granularity in the reports.  The set-bits-in-a-bitmask
approach means that when there are multiple candidate functions, the
report will reflect the match(es) that got the furthest, which seems
correct.  Also, we can avoid mentioning "maybe add casts" unless
failure to match argument types is actually the issue.

The specific messages I've written could perhaps do with more
bike-shedding.

Reported-by: Dominique Devienne <ddevienne@gmail.com>
Author: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/1756041.1754616558@sss.pgh.pa.us
---
 src/backend/catalog/namespace.c               |  92 +++++++++----
 src/backend/catalog/pg_aggregate.c            |   2 +
 src/backend/parser/parse_func.c               | 125 ++++++++++++++++--
 src/backend/utils/adt/regproc.c               |  13 +-
 src/backend/utils/adt/ruleutils.c             |   2 +
 src/include/catalog/namespace.h               |  20 ++-
 src/include/parser/parse_func.h               |   1 +
 src/pl/plperl/expected/plperl_elog.out        |   2 +-
 src/pl/plperl/expected/plperl_elog_1.out      |   2 +-
 src/pl/plpython/expected/plpython_error.out   |   2 +-
 .../traces/pipeline_abort.trace               |   2 +-
 .../expected/test_extensions.out              |   3 +-
 .../regress/expected/create_procedure.out     |   2 +-
 src/test/regress/expected/misc_functions.out  |   4 +-
 src/test/regress/expected/plpgsql.out         |   2 +-
 src/test/regress/expected/polymorphism.out    |  60 ++++++++-
 src/test/regress/expected/temp.out            |   2 +-
 src/test/regress/sql/polymorphism.sql         |  17 +++
 18 files changed, 297 insertions(+), 56 deletions(-)

diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index d97d632a7ef..524f325be03 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -233,7 +233,7 @@ static void RemoveTempRelationsCallback(int code, Datum arg);
 static void InvalidationCallback(Datum arg, int cacheid, uint32 hashvalue);
 static bool MatchNamedCall(HeapTuple proctup, int nargs, List *argnames,
 						   bool include_out_arguments, int pronargs,
-						   int **argnumbers);
+						   int **argnumbers, int *fgc_flags);
 
 /*
  * Recomputing the namespace path can be costly when done frequently, such as
@@ -1118,15 +1118,15 @@ TypeIsVisibleExt(Oid typid, bool *is_missing)
 
 /*
  * FuncnameGetCandidates
- *		Given a possibly-qualified function name and argument count,
+ *		Given a possibly-qualified routine name, argument count, and arg names,
  *		retrieve a list of the possible matches.
  *
- * If nargs is -1, we return all functions matching the given name,
+ * If nargs is -1, we return all routines matching the given name,
  * regardless of argument count.  (argnames must be NIL, and expand_variadic
  * and expand_defaults must be false, in this case.)
  *
  * If argnames isn't NIL, we are considering a named- or mixed-notation call,
- * and only functions having all the listed argument names will be returned.
+ * and only routines having all the listed argument names will be returned.
  * (We assume that length(argnames) <= nargs and all the passed-in names are
  * distinct.)  The returned structs will include an argnumbers array showing
  * the actual argument index for each logical argument position.
@@ -1184,14 +1184,21 @@ TypeIsVisibleExt(Oid typid, bool *is_missing)
  * The caller might end up discarding such an entry anyway, but if it selects
  * such an entry it should react as though the call were ambiguous.
  *
- * If missing_ok is true, an empty list (NULL) is returned if the name was
- * schema-qualified with a schema that does not exist.  Likewise if no
- * candidate is found for other reasons.
+ * We return an empty list (NULL) if no suitable matches can be found.
+ * If the function name was schema-qualified with a schema that does not
+ * exist, then we return an empty list if missing_ok is true and otherwise
+ * throw an error.  (missing_ok does not affect the behavior otherwise.)
+ *
+ * The output argument *fgc_flags is filled with a bitmask indicating how
+ * far we were able to match the supplied information.  This is not of much
+ * interest if any candidates were found, but if not, it can help callers
+ * produce an on-point error message.
  */
 FuncCandidateList
 FuncnameGetCandidates(List *names, int nargs, List *argnames,
 					  bool expand_variadic, bool expand_defaults,
-					  bool include_out_arguments, bool missing_ok)
+					  bool include_out_arguments, bool missing_ok,
+					  int *fgc_flags)
 {
 	FuncCandidateList resultList = NULL;
 	bool		any_special = false;
@@ -1204,15 +1211,20 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames,
 	/* check for caller error */
 	Assert(nargs >= 0 || !(expand_variadic | expand_defaults));
 
+	/* initialize output fgc_flags to empty */
+	*fgc_flags = 0;
+
 	/* deconstruct the name list */
 	DeconstructQualifiedName(names, &schemaname, &funcname);
 
 	if (schemaname)
 	{
 		/* use exact schema given */
+		*fgc_flags |= FGC_SCHEMA_GIVEN; /* report that a schema is given */
 		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
 		if (!OidIsValid(namespaceId))
 			return NULL;
+		*fgc_flags |= FGC_SCHEMA_MATCH; /* report that the schema is valid */
 	}
 	else
 	{
@@ -1263,6 +1275,8 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames,
 				continue;		/* proc is not in search path */
 		}
 
+		*fgc_flags |= FGC_NAME_MATCH;	/* we found a matching routine name */
+
 		/*
 		 * If we are asked to match to OUT arguments, then use the
 		 * proallargtypes array (which includes those); otherwise use
@@ -1297,16 +1311,6 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames,
 			/*
 			 * Call uses named or mixed notation
 			 *
-			 * Named or mixed notation can match a variadic function only if
-			 * expand_variadic is off; otherwise there is no way to match the
-			 * presumed-nameless parameters expanded from the variadic array.
-			 */
-			if (OidIsValid(procform->provariadic) && expand_variadic)
-				continue;
-			va_elem_type = InvalidOid;
-			variadic = false;
-
-			/*
 			 * Check argument count.
 			 */
 			Assert(nargs >= 0); /* -1 not supported with argnames */
@@ -1325,12 +1329,33 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames,
 			if (pronargs != nargs && !use_defaults)
 				continue;
 
+			/* We found a routine with a suitable number of arguments */
+			*fgc_flags |= FGC_ARGCOUNT_MATCH;
+
 			/* Check for argument name match, generate positional mapping */
 			if (!MatchNamedCall(proctup, nargs, argnames,
 								include_out_arguments, pronargs,
-								&argnumbers))
+								&argnumbers, fgc_flags))
 				continue;
 
+			/*
+			 * Named or mixed notation can match a variadic function only if
+			 * expand_variadic is off; otherwise there is no way to match the
+			 * presumed-nameless parameters expanded from the variadic array.
+			 * However, we postpone the check until here because we want to
+			 * perform argument name matching anyway (using the variadic array
+			 * argument's name).  This allows us to give an on-point error
+			 * message if the user forgets to say VARIADIC in what would have
+			 * been a valid call with it.
+			 */
+			if (OidIsValid(procform->provariadic) && expand_variadic)
+				continue;
+			va_elem_type = InvalidOid;
+			variadic = false;
+
+			/* We found a fully-valid call using argument names */
+			*fgc_flags |= FGC_ARGNAMES_VALID;
+
 			/* Named argument matching is always "special" */
 			any_special = true;
 		}
@@ -1372,6 +1397,9 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames,
 			/* Ignore if it doesn't match requested argument count */
 			if (nargs >= 0 && pronargs != nargs && !variadic && !use_defaults)
 				continue;
+
+			/* We found a routine with a suitable number of arguments */
+			*fgc_flags |= FGC_ARGCOUNT_MATCH;
 		}
 
 		/*
@@ -1580,11 +1608,13 @@ FuncnameGetCandidates(List *names, int nargs, List *argnames,
  * the mapping from call argument positions to actual function argument
  * numbers.  Defaulted arguments are included in this map, at positions
  * after the last supplied argument.
+ *
+ * We also add flag bits to *fgc_flags reporting on how far the match got.
  */
 static bool
 MatchNamedCall(HeapTuple proctup, int nargs, List *argnames,
 			   bool include_out_arguments, int pronargs,
-			   int **argnumbers)
+			   int **argnumbers, int *fgc_flags)
 {
 	Form_pg_proc procform = (Form_pg_proc) GETSTRUCT(proctup);
 	int			numposargs = nargs - list_length(argnames);
@@ -1593,6 +1623,7 @@ MatchNamedCall(HeapTuple proctup, int nargs, List *argnames,
 	char	  **p_argnames;
 	char	   *p_argmodes;
 	bool		arggiven[FUNC_MAX_ARGS];
+	bool		arg_filled_twice = false;
 	bool		isnull;
 	int			ap;				/* call args position */
 	int			pp;				/* proargs position */
@@ -1646,9 +1677,9 @@ MatchNamedCall(HeapTuple proctup, int nargs, List *argnames,
 				continue;
 			if (p_argnames[i] && strcmp(p_argnames[i], argname) == 0)
 			{
-				/* fail if argname matches a positional argument */
+				/* note if argname matches a positional argument */
 				if (arggiven[pp])
-					return false;
+					arg_filled_twice = true;
 				arggiven[pp] = true;
 				(*argnumbers)[ap] = pp;
 				found = true;
@@ -1665,6 +1696,16 @@ MatchNamedCall(HeapTuple proctup, int nargs, List *argnames,
 
 	Assert(ap == nargs);		/* processed all actual parameters */
 
+	/* If we get here, the function did match all the supplied argnames */
+	*fgc_flags |= FGC_ARGNAMES_MATCH;
+
+	/* ... however, some of them might have been placed wrong */
+	if (arg_filled_twice)
+		return false;			/* some argname matched a positional argument */
+
+	/* If we get here, the call doesn't violate the rules for mixed notation */
+	*fgc_flags |= FGC_ARGNAMES_PLACED;
+
 	/* Check for default arguments */
 	if (nargs < pronargs)
 	{
@@ -1683,6 +1724,9 @@ MatchNamedCall(HeapTuple proctup, int nargs, List *argnames,
 
 	Assert(ap == pronargs);		/* processed all function parameters */
 
+	/* If we get here, the call supplies all the required arguments */
+	*fgc_flags |= FGC_ARGNAMES_ALL;
+
 	return true;
 }
 
@@ -1746,11 +1790,13 @@ FunctionIsVisibleExt(Oid funcid, bool *is_missing)
 		char	   *proname = NameStr(procform->proname);
 		int			nargs = procform->pronargs;
 		FuncCandidateList clist;
+		int			fgc_flags;
 
 		visible = false;
 
 		clist = FuncnameGetCandidates(list_make1(makeString(proname)),
-									  nargs, NIL, false, false, false, false);
+									  nargs, NIL, false, false, false, false,
+									  &fgc_flags);
 
 		for (; clist; clist = clist->next)
 		{
diff --git a/src/backend/catalog/pg_aggregate.c b/src/backend/catalog/pg_aggregate.c
index c62e8acd413..a1cb5719a0c 100644
--- a/src/backend/catalog/pg_aggregate.c
+++ b/src/backend/catalog/pg_aggregate.c
@@ -836,6 +836,7 @@ lookup_agg_function(List *fnName,
 	Oid			vatype;
 	Oid		   *true_oid_array;
 	FuncDetailCode fdresult;
+	int			fgc_flags;
 	AclResult	aclresult;
 	int			i;
 
@@ -848,6 +849,7 @@ lookup_agg_function(List *fnName,
 	 */
 	fdresult = func_get_detail(fnName, NIL, NIL,
 							   nargs, input_types, false, false, false,
+							   &fgc_flags,
 							   &fnOid, rettype, &retset,
 							   &nvargs, &vatype,
 							   &true_oid_array, NULL);
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 583bbbf232f..febccb74843 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -42,6 +42,8 @@ typedef enum
 	FUNCLOOKUP_AMBIGUOUS,
 } FuncLookupError;
 
+static int	func_lookup_failure_details(int fgc_flags, List *argnames,
+										bool proc_call);
 static void unify_hypothetical_args(ParseState *pstate,
 									List *fargs, int numAggregatedArgs,
 									Oid *actual_arg_types, Oid *declared_arg_types);
@@ -115,6 +117,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	int			nvargs;
 	Oid			vatype;
 	FuncDetailCode fdresult;
+	int			fgc_flags;
 	char		aggkind = 0;
 	ParseCallbackState pcbstate;
 
@@ -266,6 +269,7 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	fdresult = func_get_detail(funcname, fargs, argnames, nargs,
 							   actual_arg_types,
 							   !func_variadic, true, proc_call,
+							   &fgc_flags,
 							   &funcid, &rettype, &retset,
 							   &nvargs, &vatype,
 							   &declared_arg_types, &argdefaults);
@@ -601,7 +605,9 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 
 		/*
 		 * No function, and no column either.  Since we're dealing with
-		 * function notation, report "function does not exist".
+		 * function notation, report "function/procedure does not exist".
+		 * Depending on what was returned in fgc_flags, we can add some color
+		 * to that with detail or hint messages.
 		 */
 		if (list_length(agg_order) > 1 && !agg_within_group)
 		{
@@ -622,8 +628,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 					 errmsg("procedure %s does not exist",
 							func_signature_string(funcname, nargs, argnames,
 												  actual_arg_types)),
-					 errhint("No procedure matches the given name and argument types. "
-							 "You might need to add explicit type casts."),
+					 func_lookup_failure_details(fgc_flags, argnames,
+												 proc_call),
 					 parser_errposition(pstate, location)));
 		else
 			ereport(ERROR,
@@ -631,8 +637,8 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 					 errmsg("function %s does not exist",
 							func_signature_string(funcname, nargs, argnames,
 												  actual_arg_types)),
-					 errhint("No function matches the given name and argument types. "
-							 "You might need to add explicit type casts."),
+					 func_lookup_failure_details(fgc_flags, argnames,
+												 proc_call),
 					 parser_errposition(pstate, location)));
 	}
 
@@ -905,6 +911,93 @@ ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs,
 	return retval;
 }
 
+/*
+ * Interpret the fgc_flags and issue a suitable detail or hint message.
+ *
+ * Helper function to reduce code duplication while throwing a
+ * function-not-found error.
+ */
+static int
+func_lookup_failure_details(int fgc_flags, List *argnames, bool proc_call)
+{
+	/*
+	 * If not FGC_NAME_MATCH, we shouldn't raise the question of whether the
+	 * arguments are wrong.  It does seem worth calling the search_path to the
+	 * user's mind if the function name was not schema-qualified; but if it
+	 * was, there's really nothing to add to the basic "function/procedure %s
+	 * does not exist" message.
+	 *
+	 * Note: we passed missing_ok = false to FuncnameGetCandidates, so there's
+	 * no need to consider FGC_SCHEMA_MATCH here: we'd have already thrown an
+	 * error if an explicitly-given schema doesn't exist.
+	 */
+	if (!(fgc_flags & FGC_NAME_MATCH))
+	{
+		if (fgc_flags & FGC_SCHEMA_GIVEN)
+			return 0;			/* schema-qualified name */
+		else if (proc_call)
+			return errdetail("There is no procedure of that name in the search_path.");
+		else
+			return errdetail("There is no function of that name in the search_path.");
+	}
+
+	/*
+	 * Next, complain if nothing had the right number of arguments.  (This
+	 * takes precedence over wrong-argnames cases because we won't even look
+	 * at the argnames unless there's a workable number of arguments.)
+	 */
+	if (!(fgc_flags & FGC_ARGCOUNT_MATCH))
+	{
+		if (proc_call)
+			return errdetail("No procedure of that name has the right number of arguments.");
+		else
+			return errdetail("No function of that name has the right number of arguments.");
+	}
+
+	/*
+	 * If there are argnames, and we failed to match them, again we should
+	 * mention that and not bring up the argument types.
+	 */
+	if (argnames != NIL && !(fgc_flags & FGC_ARGNAMES_MATCH))
+	{
+		if (proc_call)
+			return errdetail("No procedure of that name matches the given argument names.");
+		else
+			return errdetail("No function of that name matches the given argument names.");
+	}
+
+	/*
+	 * We could have matched all the given argnames and still not have had a
+	 * valid call, either because of improper use of mixed notation, or
+	 * because of missing arguments, or because the user misused VARIADIC. The
+	 * rules about named-argument matching are finicky enough that it's worth
+	 * trying to be specific about the problem.  (The messages here are chosen
+	 * with full knowledge of the steps that namespace.c uses while checking a
+	 * potential match.)
+	 */
+	if (argnames != NIL && !(fgc_flags & FGC_ARGNAMES_PLACED))
+		return errdetail("Named arguments were incorrectly combined with positional arguments.");
+
+	if (argnames != NIL && !(fgc_flags & FGC_ARGNAMES_ALL))
+		return errdetail("Not all required arguments were supplied.");
+
+	if (argnames != NIL && !(fgc_flags & FGC_ARGNAMES_VALID))
+		return errhint("This call would be correct if the variadic array were labeled VARIADIC and placed last.");
+
+	if (fgc_flags & FGC_VARIADIC_FAIL)
+		return errhint("The VARIADIC parameter must be placed last, even when using argument names.");
+
+	/*
+	 * Otherwise, give our traditional hint about argument types and casting.
+	 */
+	if (proc_call)
+		return errhint("No procedure matches the given name and argument types. "
+					   "You might need to add explicit type casts.");
+	else
+		return errhint("No function matches the given name and argument types. "
+					   "You might need to add explicit type casts.");
+}
+
 
 /* func_match_argtypes()
  *
@@ -1372,9 +1465,14 @@ func_select_candidate(int nargs,
  *	1) check for possible interpretation as a type coercion request
  *	2) apply the ambiguous-function resolution rules
  *
- * Return values *funcid through *true_typeids receive info about the function.
- * If argdefaults isn't NULL, *argdefaults receives a list of any default
- * argument expressions that need to be added to the given arguments.
+ * If there is no match at all, we return FUNCDETAIL_NOTFOUND, and *fgc_flags
+ * is filled with some flags that may be useful for issuing an on-point error
+ * message (see FuncnameGetCandidates).
+ *
+ * On success, return values *funcid through *true_typeids receive info about
+ * the function.  If argdefaults isn't NULL, *argdefaults receives a list of
+ * any default argument expressions that need to be added to the given
+ * arguments.
  *
  * When processing a named- or mixed-notation call (ie, fargnames isn't NIL),
  * the returned true_typeids and argdefaults are ordered according to the
@@ -1400,6 +1498,7 @@ func_get_detail(List *funcname,
 				bool expand_variadic,
 				bool expand_defaults,
 				bool include_out_arguments,
+				int *fgc_flags, /* return value */
 				Oid *funcid,	/* return value */
 				Oid *rettype,	/* return value */
 				bool *retset,	/* return value */
@@ -1424,7 +1523,8 @@ func_get_detail(List *funcname,
 	/* Get list of possible candidates from namespace search */
 	raw_candidates = FuncnameGetCandidates(funcname, nargs, fargnames,
 										   expand_variadic, expand_defaults,
-										   include_out_arguments, false);
+										   include_out_arguments, false,
+										   fgc_flags);
 
 	/*
 	 * Quickly check if there is an exact match to the input datatypes (there
@@ -1594,7 +1694,10 @@ func_get_detail(List *funcname,
 		 */
 		if (fargnames != NIL && !expand_variadic && nargs > 0 &&
 			best_candidate->argnumbers[nargs - 1] != nargs - 1)
+		{
+			*fgc_flags |= FGC_VARIADIC_FAIL;
 			return FUNCDETAIL_NOTFOUND;
+		}
 
 		*funcid = best_candidate->oid;
 		*nvargs = best_candidate->nvargs;
@@ -2053,6 +2156,7 @@ LookupFuncNameInternal(ObjectType objtype, List *funcname,
 {
 	Oid			result = InvalidOid;
 	FuncCandidateList clist;
+	int			fgc_flags;
 
 	/* NULL argtypes allowed for nullary functions only */
 	Assert(argtypes != NULL || nargs == 0);
@@ -2062,7 +2166,8 @@ LookupFuncNameInternal(ObjectType objtype, List *funcname,
 
 	/* Get list of candidate objects */
 	clist = FuncnameGetCandidates(funcname, nargs, NIL, false, false,
-								  include_out_arguments, missing_ok);
+								  include_out_arguments, missing_ok,
+								  &fgc_flags);
 
 	/* Scan list for a match to the arg types (if specified) and the objtype */
 	for (; clist != NULL; clist = clist->next)
diff --git a/src/backend/utils/adt/regproc.c b/src/backend/utils/adt/regproc.c
index b8bbe95e82e..0c5dec025d7 100644
--- a/src/backend/utils/adt/regproc.c
+++ b/src/backend/utils/adt/regproc.c
@@ -71,6 +71,7 @@ regprocin(PG_FUNCTION_ARGS)
 	RegProcedure result;
 	List	   *names;
 	FuncCandidateList clist;
+	int			fgc_flags;
 
 	/* Handle "-" or numeric OID */
 	if (parseDashOrOid(pro_name_or_oid, &result, escontext))
@@ -93,7 +94,8 @@ regprocin(PG_FUNCTION_ARGS)
 	if (names == NIL)
 		PG_RETURN_NULL();
 
-	clist = FuncnameGetCandidates(names, -1, NIL, false, false, false, true);
+	clist = FuncnameGetCandidates(names, -1, NIL, false, false, false, true,
+								  &fgc_flags);
 
 	if (clist == NULL)
 		ereturn(escontext, (Datum) 0,
@@ -164,13 +166,15 @@ regprocout(PG_FUNCTION_ARGS)
 		{
 			char	   *nspname;
 			FuncCandidateList clist;
+			int			fgc_flags;
 
 			/*
 			 * Would this proc be found (uniquely!) by regprocin? If not,
 			 * qualify it.
 			 */
 			clist = FuncnameGetCandidates(list_make1(makeString(proname)),
-										  -1, NIL, false, false, false, false);
+										  -1, NIL, false, false, false, false,
+										  &fgc_flags);
 			if (clist != NULL && clist->next == NULL &&
 				clist->oid == proid)
 				nspname = NULL;
@@ -231,6 +235,7 @@ regprocedurein(PG_FUNCTION_ARGS)
 	int			nargs;
 	Oid			argtypes[FUNC_MAX_ARGS];
 	FuncCandidateList clist;
+	int			fgc_flags;
 
 	/* Handle "-" or numeric OID */
 	if (parseDashOrOid(pro_name_or_oid, &result, escontext))
@@ -251,8 +256,8 @@ regprocedurein(PG_FUNCTION_ARGS)
 							  escontext))
 		PG_RETURN_NULL();
 
-	clist = FuncnameGetCandidates(names, nargs, NIL, false, false,
-								  false, true);
+	clist = FuncnameGetCandidates(names, nargs, NIL, false, false, false, true,
+								  &fgc_flags);
 
 	for (; clist; clist = clist->next)
 	{
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 3d6e6bdbfd2..0408a95941d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -13265,6 +13265,7 @@ generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes,
 	bool		use_variadic;
 	char	   *nspname;
 	FuncDetailCode p_result;
+	int			fgc_flags;
 	Oid			p_funcid;
 	Oid			p_rettype;
 	bool		p_retset;
@@ -13323,6 +13324,7 @@ generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes,
 		p_result = func_get_detail(list_make1(makeString(proname)),
 								   NIL, argnames, nargs, argtypes,
 								   !use_variadic, true, false,
+								   &fgc_flags,
 								   &p_funcid, &p_rettype,
 								   &p_retset, &p_nvargs, &p_vatype,
 								   &p_true_typeids, NULL);
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 8c7ccc69a3c..a7bc24ead86 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -39,6 +39,23 @@ typedef struct _FuncCandidateList
 	Oid			args[FLEXIBLE_ARRAY_MEMBER];	/* arg types */
 }		   *FuncCandidateList;
 
+/*
+ * FuncnameGetCandidates also returns a bitmask containing these flags,
+ * which report on what it found or didn't find.  They can help callers
+ * produce better error reports after a function lookup failure.
+ */
+#define FGC_SCHEMA_GIVEN	0x0001	/* Func name includes a schema */
+#define FGC_SCHEMA_MATCH	0x0002	/* Found the explicitly-specified schema */
+#define FGC_NAME_MATCH		0x0004	/* Found a routine name match */
+#define FGC_ARGCOUNT_MATCH	0x0008	/* Found a func with right # of args */
+/* These bits relate only to calls using named or mixed arguments: */
+#define FGC_ARGNAMES_MATCH	0x0010	/* Found a func matching all argnames */
+#define FGC_ARGNAMES_PLACED	0x0020	/* Found argnames validly placed */
+#define FGC_ARGNAMES_ALL	0x0040	/* Found a func with no missing args */
+#define FGC_ARGNAMES_VALID	0x0080	/* Found a fully-valid use of argnames */
+/* These bits are actually filled by func_get_detail: */
+#define FGC_VARIADIC_FAIL	0x0100	/* Disallowed VARIADIC with named args */
+
 /*
  * Result of checkTempNamespaceStatus
  */
@@ -102,7 +119,8 @@ extern FuncCandidateList FuncnameGetCandidates(List *names,
 											   bool expand_variadic,
 											   bool expand_defaults,
 											   bool include_out_arguments,
-											   bool missing_ok);
+											   bool missing_ok,
+											   int *fgc_flags);
 extern bool FunctionIsVisible(Oid funcid);
 
 extern Oid	OpernameGetOprid(List *names, Oid oprleft, Oid oprright);
diff --git a/src/include/parser/parse_func.h b/src/include/parser/parse_func.h
index a6f24b83d84..218bb14c5d6 100644
--- a/src/include/parser/parse_func.h
+++ b/src/include/parser/parse_func.h
@@ -40,6 +40,7 @@ extern FuncDetailCode func_get_detail(List *funcname,
 									  int nargs, Oid *argtypes,
 									  bool expand_variadic, bool expand_defaults,
 									  bool include_out_arguments,
+									  int *fgc_flags,
 									  Oid *funcid, Oid *rettype,
 									  bool *retset, int *nvargs, Oid *vatype,
 									  Oid **true_typeids, List **argdefaults);
diff --git a/src/pl/plperl/expected/plperl_elog.out b/src/pl/plperl/expected/plperl_elog.out
index a6d35cb79c4..df5a3fa23aa 100644
--- a/src/pl/plperl/expected/plperl_elog.out
+++ b/src/pl/plperl/expected/plperl_elog.out
@@ -41,7 +41,7 @@ select uses_global();
 ERROR:  function uses_global() does not exist
 LINE 1: select uses_global();
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  There is no function of that name in the search_path.
 SET plperl.use_strict = false;
 create or replace function uses_global() returns text language plperl as $$
 
diff --git a/src/pl/plperl/expected/plperl_elog_1.out b/src/pl/plperl/expected/plperl_elog_1.out
index 85aa460ec4c..2592d987f40 100644
--- a/src/pl/plperl/expected/plperl_elog_1.out
+++ b/src/pl/plperl/expected/plperl_elog_1.out
@@ -41,7 +41,7 @@ select uses_global();
 ERROR:  function uses_global() does not exist
 LINE 1: select uses_global();
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  There is no function of that name in the search_path.
 SET plperl.use_strict = false;
 create or replace function uses_global() returns text language plperl as $$
 
diff --git a/src/pl/plpython/expected/plpython_error.out b/src/pl/plpython/expected/plpython_error.out
index fd9cd73be74..290cf8d25e6 100644
--- a/src/pl/plpython/expected/plpython_error.out
+++ b/src/pl/plpython/expected/plpython_error.out
@@ -63,7 +63,7 @@ SELECT exception_index_invalid_nested();
 ERROR:  spiexceptions.UndefinedFunction: function test5(unknown) does not exist
 LINE 1: SELECT test5('foo')
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  There is no function of that name in the search_path.
 QUERY:  SELECT test5('foo')
 CONTEXT:  Traceback (most recent call last):
   PL/Python function "exception_index_invalid_nested", line 1, in <module>
diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
index cf6ccec6b9d..3f82eee8e9c 100644
--- a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
+++ b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
@@ -27,7 +27,7 @@ B	4	ParseComplete
 B	4	BindComplete
 B	4	NoData
 B	15	CommandComplete	 "INSERT 0 1"
-B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "42883" M "function no_such_function(integer) does not exist" H "No function matches the given name and argument types. You might need to add explicit type casts." P "8" F "SSSS" L "SSSS" R "SSSS" \x00
+B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "42883" M "function no_such_function(integer) does not exist" D "There is no function of that name in the search_path." P "8" F "SSSS" L "SSSS" R "SSSS" \x00
 B	5	ReadyForQuery	 I
 B	4	ParseComplete
 B	4	BindComplete
diff --git a/src/test/modules/test_extensions/expected/test_extensions.out b/src/test/modules/test_extensions/expected/test_extensions.out
index 72bae1bf254..be8f51d6be2 100644
--- a/src/test/modules/test_extensions/expected/test_extensions.out
+++ b/src/test/modules/test_extensions/expected/test_extensions.out
@@ -333,7 +333,7 @@ SELECT ext_cor_func();
 ERROR:  function ext_cor_func() does not exist
 LINE 1: SELECT ext_cor_func();
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  There is no function of that name in the search_path.
 SELECT * FROM ext_cor_view;
           col           
 ------------------------
@@ -649,7 +649,6 @@ SELECT dep_req3b();  -- fails
 ERROR:  function public.dep_req2() does not exist
 LINE 1:  SELECT public.dep_req2() || ' req3b' 
                 ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
 QUERY:   SELECT public.dep_req2() || ' req3b' 
 CONTEXT:  SQL function "dep_req3b" statement 1
 DROP EXTENSION test_ext_req_schema3;
diff --git a/src/test/regress/expected/create_procedure.out b/src/test/regress/expected/create_procedure.out
index 45b402e25e7..cd8a77c1ade 100644
--- a/src/test/regress/expected/create_procedure.out
+++ b/src/test/regress/expected/create_procedure.out
@@ -2,7 +2,7 @@ CALL nonexistent();  -- error
 ERROR:  procedure nonexistent() does not exist
 LINE 1: CALL nonexistent();
              ^
-HINT:  No procedure matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  There is no procedure of that name in the search_path.
 CALL random();  -- error
 ERROR:  random() is not a procedure
 LINE 1: CALL random();
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index c3b2b9d8603..4f8e6892102 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -171,12 +171,12 @@ SELECT num_nonnulls();
 ERROR:  function num_nonnulls() does not exist
 LINE 1: SELECT num_nonnulls();
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  No function of that name has the right number of arguments.
 SELECT num_nulls();
 ERROR:  function num_nulls() does not exist
 LINE 1: SELECT num_nulls();
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  No function of that name has the right number of arguments.
 --
 -- canonicalize_path()
 --
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index d8ce39dba3c..7fd0481710c 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -3072,7 +3072,7 @@ select shadowtest(1);
 ERROR:  function shadowtest(integer) does not exist
 LINE 1: select shadowtest(1);
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  There is no function of that name in the search_path.
 reset plpgsql.extra_errors;
 reset plpgsql.extra_warnings;
 create or replace function shadowtest(f1 int)
diff --git a/src/test/regress/expected/polymorphism.out b/src/test/regress/expected/polymorphism.out
index 94eedfe375e..b6f5fb126fa 100644
--- a/src/test/regress/expected/polymorphism.out
+++ b/src/test/regress/expected/polymorphism.out
@@ -990,7 +990,7 @@ select myleast(); -- fail
 ERROR:  function myleast() does not exist
 LINE 1: select myleast();
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  No function of that name has the right number of arguments.
 -- test with variadic call parameter
 select myleast(variadic array[1,2,3,4,-1]);
  myleast 
@@ -1154,7 +1154,7 @@ select dfunc(10, 20, 30);  -- fail
 ERROR:  function dfunc(integer, integer, integer) does not exist
 LINE 1: select dfunc(10, 20, 30);
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  No function of that name has the right number of arguments.
 drop function dfunc();  -- fail
 ERROR:  function dfunc() does not exist
 drop function dfunc(int);  -- fail
@@ -1310,7 +1310,7 @@ select dfunc();  -- fail
 ERROR:  function dfunc() does not exist
 LINE 1: select dfunc();
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  No function of that name has the right number of arguments.
 select dfunc(10);
  dfunc 
 -------
@@ -1417,7 +1417,7 @@ select * from dfunc(0);  -- fail
 ERROR:  function dfunc(integer) does not exist
 LINE 1: select * from dfunc(0);
                       ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  No function of that name has the right number of arguments.
 select * from dfunc(1,2);
  a | b | c | d 
 ---+---+---+---
@@ -1448,18 +1448,64 @@ select * from dfunc(x := 10, b := 20, c := 30);  -- fail, unknown param
 ERROR:  function dfunc(x => integer, b => integer, c => integer) does not exist
 LINE 1: select * from dfunc(x := 10, b := 20, c := 30);
                       ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  No function of that name matches the given argument names.
 select * from dfunc(10, 10, a := 20);  -- fail, a overlaps positional parameter
 ERROR:  function dfunc(integer, integer, a => integer) does not exist
 LINE 1: select * from dfunc(10, 10, a := 20);
                       ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  Named arguments were incorrectly combined with positional arguments.
 select * from dfunc(1,c := 2,d := 3); -- fail, no value for b
 ERROR:  function dfunc(integer, c => integer, d => integer) does not exist
 LINE 1: select * from dfunc(1,c := 2,d := 3);
                       ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  Not all required arguments were supplied.
 drop function dfunc(int, int, int, int);
+create function xleast(x numeric, variadic arr numeric[])
+  returns numeric as $$
+  select least(x, min(arr[i])) from generate_subscripts(arr, 1) g(i);
+$$ language sql;
+select xleast(x => 1, variadic arr => array[2,3]);
+ xleast 
+--------
+      1
+(1 row)
+
+select xleast(1, variadic arr => array[2,3]);
+ xleast 
+--------
+      1
+(1 row)
+
+select xleast(foo => 1, variadic arr => array[2,3]);  -- wrong argument name
+ERROR:  function xleast(foo => integer, arr => integer[]) does not exist
+LINE 1: select xleast(foo => 1, variadic arr => array[2,3]);
+               ^
+DETAIL:  No function of that name matches the given argument names.
+select xleast(x => 1, variadic array[2,3]);  -- misuse of mixed notation
+ERROR:  positional argument cannot follow named argument
+LINE 1: select xleast(x => 1, variadic array[2,3]);
+                                       ^
+select xleast(1, variadic x => array[2,3]);  -- misuse of mixed notation
+ERROR:  function xleast(integer, x => integer[]) does not exist
+LINE 1: select xleast(1, variadic x => array[2,3]);
+               ^
+DETAIL:  Named arguments were incorrectly combined with positional arguments.
+select xleast(arr => array[1], variadic x => 3);  -- wrong arg is VARIADIC
+ERROR:  function xleast(arr => integer[], x => integer) does not exist
+LINE 1: select xleast(arr => array[1], variadic x => 3);
+               ^
+HINT:  The VARIADIC parameter must be placed last, even when using argument names.
+select xleast(arr => array[1], x => 3);  -- failed to use VARIADIC
+ERROR:  function xleast(arr => integer[], x => integer) does not exist
+LINE 1: select xleast(arr => array[1], x => 3);
+               ^
+HINT:  This call would be correct if the variadic array were labeled VARIADIC and placed last.
+select xleast(arr => 1, variadic x => array[2,3]);  -- mixed-up args
+ERROR:  function xleast(arr => integer, x => integer[]) does not exist
+LINE 1: select xleast(arr => 1, variadic x => array[2,3]);
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+drop function xleast(x numeric, variadic arr numeric[]);
 -- test with different parameter types
 create function dfunc(a varchar, b numeric, c date = current_date)
   returns table (a varchar, b numeric, c date) as $$
diff --git a/src/test/regress/expected/temp.out b/src/test/regress/expected/temp.out
index 370361543b3..c20e70d26a2 100644
--- a/src/test/regress/expected/temp.out
+++ b/src/test/regress/expected/temp.out
@@ -229,7 +229,7 @@ select nonempty('');
 ERROR:  function nonempty(unknown) does not exist
 LINE 1: select nonempty('');
                ^
-HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+DETAIL:  There is no function of that name in the search_path.
 select pg_temp.nonempty('');
 ERROR:  value for domain nonempty violates check constraint "nonempty_check"
 -- other syntax matches rules for tables
diff --git a/src/test/regress/sql/polymorphism.sql b/src/test/regress/sql/polymorphism.sql
index fa57db6559c..023d67751ea 100644
--- a/src/test/regress/sql/polymorphism.sql
+++ b/src/test/regress/sql/polymorphism.sql
@@ -873,6 +873,23 @@ select * from dfunc(1,c := 2,d := 3); -- fail, no value for b
 
 drop function dfunc(int, int, int, int);
 
+create function xleast(x numeric, variadic arr numeric[])
+  returns numeric as $$
+  select least(x, min(arr[i])) from generate_subscripts(arr, 1) g(i);
+$$ language sql;
+
+select xleast(x => 1, variadic arr => array[2,3]);
+select xleast(1, variadic arr => array[2,3]);
+
+select xleast(foo => 1, variadic arr => array[2,3]);  -- wrong argument name
+select xleast(x => 1, variadic array[2,3]);  -- misuse of mixed notation
+select xleast(1, variadic x => array[2,3]);  -- misuse of mixed notation
+select xleast(arr => array[1], variadic x => 3);  -- wrong arg is VARIADIC
+select xleast(arr => array[1], x => 3);  -- failed to use VARIADIC
+select xleast(arr => 1, variadic x => array[2,3]);  -- mixed-up args
+
+drop function xleast(x numeric, variadic arr numeric[]);
+
 -- test with different parameter types
 create function dfunc(a varchar, b numeric, c date = current_date)
   returns table (a varchar, b numeric, c date) as $$
-- 
2.43.7

