From 86ea629bc4ede9c51f9c000caddf1969b593b569 Mon Sep 17 00:00:00 2001
From: Abhishek Chanda <abhishek.becs@gmail.com>
Date: Sat, 12 Apr 2025 22:30:17 -0500
Subject: [PATCH v3] Print empty table when a given object is not found in a
 slash command

Currently, in a few cases we return an error of the form
Did not find any XXXX named YYYY while in some cases we
print a table with no rows. This patch changes a few of
the former to the later so that we have an uniform
interface.
---
 src/bin/psql/describe.c     | 377 ++++++++++++++++++++++--------------
 src/bin/psql/t/001_basic.pl |  26 +++
 2 files changed, 258 insertions(+), 145 deletions(-)

diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 36f24502842..de1bb31b231 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -136,6 +136,12 @@ describeAggregates(const char *pattern, bool verbose, bool showSystem)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -210,6 +216,12 @@ describeAccessMethods(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -272,6 +284,12 @@ describeTablespaces(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -621,6 +639,12 @@ describeFunctions(const char *functypes, const char *func_pattern,
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (func_pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 
@@ -727,6 +751,12 @@ describeTypes(const char *pattern, bool verbose, bool showSystem)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -928,6 +958,12 @@ describeOperators(const char *oper_pattern,
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (oper_pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 
@@ -1037,6 +1073,12 @@ listAllDbs(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -1270,6 +1312,13 @@ listDefaultACLs(const char *pattern)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		termPQExpBuffer(&buf);
+		PQclear(res);
+		return false;
+	}
+
 	termPQExpBuffer(&buf);
 	PQclear(res);
 	return true;
@@ -1522,16 +1571,14 @@ describeTableDetails(const char *pattern, bool verbose, bool showSystem)
 
 	if (PQntuples(res) == 0)
 	{
-		if (!pset.quiet)
-		{
-			if (pattern)
-				pg_log_error("Did not find any relation named \"%s\".",
-							 pattern);
-			else
-				pg_log_error("Did not find any relations.");
-		}
 		PQclear(res);
-		return false;
+		/*
+		 * Print an empty table and return failure when no objects match
+		 */
+		if (pattern)
+			return listTables("tvmsE", pattern, verbose, showSystem);
+		else
+			return true;
 	}
 
 	for (i = 0; i < PQntuples(res); i++)
@@ -1720,14 +1767,6 @@ describeOneTableDetails(const char *schemaname,
 	if (!res)
 		goto error_return;
 
-	/* Did we get anything? */
-	if (PQntuples(res) == 0)
-	{
-		if (!pset.quiet)
-			pg_log_error("Did not find any relation with OID %s.", oid);
-		goto error_return;
-	}
-
 	tableinfo.checks = atoi(PQgetvalue(res, 0, 0));
 	tableinfo.relkind = *(PQgetvalue(res, 0, 1));
 	tableinfo.hasindex = strcmp(PQgetvalue(res, 0, 2), "t") == 0;
@@ -3797,6 +3836,7 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
 		return false;
 
 	nrows = PQntuples(res);
+
 	attr = pg_malloc0((nrows + 1) * sizeof(*attr));
 
 	printTableInit(&cont, &myopt, _("List of roles"), ncols, nrows);
@@ -3873,6 +3913,12 @@ describeRoles(const char *pattern, bool verbose, bool showSystem)
 		free(attr[i]);
 	free(attr);
 
+	if (pattern && nrows == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -3921,29 +3967,15 @@ listDbRoleSettings(const char *pattern, const char *pattern2)
 	if (!res)
 		return false;
 
-	/*
-	 * Most functions in this file are content to print an empty table when
-	 * there are no matching objects.  We intentionally deviate from that
-	 * here, but only in !quiet mode, because of the possibility that the user
-	 * is confused about what the two pattern arguments mean.
-	 */
-	if (PQntuples(res) == 0 && !pset.quiet)
-	{
-		if (pattern && pattern2)
-			pg_log_error("Did not find any settings for role \"%s\" and database \"%s\".",
-						 pattern, pattern2);
-		else if (pattern)
-			pg_log_error("Did not find any settings for role \"%s\".",
-						 pattern);
-		else
-			pg_log_error("Did not find any settings.");
-	}
-	else
-	{
-		myopt.title = _("List of settings");
-		myopt.translate_header = true;
+	myopt.title = _("List of settings");
+	myopt.translate_header = true;
 
-		printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	if ((pattern || pattern2) && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
 	}
 
 	PQclear(res);
@@ -4018,6 +4050,12 @@ describeRoleGrants(const char *pattern, bool showSystem)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -4201,76 +4239,25 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 	if (!res)
 		return false;
 
-	/*
-	 * Most functions in this file are content to print an empty table when
-	 * there are no matching objects.  We intentionally deviate from that
-	 * here, but only in !quiet mode, for historical reasons.
-	 */
-	if (PQntuples(res) == 0 && !pset.quiet)
-	{
-		if (pattern)
-		{
-			if (ntypes != 1)
-				pg_log_error("Did not find any relations named \"%s\".",
-							 pattern);
-			else if (showTables)
-				pg_log_error("Did not find any tables named \"%s\".",
-							 pattern);
-			else if (showIndexes)
-				pg_log_error("Did not find any indexes named \"%s\".",
-							 pattern);
-			else if (showViews)
-				pg_log_error("Did not find any views named \"%s\".",
-							 pattern);
-			else if (showMatViews)
-				pg_log_error("Did not find any materialized views named \"%s\".",
-							 pattern);
-			else if (showSeq)
-				pg_log_error("Did not find any sequences named \"%s\".",
-							 pattern);
-			else if (showForeign)
-				pg_log_error("Did not find any foreign tables named \"%s\".",
-							 pattern);
-			else				/* should not get here */
-				pg_log_error_internal("Did not find any ??? named \"%s\".",
-									  pattern);
-		}
-		else
-		{
-			if (ntypes != 1)
-				pg_log_error("Did not find any relations.");
-			else if (showTables)
-				pg_log_error("Did not find any tables.");
-			else if (showIndexes)
-				pg_log_error("Did not find any indexes.");
-			else if (showViews)
-				pg_log_error("Did not find any views.");
-			else if (showMatViews)
-				pg_log_error("Did not find any materialized views.");
-			else if (showSeq)
-				pg_log_error("Did not find any sequences.");
-			else if (showForeign)
-				pg_log_error("Did not find any foreign tables.");
-			else				/* should not get here */
-				pg_log_error_internal("Did not find any ??? relations.");
-		}
-	}
-	else
-	{
-		myopt.title =
-			(ntypes != 1) ? _("List of relations") :
-			(showTables) ? _("List of tables") :
-			(showIndexes) ? _("List of indexes") :
-			(showViews) ? _("List of views") :
-			(showMatViews) ? _("List of materialized views") :
-			(showSeq) ? _("List of sequences") :
-			(showForeign) ? _("List of foreign tables") :
-			"List of ???";		/* should not get here */
-		myopt.translate_header = true;
-		myopt.translate_columns = translate_columns;
-		myopt.n_translate_columns = lengthof(translate_columns);
+	myopt.title =
+		(ntypes != 1) ? _("List of relations") :
+		(showTables) ? _("List of tables") :
+		(showIndexes) ? _("List of indexes") :
+		(showViews) ? _("List of views") :
+		(showMatViews) ? _("List of materialized views") :
+		(showSeq) ? _("List of sequences") :
+		(showForeign) ? _("List of foreign tables") :
+		"List of ???";			/* should not get here */
+	myopt.translate_header = true;
+	myopt.translate_columns = translate_columns;
+	myopt.n_translate_columns = lengthof(translate_columns);
 
-		printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
 	}
 
 	PQclear(res);
@@ -4568,6 +4555,12 @@ listLanguages(const char *pattern, bool verbose, bool showSystem)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -4652,6 +4645,12 @@ listDomains(const char *pattern, bool verbose, bool showSystem)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -4732,6 +4731,12 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -4800,6 +4805,12 @@ describeConfigurationParameters(const char *pattern, bool verbose,
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -4880,6 +4891,12 @@ listEventTriggers(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -4976,6 +4993,12 @@ listExtendedStats(const char *pattern)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -5223,6 +5246,12 @@ listCollations(const char *pattern, bool verbose, bool showSystem)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -5327,6 +5356,13 @@ listSchemas(const char *pattern, bool verbose, bool showSystem)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		termPQExpBuffer(&buf);
+		PQclear(res);
+		return false;
+	}
+
 	termPQExpBuffer(&buf);
 	PQclear(res);
 
@@ -5398,6 +5434,12 @@ listTSParsers(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -5440,16 +5482,11 @@ listTSParsersVerbose(const char *pattern)
 
 	if (PQntuples(res) == 0)
 	{
-		if (!pset.quiet)
-		{
-			if (pattern)
-				pg_log_error("Did not find any text search parser named \"%s\".",
-							 pattern);
-			else
-				pg_log_error("Did not find any text search parsers.");
-		}
 		PQclear(res);
-		return false;
+		if (pattern)
+			return listTSParsers(pattern, false);
+		else
+			return true;
 	}
 
 	for (i = 0; i < PQntuples(res); i++)
@@ -5775,6 +5812,12 @@ listTSConfigs(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -5818,16 +5861,11 @@ listTSConfigsVerbose(const char *pattern)
 
 	if (PQntuples(res) == 0)
 	{
-		if (!pset.quiet)
-		{
-			if (pattern)
-				pg_log_error("Did not find any text search configuration named \"%s\".",
-							 pattern);
-			else
-				pg_log_error("Did not find any text search configurations.");
-		}
 		PQclear(res);
-		return false;
+		if (pattern)
+			return listTSConfigs(pattern, false);
+		else
+			return true;
 	}
 
 	for (i = 0; i < PQntuples(res); i++)
@@ -5996,6 +6034,12 @@ listForeignDataWrappers(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -6072,6 +6116,12 @@ listForeignServers(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -6127,6 +6177,12 @@ listUserMappings(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -6199,6 +6255,12 @@ listForeignTables(const char *pattern, bool verbose)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -6253,6 +6315,12 @@ listExtensions(const char *pattern)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 }
@@ -6293,16 +6361,11 @@ listExtensionContents(const char *pattern)
 
 	if (PQntuples(res) == 0)
 	{
-		if (!pset.quiet)
-		{
-			if (pattern)
-				pg_log_error("Did not find any extension named \"%s\".",
-							 pattern);
-			else
-				pg_log_error("Did not find any extensions.");
-		}
 		PQclear(res);
-		return false;
+		if (pattern)
+			return listExtensions(pattern);
+		else
+			return true;
 	}
 
 	for (i = 0; i < PQntuples(res); i++)
@@ -6510,6 +6573,12 @@ listPublications(const char *pattern)
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if (pattern && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 
 	return true;
@@ -6660,18 +6729,12 @@ describePublications(const char *pattern)
 
 	if (PQntuples(res) == 0)
 	{
-		if (!pset.quiet)
-		{
-			if (pattern)
-				pg_log_error("Did not find any publication named \"%s\".",
-							 pattern);
-			else
-				pg_log_error("Did not find any publications.");
-		}
-
 		termPQExpBuffer(&buf);
 		PQclear(res);
-		return false;
+		if (pattern)
+			return listPublications(pattern);
+		else
+			return true;
 	}
 
 	for (i = 0; i < PQntuples(res); i++)
@@ -7051,6 +7114,12 @@ listOperatorClasses(const char *access_method_pattern,
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if ((access_method_pattern || type_pattern) && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 
@@ -7139,6 +7208,12 @@ listOperatorFamilies(const char *access_method_pattern,
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if ((access_method_pattern || type_pattern) && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 
@@ -7246,6 +7321,12 @@ listOpFamilyOperators(const char *access_method_pattern,
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if ((access_method_pattern || family_pattern) && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 
@@ -7338,6 +7419,12 @@ listOpFamilyFunctions(const char *access_method_pattern,
 
 	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
 
+	if ((access_method_pattern || family_pattern) && PQntuples(res) == 0)
+	{
+		PQclear(res);
+		return false;
+	}
+
 	PQclear(res);
 	return true;
 
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index cf07a9dbd5e..011a6508df0 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -537,4 +537,30 @@ psql_fails_like(
 	qr/backslash commands are restricted; only \\unrestrict is allowed/,
 	'meta-command in restrict mode fails');
 
+# Test that \d commands return exit code 1 when pattern matches nothing
+# and don't print "Did not find" error messages
+{
+	my ($ret, $stdout, $stderr);
+
+	($ret, $stdout, $stderr) = $node->psql('postgres', '\\d nonexistent_table');
+	isnt($ret, 0, '\\d with non-existent pattern: exit code not 0');
+	unlike($stderr, qr/Did not find/, '\\d with non-existent pattern: no error message');
+
+	($ret, $stdout, $stderr) = $node->psql('postgres', '\\dt nonexistent_table');
+	isnt($ret, 0, '\\dt with non-existent pattern: exit code not 0');
+	unlike($stderr, qr/Did not find/, '\\dt with non-existent pattern: no error message');
+
+	($ret, $stdout, $stderr) = $node->psql('postgres', '\\df nonexistent_func');
+	isnt($ret, 0, '\\df with non-existent pattern: exit code not 0');
+	unlike($stderr, qr/Did not find/, '\\df with non-existent pattern: no error message');
+
+	($ret, $stdout, $stderr) = $node->psql('postgres', '\\dx+ nonexistent_ext');
+	isnt($ret, 0, '\\dx+ with non-existent pattern: exit code not 0');
+	unlike($stderr, qr/Did not find/, '\\dx+ with non-existent pattern: no error message');
+
+	# Test that \d commands without patterns succeed even with no results
+	($ret, $stdout, $stderr) = $node->psql('postgres', '\\d');
+	is($ret, 0, '\\d without pattern: exit code 0');
+}
+
 done_testing();
-- 
2.50.1

