[patch] Proposal for \rotate in psql

Started by Daniel Veriteover 10 years ago134 messages
#1Daniel Verite
daniel@manitou-mail.org
1 attachment(s)

Hi,

This is a reboot of my previous proposal for pivoting results in psql,
with a new patch that generalizes the idea further through a command
now named \rotate, and some examples.

So the concept is: having an existing query in the query buffer,
the user can specify two column numbers C1 and C2 (by default the 1st
and 2nd) as an argument to a \rotate command.

The query results are then displayed in a 2D grid such that each tuple
(vx, vy, va, vb,...) is shown as |va vb...| in a cell at coordinates (vx,vy).
The values vx,xy come from columns C1,C2 respectively and are
represented in the output as an horizontal and a vertical header.

A cell may hold several columns from several rows, growing horizontally and
vertically (\n inside the cell) if necessary to show all results.

The examples below should be read with a monospaced font as in psql,
otherwise they will look pretty bad.

1. Example with only 2 columns, querying login/group membership from the
catalog.
Query:

SELECT r.rolname as username,r1.rolname as groupname
FROM pg_catalog.pg_roles r LEFT JOIN pg_catalog.pg_auth_members m
ON (m.member = r.oid)
LEFT JOIN pg_roles r1 ON (m.roleid=r1.oid)
WHERE r.rolcanlogin
ORDER BY 1

Sample results:
username | groupname
------------+-----------
daniel | mailusers
drupal |
dv | admin
dv | common
extc | readonly
extu |
foobar |
joel |
mailreader | readonly
manitou | mailusers
manitou | admin
postgres |
u1 | common
u2 | mailusers
zaz | mailusers

Applying \rotate gives:
Rotated query results
username | admin | common | mailusers | readonly
------------+-------+--------+-----------+----------
daniel | | | X |
drupal | | | |
dv | X | X | |
extc | | | | X
extu | | | |
foobar | | | |
joel | | | |
mailreader | | | | X
manitou | X | | X |
postgres | | | |
u1 | | X | |
u2 | | | X |
zaz | | | X |

The 'X' inside cells is automatically added as there are only
2 columns. If there was a 3rd column, the content of that column would
be displayed instead (as in the next example).

What's good in that \rotate display compared to the classic output is that
it's more apparent, visually speaking, that such user belongs or not to such
group or another.

2. Example with a unicode checkmark added as 3rd column, and
unicode linestyle and borders (to be seen with a mono-spaced font):

SELECT r.rolname as username,r1.rolname as groupname, chr(10003)
FROM pg_catalog.pg_roles r LEFT JOIN pg_catalog.pg_auth_members m
ON (m.member = r.oid)
LEFT JOIN pg_roles r1 ON (m.roleid=r1.oid)
WHERE r.rolcanlogin
ORDER BY 1

Rotated query results
┌────────────┬───────┬───�
�────┬───────────┬────────�
��─┐
│ username │ admin │ common │ mailusers │ readonly │
├────────────┼───────┼───�
�────┼───────────┼────────�
��─┤
│ daniel │ │ │ ✓ │ │
│ drupal │ │ │ │ │
│ dv │ ✓ │ ✓ │ │ │
│ extc │ │ │ │ ✓ │
│ extu │ │ │ │ │
│ foobar │ │ │ │ │
│ joel │ │ │ │ │
│ mailreader │ │ │ │ ✓ │
│ manitou │ ✓ │ │ ✓ │ │
│ postgres │ │ │ │ │
│ u1 │ │ ✓ │ │ │
│ u2 │ │ │ ✓ │ │
│ zaz │ │ │ ✓ │ │
└────────────┴───────┴───�
�────┴───────────┴────────�
��─┘

What I like in that representation is that it looks good enough
to be pasted directly into a document in a word processor.

3. It can be rotated easily in the other direction, with:
\rotate 2 1

(Cut horizontally to fit in a mail, the actual output is 116 chars wide).

Rotated query results
┌───────────┬────────┬───�
�────┬────┬──────┬──────┬─�
��──────┬──────┬────
│ username │ daniel │ drupal │ dv │ extc │ extu │ foobar │
joel │ mai...
├───────────┼────────┼───�
�────┼────┼──────┼──────┼─�
��──────┼──────┼────
│ mailusers │ ✓ │ │ │ │ │ │

│ admin │ │ │ ✓ │ │ │ │

│ common │ │ │ ✓ │ │ │ │

│ readonly │ │ │ │ ✓ │ │ │
│ ✓
└───────────┴────────┴───�
�────┴────┴──────┴──────┴─�
��──────┴──────┴────

4. Example with 3 columns and a count as the value to visualize along
two axis: date and category.
I'm using the number of mails posted per month in a few PG mailing lists,
broken down by list (which are tags in my schema).

Query:
SELECT date_trunc('month', msg_date)::date as month,
t.name,
count(*) as cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name
ORDER BY 1,2;

Results:
month | name | cnt
------------+-------------+------
2014-05-01 | announce | 19
2014-05-01 | general | 550
2014-05-01 | hackers | 1914
2014-05-01 | interfaces | 4
2014-05-01 | performance | 122
2014-06-01 | announce | 10
2014-06-01 | general | 499
2014-06-01 | hackers | 2008
2014-06-01 | interfaces | 10
2014-06-01 | performance | 137
2014-07-01 | announce | 12
2014-07-01 | general | 703
2014-07-01 | hackers | 1504
2014-07-01 | interfaces | 6
2014-07-01 | performance | 142
2014-08-01 | announce | 9
2014-08-01 | general | 616
2014-08-01 | hackers | 1864
2014-08-01 | interfaces | 11
2014-08-01 | performance | 116
2014-09-01 | announce | 10
2014-09-01 | general | 645
2014-09-01 | hackers | 2364
2014-09-01 | interfaces | 3
2014-09-01 | performance | 105
2014-10-01 | announce | 13
2014-10-01 | general | 476
2014-10-01 | hackers | 2325
2014-10-01 | interfaces | 10
2014-10-01 | performance | 137
2014-11-01 | announce | 10
2014-11-01 | general | 457
2014-11-01 | hackers | 1810
2014-11-01 | performance | 109
2014-12-01 | announce | 11
2014-12-01 | general | 623
2014-12-01 | hackers | 2043
2014-12-01 | interfaces | 1
2014-12-01 | performance | 71
(39 rows)

\rotate gives:
Rotated query results
month | announce | general | hackers | interfaces | performance
------------+----------+---------+---------+------------+-------------
2014-05-01 | 19 | 550 | 1914 | 4 | 122
2014-06-01 | 10 | 499 | 2008 | 10 | 137
2014-07-01 | 12 | 703 | 1504 | 6 | 142
2014-08-01 | 9 | 616 | 1864 | 11 | 116
2014-09-01 | 10 | 645 | 2364 | 3 | 105
2014-10-01 | 13 | 476 | 2325 | 10 | 137
2014-11-01 | 10 | 457 | 1810 | | 109
2014-12-01 | 11 | 623 | 2043 | 1 | 71

Advantage: we can figure out the trends, and notice empty slots,
much quicker than with the previous output. It seems smaller
but there is the same amount of information.

5. Example with an additional column showing if the count grows up or down
compared to the previous month. This shows how the contents get stacked
inside cells when they come from several columns and rows.

Query:

SELECT to_char(mon, 'yyyy-mm') as month,
name,
CASE when lag(name,1) over(order by name,mon)=name then
case sign(cnt-(lag(cnt,1) over(order by name,mon)))
when 1 then chr(8593)
when 0 then chr(8597)
when -1 then chr(8595)
else ' ' end
END,
cnt
from (SELECT date_trunc('month', msg_date)::date as mon, t.name,count(*) as
cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name) l order by 2,1;

Result:
month | name | case | cnt
---------+-------------+------+------
2014-05 | announce | | 19
2014-06 | announce | ↓ | 10
2014-07 | announce | ↑ | 12
2014-08 | announce | ↓ | 9
2014-09 | announce | ↑ | 10
2014-10 | announce | ↑ | 13
2014-11 | announce | ↓ | 10
2014-12 | announce | ↑ | 11
2014-05 | general | | 550
2014-06 | general | ↓ | 499
2014-07 | general | ↑ | 703
2014-08 | general | ↓ | 616
2014-09 | general | ↑ | 645
2014-10 | general | ↓ | 476
2014-11 | general | ↓ | 457
2014-12 | general | ↑ | 623
2014-05 | hackers | | 1914
2014-06 | hackers | ↑ | 2008
2014-07 | hackers | ↓ | 1504
2014-08 | hackers | ↑ | 1864
2014-09 | hackers | ↑ | 2364
2014-10 | hackers | ↓ | 2325
2014-11 | hackers | ↓ | 1810
2014-12 | hackers | ↑ | 2043
2014-05 | interfaces | | 4
2014-06 | interfaces | ↑ | 10
2014-07 | interfaces | ↓ | 6
2014-08 | interfaces | ↑ | 11
2014-09 | interfaces | ↓ | 3
2014-10 | interfaces | ↑ | 10
2014-12 | interfaces | ↓ | 1
2014-05 | performance | | 122
2014-06 | performance | ↑ | 137
2014-07 | performance | ↑ | 142
2014-08 | performance | ↓ | 116
2014-09 | performance | ↓ | 105
2014-10 | performance | ↑ | 137
2014-11 | performance | ↓ | 109
2014-12 | performance | ↓ | 71
(39 rows)

\rotate:

Rotated query results
month | announce | general | hackers | interfaces | performance
---------+----------+---------+---------+------------+-------------
2014-05 | 19 | 550 | 1914 | 4 | 122
2014-06 | ↓ 10 | ↓ 499 | ↑ 2008 | ↑ 10 | ↑ 137
2014-07 | ↑ 12 | ↑ 703 | ↓ 1504 | ↓ 6 | ↑ 142
2014-08 | ↓ 9 | ↓ 616 | ↑ 1864 | ↑ 11 | ↓ 116
2014-09 | ↑ 10 | ↑ 645 | ↑ 2364 | ↓ 3 | ↓ 105
2014-10 | ↑ 13 | ↓ 476 | ↓ 2325 | ↑ 10 | ↑ 137
2014-11 | ↓ 10 | ↓ 457 | ↓ 1810 | | ↓ 109
2014-12 | ↑ 11 | ↑ 623 | ↑ 2043 | ↓ 1 | ↓ 71
(8 rows)

The output columns 3 and 4 of the same row get projected into the same
cell, laid out horizontally (separated by space).

6. Example with the same query but rotated differently so that
it's split into two columns: the counts that go up from the previous
and those that go down. I'm also cheating a bit by
casting name and cnt to char(N) for a better alignment

SELECT to_char(mon, 'yyyy-mm') as month,
name::char(12),
CASE when lag(name,1) over(order by name,mon)=name then
case sign(cnt-(lag(cnt,1) over(order by name,mon)))
when 1 then chr(8593)
when 0 then chr(8597)
when -1 then chr(8595)
else ' ' end
END,
cnt::char(8)
from (SELECT date_trunc('month', msg_date)::date as mon, t.name,count(*) as
cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name) l order by 2,1;

\rotate 1 3

+---------+-----------------------+-----------------------+
|  month  |	      ↑	    |		↓	      |
+---------+-----------------------+-----------------------+
| 2014-05 |			  |			  |
| 2014-06 | hackers	 2008	 +| announce	 10	 +|
|	  | interfaces	 10	 +| general	 499	  |
|	  | performance  137	  |			  |
| 2014-07 | announce	 12	 +| hackers	 1504	 +|
|	  | general	 703	 +| interfaces	 6	  |
|	  | performance  142	  |			  |
| 2014-08 | hackers	 1864	 +| announce	 9	 +|
|	  | interfaces	 11	  | general	 616	 +|
|	  |			  | performance  116	  |
| 2014-09 | announce	 10	 +| interfaces	 3	 +|
|	  | general	 645	 +| performance  105	  |
|	  | hackers	 2364	  |			  |
| 2014-10 | announce	 13	 +| general	 476	 +|
|	  | interfaces	 10	 +| hackers	 2325	  |
|	  | performance  137	  |			  |
| 2014-11 |			  | announce	 10	 +|
|	  |			  | general	 457	 +|
|	  |			  | hackers	 1810	 +|
|	  |			  | performance  109	  |
| 2014-12 | announce	 11	 +| interfaces	 1	 +|
|	  | general	 623	 +| performance  71	  |
|	  | hackers	 2043	  |			  |
+---------+-----------------------+-----------------------+

As there are several rows that match the vertical/horizontal filter,
(for example 3 results for 2014-06 as row and "arrow up" as column),
they are stacked vertically inside the cell, in addition to
"name" and "cnt" being shown side by side horizontally.

Note that no number show up for 2014-05; this is because they're not
associated with arrow up or down; empty as a column is discarded.
Maybe it shouldn't. In this case, the numbers for 2014-05 would be in a
column with an empty name.

Conclusion, the point of \rotate:

When analyzing query results, these rotated representations may be
useful or not depending on the cases, but the point is that they require
no effort to be obtained through \rotate X Y
It's so easy to play with various combinations to see if the result
makes sense, and if it reveals something about the data.
(it still reexecutes the query each time, tough).

We can get more or less the same results with crosstab/pivot, as it's the
same basic concept, but with much more effort spent on getting the SQL right,
plus the fact that columns not known in advance cannot be returned pivoted
in a single pass in SQL, a severe complication that the client-side doesn't
have.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-rotate.difftext/x-patch; name=psql-rotate.diffDownload
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index f1336d5..92f9f44 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o rotate.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 6181a61..d6c440a 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -48,6 +48,7 @@
 #include "psqlscan.h"
 #include "settings.h"
 #include "variables.h"
+#include "rotate.h"
 
 /*
  * Editable database object types.
@@ -1083,6 +1084,23 @@ exec_command(const char *cmd,
 		free(pw2);
 	}
 
+	/* \rotate -- execute a query and show results rotated along axis */
+	else if (strcmp(cmd, "rotate") == 0)
+	{
+		char	*opt1,
+				*opt2;
+
+		opt1 = psql_scan_slash_option(scan_state,
+										 OT_NORMAL, NULL, true);
+		opt2 = psql_scan_slash_option(scan_state,
+										 OT_NORMAL, NULL, true);
+
+		success = doRotate(opt1, opt2, query_buf);
+
+		free(opt1);
+		free(opt2);
+	}
+
 	/* \prompt -- prompt and set variable */
 	else if (strcmp(cmd, "prompt") == 0)
 	{
diff --git a/src/bin/psql/rotate.c b/src/bin/psql/rotate.c
new file mode 100644
index 0000000..094ea03
--- /dev/null
+++ b/src/bin/psql/rotate.c
@@ -0,0 +1,352 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/rotate.c
+ */
+
+#include "common.h"
+#include "pqexpbuffer.h"
+#include "settings.h"
+#include "rotate.h"
+
+#include <string.h>
+
+static int
+headerCompare(const void *a, const void *b)
+{
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+static void
+accumHeader(char* name, int* count, struct pivot_field **sorted_tab, int row_number)
+{
+	struct pivot_field *p;
+
+	/*
+	 * Search for name in sorted_tab. If it doesn't exist, insert it,
+	 * otherwise do nothing.
+	 */
+
+	if (*count >= 1)
+	{
+		p = (struct pivot_field*) bsearch(&name,
+										  *sorted_tab,
+										  *count,
+										  sizeof(struct pivot_field),
+										  headerCompare);
+	}
+	else
+		p=NULL;
+
+	if (!p)
+	{
+		*sorted_tab = pg_realloc(*sorted_tab, sizeof(struct pivot_field) * (1+*count));
+		(*sorted_tab)[*count].name = name;
+		(*sorted_tab)[*count].rank = *count;
+		(*count)++;
+
+		qsort(*sorted_tab,
+			  *count,
+			  sizeof(struct pivot_field),
+			  headerCompare);
+	}
+}
+
+static void
+printRotation(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, k, rn;
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	popt.title = _("Rotated query results");
+	printTableInit(&cont, &popt.topt, popt.title,
+				   num_columns+1, num_rows);
+
+	/* The name of the first column is kept unchanged by the rotation */
+	printTableAddHeader(&cont, PQfname(results, 0),
+						popt.translate_header, 'l');
+
+	/* The names of the next columns come from piv_columns[] */
+	for (i = 0; i < num_columns; i++)
+	{
+		printTableAddHeader(&cont, piv_columns[i].name,
+							popt.translate_header, 'l');
+	}
+
+	/* Set row names in the first output column */
+	for (i = 0; i < num_rows; i++)
+	{
+		k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		char* row_name;
+		char* col_name;
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		row_number = col_number = -1;
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+		{
+			row_name = PQgetvalue(results, rn, field_for_rows);
+			p = (struct pivot_field*) bsearch(&row_name,
+											  piv_rows,
+											  num_rows,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				row_number = p->rank;
+		}
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+		{
+			col_name = PQgetvalue(results, rn, field_for_columns);
+			p = (struct pivot_field*) bsearch(&col_name,
+											  piv_columns,
+											  num_columns,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				col_number = (p - piv_columns);
+		}
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+			int k = 0;
+
+			do {
+				char *content;
+
+				if (PQnfields(results) == 2)
+				{
+					/*
+					  special case: when the source has only 2 columns, use a
+					  X (cross/checkmark) for the cell content, and set
+					  src_col to a virtual additional column.
+					*/
+					content = "X";
+					src_col = 3;
+				}
+				else if (src_col == field_for_rows || src_col == field_for_columns)
+				{
+					/*
+					  The source values that produce headers are not processed
+					  in this loop, only the values that end up inside the grid.
+					*/
+					src_col++;
+					continue;
+				}
+				else
+				{
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (k==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+				k++;
+				src_col++;
+			} while (src_col < PQnfields(results));
+		}
+	}
+
+	printTable(&cont, pset.queryFout, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+/*
+ * doRotate -- handler for \rotate
+ *
+ */
+bool
+doRotate(const char* opt_field_for_rows,
+		 const char* opt_field_for_columns,
+		 PQExpBuffer query_buf)
+{
+	PGresult   *res;
+	int		rn;
+	struct pivot_field	*piv_columns = NULL;
+	struct pivot_field	*piv_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	OK = true;
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows;
+
+	if (!query_buf || query_buf->len <= 0)
+	{
+		psql_error(_("\\rotate cannot be used with an empty query\n"));
+		return false;
+	}
+
+	if (!opt_field_for_rows)
+		field_for_rows = 0;
+	else
+	{
+		field_for_rows = atoi(opt_field_for_rows)-1;
+	}
+
+	if (!opt_field_for_columns)
+		field_for_columns = 1;
+	else
+	{
+		field_for_columns = atoi(opt_field_for_columns)-1;
+	}
+
+
+	res = PSQLexec(query_buf->data);
+
+	if (!res)
+		return false;			/* error processing has been done by PSQLexec() */
+
+	if (PQresultStatus(res) == PGRES_TUPLES_OK)
+	{
+		if (PQnfields(res) < 2)
+		{
+			psql_error(_("A query must return at least two columns to rotate its output\n"));
+			OK = false;
+		}
+		else if (field_for_rows < 0  || field_for_rows >= PQnfields(res)  ||
+				 field_for_columns < 0 || field_for_columns >= PQnfields(res) )
+		{
+			psql_error(_("Invalid column number\n"));
+			OK = false;
+		}
+		else
+		{
+			/*
+			 * First pass: accumulate row names and column names, each into their
+			 * sorted array
+			 */
+			for (rn = 0; rn < PQntuples(res); rn++)
+			{
+				if (!PQgetisnull(res, rn, field_for_rows))
+				{
+					accumHeader(PQgetvalue(res, rn, field_for_rows), &num_rows, &piv_rows, rn);
+				}
+
+				if (!PQgetisnull(res, rn, field_for_columns))
+				{
+					accumHeader(PQgetvalue(res, rn, field_for_columns), &num_columns, &piv_columns, rn);
+				}
+			}
+
+			/*
+			 * Second pass: print the rotated results
+			 */
+			printRotation(res,
+						  num_columns,
+						  piv_columns,
+						  field_for_columns,
+						  num_rows,
+						  piv_rows,
+						  field_for_rows);
+		}
+
+	}
+
+	pg_free(piv_columns);
+	pg_free(piv_rows);
+
+	PQclear(res);
+
+	return OK;
+}
+
diff --git a/src/bin/psql/rotate.h b/src/bin/psql/rotate.h
new file mode 100644
index 0000000..8fed224
--- /dev/null
+++ b/src/bin/psql/rotate.h
@@ -0,0 +1,28 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/rotate.h
+ */
+
+#ifndef ROTATE_H
+#define ROTATE_H
+
+struct pivot_field
+{
+	/* pointer obtained by PGgetvalue() in column 0 or 1 */
+	char*	name;
+
+	/* rank of that field in the list of fields, starting at 0.
+	   rank=N means it's the Nth distinct field encountered when looping
+	   through rows in their initial order */
+	int		rank;
+};
+
+/* prototypes */
+extern bool doRotate(const char* opt_field_for_rows,
+					 const char* opt_field_for_columns,
+					 PQExpBuffer query_buf);
+
+#endif   /* ROTATE_H */
#2Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#1)
Re: [patch] Proposal for \rotate in psql

2015-08-29 0:48 GMT+02:00 Daniel Verite <daniel@manitou-mail.org>:

Hi,

This is a reboot of my previous proposal for pivoting results in psql,
with a new patch that generalizes the idea further through a command
now named \rotate, and some examples.

So the concept is: having an existing query in the query buffer,
the user can specify two column numbers C1 and C2 (by default the 1st
and 2nd) as an argument to a \rotate command.

The query results are then displayed in a 2D grid such that each tuple
(vx, vy, va, vb,...) is shown as |va vb...| in a cell at coordinates
(vx,vy).
The values vx,xy come from columns C1,C2 respectively and are
represented in the output as an horizontal and a vertical header.

A cell may hold several columns from several rows, growing horizontally and
vertically (\n inside the cell) if necessary to show all results.

The examples below should be read with a monospaced font as in psql,
otherwise they will look pretty bad.

1. Example with only 2 columns, querying login/group membership from the
catalog.
Query:

SELECT r.rolname as username,r1.rolname as groupname
FROM pg_catalog.pg_roles r LEFT JOIN pg_catalog.pg_auth_members m
ON (m.member = r.oid)
LEFT JOIN pg_roles r1 ON (m.roleid=r1.oid)
WHERE r.rolcanlogin
ORDER BY 1

Sample results:
username | groupname
------------+-----------
daniel | mailusers
drupal |
dv | admin
dv | common
extc | readonly
extu |
foobar |
joel |
mailreader | readonly
manitou | mailusers
manitou | admin
postgres |
u1 | common
u2 | mailusers
zaz | mailusers

Applying \rotate gives:
Rotated query results
username | admin | common | mailusers | readonly
------------+-------+--------+-----------+----------
daniel | | | X |
drupal | | | |
dv | X | X | |
extc | | | | X
extu | | | |
foobar | | | |
joel | | | |
mailreader | | | | X
manitou | X | | X |
postgres | | | |
u1 | | X | |
u2 | | | X |
zaz | | | X |

The 'X' inside cells is automatically added as there are only
2 columns. If there was a 3rd column, the content of that column would
be displayed instead (as in the next example).

What's good in that \rotate display compared to the classic output is that
it's more apparent, visually speaking, that such user belongs or not to
such
group or another.

2. Example with a unicode checkmark added as 3rd column, and
unicode linestyle and borders (to be seen with a mono-spaced font):

SELECT r.rolname as username,r1.rolname as groupname, chr(10003)
FROM pg_catalog.pg_roles r LEFT JOIN pg_catalog.pg_auth_members m
ON (m.member = r.oid)
LEFT JOIN pg_roles r1 ON (m.roleid=r1.oid)
WHERE r.rolcanlogin
ORDER BY 1

Rotated query results
┌────────────┬───────┬───�”
�────┬───────────┬────────â
��─┐
│ username │ admin │ common │ mailusers │ readonly │
├────────────┼───────┼───�”
�────┼───────────┼────────â
��─┤
│ daniel │ │ │ ✓ │ │
│ drupal │ │ │ │ │
│ dv │ ✓ │ ✓ │ │ │
│ extc │ │ │ │ ✓ │
│ extu │ │ │ │ │
│ foobar │ │ │ │ │
│ joel │ │ │ │ │
│ mailreader │ │ │ │ ✓ │
│ manitou │ ✓ │ │ ✓ │ │
│ postgres │ │ │ │ │
│ u1 │ │ ✓ │ │ │
│ u2 │ │ │ ✓ │ │
│ zaz │ │ │ ✓ │ │
└────────────┴───────┴───�”
�────┴───────────┴────────â
��─┘

What I like in that representation is that it looks good enough
to be pasted directly into a document in a word processor.

3. It can be rotated easily in the other direction, with:
\rotate 2 1

(Cut horizontally to fit in a mail, the actual output is 116 chars wide).

Rotated query results
┌───────────┬────────┬───�”
�────┬────┬──────┬──────┬─â
��──────┬──────┬────
│ username │ daniel │ drupal │ dv │ extc │ extu │ foobar │
joel │ mai...
├───────────┼────────┼───�”
�────┼────┼──────┼──────┼─â
��──────┼──────┼────
│ mailusers │ ✓ │ │ │ │ │ │

│ admin │ │ │ ✓ │ │ │ │

│ common │ │ │ ✓ │ │ │ │

│ readonly │ │ │ │ ✓ │ │ │
│ ✓
└───────────┴────────┴───�”
�────┴────┴──────┴──────┴─â
��──────┴──────┴────

4. Example with 3 columns and a count as the value to visualize along
two axis: date and category.
I'm using the number of mails posted per month in a few PG mailing lists,
broken down by list (which are tags in my schema).

Query:
SELECT date_trunc('month', msg_date)::date as month,
t.name,
count(*) as cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name
ORDER BY 1,2;

Results:
month | name | cnt
------------+-------------+------
2014-05-01 | announce | 19
2014-05-01 | general | 550
2014-05-01 | hackers | 1914
2014-05-01 | interfaces | 4
2014-05-01 | performance | 122
2014-06-01 | announce | 10
2014-06-01 | general | 499
2014-06-01 | hackers | 2008
2014-06-01 | interfaces | 10
2014-06-01 | performance | 137
2014-07-01 | announce | 12
2014-07-01 | general | 703
2014-07-01 | hackers | 1504
2014-07-01 | interfaces | 6
2014-07-01 | performance | 142
2014-08-01 | announce | 9
2014-08-01 | general | 616
2014-08-01 | hackers | 1864
2014-08-01 | interfaces | 11
2014-08-01 | performance | 116
2014-09-01 | announce | 10
2014-09-01 | general | 645
2014-09-01 | hackers | 2364
2014-09-01 | interfaces | 3
2014-09-01 | performance | 105
2014-10-01 | announce | 13
2014-10-01 | general | 476
2014-10-01 | hackers | 2325
2014-10-01 | interfaces | 10
2014-10-01 | performance | 137
2014-11-01 | announce | 10
2014-11-01 | general | 457
2014-11-01 | hackers | 1810
2014-11-01 | performance | 109
2014-12-01 | announce | 11
2014-12-01 | general | 623
2014-12-01 | hackers | 2043
2014-12-01 | interfaces | 1
2014-12-01 | performance | 71
(39 rows)

\rotate gives:
Rotated query results
month | announce | general | hackers | interfaces | performance
------------+----------+---------+---------+------------+-------------
2014-05-01 | 19 | 550 | 1914 | 4 | 122
2014-06-01 | 10 | 499 | 2008 | 10 | 137
2014-07-01 | 12 | 703 | 1504 | 6 | 142
2014-08-01 | 9 | 616 | 1864 | 11 | 116
2014-09-01 | 10 | 645 | 2364 | 3 | 105
2014-10-01 | 13 | 476 | 2325 | 10 | 137
2014-11-01 | 10 | 457 | 1810 | | 109
2014-12-01 | 11 | 623 | 2043 | 1 | 71

Advantage: we can figure out the trends, and notice empty slots,
much quicker than with the previous output. It seems smaller
but there is the same amount of information.

5. Example with an additional column showing if the count grows up or down
compared to the previous month. This shows how the contents get stacked
inside cells when they come from several columns and rows.

Query:

SELECT to_char(mon, 'yyyy-mm') as month,
name,
CASE when lag(name,1) over(order by name,mon)=name then
case sign(cnt-(lag(cnt,1) over(order by name,mon)))
when 1 then chr(8593)
when 0 then chr(8597)
when -1 then chr(8595)
else ' ' end
END,
cnt
from (SELECT date_trunc('month', msg_date)::date as mon, t.name,count(*)
as
cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name) l order by 2,1;

Result:
month | name | case | cnt
---------+-------------+------+------
2014-05 | announce | | 19
2014-06 | announce | ↓ | 10
2014-07 | announce | ↑ | 12
2014-08 | announce | ↓ | 9
2014-09 | announce | ↑ | 10
2014-10 | announce | ↑ | 13
2014-11 | announce | ↓ | 10
2014-12 | announce | ↑ | 11
2014-05 | general | | 550
2014-06 | general | ↓ | 499
2014-07 | general | ↑ | 703
2014-08 | general | ↓ | 616
2014-09 | general | ↑ | 645
2014-10 | general | ↓ | 476
2014-11 | general | ↓ | 457
2014-12 | general | ↑ | 623
2014-05 | hackers | | 1914
2014-06 | hackers | ↑ | 2008
2014-07 | hackers | ↓ | 1504
2014-08 | hackers | ↑ | 1864
2014-09 | hackers | ↑ | 2364
2014-10 | hackers | ↓ | 2325
2014-11 | hackers | ↓ | 1810
2014-12 | hackers | ↑ | 2043
2014-05 | interfaces | | 4
2014-06 | interfaces | ↑ | 10
2014-07 | interfaces | ↓ | 6
2014-08 | interfaces | ↑ | 11
2014-09 | interfaces | ↓ | 3
2014-10 | interfaces | ↑ | 10
2014-12 | interfaces | ↓ | 1
2014-05 | performance | | 122
2014-06 | performance | ↑ | 137
2014-07 | performance | ↑ | 142
2014-08 | performance | ↓ | 116
2014-09 | performance | ↓ | 105
2014-10 | performance | ↑ | 137
2014-11 | performance | ↓ | 109
2014-12 | performance | ↓ | 71
(39 rows)

\rotate:

Rotated query results
month | announce | general | hackers | interfaces | performance
---------+----------+---------+---------+------------+-------------
2014-05 | 19 | 550 | 1914 | 4 | 122
2014-06 | ↓ 10 | ↓ 499 | ↑ 2008 | ↑ 10 | ↑ 137
2014-07 | ↑ 12 | ↑ 703 | ↓ 1504 | ↓ 6 | ↑ 142
2014-08 | ↓ 9 | ↓ 616 | ↑ 1864 | ↑ 11 | ↓ 116
2014-09 | ↑ 10 | ↑ 645 | ↑ 2364 | ↓ 3 | ↓ 105
2014-10 | ↑ 13 | ↓ 476 | ↓ 2325 | ↑ 10 | ↑ 137
2014-11 | ↓ 10 | ↓ 457 | ↓ 1810 | | ↓ 109
2014-12 | ↑ 11 | ↑ 623 | ↑ 2043 | ↓ 1 | ↓ 71
(8 rows)

The output columns 3 and 4 of the same row get projected into the same
cell, laid out horizontally (separated by space).

6. Example with the same query but rotated differently so that
it's split into two columns: the counts that go up from the previous
and those that go down. I'm also cheating a bit by
casting name and cnt to char(N) for a better alignment

SELECT to_char(mon, 'yyyy-mm') as month,
name::char(12),
CASE when lag(name,1) over(order by name,mon)=name then
case sign(cnt-(lag(cnt,1) over(order by name,mon)))
when 1 then chr(8593)
when 0 then chr(8597)
when -1 then chr(8595)
else ' ' end
END,
cnt::char(8)
from (SELECT date_trunc('month', msg_date)::date as mon, t.name,count(*)
as
cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name) l order by 2,1;

\rotate 1 3

+---------+-----------------------+-----------------------+
|  month  |           ↑     |           ↓             |
+---------+-----------------------+-----------------------+
| 2014-05 |                       |                       |
| 2014-06 | hackers      2008    +| announce     10      +|
|         | interfaces   10      +| general      499      |
|         | performance  137      |                       |
| 2014-07 | announce     12      +| hackers      1504    +|
|         | general      703     +| interfaces   6        |
|         | performance  142      |                       |
| 2014-08 | hackers      1864    +| announce     9       +|
|         | interfaces   11       | general      616     +|
|         |                       | performance  116      |
| 2014-09 | announce     10      +| interfaces   3       +|
|         | general      645     +| performance  105      |
|         | hackers      2364     |                       |
| 2014-10 | announce     13      +| general      476     +|
|         | interfaces   10      +| hackers      2325     |
|         | performance  137      |                       |
| 2014-11 |                       | announce     10      +|
|         |                       | general      457     +|
|         |                       | hackers      1810    +|
|         |                       | performance  109      |
| 2014-12 | announce     11      +| interfaces   1       +|
|         | general      623     +| performance  71       |
|         | hackers      2043     |                       |
+---------+-----------------------+-----------------------+

As there are several rows that match the vertical/horizontal filter,
(for example 3 results for 2014-06 as row and "arrow up" as column),
they are stacked vertically inside the cell, in addition to
"name" and "cnt" being shown side by side horizontally.

Note that no number show up for 2014-05; this is because they're not
associated with arrow up or down; empty as a column is discarded.
Maybe it shouldn't. In this case, the numbers for 2014-05 would be in a
column with an empty name.

Conclusion, the point of \rotate:

When analyzing query results, these rotated representations may be
useful or not depending on the cases, but the point is that they require
no effort to be obtained through \rotate X Y
It's so easy to play with various combinations to see if the result
makes sense, and if it reveals something about the data.
(it still reexecutes the query each time, tough).

We can get more or less the same results with crosstab/pivot, as it's the
same basic concept, but with much more effort spent on getting the SQL
right,
plus the fact that columns not known in advance cannot be returned pivoted
in a single pass in SQL, a severe complication that the client-side doesn't
have.

simple and user friendy

nice

+1

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3David Fetter
david@fetter.org
In reply to: Daniel Verite (#1)
Re: [patch] Proposal for \rotate in psql

On Sat, Aug 29, 2015 at 12:48:23AM +0200, Daniel Verite wrote:

Hi,

This is a reboot of my previous proposal for pivoting results in psql,
with a new patch that generalizes the idea further through a command
now named \rotate, and some examples.

Neat!

Thanks for putting this together :)

Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4Daniel Verite
daniel@manitou-mail.org
In reply to: Daniel Verite (#1)
Re: [patch] Proposal for \rotate in psql

I wrote:

What I like in that representation is that it looks good enough
to be pasted directly into a document in a word processor.

And ironically, the nice unicode borders came out all garbled
in the mail, thanks to a glitch in my setup that mis-reformatted them
before sending.

Sorry about that, the results with unicode linestyle were supposed to be
as follows:

Example 2:

Rotated query results
┌────────────┬───────┬────────┬───────────┬──────────┐
│ username │ admin │ common │ mailusers │ readonly │
├────────────┼───────┼────────┼───────────┼──────────┤
│ daniel │ │ │ ✓ │ │
│ drupal │ │ │ │ │
│ dv │ ✓ │ ✓ │ │ │
│ extc │ │ │ │ ✓ │
│ extu │ │ │ │ │
│ foobar │ │ │ │ │
│ joel │ │ │ │ │
│ mailreader │ │ │ │ ✓ │
│ manitou │ ✓ │ │ ✓ │ │
│ postgres │ │ │ │ │
│ u1 │ │ ✓ │ │ │
│ u2 │ │ │ ✓ │ │
│ zaz │ │ │ ✓ │ │
└────────────┴───────┴────────┴───────────┴──────────┘

Example 3, rotated in the other direction

(Cut horizontally to fit in a mail, the actual output is 116 chars wide).

Rotated query results
┌───────────┬────────┬────────┬────┬──────┬──────┬────────┬──────┬────
│ username │ daniel │ drupal │ dv │ extc │ extu │ foobar │ joel │ mai...
├───────────┼────────┼────────┼────┼──────┼──────┼────────┼──────┼────
│ mailusers │ ✓ │ │ │ │ │ │ │
│ admin │ │ │ ✓ │ │ │ │ │
│ common │ │ │ ✓ │ │ │ │ │
│ readonly │ │ │ │ ✓ │ │ │ │ ✓
└───────────┴────────┴────────┴────┴──────┴──────┴────────┴──────┴────

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#2)
Re: [patch] Proposal for \rotate in psql

2015-08-29 5:57 GMT+02:00 Pavel Stehule <pavel.stehule@gmail.com>:

2015-08-29 0:48 GMT+02:00 Daniel Verite <daniel@manitou-mail.org>:

Hi,

This is a reboot of my previous proposal for pivoting results in psql,
with a new patch that generalizes the idea further through a command
now named \rotate, and some examples.

So the concept is: having an existing query in the query buffer,
the user can specify two column numbers C1 and C2 (by default the 1st
and 2nd) as an argument to a \rotate command.

The query results are then displayed in a 2D grid such that each tuple
(vx, vy, va, vb,...) is shown as |va vb...| in a cell at coordinates
(vx,vy).
The values vx,xy come from columns C1,C2 respectively and are
represented in the output as an horizontal and a vertical header.

A cell may hold several columns from several rows, growing horizontally
and
vertically (\n inside the cell) if necessary to show all results.

The examples below should be read with a monospaced font as in psql,
otherwise they will look pretty bad.

1. Example with only 2 columns, querying login/group membership from the
catalog.
Query:

SELECT r.rolname as username,r1.rolname as groupname
FROM pg_catalog.pg_roles r LEFT JOIN pg_catalog.pg_auth_members m
ON (m.member = r.oid)
LEFT JOIN pg_roles r1 ON (m.roleid=r1.oid)
WHERE r.rolcanlogin
ORDER BY 1

Sample results:
username | groupname
------------+-----------
daniel | mailusers
drupal |
dv | admin
dv | common
extc | readonly
extu |
foobar |
joel |
mailreader | readonly
manitou | mailusers
manitou | admin
postgres |
u1 | common
u2 | mailusers
zaz | mailusers

Applying \rotate gives:
Rotated query results
username | admin | common | mailusers | readonly
------------+-------+--------+-----------+----------
daniel | | | X |
drupal | | | |
dv | X | X | |
extc | | | | X
extu | | | |
foobar | | | |
joel | | | |
mailreader | | | | X
manitou | X | | X |
postgres | | | |
u1 | | X | |
u2 | | | X |
zaz | | | X |

The 'X' inside cells is automatically added as there are only
2 columns. If there was a 3rd column, the content of that column would
be displayed instead (as in the next example).

What's good in that \rotate display compared to the classic output is that
it's more apparent, visually speaking, that such user belongs or not to
such
group or another.

2. Example with a unicode checkmark added as 3rd column, and
unicode linestyle and borders (to be seen with a mono-spaced font):

SELECT r.rolname as username,r1.rolname as groupname, chr(10003)
FROM pg_catalog.pg_roles r LEFT JOIN pg_catalog.pg_auth_members m
ON (m.member = r.oid)
LEFT JOIN pg_roles r1 ON (m.roleid=r1.oid)
WHERE r.rolcanlogin
ORDER BY 1

Rotated query results
┌────────────┬───────┬───�”
�────┬───────────┬────────â
��─┐
│ username │ admin │ common │ mailusers │ readonly │
├────────────┼───────┼───�”
�────┼───────────┼────────â
��─┤
│ daniel │ │ │ ✓ │ │
│ drupal │ │ │ │ │
│ dv │ ✓ │ ✓ │ │ │
│ extc │ │ │ │ ✓ │
│ extu │ │ │ │ │
│ foobar │ │ │ │ │
│ joel │ │ │ │ │
│ mailreader │ │ │ │ ✓ │
│ manitou │ ✓ │ │ ✓ │ │
│ postgres │ │ │ │ │
│ u1 │ │ ✓ │ │ │
│ u2 │ │ │ ✓ │ │
│ zaz │ │ │ ✓ │ │
└────────────┴───────┴───�”
�────┴───────────┴────────â
��─┘

What I like in that representation is that it looks good enough
to be pasted directly into a document in a word processor.

3. It can be rotated easily in the other direction, with:
\rotate 2 1

(Cut horizontally to fit in a mail, the actual output is 116 chars wide).

Rotated query results
┌───────────┬────────┬───�”
�────┬────┬──────┬──────┬─â
��──────┬──────┬────
│ username │ daniel │ drupal │ dv │ extc │ extu │ foobar │
joel │ mai...
├───────────┼────────┼───�”
�────┼────┼──────┼──────┼─â
��──────┼──────┼────
│ mailusers │ ✓ │ │ │ │ │ │

│ admin │ │ │ ✓ │ │ │ │

│ common │ │ │ ✓ │ │ │ │

│ readonly │ │ │ │ ✓ │ │ │
│ ✓
└───────────┴────────┴───�”
�────┴────┴──────┴──────┴─â
��──────┴──────┴────

4. Example with 3 columns and a count as the value to visualize along
two axis: date and category.
I'm using the number of mails posted per month in a few PG mailing lists,
broken down by list (which are tags in my schema).

Query:
SELECT date_trunc('month', msg_date)::date as month,
t.name,
count(*) as cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name
ORDER BY 1,2;

Results:
month | name | cnt
------------+-------------+------
2014-05-01 | announce | 19
2014-05-01 | general | 550
2014-05-01 | hackers | 1914
2014-05-01 | interfaces | 4
2014-05-01 | performance | 122
2014-06-01 | announce | 10
2014-06-01 | general | 499
2014-06-01 | hackers | 2008
2014-06-01 | interfaces | 10
2014-06-01 | performance | 137
2014-07-01 | announce | 12
2014-07-01 | general | 703
2014-07-01 | hackers | 1504
2014-07-01 | interfaces | 6
2014-07-01 | performance | 142
2014-08-01 | announce | 9
2014-08-01 | general | 616
2014-08-01 | hackers | 1864
2014-08-01 | interfaces | 11
2014-08-01 | performance | 116
2014-09-01 | announce | 10
2014-09-01 | general | 645
2014-09-01 | hackers | 2364
2014-09-01 | interfaces | 3
2014-09-01 | performance | 105
2014-10-01 | announce | 13
2014-10-01 | general | 476
2014-10-01 | hackers | 2325
2014-10-01 | interfaces | 10
2014-10-01 | performance | 137
2014-11-01 | announce | 10
2014-11-01 | general | 457
2014-11-01 | hackers | 1810
2014-11-01 | performance | 109
2014-12-01 | announce | 11
2014-12-01 | general | 623
2014-12-01 | hackers | 2043
2014-12-01 | interfaces | 1
2014-12-01 | performance | 71
(39 rows)

\rotate gives:
Rotated query results
month | announce | general | hackers | interfaces | performance
------------+----------+---------+---------+------------+-------------
2014-05-01 | 19 | 550 | 1914 | 4 | 122
2014-06-01 | 10 | 499 | 2008 | 10 | 137
2014-07-01 | 12 | 703 | 1504 | 6 | 142
2014-08-01 | 9 | 616 | 1864 | 11 | 116
2014-09-01 | 10 | 645 | 2364 | 3 | 105
2014-10-01 | 13 | 476 | 2325 | 10 | 137
2014-11-01 | 10 | 457 | 1810 | | 109
2014-12-01 | 11 | 623 | 2043 | 1 | 71

Advantage: we can figure out the trends, and notice empty slots,
much quicker than with the previous output. It seems smaller
but there is the same amount of information.

5. Example with an additional column showing if the count grows up or down
compared to the previous month. This shows how the contents get stacked
inside cells when they come from several columns and rows.

Query:

SELECT to_char(mon, 'yyyy-mm') as month,
name,
CASE when lag(name,1) over(order by name,mon)=name then
case sign(cnt-(lag(cnt,1) over(order by name,mon)))
when 1 then chr(8593)
when 0 then chr(8597)
when -1 then chr(8595)
else ' ' end
END,
cnt
from (SELECT date_trunc('month', msg_date)::date as mon, t.name,count(*)
as
cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name) l order by 2,1;

Result:
month | name | case | cnt
---------+-------------+------+------
2014-05 | announce | | 19
2014-06 | announce | ↓ | 10
2014-07 | announce | ↑ | 12
2014-08 | announce | ↓ | 9
2014-09 | announce | ↑ | 10
2014-10 | announce | ↑ | 13
2014-11 | announce | ↓ | 10
2014-12 | announce | ↑ | 11
2014-05 | general | | 550
2014-06 | general | ↓ | 499
2014-07 | general | ↑ | 703
2014-08 | general | ↓ | 616
2014-09 | general | ↑ | 645
2014-10 | general | ↓ | 476
2014-11 | general | ↓ | 457
2014-12 | general | ↑ | 623
2014-05 | hackers | | 1914
2014-06 | hackers | ↑ | 2008
2014-07 | hackers | ↓ | 1504
2014-08 | hackers | ↑ | 1864
2014-09 | hackers | ↑ | 2364
2014-10 | hackers | ↓ | 2325
2014-11 | hackers | ↓ | 1810
2014-12 | hackers | ↑ | 2043
2014-05 | interfaces | | 4
2014-06 | interfaces | ↑ | 10
2014-07 | interfaces | ↓ | 6
2014-08 | interfaces | ↑ | 11
2014-09 | interfaces | ↓ | 3
2014-10 | interfaces | ↑ | 10
2014-12 | interfaces | ↓ | 1
2014-05 | performance | | 122
2014-06 | performance | ↑ | 137
2014-07 | performance | ↑ | 142
2014-08 | performance | ↓ | 116
2014-09 | performance | ↓ | 105
2014-10 | performance | ↑ | 137
2014-11 | performance | ↓ | 109
2014-12 | performance | ↓ | 71
(39 rows)

\rotate:

Rotated query results
month | announce | general | hackers | interfaces | performance
---------+----------+---------+---------+------------+-------------
2014-05 | 19 | 550 | 1914 | 4 | 122
2014-06 | ↓ 10 | ↓ 499 | ↑ 2008 | ↑ 10 | ↑ 137
2014-07 | ↑ 12 | ↑ 703 | ↓ 1504 | ↓ 6 | ↑ 142
2014-08 | ↓ 9 | ↓ 616 | ↑ 1864 | ↑ 11 | ↓ 116
2014-09 | ↑ 10 | ↑ 645 | ↑ 2364 | ↓ 3 | ↓ 105
2014-10 | ↑ 13 | ↓ 476 | ↓ 2325 | ↑ 10 | ↑ 137
2014-11 | ↓ 10 | ↓ 457 | ↓ 1810 | | ↓ 109
2014-12 | ↑ 11 | ↑ 623 | ↑ 2043 | ↓ 1 | ↓ 71
(8 rows)

The output columns 3 and 4 of the same row get projected into the same
cell, laid out horizontally (separated by space).

6. Example with the same query but rotated differently so that
it's split into two columns: the counts that go up from the previous
and those that go down. I'm also cheating a bit by
casting name and cnt to char(N) for a better alignment

SELECT to_char(mon, 'yyyy-mm') as month,
name::char(12),
CASE when lag(name,1) over(order by name,mon)=name then
case sign(cnt-(lag(cnt,1) over(order by name,mon)))
when 1 then chr(8593)
when 0 then chr(8597)
when -1 then chr(8595)
else ' ' end
END,
cnt::char(8)
from (SELECT date_trunc('month', msg_date)::date as mon, t.name,count(*)
as
cnt
FROM mail JOIN mail_tags using(mail_id) JOIN tags t
on(t.tag_id=mail_tags.tag)
WHERE t.tag_id in (7,8,12,34,79)
AND msg_date>='2014-05-01'::date and msg_date<'2015-01-01'::date
GROUP BY date_trunc('month', msg_date)::date, t.name) l order by 2,1;

\rotate 1 3

+---------+-----------------------+-----------------------+
|  month  |           ↑     |           ↓             |
+---------+-----------------------+-----------------------+
| 2014-05 |                       |                       |
| 2014-06 | hackers      2008    +| announce     10      +|
|         | interfaces   10      +| general      499      |
|         | performance  137      |                       |
| 2014-07 | announce     12      +| hackers      1504    +|
|         | general      703     +| interfaces   6        |
|         | performance  142      |                       |
| 2014-08 | hackers      1864    +| announce     9       +|
|         | interfaces   11       | general      616     +|
|         |                       | performance  116      |
| 2014-09 | announce     10      +| interfaces   3       +|
|         | general      645     +| performance  105      |
|         | hackers      2364     |                       |
| 2014-10 | announce     13      +| general      476     +|
|         | interfaces   10      +| hackers      2325     |
|         | performance  137      |                       |
| 2014-11 |                       | announce     10      +|
|         |                       | general      457     +|
|         |                       | hackers      1810    +|
|         |                       | performance  109      |
| 2014-12 | announce     11      +| interfaces   1       +|
|         | general      623     +| performance  71       |
|         | hackers      2043     |                       |
+---------+-----------------------+-----------------------+

As there are several rows that match the vertical/horizontal filter,
(for example 3 results for 2014-06 as row and "arrow up" as column),
they are stacked vertically inside the cell, in addition to
"name" and "cnt" being shown side by side horizontally.

Note that no number show up for 2014-05; this is because they're not
associated with arrow up or down; empty as a column is discarded.
Maybe it shouldn't. In this case, the numbers for 2014-05 would be in a
column with an empty name.

Conclusion, the point of \rotate:

When analyzing query results, these rotated representations may be
useful or not depending on the cases, but the point is that they require
no effort to be obtained through \rotate X Y
It's so easy to play with various combinations to see if the result
makes sense, and if it reveals something about the data.
(it still reexecutes the query each time, tough).

We can get more or less the same results with crosstab/pivot, as it's the
same basic concept, but with much more effort spent on getting the SQL
right,
plus the fact that columns not known in advance cannot be returned pivoted
in a single pass in SQL, a severe complication that the client-side
doesn't
have.

simple and user friendy

nice

+1

Pavel

the name "rotate" is not correct - maybe "\cross" ?

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#6Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#5)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

the name "rotate" is not correct - maybe "\cross" ?

I'm not dead set on \rotate and suggested other names
previously in [1]/messages/by-id/cd521513-1349-4698-b93c-693199962e23@mm, but none of them seems decisively
superior.

The rationale behind rotate is that, it's a synonym of pivot
as a verb, and it's not already used for other things in SQL.

Incidentally I'm discovering by googling that people actually
searched previously for that feature with that name:
http://postgresql.nabble.com/rotate-psql-output-td3046832.html

OTOH "cross" is already used in the database vocabulary for
cross joins. Also I find it used too in "cross-db queries" or the
"cross apply" of other engines.
I think that plays against it for choosing it to designate
something different again.

However, maybe \across may be a better fit, or "cross"
combined with some other word, as in \crossview .
Not sure how that sounds to a native english speaker.

[1]: /messages/by-id/cd521513-1349-4698-b93c-693199962e23@mm
/messages/by-id/cd521513-1349-4698-b93c-693199962e23@mm

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Greg Stark
stark@mit.edu
In reply to: Daniel Verite (#6)
Re: [patch] Proposal for \rotate in psql

On Fri, Sep 4, 2015 at 5:08 PM, Daniel Verite <daniel@manitou-mail.org> wrote:

I'm not dead set on \rotate and suggested other names
previously in [1], but none of them seems decisively
superior.

Fwiw I like \rotate. It's pretty clear what it means and it sounds
similar to but not exactly the same as pivot.

--
greg

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#8Pavel Stehule
pavel.stehule@gmail.com
In reply to: Greg Stark (#7)
Re: [patch] Proposal for \rotate in psql

2015-09-07 22:14 GMT+02:00 Greg Stark <stark@mit.edu>:

On Fri, Sep 4, 2015 at 5:08 PM, Daniel Verite <daniel@manitou-mail.org>
wrote:

I'm not dead set on \rotate and suggested other names
previously in [1], but none of them seems decisively
superior.

Fwiw I like \rotate. It's pretty clear what it means and it sounds
similar to but not exactly the same as pivot.

rotate ~ sounds like transpose matrix, what is not true in this case.

Pavel

Show quoted text

--
greg

#9David G. Johnston
david.g.johnston@gmail.com
In reply to: Pavel Stehule (#8)
Re: [patch] Proposal for \rotate in psql

On Mon, Sep 7, 2015 at 4:18 PM, Pavel Stehule <pavel.stehule@gmail.com>
wrote:

2015-09-07 22:14 GMT+02:00 Greg Stark <stark@mit.edu>:

On Fri, Sep 4, 2015 at 5:08 PM, Daniel Verite <daniel@manitou-mail.org>
wrote:

I'm not dead set on \rotate and suggested other names
previously in [1], but none of them seems decisively
superior.

Fwiw I like \rotate. It's pretty clear what it means and it sounds
similar to but not exactly the same as pivot.

rotate ~ sounds like transpose matrix, what is not true in this case.

So? If PostgreSQL had any native matrix processing capabilities this would
maybe warrant a bit of consideration.

https://github.com/hadley/tidyr

\spread
\unfold
\rotate

Given the role that psql performs I do think \rotate to be the least
problematic choice; I concur that avoiding \pivot is desirable due to SQL's
usage.

David J.

#10Robert Haas
robertmhaas@gmail.com
In reply to: David G. Johnston (#9)
Re: [patch] Proposal for \rotate in psql

On Mon, Sep 7, 2015 at 5:08 PM, David G. Johnston
<david.g.johnston@gmail.com> wrote:

Given the role that psql performs I do think \rotate to be the least
problematic choice; I concur that avoiding \pivot is desirable due to SQL's
usage.

I can't agree. Rotating a matrix has a well-defined meaning, and this
does something that is not that.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11David G. Johnston
david.g.johnston@gmail.com
In reply to: Robert Haas (#10)
Re: [patch] Proposal for \rotate in psql

On Tue, Sep 8, 2015 at 1:38 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Sep 7, 2015 at 5:08 PM, David G. Johnston
<david.g.johnston@gmail.com> wrote:

Given the role that psql performs I do think \rotate to be the least
problematic choice; I concur that avoiding \pivot is desirable due to

SQL's

usage.

I can't agree. Rotating a matrix has a well-defined meaning, and this
does something that is not that.

​Even though the input data is a table and not a matrix? Do you have an
alternative choice you'd like to defend?

David J.

#12Robert Haas
robertmhaas@gmail.com
In reply to: David G. Johnston (#11)
Re: [patch] Proposal for \rotate in psql

On Tue, Sep 8, 2015 at 2:10 PM, David G. Johnston
<david.g.johnston@gmail.com> wrote:

On Tue, Sep 8, 2015 at 1:38 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Sep 7, 2015 at 5:08 PM, David G. Johnston
<david.g.johnston@gmail.com> wrote:

Given the role that psql performs I do think \rotate to be the least
problematic choice; I concur that avoiding \pivot is desirable due to
SQL's
usage.

I can't agree. Rotating a matrix has a well-defined meaning, and this
does something that is not that.

Even though the input data is a table and not a matrix?

Yes, I think rotating a table also has a pretty well-defined meaning.

Do you have an
alternative choice you'd like to defend?

Not particularly. If everybody picks one thing they like and argues
strenuously for it, we'll never get anywhere. I think it's enough to
say that I think this particular choice isn't the best. It's not as
if no other suggestions have been made.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#8)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

rotate ~ sounds like transpose matrix, what is not true in this case.

The various definitions that I can see, such as
http://dictionary.reference.com/browse/rotate
make no mention of matrices. It applies to anything that
moves around a pivot or axis.

OTOH, the established term for the matrix operation you're
referring to appears to be "transpose", as you mention.
https://en.wikipedia.org/wiki/Transpose

I notice that according to
http://www.thesaurus.com/browse/transpose
"rotate" is not present in the 25+ synonyms they suggest for
"transpose".

In the above wikipedia article about matrix transposition,
"rotate" is also never used anywhere.

"rotate matrix" does not exist for google ngrams, whereas
"transpose matrix" does.
https://books.google.com/ngrams

Overall I don't see the evidence that "rotate" alone would
suggest transposing a matrix.

Now it appears that there is a concept in linear algebra named
"rotation matrix", defined as:
https://en.wikipedia.org/wiki/Rotation_matrix
that seems quite relevant for 3D software.

But as psql is not a tool for linear algebra or 3D in the first place,
who could realistically be deceived?

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#14Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#13)
Re: [patch] Proposal for \rotate in psql

2015-09-08 22:55 GMT+02:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

rotate ~ sounds like transpose matrix, what is not true in this case.

for me the relation rotation is exactly what \x does

Show quoted text

The various definitions that I can see, such as
http://dictionary.reference.com/browse/rotate
make no mention of matrices. It applies to anything that
moves around a pivot or axis.

OTOH, the established term for the matrix operation you're
referring to appears to be "transpose", as you mention.
https://en.wikipedia.org/wiki/Transpose

I notice that according to
http://www.thesaurus.com/browse/transpose
"rotate" is not present in the 25+ synonyms they suggest for
"transpose".

In the above wikipedia article about matrix transposition,
"rotate" is also never used anywhere.

"rotate matrix" does not exist for google ngrams, whereas
"transpose matrix" does.
https://books.google.com/ngrams

Overall I don't see the evidence that "rotate" alone would
suggest transposing a matrix.

Now it appears that there is a concept in linear algebra named
"rotation matrix", defined as:
https://en.wikipedia.org/wiki/Rotation_matrix
that seems quite relevant for 3D software.

But as psql is not a tool for linear algebra or 3D in the first place,
who could realistically be deceived?

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#15Robert Haas
robertmhaas@gmail.com
In reply to: Pavel Stehule (#14)
Re: [patch] Proposal for \rotate in psql

On Tue, Sep 8, 2015 at 4:58 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:

2015-09-08 22:55 GMT+02:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

rotate ~ sounds like transpose matrix, what is not true in this case.

for me the relation rotation is exactly what \x does

\x doesn't exactly rotate it either. \x puts the column headers down
the side instead of across the top, but it doesn't put the rows across
the top instead of down the side. Rather, each row is listed in a
separate chunk. This feature is doing something else again. I've
actually never seen this particular transformation anywhere except for
Microsoft Excel's pivot tables, which I still find confusing.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#16Pavel Stehule
pavel.stehule@gmail.com
In reply to: Robert Haas (#15)
Re: [patch] Proposal for \rotate in psql

\x doesn't exactly rotate it either. \x puts the column headers down
the side instead of across the top, but it doesn't put the rows across
the top instead of down the side. Rather, each row is listed in a
separate chunk.

true, it is rotation per one row. I was wrong.

Show quoted text

This feature is doing something else again. I've
actually never seen this particular transformation anywhere except for
Microsoft Excel's pivot tables, which I still find confusing.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#17Daniel Verite
daniel@manitou-mail.org
In reply to: Daniel Verite (#1)
1 attachment(s)
Re: [patch] Proposal for \rotate in psql

Hi,

This is the 2nd iteration of this patch, for comments and review.

Changes:

- the arguments can be column names (rather than only numbers).

- the horizontal headers are sorted server-side according to their original
type. DESC order is possible by prefixing the column arg with a minus sign.

- the command is now modelled after \g so it can be used
in place of \g

- the title is no longer set by the command, it was getting in the
way when outputting to data file.

- there's a hard limit on 1600 columns. This is to fail early and clean
on large resultsets that are not amenable to being rotated.

- includes SGML user doc.

As I don't have plans for further improvements, I'll submit this one
to the open commitfest.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-rotate-v2.difftext/x-patch; name=psql-rotate-v2.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f5c9552..0ed2e3d 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2445,6 +2445,96 @@ lo_import 152801
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><literal>\rotate [ <replaceable class="parameter">colV</replaceable>  [-]<replaceable class="parameter">colH</replaceable>] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows the results
+        inside a crosstab grid.  The contents at
+        output column <replaceable class="parameter">colV</replaceable> are transformed into a
+        vertical header and the contents at
+        output column <replaceable class="parameter">colH</replaceable> into a
+        horizontal header. The results for the other output columns are projected inside the grid.
+
+        </para>
+        <para>
+        <replaceable class="parameter">colV</replaceable>
+        and <replaceable class="parameter">colH</replaceable> can indicate a
+        column position (starting at 1), or a column name. Normal case folding
+        and quoting rules apply on column names. By default,
+        <replaceable class="parameter">colV</replaceable> designates column 1
+        and <replaceable class="parameter">colH</replaceable> column 2.
+        A query having less than two output columns cannot be rotated, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+
+        </para>
+        <para>
+        The vertical header, displayed as the leftmost column in the output,
+        contains the set of all distinct values found in
+        column <replaceable class="parameter">colV</replaceable>, in the order
+        of their first appearance in the results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row in the output,
+        contains the set of all distinct non-null values found in
+        column <replaceable class="parameter">colH</replaceable>.  It is
+        sorted in ascending order of values, unless a minus (-) sign
+        precedes <replaceable class="parameter">colH</replaceable>, in which
+        case this order is reversed.
+        </para>
+
+        <para>
+        The query results being tuples of <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        a cell is output at the intersection <literal>(x,y)</literal> in the grid,
+        and its contents are determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the results such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed in the cell as
+         if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to one row
+         except that the values coming from different rows are stacked
+         vertically, rows being separated by newline characters inside
+         the same cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
 
       <varlistentry>
         <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index f1336d5..92f9f44 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o rotate.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 8156b76..3550d6c 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -46,6 +46,7 @@
 #include "mainloop.h"
 #include "print.h"
 #include "psqlscan.h"
+#include "rotate.h"
 #include "settings.h"
 #include "variables.h"
 
@@ -1083,6 +1084,34 @@ exec_command(const char *cmd,
 		free(pw2);
 	}
 
+	/* \rotate -- execute a query and show results rotated along axis */
+	else if (strcmp(cmd, "rotate") == 0)
+	{
+		char	*opt1,
+				*opt2;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.rotate_col_V = opt1 ? pg_strdup(opt1): NULL;
+			pset.rotate_col_H = opt2 ? pg_strdup(opt2): NULL;
+			pset.rotate_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+	}
+
 	/* \prompt -- prompt and set variable */
 	else if (strcmp(cmd, "prompt") == 0)
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 0e266a3..032db38 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -23,6 +23,7 @@
 #include "command.h"
 #include "copy.h"
 #include "mbprint.h"
+#include "rotate.h"
 
 
 
@@ -1061,7 +1062,12 @@ SendQuery(const char *query)
 
 		/* but printing results isn't: */
 		if (OK && results)
-			OK = PrintQueryResults(results);
+		{
+			if (pset.rotate_output)
+				OK = PrintRotate(results, pset.rotate_col_V, pset.rotate_col_H);
+			else
+				OK = PrintQueryResults(results);
+		}
 	}
 	else
 	{
@@ -1177,6 +1183,17 @@ sendquery_cleanup:
 		pset.gset_prefix = NULL;
 	}
 
+	pset.rotate_output = false;
+	if (pset.rotate_col_V)
+	{
+		free(pset.rotate_col_V);
+		pset.rotate_col_V = NULL;
+	}
+	if (pset.rotate_col_H)
+	{
+		free(pset.rotate_col_H);
+		pset.rotate_col_H = NULL;
+	}
 	return OK;
 }
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 5b63e76..3dde055 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -175,6 +175,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\rotate [COLV COLH]    execute query and rotate results into crosstab view\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/rotate.c b/src/bin/psql/rotate.c
new file mode 100644
index 0000000..7dcc3e9
--- /dev/null
+++ b/src/bin/psql/rotate.c
@@ -0,0 +1,566 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/rotate.c
+ */
+
+#include "common.h"
+#include "pqexpbuffer.h"
+#include "rotate.h"
+#include "settings.h"
+
+#include <string.h>
+
+static int
+headerCompare(const void *a, const void *b)
+{
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+static void
+accumHeader(char* name, int* count, struct pivot_field **sorted_tab, int row_number)
+{
+	struct pivot_field *p;
+
+	/*
+	 * Search for name in sorted_tab. If it doesn't exist, insert it,
+	 * otherwise do nothing.
+	 */
+
+	if (*count >= 1)
+	{
+		p = (struct pivot_field*) bsearch(&name,
+										  *sorted_tab,
+										  *count,
+										  sizeof(struct pivot_field),
+										  headerCompare);
+	}
+	else
+		p=NULL;
+
+	if (!p)
+	{
+		*sorted_tab = pg_realloc(*sorted_tab, sizeof(struct pivot_field) * (1+*count));
+		(*sorted_tab)[*count].name = name;
+		(*sorted_tab)[*count].rank = *count;
+		(*count)++;
+
+		qsort(*sorted_tab,
+			  *count,
+			  sizeof(struct pivot_field),
+			  headerCompare);
+	}
+}
+
+/*
+ * Send a query to sort all column values cast to the Oid passed in a VALUES clause
+ */
+static bool
+sortColumns(Oid coltype, struct pivot_field *columns, int nb_cols, int direction)
+{
+	bool retval = false;
+	PGresult *res = NULL;
+	PQExpBufferData query;
+	int i;
+	Oid *param_types;
+	const char** param_values;
+	int* param_lengths;
+	int* param_formats;
+
+	if (nb_cols < 2)
+		return true;					/* nothing to sort */
+
+	param_types = (Oid*) pg_malloc(nb_cols*sizeof(Oid));
+	param_values = (const char**) pg_malloc(nb_cols*sizeof(char*));
+	param_lengths = (int*) pg_malloc(nb_cols*sizeof(int));
+	param_formats = (int*) pg_malloc(nb_cols*sizeof(int));
+
+	initPQExpBuffer(&query);
+
+	/*
+	 * The query returns the original position of each value in our list,
+	 * ordered by its new position. The value itself is not returned.
+	 */
+	appendPQExpBuffer(&query, "SELECT n FROM (VALUES");
+
+	for (i=1; i <= nb_cols; i++)
+	{
+		if (i < nb_cols)
+			appendPQExpBuffer(&query, "($%d,%d),", i, i);
+		else
+		{
+			appendPQExpBuffer(&query, "($%d,%d)) AS l(x,n) ORDER BY x", i, i);
+			if (direction < 0)
+				appendPQExpBuffer(&query, " DESC");
+		}
+
+		param_types[i-1] = coltype;
+		param_values[i-1] = columns[i-1].name;
+		param_lengths[i-1] = strlen(columns[i-1].name);
+		param_formats[i-1] = 0;
+	}
+
+	res = PQexecParams(pset.db,
+					   query.data,
+					   nb_cols,
+					   param_types,
+					   param_values,
+					   param_lengths,
+					   param_formats,
+					   0);
+
+	if (res)
+	{
+		ExecStatusType status = PQresultStatus(res);
+		if (status == PGRES_TUPLES_OK)
+		{
+			for (i=0; i < PQntuples(res); i++)
+			{
+				int old_pos = atoi(PQgetvalue(res, i, 0));
+
+				if (old_pos < 1 || old_pos > nb_cols || i >= nb_cols)
+				{
+					/*
+					 * A position outside of the range is normally impossible.
+					 * If this happens, we're facing a malfunctioning or hostile
+					 * server or middleware.
+					 */
+					psql_error(_("Unexpected value when sorting horizontal headers"));
+					goto cleanup;
+				}
+				else
+				{
+					columns[old_pos-1].rank = i;
+				}
+			}
+		}
+		else
+		{
+			psql_error(_("Query error when sorting horizontal headers: %s"),
+					   PQerrorMessage(pset.db));
+			goto cleanup;
+		}
+	}
+
+	retval = true;
+
+cleanup:
+	termPQExpBuffer(&query);
+	if (res)
+		PQclear(res);
+	pg_free(param_types);
+	pg_free(param_values);
+	pg_free(param_lengths);
+	pg_free(param_formats);
+	return retval;
+}
+
+static void
+printRotation(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, rn;
+	int* horiz_map;			 /* map indices from sorted horizontal headers to piv_columns */
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the rotation */
+	printTableAddHeader(&cont, PQfname(results, 0), false, 'l');
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+	 *  associating each piv_columns[].rank to its index in piv_columns.
+	 *  This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+	for (i = 0; i < num_columns; i++)
+	{
+		printTableAddHeader(&cont, piv_columns[horiz_map[i]].name, false, 'l');
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		char* row_name;
+		char* col_name;
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		row_number = col_number = -1;
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+		{
+			row_name = PQgetvalue(results, rn, field_for_rows);
+			p = (struct pivot_field*) bsearch(&row_name,
+											  piv_rows,
+											  num_rows,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				row_number = p->rank;
+		}
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+		{
+			col_name = PQgetvalue(results, rn, field_for_columns);
+			p = (struct pivot_field*) bsearch(&col_name,
+											  piv_columns,
+											  num_columns,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				col_number = p->rank;
+		}
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+			int k = 0;
+
+			do {
+				char *content;
+
+				if (PQnfields(results) == 2)
+				{
+					/*
+					  special case: when the source has only 2 columns, use a
+					  X (cross/checkmark) for the cell content, and set
+					  src_col to a virtual additional column.
+					*/
+					content = "X";
+					src_col = 3;
+				}
+				else if (src_col == field_for_rows || src_col == field_for_columns)
+				{
+					/*
+					  The source values that produce headers are not processed
+					  in this loop, only the values that end up inside the grid.
+					*/
+					src_col++;
+					continue;
+				}
+				else
+				{
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (k==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+				k++;
+				src_col++;
+			} while (src_col < PQnfields(results));
+		}
+	}
+
+	printTable(&cont, pset.queryFout, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled 
+ */
+static int
+fieldnameCmp(const char* arg, const char* fieldname)
+{
+	const unsigned char* p = (const unsigned char*) arg;
+	const unsigned char* f = (const unsigned char*) fieldname;
+	unsigned char c;
+
+	if (*p++ != '"')
+		return pg_strcasecmp(arg, fieldname);
+
+	while ((c=*p++))
+	{
+		if (c=='"')
+		{
+			if (*p=='"')
+				p++;			/* skip second quote and continue */
+			else if (*p=='\0')
+			    return *p-*f;		/* p finishes before f or is identical */
+
+		}
+		if (*f=='\0')
+			return 1;			/* f finishes before p */
+		if (c!=*f)
+			return c-*f;
+		f++;
+	}
+	return (*f=='\0') ? 0 : 1;
+}
+
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *  on success, the 0-based index of the column
+ *  or -1 if the column number or name cannot correspond to the PGresult,
+ *        or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char* arg, PGresult* res)
+{
+	int idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0  || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int i;
+		idx = -1;
+		for (i=0; i < PQnfields(res); i++)
+		{
+			if (fieldnameCmp(arg, PQfname(res, i)) == 0)
+			{
+				if (idx>=0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+bool
+PrintRotate(PGresult* res,
+			const char* opt_field_for_rows,    /* COLV or null */
+			const char* opt_field_for_columns) /* [-]COLH or null */
+{
+	int		rn;
+	struct pivot_field	*piv_columns = NULL;
+	struct pivot_field	*piv_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	retval = false;
+	int		columns_sort_direction = 1; /* 1:ascending, -1:descending */
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows;
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		goto error_return;
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("A query to rotate must return at least two columns\n"));
+		goto error_return;
+	}
+
+	field_for_rows = (opt_field_for_rows != NULL)
+		? indexOfColumn(opt_field_for_rows, res)
+		: 0;
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		/*
+		 * descending sort is requested if the column reference is
+		 * preceded with a minus sign
+		 */
+		if (*opt_field_for_columns == '-')
+		{
+			columns_sort_direction = -1;
+			opt_field_for_columns++;
+		}
+		field_for_columns = indexOfColumn(opt_field_for_columns, res);
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * First pass: accumulate row names and column names, each into their
+	 * array. Use client-side sort but only to build the set of DISTINCT
+	 * values. The final order displayed depends only on server-side
+	 * sorts.
+	 */
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		if (!PQgetisnull(res, rn, field_for_rows))
+		{
+			accumHeader(PQgetvalue(res, rn, field_for_rows),
+						&num_rows,
+						&piv_rows,
+						rn);
+		}
+
+		if (!PQgetisnull(res, rn, field_for_columns))
+		{
+			accumHeader(PQgetvalue(res, rn, field_for_columns),
+						&num_columns,
+						&piv_columns,
+						rn);
+			if (num_columns > 1600)
+			{
+				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+				goto error_return;
+			}
+		}
+	}
+
+	/*
+	 * Second pass: sort the list of target columns on the server.
+	 */
+	if (!sortColumns(PQftype(res, field_for_columns),
+					 piv_columns,
+					 num_columns,
+					 columns_sort_direction))
+		goto error_return;
+
+	/*
+	 * Third pass: print the rotated results.
+	 */
+	printRotation(res,
+				  num_columns,
+				  piv_columns,
+				  field_for_columns,
+				  num_rows,
+				  piv_rows,
+				  field_for_rows);
+
+	retval = true;
+
+error_return:
+	pg_free(piv_columns);
+	pg_free(piv_rows);
+
+	return retval;
+}
diff --git a/src/bin/psql/rotate.h b/src/bin/psql/rotate.h
new file mode 100644
index 0000000..ba027b6
--- /dev/null
+++ b/src/bin/psql/rotate.h
@@ -0,0 +1,33 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/rotate.h
+ */
+
+#ifndef ROTATE_H
+#define ROTATE_H
+
+struct pivot_field
+{
+	/* Pointer obtained from PGgetvalue() for colV or colH */
+	char*	name;
+
+	/* Rank of the field in its list, starting at 0.
+	 * - For headers stacked vertically, rank=N means it's the
+	 *   Nth distinct field encountered when looping through rows
+	 *   in their initial order.
+	 * - For headers stacked horizontally, rank is obtained
+	 *   by server-side sorting in sortColumns()
+	 */
+	int		rank;
+};
+
+/* prototypes */
+extern bool
+PrintRotate(PGresult* res,
+			const char* opt_field_for_rows,
+			const char* opt_field_for_columns);
+
+#endif   /* ROTATE_H */
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 1885bb1..2164928 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -90,6 +90,9 @@ typedef struct _psqlSettings
 
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	bool		rotate_output;  /* one-shot request to print rotated results */
+	char		*rotate_col_V;  /* one-shot \rotate 1st argument */
+	char		*rotate_col_H;  /* one-shot \rotate 2nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
#18Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#13)
Re: [patch] Proposal for \rotate in psql

2015-09-08 22:55 GMT+02:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

rotate ~ sounds like transpose matrix, what is not true in this case.

The various definitions that I can see, such as
http://dictionary.reference.com/browse/rotate
make no mention of matrices. It applies to anything that
moves around a pivot or axis.

OTOH, the established term for the matrix operation you're
referring to appears to be "transpose", as you mention.
https://en.wikipedia.org/wiki/Transpose

I notice that according to
http://www.thesaurus.com/browse/transpose
"rotate" is not present in the 25+ synonyms they suggest for
"transpose".

In the above wikipedia article about matrix transposition,
"rotate" is also never used anywhere.

"rotate matrix" does not exist for google ngrams, whereas
"transpose matrix" does.
https://books.google.com/ngrams

Overall I don't see the evidence that "rotate" alone would
suggest transposing a matrix.

Now it appears that there is a concept in linear algebra named
"rotation matrix", defined as:
https://en.wikipedia.org/wiki/Rotation_matrix
that seems quite relevant for 3D software.

But as psql is not a tool for linear algebra or 3D in the first place,
who could realistically be deceived?

in the help inside your last patch, you are using "crosstab". Cannto be
crosstab the name for this feature?

Regards

Pavel

#19Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#17)
Re: [patch] Proposal for \rotate in psql

Hi

2015-09-16 11:35 GMT+02:00 Daniel Verite <daniel@manitou-mail.org>:

Hi,

This is the 2nd iteration of this patch, for comments and review.

my comments:

1. I don't understand why you are use two methods for sorting columns
(qsort, and query with ORDER BY)

2. Data column are not well aligned - numbers are aligned as text

3. When data are multiattribute - then merging together with space
separator is not practical

* important information is lost
* same transformation can be done as expression, so this feature is
useless

Is possible to use one cell per attribute (don't do merge)?

DATA QUERY: SELECT dim1, dim2, sum(x), avg(x) FROM .. GROUP BY dim1, dim2

and result header of rotate can be

DIM1 | dim2_val1/sum | dim2_val1/avg | dim2_val2/sum | dim2_val2/avg | ...

#20Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#19)
Re: [patch] Proposal for \rotate in psql

3. When data are multiattribute - then merging together with space

separator is not practical

* important information is lost
* same transformation can be done as expression, so this feature is
useless

Is possible to use one cell per attribute (don't do merge)?

DATA QUERY: SELECT dim1, dim2, sum(x), avg(x) FROM .. GROUP BY dim1, dim2

and result header of rotate can be

DIM1 | dim2_val1/sum | dim2_val1/avg | dim2_val2/sum | dim2_val2/avg |
...

Last point can wait - we don't need to show pivot table with all details
perfectly in first step.

The main issue of this patch is name - "rotate" is really pretty strange
for me. Please, change it :) - crosstab is much better

Regards

Pavel

#21Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#18)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

in the help inside your last patch, you are using "crosstab". Cannto be
crosstab the name for this feature?

If it wasn't taken already by contrib/tablefunc, that would be a first
choice. But now, when searching for crosstab+postgresql, pages of
results come out concerning the crosstab() function.

So not using \crosstab is deliberate; it's to prevent confusion with
the server-side function.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#21)
Re: [patch] Proposal for \rotate in psql

2015-09-18 13:36 GMT+02:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

in the help inside your last patch, you are using "crosstab". Cannto be
crosstab the name for this feature?

If it wasn't taken already by contrib/tablefunc, that would be a first
choice. But now, when searching for crosstab+postgresql, pages of
results come out concerning the crosstab() function.

So not using \crosstab is deliberate; it's to prevent confusion with
the server-side function.

I don't afraid about this - crosstab is a function in extension. Psql
backslash commands living in different worlds. When we introduce new
command, then google will adapt on it.

For this use case the correct keywords are "crosstab psql postgres"

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#23Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#19)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

my comments:

1. I don't understand why you are use two methods for sorting columns
(qsort, and query with ORDER BY)

qsort (with strcmp as the comparator) is only used to determine the
set of distinct values for the vertical and horizontal headers.
In fact this is just to allow the use of bsearch() when doing that
instead of a slower sequential search.

Once the values for the horizontal headers are known, they
are passed to the server for sorting with server-side semantics
according to their type. The order will differ from qsort/strcmp
found as the comparison semantics are different.

The values for the vertical header are not sorted server-side
because we keep the order of the query for displaying
top to bottom.
In the typical use case , it will have ORDER BY 1[,2] possibly
with one/two DESC qualifiers.

2. Data column are not well aligned - numbers are aligned as text

Yes. I'll look shortly into fixing that.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#24Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#23)
Re: [patch] Proposal for \rotate in psql

2015-09-18 13:59 GMT+02:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

my comments:

1. I don't understand why you are use two methods for sorting columns
(qsort, and query with ORDER BY)

qsort (with strcmp as the comparator) is only used to determine the
set of distinct values for the vertical and horizontal headers.
In fact this is just to allow the use of bsearch() when doing that
instead of a slower sequential search.

Once the values for the horizontal headers are known, they
are passed to the server for sorting with server-side semantics
according to their type. The order will differ from qsort/strcmp
found as the comparison semantics are different.

The values for the vertical header are not sorted server-side
because we keep the order of the query for displaying
top to bottom.
In the typical use case , it will have ORDER BY 1[,2] possibly
with one/two DESC qualifiers.

ok

Show quoted text

2. Data column are not well aligned - numbers are aligned as text

Yes. I'll look shortly into fixing that.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#25Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#19)
1 attachment(s)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

2. Data column are not well aligned - numbers are aligned as text

Thanks for spotting that, it's fixed in the attached new iteration of
the patch.

3. When data are multiattribute - then merging together with space separator
is not practical

* important information is lost
* same transformation can be done as expression, so this feature is
useless

The primary use case is for queries with 3 output columns
(A,B,C) where A and B are dimensions and C is uniquely
determined by (A,B).

How columns 4 and above get displayed is not essential to the
feature, as it's somehow a degenerate case. As you note, it
could be avoided by the user limiting the query to 3 columns,
and providing whatever expression fits for the 3rd column.

Still if the query has > 3 columns, it has to be dealt with.
The choices I've considered:

a- Just error out.

b- Force the user to specify which single column should be taken
as the value. That would be an additional argument to the command,
or the fixed 3rd column in the invocation without arg.

c- Stack the values horizontally in the same cell with a separator.
As the query implies f(A,B)=(C,D,E) we display C D E in the cell
at coordinates (A,B). It's what it does currently.

[a] is not very user friendly.
[b] seems acceptable. It discards columns but the user decides which.
[c] is meant as a best effort at not discarding anything.

When [c] gives poor results, the next step from my point of view
would be for the user to rework the query, just like in the general case
when a query is not satisfying.

You're suggesting a [d] choice, subdividing the horizontal headers.
It seems to me like a pretty radical change, multiplying the number
of columns, and it has also the potential to give poor results visually.
Let's see if more feedback comes.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-rotate-v3.difftext/x-patch; name=psql-rotate-v3.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f5c9552..0ed2e3d 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2445,6 +2445,96 @@ lo_import 152801
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><literal>\rotate [ <replaceable class="parameter">colV</replaceable>  [-]<replaceable class="parameter">colH</replaceable>] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows the results
+        inside a crosstab grid.  The contents at
+        output column <replaceable class="parameter">colV</replaceable> are transformed into a
+        vertical header and the contents at
+        output column <replaceable class="parameter">colH</replaceable> into a
+        horizontal header. The results for the other output columns are projected inside the grid.
+
+        </para>
+        <para>
+        <replaceable class="parameter">colV</replaceable>
+        and <replaceable class="parameter">colH</replaceable> can indicate a
+        column position (starting at 1), or a column name. Normal case folding
+        and quoting rules apply on column names. By default,
+        <replaceable class="parameter">colV</replaceable> designates column 1
+        and <replaceable class="parameter">colH</replaceable> column 2.
+        A query having less than two output columns cannot be rotated, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+
+        </para>
+        <para>
+        The vertical header, displayed as the leftmost column in the output,
+        contains the set of all distinct values found in
+        column <replaceable class="parameter">colV</replaceable>, in the order
+        of their first appearance in the results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row in the output,
+        contains the set of all distinct non-null values found in
+        column <replaceable class="parameter">colH</replaceable>.  It is
+        sorted in ascending order of values, unless a minus (-) sign
+        precedes <replaceable class="parameter">colH</replaceable>, in which
+        case this order is reversed.
+        </para>
+
+        <para>
+        The query results being tuples of <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        a cell is output at the intersection <literal>(x,y)</literal> in the grid,
+        and its contents are determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the results such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed in the cell as
+         if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to one row
+         except that the values coming from different rows are stacked
+         vertically, rows being separated by newline characters inside
+         the same cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
 
       <varlistentry>
         <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index f1336d5..92f9f44 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o rotate.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 8156b76..3550d6c 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -46,6 +46,7 @@
 #include "mainloop.h"
 #include "print.h"
 #include "psqlscan.h"
+#include "rotate.h"
 #include "settings.h"
 #include "variables.h"
 
@@ -1083,6 +1084,34 @@ exec_command(const char *cmd,
 		free(pw2);
 	}
 
+	/* \rotate -- execute a query and show results rotated along axis */
+	else if (strcmp(cmd, "rotate") == 0)
+	{
+		char	*opt1,
+				*opt2;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.rotate_col_V = opt1 ? pg_strdup(opt1): NULL;
+			pset.rotate_col_H = opt2 ? pg_strdup(opt2): NULL;
+			pset.rotate_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+	}
+
 	/* \prompt -- prompt and set variable */
 	else if (strcmp(cmd, "prompt") == 0)
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 0e266a3..032db38 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -23,6 +23,7 @@
 #include "command.h"
 #include "copy.h"
 #include "mbprint.h"
+#include "rotate.h"
 
 
 
@@ -1061,7 +1062,12 @@ SendQuery(const char *query)
 
 		/* but printing results isn't: */
 		if (OK && results)
-			OK = PrintQueryResults(results);
+		{
+			if (pset.rotate_output)
+				OK = PrintRotate(results, pset.rotate_col_V, pset.rotate_col_H);
+			else
+				OK = PrintQueryResults(results);
+		}
 	}
 	else
 	{
@@ -1177,6 +1183,17 @@ sendquery_cleanup:
 		pset.gset_prefix = NULL;
 	}
 
+	pset.rotate_output = false;
+	if (pset.rotate_col_V)
+	{
+		free(pset.rotate_col_V);
+		pset.rotate_col_V = NULL;
+	}
+	if (pset.rotate_col_H)
+	{
+		free(pset.rotate_col_H);
+		pset.rotate_col_H = NULL;
+	}
 	return OK;
 }
 
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 5b63e76..3dde055 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -175,6 +175,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\rotate [COLV COLH]    execute query and rotate results into crosstab view\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
index cab9e6e..0691723 100644
--- a/src/bin/psql/print.c
+++ b/src/bin/psql/print.c
@@ -3164,30 +3164,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt, FILE *fout, FILE *f
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3229,6 +3208,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt, FILE *fout, FILE *f
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
index b0b6bf5..db6dfb5 100644
--- a/src/bin/psql/print.h
+++ b/src/bin/psql/print.h
@@ -171,7 +171,7 @@ extern FILE *PageOutput(int lines, const printTableOpt *topt);
 extern void ClosePager(FILE *pagerpipe);
 
 extern void html_escaped_print(const char *in, FILE *fout);
-
+extern char column_type_alignment(Oid);
 extern void printTableInit(printTableContent *const content,
 			   const printTableOpt *opt, const char *title,
 			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/rotate.c b/src/bin/psql/rotate.c
new file mode 100644
index 0000000..540252f
--- /dev/null
+++ b/src/bin/psql/rotate.c
@@ -0,0 +1,603 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/rotate.c
+ */
+
+#include "common.h"
+#include "pqexpbuffer.h"
+#include "rotate.h"
+#include "settings.h"
+
+#include <string.h>
+
+static int
+headerCompare(const void *a, const void *b)
+{
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+static void
+accumHeader(char* name, int* count, struct pivot_field **sorted_tab, int row_number)
+{
+	struct pivot_field *p;
+
+	/*
+	 * Search for name in sorted_tab. If it doesn't exist, insert it,
+	 * otherwise do nothing.
+	 */
+
+	if (*count >= 1)
+	{
+		p = (struct pivot_field*) bsearch(&name,
+										  *sorted_tab,
+										  *count,
+										  sizeof(struct pivot_field),
+										  headerCompare);
+	}
+	else
+		p=NULL;
+
+	if (!p)
+	{
+		*sorted_tab = pg_realloc(*sorted_tab, sizeof(struct pivot_field) * (1+*count));
+		(*sorted_tab)[*count].name = name;
+		(*sorted_tab)[*count].rank = *count;
+		(*count)++;
+
+		qsort(*sorted_tab,
+			  *count,
+			  sizeof(struct pivot_field),
+			  headerCompare);
+	}
+}
+
+/*
+ * Send a query to sort all column values cast to the Oid passed in a VALUES clause
+ */
+static bool
+sortColumns(Oid coltype, struct pivot_field *columns, int nb_cols, int direction)
+{
+	bool retval = false;
+	PGresult *res = NULL;
+	PQExpBufferData query;
+	int i;
+	Oid *param_types;
+	const char** param_values;
+	int* param_lengths;
+	int* param_formats;
+
+	if (nb_cols < 2)
+		return true;					/* nothing to sort */
+
+	param_types = (Oid*) pg_malloc(nb_cols*sizeof(Oid));
+	param_values = (const char**) pg_malloc(nb_cols*sizeof(char*));
+	param_lengths = (int*) pg_malloc(nb_cols*sizeof(int));
+	param_formats = (int*) pg_malloc(nb_cols*sizeof(int));
+
+	initPQExpBuffer(&query);
+
+	/*
+	 * The query returns the original position of each value in our list,
+	 * ordered by its new position. The value itself is not returned.
+	 */
+	appendPQExpBuffer(&query, "SELECT n FROM (VALUES");
+
+	for (i=1; i <= nb_cols; i++)
+	{
+		if (i < nb_cols)
+			appendPQExpBuffer(&query, "($%d,%d),", i, i);
+		else
+		{
+			appendPQExpBuffer(&query, "($%d,%d)) AS l(x,n) ORDER BY x", i, i);
+			if (direction < 0)
+				appendPQExpBuffer(&query, " DESC");
+		}
+
+		param_types[i-1] = coltype;
+		param_values[i-1] = columns[i-1].name;
+		param_lengths[i-1] = strlen(columns[i-1].name);
+		param_formats[i-1] = 0;
+	}
+
+	res = PQexecParams(pset.db,
+					   query.data,
+					   nb_cols,
+					   param_types,
+					   param_values,
+					   param_lengths,
+					   param_formats,
+					   0);
+
+	if (res)
+	{
+		ExecStatusType status = PQresultStatus(res);
+		if (status == PGRES_TUPLES_OK)
+		{
+			for (i=0; i < PQntuples(res); i++)
+			{
+				int old_pos = atoi(PQgetvalue(res, i, 0));
+
+				if (old_pos < 1 || old_pos > nb_cols || i >= nb_cols)
+				{
+					/*
+					 * A position outside of the range is normally impossible.
+					 * If this happens, we're facing a malfunctioning or hostile
+					 * server or middleware.
+					 */
+					psql_error(_("Unexpected value when sorting horizontal headers"));
+					goto cleanup;
+				}
+				else
+				{
+					columns[old_pos-1].rank = i;
+				}
+			}
+		}
+		else
+		{
+			psql_error(_("Query error when sorting horizontal headers: %s"),
+					   PQerrorMessage(pset.db));
+			goto cleanup;
+		}
+	}
+
+	retval = true;
+
+cleanup:
+	termPQExpBuffer(&query);
+	if (res)
+		PQclear(res);
+	pg_free(param_types);
+	pg_free(param_values);
+	pg_free(param_lengths);
+	pg_free(param_formats);
+	return retval;
+}
+
+static void
+printRotation(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, rn;
+	char col_align = 'l';		/* alignment for values inside the grid */
+	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the rotation */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+						column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+	 *  associating each piv_columns[].rank to its index in piv_columns.
+	 *  This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+	/*
+	 * In the case of 3 output columns, the contents in the cells are exactly
+	 * the contents of the "value" column (3rd column by default), so their
+	 * alignment is determined by PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (PQnfields(results) == 3)
+	{
+		int colnum;				/* column placed inside the grid */
+		/*
+		 * find colnum in the permutations of (0,1,2) where colnum is
+		 * neither field_for_rows nor field_for_columns
+		 */
+		switch (field_for_rows)
+		{
+		case 0:
+			colnum = (field_for_columns == 1) ? 2 : 1;
+			break;
+		case 1:
+			colnum = (field_for_columns == 0) ? 2: 0;
+			break;
+		default:				/* should be always 2 */
+			colnum = (field_for_columns == 0) ? 1: 0;
+			break;
+		}
+		col_align = column_type_alignment(PQftype(results, colnum));
+	}
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		printTableAddHeader(&cont,
+							piv_columns[horiz_map[i]].name,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		char* row_name;
+		char* col_name;
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		row_number = col_number = -1;
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+		{
+			row_name = PQgetvalue(results, rn, field_for_rows);
+			p = (struct pivot_field*) bsearch(&row_name,
+											  piv_rows,
+											  num_rows,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				row_number = p->rank;
+		}
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+		{
+			col_name = PQgetvalue(results, rn, field_for_columns);
+			p = (struct pivot_field*) bsearch(&col_name,
+											  piv_columns,
+											  num_columns,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				col_number = p->rank;
+		}
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+			int k = 0;
+
+			do {
+				char *content;
+
+				if (PQnfields(results) == 2)
+				{
+					/*
+					  special case: when the source has only 2 columns, use a
+					  X (cross/checkmark) for the cell content, and set
+					  src_col to a virtual additional column.
+					*/
+					content = "X";
+					src_col = 3;
+				}
+				else if (src_col == field_for_rows || src_col == field_for_columns)
+				{
+					/*
+					  The source values that produce headers are not processed
+					  in this loop, only the values that end up inside the grid.
+					*/
+					src_col++;
+					continue;
+				}
+				else
+				{
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (k==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+				k++;
+				src_col++;
+			} while (src_col < PQnfields(results));
+		}
+	}
+
+	printTable(&cont, pset.queryFout, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static int
+fieldnameCmp(const char* arg, const char* fieldname)
+{
+	const unsigned char* p = (const unsigned char*) arg;
+	const unsigned char* f = (const unsigned char*) fieldname;
+	unsigned char c;
+
+	if (*p++ != '"')
+		return pg_strcasecmp(arg, fieldname);
+
+	while ((c=*p++))
+	{
+		if (c=='"')
+		{
+			if (*p=='"')
+				p++;			/* skip second quote and continue */
+			else if (*p=='\0')
+				return *p-*f;		/* p finishes before f or is identical */
+
+		}
+		if (*f=='\0')
+			return 1;			/* f finishes before p */
+		if (c!=*f)
+			return c-*f;
+		f++;
+	}
+	return (*f=='\0') ? 0 : 1;
+}
+
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *  on success, the 0-based index of the column
+ *  or -1 if the column number or name cannot correspond to the PGresult,
+ *        or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char* arg, PGresult* res)
+{
+	int idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0  || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int i;
+		idx = -1;
+		for (i=0; i < PQnfields(res); i++)
+		{
+			if (fieldnameCmp(arg, PQfname(res, i)) == 0)
+			{
+				if (idx>=0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+bool
+PrintRotate(PGresult* res,
+			const char* opt_field_for_rows,    /* COLV or null */
+			const char* opt_field_for_columns) /* [-]COLH or null */
+{
+	int		rn;
+	struct pivot_field	*piv_columns = NULL;
+	struct pivot_field	*piv_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	retval = false;
+	int		columns_sort_direction = 1; /* 1:ascending, -1:descending */
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows;
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		goto error_return;
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("A query to rotate must return at least two columns\n"));
+		goto error_return;
+	}
+
+	field_for_rows = (opt_field_for_rows != NULL)
+		? indexOfColumn(opt_field_for_rows, res)
+		: 0;
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		/*
+		 * descending sort is requested if the column reference is
+		 * preceded with a minus sign
+		 */
+		if (*opt_field_for_columns == '-')
+		{
+			columns_sort_direction = -1;
+			opt_field_for_columns++;
+		}
+		field_for_columns = indexOfColumn(opt_field_for_columns, res);
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * First pass: accumulate row names and column names, each into their
+	 * array. Use client-side sort but only to build the set of DISTINCT
+	 * values. The final order displayed depends only on server-side
+	 * sorts.
+	 */
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		if (!PQgetisnull(res, rn, field_for_rows))
+		{
+			accumHeader(PQgetvalue(res, rn, field_for_rows),
+						&num_rows,
+						&piv_rows,
+						rn);
+		}
+
+		if (!PQgetisnull(res, rn, field_for_columns))
+		{
+			accumHeader(PQgetvalue(res, rn, field_for_columns),
+						&num_columns,
+						&piv_columns,
+						rn);
+			if (num_columns > 1600)
+			{
+				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+				goto error_return;
+			}
+		}
+	}
+
+	/*
+	 * Second pass: sort the list of target columns on the server.
+	 */
+	if (!sortColumns(PQftype(res, field_for_columns),
+					 piv_columns,
+					 num_columns,
+					 columns_sort_direction))
+		goto error_return;
+
+	/*
+	 * Third pass: print the rotated results.
+	 */
+	printRotation(res,
+				  num_columns,
+				  piv_columns,
+				  field_for_columns,
+				  num_rows,
+				  piv_rows,
+				  field_for_rows);
+
+	retval = true;
+
+error_return:
+	pg_free(piv_columns);
+	pg_free(piv_rows);
+
+	return retval;
+}
diff --git a/src/bin/psql/rotate.h b/src/bin/psql/rotate.h
new file mode 100644
index 0000000..ba027b6
--- /dev/null
+++ b/src/bin/psql/rotate.h
@@ -0,0 +1,33 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/rotate.h
+ */
+
+#ifndef ROTATE_H
+#define ROTATE_H
+
+struct pivot_field
+{
+	/* Pointer obtained from PGgetvalue() for colV or colH */
+	char*	name;
+
+	/* Rank of the field in its list, starting at 0.
+	 * - For headers stacked vertically, rank=N means it's the
+	 *   Nth distinct field encountered when looping through rows
+	 *   in their initial order.
+	 * - For headers stacked horizontally, rank is obtained
+	 *   by server-side sorting in sortColumns()
+	 */
+	int		rank;
+};
+
+/* prototypes */
+extern bool
+PrintRotate(PGresult* res,
+			const char* opt_field_for_rows,
+			const char* opt_field_for_columns);
+
+#endif   /* ROTATE_H */
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 1885bb1..2164928 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -90,6 +90,9 @@ typedef struct _psqlSettings
 
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	bool		rotate_output;  /* one-shot request to print rotated results */
+	char		*rotate_col_V;  /* one-shot \rotate 1st argument */
+	char		*rotate_col_H;  /* one-shot \rotate 2nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
#26Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#25)
Re: [patch] Proposal for \rotate in psql

You're suggesting a [d] choice, subdividing the horizontal headers.
It seems to me like a pretty radical change, multiplying the number
of columns, and it has also the potential to give poor results visually.
Let's see if more feedback comes.

yes, I know, plan @d needs lot of new code - and described feature is from
"nice to have" kind.

The prerequisite is enhancing drawing system in psql to support
multiattribute (records) cells - what can be nice feature generally.

Some like:

id | C1 | C2 |
+---+----+---+---+---+---+
|A1 | A2 |A3 |A4 |A5 |A6 |
====+===+====+===+===+===+===+
| | | | | | |

Without this possibility plan @d is impossible.

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#27Marcin Mańk
marcin@maniek.info
In reply to: Daniel Verite (#21)
Re: [patch] Proposal for \rotate in psql

W dniu piątek, 18 września 2015 Daniel Verite <daniel@manitou-mail.org>
napisał(a):

Pavel Stehule wrote:

in the help inside your last patch, you are using "crosstab". Cannto be
crosstab the name for this feature?

If it wasn't taken already by contrib/tablefunc, that would be a first
choice. But now, when searching for crosstab+postgresql, pages of
results come out concerning the crosstab() function.

How about transpose (or flip)?

#28Pavel Stehule
pavel.stehule@gmail.com
In reply to: Marcin Mańk (#27)
Re: [patch] Proposal for \rotate in psql

2015-09-19 8:03 GMT+02:00 Marcin Mańk <marcin@maniek.info>:

W dniu piątek, 18 września 2015 Daniel Verite <daniel@manitou-mail.org>
napisał(a):

Pavel Stehule wrote:

in the help inside your last patch, you are using "crosstab". Cannto be
crosstab the name for this feature?

If it wasn't taken already by contrib/tablefunc, that would be a first
choice. But now, when searching for crosstab+postgresql, pages of
results come out concerning the crosstab() function.

How about transpose (or flip)?

transpose or flip are synonyms for rotate

Pavel

#29Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#22)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

So not using \crosstab is deliberate; it's to prevent confusion with
the server-side function.

I don't afraid about this - crosstab is a function in extension. Psql
backslash commands living in different worlds.

Sure, but the confusion would be assuming that \crosstab is some sort
of frontend for crosstab() queries, like for example \copy is a frontend
for COPY.
That mistake seems plausible if the same name is reused, and much less
plausible otherwise.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#30Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#29)
Re: [patch] Proposal for \rotate in psql

Hi

I am looking on this last patch. I talked about the name of this command
with more people, and the name "rotate" is unhappy. The correct name for
this visualization technique is "crosstab" (see google "crosstab"). The
conflict with our extension is unhappy, but using "rotate" is more worst -
(see google "rotate"). The term "rotate" is used less time (related to
topic), and usually with zero informed people. More, in attached doc, the
word "crosstab" is pretty often used, and then the word "rotate" has not
sense.

The important question is sorting output. The vertical header is sorted by
first appearance in result. The horizontal header is sorted in ascending or
descending order. This is unfriendly for often use case - month names. This
can be solved by third parameter - sort function.

Regards

Pavel

#31Joe Conway
mail@joeconway.com
In reply to: Pavel Stehule (#30)
Re: [patch] Proposal for \rotate in psql

On 11/04/2015 04:09 AM, Pavel Stehule wrote:

I am looking on this last patch. I talked about the name of this command
with more people, and the name "rotate" is unhappy. The correct name for
this visualization technique is "crosstab" (see google "crosstab"). The
conflict with our extension is unhappy, but using "rotate" is more worst
- (see google "rotate"). The term "rotate" is used less time (related to
topic), and usually with zero informed people. More, in attached doc,
the word "crosstab" is pretty often used, and then the word "rotate" has
not sense.

Apologies if this has already been suggested (as I have not followed the
entire thread), but if you don't want to conflict with the name
crosstab, perhaps "pivot" would be better?

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#32David Fetter
david@fetter.org
In reply to: Joe Conway (#31)
Re: [patch] Proposal for \rotate in psql

On Wed, Nov 04, 2015 at 08:20:28AM -0800, Joe Conway wrote:

On 11/04/2015 04:09 AM, Pavel Stehule wrote:

I am looking on this last patch. I talked about the name of this command
with more people, and the name "rotate" is unhappy. The correct name for
this visualization technique is "crosstab" (see google "crosstab"). The
conflict with our extension is unhappy, but using "rotate" is more worst
- (see google "rotate"). The term "rotate" is used less time (related to
topic), and usually with zero informed people. More, in attached doc,
the word "crosstab" is pretty often used, and then the word "rotate" has
not sense.

Apologies if this has already been suggested (as I have not followed the
entire thread), but if you don't want to conflict with the name
crosstab, perhaps "pivot" would be better?

As I mentioned earlier, I'm hoping we can keep PIVOT reserved for the
server side, where all our competitors except DB2 (and MySQL if you
count that) have it.

Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#33Pavel Stehule
pavel.stehule@gmail.com
In reply to: Joe Conway (#31)
Re: [patch] Proposal for \rotate in psql

2015-11-04 17:20 GMT+01:00 Joe Conway <mail@joeconway.com>:

On 11/04/2015 04:09 AM, Pavel Stehule wrote:

I am looking on this last patch. I talked about the name of this command
with more people, and the name "rotate" is unhappy. The correct name for
this visualization technique is "crosstab" (see google "crosstab"). The
conflict with our extension is unhappy, but using "rotate" is more worst
- (see google "rotate"). The term "rotate" is used less time (related to
topic), and usually with zero informed people. More, in attached doc,
the word "crosstab" is pretty often used, and then the word "rotate" has
not sense.

Apologies if this has already been suggested (as I have not followed the
entire thread), but if you don't want to conflict with the name
crosstab, perhaps "pivot" would be better?

it is between "rotate" and "crosstab". This name is related to PIVOT
operator, what is feature that we want, but it isn't hard problem because
we have COPY statement and \copy and we can live with it too.

If I understand to text on wiki, the name is used for tool, that can do
little bit more things, but it is often used for this technique (so it is
much better than "rotate"). I don't understand well, why "crosstab" is too
wrong name. This is pretty similar to COPY - and I know, so in first
minutes hard to explain this difference between COPY and \copy to
beginners, but after day of using there is not any problem.

Regards

Pavel

Show quoted text

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#34Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#30)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

I am looking on this last patch. I talked about the name of this command
with more people, and the name "rotate" is unhappy. The correct name for
this visualization technique is "crosstab" (see google "crosstab"). The
conflict with our extension is unhappy, but using "rotate" is more worst -
(see google "rotate"). The term "rotate" is used less time (related to
topic), and usually with zero informed people. More, in attached doc, the
word "crosstab" is pretty often used, and then the word "rotate" has not
sense.

First, thanks for looking again at the patch and for your feedback.

I note that you dislike and oppose the current name, as previously
when that choice of name was discussed quite a bit.
However I disagree that "rotate" doesn't make sense. On the semantics
side, several people have expressed upthread that it was OK, as a
plausible synonym for "pivot". If it's unencumbered by previous use
in this context, then all the better, I'd say why not corner it for our
own use?
It's not as if we had to cling to others people choices for psql
meta-commands.

Anyway that's just a name. It shall be changed eventually to
whatever the consensus is, if one happens to emerge.

The important question is sorting output. The vertical header is
sorted by first appearance in result. The horizontal header is
sorted in ascending or descending order. This is unfriendly for
often use case - month names. This can be solved by third parameter
- sort function.

Right, it's not possible currently to sort the horizontal header by
something else than the values in it.
I agree that it would be best to allow it if there's a reasonable way to
implement it. I'm not sure about letting the user provide a function
in argument.
In the case of the month names example, the function
f(monthname)=number-of-month may not exist. If the
user has to create it beforehand, it feels a bit demanding
for a display feature.

I wonder if this ordering information could be instead deduced
somehow from the non-pivoted resultset at a lower cost.
I'll try to think more and experiment around this.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#35Daniel Verite
daniel@manitou-mail.org
In reply to: Joe Conway (#31)
Re: [patch] Proposal for \rotate in psql

Joe Conway wrote:

but if you don't want to conflict with the name
crosstab, perhaps "pivot" would be better?

Initially I had chosen \pivot without much thought about it,
but the objection was raised that a PIVOT/UNPIVOT SQL feature
would likely exist in core in a next release independantly from psql.

If things unfold as envisioned, we would end up in the future with:
- crosstab() from tablefunc's contrib module.
- SELECT (...) PIVOT [serialization?] (...) in the SQL grammar.
- \rotate or yet another name for the client-side approach.

The reason to avoid both \crosstab or \pivot for the psql feature is
the fear of confusion for the less expert users who don't feel
the clear-cut separation between core and extensions and client
versus server, they mostly know that they're trying to use
a feature named $X. When they search for $X, it's better when
they don't find something else and possibly jump to wrong
conclusions about what it does and how it works.

Or maybe that's worrying about things that will never matter in
reality, that's possible too.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#36Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#34)
Re: [patch] Proposal for \rotate in psql

2015-11-05 0:07 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

I am looking on this last patch. I talked about the name of this command
with more people, and the name "rotate" is unhappy. The correct name for
this visualization technique is "crosstab" (see google "crosstab"). The
conflict with our extension is unhappy, but using "rotate" is more worst

-

(see google "rotate"). The term "rotate" is used less time (related to
topic), and usually with zero informed people. More, in attached doc, the
word "crosstab" is pretty often used, and then the word "rotate" has not
sense.

First, thanks for looking again at the patch and for your feedback.

I note that you dislike and oppose the current name, as previously
when that choice of name was discussed quite a bit.
However I disagree that "rotate" doesn't make sense. On the semantics
side, several people have expressed upthread that it was OK, as a
plausible synonym for "pivot". If it's unencumbered by previous use
in this context, then all the better, I'd say why not corner it for our
own use?
It's not as if we had to cling to others people choices for psql
meta-commands.

If there is correct and commonly used name, then using any other word is
wrong. More, if this word can be associated with different semantic. I know
so some people uses the "rotate', but it has a minimal cost, if these
people doesn't know existing terminology. My opinion is pretty strong in
this topic, mainly if we have to fix this name forever. It isn't internal
name, but clearly visible name.

Anyway that's just a name. It shall be changed eventually to
whatever the consensus is, if one happens to emerge.

The important question is sorting output. The vertical header is
sorted by first appearance in result. The horizontal header is
sorted in ascending or descending order. This is unfriendly for
often use case - month names. This can be solved by third parameter
- sort function.

Right, it's not possible currently to sort the horizontal header by
something else than the values in it.
I agree that it would be best to allow it if there's a reasonable way to
implement it. I'm not sure about letting the user provide a function
in argument.
In the case of the month names example, the function
f(monthname)=number-of-month may not exist. If the
user has to create it beforehand, it feels a bit demanding
for a display feature.

I wonder if this ordering information could be instead deduced
somehow from the non-pivoted resultset at a lower cost.
I'll try to think more and experiment around this.

It can be nice. These names can be transformed to numbers, but it lost some
information value. From the ideas that I found, the sort function is less
ugly. I invite any proposals. On second hand - this is not major issue -
it is "nice to have" category - and can to help with user adoption of this
function - the time dimensions (dows, months) are usual.

Maybe more simple idea - using transform function - the data in non-pivoted
can be numbers - and some parameter can transform numbers to text used in
horizontal header. It can pretty simple for implementation.

Regards

Pavel

p.s. Although I have maybe unlikely comments - I like this feature. It can
help, and it can be really valuable and visible psql feature.

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#37Craig Ringer
craig@2ndquadrant.com
In reply to: Pavel Stehule (#33)
Re: [patch] Proposal for \rotate in psql

On 5 November 2015 at 05:22, Pavel Stehule <pavel.stehule@gmail.com> wrote:

If I understand to text on wiki, the name is used for tool, that can do
little bit more things, but it is often used for this technique (so it is
much better than "rotate"). I don't understand well, why "crosstab" is too
wrong name. This is pretty similar to COPY - and I know, so in first minutes
hard to explain this difference between COPY and \copy to beginners, but
after day of using there is not any problem.

I see constant confusion between \copy and COPY. It's a really good
reason NOT to overload other psql commands IMO.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#38Pavel Stehule
pavel.stehule@gmail.com
In reply to: Craig Ringer (#37)
Re: [patch] Proposal for \rotate in psql

2015-11-05 7:39 GMT+01:00 Craig Ringer <craig@2ndquadrant.com>:

On 5 November 2015 at 05:22, Pavel Stehule <pavel.stehule@gmail.com>
wrote:

If I understand to text on wiki, the name is used for tool, that can do
little bit more things, but it is often used for this technique (so it is
much better than "rotate"). I don't understand well, why "crosstab" is

too

wrong name. This is pretty similar to COPY - and I know, so in first

minutes

hard to explain this difference between COPY and \copy to beginners, but
after day of using there is not any problem.

I see constant confusion between \copy and COPY. It's a really good
reason NOT to overload other psql commands IMO.

but crosstab is one old function from old extension with unfriendly design.
When we support PIVOT/UNPIVOT - the crosstab function will be obsolete. It
is not often used command.

Regards

Pavel

Show quoted text

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#39Joe Conway
mail@joeconway.com
In reply to: Pavel Stehule (#38)
Re: [patch] Proposal for \rotate in psql

On 11/04/2015 10:46 PM, Pavel Stehule wrote:

2015-11-05 7:39 GMT+01:00 Craig Ringer wrote:
I see constant confusion between \copy and COPY. It's a really good
reason NOT to overload other psql commands IMO.

but crosstab is one old function from old extension with unfriendly
design.

Hey, I resemble that remark ;-)

When we support PIVOT/UNPIVOT - the crosstab function will be
obsolete. It is not often used command.

But agreed, once we have proper support for PIVOT built into the
grammar, the entire tablefunc extension becomes obsolete, so perhaps
overloading \crosstab is not so bad.

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#40Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Joe Conway (#39)
Re: [patch] Proposal for \rotate in psql

Joe Conway wrote:

On 11/04/2015 10:46 PM, Pavel Stehule wrote:

but crosstab is one old function from old extension with unfriendly
design.

Hey, I resemble that remark ;-)

You may be old all you want, but certainly not unfriendly!

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#41Pavel Stehule
pavel.stehule@gmail.com
In reply to: Joe Conway (#39)
Re: [patch] Proposal for \rotate in psql

2015-11-05 17:17 GMT+01:00 Joe Conway <mail@joeconway.com>:

On 11/04/2015 10:46 PM, Pavel Stehule wrote:

2015-11-05 7:39 GMT+01:00 Craig Ringer wrote:
I see constant confusion between \copy and COPY. It's a really good
reason NOT to overload other psql commands IMO.

but crosstab is one old function from old extension with unfriendly
design.

Hey, I resemble that remark ;-)

I am sorry, Joe - no any personal attack - I'll pay a beer for you if you
visit Prague :)

Pavel

Show quoted text

When we support PIVOT/UNPIVOT - the crosstab function will be
obsolete. It is not often used command.

But agreed, once we have proper support for PIVOT built into the
grammar, the entire tablefunc extension becomes obsolete, so perhaps
overloading \crosstab is not so bad.

Joe

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#42Joe Conway
mail@joeconway.com
In reply to: Pavel Stehule (#41)
Re: [patch] Proposal for \rotate in psql

On 11/05/2015 12:56 PM, Pavel Stehule wrote:

2015-11-05 17:17 GMT+01:00 Joe Conway wrote:
Hey, I resemble that remark ;-)

I am sorry, Joe - no any personal attack - I'll pay a beer for you if
you visit Prague :)

No offense taken, but I might take you up on that beer someday ;-)

--
Crunchy Data - http://crunchydata.com
PostgreSQL Support for Secure Enterprises
Consulting, Training, & Open Source Development

#43Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#30)
1 attachment(s)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

[ \rotate being a wrong name ]

Here's an updated patch.

First it renames the command to \crosstabview, which hopefully may
be more consensual, at least it's semantically much closer to crosstab .

The important question is sorting output. The vertical header is
sorted by first appearance in result. The horizontal header is
sorted in ascending or descending order. This is unfriendly for
often use case - month names. This can be solved by third parameter
- sort function.

I've thought that sorting with an external function would be too
complicated for this command, but sorting ascending by default
was not the right choice either.
So I've changed to sorting by first appearance in result (like the vertical
header), and sorting ascending or descending only when specified
(with +colH or -colH syntax).

So the synopsis becomes: \crosstabview [ colV [+ | -]colH ]

Example with a time series (daily mean temperatures in Paris,2014),
month names across, day numbers down :

select
to_char(w_date,'DD') as day ,
to_char(w_date,'Mon') as month,
w_temp from weather
where w_date between '2014-01-01' and '2014-12-31'
order by w_date
\crosstabview

day | Jan | Feb | Mar | Apr | May | Jun | ...[cut]
-----+-----+-----+-----+-----+-----+-----+-
01 | 8 | 8 | 6 | 16 | 12 | 15 |
02 | 10 | 6 | 6 | 15 | 12 | 16 |
03 | 11 | 5 | 7 | 14 | 11 | 17 |
04 | 10 | 6 | 8 | 12 | 12 | 14 |
05 | 6 | 7 | 8 | 14 | 16 | 14 |
06 | 10 | 9 | 9 | 16 | 17 | 20 |
07 | 11 | 10 | 10 | 18 | 14 | 24 |
08 | 11 | 8 | 12 | 10 | 13 | 22 |
09 | 10 | 6 | 14 | 12 | 16 | 22 |
10 | 6 | 7 | 14 | 14 | 14 | 19 |
11 | 7 | 6 | 12 | 14 | 12 | 21 |
...cut..
28 | 4 | 7 | 10 | 12 | 14 | 16 |
29 | 4 | | 14 | 10 | 15 | 16 |
30 | 5 | | 14 | 14 | 17 | 18 |
31 | 5 | | 14 | | 16 | |

The month names come out in the expected order here,
contrary to what happened with the previous iteration of
the patch which forced a sort in all cases.
Here it plays out well because the single "ORDER BY w_date" is
simultaneously OK for the vertical and horizontal headers,
a common case for time series.

For more complicated cases, when the horizontal and vertical
headers should be ordered independantly, and
in addition the horizontal header should not be sorted
by its values, I've toyed with the idea of sorting by another
column which would supposedly be added in the query
just for sorting, but it loses much in simplicity. For the more
complex stuff, users can always turn to the server-side methods
if needed.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-rotate-v4.difftext/x-patch; name=psql-rotate-v4.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 5899bb4..3836fbd 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2449,6 +2449,95 @@ lo_import 152801
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><literal>\crosstabview [ <replaceable class="parameter">colV</replaceable>  [-|+]<replaceable class="parameter">colH</replaceable> ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows the results
+        inside a crosstab grid. The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header and the output column
+        <replaceable class="parameter">colH</replaceable> becomes a horizontal header.
+        The results for the other output columns are projected inside the grid.
+        </para>
+
+        <para>
+        <replaceable class="parameter">colV</replaceable>
+        and <replaceable class="parameter">colH</replaceable> can indicate a
+        column position (starting at 1), or a column name. Normal case folding
+        and quoting rules apply on column names. By default,
+        <replaceable class="parameter">colV</replaceable> is column 1
+        and <replaceable class="parameter">colH</replaceable> is column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the set of all distinct values found in
+        column <replaceable class="parameter">colV</replaceable>, in the order
+        of their first appearance in the query results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the set of all distinct non-null values found in
+        column <replaceable class="parameter">colH</replaceable>.  They come
+        by default in their order of appearance in the query results, or in ascending
+        order if a plus (+) sign precedes <replaceable class="parameter">colH</replaceable>,
+        or in descending order if it's a minus (-) sign.
+        </para>
+
+        <para>
+        The query results being tuples of <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        a cell located at the intersection <literal>(x,y)</literal> in the grid
+        has contents determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the results such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed in the cell as
+         if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to one row
+         except that the values coming from different rows are stacked
+         vertically, rows being separated by newline characters inside
+         the same cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
 
       <varlistentry>
         <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index f1336d5..9cb0c4a 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o crosstabview.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 438a4ec..56f37a6 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -46,6 +46,7 @@
 #include "mainloop.h"
 #include "print.h"
 #include "psqlscan.h"
+#include "crosstabview.h"
 #include "settings.h"
 #include "variables.h"
 
@@ -1081,6 +1082,34 @@ exec_command(const char *cmd,
 		free(pw2);
 	}
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		char	*opt1,
+				*opt2;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1): NULL;
+			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2): NULL;
+			pset.crosstabview_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+	}
+
 	/* \prompt -- prompt and set variable */
 	else if (strcmp(cmd, "prompt") == 0)
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 0e266a3..4329fdc 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -23,6 +23,7 @@
 #include "command.h"
 #include "copy.h"
 #include "mbprint.h"
+#include "crosstabview.h"
 
 
 
@@ -1061,7 +1062,14 @@ SendQuery(const char *query)
 
 		/* but printing results isn't: */
 		if (OK && results)
-			OK = PrintQueryResults(results);
+		{
+			if (pset.crosstabview_output)
+				OK = PrintResultsInCrossTab(results,
+											pset.crosstabview_col_V,
+											pset.crosstabview_col_H);
+			else
+				OK = PrintQueryResults(results);
+		}
 	}
 	else
 	{
@@ -1177,6 +1185,17 @@ sendquery_cleanup:
 		pset.gset_prefix = NULL;
 	}
 
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
 	return OK;
 }
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..08b4203
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,608 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+
+#include "common.h"
+#include "pqexpbuffer.h"
+#include "crosstabview.h"
+#include "settings.h"
+
+#include <string.h>
+
+static int
+headerCompare(const void *a, const void *b)
+{
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+static void
+accumHeader(char* name, int* count, struct pivot_field **sorted_tab, int row_number)
+{
+	struct pivot_field *p;
+
+	/*
+	 * Search for name in sorted_tab. If it doesn't exist, insert it,
+	 * otherwise do nothing.
+	 */
+
+	if (*count >= 1)
+	{
+		p = (struct pivot_field*) bsearch(&name,
+										  *sorted_tab,
+										  *count,
+										  sizeof(struct pivot_field),
+										  headerCompare);
+	}
+	else
+		p=NULL;
+
+	if (!p)
+	{
+		*sorted_tab = pg_realloc(*sorted_tab, sizeof(struct pivot_field) * (1+*count));
+		(*sorted_tab)[*count].name = name;
+		(*sorted_tab)[*count].rank = *count;
+		(*count)++;
+
+		qsort(*sorted_tab,
+			  *count,
+			  sizeof(struct pivot_field),
+			  headerCompare);
+	}
+}
+
+/*
+ * Send a query to sort all column values cast to the Oid passed in a VALUES clause
+ */
+static bool
+sortColumns(Oid coltype, struct pivot_field *columns, int nb_cols, int direction)
+{
+	bool retval = false;
+	PGresult *res = NULL;
+	PQExpBufferData query;
+	int i;
+	Oid *param_types;
+	const char** param_values;
+	int* param_lengths;
+	int* param_formats;
+
+	if (nb_cols < 2 || direction==0)
+		return true;					/* nothing to sort */
+
+	param_types = (Oid*) pg_malloc(nb_cols*sizeof(Oid));
+	param_values = (const char**) pg_malloc(nb_cols*sizeof(char*));
+	param_lengths = (int*) pg_malloc(nb_cols*sizeof(int));
+	param_formats = (int*) pg_malloc(nb_cols*sizeof(int));
+
+	initPQExpBuffer(&query);
+
+	/*
+	 * The query returns the original position of each value in our list,
+	 * ordered by its new position. The value itself is not returned.
+	 */
+	appendPQExpBuffer(&query, "SELECT n FROM (VALUES");
+
+	for (i=1; i <= nb_cols; i++)
+	{
+		if (i < nb_cols)
+			appendPQExpBuffer(&query, "($%d,%d),", i, i);
+		else
+		{
+			appendPQExpBuffer(&query, "($%d,%d)) AS l(x,n) ORDER BY x", i, i);
+			if (direction < 0)
+				appendPQExpBuffer(&query, " DESC");
+		}
+
+		param_types[i-1] = coltype;
+		param_values[i-1] = columns[i-1].name;
+		param_lengths[i-1] = strlen(columns[i-1].name);
+		param_formats[i-1] = 0;
+	}
+
+	res = PQexecParams(pset.db,
+					   query.data,
+					   nb_cols,
+					   param_types,
+					   param_values,
+					   param_lengths,
+					   param_formats,
+					   0);
+
+	if (res)
+	{
+		ExecStatusType status = PQresultStatus(res);
+		if (status == PGRES_TUPLES_OK)
+		{
+			for (i=0; i < PQntuples(res); i++)
+			{
+				int old_pos = atoi(PQgetvalue(res, i, 0));
+
+				if (old_pos < 1 || old_pos > nb_cols || i >= nb_cols)
+				{
+					/*
+					 * A position outside of the range is normally impossible.
+					 * If this happens, we're facing a malfunctioning or hostile
+					 * server or middleware.
+					 */
+					psql_error(_("Unexpected value when sorting horizontal headers"));
+					goto cleanup;
+				}
+				else
+				{
+					columns[old_pos-1].rank = i;
+				}
+			}
+		}
+		else
+		{
+			psql_error(_("Query error when sorting horizontal headers: %s"),
+					   PQerrorMessage(pset.db));
+			goto cleanup;
+		}
+	}
+
+	retval = true;
+
+cleanup:
+	termPQExpBuffer(&query);
+	if (res)
+		PQclear(res);
+	pg_free(param_types);
+	pg_free(param_values);
+	pg_free(param_lengths);
+	pg_free(param_formats);
+	return retval;
+}
+
+static void
+printCrosstab(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, rn;
+	char col_align = 'l';		/* alignment for values inside the grid */
+	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+						column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+	 *  associating each piv_columns[].rank to its index in piv_columns.
+	 *  This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+	/*
+	 * In the case of 3 output columns, the contents in the cells are exactly
+	 * the contents of the "value" column (3rd column by default), so their
+	 * alignment is determined by PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (PQnfields(results) == 3)
+	{
+		int colnum;				/* column placed inside the grid */
+		/*
+		 * find colnum in the permutations of (0,1,2) where colnum is
+		 * neither field_for_rows nor field_for_columns
+		 */
+		switch (field_for_rows)
+		{
+		case 0:
+			colnum = (field_for_columns == 1) ? 2 : 1;
+			break;
+		case 1:
+			colnum = (field_for_columns == 0) ? 2: 0;
+			break;
+		default:				/* should be always 2 */
+			colnum = (field_for_columns == 0) ? 1: 0;
+			break;
+		}
+		col_align = column_type_alignment(PQftype(results, colnum));
+	}
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		printTableAddHeader(&cont,
+							piv_columns[horiz_map[i]].name,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		char* row_name;
+		char* col_name;
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		row_number = col_number = -1;
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+		{
+			row_name = PQgetvalue(results, rn, field_for_rows);
+			p = (struct pivot_field*) bsearch(&row_name,
+											  piv_rows,
+											  num_rows,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				row_number = p->rank;
+		}
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+		{
+			col_name = PQgetvalue(results, rn, field_for_columns);
+			p = (struct pivot_field*) bsearch(&col_name,
+											  piv_columns,
+											  num_columns,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				col_number = p->rank;
+		}
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+			int k = 0;
+
+			do {
+				char *content;
+
+				if (PQnfields(results) == 2)
+				{
+					/*
+					  special case: when the source has only 2 columns, use a
+					  X (cross/checkmark) for the cell content, and set
+					  src_col to a virtual additional column.
+					*/
+					content = "X";
+					src_col = 3;
+				}
+				else if (src_col == field_for_rows || src_col == field_for_columns)
+				{
+					/*
+					  The source values that produce headers are not processed
+					  in this loop, only the values that end up inside the grid.
+					*/
+					src_col++;
+					continue;
+				}
+				else
+				{
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (k==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+				k++;
+				src_col++;
+			} while (src_col < PQnfields(results));
+		}
+	}
+
+	printTable(&cont, pset.queryFout, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static int
+fieldnameCmp(const char* arg, const char* fieldname)
+{
+	const unsigned char* p = (const unsigned char*) arg;
+	const unsigned char* f = (const unsigned char*) fieldname;
+	unsigned char c;
+
+	if (*p++ != '"')
+		return pg_strcasecmp(arg, fieldname);
+
+	while ((c=*p++))
+	{
+		if (c=='"')
+		{
+			if (*p=='"')
+				p++;			/* skip second quote and continue */
+			else if (*p=='\0')
+				return *p-*f;		/* p finishes before f or is identical */
+
+		}
+		if (*f=='\0')
+			return 1;			/* f finishes before p */
+		if (c!=*f)
+			return c-*f;
+		f++;
+	}
+	return (*f=='\0') ? 0 : 1;
+}
+
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *  on success, the 0-based index of the column
+ *  or -1 if the column number or name is not found in the result's structure,
+ *        or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char* arg, PGresult* res)
+{
+	int idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0  || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int i;
+		idx = -1;
+		for (i=0; i < PQnfields(res); i++)
+		{
+			if (fieldnameCmp(arg, PQfname(res, i)) == 0)
+			{
+				if (idx>=0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+bool
+PrintResultsInCrossTab(PGresult* res,
+					   const char* opt_field_for_rows,    /* COLV or null */
+					   const char* opt_field_for_columns) /* [-+]COLH or null */
+{
+	int		rn;
+	struct pivot_field	*piv_columns = NULL;
+	struct pivot_field	*piv_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	retval = false;
+	int		columns_sort_direction = 0; /* 1:ascending, 0:none, -1:descending */
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows;
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		goto error_return;
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	field_for_rows = (opt_field_for_rows != NULL)
+		? indexOfColumn(opt_field_for_rows, res)
+		: 0;
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		/*
+		 * descending sort is requested if the column reference is
+		 * preceded with a minus sign
+		 */
+		if (*opt_field_for_columns == '-')
+		{
+			columns_sort_direction = -1;
+			opt_field_for_columns++;
+		}
+		else if (*opt_field_for_columns == '+')
+		{
+			columns_sort_direction = 1;
+			opt_field_for_columns++;
+		}
+		field_for_columns = indexOfColumn(opt_field_for_columns, res);
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * First pass: accumulate row names and column names, each into their
+	 * array. Use client-side sort but only to build the set of DISTINCT
+	 * values. The final order displayed depends only on server-side
+	 * sorts.
+	 */
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		if (!PQgetisnull(res, rn, field_for_rows))
+		{
+			accumHeader(PQgetvalue(res, rn, field_for_rows),
+						&num_rows,
+						&piv_rows,
+						rn);
+		}
+
+		if (!PQgetisnull(res, rn, field_for_columns))
+		{
+			accumHeader(PQgetvalue(res, rn, field_for_columns),
+						&num_columns,
+						&piv_columns,
+						rn);
+			if (num_columns > 1600)
+			{
+				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+				goto error_return;
+			}
+		}
+	}
+
+	/*
+	 * Second pass: sort the list of target columns on the server.
+	 */
+	if (!sortColumns(PQftype(res, field_for_columns),
+					 piv_columns,
+					 num_columns,
+					 columns_sort_direction))
+		goto error_return;
+
+	/*
+	 * Third pass: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns,
+				  piv_columns,
+				  field_for_columns,
+				  num_rows,
+				  piv_rows,
+				  field_for_rows);
+
+	retval = true;
+
+error_return:
+	pg_free(piv_columns);
+	pg_free(piv_rows);
+
+	return retval;
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..c18efef
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,33 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+struct pivot_field
+{
+	/* Pointer obtained from PGgetvalue() for colV or colH */
+	char*	name;
+
+	/* Rank of the field in its list, starting at 0.
+	 * - For headers stacked vertically, rank=N means it's the
+	 *   Nth distinct field encountered when looping through rows
+	 *   in their initial order.
+	 * - For headers stacked horizontally, rank is obtained
+	 *   by server-side sorting in sortColumns()
+	 */
+	int		rank;
+};
+
+/* prototypes */
+extern bool
+PrintResultsInCrossTab(PGresult* res,
+					   const char* opt_field_for_rows,
+					   const char* opt_field_for_columns);
+
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 5b63e76..c38d51d 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -175,6 +175,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [V H]    execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
index ad4350e..08d80db 100644
--- a/src/bin/psql/print.c
+++ b/src/bin/psql/print.c
@@ -3170,30 +3170,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt, FILE *fout, FILE *f
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3235,6 +3214,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt, FILE *fout, FILE *f
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
index b0b6bf5..db6dfb5 100644
--- a/src/bin/psql/print.h
+++ b/src/bin/psql/print.h
@@ -171,7 +171,7 @@ extern FILE *PageOutput(int lines, const printTableOpt *topt);
 extern void ClosePager(FILE *pagerpipe);
 
 extern void html_escaped_print(const char *in, FILE *fout);
-
+extern char column_type_alignment(Oid);
 extern void printTableInit(printTableContent *const content,
 			   const printTableOpt *opt, const char *title,
 			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 1885bb1..6069024 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -90,6 +90,9 @@ typedef struct _psqlSettings
 
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
#44Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#43)
Re: [patch] Proposal for \rotate in psql

2015-11-30 16:34 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

[ \rotate being a wrong name ]

Here's an updated patch.

First it renames the command to \crosstabview, which hopefully may
be more consensual, at least it's semantically much closer to crosstab .

thank you very much :)

The important question is sorting output. The vertical header is
sorted by first appearance in result. The horizontal header is
sorted in ascending or descending order. This is unfriendly for
often use case - month names. This can be solved by third parameter
- sort function.

I've thought that sorting with an external function would be too
complicated for this command, but sorting ascending by default
was not the right choice either.
So I've changed to sorting by first appearance in result (like the vertical
header), and sorting ascending or descending only when specified
(with +colH or -colH syntax).

So the synopsis becomes: \crosstabview [ colV [+ | -]colH ]

Example with a time series (daily mean temperatures in Paris,2014),
month names across, day numbers down :

select
to_char(w_date,'DD') as day ,
to_char(w_date,'Mon') as month,
w_temp from weather
where w_date between '2014-01-01' and '2014-12-31'
order by w_date
\crosstabview

day | Jan | Feb | Mar | Apr | May | Jun | ...[cut]
-----+-----+-----+-----+-----+-----+-----+-
01 | 8 | 8 | 6 | 16 | 12 | 15 |
02 | 10 | 6 | 6 | 15 | 12 | 16 |
03 | 11 | 5 | 7 | 14 | 11 | 17 |
04 | 10 | 6 | 8 | 12 | 12 | 14 |
05 | 6 | 7 | 8 | 14 | 16 | 14 |
06 | 10 | 9 | 9 | 16 | 17 | 20 |
07 | 11 | 10 | 10 | 18 | 14 | 24 |
08 | 11 | 8 | 12 | 10 | 13 | 22 |
09 | 10 | 6 | 14 | 12 | 16 | 22 |
10 | 6 | 7 | 14 | 14 | 14 | 19 |
11 | 7 | 6 | 12 | 14 | 12 | 21 |
...cut..
28 | 4 | 7 | 10 | 12 | 14 | 16 |
29 | 4 | | 14 | 10 | 15 | 16 |
30 | 5 | | 14 | 14 | 17 | 18 |
31 | 5 | | 14 | | 16 | |

The month names come out in the expected order here,
contrary to what happened with the previous iteration of
the patch which forced a sort in all cases.
Here it plays out well because the single "ORDER BY w_date" is
simultaneously OK for the vertical and horizontal headers,
a common case for time series.

For more complicated cases, when the horizontal and vertical
headers should be ordered independantly, and
in addition the horizontal header should not be sorted
by its values, I've toyed with the idea of sorting by another
column which would supposedly be added in the query
just for sorting, but it loses much in simplicity. For the more
complex stuff, users can always turn to the server-side methods
if needed.

it is looking well

I'll do review tomorrow

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#45Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#43)
Re: [patch] Proposal for \rotate in psql

2015-11-30 16:34 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

[ \rotate being a wrong name ]

Here's an updated patch.

Today I have a time to play with it. I am sorry for delay.

First it renames the command to \crosstabview, which hopefully may
be more consensual, at least it's semantically much closer to crosstab .

Thank you very much - it is good name.

The important question is sorting output. The vertical header is
sorted by first appearance in result. The horizontal header is
sorted in ascending or descending order. This is unfriendly for
often use case - month names. This can be solved by third parameter
- sort function.

I've thought that sorting with an external function would be too
complicated for this command, but sorting ascending by default
was not the right choice either.
So I've changed to sorting by first appearance in result (like the vertical
header), and sorting ascending or descending only when specified
(with +colH or -colH syntax).

So the synopsis becomes: \crosstabview [ colV [+ | -]colH ]

Example with a time series (daily mean temperatures in Paris,2014),
month names across, day numbers down :

select
to_char(w_date,'DD') as day ,
to_char(w_date,'Mon') as month,
w_temp from weather
where w_date between '2014-01-01' and '2014-12-31'
order by w_date
\crosstabview

day | Jan | Feb | Mar | Apr | May | Jun | ...[cut]
-----+-----+-----+-----+-----+-----+-----+-
01 | 8 | 8 | 6 | 16 | 12 | 15 |
02 | 10 | 6 | 6 | 15 | 12 | 16 |
03 | 11 | 5 | 7 | 14 | 11 | 17 |
04 | 10 | 6 | 8 | 12 | 12 | 14 |
05 | 6 | 7 | 8 | 14 | 16 | 14 |
06 | 10 | 9 | 9 | 16 | 17 | 20 |
07 | 11 | 10 | 10 | 18 | 14 | 24 |
08 | 11 | 8 | 12 | 10 | 13 | 22 |
09 | 10 | 6 | 14 | 12 | 16 | 22 |
10 | 6 | 7 | 14 | 14 | 14 | 19 |
11 | 7 | 6 | 12 | 14 | 12 | 21 |
...cut..
28 | 4 | 7 | 10 | 12 | 14 | 16 |
29 | 4 | | 14 | 10 | 15 | 16 |
30 | 5 | | 14 | 14 | 17 | 18 |
31 | 5 | | 14 | | 16 | |

The month names come out in the expected order here,
contrary to what happened with the previous iteration of
the patch which forced a sort in all cases.
Here it plays out well because the single "ORDER BY w_date" is
simultaneously OK for the vertical and horizontal headers,
a common case for time series.

For more complicated cases, when the horizontal and vertical
headers should be ordered independantly, and
in addition the horizontal header should not be sorted
by its values, I've toyed with the idea of sorting by another
column which would supposedly be added in the query
just for sorting, but it loses much in simplicity. For the more
complex stuff, users can always turn to the server-side methods
if needed.

.Usually you have not natural order for both dimensions - I miss a
possibility to set [+/-] order for vertical dimension

For my query

select sum(amount) as amount, to_char(date_trunc('month', closed),'TMmon')
as Month, customer
from data group by customer, to_char(date_trunc('month', closed),
'TMmon'), extract(month from closed)
order by extract(month from closed);

I cannot to push order by customer - and I have to use

select sum(amount) as amount, extract(month from closed) as Month, customer
from data group by customer, extract(month from closed) order by customer;

and \crosstabview 3 +2

So possibility to enforce order for vertical dimension and use data order
for horizontal dimension can be really useful. Other way using special
column for sorting

some like \crosstabview verticalcolumn horizontalcolumn
sorthorizontalcolumn

Next - I use "fetch_count" > 0. Your new version work only with "fetch_cunt
<= 0". It is limit - but I am thinking it is acceptable.In this case some
warning should be displayed - some like "crosstabview doesn't work with
FETCH_COUNT > 0"

I miss support for autocomplete and \?

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#46Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#45)
1 attachment(s)
Re: [patch] Proposal for \rotate in psql

2015-12-05 8:59 GMT+01:00 Pavel Stehule <pavel.stehule@gmail.com>:

2015-11-30 16:34 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

[ \rotate being a wrong name ]

Here's an updated patch.

Today I have a time to play with it. I am sorry for delay.

First it renames the command to \crosstabview, which hopefully may
be more consensual, at least it's semantically much closer to crosstab .

Thank you very much - it is good name.

The important question is sorting output. The vertical header is
sorted by first appearance in result. The horizontal header is
sorted in ascending or descending order. This is unfriendly for
often use case - month names. This can be solved by third parameter
- sort function.

I've thought that sorting with an external function would be too
complicated for this command, but sorting ascending by default
was not the right choice either.
So I've changed to sorting by first appearance in result (like the
vertical
header), and sorting ascending or descending only when specified
(with +colH or -colH syntax).

So the synopsis becomes: \crosstabview [ colV [+ | -]colH ]

Example with a time series (daily mean temperatures in Paris,2014),
month names across, day numbers down :

select
to_char(w_date,'DD') as day ,
to_char(w_date,'Mon') as month,
w_temp from weather
where w_date between '2014-01-01' and '2014-12-31'
order by w_date
\crosstabview

day | Jan | Feb | Mar | Apr | May | Jun | ...[cut]
-----+-----+-----+-----+-----+-----+-----+-
01 | 8 | 8 | 6 | 16 | 12 | 15 |
02 | 10 | 6 | 6 | 15 | 12 | 16 |
03 | 11 | 5 | 7 | 14 | 11 | 17 |
04 | 10 | 6 | 8 | 12 | 12 | 14 |
05 | 6 | 7 | 8 | 14 | 16 | 14 |
06 | 10 | 9 | 9 | 16 | 17 | 20 |
07 | 11 | 10 | 10 | 18 | 14 | 24 |
08 | 11 | 8 | 12 | 10 | 13 | 22 |
09 | 10 | 6 | 14 | 12 | 16 | 22 |
10 | 6 | 7 | 14 | 14 | 14 | 19 |
11 | 7 | 6 | 12 | 14 | 12 | 21 |
...cut..
28 | 4 | 7 | 10 | 12 | 14 | 16 |
29 | 4 | | 14 | 10 | 15 | 16 |
30 | 5 | | 14 | 14 | 17 | 18 |
31 | 5 | | 14 | | 16 | |

The month names come out in the expected order here,
contrary to what happened with the previous iteration of
the patch which forced a sort in all cases.
Here it plays out well because the single "ORDER BY w_date" is
simultaneously OK for the vertical and horizontal headers,
a common case for time series.

For more complicated cases, when the horizontal and vertical
headers should be ordered independantly, and
in addition the horizontal header should not be sorted
by its values, I've toyed with the idea of sorting by another
column which would supposedly be added in the query
just for sorting, but it loses much in simplicity. For the more
complex stuff, users can always turn to the server-side methods
if needed.

.Usually you have not natural order for both dimensions - I miss a
possibility to set [+/-] order for vertical dimension

For my query

select sum(amount) as amount, to_char(date_trunc('month', closed),'TMmon')
as Month, customer
from data group by customer, to_char(date_trunc('month', closed),
'TMmon'), extract(month from closed)
order by extract(month from closed);

I cannot to push order by customer - and I have to use

select sum(amount) as amount, extract(month from closed) as Month,
customer from data group by customer, extract(month from closed) order by
customer;

and \crosstabview 3 +2

So possibility to enforce order for vertical dimension and use data order
for horizontal dimension can be really useful. Other way using special
column for sorting

some like \crosstabview verticalcolumn horizontalcolumn
sorthorizontalcolumn

Next - I use "fetch_count" > 0. Your new version work only with
"fetch_cunt <= 0". It is limit - but I am thinking it is acceptable.In this
case some warning should be displayed - some like "crosstabview doesn't
work with FETCH_COUNT > 0"

I miss support for autocomplete and \?

Regards

Pavel

I did few minor changes in your patch

1. autocomplete + warning on active FETCH_COUNT (the worning should be
replaced by error, the statement show nothing)

2. support for labels

postgres=# \d data
Table "public.data"
┌──────────┬─────────┬───────────┐
│ Column │ Type │ Modifiers │
╞══════════╪═════════╪═══════════╡
│ id │ integer │ │
│ customer │ text │ │
│ name │ text │ │
│ amount │ integer │ │
│ expected │ text │ │
│ closed │ date │ │
└──────────┴─────────┴───────────┘

postgres=# select sum(amount) as amount, extract(month from closed) as
Month, to_char(date_trunc('month', closed), 'TMmon') as label, customer
from data group by customer, to_char(date_trunc('month', closed), 'TMmon'),
extract(month from closed) order by customer;

postgres=# \crosstabview 4 +month label
┌──────────────────────────┬───────┬───────┬────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ customer │ led │ úno │ bře │ dub │ kvě │ čen
│ čec │ srp │ zář │ říj │ lis │
╞══════════════════════════╪═══════╪═══════╪════════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╡
│ A********** │ │ │ │ │ │
│ │ │ │ 13000 │ │
│ A******** │ │ │ 8000 │ │ │
│ │ │ │ │ │
│ B***** │ │ │ │ │ │
│ │ │ │ │ 3200 │
│ B*********************** │ │ │ │ │ │
│ │ │ 26200 │ │ │
│ B********* │ │ │ │ │ │
│ 14000 │ │ │ │ │
│ C********** │ │ │ │ 7740 │ │
│ │ │ │ │ │
│ C*** │ │ │ │ │ │
│ │ │ 26000 │ │ │
│ C***** │ │ │ │ 12000 │ │
│ │ │ │ │ │
│ G******* │ 30200 │ 26880 │ 13536 │ 39360 │ 60480 │ 54240
│ 44160 │ 16320 │ 29760 │ 22560 │ 20160 │
│ G*************** │ │ │ │ │ │ 25500
│ │ │ │ │ │
│ G********** │ │ │ │ │ │ 16000
│ │ │ │ │ │
│ I************* │ │ │ │ │ │
│ │ 27920 │ │ │ │
│ i**** │ │ │ │ 13500 │ │
│ │ │ │ │ │
│ n********* │ │ │ │ │ │
│ 12600 │ │ │ │ │
│ Q** │ │ │ │ │ 16700 │
│ │ │ │ │ │
│ S******* │ │ │ │ │ │
│ 8000 │ │ │ │ │
│ S******* │ │ │ │ │ 5368 │
│ │ │ │ │ │
│ s******* │ │ │ 5000 │ 3200 │ │
│ │ │ │ │ │
└──────────────────────────┴───────┴───────┴────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
(18 rows)

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-rotate-v5.difftext/plain; charset=US-ASCII; name=psql-rotate-v5.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
new file mode 100644
index e4f72a8..ed802b3
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** lo_import 152801
*** 2449,2454 ****
--- 2449,2543 ----
          </listitem>
        </varlistentry>
  
+       <varlistentry>
+         <term><literal>\crosstabview [ <replaceable class="parameter">colV</replaceable>  [-|+]<replaceable class="parameter">colH</replaceable> ] </literal></term>
+         <listitem>
+         <para>
+         Execute the current query buffer (like <literal>\g</literal>) and shows the results
+         inside a crosstab grid. The output column <replaceable class="parameter">colV</replaceable>
+         becomes a vertical header and the output column
+         <replaceable class="parameter">colH</replaceable> becomes a horizontal header.
+         The results for the other output columns are projected inside the grid.
+         </para>
+ 
+         <para>
+         <replaceable class="parameter">colV</replaceable>
+         and <replaceable class="parameter">colH</replaceable> can indicate a
+         column position (starting at 1), or a column name. Normal case folding
+         and quoting rules apply on column names. By default,
+         <replaceable class="parameter">colV</replaceable> is column 1
+         and <replaceable class="parameter">colH</replaceable> is column 2.
+         A query having only one output column cannot be viewed in crosstab, and
+         <replaceable class="parameter">colH</replaceable> must differ from
+         <replaceable class="parameter">colV</replaceable>.
+         </para>
+ 
+         <para>
+         The vertical header, displayed as the leftmost column,
+         contains the set of all distinct values found in
+         column <replaceable class="parameter">colV</replaceable>, in the order
+         of their first appearance in the query results.
+         </para>
+         <para>
+         The horizontal header, displayed as the first row,
+         contains the set of all distinct non-null values found in
+         column <replaceable class="parameter">colH</replaceable>.  They come
+         by default in their order of appearance in the query results, or in ascending
+         order if a plus (+) sign precedes <replaceable class="parameter">colH</replaceable>,
+         or in descending order if it's a minus (-) sign.
+         </para>
+ 
+         <para>
+         The query results being tuples of <literal>N</literal> columns
+         (including <replaceable class="parameter">colV</replaceable> and
+         <replaceable class="parameter">colH</replaceable>),
+         for each distinct value <literal>x</literal> of
+         <replaceable class="parameter">colH</replaceable>
+         and each distinct value <literal>y</literal> of
+         <replaceable class="parameter">colV</replaceable>,
+         a cell located at the intersection <literal>(x,y)</literal> in the grid
+         has contents determined by these rules:
+         <itemizedlist>
+         <listitem>
+         <para>
+          if there is no corresponding row in the results such that the value
+          for <replaceable class="parameter">colH</replaceable>
+          is <literal>x</literal> and the value
+          for <replaceable class="parameter">colV</replaceable>
+          is <literal>y</literal>, the cell is empty.
+         </para>
+         </listitem>
+ 
+         <listitem>
+         <para>
+          if there is exactly one row such that the value
+          for <replaceable class="parameter">colH</replaceable>
+          is <literal>x</literal> and the value
+          for <replaceable class="parameter">colV</replaceable>
+          is <literal>y</literal>, then the <literal>N-2</literal> other
+          columns are displayed in the cell, separated between each other by
+          a space character if needed.
+ 
+          If <literal>N=2</literal>, the letter <literal>X</literal> is displayed in the cell as
+          if a virtual third column contained that character.
+         </para>
+         </listitem>
+ 
+         <listitem>
+         <para>
+          if there are several corresponding rows, the behavior is identical to one row
+          except that the values coming from different rows are stacked
+          vertically, rows being separated by newline characters inside
+          the same cell.
+         </para>
+         </listitem>
+ 
+         </itemizedlist>
+         </para>
+ 
+         </listitem>
+       </varlistentry>
+ 
  
        <varlistentry>
          <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
new file mode 100644
index f1336d5..9cb0c4a
*** a/src/bin/psql/Makefile
--- b/src/bin/psql/Makefile
*************** override CPPFLAGS := -I. -I$(srcdir) -I$
*** 23,29 ****
  OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
  	startup.o prompt.o variables.o large_obj.o print.o describe.o \
  	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
! 	sql_help.o \
  	$(WIN32RES)
  
  
--- 23,29 ----
  OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
  	startup.o prompt.o variables.o large_obj.o print.o describe.o \
  	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
! 	sql_help.o crosstabview.o \
  	$(WIN32RES)
  
  
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
new file mode 100644
index 8eca4cf..8305d1e
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 46,51 ****
--- 46,52 ----
  #include "mainloop.h"
  #include "print.h"
  #include "psqlscan.h"
+ #include "crosstabview.h"
  #include "settings.h"
  #include "variables.h"
  
*************** exec_command(const char *cmd,
*** 1081,1086 ****
--- 1082,1119 ----
  		free(pw2);
  	}
  
+ 	/* \crosstabview -- execute a query and display results in crosstab */
+ 	else if (strcmp(cmd, "crosstabview") == 0)
+ 	{
+ 		char	*opt1,
+ 				*opt2,
+ 				*opt3;
+ 
+ 		opt1 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 		opt2 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 		opt3 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 
+ 		if (opt1 && !opt2)
+ 		{
+ 			psql_error(_("\\%s: missing second argument\n"), cmd);
+ 			success = false;
+ 		}
+ 		else
+ 		{
+ 			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1) : NULL;
+ 			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2) : NULL;
+ 			pset.crosstabview_col_L = opt3 ? pg_strdup(opt3) : NULL;
+ 			pset.crosstabview_output = true;
+ 			status = PSQL_CMD_SEND;
+ 		}
+ 
+ 		free(opt1);
+ 		free(opt2);
+ 	}
+ 
  	/* \prompt -- prompt and set variable */
  	else if (strcmp(cmd, "prompt") == 0)
  	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 3254a14..8a7984d
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 24,29 ****
--- 24,30 ----
  #include "command.h"
  #include "copy.h"
  #include "mbprint.h"
+ #include "crosstabview.h"
  
  
  
*************** SendQuery(const char *query)
*** 1062,1071 ****
  
  		/* but printing results isn't: */
  		if (OK && results)
! 			OK = PrintQueryResults(results);
  	}
  	else
  	{
  		/* Fetch-in-segments mode */
  		OK = ExecQueryUsingCursor(query, &elapsed_msec);
  		ResetCancelConn();
--- 1063,1084 ----
  
  		/* but printing results isn't: */
  		if (OK && results)
! 		{
! 			if (pset.crosstabview_output)
! 				OK = PrintResultsInCrossTab(results,
! 											pset.crosstabview_col_V,
! 											pset.crosstabview_col_H,
! 											pset.crosstabview_col_L);
! 			else
! 				OK = PrintQueryResults(results);
! 		}
  	}
  	else
  	{
+ 		if (pset.crosstabview_output)
+ 			psql_error("\\crosstabview is ignored due active FETCH_COUNT = %d\n",
+ 					pset.fetch_count);
+ 
  		/* Fetch-in-segments mode */
  		OK = ExecQueryUsingCursor(query, &elapsed_msec);
  		ResetCancelConn();
*************** sendquery_cleanup:
*** 1178,1183 ****
--- 1191,1213 ----
  		pset.gset_prefix = NULL;
  	}
  
+ 	pset.crosstabview_output = false;
+ 	if (pset.crosstabview_col_V)
+ 	{
+ 		free(pset.crosstabview_col_V);
+ 		pset.crosstabview_col_V = NULL;
+ 	}
+ 	if (pset.crosstabview_col_H)
+ 	{
+ 		free(pset.crosstabview_col_H);
+ 		pset.crosstabview_col_H = NULL;
+ 	}
+ 	if (pset.crosstabview_col_L)
+ 	{
+ 		free(pset.crosstabview_col_H);
+ 		pset.crosstabview_col_H = NULL;
+ 	}
+ 
  	return OK;
  }
  
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index ...6dce5a4
*** a/src/bin/psql/crosstabview.c
--- b/src/bin/psql/crosstabview.c
***************
*** 0 ****
--- 1,636 ----
+ /*
+  * psql - the PostgreSQL interactive terminal
+  *
+  * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+  *
+  * src/bin/psql/crosstabview.c
+  */
+ 
+ #include "common.h"
+ #include "pqexpbuffer.h"
+ #include "crosstabview.h"
+ #include "settings.h"
+ 
+ #include <string.h>
+ 
+ static int
+ headerCompare(const void *a, const void *b)
+ {
+ 	return strcmp( ((struct pivot_field*)a)->name,
+ 				   ((struct pivot_field*)b)->name);
+ }
+ 
+ static void
+ accumHeader(char* name, char *label, int* count, struct pivot_field **sorted_tab, int row_number)
+ {
+ 	struct pivot_field *p;
+ 
+ 	/*
+ 	 * Search for name in sorted_tab. If it doesn't exist, insert it,
+ 	 * otherwise do nothing.
+ 	 */
+ 
+ 	if (*count >= 1)
+ 	{
+ 		p = (struct pivot_field*) bsearch(&name,
+ 										  *sorted_tab,
+ 										  *count,
+ 										  sizeof(struct pivot_field),
+ 										  headerCompare);
+ 	}
+ 	else
+ 		p=NULL;
+ 
+ 	if (!p)
+ 	{
+ 		*sorted_tab = pg_realloc(*sorted_tab, sizeof(struct pivot_field) * (1+*count));
+ 		(*sorted_tab)[*count].name = name;
+ 		(*sorted_tab)[*count].label = label;
+ 		(*sorted_tab)[*count].rank = *count;
+ 		(*count)++;
+ 
+ 		qsort(*sorted_tab,
+ 			  *count,
+ 			  sizeof(struct pivot_field),
+ 			  headerCompare);
+ 	}
+ }
+ 
+ /*
+  * Send a query to sort all column values cast to the Oid passed in a VALUES clause
+  */
+ static bool
+ sortColumns(Oid coltype, struct pivot_field *columns, int nb_cols, int direction)
+ {
+ 	bool retval = false;
+ 	PGresult *res = NULL;
+ 	PQExpBufferData query;
+ 	int i;
+ 	Oid *param_types;
+ 	const char** param_values;
+ 	int* param_lengths;
+ 	int* param_formats;
+ 
+ 	if (nb_cols < 2 || direction==0)
+ 		return true;					/* nothing to sort */
+ 
+ 	param_types = (Oid*) pg_malloc(nb_cols*sizeof(Oid));
+ 	param_values = (const char**) pg_malloc(nb_cols*sizeof(char*));
+ 	param_lengths = (int*) pg_malloc(nb_cols*sizeof(int));
+ 	param_formats = (int*) pg_malloc(nb_cols*sizeof(int));
+ 
+ 	initPQExpBuffer(&query);
+ 
+ 	/*
+ 	 * The query returns the original position of each value in our list,
+ 	 * ordered by its new position. The value itself is not returned.
+ 	 */
+ 	appendPQExpBuffer(&query, "SELECT n FROM (VALUES");
+ 
+ 	for (i=1; i <= nb_cols; i++)
+ 	{
+ 		if (i < nb_cols)
+ 			appendPQExpBuffer(&query, "($%d,%d),", i, i);
+ 		else
+ 		{
+ 			appendPQExpBuffer(&query, "($%d,%d)) AS l(x,n) ORDER BY x", i, i);
+ 			if (direction < 0)
+ 				appendPQExpBuffer(&query, " DESC");
+ 		}
+ 
+ 		param_types[i-1] = coltype;
+ 		param_values[i-1] = columns[i-1].name;
+ 		param_lengths[i-1] = strlen(columns[i-1].name);
+ 		param_formats[i-1] = 0;
+ 	}
+ 
+ 	res = PQexecParams(pset.db,
+ 					   query.data,
+ 					   nb_cols,
+ 					   param_types,
+ 					   param_values,
+ 					   param_lengths,
+ 					   param_formats,
+ 					   0);
+ 
+ 	if (res)
+ 	{
+ 		ExecStatusType status = PQresultStatus(res);
+ 		if (status == PGRES_TUPLES_OK)
+ 		{
+ 			for (i=0; i < PQntuples(res); i++)
+ 			{
+ 				int old_pos = atoi(PQgetvalue(res, i, 0));
+ 
+ 				if (old_pos < 1 || old_pos > nb_cols || i >= nb_cols)
+ 				{
+ 					/*
+ 					 * A position outside of the range is normally impossible.
+ 					 * If this happens, we're facing a malfunctioning or hostile
+ 					 * server or middleware.
+ 					 */
+ 					psql_error(_("Unexpected value when sorting horizontal headers"));
+ 					goto cleanup;
+ 				}
+ 				else
+ 				{
+ 					columns[old_pos-1].rank = i;
+ 				}
+ 			}
+ 		}
+ 		else
+ 		{
+ 			psql_error(_("Query error when sorting horizontal headers: %s"),
+ 					   PQerrorMessage(pset.db));
+ 			goto cleanup;
+ 		}
+ 	}
+ 
+ 	retval = true;
+ 
+ cleanup:
+ 	termPQExpBuffer(&query);
+ 	if (res)
+ 		PQclear(res);
+ 	pg_free(param_types);
+ 	pg_free(param_values);
+ 	pg_free(param_lengths);
+ 	pg_free(param_formats);
+ 	return retval;
+ }
+ 
+ static void
+ printCrosstab(const PGresult *results,
+ 			  int num_columns,
+ 			  struct pivot_field *piv_columns,
+ 			  int field_for_columns,
+ 			  int num_rows,
+ 			  struct pivot_field *piv_rows,
+ 			  int field_for_rows,
+ 			  int field_for_labels)
+ {
+ 	printQueryOpt popt = pset.popt;
+ 	printTableContent cont;
+ 	int	i, j, rn;
+ 	char col_align = 'l';		/* alignment for values inside the grid */
+ 	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+ 	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+ 								 *  in this function, when cells cannot simply point to
+ 								 *  PQgetvalue(results, ...) */
+ 
+ 	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+ 
+ 	/* Step 1: set target column names (horizontal header) */
+ 
+ 	/* The name of the first column is kept unchanged by the pivoting */
+ 	printTableAddHeader(&cont,
+ 						PQfname(results, field_for_rows),
+ 						false,
+ 						column_type_alignment(PQftype(results, field_for_rows)));
+ 
+ 	/*
+ 	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+ 	 *  associating each piv_columns[].rank to its index in piv_columns.
+ 	 *  This avoids an O(N^2) loop later
+ 	 */
+ 	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		horiz_map[piv_columns[i].rank] = i;
+ 	}
+ 
+ 	/*
+ 	 * In the case of 3 output columns, the contents in the cells are exactly
+ 	 * the contents of the "value" column (3rd column by default), so their
+ 	 * alignment is determined by PQftype(). Otherwise the contents are
+ 	 * made-up strings, so the alignment is 'l'
+ 	 */
+ 	if (PQnfields(results) == 3)
+ 	{
+ 		int colnum;				/* column placed inside the grid */
+ 		/*
+ 		 * find colnum in the permutations of (0,1,2) where colnum is
+ 		 * neither field_for_rows nor field_for_columns
+ 		 */
+ 		switch (field_for_rows)
+ 		{
+ 		case 0:
+ 			colnum = (field_for_columns == 1) ? 2 : 1;
+ 			break;
+ 		case 1:
+ 			colnum = (field_for_columns == 0) ? 2: 0;
+ 			break;
+ 		default:				/* should be always 2 */
+ 			colnum = (field_for_columns == 0) ? 1: 0;
+ 			break;
+ 		}
+ 		col_align = column_type_alignment(PQftype(results, colnum));
+ 	}
+ 	else
+ 		col_align = 'l';
+ 
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		printTableAddHeader(&cont,
+ 						field_for_labels < 0 ?
+ 							piv_columns[horiz_map[i]].name :
+ 							piv_columns[horiz_map[i]].label,
+ 							false,
+ 							col_align);
+ 	}
+ 	pg_free(horiz_map);
+ 
+ 	/* Step 2: set row names in the first output column (vertical header) */
+ 	for (i = 0; i < num_rows; i++)
+ 	{
+ 		int k = piv_rows[i].rank;
+ 		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+ 		/* Initialize all cells inside the grid to an empty value */
+ 		for (j = 0; j < num_columns; j++)
+ 			cont.cells[k*(num_columns+1)+j+1] = "";
+ 	}
+ 	cont.cellsadded = num_rows * (num_columns+1);
+ 
+ 	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+ 
+ 	/* Step 3: set all the cells "inside the grid" */
+ 	for (rn = 0; rn < PQntuples(results); rn++)
+ 	{
+ 		char* row_name;
+ 		char* col_name;
+ 		int row_number;
+ 		int col_number;
+ 		struct pivot_field *p;
+ 
+ 		row_number = col_number = -1;
+ 		/* Find target row */
+ 		if (!PQgetisnull(results, rn, field_for_rows))
+ 		{
+ 			row_name = PQgetvalue(results, rn, field_for_rows);
+ 			p = (struct pivot_field*) bsearch(&row_name,
+ 											  piv_rows,
+ 											  num_rows,
+ 											  sizeof(struct pivot_field),
+ 											  headerCompare);
+ 			if (p)
+ 				row_number = p->rank;
+ 		}
+ 
+ 		/* Find target column */
+ 		if (!PQgetisnull(results, rn, field_for_columns))
+ 		{
+ 			col_name = PQgetvalue(results, rn, field_for_columns);
+ 			p = (struct pivot_field*) bsearch(&col_name,
+ 											  piv_columns,
+ 											  num_columns,
+ 											  sizeof(struct pivot_field),
+ 											  headerCompare);
+ 			if (p)
+ 				col_number = p->rank;
+ 		}
+ 
+ 		/* Place value into cell */
+ 		if (col_number>=0 && row_number>=0)
+ 		{
+ 			int idx = 1 + col_number + row_number*(num_columns+1);
+ 			int src_col = 0;			/* column number in source result */
+ 			int k = 0;
+ 
+ 			do {
+ 				char *content;
+ 
+ 				if (PQnfields(results) == 2)
+ 				{
+ 					/*
+ 					  special case: when the source has only 2 columns, use a
+ 					  X (cross/checkmark) for the cell content, and set
+ 					  src_col to a virtual additional column.
+ 					*/
+ 					content = "X";
+ 					src_col = 3;
+ 				}
+ 				else if (src_col == field_for_rows || src_col == field_for_columns
+ 					    || src_col == field_for_labels)
+ 				{
+ 					/*
+ 					  The source values that produce headers are not processed
+ 					  in this loop, only the values that end up inside the grid.
+ 					*/
+ 					src_col++;
+ 					continue;
+ 				}
+ 				else
+ 				{
+ 					content = (!PQgetisnull(results, rn, src_col)) ?
+ 						PQgetvalue(results, rn, src_col) :
+ 						(popt.nullPrint ? popt.nullPrint : "");
+ 				}
+ 
+ 				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+ 				{
+ 					/*
+ 					 * Multiple values for the same (row,col) are projected
+ 					 * into the same cell. When this happens, separate the
+ 					 * previous content of the cell from the new value by a
+ 					 * newline.
+ 					 */
+ 					int content_size =
+ 						strlen(cont.cells[idx])
+ 						+ 2 			/* room for [CR],LF or space */
+ 						+ strlen(content)
+ 						+ 1;			/* '\0' */
+ 					char *new_content;
+ 
+ 					/*
+ 					 * idx2 is an index into allocated_cells. It differs from
+ 					 * idx (index into cont.cells), because vertical and
+ 					 * horizontal headers are included in `cont.cells` but
+ 					 * excluded from allocated_cells.
+ 					 */
+ 					int idx2 = (row_number * num_columns) + col_number;
+ 
+ 					if (allocated_cells[idx2] != NULL)
+ 					{
+ 						new_content = pg_realloc(allocated_cells[idx2], content_size);
+ 					}
+ 					else
+ 					{
+ 						/*
+ 						 * At this point, cont.cells[idx] still contains a
+ 						 * PQgetvalue() pointer.  Just after, it will contain
+ 						 * a new pointer maintained in allocated_cells[], and
+ 						 * freed at the end of this function.
+ 						 */
+ 						new_content = pg_malloc(content_size);
+ 						strcpy(new_content, cont.cells[idx]);
+ 					}
+ 					cont.cells[idx] = new_content;
+ 					allocated_cells[idx2] = new_content;
+ 
+ 					/*
+ 					 * Contents that are on adjacent columns in the source results get
+ 					 * separated by one space in the target.
+ 					 * Contents that are on different rows in the source get
+ 					 * separated by newlines in the target.
+ 					 */
+ 					if (k==0)
+ 						strcat(new_content, "\n");
+ 					else
+ 						strcat(new_content, " ");
+ 					strcat(new_content, content);
+ 				}
+ 				else
+ 				{
+ 					cont.cells[idx] = content;
+ 				}
+ 				k++;
+ 				src_col++;
+ 			} while (src_col < PQnfields(results));
+ 		}
+ 	}
+ 
+ 	printTable(&cont, pset.queryFout, false, pset.logfile);
+ 	printTableCleanup(&cont);
+ 
+ 
+ 	for (i=0; i < num_rows * num_columns; i++)
+ 	{
+ 		if (allocated_cells[i] != NULL)
+ 			pg_free(allocated_cells[i]);
+ 	}
+ 
+ 	pg_free(allocated_cells);
+ }
+ 
+ /*
+  * Compare a user-supplied argument against a field name obtained by PQfname(),
+  * which is already case-folded.
+  * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+  * do a case-sensitive comparison with these rules:
+  * - double quotes enclosing 'arg' are filtered out
+  * - double quotes inside 'arg' are expected to be doubled
+  */
+ static int
+ fieldnameCmp(const char* arg, const char* fieldname)
+ {
+ 	const unsigned char* p = (const unsigned char*) arg;
+ 	const unsigned char* f = (const unsigned char*) fieldname;
+ 	unsigned char c;
+ 
+ 	if (*p++ != '"')
+ 		return pg_strcasecmp(arg, fieldname);
+ 
+ 	while ((c=*p++))
+ 	{
+ 		if (c=='"')
+ 		{
+ 			if (*p=='"')
+ 				p++;			/* skip second quote and continue */
+ 			else if (*p=='\0')
+ 				return *p-*f;		/* p finishes before f or is identical */
+ 
+ 		}
+ 		if (*f=='\0')
+ 			return 1;			/* f finishes before p */
+ 		if (c!=*f)
+ 			return c-*f;
+ 		f++;
+ 	}
+ 	return (*f=='\0') ? 0 : 1;
+ }
+ 
+ 
+ /*
+  * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+  * Returns:
+  *  on success, the 0-based index of the column
+  *  or -1 if the column number or name is not found in the result's structure,
+  *        or if it's ambiguous (arg corresponding to several columns)
+  */
+ static int
+ indexOfColumn(const char* arg, PGresult* res)
+ {
+ 	int idx;
+ 
+ 	if (strspn(arg, "0123456789") == strlen(arg))
+ 	{
+ 		/* if arg contains only digits, it's a column number */
+ 		idx = atoi(arg) - 1;
+ 		if (idx < 0  || idx >= PQnfields(res))
+ 		{
+ 			psql_error(_("Invalid column number: %s\n"), arg);
+ 			return -1;
+ 		}
+ 	}
+ 	else
+ 	{
+ 		int i;
+ 		idx = -1;
+ 		for (i=0; i < PQnfields(res); i++)
+ 		{
+ 			if (fieldnameCmp(arg, PQfname(res, i)) == 0)
+ 			{
+ 				if (idx>=0)
+ 				{
+ 					/* if another idx was already found for the same name */
+ 					psql_error(_("Ambiguous column name: %s\n"), arg);
+ 					return -1;
+ 				}
+ 				idx = i;
+ 			}
+ 		}
+ 		if (idx == -1)
+ 		{
+ 			psql_error(_("Invalid column name: %s\n"), arg);
+ 			return -1;
+ 		}
+ 	}
+ 	return idx;
+ }
+ 
+ bool
+ PrintResultsInCrossTab(PGresult* res,
+ 					   const char* opt_field_for_rows,    /* COLV or null */
+ 					   const char* opt_field_for_columns, /* [-+]COLH or null */
+ 					   const char* opt_field_for_labels)  /* COLL or NULL */
+ {
+ 	int		rn;
+ 	struct pivot_field	*piv_columns = NULL;
+ 	struct pivot_field	*piv_rows = NULL;
+ 	int		num_columns = 0;
+ 	int		num_rows = 0;
+ 	bool 	retval = false;
+ 	int		columns_sort_direction = 0; /* 1:ascending, 0:none, -1:descending */
+ 	int		field_for_labels;
+ 
+ 	/* 0-based index of the field whose distinct values will become COLUMN headers */
+ 	int		field_for_columns;
+ 
+ 	/* 0-based index of the field whose distinct values will become ROW headers */
+ 	int		field_for_rows;
+ 
+ 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ 		goto error_return;
+ 
+ 	if (PQnfields(res) < 2)
+ 	{
+ 		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	if (PQnfields(res) < 3 && opt_field_for_labels != NULL)
+ 	{
+ 		psql_error(_("The query must return at least three columns to be shown in crosstab\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	field_for_rows = (opt_field_for_rows != NULL)
+ 		? indexOfColumn(opt_field_for_rows, res)
+ 		: 0;
+ 
+ 	if (field_for_rows < 0)
+ 		goto error_return;
+ 
+ 	if (opt_field_for_columns == NULL)
+ 		field_for_columns = 1;
+ 	else
+ 	{
+ 		/*
+ 		 * descending sort is requested if the column reference is
+ 		 * preceded with a minus sign
+ 		 */
+ 		if (*opt_field_for_columns == '-')
+ 		{
+ 			columns_sort_direction = -1;
+ 			opt_field_for_columns++;
+ 		}
+ 		else if (*opt_field_for_columns == '+')
+ 		{
+ 			columns_sort_direction = 1;
+ 			opt_field_for_columns++;
+ 		}
+ 		field_for_columns = indexOfColumn(opt_field_for_columns, res);
+ 		if (field_for_columns < 0)
+ 			goto error_return;
+ 	}
+ 
+ 
+ 	if (field_for_columns == field_for_rows)
+ 	{
+ 		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	if (opt_field_for_labels)
+ 	{
+ 		field_for_labels = indexOfColumn(opt_field_for_labels, res);
+ 		if (field_for_labels == field_for_columns || field_for_labels == field_for_rows)
+ 		{
+ 			psql_error(_("The same column cannot be used for both vertical and horizontal headers or label\n"));
+ 			goto error_return;
+ 		}
+ 	}
+ 	else
+ 		field_for_labels = -1;
+ 
+ 	/*
+ 	 * First pass: accumulate row names and column names, each into their
+ 	 * array. Use client-side sort but only to build the set of DISTINCT
+ 	 * values. The final order displayed depends only on server-side
+ 	 * sorts.
+ 	 */
+ 	for (rn = 0; rn < PQntuples(res); rn++)
+ 	{
+ 		if (!PQgetisnull(res, rn, field_for_rows))
+ 		{
+ 			accumHeader(PQgetvalue(res, rn, field_for_rows), 
+ 				    NULL,
+ 						&num_rows,
+ 						&piv_rows,
+ 						rn);
+ 		}
+ 
+ 		if (!PQgetisnull(res, rn, field_for_columns))
+ 		{
+ 			accumHeader(PQgetvalue(res, rn, field_for_columns),
+ 				    field_for_labels >= 0 ? PQgetvalue(res, rn, field_for_labels) : NULL,
+ 						&num_columns,
+ 						&piv_columns,
+ 						rn);
+ 			if (num_columns > 1600)
+ 			{
+ 				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+ 				goto error_return;
+ 			}
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Second pass: sort the list of target columns on the server.
+ 	 */
+ 	if (!sortColumns(PQftype(res, field_for_columns),
+ 					 piv_columns,
+ 					 num_columns,
+ 					 columns_sort_direction))
+ 		goto error_return;
+ 
+ 	/*
+ 	 * Third pass: print the crosstab'ed results.
+ 	 */
+ 	printCrosstab(res,
+ 				  num_columns,
+ 				  piv_columns,
+ 				  field_for_columns,
+ 				  num_rows,
+ 				  piv_rows,
+ 				  field_for_rows,
+ 				  field_for_labels);
+ 
+ 	retval = true;
+ 
+ error_return:
+ 	pg_free(piv_columns);
+ 	pg_free(piv_rows);
+ 
+ 	return retval;
+ }
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index ...58cd33b
*** a/src/bin/psql/crosstabview.h
--- b/src/bin/psql/crosstabview.h
***************
*** 0 ****
--- 1,35 ----
+ /*
+  * psql - the PostgreSQL interactive terminal
+  *
+  * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+  *
+  * src/bin/psql/crosstabview.h
+  */
+ 
+ #ifndef CROSSTABVIEW_H
+ #define CROSSTABVIEW_H
+ 
+ struct pivot_field
+ {
+ 	/* Pointer obtained from PGgetvalue() for colV or colH */
+ 	char*	name;
+ 	char*	label;
+ 
+ 	/* Rank of the field in its list, starting at 0.
+ 	 * - For headers stacked vertically, rank=N means it's the
+ 	 *   Nth distinct field encountered when looping through rows
+ 	 *   in their initial order.
+ 	 * - For headers stacked horizontally, rank is obtained
+ 	 *   by server-side sorting in sortColumns()
+ 	 */
+ 	int		rank;
+ };
+ 
+ /* prototypes */
+ extern bool
+ PrintResultsInCrossTab(PGresult* res,
+ 					   const char* opt_field_for_rows,
+ 					   const char* opt_field_for_columns,
+ 					   const char* opt_field_for_labels);
+ 
+ #endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
new file mode 100644
index 5b63e76..e4a9ddf
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
*************** slashUsage(unsigned short int pager)
*** 175,180 ****
--- 175,181 ----
  	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
  	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
  	fprintf(output, _("  \\q                     quit psql\n"));
+ 	fprintf(output, _("  \\crosstabview [V H L]  execute query and display results in crosstab\n"));
  	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
  	fprintf(output, "\n");
  
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
new file mode 100644
index 190f2bc..4e4b5e7
*** a/src/bin/psql/print.c
--- b/src/bin/psql/print.c
*************** printQuery(const PGresult *result, const
*** 3239,3268 ****
  
  	for (i = 0; i < cont.ncolumns; i++)
  	{
- 		char		align;
- 		Oid			ftype = PQftype(result, i);
- 
- 		switch (ftype)
- 		{
- 			case INT2OID:
- 			case INT4OID:
- 			case INT8OID:
- 			case FLOAT4OID:
- 			case FLOAT8OID:
- 			case NUMERICOID:
- 			case OIDOID:
- 			case XIDOID:
- 			case CIDOID:
- 			case CASHOID:
- 				align = 'r';
- 				break;
- 			default:
- 				align = 'l';
- 				break;
- 		}
- 
  		printTableAddHeader(&cont, PQfname(result, i),
! 							opt->translate_header, align);
  	}
  
  	/* set cells */
--- 3239,3247 ----
  
  	for (i = 0; i < cont.ncolumns; i++)
  	{
  		printTableAddHeader(&cont, PQfname(result, i),
! 							opt->translate_header,
! 							column_type_alignment(PQftype(result, i)));
  	}
  
  	/* set cells */
*************** printQuery(const PGresult *result, const
*** 3304,3309 ****
--- 3283,3313 ----
  	printTableCleanup(&cont);
  }
  
+ char
+ column_type_alignment(Oid ftype)
+ {
+ 	char		align;
+ 
+ 	switch (ftype)
+ 	{
+ 		case INT2OID:
+ 		case INT4OID:
+ 		case INT8OID:
+ 		case FLOAT4OID:
+ 		case FLOAT8OID:
+ 		case NUMERICOID:
+ 		case OIDOID:
+ 		case XIDOID:
+ 		case CIDOID:
+ 		case CASHOID:
+ 			align = 'r';
+ 			break;
+ 		default:
+ 			align = 'l';
+ 			break;
+ 	}
+ 	return align;
+ }
  
  void
  setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
new file mode 100644
index df514cf..409f3e5
*** a/src/bin/psql/print.h
--- b/src/bin/psql/print.h
*************** extern FILE *PageOutput(int lines, const
*** 171,177 ****
  extern void ClosePager(FILE *pagerpipe);
  
  extern void html_escaped_print(const char *in, FILE *fout);
! 
  extern void printTableInit(printTableContent *const content,
  			   const printTableOpt *opt, const char *title,
  			   const int ncolumns, const int nrows);
--- 171,177 ----
  extern void ClosePager(FILE *pagerpipe);
  
  extern void html_escaped_print(const char *in, FILE *fout);
! extern char column_type_alignment(Oid);
  extern void printTableInit(printTableContent *const content,
  			   const printTableOpt *opt, const char *title,
  			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
new file mode 100644
index 1885bb1..429d6fb
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
*************** typedef struct _psqlSettings
*** 90,95 ****
--- 90,99 ----
  
  	char	   *gfname;			/* one-shot file output argument for \g */
  	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+ 	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+ 	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+ 	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
+ 	char		*crosstabview_col_L;  /* one-shot \crosstabview 3nd argument */
  
  	bool		notty;			/* stdin or stdout is not a tty (as determined
  								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
new file mode 100644
index b58ec14..0f79e0a
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
*************** psql_completion(const char *text, int st
*** 936,943 ****
  
  	static const char *const backslash_commands[] = {
  		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
! 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
! 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
  		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
  		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\drds", "\\ds", "\\dS",
  		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy",
--- 936,943 ----
  
  	static const char *const backslash_commands[] = {
  		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
! 		"\\crosstabview", "\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp",
! 		"\\dD", "\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
  		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
  		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\drds", "\\ds", "\\dS",
  		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy",
#47Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#46)
Re: [patch] Proposal for \rotate in psql

postgres=# \crosstabview 4 +month label

Maybe using optional int order column instead label is better - then you
can do sort on client side

so the syntax can be "\crosstabview VCol [+/-]HCol [[+-]HOrderCol]

Regards

Pavel

Show quoted text

┌──────────────────────────┬───────┬───────┬────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ customer │ led │ úno │ bře │ dub │ kvě │
čen │ čec │ srp │ zář │ říj │ lis │

╞══════════════════════════╪═══════╪═══════╪════════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╡
│ A********** │ │ │ │ │
│ │ │ │ │ 13000 │ │
│ A******** │ │ │ 8000 │ │
│ │ │ │ │ │ │
│ B***** │ │ │ │ │
│ │ │ │ │ │ 3200 │
│ B*********************** │ │ │ │ │
│ │ │ │ 26200 │ │ │
│ B********* │ │ │ │ │
│ │ 14000 │ │ │ │ │
│ C********** │ │ │ │ 7740 │
│ │ │ │ │ │ │
│ C*** │ │ │ │ │
│ │ │ │ 26000 │ │ │
│ C***** │ │ │ │ 12000 │
│ │ │ │ │ │ │
│ G******* │ 30200 │ 26880 │ 13536 │ 39360 │ 60480 │
54240 │ 44160 │ 16320 │ 29760 │ 22560 │ 20160 │
│ G*************** │ │ │ │ │ │
25500 │ │ │ │ │ │
│ G********** │ │ │ │ │ │
16000 │ │ │ │ │ │
│ I************* │ │ │ │ │
│ │ │ 27920 │ │ │ │
│ i**** │ │ │ │ 13500 │
│ │ │ │ │ │ │
│ n********* │ │ │ │ │
│ │ 12600 │ │ │ │ │
│ Q** │ │ │ │ │ 16700
│ │ │ │ │ │ │
│ S******* │ │ │ │ │
│ │ 8000 │ │ │ │ │
│ S******* │ │ │ │ │ 5368
│ │ │ │ │ │ │
│ s******* │ │ │ 5000 │ 3200 │
│ │ │ │ │ │ │

└──────────────────────────┴───────┴───────┴────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
(18 rows)

Regards

Pavel

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#48Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#47)
1 attachment(s)
Re: [patch] Proposal for \rotate in psql

2015-12-10 19:29 GMT+01:00 Pavel Stehule <pavel.stehule@gmail.com>:

postgres=# \crosstabview 4 +month label

Maybe using optional int order column instead label is better - then you
can do sort on client side

so the syntax can be "\crosstabview VCol [+/-]HCol [[+-]HOrderCol]

here is patch - supported syntax: \crosstabview VCol [+/-]HCol [HOrderCol]

Order column should to contains any numeric value. Values are sorted on
client side

Regards

Pavel

Show quoted text

Regards

Pavel

┌──────────────────────────┬───────┬───────┬────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ customer │ led │ úno │ bře │ dub │ kvě │
čen │ čec │ srp │ zář │ říj │ lis │

╞══════════════════════════╪═══════╪═══════╪════════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╡
│ A********** │ │ │ │ │
│ │ │ │ │ 13000 │ │
│ A******** │ │ │ 8000 │ │
│ │ │ │ │ │ │
│ B***** │ │ │ │ │
│ │ │ │ │ │ 3200 │
│ B*********************** │ │ │ │ │
│ │ │ │ 26200 │ │ │
│ B********* │ │ │ │ │
│ │ 14000 │ │ │ │ │
│ C********** │ │ │ │ 7740 │
│ │ │ │ │ │ │
│ C*** │ │ │ │ │
│ │ │ │ 26000 │ │ │
│ C***** │ │ │ │ 12000 │
│ │ │ │ │ │ │
│ G******* │ 30200 │ 26880 │ 13536 │ 39360 │ 60480 │
54240 │ 44160 │ 16320 │ 29760 │ 22560 │ 20160 │
│ G*************** │ │ │ │ │ │
25500 │ │ │ │ │ │
│ G********** │ │ │ │ │ │
16000 │ │ │ │ │ │
│ I************* │ │ │ │ │
│ │ │ 27920 │ │ │ │
│ i**** │ │ │ │ 13500 │
│ │ │ │ │ │ │
│ n********* │ │ │ │ │
│ │ 12600 │ │ │ │ │
│ Q** │ │ │ │ │ 16700
│ │ │ │ │ │ │
│ S******* │ │ │ │ │
│ │ 8000 │ │ │ │ │
│ S******* │ │ │ │ │ 5368
│ │ │ │ │ │ │
│ s******* │ │ │ 5000 │ 3200 │
│ │ │ │ │ │ │

└──────────────────────────┴───────┴───────┴────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
(18 rows)

Regards

Pavel

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-rotate-v6.difftext/plain; charset=US-ASCII; name=psql-rotate-v6.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
new file mode 100644
index 47e9da2..a45f4b8
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** lo_import 152801
*** 2461,2466 ****
--- 2461,2555 ----
          </listitem>
        </varlistentry>
  
+       <varlistentry>
+         <term><literal>\crosstabview [ <replaceable class="parameter">colV</replaceable>  [-|+]<replaceable class="parameter">colH</replaceable> ] </literal></term>
+         <listitem>
+         <para>
+         Execute the current query buffer (like <literal>\g</literal>) and shows the results
+         inside a crosstab grid. The output column <replaceable class="parameter">colV</replaceable>
+         becomes a vertical header and the output column
+         <replaceable class="parameter">colH</replaceable> becomes a horizontal header.
+         The results for the other output columns are projected inside the grid.
+         </para>
+ 
+         <para>
+         <replaceable class="parameter">colV</replaceable>
+         and <replaceable class="parameter">colH</replaceable> can indicate a
+         column position (starting at 1), or a column name. Normal case folding
+         and quoting rules apply on column names. By default,
+         <replaceable class="parameter">colV</replaceable> is column 1
+         and <replaceable class="parameter">colH</replaceable> is column 2.
+         A query having only one output column cannot be viewed in crosstab, and
+         <replaceable class="parameter">colH</replaceable> must differ from
+         <replaceable class="parameter">colV</replaceable>.
+         </para>
+ 
+         <para>
+         The vertical header, displayed as the leftmost column,
+         contains the set of all distinct values found in
+         column <replaceable class="parameter">colV</replaceable>, in the order
+         of their first appearance in the query results.
+         </para>
+         <para>
+         The horizontal header, displayed as the first row,
+         contains the set of all distinct non-null values found in
+         column <replaceable class="parameter">colH</replaceable>.  They come
+         by default in their order of appearance in the query results, or in ascending
+         order if a plus (+) sign precedes <replaceable class="parameter">colH</replaceable>,
+         or in descending order if it's a minus (-) sign.
+         </para>
+ 
+         <para>
+         The query results being tuples of <literal>N</literal> columns
+         (including <replaceable class="parameter">colV</replaceable> and
+         <replaceable class="parameter">colH</replaceable>),
+         for each distinct value <literal>x</literal> of
+         <replaceable class="parameter">colH</replaceable>
+         and each distinct value <literal>y</literal> of
+         <replaceable class="parameter">colV</replaceable>,
+         a cell located at the intersection <literal>(x,y)</literal> in the grid
+         has contents determined by these rules:
+         <itemizedlist>
+         <listitem>
+         <para>
+          if there is no corresponding row in the results such that the value
+          for <replaceable class="parameter">colH</replaceable>
+          is <literal>x</literal> and the value
+          for <replaceable class="parameter">colV</replaceable>
+          is <literal>y</literal>, the cell is empty.
+         </para>
+         </listitem>
+ 
+         <listitem>
+         <para>
+          if there is exactly one row such that the value
+          for <replaceable class="parameter">colH</replaceable>
+          is <literal>x</literal> and the value
+          for <replaceable class="parameter">colV</replaceable>
+          is <literal>y</literal>, then the <literal>N-2</literal> other
+          columns are displayed in the cell, separated between each other by
+          a space character if needed.
+ 
+          If <literal>N=2</literal>, the letter <literal>X</literal> is displayed in the cell as
+          if a virtual third column contained that character.
+         </para>
+         </listitem>
+ 
+         <listitem>
+         <para>
+          if there are several corresponding rows, the behavior is identical to one row
+          except that the values coming from different rows are stacked
+          vertically, rows being separated by newline characters inside
+          the same cell.
+         </para>
+         </listitem>
+ 
+         </itemizedlist>
+         </para>
+ 
+         </listitem>
+       </varlistentry>
+ 
  
        <varlistentry>
          <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
new file mode 100644
index f1336d5..9cb0c4a
*** a/src/bin/psql/Makefile
--- b/src/bin/psql/Makefile
*************** override CPPFLAGS := -I. -I$(srcdir) -I$
*** 23,29 ****
  OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
  	startup.o prompt.o variables.o large_obj.o print.o describe.o \
  	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
! 	sql_help.o \
  	$(WIN32RES)
  
  
--- 23,29 ----
  OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
  	startup.o prompt.o variables.o large_obj.o print.o describe.o \
  	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
! 	sql_help.o crosstabview.o \
  	$(WIN32RES)
  
  
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
new file mode 100644
index cf6876b..c743f57
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 46,51 ****
--- 46,52 ----
  #include "mainloop.h"
  #include "print.h"
  #include "psqlscan.h"
+ #include "crosstabview.h"
  #include "settings.h"
  #include "variables.h"
  
*************** exec_command(const char *cmd,
*** 1081,1086 ****
--- 1082,1119 ----
  		free(pw2);
  	}
  
+ 	/* \crosstabview -- execute a query and display results in crosstab */
+ 	else if (strcmp(cmd, "crosstabview") == 0)
+ 	{
+ 		char	*opt1,
+ 				*opt2,
+ 				*opt3;
+ 
+ 		opt1 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 		opt2 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 		opt3 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 
+ 		if (opt1 && !opt2)
+ 		{
+ 			psql_error(_("\\%s: missing second argument\n"), cmd);
+ 			success = false;
+ 		}
+ 		else
+ 		{
+ 			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1) : NULL;
+ 			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2) : NULL;
+ 			pset.crosstabview_col_O = opt3 ? pg_strdup(opt3) : NULL;
+ 			pset.crosstabview_output = true;
+ 			status = PSQL_CMD_SEND;
+ 		}
+ 
+ 		free(opt1);
+ 		free(opt2);
+ 	}
+ 
  	/* \prompt -- prompt and set variable */
  	else if (strcmp(cmd, "prompt") == 0)
  	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index a287eee..26cd233
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 24,29 ****
--- 24,30 ----
  #include "command.h"
  #include "copy.h"
  #include "mbprint.h"
+ #include "crosstabview.h"
  
  
  static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
*************** SendQuery(const char *query)
*** 1076,1085 ****
  
  		/* but printing results isn't: */
  		if (OK && results)
! 			OK = PrintQueryResults(results);
  	}
  	else
  	{
  		/* Fetch-in-segments mode */
  		OK = ExecQueryUsingCursor(query, &elapsed_msec);
  		ResetCancelConn();
--- 1077,1102 ----
  
  		/* but printing results isn't: */
  		if (OK && results)
! 		{
! 			if (pset.crosstabview_output)
! 				OK = PrintResultsInCrossTab(results,
! 											pset.crosstabview_col_V,
! 											pset.crosstabview_col_H,
! 											pset.crosstabview_col_O);
! 			else
! 				OK = PrintQueryResults(results);
! 		}
  	}
  	else
  	{
+ 		if (pset.crosstabview_output)
+ 		{
+ 			psql_error("\\crosstabview cannot be executed since active FETCH_COUNT = %d\n",
+ 					pset.fetch_count);
+ 			OK = false;
+ 			goto sendquery_cleanup;
+ 		}
+ 
  		/* Fetch-in-segments mode */
  		OK = ExecQueryUsingCursor(query, &elapsed_msec);
  		ResetCancelConn();
*************** sendquery_cleanup:
*** 1192,1197 ****
--- 1209,1231 ----
  		pset.gset_prefix = NULL;
  	}
  
+ 	pset.crosstabview_output = false;
+ 	if (pset.crosstabview_col_V)
+ 	{
+ 		free(pset.crosstabview_col_V);
+ 		pset.crosstabview_col_V = NULL;
+ 	}
+ 	if (pset.crosstabview_col_H)
+ 	{
+ 		free(pset.crosstabview_col_H);
+ 		pset.crosstabview_col_H = NULL;
+ 	}
+ 	if (pset.crosstabview_col_O)
+ 	{
+ 		free(pset.crosstabview_col_O);
+ 		pset.crosstabview_col_O = NULL;
+ 	}
+ 
  	return OK;
  }
  
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index ...c11c628
*** a/src/bin/psql/crosstabview.c
--- b/src/bin/psql/crosstabview.c
***************
*** 0 ****
--- 1,749 ----
+ /*
+  * psql - the PostgreSQL interactive terminal
+  *
+  * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+  *
+  * src/bin/psql/crosstabview.c
+  */
+ 
+ 
+ #include "common.h"
+ #include "pqexpbuffer.h"
+ #include "crosstabview.h"
+ #include "settings.h"
+ 
+ #include "catalog/pg_type.h"
+ 
+ #include <string.h>
+ 
+ static int
+ headerCompare(const void *a, const void *b)
+ {
+ 	return strcmp( ((struct pivot_field*)a)->name,
+ 				   ((struct pivot_field*)b)->name);
+ }
+ 
+ static void
+ accumHeader(char* name, char* outer_rank, int* count, struct pivot_field **sorted_tab, int row_number)
+ {
+ 	struct pivot_field *p;
+ 
+ 	/*
+ 	 * Search for name in sorted_tab. If it doesn't exist, insert it,
+ 	 * otherwise do nothing.
+ 	 */
+ 
+ 	if (*count >= 1)
+ 	{
+ 		p = (struct pivot_field*) bsearch(&name,
+ 										  *sorted_tab,
+ 										  *count,
+ 										  sizeof(struct pivot_field),
+ 										  headerCompare);
+ 	}
+ 	else
+ 		p=NULL;
+ 
+ 	if (!p)
+ 	{
+ 		*sorted_tab = pg_realloc(*sorted_tab, sizeof(struct pivot_field) * (1+*count));
+ 		(*sorted_tab)[*count].name = name;
+ 		(*sorted_tab)[*count].outer_rank = outer_rank ? strtod(outer_rank, NULL) : -1.0;
+ 		(*sorted_tab)[*count].rank = *count;
+ 		(*count)++;
+ 
+ 		qsort(*sorted_tab,
+ 			  *count,
+ 			  sizeof(struct pivot_field),
+ 			  headerCompare);
+ 	}
+ }
+ 
+ /*
+  * Auxilary structure using for sorting only
+  */
+ struct p_rank
+ {
+ 	double		outer_rank;
+ 	int	old_pos;
+ };
+ 
+ static int
+ RankCompare(const void *a, const void *b)
+ {
+ 	return ((struct p_rank*)a)->outer_rank - ((struct p_rank*)b)->outer_rank ;
+ }
+ 
+ static int
+ RankCompareDesc(const void *a, const void *b)
+ {
+ 	return ((struct p_rank*) b)->outer_rank - ((struct p_rank*) a)->outer_rank;
+ }
+ 
+ 
+ /*
+  * Resort header by rank, ensure uniq outer ranks
+  */
+ static bool
+ sortColumnsByRank(struct pivot_field *columns, int nb_cols, int direction)
+ {
+ 	int		i;
+ 	struct p_rank	*p_ranks;
+ 	bool		retval = true;
+ 
+ 	p_ranks = (struct p_rank *) pg_malloc(nb_cols * sizeof(struct p_rank));
+ 
+ 	for (i = 0; i < nb_cols; i++)
+ 	{
+ 		p_ranks[i].outer_rank = columns[i].outer_rank;
+ 		p_ranks[i].old_pos = i;
+ 	}
+ 
+ 	if (direction >= 0)
+ 		qsort(p_ranks,
+ 			  nb_cols,
+ 			  sizeof(struct p_rank),
+ 			  RankCompare);
+ 	else
+ 		qsort(p_ranks,
+ 			  nb_cols,
+ 			  sizeof(struct p_rank),
+ 			  RankCompareDesc);
+ 
+ 	for (i = 0; i < nb_cols; i++)
+ 	{
+ 		/* two adjecent outer sorted ranks should be different */
+ 		if (i > 0 && p_ranks[i].outer_rank == p_ranks[i - 1].outer_rank)
+ 		{
+ 			psql_error("Outer ranks are not unique.");
+ 			goto cleanup;
+ 		}
+ 
+ 		columns[p_ranks[i].old_pos].rank = i;
+ 	}
+ 
+ 	retval = true;
+ 
+ cleanup:
+ 
+ 	pg_free(p_ranks);
+ 
+ 	return retval;
+ }
+ 
+ /*
+  * Send a query to sort all column values cast to the Oid passed in a VALUES clause
+  */
+ static bool
+ sortColumns(Oid coltype, struct pivot_field *columns, int nb_cols, int direction)
+ {
+ 	bool retval = false;
+ 	PGresult *res = NULL;
+ 	PQExpBufferData query;
+ 	int i;
+ 	Oid *param_types;
+ 	const char** param_values;
+ 	int* param_lengths;
+ 	int* param_formats;
+ 
+ 	if (nb_cols < 2 || direction==0)
+ 		return true;					/* nothing to sort */
+ 
+ 	param_types = (Oid*) pg_malloc(nb_cols*sizeof(Oid));
+ 	param_values = (const char**) pg_malloc(nb_cols*sizeof(char*));
+ 	param_lengths = (int*) pg_malloc(nb_cols*sizeof(int));
+ 	param_formats = (int*) pg_malloc(nb_cols*sizeof(int));
+ 
+ 	initPQExpBuffer(&query);
+ 
+ 	/*
+ 	 * The query returns the original position of each value in our list,
+ 	 * ordered by its new position. The value itself is not returned.
+ 	 */
+ 	appendPQExpBuffer(&query, "SELECT n FROM (VALUES");
+ 
+ 	for (i=1; i <= nb_cols; i++)
+ 	{
+ 		if (i < nb_cols)
+ 			appendPQExpBuffer(&query, "($%d,%d),", i, i);
+ 		else
+ 		{
+ 			appendPQExpBuffer(&query, "($%d,%d)) AS l(x,n) ORDER BY x", i, i);
+ 			if (direction < 0)
+ 				appendPQExpBuffer(&query, " DESC");
+ 		}
+ 
+ 		param_types[i-1] = coltype;
+ 		param_values[i-1] = columns[i-1].name;
+ 		param_lengths[i-1] = strlen(columns[i-1].name);
+ 		param_formats[i-1] = 0;
+ 	}
+ 
+ 	res = PQexecParams(pset.db,
+ 					   query.data,
+ 					   nb_cols,
+ 					   param_types,
+ 					   param_values,
+ 					   param_lengths,
+ 					   param_formats,
+ 					   0);
+ 
+ 	if (res)
+ 	{
+ 		ExecStatusType status = PQresultStatus(res);
+ 		if (status == PGRES_TUPLES_OK)
+ 		{
+ 			for (i=0; i < PQntuples(res); i++)
+ 			{
+ 				int old_pos = atoi(PQgetvalue(res, i, 0));
+ 
+ 				if (old_pos < 1 || old_pos > nb_cols || i >= nb_cols)
+ 				{
+ 					/*
+ 					 * A position outside of the range is normally impossible.
+ 					 * If this happens, we're facing a malfunctioning or hostile
+ 					 * server or middleware.
+ 					 */
+ 					psql_error(_("Unexpected value when sorting horizontal headers"));
+ 					goto cleanup;
+ 				}
+ 				else
+ 				{
+ 					columns[old_pos-1].rank = i;
+ 				}
+ 			}
+ 		}
+ 		else
+ 		{
+ 			psql_error(_("Query error when sorting horizontal headers: %s"),
+ 					   PQerrorMessage(pset.db));
+ 			goto cleanup;
+ 		}
+ 	}
+ 
+ 	retval = true;
+ 
+ cleanup:
+ 	termPQExpBuffer(&query);
+ 	if (res)
+ 		PQclear(res);
+ 	pg_free(param_types);
+ 	pg_free(param_values);
+ 	pg_free(param_lengths);
+ 	pg_free(param_formats);
+ 	return retval;
+ }
+ 
+ static void
+ printCrosstab(const PGresult *results,
+ 			  int num_columns,
+ 			  struct pivot_field *piv_columns,
+ 			  int field_for_columns,
+ 			  int num_rows,
+ 			  struct pivot_field *piv_rows,
+ 			  int field_for_rows,
+ 			  int field_for_ranks)
+ {
+ 	printQueryOpt popt = pset.popt;
+ 	printTableContent cont;
+ 	int	i, j, rn;
+ 	char col_align = 'l';		/* alignment for values inside the grid */
+ 	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+ 	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+ 								 *  in this function, when cells cannot simply point to
+ 								 *  PQgetvalue(results, ...) */
+ 
+ 	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+ 
+ 	/* Step 1: set target column names (horizontal header) */
+ 
+ 	/* The name of the first column is kept unchanged by the pivoting */
+ 	printTableAddHeader(&cont,
+ 						PQfname(results, field_for_rows),
+ 						false,
+ 						column_type_alignment(PQftype(results, field_for_rows)));
+ 
+ 	/*
+ 	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+ 	 *  associating each piv_columns[].rank to its index in piv_columns.
+ 	 *  This avoids an O(N^2) loop later
+ 	 */
+ 	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		horiz_map[piv_columns[i].rank] = i;
+ 	}
+ 
+ 	/*
+ 	 * In the case of 3 output columns, the contents in the cells are exactly
+ 	 * the contents of the "value" column (3rd column by default), so their
+ 	 * alignment is determined by PQftype(). Otherwise the contents are
+ 	 * made-up strings, so the alignment is 'l'
+ 	 */
+ 	if (PQnfields(results) == 3)
+ 	{
+ 		int colnum;				/* column placed inside the grid */
+ 		/*
+ 		 * find colnum in the permutations of (0,1,2) where colnum is
+ 		 * neither field_for_rows nor field_for_columns
+ 		 */
+ 		switch (field_for_rows)
+ 		{
+ 		case 0:
+ 			colnum = (field_for_columns == 1) ? 2 : 1;
+ 			break;
+ 		case 1:
+ 			colnum = (field_for_columns == 0) ? 2: 0;
+ 			break;
+ 		default:				/* should be always 2 */
+ 			colnum = (field_for_columns == 0) ? 1: 0;
+ 			break;
+ 		}
+ 		col_align = column_type_alignment(PQftype(results, colnum));
+ 	}
+ 	else
+ 		col_align = 'l';
+ 
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		printTableAddHeader(&cont,
+ 							piv_columns[horiz_map[i]].name,
+ 							false,
+ 							col_align);
+ 	}
+ 	pg_free(horiz_map);
+ 
+ 	/* Step 2: set row names in the first output column (vertical header) */
+ 	for (i = 0; i < num_rows; i++)
+ 	{
+ 		int k = piv_rows[i].rank;
+ 		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+ 		/* Initialize all cells inside the grid to an empty value */
+ 		for (j = 0; j < num_columns; j++)
+ 			cont.cells[k*(num_columns+1)+j+1] = "";
+ 	}
+ 	cont.cellsadded = num_rows * (num_columns+1);
+ 
+ 	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+ 
+ 	/* Step 3: set all the cells "inside the grid" */
+ 	for (rn = 0; rn < PQntuples(results); rn++)
+ 	{
+ 		char* row_name;
+ 		char* col_name;
+ 		int row_number;
+ 		int col_number;
+ 		struct pivot_field *p;
+ 
+ 		row_number = col_number = -1;
+ 		/* Find target row */
+ 		if (!PQgetisnull(results, rn, field_for_rows))
+ 		{
+ 			row_name = PQgetvalue(results, rn, field_for_rows);
+ 			p = (struct pivot_field*) bsearch(&row_name,
+ 											  piv_rows,
+ 											  num_rows,
+ 											  sizeof(struct pivot_field),
+ 											  headerCompare);
+ 			if (p)
+ 				row_number = p->rank;
+ 		}
+ 
+ 		/* Find target column */
+ 		if (!PQgetisnull(results, rn, field_for_columns))
+ 		{
+ 			col_name = PQgetvalue(results, rn, field_for_columns);
+ 			p = (struct pivot_field*) bsearch(&col_name,
+ 											  piv_columns,
+ 											  num_columns,
+ 											  sizeof(struct pivot_field),
+ 											  headerCompare);
+ 			if (p)
+ 				col_number = p->rank;
+ 		}
+ 
+ 		/* Place value into cell */
+ 		if (col_number>=0 && row_number>=0)
+ 		{
+ 			int idx = 1 + col_number + row_number*(num_columns+1);
+ 			int src_col = 0;			/* column number in source result */
+ 			int k = 0;
+ 
+ 			do {
+ 				char *content;
+ 
+ 				if (PQnfields(results) == 2)
+ 				{
+ 					/*
+ 					  special case: when the source has only 2 columns, use a
+ 					  X (cross/checkmark) for the cell content, and set
+ 					  src_col to a virtual additional column.
+ 					*/
+ 					content = "X";
+ 					src_col = 3;
+ 				}
+ 				else if (src_col == field_for_rows || src_col == field_for_columns
+ 					    || src_col == field_for_ranks)
+ 				{
+ 					/*
+ 					  The source values that produce headers are not processed
+ 					  in this loop, only the values that end up inside the grid.
+ 					*/
+ 					src_col++;
+ 					continue;
+ 				}
+ 				else
+ 				{
+ 					content = (!PQgetisnull(results, rn, src_col)) ?
+ 						PQgetvalue(results, rn, src_col) :
+ 						(popt.nullPrint ? popt.nullPrint : "");
+ 				}
+ 
+ 				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+ 				{
+ 					/*
+ 					 * Multiple values for the same (row,col) are projected
+ 					 * into the same cell. When this happens, separate the
+ 					 * previous content of the cell from the new value by a
+ 					 * newline.
+ 					 */
+ 					int content_size =
+ 						strlen(cont.cells[idx])
+ 						+ 2 			/* room for [CR],LF or space */
+ 						+ strlen(content)
+ 						+ 1;			/* '\0' */
+ 					char *new_content;
+ 
+ 					/*
+ 					 * idx2 is an index into allocated_cells. It differs from
+ 					 * idx (index into cont.cells), because vertical and
+ 					 * horizontal headers are included in `cont.cells` but
+ 					 * excluded from allocated_cells.
+ 					 */
+ 					int idx2 = (row_number * num_columns) + col_number;
+ 
+ 					if (allocated_cells[idx2] != NULL)
+ 					{
+ 						new_content = pg_realloc(allocated_cells[idx2], content_size);
+ 					}
+ 					else
+ 					{
+ 						/*
+ 						 * At this point, cont.cells[idx] still contains a
+ 						 * PQgetvalue() pointer.  Just after, it will contain
+ 						 * a new pointer maintained in allocated_cells[], and
+ 						 * freed at the end of this function.
+ 						 */
+ 						new_content = pg_malloc(content_size);
+ 						strcpy(new_content, cont.cells[idx]);
+ 					}
+ 					cont.cells[idx] = new_content;
+ 					allocated_cells[idx2] = new_content;
+ 
+ 					/*
+ 					 * Contents that are on adjacent columns in the source results get
+ 					 * separated by one space in the target.
+ 					 * Contents that are on different rows in the source get
+ 					 * separated by newlines in the target.
+ 					 */
+ 					if (k==0)
+ 						strcat(new_content, "\n");
+ 					else
+ 						strcat(new_content, " ");
+ 					strcat(new_content, content);
+ 				}
+ 				else
+ 				{
+ 					cont.cells[idx] = content;
+ 				}
+ 				k++;
+ 				src_col++;
+ 			} while (src_col < PQnfields(results));
+ 		}
+ 	}
+ 
+ 	printTable(&cont, pset.queryFout, false, pset.logfile);
+ 	printTableCleanup(&cont);
+ 
+ 
+ 	for (i=0; i < num_rows * num_columns; i++)
+ 	{
+ 		if (allocated_cells[i] != NULL)
+ 			pg_free(allocated_cells[i]);
+ 	}
+ 
+ 	pg_free(allocated_cells);
+ }
+ 
+ /*
+  * Compare a user-supplied argument against a field name obtained by PQfname(),
+  * which is already case-folded.
+  * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+  * do a case-sensitive comparison with these rules:
+  * - double quotes enclosing 'arg' are filtered out
+  * - double quotes inside 'arg' are expected to be doubled
+  */
+ static int
+ fieldnameCmp(const char* arg, const char* fieldname)
+ {
+ 	const unsigned char* p = (const unsigned char*) arg;
+ 	const unsigned char* f = (const unsigned char*) fieldname;
+ 	unsigned char c;
+ 
+ 	if (*p++ != '"')
+ 		return pg_strcasecmp(arg, fieldname);
+ 
+ 	while ((c=*p++))
+ 	{
+ 		if (c=='"')
+ 		{
+ 			if (*p=='"')
+ 				p++;			/* skip second quote and continue */
+ 			else if (*p=='\0')
+ 				return *p-*f;		/* p finishes before f or is identical */
+ 
+ 		}
+ 		if (*f=='\0')
+ 			return 1;			/* f finishes before p */
+ 		if (c!=*f)
+ 			return c-*f;
+ 		f++;
+ 	}
+ 	return (*f=='\0') ? 0 : 1;
+ }
+ 
+ 
+ /*
+  * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+  * Returns:
+  *  on success, the 0-based index of the column
+  *  or -1 if the column number or name is not found in the result's structure,
+  *        or if it's ambiguous (arg corresponding to several columns)
+  */
+ static int
+ indexOfColumn(const char* arg, PGresult* res)
+ {
+ 	int idx;
+ 
+ 	if (strspn(arg, "0123456789") == strlen(arg))
+ 	{
+ 		/* if arg contains only digits, it's a column number */
+ 		idx = atoi(arg) - 1;
+ 		if (idx < 0  || idx >= PQnfields(res))
+ 		{
+ 			psql_error(_("Invalid column number: %s\n"), arg);
+ 			return -1;
+ 		}
+ 	}
+ 	else
+ 	{
+ 		int i;
+ 		idx = -1;
+ 		for (i=0; i < PQnfields(res); i++)
+ 		{
+ 			if (fieldnameCmp(arg, PQfname(res, i)) == 0)
+ 			{
+ 				if (idx>=0)
+ 				{
+ 					/* if another idx was already found for the same name */
+ 					psql_error(_("Ambiguous column name: %s\n"), arg);
+ 					return -1;
+ 				}
+ 				idx = i;
+ 			}
+ 		}
+ 		if (idx == -1)
+ 		{
+ 			psql_error(_("Invalid column name: %s\n"), arg);
+ 			return -1;
+ 		}
+ 	}
+ 	return idx;
+ }
+ 
+ bool
+ PrintResultsInCrossTab(PGresult* res,
+ 					   const char* opt_field_for_rows,    /* COLV or null */
+ 					   const char* opt_field_for_columns, /* [-+]COLH or null */
+ 					   const char* opt_field_for_ranks)   /* COLL or NULL */
+ {
+ 	int		rn;
+ 	struct pivot_field	*piv_columns = NULL;
+ 	struct pivot_field	*piv_rows = NULL;
+ 	int		num_columns = 0;
+ 	int		num_rows = 0;
+ 	bool 	retval = false;
+ 	int		columns_sort_direction = 0; /* 1:ascending, 0:none, -1:descending */
+ 	int		field_for_ranks;
+ 
+ 	/* 0-based index of the field whose distinct values will become COLUMN headers */
+ 	int		field_for_columns;
+ 
+ 	/* 0-based index of the field whose distinct values will become ROW headers */
+ 	int		field_for_rows;
+ 
+ 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ 		goto error_return;
+ 
+ 	if (PQnfields(res) < 2)
+ 	{
+ 		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	if (PQnfields(res) < 3 && opt_field_for_ranks != NULL)
+ 	{
+ 		psql_error(_("The query must return at least three columns to be shown in crosstab\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	field_for_rows = (opt_field_for_rows != NULL)
+ 		? indexOfColumn(opt_field_for_rows, res)
+ 		: 0;
+ 
+ 	if (field_for_rows < 0)
+ 		goto error_return;
+ 
+ 	if (opt_field_for_columns == NULL)
+ 		field_for_columns = 1;
+ 	else
+ 	{
+ 		/*
+ 		 * descending sort is requested if the column reference is
+ 		 * preceded with a minus sign
+ 		 */
+ 		if (*opt_field_for_columns == '-')
+ 		{
+ 			columns_sort_direction = -1;
+ 			opt_field_for_columns++;
+ 		}
+ 		else if (*opt_field_for_columns == '+')
+ 		{
+ 			columns_sort_direction = 1;
+ 			opt_field_for_columns++;
+ 		}
+ 		field_for_columns = indexOfColumn(opt_field_for_columns, res);
+ 		if (field_for_columns < 0)
+ 			goto error_return;
+ 	}
+ 
+ 	if (field_for_columns == field_for_rows)
+ 	{
+ 		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	if (opt_field_for_ranks)
+ 	{
+ 		Oid		ftype;
+ 
+ 		field_for_ranks = indexOfColumn(opt_field_for_ranks, res);
+ 		if (field_for_ranks == field_for_columns || field_for_ranks == field_for_rows)
+ 		{
+ 			psql_error(_("The same column cannot be used for both vertical and horizontal headers or ranks\n"));
+ 			goto error_return;
+ 		}
+ 
+ 		/* the rank column should be integer */
+ 		ftype = PQftype(res, field_for_ranks);
+ 		switch (ftype)
+ 		{
+ 			case INT2OID:
+ 			case INT4OID:
+ 			case INT8OID:
+ 			case FLOAT4OID:
+ 			case FLOAT8OID:
+ 			case NUMERICOID:
+ 				break;
+ 			default:
+ 				psql_error(_("The rank value should be short int or int.\n"));
+ 				goto error_return;
+ 		}
+ 	}
+ 	else
+ 		field_for_ranks = -1;
+ 
+ 	/*
+ 	 * First pass: accumulate row names and column names, each into their
+ 	 * array. Use client-side sort but only to build the set of DISTINCT
+ 	 * values. The final order displayed depends only on server-side
+ 	 * sorts.
+ 	 */
+ 	for (rn = 0; rn < PQntuples(res); rn++)
+ 	{
+ 		if (!PQgetisnull(res, rn, field_for_rows))
+ 		{
+ 			accumHeader(PQgetvalue(res, rn, field_for_rows), 
+ 				    NULL,
+ 						&num_rows,
+ 						&piv_rows,
+ 						rn);
+ 		}
+ 
+ 		if (!PQgetisnull(res, rn, field_for_columns))
+ 		{
+ 			if (field_for_ranks >= 0)
+ 			{
+ 				/* ensure not null values */
+ 				if (PQgetisnull(res, rn, field_for_ranks))
+ 				{
+ 					psql_error(_("The rank value should be short int or int.\n"));
+ 					goto error_return;
+ 				}
+ 
+ 				accumHeader(PQgetvalue(res, rn, field_for_columns),
+ 					    PQgetvalue(res, rn, field_for_ranks),
+ 							&num_columns,
+ 							&piv_columns,
+ 							rn);
+ 			}
+ 			else
+ 				accumHeader(PQgetvalue(res, rn, field_for_columns),
+ 					    NULL,
+ 							&num_columns,
+ 							&piv_columns,
+ 							rn);
+ 			if (num_columns > 1600)
+ 			{
+ 				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+ 				goto error_return;
+ 			}
+ 		}
+ 	}
+ 
+ 	if (field_for_ranks >= 0)
+ 	{
+ 		if (!sortColumnsByRank(piv_columns, num_columns,
+ 						 columns_sort_direction))
+ 			goto error_return;
+ 	}
+ 
+ 	/*
+ 	 * Second pass: sort the list of target columns on the server.
+ 	 */
+ 	else if (!sortColumns(PQftype(res, field_for_columns),
+ 					 piv_columns,
+ 					 num_columns,
+ 					 columns_sort_direction))
+ 		goto error_return;
+ 
+ 	/*
+ 	 * Third pass: print the crosstab'ed results.
+ 	 */
+ 	printCrosstab(res,
+ 				  num_columns,
+ 				  piv_columns,
+ 				  field_for_columns,
+ 				  num_rows,
+ 				  piv_rows,
+ 				  field_for_rows,
+ 				  field_for_ranks);
+ 
+ 	retval = true;
+ 
+ error_return:
+ 	pg_free(piv_columns);
+ 	pg_free(piv_rows);
+ 
+ 	return retval;
+ }
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index ...dd26322
*** a/src/bin/psql/crosstabview.h
--- b/src/bin/psql/crosstabview.h
***************
*** 0 ****
--- 1,36 ----
+ /*
+  * psql - the PostgreSQL interactive terminal
+  *
+  * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+  *
+  * src/bin/psql/crosstabview.h
+  */
+ 
+ #ifndef CROSSTABVIEW_H
+ #define CROSSTABVIEW_H
+ 
+ struct pivot_field
+ {
+ 	/* Pointer obtained from PGgetvalue() for colV or colH */
+ 	char*	name;
+ 
+ 	/* Rank of the field in its list, starting at 0.
+ 	 * - For headers stacked vertically, rank=N means it's the
+ 	 *   Nth distinct field encountered when looping through rows
+ 	 *   in their initial order.
+ 	 * - For headers stacked horizontally, rank is obtained
+ 	 *   by server-side sorting in sortColumns(), or explicitly
+ 	 *   from rank column
+ 	 */
+ 	int		rank;
+ 	double		outer_rank;
+ };
+ 
+ /* prototypes */
+ extern bool
+ PrintResultsInCrossTab(PGresult* res,
+ 					   const char* opt_field_for_rows,
+ 					   const char* opt_field_for_columns,
+ 					   const char* opt_field_for_ranks);
+ 
+ #endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
new file mode 100644
index 5b63e76..a893a64
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
*************** slashUsage(unsigned short int pager)
*** 175,180 ****
--- 175,181 ----
  	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
  	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
  	fprintf(output, _("  \\q                     quit psql\n"));
+ 	fprintf(output, _("  \\crosstabview [V H [R]] execute query and display results in crosstab\n"));
  	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
  	fprintf(output, "\n");
  
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
new file mode 100644
index 05d4b31..b2f8c2b
*** a/src/bin/psql/print.c
--- b/src/bin/psql/print.c
*************** printQuery(const PGresult *result, const
*** 3291,3320 ****
  
  	for (i = 0; i < cont.ncolumns; i++)
  	{
- 		char		align;
- 		Oid			ftype = PQftype(result, i);
- 
- 		switch (ftype)
- 		{
- 			case INT2OID:
- 			case INT4OID:
- 			case INT8OID:
- 			case FLOAT4OID:
- 			case FLOAT8OID:
- 			case NUMERICOID:
- 			case OIDOID:
- 			case XIDOID:
- 			case CIDOID:
- 			case CASHOID:
- 				align = 'r';
- 				break;
- 			default:
- 				align = 'l';
- 				break;
- 		}
- 
  		printTableAddHeader(&cont, PQfname(result, i),
! 							opt->translate_header, align);
  	}
  
  	/* set cells */
--- 3291,3299 ----
  
  	for (i = 0; i < cont.ncolumns; i++)
  	{
  		printTableAddHeader(&cont, PQfname(result, i),
! 							opt->translate_header,
! 							column_type_alignment(PQftype(result, i)));
  	}
  
  	/* set cells */
*************** printQuery(const PGresult *result, const
*** 3356,3361 ****
--- 3335,3365 ----
  	printTableCleanup(&cont);
  }
  
+ char
+ column_type_alignment(Oid ftype)
+ {
+ 	char		align;
+ 
+ 	switch (ftype)
+ 	{
+ 		case INT2OID:
+ 		case INT4OID:
+ 		case INT8OID:
+ 		case FLOAT4OID:
+ 		case FLOAT8OID:
+ 		case NUMERICOID:
+ 		case OIDOID:
+ 		case XIDOID:
+ 		case CIDOID:
+ 		case CASHOID:
+ 			align = 'r';
+ 			break;
+ 		default:
+ 			align = 'l';
+ 			break;
+ 	}
+ 	return align;
+ }
  
  void
  setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
new file mode 100644
index fd56598..218b185
*** a/src/bin/psql/print.h
--- b/src/bin/psql/print.h
*************** extern FILE *PageOutput(int lines, const
*** 175,181 ****
  extern void ClosePager(FILE *pagerpipe);
  
  extern void html_escaped_print(const char *in, FILE *fout);
! 
  extern void printTableInit(printTableContent *const content,
  			   const printTableOpt *opt, const char *title,
  			   const int ncolumns, const int nrows);
--- 175,181 ----
  extern void ClosePager(FILE *pagerpipe);
  
  extern void html_escaped_print(const char *in, FILE *fout);
! extern char column_type_alignment(Oid);
  extern void printTableInit(printTableContent *const content,
  			   const printTableOpt *opt, const char *title,
  			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
new file mode 100644
index 1885bb1..5993c1d
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
*************** typedef struct _psqlSettings
*** 90,95 ****
--- 90,99 ----
  
  	char	   *gfname;			/* one-shot file output argument for \g */
  	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+ 	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+ 	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+ 	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
+ 	char		*crosstabview_col_O;  /* one-shot \crosstabview 3nd argument */
  
  	bool		notty;			/* stdin or stdout is not a tty (as determined
  								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
new file mode 100644
index 8c48881..3337256
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
*************** psql_completion(const char *text, int st
*** 936,943 ****
  
  	static const char *const backslash_commands[] = {
  		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
! 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
! 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
  		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
  		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\drds", "\\ds", "\\dS",
  		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy",
--- 936,943 ----
  
  	static const char *const backslash_commands[] = {
  		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
! 		"\\crosstabview", "\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp",
! 		"\\dD", "\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
  		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
  		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\drds", "\\ds", "\\dS",
  		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy",
#49Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#48)
1 attachment(s)
Re: [patch] Proposal for \rotate in psql

2015-12-13 8:14 GMT+01:00 Pavel Stehule <pavel.stehule@gmail.com>:

2015-12-10 19:29 GMT+01:00 Pavel Stehule <pavel.stehule@gmail.com>:

postgres=# \crosstabview 4 +month label

Maybe using optional int order column instead label is better - then you
can do sort on client side

so the syntax can be "\crosstabview VCol [+/-]HCol [[+-]HOrderCol]

here is patch - supported syntax: \crosstabview VCol [+/-]HCol [HOrderCol]

Order column should to contains any numeric value. Values are sorted on
client side

fixed error messages

Show quoted text

Regards

Pavel

Regards

Pavel

┌──────────────────────────┬───────┬───────┬────────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ customer │ led │ úno │ bře │ dub │ kvě │
čen │ čec │ srp │ zář │ říj │ lis │

╞══════════════════════════╪═══════╪═══════╪════════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╪═══════╡
│ A********** │ │ │ │ │
│ │ │ │ │ 13000 │ │
│ A******** │ │ │ 8000 │ │
│ │ │ │ │ │ │
│ B***** │ │ │ │ │
│ │ │ │ │ │ 3200 │
│ B*********************** │ │ │ │ │
│ │ │ │ 26200 │ │ │
│ B********* │ │ │ │ │
│ │ 14000 │ │ │ │ │
│ C********** │ │ │ │ 7740 │
│ │ │ │ │ │ │
│ C*** │ │ │ │ │
│ │ │ │ 26000 │ │ │
│ C***** │ │ │ │ 12000 │
│ │ │ │ │ │ │
│ G******* │ 30200 │ 26880 │ 13536 │ 39360 │ 60480 │
54240 │ 44160 │ 16320 │ 29760 │ 22560 │ 20160 │
│ G*************** │ │ │ │ │ │
25500 │ │ │ │ │ │
│ G********** │ │ │ │ │ │
16000 │ │ │ │ │ │
│ I************* │ │ │ │ │
│ │ │ 27920 │ │ │ │
│ i**** │ │ │ │ 13500 │
│ │ │ │ │ │ │
│ n********* │ │ │ │ │
│ │ 12600 │ │ │ │ │
│ Q** │ │ │ │ │ 16700
│ │ │ │ │ │ │
│ S******* │ │ │ │ │
│ │ 8000 │ │ │ │ │
│ S******* │ │ │ │ │ 5368
│ │ │ │ │ │ │
│ s******* │ │ │ 5000 │ 3200 │
│ │ │ │ │ │ │

└──────────────────────────┴───────┴───────┴────────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
(18 rows)

Regards

Pavel

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-rotate-v7.difftext/plain; charset=US-ASCII; name=psql-rotate-v7.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
new file mode 100644
index 47e9da2..a45f4b8
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** lo_import 152801
*** 2461,2466 ****
--- 2461,2555 ----
          </listitem>
        </varlistentry>
  
+       <varlistentry>
+         <term><literal>\crosstabview [ <replaceable class="parameter">colV</replaceable>  [-|+]<replaceable class="parameter">colH</replaceable> ] </literal></term>
+         <listitem>
+         <para>
+         Execute the current query buffer (like <literal>\g</literal>) and shows the results
+         inside a crosstab grid. The output column <replaceable class="parameter">colV</replaceable>
+         becomes a vertical header and the output column
+         <replaceable class="parameter">colH</replaceable> becomes a horizontal header.
+         The results for the other output columns are projected inside the grid.
+         </para>
+ 
+         <para>
+         <replaceable class="parameter">colV</replaceable>
+         and <replaceable class="parameter">colH</replaceable> can indicate a
+         column position (starting at 1), or a column name. Normal case folding
+         and quoting rules apply on column names. By default,
+         <replaceable class="parameter">colV</replaceable> is column 1
+         and <replaceable class="parameter">colH</replaceable> is column 2.
+         A query having only one output column cannot be viewed in crosstab, and
+         <replaceable class="parameter">colH</replaceable> must differ from
+         <replaceable class="parameter">colV</replaceable>.
+         </para>
+ 
+         <para>
+         The vertical header, displayed as the leftmost column,
+         contains the set of all distinct values found in
+         column <replaceable class="parameter">colV</replaceable>, in the order
+         of their first appearance in the query results.
+         </para>
+         <para>
+         The horizontal header, displayed as the first row,
+         contains the set of all distinct non-null values found in
+         column <replaceable class="parameter">colH</replaceable>.  They come
+         by default in their order of appearance in the query results, or in ascending
+         order if a plus (+) sign precedes <replaceable class="parameter">colH</replaceable>,
+         or in descending order if it's a minus (-) sign.
+         </para>
+ 
+         <para>
+         The query results being tuples of <literal>N</literal> columns
+         (including <replaceable class="parameter">colV</replaceable> and
+         <replaceable class="parameter">colH</replaceable>),
+         for each distinct value <literal>x</literal> of
+         <replaceable class="parameter">colH</replaceable>
+         and each distinct value <literal>y</literal> of
+         <replaceable class="parameter">colV</replaceable>,
+         a cell located at the intersection <literal>(x,y)</literal> in the grid
+         has contents determined by these rules:
+         <itemizedlist>
+         <listitem>
+         <para>
+          if there is no corresponding row in the results such that the value
+          for <replaceable class="parameter">colH</replaceable>
+          is <literal>x</literal> and the value
+          for <replaceable class="parameter">colV</replaceable>
+          is <literal>y</literal>, the cell is empty.
+         </para>
+         </listitem>
+ 
+         <listitem>
+         <para>
+          if there is exactly one row such that the value
+          for <replaceable class="parameter">colH</replaceable>
+          is <literal>x</literal> and the value
+          for <replaceable class="parameter">colV</replaceable>
+          is <literal>y</literal>, then the <literal>N-2</literal> other
+          columns are displayed in the cell, separated between each other by
+          a space character if needed.
+ 
+          If <literal>N=2</literal>, the letter <literal>X</literal> is displayed in the cell as
+          if a virtual third column contained that character.
+         </para>
+         </listitem>
+ 
+         <listitem>
+         <para>
+          if there are several corresponding rows, the behavior is identical to one row
+          except that the values coming from different rows are stacked
+          vertically, rows being separated by newline characters inside
+          the same cell.
+         </para>
+         </listitem>
+ 
+         </itemizedlist>
+         </para>
+ 
+         </listitem>
+       </varlistentry>
+ 
  
        <varlistentry>
          <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
new file mode 100644
index f1336d5..9cb0c4a
*** a/src/bin/psql/Makefile
--- b/src/bin/psql/Makefile
*************** override CPPFLAGS := -I. -I$(srcdir) -I$
*** 23,29 ****
  OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
  	startup.o prompt.o variables.o large_obj.o print.o describe.o \
  	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
! 	sql_help.o \
  	$(WIN32RES)
  
  
--- 23,29 ----
  OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
  	startup.o prompt.o variables.o large_obj.o print.o describe.o \
  	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
! 	sql_help.o crosstabview.o \
  	$(WIN32RES)
  
  
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
new file mode 100644
index cf6876b..c743f57
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 46,51 ****
--- 46,52 ----
  #include "mainloop.h"
  #include "print.h"
  #include "psqlscan.h"
+ #include "crosstabview.h"
  #include "settings.h"
  #include "variables.h"
  
*************** exec_command(const char *cmd,
*** 1081,1086 ****
--- 1082,1119 ----
  		free(pw2);
  	}
  
+ 	/* \crosstabview -- execute a query and display results in crosstab */
+ 	else if (strcmp(cmd, "crosstabview") == 0)
+ 	{
+ 		char	*opt1,
+ 				*opt2,
+ 				*opt3;
+ 
+ 		opt1 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 		opt2 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 		opt3 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 
+ 		if (opt1 && !opt2)
+ 		{
+ 			psql_error(_("\\%s: missing second argument\n"), cmd);
+ 			success = false;
+ 		}
+ 		else
+ 		{
+ 			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1) : NULL;
+ 			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2) : NULL;
+ 			pset.crosstabview_col_O = opt3 ? pg_strdup(opt3) : NULL;
+ 			pset.crosstabview_output = true;
+ 			status = PSQL_CMD_SEND;
+ 		}
+ 
+ 		free(opt1);
+ 		free(opt2);
+ 	}
+ 
  	/* \prompt -- prompt and set variable */
  	else if (strcmp(cmd, "prompt") == 0)
  	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index a287eee..26cd233
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 24,29 ****
--- 24,30 ----
  #include "command.h"
  #include "copy.h"
  #include "mbprint.h"
+ #include "crosstabview.h"
  
  
  static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
*************** SendQuery(const char *query)
*** 1076,1085 ****
  
  		/* but printing results isn't: */
  		if (OK && results)
! 			OK = PrintQueryResults(results);
  	}
  	else
  	{
  		/* Fetch-in-segments mode */
  		OK = ExecQueryUsingCursor(query, &elapsed_msec);
  		ResetCancelConn();
--- 1077,1102 ----
  
  		/* but printing results isn't: */
  		if (OK && results)
! 		{
! 			if (pset.crosstabview_output)
! 				OK = PrintResultsInCrossTab(results,
! 											pset.crosstabview_col_V,
! 											pset.crosstabview_col_H,
! 											pset.crosstabview_col_O);
! 			else
! 				OK = PrintQueryResults(results);
! 		}
  	}
  	else
  	{
+ 		if (pset.crosstabview_output)
+ 		{
+ 			psql_error("\\crosstabview cannot be executed since active FETCH_COUNT = %d\n",
+ 					pset.fetch_count);
+ 			OK = false;
+ 			goto sendquery_cleanup;
+ 		}
+ 
  		/* Fetch-in-segments mode */
  		OK = ExecQueryUsingCursor(query, &elapsed_msec);
  		ResetCancelConn();
*************** sendquery_cleanup:
*** 1192,1197 ****
--- 1209,1231 ----
  		pset.gset_prefix = NULL;
  	}
  
+ 	pset.crosstabview_output = false;
+ 	if (pset.crosstabview_col_V)
+ 	{
+ 		free(pset.crosstabview_col_V);
+ 		pset.crosstabview_col_V = NULL;
+ 	}
+ 	if (pset.crosstabview_col_H)
+ 	{
+ 		free(pset.crosstabview_col_H);
+ 		pset.crosstabview_col_H = NULL;
+ 	}
+ 	if (pset.crosstabview_col_O)
+ 	{
+ 		free(pset.crosstabview_col_O);
+ 		pset.crosstabview_col_O = NULL;
+ 	}
+ 
  	return OK;
  }
  
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index ...ac6c6f8
*** a/src/bin/psql/crosstabview.c
--- b/src/bin/psql/crosstabview.c
***************
*** 0 ****
--- 1,750 ----
+ /*
+  * psql - the PostgreSQL interactive terminal
+  *
+  * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+  *
+  * src/bin/psql/crosstabview.c
+  */
+ 
+ 
+ #include "common.h"
+ #include "pqexpbuffer.h"
+ #include "crosstabview.h"
+ #include "settings.h"
+ 
+ #include "catalog/pg_type.h"
+ 
+ #include <string.h>
+ 
+ static int
+ headerCompare(const void *a, const void *b)
+ {
+ 	return strcmp( ((struct pivot_field*)a)->name,
+ 				   ((struct pivot_field*)b)->name);
+ }
+ 
+ static void
+ accumHeader(char* name, char* outer_rank, int* count, struct pivot_field **sorted_tab, int row_number)
+ {
+ 	struct pivot_field *p;
+ 
+ 	/*
+ 	 * Search for name in sorted_tab. If it doesn't exist, insert it,
+ 	 * otherwise do nothing.
+ 	 */
+ 
+ 	if (*count >= 1)
+ 	{
+ 		p = (struct pivot_field*) bsearch(&name,
+ 										  *sorted_tab,
+ 										  *count,
+ 										  sizeof(struct pivot_field),
+ 										  headerCompare);
+ 	}
+ 	else
+ 		p=NULL;
+ 
+ 	if (!p)
+ 	{
+ 		*sorted_tab = pg_realloc(*sorted_tab, sizeof(struct pivot_field) * (1+*count));
+ 		(*sorted_tab)[*count].name = name;
+ 		(*sorted_tab)[*count].outer_rank = outer_rank ? strtod(outer_rank, NULL) : -1.0;
+ 		(*sorted_tab)[*count].rank = *count;
+ 		(*count)++;
+ 
+ 		qsort(*sorted_tab,
+ 			  *count,
+ 			  sizeof(struct pivot_field),
+ 			  headerCompare);
+ 	}
+ }
+ 
+ /*
+  * Auxilary structure using for sorting only
+  */
+ struct p_rank
+ {
+ 	double		outer_rank;
+ 	int	old_pos;
+ };
+ 
+ static int
+ RankCompare(const void *a, const void *b)
+ {
+ 	return ((struct p_rank*)a)->outer_rank - ((struct p_rank*)b)->outer_rank ;
+ }
+ 
+ static int
+ RankCompareDesc(const void *a, const void *b)
+ {
+ 	return ((struct p_rank*) b)->outer_rank - ((struct p_rank*) a)->outer_rank;
+ }
+ 
+ 
+ /*
+  * Resort header by rank, ensure uniq outer ranks
+  */
+ static bool
+ sortColumnsByRank(struct pivot_field *columns, int nb_cols, int direction)
+ {
+ 	int		i;
+ 	struct p_rank	*p_ranks;
+ 	bool		retval = true;
+ 
+ 	p_ranks = (struct p_rank *) pg_malloc(nb_cols * sizeof(struct p_rank));
+ 
+ 	for (i = 0; i < nb_cols; i++)
+ 	{
+ 		p_ranks[i].outer_rank = columns[i].outer_rank;
+ 		p_ranks[i].old_pos = i;
+ 	}
+ 
+ 	if (direction >= 0)
+ 		qsort(p_ranks,
+ 			  nb_cols,
+ 			  sizeof(struct p_rank),
+ 			  RankCompare);
+ 	else
+ 		qsort(p_ranks,
+ 			  nb_cols,
+ 			  sizeof(struct p_rank),
+ 			  RankCompareDesc);
+ 
+ 	for (i = 0; i < nb_cols; i++)
+ 	{
+ 		/* two adjecent outer sorted ranks should be different */
+ 		if (i > 0 && p_ranks[i].outer_rank == p_ranks[i - 1].outer_rank)
+ 		{
+ 			psql_error("Outer ranks are not unique.");
+ 			goto cleanup;
+ 		}
+ 
+ 		columns[p_ranks[i].old_pos].rank = i;
+ 	}
+ 
+ 	retval = true;
+ 
+ cleanup:
+ 
+ 	pg_free(p_ranks);
+ 
+ 	return retval;
+ }
+ 
+ /*
+  * Send a query to sort all column values cast to the Oid passed in a VALUES clause
+  */
+ static bool
+ sortColumns(Oid coltype, struct pivot_field *columns, int nb_cols, int direction)
+ {
+ 	bool retval = false;
+ 	PGresult *res = NULL;
+ 	PQExpBufferData query;
+ 	int i;
+ 	Oid *param_types;
+ 	const char** param_values;
+ 	int* param_lengths;
+ 	int* param_formats;
+ 
+ 	if (nb_cols < 2 || direction==0)
+ 		return true;					/* nothing to sort */
+ 
+ 	param_types = (Oid*) pg_malloc(nb_cols*sizeof(Oid));
+ 	param_values = (const char**) pg_malloc(nb_cols*sizeof(char*));
+ 	param_lengths = (int*) pg_malloc(nb_cols*sizeof(int));
+ 	param_formats = (int*) pg_malloc(nb_cols*sizeof(int));
+ 
+ 	initPQExpBuffer(&query);
+ 
+ 	/*
+ 	 * The query returns the original position of each value in our list,
+ 	 * ordered by its new position. The value itself is not returned.
+ 	 */
+ 	appendPQExpBuffer(&query, "SELECT n FROM (VALUES");
+ 
+ 	for (i=1; i <= nb_cols; i++)
+ 	{
+ 		if (i < nb_cols)
+ 			appendPQExpBuffer(&query, "($%d,%d),", i, i);
+ 		else
+ 		{
+ 			appendPQExpBuffer(&query, "($%d,%d)) AS l(x,n) ORDER BY x", i, i);
+ 			if (direction < 0)
+ 				appendPQExpBuffer(&query, " DESC");
+ 		}
+ 
+ 		param_types[i-1] = coltype;
+ 		param_values[i-1] = columns[i-1].name;
+ 		param_lengths[i-1] = strlen(columns[i-1].name);
+ 		param_formats[i-1] = 0;
+ 	}
+ 
+ 	res = PQexecParams(pset.db,
+ 					   query.data,
+ 					   nb_cols,
+ 					   param_types,
+ 					   param_values,
+ 					   param_lengths,
+ 					   param_formats,
+ 					   0);
+ 
+ 	if (res)
+ 	{
+ 		ExecStatusType status = PQresultStatus(res);
+ 		if (status == PGRES_TUPLES_OK)
+ 		{
+ 			for (i=0; i < PQntuples(res); i++)
+ 			{
+ 				int old_pos = atoi(PQgetvalue(res, i, 0));
+ 
+ 				if (old_pos < 1 || old_pos > nb_cols || i >= nb_cols)
+ 				{
+ 					/*
+ 					 * A position outside of the range is normally impossible.
+ 					 * If this happens, we're facing a malfunctioning or hostile
+ 					 * server or middleware.
+ 					 */
+ 					psql_error(_("Unexpected value when sorting horizontal headers"));
+ 					goto cleanup;
+ 				}
+ 				else
+ 				{
+ 					columns[old_pos-1].rank = i;
+ 				}
+ 			}
+ 		}
+ 		else
+ 		{
+ 			psql_error(_("Query error when sorting horizontal headers: %s"),
+ 					   PQerrorMessage(pset.db));
+ 			goto cleanup;
+ 		}
+ 	}
+ 
+ 	retval = true;
+ 
+ cleanup:
+ 	termPQExpBuffer(&query);
+ 	if (res)
+ 		PQclear(res);
+ 	pg_free(param_types);
+ 	pg_free(param_values);
+ 	pg_free(param_lengths);
+ 	pg_free(param_formats);
+ 	return retval;
+ }
+ 
+ static void
+ printCrosstab(const PGresult *results,
+ 			  int num_columns,
+ 			  struct pivot_field *piv_columns,
+ 			  int field_for_columns,
+ 			  int num_rows,
+ 			  struct pivot_field *piv_rows,
+ 			  int field_for_rows,
+ 			  int field_for_ranks)
+ {
+ 	printQueryOpt popt = pset.popt;
+ 	printTableContent cont;
+ 	int	i, j, rn;
+ 	char col_align = 'l';		/* alignment for values inside the grid */
+ 	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+ 	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+ 								 *  in this function, when cells cannot simply point to
+ 								 *  PQgetvalue(results, ...) */
+ 
+ 	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+ 
+ 	/* Step 1: set target column names (horizontal header) */
+ 
+ 	/* The name of the first column is kept unchanged by the pivoting */
+ 	printTableAddHeader(&cont,
+ 						PQfname(results, field_for_rows),
+ 						false,
+ 						column_type_alignment(PQftype(results, field_for_rows)));
+ 
+ 	/*
+ 	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+ 	 *  associating each piv_columns[].rank to its index in piv_columns.
+ 	 *  This avoids an O(N^2) loop later
+ 	 */
+ 	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		horiz_map[piv_columns[i].rank] = i;
+ 	}
+ 
+ 	/*
+ 	 * In the case of 3 output columns, the contents in the cells are exactly
+ 	 * the contents of the "value" column (3rd column by default), so their
+ 	 * alignment is determined by PQftype(). Otherwise the contents are
+ 	 * made-up strings, so the alignment is 'l'
+ 	 */
+ 	if (PQnfields(results) == 3)
+ 	{
+ 		int colnum;				/* column placed inside the grid */
+ 		/*
+ 		 * find colnum in the permutations of (0,1,2) where colnum is
+ 		 * neither field_for_rows nor field_for_columns
+ 		 */
+ 		switch (field_for_rows)
+ 		{
+ 		case 0:
+ 			colnum = (field_for_columns == 1) ? 2 : 1;
+ 			break;
+ 		case 1:
+ 			colnum = (field_for_columns == 0) ? 2: 0;
+ 			break;
+ 		default:				/* should be always 2 */
+ 			colnum = (field_for_columns == 0) ? 1: 0;
+ 			break;
+ 		}
+ 		col_align = column_type_alignment(PQftype(results, colnum));
+ 	}
+ 	else
+ 		col_align = 'l';
+ 
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		printTableAddHeader(&cont,
+ 							piv_columns[horiz_map[i]].name,
+ 							false,
+ 							col_align);
+ 	}
+ 	pg_free(horiz_map);
+ 
+ 	/* Step 2: set row names in the first output column (vertical header) */
+ 	for (i = 0; i < num_rows; i++)
+ 	{
+ 		int k = piv_rows[i].rank;
+ 		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+ 		/* Initialize all cells inside the grid to an empty value */
+ 		for (j = 0; j < num_columns; j++)
+ 			cont.cells[k*(num_columns+1)+j+1] = "";
+ 	}
+ 	cont.cellsadded = num_rows * (num_columns+1);
+ 
+ 	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+ 
+ 	/* Step 3: set all the cells "inside the grid" */
+ 	for (rn = 0; rn < PQntuples(results); rn++)
+ 	{
+ 		char* row_name;
+ 		char* col_name;
+ 		int row_number;
+ 		int col_number;
+ 		struct pivot_field *p;
+ 
+ 		row_number = col_number = -1;
+ 		/* Find target row */
+ 		if (!PQgetisnull(results, rn, field_for_rows))
+ 		{
+ 			row_name = PQgetvalue(results, rn, field_for_rows);
+ 			p = (struct pivot_field*) bsearch(&row_name,
+ 											  piv_rows,
+ 											  num_rows,
+ 											  sizeof(struct pivot_field),
+ 											  headerCompare);
+ 			if (p)
+ 				row_number = p->rank;
+ 		}
+ 
+ 		/* Find target column */
+ 		if (!PQgetisnull(results, rn, field_for_columns))
+ 		{
+ 			col_name = PQgetvalue(results, rn, field_for_columns);
+ 			p = (struct pivot_field*) bsearch(&col_name,
+ 											  piv_columns,
+ 											  num_columns,
+ 											  sizeof(struct pivot_field),
+ 											  headerCompare);
+ 			if (p)
+ 				col_number = p->rank;
+ 		}
+ 
+ 		/* Place value into cell */
+ 		if (col_number>=0 && row_number>=0)
+ 		{
+ 			int idx = 1 + col_number + row_number*(num_columns+1);
+ 			int src_col = 0;			/* column number in source result */
+ 			int k = 0;
+ 
+ 			do {
+ 				char *content;
+ 
+ 				if (PQnfields(results) == 2)
+ 				{
+ 					/*
+ 					  special case: when the source has only 2 columns, use a
+ 					  X (cross/checkmark) for the cell content, and set
+ 					  src_col to a virtual additional column.
+ 					*/
+ 					content = "X";
+ 					src_col = 3;
+ 				}
+ 				else if (src_col == field_for_rows || src_col == field_for_columns
+ 					    || src_col == field_for_ranks)
+ 				{
+ 					/*
+ 					  The source values that produce headers are not processed
+ 					  in this loop, only the values that end up inside the grid.
+ 					*/
+ 					src_col++;
+ 					continue;
+ 				}
+ 				else
+ 				{
+ 					content = (!PQgetisnull(results, rn, src_col)) ?
+ 						PQgetvalue(results, rn, src_col) :
+ 						(popt.nullPrint ? popt.nullPrint : "");
+ 				}
+ 
+ 				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+ 				{
+ 					/*
+ 					 * Multiple values for the same (row,col) are projected
+ 					 * into the same cell. When this happens, separate the
+ 					 * previous content of the cell from the new value by a
+ 					 * newline.
+ 					 */
+ 					int content_size =
+ 						strlen(cont.cells[idx])
+ 						+ 2 			/* room for [CR],LF or space */
+ 						+ strlen(content)
+ 						+ 1;			/* '\0' */
+ 					char *new_content;
+ 
+ 					/*
+ 					 * idx2 is an index into allocated_cells. It differs from
+ 					 * idx (index into cont.cells), because vertical and
+ 					 * horizontal headers are included in `cont.cells` but
+ 					 * excluded from allocated_cells.
+ 					 */
+ 					int idx2 = (row_number * num_columns) + col_number;
+ 
+ 					if (allocated_cells[idx2] != NULL)
+ 					{
+ 						new_content = pg_realloc(allocated_cells[idx2], content_size);
+ 					}
+ 					else
+ 					{
+ 						/*
+ 						 * At this point, cont.cells[idx] still contains a
+ 						 * PQgetvalue() pointer.  Just after, it will contain
+ 						 * a new pointer maintained in allocated_cells[], and
+ 						 * freed at the end of this function.
+ 						 */
+ 						new_content = pg_malloc(content_size);
+ 						strcpy(new_content, cont.cells[idx]);
+ 					}
+ 					cont.cells[idx] = new_content;
+ 					allocated_cells[idx2] = new_content;
+ 
+ 					/*
+ 					 * Contents that are on adjacent columns in the source results get
+ 					 * separated by one space in the target.
+ 					 * Contents that are on different rows in the source get
+ 					 * separated by newlines in the target.
+ 					 */
+ 					if (k==0)
+ 						strcat(new_content, "\n");
+ 					else
+ 						strcat(new_content, " ");
+ 					strcat(new_content, content);
+ 				}
+ 				else
+ 				{
+ 					cont.cells[idx] = content;
+ 				}
+ 				k++;
+ 				src_col++;
+ 			} while (src_col < PQnfields(results));
+ 		}
+ 	}
+ 
+ 	printTable(&cont, pset.queryFout, false, pset.logfile);
+ 	printTableCleanup(&cont);
+ 
+ 
+ 	for (i=0; i < num_rows * num_columns; i++)
+ 	{
+ 		if (allocated_cells[i] != NULL)
+ 			pg_free(allocated_cells[i]);
+ 	}
+ 
+ 	pg_free(allocated_cells);
+ }
+ 
+ /*
+  * Compare a user-supplied argument against a field name obtained by PQfname(),
+  * which is already case-folded.
+  * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+  * do a case-sensitive comparison with these rules:
+  * - double quotes enclosing 'arg' are filtered out
+  * - double quotes inside 'arg' are expected to be doubled
+  */
+ static int
+ fieldnameCmp(const char* arg, const char* fieldname)
+ {
+ 	const unsigned char* p = (const unsigned char*) arg;
+ 	const unsigned char* f = (const unsigned char*) fieldname;
+ 	unsigned char c;
+ 
+ 	if (*p++ != '"')
+ 		return pg_strcasecmp(arg, fieldname);
+ 
+ 	while ((c=*p++))
+ 	{
+ 		if (c=='"')
+ 		{
+ 			if (*p=='"')
+ 				p++;			/* skip second quote and continue */
+ 			else if (*p=='\0')
+ 				return *p-*f;		/* p finishes before f or is identical */
+ 
+ 		}
+ 		if (*f=='\0')
+ 			return 1;			/* f finishes before p */
+ 		if (c!=*f)
+ 			return c-*f;
+ 		f++;
+ 	}
+ 	return (*f=='\0') ? 0 : 1;
+ }
+ 
+ 
+ /*
+  * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+  * Returns:
+  *  on success, the 0-based index of the column
+  *  or -1 if the column number or name is not found in the result's structure,
+  *        or if it's ambiguous (arg corresponding to several columns)
+  */
+ static int
+ indexOfColumn(const char* arg, PGresult* res)
+ {
+ 	int idx;
+ 
+ 	if (strspn(arg, "0123456789") == strlen(arg))
+ 	{
+ 		/* if arg contains only digits, it's a column number */
+ 		idx = atoi(arg) - 1;
+ 		if (idx < 0  || idx >= PQnfields(res))
+ 		{
+ 			psql_error(_("Invalid column number: %s\n"), arg);
+ 			return -1;
+ 		}
+ 	}
+ 	else
+ 	{
+ 		int i;
+ 		idx = -1;
+ 		for (i=0; i < PQnfields(res); i++)
+ 		{
+ 			if (fieldnameCmp(arg, PQfname(res, i)) == 0)
+ 			{
+ 				if (idx>=0)
+ 				{
+ 					/* if another idx was already found for the same name */
+ 					psql_error(_("Ambiguous column name: %s\n"), arg);
+ 					return -1;
+ 				}
+ 				idx = i;
+ 			}
+ 		}
+ 		if (idx == -1)
+ 		{
+ 			psql_error(_("Invalid column name: %s\n"), arg);
+ 			return -1;
+ 		}
+ 	}
+ 	return idx;
+ }
+ 
+ bool
+ PrintResultsInCrossTab(PGresult* res,
+ 					   const char* opt_field_for_rows,    /* COLV or null */
+ 					   const char* opt_field_for_columns, /* [-+]COLH or null */
+ 					   const char* opt_field_for_ranks)   /* COLL or NULL */
+ {
+ 	int		rn;
+ 	struct pivot_field	*piv_columns = NULL;
+ 	struct pivot_field	*piv_rows = NULL;
+ 	int		num_columns = 0;
+ 	int		num_rows = 0;
+ 	bool 	retval = false;
+ 	int		columns_sort_direction = 0; /* 1:ascending, 0:none, -1:descending */
+ 	int		field_for_ranks;
+ 
+ 	/* 0-based index of the field whose distinct values will become COLUMN headers */
+ 	int		field_for_columns;
+ 
+ 	/* 0-based index of the field whose distinct values will become ROW headers */
+ 	int		field_for_rows;
+ 
+ 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ 		goto error_return;
+ 
+ 	if (PQnfields(res) < 2)
+ 	{
+ 		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	if (PQnfields(res) < 3 && opt_field_for_ranks != NULL)
+ 	{
+ 		psql_error(_("The query must return at least three columns to be shown in crosstab\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	field_for_rows = (opt_field_for_rows != NULL)
+ 		? indexOfColumn(opt_field_for_rows, res)
+ 		: 0;
+ 
+ 	if (field_for_rows < 0)
+ 		goto error_return;
+ 
+ 	if (opt_field_for_columns == NULL)
+ 		field_for_columns = 1;
+ 	else
+ 	{
+ 		/*
+ 		 * descending sort is requested if the column reference is
+ 		 * preceded with a minus sign
+ 		 */
+ 		if (*opt_field_for_columns == '-')
+ 		{
+ 			columns_sort_direction = -1;
+ 			opt_field_for_columns++;
+ 		}
+ 		else if (*opt_field_for_columns == '+')
+ 		{
+ 			columns_sort_direction = 1;
+ 			opt_field_for_columns++;
+ 		}
+ 		field_for_columns = indexOfColumn(opt_field_for_columns, res);
+ 		if (field_for_columns < 0)
+ 			goto error_return;
+ 	}
+ 
+ 	if (field_for_columns == field_for_rows)
+ 	{
+ 		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	if (opt_field_for_ranks)
+ 	{
+ 		Oid		ftype;
+ 
+ 		field_for_ranks = indexOfColumn(opt_field_for_ranks, res);
+ 		if (field_for_ranks == field_for_columns || field_for_ranks == field_for_rows)
+ 		{
+ 			psql_error(_("The same column cannot be used for both vertical and horizontal headers or ranks\n"));
+ 			goto error_return;
+ 		}
+ 
+ 		/* the rank column should be integer */
+ 		ftype = PQftype(res, field_for_ranks);
+ 		switch (ftype)
+ 		{
+ 			case INT2OID:
+ 			case INT4OID:
+ 			case INT8OID:
+ 			case FLOAT4OID:
+ 			case FLOAT8OID:
+ 			case NUMERICOID:
+ 				break;
+ 			default:
+ 				psql_error(_("The rank value should be numeric value.\n"));
+ 				goto error_return;
+ 		}
+ 	}
+ 	else
+ 		field_for_ranks = -1;
+ 
+ 	/*
+ 	 * First pass: accumulate row names and column names, each into their
+ 	 * array. Use client-side sort but only to build the set of DISTINCT
+ 	 * values. The final order displayed depends only on server-side
+ 	 * sorts.
+ 	 */
+ 	for (rn = 0; rn < PQntuples(res); rn++)
+ 	{
+ 		if (!PQgetisnull(res, rn, field_for_rows))
+ 		{
+ 			accumHeader(PQgetvalue(res, rn, field_for_rows), 
+ 				    NULL,
+ 						&num_rows,
+ 						&piv_rows,
+ 						rn);
+ 		}
+ 
+ 		if (!PQgetisnull(res, rn, field_for_columns))
+ 		{
+ 			if (field_for_ranks >= 0)
+ 			{
+ 				/* ensure not null values */
+ 				if (PQgetisnull(res, rn, field_for_ranks))
+ 				{
+ 					psql_error(_("The rank value should not be null.\n"));
+ 					goto error_return;
+ 				}
+ 
+ 				accumHeader(PQgetvalue(res, rn, field_for_columns),
+ 					    PQgetvalue(res, rn, field_for_ranks),
+ 							&num_columns,
+ 							&piv_columns,
+ 							rn);
+ 			}
+ 			else
+ 				accumHeader(PQgetvalue(res, rn, field_for_columns),
+ 					    NULL,
+ 							&num_columns,
+ 							&piv_columns,
+ 							rn);
+ 			if (num_columns > 1600)
+ 			{
+ 				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+ 				goto error_return;
+ 			}
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Second pass: sort the list of target columns (on the client).
+ 	 */
+ 	if (field_for_ranks >= 0 && !sortColumnsByRank(piv_columns,
+ 							num_columns,
+ 								columns_sort_direction))
+ 			goto error_return;
+ 
+ 	/*
+ 	 * Second pass: sort the list of target columns (on the server).
+ 	 */
+ 	else if (!sortColumns(PQftype(res, field_for_columns),
+ 					 piv_columns,
+ 					 num_columns,
+ 					 columns_sort_direction))
+ 		goto error_return;
+ 
+ 	/*
+ 	 * Third pass: print the crosstab'ed results.
+ 	 */
+ 	printCrosstab(res,
+ 				  num_columns,
+ 				  piv_columns,
+ 				  field_for_columns,
+ 				  num_rows,
+ 				  piv_rows,
+ 				  field_for_rows,
+ 				  field_for_ranks);
+ 
+ 	retval = true;
+ 
+ error_return:
+ 	pg_free(piv_columns);
+ 	pg_free(piv_rows);
+ 
+ 	return retval;
+ }
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index ...dd26322
*** a/src/bin/psql/crosstabview.h
--- b/src/bin/psql/crosstabview.h
***************
*** 0 ****
--- 1,36 ----
+ /*
+  * psql - the PostgreSQL interactive terminal
+  *
+  * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+  *
+  * src/bin/psql/crosstabview.h
+  */
+ 
+ #ifndef CROSSTABVIEW_H
+ #define CROSSTABVIEW_H
+ 
+ struct pivot_field
+ {
+ 	/* Pointer obtained from PGgetvalue() for colV or colH */
+ 	char*	name;
+ 
+ 	/* Rank of the field in its list, starting at 0.
+ 	 * - For headers stacked vertically, rank=N means it's the
+ 	 *   Nth distinct field encountered when looping through rows
+ 	 *   in their initial order.
+ 	 * - For headers stacked horizontally, rank is obtained
+ 	 *   by server-side sorting in sortColumns(), or explicitly
+ 	 *   from rank column
+ 	 */
+ 	int		rank;
+ 	double		outer_rank;
+ };
+ 
+ /* prototypes */
+ extern bool
+ PrintResultsInCrossTab(PGresult* res,
+ 					   const char* opt_field_for_rows,
+ 					   const char* opt_field_for_columns,
+ 					   const char* opt_field_for_ranks);
+ 
+ #endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
new file mode 100644
index 5b63e76..a893a64
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
*************** slashUsage(unsigned short int pager)
*** 175,180 ****
--- 175,181 ----
  	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
  	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
  	fprintf(output, _("  \\q                     quit psql\n"));
+ 	fprintf(output, _("  \\crosstabview [V H [R]] execute query and display results in crosstab\n"));
  	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
  	fprintf(output, "\n");
  
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
new file mode 100644
index 05d4b31..b2f8c2b
*** a/src/bin/psql/print.c
--- b/src/bin/psql/print.c
*************** printQuery(const PGresult *result, const
*** 3291,3320 ****
  
  	for (i = 0; i < cont.ncolumns; i++)
  	{
- 		char		align;
- 		Oid			ftype = PQftype(result, i);
- 
- 		switch (ftype)
- 		{
- 			case INT2OID:
- 			case INT4OID:
- 			case INT8OID:
- 			case FLOAT4OID:
- 			case FLOAT8OID:
- 			case NUMERICOID:
- 			case OIDOID:
- 			case XIDOID:
- 			case CIDOID:
- 			case CASHOID:
- 				align = 'r';
- 				break;
- 			default:
- 				align = 'l';
- 				break;
- 		}
- 
  		printTableAddHeader(&cont, PQfname(result, i),
! 							opt->translate_header, align);
  	}
  
  	/* set cells */
--- 3291,3299 ----
  
  	for (i = 0; i < cont.ncolumns; i++)
  	{
  		printTableAddHeader(&cont, PQfname(result, i),
! 							opt->translate_header,
! 							column_type_alignment(PQftype(result, i)));
  	}
  
  	/* set cells */
*************** printQuery(const PGresult *result, const
*** 3356,3361 ****
--- 3335,3365 ----
  	printTableCleanup(&cont);
  }
  
+ char
+ column_type_alignment(Oid ftype)
+ {
+ 	char		align;
+ 
+ 	switch (ftype)
+ 	{
+ 		case INT2OID:
+ 		case INT4OID:
+ 		case INT8OID:
+ 		case FLOAT4OID:
+ 		case FLOAT8OID:
+ 		case NUMERICOID:
+ 		case OIDOID:
+ 		case XIDOID:
+ 		case CIDOID:
+ 		case CASHOID:
+ 			align = 'r';
+ 			break;
+ 		default:
+ 			align = 'l';
+ 			break;
+ 	}
+ 	return align;
+ }
  
  void
  setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
new file mode 100644
index fd56598..218b185
*** a/src/bin/psql/print.h
--- b/src/bin/psql/print.h
*************** extern FILE *PageOutput(int lines, const
*** 175,181 ****
  extern void ClosePager(FILE *pagerpipe);
  
  extern void html_escaped_print(const char *in, FILE *fout);
! 
  extern void printTableInit(printTableContent *const content,
  			   const printTableOpt *opt, const char *title,
  			   const int ncolumns, const int nrows);
--- 175,181 ----
  extern void ClosePager(FILE *pagerpipe);
  
  extern void html_escaped_print(const char *in, FILE *fout);
! extern char column_type_alignment(Oid);
  extern void printTableInit(printTableContent *const content,
  			   const printTableOpt *opt, const char *title,
  			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
new file mode 100644
index 1885bb1..5993c1d
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
*************** typedef struct _psqlSettings
*** 90,95 ****
--- 90,99 ----
  
  	char	   *gfname;			/* one-shot file output argument for \g */
  	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+ 	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+ 	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+ 	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
+ 	char		*crosstabview_col_O;  /* one-shot \crosstabview 3nd argument */
  
  	bool		notty;			/* stdin or stdout is not a tty (as determined
  								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
new file mode 100644
index 8c48881..3337256
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
*************** psql_completion(const char *text, int st
*** 936,943 ****
  
  	static const char *const backslash_commands[] = {
  		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
! 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
! 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
  		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
  		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\drds", "\\ds", "\\dS",
  		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy",
--- 936,943 ----
  
  	static const char *const backslash_commands[] = {
  		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
! 		"\\crosstabview", "\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp",
! 		"\\dD", "\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
  		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
  		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\drds", "\\ds", "\\dS",
  		"\\dt", "\\dT", "\\dv", "\\du", "\\dx", "\\dy",
#50Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#48)
1 attachment(s)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

postgres=# \crosstabview 4 +month label

Maybe using optional int order column instead label is better - then you can
do sort on client side

so the syntax can be "\crosstabview VCol [+/-]HCol [[+-]HOrderCol]

In the meantime I've followed a different idea: allowing the
vertical header to be sorted too, still server-side.

That's because to me, the first impulse for a user noticing that
it's not sorted vertically would be to write
\crosstabview +customer month
rather than figure out the
\crosstabview customer +month_number month_name
invocation.
But both ways aren't even mutually exclusive. We could support
\crosstabview [+|-]colV[:labelV] [+|-]colH[:labelH]
it's more complicated to understand, but not harder to implement.

Also, a non-zero FETCH_COUNT is supported by this version of the patch,
if the first internal FETCH retrieves less than FETCH_COUNT rows.
Otherwise a specific error is emitted.

Also there are minor changes in arguments and callers following
recent code changes for \o

Trying to crosstab with 10k+ distinct values vertically, I've noticed
that the current code is too slow, spending too much time
sorting. I'm currently replacing its simple arrays of distinct values
with AVL binary trees, which I expect to be much more efficient for
this.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-crosstabview-v8.difftext/x-patch; name=psql-crosstabview-v8.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index e4f72a8..2a998b2 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2449,6 +2449,95 @@ lo_import 152801
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><literal>\crosstabview [ [-|+]<replaceable class="parameter">colV</replaceable>  [-|+]<replaceable class="parameter">colH</replaceable> ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows the results
+        inside a crosstab grid. The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header and the output column
+        <replaceable class="parameter">colH</replaceable> becomes a horizontal header.
+        The results for the other output columns are projected inside the grid.
+        </para>
+
+        <para>
+        <replaceable class="parameter">colV</replaceable>
+        and <replaceable class="parameter">colH</replaceable> can indicate a
+        column position (starting at 1), or a column name. Normal case folding
+        and quoting rules apply on column names. By default,
+        <replaceable class="parameter">colV</replaceable> is column 1
+        and <replaceable class="parameter">colH</replaceable> is column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the set of all distinct values found in
+        column <replaceable class="parameter">colV</replaceable>, in the order
+        of their first appearance in the query results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the set of all distinct non-null values found in
+        column <replaceable class="parameter">colH</replaceable>.  They come
+        by default in their order of appearance in the query results, or in ascending
+        order if a plus (+) sign precedes <replaceable class="parameter">colH</replaceable>,
+        or in descending order if it's a minus (-) sign.
+        </para>
+
+        <para>
+        The query results being tuples of <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        a cell located at the intersection <literal>(x,y)</literal> in the grid
+        has contents determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the results such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed in the cell as
+         if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to one row
+         except that the values coming from different rows are stacked
+         vertically, rows being separated by newline characters inside
+         the same cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
 
       <varlistentry>
         <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index f1336d5..9cb0c4a 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o crosstabview.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 6e8c623..6ba5efa 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -46,6 +46,7 @@
 #include "mainloop.h"
 #include "print.h"
 #include "psqlscan.h"
+#include "crosstabview.h"
 #include "settings.h"
 #include "variables.h"
 
@@ -1081,6 +1082,34 @@ exec_command(const char *cmd,
 		free(pw2);
 	}
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		char	*opt1,
+				*opt2;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1): NULL;
+			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2): NULL;
+			pset.crosstabview_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+	}
+
 	/* \prompt -- prompt and set variable */
 	else if (strcmp(cmd, "prompt") == 0)
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index a287eee..d8c0f70 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -24,6 +24,7 @@
 #include "command.h"
 #include "copy.h"
 #include "mbprint.h"
+#include "crosstabview.h"
 
 
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
@@ -906,6 +907,8 @@ PrintQueryResults(PGresult *results)
 			/* store or print the data ... */
 			if (pset.gset_prefix)
 				success = StoreQueryTuple(results);
+			else if (pset.crosstabview_output)
+				success = PrintResultsInCrossTab(results);
 			else
 				success = PrintQueryTuples(results);
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
@@ -1076,7 +1079,9 @@ SendQuery(const char *query)
 
 		/* but printing results isn't: */
 		if (OK && results)
+		{
 			OK = PrintQueryResults(results);
+		}
 	}
 	else
 	{
@@ -1192,6 +1197,18 @@ sendquery_cleanup:
 		pset.gset_prefix = NULL;
 	}
 
+	/* reset \crosstabview settings */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
 	return OK;
 }
 
@@ -1354,7 +1371,25 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 			is_pager = true;
 		}
 
-		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
+		if (pset.crosstabview_output)
+		{
+			if (ntuples < fetch_count)
+				PrintResultsInCrossTab(results);
+			else
+			{
+				/*
+				  crosstabview is denied if the whole set of rows is not
+				  guaranteed to be fetched in the first iteration, because
+				  it's expected in memory as a single PGresult structure.
+				*/
+				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
+					fetch_count);
+				PQclear(results);
+				break;
+			}
+		}
+		else
+			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
 
 		PQclear(results);
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..6d13b57
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,644 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+
+#include "common.h"
+#include "pqexpbuffer.h"
+#include "crosstabview.h"
+#include "settings.h"
+
+#include <string.h>
+
+static int
+headerCompare(const void *a, const void *b)
+{
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+static void
+accumHeader(char* name, int* count, struct pivot_field **sorted_tab, int row_number)
+{
+	struct pivot_field *p;
+
+	/*
+	 * Search for name in sorted_tab. If it doesn't exist, insert it,
+	 * otherwise do nothing.
+	 */
+
+	if (*count >= 1)
+	{
+		p = (struct pivot_field*) bsearch(&name,
+										  *sorted_tab,
+										  *count,
+										  sizeof(struct pivot_field),
+										  headerCompare);
+	}
+	else
+		p=NULL;
+
+	if (!p)
+	{
+		*sorted_tab = pg_realloc(*sorted_tab, sizeof(struct pivot_field) * (1+*count));
+		(*sorted_tab)[*count].name = name;
+		(*sorted_tab)[*count].rank = *count;
+		(*count)++;
+
+		qsort(*sorted_tab,
+			  *count,
+			  sizeof(struct pivot_field),
+			  headerCompare);
+	}
+}
+
+/*
+ * Send a query to sort all column values cast to the Oid passed in a VALUES clause
+ */
+static bool
+sortColumns(Oid coltype, struct pivot_field *columns, int nb_cols, int direction)
+{
+	bool retval = false;
+	PGresult *res = NULL;
+	PQExpBufferData query;
+	int i;
+	Oid *param_types;
+	const char** param_values;
+	int* param_lengths;
+	int* param_formats;
+
+	if (nb_cols < 2 || direction==0)
+		return true;					/* nothing to sort */
+
+	param_types = (Oid*) pg_malloc(nb_cols*sizeof(Oid));
+	param_values = (const char**) pg_malloc(nb_cols*sizeof(char*));
+	param_lengths = (int*) pg_malloc(nb_cols*sizeof(int));
+	param_formats = (int*) pg_malloc(nb_cols*sizeof(int));
+
+	initPQExpBuffer(&query);
+
+	/*
+	 * The query returns the original position of each value in our list,
+	 * ordered by its new position. The value itself is not returned.
+	 */
+	appendPQExpBuffer(&query, "SELECT n FROM (VALUES");
+
+	for (i=1; i <= nb_cols; i++)
+	{
+		if (i < nb_cols)
+			appendPQExpBuffer(&query, "($%d,%d),", i, i);
+		else
+		{
+			appendPQExpBuffer(&query, "($%d,%d)) AS l(x,n) ORDER BY x", i, i);
+			if (direction < 0)
+				appendPQExpBuffer(&query, " DESC");
+		}
+
+		param_types[i-1] = coltype;
+		param_values[i-1] = columns[i-1].name;
+		param_lengths[i-1] = strlen(columns[i-1].name);
+		param_formats[i-1] = 0;
+	}
+
+	res = PQexecParams(pset.db,
+					   query.data,
+					   nb_cols,
+					   param_types,
+					   param_values,
+					   param_lengths,
+					   param_formats,
+					   0);
+
+	if (res)
+	{
+		ExecStatusType status = PQresultStatus(res);
+		if (status == PGRES_TUPLES_OK)
+		{
+			for (i=0; i < PQntuples(res); i++)
+			{
+				int old_pos = atoi(PQgetvalue(res, i, 0));
+
+				if (old_pos < 1 || old_pos > nb_cols || i >= nb_cols)
+				{
+					/*
+					 * A position outside of the range is normally impossible.
+					 * If this happens, we're facing a malfunctioning or hostile
+					 * server or middleware.
+					 */
+					psql_error(_("Unexpected value when sorting horizontal headers"));
+					goto cleanup;
+				}
+				else
+				{
+					columns[old_pos-1].rank = i;
+				}
+			}
+		}
+		else
+		{
+			psql_error(_("Query error when sorting horizontal headers: %s"),
+					   PQerrorMessage(pset.db));
+			goto cleanup;
+		}
+	}
+
+	retval = true;
+
+cleanup:
+	termPQExpBuffer(&query);
+	if (res)
+		PQclear(res);
+	pg_free(param_types);
+	pg_free(param_values);
+	pg_free(param_lengths);
+	pg_free(param_formats);
+	return retval;
+}
+
+static void
+printCrosstab(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, rn;
+	char col_align = 'l';		/* alignment for values inside the grid */
+	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+						column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+	 *  associating each piv_columns[].rank to its index in piv_columns.
+	 *  This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+	/*
+	 * In the case of 3 output columns, the contents in the cells are exactly
+	 * the contents of the "value" column (3rd column by default), so their
+	 * alignment is determined by PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (PQnfields(results) == 3)
+	{
+		int colnum;				/* column placed inside the grid */
+		/*
+		 * find colnum in the permutations of (0,1,2) where colnum is
+		 * neither field_for_rows nor field_for_columns
+		 */
+		switch (field_for_rows)
+		{
+		case 0:
+			colnum = (field_for_columns == 1) ? 2 : 1;
+			break;
+		case 1:
+			colnum = (field_for_columns == 0) ? 2: 0;
+			break;
+		default:				/* should be always 2 */
+			colnum = (field_for_columns == 0) ? 1: 0;
+			break;
+		}
+		col_align = column_type_alignment(PQftype(results, colnum));
+	}
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		printTableAddHeader(&cont,
+							piv_columns[horiz_map[i]].name,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		char* row_name;
+		char* col_name;
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		row_number = col_number = -1;
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+		{
+			row_name = PQgetvalue(results, rn, field_for_rows);
+			p = (struct pivot_field*) bsearch(&row_name,
+											  piv_rows,
+											  num_rows,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				row_number = p->rank;
+		}
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+		{
+			col_name = PQgetvalue(results, rn, field_for_columns);
+			p = (struct pivot_field*) bsearch(&col_name,
+											  piv_columns,
+											  num_columns,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				col_number = p->rank;
+		}
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+			int k = 0;
+
+			do {
+				char *content;
+
+				if (PQnfields(results) == 2)
+				{
+					/*
+					  special case: when the source has only 2 columns, use a
+					  X (cross/checkmark) for the cell content, and set
+					  src_col to a virtual additional column.
+					*/
+					content = "X";
+					src_col = 3;
+				}
+				else if (src_col == field_for_rows || src_col == field_for_columns)
+				{
+					/*
+					  The source values that produce headers are not processed
+					  in this loop, only the values that end up inside the grid.
+					*/
+					src_col++;
+					continue;
+				}
+				else
+				{
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (k==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+				k++;
+				src_col++;
+			} while (src_col < PQnfields(results));
+		}
+	}
+
+	printTable(&cont, pset.queryFout, false, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static int
+fieldnameCmp(const char* arg, const char* fieldname)
+{
+	const unsigned char* p = (const unsigned char*) arg;
+	const unsigned char* f = (const unsigned char*) fieldname;
+	unsigned char c;
+
+	if (*p++ != '"')
+		return pg_strcasecmp(arg, fieldname);
+
+	while ((c=*p++))
+	{
+		if (c=='"')
+		{
+			if (*p=='"')
+				p++;			/* skip second quote and continue */
+			else if (*p=='\0')
+				return *p-*f;		/* p finishes before f or is identical */
+
+		}
+		if (*f=='\0')
+			return 1;			/* f finishes before p */
+		if (c!=*f)
+			return c-*f;
+		f++;
+	}
+	return (*f=='\0') ? 0 : 1;
+}
+
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *  on success, the 0-based index of the column
+ *  or -1 if the column number or name is not found in the result's structure,
+ *        or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char* arg, PGresult* res)
+{
+	int idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0  || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int i;
+		idx = -1;
+		for (i=0; i < PQnfields(res); i++)
+		{
+			if (fieldnameCmp(arg, PQfname(res, i)) == 0)
+			{
+				if (idx>=0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+bool
+PrintResultsInCrossTab(PGresult* res)
+{
+	/* [-|+]COLV or null */
+	const char* opt_field_for_rows = pset.crosstabview_col_V;
+	/* [-|+]COLH or null */
+	const char* opt_field_for_columns = pset.crosstabview_col_H;
+	int		rn;
+	struct pivot_field	*piv_columns = NULL;
+	struct pivot_field	*piv_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	retval = false;
+	int		columns_sort_direction = 0; /* 1:ascending, 0:none, -1:descending */
+	int		rows_sort_direction = 0; /* 1:ascending, 0:none, -1:descending */
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows;
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		goto error_return;
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	/* field for rows in crosstab */
+	if (opt_field_for_rows == NULL)
+		field_for_rows = 0;
+	else
+	{
+		if (*opt_field_for_rows == '-')
+		{
+			rows_sort_direction = -1;
+			opt_field_for_rows++;
+		}
+		else if (*opt_field_for_rows == '+')
+		{
+			rows_sort_direction = 1;
+			opt_field_for_rows++;
+		}
+		field_for_rows = indexOfColumn(opt_field_for_rows, res);
+		if (field_for_rows < 0)
+			goto error_return;
+	}
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	/* field for columns in crosstab */
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		/*
+		 * descending sort is requested if the column reference is
+		 * preceded with a minus sign
+		 */
+		if (*opt_field_for_columns == '-')
+		{
+			columns_sort_direction = -1;
+			opt_field_for_columns++;
+		}
+		else if (*opt_field_for_columns == '+')
+		{
+			columns_sort_direction = 1;
+			opt_field_for_columns++;
+		}
+		field_for_columns = indexOfColumn(opt_field_for_columns, res);
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * First part: accumulate row names and column names, each into their
+	 * array. Use client-side sort but only to build the set of DISTINCT
+	 * values. The final order displayed depends only on server-side
+	 * sorts.
+	 */
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		if (!PQgetisnull(res, rn, field_for_rows))
+		{
+			accumHeader(PQgetvalue(res, rn, field_for_rows),
+						&num_rows,
+						&piv_rows,
+						rn);
+		}
+
+		if (!PQgetisnull(res, rn, field_for_columns))
+		{
+			accumHeader(PQgetvalue(res, rn, field_for_columns),
+						&num_columns,
+						&piv_columns,
+						rn);
+			if (num_columns > 1600)
+			{
+				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+				goto error_return;
+			}
+		}
+	}
+
+	/*
+	 * Second part: sort the list of target columns on the server.
+	 */
+	if (columns_sort_direction != 0)
+	{
+		if (!sortColumns(PQftype(res, field_for_columns),
+						 piv_columns,
+						 num_columns,
+						 columns_sort_direction))
+			goto error_return;
+	}
+
+
+	/*
+	 * Third part: sort the list of target columns on the server.
+	 */
+	if (rows_sort_direction != 0)
+	{
+		if (!sortColumns(PQftype(res, field_for_rows),
+						 piv_rows,
+						 num_rows,
+						 rows_sort_direction))
+			goto error_return;
+	}
+
+	/*
+	 * Fourth part: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns,
+				  piv_columns,
+				  field_for_columns,
+				  num_rows,
+				  piv_rows,
+				  field_for_rows);
+
+	retval = true;
+
+error_return:
+	pg_free(piv_columns);
+	pg_free(piv_rows);
+
+	return retval;
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..e8a0e01
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,30 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+struct pivot_field
+{
+	/* Pointer obtained from PGgetvalue() for colV or colH */
+	char*	name;
+
+	/* Rank of the field in its list, starting at 0.
+	 * - For headers stacked vertically, rank=N means it's the
+	 *   Nth distinct field encountered when looping through rows
+	 *   in their initial order.
+	 * - For headers stacked horizontally, rank is obtained
+	 *   by server-side sorting in sortColumns()
+	 */
+	int		rank;
+};
+
+/* prototypes */
+extern bool
+PrintResultsInCrossTab(PGresult* res);
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 5b63e76..c38d51d 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -175,6 +175,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [V H]    execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
index 05d4b31..b2f8c2b 100644
--- a/src/bin/psql/print.c
+++ b/src/bin/psql/print.c
@@ -3291,30 +3291,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3356,6 +3335,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
index fd565984..218b185 100644
--- a/src/bin/psql/print.h
+++ b/src/bin/psql/print.h
@@ -175,7 +175,7 @@ extern FILE *PageOutput(int lines, const printTableOpt *topt);
 extern void ClosePager(FILE *pagerpipe);
 
 extern void html_escaped_print(const char *in, FILE *fout);
-
+extern char column_type_alignment(Oid);
 extern void printTableInit(printTableContent *const content,
 			   const printTableOpt *opt, const char *title,
 			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 1885bb1..6069024 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -90,6 +90,9 @@ typedef struct _psqlSettings
 
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index b58ec14..07ba26c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -935,7 +935,8 @@ psql_completion(const char *text, int start, int end)
 	};
 
 	static const char *const backslash_commands[] = {
-		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
+		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
+		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
#51Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#49)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

here is patch - supported syntax: \crosstabview VCol [+/-]HCol [HOrderCol]

Order column should to contains any numeric value. Values are sorted on
client side

If I understand correctly, I see a problem with HOrderCol.

If the vertical header consists of, for example, a series of
event names, and it should be sorted by event date, then
the requirement of HOrderCol being strictly numeric is
problematic, in a way that the previous proposal was not, isn't it?

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#52Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#50)
Re: [patch] Proposal for \rotate in psql

2015-12-14 23:09 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

postgres=# \crosstabview 4 +month label

Maybe using optional int order column instead label is better - then you

can

do sort on client side

so the syntax can be "\crosstabview VCol [+/-]HCol [[+-]HOrderCol]

In the meantime I've followed a different idea: allowing the
vertical header to be sorted too, still server-side.

That's because to me, the first impulse for a user noticing that
it's not sorted vertically would be to write
\crosstabview +customer month
rather than figure out the
\crosstabview customer +month_number month_name
invocation.
But both ways aren't even mutually exclusive. We could support
\crosstabview [+|-]colV[:labelV] [+|-]colH[:labelH]
it's more complicated to understand, but not harder to implement.

yes, I was able to do what I would - although the query was little bit
strange

select amount, label, customer from (select sum(amount) as amount,
extract(month from closed)::int - 1 as Month, to_char(date_trunc('month',
closed), 'TMmon') as label, customer from data group by customer,
to_char(date_trunc('month', closed), 'TMmon'), extract(month from closed)
union select sum(amount), extract(month from closed)::int - 1 as month,
to_char(date_trunc('month', closed), 'TMmon') as label, '**** TOTAL ****'
from data group by to_char(date_trunc('month', closed), 'TMmon'),
extract(month from closed)::int - 1 order by month) x

\crosstabview +3 2

Also, a non-zero FETCH_COUNT is supported by this version of the patch,
if the first internal FETCH retrieves less than FETCH_COUNT rows.
Otherwise a specific error is emitted.

good idea

Also there are minor changes in arguments and callers following
recent code changes for \o

Trying to crosstab with 10k+ distinct values vertically, I've noticed
that the current code is too slow, spending too much time
sorting. I'm currently replacing its simple arrays of distinct values
with AVL binary trees, which I expect to be much more efficient for
this.

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#53Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#51)
Re: [patch] Proposal for \rotate in psql

2015-12-14 23:15 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

here is patch - supported syntax: \crosstabview VCol [+/-]HCol

[HOrderCol]

Order column should to contains any numeric value. Values are sorted on
client side

If I understand correctly, I see a problem with HOrderCol.

If the vertical header consists of, for example, a series of
event names, and it should be sorted by event date, then
the requirement of HOrderCol being strictly numeric is
problematic, in a way that the previous proposal was not, isn't it?

I don't think - If you are able to do sort on server side, then you can use
window functions and attach some numeric in correct order.

But the situation is more simple probably - usually you are able to
transform the field for order to number, so you don't need window functions
- the transform function is enough.

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#54Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#50)
Re: [patch] Proposal for \rotate in psql

2015-12-14 23:09 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

postgres=# \crosstabview 4 +month label

Maybe using optional int order column instead label is better - then you

can

do sort on client side

so the syntax can be "\crosstabview VCol [+/-]HCol [[+-]HOrderCol]

In the meantime I've followed a different idea: allowing the
vertical header to be sorted too, still server-side.

That's because to me, the first impulse for a user noticing that
it's not sorted vertically would be to write
\crosstabview +customer month
rather than figure out the
\crosstabview customer +month_number month_name
invocation.
But both ways aren't even mutually exclusive. We could support
\crosstabview [+|-]colV[:labelV] [+|-]colH[:labelH]
it's more complicated to understand, but not harder to implement.

Also, a non-zero FETCH_COUNT is supported by this version of the patch,
if the first internal FETCH retrieves less than FETCH_COUNT rows.
Otherwise a specific error is emitted.

Also there are minor changes in arguments and callers following
recent code changes for \o

Trying to crosstab with 10k+ distinct values vertically, I've noticed
that the current code is too slow, spending too much time
sorting. I'm currently replacing its simple arrays of distinct values
with AVL binary trees, which I expect to be much more efficient for
this.

I played with last version and it is looking well. I have only one notice,
but it is subjective - so can be ignored if you don't like it.

The symbol 'X' in two column mode should be centred - now it is aligned to
left, what is not nice. For unicode line style I prefer some unicode symbol
- your chr(10003) is nice.

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#55Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#54)
Re: [patch] Proposal for \rotate in psql

2015-12-17 21:33 GMT+01:00 Pavel Stehule <pavel.stehule@gmail.com>:

2015-12-14 23:09 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

postgres=# \crosstabview 4 +month label

Maybe using optional int order column instead label is better - then

you can

do sort on client side

so the syntax can be "\crosstabview VCol [+/-]HCol [[+-]HOrderCol]

In the meantime I've followed a different idea: allowing the
vertical header to be sorted too, still server-side.

That's because to me, the first impulse for a user noticing that
it's not sorted vertically would be to write
\crosstabview +customer month
rather than figure out the
\crosstabview customer +month_number month_name
invocation.
But both ways aren't even mutually exclusive. We could support
\crosstabview [+|-]colV[:labelV] [+|-]colH[:labelH]
it's more complicated to understand, but not harder to implement.

Also, a non-zero FETCH_COUNT is supported by this version of the patch,
if the first internal FETCH retrieves less than FETCH_COUNT rows.
Otherwise a specific error is emitted.

Also there are minor changes in arguments and callers following
recent code changes for \o

Trying to crosstab with 10k+ distinct values vertically, I've noticed
that the current code is too slow, spending too much time
sorting. I'm currently replacing its simple arrays of distinct values
with AVL binary trees, which I expect to be much more efficient for
this.

I played with last version and it is looking well. I have only one notice,
but it is subjective - so can be ignored if you don't like it.

The symbol 'X' in two column mode should be centred - now it is aligned to
left, what is not nice. For unicode line style I prefer some unicode symbol
- your chr(10003) is nice.

I checked code and I have only one note. The name "sortColumns" is not
valid now, and it isn't well - maybe ServerSideSort or some similar can be
better. The error message "Unexpected value when sorting horizontal
headers" is obsolete too.

Regards

Pavel

Show quoted text

Regards

Pavel

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#56Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#54)
Re: [patch] Proposal for \rotate in psql

Pavel Stehule wrote:

The symbol 'X' in two column mode should be centred - now it is aligned to
left, what is not nice

Currently print.c does not support centered alignment, only left and right.
Should we add it, it would have to work for all output formats
(except obviously for "unaligned"):
- aligned
- wrapped
- html
- latex
- latex-longtable
- troff-ms
- asciidoc

Because of this, I believe that adding support for a 'c' alignment
might be a significant patch by itself, and that it should be considered
separately.

I agree that if it existed, the crosstabview command should use it
as you mention, but I'm not volunteering to implement it myself, at
least not in the short term.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#57Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#56)
Re: [patch] Proposal for \rotate in psql

2015-12-18 21:21 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

The symbol 'X' in two column mode should be centred - now it is aligned

to

left, what is not nice

Currently print.c does not support centered alignment, only left and right.
Should we add it, it would have to work for all output formats
(except obviously for "unaligned"):
- aligned
- wrapped
- html
- latex
- latex-longtable
- troff-ms
- asciidoc

Because of this, I believe that adding support for a 'c' alignment
might be a significant patch by itself, and that it should be considered
separately.

ok

I agree that if it existed, the crosstabview command should use it
as you mention, but I'm not volunteering to implement it myself, at
least not in the short term.

I'll look how much work it is

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

#58Pavel Stehule
pavel.stehule@gmail.com
In reply to: Pavel Stehule (#57)
1 attachment(s)
Re: [patch] Proposal for \rotate in psql

2015-12-19 6:55 GMT+01:00 Pavel Stehule <pavel.stehule@gmail.com>:

2015-12-18 21:21 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Pavel Stehule wrote:

The symbol 'X' in two column mode should be centred - now it is aligned

to

left, what is not nice

Currently print.c does not support centered alignment, only left and
right.
Should we add it, it would have to work for all output formats
(except obviously for "unaligned"):
- aligned
- wrapped
- html
- latex
- latex-longtable
- troff-ms
- asciidoc

Because of this, I believe that adding support for a 'c' alignment
might be a significant patch by itself, and that it should be considered
separately.

ok

I agree that if it existed, the crosstabview command should use it
as you mention, but I'm not volunteering to implement it myself, at
least not in the short term.

I'll look how much work it is

attached patch allows align to center.

everywhere where left/right align was allowed, the center align is allowed

Regards

Pavel

Show quoted text

Regards

Pavel

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-align-center.difftext/plain; charset=US-ASCII; name=psql-align-center.diffDownload
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 6d13b57..c9e11c2
*** a/src/bin/psql/crosstabview.c
--- b/src/bin/psql/crosstabview.c
*************** printCrosstab(const PGresult *results,
*** 203,209 ****
  	 * alignment is determined by PQftype(). Otherwise the contents are
  	 * made-up strings, so the alignment is 'l'
  	 */
! 	if (PQnfields(results) == 3)
  	{
  		int colnum;				/* column placed inside the grid */
  		/*
--- 203,213 ----
  	 * alignment is determined by PQftype(). Otherwise the contents are
  	 * made-up strings, so the alignment is 'l'
  	 */
! 	if (PQnfields(results) == 2)
! 	{
! 		col_align = 'c';
! 	}
! 	else if (PQnfields(results) == 3)
  	{
  		int colnum;				/* column placed inside the grid */
  		/*
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
new file mode 100644
index b2f8c2b..6e2cc6e
*** a/src/bin/psql/print.c
--- b/src/bin/psql/print.c
*************** print_aligned_text(const printTableConte
*** 1042,1059 ****
  					{
  						/* spaces first */
  						fprintf(fout, "%*s", width_wrap[j] - chars_to_output, "");
- 						fputnbytes(fout,
- 								 (char *) (this_line->ptr + bytes_output[j]),
- 								   bytes_to_output);
  					}
! 					else	/* Left aligned cell */
  					{
! 						/* spaces second */
! 						fputnbytes(fout,
! 								 (char *) (this_line->ptr + bytes_output[j]),
! 								   bytes_to_output);
  					}
  
  					bytes_output[j] += bytes_to_output;
  
  					/* Do we have more text to wrap? */
--- 1042,1059 ----
  					{
  						/* spaces first */
  						fprintf(fout, "%*s", width_wrap[j] - chars_to_output, "");
  					}
! 					else if (cont->aligns[j] == 'c') /* Center aligned cell */
  					{
! 						/* spaces first */
! 						fprintf(fout, "%*s",
! 							 (width_wrap[j] - chars_to_output) / 2, "");
  					}
  
+ 					fputnbytes(fout,
+ 							 (char *) (this_line->ptr + bytes_output[j]),
+ 							   bytes_to_output);
+ 
  					bytes_output[j] += bytes_to_output;
  
  					/* Do we have more text to wrap? */
*************** print_aligned_text(const printTableConte
*** 1083,1095 ****
  				 * If left-aligned, pad out remaining space if needed (not
  				 * last column, and/or wrap marks required).
  				 */
! 				if (cont->aligns[j] != 'r')		/* Left aligned cell */
  				{
! 					if (finalspaces ||
! 						wrap[j] == PRINT_LINE_WRAP_WRAP ||
! 						wrap[j] == PRINT_LINE_WRAP_NEWLINE)
  						fprintf(fout, "%*s",
! 								width_wrap[j] - chars_to_output, "");
  				}
  
  				/* Print right-hand wrap or newline mark */
--- 1083,1104 ----
  				 * If left-aligned, pad out remaining space if needed (not
  				 * last column, and/or wrap marks required).
  				 */
! 				if (finalspaces ||
! 					wrap[j] == PRINT_LINE_WRAP_WRAP ||
! 					wrap[j] == PRINT_LINE_WRAP_NEWLINE)
  				{
! 					if (cont->aligns[j] == 'c')
! 					{
! 						/* last spaces */
  						fprintf(fout, "%*s",
! 								    width_wrap[j] - chars_to_output 
! 								 - ((width_wrap[j] - chars_to_output) / 2 ), "");
! 					}
! 					else if (cont->aligns[j] == 'l')
! 					{
! 						/* last spaces */
! 						fprintf(fout, "%*s", width_wrap[j] - chars_to_output, "");
! 					}
  				}
  
  				/* Print right-hand wrap or newline mark */
*************** html_escaped_print(const char *in, FILE
*** 1778,1783 ****
--- 1787,1805 ----
  	}
  }
  
+ /*
+  * Returns align value in html format
+  */
+ static const char *
+ format_html_align_attr(char align)
+ {
+ 	if (align == 'r')
+ 		return "right";
+ 	else if (align == 'c')
+ 		return "center";
+ 	else
+ 		return "left";
+ }
  
  static void
  print_html_text(const printTableContent *cont, FILE *fout)
*************** print_html_text(const printTableContent
*** 1830,1836 ****
  			fputs("  <tr valign=\"top\">\n", fout);
  		}
  
! 		fprintf(fout, "    <td align=\"%s\">", cont->aligns[(i) % cont->ncolumns] == 'r' ? "right" : "left");
  		/* is string only whitespace? */
  		if ((*ptr)[strspn(*ptr, " \t")] == '\0')
  			fputs("&nbsp; ", fout);
--- 1852,1860 ----
  			fputs("  <tr valign=\"top\">\n", fout);
  		}
  
! 		fprintf(fout, "    <td align=\"%s\">",
! 			    format_html_align_attr(cont->aligns[(i) % cont->ncolumns]));
! 
  		/* is string only whitespace? */
  		if ((*ptr)[strspn(*ptr, " \t")] == '\0')
  			fputs("&nbsp; ", fout);
*************** print_html_vertical(const printTableCont
*** 1916,1922 ****
  		html_escaped_print(cont->headers[i % cont->ncolumns], fout);
  		fputs("</th>\n", fout);
  
! 		fprintf(fout, "    <td align=\"%s\">", cont->aligns[i % cont->ncolumns] == 'r' ? "right" : "left");
  		/* is string only whitespace? */
  		if ((*ptr)[strspn(*ptr, " \t")] == '\0')
  			fputs("&nbsp; ", fout);
--- 1940,1950 ----
  		html_escaped_print(cont->headers[i % cont->ncolumns], fout);
  		fputs("</th>\n", fout);
  
! 
! 
! 		fprintf(fout, "    <td align=\"%s\">", 
! 			    format_html_align_attr(cont->aligns[i % cont->ncolumns]));
! 
  		/* is string only whitespace? */
  		if ((*ptr)[strspn(*ptr, " \t")] == '\0')
  			fputs("&nbsp; ", fout);
*************** asciidoc_escaped_print(const char *in, F
*** 1971,1976 ****
--- 1999,2015 ----
  	}
  }
  
+ static const char *
+ format_asciidoc_align_attr(char align)
+ {
+ 	if (align == 'r')
+ 		return ">l";
+ 	else if (align == 'c')
+ 		return "^l";
+ 	else
+ 		return "<l";
+ }
+ 
  static void
  print_asciidoc_text(const printTableContent *cont, FILE *fout)
  {
*************** print_asciidoc_text(const printTableCont
*** 2001,2007 ****
  		{
  			if (i != 0)
  				fputs(",", fout);
! 			fprintf(fout, "%s", cont->aligns[(i) % cont->ncolumns] == 'r' ? ">l" : "<l");
  		}
  		fputs("\"", fout);
  		switch (opt_border)
--- 2040,2047 ----
  		{
  			if (i != 0)
  				fputs(",", fout);
! 			fprintf(fout, "%s",
! 				    format_asciidoc_align_attr(cont->aligns[(i) % cont->ncolumns]));
  		}
  		fputs("\"", fout);
  		switch (opt_border)
*************** print_asciidoc_vertical(const printTable
*** 2142,2148 ****
  		fputs("<l|", fout);
  		asciidoc_escaped_print(cont->headers[i % cont->ncolumns], fout);
  
! 		fprintf(fout, " %s|", cont->aligns[i % cont->ncolumns] == 'r' ? ">l" : "<l");
  		/* is string only whitespace? */
  		if ((*ptr)[strspn(*ptr, " \t")] == '\0')
  			fputs(" ", fout);
--- 2182,2190 ----
  		fputs("<l|", fout);
  		asciidoc_escaped_print(cont->headers[i % cont->ncolumns], fout);
  
! 		fprintf(fout, " %s|",
! 				    format_asciidoc_align_attr(cont->aligns[(i) % cont->ncolumns]));
! 
  		/* is string only whitespace? */
  		if ((*ptr)[strspn(*ptr, " \t")] == '\0')
  			fputs(" ", fout);
*************** print_latex_longtable_text(const printTa
*** 2343,2349 ****
  
  		for (i = 0; i < cont->ncolumns; i++)
  		{
! 			/* longtable supports either a width (p) or an alignment (l/r) */
  			/* Are we left-justified and was a proportional width specified? */
  			if (*(cont->aligns + i) == 'l' && opt_table_attr)
  			{
--- 2385,2391 ----
  
  		for (i = 0; i < cont->ncolumns; i++)
  		{
! 			/* longtable supports either a width (p) or an alignment (l/c/r) */
  			/* Are we left-justified and was a proportional width specified? */
  			if (*(cont->aligns + i) == 'l' && opt_table_attr)
  			{
*************** printTableInit(printTableContent *const
*** 2950,2957 ****
   * If translate is true, the function will pass the header through gettext.
   * Otherwise, the header will not be translated.
   *
!  * align is either 'l' or 'r', and specifies the alignment for cells in this
!  * column.
   */
  void
  printTableAddHeader(printTableContent *const content, char *header,
--- 2992,2999 ----
   * If translate is true, the function will pass the header through gettext.
   * Otherwise, the header will not be translated.
   *
!  * align is either 'l' or 'c' or'r', and specifies the alignment for cells
!  * in this column.
   */
  void
  printTableAddHeader(printTableContent *const content, char *header,
#59Michael Paquier
michael.paquier@gmail.com
In reply to: Pavel Stehule (#58)
Re: [patch] Proposal for \rotate in psql

On Sun, Dec 20, 2015 at 6:49 AM, Pavel Stehule <pavel.stehule@gmail.com> wrote:

attached patch allows align to center.

everywhere where left/right align was allowed, the center align is allowed

Moved to next CF, there is a fresh and new patch.
--
Michael

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#60Daniel Verite
daniel@manitou-mail.org
In reply to: Daniel Verite (#50)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Hi,

Here's an updated patch that replaces sorted arrays by AVL binary trees
when gathering distinct values for the columns involved in the pivot.
The change is essential for large resultsets. For instance,
it allows to process a query like this (10 million rows x 10 columns):

select x,(random()*10)::int, (random()*1000)::int from
generate_series(1,10000000) as x
\crosstabview

which takes about 30 seconds to run and display on my machine with the
attached patch. That puts it seemingly in the same ballpark than
the equivalent test with the server-side crosstab().

With the previous iterations of the patch, this test would never end,
even with much smaller sets, as the execution time of the 1st step
grew exponentially with the number of distinct keys.
The exponential effect starts to be felt at about 10k values on my low-end
CPU,
and from there quickly becomes problematic.

As a client-side display feature, processing millions of rows like in
the query above does not necessarily make sense, it's pushing the
envelope, but stalling way below 100k rows felt lame, so I'm happy to get
rid of that limitation.

However, there is another one. The above example does not need or request
an additional sort step, but if it did, sorting more than 65535 entries in
the vertical header would error out, because values are shipped as
parameters to PQexecParams(), which only accepts that much.
To avoid the problem, when the rows in the output "grid" exceed 2^16 and
they need to be sorted, the user must let the sort being driven by ORDER BY
beforehand in the query, knowing that the pivot will keep the original
ordering intact in the vertical header.

I'm still thinking about extending this based on Pavel's diff for the
"label" column, so that
\crosstabview [+|-]colV[:colSortH] [+|-]colH[:colSortH]
would mean to use colV/H as grid headers but sort them according
to colSortV/H.
I prefer that syntax over adding more parameters, and also I'd like
to have it work in both V and H directions.

Aside from the AVL trees, there are a few other minor changes in that
patch:
- move non-exportable structs from the .h to the .c
- move code in common.c to respect alphabetical ordering
- if vertical sort is requested, add explicit check against more than 65535
params instead of letting the sort query fail
- report all failure cases of the sort query
- rename sortColumns to serverSort and use less the term "columns" in
comments and variables.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-crosstabview-v9.difftext/x-patch; name=psql-crosstabview-v9.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 6d0cb3d..563324d 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2485,6 +2485,95 @@ lo_import 152801
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><literal>\crosstabview [ [-|+]<replaceable class="parameter">colV</replaceable>  [-|+]<replaceable class="parameter">colH</replaceable> ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows the results
+        inside a crosstab grid. The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header and the output column
+        <replaceable class="parameter">colH</replaceable> becomes a horizontal header.
+        The results for the other output columns are projected inside the grid.
+        </para>
+
+        <para>
+        <replaceable class="parameter">colV</replaceable>
+        and <replaceable class="parameter">colH</replaceable> can indicate a
+        column position (starting at 1), or a column name. Normal case folding
+        and quoting rules apply on column names. By default,
+        <replaceable class="parameter">colV</replaceable> is column 1
+        and <replaceable class="parameter">colH</replaceable> is column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the set of all distinct values found in
+        column <replaceable class="parameter">colV</replaceable>, in the order
+        of their first appearance in the query results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the set of all distinct non-null values found in
+        column <replaceable class="parameter">colH</replaceable>.  They come
+        by default in their order of appearance in the query results, or in ascending
+        order if a plus (+) sign precedes <replaceable class="parameter">colH</replaceable>,
+        or in descending order if it's a minus (-) sign.
+        </para>
+
+        <para>
+        The query results being tuples of <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        a cell located at the intersection <literal>(x,y)</literal> in the grid
+        has contents determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the results such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed in the cell as
+         if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to one row
+         except that the values coming from different rows are stacked
+         vertically, rows being separated by newline characters inside
+         the same cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
 
       <varlistentry>
         <term><literal>\s [ <replaceable class="parameter">filename</replaceable> ]</literal></term>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index f1336d5..9cb0c4a 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o crosstabview.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index cf6876b..b5ec8af 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -39,6 +39,7 @@
 
 #include "common.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "describe.h"
 #include "help.h"
 #include "input.h"
@@ -364,6 +365,34 @@ exec_command(const char *cmd,
 	else if (strcmp(cmd, "copyright") == 0)
 		print_copyright();
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		char	*opt1,
+				*opt2;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1): NULL;
+			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2): NULL;
+			pset.crosstabview_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+	}
+
 	/* \d* commands */
 	else if (cmd[0] == 'd')
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index a287eee..f5db53d 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -24,6 +24,7 @@
 #include "command.h"
 #include "copy.h"
 #include "mbprint.h"
+#include "crosstabview.h"
 
 
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
@@ -906,6 +907,8 @@ PrintQueryResults(PGresult *results)
 			/* store or print the data ... */
 			if (pset.gset_prefix)
 				success = StoreQueryTuple(results);
+			else if (pset.crosstabview_output)
+				success = PrintResultsInCrossTab(results);
 			else
 				success = PrintQueryTuples(results);
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
@@ -1192,6 +1195,18 @@ sendquery_cleanup:
 		pset.gset_prefix = NULL;
 	}
 
+	/* reset \crosstabview settings */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
 	return OK;
 }
 
@@ -1354,7 +1369,25 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 			is_pager = true;
 		}
 
-		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
+		if (pset.crosstabview_output)
+		{
+			if (ntuples < fetch_count)
+				PrintResultsInCrossTab(results);
+			else
+			{
+				/*
+				  crosstabview is denied if the whole set of rows is not
+				  guaranteed to be fetched in the first iteration, because
+				  it's expected in memory as a single PGresult structure.
+				*/
+				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
+					fetch_count);
+				PQclear(results);
+				break;
+			}
+		}
+		else
+			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
 
 		PQclear(results);
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..82f3e2e
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,845 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+
+#include "common.h"
+#include "crosstabview.h"
+#include "pqexpbuffer.h"
+#include "settings.h"
+#include <string.h>
+
+/*
+ * Value/position from the resultset that goes into the horizontal or vertical
+ * crosstabview header.
+ */
+struct pivot_field
+{
+	/*
+	 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
+	 * value becomes an entry in the vertical header (colV), or horizontal
+	 * header (colH)
+	 */
+	char	   *name;
+
+	/*
+	 * Rank of this value, starting at 0. Initially, it's the relative position
+	 * of the first appearance of this value in the resultset.
+	 * For example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
+	 * After all values have been gathered, ranks may be updated by serverSort()
+	 */
+	int			rank;
+};
+
+/* Node in avl_tree */
+struct avl_node
+{
+	/* Node contents */
+	struct pivot_field field;
+
+	/*
+	 * Height of this node in the tree (number of nodes on the longest
+	 * path to a leaf).
+	 */
+	int			height;
+
+	/*
+	 * Child nodes. [0] points to left subtree, [1] to right subtree.
+	 * Never NULL, points to the empty node avl_tree.end when no left
+	 * or right value.
+	 */
+	struct avl_node *childs[2];
+};
+
+/*
+ * Control structure for the AVL tree (binary search tree kept
+ * balanced with the AVL algorithm)
+ */
+struct avl_tree
+{
+	int			count;			/* Total number of nodes */
+	int freecnt;
+	struct avl_node *root;		/* root of the tree */
+	struct avl_node *end;		/* Immutable dereferenceable empty tree */
+};
+
+/*
+ * The avl* functions below provide a minimalistic implementation of AVL binary
+ * trees, to efficiently collect the distinct values that will form the horizontal
+ * and vertical headers. It only supports adding new values, no removal or even
+ * search.
+ */
+static void
+avlInit(struct avl_tree *tree)
+{
+	tree->end = (struct avl_node*) pg_malloc0(sizeof(struct avl_node));
+	tree->end->childs[0] = tree->end->childs[1] = tree->end;
+	tree->count = 0;
+	tree->root = tree->end;
+	tree->freecnt=0;
+}
+
+/* Deallocate recursively an AVL tree, starting from node */
+static void
+avlFree(struct avl_tree* tree, struct avl_node* node)
+{
+	if (node->childs[0] != tree->end)
+	{
+		avlFree(tree, node->childs[0]);
+		pg_free(node->childs[0]);
+	}
+	if (node->childs[1] != tree->end)
+	{
+		avlFree(tree, node->childs[1]);
+		pg_free(node->childs[1]);
+	}
+	if (node == tree->root) {
+		/* free the root separately as it's not child of anything */
+		if (node != tree->end)
+			pg_free(node);
+		/* free the tree->end struct only once and when all else is freed */
+		pg_free(tree->end);
+	}
+}
+
+/* Set the height to 1 plus the greatest of left and right heights */
+static void
+avlUpdateHeight(struct avl_node *n)
+{
+	n->height = 1 + (n->childs[0]->height > n->childs[1]->height ?
+					 n->childs[0]->height:
+					 n->childs[1]->height);
+}
+
+/* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
+static struct avl_node*
+avlRotate(struct avl_node **current, int dir)
+{
+	struct avl_node *before = *current;
+	struct avl_node *after = (*current)->childs[dir];
+
+	*current = after;
+	before->childs[dir] = after->childs[!dir];
+	avlUpdateHeight(before);
+	after->childs[!dir] = before;
+
+	return after;
+}
+
+static int
+avlBalance(struct avl_node *n)
+{
+	return n->childs[0]->height - n->childs[1]->height;
+}
+
+/*
+ * After an insertion, possibly rebalance the tree so that the left and right
+ * node heights don't differ by more than 1.
+ * May update *node.
+ */
+static void
+avlAdjustBalance(struct avl_tree *tree, struct avl_node **node)
+{
+	struct avl_node *current = *node;
+	int b = avlBalance(current)/2;
+	if (b != 0)
+	{
+		int dir = (1 - b)/2;
+		if (avlBalance(current->childs[dir]) == -b)
+		  avlRotate(&current->childs[dir], !dir);
+		current = avlRotate(node, dir);
+	}
+	if (current != tree->end)
+	  avlUpdateHeight(current);
+}
+
+/*
+ * Insert a new value/field, starting from *node, reaching the
+ * correct position in the tree by recursion.
+ * Possibly rebalance the tree and possibly update *node.
+ * Do nothing if the value is already present in the tree.
+ */
+static void
+avlInsertNode(struct avl_tree* tree,
+			  struct avl_node **node,
+			  struct pivot_field field)
+{
+	struct avl_node *current = *node;
+
+	if (current == tree->end)
+	{
+		struct avl_node * new_node = (struct avl_node*)
+			pg_malloc(sizeof(struct avl_node));
+		new_node->height = 1;
+		new_node->field = field;
+		new_node->childs[0] = new_node->childs[1] = tree->end;
+		tree->count++;
+		*node = new_node;
+	}
+	else
+	{
+		int cmp = strcmp(field.name, current->field.name);
+		if (cmp != 0)
+		{
+			avlInsertNode(tree,
+						  cmp > 0 ? &current->childs[1] : &current->childs[0],
+						  field);
+			avlAdjustBalance(tree, node);
+		}
+	}
+}
+
+/* Insert the value into the AVL tree, if it does not preexist */
+static void
+avlMergeValue(struct avl_tree* tree, char* name)
+{
+	struct pivot_field field;
+	field.name = name;
+	field.rank = tree->count;
+	avlInsertNode(tree, &tree->root, field);
+}
+
+/*
+ * Recursively extract node values into the names array, in sorted order with a
+ * left-to-right tree traversal.
+ * Return the next candidate offset to write into the names array.
+ * fields[] must be preallocated to hold tree->count entries
+ */
+static int
+avlCollectFields(struct avl_tree* tree,
+				 struct avl_node* node,
+				 struct pivot_field* fields,
+				 int idx)
+{
+	if (node == tree->end)
+		return idx;
+	idx = avlCollectFields(tree, node->childs[0], fields, idx);
+	fields[idx] = node->field;
+	return avlCollectFields(tree, node->childs[1], fields,  idx+1);
+}
+
+/*
+ * Send a query to sort the list of values in a horizontal or vertical
+ * crosstabview header, and update every source[].rank field with the 
+ * new relative position of each value.
+ * coltype is the type's OID for the column from which the values come.
+ * direction is -1 for descending, 1 for ascending, 0 for no sort.
+ */
+static bool
+serverSort(Oid coltype, struct pivot_field *source, int nb_values, int direction)
+{
+	bool retval = false;
+	PGresult *res = NULL;
+	PQExpBufferData query;
+	int i;
+	Oid *param_types;
+	const char** param_values;
+	int* param_lengths;
+	int* param_formats;
+
+	if (nb_values < 2 || direction==0)
+		return true;					/* nothing to sort */
+
+	param_types = (Oid*) pg_malloc(nb_values*sizeof(Oid));
+	param_values = (const char**) pg_malloc(nb_values*sizeof(char*));
+	param_lengths = (int*) pg_malloc(nb_values*sizeof(int));
+	param_formats = (int*) pg_malloc(nb_values*sizeof(int));
+
+	initPQExpBuffer(&query);
+
+	/*
+	 * The query returns the original position of each value in our list,
+	 * ordered by its new position. The value itself is not returned.
+	 */
+	appendPQExpBufferStr(&query, "SELECT n FROM (VALUES");
+
+	for (i=1; i <= nb_values; i++)
+	{
+		if (i < nb_values)
+			appendPQExpBuffer(&query, "($%d,%d),", i, i);
+		else
+		{
+			appendPQExpBuffer(&query, "($%d,%d)) AS l(x,n) ORDER BY x", i, i);
+			if (direction < 0)
+				appendPQExpBufferStr(&query, " DESC");
+		}
+
+		param_types[i-1] = coltype;
+		param_values[i-1] = source[i-1].name;
+		param_lengths[i-1] = strlen(source[i-1].name);
+		param_formats[i-1] = 0;
+	}
+
+	res = PQexecParams(pset.db,
+					   query.data,
+					   nb_values,
+					   param_types,
+					   param_values,
+					   param_lengths,
+					   param_formats,
+					   0);
+
+	if (res && PQresultStatus(res) == PGRES_TUPLES_OK)
+	{
+		for (i=0; i < PQntuples(res); i++)
+		{
+			int old_pos = atoi(PQgetvalue(res, i, 0));
+
+			if (old_pos < 1 || old_pos > nb_values || i >= nb_values)
+			{
+				/*
+				 * A position outside of the range is normally impossible.
+				 * If this happens, we're facing a malfunctioning or hostile
+				 * server or middleware.
+				 */
+				psql_error(_("Unexpected rank when sorting crosstabview headers"));
+				goto cleanup;
+			}
+			else
+			{
+				source[old_pos-1].rank = i;
+			}
+		}
+	}
+	else
+	{
+		psql_error(_("Query error when sorting crosstabview header: %s"),
+				   PQerrorMessage(pset.db));
+		goto cleanup;
+	}
+
+	retval = true;
+
+cleanup:
+	termPQExpBuffer(&query);
+	if (res)
+		PQclear(res);
+	pg_free(param_types);
+	pg_free(param_values);
+	pg_free(param_lengths);
+	pg_free(param_formats);
+	return retval;
+}
+
+/* for bsearch() inside a sorted array of struct pivot_field */
+static int
+headerCompare(const void *a, const void *b)
+{
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+
+static void
+printCrosstab(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, rn;
+	char col_align = 'l';		/* alignment for values inside the grid */
+	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+						column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+	 *  associating each piv_columns[].rank to its index in piv_columns.
+	 *  This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+	/*
+	 * In the case of 3 output columns, the contents in the cells are exactly
+	 * the contents of the "value" column (3rd column by default), so their
+	 * alignment is determined by PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (PQnfields(results) == 3)
+	{
+		int colnum;				/* column placed inside the grid */
+		/*
+		 * find colnum in the permutations of (0,1,2) where colnum is
+		 * neither field_for_rows nor field_for_columns
+		 */
+		switch (field_for_rows)
+		{
+		case 0:
+			colnum = (field_for_columns == 1) ? 2 : 1;
+			break;
+		case 1:
+			colnum = (field_for_columns == 0) ? 2: 0;
+			break;
+		default:				/* should be always 2 */
+			colnum = (field_for_columns == 0) ? 1: 0;
+			break;
+		}
+		col_align = column_type_alignment(PQftype(results, colnum));
+	}
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		printTableAddHeader(&cont,
+							piv_columns[horiz_map[i]].name,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		char* row_name;
+		char* col_name;
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		row_number = col_number = -1;
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+		{
+			row_name = PQgetvalue(results, rn, field_for_rows);
+			p = (struct pivot_field*) bsearch(&row_name,
+											  piv_rows,
+											  num_rows,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				row_number = p->rank;
+		}
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+		{
+			col_name = PQgetvalue(results, rn, field_for_columns);
+			p = (struct pivot_field*) bsearch(&col_name,
+											  piv_columns,
+											  num_columns,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				col_number = p->rank;
+		}
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+			int k = 0;
+
+			do {
+				char *content;
+
+				if (PQnfields(results) == 2)
+				{
+					/*
+					  special case: when the source has only 2 columns, use a
+					  X (cross/checkmark) for the cell content, and set
+					  src_col to a virtual additional column.
+					*/
+					content = "X";
+					src_col = 3;
+				}
+				else if (src_col == field_for_rows || src_col == field_for_columns)
+				{
+					/*
+					  The source values that produce headers are not processed
+					  in this loop, only the values that end up inside the grid.
+					*/
+					src_col++;
+					continue;
+				}
+				else
+				{
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (k==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+				k++;
+				src_col++;
+			} while (src_col < PQnfields(results));
+		}
+	}
+
+	printTable(&cont, pset.queryFout, false, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static int
+fieldNameCmp(const char* arg, const char* fieldname)
+{
+	const unsigned char* p = (const unsigned char*) arg;
+	const unsigned char* f = (const unsigned char*) fieldname;
+	unsigned char c;
+
+	if (*p++ != '"')
+		return pg_strcasecmp(arg, fieldname);
+
+	while ((c=*p++))
+	{
+		if (c=='"')
+		{
+			if (*p=='"')
+				p++;			/* skip second quote and continue */
+			else if (*p=='\0')
+				return *p-*f;		/* p finishes before f or is identical */
+
+		}
+		if (*f=='\0')
+			return 1;			/* f finishes before p */
+		if (c!=*f)
+			return c-*f;
+		f++;
+	}
+	return (*f=='\0') ? 0 : 1;
+}
+
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *  on success, the 0-based index of the column
+ *  or -1 if the column number or name is not found in the result's structure,
+ *        or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char* arg, PGresult* res)
+{
+	int idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0  || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int i;
+		idx = -1;
+		for (i=0; i < PQnfields(res); i++)
+		{
+			if (fieldNameCmp(arg, PQfname(res, i)) == 0)
+			{
+				if (idx>=0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+/*
+ * Main function.
+ * Process the data from *res according the display options in pset (global),
+ * to generate the horizontal and vertical headers contents,
+ * then call printCrosstab() for the actual output.
+ */
+bool
+PrintResultsInCrossTab(PGresult* res)
+{
+	/* [-|+]COLV or null */
+	const char* opt_field_for_rows = pset.crosstabview_col_V;
+	/* [-|+]COLH or null */
+	const char* opt_field_for_columns = pset.crosstabview_col_H;
+	int		rn;
+	struct avl_tree	piv_columns;
+	struct avl_tree	piv_rows;
+	struct pivot_field* array_columns = NULL;
+	struct pivot_field* array_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	retval = false;
+	int		columns_sort_direction = 0; /* 1:ascending, 0:none, -1:descending */
+	int		rows_sort_direction = 0; /* 1:ascending, 0:none, -1:descending */
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows;
+
+	avlInit(&piv_rows);
+	avlInit(&piv_columns);
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		goto error_return;
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	/* field for rows in crosstab */
+	if (opt_field_for_rows == NULL)
+		field_for_rows = 0;
+	else
+	{
+		if (*opt_field_for_rows == '-')
+		{
+			rows_sort_direction = -1;
+			opt_field_for_rows++;
+		}
+		else if (*opt_field_for_rows == '+')
+		{
+			rows_sort_direction = 1;
+			opt_field_for_rows++;
+		}
+		field_for_rows = indexOfColumn(opt_field_for_rows, res);
+		if (field_for_rows < 0)
+			goto error_return;
+	}
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	/* field for columns in crosstab */
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		/*
+		 * descending sort is requested if the column reference is
+		 * preceded with a minus sign
+		 */
+		if (*opt_field_for_columns == '-')
+		{
+			columns_sort_direction = -1;
+			opt_field_for_columns++;
+		}
+		else if (*opt_field_for_columns == '+')
+		{
+			columns_sort_direction = 1;
+			opt_field_for_columns++;
+		}
+		field_for_columns = indexOfColumn(opt_field_for_columns, res);
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * First part: accumulate the names that go into the vertical and
+	 * horizontal headers, each into an AVL binary tree to build the set of
+	 * DISTINCT values.
+	 */
+
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		if (!PQgetisnull(res, rn, field_for_rows))
+		{
+			avlMergeValue(&piv_rows,
+						  PQgetvalue(res, rn, field_for_rows));
+			if (rows_sort_direction!=0 && piv_rows.count > 65535)
+			{
+				psql_error(_("Maximum number of rows to sort (65535) exceeded for the vertical header\n"));
+				goto error_return;
+			}
+		}
+
+		if (!PQgetisnull(res, rn, field_for_columns))
+		{
+			avlMergeValue(&piv_columns,
+						  PQgetvalue(res, rn, field_for_columns));
+			if (piv_columns.count > 1600)
+			{
+				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+				goto error_return;
+			}
+		}
+	}
+
+
+	/*
+	 * Second part: Generate sorted arrays from the AVL trees.
+	 */
+
+	num_columns = piv_columns.count;
+	num_rows = piv_rows.count;
+
+	array_columns = (struct pivot_field*) pg_malloc(sizeof(struct pivot_field) * num_columns);
+	array_rows = (struct pivot_field*) pg_malloc(sizeof(struct pivot_field) * num_rows);
+
+	avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0);
+	avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0);
+
+	/*
+	 * Third part: sort (by server-side query) the horizontal and/or vertical
+	 * header when applicable.
+	 */
+	if (columns_sort_direction != 0)
+	{
+		if (!serverSort(PQftype(res, field_for_columns),
+						array_columns,
+						num_columns,
+						columns_sort_direction))
+			goto error_return;
+	}
+
+	if (rows_sort_direction != 0)
+	{
+		if (!serverSort(PQftype(res, field_for_rows),
+						array_rows,
+						num_rows,
+						rows_sort_direction))
+			goto error_return;
+	}
+
+	/*
+	 * Fourth part: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns,
+				  array_columns,
+				  field_for_columns,
+				  num_rows,
+				  array_rows,
+				  field_for_rows);
+
+	retval = true;
+
+error_return:
+	avlFree(&piv_columns, piv_columns.root);
+	avlFree(&piv_rows, piv_rows.root);
+	pg_free(array_columns);
+	pg_free(array_rows);
+
+	return retval;
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..ccbfa02
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,14 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2015, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+/* prototypes */
+extern bool	PrintResultsInCrossTab(PGresult *res);
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 5f240be..3924046 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -175,6 +175,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [V H]    execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
index 05d4b31..b2f8c2b 100644
--- a/src/bin/psql/print.c
+++ b/src/bin/psql/print.c
@@ -3291,30 +3291,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3356,6 +3335,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
index fd565984..218b185 100644
--- a/src/bin/psql/print.h
+++ b/src/bin/psql/print.h
@@ -175,7 +175,7 @@ extern FILE *PageOutput(int lines, const printTableOpt *topt);
 extern void ClosePager(FILE *pagerpipe);
 
 extern void html_escaped_print(const char *in, FILE *fout);
-
+extern char column_type_alignment(Oid);
 extern void printTableInit(printTableContent *const content,
 			   const printTableOpt *opt, const char *title,
 			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 1885bb1..6069024 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -90,6 +90,9 @@ typedef struct _psqlSettings
 
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index df678d5..bdaa92b 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1172,7 +1172,8 @@ psql_completion(const char *text, int start, int end)
 
 	/* psql's backslash commands. */
 	static const char *const backslash_commands[] = {
-		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
+		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
+		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
#61Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#60)
Re: [patch] Proposal for \crosstabview in psql

2015-12-23 21:36 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Hi,

Here's an updated patch that replaces sorted arrays by AVL binary trees
when gathering distinct values for the columns involved in the pivot.
The change is essential for large resultsets. For instance,
it allows to process a query like this (10 million rows x 10 columns):

select x,(random()*10)::int, (random()*1000)::int from
generate_series(1,10000000) as x
\crosstabview

which takes about 30 seconds to run and display on my machine with the
attached patch. That puts it seemingly in the same ballpark than
the equivalent test with the server-side crosstab().

With the previous iterations of the patch, this test would never end,
even with much smaller sets, as the execution time of the 1st step
grew exponentially with the number of distinct keys.
The exponential effect starts to be felt at about 10k values on my low-end
CPU,
and from there quickly becomes problematic.

As a client-side display feature, processing millions of rows like in
the query above does not necessarily make sense, it's pushing the
envelope, but stalling way below 100k rows felt lame, so I'm happy to get
rid of that limitation.

However, there is another one. The above example does not need or request
an additional sort step, but if it did, sorting more than 65535 entries in
the vertical header would error out, because values are shipped as
parameters to PQexecParams(), which only accepts that much.
To avoid the problem, when the rows in the output "grid" exceed 2^16 and
they need to be sorted, the user must let the sort being driven by ORDER
BY
beforehand in the query, knowing that the pivot will keep the original
ordering intact in the vertical header.

I'm still thinking about extending this based on Pavel's diff for the
"label" column, so that
\crosstabview [+|-]colV[:colSortH] [+|-]colH[:colSortH]
would mean to use colV/H as grid headers but sort them according
to colSortV/H.
I prefer that syntax over adding more parameters, and also I'd like
to have it work in both V and H directions.

This syntax is good - simple, readable

Pavel

Show quoted text

Aside from the AVL trees, there are a few other minor changes in that
patch:
- move non-exportable structs from the .h to the .c
- move code in common.c to respect alphabetical ordering
- if vertical sort is requested, add explicit check against more than 65535
params instead of letting the sort query fail
- report all failure cases of the sort query
- rename sortColumns to serverSort and use less the term "columns" in
comments and variables.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#62Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#60)
Re: [patch] Proposal for \crosstabview in psql

Hi

2015-12-23 21:36 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Hi,

Here's an updated patch that replaces sorted arrays by AVL binary trees
when gathering distinct values for the columns involved in the pivot.
The change is essential for large resultsets. For instance,
it allows to process a query like this (10 million rows x 10 columns):

select x,(random()*10)::int, (random()*1000)::int from
generate_series(1,10000000) as x
\crosstabview

which takes about 30 seconds to run and display on my machine with the
attached patch. That puts it seemingly in the same ballpark than
the equivalent test with the server-side crosstab().

With the previous iterations of the patch, this test would never end,
even with much smaller sets, as the execution time of the 1st step
grew exponentially with the number of distinct keys.
The exponential effect starts to be felt at about 10k values on my low-end
CPU,
and from there quickly becomes problematic.

As a client-side display feature, processing millions of rows like in
the query above does not necessarily make sense, it's pushing the
envelope, but stalling way below 100k rows felt lame, so I'm happy to get
rid of that limitation.

However, there is another one. The above example does not need or request
an additional sort step, but if it did, sorting more than 65535 entries in
the vertical header would error out, because values are shipped as
parameters to PQexecParams(), which only accepts that much.
To avoid the problem, when the rows in the output "grid" exceed 2^16 and
they need to be sorted, the user must let the sort being driven by ORDER
BY
beforehand in the query, knowing that the pivot will keep the original
ordering intact in the vertical header.

I'm still thinking about extending this based on Pavel's diff for the
"label" column, so that
\crosstabview [+|-]colV[:colSortH] [+|-]colH[:colSortH]
would mean to use colV/H as grid headers but sort them according
to colSortV/H.
I prefer that syntax over adding more parameters, and also I'd like
to have it work in both V and H directions.

Aside from the AVL trees, there are a few other minor changes in that
patch:
- move non-exportable structs from the .h to the .c
- move code in common.c to respect alphabetical ordering
- if vertical sort is requested, add explicit check against more than 65535
params instead of letting the sort query fail
- report all failure cases of the sort query
- rename sortColumns to serverSort and use less the term "columns" in
comments and variables.

I checked this version and it is looking well.

* all regress tests passed
* patch is clean, well documented, well formatted
* no objection related to code
* current limits 65K * 1600 is good enough, I don't see it as limiting

I am looking for next version

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#63Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#61)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Hi,

Here's an updated patch improving on how the horizontal and vertical
headers can be sorted.

The discussion upthread went into how it was desirable
to have independant sorts for these headers, possibly driven
by another column, in addition to the query's ORDER BY.

Thus the options now accepted are:

\crosstabview [ [-|+]colV[:scolV] [-|+]colH[:scolH] [colG1[,colG2...]] ]

The optional scolV/scolH columns drive sorts for respectively
colV/colH (colV:scolV somehow means SELECT colV from... order by scolV)

colG1,... in 3rd arg indicate the columns whose contents form the grid
cells, the typical use case being that there's only one such column.
By default it's all columns minus colV and colH.

For example,

SELECT
cust_id,
cust_name,
cust_date,
date_part('month, sales_date),
to_char(sales_date, 'Mon') as month,
amount
FROM sales_view
WHERE [predicates]
[ORDER BY ...]

If we want to look at <amount> in a grid with months names across, sorted
by month number, and customer name in the vertical header, sorted by date of
acquisition, we could do this:

\crosstabview +cust_name:cust_date +5:4 amount

or letting the vertical header being sorted by the query's ORDER BY,
and the horizontal header same as above:

\crosstabview cust_name +5:4 amount

or sorting vertically by name, if it happens that the ORDER BY is missing or
is on something else:

\crosstabview +cust_name +5:4 amount

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-crosstabview-v10.difftext/x-patch; name=psql-crosstabview-v10.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 6d0cb3d..a242ec4 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -990,6 +990,123 @@ testdb=&gt;
       </varlistentry>
 
       <varlistentry>
+        <term><literal>\crosstabview [
+            [-|+]<replaceable class="parameter">colV</replaceable>
+            [:<replaceable class="parameter">scolV</replaceable>]
+            [-|+]<replaceable class="parameter">colH</replaceable>
+            [:<replaceable class="parameter">scolH</replaceable>]
+            [<replaceable class="parameter">colG1[,colG2...]</replaceable>]
+            ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows
+        the results inside a crosstab grid.
+        The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header, optionally sorted by <replaceable class="parameter">scolV</replaceable>,
+        and the output column <replaceable class="parameter">colH</replaceable>
+        becomes a horizontal header, optionally sorted by
+        <replaceable class="parameter">scolH</replaceable>.
+
+        <replaceable class="parameter">colG1[,colG2...]</replaceable>
+        is the list of output columns to project into the grid.
+        By default, all output columns of the query except 
+        <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>
+        are included in this list.
+        </para>
+
+        <para>
+        All columns can be refered to by their position (starting at 1), or by
+        their name. Normal case folding and quoting rules apply on column
+        names. By default,
+        <replaceable class="parameter">colV</replaceable> corresponds to column 1
+        and <replaceable class="parameter">colH</replaceable> to column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the set of all distinct values found in
+        column <replaceable class="parameter">colV</replaceable>, in the order
+        of their first appearance in the query results,
+        or in ascending order if a plus (+) sign precedes
+        <replaceable class="parameter">colV</replaceable>,
+        or in descending order if it's a minus (-) sign.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the set of all distinct non-null values found in
+        column <replaceable class="parameter">colH</replaceable>.  They come
+        by default in their order of appearance in the query results,
+        or in ascending order if a plus (+) sign precedes
+        <replaceable class="parameter">colH</replaceable>,
+        or in descending order if it's a minus (-) sign.
+
+        Also, they can optionally be sorted by another column, if
+        <replaceable class="parameter">colV</replaceable>
+        (respectively <replaceable class="parameter">colH</replaceable>) is
+        immediately followed by a colon and a reference to another column
+        <replaceable class="parameter">scolV</replaceable>
+        (respectively <replaceable class="parameter">scolH</replaceable>).
+        </para>
+
+        <para>
+        Inside the crosstab grid,
+        given a query output with <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        the contents of a cell located at the intersection
+        <literal>(x,y)</literal> is determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the query results such that the
+         value for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns or the columns listed in
+         <replaceable class="parameter">colG1[,colG2...]</replaceable>
+         are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed
+         in the cell as if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to
+         the case of one row except that the values coming from different rows
+         are stacked vertically, the different source rows being separated by
+         newline characters inside the cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term><literal>\d[S+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
 
         <listitem>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index 66e14fb..9d29fe1 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o crosstabview.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 9750a5b..e4db76e 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -39,6 +39,7 @@
 
 #include "common.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "describe.h"
 #include "help.h"
 #include "input.h"
@@ -364,6 +365,39 @@ exec_command(const char *cmd,
 	else if (strcmp(cmd, "copyright") == 0)
 		print_copyright();
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		char	*opt1,
+			*opt2,
+			*opt3;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt3 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1): NULL;
+			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2): NULL;
+			pset.crosstabview_cols_grid = opt3 ? pg_strdup(opt3): NULL;
+			pset.crosstabview_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+		free(opt3);
+	}
+
 	/* \d* commands */
 	else if (cmd[0] == 'd')
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 2cb2e9b..b368883 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -24,6 +24,7 @@
 #include "command.h"
 #include "copy.h"
 #include "mbprint.h"
+#include "crosstabview.h"
 
 
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
@@ -906,6 +907,8 @@ PrintQueryResults(PGresult *results)
 			/* store or print the data ... */
 			if (pset.gset_prefix)
 				success = StoreQueryTuple(results);
+			else if (pset.crosstabview_output)
+				success = PrintResultsInCrossTab(results);
 			else
 				success = PrintQueryTuples(results);
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
@@ -1192,6 +1195,23 @@ sendquery_cleanup:
 		pset.gset_prefix = NULL;
 	}
 
+	/* reset \crosstabview settings */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
+	if (pset.crosstabview_cols_grid)
+	{
+		free(pset.crosstabview_cols_grid);
+		pset.crosstabview_cols_grid = NULL;
+	}
 	return OK;
 }
 
@@ -1354,7 +1374,25 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 			is_pager = true;
 		}
 
-		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
+		if (pset.crosstabview_output)
+		{
+			if (ntuples < fetch_count)
+				PrintResultsInCrossTab(results);
+			else
+			{
+				/*
+				  crosstabview is denied if the whole set of rows is not
+				  guaranteed to be fetched in the first iteration, because
+				  it's expected in memory as a single PGresult structure.
+				*/
+				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
+					fetch_count);
+				PQclear(results);
+				break;
+			}
+		}
+		else
+			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
 
 		PQclear(results);
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..65b10ff
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,1038 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+
+#include "common.h"
+#include "crosstabview.h"
+#include "pqexpbuffer.h"
+#include "settings.h"
+#include <string.h>
+
+/*
+ * Value/position from the resultset that goes into the horizontal or vertical
+ * crosstabview header.
+ */
+struct pivot_field
+{
+	/*
+	 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
+	 * value becomes an entry in the vertical header (colV), or horizontal
+	 * header (colH)
+	 */
+	char	   *name;
+
+	/*
+	 * When a sort is requested on an alternative column, this holds
+	 * PQgetvalue() for the sort column and the first row
+	 * from which <name> is obtained
+	 */
+	char	   *sort_value;
+
+	/*
+	 * Rank of this value, starting at 0. Initially, it's the relative position
+	 * of the first appearance of this value in the resultset.
+	 * For example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
+	 * After all values have been gathered, ranks may be updated by serverSort()
+	 */
+	int			rank;
+};
+
+/* Node in avl_tree */
+struct avl_node
+{
+	/* Node contents */
+	struct pivot_field field;
+
+	/*
+	 * Height of this node in the tree (number of nodes on the longest
+	 * path to a leaf).
+	 */
+	int			height;
+
+	/*
+	 * Child nodes. [0] points to left subtree, [1] to right subtree.
+	 * Never NULL, points to the empty node avl_tree.end when no left
+	 * or right value.
+	 */
+	struct avl_node *childs[2];
+};
+
+/*
+ * Control structure for the AVL tree (binary search tree kept
+ * balanced with the AVL algorithm)
+ */
+struct avl_tree
+{
+	int			count;			/* Total number of nodes */
+	struct avl_node *root;		/* root of the tree */
+	struct avl_node *end;		/* Immutable dereferenceable empty tree */
+};
+
+/*
+ * The avl* functions below provide a minimalistic implementation of AVL binary
+ * trees, to efficiently collect the distinct values that will form the horizontal
+ * and vertical headers. It only supports adding new values, no removal or even
+ * search.
+ */
+static void
+avlInit(struct avl_tree *tree)
+{
+	tree->end = (struct avl_node*) pg_malloc0(sizeof(struct avl_node));
+	tree->end->childs[0] = tree->end->childs[1] = tree->end;
+	tree->count = 0;
+	tree->root = tree->end;
+}
+
+/* Deallocate recursively an AVL tree, starting from node */
+static void
+avlFree(struct avl_tree* tree, struct avl_node* node)
+{
+	if (node->childs[0] != tree->end)
+	{
+		avlFree(tree, node->childs[0]);
+		pg_free(node->childs[0]);
+	}
+	if (node->childs[1] != tree->end)
+	{
+		avlFree(tree, node->childs[1]);
+		pg_free(node->childs[1]);
+	}
+	if (node == tree->root) {
+		/* free the root separately as it's not child of anything */
+		if (node != tree->end)
+			pg_free(node);
+		/* free the tree->end struct only once and when all else is freed */
+		pg_free(tree->end);
+	}
+}
+
+/* Set the height to 1 plus the greatest of left and right heights */
+static void
+avlUpdateHeight(struct avl_node *n)
+{
+	n->height = 1 + (n->childs[0]->height > n->childs[1]->height ?
+					 n->childs[0]->height:
+					 n->childs[1]->height);
+}
+
+/* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
+static struct avl_node*
+avlRotate(struct avl_node **current, int dir)
+{
+	struct avl_node *before = *current;
+	struct avl_node *after = (*current)->childs[dir];
+
+	*current = after;
+	before->childs[dir] = after->childs[!dir];
+	avlUpdateHeight(before);
+	after->childs[!dir] = before;
+
+	return after;
+}
+
+static int
+avlBalance(struct avl_node *n)
+{
+	return n->childs[0]->height - n->childs[1]->height;
+}
+
+/*
+ * After an insertion, possibly rebalance the tree so that the left and right
+ * node heights don't differ by more than 1.
+ * May update *node.
+ */
+static void
+avlAdjustBalance(struct avl_tree *tree, struct avl_node **node)
+{
+	struct avl_node *current = *node;
+	int b = avlBalance(current)/2;
+	if (b != 0)
+	{
+		int dir = (1 - b)/2;
+		if (avlBalance(current->childs[dir]) == -b)
+		  avlRotate(&current->childs[dir], !dir);
+		current = avlRotate(node, dir);
+	}
+	if (current != tree->end)
+	  avlUpdateHeight(current);
+}
+
+/*
+ * Insert a new value/field, starting from *node, reaching the
+ * correct position in the tree by recursion.
+ * Possibly rebalance the tree and possibly update *node.
+ * Do nothing if the value is already present in the tree.
+ */
+static void
+avlInsertNode(struct avl_tree* tree,
+			  struct avl_node **node,
+			  struct pivot_field field)
+{
+	struct avl_node *current = *node;
+
+	if (current == tree->end)
+	{
+		struct avl_node * new_node = (struct avl_node*)
+			pg_malloc(sizeof(struct avl_node));
+		new_node->height = 1;
+		new_node->field = field;
+		new_node->childs[0] = new_node->childs[1] = tree->end;
+		tree->count++;
+		*node = new_node;
+	}
+	else
+	{
+		int cmp = strcmp(field.name, current->field.name);
+		if (cmp != 0)
+		{
+			avlInsertNode(tree,
+						  cmp > 0 ? &current->childs[1] : &current->childs[0],
+						  field);
+			avlAdjustBalance(tree, node);
+		}
+	}
+}
+
+/* Insert the value into the AVL tree, if it does not preexist */
+static void
+avlMergeValue(struct avl_tree* tree, char* name, char* sort_value)
+{
+	struct pivot_field field;
+	field.name = name;
+	field.rank = tree->count;
+	field.sort_value = sort_value;
+	avlInsertNode(tree, &tree->root, field);
+}
+
+/*
+ * Recursively extract node values into the names array, in sorted order with a
+ * left-to-right tree traversal.
+ * Return the next candidate offset to write into the names array.
+ * fields[] must be preallocated to hold tree->count entries
+ */
+static int
+avlCollectFields(struct avl_tree* tree,
+				 struct avl_node* node,
+				 struct pivot_field* fields,
+				 int idx)
+{
+	if (node == tree->end)
+		return idx;
+	idx = avlCollectFields(tree, node->childs[0], fields, idx);
+	fields[idx] = node->field;
+	return avlCollectFields(tree, node->childs[1], fields,  idx+1);
+}
+
+/*
+ * Send a query to sort the list of values in a horizontal or vertical
+ * crosstabview header, and update every source[].rank field with the
+ * new relative position of each value.
+ * coltype is the type's OID for the column from which the values come.
+ * direction is -1 for descending, 1 for ascending, 0 for no sort.
+ */
+static bool
+serverSort(Oid coltype, struct pivot_field *source, int nb_values, int direction)
+{
+	bool retval = false;
+	PGresult *res = NULL;
+	PQExpBufferData query;
+	int i;
+	Oid *param_types;
+	const char** param_values;
+	int* param_formats;
+
+	if (nb_values < 2 || direction==0)
+		return true;					/* nothing to sort */
+
+	param_types = (Oid*) pg_malloc(nb_values*sizeof(Oid));
+	param_values = (const char**) pg_malloc(nb_values*sizeof(char*));
+	param_formats = (int*) pg_malloc(nb_values*sizeof(int));
+
+	initPQExpBuffer(&query);
+
+	/*
+	 * The query returns the original position of each value in our list,
+	 * ordered by its new position. The value itself is not returned.
+	 */
+	appendPQExpBufferStr(&query, "SELECT n FROM (VALUES");
+
+	for (i=0; i < nb_values; i++)
+	{
+		appendPQExpBuffer(&query, "($%d,%d)", i+1, i+1);
+		if (i < nb_values-1)
+			appendPQExpBufferChar(&query, ',');
+		param_types[i] = coltype;
+		param_values[i] = source[i].sort_value;
+		param_formats[i] = 0;
+	}
+
+	appendPQExpBuffer(&query, ") AS l(x,n) ORDER BY x %s",
+					  direction < 0 ? "DESC" : "ASC");
+
+	res = PQexecParams(pset.db,
+					   query.data,
+					   nb_values,
+					   param_types,
+					   param_values,
+					   NULL,	/* lengths not necessary for text format */
+					   param_formats,
+					   0);
+
+	if (res && PQresultStatus(res) == PGRES_TUPLES_OK)
+	{
+		for (i=0; i < PQntuples(res); i++)
+		{
+			int old_pos = atoi(PQgetvalue(res, i, 0));
+
+			if (old_pos < 1 || old_pos > nb_values || i >= nb_values)
+			{
+				/*
+				 * A position outside of the range is normally impossible.
+				 * If this happens, we're facing a malfunctioning or hostile
+				 * server or middleware.
+				 */
+				psql_error(_("Unexpected rank when sorting crosstabview headers\n"));
+				goto cleanup;
+			}
+			else
+			{
+				source[old_pos-1].rank = i;
+			}
+		}
+	}
+	else
+	{
+		psql_error(_("Query error when sorting crosstabview header: %s"),
+				   PQerrorMessage(pset.db));
+		goto cleanup;
+	}
+
+	retval = true;
+
+cleanup:
+	termPQExpBuffer(&query);
+	if (res)
+		PQclear(res);
+	pg_free(param_types);
+	pg_free(param_values);
+	pg_free(param_formats);
+	return retval;
+}
+
+/* for bsearch() inside a sorted array of struct pivot_field */
+static int
+headerCompare(const void *a, const void *b)
+{
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+
+/*
+ * Output the pivoted resultset with the printTable* functions
+ */
+static void
+printCrosstab(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows,
+			  int *colsG,
+			  int colsG_num)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, rn;
+	char col_align = 'l';		/* alignment for values inside the grid */
+	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+						column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+	 *  associating each piv_columns[].rank to its index in piv_columns.
+	 *  This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+
+	/*
+	 * In the common case of only one field projected into the cells, the
+	 * display alignment depends on its PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (colsG_num == 1)
+		col_align = column_type_alignment(PQftype(results, colsG[0]));
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		printTableAddHeader(&cont,
+							piv_columns[horiz_map[i]].name,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		char* row_name;
+		char* col_name;
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		row_number = col_number = -1;
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+		{
+			row_name = PQgetvalue(results, rn, field_for_rows);
+			p = (struct pivot_field*) bsearch(&row_name,
+											  piv_rows,
+											  num_rows,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				row_number = p->rank;
+		}
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+		{
+			col_name = PQgetvalue(results, rn, field_for_columns);
+			p = (struct pivot_field*) bsearch(&col_name,
+											  piv_columns,
+											  num_columns,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				col_number = p->rank;
+		}
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+
+			/*
+			 * special case: when the source has only 2 columns, use a
+			 * X (cross/checkmark) for the cell content, and set
+			 * src_col to a virtual additional column.
+			 */
+			if (PQnfields(results) == 2)
+				src_col = -1;
+
+			for (i=0; i<colsG_num || src_col==-1; i++)
+			{
+				char *content;
+
+				if (src_col == -1)
+				{
+					content = "X";
+				}
+				else
+				{
+					src_col = colsG[i];
+
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (i==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+
+				/* special case of the "virtual column" for checkmark */
+				if (src_col == -1)
+					break;
+			}
+		}
+	}
+
+	printTable(&cont, pset.queryFout, false, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static bool
+fieldNameEquals(const char *arg, const char *fieldname)
+{
+	const char* p = arg;
+	const char* f = fieldname;
+	char c;
+
+	if (*p++ != '"')
+		return !pg_strcasecmp(arg, fieldname);
+
+	while ((c = *p++))
+	{
+		if (c == '"')
+		{
+			if (*p == '"')
+				p++;			/* skip second quote and continue */
+			else if (*p == '\0')
+				return (*f == '\0');	/* p is shorter than f, or is identical */
+		}
+		if (*f == '\0')
+			return false;			/* f is shorter than p */
+		if (c != *f)				/* found one byte that differs */
+			return false;
+		f++;
+	}
+	return (*f=='\0');
+}
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *  on success, the 0-based index of the column
+ *  or -1 if the column number or name is not found in the result's structure,
+ *        or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char *arg, PGresult *res)
+{
+	int idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0  || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int i;
+		idx = -1;
+		for (i=0; i < PQnfields(res); i++)
+		{
+			if (fieldNameEquals(arg, PQfname(res, i)))
+			{
+				if (idx>=0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+/*
+ * Parse col1[<sep>col2][<sep>col3]...
+ * where colN can be:
+ * - a number from 1 to PQnfields(res)
+ * - an unquoted column name matching (case insensitively) one of PQfname(res,...)
+ * - a quoted column name matching (case sensitively) one of PQfname(res,...)
+ * max_columns: 0 if no maximum
+ */
+static int
+parseColumnRefs(char* arg,
+				PGresult *res,
+				int **col_numbers,
+				int max_columns,
+				char separator)
+{
+	char *p = arg;
+	char c;
+	int col_num = -1;
+	int nb_cols = 0;
+	char* field_start = NULL;
+	*col_numbers = NULL;
+	while ((c = *p) != '\0')
+	{
+		bool quoted_field = false;
+		field_start = p;
+
+		/* first char */
+		if (c == '"')
+		{
+			quoted_field = true;
+			p++;
+		}
+
+		while ((c = *p) != '\0')
+		{
+			if (c == separator && !quoted_field)
+				break;
+			if (c == '"')		/* end of field or embedded double quote */
+			{
+				p++;
+				if (*p == '"')
+				{
+					if (quoted_field)
+					{
+						p++;
+						continue;
+					}
+				}
+				else if (quoted_field && *p == separator)
+					break;
+			}
+			p += PQmblen(p, pset.encoding);
+		}
+
+		if (p != field_start)
+		{
+			/* look up the column and add its index into *col_numbers */
+			if (max_columns != 0 && nb_cols == max_columns)
+			{
+				psql_error(_("No more than %d column references expected\n"), max_columns);
+				goto errfail;
+			}
+			c = *p;
+			*p = '\0';
+			col_num = indexOfColumn(field_start, res);
+			*p = c;
+			if (col_num < 0)
+				goto errfail;
+			*col_numbers = (int*)pg_realloc(*col_numbers, (1+nb_cols)*sizeof(int));
+			(*col_numbers)[nb_cols++] = col_num;
+		}
+		else
+		{
+			psql_error(_("Empty column reference\n"));
+			goto errfail;
+		}
+
+		if (*p)
+			p += PQmblen(p, pset.encoding);
+	}
+	return nb_cols;
+
+errfail:
+	pg_free(*col_numbers);
+	*col_numbers = NULL;
+	return -1;
+}
+
+
+/*
+ * Main function.
+ * Process the data from *res according the display options in pset (global),
+ * to generate the horizontal and vertical headers contents,
+ * then call printCrosstab() for the actual output.
+ */
+bool
+PrintResultsInCrossTab(PGresult* res)
+{
+	/* [-|+]COLV or null */
+	char* opt_field_for_rows = pset.crosstabview_col_V;
+	/* [-|+]COLH or null */
+	char* opt_field_for_columns = pset.crosstabview_col_H;
+	int		rn;
+	struct avl_tree	piv_columns;
+	struct avl_tree	piv_rows;
+	struct pivot_field* array_columns = NULL;
+	struct pivot_field* array_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	retval = false;
+	int		columns_sort_direction; /* 1:ascending, 0:none, -1:descending */
+	int		rows_sort_direction;    /* 1:ascending, 0:none, -1:descending */
+	int		*colsV = NULL, *colsH = NULL, *colsG = NULL;
+	int		colsG_num;
+	int		nn;
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns = -1;
+	int		sort_field_for_columns = -1;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows = -1;
+	int		sort_field_for_rows = -1;
+
+	avlInit(&piv_rows);
+	avlInit(&piv_columns);
+
+	if (res == NULL)
+	{
+		psql_error(_("No result\n"));
+		goto error_return;
+	}
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		psql_error(_("The query must return results to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the vertical header (1st arg)
+	 * displayed in the left-most column. Determine:
+	 * - the sort direction if any
+	 * - the field number of that column in the PGresult
+	 * - the field number of the associated sort column if any
+	 */
+
+	if (opt_field_for_rows == NULL)
+	{
+		field_for_rows = sort_field_for_rows = 0;
+		rows_sort_direction = 0;
+	}
+	else
+	{
+		if (*opt_field_for_rows == '-')
+		{
+			rows_sort_direction = -1;
+			opt_field_for_rows++;
+		}
+		else if (*opt_field_for_rows == '+')
+		{
+			rows_sort_direction = 1;
+			opt_field_for_rows++;
+		}
+		else
+			rows_sort_direction = 0;
+
+		nn = parseColumnRefs(opt_field_for_rows, res, &colsV, 2, ':');
+		if (nn < 0)
+			goto error_return;
+		if (nn==1)
+		{
+			field_for_rows = colsV[0];
+			sort_field_for_rows = field_for_rows;
+		}
+		else
+		{
+			field_for_rows = colsV[0];
+			sort_field_for_rows = colsV[1];
+			if (rows_sort_direction == 0)
+			{
+				psql_error(_("Sort column specified without a sort direction\n"));
+				goto error_return;
+			}
+		}
+	}
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	/*
+	 * Arguments processing for the horizontal header (2nd arg)
+	 * (pivoted column that gets displayed as the first row).
+	 * Determine:
+	 * - the sort direction if any
+	 * - the field number of that column in the PGresult
+	 * - the field number of the associated sort column if any
+	 */
+
+	if (opt_field_for_columns == NULL)
+	{
+		field_for_columns = sort_field_for_columns = 1;
+		columns_sort_direction = 0;
+	}
+	else
+	{
+		/*
+		 * descending sort is requested if the column reference is
+		 * preceded with a minus sign
+		 */
+		if (*opt_field_for_columns == '-')
+		{
+			columns_sort_direction = -1;
+			opt_field_for_columns++;
+		}
+		else if (*opt_field_for_columns == '+')
+		{
+			columns_sort_direction = 1;
+			opt_field_for_columns++;
+		}
+		else
+			columns_sort_direction = 0;
+
+		nn = parseColumnRefs(opt_field_for_columns, res, &colsH, 2, ':');
+		if (nn <= 0)
+			goto error_return;
+		if (nn==1)
+		{
+			field_for_columns = colsH[0];
+			sort_field_for_columns = field_for_columns;
+		}
+		else
+		{
+			field_for_columns = colsH[0];
+			sort_field_for_columns = colsH[1];
+			if (columns_sort_direction == 0)
+			{
+				psql_error(_("Sort column specified without a sort direction\n"));
+				goto error_return;
+			}
+		}
+
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the columns aside from headers (3rd arg)
+	 * Determine the columns to display in the grid and their order.
+	 */
+	if (pset.crosstabview_cols_grid == NULL)
+	{
+		/*
+		 * By defaut, all the fields from PGresult get displayed into the grid,
+		 * except the two fields that go into the vertical and horizontal
+		 * headers.
+		 */
+		if (PQnfields(res) > 2)
+		{
+			int i, j=0;
+			colsG = (int*)pg_malloc(sizeof(int) * (PQnfields(res)-2));
+			for (i=0; i<PQnfields(res); i++)
+			{
+				if (i!=field_for_rows && i!=field_for_columns)
+					colsG[j++] = i;
+			}
+			colsG_num = PQnfields(res)-2;
+		}
+		else
+		{
+			colsG = NULL;
+			colsG_num = 0;
+		}
+	}
+	else
+	{
+		/*
+		 * Non-default case: a list of fields is given.
+		 * Parse that list to determine the fields to display into the grid,
+		 * and in what order.
+		 * The list format is colA[,colB[,colC...]]
+		 */
+		colsG_num = parseColumnRefs(pset.crosstabview_cols_grid,
+									res, &colsG, PQnfields(res), ',');
+		if (colsG_num <= 0)
+			goto error_return;
+	}
+
+	/*
+	 * First part: accumulate the names that go into the vertical and
+	 * horizontal headers, each into an AVL binary tree to build the set of
+	 * DISTINCT values.
+	 */
+
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		if (!PQgetisnull(res, rn, field_for_rows))
+		{
+			avlMergeValue(&piv_rows,
+						  PQgetvalue(res, rn, field_for_rows),
+						  PQgetvalue(res, rn, sort_field_for_rows));
+
+			if (rows_sort_direction!=0 && piv_rows.count > 65535)
+			{
+				psql_error(_("Maximum number of rows to sort (65535) exceeded for the vertical header\n"));
+				goto error_return;
+			}
+		}
+
+		if (!PQgetisnull(res, rn, field_for_columns))
+		{
+			avlMergeValue(&piv_columns,
+						  PQgetvalue(res, rn, field_for_columns),
+						  PQgetvalue(res, rn, sort_field_for_columns));
+
+			if (piv_columns.count > 1600)
+			{
+				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+				goto error_return;
+			}
+		}
+	}
+
+
+	/*
+	 * Second part: Generate sorted arrays from the AVL trees.
+	 */
+
+	num_columns = piv_columns.count;
+	num_rows = piv_rows.count;
+
+	array_columns = (struct pivot_field*) pg_malloc(sizeof(struct pivot_field) * num_columns);
+	array_rows = (struct pivot_field*) pg_malloc(sizeof(struct pivot_field) * num_rows);
+
+	avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0);
+	avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0);
+
+	/*
+	 * Third part: sort (by server-side query) the horizontal and/or vertical
+	 * header when applicable.
+	 */
+	if (columns_sort_direction != 0)
+	{
+		if (!serverSort(PQftype(res, sort_field_for_columns),
+						array_columns,
+						num_columns,
+						columns_sort_direction))
+			goto error_return;
+	}
+
+	if (rows_sort_direction != 0)
+	{
+		if (!serverSort(PQftype(res, sort_field_for_rows),
+						array_rows,
+						num_rows,
+						rows_sort_direction))
+			goto error_return;
+	}
+
+	/*
+	 * Fourth part: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns,
+				  array_columns,
+				  field_for_columns,
+				  num_rows,
+				  array_rows,
+				  field_for_rows,
+				  colsG,
+				  colsG_num);
+
+	retval = true;
+
+error_return:
+	avlFree(&piv_columns, piv_columns.root);
+	avlFree(&piv_rows, piv_rows.root);
+	pg_free(array_columns);
+	pg_free(array_rows);
+	pg_free(colsV);
+	pg_free(colsH);
+	pg_free(colsG);
+
+	return retval;
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..d374cfe
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,14 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+/* prototypes */
+extern bool	PrintResultsInCrossTab(PGresult *res);
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 59f6f25..26afa68 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -175,6 +175,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [V H]    execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
index 8958903..f3d9a83 100644
--- a/src/bin/psql/print.c
+++ b/src/bin/psql/print.c
@@ -3291,30 +3291,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3356,6 +3335,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
index 868fcdd..4f3d85a 100644
--- a/src/bin/psql/print.h
+++ b/src/bin/psql/print.h
@@ -175,7 +175,7 @@ extern FILE *PageOutput(int lines, const printTableOpt *topt);
 extern void ClosePager(FILE *pagerpipe);
 
 extern void html_escaped_print(const char *in, FILE *fout);
-
+extern char column_type_alignment(Oid);
 extern void printTableInit(printTableContent *const content,
 			   const printTableOpt *opt, const char *title,
 			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 20a6470..9b7f7c4 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -90,6 +90,10 @@ typedef struct _psqlSettings
 
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
+	char		*crosstabview_cols_grid;  /* one-shot \crosstabview 3nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index f09e65c..50cf55f 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1272,7 +1272,8 @@ psql_completion(const char *text, int start, int end)
 
 	/* psql's backslash commands. */
 	static const char *const backslash_commands[] = {
-		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
+		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
+		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
#64Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#63)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Hi

2016-01-22 19:53 GMT+01:00 Daniel Verite <daniel@manitou-mail.org>:

Hi,

Here's an updated patch improving on how the horizontal and vertical
headers can be sorted.

The discussion upthread went into how it was desirable
to have independant sorts for these headers, possibly driven
by another column, in addition to the query's ORDER BY.

Thus the options now accepted are:

\crosstabview [ [-|+]colV[:scolV] [-|+]colH[:scolH] [colG1[,colG2...]] ]

The optional scolV/scolH columns drive sorts for respectively
colV/colH (colV:scolV somehow means SELECT colV from... order by scolV)

colG1,... in 3rd arg indicate the columns whose contents form the grid
cells, the typical use case being that there's only one such column.
By default it's all columns minus colV and colH.

For example,

SELECT
cust_id,
cust_name,
cust_date,
date_part('month, sales_date),
to_char(sales_date, 'Mon') as month,
amount
FROM sales_view
WHERE [predicates]
[ORDER BY ...]

If we want to look at <amount> in a grid with months names across, sorted
by month number, and customer name in the vertical header, sorted by date
of
acquisition, we could do this:

\crosstabview +cust_name:cust_date +5:4 amount

or letting the vertical header being sorted by the query's ORDER BY,
and the horizontal header same as above:

\crosstabview cust_name +5:4 amount

or sorting vertically by name, if it happens that the ORDER BY is missing
or
is on something else:

\crosstabview +cust_name +5:4 amount

I am playing with this patch, and I have following comments:

1. maybe we can decrease name to shorter "crossview" ?? I am happy with
crosstabview too, just crossview is correct too, and shorter

2. Columns used for ordering should not be displayed by default. I can live
with current behave, but hiding ordering columns is much more practical for
me

3. This code is longer, so some regress tests are recommended - attached
simple test case

Regards

Pavel

Show quoted text

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

regresstest.sqlapplication/sql; name=regresstest.sqlDownload
#65Daniel Verite
daniel@manitou-mail.org
In reply to: Pavel Stehule (#64)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Pavel Stehule wrote:

1. maybe we can decrease name to shorter "crossview" ?? I am happy with
crosstabview too, just crossview is correct too, and shorter

I'm in favor of keeping crosstabview. It's more explicit, only
3 characters longer and we have tab completion anyway.

2. Columns used for ordering should not be displayed by default. I can live
with current behave, but hiding ordering columns is much more practical for
me

I can see why, but I'm concerned about a consequence:
say we have 4 columns A,B,C,D and user does \crosstabview +A:B +C:D
If B and D are excluded by default, then there's nothing
left to display inside the grid.
It doesn't feel quite right. There's something counter-intuitive
in the fact that values in the grid would disappear depending on
whether and how headers are sorted.
With the 3rd argument, we let the user decide what they want
to see.

3. This code is longer, so some regress tests are recommended - attached
simple test case

I've added a few regression tests to the psql testsuite
based on your sample data. New patch with these tests
included is attached, make check passes.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-crosstabview-v11.difftext/x-patch; name=psql-crosstabview-v11.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 6d0cb3d..a242ec4 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -990,6 +990,123 @@ testdb=&gt;
       </varlistentry>
 
       <varlistentry>
+        <term><literal>\crosstabview [
+            [-|+]<replaceable class="parameter">colV</replaceable>
+            [:<replaceable class="parameter">scolV</replaceable>]
+            [-|+]<replaceable class="parameter">colH</replaceable>
+            [:<replaceable class="parameter">scolH</replaceable>]
+            [<replaceable class="parameter">colG1[,colG2...]</replaceable>]
+            ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows
+        the results inside a crosstab grid.
+        The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header, optionally sorted by <replaceable class="parameter">scolV</replaceable>,
+        and the output column <replaceable class="parameter">colH</replaceable>
+        becomes a horizontal header, optionally sorted by
+        <replaceable class="parameter">scolH</replaceable>.
+
+        <replaceable class="parameter">colG1[,colG2...]</replaceable>
+        is the list of output columns to project into the grid.
+        By default, all output columns of the query except 
+        <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>
+        are included in this list.
+        </para>
+
+        <para>
+        All columns can be refered to by their position (starting at 1), or by
+        their name. Normal case folding and quoting rules apply on column
+        names. By default,
+        <replaceable class="parameter">colV</replaceable> corresponds to column 1
+        and <replaceable class="parameter">colH</replaceable> to column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the set of all distinct values found in
+        column <replaceable class="parameter">colV</replaceable>, in the order
+        of their first appearance in the query results,
+        or in ascending order if a plus (+) sign precedes
+        <replaceable class="parameter">colV</replaceable>,
+        or in descending order if it's a minus (-) sign.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the set of all distinct non-null values found in
+        column <replaceable class="parameter">colH</replaceable>.  They come
+        by default in their order of appearance in the query results,
+        or in ascending order if a plus (+) sign precedes
+        <replaceable class="parameter">colH</replaceable>,
+        or in descending order if it's a minus (-) sign.
+
+        Also, they can optionally be sorted by another column, if
+        <replaceable class="parameter">colV</replaceable>
+        (respectively <replaceable class="parameter">colH</replaceable>) is
+        immediately followed by a colon and a reference to another column
+        <replaceable class="parameter">scolV</replaceable>
+        (respectively <replaceable class="parameter">scolH</replaceable>).
+        </para>
+
+        <para>
+        Inside the crosstab grid,
+        given a query output with <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        the contents of a cell located at the intersection
+        <literal>(x,y)</literal> is determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the query results such that the
+         value for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns or the columns listed in
+         <replaceable class="parameter">colG1[,colG2...]</replaceable>
+         are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed
+         in the cell as if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to
+         the case of one row except that the values coming from different rows
+         are stacked vertically, the different source rows being separated by
+         newline characters inside the cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term><literal>\d[S+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
 
         <listitem>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index 66e14fb..9d29fe1 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o crosstabview.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 9750a5b..e4db76e 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -39,6 +39,7 @@
 
 #include "common.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "describe.h"
 #include "help.h"
 #include "input.h"
@@ -364,6 +365,39 @@ exec_command(const char *cmd,
 	else if (strcmp(cmd, "copyright") == 0)
 		print_copyright();
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		char	*opt1,
+			*opt2,
+			*opt3;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt3 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1): NULL;
+			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2): NULL;
+			pset.crosstabview_cols_grid = opt3 ? pg_strdup(opt3): NULL;
+			pset.crosstabview_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+		free(opt3);
+	}
+
 	/* \d* commands */
 	else if (cmd[0] == 'd')
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 2cb2e9b..b368883 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -24,6 +24,7 @@
 #include "command.h"
 #include "copy.h"
 #include "mbprint.h"
+#include "crosstabview.h"
 
 
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
@@ -906,6 +907,8 @@ PrintQueryResults(PGresult *results)
 			/* store or print the data ... */
 			if (pset.gset_prefix)
 				success = StoreQueryTuple(results);
+			else if (pset.crosstabview_output)
+				success = PrintResultsInCrossTab(results);
 			else
 				success = PrintQueryTuples(results);
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
@@ -1192,6 +1195,23 @@ sendquery_cleanup:
 		pset.gset_prefix = NULL;
 	}
 
+	/* reset \crosstabview settings */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
+	if (pset.crosstabview_cols_grid)
+	{
+		free(pset.crosstabview_cols_grid);
+		pset.crosstabview_cols_grid = NULL;
+	}
 	return OK;
 }
 
@@ -1354,7 +1374,25 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 			is_pager = true;
 		}
 
-		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
+		if (pset.crosstabview_output)
+		{
+			if (ntuples < fetch_count)
+				PrintResultsInCrossTab(results);
+			else
+			{
+				/*
+				  crosstabview is denied if the whole set of rows is not
+				  guaranteed to be fetched in the first iteration, because
+				  it's expected in memory as a single PGresult structure.
+				*/
+				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
+					fetch_count);
+				PQclear(results);
+				break;
+			}
+		}
+		else
+			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
 
 		PQclear(results);
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..65b10ff
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,1038 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+
+#include "common.h"
+#include "crosstabview.h"
+#include "pqexpbuffer.h"
+#include "settings.h"
+#include <string.h>
+
+/*
+ * Value/position from the resultset that goes into the horizontal or vertical
+ * crosstabview header.
+ */
+struct pivot_field
+{
+	/*
+	 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
+	 * value becomes an entry in the vertical header (colV), or horizontal
+	 * header (colH)
+	 */
+	char	   *name;
+
+	/*
+	 * When a sort is requested on an alternative column, this holds
+	 * PQgetvalue() for the sort column and the first row
+	 * from which <name> is obtained
+	 */
+	char	   *sort_value;
+
+	/*
+	 * Rank of this value, starting at 0. Initially, it's the relative position
+	 * of the first appearance of this value in the resultset.
+	 * For example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
+	 * After all values have been gathered, ranks may be updated by serverSort()
+	 */
+	int			rank;
+};
+
+/* Node in avl_tree */
+struct avl_node
+{
+	/* Node contents */
+	struct pivot_field field;
+
+	/*
+	 * Height of this node in the tree (number of nodes on the longest
+	 * path to a leaf).
+	 */
+	int			height;
+
+	/*
+	 * Child nodes. [0] points to left subtree, [1] to right subtree.
+	 * Never NULL, points to the empty node avl_tree.end when no left
+	 * or right value.
+	 */
+	struct avl_node *childs[2];
+};
+
+/*
+ * Control structure for the AVL tree (binary search tree kept
+ * balanced with the AVL algorithm)
+ */
+struct avl_tree
+{
+	int			count;			/* Total number of nodes */
+	struct avl_node *root;		/* root of the tree */
+	struct avl_node *end;		/* Immutable dereferenceable empty tree */
+};
+
+/*
+ * The avl* functions below provide a minimalistic implementation of AVL binary
+ * trees, to efficiently collect the distinct values that will form the horizontal
+ * and vertical headers. It only supports adding new values, no removal or even
+ * search.
+ */
+static void
+avlInit(struct avl_tree *tree)
+{
+	tree->end = (struct avl_node*) pg_malloc0(sizeof(struct avl_node));
+	tree->end->childs[0] = tree->end->childs[1] = tree->end;
+	tree->count = 0;
+	tree->root = tree->end;
+}
+
+/* Deallocate recursively an AVL tree, starting from node */
+static void
+avlFree(struct avl_tree* tree, struct avl_node* node)
+{
+	if (node->childs[0] != tree->end)
+	{
+		avlFree(tree, node->childs[0]);
+		pg_free(node->childs[0]);
+	}
+	if (node->childs[1] != tree->end)
+	{
+		avlFree(tree, node->childs[1]);
+		pg_free(node->childs[1]);
+	}
+	if (node == tree->root) {
+		/* free the root separately as it's not child of anything */
+		if (node != tree->end)
+			pg_free(node);
+		/* free the tree->end struct only once and when all else is freed */
+		pg_free(tree->end);
+	}
+}
+
+/* Set the height to 1 plus the greatest of left and right heights */
+static void
+avlUpdateHeight(struct avl_node *n)
+{
+	n->height = 1 + (n->childs[0]->height > n->childs[1]->height ?
+					 n->childs[0]->height:
+					 n->childs[1]->height);
+}
+
+/* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
+static struct avl_node*
+avlRotate(struct avl_node **current, int dir)
+{
+	struct avl_node *before = *current;
+	struct avl_node *after = (*current)->childs[dir];
+
+	*current = after;
+	before->childs[dir] = after->childs[!dir];
+	avlUpdateHeight(before);
+	after->childs[!dir] = before;
+
+	return after;
+}
+
+static int
+avlBalance(struct avl_node *n)
+{
+	return n->childs[0]->height - n->childs[1]->height;
+}
+
+/*
+ * After an insertion, possibly rebalance the tree so that the left and right
+ * node heights don't differ by more than 1.
+ * May update *node.
+ */
+static void
+avlAdjustBalance(struct avl_tree *tree, struct avl_node **node)
+{
+	struct avl_node *current = *node;
+	int b = avlBalance(current)/2;
+	if (b != 0)
+	{
+		int dir = (1 - b)/2;
+		if (avlBalance(current->childs[dir]) == -b)
+		  avlRotate(&current->childs[dir], !dir);
+		current = avlRotate(node, dir);
+	}
+	if (current != tree->end)
+	  avlUpdateHeight(current);
+}
+
+/*
+ * Insert a new value/field, starting from *node, reaching the
+ * correct position in the tree by recursion.
+ * Possibly rebalance the tree and possibly update *node.
+ * Do nothing if the value is already present in the tree.
+ */
+static void
+avlInsertNode(struct avl_tree* tree,
+			  struct avl_node **node,
+			  struct pivot_field field)
+{
+	struct avl_node *current = *node;
+
+	if (current == tree->end)
+	{
+		struct avl_node * new_node = (struct avl_node*)
+			pg_malloc(sizeof(struct avl_node));
+		new_node->height = 1;
+		new_node->field = field;
+		new_node->childs[0] = new_node->childs[1] = tree->end;
+		tree->count++;
+		*node = new_node;
+	}
+	else
+	{
+		int cmp = strcmp(field.name, current->field.name);
+		if (cmp != 0)
+		{
+			avlInsertNode(tree,
+						  cmp > 0 ? &current->childs[1] : &current->childs[0],
+						  field);
+			avlAdjustBalance(tree, node);
+		}
+	}
+}
+
+/* Insert the value into the AVL tree, if it does not preexist */
+static void
+avlMergeValue(struct avl_tree* tree, char* name, char* sort_value)
+{
+	struct pivot_field field;
+	field.name = name;
+	field.rank = tree->count;
+	field.sort_value = sort_value;
+	avlInsertNode(tree, &tree->root, field);
+}
+
+/*
+ * Recursively extract node values into the names array, in sorted order with a
+ * left-to-right tree traversal.
+ * Return the next candidate offset to write into the names array.
+ * fields[] must be preallocated to hold tree->count entries
+ */
+static int
+avlCollectFields(struct avl_tree* tree,
+				 struct avl_node* node,
+				 struct pivot_field* fields,
+				 int idx)
+{
+	if (node == tree->end)
+		return idx;
+	idx = avlCollectFields(tree, node->childs[0], fields, idx);
+	fields[idx] = node->field;
+	return avlCollectFields(tree, node->childs[1], fields,  idx+1);
+}
+
+/*
+ * Send a query to sort the list of values in a horizontal or vertical
+ * crosstabview header, and update every source[].rank field with the
+ * new relative position of each value.
+ * coltype is the type's OID for the column from which the values come.
+ * direction is -1 for descending, 1 for ascending, 0 for no sort.
+ */
+static bool
+serverSort(Oid coltype, struct pivot_field *source, int nb_values, int direction)
+{
+	bool retval = false;
+	PGresult *res = NULL;
+	PQExpBufferData query;
+	int i;
+	Oid *param_types;
+	const char** param_values;
+	int* param_formats;
+
+	if (nb_values < 2 || direction==0)
+		return true;					/* nothing to sort */
+
+	param_types = (Oid*) pg_malloc(nb_values*sizeof(Oid));
+	param_values = (const char**) pg_malloc(nb_values*sizeof(char*));
+	param_formats = (int*) pg_malloc(nb_values*sizeof(int));
+
+	initPQExpBuffer(&query);
+
+	/*
+	 * The query returns the original position of each value in our list,
+	 * ordered by its new position. The value itself is not returned.
+	 */
+	appendPQExpBufferStr(&query, "SELECT n FROM (VALUES");
+
+	for (i=0; i < nb_values; i++)
+	{
+		appendPQExpBuffer(&query, "($%d,%d)", i+1, i+1);
+		if (i < nb_values-1)
+			appendPQExpBufferChar(&query, ',');
+		param_types[i] = coltype;
+		param_values[i] = source[i].sort_value;
+		param_formats[i] = 0;
+	}
+
+	appendPQExpBuffer(&query, ") AS l(x,n) ORDER BY x %s",
+					  direction < 0 ? "DESC" : "ASC");
+
+	res = PQexecParams(pset.db,
+					   query.data,
+					   nb_values,
+					   param_types,
+					   param_values,
+					   NULL,	/* lengths not necessary for text format */
+					   param_formats,
+					   0);
+
+	if (res && PQresultStatus(res) == PGRES_TUPLES_OK)
+	{
+		for (i=0; i < PQntuples(res); i++)
+		{
+			int old_pos = atoi(PQgetvalue(res, i, 0));
+
+			if (old_pos < 1 || old_pos > nb_values || i >= nb_values)
+			{
+				/*
+				 * A position outside of the range is normally impossible.
+				 * If this happens, we're facing a malfunctioning or hostile
+				 * server or middleware.
+				 */
+				psql_error(_("Unexpected rank when sorting crosstabview headers\n"));
+				goto cleanup;
+			}
+			else
+			{
+				source[old_pos-1].rank = i;
+			}
+		}
+	}
+	else
+	{
+		psql_error(_("Query error when sorting crosstabview header: %s"),
+				   PQerrorMessage(pset.db));
+		goto cleanup;
+	}
+
+	retval = true;
+
+cleanup:
+	termPQExpBuffer(&query);
+	if (res)
+		PQclear(res);
+	pg_free(param_types);
+	pg_free(param_values);
+	pg_free(param_formats);
+	return retval;
+}
+
+/* for bsearch() inside a sorted array of struct pivot_field */
+static int
+headerCompare(const void *a, const void *b)
+{
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+
+/*
+ * Output the pivoted resultset with the printTable* functions
+ */
+static void
+printCrosstab(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows,
+			  int *colsG,
+			  int colsG_num)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, rn;
+	char col_align = 'l';		/* alignment for values inside the grid */
+	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+						column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+	 *  associating each piv_columns[].rank to its index in piv_columns.
+	 *  This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+
+	/*
+	 * In the common case of only one field projected into the cells, the
+	 * display alignment depends on its PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (colsG_num == 1)
+		col_align = column_type_alignment(PQftype(results, colsG[0]));
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		printTableAddHeader(&cont,
+							piv_columns[horiz_map[i]].name,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name;
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		char* row_name;
+		char* col_name;
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		row_number = col_number = -1;
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+		{
+			row_name = PQgetvalue(results, rn, field_for_rows);
+			p = (struct pivot_field*) bsearch(&row_name,
+											  piv_rows,
+											  num_rows,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				row_number = p->rank;
+		}
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+		{
+			col_name = PQgetvalue(results, rn, field_for_columns);
+			p = (struct pivot_field*) bsearch(&col_name,
+											  piv_columns,
+											  num_columns,
+											  sizeof(struct pivot_field),
+											  headerCompare);
+			if (p)
+				col_number = p->rank;
+		}
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+
+			/*
+			 * special case: when the source has only 2 columns, use a
+			 * X (cross/checkmark) for the cell content, and set
+			 * src_col to a virtual additional column.
+			 */
+			if (PQnfields(results) == 2)
+				src_col = -1;
+
+			for (i=0; i<colsG_num || src_col==-1; i++)
+			{
+				char *content;
+
+				if (src_col == -1)
+				{
+					content = "X";
+				}
+				else
+				{
+					src_col = colsG[i];
+
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (i==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+
+				/* special case of the "virtual column" for checkmark */
+				if (src_col == -1)
+					break;
+			}
+		}
+	}
+
+	printTable(&cont, pset.queryFout, false, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static bool
+fieldNameEquals(const char *arg, const char *fieldname)
+{
+	const char* p = arg;
+	const char* f = fieldname;
+	char c;
+
+	if (*p++ != '"')
+		return !pg_strcasecmp(arg, fieldname);
+
+	while ((c = *p++))
+	{
+		if (c == '"')
+		{
+			if (*p == '"')
+				p++;			/* skip second quote and continue */
+			else if (*p == '\0')
+				return (*f == '\0');	/* p is shorter than f, or is identical */
+		}
+		if (*f == '\0')
+			return false;			/* f is shorter than p */
+		if (c != *f)				/* found one byte that differs */
+			return false;
+		f++;
+	}
+	return (*f=='\0');
+}
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *  on success, the 0-based index of the column
+ *  or -1 if the column number or name is not found in the result's structure,
+ *        or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char *arg, PGresult *res)
+{
+	int idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0  || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int i;
+		idx = -1;
+		for (i=0; i < PQnfields(res); i++)
+		{
+			if (fieldNameEquals(arg, PQfname(res, i)))
+			{
+				if (idx>=0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+/*
+ * Parse col1[<sep>col2][<sep>col3]...
+ * where colN can be:
+ * - a number from 1 to PQnfields(res)
+ * - an unquoted column name matching (case insensitively) one of PQfname(res,...)
+ * - a quoted column name matching (case sensitively) one of PQfname(res,...)
+ * max_columns: 0 if no maximum
+ */
+static int
+parseColumnRefs(char* arg,
+				PGresult *res,
+				int **col_numbers,
+				int max_columns,
+				char separator)
+{
+	char *p = arg;
+	char c;
+	int col_num = -1;
+	int nb_cols = 0;
+	char* field_start = NULL;
+	*col_numbers = NULL;
+	while ((c = *p) != '\0')
+	{
+		bool quoted_field = false;
+		field_start = p;
+
+		/* first char */
+		if (c == '"')
+		{
+			quoted_field = true;
+			p++;
+		}
+
+		while ((c = *p) != '\0')
+		{
+			if (c == separator && !quoted_field)
+				break;
+			if (c == '"')		/* end of field or embedded double quote */
+			{
+				p++;
+				if (*p == '"')
+				{
+					if (quoted_field)
+					{
+						p++;
+						continue;
+					}
+				}
+				else if (quoted_field && *p == separator)
+					break;
+			}
+			p += PQmblen(p, pset.encoding);
+		}
+
+		if (p != field_start)
+		{
+			/* look up the column and add its index into *col_numbers */
+			if (max_columns != 0 && nb_cols == max_columns)
+			{
+				psql_error(_("No more than %d column references expected\n"), max_columns);
+				goto errfail;
+			}
+			c = *p;
+			*p = '\0';
+			col_num = indexOfColumn(field_start, res);
+			*p = c;
+			if (col_num < 0)
+				goto errfail;
+			*col_numbers = (int*)pg_realloc(*col_numbers, (1+nb_cols)*sizeof(int));
+			(*col_numbers)[nb_cols++] = col_num;
+		}
+		else
+		{
+			psql_error(_("Empty column reference\n"));
+			goto errfail;
+		}
+
+		if (*p)
+			p += PQmblen(p, pset.encoding);
+	}
+	return nb_cols;
+
+errfail:
+	pg_free(*col_numbers);
+	*col_numbers = NULL;
+	return -1;
+}
+
+
+/*
+ * Main function.
+ * Process the data from *res according the display options in pset (global),
+ * to generate the horizontal and vertical headers contents,
+ * then call printCrosstab() for the actual output.
+ */
+bool
+PrintResultsInCrossTab(PGresult* res)
+{
+	/* [-|+]COLV or null */
+	char* opt_field_for_rows = pset.crosstabview_col_V;
+	/* [-|+]COLH or null */
+	char* opt_field_for_columns = pset.crosstabview_col_H;
+	int		rn;
+	struct avl_tree	piv_columns;
+	struct avl_tree	piv_rows;
+	struct pivot_field* array_columns = NULL;
+	struct pivot_field* array_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	retval = false;
+	int		columns_sort_direction; /* 1:ascending, 0:none, -1:descending */
+	int		rows_sort_direction;    /* 1:ascending, 0:none, -1:descending */
+	int		*colsV = NULL, *colsH = NULL, *colsG = NULL;
+	int		colsG_num;
+	int		nn;
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns = -1;
+	int		sort_field_for_columns = -1;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows = -1;
+	int		sort_field_for_rows = -1;
+
+	avlInit(&piv_rows);
+	avlInit(&piv_columns);
+
+	if (res == NULL)
+	{
+		psql_error(_("No result\n"));
+		goto error_return;
+	}
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		psql_error(_("The query must return results to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the vertical header (1st arg)
+	 * displayed in the left-most column. Determine:
+	 * - the sort direction if any
+	 * - the field number of that column in the PGresult
+	 * - the field number of the associated sort column if any
+	 */
+
+	if (opt_field_for_rows == NULL)
+	{
+		field_for_rows = sort_field_for_rows = 0;
+		rows_sort_direction = 0;
+	}
+	else
+	{
+		if (*opt_field_for_rows == '-')
+		{
+			rows_sort_direction = -1;
+			opt_field_for_rows++;
+		}
+		else if (*opt_field_for_rows == '+')
+		{
+			rows_sort_direction = 1;
+			opt_field_for_rows++;
+		}
+		else
+			rows_sort_direction = 0;
+
+		nn = parseColumnRefs(opt_field_for_rows, res, &colsV, 2, ':');
+		if (nn < 0)
+			goto error_return;
+		if (nn==1)
+		{
+			field_for_rows = colsV[0];
+			sort_field_for_rows = field_for_rows;
+		}
+		else
+		{
+			field_for_rows = colsV[0];
+			sort_field_for_rows = colsV[1];
+			if (rows_sort_direction == 0)
+			{
+				psql_error(_("Sort column specified without a sort direction\n"));
+				goto error_return;
+			}
+		}
+	}
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	/*
+	 * Arguments processing for the horizontal header (2nd arg)
+	 * (pivoted column that gets displayed as the first row).
+	 * Determine:
+	 * - the sort direction if any
+	 * - the field number of that column in the PGresult
+	 * - the field number of the associated sort column if any
+	 */
+
+	if (opt_field_for_columns == NULL)
+	{
+		field_for_columns = sort_field_for_columns = 1;
+		columns_sort_direction = 0;
+	}
+	else
+	{
+		/*
+		 * descending sort is requested if the column reference is
+		 * preceded with a minus sign
+		 */
+		if (*opt_field_for_columns == '-')
+		{
+			columns_sort_direction = -1;
+			opt_field_for_columns++;
+		}
+		else if (*opt_field_for_columns == '+')
+		{
+			columns_sort_direction = 1;
+			opt_field_for_columns++;
+		}
+		else
+			columns_sort_direction = 0;
+
+		nn = parseColumnRefs(opt_field_for_columns, res, &colsH, 2, ':');
+		if (nn <= 0)
+			goto error_return;
+		if (nn==1)
+		{
+			field_for_columns = colsH[0];
+			sort_field_for_columns = field_for_columns;
+		}
+		else
+		{
+			field_for_columns = colsH[0];
+			sort_field_for_columns = colsH[1];
+			if (columns_sort_direction == 0)
+			{
+				psql_error(_("Sort column specified without a sort direction\n"));
+				goto error_return;
+			}
+		}
+
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the columns aside from headers (3rd arg)
+	 * Determine the columns to display in the grid and their order.
+	 */
+	if (pset.crosstabview_cols_grid == NULL)
+	{
+		/*
+		 * By defaut, all the fields from PGresult get displayed into the grid,
+		 * except the two fields that go into the vertical and horizontal
+		 * headers.
+		 */
+		if (PQnfields(res) > 2)
+		{
+			int i, j=0;
+			colsG = (int*)pg_malloc(sizeof(int) * (PQnfields(res)-2));
+			for (i=0; i<PQnfields(res); i++)
+			{
+				if (i!=field_for_rows && i!=field_for_columns)
+					colsG[j++] = i;
+			}
+			colsG_num = PQnfields(res)-2;
+		}
+		else
+		{
+			colsG = NULL;
+			colsG_num = 0;
+		}
+	}
+	else
+	{
+		/*
+		 * Non-default case: a list of fields is given.
+		 * Parse that list to determine the fields to display into the grid,
+		 * and in what order.
+		 * The list format is colA[,colB[,colC...]]
+		 */
+		colsG_num = parseColumnRefs(pset.crosstabview_cols_grid,
+									res, &colsG, PQnfields(res), ',');
+		if (colsG_num <= 0)
+			goto error_return;
+	}
+
+	/*
+	 * First part: accumulate the names that go into the vertical and
+	 * horizontal headers, each into an AVL binary tree to build the set of
+	 * DISTINCT values.
+	 */
+
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		if (!PQgetisnull(res, rn, field_for_rows))
+		{
+			avlMergeValue(&piv_rows,
+						  PQgetvalue(res, rn, field_for_rows),
+						  PQgetvalue(res, rn, sort_field_for_rows));
+
+			if (rows_sort_direction!=0 && piv_rows.count > 65535)
+			{
+				psql_error(_("Maximum number of rows to sort (65535) exceeded for the vertical header\n"));
+				goto error_return;
+			}
+		}
+
+		if (!PQgetisnull(res, rn, field_for_columns))
+		{
+			avlMergeValue(&piv_columns,
+						  PQgetvalue(res, rn, field_for_columns),
+						  PQgetvalue(res, rn, sort_field_for_columns));
+
+			if (piv_columns.count > 1600)
+			{
+				psql_error(_("Maximum number of columns (1600) exceeded\n"));
+				goto error_return;
+			}
+		}
+	}
+
+
+	/*
+	 * Second part: Generate sorted arrays from the AVL trees.
+	 */
+
+	num_columns = piv_columns.count;
+	num_rows = piv_rows.count;
+
+	array_columns = (struct pivot_field*) pg_malloc(sizeof(struct pivot_field) * num_columns);
+	array_rows = (struct pivot_field*) pg_malloc(sizeof(struct pivot_field) * num_rows);
+
+	avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0);
+	avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0);
+
+	/*
+	 * Third part: sort (by server-side query) the horizontal and/or vertical
+	 * header when applicable.
+	 */
+	if (columns_sort_direction != 0)
+	{
+		if (!serverSort(PQftype(res, sort_field_for_columns),
+						array_columns,
+						num_columns,
+						columns_sort_direction))
+			goto error_return;
+	}
+
+	if (rows_sort_direction != 0)
+	{
+		if (!serverSort(PQftype(res, sort_field_for_rows),
+						array_rows,
+						num_rows,
+						rows_sort_direction))
+			goto error_return;
+	}
+
+	/*
+	 * Fourth part: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns,
+				  array_columns,
+				  field_for_columns,
+				  num_rows,
+				  array_rows,
+				  field_for_rows,
+				  colsG,
+				  colsG_num);
+
+	retval = true;
+
+error_return:
+	avlFree(&piv_columns, piv_columns.root);
+	avlFree(&piv_rows, piv_rows.root);
+	pg_free(array_columns);
+	pg_free(array_rows);
+	pg_free(colsV);
+	pg_free(colsH);
+	pg_free(colsG);
+
+	return retval;
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..d374cfe
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,14 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+/* prototypes */
+extern bool	PrintResultsInCrossTab(PGresult *res);
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 59f6f25..26afa68 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -175,6 +175,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [V H]    execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
index 8958903..f3d9a83 100644
--- a/src/bin/psql/print.c
+++ b/src/bin/psql/print.c
@@ -3291,30 +3291,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3356,6 +3335,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
index 868fcdd..4f3d85a 100644
--- a/src/bin/psql/print.h
+++ b/src/bin/psql/print.h
@@ -175,7 +175,7 @@ extern FILE *PageOutput(int lines, const printTableOpt *topt);
 extern void ClosePager(FILE *pagerpipe);
 
 extern void html_escaped_print(const char *in, FILE *fout);
-
+extern char column_type_alignment(Oid);
 extern void printTableInit(printTableContent *const content,
 			   const printTableOpt *opt, const char *title,
 			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 20a6470..9b7f7c4 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -90,6 +90,10 @@ typedef struct _psqlSettings
 
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
+	char		*crosstabview_cols_grid;  /* one-shot \crosstabview 3nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5f27120..c1e93f2 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1273,7 +1273,8 @@ psql_completion(const char *text, int start, int end)
 
 	/* psql's backslash commands. */
 	static const char *const backslash_commands[] = {
-		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
+		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
+		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 178a809..7861360 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -2665,3 +2665,110 @@ NOTICE:  foo
 CONTEXT:  PL/pgSQL function inline_code_block line 3 at RAISE
 ERROR:  bar
 CONTEXT:  PL/pgSQL function inline_code_block line 4 at RAISE
+-- \crosstabview
+\pset expanded off
+\pset columns 0
+\pset border 1
+\pset format aligned
+BEGIN;
+CREATE TEMPORARY TABLE fruits (
+    name text,
+    num integer,
+    imported date
+) ON COMMIT DROP;
+\copy fruits from stdin;
+-- vertical header post-sorted, horizontal header sorted by query
+select name, to_char(imported, 'mon') mon, sum(num) as sum
+from fruits group by 1,2 order by 2
+ \crosstabview -1 2
+     name     | apr | aug | feb | jan | jul | jun | mar  | nov | oct | sep 
+--------------+-----+-----+-----+-----+-----+-----+------+-----+-----+-----
+ Strawberries |  32 |     |     |     |     |     |   50 |     |     |    
+ Squash       |     |     |     |     |  80 |     |      |     |     |    
+ Saffron      |     |     |     |     | 536 |     |      |     |     |    
+ Naranjillo   |     |     |     |     | 126 |     |      |     |     | 262
+ Ice Plant    | 135 | 279 |     |     |     |     |      |     |     |    
+ Grape        | 393 |     | 260 | 300 | 441 | 542 | 1353 |     | 225 |    
+ Ginger       |     |     |     |     | 160 |     |      |     |     |    
+ Coconut Palm |     |     |     |     |     |     |      |     |     |  26
+ Citron       | 774 |     |     |     |     |     |      |     |     |    
+ Celery       |  12 |     |     |     |     |     |      |     |     |    
+ Blueberry    |     |     |     |     |     |     |      |  32 |     |    
+ Avocado      |     |     |     |     |     |     |      |     | 130 |    
+ Apple        |     |     |     |     | 140 |     |      |     |     |    
+ Angelica     |     |     |     |     |     |     |   80 |     |     |    
+(14 rows)
+
+-- include NULL value (to be discarded from header)
+select name, to_char(imported, 'mon') mon, sum(num) as sum
+from fruits group by 1,2
+union select null, 'jan', 1
+order by 2 \crosstabview -1 2
+     name     | apr | aug | feb | jan | jul | jun | mar  | nov | oct | sep 
+--------------+-----+-----+-----+-----+-----+-----+------+-----+-----+-----
+ Strawberries |  32 |     |     |     |     |     |   50 |     |     |    
+ Squash       |     |     |     |     |  80 |     |      |     |     |    
+ Saffron      |     |     |     |     | 536 |     |      |     |     |    
+ Naranjillo   |     |     |     |     | 126 |     |      |     |     | 262
+ Ice Plant    | 135 | 279 |     |     |     |     |      |     |     |    
+ Grape        | 393 |     | 260 | 300 | 441 | 542 | 1353 |     | 225 |    
+ Ginger       |     |     |     |     | 160 |     |      |     |     |    
+ Coconut Palm |     |     |     |     |     |     |      |     |     |  26
+ Citron       | 774 |     |     |     |     |     |      |     |     |    
+ Celery       |  12 |     |     |     |     |     |      |     |     |    
+ Blueberry    |     |     |     |     |     |     |      |  32 |     |    
+ Avocado      |     |     |     |     |     |     |      |     | 130 |    
+ Apple        |     |     |     |     | 140 |     |      |     |     |    
+ Angelica     |     |     |     |     |     |     |   80 |     |     |    
+(14 rows)
+
+-- 4 fields
+-- include a NULL value in the horizontal header (to be discarded)
+-- fieldname with double quotes
+-- descending sort on date
+select name as "name of ""fruit""", to_char(imported, 'mon') mon, imported, sum(num) as sum
+from fruits group by 1,2,3
+union select 'Citron', NULL, NULL as imported, 1
+\crosstabview -"name of ""fruit""" -2:3 sum
+ name of "fruit" | nov | oct | sep | aug | jul | jun | apr | mar  | feb | jan 
+-----------------+-----+-----+-----+-----+-----+-----+-----+------+-----+-----
+ Strawberries    |     |     |     |     |     |     |  32 |   50 |     |    
+ Squash          |     |     |     |     |  80 |     |     |      |     |    
+ Saffron         |     |     |     |     | 536 |     |     |      |     |    
+ Naranjillo      |     |     | 262 |     | 126 |     |     |      |     |    
+ Ice Plant       |     |     |     | 279 |     |     | 135 |      |     |    
+ Grape           |     | 225 |     |     | 441 | 542 | 393 | 1353 | 260 | 300
+ Ginger          |     |     |     |     | 160 |     |     |      |     |    
+ Coconut Palm    |     |     |  26 |     |     |     |     |      |     |    
+ Citron          |     |     |     |     |     |     | 774 |      |     |    
+ Celery          |     |     |     |     |     |     |  12 |      |     |    
+ Blueberry       |  32 |     |     |     |     |     |     |      |     |    
+ Avocado         |     | 130 |     |     |     |     |     |      |     |    
+ Apple           |     |     |     |     | 140 |     |     |      |     |    
+ Angelica        |     |     |     |     |     |     |     |   80 |     |    
+(14 rows)
+
+-- 4 fields, double sort, field referenced by names
+select sum, name, to_char(imported, 'mon') mon, imported
+  from (select sum(num), name, date_trunc('month',imported)::date as imported
+   from fruits group by 2,3) x
+\crosstabview +name +mon:imported sum
+     name     | jan | feb | mar  | apr | jun | jul | aug | sep | oct | nov 
+--------------+-----+-----+------+-----+-----+-----+-----+-----+-----+-----
+ Angelica     |     |     |   80 |     |     |     |     |     |     |    
+ Apple        |     |     |      |     |     | 140 |     |     |     |    
+ Avocado      |     |     |      |     |     |     |     |     | 130 |    
+ Blueberry    |     |     |      |     |     |     |     |     |     |  32
+ Celery       |     |     |      |  12 |     |     |     |     |     |    
+ Citron       |     |     |      | 774 |     |     |     |     |     |    
+ Coconut Palm |     |     |      |     |     |     |     |  26 |     |    
+ Ginger       |     |     |      |     |     | 160 |     |     |     |    
+ Grape        | 300 | 260 | 1353 | 393 | 542 | 441 |     |     | 225 |    
+ Ice Plant    |     |     |      | 135 |     |     | 279 |     |     |    
+ Naranjillo   |     |     |      |     |     | 126 |     | 262 |     |    
+ Saffron      |     |     |      |     |     | 536 |     |     |     |    
+ Squash       |     |     |      |     |     |  80 |     |     |     |    
+ Strawberries |     |     |   50 |  32 |     |     |     |     |     |    
+(14 rows)
+
+COMMIT;
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 2f81380..77e22f6 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -351,3 +351,69 @@ begin
   raise notice 'foo';
   raise exception 'bar';
 end $$;
+
+-- \crosstabview
+
+\pset expanded off
+\pset columns 0
+\pset border 1
+\pset format aligned
+BEGIN;
+CREATE TEMPORARY TABLE fruits (
+    name text,
+    num integer,
+    imported date
+) ON COMMIT DROP;
+\copy fruits from stdin;
+Grape	300	2015-01-08
+Grape	260	2015-02-09
+Grape	1353	2015-03-11
+Grape	393	2015-04-15
+Grape	542	2015-06-09
+Grape	441	2015-07-20
+Grape	225	2015-10-09
+Strawberries	50	2015-03-11
+Strawberries	32	2015-04-22
+Saffron	536	2015-07-12
+Squash	80	2015-07-14
+Citron	774	2015-04-02
+Celery	12	2015-04-07
+Coconut Palm	26	2015-09-16
+Blueberry	32	2015-11-16
+Angelica	80	2015-03-20
+Avocado	130	2015-10-19
+Apple	140	2015-07-02
+Ginger	160	2015-07-15
+Ice Plant	135	2015-04-03
+Ice Plant	279	2015-08-04
+Naranjillo	126	2015-07-31
+Naranjillo	262	2015-09-11
+\.
+
+-- vertical header post-sorted, horizontal header sorted by query
+select name, to_char(imported, 'mon') mon, sum(num) as sum
+from fruits group by 1,2 order by 2
+ \crosstabview -1 2
+
+-- include NULL value (to be discarded from header)
+select name, to_char(imported, 'mon') mon, sum(num) as sum
+from fruits group by 1,2
+union select null, 'jan', 1
+order by 2 \crosstabview -1 2
+
+-- 4 fields
+-- include a NULL value in the horizontal header (to be discarded)
+-- fieldname with double quotes
+-- descending sort on date
+select name as "name of ""fruit""", to_char(imported, 'mon') mon, imported, sum(num) as sum
+from fruits group by 1,2,3
+union select 'Citron', NULL, NULL as imported, 1
+\crosstabview -"name of ""fruit""" -2:3 sum
+
+-- 4 fields, double sort, field referenced by names
+select sum, name, to_char(imported, 'mon') mon, imported
+  from (select sum(num), name, date_trunc('month',imported)::date as imported
+   from fruits group by 2,3) x
+\crosstabview +name +mon:imported sum
+
+COMMIT;
#66Pavel Stehule
pavel.stehule@gmail.com
In reply to: Daniel Verite (#63)
Re: [patch] Proposal for \crosstabview in psql

Hi

I tested last version, v11 and I have not any objection

It is working as expected

all regress tests passed, there is related documentation and new test is
attached.

This patch is ready form commiter.

Daniel, thank you very much, it is interesting feature.

Regards

Pavel

#67Teodor Sigaev
teodor@sigaev.ru
In reply to: Daniel Verite (#65)
Re: [patch] Proposal for \crosstabview in psql

Hi!

Interesting feature, but it's not very obvious how to use it. I'd like to see
some example(s) in documentation.

And I see an implementation of AVL tree in psql source code
(src/bin/psql/crosstabview.c). Postgres already has a Red-Black tree
implementation in
src/include/lib/rbtree.h and src/backend/lib/rbtree.c. Is any advantage of using
AVL tree here? I have some doubt about that and this implementation, obviously,
will not be well tested. But I see in comments that implementation is reduced to
insert only and it doesn't use the fact of ordering tree, so, even hash could be
used.

Daniel Verite wrote:

Pavel Stehule wrote:

1. maybe we can decrease name to shorter "crossview" ?? I am happy with
crosstabview too, just crossview is correct too, and shorter

I'm in favor of keeping crosstabview. It's more explicit, only
3 characters longer and we have tab completion anyway.

2. Columns used for ordering should not be displayed by default. I can live
with current behave, but hiding ordering columns is much more practical for
me

I can see why, but I'm concerned about a consequence:
say we have 4 columns A,B,C,D and user does \crosstabview +A:B +C:D
If B and D are excluded by default, then there's nothing
left to display inside the grid.
It doesn't feel quite right. There's something counter-intuitive
in the fact that values in the grid would disappear depending on
whether and how headers are sorted.
With the 3rd argument, we let the user decide what they want
to see.

3. This code is longer, so some regress tests are recommended - attached
simple test case

I've added a few regression tests to the psql testsuite
based on your sample data. New patch with these tests
included is attached, make check passes.

Best regards,

--
Teodor Sigaev E-mail: teodor@sigaev.ru
WWW: http://www.sigaev.ru/

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#68Daniel Verite
daniel@manitou-mail.org
In reply to: Teodor Sigaev (#67)
Re: [patch] Proposal for \crosstabview in psql

Teodor Sigaev wrote:

Interesting feature, but it's not very obvious how to use it. I'd like to
see some example(s) in documentation.

I'm thinking of making a wiki page, because examples pretty much
require showing resultsets, and I'm not sure this would fly
with our current psql documentation, which is quite compact.

The current bit of doc I've produced is 53 lines long in manpage
format already. The text has not been proofread by a native
English speaker yet, so part of the problem might be that
it's just me struggling with english :)

And I see an implementation of AVL tree in psql source code
(src/bin/psql/crosstabview.c). Postgres already has a Red-Black tree
implementation in src/include/lib/rbtree.h and
src/backend/lib/rbtree.c. Is any advantage of using AVL tree here? I
have some doubt about that and this implementation, obviously, will
not be well tested. But I see in comments that implementation is
reduced to insert only and it doesn't use the fact of ordering tree,
so, even hash could be used.

Yes. I expect too that a RB tree or a hash-based algorithm would do
the job and perform well.

The AVL implementation in crosstabview is purposely simplified
and specialized for this job, resulting in ~185 lines of code
versus ~850 lines for rb-tree.c
But I understand the argument that the existing rb-tree has been
battle-tested, whereas this code hasn't.

I'm looking at rb-tree.c and thinking what it would take to
incorporate it:
1. duplicating or linking from backend/lib/rb-tree.c into psql/
2. replacing the elog() calls with something else in the case of psql
3. updating the crosstabview data structures and call sites.

While I'm OK with #3, #1 and #2 seem wrong.
I could adapt rb-tree.c so that the same code can be used
both client-side and server-side, but touching server-side
code for this feature and creating links in the source tree
feels invasive and overkill.

Another approach is to replace AVL with an hash-based algorithm,
but that raises the same sort of question. If crosstabview comes
with its specific implementation, why use that rather than existing
server-side code? But at a glance, utils/hash/dynahash.c seems quite
hard to convert for client-side use.

I'm open to ideas on this. In particular, if we have a hash table
implementation that is already blessed by the project and small enough
to make sense in psql, I'd be happy to consider it.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#69Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Daniel Verite (#68)
Re: [patch] Proposal for \crosstabview in psql

Daniel Verite wrote:

Teodor Sigaev wrote:

Interesting feature, but it's not very obvious how to use it. I'd like to
see some example(s) in documentation.

I'm thinking of making a wiki page, because examples pretty much
require showing resultsets, and I'm not sure this would fly
with our current psql documentation, which is quite compact.

Yeah, we need to keep in mind that the psql doc is processed as a
manpage also, so it may not be a great idea to add too many things
there. But I also agree that some good examples would be useful.

FWIW I think the general idea of this feature (client-side resultset
"pivoting") is a good one, but I don't really have an opinion regarding
your specific proposal. I think you should first seek some more
consensus about the proposed design; in your original thread [1]It's a good idea to add links to previous threads where things were discussed. I had to search for www.postgresql.org/message-id/flat/78543039-c708-4f5d-a66f-0c0fbcda1f76@mm because you didn't provide a link to it when you started the second thread. several
guys defended the idea of having this be a psql feature, and the idea of
this being a parallel to \x seems a very sensible one, but there's
really been no discussion on whether your proposed "+/-" syntax to
change sort order makes sense, for one thing.

So please can we have that wiki page so that the syntax can be hammered
out a bit more.

I'm closing this as returned-with-feedback for now.

[1]: It's a good idea to add links to previous threads where things were discussed. I had to search for www.postgresql.org/message-id/flat/78543039-c708-4f5d-a66f-0c0fbcda1f76@mm because you didn't provide a link to it when you started the second thread.
discussed. I had to search for
www.postgresql.org/message-id/flat/78543039-c708-4f5d-a66f-0c0fbcda1f76@mm
because you didn't provide a link to it when you started the second
thread.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#70Pavel Stehule
pavel.stehule@gmail.com
In reply to: Alvaro Herrera (#69)
Re: [patch] Proposal for \crosstabview in psql

Hi

FWIW I think the general idea of this feature (client-side resultset
"pivoting") is a good one, but I don't really have an opinion regarding
your specific proposal. I think you should first seek some more
consensus about the proposed design; in your original thread [1] several
guys defended the idea of having this be a psql feature, and the idea of
this being a parallel to \x seems a very sensible one, but there's
really been no discussion on whether your proposed "+/-" syntax to
change sort order makes sense, for one thing.

I am sorry, but I disagree - the discussion about implementation was more
than two months, and I believe so anybody who would to discuss had enough
time to discuss. This feature and design was changed significantly and
there was not anybody who sent feature design objection.

This feature has only small relation to SQL PIVOTING feature - it is just
form of view and I am agree with Daniel about sense of this feature.

Regards

Pavel

Show quoted text

So please can we have that wiki page so that the syntax can be hammered
out a bit more.

I'm closing this as returned-with-feedback for now.

[1] It's a good idea to add links to previous threads where things were
discussed. I had to search for
www.postgresql.org/message-id/flat/78543039-c708-4f5d-a66f-0c0fbcda1f76@mm
because you didn't provide a link to it when you started the second
thread.

--
Álvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#71Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Pavel Stehule (#70)
Re: [patch] Proposal for \crosstabview in psql

Pavel Stehule wrote:

FWIW I think the general idea of this feature (client-side resultset
"pivoting") is a good one, but I don't really have an opinion regarding
your specific proposal. I think you should first seek some more
consensus about the proposed design; in your original thread [1] several
guys defended the idea of having this be a psql feature, and the idea of
this being a parallel to \x seems a very sensible one,

Sorry, I meant \q here, not \x.

but there's really been no discussion on whether your proposed "+/-"
syntax to change sort order makes sense, for one thing.

I am sorry, but I disagree - the discussion about implementation was more
than two months, and I believe so anybody who would to discuss had enough
time to discuss. This feature and design was changed significantly and
there was not anybody who sent feature design objection.

I just rechecked the thread. In my reading, lots of people argued
whether it should be called \rotate or \pivot or \crosstab; it seems the
\crosstabview proposal was determined to be best. I can support that
decision. But once the details were discussed, it was only you and
Daniel left in the thread; nobody else participated. While I understand
that you may think that "silence is consent", what I am afraid of is
that some committer will look at this two months from now and say "I
hate this Hcol+ stuff, -1 from me" and send the patch back for syntax
rework. IMO it's better to have more people chime in here so that the
patch that we discuss during the next commitfest is really the best one
we can think of.

Also, what about the business of putting "x" if there's no third column?
Three months from now some Czech psql hacker will say "we should use
Unicode chars for this" and we will be forever stuck with \pset
unicode_crosstab_marker to change the character to a ☑ BALLOT BOX WITH
CZECH. Maybe we should think that a bit harder -- for example, what
about just rejecting the case with no third column and forcing the user
to add a third column with the character they choose? That way you
avoid that mess.

This feature has only small relation to SQL PIVOTING feature - it is just
form of view and I am agree with Daniel about sense of this feature.

Yes, I don't disagree there. Robert Haas and David Fetter also
expressed their support for psql-side processing, so I think we're good
there.

--
Álvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#72Pavel Stehule
pavel.stehule@gmail.com
In reply to: Alvaro Herrera (#71)
Re: [patch] Proposal for \crosstabview in psql

Hi

I just rechecked the thread. In my reading, lots of people argued
whether it should be called \rotate or \pivot or \crosstab; it seems the
\crosstabview proposal was determined to be best. I can support that
decision. But once the details were discussed, it was only you and
Daniel left in the thread; nobody else participated. While I understand
that you may think that "silence is consent", what I am afraid of is
that some committer will look at this two months from now and say "I
hate this Hcol+ stuff, -1 from me" and send the patch back for syntax
rework. IMO it's better to have more people chime in here so that the
patch that we discuss during the next commitfest is really the best one
we can think of.

I have not a feeling so we did some with Daniel privately. All work was
public (I checked my mailbox) - but what is unhappy - in more mailing list
threads (not sure how it is possible, because subjects looks same). The
discus about the design was public, I am sure. It was relative longer
process, with good progress (from my perspective), because Daniel accepts
and fixed all my objection. The proposed syntax is fully consistent with
other psql commands - hard to create something new there, because psql
parser is pretty limited. Although I am thinking so syntax is good, clean
and useful I am open to discuss about it. Please, try the last design, last
patch - I spent lot of hours (and I am sure so Daniel much more) in
thinking how this can be designed better.

Also, what about the business of putting "x" if there's no third column?
Three months from now some Czech psql hacker will say "we should use
Unicode chars for this" and we will be forever stuck with \pset
unicode_crosstab_marker to change the character to a ☑ BALLOT BOX WITH
CZECH. Maybe we should think that a bit harder -- for example, what
about just rejecting the case with no third column and forcing the user
to add a third column with the character they choose? That way you
avoid that mess.

These features are in category "nice to have". There are no problem to do
in last commitfest or in next release cycle. It is not reason why to block
commit of this feature, and I am sure so lot of users can be pretty happy
with "basic" version of this patch. The all necessary functionality is
there and working. We can continue on development in other cycles, but for
this cycle, I am sure, so all necessary work is done.

This feature has only small relation to SQL PIVOTING feature - it is just
form of view and I am agree with Daniel about sense of this feature.

Yes, I don't disagree there. Robert Haas and David Fetter also
expressed their support for psql-side processing, so I think we're good
there.

great. Personally, I have not any objection against current state. If
anybody has, please do it early. We can move to forward. This is nice
feature, good patch, and there is not reason why stop here.

Regards

Pavel

Show quoted text

--
Álvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#73Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Pavel Stehule (#72)
Re: [patch] Proposal for \crosstabview in psql

On 9 February 2016 at 05:24, Pavel Stehule <pavel.stehule@gmail.com> wrote:

I have not a feeling so we did some with Daniel privately. All work was
public (I checked my mailbox) - but what is unhappy - in more mailing list
threads (not sure how it is possible, because subjects looks same). The
discus about the design was public, I am sure. It was relative longer
process, with good progress (from my perspective), because Daniel accepts
and fixed all my objection. The proposed syntax is fully consistent with
other psql commands - hard to create something new there, because psql
parser is pretty limited. Although I am thinking so syntax is good, clean
and useful I am open to discuss about it. Please, try the last design, last
patch - I spent lot of hours (and I am sure so Daniel much more) in thinking
how this can be designed better.

Looking at this patch, I have mixed feelings about it. On the one hand
I really like the look of the output, and I can see that the non-fixed
nature of the output columns makes this hard to achieve server-side.

But on the other hand, this seems to be going way beyond the normal
level of result formatting that something like \x does, and I find the
syntax for sorting particularly ugly. I can understand the need to
sort the colH values, but it seems to me that the result rows should
just be returned in the order the server returns them -- i.e., I don't
think we should allow sorting colV values client-side, overriding a
server-side ORDER BY clause in the query.

Client-side sorting makes me uneasy in general, and I think it should
be restricted to just sorting the columns that appear in the output
(the colH values). This would also allow the syntax to be simplified:

\crosstabview [colV] [colH] [colG1[,colG2...]] [sortCol [asc|desc]]

Overall, I like the feature, but I'm not convinced that it's ready in
its current form.

For the future (not in this first version of the patch), since the
transformation is more than just a \x-type formatting of the query
results, a nice-to-have feature would be a way to save the results
somewhere -- say by making it play nicely with \g or \copy somehow,
but I admit that I don't know exactly how that would work.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#74Pavel Stehule
pavel.stehule@gmail.com
In reply to: Dean Rasheed (#73)
Re: [patch] Proposal for \crosstabview in psql

Hi

Looking at this patch, I have mixed feelings about it. On the one hand

I really like the look of the output, and I can see that the non-fixed
nature of the output columns makes this hard to achieve server-side.

But on the other hand, this seems to be going way beyond the normal
level of result formatting that something like \x does, and I find the
syntax for sorting particularly ugly. I can understand the need to
sort the colH values, but it seems to me that the result rows should
just be returned in the order the server returns them -- i.e., I don't
think we should allow sorting colV values client-side, overriding a
server-side ORDER BY clause in the query.

This feature has zero relation with \x option, and any link to this option
is confusing. This is important, elsewhere we are on start again, where I
did long discuss with Daniel about the name, when I blocked the name
"rotate".

Client-side sorting makes me uneasy in general, and I think it should
be restricted to just sorting the columns that appear in the output
(the colH values). This would also allow the syntax to be simplified:

\crosstabview [colV] [colH] [colG1[,colG2...]] [sortCol [asc|desc]]

The sorting on client side is necessary - minimally in one direction,
because you cannot to create perfect sorting for both dimensions.
Possibility to order in second dimension is just pretty comfortable -
because you don't need to think two steps forward - when you create SQL
query.

I have a basic use case that should be supported well, and it is supported
well by last version of this patch. The evaluation of syntax is subjective.
We can compare Daniel's syntax and your proposal.

The use case: I have a table with the invoices with attributes (date, name
and amount). I would to take a report of amounts across months and
customers. Horizontal dimension is month (name), vertical dimension is name
of customers. I need sorting of months in semantic order and customers in
alphabet order.

So my query is:

SELECT name, to_char(date, 'mon') AS month, extract(month from date) AS
month_order, sum(amount) AS amount FROM invoices GROUP BY 1,2,3;

and crosstabview command (per Daniel proposal)

\crosstabview +name +month:month_order amount

But if I don't need column header in human readable form, I can do

\crosstabview +name +month_order amount

What is solution of this use case with your proposal??

I agree so this syntax is pretty raw. But it is consistent with other psql
statements and there are not possible conflicts.

What I mean? Your syntax is not unambiguous: \crosstabview [colV] [colH]
[colG1[,colG2...]] [sortCol [asc|desc]] - when I would to enter sort order
column, I have to enter one or more colG1,... or I have to enter explicitly
asc, desc keyword.

Regards

Pavel

Show quoted text

Overall, I like the feature, but I'm not convinced that it's ready in
its current form.

For the future (not in this first version of the patch), since the
transformation is more than just a \x-type formatting of the query
results, a nice-to-have feature would be a way to save the results
somewhere -- say by making it play nicely with \g or \copy somehow,
but I admit that I don't know exactly how that would work.

Regards,
Dean

#75Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Pavel Stehule (#74)
Re: [patch] Proposal for \crosstabview in psql

On 9 February 2016 at 10:09, Pavel Stehule <pavel.stehule@gmail.com> wrote:

The sorting on client side is necessary - minimally in one direction,
because you cannot to create perfect sorting for both dimensions.
Possibility to order in second dimension is just pretty comfortable -
because you don't need to think two steps forward - when you create SQL
query.

I have a basic use case that should be supported well, and it is supported
well by last version of this patch. The evaluation of syntax is subjective.
We can compare Daniel's syntax and your proposal.

The use case: I have a table with the invoices with attributes (date, name
and amount). I would to take a report of amounts across months and
customers. Horizontal dimension is month (name), vertical dimension is name
of customers. I need sorting of months in semantic order and customers in
alphabet order.

So my query is:

SELECT name, to_char(date, 'mon') AS month, extract(month from date) AS
month_order, sum(amount) AS amount FROM invoices GROUP BY 1,2,3;

and crosstabview command (per Daniel proposal)

\crosstabview +name +month:month_order amount

But if I don't need column header in human readable form, I can do

\crosstabview +name +month_order amount

What is solution of this use case with your proposal??

So it would just be

SELECT name,
to_char(date, 'mon') AS month,
sum(amount) AS amount,
extract(month from date) AS month_order
FROM invoices
GROUP BY 1,2,3
ORDER BY name
\crosstabview name month amount month_order

Note that I might also want to pass additional sort options, such as
"ORDER BY name NULLS LAST", which the existing syntax doesn't allow.
In the new syntax, such sort options could be trivially supported in
both the server- and client-side sorts:

SELECT name, to_char(date, 'mon') AS month,
extract(month from date) AS month_order, sum(amount) AS amount
FROM invoices
GROUP BY 1,2,3
ORDER BY name NULLS LAST
\crosstabview name month amount month_order asc nulls last

This is probably not an issue in this example, but it might well be in
other cases. The +/-scol syntax is always going to be limited in what
it can support.

I agree so this syntax is pretty raw. But it is consistent with other psql
statements and there are not possible conflicts.

What I mean? Your syntax is not unambiguous: \crosstabview [colV] [colH]
[colG1[,colG2...]] [sortCol [asc|desc]] - when I would to enter sort order
column, I have to enter one or more colG1,... or I have to enter explicitly
asc, desc keyword.

That is resolved by the comma that precedes colG2, etc. isn't it?

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#76Pavel Stehule
pavel.stehule@gmail.com
In reply to: Dean Rasheed (#75)
Re: [patch] Proposal for \crosstabview in psql

SELECT name, to_char(date, 'mon') AS month, extract(month from date) AS
month_order, sum(amount) AS amount FROM invoices GROUP BY 1,2,3;

and crosstabview command (per Daniel proposal)

\crosstabview +name +month:month_order amount

But if I don't need column header in human readable form, I can do

\crosstabview +name +month_order amount

What is solution of this use case with your proposal??

So it would just be

SELECT name,
to_char(date, 'mon') AS month,
sum(amount) AS amount,
extract(month from date) AS month_order
FROM invoices
GROUP BY 1,2,3
ORDER BY name
\crosstabview name month amount month_order

Warning: :) Now I am subjective. The Daniel syntax "\crosstabview +name
+month:month_order amount" looks more readable for me, because related
things are near to self.

Note that I might also want to pass additional sort options, such as
"ORDER BY name NULLS LAST", which the existing syntax doesn't allow.
In the new syntax, such sort options could be trivially supported in
both the server- and client-side sorts:

SELECT name, to_char(date, 'mon') AS month,
extract(month from date) AS month_order, sum(amount) AS amount
FROM invoices
GROUP BY 1,2,3
ORDER BY name NULLS LAST
\crosstabview name month amount month_order asc nulls last

I understand - if I compare these two syntaxes I and I am trying be
objective, then I see

your:
  + respect SQL clauses ordering, allows pretty complex ORDER BY clause
  - possible to fail on unexpected syntax errors
  +/- more verbose
  - allow only one client side sort
  - less expressive
Daniel:
  + cannot to fail on syntax error
  + more compacts (not necessary to specify ORDER BY clauses)
  + allow to specify sort in both dimensions
  + more expressive (+colH is more expressive than colV colH col colH
  - doesn't allow to complex order clauses in both dimensions

This is probably not an issue in this example, but it might well be in
other cases. The +/-scol syntax is always going to be limited in what
it can support.

the +/- syntax can be enhanced by additional attributes - this is only
syntax (but then there is a risk of possible syntax errors)

I agree so this syntax is pretty raw. But it is consistent with other

psql

statements and there are not possible conflicts.

What I mean? Your syntax is not unambiguous: \crosstabview [colV] [colH]
[colG1[,colG2...]] [sortCol [asc|desc]] - when I would to enter sort

order

column, I have to enter one or more colG1,... or I have to enter

explicitly

asc, desc keyword.

That is resolved by the comma that precedes colG2, etc. isn't it?

but colG1 is optional. What if you miss any colGx ?

Regards

Pavel

Show quoted text

Regards,
Dean

#77Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Pavel Stehule (#76)
Re: [patch] Proposal for \crosstabview in psql

On 9 February 2016 at 11:06, Pavel Stehule <pavel.stehule@gmail.com> wrote:

+ respect SQL clauses ordering, allows pretty complex ORDER BY clause

That, to me is the key point. SQL already allows very powerful
sorting, so psql should not just throw away the query's sort order and
replace it with something much more basic and limited. The exact
syntax can be debated, but I don't think psql should be doing row
sorting.

I also don't believe that extending the +/- sort syntax to support
more advanced options will be particularly easy, and the result is
likely to be even less readable. It also requires the user to learn
another syntax, when they will already be familiar with SQL's sort
syntax.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#78Daniel Verite
daniel@manitou-mail.org
In reply to: Alvaro Herrera (#69)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

So please can we have that wiki page so that the syntax can be hammered
out a bit more.

Sure, I'm on it.

I'm closing this as returned-with-feedback for now.

Well, the feedback it got during months was incorporated into
the patch in the form of significant improvements, and
at the end of this CF it was at the point that it has really been
polished, and no other feedback was coming.

I'll resubmit.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#79Daniel Verite
daniel@manitou-mail.org
In reply to: Alvaro Herrera (#71)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

While I understand that you may think that "silence is consent",
what I am afraid of is that some committer will look at this two
months from now and say "I hate this Hcol+ stuff, -1 from me" and
send the patch back for syntax rework. IMO it's better to have more
people chime in here so that the patch that we discuss during the
next commitfest is really the best one we can think of.

Yes, but on the other hand we can't force people to participate.
If a patch is moving forward and being discussed here between
one author and one reviewer, and nothing particularly wrong
pops out in what is discussed, the reality if that other people will
not intervene.

Besides, as it being mentioned here frequently, all patches, even
much more important ones, are short on reviews and reviewers
and testing, still new stuff must keep getting in the source tree
to progress.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#80Daniel Verite
daniel@manitou-mail.org
In reply to: Alvaro Herrera (#71)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

Also, what about the business of putting "x" if there's no third column?
Three months from now some Czech psql hacker will say "we should use
Unicode chars for this" and we will be forever stuck with \pset
unicode_crosstab_marker to change the character to a ☑ BALLOT BOX WITH
CZECH. Maybe we should think that a bit harder -- for example, what
about just rejecting the case with no third column and forcing the user
to add a third column with the character they choose? That way you
avoid that mess.

Yes, that implicit "X" with 2 column resultsets is not essential, it may
be removed without real damage.

About the possible suggestion to have a \pset unicode_crosstab_marker,
my opinion would be that it's not important enough to justify a new
\pset setting.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#81Daniel Verite
daniel@manitou-mail.org
In reply to: Dean Rasheed (#73)
Re: [patch] Proposal for \crosstabview in psql

Dean Rasheed wrote:

I don't think we should allow sorting colV values client-side,
overriding a server-side ORDER BY clause in the query.

I shared that opinion until (IIRC) the v8 or v9 of the patch.
Most of the evolution of this patch has been to go
from no client-side sorting option at all, to the full range
of possibilities, ascending or descending, and in both
vertical and horizontal directions.

I agree that colV sorting can be achieved through the
query's ORDER BY, which additionally is more efficient
so it should be the primary choice.

The reason to allow [+/-]colV in \crosstabview is because
I think the average user will expect it, by symmetry with colH.
As the display is reorganized to be like a "grid" instead of a "list
with several columns", we shift the focus to the symmetry
between horizontal and vertical headers, rather than on
the pre-crosstab form of the resultset, even if it's the
same data.
It's easier for the user to just stick a + in front of a column
reference than to figure out that the same result could
be achieved by editing the query and changing/adding
an ORDER BY.

Or said otherwise, having the [+/-] colV sorting is a way to
avoid the question:
"we can sort the horizontal header, so why can't we sort the
vertical header just the same?"

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#82Daniel Verite
daniel@manitou-mail.org
In reply to: Dean Rasheed (#75)
Re: [patch] Proposal for \crosstabview in psql

Dean Rasheed wrote:

Note that I might also want to pass additional sort options, such as
"ORDER BY name NULLS LAST", which the existing syntax doesn't allow.
In the new syntax, such sort options could be trivially supported in
both the server- and client-side sorts:

Note that NULL values in the column that pivots are discarded
by \crosstabview, because NULL as the name of a column does not
make sense.

The doc (in the patch) says:

"The horizontal header, displayed as the first row, contains the set of
all distinct non-null values found in column colH"

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#83Tom Lane
tgl@sss.pgh.pa.us
In reply to: Daniel Verite (#81)
Re: [patch] Proposal for \crosstabview in psql

"Daniel Verite" <daniel@manitou-mail.org> writes:

Dean Rasheed wrote:

I don't think we should allow sorting colV values client-side,
overriding a server-side ORDER BY clause in the query.

I shared that opinion until (IIRC) the v8 or v9 of the patch.
Most of the evolution of this patch has been to go
from no client-side sorting option at all, to the full range
of possibilities, ascending or descending, and in both
vertical and horizontal directions.

I haven't been paying attention to this thread ... but it is sure
sounding like this feature has gotten totally out of hand. Suggest
reconsidering your design goals.

Or said otherwise, having the [+/-] colV sorting is a way to
avoid the question:
"we can sort the horizontal header, so why can't we sort the
vertical header just the same?"

I would turn that around, and ask why not remove *both* those things.

I do not think we want any client-side sorting in this feature at all,
because the minute you have any such thing, you are going to have an
absolutely never-ending stream of demands for more sorting features:
multi column, numeric vs text, ASC vs DESC, locale-aware, etc etc etc.
I'd rather reject the feature altogether than expect that psql is going
to have to grow all of that.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#84Pavel Stehule
pavel.stehule@gmail.com
In reply to: Tom Lane (#83)
Re: [patch] Proposal for \crosstabview in psql

I haven't been paying attention to this thread ... but it is sure

sounding like this feature has gotten totally out of hand. Suggest
reconsidering your design goals.

Or said otherwise, having the [+/-] colV sorting is a way to
avoid the question:
"we can sort the horizontal header, so why can't we sort the
vertical header just the same?"

I would turn that around, and ask why not remove *both* those things.

I do not think we want any client-side sorting in this feature at all,
because the minute you have any such thing, you are going to have an
absolutely never-ending stream of demands for more sorting features:
multi column, numeric vs text, ASC vs DESC, locale-aware, etc etc etc.
I'd rather reject the feature altogether than expect that psql is going
to have to grow all of that.

I am thinking so without possibility to sort data on client side, this
feature will be significantly limited. You cannot do server side sort for
both dimensions. Working with 2d report when one dimension is unsorted is
not friendly.

But the client side sorting can be limited to number's or C locale sorting.
I don't think so full sort possibilities are necessary.

Regards

Pavel

Show quoted text

regards, tom lane

#85Daniel Verite
daniel@manitou-mail.org
In reply to: Tom Lane (#83)
Re: [patch] Proposal for \crosstabview in psql

Tom Lane wrote:

I do not think we want any client-side sorting in this feature at all,
because the minute you have any such thing, you are going to have an
absolutely never-ending stream of demands for more sorting features:
multi column, numeric vs text, ASC vs DESC, locale-aware, etc etc etc.

It doesn't really do any client-side sorting, the rest of the thread might
refer to it like that by oversimplification, but if the command requests
a header to be sorted, a "backdoor-style" query of this form
is sent to the server, with PQexecParams():

SELECT n FROM (VALUES ($1,1),($2,2),($3,3)...) ) AS l(x,n)
ORDER BY x [DESC]
where the values to display in the header are bound to
$1,$2,.. and the type associated with these parameters is
the PQftype() of the field from which these values come.
Then the <n> values coming back ordered by <x> tell us
where to position the values corresponding to $1,$2... in the
sorted header.

There are some cases when this sort cannot work.
For example if the field is an anonymous type or a ROW().
Or if the field is POINT(x,y), because our "point" type
does not support order by.
I believe these are corner cases for this feature. In these
cases, psql just displays the error message that PQexecParams()
emits.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#86Jim Nasby
Jim.Nasby@BlueTreble.com
In reply to: Daniel Verite (#79)
Re: [patch] Proposal for \crosstabview in psql

On 2/9/16 8:40 AM, Daniel Verite wrote:

Alvaro Herrera wrote:

While I understand that you may think that "silence is consent",
what I am afraid of is that some committer will look at this two
months from now and say "I hate this Hcol+ stuff, -1 from me" and
send the patch back for syntax rework. IMO it's better to have more
people chime in here so that the patch that we discuss during the
next commitfest is really the best one we can think of.

Yes, but on the other hand we can't force people to participate.
If a patch is moving forward and being discussed here between
one author and one reviewer, and nothing particularly wrong
pops out in what is discussed, the reality if that other people will
not intervene.

The problem is that assumes people are still reading the thread. This is
a feature I'm very interested, but at some point I just gave up on
trying to follow it because of the volume of messages. I bet a lot of
others did the same.

I think in this case, what should have happened is that once an issue
with the design of the feature itself was identified, a new thread
should have been started to discuss that part in particular. That would
have re-raised attention and made it easier for people to follow that
specific part of the discussion, even if they don't care about some if
the code intricacies.

Besides, as it being mentioned here frequently, all patches, even
much more important ones, are short on reviews and reviewers
and testing, still new stuff must keep getting in the source tree
to progress.

Sure, and new stuff will be making it in. The question is: will *your*
new stuff be making it in?

Believe me, I know how burdensome getting new features pushed is.
Frankly it shouldn't be this hard, and I certainly don't blame you for
being frustrated. But none of that changes the fact that the bar for
including code is very high and if you don't meet it then your stuff
won't make it in.
--
Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
Experts in Analytics, Data Architecture and PostgreSQL
Data in Trouble? Get it in Treble! http://BlueTreble.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#87Andres Freund
andres@anarazel.de
In reply to: Dean Rasheed (#73)
Re: [patch] Proposal for \crosstabview in psql

On 2016-02-09 09:27:04 +0000, Dean Rasheed wrote:

Looking at this patch, I have mixed feelings about it. On the one hand
I really like the look of the output, and I can see that the non-fixed
nature of the output columns makes this hard to achieve server-side.

But on the other hand, this seems to be going way beyond the normal
level of result formatting that something like \x does, and I find the
syntax for sorting particularly ugly.

I've pretty similar doubts. Addinging features to psql which are complex
enough that it's likely that people will be forced to parse psql
output... On the other hand, a proper server side solution won't be
easy; so maybe this is a okay enough stopgap.

Greetings,

Andres Freund

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#88Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Andres Freund (#87)
Re: [patch] Proposal for \crosstabview in psql

On 11 February 2016 at 08:43, Andres Freund <andres@anarazel.de> wrote:

On 2016-02-09 09:27:04 +0000, Dean Rasheed wrote:

Looking at this patch, I have mixed feelings about it. On the one hand
I really like the look of the output, and I can see that the non-fixed
nature of the output columns makes this hard to achieve server-side.

But on the other hand, this seems to be going way beyond the normal
level of result formatting that something like \x does, and I find the
syntax for sorting particularly ugly.

I've pretty similar doubts. Addinging features to psql which are complex
enough that it's likely that people will be forced to parse psql
output... On the other hand, a proper server side solution won't be
easy; so maybe this is a okay enough stopgap.

Well to be clear, I like the idea of this feature, and I'm not trying
to stand in the way of progressing it. However, I can't see myself
committing it in its current form.

My biggest problem is with the sorting, for all the reasons discussed
above. There is absolutely no reason for \crosstabview to be
re-sorting rows -- they should just be left in the original query
result order. Sorting columns is a little more understandable, since
there is no way for the original query to control the order in which
the colV values come out, but Tom raises a good point -- there are far
too many bells and whistles when it comes to sorting, and we don't
want to be adding all of them to the psql syntax.

Thinking about this some more though, perhaps *sorting* the columns is
the wrong way to be thinking about it. Perhaps a better approach would
be to allow the columns to be *listed* (optionally, using a separate
query). Something like the following (don't get too hung up on the
syntax):

SELECT name,
to_char(date, 'Mon') AS month,
sum(amount) AS amount
FROM invoices
GROUP BY 1,2
ORDER BY name
\crosstabview cols = (select to_char(d, 'Mon') from
generate_series('2000-01-01'::date, '2000-12-01', '1 month') d)

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#89Pavel Stehule
pavel.stehule@gmail.com
In reply to: Dean Rasheed (#88)
Re: [patch] Proposal for \crosstabview in psql

Thinking about this some more though, perhaps *sorting* the columns is

the wrong way to be thinking about it. Perhaps a better approach would
be to allow the columns to be *listed* (optionally, using a separate
query). Something like the following (don't get too hung up on the
syntax):

SELECT name,
to_char(date, 'Mon') AS month,
sum(amount) AS amount
FROM invoices
GROUP BY 1,2
ORDER BY name
\crosstabview cols = (select to_char(d, 'Mon') from
generate_series('2000-01-01'::date, '2000-12-01', '1 month') d)

The idea is ok, but this design cannot be described as user friendly. The
work with time dimension is pretty common, and should be supported by some
short user friendly syntax.

Regards

Pavel

Show quoted text

Regards,
Dean

#90Daniel Verite
daniel@manitou-mail.org
In reply to: Alvaro Herrera (#69)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

So please can we have that wiki page so that the syntax can be hammered
out a bit more.

I've added a wiki page with explanation and examples here:

https://wiki.postgresql.org/wiki/Crosstabview

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#91Daniel Verite
daniel@manitou-mail.org
In reply to: Dean Rasheed (#88)
Re: [patch] Proposal for \crosstabview in psql

Dean Rasheed wrote:

My biggest problem is with the sorting, for all the reasons discussed
above. There is absolutely no reason for \crosstabview to be
re-sorting rows -- they should just be left in the original query
result order

We want the option to sort the vertical the header in a late additional
step when the ORDER BY of the query is already assigned to another
purpose.

I've submitted this example on the wiki:
https://wiki.postgresql.org/wiki/Crosstabview

create view v_data as
select * from ( values
('v1','h2','foo', '2015-04-01'::date),
('v2','h1','bar', '2015-01-02'),
('v1','h0','baz', '2015-07-12'),
('v0','h4','qux', '2015-07-15')
) as l(v,h,c,d);

Normal top-down display:

select v,to_char(d,'Mon') as m, c from v_data order by d;

v | m | c
----+-----+-----
v2 | Jan | bar
v1 | Apr | foo
v1 | Jul | baz
v0 | Jul | qux

Crosstabview display without any additional sort:

\crosstabview v m c

v | Jan | Apr | Jul
----+-----+-----+-----
v2 | bar | |
v1 | | foo | baz
v0 | | | qux

"d" is not present the resultset but it drives the sort
so that month names come out in the natural order.

\crosstabview does not discard the order of colH nor the order of colV,
it follows both, so that we get v2,v1,v0 in this order in the leftmost
column (vertical header) just like in the resultset.

At this point, it seems to me that it's perfectly reasonable for our user
to expect the possibility of sorting additionally by "v" , without
changing the query and without changing the order of the horizontal
header:

\crosstabview +v m c

v | Jan | Apr | Jul
----+-----+-----+-----
v0 | | | qux
v1 | | foo | baz
v2 | bar | |

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#92Jim Nasby
Jim.Nasby@BlueTreble.com
In reply to: Dean Rasheed (#88)
Re: [patch] Proposal for \crosstabview in psql

On 2/11/16 4:21 AM, Dean Rasheed wrote:

Thinking about this some more though, perhaps*sorting* the columns is
the wrong way to be thinking about it. Perhaps a better approach would
be to allow the columns to be*listed* (optionally, using a separate
query). Something like the following (don't get too hung up on the
syntax):

SELECT name,
to_char(date, 'Mon') AS month,
sum(amount) AS amount
FROM invoices
GROUP BY 1,2
ORDER BY name
\crosstabview cols = (select to_char(d, 'Mon') from
generate_series('2000-01-01'::date, '2000-12-01', '1 month') d)

My concern with that is that often you don't know what the columns will
be, because you don't know what exact data the query will produce. So to
use this syntax you'd have to re-create a huge chunk of the original
query. :(
--
Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
Experts in Analytics, Data Architecture and PostgreSQL
Data in Trouble? Get it in Treble! http://BlueTreble.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#93Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Daniel Verite (#91)
Re: [patch] Proposal for \crosstabview in psql

On 15 February 2016 at 14:08, Daniel Verite <daniel@manitou-mail.org> wrote:

Dean Rasheed wrote:

My biggest problem is with the sorting, for all the reasons discussed
above. There is absolutely no reason for \crosstabview to be
re-sorting rows -- they should just be left in the original query
result order

Normal top-down display:

select v,to_char(d,'Mon') as m, c from v_data order by d;

v | m | c
----+-----+-----
v2 | Jan | bar
v1 | Apr | foo
v1 | Jul | baz
v0 | Jul | qux

At this point, it seems to me that it's perfectly reasonable for our user
to expect the possibility of sorting additionally by "v" , without
changing the query and without changing the order of the horizontal
header:

\crosstabview +v m c

v | Jan | Apr | Jul
----+-----+-----+-----
v0 | | | qux
v1 | | foo | baz
v2 | bar | |

I don't find that example particularly compelling. If I want to sort
the rows coming out of a query, my first thought is always going to be
to add/adjust the query's ORDER BY clause, not use some weird +/- psql
syntax.

The crux of the problem here is that in a pivoted query resultset SQL
can be used to control the order of the rows or the columns, but not
both at the same time. IMO it is more natural to use SQL to control
the order of the rows. The columns are the result of the psql
pivoting, so it's reasonable to control them via psql options.

A couple of other points to bear in mind:

The number of columns is always going to be quite limited (at most
1600, and usually far less than that), whereas the number of rows
could be arbitrarily large. So sorting the rows client-side in the way
that you are could get very inefficient, whereas that's not such a
problem for the columns.

The column values are non-NULL, so they require a more limited set of
sort options, whereas the rows could be anything, and people will want
all the sort options to be available.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#94Dean Rasheed
dean.a.rasheed@gmail.com
In reply to: Jim Nasby (#92)
Re: [patch] Proposal for \crosstabview in psql

On 17 February 2016 at 02:32, Jim Nasby <Jim.Nasby@bluetreble.com> wrote:

On 2/11/16 4:21 AM, Dean Rasheed wrote:

Thinking about this some more though, perhaps*sorting* the columns is
the wrong way to be thinking about it. Perhaps a better approach would
be to allow the columns to be*listed* (optionally, using a separate
query). Something like the following (don't get too hung up on the
syntax):

SELECT name,
to_char(date, 'Mon') AS month,
sum(amount) AS amount
FROM invoices
GROUP BY 1,2
ORDER BY name
\crosstabview cols = (select to_char(d, 'Mon') from
generate_series('2000-01-01'::date, '2000-12-01', '1 month') d)

My concern with that is that often you don't know what the columns will be,
because you don't know what exact data the query will produce. So to use
this syntax you'd have to re-create a huge chunk of the original query. :(

Yeah, that's a reasonable concern.

On the flip side, one of the advantages of the above syntax is that
you have absolute control over the columns, whereas with the
sort-based syntax you might find some columns missing (e.g., if there
were no invoices in August) and that could lead to confusion parsing
the results.

I'm not totally opposed to specifying a column sort order in psql, and
perhaps there's a way to support both 'cols' and 'col_order' options
in psql, since there are different situations where one or the other
might be more useful.

What I am opposed to is specifying the row order in psql, because IMO
that's something that should be done entirely in the SQL query.

Regards,
Dean

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#95Daniel Verite
daniel@manitou-mail.org
In reply to: Jim Nasby (#92)
Re: [patch] Proposal for \crosstabview in psql

Jim Nasby wrote:

ORDER BY name
\crosstabview cols = (select to_char(d, 'Mon') from
generate_series('2000-01-01'::date, '2000-12-01', '1 month') d)

My concern with that is that often you don't know what the columns will
be, because you don't know what exact data the query will produce. So to
use this syntax you'd have to re-create a huge chunk of the original
query. :(

Also, if that additional query refers to tables, it should be executed
with the same data visibility as the main query. Doesn't that mean
that both queries should happen within the same repeatable
read transaction?

Another impractical aspect of this approach is that a
meta-command invocation in psql must fit on a single line, so
queries containing newlines are not acceptable as argument.
This problem exists with "\copy (select...) to ..." already.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#96Peter Eisentraut
peter_e@gmx.net
In reply to: Daniel Verite (#82)
Re: [patch] Proposal for \crosstabview in psql

On 2/9/16 11:21 AM, Daniel Verite wrote:

Note that NULL values in the column that pivots are discarded
by \crosstabview, because NULL as the name of a column does not
make sense.

Why not?

All you're doing is printing it out, and psql is quite capable of
printing a null value.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#97Jim Nasby
Jim.Nasby@BlueTreble.com
In reply to: Dean Rasheed (#94)
Re: [patch] Proposal for \crosstabview in psql

On 2/17/16 9:03 AM, Dean Rasheed wrote:

I'm not totally opposed to specifying a column sort order in psql, and
perhaps there's a way to support both 'cols' and 'col_order' options
in psql, since there are different situations where one or the other
might be more useful.

Yeah. If there was some magic way to reference the underlying data with
your syntax it probably wouldn't be that bad. AIUI normally we're just
dumping data into a Portal and there's no option to read back from it,
but if the query results were first put in a tuplestore then I suspect
it wouldn't be that hard to query against it and produce another result set.

What I am opposed to is specifying the row order in psql, because IMO
that's something that should be done entirely in the SQL query.

+1
--
Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
Experts in Analytics, Data Architecture and PostgreSQL
Data in Trouble? Get it in Treble! http://BlueTreble.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#98Daniel Verite
daniel@manitou-mail.org
In reply to: Daniel Verite (#95)
Re: [patch] Proposal for \crosstabview in psql

Daniel Verite wrote:

ORDER BY name
\crosstabview cols = (select to_char(d, 'Mon') from
generate_series('2000-01-01'::date, '2000-12-01', '1 month') d)

My concern with that is that often you don't know what the columns will
be, because you don't know what exact data the query will produce. So to
use this syntax you'd have to re-create a huge chunk of the original
query. :(

Also, if that additional query refers to tables, it should be executed
with the same data visibility as the main query. Doesn't that mean
that both queries should happen within the same repeatable
read transaction?

Another impractical aspect of this approach is that a
meta-command invocation in psql must fit on a single line, so
queries containing newlines are not acceptable as argument.
This problem exists with "\copy (select...) to ..." already.

Thinking more about that, it occurs to me that if the sort must come
from a user-supplied bit of SQL, it would be simpler to just direct the
user to submit it in the main query, in an additional dedicated column.

For instance, to get a specific, separate order on "h",
let the user change this:

SELECT v, h, c FROM v_data ORDER BY v;

into that:

SELECT v, h, row_number() over(order by h) as hn, c
FROM v_data ORDER BY v;

then with a relatively simple modification to the patch,
this invocation:

\crosstabview v h:hn c

would display "h" in the horizontal header ordered by "hn".

ISTM this handles two objections raised upthread:

1. The ORDER BY inside OVER() can be augmented with additional
clauses such as lc_collate, desc, nulls last, etc... contrary to
the controversed "+/-" syntax.

2. a post-sort "backdoor" query is no longer necessary.

The drawback for me is that this change doesn't play out with
my original scenario for the command, which is to give the ability to
scrutinize query results in crosstab mode, playing with variations on
what column is pivoted and how headers for both directions get sorted,
while ideally not changing _at all_ the original query in the query
buffer, but just invoking successive \crosstabview [args] commands
with varying arguments.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#99Daniel Verite
daniel@manitou-mail.org
In reply to: Peter Eisentraut (#96)
Re: [patch] Proposal for \crosstabview in psql

Peter Eisentraut wrote:

On 2/9/16 11:21 AM, Daniel Verite wrote:

Note that NULL values in the column that pivots are discarded
by \crosstabview, because NULL as the name of a column does not
make sense.

Why not?

All you're doing is printing it out, and psql is quite capable of
printing a null value.

Initially it's by analogy with the crosstab SRF, but it's true
that the same principle does not have to apply to crosstabview.

The code could set in the header whatever text "pset null" is set to,
at the place where a pivoted NULL would be supposed to go
if it was not filtered out in the first place.

I'll consider implementing that change if there's no objection.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#100Daniel Verite
daniel@manitou-mail.org
In reply to: Dean Rasheed (#93)
Re: [patch] Proposal for \crosstabview in psql

Dean Rasheed wrote:

If I want to sort the rows coming out of a query, my first thought
is always going to be to add/adjust the query's ORDER BY clause, not
use some weird +/- psql syntax.

About the vertical sort, I agree on all your points.
It's best to rely on ORDER BY for all the reasons mentioned,
as opposed to a separate sort in a second step.

But you're considering the case when a user is designing
or adapting a query for the purpose of crosstab
viewing. As mentioned in my previous reply (about the
methods to achieve horizontal sort), that scenario is not really
what motivates the feature in the first place.

If removing that sort option is required to move forward
with the patch because it's controversial, so be it,
but overall I don't see this as a benefit for the end user,
it's just an option.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#101Robert Haas
robertmhaas@gmail.com
In reply to: Daniel Verite (#100)
Re: [patch] Proposal for \crosstabview in psql

On Thu, Feb 18, 2016 at 9:23 AM, Daniel Verite <daniel@manitou-mail.org> wrote:

Dean Rasheed wrote:

If I want to sort the rows coming out of a query, my first thought
is always going to be to add/adjust the query's ORDER BY clause, not
use some weird +/- psql syntax.

About the vertical sort, I agree on all your points.
It's best to rely on ORDER BY for all the reasons mentioned,
as opposed to a separate sort in a second step.

But you're considering the case when a user is designing
or adapting a query for the purpose of crosstab
viewing. As mentioned in my previous reply (about the
methods to achieve horizontal sort), that scenario is not really
what motivates the feature in the first place.

If removing that sort option is required to move forward
with the patch because it's controversial, so be it,
but overall I don't see this as a benefit for the end user,
it's just an option.

Discussion on this patch seems to have died off. I'm probably not
going to win any popularity contests for saying this, but I think we
should reject this patch. I don't feel like this is really a psql
feature: it's a powerful data visualization tool which we're proposing
to jam into psql. I don't think that's psql's purpose. I also think
it's quite possible that there could be an unbounded number of
slightly different things that people want here, and if we take this
one and a couple more, the code for these individual features could
come to be larger than all of psql, even though probably 95% of psql
users would never use any of those.

Now, that having been said, if other people want this feature to go in
and are willing to do the work to get it in, I've said my piece and
won't complain further. There are a couple of committers who have
taken positive interest in this thread, so that's good. However,
there are also a couple of committers who have expressed doubts
similar to mine, so that's not so good. But worse than either of
those things, there is no real agreement on what the overall design of
this feature should be. Everybody wants something a little different,
for different reasons. If we can't come to an agreement, more or less
immediately, on what to try to get into 9.6, then this can't go into
this release. Whether it should go into a future release is a
question we can leave for another time.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#102Daniel Verite
daniel@manitou-mail.org
In reply to: Robert Haas (#101)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Robert Haas wrote:

But worse than either of those things, there is no real
agreement on what the overall design of this feature
should be.

The part in the design that raised concerns upthread is
essentially how headers sorting is exposed to the user and
implemented.

As suggested in [1]/messages/by-id/3d513263-104b-41e3-b1c7-4ad4bd99c491@mm, I've made some drastic changes in the
attached patch to take the comments (from Dean R., Tom L.)
into account. The idea is to limit to the bare minimum
the involvement of psql in sorting:

- the +/- syntax goes away

- the possibility of post-sorting the values through a backdoor
query goes away too, for both headers.

- the vertical order of the crosstab view is now driven solely by the
order in the query

- the order of the horizontal header can be optionally specified
by a column expected to contain an integer, with the syntax
\crosstabview colv colh:scolh [other cols]
which means "colh" will be sorted by "scolh".
It still defaults to whatever order "colh" comes in from the results

Concerning the optional "scolh", there are cases where it might pre-exist
naturally, such as a month number going in pair with a month name.
In other cases, a user may add it as a kind of "synthetic column"
by way of a window function, for example:
SELECT ...other columns...,
(row_number() over(order by something [order options]) as scolh
FROM...
Only the relative order of scolh values is taken into account, the value
itself
has no meaning for crosstabview.

- also NULLs are no longer excluded from headers, per Peter E.
comment in [2]/messages/by-id/56C4E344.6070903@gmx.net.

[1]: /messages/by-id/3d513263-104b-41e3-b1c7-4ad4bd99c491@mm
/messages/by-id/3d513263-104b-41e3-b1c7-4ad4bd99c491@mm

[2]: /messages/by-id/56C4E344.6070903@gmx.net

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-crosstabview-v12.difftext/x-patch; name=psql-crosstabview-v12.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 8a85804..da0621b 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -990,6 +990,113 @@ testdb=&gt;
       </varlistentry>
 
       <varlistentry>
+        <term><literal>\crosstabview [
+            <replaceable class="parameter">colV</replaceable>
+            <replaceable class="parameter">colH</replaceable>
+            [:<replaceable class="parameter">scolH</replaceable>]
+            [<replaceable class="parameter">colG1[,colG2...]</replaceable>]
+            ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows
+        the results inside a crosstab grid.
+        The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header
+        and the output column <replaceable class="parameter">colH</replaceable>
+        becomes a horizontal header, optionally sorted by ranking data obtained
+        from <replaceable class="parameter">scolH</replaceable>.
+
+        <replaceable class="parameter">colG1[,colG2...]</replaceable>
+        is the list of output columns to project into the grid.
+        By default, all output columns of the query except 
+        <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>
+        are included in this list.
+        </para>
+
+        <para>
+        All columns can be refered to by their position (starting at 1), or by
+        their name. Normal case folding and quoting rules apply on column
+        names. By default,
+        <replaceable class="parameter">colV</replaceable> corresponds to column 1
+        and <replaceable class="parameter">colH</replaceable> to column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the deduplicated values found in
+        column <replaceable class="parameter">colV</replaceable>, in the same
+        order as in the query results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the deduplicated values found in
+        column <replaceable class="parameter">colH</replaceable>, in
+        the order of appearance in the query results.
+        If specified, the optional <replaceable class="parameter">scolH</replaceable>
+        argument refers to a column whose values should be integer numbers
+        by which <replaceable class="parameter">colH</replaceable> will be sorted
+        to be positioned in the horizontal header.
+        </para>
+
+        <para>
+        Inside the crosstab grid,
+        given a query output with <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        the contents of a cell located at the intersection
+        <literal>(x,y)</literal> is determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the query results such that the
+         value for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns or the columns listed in
+         <replaceable class="parameter">colG1[,colG2...]</replaceable>
+         are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed
+         in the cell as if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to
+         the case of one row except that the values coming from different rows
+         are stacked vertically, the different source rows being separated by
+         newline characters inside the cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term><literal>\d[S+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
 
         <listitem>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index 66e14fb..9d29fe1 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ override CPPFLAGS := -I. -I$(srcdir) -I$(libpq_srcdir) -I$(top_srcdir)/src/bin/p
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o print.o describe.o \
 	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
-	sql_help.o \
+	sql_help.o crosstabview.o \
 	$(WIN32RES)
 
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 9750a5b..e4db76e 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -39,6 +39,7 @@
 
 #include "common.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "describe.h"
 #include "help.h"
 #include "input.h"
@@ -364,6 +365,39 @@ exec_command(const char *cmd,
 	else if (strcmp(cmd, "copyright") == 0)
 		print_copyright();
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		char	*opt1,
+			*opt2,
+			*opt3;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt3 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1): NULL;
+			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2): NULL;
+			pset.crosstabview_cols_grid = opt3 ? pg_strdup(opt3): NULL;
+			pset.crosstabview_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+		free(opt3);
+	}
+
 	/* \d* commands */
 	else if (cmd[0] == 'd')
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index 2cb2e9b..b368883 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -24,6 +24,7 @@
 #include "command.h"
 #include "copy.h"
 #include "mbprint.h"
+#include "crosstabview.h"
 
 
 static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
@@ -906,6 +907,8 @@ PrintQueryResults(PGresult *results)
 			/* store or print the data ... */
 			if (pset.gset_prefix)
 				success = StoreQueryTuple(results);
+			else if (pset.crosstabview_output)
+				success = PrintResultsInCrossTab(results);
 			else
 				success = PrintQueryTuples(results);
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
@@ -1192,6 +1195,23 @@ sendquery_cleanup:
 		pset.gset_prefix = NULL;
 	}
 
+	/* reset \crosstabview settings */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
+	if (pset.crosstabview_cols_grid)
+	{
+		free(pset.crosstabview_cols_grid);
+		pset.crosstabview_cols_grid = NULL;
+	}
 	return OK;
 }
 
@@ -1354,7 +1374,25 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 			is_pager = true;
 		}
 
-		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
+		if (pset.crosstabview_output)
+		{
+			if (ntuples < fetch_count)
+				PrintResultsInCrossTab(results);
+			else
+			{
+				/*
+				  crosstabview is denied if the whole set of rows is not
+				  guaranteed to be fetched in the first iteration, because
+				  it's expected in memory as a single PGresult structure.
+				*/
+				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
+					fetch_count);
+				PQclear(results);
+				break;
+			}
+		}
+		else
+			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
 
 		PQclear(results);
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..4e21162
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,938 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+
+#include "common.h"
+#include "crosstabview.h"
+#include "pqexpbuffer.h"
+#include "settings.h"
+#include <string.h>
+
+/*
+ * Value/position from the resultset that goes into the horizontal or vertical
+ * crosstabview header.
+ */
+struct pivot_field
+{
+	/*
+	 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
+	 * value becomes an entry in the vertical header (colV), or horizontal
+	 * header (colH).
+	 * A Null value is represented by a NULL pointer.
+	 */
+	char	   *name;
+
+	/*
+	 * When a sort is requested on an alternative column, this holds
+	 * PQgetvalue() for the sort column corresponding to <name>. If
+	 * <name> appear multiple times, it's the first value in the
+	 * order of the results that is kept.
+	 * A Null value is represented by a NULL pointer.
+	 */
+	char	   *sort_value;
+
+	/*
+	 * Rank of this value, starting at 0. Initially, it's the relative position
+	 * of the first appearance of <name> in the resultset.
+	 * For example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
+	 * When a sort column is specified, ranks get updated in a final pass to reflect
+	 * the desired order.
+	 */
+	int			rank;
+};
+
+/* Node in avl_tree */
+struct avl_node
+{
+	/* Node contents */
+	struct pivot_field field;
+
+	/*
+	 * Height of this node in the tree (number of nodes on the longest
+	 * path to a leaf).
+	 */
+	int			height;
+
+	/*
+	 * Child nodes. [0] points to left subtree, [1] to right subtree.
+	 * Never NULL, points to the empty node avl_tree.end when no left
+	 * or right value.
+	 */
+	struct avl_node *childs[2];
+};
+
+/*
+ * Control structure for the AVL tree (binary search tree kept
+ * balanced with the AVL algorithm)
+ */
+struct avl_tree
+{
+	int			count;			/* Total number of nodes */
+	struct avl_node *root;		/* root of the tree */
+	struct avl_node *end;		/* Immutable dereferenceable empty tree */
+};
+
+/*
+ * Value comparator for vertical and horizontal headers
+ * used for deduplication only.
+ * - null values are considered equal
+ * - non-null < null
+ * - non-null values are compared with strcmp()
+ */
+static int
+pivotFieldCompare(const void *a, const void *b)
+{
+	struct pivot_field* pa = (struct pivot_field*) a;
+	struct pivot_field* pb = (struct pivot_field*) b;
+
+	/* test null values */
+	if (!pb->name)
+		return pa->name ? -1 : 0;
+	else if (!pa->name)
+		return 1;
+	/* non-null values */
+	return strcmp( ((struct pivot_field*)a)->name,
+				   ((struct pivot_field*)b)->name);
+}
+
+static int
+rankCompare(const void* a, const void* b)
+{
+	return *((int*)a) - *((int*)b);
+}
+
+/*
+ * The avl* functions below provide a minimalistic implementation of AVL binary
+ * trees, to efficiently collect the distinct values that will form the horizontal
+ * and vertical headers. It only supports adding new values, no removal or even
+ * search.
+ */
+static void
+avlInit(struct avl_tree *tree)
+{
+	tree->end = (struct avl_node*) pg_malloc0(sizeof(struct avl_node));
+	tree->end->childs[0] = tree->end->childs[1] = tree->end;
+	tree->count = 0;
+	tree->root = tree->end;
+}
+
+/* Deallocate recursively an AVL tree, starting from node */
+static void
+avlFree(struct avl_tree* tree, struct avl_node* node)
+{
+	if (node->childs[0] != tree->end)
+	{
+		avlFree(tree, node->childs[0]);
+		pg_free(node->childs[0]);
+	}
+	if (node->childs[1] != tree->end)
+	{
+		avlFree(tree, node->childs[1]);
+		pg_free(node->childs[1]);
+	}
+	if (node == tree->root) {
+		/* free the root separately as it's not child of anything */
+		if (node != tree->end)
+			pg_free(node);
+		/* free the tree->end struct only once and when all else is freed */
+		pg_free(tree->end);
+	}
+}
+
+/* Set the height to 1 plus the greatest of left and right heights */
+static void
+avlUpdateHeight(struct avl_node *n)
+{
+	n->height = 1 + (n->childs[0]->height > n->childs[1]->height ?
+					 n->childs[0]->height:
+					 n->childs[1]->height);
+}
+
+/* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
+static struct avl_node*
+avlRotate(struct avl_node **current, int dir)
+{
+	struct avl_node *before = *current;
+	struct avl_node *after = (*current)->childs[dir];
+
+	*current = after;
+	before->childs[dir] = after->childs[!dir];
+	avlUpdateHeight(before);
+	after->childs[!dir] = before;
+
+	return after;
+}
+
+static int
+avlBalance(struct avl_node *n)
+{
+	return n->childs[0]->height - n->childs[1]->height;
+}
+
+/*
+ * After an insertion, possibly rebalance the tree so that the left and right
+ * node heights don't differ by more than 1.
+ * May update *node.
+ */
+static void
+avlAdjustBalance(struct avl_tree *tree, struct avl_node **node)
+{
+	struct avl_node *current = *node;
+	int b = avlBalance(current)/2;
+	if (b != 0)
+	{
+		int dir = (1 - b)/2;
+		if (avlBalance(current->childs[dir]) == -b)
+		  avlRotate(&current->childs[dir], !dir);
+		current = avlRotate(node, dir);
+	}
+	if (current != tree->end)
+	  avlUpdateHeight(current);
+}
+
+/*
+ * Insert a new value/field, starting from *node, reaching the
+ * correct position in the tree by recursion.
+ * Possibly rebalance the tree and possibly update *node.
+ * Do nothing if the value is already present in the tree.
+ */
+static void
+avlInsertNode(struct avl_tree* tree,
+			  struct avl_node **node,
+			  struct pivot_field field)
+{
+	struct avl_node *current = *node;
+
+	if (current == tree->end)
+	{
+		struct avl_node * new_node = (struct avl_node*)
+			pg_malloc(sizeof(struct avl_node));
+		new_node->height = 1;
+		new_node->field = field;
+		new_node->childs[0] = new_node->childs[1] = tree->end;
+		tree->count++;
+		*node = new_node;
+	}
+	else
+	{
+		int cmp = pivotFieldCompare(&field, &current->field);
+		if (cmp != 0)
+		{
+			avlInsertNode(tree,
+						  cmp > 0 ? &current->childs[1] : &current->childs[0],
+						  field);
+			avlAdjustBalance(tree, node);
+		}
+	}
+}
+
+/* Insert the value into the AVL tree, if it does not preexist */
+static void
+avlMergeValue(struct avl_tree* tree, char* name, char* sort_value)
+{
+	struct pivot_field field;
+	field.name = name;
+	field.rank = tree->count;
+	field.sort_value = sort_value;
+	avlInsertNode(tree, &tree->root, field);
+}
+
+/*
+ * Recursively extract node values into the names array, in sorted order with a
+ * left-to-right tree traversal.
+ * Return the next candidate offset to write into the names array.
+ * fields[] must be preallocated to hold tree->count entries
+ */
+static int
+avlCollectFields(struct avl_tree* tree,
+				 struct avl_node* node,
+				 struct pivot_field* fields,
+				 int idx)
+{
+	if (node == tree->end)
+		return idx;
+	idx = avlCollectFields(tree, node->childs[0], fields, idx);
+	fields[idx] = node->field;
+	return avlCollectFields(tree, node->childs[1], fields,  idx+1);
+}
+
+
+static void
+rankSort(int num_columns, struct pivot_field* piv_columns)
+{
+	int* hmap; /* [[offset in piv_columns, rank], ...for every header entry] */
+	int i;
+
+	hmap = (int*) pg_malloc(sizeof(int) * num_columns * 2);
+	for (i = 0; i < num_columns; i++)
+	{
+		char *val = piv_columns[i].sort_value;
+		/* ranking information is valid if non null and matches /^-?\d+$/ */
+		if (val && ((*val == '-' && strspn(val+1, "0123456789") == strlen(val+1) )
+					|| strspn(val, "0123456789") == strlen(val)))
+		{
+			hmap[i*2] = atoi(val);
+			hmap[i*2+1] = i;
+		}
+		else
+		{
+			/* invalid rank information ignored (equivalent to rank 0) */
+			hmap[i*2] = 0;
+			hmap[i*2+1] = i;
+		}
+	}
+
+	qsort(hmap, num_columns, sizeof(int)*2, rankCompare);
+
+	for (i=0; i < num_columns; i++)
+	{
+		piv_columns[hmap[i*2+1]].rank = i;
+	}
+
+	pg_free(hmap);
+}
+
+
+/*
+ * Output the pivoted resultset with the printTable* functions
+ */
+static void
+printCrosstab(const PGresult *results,
+			  int num_columns,
+			  struct pivot_field *piv_columns,
+			  int field_for_columns,
+			  int num_rows,
+			  struct pivot_field *piv_rows,
+			  int field_for_rows,
+			  int *colsG,
+			  int colsG_num)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int	i, j, rn;
+	char col_align = 'l';		/* alignment for values inside the grid */
+	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+								 *  in this function, when cells cannot simply point to
+								 *  PQgetvalue(results, ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+						column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+	 *  associating each piv_columns[].rank to its index in piv_columns.
+	 *  This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+	/*
+	 * In the common case of only one field projected into the cells, the
+	 * display alignment depends on its PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (colsG_num == 1)
+		col_align = column_type_alignment(PQftype(results, colsG[0]));
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		char *colname = piv_columns[horiz_map[i]].name ?
+			piv_columns[horiz_map[i]].name :
+			(popt.nullPrint ? popt.nullPrint : "");
+
+		printTableAddHeader(&cont,
+							colname,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int k = piv_rows[i].rank;
+		cont.cells[k*(num_columns+1)] = piv_rows[i].name ?
+			piv_rows[i].name :
+			(popt.nullPrint ? popt.nullPrint : "");
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k*(num_columns+1)+j+1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns+1);
+
+	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		int row_number;
+		int col_number;
+		struct pivot_field *p;
+
+		/* Find target row */
+		struct pivot_field elt;
+		if (!PQgetisnull(results, rn, field_for_rows))
+			elt.name = PQgetvalue(results, rn, field_for_rows);
+		else
+			elt.name = NULL;
+		p = (struct pivot_field*) bsearch(&elt,
+										  piv_rows,
+										  num_rows,
+										  sizeof(struct pivot_field),
+										  pivotFieldCompare);
+
+		row_number = p ? p->rank : -1;
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+			elt.name = PQgetvalue(results, rn, field_for_columns);
+		else
+			elt.name = NULL;
+
+		p = (struct pivot_field*) bsearch(&elt,
+										  piv_columns,
+										  num_columns,
+										  sizeof(struct pivot_field),
+										  pivotFieldCompare);
+		col_number = p? p->rank : -1;
+
+		/* Place value into cell */
+		if (col_number>=0 && row_number>=0)
+		{
+			int idx = 1 + col_number + row_number*(num_columns+1);
+			int src_col = 0;			/* column number in source result */
+
+			/*
+			 * special case: when the source has only 2 columns, use a
+			 * X (cross/checkmark) for the cell content, and set
+			 * src_col to a virtual additional column.
+			 */
+			if (PQnfields(results) == 2)
+				src_col = -1;
+
+			for (i=0; i<colsG_num || src_col==-1; i++)
+			{
+				char *content;
+
+				if (src_col == -1)
+				{
+					content = "X";
+				}
+				else
+				{
+					src_col = colsG[i];
+
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int content_size =
+						strlen(cont.cells[idx])
+						+ 2 			/* room for [CR],LF or space */
+						+ strlen(content)
+						+ 1;			/* '\0' */
+					char *new_content;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source results get
+					 * separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (i==0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+
+				/* special case of the "virtual column" for checkmark */
+				if (src_col == -1)
+					break;
+			}
+		}
+	}
+
+	printTable(&cont, pset.queryFout, false, pset.logfile);
+	printTableCleanup(&cont);
+
+
+	for (i=0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static bool
+fieldNameEquals(const char *arg, const char *fieldname)
+{
+	const char* p = arg;
+	const char* f = fieldname;
+	char c;
+
+	if (*p++ != '"')
+		return !pg_strcasecmp(arg, fieldname);
+
+	while ((c = *p++))
+	{
+		if (c == '"')
+		{
+			if (*p == '"')
+				p++;			/* skip second quote and continue */
+			else if (*p == '\0')
+				return (*f == '\0');	/* p is shorter than f, or is identical */
+		}
+		if (*f == '\0')
+			return false;			/* f is shorter than p */
+		if (c != *f)				/* found one byte that differs */
+			return false;
+		f++;
+	}
+	return (*f=='\0');
+}
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *  on success, the 0-based index of the column
+ *  or -1 if the column number or name is not found in the result's structure,
+ *        or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char *arg, PGresult *res)
+{
+	int idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0  || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int i;
+		idx = -1;
+		for (i=0; i < PQnfields(res); i++)
+		{
+			if (fieldNameEquals(arg, PQfname(res, i)))
+			{
+				if (idx>=0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+/*
+ * Parse col1[<sep>col2][<sep>col3]...
+ * where colN can be:
+ * - a number from 1 to PQnfields(res)
+ * - an unquoted column name matching (case insensitively) one of PQfname(res,...)
+ * - a quoted column name matching (case sensitively) one of PQfname(res,...)
+ * max_columns: 0 if no maximum
+ */
+static int
+parseColumnRefs(char* arg,
+				PGresult *res,
+				int **col_numbers,
+				int max_columns,
+				char separator)
+{
+	char *p = arg;
+	char c;
+	int col_num = -1;
+	int nb_cols = 0;
+	char* field_start = NULL;
+	*col_numbers = NULL;
+	while ((c = *p) != '\0')
+	{
+		bool quoted_field = false;
+		field_start = p;
+
+		/* first char */
+		if (c == '"')
+		{
+			quoted_field = true;
+			p++;
+		}
+
+		while ((c = *p) != '\0')
+		{
+			if (c == separator && !quoted_field)
+				break;
+			if (c == '"')		/* end of field or embedded double quote */
+			{
+				p++;
+				if (*p == '"')
+				{
+					if (quoted_field)
+					{
+						p++;
+						continue;
+					}
+				}
+				else if (quoted_field && *p == separator)
+					break;
+			}
+			p += PQmblen(p, pset.encoding);
+		}
+
+		if (p != field_start)
+		{
+			/* look up the column and add its index into *col_numbers */
+			if (max_columns != 0 && nb_cols == max_columns)
+			{
+				psql_error(_("No more than %d column references expected\n"), max_columns);
+				goto errfail;
+			}
+			c = *p;
+			*p = '\0';
+			col_num = indexOfColumn(field_start, res);
+			*p = c;
+			if (col_num < 0)
+				goto errfail;
+			*col_numbers = (int*)pg_realloc(*col_numbers, (1+nb_cols)*sizeof(int));
+			(*col_numbers)[nb_cols++] = col_num;
+		}
+		else
+		{
+			psql_error(_("Empty column reference\n"));
+			goto errfail;
+		}
+
+		if (*p)
+			p += PQmblen(p, pset.encoding);
+	}
+	return nb_cols;
+
+errfail:
+	pg_free(*col_numbers);
+	*col_numbers = NULL;
+	return -1;
+}
+
+
+/*
+ * Main function.
+ * Process the data from *res according the display options in pset (global),
+ * to generate the horizontal and vertical headers contents,
+ * then call printCrosstab() for the actual output.
+ */
+bool
+PrintResultsInCrossTab(PGresult* res)
+{
+	/* COLV or null */
+	char* opt_field_for_rows = pset.crosstabview_col_V;
+	/* COLH[:SCOLH] or null */
+	char* opt_field_for_columns = pset.crosstabview_col_H;
+	int		rn;
+	struct avl_tree	piv_columns;
+	struct avl_tree	piv_rows;
+	struct pivot_field* array_columns = NULL;
+	struct pivot_field* array_rows = NULL;
+	int		num_columns = 0;
+	int		num_rows = 0;
+	bool 	retval = false;
+	/*
+	 * column definitions involved in the vertical header, horizontal header,
+	 * and grid
+	 */
+	int		*colsV = NULL, *colsH = NULL, *colsG = NULL;
+	int		colsG_num;
+	int		nn;
+
+	/* 0-based index of the field whose distinct values will become COLUMN headers */
+	int		field_for_columns = -1;
+	int		sort_field_for_columns = -1;
+
+	/* 0-based index of the field whose distinct values will become ROW headers */
+	int		field_for_rows = -1;
+
+	avlInit(&piv_rows);
+	avlInit(&piv_columns);
+
+	if (res == NULL)
+	{
+		psql_error(_("No result\n"));
+		goto error_return;
+	}
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		psql_error(_("The query must return results to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the vertical header (1st arg)
+	 * displayed in the left-most column. Only a reference to a field
+	 * is accepted (no sort column).
+	 */
+
+	if (opt_field_for_rows == NULL)
+	{
+		field_for_rows = 0;
+	}
+	else
+	{
+		nn = parseColumnRefs(opt_field_for_rows, res, &colsV, 1, ':');
+		if (nn != 1)
+			goto error_return;
+		field_for_rows = colsV[0];
+	}
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	/*
+	 * Arguments processing for the horizontal header (2nd arg)
+	 * (pivoted column that gets displayed as the first row).
+	 * Determine:
+	 * - the sort direction if any
+	 * - the field number of that column in the PGresult
+	 * - the field number of the associated sort column if any
+	 */
+
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		nn = parseColumnRefs(opt_field_for_columns, res, &colsH, 2, ':');
+		if (nn <= 0)
+			goto error_return;
+		if (nn==1)
+			field_for_columns = colsH[0];
+		else
+		{
+			field_for_columns = colsH[0];
+			sort_field_for_columns = colsH[1];
+		}
+
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the columns aside from headers (3rd arg)
+	 * Determine the columns to display in the grid and their order.
+	 */
+	if (pset.crosstabview_cols_grid == NULL)
+	{
+		/*
+		 * By defaut, all the fields from PGresult get displayed into the grid,
+		 * except the two fields that go into the vertical and horizontal
+		 * headers.
+		 */
+		if (PQnfields(res) > 2)
+		{
+			int i, j=0;
+			colsG = (int*)pg_malloc(sizeof(int) * (PQnfields(res)-2));
+			for (i=0; i<PQnfields(res); i++)
+			{
+				if (i!=field_for_rows && i!=field_for_columns)
+					colsG[j++] = i;
+			}
+			colsG_num = PQnfields(res)-2;
+		}
+		else
+		{
+			colsG = NULL;
+			colsG_num = 0;
+		}
+	}
+	else
+	{
+		/*
+		 * Non-default case: a list of fields is given.
+		 * Parse that list to determine the fields to display into the grid,
+		 * and in what order.
+		 * The list format is colA[,colB[,colC...]]
+		 */
+		colsG_num = parseColumnRefs(pset.crosstabview_cols_grid,
+									res, &colsG, PQnfields(res), ',');
+		if (colsG_num <= 0)
+			goto error_return;
+	}
+
+	/*
+	 * First part: accumulate the names that go into the vertical and
+	 * horizontal headers, each into an AVL binary tree to build the set of
+	 * DISTINCT values.
+	 */
+
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		/* horizontal */
+		char* val = PQgetisnull(res, rn, field_for_columns) ? NULL:
+			PQgetvalue(res, rn, field_for_columns);
+
+		if (sort_field_for_columns >= 0)
+		{
+			char* val1 = PQgetisnull(res, rn, sort_field_for_columns) ? NULL:
+				PQgetvalue(res, rn, sort_field_for_columns);
+
+			avlMergeValue(&piv_columns, val, val1);
+		}
+		else
+		{
+			avlMergeValue(&piv_columns, val, NULL);
+		}
+
+		if (piv_columns.count > 1600)
+		{
+			psql_error(_("Maximum number of columns (1600) exceeded\n"));
+			goto error_return;
+		}
+
+		/* vertical */
+		val = PQgetisnull(res, rn, field_for_rows) ? NULL:
+			PQgetvalue(res, rn, field_for_rows);
+
+		avlMergeValue(&piv_rows, val, NULL);
+	}
+
+	/*
+	 * Second part: Generate sorted arrays from the AVL trees.
+	 */
+
+	num_columns = piv_columns.count;
+	num_rows = piv_rows.count;
+
+	array_columns = (struct pivot_field*)
+		pg_malloc(sizeof(struct pivot_field) * num_columns);
+
+	array_rows = (struct pivot_field*)
+		pg_malloc(sizeof(struct pivot_field) * num_rows);
+
+	avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0);
+	avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0);
+
+	/*
+	 * Third part: optionally, process the ranking data for the horizontal
+	 * header
+	 */
+	if (sort_field_for_columns >= 0)
+		rankSort(num_columns, array_columns);
+
+	/*
+	 * Fourth part: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns,
+				  array_columns,
+				  field_for_columns,
+				  num_rows,
+				  array_rows,
+				  field_for_rows,
+				  colsG,
+				  colsG_num);
+
+	retval = true;
+
+error_return:
+	avlFree(&piv_columns, piv_columns.root);
+	avlFree(&piv_rows, piv_rows.root);
+	pg_free(array_columns);
+	pg_free(array_rows);
+	pg_free(colsV);
+	pg_free(colsH);
+	pg_free(colsG);
+
+	return retval;
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..d374cfe
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,14 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+/* prototypes */
+extern bool	PrintResultsInCrossTab(PGresult *res);
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 59f6f25..f5411ac 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -175,6 +175,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [COLUMNS] execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
index 85dbd30..32360a6 100644
--- a/src/bin/psql/print.c
+++ b/src/bin/psql/print.c
@@ -3293,30 +3293,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3358,6 +3337,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
index 005ba15..d5d62b2 100644
--- a/src/bin/psql/print.h
+++ b/src/bin/psql/print.h
@@ -174,7 +174,7 @@ extern FILE *PageOutput(int lines, const printTableOpt *topt);
 extern void ClosePager(FILE *pagerpipe);
 
 extern void html_escaped_print(const char *in, FILE *fout);
-
+extern char column_type_alignment(Oid);
 extern void printTableInit(printTableContent *const content,
 			   const printTableOpt *opt, const char *title,
 			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 20a6470..9b7f7c4 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -90,6 +90,10 @@ typedef struct _psqlSettings
 
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
+	char		*crosstabview_cols_grid;  /* one-shot \crosstabview 3nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 6a81416..5f18a5d 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1273,7 +1273,8 @@ psql_completion(const char *text, int start, int end)
 
 	/* psql's backslash commands. */
 	static const char *const backslash_commands[] = {
-		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
+		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
+		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
#103Pavel Stehule
pavel.stehule@gmail.com
In reply to: Robert Haas (#101)
Re: [patch] Proposal for \crosstabview in psql

2016-03-11 14:49 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

On Thu, Feb 18, 2016 at 9:23 AM, Daniel Verite <daniel@manitou-mail.org>
wrote:

Dean Rasheed wrote:

If I want to sort the rows coming out of a query, my first thought
is always going to be to add/adjust the query's ORDER BY clause, not
use some weird +/- psql syntax.

About the vertical sort, I agree on all your points.
It's best to rely on ORDER BY for all the reasons mentioned,
as opposed to a separate sort in a second step.

But you're considering the case when a user is designing
or adapting a query for the purpose of crosstab
viewing. As mentioned in my previous reply (about the
methods to achieve horizontal sort), that scenario is not really
what motivates the feature in the first place.

If removing that sort option is required to move forward
with the patch because it's controversial, so be it,
but overall I don't see this as a benefit for the end user,
it's just an option.

Discussion on this patch seems to have died off. I'm probably not
going to win any popularity contests for saying this, but I think we
should reject this patch. I don't feel like this is really a psql
feature: it's a powerful data visualization tool which we're proposing
to jam into psql. I don't think that's psql's purpose. I also think
it's quite possible that there could be an unbounded number of
slightly different things that people want here, and if we take this
one and a couple more, the code for these individual features could
come to be larger than all of psql, even though probably 95% of psql
users would never use any of those.

crosstabview is really visualization tool. **But now, there are not any
other tool available from terminal.** So this can be significant help to
all people who would to use this functionality.

The psql has lot of features for 5% users. Currently it is famous not as
"bloated software" but like most comfortable sql console on the world. The
implementation of crosstabview is not complex and with last Daniel's
modification the complexity is less.

The crosstabview is not 100% equal to ANSI SQL PIVOT clause. The ANSI SQL
command is much more rigid (it is one stage statement with predefined
columns), so argument of duplicate implementation one things is not valid.
Probably we would not implement non ANSI SQL feature on server.

Regards

Pavel

Show quoted text

Now, that having been said, if other people want this feature to go in
and are willing to do the work to get it in, I've said my piece and
won't complain further. There are a couple of committers who have
taken positive interest in this thread, so that's good. However,
there are also a couple of committers who have expressed doubts
similar to mine, so that's not so good. But worse than either of
those things, there is no real agreement on what the overall design of
this feature should be. Everybody wants something a little different,
for different reasons. If we can't come to an agreement, more or less
immediately, on what to try to get into 9.6, then this can't go into
this release. Whether it should go into a future release is a
question we can leave for another time.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#104Jim Nasby
Jim.Nasby@BlueTreble.com
In reply to: Pavel Stehule (#103)
Re: [patch] Proposal for \crosstabview in psql

On 3/13/16 12:48 AM, Pavel Stehule wrote:

crosstabview is really visualization tool. **But now, there are not any
other tool available from terminal.** So this can be significant help to
all people who would to use this functionality.

Not just the terminal either. Offhand I'm not aware of *any* fairly
simple tool that provides crosstab. There's a bunch of
complicated/expensive BI tools that do, but unless you've gone through
the trouble of getting one of those setup you're currently pretty stuck.

Ultimately I'd really like some way to remove/reduce the restriction of
result set definitions needing to be determined at plan time. That would
open the door for server-side crosstab/pivot as well a a host of other
things (such as dynamically turning a hstore/json/xml field into a
recordset).
--
Jim Nasby, Data Architect, Blue Treble Consulting, Austin TX
Experts in Analytics, Data Architecture and PostgreSQL
Data in Trouble? Get it in Treble! http://BlueTreble.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#105Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Jim Nasby (#104)
Re: [patch] Proposal for \crosstabview in psql

Jim Nasby wrote:

On 3/13/16 12:48 AM, Pavel Stehule wrote:

crosstabview is really visualization tool. **But now, there are not any
other tool available from terminal.** So this can be significant help to
all people who would to use this functionality.

Not just the terminal either. Offhand I'm not aware of *any* fairly simple
tool that provides crosstab. There's a bunch of complicated/expensive BI
tools that do, but unless you've gone through the trouble of getting one of
those setup you're currently pretty stuck.

I'm definitely +1 for this feature in psql also.

Some years ago we had a discussion about splitting psql in two parts, a
bare-bones one which would help script-writing and another one with
fancy features; we decided to keep one tool to rule them all and made
the implicit decision that we would grow exotic, sophisticated features
into psql. ISTM that this patch is going in that direction.

Ultimately I'd really like some way to remove/reduce the restriction of
result set definitions needing to be determined at plan time. That would
open the door for server-side crosstab/pivot as well a a host of other
things (such as dynamically turning a hstore/json/xml field into a
recordset).

That seems so far down the road that I don't think it should block the
psql feature being proposed in this thread, but yes I would like that
one too.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#106Daniel Verite
daniel@manitou-mail.org
In reply to: Jim Nasby (#104)
Re: [patch] Proposal for \crosstabview in psql

Jim Nasby wrote:

Ultimately I'd really like some way to remove/reduce the restriction of
result set definitions needing to be determined at plan time. That would
open the door for server-side crosstab/pivot as well a a host of other
things (such as dynamically turning a hstore/json/xml field into a
recordset).

Ultimately I'd really like some way to remove/reduce the restriction of
result set definitions needing to be determined at plan time. That would
open the door for server-side crosstab/pivot as well a a host of other
things (such as dynamically turning a hstore/json/xml field into a
recordset).

That would go against a basic expectation of prepared statements, the
fact that queries can be parsed/prepared without any part of them
being executed.

For a dynamic pivot, but probably also for the other examples you
have in mind, the SQL engine wouldn't be able to determine the output
columns without executing a least a subselect to look inside some
table(s).

I suspect that the implications of this would be so far reaching and
problematic that it will just not happen.

It seems to me that a dynamic pivot will always consist of
two SQL queries that can never be combined into one,
unless using a workaround à la Oracle, which encapsulates the
entire dynamic resultset into an XML blob as output.
The problem here being that the client-side tools
that people routinely use are not equipped to process it anyway;
at least that's what I find by anecdotal evidence for instance in:
https://community.oracle.com/thread/2133154?tstart=0
or
http://stackoverflow.com/questions/19298424
or
https://community.oracle.com/thread/2388982?tstart=0

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#107Robert Haas
robertmhaas@gmail.com
In reply to: Daniel Verite (#102)
Re: [patch] Proposal for \crosstabview in psql

On Sat, Mar 12, 2016 at 10:34 AM, Daniel Verite <daniel@manitou-mail.org> wrote:

But worse than either of those things, there is no real
agreement on what the overall design of this feature
should be.

The part in the design that raised concerns upthread is
essentially how headers sorting is exposed to the user and
implemented.

As suggested in [1], I've made some drastic changes in the
attached patch to take the comments (from Dean R., Tom L.)
into account.
[ ... lengthy explanation ... ]
- also NULLs are no longer excluded from headers, per Peter E.
comment in [2].

Dean, Tom, Peter, what do you think of the new version?

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#108Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#107)
Re: [patch] Proposal for \crosstabview in psql

On Mon, Mar 14, 2016 at 2:55 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Sat, Mar 12, 2016 at 10:34 AM, Daniel Verite <daniel@manitou-mail.org> wrote:

But worse than either of those things, there is no real
agreement on what the overall design of this feature
should be.

The part in the design that raised concerns upthread is
essentially how headers sorting is exposed to the user and
implemented.

As suggested in [1], I've made some drastic changes in the
attached patch to take the comments (from Dean R., Tom L.)
into account.
[ ... lengthy explanation ... ]
- also NULLs are no longer excluded from headers, per Peter E.
comment in [2].

Dean, Tom, Peter, what do you think of the new version?

Is anyone up for re-reviewing this? If not, I think we're going to
have to reject this for lack of interest.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#109Pavel Stehule
pavel.stehule@gmail.com
In reply to: Robert Haas (#108)
Re: [patch] Proposal for \crosstabview in psql

2016-03-19 15:45 GMT+01:00 Robert Haas <robertmhaas@gmail.com>:

On Mon, Mar 14, 2016 at 2:55 PM, Robert Haas <robertmhaas@gmail.com>
wrote:

On Sat, Mar 12, 2016 at 10:34 AM, Daniel Verite <daniel@manitou-mail.org>

wrote:

But worse than either of those things, there is no real
agreement on what the overall design of this feature
should be.

The part in the design that raised concerns upthread is
essentially how headers sorting is exposed to the user and
implemented.

As suggested in [1], I've made some drastic changes in the
attached patch to take the comments (from Dean R., Tom L.)
into account.
[ ... lengthy explanation ... ]
- also NULLs are no longer excluded from headers, per Peter E.
comment in [2].

Dean, Tom, Peter, what do you think of the new version?

Is anyone up for re-reviewing this? If not, I think we're going to
have to reject this for lack of interest.

Can I do review?

Pavel

Show quoted text

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#110Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Pavel Stehule (#109)
Re: [patch] Proposal for \crosstabview in psql

Pavel Stehule wrote:

Can I do review?

Of course.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#111Pavel Stehule
pavel.stehule@gmail.com
In reply to: Alvaro Herrera (#110)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Hi

2016-03-19 16:31 GMT+01:00 Alvaro Herrera <alvherre@2ndquadrant.com>:

Pavel Stehule wrote:

Can I do review?

Of course.

I did review of last patch. I had to do small changes to run the code due
last Tom's changes in psql. Updated patch is attached.

The last changes in this patch are two:

1. Remove strange server side sorting
2. Cleaning/reducing interface

Other code is +/- without changes. There was lot of discussion in this
thread, I would not to repeat it.

I'll comment the changes:

@1 using server side sorting was really generic, but strange. Now, the
crosstabview works without it without any significant functionality
degradation.

@2 interface is minimalist - but good enough - I am thinking so it is good
start point. I was able to run my examples without problems. The previous
API was more comfortable - "+","-" symbols allows to specify order quickly,
but without a agreement we can live without this feature. Now, a order of
data is controlled fully by SQL. crosstabview does data visualization only.
I have not any objection to this last design. It is reduced to minimum, but
still it works well.

* All regress tests passed
* A code is well and well commented
* No new warnings or compilation issues
* Documentation is clean

I have two minor notes, can be fixed simply, if we accept this last design:

1. can be nice if documentation will contains one example
2. some regress tests

From my perspective, it is ready for commiter. Daniel solved the most big
issues.

Regards

Pavel

Show quoted text

--
Álvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

psql-crosstabview-v13.difftext/plain; charset=US-ASCII; name=psql-crosstabview-v13.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
new file mode 100644
index 8a85804..da0621b
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** testdb=&gt;
*** 990,995 ****
--- 990,1102 ----
        </varlistentry>
  
        <varlistentry>
+         <term><literal>\crosstabview [
+             <replaceable class="parameter">colV</replaceable>
+             <replaceable class="parameter">colH</replaceable>
+             [:<replaceable class="parameter">scolH</replaceable>]
+             [<replaceable class="parameter">colG1[,colG2...]</replaceable>]
+             ] </literal></term>
+         <listitem>
+         <para>
+         Execute the current query buffer (like <literal>\g</literal>) and shows
+         the results inside a crosstab grid.
+         The output column <replaceable class="parameter">colV</replaceable>
+         becomes a vertical header
+         and the output column <replaceable class="parameter">colH</replaceable>
+         becomes a horizontal header, optionally sorted by ranking data obtained
+         from <replaceable class="parameter">scolH</replaceable>.
+ 
+         <replaceable class="parameter">colG1[,colG2...]</replaceable>
+         is the list of output columns to project into the grid.
+         By default, all output columns of the query except 
+         <replaceable class="parameter">colV</replaceable> and
+         <replaceable class="parameter">colH</replaceable>
+         are included in this list.
+         </para>
+ 
+         <para>
+         All columns can be refered to by their position (starting at 1), or by
+         their name. Normal case folding and quoting rules apply on column
+         names. By default,
+         <replaceable class="parameter">colV</replaceable> corresponds to column 1
+         and <replaceable class="parameter">colH</replaceable> to column 2.
+         A query having only one output column cannot be viewed in crosstab, and
+         <replaceable class="parameter">colH</replaceable> must differ from
+         <replaceable class="parameter">colV</replaceable>.
+         </para>
+ 
+         <para>
+         The vertical header, displayed as the leftmost column,
+         contains the deduplicated values found in
+         column <replaceable class="parameter">colV</replaceable>, in the same
+         order as in the query results.
+         </para>
+         <para>
+         The horizontal header, displayed as the first row,
+         contains the deduplicated values found in
+         column <replaceable class="parameter">colH</replaceable>, in
+         the order of appearance in the query results.
+         If specified, the optional <replaceable class="parameter">scolH</replaceable>
+         argument refers to a column whose values should be integer numbers
+         by which <replaceable class="parameter">colH</replaceable> will be sorted
+         to be positioned in the horizontal header.
+         </para>
+ 
+         <para>
+         Inside the crosstab grid,
+         given a query output with <literal>N</literal> columns
+         (including <replaceable class="parameter">colV</replaceable> and
+         <replaceable class="parameter">colH</replaceable>),
+         for each distinct value <literal>x</literal> of
+         <replaceable class="parameter">colH</replaceable>
+         and each distinct value <literal>y</literal> of
+         <replaceable class="parameter">colV</replaceable>,
+         the contents of a cell located at the intersection
+         <literal>(x,y)</literal> is determined by these rules:
+         <itemizedlist>
+         <listitem>
+         <para>
+          if there is no corresponding row in the query results such that the
+          value for <replaceable class="parameter">colH</replaceable>
+          is <literal>x</literal> and the value
+          for <replaceable class="parameter">colV</replaceable>
+          is <literal>y</literal>, the cell is empty.
+         </para>
+         </listitem>
+ 
+         <listitem>
+         <para>
+          if there is exactly one row such that the value
+          for <replaceable class="parameter">colH</replaceable>
+          is <literal>x</literal> and the value
+          for <replaceable class="parameter">colV</replaceable>
+          is <literal>y</literal>, then the <literal>N-2</literal> other
+          columns or the columns listed in
+          <replaceable class="parameter">colG1[,colG2...]</replaceable>
+          are displayed in the cell, separated between each other by
+          a space character if needed.
+ 
+          If <literal>N=2</literal>, the letter <literal>X</literal> is displayed
+          in the cell as if a virtual third column contained that character.
+         </para>
+         </listitem>
+ 
+         <listitem>
+         <para>
+          if there are several corresponding rows, the behavior is identical to
+          the case of one row except that the values coming from different rows
+          are stacked vertically, the different source rows being separated by
+          newline characters inside the cell.
+         </para>
+         </listitem>
+ 
+         </itemizedlist>
+         </para>
+ 
+         </listitem>
+       </varlistentry>
+ 
+       <varlistentry>
          <term><literal>\d[S+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
  
          <listitem>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
new file mode 100644
index 5f4038e..78a844e
*** a/src/bin/psql/Makefile
--- b/src/bin/psql/Makefile
*************** override CPPFLAGS := -I. -I$(srcdir) -I$
*** 23,29 ****
  OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
  	startup.o prompt.o variables.o large_obj.o print.o describe.o \
  	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
! 	sql_help.o psqlscan.o psqlscanslash.o \
  	$(WIN32RES)
  
  
--- 23,29 ----
  OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
  	startup.o prompt.o variables.o large_obj.o print.o describe.o \
  	tab-complete.o mbprint.o dumputils.o keywords.o kwlookup.o \
! 	sql_help.o psqlscan.o psqlscanslash.o crosstabview.o \
  	$(WIN32RES)
  
  
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
new file mode 100644
index eef6e4b..0fc9378
*** a/src/bin/psql/command.c
--- b/src/bin/psql/command.c
***************
*** 39,44 ****
--- 39,45 ----
  
  #include "common.h"
  #include "copy.h"
+ #include "crosstabview.h"
  #include "describe.h"
  #include "help.h"
  #include "input.h"
*************** exec_command(const char *cmd,
*** 364,369 ****
--- 365,403 ----
  	else if (strcmp(cmd, "copyright") == 0)
  		print_copyright();
  
+ 	/* \crosstabview -- execute a query and display results in crosstab */
+ 	else if (strcmp(cmd, "crosstabview") == 0)
+ 	{
+ 		char	*opt1,
+ 			*opt2,
+ 			*opt3;
+ 
+ 		opt1 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 		opt2 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 		opt3 = psql_scan_slash_option(scan_state,
+ 									  OT_NORMAL, NULL, false);
+ 
+ 		if (opt1 && !opt2)
+ 		{
+ 			psql_error(_("\\%s: missing second argument\n"), cmd);
+ 			success = false;
+ 		}
+ 		else
+ 		{
+ 			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1): NULL;
+ 			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2): NULL;
+ 			pset.crosstabview_cols_grid = opt3 ? pg_strdup(opt3): NULL;
+ 			pset.crosstabview_output = true;
+ 			status = PSQL_CMD_SEND;
+ 		}
+ 
+ 		free(opt1);
+ 		free(opt2);
+ 		free(opt3);
+ 	}
+ 
  	/* \d* commands */
  	else if (cmd[0] == 'd')
  	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index 2b67a43..2f1b9e6
*** a/src/bin/psql/common.c
--- b/src/bin/psql/common.c
***************
*** 24,29 ****
--- 24,30 ----
  #include "command.h"
  #include "copy.h"
  #include "mbprint.h"
+ #include "crosstabview.h"
  
  
  static bool ExecQueryUsingCursor(const char *query, double *elapsed_msec);
*************** PrintQueryResults(PGresult *results)
*** 965,970 ****
--- 966,973 ----
  			/* store or print the data ... */
  			if (pset.gset_prefix)
  				success = StoreQueryTuple(results);
+ 			else if (pset.crosstabview_output)
+ 				success = PrintResultsInCrossTab(results);
  			else
  				success = PrintQueryTuples(results);
  			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
*************** sendquery_cleanup:
*** 1251,1256 ****
--- 1254,1276 ----
  		pset.gset_prefix = NULL;
  	}
  
+ 	/* reset \crosstabview settings */
+ 	pset.crosstabview_output = false;
+ 	if (pset.crosstabview_col_V)
+ 	{
+ 		free(pset.crosstabview_col_V);
+ 		pset.crosstabview_col_V = NULL;
+ 	}
+ 	if (pset.crosstabview_col_H)
+ 	{
+ 		free(pset.crosstabview_col_H);
+ 		pset.crosstabview_col_H = NULL;
+ 	}
+ 	if (pset.crosstabview_cols_grid)
+ 	{
+ 		free(pset.crosstabview_cols_grid);
+ 		pset.crosstabview_cols_grid = NULL;
+ 	}
  	return OK;
  }
  
*************** ExecQueryUsingCursor(const char *query,
*** 1413,1419 ****
  			is_pager = true;
  		}
  
! 		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
  
  		PQclear(results);
  
--- 1433,1457 ----
  			is_pager = true;
  		}
  
! 		if (pset.crosstabview_output)
! 		{
! 			if (ntuples < fetch_count)
! 				PrintResultsInCrossTab(results);
! 			else
! 			{
! 				/*
! 				  crosstabview is denied if the whole set of rows is not
! 				  guaranteed to be fetched in the first iteration, because
! 				  it's expected in memory as a single PGresult structure.
! 				*/
! 				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
! 					fetch_count);
! 				PQclear(results);
! 				break;
! 			}
! 		}
! 		else
! 			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
  
  		PQclear(results);
  
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index ...4edcbb8
*** a/src/bin/psql/crosstabview.c
--- b/src/bin/psql/crosstabview.c
***************
*** 0 ****
--- 1,940 ----
+ /*
+  * psql - the PostgreSQL interactive terminal
+  *
+  * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+  *
+  * src/bin/psql/crosstabview.c
+  */
+ 
+ #include "postgres_fe.h"
+ #include "common.h"
+ #include "crosstabview.h"
+ #include "pqexpbuffer.h"
+ #include "settings.h"
+ #include <string.h>
+ 
+ 
+ /*
+  * Value/position from the resultset that goes into the horizontal or vertical
+  * crosstabview header.
+  */
+ struct pivot_field
+ {
+ 	/*
+ 	 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
+ 	 * value becomes an entry in the vertical header (colV), or horizontal
+ 	 * header (colH).
+ 	 * A Null value is represented by a NULL pointer.
+ 	 */
+ 	char	   *name;
+ 
+ 	/*
+ 	 * When a sort is requested on an alternative column, this holds
+ 	 * PQgetvalue() for the sort column corresponding to <name>. If
+ 	 * <name> appear multiple times, it's the first value in the
+ 	 * order of the results that is kept.
+ 	 * A Null value is represented by a NULL pointer.
+ 	 */
+ 	char	   *sort_value;
+ 
+ 	/*
+ 	 * Rank of this value, starting at 0. Initially, it's the relative position
+ 	 * of the first appearance of <name> in the resultset.
+ 	 * For example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
+ 	 * When a sort column is specified, ranks get updated in a final pass to reflect
+ 	 * the desired order.
+ 	 */
+ 	int			rank;
+ };
+ 
+ /* Node in avl_tree */
+ struct avl_node
+ {
+ 	/* Node contents */
+ 	struct pivot_field field;
+ 
+ 	/*
+ 	 * Height of this node in the tree (number of nodes on the longest
+ 	 * path to a leaf).
+ 	 */
+ 	int			height;
+ 
+ 	/*
+ 	 * Child nodes. [0] points to left subtree, [1] to right subtree.
+ 	 * Never NULL, points to the empty node avl_tree.end when no left
+ 	 * or right value.
+ 	 */
+ 	struct avl_node *childs[2];
+ };
+ 
+ /*
+  * Control structure for the AVL tree (binary search tree kept
+  * balanced with the AVL algorithm)
+  */
+ struct avl_tree
+ {
+ 	int			count;			/* Total number of nodes */
+ 	struct avl_node *root;		/* root of the tree */
+ 	struct avl_node *end;		/* Immutable dereferenceable empty tree */
+ };
+ 
+ /*
+  * Value comparator for vertical and horizontal headers
+  * used for deduplication only.
+  * - null values are considered equal
+  * - non-null < null
+  * - non-null values are compared with strcmp()
+  */
+ static int
+ pivotFieldCompare(const void *a, const void *b)
+ {
+ 	struct pivot_field* pa = (struct pivot_field*) a;
+ 	struct pivot_field* pb = (struct pivot_field*) b;
+ 
+ 	/* test null values */
+ 	if (!pb->name)
+ 		return pa->name ? -1 : 0;
+ 	else if (!pa->name)
+ 		return 1;
+ 	/* non-null values */
+ 	return strcmp( ((struct pivot_field*)a)->name,
+ 				   ((struct pivot_field*)b)->name);
+ }
+ 
+ static int
+ rankCompare(const void* a, const void* b)
+ {
+ 	return *((int*)a) - *((int*)b);
+ }
+ 
+ /*
+  * The avl* functions below provide a minimalistic implementation of AVL binary
+  * trees, to efficiently collect the distinct values that will form the horizontal
+  * and vertical headers. It only supports adding new values, no removal or even
+  * search.
+  */
+ static void
+ avlInit(struct avl_tree *tree)
+ {
+ 	tree->end = (struct avl_node*) pg_malloc0(sizeof(struct avl_node));
+ 	tree->end->childs[0] = tree->end->childs[1] = tree->end;
+ 	tree->count = 0;
+ 	tree->root = tree->end;
+ }
+ 
+ /* Deallocate recursively an AVL tree, starting from node */
+ static void
+ avlFree(struct avl_tree* tree, struct avl_node* node)
+ {
+ 	if (node->childs[0] != tree->end)
+ 	{
+ 		avlFree(tree, node->childs[0]);
+ 		pg_free(node->childs[0]);
+ 	}
+ 	if (node->childs[1] != tree->end)
+ 	{
+ 		avlFree(tree, node->childs[1]);
+ 		pg_free(node->childs[1]);
+ 	}
+ 	if (node == tree->root) {
+ 		/* free the root separately as it's not child of anything */
+ 		if (node != tree->end)
+ 			pg_free(node);
+ 		/* free the tree->end struct only once and when all else is freed */
+ 		pg_free(tree->end);
+ 	}
+ }
+ 
+ /* Set the height to 1 plus the greatest of left and right heights */
+ static void
+ avlUpdateHeight(struct avl_node *n)
+ {
+ 	n->height = 1 + (n->childs[0]->height > n->childs[1]->height ?
+ 					 n->childs[0]->height:
+ 					 n->childs[1]->height);
+ }
+ 
+ /* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
+ static struct avl_node*
+ avlRotate(struct avl_node **current, int dir)
+ {
+ 	struct avl_node *before = *current;
+ 	struct avl_node *after = (*current)->childs[dir];
+ 
+ 	*current = after;
+ 	before->childs[dir] = after->childs[!dir];
+ 	avlUpdateHeight(before);
+ 	after->childs[!dir] = before;
+ 
+ 	return after;
+ }
+ 
+ static int
+ avlBalance(struct avl_node *n)
+ {
+ 	return n->childs[0]->height - n->childs[1]->height;
+ }
+ 
+ /*
+  * After an insertion, possibly rebalance the tree so that the left and right
+  * node heights don't differ by more than 1.
+  * May update *node.
+  */
+ static void
+ avlAdjustBalance(struct avl_tree *tree, struct avl_node **node)
+ {
+ 	struct avl_node *current = *node;
+ 	int b = avlBalance(current)/2;
+ 	if (b != 0)
+ 	{
+ 		int dir = (1 - b)/2;
+ 		if (avlBalance(current->childs[dir]) == -b)
+ 		  avlRotate(&current->childs[dir], !dir);
+ 		current = avlRotate(node, dir);
+ 	}
+ 	if (current != tree->end)
+ 	  avlUpdateHeight(current);
+ }
+ 
+ /*
+  * Insert a new value/field, starting from *node, reaching the
+  * correct position in the tree by recursion.
+  * Possibly rebalance the tree and possibly update *node.
+  * Do nothing if the value is already present in the tree.
+  */
+ static void
+ avlInsertNode(struct avl_tree* tree,
+ 			  struct avl_node **node,
+ 			  struct pivot_field field)
+ {
+ 	struct avl_node *current = *node;
+ 
+ 	if (current == tree->end)
+ 	{
+ 		struct avl_node * new_node = (struct avl_node*)
+ 			pg_malloc(sizeof(struct avl_node));
+ 		new_node->height = 1;
+ 		new_node->field = field;
+ 		new_node->childs[0] = new_node->childs[1] = tree->end;
+ 		tree->count++;
+ 		*node = new_node;
+ 	}
+ 	else
+ 	{
+ 		int cmp = pivotFieldCompare(&field, &current->field);
+ 		if (cmp != 0)
+ 		{
+ 			avlInsertNode(tree,
+ 						  cmp > 0 ? &current->childs[1] : &current->childs[0],
+ 						  field);
+ 			avlAdjustBalance(tree, node);
+ 		}
+ 	}
+ }
+ 
+ /* Insert the value into the AVL tree, if it does not preexist */
+ static void
+ avlMergeValue(struct avl_tree* tree, char* name, char* sort_value)
+ {
+ 	struct pivot_field field;
+ 	field.name = name;
+ 	field.rank = tree->count;
+ 	field.sort_value = sort_value;
+ 	avlInsertNode(tree, &tree->root, field);
+ }
+ 
+ /*
+  * Recursively extract node values into the names array, in sorted order with a
+  * left-to-right tree traversal.
+  * Return the next candidate offset to write into the names array.
+  * fields[] must be preallocated to hold tree->count entries
+  */
+ static int
+ avlCollectFields(struct avl_tree* tree,
+ 				 struct avl_node* node,
+ 				 struct pivot_field* fields,
+ 				 int idx)
+ {
+ 	if (node == tree->end)
+ 		return idx;
+ 	idx = avlCollectFields(tree, node->childs[0], fields, idx);
+ 	fields[idx] = node->field;
+ 	return avlCollectFields(tree, node->childs[1], fields,  idx+1);
+ }
+ 
+ 
+ static void
+ rankSort(int num_columns, struct pivot_field* piv_columns)
+ {
+ 	int* hmap; /* [[offset in piv_columns, rank], ...for every header entry] */
+ 	int i;
+ 
+ 	hmap = (int*) pg_malloc(sizeof(int) * num_columns * 2);
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		char *val = piv_columns[i].sort_value;
+ 		/* ranking information is valid if non null and matches /^-?\d+$/ */
+ 		if (val && ((*val == '-' && strspn(val+1, "0123456789") == strlen(val+1) )
+ 					|| strspn(val, "0123456789") == strlen(val)))
+ 		{
+ 			hmap[i*2] = atoi(val);
+ 			hmap[i*2+1] = i;
+ 		}
+ 		else
+ 		{
+ 			/* invalid rank information ignored (equivalent to rank 0) */
+ 			hmap[i*2] = 0;
+ 			hmap[i*2+1] = i;
+ 		}
+ 	}
+ 
+ 	qsort(hmap, num_columns, sizeof(int)*2, rankCompare);
+ 
+ 	for (i=0; i < num_columns; i++)
+ 	{
+ 		piv_columns[hmap[i*2+1]].rank = i;
+ 	}
+ 
+ 	pg_free(hmap);
+ }
+ 
+ 
+ /*
+  * Output the pivoted resultset with the printTable* functions
+  */
+ static void
+ printCrosstab(const PGresult *results,
+ 			  int num_columns,
+ 			  struct pivot_field *piv_columns,
+ 			  int field_for_columns,
+ 			  int num_rows,
+ 			  struct pivot_field *piv_rows,
+ 			  int field_for_rows,
+ 			  int *colsG,
+ 			  int colsG_num)
+ {
+ 	printQueryOpt popt = pset.popt;
+ 	printTableContent cont;
+ 	int	i, j, rn;
+ 	char col_align = 'l';		/* alignment for values inside the grid */
+ 	int* horiz_map;			 	/* map indices from sorted horizontal headers to piv_columns */
+ 	char** allocated_cells; 	/*  Pointers for cell contents that are allocated
+ 								 *  in this function, when cells cannot simply point to
+ 								 *  PQgetvalue(results, ...) */
+ 
+ 	printTableInit(&cont, &popt.topt, popt.title, num_columns+1, num_rows);
+ 
+ 	/* Step 1: set target column names (horizontal header) */
+ 
+ 	/* The name of the first column is kept unchanged by the pivoting */
+ 	printTableAddHeader(&cont,
+ 						PQfname(results, field_for_rows),
+ 						false,
+ 						column_type_alignment(PQftype(results, field_for_rows)));
+ 
+ 	/*
+ 	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse map
+ 	 *  associating each piv_columns[].rank to its index in piv_columns.
+ 	 *  This avoids an O(N^2) loop later
+ 	 */
+ 	horiz_map = (int*) pg_malloc(sizeof(int) * num_columns);
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		horiz_map[piv_columns[i].rank] = i;
+ 	}
+ 
+ 	/*
+ 	 * In the common case of only one field projected into the cells, the
+ 	 * display alignment depends on its PQftype(). Otherwise the contents are
+ 	 * made-up strings, so the alignment is 'l'
+ 	 */
+ 	if (colsG_num == 1)
+ 		col_align = column_type_alignment(PQftype(results, colsG[0]));
+ 	else
+ 		col_align = 'l';
+ 
+ 	for (i = 0; i < num_columns; i++)
+ 	{
+ 		char *colname = piv_columns[horiz_map[i]].name ?
+ 			piv_columns[horiz_map[i]].name :
+ 			(popt.nullPrint ? popt.nullPrint : "");
+ 
+ 		printTableAddHeader(&cont,
+ 							colname,
+ 							false,
+ 							col_align);
+ 	}
+ 	pg_free(horiz_map);
+ 
+ 	/* Step 2: set row names in the first output column (vertical header) */
+ 	for (i = 0; i < num_rows; i++)
+ 	{
+ 		int k = piv_rows[i].rank;
+ 		cont.cells[k*(num_columns+1)] = piv_rows[i].name ?
+ 			piv_rows[i].name :
+ 			(popt.nullPrint ? popt.nullPrint : "");
+ 		/* Initialize all cells inside the grid to an empty value */
+ 		for (j = 0; j < num_columns; j++)
+ 			cont.cells[k*(num_columns+1)+j+1] = "";
+ 	}
+ 	cont.cellsadded = num_rows * (num_columns+1);
+ 
+ 	allocated_cells = (char**) pg_malloc0(num_rows * num_columns * sizeof(char*));
+ 
+ 	/* Step 3: set all the cells "inside the grid" */
+ 	for (rn = 0; rn < PQntuples(results); rn++)
+ 	{
+ 		int row_number;
+ 		int col_number;
+ 		struct pivot_field *p;
+ 
+ 		/* Find target row */
+ 		struct pivot_field elt;
+ 		if (!PQgetisnull(results, rn, field_for_rows))
+ 			elt.name = PQgetvalue(results, rn, field_for_rows);
+ 		else
+ 			elt.name = NULL;
+ 		p = (struct pivot_field*) bsearch(&elt,
+ 										  piv_rows,
+ 										  num_rows,
+ 										  sizeof(struct pivot_field),
+ 										  pivotFieldCompare);
+ 
+ 		row_number = p ? p->rank : -1;
+ 
+ 		/* Find target column */
+ 		if (!PQgetisnull(results, rn, field_for_columns))
+ 			elt.name = PQgetvalue(results, rn, field_for_columns);
+ 		else
+ 			elt.name = NULL;
+ 
+ 		p = (struct pivot_field*) bsearch(&elt,
+ 										  piv_columns,
+ 										  num_columns,
+ 										  sizeof(struct pivot_field),
+ 										  pivotFieldCompare);
+ 		col_number = p? p->rank : -1;
+ 
+ 		/* Place value into cell */
+ 		if (col_number>=0 && row_number>=0)
+ 		{
+ 			int idx = 1 + col_number + row_number*(num_columns+1);
+ 			int src_col = 0;			/* column number in source result */
+ 
+ 			/*
+ 			 * special case: when the source has only 2 columns, use a
+ 			 * X (cross/checkmark) for the cell content, and set
+ 			 * src_col to a virtual additional column.
+ 			 */
+ 			if (PQnfields(results) == 2)
+ 				src_col = -1;
+ 
+ 			for (i=0; i<colsG_num || src_col==-1; i++)
+ 			{
+ 				char *content;
+ 
+ 				if (src_col == -1)
+ 				{
+ 					content = "X";
+ 				}
+ 				else
+ 				{
+ 					src_col = colsG[i];
+ 
+ 					content = (!PQgetisnull(results, rn, src_col)) ?
+ 						PQgetvalue(results, rn, src_col) :
+ 						(popt.nullPrint ? popt.nullPrint : "");
+ 				}
+ 
+ 				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+ 				{
+ 					/*
+ 					 * Multiple values for the same (row,col) are projected
+ 					 * into the same cell. When this happens, separate the
+ 					 * previous content of the cell from the new value by a
+ 					 * newline.
+ 					 */
+ 					int content_size =
+ 						strlen(cont.cells[idx])
+ 						+ 2 			/* room for [CR],LF or space */
+ 						+ strlen(content)
+ 						+ 1;			/* '\0' */
+ 					char *new_content;
+ 
+ 					/*
+ 					 * idx2 is an index into allocated_cells. It differs from
+ 					 * idx (index into cont.cells), because vertical and
+ 					 * horizontal headers are included in `cont.cells` but
+ 					 * excluded from allocated_cells.
+ 					 */
+ 					int idx2 = (row_number * num_columns) + col_number;
+ 
+ 					if (allocated_cells[idx2] != NULL)
+ 					{
+ 						new_content = pg_realloc(allocated_cells[idx2], content_size);
+ 					}
+ 					else
+ 					{
+ 						/*
+ 						 * At this point, cont.cells[idx] still contains a
+ 						 * PQgetvalue() pointer.  Just after, it will contain
+ 						 * a new pointer maintained in allocated_cells[], and
+ 						 * freed at the end of this function.
+ 						 */
+ 						new_content = pg_malloc(content_size);
+ 						strcpy(new_content, cont.cells[idx]);
+ 					}
+ 					cont.cells[idx] = new_content;
+ 					allocated_cells[idx2] = new_content;
+ 
+ 					/*
+ 					 * Contents that are on adjacent columns in the source results get
+ 					 * separated by one space in the target.
+ 					 * Contents that are on different rows in the source get
+ 					 * separated by newlines in the target.
+ 					 */
+ 					if (i==0)
+ 						strcat(new_content, "\n");
+ 					else
+ 						strcat(new_content, " ");
+ 					strcat(new_content, content);
+ 				}
+ 				else
+ 				{
+ 					cont.cells[idx] = content;
+ 				}
+ 
+ 				/* special case of the "virtual column" for checkmark */
+ 				if (src_col == -1)
+ 					break;
+ 			}
+ 		}
+ 	}
+ 
+ 	printTable(&cont, pset.queryFout, false, pset.logfile);
+ 	printTableCleanup(&cont);
+ 
+ 
+ 	for (i=0; i < num_rows * num_columns; i++)
+ 	{
+ 		if (allocated_cells[i] != NULL)
+ 			pg_free(allocated_cells[i]);
+ 	}
+ 
+ 	pg_free(allocated_cells);
+ }
+ 
+ 
+ /*
+  * Compare a user-supplied argument against a field name obtained by PQfname(),
+  * which is already case-folded.
+  * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+  * do a case-sensitive comparison with these rules:
+  * - double quotes enclosing 'arg' are filtered out
+  * - double quotes inside 'arg' are expected to be doubled
+  */
+ static bool
+ fieldNameEquals(const char *arg, const char *fieldname)
+ {
+ 	const char* p = arg;
+ 	const char* f = fieldname;
+ 	char c;
+ 
+ 	if (*p++ != '"')
+ 		return !pg_strcasecmp(arg, fieldname);
+ 
+ 	while ((c = *p++))
+ 	{
+ 		if (c == '"')
+ 		{
+ 			if (*p == '"')
+ 				p++;			/* skip second quote and continue */
+ 			else if (*p == '\0')
+ 				return (*f == '\0');	/* p is shorter than f, or is identical */
+ 		}
+ 		if (*f == '\0')
+ 			return false;			/* f is shorter than p */
+ 		if (c != *f)				/* found one byte that differs */
+ 			return false;
+ 		f++;
+ 	}
+ 	return (*f=='\0');
+ }
+ 
+ /*
+  * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+  * Returns:
+  *  on success, the 0-based index of the column
+  *  or -1 if the column number or name is not found in the result's structure,
+  *        or if it's ambiguous (arg corresponding to several columns)
+  */
+ static int
+ indexOfColumn(const char *arg, PGresult *res)
+ {
+ 	int idx;
+ 
+ 	if (strspn(arg, "0123456789") == strlen(arg))
+ 	{
+ 		/* if arg contains only digits, it's a column number */
+ 		idx = atoi(arg) - 1;
+ 		if (idx < 0  || idx >= PQnfields(res))
+ 		{
+ 			psql_error(_("Invalid column number: %s\n"), arg);
+ 			return -1;
+ 		}
+ 	}
+ 	else
+ 	{
+ 		int i;
+ 		idx = -1;
+ 		for (i=0; i < PQnfields(res); i++)
+ 		{
+ 			if (fieldNameEquals(arg, PQfname(res, i)))
+ 			{
+ 				if (idx>=0)
+ 				{
+ 					/* if another idx was already found for the same name */
+ 					psql_error(_("Ambiguous column name: %s\n"), arg);
+ 					return -1;
+ 				}
+ 				idx = i;
+ 			}
+ 		}
+ 		if (idx == -1)
+ 		{
+ 			psql_error(_("Invalid column name: %s\n"), arg);
+ 			return -1;
+ 		}
+ 	}
+ 	return idx;
+ }
+ 
+ /*
+  * Parse col1[<sep>col2][<sep>col3]...
+  * where colN can be:
+  * - a number from 1 to PQnfields(res)
+  * - an unquoted column name matching (case insensitively) one of PQfname(res,...)
+  * - a quoted column name matching (case sensitively) one of PQfname(res,...)
+  * max_columns: 0 if no maximum
+  */
+ static int
+ parseColumnRefs(char* arg,
+ 				PGresult *res,
+ 				int **col_numbers,
+ 				int max_columns,
+ 				char separator)
+ {
+ 	char *p = arg;
+ 	char c;
+ 	int col_num = -1;
+ 	int nb_cols = 0;
+ 	char* field_start = NULL;
+ 	*col_numbers = NULL;
+ 	while ((c = *p) != '\0')
+ 	{
+ 		bool quoted_field = false;
+ 		field_start = p;
+ 
+ 		/* first char */
+ 		if (c == '"')
+ 		{
+ 			quoted_field = true;
+ 			p++;
+ 		}
+ 
+ 		while ((c = *p) != '\0')
+ 		{
+ 			if (c == separator && !quoted_field)
+ 				break;
+ 			if (c == '"')		/* end of field or embedded double quote */
+ 			{
+ 				p++;
+ 				if (*p == '"')
+ 				{
+ 					if (quoted_field)
+ 					{
+ 						p++;
+ 						continue;
+ 					}
+ 				}
+ 				else if (quoted_field && *p == separator)
+ 					break;
+ 			}
+ 			p += PQmblen(p, pset.encoding);
+ 		}
+ 
+ 		if (p != field_start)
+ 		{
+ 			/* look up the column and add its index into *col_numbers */
+ 			if (max_columns != 0 && nb_cols == max_columns)
+ 			{
+ 				psql_error(_("No more than %d column references expected\n"), max_columns);
+ 				goto errfail;
+ 			}
+ 			c = *p;
+ 			*p = '\0';
+ 			col_num = indexOfColumn(field_start, res);
+ 			*p = c;
+ 			if (col_num < 0)
+ 				goto errfail;
+ 			*col_numbers = (int*)pg_realloc(*col_numbers, (1+nb_cols)*sizeof(int));
+ 			(*col_numbers)[nb_cols++] = col_num;
+ 		}
+ 		else
+ 		{
+ 			psql_error(_("Empty column reference\n"));
+ 			goto errfail;
+ 		}
+ 
+ 		if (*p)
+ 			p += PQmblen(p, pset.encoding);
+ 	}
+ 	return nb_cols;
+ 
+ errfail:
+ 	pg_free(*col_numbers);
+ 	*col_numbers = NULL;
+ 	return -1;
+ }
+ 
+ 
+ /*
+  * Main function.
+  * Process the data from *res according the display options in pset (global),
+  * to generate the horizontal and vertical headers contents,
+  * then call printCrosstab() for the actual output.
+  */
+ bool
+ PrintResultsInCrossTab(PGresult* res)
+ {
+ 	/* COLV or null */
+ 	char* opt_field_for_rows = pset.crosstabview_col_V;
+ 	/* COLH[:SCOLH] or null */
+ 	char* opt_field_for_columns = pset.crosstabview_col_H;
+ 	int		rn;
+ 	struct avl_tree	piv_columns;
+ 	struct avl_tree	piv_rows;
+ 	struct pivot_field* array_columns = NULL;
+ 	struct pivot_field* array_rows = NULL;
+ 	int		num_columns = 0;
+ 	int		num_rows = 0;
+ 	bool 	retval = false;
+ 	/*
+ 	 * column definitions involved in the vertical header, horizontal header,
+ 	 * and grid
+ 	 */
+ 	int		*colsV = NULL, *colsH = NULL, *colsG = NULL;
+ 	int		colsG_num;
+ 	int		nn;
+ 
+ 	/* 0-based index of the field whose distinct values will become COLUMN headers */
+ 	int		field_for_columns = -1;
+ 	int		sort_field_for_columns = -1;
+ 
+ 	/* 0-based index of the field whose distinct values will become ROW headers */
+ 	int		field_for_rows = -1;
+ 
+ 	avlInit(&piv_rows);
+ 	avlInit(&piv_columns);
+ 
+ 	if (res == NULL)
+ 	{
+ 		psql_error(_("No result\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+ 	{
+ 		psql_error(_("The query must return results to be shown in crosstab\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	if (PQnfields(res) < 2)
+ 	{
+ 		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	/*
+ 	 * Arguments processing for the vertical header (1st arg)
+ 	 * displayed in the left-most column. Only a reference to a field
+ 	 * is accepted (no sort column).
+ 	 */
+ 
+ 	if (opt_field_for_rows == NULL)
+ 	{
+ 		field_for_rows = 0;
+ 	}
+ 	else
+ 	{
+ 		nn = parseColumnRefs(opt_field_for_rows, res, &colsV, 1, ':');
+ 		if (nn != 1)
+ 			goto error_return;
+ 		field_for_rows = colsV[0];
+ 	}
+ 
+ 	if (field_for_rows < 0)
+ 		goto error_return;
+ 
+ 	/*
+ 	 * Arguments processing for the horizontal header (2nd arg)
+ 	 * (pivoted column that gets displayed as the first row).
+ 	 * Determine:
+ 	 * - the sort direction if any
+ 	 * - the field number of that column in the PGresult
+ 	 * - the field number of the associated sort column if any
+ 	 */
+ 
+ 	if (opt_field_for_columns == NULL)
+ 		field_for_columns = 1;
+ 	else
+ 	{
+ 		nn = parseColumnRefs(opt_field_for_columns, res, &colsH, 2, ':');
+ 		if (nn <= 0)
+ 			goto error_return;
+ 		if (nn==1)
+ 			field_for_columns = colsH[0];
+ 		else
+ 		{
+ 			field_for_columns = colsH[0];
+ 			sort_field_for_columns = colsH[1];
+ 		}
+ 
+ 		if (field_for_columns < 0)
+ 			goto error_return;
+ 	}
+ 
+ 	if (field_for_columns == field_for_rows)
+ 	{
+ 		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+ 		goto error_return;
+ 	}
+ 
+ 	/*
+ 	 * Arguments processing for the columns aside from headers (3rd arg)
+ 	 * Determine the columns to display in the grid and their order.
+ 	 */
+ 	if (pset.crosstabview_cols_grid == NULL)
+ 	{
+ 		/*
+ 		 * By defaut, all the fields from PGresult get displayed into the grid,
+ 		 * except the two fields that go into the vertical and horizontal
+ 		 * headers.
+ 		 */
+ 		if (PQnfields(res) > 2)
+ 		{
+ 			int i, j=0;
+ 			colsG = (int*)pg_malloc(sizeof(int) * (PQnfields(res)-2));
+ 			for (i=0; i<PQnfields(res); i++)
+ 			{
+ 				if (i!=field_for_rows && i!=field_for_columns)
+ 					colsG[j++] = i;
+ 			}
+ 			colsG_num = PQnfields(res)-2;
+ 		}
+ 		else
+ 		{
+ 			colsG = NULL;
+ 			colsG_num = 0;
+ 		}
+ 	}
+ 	else
+ 	{
+ 		/*
+ 		 * Non-default case: a list of fields is given.
+ 		 * Parse that list to determine the fields to display into the grid,
+ 		 * and in what order.
+ 		 * The list format is colA[,colB[,colC...]]
+ 		 */
+ 		colsG_num = parseColumnRefs(pset.crosstabview_cols_grid,
+ 									res, &colsG, PQnfields(res), ',');
+ 		if (colsG_num <= 0)
+ 			goto error_return;
+ 	}
+ 
+ 	/*
+ 	 * First part: accumulate the names that go into the vertical and
+ 	 * horizontal headers, each into an AVL binary tree to build the set of
+ 	 * DISTINCT values.
+ 	 */
+ 
+ 	for (rn = 0; rn < PQntuples(res); rn++)
+ 	{
+ 		/* horizontal */
+ 		char* val = PQgetisnull(res, rn, field_for_columns) ? NULL:
+ 			PQgetvalue(res, rn, field_for_columns);
+ 
+ 		if (sort_field_for_columns >= 0)
+ 		{
+ 			char* val1 = PQgetisnull(res, rn, sort_field_for_columns) ? NULL:
+ 				PQgetvalue(res, rn, sort_field_for_columns);
+ 
+ 			avlMergeValue(&piv_columns, val, val1);
+ 		}
+ 		else
+ 		{
+ 			avlMergeValue(&piv_columns, val, NULL);
+ 		}
+ 
+ 		if (piv_columns.count > 1600)
+ 		{
+ 			psql_error(_("Maximum number of columns (1600) exceeded\n"));
+ 			goto error_return;
+ 		}
+ 
+ 		/* vertical */
+ 		val = PQgetisnull(res, rn, field_for_rows) ? NULL:
+ 			PQgetvalue(res, rn, field_for_rows);
+ 
+ 		avlMergeValue(&piv_rows, val, NULL);
+ 	}
+ 
+ 	/*
+ 	 * Second part: Generate sorted arrays from the AVL trees.
+ 	 */
+ 
+ 	num_columns = piv_columns.count;
+ 	num_rows = piv_rows.count;
+ 
+ 	array_columns = (struct pivot_field*)
+ 		pg_malloc(sizeof(struct pivot_field) * num_columns);
+ 
+ 	array_rows = (struct pivot_field*)
+ 		pg_malloc(sizeof(struct pivot_field) * num_rows);
+ 
+ 	avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0);
+ 	avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0);
+ 
+ 	/*
+ 	 * Third part: optionally, process the ranking data for the horizontal
+ 	 * header
+ 	 */
+ 	if (sort_field_for_columns >= 0)
+ 		rankSort(num_columns, array_columns);
+ 
+ 	/*
+ 	 * Fourth part: print the crosstab'ed results.
+ 	 */
+ 	printCrosstab(res,
+ 				  num_columns,
+ 				  array_columns,
+ 				  field_for_columns,
+ 				  num_rows,
+ 				  array_rows,
+ 				  field_for_rows,
+ 				  colsG,
+ 				  colsG_num);
+ 
+ 	retval = true;
+ 
+ error_return:
+ 	avlFree(&piv_columns, piv_columns.root);
+ 	avlFree(&piv_rows, piv_rows.root);
+ 	pg_free(array_columns);
+ 	pg_free(array_rows);
+ 	pg_free(colsV);
+ 	pg_free(colsH);
+ 	pg_free(colsG);
+ 
+ 	return retval;
+ }
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index ...d374cfe
*** a/src/bin/psql/crosstabview.h
--- b/src/bin/psql/crosstabview.h
***************
*** 0 ****
--- 1,14 ----
+ /*
+  * psql - the PostgreSQL interactive terminal
+  *
+  * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+  *
+  * src/bin/psql/crosstabview.h
+  */
+ 
+ #ifndef CROSSTABVIEW_H
+ #define CROSSTABVIEW_H
+ 
+ /* prototypes */
+ extern bool	PrintResultsInCrossTab(PGresult *res);
+ #endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
new file mode 100644
index 59f6f25..f5411ac
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
*************** slashUsage(unsigned short int pager)
*** 175,180 ****
--- 175,181 ----
  	fprintf(output, _("  \\g [FILE] or ;         execute query (and send results to file or |pipe)\n"));
  	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
  	fprintf(output, _("  \\q                     quit psql\n"));
+ 	fprintf(output, _("  \\crosstabview [COLUMNS] execute query and display results in crosstab\n"));
  	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
  	fprintf(output, "\n");
  
diff --git a/src/bin/psql/print.c b/src/bin/psql/print.c
new file mode 100644
index f25a66e..508f664
*** a/src/bin/psql/print.c
--- b/src/bin/psql/print.c
*************** printQuery(const PGresult *result, const
*** 3293,3322 ****
  
  	for (i = 0; i < cont.ncolumns; i++)
  	{
- 		char		align;
- 		Oid			ftype = PQftype(result, i);
- 
- 		switch (ftype)
- 		{
- 			case INT2OID:
- 			case INT4OID:
- 			case INT8OID:
- 			case FLOAT4OID:
- 			case FLOAT8OID:
- 			case NUMERICOID:
- 			case OIDOID:
- 			case XIDOID:
- 			case CIDOID:
- 			case CASHOID:
- 				align = 'r';
- 				break;
- 			default:
- 				align = 'l';
- 				break;
- 		}
- 
  		printTableAddHeader(&cont, PQfname(result, i),
! 							opt->translate_header, align);
  	}
  
  	/* set cells */
--- 3293,3301 ----
  
  	for (i = 0; i < cont.ncolumns; i++)
  	{
  		printTableAddHeader(&cont, PQfname(result, i),
! 							opt->translate_header,
! 							column_type_alignment(PQftype(result, i)));
  	}
  
  	/* set cells */
*************** printQuery(const PGresult *result, const
*** 3358,3363 ****
--- 3337,3367 ----
  	printTableCleanup(&cont);
  }
  
+ char
+ column_type_alignment(Oid ftype)
+ {
+ 	char		align;
+ 
+ 	switch (ftype)
+ 	{
+ 		case INT2OID:
+ 		case INT4OID:
+ 		case INT8OID:
+ 		case FLOAT4OID:
+ 		case FLOAT8OID:
+ 		case NUMERICOID:
+ 		case OIDOID:
+ 		case XIDOID:
+ 		case CIDOID:
+ 		case CASHOID:
+ 			align = 'r';
+ 			break;
+ 		default:
+ 			align = 'l';
+ 			break;
+ 	}
+ 	return align;
+ }
  
  void
  setDecimalLocale(void)
diff --git a/src/bin/psql/print.h b/src/bin/psql/print.h
new file mode 100644
index 9033c4b..4b8342d
*** a/src/bin/psql/print.h
--- b/src/bin/psql/print.h
*************** extern FILE *PageOutput(int lines, const
*** 174,180 ****
  extern void ClosePager(FILE *pagerpipe);
  
  extern void html_escaped_print(const char *in, FILE *fout);
! 
  extern void printTableInit(printTableContent *const content,
  			   const printTableOpt *opt, const char *title,
  			   const int ncolumns, const int nrows);
--- 174,180 ----
  extern void ClosePager(FILE *pagerpipe);
  
  extern void html_escaped_print(const char *in, FILE *fout);
! extern char column_type_alignment(Oid);
  extern void printTableInit(printTableContent *const content,
  			   const printTableOpt *opt, const char *title,
  			   const int ncolumns, const int nrows);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
new file mode 100644
index 20a6470..9b7f7c4
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
*************** typedef struct _psqlSettings
*** 90,95 ****
--- 90,99 ----
  
  	char	   *gfname;			/* one-shot file output argument for \g */
  	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
+ 	bool		crosstabview_output;  /* one-shot request to print results in crosstab */
+ 	char		*crosstabview_col_V;  /* one-shot \crosstabview 1st argument */
+ 	char		*crosstabview_col_H;  /* one-shot \crosstabview 2nd argument */
+ 	char		*crosstabview_cols_grid;  /* one-shot \crosstabview 3nd argument */
  
  	bool		notty;			/* stdin or stdout is not a tty (as determined
  								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
new file mode 100644
index 6a81416..5f18a5d
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
*************** psql_completion(const char *text, int st
*** 1273,1279 ****
  
  	/* psql's backslash commands. */
  	static const char *const backslash_commands[] = {
! 		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
  		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
  		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
  		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
--- 1273,1280 ----
  
  	/* psql's backslash commands. */
  	static const char *const backslash_commands[] = {
! 		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
! 		"\\copyright", "\\crosstabview",
  		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
  		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
  		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
#112Robert Haas
robertmhaas@gmail.com
In reply to: Pavel Stehule (#111)
Re: [patch] Proposal for \crosstabview in psql

On Sun, Mar 20, 2016 at 5:27 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:

From my perspective, it is ready for commiter. Daniel solved the most big
issues.

OK, so that brings us back to: is there any committer who likes this
enough to want to look at committing it? My view hasn't changed much
since /messages/by-id/CA+TgmoZ4yAduq9j8XTGRBh868JH2nj_NW_qgkXB32CeDsVTy0w@mail.gmail.com

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#113Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Robert Haas (#112)
Re: [patch] Proposal for \crosstabview in psql

Robert Haas wrote:

On Sun, Mar 20, 2016 at 5:27 PM, Pavel Stehule <pavel.stehule@gmail.com> wrote:

From my perspective, it is ready for commiter. Daniel solved the most big
issues.

OK, so that brings us back to: is there any committer who likes this
enough to want to look at committing it? My view hasn't changed much
since /messages/by-id/CA+TgmoZ4yAduq9j8XTGRBh868JH2nj_NW_qgkXB32CeDsVTy0w@mail.gmail.com

I volunteer for that, but it'll be a few days so if anyone else is
interested, feel free.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#114Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Pavel Stehule (#111)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

I've been looking at this patch. First thing was to rebase on top of
recent psql code restructuring; second, pgindent; third, reordered the
code in crosstabview.c more sensibly (had to add prototypes). New
version attached.

Then I looked at the docs to try to figure out exactly how it works.
I'm surprised that there's not a single example added to the psql
manpage. Please add one.

I then tested it a bit, "kick the tires" so to speak. I noticed that
error handling is broken. For instance, observe the query prompt after
the error:

regression=# select * from pg_class \crosstabview relnatts
\crosstabview: missing second argument
regression-#

At this point the query buffer contains the query (you can see it with
\e), which seems bogus to me. The query buffer needs to be reset.
Compare \gexec:
alvherre=# select 1 \gexec
ERROR: error de sintaxis en o cerca de �1�
L�NEA 1: 1
^
alvherre=#

Also, using bogus column names as arguments cause state to get all
bogus:

alvherre=# select * from pg_class \crosstabview relnatts relkinda
Invalid column name: relkinda
alvherre=# select 1;
The query must return at least two columns to be shown in crosstab

Note that the second query is not crosstab at all, yet the error message
is entirely bogus. This one is probably the same bug:

alvherre=# select 'one', 'two';
Invalid column name: relnatts

Apparently, once in that state, not even a successful query crosstab
display resets the state correctly:

alvherre=# select * from pg_class \crosstabview relnatts relkinda
Invalid column name: relkinda
alvherre=# select 'one' as relnatts, 'two' as relkinda \crosstabview
relnatts | two
----------+-----
one | X
(1 fila)

alvherre=# select 1;
The query must return at least two columns to be shown in crosstab

Please fix this.

Some additional items:

* A few examples in docs. The psql manpage should have at least two new
examples showing the crosstab features, one with the simplest case you
can think of, and another one showing all the features.

* Add regression test cases somewhere for the regression database.
Probably use "FROM tenk1 WHERE hundred < 5", which provides you with 500
rows, enough for many interesting games. Make sure to test all the
provided features. I would use a new psql.sql file for this.

* How did you come up with the 1600 value? Whatever it is, please use a
#define instead of hardcoding it.

* In the "if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')"
block (line 497 in the attached), can't we do the same thing by using
psprintf?

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

psql-crosstabview-v14.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index d8b9a03..536141c 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -990,6 +990,113 @@ testdb=&gt;
       </varlistentry>
 
       <varlistentry>
+        <term><literal>\crosstabview [
+            <replaceable class="parameter">colV</replaceable>
+            <replaceable class="parameter">colH</replaceable>
+            [:<replaceable class="parameter">scolH</replaceable>]
+            [<replaceable class="parameter">colG1[,colG2...]</replaceable>]
+            ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows
+        the results inside a crosstab grid.
+        The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header
+        and the output column <replaceable class="parameter">colH</replaceable>
+        becomes a horizontal header, optionally sorted by ranking data obtained
+        from <replaceable class="parameter">scolH</replaceable>.
+
+        <replaceable class="parameter">colG1[,colG2...]</replaceable>
+        is the list of output columns to project into the grid.
+        By default, all output columns of the query except 
+        <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>
+        are included in this list.
+        </para>
+
+        <para>
+        All columns can be refered to by their position (starting at 1), or by
+        their name. Normal case folding and quoting rules apply on column
+        names. By default,
+        <replaceable class="parameter">colV</replaceable> corresponds to column 1
+        and <replaceable class="parameter">colH</replaceable> to column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the deduplicated values found in
+        column <replaceable class="parameter">colV</replaceable>, in the same
+        order as in the query results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the deduplicated values found in
+        column <replaceable class="parameter">colH</replaceable>, in
+        the order of appearance in the query results.
+        If specified, the optional <replaceable class="parameter">scolH</replaceable>
+        argument refers to a column whose values should be integer numbers
+        by which <replaceable class="parameter">colH</replaceable> will be sorted
+        to be positioned in the horizontal header.
+        </para>
+
+        <para>
+        Inside the crosstab grid,
+        given a query output with <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        the contents of a cell located at the intersection
+        <literal>(x,y)</literal> is determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the query results such that the
+         value for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns or the columns listed in
+         <replaceable class="parameter">colG1[,colG2...]</replaceable>
+         are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed
+         in the cell as if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to
+         the case of one row except that the values coming from different rows
+         are stacked vertically, the different source rows being separated by
+         newline characters inside the cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term><literal>\d[S+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
 
         <listitem>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index d1c3b77..1f6a289 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ LDFLAGS += -L$(top_builddir)/src/fe_utils -lpgfeutils -lpq
 
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o describe.o \
-	tab-complete.o \
+	crosstabview.o tab-complete.o \
 	sql_help.o psqlscanslash.o \
 	$(WIN32RES)
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 1d326a8..b49a0c1 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -39,6 +39,7 @@
 
 #include "common.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "describe.h"
 #include "help.h"
 #include "input.h"
@@ -364,6 +365,39 @@ exec_command(const char *cmd,
 	else if (strcmp(cmd, "copyright") == 0)
 		print_copyright();
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		char	   *opt1,
+				   *opt2,
+				   *opt3;
+
+		opt1 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt2 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		opt3 = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		if (opt1 && !opt2)
+		{
+			psql_error(_("\\%s: missing second argument\n"), cmd);
+			success = false;
+		}
+		else
+		{
+			pset.crosstabview_col_V = opt1 ? pg_strdup(opt1) : NULL;
+			pset.crosstabview_col_H = opt2 ? pg_strdup(opt2) : NULL;
+			pset.crosstabview_cols_grid = opt3 ? pg_strdup(opt3) : NULL;
+			pset.crosstabview_output = true;
+			status = PSQL_CMD_SEND;
+		}
+
+		free(opt1);
+		free(opt2);
+		free(opt3);
+	}
+
 	/* \d* commands */
 	else if (cmd[0] == 'd')
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index df3441c..3fbdb08 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -23,6 +23,7 @@
 #include "settings.h"
 #include "command.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "fe_utils/mbprint.h"
 
 
@@ -1064,6 +1065,8 @@ PrintQueryResults(PGresult *results)
 				success = StoreQueryTuple(results);
 			else if (pset.gexec_flag)
 				success = ExecQueryTuples(results);
+			else if (pset.crosstabview_output)
+				success = PrintResultsInCrossTab(results);
 			else
 				success = PrintQueryTuples(results);
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
@@ -1520,7 +1523,25 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 			is_pager = true;
 		}
 
-		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
+		if (pset.crosstabview_output)
+		{
+			if (ntuples < fetch_count)
+				PrintResultsInCrossTab(results);
+			else
+			{
+				/*
+				 * crosstabview is denied if the whole set of rows is not
+				 * guaranteed to be fetched in the first iteration, because
+				 * it's expected in memory as a single PGresult structure.
+				 */
+				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
+						   fetch_count);
+				PQclear(results);
+				break;
+			}
+		}
+		else
+			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
 
 		ClearOrSaveResult(results);
 
@@ -1599,6 +1620,23 @@ cleanup:
 		*elapsed_msec += INSTR_TIME_GET_MILLISEC(after);
 	}
 
+	/* reset \crosstabview settings */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
+	if (pset.crosstabview_cols_grid)
+	{
+		free(pset.crosstabview_cols_grid);
+		pset.crosstabview_cols_grid = NULL;
+	}
 	return OK;
 }
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..0bd1223
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,971 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+#include "postgres_fe.h"
+
+#include <string.h>
+
+#include "common.h"
+#include "crosstabview.h"
+#include "pqexpbuffer.h"
+#include "settings.h"
+
+
+/*
+ * Value/position from the resultset that goes into the horizontal or vertical
+ * crosstabview header.
+ */
+typedef struct _pivot_field
+{
+	/*
+	 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
+	 * value becomes an entry in the vertical header (colV), or horizontal
+	 * header (colH). A Null value is represented by a NULL pointer.
+	 */
+	char	   *name;
+
+	/*
+	 * When a sort is requested on an alternative column, this holds
+	 * PQgetvalue() for the sort column corresponding to <name>. If <name>
+	 * appear multiple times, it's the first value in the order of the results
+	 * that is kept. A Null value is represented by a NULL pointer.
+	 */
+	char	   *sort_value;
+
+	/*
+	 * Rank of this value, starting at 0. Initially, it's the relative
+	 * position of the first appearance of <name> in the resultset. For
+	 * example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
+	 * When a sort column is specified, ranks get updated in a final pass to
+	 * reflect the desired order.
+	 */
+	int			rank;
+} pivot_field;
+
+/* Node in avl_tree */
+typedef struct _avl_node
+{
+	/* Node contents */
+	pivot_field field;
+
+	/*
+	 * Height of this node in the tree (number of nodes on the longest path to
+	 * a leaf).
+	 */
+	int			height;
+
+	/*
+	 * Child nodes. [0] points to left subtree, [1] to right subtree. Never
+	 * NULL, points to the empty node avl_tree.end when no left or right
+	 * value.
+	 */
+	struct _avl_node *childs[2];
+} avl_node;
+
+/*
+ * Control structure for the AVL tree (binary search tree kept
+ * balanced with the AVL algorithm)
+ */
+typedef struct _avl_tree
+{
+	int			count;			/* Total number of nodes */
+	avl_node   *root;			/* root of the tree */
+	avl_node   *end;			/* Immutable dereferenceable empty tree */
+} avl_tree;
+
+
+static void printCrosstab(const PGresult *results, int num_columns,
+			  pivot_field *piv_columns, int field_for_columns, int num_rows,
+			  pivot_field *piv_rows, int field_for_rows, int *colsG,
+			  int colsG_num);
+static int parseColumnRefs(char *arg, PGresult *res, int **col_numbers,
+				int max_columns, char separator);
+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,
+				 pivot_field *fields, int idx);
+static void avlFree(avl_tree *tree, avl_node *node);
+static void rankSort(int num_columns, pivot_field *piv_columns);
+static int	indexOfColumn(const char *arg, PGresult *res);
+static int	pivotFieldCompare(const void *a, const void *b);
+static int	rankCompare(const void *a, const void *b);
+
+
+/*
+ * Main entry point to this module.
+ *
+ * Process the data from *res according the display options in pset (global),
+ * to generate the horizontal and vertical headers contents,
+ * then call printCrosstab() for the actual output.
+ */
+bool
+PrintResultsInCrossTab(PGresult *res)
+{
+	/* COLV or null */
+	char	   *opt_field_for_rows = pset.crosstabview_col_V;
+
+	/* COLH[:SCOLH] or null */
+	char	   *opt_field_for_columns = pset.crosstabview_col_H;
+	int			rn;
+	avl_tree	piv_columns;
+	avl_tree	piv_rows;
+	pivot_field *array_columns = NULL;
+	pivot_field *array_rows = NULL;
+	int			num_columns = 0;
+	int			num_rows = 0;
+	bool		retval = false;
+
+	/*
+	 * column definitions involved in the vertical header, horizontal header,
+	 * and grid
+	 */
+	int		   *colsV = NULL,
+			   *colsH = NULL,
+			   *colsG = NULL;
+	int			colsG_num;
+	int			nn;
+
+	/*
+	 * 0-based index of the field whose distinct values will become COLUMN
+	 * headers
+	 */
+	int			field_for_columns = -1;
+	int			sort_field_for_columns = -1;
+
+	/*
+	 * 0-based index of the field whose distinct values will become ROW
+	 * headers
+	 */
+	int			field_for_rows = -1;
+
+	avlInit(&piv_rows);
+	avlInit(&piv_columns);
+
+	if (res == NULL)
+	{
+		psql_error(_("No result\n"));
+		goto error_return;
+	}
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		psql_error(_("The query must return results to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the vertical header (1st arg) displayed in the
+	 * left-most column. Only a reference to a field is accepted (no sort
+	 * column).
+	 */
+
+	if (opt_field_for_rows == NULL)
+	{
+		field_for_rows = 0;
+	}
+	else
+	{
+		nn = parseColumnRefs(opt_field_for_rows, res, &colsV, 1, ':');
+		if (nn != 1)
+			goto error_return;
+		field_for_rows = colsV[0];
+	}
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	/*----------
+	 * Arguments processing for the horizontal header (2nd arg)
+	 * (pivoted column that gets displayed as the first row).
+	 * Determine:
+	 * - the sort direction if any
+	 * - the field number of that column in the PGresult
+	 * - the field number of the associated sort column if any
+	 */
+
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		nn = parseColumnRefs(opt_field_for_columns, res, &colsH, 2, ':');
+		if (nn <= 0)
+			goto error_return;
+		if (nn == 1)
+			field_for_columns = colsH[0];
+		else
+		{
+			field_for_columns = colsH[0];
+			sort_field_for_columns = colsH[1];
+		}
+
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the columns aside from headers (3rd arg)
+	 * Determine the columns to display in the grid and their order.
+	 */
+	if (pset.crosstabview_cols_grid == NULL)
+	{
+		/*
+		 * By defaut, all the fields from PGresult get displayed into the
+		 * grid, except the two fields that go into the vertical and
+		 * horizontal headers.
+		 */
+		if (PQnfields(res) > 2)
+		{
+			int			i,
+						j = 0;
+
+			colsG = (int *) pg_malloc(sizeof(int) * (PQnfields(res) - 2));
+			for (i = 0; i < PQnfields(res); i++)
+			{
+				if (i != field_for_rows && i != field_for_columns)
+					colsG[j++] = i;
+			}
+			colsG_num = PQnfields(res) - 2;
+		}
+		else
+		{
+			colsG = NULL;
+			colsG_num = 0;
+		}
+	}
+	else
+	{
+		/*
+		 * Non-default case: a list of fields is given. Parse that list to
+		 * determine the fields to display into the grid, and in what order.
+		 * The list format is colA[,colB[,colC...]]
+		 */
+		colsG_num = parseColumnRefs(pset.crosstabview_cols_grid,
+									res, &colsG, PQnfields(res), ',');
+		if (colsG_num <= 0)
+			goto error_return;
+	}
+
+	/*
+	 * First part: accumulate the names that go into the vertical and
+	 * horizontal headers, each into an AVL binary tree to build the set of
+	 * DISTINCT values.
+	 */
+
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		/* horizontal */
+		char	   *val;
+		char	   *val1;
+
+		val = PQgetisnull(res, rn, field_for_columns) ? NULL :
+			PQgetvalue(res, rn, field_for_columns);
+		val1 = NULL;
+
+		if (sort_field_for_columns >= 0 &&
+			!PQgetisnull(res, rn, sort_field_for_columns))
+			val1 = PQgetvalue(res, rn, sort_field_for_columns);
+
+		avlMergeValue(&piv_columns, val, val1);
+
+		if (piv_columns.count > 1600)
+		{
+			psql_error(_("Maximum number of columns (1600) exceeded\n"));
+			goto error_return;
+		}
+
+		/* vertical */
+		val = PQgetisnull(res, rn, field_for_rows) ? NULL :
+			PQgetvalue(res, rn, field_for_rows);
+
+		avlMergeValue(&piv_rows, val, NULL);
+	}
+
+	/*
+	 * Second part: Generate sorted arrays from the AVL trees.
+	 */
+
+	num_columns = piv_columns.count;
+	num_rows = piv_rows.count;
+
+	array_columns = (pivot_field *)
+		pg_malloc(sizeof(pivot_field) * num_columns);
+
+	array_rows = (pivot_field *)
+		pg_malloc(sizeof(pivot_field) * num_rows);
+
+	avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0);
+	avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0);
+
+	/*
+	 * Third part: optionally, process the ranking data for the horizontal
+	 * header
+	 */
+	if (sort_field_for_columns >= 0)
+		rankSort(num_columns, array_columns);
+
+	/*
+	 * Fourth part: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns,
+				  array_columns,
+				  field_for_columns,
+				  num_rows,
+				  array_rows,
+				  field_for_rows,
+				  colsG,
+				  colsG_num);
+
+	retval = true;
+
+error_return:
+	avlFree(&piv_columns, piv_columns.root);
+	avlFree(&piv_rows, piv_rows.root);
+	pg_free(array_columns);
+	pg_free(array_rows);
+	pg_free(colsV);
+	pg_free(colsH);
+	pg_free(colsG);
+
+	return retval;
+}
+
+/*
+ * Output the pivoted resultset with the printTable* functions
+ */
+static void
+printCrosstab(const PGresult *results, int num_columns,
+			  pivot_field *piv_columns, int field_for_columns, int num_rows,
+			  pivot_field *piv_rows, int field_for_rows, int *colsG,
+			  int colsG_num)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int			i,
+				j,
+				rn;
+	char		col_align = 'l';	/* alignment for values inside the grid */
+	int		   *horiz_map;		/* map indices from sorted horizontal headers
+								 * to piv_columns */
+	char	  **allocated_cells;/* Pointers for cell contents that are
+								 * allocated in this function, when cells
+								 * cannot simply point to PQgetvalue(results,
+								 * ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns + 1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+					column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse
+	 * map associating each piv_columns[].rank to its index in piv_columns.
+	 * This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int *) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+	/*
+	 * In the common case of only one field projected into the cells, the
+	 * display alignment depends on its PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (colsG_num == 1)
+		col_align = column_type_alignment(PQftype(results, colsG[0]));
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		char	   *colname = piv_columns[horiz_map[i]].name ?
+		piv_columns[horiz_map[i]].name :
+		(popt.nullPrint ? popt.nullPrint : "");
+
+		printTableAddHeader(&cont,
+							colname,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int			k = piv_rows[i].rank;
+
+		cont.cells[k * (num_columns + 1)] = piv_rows[i].name ?
+			piv_rows[i].name :
+			(popt.nullPrint ? popt.nullPrint : "");
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k * (num_columns + 1) + j + 1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns + 1);
+
+	allocated_cells = (char **) pg_malloc0(num_rows * num_columns * sizeof(char *));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		int			row_number;
+		int			col_number;
+		pivot_field *p;
+
+		/* Find target row */
+		pivot_field elt;
+
+		if (!PQgetisnull(results, rn, field_for_rows))
+			elt.name = PQgetvalue(results, rn, field_for_rows);
+		else
+			elt.name = NULL;
+		p = (pivot_field *) bsearch(&elt,
+									piv_rows,
+									num_rows,
+									sizeof(pivot_field),
+									pivotFieldCompare);
+
+		row_number = p ? p->rank : -1;
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+			elt.name = PQgetvalue(results, rn, field_for_columns);
+		else
+			elt.name = NULL;
+
+		p = (pivot_field *) bsearch(&elt,
+									piv_columns,
+									num_columns,
+									sizeof(pivot_field),
+									pivotFieldCompare);
+		col_number = p ? p->rank : -1;
+
+		/* Place value into cell */
+		if (col_number >= 0 && row_number >= 0)
+		{
+			int			idx = 1 + col_number + row_number * (num_columns + 1);
+			int			src_col = 0;	/* column number in source result */
+
+			/*
+			 * special case: when the source has only 2 columns, use a X
+			 * (cross/checkmark) for the cell content, and set src_col to a
+			 * virtual additional column.
+			 */
+			if (PQnfields(results) == 2)
+				src_col = -1;
+
+			for (i = 0; i < colsG_num || src_col == -1; i++)
+			{
+				char	   *content;
+
+				if (src_col == -1)
+				{
+					content = "X";
+				}
+				else
+				{
+					src_col = colsG[i];
+
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int			content_size;
+					char	   *new_content;
+
+					content_size = strlen(cont.cells[idx]) + 2 + strlen(content) + 1;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int			idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source
+					 * results get separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (i == 0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+
+				/* special case of the "virtual column" for checkmark */
+				if (src_col == -1)
+					break;
+			}
+		}
+	}
+
+	printTable(&cont, pset.queryFout, false, pset.logfile);
+	printTableCleanup(&cont);
+
+	for (i = 0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+/*
+ * Parse col1[<sep>col2][<sep>col3]...
+ * where colN can be:
+ * - a number from 1 to PQnfields(res)
+ * - an unquoted column name matching (case insensitively) one of PQfname(res,...)
+ * - a quoted column name matching (case sensitively) one of PQfname(res,...)
+ * max_columns: 0 if no maximum
+ */
+static int
+parseColumnRefs(char *arg,
+				PGresult *res,
+				int **col_numbers,
+				int max_columns,
+				char separator)
+{
+	char	   *p = arg;
+	char		c;
+	int			col_num = -1;
+	int			nb_cols = 0;
+	char	   *field_start = NULL;
+
+	*col_numbers = NULL;
+	while ((c = *p) != '\0')
+	{
+		bool		quoted_field = false;
+
+		field_start = p;
+
+		/* first char */
+		if (c == '"')
+		{
+			quoted_field = true;
+			p++;
+		}
+
+		while ((c = *p) != '\0')
+		{
+			if (c == separator && !quoted_field)
+				break;
+			if (c == '"')		/* end of field or embedded double quote */
+			{
+				p++;
+				if (*p == '"')
+				{
+					if (quoted_field)
+					{
+						p++;
+						continue;
+					}
+				}
+				else if (quoted_field && *p == separator)
+					break;
+			}
+			p += PQmblen(p, pset.encoding);
+		}
+
+		if (p != field_start)
+		{
+			/* look up the column and add its index into *col_numbers */
+			if (max_columns != 0 && nb_cols == max_columns)
+			{
+				psql_error(_("No more than %d column references expected\n"), max_columns);
+				goto errfail;
+			}
+			c = *p;
+			*p = '\0';
+			col_num = indexOfColumn(field_start, res);
+			*p = c;
+			if (col_num < 0)
+				goto errfail;
+			*col_numbers = (int *) pg_realloc(*col_numbers, (1 + nb_cols) * sizeof(int));
+			(*col_numbers)[nb_cols++] = col_num;
+		}
+		else
+		{
+			psql_error(_("Empty column reference\n"));
+			goto errfail;
+		}
+
+		if (*p)
+			p += PQmblen(p, pset.encoding);
+	}
+	return nb_cols;
+
+errfail:
+	pg_free(*col_numbers);
+	*col_numbers = NULL;
+	return -1;
+}
+
+/*
+ * The avl* functions below provide a minimalistic implementation of AVL binary
+ * trees, to efficiently collect the distinct values that will form the horizontal
+ * and vertical headers. It only supports adding new values, no removal or even
+ * search.
+ */
+static void
+avlInit(avl_tree *tree)
+{
+	tree->end = (avl_node *) pg_malloc0(sizeof(avl_node));
+	tree->end->childs[0] = tree->end->childs[1] = tree->end;
+	tree->count = 0;
+	tree->root = tree->end;
+}
+
+/* Deallocate recursively an AVL tree, starting from node */
+static void
+avlFree(avl_tree *tree, avl_node *node)
+{
+	if (node->childs[0] != tree->end)
+	{
+		avlFree(tree, node->childs[0]);
+		pg_free(node->childs[0]);
+	}
+	if (node->childs[1] != tree->end)
+	{
+		avlFree(tree, node->childs[1]);
+		pg_free(node->childs[1]);
+	}
+	if (node == tree->root)
+	{
+		/* free the root separately as it's not child of anything */
+		if (node != tree->end)
+			pg_free(node);
+		/* free the tree->end struct only once and when all else is freed */
+		pg_free(tree->end);
+	}
+}
+
+/* Set the height to 1 plus the greatest of left and right heights */
+static void
+avlUpdateHeight(avl_node *n)
+{
+	n->height = 1 + (n->childs[0]->height > n->childs[1]->height ?
+					 n->childs[0]->height :
+					 n->childs[1]->height);
+}
+
+/* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
+static avl_node *
+avlRotate(avl_node **current, int dir)
+{
+	avl_node   *before = *current;
+	avl_node   *after = (*current)->childs[dir];
+
+	*current = after;
+	before->childs[dir] = after->childs[!dir];
+	avlUpdateHeight(before);
+	after->childs[!dir] = before;
+
+	return after;
+}
+
+static int
+avlBalance(avl_node *n)
+{
+	return n->childs[0]->height - n->childs[1]->height;
+}
+
+/*
+ * After an insertion, possibly rebalance the tree so that the left and right
+ * node heights don't differ by more than 1.
+ * May update *node.
+ */
+static void
+avlAdjustBalance(avl_tree *tree, avl_node **node)
+{
+	avl_node   *current = *node;
+	int			b = avlBalance(current) / 2;
+
+	if (b != 0)
+	{
+		int			dir = (1 - b) / 2;
+
+		if (avlBalance(current->childs[dir]) == -b)
+			avlRotate(&current->childs[dir], !dir);
+		current = avlRotate(node, dir);
+	}
+	if (current != tree->end)
+		avlUpdateHeight(current);
+}
+
+/*
+ * Insert a new value/field, starting from *node, reaching the correct position
+ * in the tree by recursion.  Possibly rebalance the tree and possibly update
+ * *node.  Do nothing if the value is already present in the tree.
+ */
+static void
+avlInsertNode(avl_tree *tree, avl_node **node, pivot_field field)
+{
+	avl_node   *current = *node;
+
+	if (current == tree->end)
+	{
+		avl_node   *new_node = (avl_node *)
+		pg_malloc(sizeof(avl_node));
+
+		new_node->height = 1;
+		new_node->field = field;
+		new_node->childs[0] = new_node->childs[1] = tree->end;
+		tree->count++;
+		*node = new_node;
+	}
+	else
+	{
+		int			cmp = pivotFieldCompare(&field, &current->field);
+
+		if (cmp != 0)
+		{
+			avlInsertNode(tree,
+						  cmp > 0 ? &current->childs[1] : &current->childs[0],
+						  field);
+			avlAdjustBalance(tree, node);
+		}
+	}
+}
+
+/* Insert the value into the AVL tree, if it does not preexist */
+static void
+avlMergeValue(avl_tree *tree, char *name, char *sort_value)
+{
+	pivot_field field;
+
+	field.name = name;
+	field.rank = tree->count;
+	field.sort_value = sort_value;
+	avlInsertNode(tree, &tree->root, field);
+}
+
+/*
+ * Recursively extract node values into the names array, in sorted order with a
+ * left-to-right tree traversal.
+ * Return the next candidate offset to write into the names array.
+ * fields[] must be preallocated to hold tree->count entries
+ */
+static int
+avlCollectFields(avl_tree *tree, avl_node *node, pivot_field *fields, int idx)
+{
+	if (node == tree->end)
+		return idx;
+
+	idx = avlCollectFields(tree, node->childs[0], fields, idx);
+	fields[idx] = node->field;
+	return avlCollectFields(tree, node->childs[1], fields, idx + 1);
+}
+
+static void
+rankSort(int num_columns, pivot_field *piv_columns)
+{
+	int		   *hmap;			/* [[offset in piv_columns, rank], ...for
+								 * every header entry] */
+	int			i;
+
+	hmap = (int *) pg_malloc(sizeof(int) * num_columns * 2);
+	for (i = 0; i < num_columns; i++)
+	{
+		char	   *val = piv_columns[i].sort_value;
+
+		/* ranking information is valid if non null and matches /^-?\d+$/ */
+		if (val &&
+			((*val == '-' &&
+			  strspn(val + 1, "0123456789") == strlen(val + 1)) ||
+			 strspn(val, "0123456789") == strlen(val)))
+		{
+			hmap[i * 2] = atoi(val);
+			hmap[i * 2 + 1] = i;
+		}
+		else
+		{
+			/* invalid rank information ignored (equivalent to rank 0) */
+			hmap[i * 2] = 0;
+			hmap[i * 2 + 1] = i;
+		}
+	}
+
+	qsort(hmap, num_columns, sizeof(int) * 2, rankCompare);
+
+	for (i = 0; i < num_columns; i++)
+	{
+		piv_columns[hmap[i * 2 + 1]].rank = i;
+	}
+
+	pg_free(hmap);
+}
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static bool
+fieldNameEquals(const char *arg, const char *fieldname)
+{
+	const char *p = arg;
+	const char *f = fieldname;
+	char		c;
+
+	if (*p++ != '"')
+		return !pg_strcasecmp(arg, fieldname);
+
+	while ((c = *p++))
+	{
+		if (c == '"')
+		{
+			if (*p == '"')
+				p++;			/* skip second quote and continue */
+			else if (*p == '\0')
+				return (*f == '\0');	/* p is shorter than f, or is
+										 * identical */
+		}
+		if (*f == '\0')
+			return false;		/* f is shorter than p */
+		if (c != *f)			/* found one byte that differs */
+			return false;
+		f++;
+	}
+	return (*f == '\0');
+}
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *	on success, the 0-based index of the column
+ *	or -1 if the column number or name is not found in the result's structure,
+ *		  or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char *arg, PGresult *res)
+{
+	int			idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0 || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int			i;
+
+		idx = -1;
+		for (i = 0; i < PQnfields(res); i++)
+		{
+			if (fieldNameEquals(arg, PQfname(res, i)))
+			{
+				if (idx >= 0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+/*
+ * Value comparator for vertical and horizontal headers
+ * used for deduplication only.
+ * - null values are considered equal
+ * - non-null < null
+ * - non-null values are compared with strcmp()
+ */
+static int
+pivotFieldCompare(const void *a, const void *b)
+{
+	pivot_field *pa = (pivot_field *) a;
+	pivot_field *pb = (pivot_field *) b;
+
+	/* test null values */
+	if (!pb->name)
+		return pa->name ? -1 : 0;
+	else if (!pa->name)
+		return 1;
+
+	/* non-null values */
+	return strcmp(((pivot_field *) a)->name,
+				  ((pivot_field *) b)->name);
+}
+
+static int
+rankCompare(const void *a, const void *b)
+{
+	return *((int *) a) - *((int *) b);
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..184e045
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,14 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+/* prototypes */
+extern bool PrintResultsInCrossTab(PGresult *res);
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 7549451..96e5628 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -177,6 +177,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\gexec                 execute query, then execute each value in its result\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [COLUMNS] execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index c69f6ba..9340ef2 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -93,6 +93,11 @@ typedef struct _psqlSettings
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
 	bool		gexec_flag;		/* one-shot flag to execute query's results */
+	bool		crosstabview_output;	/* one-shot request to print results
+										 * in crosstab */
+	char	   *crosstabview_col_V;		/* one-shot \crosstabview 1st argument */
+	char	   *crosstabview_col_H;		/* one-shot \crosstabview 2nd argument */
+	char	   *crosstabview_cols_grid; /* one-shot \crosstabview 3nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index cb8a06d..5c10005 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1274,7 +1274,8 @@ psql_completion(const char *text, int start, int end)
 
 	/* psql's backslash commands. */
 	static const char *const backslash_commands[] = {
-		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
+		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
+		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index 30efd3f..1ec74f1 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3295,30 +3295,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3360,6 +3339,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/include/fe_utils/print.h b/src/include/fe_utils/print.h
index ff90237..18aee93 100644
--- a/src/include/fe_utils/print.h
+++ b/src/include/fe_utils/print.h
@@ -206,6 +206,8 @@ extern void printTable(const printTableContent *cont,
 extern void printQuery(const PGresult *result, const printQueryOpt *opt,
 		   FILE *fout, bool is_pager, FILE *flog);
 
+extern char column_type_alignment(Oid);
+
 extern void setDecimalLocale(void);
 extern const printTextFormat *get_line_style(const printTableOpt *opt);
 extern void refresh_utf8format(const printTableOpt *opt);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e293fc0..de903a0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1,3 +1,7 @@
+EditableObjectType
+pivot_field
+avl_tree
+avl_node
 ABITVEC
 ACCESS_ALLOWED_ACE
 ACL
#115Daniel Verite
daniel@manitou-mail.org
In reply to: Alvaro Herrera (#114)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

Thanks for looking into that patch!

regression=# select * from pg_class \crosstabview relnatts
\crosstabview: missing second argument
regression-#

Fixed. This was modelled after the behavior of:
select 1 \badcommand
but I've changed to mimic what happens with:
select 1 \g /some/invalid/path
the query buffer is not discarded by the error but the prompt
is ready for a fresh new command.

alvherre=# select * from pg_class \crosstabview relnatts relkinda
Invalid column name: relkinda
alvherre=# select 1;
The query must return at least two columns to be shown in crosstab

Definitely a bug. Fixed.

Also fixed a one-off bug with quoted columns: in parseColumnRefs(),
first call to PQmblen(), I wrongly assumed that
PQmblen("", ..) returns 0, whereas in fact it returns 1.

* A few examples in docs. The psql manpage should have at least two new
examples showing the crosstab features, one with the simplest case you
can think of, and another one showing all the features.

Added that in the EXAMPLES section at the very end of the manpage.

* Add regression test cases somewhere for the regression database.
Probably use "FROM tenk1 WHERE hundred < 5", which provides you with 500
rows, enough for many interesting games. Make sure to test all the
provided features. I would use a new psql.sql file for this.

Looking into regression tests, not yet done.

* How did you come up with the 1600 value? Whatever it is, please use a
#define instead of hardcoding it.

Done with accompanying comment in crosstabview.h

* In the "if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')"
block (line 497 in the attached), can't we do the same thing by using
psprintf?

In that block, we can't pass a cell contents as a valist and be done with
that cell, because duplicates of (col value,row value) may happen
at any iteration of the upper loop over PQntuples(results). Any cell really
may need reallocation unpredictably until that loop is done, whereas
psprintf starts by allocating a new buffer unconditionally, so it doesn't
look
to me like it could help to simplify that block.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-crosstabview-v15.difftext/x-patch; name=psql-crosstabview-v15.diffDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index d8b9a03..1072733 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -990,6 +990,113 @@ testdb=&gt;
       </varlistentry>
 
       <varlistentry>
+        <term><literal>\crosstabview [
+            <replaceable class="parameter">colV</replaceable>
+            <replaceable class="parameter">colH</replaceable>
+            [:<replaceable class="parameter">scolH</replaceable>]
+            [<replaceable class="parameter">colG1[,colG2...]</replaceable>]
+            ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows
+        the results inside a crosstab grid.
+        The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header
+        and the output column <replaceable class="parameter">colH</replaceable>
+        becomes a horizontal header, optionally sorted by ranking data obtained
+        from <replaceable class="parameter">scolH</replaceable>.
+
+        <replaceable class="parameter">colG1[,colG2...]</replaceable>
+        is the list of output columns to project into the grid.
+        By default, all output columns of the query except 
+        <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>
+        are included in this list.
+        </para>
+
+        <para>
+        All columns can be refered to by their position (starting at 1), or by
+        their name. Normal case folding and quoting rules apply on column
+        names. By default,
+        <replaceable class="parameter">colV</replaceable> corresponds to column 1
+        and <replaceable class="parameter">colH</replaceable> to column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the deduplicated values found in
+        column <replaceable class="parameter">colV</replaceable>, in the same
+        order as in the query results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the deduplicated values found in
+        column <replaceable class="parameter">colH</replaceable>, in
+        the order of appearance in the query results.
+        If specified, the optional <replaceable class="parameter">scolH</replaceable>
+        argument refers to a column whose values should be integer numbers
+        by which <replaceable class="parameter">colH</replaceable> will be sorted
+        to be positioned in the horizontal header.
+        </para>
+
+        <para>
+        Inside the crosstab grid,
+        given a query output with <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        the contents of a cell located at the intersection
+        <literal>(x,y)</literal> is determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the query results such that the
+         value for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns or the columns listed in
+         <replaceable class="parameter">colG1[,colG2...]</replaceable>
+         are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed
+         in the cell as if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to
+         the case of one row except that the values coming from different rows
+         are stacked vertically, the different source rows being separated by
+         newline characters inside the cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term><literal>\d[S+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
 
         <listitem>
@@ -4066,6 +4173,46 @@ first  | 4
 second | four
 </programlisting></para>
 
+<para>
+When suitable, query results can be shown in a crosstab representation with the \crosstabview command.
+<programlisting>
+testdb=&gt; <userinput>SELECT first, second ,first&lt;=2 AS ge2 FROM my_table;</userinput>
+ first | second | ge2 
+-------+--------+-----
+     1 | one    | f
+     2 | two    | t
+     3 | three  | t
+     4 | four   | t
+(4 rows)
+
+testdb=&gt; <userinput>\crosstabview first second</userinput>
+ first | one | two | three | four 
+-------+-----+-----+-------+------
+     1 | f   |     |       | 
+     2 |     | t   |       | 
+     3 |     |     | t     | 
+     4 |     |     |       | t
+(4 rows)
+</programlisting>
+
+This second example shows a multiplication table with rows sorted in reverse
+numerical order and columns with an independant, ascending numerical order.
+<programlisting>
+testdb=&gt; <userinput>SELECT t1.first as "A", t2.first+100 AS "B", t1.first*(t2.first+100) as "AxB",</userinput>
+testdb(&gt; <userinput>row_number() over(order by t2.first) AS ord</userinput>
+testdb(&gt; <userinput>FROM my_table t1 CROSS JOIN my_table t2 ORDER BY 1 DESC</userinput>
+testdb(&gt; <userinput>\crosstabview A B:ord AxB</userinput>
+ A | 101 | 102 | 103 | 104 
+---+-----+-----+-----+-----
+ 4 | 404 | 408 | 412 | 416
+ 3 | 303 | 306 | 309 | 312
+ 2 | 202 | 204 | 206 | 208
+ 1 | 101 | 102 | 103 | 104
+(4 rows)
+</programlisting>
+
+</para>
+
  </refsect1>
 
 </refentry>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index d1c3b77..1f6a289 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ LDFLAGS += -L$(top_builddir)/src/fe_utils -lpgfeutils -lpq
 
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o describe.o \
-	tab-complete.o \
+	crosstabview.o tab-complete.o \
 	sql_help.o psqlscanslash.o \
 	$(WIN32RES)
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 1d326a8..aef6f23 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -39,6 +39,7 @@
 
 #include "common.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "describe.h"
 #include "help.h"
 #include "input.h"
@@ -364,6 +365,20 @@ exec_command(const char *cmd,
 	else if (strcmp(cmd, "copyright") == 0)
 		print_copyright();
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		pset.crosstabview_col_V = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		pset.crosstabview_col_H = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		pset.crosstabview_cols_grid = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		pset.crosstabview_output = true;
+		status = PSQL_CMD_SEND;
+	}
+
 	/* \d* commands */
 	else if (cmd[0] == 'd')
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index df3441c..5840331 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -23,6 +23,7 @@
 #include "settings.h"
 #include "command.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "fe_utils/mbprint.h"
 
 
@@ -1064,6 +1065,8 @@ PrintQueryResults(PGresult *results)
 				success = StoreQueryTuple(results);
 			else if (pset.gexec_flag)
 				success = ExecQueryTuples(results);
+			else if (pset.crosstabview_output)
+				success = PrintResultsInCrossTab(results);
 			else
 				success = PrintQueryTuples(results);
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
@@ -1356,6 +1359,24 @@ sendquery_cleanup:
 	/* reset \gexec trigger */
 	pset.gexec_flag = false;
 
+	/* reset \crosstabview trigger */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
+	if (pset.crosstabview_cols_grid)
+	{
+		free(pset.crosstabview_cols_grid);
+		pset.crosstabview_cols_grid = NULL;
+	}
+
 	return OK;
 }
 
@@ -1520,7 +1541,25 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 			is_pager = true;
 		}
 
-		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
+		if (pset.crosstabview_output)
+		{
+			if (ntuples < fetch_count)
+				PrintResultsInCrossTab(results);
+			else
+			{
+				/*
+				 * crosstabview is denied if the whole set of rows is not
+				 * guaranteed to be fetched in the first iteration, because
+				 * it's expected in memory as a single PGresult structure.
+				 */
+				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
+						   fetch_count);
+				PQclear(results);
+				break;
+			}
+		}
+		else
+			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
 
 		ClearOrSaveResult(results);
 
@@ -1599,6 +1638,23 @@ cleanup:
 		*elapsed_msec += INSTR_TIME_GET_MILLISEC(after);
 	}
 
+	/* reset \crosstabview settings */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
+	if (pset.crosstabview_cols_grid)
+	{
+		free(pset.crosstabview_cols_grid);
+		pset.crosstabview_cols_grid = NULL;
+	}
 	return OK;
 }
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..a9b3370
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,979 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+#include "postgres_fe.h"
+
+#include <string.h>
+
+#include "common.h"
+#include "crosstabview.h"
+#include "pqexpbuffer.h"
+#include "settings.h"
+
+
+/*
+ * Value/position from the resultset that goes into the horizontal or vertical
+ * crosstabview header.
+ */
+typedef struct _pivot_field
+{
+	/*
+	 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
+	 * value becomes an entry in the vertical header (colV), or horizontal
+	 * header (colH). A Null value is represented by a NULL pointer.
+	 */
+	char	   *name;
+
+	/*
+	 * When a sort is requested on an alternative column, this holds
+	 * PQgetvalue() for the sort column corresponding to <name>. If <name>
+	 * appear multiple times, it's the first value in the order of the results
+	 * that is kept. A Null value is represented by a NULL pointer.
+	 */
+	char	   *sort_value;
+
+	/*
+	 * Rank of this value, starting at 0. Initially, it's the relative
+	 * position of the first appearance of <name> in the resultset. For
+	 * example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
+	 * When a sort column is specified, ranks get updated in a final pass to
+	 * reflect the desired order.
+	 */
+	int			rank;
+} pivot_field;
+
+/* Node in avl_tree */
+typedef struct _avl_node
+{
+	/* Node contents */
+	pivot_field field;
+
+	/*
+	 * Height of this node in the tree (number of nodes on the longest path to
+	 * a leaf).
+	 */
+	int			height;
+
+	/*
+	 * Child nodes. [0] points to left subtree, [1] to right subtree. Never
+	 * NULL, points to the empty node avl_tree.end when no left or right
+	 * value.
+	 */
+	struct _avl_node *childs[2];
+} avl_node;
+
+/*
+ * Control structure for the AVL tree (binary search tree kept
+ * balanced with the AVL algorithm)
+ */
+typedef struct _avl_tree
+{
+	int			count;			/* Total number of nodes */
+	avl_node   *root;			/* root of the tree */
+	avl_node   *end;			/* Immutable dereferenceable empty tree */
+} avl_tree;
+
+
+static void printCrosstab(const PGresult *results, int num_columns,
+			  pivot_field *piv_columns, int field_for_columns, int num_rows,
+			  pivot_field *piv_rows, int field_for_rows, int *colsG,
+			  int colsG_num);
+static int parseColumnRefs(char *arg, PGresult *res, int **col_numbers,
+				int max_columns, char separator);
+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,
+				 pivot_field *fields, int idx);
+static void avlFree(avl_tree *tree, avl_node *node);
+static void rankSort(int num_columns, pivot_field *piv_columns);
+static int	indexOfColumn(const char *arg, PGresult *res);
+static int	pivotFieldCompare(const void *a, const void *b);
+static int	rankCompare(const void *a, const void *b);
+
+
+/*
+ * Main entry point to this module.
+ *
+ * Process the data from *res according the display options in pset (global),
+ * to generate the horizontal and vertical headers contents,
+ * then call printCrosstab() for the actual output.
+ */
+bool
+PrintResultsInCrossTab(PGresult *res)
+{
+	/* COLV or null */
+	char	   *opt_field_for_rows = pset.crosstabview_col_V;
+
+	/* COLH[:SCOLH] or null */
+	char	   *opt_field_for_columns = pset.crosstabview_col_H;
+	int			rn;
+	avl_tree	piv_columns;
+	avl_tree	piv_rows;
+	pivot_field *array_columns = NULL;
+	pivot_field *array_rows = NULL;
+	int			num_columns = 0;
+	int			num_rows = 0;
+	bool		retval = false;
+
+	/*
+	 * column definitions involved in the vertical header, horizontal header,
+	 * and grid
+	 */
+	int		   *colsV = NULL,
+			   *colsH = NULL,
+			   *colsG = NULL;
+	int			colsG_num;
+	int			nn;
+
+	/*
+	 * 0-based index of the field whose distinct values will become COLUMN
+	 * headers
+	 */
+	int			field_for_columns = -1;
+	int			sort_field_for_columns = -1;
+
+	/*
+	 * 0-based index of the field whose distinct values will become ROW
+	 * headers
+	 */
+	int			field_for_rows = -1;
+
+	avlInit(&piv_rows);
+	avlInit(&piv_columns);
+
+	if (res == NULL)
+	{
+		psql_error(_("No result\n"));
+		goto error_return;
+	}
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		psql_error(_("The query must return results to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	if (opt_field_for_rows && !opt_field_for_columns)
+	{
+		psql_error(_("A second column must be specified for the horizontal header\n"));
+		goto error_return;
+	}
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the vertical header (1st arg) displayed in the
+	 * left-most column. Only a reference to a field is accepted (no sort
+	 * column).
+	 */
+
+	if (opt_field_for_rows == NULL)
+	{
+		field_for_rows = 0;
+	}
+	else
+	{
+		nn = parseColumnRefs(opt_field_for_rows, res, &colsV, 1, ':');
+		if (nn != 1)
+			goto error_return;
+		field_for_rows = colsV[0];
+	}
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	/*----------
+	 * Arguments processing for the horizontal header (2nd arg)
+	 * (pivoted column that gets displayed as the first row).
+	 * Determine:
+	 * - the sort direction if any
+	 * - the field number of that column in the PGresult
+	 * - the field number of the associated sort column if any
+	 */
+
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		nn = parseColumnRefs(opt_field_for_columns, res, &colsH, 2, ':');
+		if (nn <= 0)
+			goto error_return;
+		if (nn == 1)
+			field_for_columns = colsH[0];
+		else
+		{
+			field_for_columns = colsH[0];
+			sort_field_for_columns = colsH[1];
+		}
+
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the columns aside from headers (3rd arg)
+	 * Determine the columns to display in the grid and their order.
+	 */
+	if (pset.crosstabview_cols_grid == NULL)
+	{
+		/*
+		 * By defaut, all the fields from PGresult get displayed into the
+		 * grid, except the two fields that go into the vertical and
+		 * horizontal headers.
+		 */
+		if (PQnfields(res) > 2)
+		{
+			int			i,
+						j = 0;
+
+			colsG = (int *) pg_malloc(sizeof(int) * (PQnfields(res) - 2));
+			for (i = 0; i < PQnfields(res); i++)
+			{
+				if (i != field_for_rows && i != field_for_columns)
+					colsG[j++] = i;
+			}
+			colsG_num = PQnfields(res) - 2;
+		}
+		else
+		{
+			colsG = NULL;
+			colsG_num = 0;
+		}
+	}
+	else
+	{
+		/*
+		 * Non-default case: a list of fields is given. Parse that list to
+		 * determine the fields to display into the grid, and in what order.
+		 * The list format is colA[,colB[,colC...]]
+		 */
+		colsG_num = parseColumnRefs(pset.crosstabview_cols_grid,
+									res, &colsG, PQnfields(res), ',');
+		if (colsG_num <= 0)
+			goto error_return;
+	}
+
+	/*
+	 * First part: accumulate the names that go into the vertical and
+	 * horizontal headers, each into an AVL binary tree to build the set of
+	 * DISTINCT values.
+	 */
+
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		/* horizontal */
+		char	   *val;
+		char	   *val1;
+
+		val = PQgetisnull(res, rn, field_for_columns) ? NULL :
+			PQgetvalue(res, rn, field_for_columns);
+		val1 = NULL;
+
+		if (sort_field_for_columns >= 0 &&
+			!PQgetisnull(res, rn, sort_field_for_columns))
+			val1 = PQgetvalue(res, rn, sort_field_for_columns);
+
+		avlMergeValue(&piv_columns, val, val1);
+
+		if (piv_columns.count > CROSSTABVIEW_MAX_COLUMNS)
+		{
+			psql_error(_("Maximum number of columns (%d) exceeded\n"),
+				CROSSTABVIEW_MAX_COLUMNS);
+			goto error_return;
+		}
+
+		/* vertical */
+		val = PQgetisnull(res, rn, field_for_rows) ? NULL :
+			PQgetvalue(res, rn, field_for_rows);
+
+		avlMergeValue(&piv_rows, val, NULL);
+	}
+
+	/*
+	 * Second part: Generate sorted arrays from the AVL trees.
+	 */
+
+	num_columns = piv_columns.count;
+	num_rows = piv_rows.count;
+
+	array_columns = (pivot_field *)
+		pg_malloc(sizeof(pivot_field) * num_columns);
+
+	array_rows = (pivot_field *)
+		pg_malloc(sizeof(pivot_field) * num_rows);
+
+	avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0);
+	avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0);
+
+	/*
+	 * Third part: optionally, process the ranking data for the horizontal
+	 * header
+	 */
+	if (sort_field_for_columns >= 0)
+		rankSort(num_columns, array_columns);
+
+	/*
+	 * Fourth part: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns,
+				  array_columns,
+				  field_for_columns,
+				  num_rows,
+				  array_rows,
+				  field_for_rows,
+				  colsG,
+				  colsG_num);
+
+	retval = true;
+
+error_return:
+	avlFree(&piv_columns, piv_columns.root);
+	avlFree(&piv_rows, piv_rows.root);
+	pg_free(array_columns);
+	pg_free(array_rows);
+	pg_free(colsV);
+	pg_free(colsH);
+	pg_free(colsG);
+
+	return retval;
+}
+
+/*
+ * Output the pivoted resultset with the printTable* functions
+ */
+static void
+printCrosstab(const PGresult *results, int num_columns,
+			  pivot_field *piv_columns, int field_for_columns, int num_rows,
+			  pivot_field *piv_rows, int field_for_rows, int *colsG,
+			  int colsG_num)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int			i,
+				j,
+				rn;
+	char		col_align = 'l';	/* alignment for values inside the grid */
+	int		   *horiz_map;		/* map indices from sorted horizontal headers
+								 * to piv_columns */
+	char	  **allocated_cells;/* Pointers for cell contents that are
+								 * allocated in this function, when cells
+								 * cannot simply point to PQgetvalue(results,
+								 * ...) */
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns + 1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+					column_type_alignment(PQftype(results, field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse
+	 * map associating each piv_columns[].rank to its index in piv_columns.
+	 * This avoids an O(N^2) loop later
+	 */
+	horiz_map = (int *) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+	{
+		horiz_map[piv_columns[i].rank] = i;
+	}
+
+	/*
+	 * In the common case of only one field projected into the cells, the
+	 * display alignment depends on its PQftype(). Otherwise the contents are
+	 * made-up strings, so the alignment is 'l'
+	 */
+	if (colsG_num == 1)
+		col_align = column_type_alignment(PQftype(results, colsG[0]));
+	else
+		col_align = 'l';
+
+	for (i = 0; i < num_columns; i++)
+	{
+		char	   *colname = piv_columns[horiz_map[i]].name ?
+		piv_columns[horiz_map[i]].name :
+		(popt.nullPrint ? popt.nullPrint : "");
+
+		printTableAddHeader(&cont,
+							colname,
+							false,
+							col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int			k = piv_rows[i].rank;
+
+		cont.cells[k * (num_columns + 1)] = piv_rows[i].name ?
+			piv_rows[i].name :
+			(popt.nullPrint ? popt.nullPrint : "");
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k * (num_columns + 1) + j + 1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns + 1);
+
+	allocated_cells = (char **) pg_malloc0(num_rows * num_columns * sizeof(char *));
+
+	/* Step 3: set all the cells "inside the grid" */
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		int			row_number;
+		int			col_number;
+		pivot_field *p;
+
+		/* Find target row */
+		pivot_field elt;
+
+		if (!PQgetisnull(results, rn, field_for_rows))
+			elt.name = PQgetvalue(results, rn, field_for_rows);
+		else
+			elt.name = NULL;
+		p = (pivot_field *) bsearch(&elt,
+									piv_rows,
+									num_rows,
+									sizeof(pivot_field),
+									pivotFieldCompare);
+
+		row_number = p ? p->rank : -1;
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+			elt.name = PQgetvalue(results, rn, field_for_columns);
+		else
+			elt.name = NULL;
+
+		p = (pivot_field *) bsearch(&elt,
+									piv_columns,
+									num_columns,
+									sizeof(pivot_field),
+									pivotFieldCompare);
+		col_number = p ? p->rank : -1;
+
+		/* Place value into cell */
+		if (col_number >= 0 && row_number >= 0)
+		{
+			int			idx = 1 + col_number + row_number * (num_columns + 1);
+			int			src_col = 0;	/* column number in source result */
+
+			/*
+			 * special case: when the source has only 2 columns, use a X
+			 * (cross/checkmark) for the cell content, and set src_col to a
+			 * virtual additional column.
+			 */
+			if (PQnfields(results) == 2)
+				src_col = -1;
+
+			for (i = 0; i < colsG_num || src_col == -1; i++)
+			{
+				char	   *content;
+
+				if (src_col == -1)
+				{
+					content = "X";
+				}
+				else
+				{
+					src_col = colsG[i];
+
+					content = (!PQgetisnull(results, rn, src_col)) ?
+						PQgetvalue(results, rn, src_col) :
+						(popt.nullPrint ? popt.nullPrint : "");
+				}
+
+				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				{
+					/*
+					 * Multiple values for the same (row,col) are projected
+					 * into the same cell. When this happens, separate the
+					 * previous content of the cell from the new value by a
+					 * newline.
+					 */
+					int			content_size;
+					char	   *new_content;
+
+					content_size = strlen(cont.cells[idx]) + 2 + strlen(content) + 1;
+
+					/*
+					 * idx2 is an index into allocated_cells. It differs from
+					 * idx (index into cont.cells), because vertical and
+					 * horizontal headers are included in `cont.cells` but
+					 * excluded from allocated_cells.
+					 */
+					int			idx2 = (row_number * num_columns) + col_number;
+
+					if (allocated_cells[idx2] != NULL)
+					{
+						new_content = pg_realloc(allocated_cells[idx2], content_size);
+					}
+					else
+					{
+						/*
+						 * At this point, cont.cells[idx] still contains a
+						 * PQgetvalue() pointer.  Just after, it will contain
+						 * a new pointer maintained in allocated_cells[], and
+						 * freed at the end of this function.
+						 */
+						new_content = pg_malloc(content_size);
+						strcpy(new_content, cont.cells[idx]);
+					}
+					cont.cells[idx] = new_content;
+					allocated_cells[idx2] = new_content;
+
+					/*
+					 * Contents that are on adjacent columns in the source
+					 * results get separated by one space in the target.
+					 * Contents that are on different rows in the source get
+					 * separated by newlines in the target.
+					 */
+					if (i == 0)
+						strcat(new_content, "\n");
+					else
+						strcat(new_content, " ");
+					strcat(new_content, content);
+				}
+				else
+				{
+					cont.cells[idx] = content;
+				}
+
+				/* special case of the "virtual column" for checkmark */
+				if (src_col == -1)
+					break;
+			}
+		}
+	}
+
+	printTable(&cont, pset.queryFout, false, pset.logfile);
+	printTableCleanup(&cont);
+
+	for (i = 0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+/*
+ * Parse col1[<sep>col2][<sep>col3]...
+ * where colN can be:
+ * - a number from 1 to PQnfields(res)
+ * - an unquoted column name matching (case insensitively) one of PQfname(res,...)
+ * - a quoted column name matching (case sensitively) one of PQfname(res,...)
+ * max_columns: 0 if no maximum
+ */
+static int
+parseColumnRefs(char *arg,
+				PGresult *res,
+				int **col_numbers,
+				int max_columns,
+				char separator)
+{
+	char	   *p = arg;
+	char		c;
+	int			col_num = -1;
+	int			nb_cols = 0;
+	char	   *field_start = NULL;
+
+	*col_numbers = NULL;
+	while ((c = *p) != '\0')
+	{
+		bool		quoted_field = false;
+
+		field_start = p;
+
+		/* first char */
+		if (c == '"')
+		{
+			quoted_field = true;
+			p++;
+		}
+
+		while ((c = *p) != '\0')
+		{
+			if (c == separator && !quoted_field)
+				break;
+			if (c == '"')		/* end of field or embedded double quote */
+			{
+				p++;
+				if (*p == '"')
+				{
+					if (quoted_field)
+					{
+						p++;
+						continue;
+					}
+				}
+				else if (quoted_field && *p == separator)
+					break;
+			}
+			if (*p)
+				p += PQmblen(p, pset.encoding);
+		}
+
+		if (p != field_start)
+		{
+			/* look up the column and add its index into *col_numbers */
+			if (max_columns != 0 && nb_cols == max_columns)
+			{
+				psql_error(_("No more than %d column references expected\n"), max_columns);
+				goto errfail;
+			}
+			c = *p;
+			*p = '\0';
+			col_num = indexOfColumn(field_start, res);
+			*p = c;
+			if (col_num < 0)
+				goto errfail;
+			*col_numbers = (int *) pg_realloc(*col_numbers, (1 + nb_cols) * sizeof(int));
+			(*col_numbers)[nb_cols++] = col_num;
+		}
+		else
+		{
+			psql_error(_("Empty column reference\n"));
+			goto errfail;
+		}
+
+		if (*p)
+			p += PQmblen(p, pset.encoding);
+	}
+	return nb_cols;
+
+errfail:
+	pg_free(*col_numbers);
+	*col_numbers = NULL;
+	return -1;
+}
+
+/*
+ * The avl* functions below provide a minimalistic implementation of AVL binary
+ * trees, to efficiently collect the distinct values that will form the horizontal
+ * and vertical headers. It only supports adding new values, no removal or even
+ * search.
+ */
+static void
+avlInit(avl_tree *tree)
+{
+	tree->end = (avl_node *) pg_malloc0(sizeof(avl_node));
+	tree->end->childs[0] = tree->end->childs[1] = tree->end;
+	tree->count = 0;
+	tree->root = tree->end;
+}
+
+/* Deallocate recursively an AVL tree, starting from node */
+static void
+avlFree(avl_tree *tree, avl_node *node)
+{
+	if (node->childs[0] != tree->end)
+	{
+		avlFree(tree, node->childs[0]);
+		pg_free(node->childs[0]);
+	}
+	if (node->childs[1] != tree->end)
+	{
+		avlFree(tree, node->childs[1]);
+		pg_free(node->childs[1]);
+	}
+	if (node == tree->root)
+	{
+		/* free the root separately as it's not child of anything */
+		if (node != tree->end)
+			pg_free(node);
+		/* free the tree->end struct only once and when all else is freed */
+		pg_free(tree->end);
+	}
+}
+
+/* Set the height to 1 plus the greatest of left and right heights */
+static void
+avlUpdateHeight(avl_node *n)
+{
+	n->height = 1 + (n->childs[0]->height > n->childs[1]->height ?
+					 n->childs[0]->height :
+					 n->childs[1]->height);
+}
+
+/* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
+static avl_node *
+avlRotate(avl_node **current, int dir)
+{
+	avl_node   *before = *current;
+	avl_node   *after = (*current)->childs[dir];
+
+	*current = after;
+	before->childs[dir] = after->childs[!dir];
+	avlUpdateHeight(before);
+	after->childs[!dir] = before;
+
+	return after;
+}
+
+static int
+avlBalance(avl_node *n)
+{
+	return n->childs[0]->height - n->childs[1]->height;
+}
+
+/*
+ * After an insertion, possibly rebalance the tree so that the left and right
+ * node heights don't differ by more than 1.
+ * May update *node.
+ */
+static void
+avlAdjustBalance(avl_tree *tree, avl_node **node)
+{
+	avl_node   *current = *node;
+	int			b = avlBalance(current) / 2;
+
+	if (b != 0)
+	{
+		int			dir = (1 - b) / 2;
+
+		if (avlBalance(current->childs[dir]) == -b)
+			avlRotate(&current->childs[dir], !dir);
+		current = avlRotate(node, dir);
+	}
+	if (current != tree->end)
+		avlUpdateHeight(current);
+}
+
+/*
+ * Insert a new value/field, starting from *node, reaching the correct position
+ * in the tree by recursion.  Possibly rebalance the tree and possibly update
+ * *node.  Do nothing if the value is already present in the tree.
+ */
+static void
+avlInsertNode(avl_tree *tree, avl_node **node, pivot_field field)
+{
+	avl_node   *current = *node;
+
+	if (current == tree->end)
+	{
+		avl_node   *new_node = (avl_node *)
+		pg_malloc(sizeof(avl_node));
+
+		new_node->height = 1;
+		new_node->field = field;
+		new_node->childs[0] = new_node->childs[1] = tree->end;
+		tree->count++;
+		*node = new_node;
+	}
+	else
+	{
+		int			cmp = pivotFieldCompare(&field, &current->field);
+
+		if (cmp != 0)
+		{
+			avlInsertNode(tree,
+						  cmp > 0 ? &current->childs[1] : &current->childs[0],
+						  field);
+			avlAdjustBalance(tree, node);
+		}
+	}
+}
+
+/* Insert the value into the AVL tree, if it does not preexist */
+static void
+avlMergeValue(avl_tree *tree, char *name, char *sort_value)
+{
+	pivot_field field;
+
+	field.name = name;
+	field.rank = tree->count;
+	field.sort_value = sort_value;
+	avlInsertNode(tree, &tree->root, field);
+}
+
+/*
+ * Recursively extract node values into the names array, in sorted order with a
+ * left-to-right tree traversal.
+ * Return the next candidate offset to write into the names array.
+ * fields[] must be preallocated to hold tree->count entries
+ */
+static int
+avlCollectFields(avl_tree *tree, avl_node *node, pivot_field *fields, int idx)
+{
+	if (node == tree->end)
+		return idx;
+
+	idx = avlCollectFields(tree, node->childs[0], fields, idx);
+	fields[idx] = node->field;
+	return avlCollectFields(tree, node->childs[1], fields, idx + 1);
+}
+
+static void
+rankSort(int num_columns, pivot_field *piv_columns)
+{
+	int		   *hmap;			/* [[offset in piv_columns, rank], ...for
+								 * every header entry] */
+	int			i;
+
+	hmap = (int *) pg_malloc(sizeof(int) * num_columns * 2);
+	for (i = 0; i < num_columns; i++)
+	{
+		char	   *val = piv_columns[i].sort_value;
+
+		/* ranking information is valid if non null and matches /^-?\d+$/ */
+		if (val &&
+			((*val == '-' &&
+			  strspn(val + 1, "0123456789") == strlen(val + 1)) ||
+			 strspn(val, "0123456789") == strlen(val)))
+		{
+			hmap[i * 2] = atoi(val);
+			hmap[i * 2 + 1] = i;
+		}
+		else
+		{
+			/* invalid rank information ignored (equivalent to rank 0) */
+			hmap[i * 2] = 0;
+			hmap[i * 2 + 1] = i;
+		}
+	}
+
+	qsort(hmap, num_columns, sizeof(int) * 2, rankCompare);
+
+	for (i = 0; i < num_columns; i++)
+	{
+		piv_columns[hmap[i * 2 + 1]].rank = i;
+	}
+
+	pg_free(hmap);
+}
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static bool
+fieldNameEquals(const char *arg, const char *fieldname)
+{
+	const char *p = arg;
+	const char *f = fieldname;
+	char		c;
+
+	if (*p++ != '"')
+		return !pg_strcasecmp(arg, fieldname);
+
+	while ((c = *p++))
+	{
+		if (c == '"')
+		{
+			if (*p == '"')
+				p++;			/* skip second quote and continue */
+			else if (*p == '\0')
+				return (*f == '\0');	/* p is shorter than f, or is
+										 * identical */
+		}
+		if (*f == '\0')
+			return false;		/* f is shorter than p */
+		if (c != *f)			/* found one byte that differs */
+			return false;
+		f++;
+	}
+	return (*f == '\0');
+}
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *	on success, the 0-based index of the column
+ *	or -1 if the column number or name is not found in the result's structure,
+ *		  or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char *arg, PGresult *res)
+{
+	int			idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0 || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int			i;
+
+		idx = -1;
+		for (i = 0; i < PQnfields(res); i++)
+		{
+			if (fieldNameEquals(arg, PQfname(res, i)))
+			{
+				if (idx >= 0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+/*
+ * Value comparator for vertical and horizontal headers
+ * used for deduplication only.
+ * - null values are considered equal
+ * - non-null < null
+ * - non-null values are compared with strcmp()
+ */
+static int
+pivotFieldCompare(const void *a, const void *b)
+{
+	pivot_field *pa = (pivot_field *) a;
+	pivot_field *pb = (pivot_field *) b;
+
+	/* test null values */
+	if (!pb->name)
+		return pa->name ? -1 : 0;
+	else if (!pa->name)
+		return 1;
+
+	/* non-null values */
+	return strcmp(((pivot_field *) a)->name,
+				  ((pivot_field *) b)->name);
+}
+
+static int
+rankCompare(const void *a, const void *b)
+{
+	return *((int *) a) - *((int *) b);
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..49c9ad1
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,26 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+/* 
+ * Limit the number of output columns generated in memory by the crosstabview
+ * algorithm. A new output column is added for each distinct value found in the
+ * column that pivots (to form the horizontal header).
+ * The purpose of this limit is to fail early instead of over-allocating or spending
+ * too much time if the crosstab to generate happens to be unreasonably large
+ * (worst case: a NxN cartesian product with N=number of tuples).
+ * The value of 1600 corresponds to the maximum columns per table in storage,
+ * but it could be as much as INT_MAX theorically.
+ */
+#define CROSSTABVIEW_MAX_COLUMNS 1600
+
+/* prototypes */
+extern bool PrintResultsInCrossTab(PGresult *res);
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 7549451..96e5628 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -177,6 +177,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\gexec                 execute query, then execute each value in its result\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [COLUMNS] execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index c69f6ba..9340ef2 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -93,6 +93,11 @@ typedef struct _psqlSettings
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
 	bool		gexec_flag;		/* one-shot flag to execute query's results */
+	bool		crosstabview_output;	/* one-shot request to print results
+										 * in crosstab */
+	char	   *crosstabview_col_V;		/* one-shot \crosstabview 1st argument */
+	char	   *crosstabview_col_H;		/* one-shot \crosstabview 2nd argument */
+	char	   *crosstabview_cols_grid; /* one-shot \crosstabview 3nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index cb8a06d..5c10005 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1274,7 +1274,8 @@ psql_completion(const char *text, int start, int end)
 
 	/* psql's backslash commands. */
 	static const char *const backslash_commands[] = {
-		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
+		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
+		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index 30efd3f..1ec74f1 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3295,30 +3295,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3360,6 +3339,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/include/fe_utils/print.h b/src/include/fe_utils/print.h
index ff90237..18aee93 100644
--- a/src/include/fe_utils/print.h
+++ b/src/include/fe_utils/print.h
@@ -206,6 +206,8 @@ extern void printTable(const printTableContent *cont,
 extern void printQuery(const PGresult *result, const printQueryOpt *opt,
 		   FILE *fout, bool is_pager, FILE *flog);
 
+extern char column_type_alignment(Oid);
+
 extern void setDecimalLocale(void);
 extern const printTextFormat *get_line_style(const printTableOpt *opt);
 extern void refresh_utf8format(const printTableOpt *opt);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e293fc0..de903a0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1,3 +1,7 @@
+EditableObjectType
+pivot_field
+avl_tree
+avl_node
 ABITVEC
 ACCESS_ALLOWED_ACE
 ACL
#116Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Daniel Verite (#115)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Daniel Verite wrote:

regression=# select * from pg_class \crosstabview relnatts
\crosstabview: missing second argument
regression-#

Fixed. This was modelled after the behavior of:
select 1 \badcommand
but I've changed to mimic what happens with:
select 1 \g /some/invalid/path
the query buffer is not discarded by the error but the prompt
is ready for a fresh new command.

Works for me.

* A few examples in docs. The psql manpage should have at least two new
examples showing the crosstab features, one with the simplest case you
can think of, and another one showing all the features.

Added that in the EXAMPLES section at the very end of the manpage.

Ok. Seems a bit too short to me, and I don't like the fact that you
can't actually run it because you need to create the my_table
beforehand. I think it'd be better if you used a VALUES clause there,
so that the reader can cut'n paste into psql to start to play with the
feature.

* In the "if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')"
block (line 497 in the attached), can't we do the same thing by using
psprintf?

In that block, we can't pass a cell contents as a valist and be done with
that cell, because duplicates of (col value,row value) may happen
at any iteration of the upper loop over PQntuples(results). Any cell really
may need reallocation unpredictably until that loop is done, whereas
psprintf starts by allocating a new buffer unconditionally, so it doesn't
look to me like it could help to simplify that block.

I don't know what you mean, but here's what I meant.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

psql-crosstabview-v15a.patchtext/x-diff; charset=us-asciiDownload
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
index b3510b9..0216dae 100644
--- a/src/bin/psql/crosstabview.c
+++ b/src/bin/psql/crosstabview.c
@@ -496,12 +496,12 @@ printCrosstab(const PGresult *results, int num_columns,
 				{
 					src_col = colsG[i];
 
-					content = (!PQgetisnull(results, rn, src_col)) ?
+					content = !PQgetisnull(results, rn, src_col) ?
 						PQgetvalue(results, rn, src_col) :
 						(popt.nullPrint ? popt.nullPrint : "");
 				}
 
-				if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')
+				if (cont.cells[idx] != NULL)
 				{
 					/*
 					 * Multiple values for the same (row,col) are projected
@@ -509,12 +509,9 @@ printCrosstab(const PGresult *results, int num_columns,
 					 * previous content of the cell from the new value by a
 					 * newline.
 					 */
-					int			content_size;
 					char	   *new_content;
 					int			idx2;
 
-					content_size = strlen(cont.cells[idx]) + 2 + strlen(content) + 1;
-
 					/*
 					 * idx2 is an index into allocated_cells. It differs from
 					 * idx (index into cont.cells), because vertical and
@@ -524,34 +521,17 @@ printCrosstab(const PGresult *results, int num_columns,
 					idx2 = (row_number * num_columns) + col_number;
 
 					if (allocated_cells[idx2] != NULL)
-					{
-						new_content = pg_realloc(allocated_cells[idx2], content_size);
-					}
+						new_content = psprintf("%s%s%s",
+											   allocated_cells[idx2],
+											   i == 0 ? "\n" : " ",
+											   content);
 					else
-					{
-						/*
-						 * At this point, cont.cells[idx] still contains a
-						 * PQgetvalue() pointer.  Just after, it will contain
-						 * a new pointer maintained in allocated_cells[], and
-						 * freed at the end of this function.
-						 */
-						new_content = pg_malloc(content_size);
-						strcpy(new_content, cont.cells[idx]);
-					}
-					cont.cells[idx] = new_content;
-					allocated_cells[idx2] = new_content;
+						new_content = psprintf("%s", content);
 
-					/*
-					 * Contents that are on adjacent columns in the source
-					 * results get separated by one space in the target.
-					 * Contents that are on different rows in the source get
-					 * separated by newlines in the target.
-					 */
-					if (i == 0)
-						strcat(new_content, "\n");
-					else
-						strcat(new_content, " ");
-					strcat(new_content, content);
+					cont.cells[idx] = new_content;
+					if (allocated_cells[idx2] != NULL)
+						pg_free(allocated_cells[idx2]);
+					allocated_cells[idx2] = new_content;
 				}
 				else
 				{
#117Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Daniel Verite (#115)
Re: [patch] Proposal for \crosstabview in psql

I wonder if the business of appending values of multiple columns
separated with spaces is doing us any good. Why not require that
there's a single column in the cell? If the user wants to put things
together, they can use format() or just || the fields together. What
benefit is there to the ' '? When I ran my first test queries over
pg_class I was surprised about this behavior:

alvherre=# select * from pg_class
alvherre=# \crosstabview relnatts relkind

relnatts | r | t | i | v
----------+------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------
26 | pg_statistic 11 11397 0 10 0 2619 0 15 380 15 2840 t f p 0 f f f f f f f t n 540 1 {alvherre=arwdDxt/alvherre} (null) | | |
30 | pg_type 11 71 0 10 0 0 0 9 358 9 0 t f p 0 t f f f f f f t n 540 1 {=r/alvherre} (null) | | |
3 | pg_user_mapping 11 11633 0 10 0 1418 0 0 0 0 0 t f p 0 t f f f f f f t n 540 1 {alvherre=arwdDxt/alvherre} (null) +| pg_toast_2604 99 11642 0 10 0 2830 0 0 0 0 0 t f p 0 f f f f f f f t n 540 1 (null) (null) +| pg_amop_opr_fam_index 11 0 0 10 403 2654 0 5 688 0 0 f f p 0 f f f f f f f t n 0 0 (null) (null) +| pg_group 11 11661 0 10 0 11660 0 0 0 0 0 f f p 0 f f t f f f f t n 0 0 {=r/alvherre} (null) +

I'm tempted to rip that out, unless you have a reason not to.

In fact, I think even the grouping of values of multiple rows with \n is
not terribly great either. Why not just require people to group the
values beforehand? You can use "string_agg(column, E'\n')" to get the
same behavior, plus you can do other things such as sum() etc.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#118Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Alvaro Herrera (#116)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

Daniel Verite wrote:

* A few examples in docs. The psql manpage should have at least two new
examples showing the crosstab features, one with the simplest case you
can think of, and another one showing all the features.

Added that in the EXAMPLES section at the very end of the manpage.

Ok. Seems a bit too short to me, and I don't like the fact that you
can't actually run it because you need to create the my_table
beforehand. I think it'd be better if you used a VALUES clause there,
so that the reader can cut'n paste into psql to start to play with the
feature.

Oh, I noticed now that my_table was created by previous examples.
Nevermind.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#119David G. Johnston
david.g.johnston@gmail.com
In reply to: Alvaro Herrera (#117)
Re: [patch] Proposal for \crosstabview in psql

On Thu, Apr 7, 2016 at 1:26 PM, Alvaro Herrera <alvherre@2ndquadrant.com>
wrote:

I wonder if the business of appending values of multiple columns
separated with spaces is doing us any good. Why not require that
there's a single column in the cell? If the user wants to put things
together, they can use format() or just || the fields together. What
benefit is there to the ' '? When I ran my first test queries over
pg_class I was surprised about this behavior:

alvherre=# select * from pg_class
alvherre=# \crosstabview relnatts relkind

relnatts |
r
| t
|
i |
v

----------+------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------
26 | pg_statistic 11 11397 0 10 0 2619 0 15 380 15 2840 t f p 0 f f
f f f f f t n 540 1 {alvherre=arwdDxt/alvherre} (null)
|
|
|
30 | pg_type 11 71 0 10 0 0 0 9 358 9 0 t f p 0 t f f f f f f t n
540 1 {=r/alvherre} (null)
|
|
|
3 | pg_user_mapping 11 11633 0 10 0 1418 0 0 0 0 0 t f p 0 t f f f
f f f t n 540 1 {alvherre=arwdDxt/alvherre} (null)
+| pg_toast_2604 99 11642 0 10 0 2830 0 0 0 0 0 t f p 0 f f f f f f f t
n 540 1 (null) (null)    +| pg_amop_opr_fam_index 11 0 0 10 403 2654 0 5
688 0 0 f f p 0 f f f f f f f t n 0 0 (null) (null)                +|
pg_group 11 11661 0 10 0 11660 0 0 0 0 0 f f p 0 f f t f f f f t n 0 0
{=r/alvherre} (null)
+

I'm tempted to rip that out, unless you have a reason not to.

In fact, I think even the grouping of values of multiple rows with \n is
not terribly great either. Why not just require people to group the
values beforehand? You can use "string_agg(column, E'\n')" to get the
same behavior, plus you can do other things such as sum() etc.

​Went and looked at the examples page and at first blush it seems like this
module only understands text. My specific concern here is dealing with
"numbers-as-text" sorting.​

​As to the question of behavior when multiple columns (and rows?) are
present: ​we need some sort of default do we not. Nothing is precluding
the user from doing their own aggregates and limiting the select-list.
That said I'm more inclined to error if the input data in not unique on
(v,h). I feel the possibility of a user query bug going unnoticed in that
scenario is reasonably large since its likely that only some combinations
of duplicates appear. I'm a bit less tentative regarding column
concatenation since I would expect that nearly every cell involved in the
output would be noticeably affected. Though, if we are going to protect
against extra rows extending that to protect against extra columns seems
fair.

Another option is, possibly conditioned on the first two columns being the
headers, to only take the column in the third position (or, the first
unassigned column).and display it.

Otherwise if multiple candidate columns are present and none are chosen for
the cell we could just error and force the user to explicitly choose.

The concatenation behavior seems like the least useful default. I'm
inclined to favor the first unassigned input column. And never allow (v,h)
is violate uniqueness.

David J.

#120Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Daniel Verite (#115)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Daniel Verite wrote:

* In the "if (cont.cells[idx] != NULL && cont.cells[idx][0] != '\0')"
block (line 497 in the attached), can't we do the same thing by using
psprintf?

In that block, we can't pass a cell contents as a valist and be done with
that cell, because duplicates of (col value,row value) may happen
at any iteration of the upper loop over PQntuples(results). Any cell really
may need reallocation unpredictably until that loop is done, whereas
psprintf starts by allocating a new buffer unconditionally, so it doesn't
look
to me like it could help to simplify that block.

I messed with that code some more, as it looked unnecessarily
complicated; please see attached and verify that it still behaves
sanely. This needs those regression tests you promised. I tested a few
cases and it seems good to me.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

Attachments:

psql-crosstabview-v16.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index d8b9a03..9c5a915 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -990,6 +990,113 @@ testdb=&gt;
       </varlistentry>
 
       <varlistentry>
+        <term><literal>\crosstabview [
+            <replaceable class="parameter">colV</replaceable>
+            <replaceable class="parameter">colH</replaceable>
+            [:<replaceable class="parameter">scolH</replaceable>]
+            [<replaceable class="parameter">colG1[,colG2...]</replaceable>]
+            ] </literal></term>
+        <listitem>
+        <para>
+        Execute the current query buffer (like <literal>\g</literal>) and shows
+        the results inside a crosstab grid.
+        The output column <replaceable class="parameter">colV</replaceable>
+        becomes a vertical header
+        and the output column <replaceable class="parameter">colH</replaceable>
+        becomes a horizontal header, optionally sorted by ranking data obtained
+        from <replaceable class="parameter">scolH</replaceable>.
+
+        <replaceable class="parameter">colG1[,colG2...]</replaceable>
+        is the list of output columns to project into the grid.
+        By default, all output columns of the query except 
+        <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>
+        are included in this list.
+        </para>
+
+        <para>
+        All columns can be refered to by their position (starting at 1), or by
+        their name. Normal case folding and quoting rules apply on column
+        names. By default,
+        <replaceable class="parameter">colV</replaceable> corresponds to column 1
+        and <replaceable class="parameter">colH</replaceable> to column 2.
+        A query having only one output column cannot be viewed in crosstab, and
+        <replaceable class="parameter">colH</replaceable> must differ from
+        <replaceable class="parameter">colV</replaceable>.
+        </para>
+
+        <para>
+        The vertical header, displayed as the leftmost column,
+        contains the deduplicated values found in
+        column <replaceable class="parameter">colV</replaceable>, in the same
+        order as in the query results.
+        </para>
+        <para>
+        The horizontal header, displayed as the first row,
+        contains the deduplicated values found in
+        column <replaceable class="parameter">colH</replaceable>, in
+        the order of appearance in the query results.
+        If specified, the optional <replaceable class="parameter">scolH</replaceable>
+        argument refers to a column whose values should be integer numbers
+        by which <replaceable class="parameter">colH</replaceable> will be sorted
+        to be positioned in the horizontal header.
+        </para>
+
+        <para>
+        Inside the crosstab grid,
+        given a query output with <literal>N</literal> columns
+        (including <replaceable class="parameter">colV</replaceable> and
+        <replaceable class="parameter">colH</replaceable>),
+        for each distinct value <literal>x</literal> of
+        <replaceable class="parameter">colH</replaceable>
+        and each distinct value <literal>y</literal> of
+        <replaceable class="parameter">colV</replaceable>,
+        the contents of a cell located at the intersection
+        <literal>(x,y)</literal> is determined by these rules:
+        <itemizedlist>
+        <listitem>
+        <para>
+         if there is no corresponding row in the query results such that the
+         value for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, the cell is empty.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there is exactly one row such that the value
+         for <replaceable class="parameter">colH</replaceable>
+         is <literal>x</literal> and the value
+         for <replaceable class="parameter">colV</replaceable>
+         is <literal>y</literal>, then the <literal>N-2</literal> other
+         columns or the columns listed in
+         <replaceable class="parameter">colG1[,colG2...]</replaceable>
+         are displayed in the cell, separated between each other by
+         a space character if needed.
+
+         If <literal>N=2</literal>, the letter <literal>X</literal> is displayed
+         in the cell as if a virtual third column contained that character.
+        </para>
+        </listitem>
+
+        <listitem>
+        <para>
+         if there are several corresponding rows, the behavior is identical to
+         the case of one row except that the values coming from different rows
+         are stacked vertically, the different source rows being separated by
+         newline characters inside the cell.
+        </para>
+        </listitem>
+
+        </itemizedlist>
+        </para>
+
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term><literal>\d[S+] [ <link linkend="APP-PSQL-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
 
         <listitem>
@@ -4066,6 +4173,47 @@ first  | 4
 second | four
 </programlisting></para>
 
+<para>
+  When suitable, query results can be shown in a crosstab representation
+  with the \crosstabview command:
+<programlisting>
+testdb=&gt; <userinput>SELECT first, second, first &gt; 2 AS gt2 FROM my_table;</userinput>
+ first | second | ge2 
+-------+--------+-----
+     1 | one    | f
+     2 | two    | f
+     3 | three  | t
+     4 | four   | t
+(4 rows)
+
+testdb=&gt; <userinput>\crosstabview first second</userinput>
+ first | one | two | three | four 
+-------+-----+-----+-------+------
+     1 | f   |     |       | 
+     2 |     | f   |       | 
+     3 |     |     | t     | 
+     4 |     |     |       | t
+(4 rows)
+</programlisting>
+
+This second example shows a multiplication table with rows sorted in reverse
+numerical order and columns with an independant, ascending numerical order.
+<programlisting>
+testdb=&gt; <userinput>SELECT t1.first as "A", t2.first+100 AS "B", t1.first*(t2.first+100) as "AxB",</userinput>
+testdb(&gt; <userinput>row_number() over(order by t2.first) AS ord</userinput>
+testdb(&gt; <userinput>FROM my_table t1 CROSS JOIN my_table t2 ORDER BY 1 DESC</userinput>
+testdb(&gt; <userinput>\crosstabview A B:ord AxB</userinput>
+ A | 101 | 102 | 103 | 104 
+---+-----+-----+-----+-----
+ 4 | 404 | 408 | 412 | 416
+ 3 | 303 | 306 | 309 | 312
+ 2 | 202 | 204 | 206 | 208
+ 1 | 101 | 102 | 103 | 104
+(4 rows)
+</programlisting>
+
+</para>
+
  </refsect1>
 
 </refentry>
diff --git a/src/bin/psql/Makefile b/src/bin/psql/Makefile
index d1c3b77..1f6a289 100644
--- a/src/bin/psql/Makefile
+++ b/src/bin/psql/Makefile
@@ -23,7 +23,7 @@ LDFLAGS += -L$(top_builddir)/src/fe_utils -lpgfeutils -lpq
 
 OBJS=	command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
 	startup.o prompt.o variables.o large_obj.o describe.o \
-	tab-complete.o \
+	crosstabview.o tab-complete.o \
 	sql_help.o psqlscanslash.o \
 	$(WIN32RES)
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 1d326a8..aef6f23 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -39,6 +39,7 @@
 
 #include "common.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "describe.h"
 #include "help.h"
 #include "input.h"
@@ -364,6 +365,20 @@ exec_command(const char *cmd,
 	else if (strcmp(cmd, "copyright") == 0)
 		print_copyright();
 
+	/* \crosstabview -- execute a query and display results in crosstab */
+	else if (strcmp(cmd, "crosstabview") == 0)
+	{
+		pset.crosstabview_col_V = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		pset.crosstabview_col_H = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+		pset.crosstabview_cols_grid = psql_scan_slash_option(scan_state,
+									  OT_NORMAL, NULL, false);
+
+		pset.crosstabview_output = true;
+		status = PSQL_CMD_SEND;
+	}
+
 	/* \d* commands */
 	else if (cmd[0] == 'd')
 	{
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
index df3441c..5840331 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -23,6 +23,7 @@
 #include "settings.h"
 #include "command.h"
 #include "copy.h"
+#include "crosstabview.h"
 #include "fe_utils/mbprint.h"
 
 
@@ -1064,6 +1065,8 @@ PrintQueryResults(PGresult *results)
 				success = StoreQueryTuple(results);
 			else if (pset.gexec_flag)
 				success = ExecQueryTuples(results);
+			else if (pset.crosstabview_output)
+				success = PrintResultsInCrossTab(results);
 			else
 				success = PrintQueryTuples(results);
 			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
@@ -1356,6 +1359,24 @@ sendquery_cleanup:
 	/* reset \gexec trigger */
 	pset.gexec_flag = false;
 
+	/* reset \crosstabview trigger */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
+	if (pset.crosstabview_cols_grid)
+	{
+		free(pset.crosstabview_cols_grid);
+		pset.crosstabview_cols_grid = NULL;
+	}
+
 	return OK;
 }
 
@@ -1520,7 +1541,25 @@ ExecQueryUsingCursor(const char *query, double *elapsed_msec)
 			is_pager = true;
 		}
 
-		printQuery(results, &my_popt, fout, is_pager, pset.logfile);
+		if (pset.crosstabview_output)
+		{
+			if (ntuples < fetch_count)
+				PrintResultsInCrossTab(results);
+			else
+			{
+				/*
+				 * crosstabview is denied if the whole set of rows is not
+				 * guaranteed to be fetched in the first iteration, because
+				 * it's expected in memory as a single PGresult structure.
+				 */
+				psql_error("\\crosstabview must be used with less than FETCH_COUNT (%d) rows\n",
+						   fetch_count);
+				PQclear(results);
+				break;
+			}
+		}
+		else
+			printQuery(results, &my_popt, fout, is_pager, pset.logfile);
 
 		ClearOrSaveResult(results);
 
@@ -1599,6 +1638,23 @@ cleanup:
 		*elapsed_msec += INSTR_TIME_GET_MILLISEC(after);
 	}
 
+	/* reset \crosstabview settings */
+	pset.crosstabview_output = false;
+	if (pset.crosstabview_col_V)
+	{
+		free(pset.crosstabview_col_V);
+		pset.crosstabview_col_V = NULL;
+	}
+	if (pset.crosstabview_col_H)
+	{
+		free(pset.crosstabview_col_H);
+		pset.crosstabview_col_H = NULL;
+	}
+	if (pset.crosstabview_cols_grid)
+	{
+		free(pset.crosstabview_cols_grid);
+		pset.crosstabview_cols_grid = NULL;
+	}
 	return OK;
 }
 
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
new file mode 100644
index 0000000..0d70e47
--- /dev/null
+++ b/src/bin/psql/crosstabview.c
@@ -0,0 +1,943 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.c
+ */
+#include "postgres_fe.h"
+
+#include <string.h>
+
+#include "common.h"
+#include "crosstabview.h"
+#include "pqexpbuffer.h"
+#include "settings.h"
+
+
+/*
+ * Value/position from the resultset that goes into the horizontal or vertical
+ * crosstabview header.
+ */
+typedef struct _pivot_field
+{
+	/*
+	 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
+	 * value becomes an entry in the vertical header (colV), or horizontal
+	 * header (colH). A Null value is represented by a NULL pointer.
+	 */
+	char	   *name;
+
+	/*
+	 * When a sort is requested on an alternative column, this holds
+	 * PQgetvalue() for the sort column corresponding to <name>. If <name>
+	 * appear multiple times, it's the first value in the order of the results
+	 * that is kept. A Null value is represented by a NULL pointer.
+	 */
+	char	   *sort_value;
+
+	/*
+	 * Rank of this value, starting at 0. Initially, it's the relative
+	 * position of the first appearance of <name> in the resultset. For
+	 * example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
+	 * When a sort column is specified, ranks get updated in a final pass to
+	 * reflect the desired order.
+	 */
+	int			rank;
+} pivot_field;
+
+/* Node in avl_tree */
+typedef struct _avl_node
+{
+	/* Node contents */
+	pivot_field field;
+
+	/*
+	 * Height of this node in the tree (number of nodes on the longest path to
+	 * a leaf).
+	 */
+	int			height;
+
+	/*
+	 * Child nodes. [0] points to left subtree, [1] to right subtree. Never
+	 * NULL, points to the empty node avl_tree.end when no left or right
+	 * value.
+	 */
+	struct _avl_node *childs[2];
+} avl_node;
+
+/*
+ * Control structure for the AVL tree (binary search tree kept
+ * balanced with the AVL algorithm)
+ */
+typedef struct _avl_tree
+{
+	int			count;			/* Total number of nodes */
+	avl_node   *root;			/* root of the tree */
+	avl_node   *end;			/* Immutable dereferenceable empty tree */
+} avl_tree;
+
+
+static void printCrosstab(const PGresult *results,
+			  int num_columns, pivot_field *piv_columns, int field_for_columns,
+			  int num_rows, pivot_field *piv_rows, int field_for_rows,
+			  int num_colsG, int *colsG);
+static int parseColumnRefs(char *arg, PGresult *res, int **col_numbers,
+				int max_columns, char separator);
+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,
+				 pivot_field *fields, int idx);
+static void avlFree(avl_tree *tree, avl_node *node);
+static void rankSort(int num_columns, pivot_field *piv_columns);
+static int	indexOfColumn(const char *arg, PGresult *res);
+static int	pivotFieldCompare(const void *a, const void *b);
+static int	rankCompare(const void *a, const void *b);
+
+
+/*
+ * Main entry point to this module.
+ *
+ * Process the data from *res according the display options in pset (global),
+ * to generate the horizontal and vertical headers contents,
+ * then call printCrosstab() for the actual output.
+ */
+bool
+PrintResultsInCrossTab(PGresult *res)
+{
+	/* COLV or null */
+	char	   *opt_field_for_rows = pset.crosstabview_col_V;
+
+	/* COLH[:SCOLH] or null */
+	char	   *opt_field_for_columns = pset.crosstabview_col_H;
+	int			rn;
+	avl_tree	piv_columns;
+	avl_tree	piv_rows;
+	pivot_field *array_columns = NULL;
+	pivot_field *array_rows = NULL;
+	int			num_columns = 0;
+	int			num_rows = 0;
+	bool		retval = false;
+
+	/*
+	 * column definitions involved in the vertical header, horizontal header,
+	 * and grid
+	 */
+	int		   *colsV = NULL,
+			   *colsH = NULL,
+			   *colsG = NULL;
+	int			num_colsG;
+	int			nn;
+
+	/*
+	 * 0-based index of the field whose distinct values will become COLUMN
+	 * headers
+	 */
+	int			field_for_columns = -1;
+	int			sort_field_for_columns = -1;
+
+	/*
+	 * 0-based index of the field whose distinct values will become ROW
+	 * headers
+	 */
+	int			field_for_rows = -1;
+
+	avlInit(&piv_rows);
+	avlInit(&piv_columns);
+
+	if (res == NULL)
+	{
+		psql_error(_("No result\n"));
+		goto error_return;
+	}
+
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		psql_error(_("The query must return results to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	if (opt_field_for_rows && !opt_field_for_columns)
+	{
+		psql_error(_("A second column must be specified for the horizontal header\n"));
+		goto error_return;
+	}
+
+	if (PQnfields(res) < 2)
+	{
+		psql_error(_("The query must return at least two columns to be shown in crosstab\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the vertical header (1st arg) displayed in the
+	 * left-most column. Only a reference to a field is accepted (no sort
+	 * column).
+	 */
+
+	if (opt_field_for_rows == NULL)
+	{
+		field_for_rows = 0;
+	}
+	else
+	{
+		nn = parseColumnRefs(opt_field_for_rows, res, &colsV, 1, ':');
+		if (nn != 1)
+			goto error_return;
+		field_for_rows = colsV[0];
+	}
+
+	if (field_for_rows < 0)
+		goto error_return;
+
+	/*----------
+	 * Arguments processing for the horizontal header (2nd arg)
+	 * (pivoted column that gets displayed as the first row).
+	 * Determine:
+	 * - the sort direction if any
+	 * - the field number of that column in the PGresult
+	 * - the field number of the associated sort column if any
+	 */
+
+	if (opt_field_for_columns == NULL)
+		field_for_columns = 1;
+	else
+	{
+		nn = parseColumnRefs(opt_field_for_columns, res, &colsH, 2, ':');
+		if (nn <= 0)
+			goto error_return;
+		if (nn == 1)
+			field_for_columns = colsH[0];
+		else
+		{
+			field_for_columns = colsH[0];
+			sort_field_for_columns = colsH[1];
+		}
+
+		if (field_for_columns < 0)
+			goto error_return;
+	}
+
+	if (field_for_columns == field_for_rows)
+	{
+		psql_error(_("The same column cannot be used for both vertical and horizontal headers\n"));
+		goto error_return;
+	}
+
+	/*
+	 * Arguments processing for the columns aside from headers (3rd arg)
+	 * Determine the columns to display in the grid and their order.
+	 */
+	if (pset.crosstabview_cols_grid == NULL)
+	{
+		/*
+		 * By defaut, all the fields from PGresult get displayed into the
+		 * grid, except the two fields that go into the vertical and
+		 * horizontal headers.
+		 */
+		if (PQnfields(res) > 2)
+		{
+			int			i,
+						j = 0;
+
+			colsG = (int *) pg_malloc(sizeof(int) * (PQnfields(res) - 2));
+			for (i = 0; i < PQnfields(res); i++)
+			{
+				if (i != field_for_rows && i != field_for_columns)
+					colsG[j++] = i;
+			}
+			num_colsG = PQnfields(res) - 2;
+		}
+		else
+		{
+			colsG = NULL;
+			num_colsG = 0;
+		}
+	}
+	else
+	{
+		/*
+		 * Non-default case: a list of fields is given. Parse that list to
+		 * determine the fields to display into the grid, and in what order.
+		 * The list format is colA[,colB[,colC...]]
+		 */
+		num_colsG = parseColumnRefs(pset.crosstabview_cols_grid,
+									res, &colsG, PQnfields(res), ',');
+		if (num_colsG <= 0)
+			goto error_return;
+	}
+
+	/*
+	 * First part: accumulate the names that go into the vertical and
+	 * horizontal headers, each into an AVL binary tree to build the set of
+	 * DISTINCT values.
+	 */
+
+	for (rn = 0; rn < PQntuples(res); rn++)
+	{
+		/* horizontal */
+		char	   *val;
+		char	   *val1;
+
+		val = PQgetisnull(res, rn, field_for_columns) ? NULL :
+			PQgetvalue(res, rn, field_for_columns);
+		val1 = NULL;
+
+		if (sort_field_for_columns >= 0 &&
+			!PQgetisnull(res, rn, sort_field_for_columns))
+			val1 = PQgetvalue(res, rn, sort_field_for_columns);
+
+		avlMergeValue(&piv_columns, val, val1);
+
+		if (piv_columns.count > CROSSTABVIEW_MAX_COLUMNS)
+		{
+			psql_error(_("Maximum number of columns (%d) exceeded\n"),
+				CROSSTABVIEW_MAX_COLUMNS);
+			goto error_return;
+		}
+
+		/* vertical */
+		val = PQgetisnull(res, rn, field_for_rows) ? NULL :
+			PQgetvalue(res, rn, field_for_rows);
+
+		avlMergeValue(&piv_rows, val, NULL);
+	}
+
+	/*
+	 * Second part: Generate sorted arrays from the AVL trees.
+	 */
+
+	num_columns = piv_columns.count;
+	num_rows = piv_rows.count;
+
+	array_columns = (pivot_field *)
+		pg_malloc(sizeof(pivot_field) * num_columns);
+
+	array_rows = (pivot_field *)
+		pg_malloc(sizeof(pivot_field) * num_rows);
+
+	avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0);
+	avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0);
+
+	/*
+	 * Third part: optionally, process the ranking data for the horizontal
+	 * header
+	 */
+	if (sort_field_for_columns >= 0)
+		rankSort(num_columns, array_columns);
+
+	/*
+	 * Fourth part: print the crosstab'ed results.
+	 */
+	printCrosstab(res,
+				  num_columns, array_columns, field_for_columns,
+				  num_rows, array_rows, field_for_rows,
+				  num_colsG, colsG);
+
+	retval = true;
+
+error_return:
+	avlFree(&piv_columns, piv_columns.root);
+	avlFree(&piv_rows, piv_rows.root);
+	pg_free(array_columns);
+	pg_free(array_rows);
+	pg_free(colsV);
+	pg_free(colsH);
+	pg_free(colsG);
+
+	return retval;
+}
+
+/*
+ * Output the pivoted resultset with the printTable* functions
+ */
+static void
+printCrosstab(const PGresult *results,
+			  int num_columns, pivot_field *piv_columns, int field_for_columns,
+			  int num_rows, pivot_field *piv_rows, int field_for_rows,
+			  int num_colsG, int *colsG)
+{
+	printQueryOpt popt = pset.popt;
+	printTableContent cont;
+	int			i,
+				j,
+				rn;
+	char		col_align;
+	int		   *horiz_map;
+	char	  **allocated_cells;
+
+	printTableInit(&cont, &popt.topt, popt.title, num_columns + 1, num_rows);
+
+	/* Step 1: set target column names (horizontal header) */
+
+	/* The name of the first column is kept unchanged by the pivoting */
+	printTableAddHeader(&cont,
+						PQfname(results, field_for_rows),
+						false,
+						column_type_alignment(PQftype(results,
+													  field_for_rows)));
+
+	/*
+	 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse
+	 * map associating each piv_columns[].rank to its index in piv_columns.
+	 * This avoids an O(N^2) loop later.
+	 */
+	horiz_map = (int *) pg_malloc(sizeof(int) * num_columns);
+	for (i = 0; i < num_columns; i++)
+		horiz_map[piv_columns[i].rank] = i;
+
+	/*
+	 * In the common case of only one field projected into the cells, the
+	 * display alignment depends on its PQftype(). Otherwise the contents are
+	 * made-up strings, so use left alignment.
+	 */
+	col_align = num_colsG == 1 ?
+		column_type_alignment(PQftype(results, colsG[0])) : 'l';
+
+	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 : "");
+
+		printTableAddHeader(&cont, colname, false, col_align);
+	}
+	pg_free(horiz_map);
+
+	/* Step 2: set row names in the first output column (vertical header) */
+	for (i = 0; i < num_rows; i++)
+	{
+		int			k = piv_rows[i].rank;
+
+		cont.cells[k * (num_columns + 1)] = piv_rows[i].name ?
+			piv_rows[i].name :
+			(popt.nullPrint ? popt.nullPrint : "");
+		/* Initialize all cells inside the grid to an empty value */
+		for (j = 0; j < num_columns; j++)
+			cont.cells[k * (num_columns + 1) + j + 1] = "";
+	}
+	cont.cellsadded = num_rows * (num_columns + 1);
+
+	/*
+	 * Step 3: fill in the content cells.
+	 *
+	 * By the time this loop is done, each of the cells in cont.cells is either
+	 * a pointer into the PGresult which must not be freed (this happens if
+	 * there's a single value for that cell), or an allocated string where the
+	 * multiple values have been concatenated together.  In the latter case,
+	 * allocated_cells also contains the pointer, so that it can be freed after
+	 * we're done.
+	 */
+	allocated_cells = (char **)
+		pg_malloc0((num_rows + 1) * (num_columns + 1) * sizeof(char *));
+	for (rn = 0; rn < PQntuples(results); rn++)
+	{
+		int			row_number;
+		int			col_number;
+		pivot_field *p;
+		pivot_field elt;
+
+		/* Find target row */
+		if (!PQgetisnull(results, rn, field_for_rows))
+			elt.name = PQgetvalue(results, rn, field_for_rows);
+		else
+			elt.name = NULL;
+		p = (pivot_field *) bsearch(&elt,
+									piv_rows,
+									num_rows,
+									sizeof(pivot_field),
+									pivotFieldCompare);
+
+		row_number = p ? p->rank : -1;
+
+		/* Find target column */
+		if (!PQgetisnull(results, rn, field_for_columns))
+			elt.name = PQgetvalue(results, rn, field_for_columns);
+		else
+			elt.name = NULL;
+
+		p = (pivot_field *) bsearch(&elt,
+									piv_columns,
+									num_columns,
+									sizeof(pivot_field),
+									pivotFieldCompare);
+		col_number = p ? p->rank : -1;
+
+		/* Place value into cell */
+		if (col_number >= 0 && row_number >= 0)
+		{
+			int			idx;
+
+			/* index into the cont.cells and allocated_cells arrays */
+			idx = 1 + col_number + row_number * (num_columns + 1);
+
+			/*
+			 * special case: when the source has only 2 columns, use a X
+			 * (cross/checkmark) for the cell content.
+			 */
+			if (PQnfields(results) == 2)
+			{
+				cont.cells[idx] = "X";
+			}
+			else
+			{
+				for (i = 0; i < num_colsG; i++)
+				{
+					char	   *content;
+
+					content = !PQgetisnull(results, rn, colsG[i]) ?
+						PQgetvalue(results, rn, colsG[i]) :
+						(popt.nullPrint ? popt.nullPrint : "");
+
+					/*
+					 * If the cell already contains a value, concatenate the new
+					 * contents together with the previous value now.
+					 */
+					if (cont.cells[idx] != NULL)
+					{
+						char	   *new_content;
+
+						/*
+						 * Form the new contents by concatenating the value of the
+						 * current cell with the preexisting contents. Separate
+						 * multiple columns in the same row with a space; for the
+						 * first column of each row, separate with a newline
+						 * instead.
+						 */
+						if (allocated_cells[idx] != NULL)
+							new_content = psprintf("%s%s%s",
+												   allocated_cells[idx],
+												   i == 0 ? "\n" : " ",
+												   content);
+						else
+							new_content = psprintf("%s", content);
+
+						cont.cells[idx] = new_content;
+						if (allocated_cells[idx] != NULL)
+							pg_free(allocated_cells[idx]);
+						allocated_cells[idx] = new_content;
+					}
+					else
+					{
+						cont.cells[idx] = content;
+					}
+				}
+			}
+		}
+	}
+
+	printTable(&cont, pset.queryFout, false, pset.logfile);
+	printTableCleanup(&cont);
+
+	for (i = 0; i < num_rows * num_columns; i++)
+	{
+		if (allocated_cells[i] != NULL)
+			pg_free(allocated_cells[i]);
+	}
+
+	pg_free(allocated_cells);
+}
+
+/*
+ * Parse col1[<sep>col2][<sep>col3]...
+ * where colN can be:
+ * - a number from 1 to PQnfields(res)
+ * - an unquoted column name matching (case insensitively) one of PQfname(res,...)
+ * - a quoted column name matching (case sensitively) one of PQfname(res,...)
+ * max_columns: 0 if no maximum
+ */
+static int
+parseColumnRefs(char *arg,
+				PGresult *res,
+				int **col_numbers,
+				int max_columns,
+				char separator)
+{
+	char	   *p = arg;
+	char		c;
+	int			col_num = -1;
+	int			nb_cols = 0;
+	char	   *field_start = NULL;
+
+	*col_numbers = NULL;
+	while ((c = *p) != '\0')
+	{
+		bool		quoted_field = false;
+
+		field_start = p;
+
+		/* first char */
+		if (c == '"')
+		{
+			quoted_field = true;
+			p++;
+		}
+
+		while ((c = *p) != '\0')
+		{
+			if (c == separator && !quoted_field)
+				break;
+			if (c == '"')		/* end of field or embedded double quote */
+			{
+				p++;
+				if (*p == '"')
+				{
+					if (quoted_field)
+					{
+						p++;
+						continue;
+					}
+				}
+				else if (quoted_field && *p == separator)
+					break;
+			}
+			if (*p)
+				p += PQmblen(p, pset.encoding);
+		}
+
+		if (p != field_start)
+		{
+			/* look up the column and add its index into *col_numbers */
+			if (max_columns != 0 && nb_cols == max_columns)
+			{
+				psql_error(_("No more than %d column references expected\n"), max_columns);
+				goto errfail;
+			}
+			c = *p;
+			*p = '\0';
+			col_num = indexOfColumn(field_start, res);
+			*p = c;
+			if (col_num < 0)
+				goto errfail;
+			*col_numbers = (int *) pg_realloc(*col_numbers, (1 + nb_cols) * sizeof(int));
+			(*col_numbers)[nb_cols++] = col_num;
+		}
+		else
+		{
+			psql_error(_("Empty column reference\n"));
+			goto errfail;
+		}
+
+		if (*p)
+			p += PQmblen(p, pset.encoding);
+	}
+	return nb_cols;
+
+errfail:
+	pg_free(*col_numbers);
+	*col_numbers = NULL;
+	return -1;
+}
+
+/*
+ * The avl* functions below provide a minimalistic implementation of AVL binary
+ * trees, to efficiently collect the distinct values that will form the horizontal
+ * and vertical headers. It only supports adding new values, no removal or even
+ * search.
+ */
+static void
+avlInit(avl_tree *tree)
+{
+	tree->end = (avl_node *) pg_malloc0(sizeof(avl_node));
+	tree->end->childs[0] = tree->end->childs[1] = tree->end;
+	tree->count = 0;
+	tree->root = tree->end;
+}
+
+/* Deallocate recursively an AVL tree, starting from node */
+static void
+avlFree(avl_tree *tree, avl_node *node)
+{
+	if (node->childs[0] != tree->end)
+	{
+		avlFree(tree, node->childs[0]);
+		pg_free(node->childs[0]);
+	}
+	if (node->childs[1] != tree->end)
+	{
+		avlFree(tree, node->childs[1]);
+		pg_free(node->childs[1]);
+	}
+	if (node == tree->root)
+	{
+		/* free the root separately as it's not child of anything */
+		if (node != tree->end)
+			pg_free(node);
+		/* free the tree->end struct only once and when all else is freed */
+		pg_free(tree->end);
+	}
+}
+
+/* Set the height to 1 plus the greatest of left and right heights */
+static void
+avlUpdateHeight(avl_node *n)
+{
+	n->height = 1 + (n->childs[0]->height > n->childs[1]->height ?
+					 n->childs[0]->height :
+					 n->childs[1]->height);
+}
+
+/* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
+static avl_node *
+avlRotate(avl_node **current, int dir)
+{
+	avl_node   *before = *current;
+	avl_node   *after = (*current)->childs[dir];
+
+	*current = after;
+	before->childs[dir] = after->childs[!dir];
+	avlUpdateHeight(before);
+	after->childs[!dir] = before;
+
+	return after;
+}
+
+static int
+avlBalance(avl_node *n)
+{
+	return n->childs[0]->height - n->childs[1]->height;
+}
+
+/*
+ * After an insertion, possibly rebalance the tree so that the left and right
+ * node heights don't differ by more than 1.
+ * May update *node.
+ */
+static void
+avlAdjustBalance(avl_tree *tree, avl_node **node)
+{
+	avl_node   *current = *node;
+	int			b = avlBalance(current) / 2;
+
+	if (b != 0)
+	{
+		int			dir = (1 - b) / 2;
+
+		if (avlBalance(current->childs[dir]) == -b)
+			avlRotate(&current->childs[dir], !dir);
+		current = avlRotate(node, dir);
+	}
+	if (current != tree->end)
+		avlUpdateHeight(current);
+}
+
+/*
+ * Insert a new value/field, starting from *node, reaching the correct position
+ * in the tree by recursion.  Possibly rebalance the tree and possibly update
+ * *node.  Do nothing if the value is already present in the tree.
+ */
+static void
+avlInsertNode(avl_tree *tree, avl_node **node, pivot_field field)
+{
+	avl_node   *current = *node;
+
+	if (current == tree->end)
+	{
+		avl_node   *new_node = (avl_node *)
+		pg_malloc(sizeof(avl_node));
+
+		new_node->height = 1;
+		new_node->field = field;
+		new_node->childs[0] = new_node->childs[1] = tree->end;
+		tree->count++;
+		*node = new_node;
+	}
+	else
+	{
+		int			cmp = pivotFieldCompare(&field, &current->field);
+
+		if (cmp != 0)
+		{
+			avlInsertNode(tree,
+						  cmp > 0 ? &current->childs[1] : &current->childs[0],
+						  field);
+			avlAdjustBalance(tree, node);
+		}
+	}
+}
+
+/* Insert the value into the AVL tree, if it does not preexist */
+static void
+avlMergeValue(avl_tree *tree, char *name, char *sort_value)
+{
+	pivot_field field;
+
+	field.name = name;
+	field.rank = tree->count;
+	field.sort_value = sort_value;
+	avlInsertNode(tree, &tree->root, field);
+}
+
+/*
+ * Recursively extract node values into the names array, in sorted order with a
+ * left-to-right tree traversal.
+ * Return the next candidate offset to write into the names array.
+ * fields[] must be preallocated to hold tree->count entries
+ */
+static int
+avlCollectFields(avl_tree *tree, avl_node *node, pivot_field *fields, int idx)
+{
+	if (node == tree->end)
+		return idx;
+
+	idx = avlCollectFields(tree, node->childs[0], fields, idx);
+	fields[idx] = node->field;
+	return avlCollectFields(tree, node->childs[1], fields, idx + 1);
+}
+
+static void
+rankSort(int num_columns, pivot_field *piv_columns)
+{
+	int		   *hmap;			/* [[offset in piv_columns, rank], ...for
+								 * every header entry] */
+	int			i;
+
+	hmap = (int *) pg_malloc(sizeof(int) * num_columns * 2);
+	for (i = 0; i < num_columns; i++)
+	{
+		char	   *val = piv_columns[i].sort_value;
+
+		/* ranking information is valid if non null and matches /^-?\d+$/ */
+		if (val &&
+			((*val == '-' &&
+			  strspn(val + 1, "0123456789") == strlen(val + 1)) ||
+			 strspn(val, "0123456789") == strlen(val)))
+		{
+			hmap[i * 2] = atoi(val);
+			hmap[i * 2 + 1] = i;
+		}
+		else
+		{
+			/* invalid rank information ignored (equivalent to rank 0) */
+			hmap[i * 2] = 0;
+			hmap[i * 2 + 1] = i;
+		}
+	}
+
+	qsort(hmap, num_columns, sizeof(int) * 2, rankCompare);
+
+	for (i = 0; i < num_columns; i++)
+	{
+		piv_columns[hmap[i * 2 + 1]].rank = i;
+	}
+
+	pg_free(hmap);
+}
+
+/*
+ * Compare a user-supplied argument against a field name obtained by PQfname(),
+ * which is already case-folded.
+ * If arg is not enclosed in double quotes, pg_strcasecmp applies, otherwise
+ * do a case-sensitive comparison with these rules:
+ * - double quotes enclosing 'arg' are filtered out
+ * - double quotes inside 'arg' are expected to be doubled
+ */
+static bool
+fieldNameEquals(const char *arg, const char *fieldname)
+{
+	const char *p = arg;
+	const char *f = fieldname;
+	char		c;
+
+	if (*p++ != '"')
+		return !pg_strcasecmp(arg, fieldname);
+
+	while ((c = *p++))
+	{
+		if (c == '"')
+		{
+			if (*p == '"')
+				p++;			/* skip second quote and continue */
+			else if (*p == '\0')
+				return (*f == '\0');	/* p is shorter than f, or is
+										 * identical */
+		}
+		if (*f == '\0')
+			return false;		/* f is shorter than p */
+		if (c != *f)			/* found one byte that differs */
+			return false;
+		f++;
+	}
+	return (*f == '\0');
+}
+
+/*
+ * arg can be a number or a column name, possibly quoted (like in an ORDER BY clause)
+ * Returns:
+ *	on success, the 0-based index of the column
+ *	or -1 if the column number or name is not found in the result's structure,
+ *		  or if it's ambiguous (arg corresponding to several columns)
+ */
+static int
+indexOfColumn(const char *arg, PGresult *res)
+{
+	int			idx;
+
+	if (strspn(arg, "0123456789") == strlen(arg))
+	{
+		/* if arg contains only digits, it's a column number */
+		idx = atoi(arg) - 1;
+		if (idx < 0 || idx >= PQnfields(res))
+		{
+			psql_error(_("Invalid column number: %s\n"), arg);
+			return -1;
+		}
+	}
+	else
+	{
+		int			i;
+
+		idx = -1;
+		for (i = 0; i < PQnfields(res); i++)
+		{
+			if (fieldNameEquals(arg, PQfname(res, i)))
+			{
+				if (idx >= 0)
+				{
+					/* if another idx was already found for the same name */
+					psql_error(_("Ambiguous column name: %s\n"), arg);
+					return -1;
+				}
+				idx = i;
+			}
+		}
+		if (idx == -1)
+		{
+			psql_error(_("Invalid column name: %s\n"), arg);
+			return -1;
+		}
+	}
+	return idx;
+}
+
+/*
+ * Value comparator for vertical and horizontal headers
+ * used for deduplication only.
+ * - null values are considered equal
+ * - non-null < null
+ * - non-null values are compared with strcmp()
+ */
+static int
+pivotFieldCompare(const void *a, const void *b)
+{
+	pivot_field *pa = (pivot_field *) a;
+	pivot_field *pb = (pivot_field *) b;
+
+	/* test null values */
+	if (!pb->name)
+		return pa->name ? -1 : 0;
+	else if (!pa->name)
+		return 1;
+
+	/* non-null values */
+	return strcmp(((pivot_field *) a)->name,
+				  ((pivot_field *) b)->name);
+}
+
+static int
+rankCompare(const void *a, const void *b)
+{
+	return *((int *) a) - *((int *) b);
+}
diff --git a/src/bin/psql/crosstabview.h b/src/bin/psql/crosstabview.h
new file mode 100644
index 0000000..4eb52a7
--- /dev/null
+++ b/src/bin/psql/crosstabview.h
@@ -0,0 +1,26 @@
+/*
+ * psql - the PostgreSQL interactive terminal
+ *
+ * Copyright (c) 2000-2016, PostgreSQL Global Development Group
+ *
+ * src/bin/psql/crosstabview.h
+ */
+
+#ifndef CROSSTABVIEW_H
+#define CROSSTABVIEW_H
+
+/*
+ * Limit the number of output columns generated in memory by the crosstabview
+ * algorithm. A new output column is added for each distinct value found in the
+ * column that pivots (to form the horizontal header).
+ * The purpose of this limit is to fail early instead of over-allocating or spending
+ * too much time if the crosstab to generate happens to be unreasonably large
+ * (worst case: a NxN cartesian product with N=number of tuples).
+ * The value of 1600 corresponds to the maximum columns per table in storage,
+ * but it could be as much as INT_MAX theorically.
+ */
+#define CROSSTABVIEW_MAX_COLUMNS 1600
+
+/* prototypes */
+extern bool PrintResultsInCrossTab(PGresult *res);
+#endif   /* CROSSTABVIEW_H */
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 7549451..96e5628 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -177,6 +177,7 @@ slashUsage(unsigned short int pager)
 	fprintf(output, _("  \\gexec                 execute query, then execute each value in its result\n"));
 	fprintf(output, _("  \\gset [PREFIX]         execute query and store results in psql variables\n"));
 	fprintf(output, _("  \\q                     quit psql\n"));
+	fprintf(output, _("  \\crosstabview [COLUMNS] execute query and display results in crosstab\n"));
 	fprintf(output, _("  \\watch [SEC]           execute query every SEC seconds\n"));
 	fprintf(output, "\n");
 
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index c69f6ba..9340ef2 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -93,6 +93,11 @@ typedef struct _psqlSettings
 	char	   *gfname;			/* one-shot file output argument for \g */
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
 	bool		gexec_flag;		/* one-shot flag to execute query's results */
+	bool		crosstabview_output;	/* one-shot request to print results
+										 * in crosstab */
+	char	   *crosstabview_col_V;		/* one-shot \crosstabview 1st argument */
+	char	   *crosstabview_col_H;		/* one-shot \crosstabview 2nd argument */
+	char	   *crosstabview_cols_grid; /* one-shot \crosstabview 3nd argument */
 
 	bool		notty;			/* stdin or stdout is not a tty (as determined
 								 * on startup) */
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index cb8a06d..5c10005 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1274,7 +1274,8 @@ psql_completion(const char *text, int start, int end)
 
 	/* psql's backslash commands. */
 	static const char *const backslash_commands[] = {
-		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy", "\\copyright",
+		"\\a", "\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
+		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\db", "\\dc", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index 30efd3f..1ec74f1 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -3295,30 +3295,9 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 
 	for (i = 0; i < cont.ncolumns; i++)
 	{
-		char		align;
-		Oid			ftype = PQftype(result, i);
-
-		switch (ftype)
-		{
-			case INT2OID:
-			case INT4OID:
-			case INT8OID:
-			case FLOAT4OID:
-			case FLOAT8OID:
-			case NUMERICOID:
-			case OIDOID:
-			case XIDOID:
-			case CIDOID:
-			case CASHOID:
-				align = 'r';
-				break;
-			default:
-				align = 'l';
-				break;
-		}
-
 		printTableAddHeader(&cont, PQfname(result, i),
-							opt->translate_header, align);
+							opt->translate_header,
+							column_type_alignment(PQftype(result, i)));
 	}
 
 	/* set cells */
@@ -3360,6 +3339,31 @@ printQuery(const PGresult *result, const printQueryOpt *opt,
 	printTableCleanup(&cont);
 }
 
+char
+column_type_alignment(Oid ftype)
+{
+	char		align;
+
+	switch (ftype)
+	{
+		case INT2OID:
+		case INT4OID:
+		case INT8OID:
+		case FLOAT4OID:
+		case FLOAT8OID:
+		case NUMERICOID:
+		case OIDOID:
+		case XIDOID:
+		case CIDOID:
+		case CASHOID:
+			align = 'r';
+			break;
+		default:
+			align = 'l';
+			break;
+	}
+	return align;
+}
 
 void
 setDecimalLocale(void)
diff --git a/src/include/fe_utils/print.h b/src/include/fe_utils/print.h
index ff90237..18aee93 100644
--- a/src/include/fe_utils/print.h
+++ b/src/include/fe_utils/print.h
@@ -206,6 +206,8 @@ extern void printTable(const printTableContent *cont,
 extern void printQuery(const PGresult *result, const printQueryOpt *opt,
 		   FILE *fout, bool is_pager, FILE *flog);
 
+extern char column_type_alignment(Oid);
+
 extern void setDecimalLocale(void);
 extern const printTextFormat *get_line_style(const printTableOpt *opt);
 extern void refresh_utf8format(const printTableOpt *opt);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e293fc0..de903a0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1,3 +1,7 @@
+EditableObjectType
+pivot_field
+avl_tree
+avl_node
 ABITVEC
 ACCESS_ALLOWED_ACE
 ACL
#121Daniel Verite
daniel@manitou-mail.org
In reply to: Alvaro Herrera (#120)
1 attachment(s)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

I messed with that code some more, as it looked unnecessarily
complicated; please see attached and verify that it still behaves
sanely. This needs those regression tests you promised. I tested a few
cases and it seems good to me.

I've fixed a couple things over v16:
- avoid passing every cell through psprintf, which happened due
to cont.cells being pre-initialized to empty strings.
- adjusted the loop freeing allocated_cells

and added the regression tests.

Attached is the diff over v16, tested with make check and valgrind.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

Attachments:

psql-crosstabview-diff-over-v16.difftext/x-patch; name=psql-crosstabview-diff-over-v16.diffDownload
diff --git a/src/bin/psql/crosstabview.c b/src/bin/psql/crosstabview.c
index 0d70e47..a20296e 100644
--- a/src/bin/psql/crosstabview.c
+++ b/src/bin/psql/crosstabview.c
@@ -360,7 +360,6 @@ printCrosstab(const PGresult *results,
 	printQueryOpt popt = pset.popt;
 	printTableContent cont;
 	int			i,
-				j,
 				rn;
 	char		col_align;
 	int		   *horiz_map;
@@ -414,9 +413,6 @@ printCrosstab(const PGresult *results,
 		cont.cells[k * (num_columns + 1)] = piv_rows[i].name ?
 			piv_rows[i].name :
 			(popt.nullPrint ? popt.nullPrint : "");
-		/* Initialize all cells inside the grid to an empty value */
-		for (j = 0; j < num_columns; j++)
-			cont.cells[k * (num_columns + 1) + j + 1] = "";
 	}
 	cont.cellsadded = num_rows * (num_columns + 1);
 
@@ -506,14 +502,10 @@ printCrosstab(const PGresult *results,
 						 * first column of each row, separate with a newline
 						 * instead.
 						 */
-						if (allocated_cells[idx] != NULL)
-							new_content = psprintf("%s%s%s",
-												   allocated_cells[idx],
-												   i == 0 ? "\n" : " ",
-												   content);
-						else
-							new_content = psprintf("%s", content);
-
+						new_content = psprintf("%s%s%s",
+											   cont.cells[idx],
+											   i == 0 ? "\n" : " ",
+											   content);
 						cont.cells[idx] = new_content;
 						if (allocated_cells[idx] != NULL)
 							pg_free(allocated_cells[idx]);
@@ -528,10 +520,20 @@ printCrosstab(const PGresult *results,
 		}
 	}
 
+	/*
+	 * The non-initialized cells must be set to an empty string for the print
+	 * functions
+	 */
+	for (i = 0; i < cont.cellsadded; i++)
+	{
+		if (cont.cells[i] == NULL)
+			cont.cells[i] = "";
+	}
+
 	printTable(&cont, pset.queryFout, false, pset.logfile);
 	printTableCleanup(&cont);
 
-	for (i = 0; i < num_rows * num_columns; i++)
+	for (i = 0; i < (num_rows + 1) * (num_columns + 1); i++)
 	{
 		if (allocated_cells[i] != NULL)
 			pg_free(allocated_cells[i]);
diff --git a/src/test/regress/expected/psql_crosstabview.out b/src/test/regress/expected/psql_crosstabview.out
new file mode 100644
index 0000000..df3824a
--- /dev/null
+++ b/src/test/regress/expected/psql_crosstabview.out
@@ -0,0 +1,158 @@
+--
+-- tests for \crosstabview
+--
+CREATE VIEW vct_data as 
+select * from ( values
+   ('v1','h2','foo', 3, '2015-04-01'::date),
+   ('v2','h1','bar', 3, '2015-01-02'),
+   ('v1','h0','baz', NULL, '2015-07-12'),
+   ('v0','h4','qux', 4, '2015-07-15'),
+   ('v0','h4','dbl', -3, '2014-12-15'),
+   ('v0',NULL,'qux', 5, '2014-03-15')
+ ) as l(v,h,c,i,d);
+-- 2 columns with implicit 'X' as 3rd column
+select v,i from vct_data order by 1,2 \crosstabview v i
+ v  | -3 | 4 | 5 | 3 |   
+----+----+---+---+---+---
+ v0 | X  | X | X |   | 
+ v1 |    |   |   | X | X
+ v2 |    |   |   | X | 
+(3 rows)
+
+-- basic usage with 3 columns
+select v, extract(year from d),count(*) from vct_data
+ group by 1, 2 order by 1,2
+ \crosstabview
+ v  | 2014 | 2015 
+----+------+------
+ v0 |    2 |    1
+ v1 |      |    2
+ v2 |      |    1
+(3 rows)
+
+-- ordered months in horizontal header, enclosed column name
+select v, to_char(d,'Mon') as "month name", extract(month from d) as num,
+ count(*) from vct_data  group by 1,2,3 order by 1
+ \crosstabview v "month name":num 4
+ v  | Jan | Mar | Apr | Jul | Dec 
+----+-----+-----+-----+-----+-----
+ v0 |     |   1 |     |   1 |   1
+ v1 |     |     |   1 |   1 |    
+ v2 |   1 |     |     |     |    
+(3 rows)
+
+-- combine contents vertically into the same cell (V/H duplicates)
+select v,h,c from vct_data order by 1,2,3
+ \crosstabview 1 2 3
+ v  | h4  |     | h0  | h2  | h1  
+----+-----+-----+-----+-----+-----
+ v0 | dbl+| qux |     |     | 
+    | qux |     |     |     | 
+ v1 |     |     | baz | foo | 
+ v2 |     |     |     |     | bar
+(3 rows)
+
+-- horizontal ASC order from window function
+select v,h,c, row_number() over(order by h) as r from vct_data order by 1,3,2
+ \crosstabview v h:r c
+ v  | h0  | h1  | h2  | h4  |     
+----+-----+-----+-----+-----+-----
+ v0 |     |     |     | dbl+| qux
+    |     |     |     | qux | 
+ v1 | baz |     | foo |     | 
+ v2 |     | bar |     |     | 
+(3 rows)
+
+-- horizontal DESC order from window function
+select v,h,c, row_number() over(order by h DESC) as r from vct_data order by 1,3,2
+ \crosstabview v h:r c
+ v  |     | h4  | h2  | h1  | h0  
+----+-----+-----+-----+-----+-----
+ v0 | qux | dbl+|     |     | 
+    |     | qux |     |     | 
+ v1 |     |     | foo |     | baz
+ v2 |     |     |     | bar | 
+(3 rows)
+
+-- horizontal ASC order from window function, NULLs pushed rightmost
+select v,h,c, row_number() over(order by h nulls last) as r from vct_data order by 1,3,2
+ \crosstabview v h:r c
+ v  | h0  | h1  | h2  | h4  |     
+----+-----+-----+-----+-----+-----
+ v0 |     |     |     | dbl+| qux
+    |     |     |     | qux | 
+ v1 | baz |     | foo |     | 
+ v2 |     | bar |     |     | 
+(3 rows)
+
+-- only null, no column name, 2 columns
+select null,null \crosstabview
+ ?column? |   
+----------+---
+          | X
+(1 row)
+
+-- only null, no column name, 3 columns
+select null,null,null \crosstabview
+ ?column? |  
+----------+--
+          | 
+(1 row)
+
+-- null combined with cell contents
+\pset null '#null#'
+select v,h,c,i from vct_data order by h,v
+ \crosstabview
+ v  |     h0     |  h1   |  h2   |   h4   | #null# 
+----+------------+-------+-------+--------+--------
+ v1 | baz #null# |       | foo 3 |        | 
+ v2 |            | bar 3 |       |        | 
+ v0 |            |       |       | qux 4 +| qux 5
+    |            |       |       | dbl -3 | 
+(3 rows)
+
+\pset null ''
+-- refer to columns by position
+select v,h,i,c from vct_data order by h,v
+ \crosstabview 2 1 4
+ h  | v1  | v2  | v0  
+----+-----+-----+-----
+ h0 | baz |     | 
+ h1 |     | bar | 
+ h2 | foo |     | 
+ h4 |     |     | qux+
+    |     |     | dbl
+    |     |     | qux
+(5 rows)
+
+-- refer to columns by positions and names mixed
+select v,h,i,c from vct_data order by h,v
+ \crosstabview 1 "h" 4
+ v  | h0  | h1  | h2  | h4  |     
+----+-----+-----+-----+-----+-----
+ v1 | baz |     | foo |     | 
+ v2 |     | bar |     |     | 
+ v0 |     |     |     | qux+| qux
+    |     |     |     | dbl | 
+(3 rows)
+
+-- error: bad column name
+select v,h,c,i from vct_data
+ \crosstabview v h j
+Invalid column name: j
+-- error: bad column number
+select v,h,i,c from vct_data
+ \crosstabview 2 1 5
+Invalid column number: 5
+-- error: same H and V columns
+select v,h,i,c from vct_data
+ \crosstabview 2 h 4
+The same column cannot be used for both vertical and horizontal headers
+-- error: too many columns
+select a,a,1 from generate_series(1,3000) as a
+ \crosstabview
+Maximum number of columns (1600) exceeded
+-- error: only one column
+select 1 \crosstabview
+The query must return at least two columns to be shown in crosstab
+DROP VIEW vct_data;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 7c7b58d..a398c6b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -89,7 +89,7 @@ test: brin gin gist spgist privileges security_label collate matview lock replic
 # ----------
 # Another group of parallel tests
 # ----------
-test: alter_generic alter_operator misc psql async dbsize misc_functions
+test: alter_generic alter_operator misc psql psql_crosstabview async dbsize misc_functions
 
 # rules cannot run concurrently with any test that creates a view
 test: rules
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 1b66516..8cbffe6 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -119,6 +119,7 @@ test: alter_generic
 test: alter_operator
 test: misc
 test: psql
+test: psql_crosstabview
 test: async
 test: dbsize
 test: misc_functions
diff --git a/src/test/regress/sql/psql_crosstabview.sql b/src/test/regress/sql/psql_crosstabview.sql
new file mode 100644
index 0000000..48a7fe1
--- /dev/null
+++ b/src/test/regress/sql/psql_crosstabview.sql
@@ -0,0 +1,83 @@
+--
+-- tests for \crosstabview
+--
+
+CREATE VIEW vct_data as
+select * from ( values
+   ('v1','h2','foo', 3, '2015-04-01'::date),
+   ('v2','h1','bar', 3, '2015-01-02'),
+   ('v1','h0','baz', NULL, '2015-07-12'),
+   ('v0','h4','qux', 4, '2015-07-15'),
+   ('v0','h4','dbl', -3, '2014-12-15'),
+   ('v0',NULL,'qux', 5, '2014-03-15')
+ ) as l(v,h,c,i,d);
+
+-- 2 columns with implicit 'X' as 3rd column
+select v,i from vct_data order by 1,2 \crosstabview v i
+
+-- basic usage with 3 columns
+select v, extract(year from d),count(*) from vct_data
+ group by 1, 2 order by 1,2
+ \crosstabview
+
+-- ordered months in horizontal header, enclosed column name
+select v, to_char(d,'Mon') as "month name", extract(month from d) as num,
+ count(*) from vct_data  group by 1,2,3 order by 1
+ \crosstabview v "month name":num 4
+
+-- combine contents vertically into the same cell (V/H duplicates)
+select v,h,c from vct_data order by 1,2,3
+ \crosstabview 1 2 3
+
+-- horizontal ASC order from window function
+select v,h,c, row_number() over(order by h) as r from vct_data order by 1,3,2
+ \crosstabview v h:r c
+
+-- horizontal DESC order from window function
+select v,h,c, row_number() over(order by h DESC) as r from vct_data order by 1,3,2
+ \crosstabview v h:r c
+
+-- horizontal ASC order from window function, NULLs pushed rightmost
+select v,h,c, row_number() over(order by h nulls last) as r from vct_data order by 1,3,2
+ \crosstabview v h:r c
+
+-- only null, no column name, 2 columns
+select null,null \crosstabview
+
+-- only null, no column name, 3 columns
+select null,null,null \crosstabview
+
+-- null combined with cell contents
+\pset null '#null#'
+select v,h,c,i from vct_data order by h,v
+ \crosstabview
+\pset null ''
+
+-- refer to columns by position
+select v,h,i,c from vct_data order by h,v
+ \crosstabview 2 1 4
+
+-- refer to columns by positions and names mixed
+select v,h,i,c from vct_data order by h,v
+ \crosstabview 1 "h" 4
+
+-- error: bad column name
+select v,h,c,i from vct_data
+ \crosstabview v h j
+
+-- error: bad column number
+select v,h,i,c from vct_data
+ \crosstabview 2 1 5
+
+-- error: same H and V columns
+select v,h,i,c from vct_data
+ \crosstabview 2 h 4
+
+-- error: too many columns
+select a,a,1 from generate_series(1,3000) as a
+ \crosstabview
+
+-- error: only one column
+select 1 \crosstabview
+
+DROP VIEW vct_data;
#122Daniel Verite
daniel@manitou-mail.org
In reply to: Alvaro Herrera (#117)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

I wonder if the business of appending values of multiple columns
separated with spaces is doing us any good. Why not require that
there's a single column in the cell? If the user wants to put things
together, they can use format() or just || the fields together. What
benefit is there to the ' '? When I ran my first test queries over
pg_class I was surprised about this behavior:
alvherre=# select * from pg_class
alvherre=# \crosstabview relnatts relkind

ISTM that this could be avoided by erroring out for lack of an
explicit 3rd column as argument. IOW, we wouldn't assume
that "no column specified" means "show all columns".

About simply ripping out the possibility of having multiple
columns into cells, it's more radical but if that part turns out to
be more confusing than useful, I don't have a problem
with removing it.

The other case of stringing multiple contents into the same cell
is when different tuples carry (row,column) duplicates.
I'm not inclined to disallow that case, I think it would go too far
in guessing what the user expects.
My expectation for a viewer is that it displays the results as far as
possible, whatever they are.
Also, showing such contents in vertically-growing cells as it
does now allows the user to spot these easily in the grid when
they happen to be outliers. I'm seeing it as useful in that case.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#123Robert Haas
robertmhaas@gmail.com
In reply to: Daniel Verite (#122)
Re: [patch] Proposal for \crosstabview in psql

On Fri, Apr 8, 2016 at 7:23 AM, Daniel Verite <daniel@manitou-mail.org> wrote:

Alvaro Herrera wrote:

I wonder if the business of appending values of multiple columns
separated with spaces is doing us any good. Why not require that
there's a single column in the cell? If the user wants to put things
together, they can use format() or just || the fields together. What
benefit is there to the ' '? When I ran my first test queries over
pg_class I was surprised about this behavior:
alvherre=# select * from pg_class
alvherre=# \crosstabview relnatts relkind

ISTM that this could be avoided by erroring out for lack of an
explicit 3rd column as argument. IOW, we wouldn't assume
that "no column specified" means "show all columns".

About simply ripping out the possibility of having multiple
columns into cells, it's more radical but if that part turns out to
be more confusing than useful, I don't have a problem
with removing it.

The other case of stringing multiple contents into the same cell
is when different tuples carry (row,column) duplicates.
I'm not inclined to disallow that case, I think it would go too far
in guessing what the user expects.
My expectation for a viewer is that it displays the results as far as
possible, whatever they are.
Also, showing such contents in vertically-growing cells as it
does now allows the user to spot these easily in the grid when
they happen to be outliers. I'm seeing it as useful in that case.

This seems like it might be converging on some sort of consensus, but
I'm wondering if we shouldn't push it to 9.7, instead of rushing
decisions that we will later have trouble changing on
backward-compatibility grounds.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#124Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Robert Haas (#123)
Re: [patch] Proposal for \crosstabview in psql

Robert Haas wrote:

This seems like it might be converging on some sort of consensus, but
I'm wondering if we shouldn't push it to 9.7, instead of rushing
decisions that we will later have trouble changing on
backward-compatibility grounds.

My intention is to commit this afternoon in the next couple of hours,
and only the most basic case is going to be supported, and the rest of
the cases (concatenation of several fields and several rows, etc) are
just going to throw errors; that way, it will be easy to add more
features later as they are agreed upon.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#125Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Daniel Verite (#122)
Re: [patch] Proposal for \crosstabview in psql

Daniel Verite wrote:

ISTM that this could be avoided by erroring out for lack of an
explicit 3rd column as argument. IOW, we wouldn't assume
that "no column specified" means "show all columns".

About simply ripping out the possibility of having multiple
columns into cells, it's more radical but if that part turns out to
be more confusing than useful, I don't have a problem
with removing it.

Okay, I've ripped that out since I wasn't comfortable with the general
idea. Once you have two data values for the same cell, the new code
raises an error, indicating the corresponding vertical and horizontal
header values; that way it's easy to spot where the problem is.

I also removed the FETCH_COUNT bits; it didn't make a lot of sense to
me. Like \gexec, the query is executed to completion when in
\crosstabview regardless of FETCH_COUNT.

The other case of stringing multiple contents into the same cell
is when different tuples carry (row,column) duplicates.
I'm not inclined to disallow that case, I think it would go too far
in guessing what the user expects.
My expectation for a viewer is that it displays the results as far as
possible, whatever they are.

The reason I made this case throw an error is that we can tweak the
behavior later. I think separating them with newlines is too cute and
will be unusable when you have values that have embedded newlines; you
can imitate that behavior with string_agg(val, E'\n') as I've done in
the regression tests. One option for improving it would be to have it
add another record, but that requires shifting the values of all cells
by the number of columns (you can see that if you change the border
options, or in HTML output etc). We can do that later.

Also, showing such contents in vertically-growing cells as it
does now allows the user to spot these easily in the grid when
they happen to be outliers. I'm seeing it as useful in that case.

It's useful, no doubt.

I pushed it.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#126Daniel Verite
daniel@manitou-mail.org
In reply to: Alvaro Herrera (#125)
Re: [patch] Proposal for \crosstabview in psql

Alvaro Herrera wrote:

I pushed it.

That's awesome, thanks! Also thanks to Pavel who reviewed and helped
continuously when iterating on this feature, and all others who
participed in this thread.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#127Christoph Berg
myon@debian.org
In reply to: Alvaro Herrera (#125)
1 attachment(s)
[patch] \crosstabview documentation

Re: Alvaro Herrera 2016-04-09 <20160408232553.GA721890@alvherre.pgsql>

It's useful, no doubt.

It's cool :)

I pushed it.

Here's a small doc patch that removes the bogus space in "colH [:scolH]"
(otherwise psql complains that it is ignoring the 4th parameter.

It also adds an index entry and adds a note to the old crosstab
functions to make people aware of \crosstabview.

Christoph

Attachments:

crosstabview.patchtext/x-diff; charset=us-asciiDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
new file mode 100644
index 1f07956..e6b5a7e
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** testdb=&gt;
*** 990,999 ****
        </varlistentry>
  
        <varlistentry>
          <term><literal>\crosstabview [
              <replaceable class="parameter">colV</replaceable>
!             <replaceable class="parameter">colH</replaceable>
!             [:<replaceable class="parameter">scolH</replaceable>]
              [<replaceable class="parameter">colD</replaceable>]
              ] </literal></term>
          <listitem>
--- 990,1002 ----
        </varlistentry>
  
        <varlistentry>
+         <indexterm>
+           <primary>crosstabview</primary>
+         </indexterm>
+ 
          <term><literal>\crosstabview [
              <replaceable class="parameter">colV</replaceable>
!             <replaceable class="parameter">colH</replaceable>[:<replaceable class="parameter">scolH</replaceable>]
              [<replaceable class="parameter">colD</replaceable>]
              ] </literal></term>
          <listitem>
diff --git a/doc/src/sgml/tablefunc.sgml b/doc/src/sgml/tablefunc.sgml
new file mode 100644
index 1d8423d..02653d5
*** a/doc/src/sgml/tablefunc.sgml
--- b/doc/src/sgml/tablefunc.sgml
*************** row2    val21   val22   val23   ...
*** 192,197 ****
--- 192,204 ----
      calling query).
     </para>
  
+    <note>
+    <para>
+    The psql <command>\crosstabview</command> provides similar functionality
+    that is easier to use (albeit not on the SQL level).
+    </para>
+    </note>
+ 
     <para>
      For example, the provided query might produce a set something like:
  <programlisting>
#128Christoph Berg
myon@debian.org
In reply to: Christoph Berg (#127)
1 attachment(s)
Re: [patch] \crosstabview documentation

Re: To PostgreSQL Hackers 2016-04-13 <20160413092312.GA21485@msg.df7cb.de>

Re: Alvaro Herrera 2016-04-09 <20160408232553.GA721890@alvherre.pgsql>

It's useful, no doubt.

It's cool :)

I pushed it.

Here's a small doc patch that removes the bogus space in "colH [:scolH]"
(otherwise psql complains that it is ignoring the 4th parameter.

It also adds an index entry and adds a note to the old crosstab
functions to make people aware of \crosstabview.

I should save before diffing, here's the version I actually wanted to
submit ...

Christoph

Attachments:

crosstabview.patch2text/plain; charset=us-asciiDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
new file mode 100644
index 1f07956..e6b5a7e
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** testdb=&gt;
*** 990,999 ****
        </varlistentry>
  
        <varlistentry>
          <term><literal>\crosstabview [
              <replaceable class="parameter">colV</replaceable>
!             <replaceable class="parameter">colH</replaceable>
!             [:<replaceable class="parameter">scolH</replaceable>]
              [<replaceable class="parameter">colD</replaceable>]
              ] </literal></term>
          <listitem>
--- 990,1002 ----
        </varlistentry>
  
        <varlistentry>
+         <indexterm>
+           <primary>crosstabview</primary>
+         </indexterm>
+ 
          <term><literal>\crosstabview [
              <replaceable class="parameter">colV</replaceable>
!             <replaceable class="parameter">colH</replaceable>[:<replaceable class="parameter">scolH</replaceable>]
              [<replaceable class="parameter">colD</replaceable>]
              ] </literal></term>
          <listitem>
diff --git a/doc/src/sgml/tablefunc.sgml b/doc/src/sgml/tablefunc.sgml
new file mode 100644
index 1d8423d..53b2a89
*** a/doc/src/sgml/tablefunc.sgml
--- b/doc/src/sgml/tablefunc.sgml
*************** row2    val21   val22   val23   ...
*** 192,197 ****
--- 192,204 ----
      calling query).
     </para>
  
+    <note>
+    <para>
+    The <command>\crosstabview</command> psql command provides similar
+    functionality that is easier to use (albeit not on the SQL level).
+    </para>
+    </note>
+ 
     <para>
      For example, the provided query might produce a set something like:
  <programlisting>
#129Tom Lane
tgl@sss.pgh.pa.us
In reply to: Christoph Berg (#128)
Re: [patch] \crosstabview documentation

Christoph Berg <myon@debian.org> writes:

Here's a small doc patch that removes the bogus space in "colH [:scolH]"
(otherwise psql complains that it is ignoring the 4th parameter.
It also adds an index entry and adds a note to the old crosstab
functions to make people aware of \crosstabview.

I should save before diffing, here's the version I actually wanted to
submit ...

Hm, we do not have <indexterm> entries attached to any other psql
meta-commands. Maybe they all should have one, or maybe not, but
I'm unconvinced about adding one for just this command. What I did
instead was to make a link target (which there *is* precedent for,
see \copy) and have the note in tablefunc.sgml link to it.

I failed to resist the temptation to edit the command description
rather heavily, too.

Pushed with revisions.

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#130Christoph Berg
myon@debian.org
In reply to: Tom Lane (#129)
Re: [patch] \crosstabview documentation

Re: Tom Lane 2016-04-13 <1854.1460562787@sss.pgh.pa.us>

Hm, we do not have <indexterm> entries attached to any other psql
meta-commands. Maybe they all should have one, or maybe not, but
I'm unconvinced about adding one for just this command. What I did
instead was to make a link target (which there *is* precedent for,
see \copy) and have the note in tablefunc.sgml link to it.

Hmm. I was looking at \? first and because the 1-liner there isn't
even attempting to explain the \crosstabview syntax, went on to
bookindex.html, but could only find the "wrong" crosstab there.

\copy is the other example where I'd think a bookindex entry would
make sense so people can look up the documentation, all the other
\backslash things are much easier to grasp.

Not sure if it's only me, but bookindex.html is where I have my
browser bookmark to start diving into the documentation.

I failed to resist the temptation to edit the command description
rather heavily, too.

Pushed with revisions.

Thanks!

Another thing about \crosstabview:

# select 1,2 \crosstabview
The query must return at least two columns to be shown in crosstab

s/two/three/, I guess.

Christoph

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#131Tom Lane
tgl@sss.pgh.pa.us
In reply to: Christoph Berg (#130)
Re: [patch] \crosstabview documentation

Christoph Berg <myon@debian.org> writes:

Another thing about \crosstabview:

# select 1,2 \crosstabview
The query must return at least two columns to be shown in crosstab

s/two/three/, I guess.

Yeah, I noticed that. See
/messages/by-id/10276.1460569515@sss.pgh.pa.us

regards, tom lane

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#132David G. Johnston
david.g.johnston@gmail.com
In reply to: Tom Lane (#131)
Re: [patch] \crosstabview documentation

On Wed, Apr 13, 2016 at 12:29 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Christoph Berg <myon@debian.org> writes:

Another thing about \crosstabview:

# select 1,2 \crosstabview
The query must return at least two columns to be shown in crosstab

s/two/three/, I guess.

Yeah, I noticed that. See
/messages/by-id/10276.1460569515@sss.pgh.pa.us

​So I guess:

"​
crosstabview with only 2 output columns
​ "​

https://wiki.postgresql.org/wiki/Crosstabview
​(last section on that page)

​never got implemented....

David J.

#133Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: David G. Johnston (#132)
Re: [patch] \crosstabview documentation

David G. Johnston wrote:

"​
crosstabview with only 2 output columns
​ "​

https://wiki.postgresql.org/wiki/Crosstabview
​(last section on that page)

​never got implemented....

It was implemented in Daniel's patch. I removed it before commit and
failed to notice the reference in the docs and help. (The reason I
removed it is that it seemed to me that hardcoding it to the character X
was going to become a nuisance in the future -- I mean why not ☒ ?
It's simple to do at the query level by having the character as a
literal in the third column.

--
Álvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#134Daniel Verite
daniel@manitou-mail.org
In reply to: David G. Johnston (#132)
Re: [patch] \crosstabview documentation

David G. Johnston wrote:

​So I guess:

"​
crosstabview with only 2 output columns
​ "​

https://wiki.postgresql.org/wiki/Crosstabview
​(last section on that page)

​never got implemented....

It was implemented but eventually removed.
I will update shortly this wiki page to reflect the status of the feature
as commited. ATM it features what was still under review a few weeks
ago, and it changed quite a bit eventually, notably on the point you
mention and on the sort capability.

Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers