]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Make crosstabview honor boolean/null display settings
authorÁlvaro Herrera <alvherre@kurilemu.de>
Fri, 26 Jun 2026 18:03:42 +0000 (20:03 +0200)
committerÁlvaro Herrera <alvherre@kurilemu.de>
Fri, 26 Jun 2026 18:03:42 +0000 (20:03 +0200)
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 <lic@highgo.com>
Reported-by: Chao Li <lic@highgo.com>
Reviewed-by: David G. Johnston <david.g.johnston@gmail.com>
Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de>
Backpatch: none needed
Discussion: https://postgr.es/m/B5E6F0A5-4B48-46D0-B5EB-CF8F8CC7D07D@gmail.com

src/bin/psql/crosstabview.c
src/test/regress/expected/psql_crosstab.out
src/test/regress/sql/psql_crosstab.sql

index 111e8823bdbe7917a7eef05fe0002f33c4a4c88f..b59437e41ebfd43fb7400c62cdf8904382f7c825 100644 (file)
@@ -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
index e09e3310165853e1b3e49191e5828ec3da22daad..e32d79d38b0f2df2e38576c77367cb60c829282e 100644 (file)
@@ -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')
index 5a4511389de69a5a4738c4999db2930722b10765..82a73981d5d79d758430b0bc9da7a79d4d4d8d9c 100644 (file)
@@ -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