From 051bcced63f9a4780ad3c66c409d4cc7e0bc9d5c Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Wed, 3 Jun 2026 14:50:09 +0800
Subject: [PATCH v1] Make crosstabview honor boolean display settings

psql's \pset display_true and display_false settings were applied by
normal query output, but not by \crosstabview. As a result, boolean
values used as row labels, column labels, or data values in crosstab
output were always shown as "t" or "f".

Add a small helper to apply the query display substitutions for NULL and
boolean values, and use it from both printQuery() and crosstabview
printing. This keeps the existing query-output behavior while making
\crosstabview respect the same display settings.

Add a regression test covering boolean row keys, column keys, and data
values in \crosstabview.

Author: Chao Li <lic@highgo.com>
Reviewed-by:
Discussion: https://postgr.es/m/
---
 src/bin/psql/crosstabview.c                 | 36 ++++++++-------
 src/fe_utils/print.c                        | 49 +++++++++++++++------
 src/include/fe_utils/print.h                |  3 ++
 src/test/regress/expected/psql_crosstab.out | 15 +++++++
 src/test/regress/sql/psql_crosstab.sql      | 10 +++++
 5 files changed, 83 insertions(+), 30 deletions(-)

diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
index 111e8823bdb..d69fd370edb 100644
--- a/src/bin/psql/crosstabview.c
+++ b/src/bin/psql/crosstabview.c
@@ -292,6 +292,9 @@ printCrosstab(const PGresult *result,
 				rn;
 	char		col_align;
 	int		   *horiz_map;
+	Oid			col_ftype = PQftype(result, field_for_columns);
+	Oid			row_ftype = PQftype(result, field_for_rows);
+	Oid			data_ftype = PQftype(result, field_for_data);
 	bool		retval = false;
 
 	printTableInit(&cont, &popt.topt, popt.title, num_columns + 1, num_rows);
@@ -302,8 +305,7 @@ printCrosstab(const PGresult *result,
 	printTableAddHeader(&cont,
 						PQfname(result, field_for_rows),
 						false,
-						column_type_alignment(PQftype(result,
-													  field_for_rows)));
+						column_type_alignment(row_ftype));
 
 	/*
 	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse
@@ -317,15 +319,14 @@ printCrosstab(const PGresult *result,
 	/*
 	 * The display alignment depends on its PQftype().
 	 */
-	col_align = column_type_alignment(PQftype(result, field_for_data));
+	col_align = column_type_alignment(data_ftype);
 
 	for (i = 0; i < num_columns; i++)
 	{
 		char	   *colname;
 
-		colname = piv_columns[horiz_map[i]].name ?
-			piv_columns[horiz_map[i]].name :
-			(popt.nullPrint ? popt.nullPrint : "");
+		colname = printQueryOptDisplayValue(piv_columns[horiz_map[i]].name,
+											col_ftype, &popt, "");
 
 		printTableAddHeader(&cont, colname, false, col_align);
 	}
@@ -335,10 +336,11 @@ printCrosstab(const PGresult *result,
 	for (i = 0; i < num_rows; i++)
 	{
 		int			k = piv_rows[i].rank;
+		int			idx = k * (num_columns + 1);
 
-		cont.cells[k * (num_columns + 1)] = piv_rows[i].name ?
-			piv_rows[i].name :
-			(popt.nullPrint ? popt.nullPrint : "");
+		cont.cells[idx] =
+			printQueryOptDisplayValue(piv_rows[i].name, row_ftype, &popt,
+									  "");
 	}
 	cont.cellsadded = num_rows * (num_columns + 1);
 
@@ -394,16 +396,18 @@ printCrosstab(const PGresult *result,
 			if (cont.cells[idx] != NULL)
 			{
 				pg_log_error("\\crosstabview: query result contains multiple data values for row \"%s\", column \"%s\"",
-							 rp->name ? rp->name :
-							 (popt.nullPrint ? popt.nullPrint : "(null)"),
-							 cp->name ? cp->name :
-							 (popt.nullPrint ? popt.nullPrint : "(null)"));
+							 printQueryOptDisplayValue(rp->name, row_ftype,
+													   &popt, "(null)"),
+							 printQueryOptDisplayValue(cp->name, col_ftype,
+													   &popt, "(null)"));
 				goto error;
 			}
 
-			cont.cells[idx] = !PQgetisnull(result, rn, field_for_data) ?
-				PQgetvalue(result, rn, field_for_data) :
-				(popt.nullPrint ? popt.nullPrint : "");
+			cont.cells[idx] =
+				printQueryOptDisplayValue(!PQgetisnull(result, rn, field_for_data) ?
+										  PQgetvalue(result, rn, field_for_data) :
+										  NULL,
+										  data_ftype, &popt, "");
 		}
 	}
 
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index f2dd52003c1..4c6f161aa4b 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3729,6 +3729,31 @@ printTable(const printTableContent *cont,
 		print_aligned_text(cont, flog, false);
 }
 
+/*
+ * Return the display representation of a query value, following pset
+ * substitutions.  The returned pointer should not be freed.
+ *
+ * value: value to display, or NULL for a null value
+ * ftype: field type of value
+ * opt: formatting options
+ * default_null: default display for a null value if opt->nullPrint isn't set
+ */
+char *
+printQueryOptDisplayValue(char *value, Oid ftype, const printQueryOpt *opt,
+						  const char *default_null)
+{
+	if (value == NULL)
+		return opt->nullPrint ? opt->nullPrint :
+			unconstify(char *, default_null);
+
+	if (ftype == BOOLOID)
+		return value[0] == 't' ?
+			(opt->truePrint ? opt->truePrint : "t") :
+			(opt->falsePrint ? opt->falsePrint : "f");
+
+	return value;
+}
+
 /*
  * Use this to print query results
  *
@@ -3769,25 +3794,21 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	{
 		for (c = 0; c < cont.ncolumns; c++)
 		{
-			char	   *cell;
+			char	   *cell = PQgetvalue(result, r, c);
+			bool		isnull = PQgetisnull(result, r, c);
 			bool		mustfree = false;
 			bool		translate;
+			Oid			ftype = PQftype(result, c);
 
-			if (PQgetisnull(result, r, c))
-				cell = opt->nullPrint ? opt->nullPrint : "";
-			else if (PQftype(result, c) == BOOLOID)
-				cell = (PQgetvalue(result, r, c)[0] == 't' ?
-						(opt->truePrint ? opt->truePrint : "t") :
-						(opt->falsePrint ? opt->falsePrint : "f"));
-			else
+			if (!isnull && ftype != BOOLOID && cont.aligns[c] == 'r' &&
+				opt->topt.numericLocale)
 			{
-				cell = PQgetvalue(result, r, c);
-				if (cont.aligns[c] == 'r' && opt->topt.numericLocale)
-				{
-					cell = format_numeric_locale(cell);
-					mustfree = true;
-				}
+				cell = format_numeric_locale(cell);
+				mustfree = true;
 			}
+			else
+				cell = printQueryOptDisplayValue(isnull ? NULL : cell,
+												 ftype, opt, "");
 
 			translate = (opt->translate_columns && opt->translate_columns[c]);
 			printTableAddCell(&cont, cell, translate, mustfree);
diff --git a/src/include/fe_utils/print.h b/src/include/fe_utils/print.h
index 94f6a593619..071c3e284a5 100644
--- a/src/include/fe_utils/print.h
+++ b/src/include/fe_utils/print.h
@@ -226,6 +226,9 @@ extern void printTableSetFooter(printTableContent *const content,
 extern void printTableCleanup(printTableContent *const content);
 extern void printTable(const printTableContent *cont,
 					   FILE *fout, bool is_pager, FILE *flog);
+extern char *printQueryOptDisplayValue(char *value, Oid ftype,
+									   const printQueryOpt *opt,
+									   const char *default_null);
 extern void printQuery(const PGresult *result, const printQueryOpt *opt,
 					   FILE *fout, bool is_pager, FILE *flog);
 
diff --git a/src/test/regress/expected/psql_crosstab.out b/src/test/regress/expected/psql_crosstab.out
index e09e3310165..4660bfbf7f5 100644
--- a/src/test/regress/expected/psql_crosstab.out
+++ b/src/test/regress/expected/psql_crosstab.out
@@ -137,6 +137,21 @@ GROUP BY v, h ORDER BY h,v
 (3 rows)
 
 \pset null ''
+-- boolean display
+\pset display_true 'true'
+\pset display_false 'false'
+SELECT false as row_key, true as col_key, true as val
+UNION ALL
+SELECT true, false, false
+ \crosstabview row_key col_key val
+ row_key | true | false 
+---------+------+-------
+ false   | true | 
+ true    |      | false
+(2 rows)
+
+\pset display_true 't'
+\pset display_false 'f'
 -- refer to columns by position
 SELECT v,h,string_agg(i::text, E'\n'), string_agg(c, E'\n')
 FROM ctv_data GROUP BY v, h ORDER BY h,v
diff --git a/src/test/regress/sql/psql_crosstab.sql b/src/test/regress/sql/psql_crosstab.sql
index 5a4511389de..00b395d7168 100644
--- a/src/test/regress/sql/psql_crosstab.sql
+++ b/src/test/regress/sql/psql_crosstab.sql
@@ -69,6 +69,16 @@ GROUP BY v, h ORDER BY h,v
  \crosstabview v h i
 \pset null ''
 
+-- boolean display
+\pset display_true 'true'
+\pset display_false 'false'
+SELECT false as row_key, true as col_key, true as val
+UNION ALL
+SELECT true, false, false
+ \crosstabview row_key col_key val
+\pset display_true 't'
+\pset display_false 'f'
+
 -- refer to columns by position
 SELECT v,h,string_agg(i::text, E'\n'), string_agg(c, E'\n')
 FROM ctv_data GROUP BY v, h ORDER BY h,v
-- 
2.50.1 (Apple Git-155)

