From: Álvaro Herrera Date: Fri, 26 Jun 2026 18:03:42 +0000 (+0200) Subject: Make crosstabview honor boolean/null display settings X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4df5fe3833a87f6629eb888ca5a385bcb0b179d9;p=thirdparty%2Fpostgresql.git Make crosstabview honor boolean/null display settings psql's \pset display_true/false settings, added by commit 645cb44c5490, affect normal query output, but not \crosstabview. As a result, boolean values used anywhere in crosstab output were always shown as "t" or "f", which is inconsistent. Change \crosstabview so that the configured values are displayed instead. While at it, make \crosstabview print the \pset null string, if any, in cells for which the query produces a NULL value. Cells for which the query produces no value continue to have the empty string. This is an oversight in the aboriginal \crosstabview commit, c09b18f21c52. Add a regression test covering all of this. Author: Chao Li Reported-by: Chao Li Reviewed-by: David G. Johnston Reviewed-by: Álvaro Herrera Backpatch: none needed Discussion: https://postgr.es/m/B5E6F0A5-4B48-46D0-B5EB-CF8F8CC7D07D@gmail.com --- diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c index 111e8823bdb..b59437e41eb 100644 --- a/src/bin/psql/crosstabview.c +++ b/src/bin/psql/crosstabview.c @@ -7,6 +7,7 @@ */ #include "postgres_fe.h" +#include "catalog/pg_type_d.h" #include "common.h" #include "common/int.h" #include "common/logging.h" @@ -82,6 +83,8 @@ static bool printCrosstab(const PGresult *result, int num_columns, pivot_field *piv_columns, int field_for_columns, int num_rows, pivot_field *piv_rows, int field_for_rows, int field_for_data); +static char *displayValue(char *value, Oid ftype, char *default_null); + static void avlInit(avl_tree *tree); static void avlMergeValue(avl_tree *tree, char *name, char *sort_value); static int avlCollectFields(avl_tree *tree, avl_node *node, @@ -292,6 +295,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 +308,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 +322,13 @@ 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 = displayValue(piv_columns[horiz_map[i]].name, col_ftype, ""); printTableAddHeader(&cont, colname, false, col_align); } @@ -335,10 +338,9 @@ 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] = displayValue(piv_rows[i].name, row_ftype, ""); } cont.cellsadded = num_rows * (num_columns + 1); @@ -384,6 +386,7 @@ printCrosstab(const PGresult *result, if (col_number >= 0 && row_number >= 0) { int idx; + char *value; /* index into the cont.cells array */ idx = 1 + col_number + row_number * (num_columns + 1); @@ -394,16 +397,16 @@ 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)")); + displayValue(rp->name, row_ftype, "(null)"), + displayValue(cp->name, col_ftype, "(null)")); goto error; } - cont.cells[idx] = !PQgetisnull(result, rn, field_for_data) ? - PQgetvalue(result, rn, field_for_data) : - (popt.nullPrint ? popt.nullPrint : ""); + if (PQgetisnull(result, rn, field_for_data)) + value = NULL; + else + value = PQgetvalue(result, rn, field_for_data); + cont.cells[idx] = displayValue(value, data_ftype, ""); } } @@ -426,6 +429,30 @@ error: return retval; } +/* + * Return the display representation of one cell value in \crosstabview, + * following pset substitutions. + * + * The returned pointer is not to be freed. + */ +static char * +displayValue(char *value, Oid ftype, char *default_null) +{ + printQueryOpt popt = pset.popt; + + if (value == NULL) + value = popt.nullPrint ? popt.nullPrint : default_null; + else if (ftype == BOOLOID) + { + if (value[0] == 't' && popt.truePrint) + value = popt.truePrint; + else if (value[0] == 'f' && popt.falsePrint) + value = popt.falsePrint; + } + + return value; +} + /* * The avl* functions below provide a minimalistic implementation of AVL binary * trees, to efficiently collect the distinct values that will form the horizontal diff --git a/src/test/regress/expected/psql_crosstab.out b/src/test/regress/expected/psql_crosstab.out index e09e3310165..e32d79d38b0 100644 --- a/src/test/regress/expected/psql_crosstab.out +++ b/src/test/regress/expected/psql_crosstab.out @@ -136,6 +136,35 @@ GROUP BY v, h ORDER BY h,v | | | | -3 | (3 rows) +\pset null '' +-- boolean display +\pset display_true 'Aye' +\pset display_false 'Nay' +\pset null 'Wut' +with rows (display_bools, col_key, val) as (values + (false, false, false), + (true, true, true), + (null, null, null), + (null, true, false), + (null, false, true), + (true, null, false), + (false, null, true) +) select * from rows + \crosstabview display_bools col_key val + display_bools | Nay | Aye | Wut +---------------+-----+-----+----- + Nay | Nay | | Aye + Aye | | Aye | Nay + Wut | Aye | Nay | Wut +(3 rows) + +with rows (tst, col, val) as (values + (true, null, 42), (true, null, 142857) +) select * from rows + \crosstabview tst col +\crosstabview: query result contains multiple data values for row "Aye", column "Wut" +\pset display_true 't' +\pset display_false 'f' \pset null '' -- refer to columns by position SELECT v,h,string_agg(i::text, E'\n'), string_agg(c, E'\n') diff --git a/src/test/regress/sql/psql_crosstab.sql b/src/test/regress/sql/psql_crosstab.sql index 5a4511389de..82a73981d5d 100644 --- a/src/test/regress/sql/psql_crosstab.sql +++ b/src/test/regress/sql/psql_crosstab.sql @@ -69,6 +69,30 @@ GROUP BY v, h ORDER BY h,v \crosstabview v h i \pset null '' +-- boolean display +\pset display_true 'Aye' +\pset display_false 'Nay' +\pset null 'Wut' +with rows (display_bools, col_key, val) as (values + (false, false, false), + (true, true, true), + (null, null, null), + (null, true, false), + (null, false, true), + (true, null, false), + (false, null, true) +) select * from rows + \crosstabview display_bools col_key val + +with rows (tst, col, val) as (values + (true, null, 42), (true, null, 142857) +) select * from rows + \crosstabview tst col + +\pset display_true 't' +\pset display_false 'f' +\pset null '' + -- 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