2 * psql - the PostgreSQL interactive terminal
4 * Copyright (c) 2000-2025, PostgreSQL Global Development Group
6 * src/bin/psql/crosstabview.c
19 * Value/position from the resultset that goes into the horizontal or vertical
20 * crosstabview header.
25 * Pointer obtained from PQgetvalue() for colV or colH. Each distinct
26 * value becomes an entry in the vertical header (colV), or horizontal
27 * header (colH). A Null value is represented by a NULL pointer.
32 * When a sort is requested on an alternative column, this holds
33 * PQgetvalue() for the sort column corresponding to <name>. If <name>
34 * appear multiple times, it's the first value in the order of the results
35 * that is kept. A Null value is represented by a NULL pointer.
40 * Rank of this value, starting at 0. Initially, it's the relative
41 * position of the first appearance of <name> in the resultset. For
42 * example, if successive rows contain B,A,C,A,D then it's B:0,A:1,C:2,D:3
43 * When a sort column is specified, ranks get updated in a final pass to
44 * reflect the desired order.
56 * Height of this node in the tree (number of nodes on the longest path to
62 * Child nodes. [0] points to left subtree, [1] to right subtree. Never
63 * NULL, points to the empty node avl_tree.end when no left or right
70 * Control structure for the AVL tree (binary search tree kept
71 * balanced with the AVL algorithm)
75 int count;
/* Total number of nodes */
82 int num_columns,
pivot_field *piv_columns,
int field_for_columns,
83 int num_rows,
pivot_field *piv_rows,
int field_for_rows,
97 * Main entry point to this module.
99 * Process the data from *res according to the options in pset (global),
100 * to generate the horizontal and vertical headers contents,
101 * then call printCrosstab() for the actual output.
114 int field_for_columns;
116 int sort_field_for_columns;
124 pg_log_error(
"\\crosstabview: statement did not return a result set");
130 pg_log_error(
"\\crosstabview: query must return at least three columns");
134 /* Process first optional arg (vertical header column) */
140 if (field_for_rows < 0)
144 /* Process second optional arg (horizontal header column) */
146 field_for_columns = 1;
150 if (field_for_columns < 0)
154 /* Insist that header columns be distinct */
155 if (field_for_columns == field_for_rows)
157 pg_log_error(
"\\crosstabview: vertical and horizontal headers must be different columns");
161 /* Process third optional arg (data column) */
167 * If the data column was not specified, we search for the one not
168 * used as either vertical or horizontal headers. Must be exactly
169 * three columns, or this won't be unique.
173 pg_log_error(
"\\crosstabview: data column must be specified when query returns more than three columns");
180 if (
i != field_for_rows &&
i != field_for_columns)
186 Assert(field_for_data >= 0);
191 if (field_for_data < 0)
195 /* Process fourth optional arg (horizontal header sort column) */
197 sort_field_for_columns = -1;
/* no sort column */
201 if (sort_field_for_columns < 0)
206 * First part: accumulate the names that go into the vertical and
207 * horizontal headers, each into an AVL binary tree to build the set of
221 if (sort_field_for_columns >= 0 &&
223 val1 =
PQgetvalue(res, rn, sort_field_for_columns);
229 pg_log_error(
"\\crosstabview: maximum number of columns (%d) exceeded",
242 * Second part: Generate sorted arrays from the AVL trees.
245 num_columns = piv_columns.
count;
246 num_rows = piv_rows.
count;
258 * Third part: optionally, process the ranking data for the horizontal
261 if (sort_field_for_columns >= 0)
262 rankSort(num_columns, array_columns);
265 * Fourth part: print the crosstab'ed result.
268 num_columns, array_columns, field_for_columns,
269 num_rows, array_rows, field_for_rows,
282 * Output the pivoted resultset with the printTable* functions. Return true
283 * if successful, false otherwise.
287 int num_columns,
pivot_field *piv_columns,
int field_for_columns,
288 int num_rows,
pivot_field *piv_rows,
int field_for_rows,
301 /* Step 1: set target column names (horizontal header) */
303 /* The name of the first column is kept unchanged by the pivoting */
305 PQfname(result, field_for_rows),
311 * To iterate over piv_columns[] by piv_columns[].rank, create a reverse
312 * map associating each piv_columns[].rank to its index in piv_columns.
313 * This avoids an O(N^2) loop later.
315 horiz_map = (
int *)
pg_malloc(
sizeof(
int) * num_columns);
316 for (
i = 0;
i < num_columns;
i++)
317 horiz_map[piv_columns[
i].rank] =
i;
320 * The display alignment depends on its PQftype().
324 for (
i = 0;
i < num_columns;
i++)
328 colname = piv_columns[horiz_map[
i]].
name ?
329 piv_columns[horiz_map[
i]].
name :
336 /* Step 2: set row names in the first output column (vertical header) */
337 for (
i = 0;
i < num_rows;
i++)
339 int k = piv_rows[
i].
rank;
341 cont.
cells[k * (num_columns + 1)] = piv_rows[
i].
name ?
345 cont.
cellsadded = num_rows * (num_columns + 1);
348 * Step 3: fill in the content cells.
350 for (rn = 0; rn <
PQntuples(result); rn++)
358 /* Find target row */
369 row_number = rp->
rank;
371 /* Find target column */
383 col_number = cp->
rank;
385 /* Place value into cell */
386 if (col_number >= 0 && row_number >= 0)
390 /* index into the cont.cells array */
391 idx = 1 + col_number + row_number * (num_columns + 1);
394 * If the cell already contains a value, raise an error.
398 pg_log_error(
"\\crosstabview: query result contains multiple data values for row \"%s\", column \"%s\"",
413 * The non-initialized cells must be set to an empty string for the print
418 if (cont.
cells[
i] == NULL)
432 * The avl* functions below provide a minimalistic implementation of AVL binary
433 * trees, to efficiently collect the distinct values that will form the horizontal
434 * and vertical headers. It only supports adding new values, no removal or even
441 tree->end->children[0] =
tree->end->children[1] =
tree->end;
446/* Deallocate recursively an AVL tree, starting from node */
460 if (node ==
tree->root)
462 /* free the root separately as it's not child of anything */
463 if (node !=
tree->end)
465 /* free the tree->end struct only once and when all else is freed */
470/* Set the height to 1 plus the greatest of left and right heights */
479/* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */
501 * After an insertion, possibly rebalance the tree so that the left and right
502 * node heights don't differ by more than 1.
513 int dir = (1 -
b) / 2;
519 if (current !=
tree->end)
524 * Insert a new value/field, starting from *node, reaching the correct position
525 * in the tree by recursion. Possibly rebalance the tree and possibly update
526 * *node. Do nothing if the value is already present in the tree.
533 if (current ==
tree->end)
539 new_node->
field = field;
558/* Insert the value into the AVL tree, if it does not preexist */
571 * Recursively extract node values into the names array, in sorted order with a
572 * left-to-right tree traversal.
573 * Return the next candidate offset to write into the names array.
574 * fields[] must be preallocated to hold tree->count entries
579 if (node ==
tree->end)
590 int *hmap;
/* [[offset in piv_columns, rank], ...for
591 * every header entry] */
594 hmap = (
int *)
pg_malloc(
sizeof(
int) * num_columns * 2);
595 for (
i = 0;
i < num_columns;
i++)
599 /* ranking information is valid if non null and matches /^-?\d+$/ */
602 strspn(
val + 1,
"0123456789") == strlen(
val + 1)) ||
603 strspn(
val,
"0123456789") == strlen(
val)))
605 hmap[
i * 2] = atoi(
val);
610 /* invalid rank information ignored (equivalent to rank 0) */
618 for (
i = 0;
i < num_columns;
i++)
620 piv_columns[hmap[
i * 2 + 1]].
rank =
i;
627 * Look up a column reference, which can be either:
628 * - a number from 1 to PQnfields(res)
629 * - a column name matching one of PQfname(res,...)
631 * Returns zero-based column number, or -1 if not found or ambiguous.
633 * Note: may modify contents of "arg" string.
640 if (
arg[0] && strspn(
arg,
"0123456789") == strlen(
arg))
642 /* if arg contains only digits, it's a column number */
646 pg_log_error(
"\\crosstabview: column number %d is out of range 1..%d",
656 * Dequote and downcase the column name. By checking for all-digits
657 * before doing this, we can ensure that a quoted name is treated as a
658 * name even if it's all digits.
662 /* Now look for match(es) among res' column names */
670 /* another idx was already found for the same name */
688 * Value comparator for vertical and horizontal headers
689 * used for deduplication only.
690 * - null values are considered equal
692 * - non-null values are compared with strcmp()
700 /* test null values */
702 return pa->
name ? -1 : 0;
706 /* non-null values */
Datum idx(PG_FUNCTION_ARGS)
static void avlUpdateHeight(avl_node *n)
static int avlCollectFields(avl_tree *tree, avl_node *node, pivot_field *fields, int idx)
static int pivotFieldCompare(const void *a, const void *b)
static void avlInsertNode(avl_tree *tree, avl_node **node, pivot_field field)
static int rankCompare(const void *a, const void *b)
struct _avl_tree avl_tree
static void avlFree(avl_tree *tree, avl_node *node)
static bool printCrosstab(const PGresult *result, int num_columns, pivot_field *piv_columns, int field_for_columns, int num_rows, pivot_field *piv_rows, int field_for_rows, int field_for_data)
static void rankSort(int num_columns, pivot_field *piv_columns)
bool PrintResultInCrosstab(const PGresult *res)
static int avlBalance(avl_node *n)
static int indexOfColumn(char *arg, const PGresult *res)
static avl_node * avlRotate(avl_node **current, int dir)
static void avlInit(avl_tree *tree)
static void avlMergeValue(avl_tree *tree, char *name, char *sort_value)
struct _avl_node avl_node
struct _pivot_field pivot_field
static void avlAdjustBalance(avl_tree *tree, avl_node **node)
#define CROSSTABVIEW_MAX_COLUMNS
Oid PQftype(const PGresult *res, int field_num)
void * pg_malloc(size_t size)
void * pg_malloc0(size_t size)
void printTableInit(printTableContent *const content, const printTableOpt *opt, const char *title, const int ncolumns, const int nrows)
void printTableCleanup(printTableContent *const content)
char column_type_alignment(Oid ftype)
void printTable(const printTableContent *cont, FILE *fout, bool is_pager, FILE *flog)
void printTableAddHeader(printTableContent *const content, char *header, const bool translate, const char align)
Assert(PointerIsAligned(start, uint64))
static int pg_cmp_s32(int32 a, int32 b)
#define pg_log_error(...)
#define qsort(a, b, c, d)
void dequote_downcase_identifier(char *str, bool downcase, int encoding)
static int cmp(const chr *x, const chr *y, size_t len)
static int before(chr x, chr y)
struct _avl_node * children[2]