| 1 | /* |
| 2 | * psql - the PostgreSQL interactive terminal |
| 3 | * |
| 4 | * Copyright (c) 2000-2019, PostgreSQL Global Development Group |
| 5 | * |
| 6 | * src/bin/psql/crosstabview.c |
| 7 | */ |
| 8 | #include "postgres_fe.h" |
| 9 | |
| 10 | #include "common.h" |
| 11 | #include "crosstabview.h" |
| 12 | #include "pqexpbuffer.h" |
| 13 | #include "psqlscanslash.h" |
| 14 | #include "settings.h" |
| 15 | |
| 16 | #include "common/logging.h" |
| 17 | |
| 18 | /* |
| 19 | * Value/position from the resultset that goes into the horizontal or vertical |
| 20 | * crosstabview header. |
| 21 | */ |
| 22 | typedef struct _pivot_field |
| 23 | { |
| 24 | /* |
| 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. |
| 28 | */ |
| 29 | char *name; |
| 30 | |
| 31 | /* |
| 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. |
| 36 | */ |
| 37 | char *sort_value; |
| 38 | |
| 39 | /* |
| 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. |
| 45 | */ |
| 46 | int rank; |
| 47 | } pivot_field; |
| 48 | |
| 49 | /* Node in avl_tree */ |
| 50 | typedef struct _avl_node |
| 51 | { |
| 52 | /* Node contents */ |
| 53 | pivot_field field; |
| 54 | |
| 55 | /* |
| 56 | * Height of this node in the tree (number of nodes on the longest path to |
| 57 | * a leaf). |
| 58 | */ |
| 59 | int height; |
| 60 | |
| 61 | /* |
| 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 |
| 64 | * value. |
| 65 | */ |
| 66 | struct _avl_node *children[2]; |
| 67 | } avl_node; |
| 68 | |
| 69 | /* |
| 70 | * Control structure for the AVL tree (binary search tree kept |
| 71 | * balanced with the AVL algorithm) |
| 72 | */ |
| 73 | typedef struct _avl_tree |
| 74 | { |
| 75 | int count; /* Total number of nodes */ |
| 76 | avl_node *root; /* root of the tree */ |
| 77 | avl_node *end; /* Immutable dereferenceable empty tree */ |
| 78 | } avl_tree; |
| 79 | |
| 80 | |
| 81 | static bool printCrosstab(const PGresult *results, |
| 82 | int num_columns, pivot_field *piv_columns, int field_for_columns, |
| 83 | int num_rows, pivot_field *piv_rows, int field_for_rows, |
| 84 | int field_for_data); |
| 85 | static void avlInit(avl_tree *tree); |
| 86 | static void avlMergeValue(avl_tree *tree, char *name, char *sort_value); |
| 87 | static int avlCollectFields(avl_tree *tree, avl_node *node, |
| 88 | pivot_field *fields, int idx); |
| 89 | static void avlFree(avl_tree *tree, avl_node *node); |
| 90 | static void rankSort(int num_columns, pivot_field *piv_columns); |
| 91 | static int indexOfColumn(char *arg, const PGresult *res); |
| 92 | static int pivotFieldCompare(const void *a, const void *b); |
| 93 | static int rankCompare(const void *a, const void *b); |
| 94 | |
| 95 | |
| 96 | /* |
| 97 | * Main entry point to this module. |
| 98 | * |
| 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. |
| 102 | */ |
| 103 | bool |
| 104 | PrintResultsInCrosstab(const PGresult *res) |
| 105 | { |
| 106 | bool retval = false; |
| 107 | avl_tree piv_columns; |
| 108 | avl_tree piv_rows; |
| 109 | pivot_field *array_columns = NULL; |
| 110 | pivot_field *array_rows = NULL; |
| 111 | int num_columns = 0; |
| 112 | int num_rows = 0; |
| 113 | int field_for_rows; |
| 114 | int field_for_columns; |
| 115 | int field_for_data; |
| 116 | int sort_field_for_columns; |
| 117 | int rn; |
| 118 | |
| 119 | avlInit(&piv_rows); |
| 120 | avlInit(&piv_columns); |
| 121 | |
| 122 | if (PQresultStatus(res) != PGRES_TUPLES_OK) |
| 123 | { |
| 124 | pg_log_error("\\crosstabview: statement did not return a result set" ); |
| 125 | goto error_return; |
| 126 | } |
| 127 | |
| 128 | if (PQnfields(res) < 3) |
| 129 | { |
| 130 | pg_log_error("\\crosstabview: query must return at least three columns" ); |
| 131 | goto error_return; |
| 132 | } |
| 133 | |
| 134 | /* Process first optional arg (vertical header column) */ |
| 135 | if (pset.ctv_args[0] == NULL) |
| 136 | field_for_rows = 0; |
| 137 | else |
| 138 | { |
| 139 | field_for_rows = indexOfColumn(pset.ctv_args[0], res); |
| 140 | if (field_for_rows < 0) |
| 141 | goto error_return; |
| 142 | } |
| 143 | |
| 144 | /* Process second optional arg (horizontal header column) */ |
| 145 | if (pset.ctv_args[1] == NULL) |
| 146 | field_for_columns = 1; |
| 147 | else |
| 148 | { |
| 149 | field_for_columns = indexOfColumn(pset.ctv_args[1], res); |
| 150 | if (field_for_columns < 0) |
| 151 | goto error_return; |
| 152 | } |
| 153 | |
| 154 | /* Insist that header columns be distinct */ |
| 155 | if (field_for_columns == field_for_rows) |
| 156 | { |
| 157 | pg_log_error("\\crosstabview: vertical and horizontal headers must be different columns" ); |
| 158 | goto error_return; |
| 159 | } |
| 160 | |
| 161 | /* Process third optional arg (data column) */ |
| 162 | if (pset.ctv_args[2] == NULL) |
| 163 | { |
| 164 | int i; |
| 165 | |
| 166 | /* |
| 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. |
| 170 | */ |
| 171 | if (PQnfields(res) != 3) |
| 172 | { |
| 173 | pg_log_error("\\crosstabview: data column must be specified when query returns more than three columns" ); |
| 174 | goto error_return; |
| 175 | } |
| 176 | |
| 177 | field_for_data = -1; |
| 178 | for (i = 0; i < PQnfields(res); i++) |
| 179 | { |
| 180 | if (i != field_for_rows && i != field_for_columns) |
| 181 | { |
| 182 | field_for_data = i; |
| 183 | break; |
| 184 | } |
| 185 | } |
| 186 | Assert(field_for_data >= 0); |
| 187 | } |
| 188 | else |
| 189 | { |
| 190 | field_for_data = indexOfColumn(pset.ctv_args[2], res); |
| 191 | if (field_for_data < 0) |
| 192 | goto error_return; |
| 193 | } |
| 194 | |
| 195 | /* Process fourth optional arg (horizontal header sort column) */ |
| 196 | if (pset.ctv_args[3] == NULL) |
| 197 | sort_field_for_columns = -1; /* no sort column */ |
| 198 | else |
| 199 | { |
| 200 | sort_field_for_columns = indexOfColumn(pset.ctv_args[3], res); |
| 201 | if (sort_field_for_columns < 0) |
| 202 | goto error_return; |
| 203 | } |
| 204 | |
| 205 | /* |
| 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 |
| 208 | * DISTINCT values. |
| 209 | */ |
| 210 | |
| 211 | for (rn = 0; rn < PQntuples(res); rn++) |
| 212 | { |
| 213 | char *val; |
| 214 | char *val1; |
| 215 | |
| 216 | /* horizontal */ |
| 217 | val = PQgetisnull(res, rn, field_for_columns) ? NULL : |
| 218 | PQgetvalue(res, rn, field_for_columns); |
| 219 | val1 = NULL; |
| 220 | |
| 221 | if (sort_field_for_columns >= 0 && |
| 222 | !PQgetisnull(res, rn, sort_field_for_columns)) |
| 223 | val1 = PQgetvalue(res, rn, sort_field_for_columns); |
| 224 | |
| 225 | avlMergeValue(&piv_columns, val, val1); |
| 226 | |
| 227 | if (piv_columns.count > CROSSTABVIEW_MAX_COLUMNS) |
| 228 | { |
| 229 | pg_log_error("\\crosstabview: maximum number of columns (%d) exceeded" , |
| 230 | CROSSTABVIEW_MAX_COLUMNS); |
| 231 | goto error_return; |
| 232 | } |
| 233 | |
| 234 | /* vertical */ |
| 235 | val = PQgetisnull(res, rn, field_for_rows) ? NULL : |
| 236 | PQgetvalue(res, rn, field_for_rows); |
| 237 | |
| 238 | avlMergeValue(&piv_rows, val, NULL); |
| 239 | } |
| 240 | |
| 241 | /* |
| 242 | * Second part: Generate sorted arrays from the AVL trees. |
| 243 | */ |
| 244 | |
| 245 | num_columns = piv_columns.count; |
| 246 | num_rows = piv_rows.count; |
| 247 | |
| 248 | array_columns = (pivot_field *) |
| 249 | pg_malloc(sizeof(pivot_field) * num_columns); |
| 250 | |
| 251 | array_rows = (pivot_field *) |
| 252 | pg_malloc(sizeof(pivot_field) * num_rows); |
| 253 | |
| 254 | avlCollectFields(&piv_columns, piv_columns.root, array_columns, 0); |
| 255 | avlCollectFields(&piv_rows, piv_rows.root, array_rows, 0); |
| 256 | |
| 257 | /* |
| 258 | * Third part: optionally, process the ranking data for the horizontal |
| 259 | * header |
| 260 | */ |
| 261 | if (sort_field_for_columns >= 0) |
| 262 | rankSort(num_columns, array_columns); |
| 263 | |
| 264 | /* |
| 265 | * Fourth part: print the crosstab'ed results. |
| 266 | */ |
| 267 | retval = printCrosstab(res, |
| 268 | num_columns, array_columns, field_for_columns, |
| 269 | num_rows, array_rows, field_for_rows, |
| 270 | field_for_data); |
| 271 | |
| 272 | error_return: |
| 273 | avlFree(&piv_columns, piv_columns.root); |
| 274 | avlFree(&piv_rows, piv_rows.root); |
| 275 | pg_free(array_columns); |
| 276 | pg_free(array_rows); |
| 277 | |
| 278 | return retval; |
| 279 | } |
| 280 | |
| 281 | /* |
| 282 | * Output the pivoted resultset with the printTable* functions. Return true |
| 283 | * if successful, false otherwise. |
| 284 | */ |
| 285 | static bool |
| 286 | printCrosstab(const PGresult *results, |
| 287 | int num_columns, pivot_field *piv_columns, int field_for_columns, |
| 288 | int num_rows, pivot_field *piv_rows, int field_for_rows, |
| 289 | int field_for_data) |
| 290 | { |
| 291 | printQueryOpt popt = pset.popt; |
| 292 | printTableContent cont; |
| 293 | int i, |
| 294 | rn; |
| 295 | char col_align; |
| 296 | int *horiz_map; |
| 297 | bool retval = false; |
| 298 | |
| 299 | printTableInit(&cont, &popt.topt, popt.title, num_columns + 1, num_rows); |
| 300 | |
| 301 | /* Step 1: set target column names (horizontal header) */ |
| 302 | |
| 303 | /* The name of the first column is kept unchanged by the pivoting */ |
| 304 | printTableAddHeader(&cont, |
| 305 | PQfname(results, field_for_rows), |
| 306 | false, |
| 307 | column_type_alignment(PQftype(results, |
| 308 | field_for_rows))); |
| 309 | |
| 310 | /* |
| 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. |
| 314 | */ |
| 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; |
| 318 | |
| 319 | /* |
| 320 | * The display alignment depends on its PQftype(). |
| 321 | */ |
| 322 | col_align = column_type_alignment(PQftype(results, field_for_data)); |
| 323 | |
| 324 | for (i = 0; i < num_columns; i++) |
| 325 | { |
| 326 | char *colname; |
| 327 | |
| 328 | colname = piv_columns[horiz_map[i]].name ? |
| 329 | piv_columns[horiz_map[i]].name : |
| 330 | (popt.nullPrint ? popt.nullPrint : "" ); |
| 331 | |
| 332 | printTableAddHeader(&cont, colname, false, col_align); |
| 333 | } |
| 334 | pg_free(horiz_map); |
| 335 | |
| 336 | /* Step 2: set row names in the first output column (vertical header) */ |
| 337 | for (i = 0; i < num_rows; i++) |
| 338 | { |
| 339 | int k = piv_rows[i].rank; |
| 340 | |
| 341 | cont.cells[k * (num_columns + 1)] = piv_rows[i].name ? |
| 342 | piv_rows[i].name : |
| 343 | (popt.nullPrint ? popt.nullPrint : "" ); |
| 344 | } |
| 345 | cont.cellsadded = num_rows * (num_columns + 1); |
| 346 | |
| 347 | /* |
| 348 | * Step 3: fill in the content cells. |
| 349 | */ |
| 350 | for (rn = 0; rn < PQntuples(results); rn++) |
| 351 | { |
| 352 | int row_number; |
| 353 | int col_number; |
| 354 | pivot_field *rp, |
| 355 | *cp; |
| 356 | pivot_field elt; |
| 357 | |
| 358 | /* Find target row */ |
| 359 | if (!PQgetisnull(results, rn, field_for_rows)) |
| 360 | elt.name = PQgetvalue(results, rn, field_for_rows); |
| 361 | else |
| 362 | elt.name = NULL; |
| 363 | rp = (pivot_field *) bsearch(&elt, |
| 364 | piv_rows, |
| 365 | num_rows, |
| 366 | sizeof(pivot_field), |
| 367 | pivotFieldCompare); |
| 368 | Assert(rp != NULL); |
| 369 | row_number = rp->rank; |
| 370 | |
| 371 | /* Find target column */ |
| 372 | if (!PQgetisnull(results, rn, field_for_columns)) |
| 373 | elt.name = PQgetvalue(results, rn, field_for_columns); |
| 374 | else |
| 375 | elt.name = NULL; |
| 376 | |
| 377 | cp = (pivot_field *) bsearch(&elt, |
| 378 | piv_columns, |
| 379 | num_columns, |
| 380 | sizeof(pivot_field), |
| 381 | pivotFieldCompare); |
| 382 | Assert(cp != NULL); |
| 383 | col_number = cp->rank; |
| 384 | |
| 385 | /* Place value into cell */ |
| 386 | if (col_number >= 0 && row_number >= 0) |
| 387 | { |
| 388 | int idx; |
| 389 | |
| 390 | /* index into the cont.cells array */ |
| 391 | idx = 1 + col_number + row_number * (num_columns + 1); |
| 392 | |
| 393 | /* |
| 394 | * If the cell already contains a value, raise an error. |
| 395 | */ |
| 396 | if (cont.cells[idx] != NULL) |
| 397 | { |
| 398 | pg_log_error("\\crosstabview: query result contains multiple data values for row \"%s\", column \"%s\"" , |
| 399 | rp->name ? rp->name : |
| 400 | (popt.nullPrint ? popt.nullPrint : "(null)" ), |
| 401 | cp->name ? cp->name : |
| 402 | (popt.nullPrint ? popt.nullPrint : "(null)" )); |
| 403 | goto error; |
| 404 | } |
| 405 | |
| 406 | cont.cells[idx] = !PQgetisnull(results, rn, field_for_data) ? |
| 407 | PQgetvalue(results, rn, field_for_data) : |
| 408 | (popt.nullPrint ? popt.nullPrint : "" ); |
| 409 | } |
| 410 | } |
| 411 | |
| 412 | /* |
| 413 | * The non-initialized cells must be set to an empty string for the print |
| 414 | * functions |
| 415 | */ |
| 416 | for (i = 0; i < cont.cellsadded; i++) |
| 417 | { |
| 418 | if (cont.cells[i] == NULL) |
| 419 | cont.cells[i] = "" ; |
| 420 | } |
| 421 | |
| 422 | printTable(&cont, pset.queryFout, false, pset.logfile); |
| 423 | retval = true; |
| 424 | |
| 425 | error: |
| 426 | printTableCleanup(&cont); |
| 427 | |
| 428 | return retval; |
| 429 | } |
| 430 | |
| 431 | /* |
| 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 |
| 435 | * search. |
| 436 | */ |
| 437 | static void |
| 438 | avlInit(avl_tree *tree) |
| 439 | { |
| 440 | tree->end = (avl_node *) pg_malloc0(sizeof(avl_node)); |
| 441 | tree->end->children[0] = tree->end->children[1] = tree->end; |
| 442 | tree->count = 0; |
| 443 | tree->root = tree->end; |
| 444 | } |
| 445 | |
| 446 | /* Deallocate recursively an AVL tree, starting from node */ |
| 447 | static void |
| 448 | avlFree(avl_tree *tree, avl_node *node) |
| 449 | { |
| 450 | if (node->children[0] != tree->end) |
| 451 | { |
| 452 | avlFree(tree, node->children[0]); |
| 453 | pg_free(node->children[0]); |
| 454 | } |
| 455 | if (node->children[1] != tree->end) |
| 456 | { |
| 457 | avlFree(tree, node->children[1]); |
| 458 | pg_free(node->children[1]); |
| 459 | } |
| 460 | if (node == tree->root) |
| 461 | { |
| 462 | /* free the root separately as it's not child of anything */ |
| 463 | if (node != tree->end) |
| 464 | pg_free(node); |
| 465 | /* free the tree->end struct only once and when all else is freed */ |
| 466 | pg_free(tree->end); |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | /* Set the height to 1 plus the greatest of left and right heights */ |
| 471 | static void |
| 472 | avlUpdateHeight(avl_node *n) |
| 473 | { |
| 474 | n->height = 1 + (n->children[0]->height > n->children[1]->height ? |
| 475 | n->children[0]->height : |
| 476 | n->children[1]->height); |
| 477 | } |
| 478 | |
| 479 | /* Rotate a subtree left (dir=0) or right (dir=1). Not recursive */ |
| 480 | static avl_node * |
| 481 | avlRotate(avl_node **current, int dir) |
| 482 | { |
| 483 | avl_node *before = *current; |
| 484 | avl_node *after = (*current)->children[dir]; |
| 485 | |
| 486 | *current = after; |
| 487 | before->children[dir] = after->children[!dir]; |
| 488 | avlUpdateHeight(before); |
| 489 | after->children[!dir] = before; |
| 490 | |
| 491 | return after; |
| 492 | } |
| 493 | |
| 494 | static int |
| 495 | avlBalance(avl_node *n) |
| 496 | { |
| 497 | return n->children[0]->height - n->children[1]->height; |
| 498 | } |
| 499 | |
| 500 | /* |
| 501 | * After an insertion, possibly rebalance the tree so that the left and right |
| 502 | * node heights don't differ by more than 1. |
| 503 | * May update *node. |
| 504 | */ |
| 505 | static void |
| 506 | avlAdjustBalance(avl_tree *tree, avl_node **node) |
| 507 | { |
| 508 | avl_node *current = *node; |
| 509 | int b = avlBalance(current) / 2; |
| 510 | |
| 511 | if (b != 0) |
| 512 | { |
| 513 | int dir = (1 - b) / 2; |
| 514 | |
| 515 | if (avlBalance(current->children[dir]) == -b) |
| 516 | avlRotate(¤t->children[dir], !dir); |
| 517 | current = avlRotate(node, dir); |
| 518 | } |
| 519 | if (current != tree->end) |
| 520 | avlUpdateHeight(current); |
| 521 | } |
| 522 | |
| 523 | /* |
| 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. |
| 527 | */ |
| 528 | static void |
| 529 | avlInsertNode(avl_tree *tree, avl_node **node, pivot_field field) |
| 530 | { |
| 531 | avl_node *current = *node; |
| 532 | |
| 533 | if (current == tree->end) |
| 534 | { |
| 535 | avl_node *new_node = (avl_node *) |
| 536 | pg_malloc(sizeof(avl_node)); |
| 537 | |
| 538 | new_node->height = 1; |
| 539 | new_node->field = field; |
| 540 | new_node->children[0] = new_node->children[1] = tree->end; |
| 541 | tree->count++; |
| 542 | *node = new_node; |
| 543 | } |
| 544 | else |
| 545 | { |
| 546 | int cmp = pivotFieldCompare(&field, ¤t->field); |
| 547 | |
| 548 | if (cmp != 0) |
| 549 | { |
| 550 | avlInsertNode(tree, |
| 551 | cmp > 0 ? ¤t->children[1] : ¤t->children[0], |
| 552 | field); |
| 553 | avlAdjustBalance(tree, node); |
| 554 | } |
| 555 | } |
| 556 | } |
| 557 | |
| 558 | /* Insert the value into the AVL tree, if it does not preexist */ |
| 559 | static void |
| 560 | avlMergeValue(avl_tree *tree, char *name, char *sort_value) |
| 561 | { |
| 562 | pivot_field field; |
| 563 | |
| 564 | field.name = name; |
| 565 | field.rank = tree->count; |
| 566 | field.sort_value = sort_value; |
| 567 | avlInsertNode(tree, &tree->root, field); |
| 568 | } |
| 569 | |
| 570 | /* |
| 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 |
| 575 | */ |
| 576 | static int |
| 577 | avlCollectFields(avl_tree *tree, avl_node *node, pivot_field *fields, int idx) |
| 578 | { |
| 579 | if (node == tree->end) |
| 580 | return idx; |
| 581 | |
| 582 | idx = avlCollectFields(tree, node->children[0], fields, idx); |
| 583 | fields[idx] = node->field; |
| 584 | return avlCollectFields(tree, node->children[1], fields, idx + 1); |
| 585 | } |
| 586 | |
| 587 | static void |
| 588 | rankSort(int num_columns, pivot_field *piv_columns) |
| 589 | { |
| 590 | int *hmap; /* [[offset in piv_columns, rank], ...for |
| 591 | * every header entry] */ |
| 592 | int i; |
| 593 | |
| 594 | hmap = (int *) pg_malloc(sizeof(int) * num_columns * 2); |
| 595 | for (i = 0; i < num_columns; i++) |
| 596 | { |
| 597 | char *val = piv_columns[i].sort_value; |
| 598 | |
| 599 | /* ranking information is valid if non null and matches /^-?\d+$/ */ |
| 600 | if (val && |
| 601 | ((*val == '-' && |
| 602 | strspn(val + 1, "0123456789" ) == strlen(val + 1)) || |
| 603 | strspn(val, "0123456789" ) == strlen(val))) |
| 604 | { |
| 605 | hmap[i * 2] = atoi(val); |
| 606 | hmap[i * 2 + 1] = i; |
| 607 | } |
| 608 | else |
| 609 | { |
| 610 | /* invalid rank information ignored (equivalent to rank 0) */ |
| 611 | hmap[i * 2] = 0; |
| 612 | hmap[i * 2 + 1] = i; |
| 613 | } |
| 614 | } |
| 615 | |
| 616 | qsort(hmap, num_columns, sizeof(int) * 2, rankCompare); |
| 617 | |
| 618 | for (i = 0; i < num_columns; i++) |
| 619 | { |
| 620 | piv_columns[hmap[i * 2 + 1]].rank = i; |
| 621 | } |
| 622 | |
| 623 | pg_free(hmap); |
| 624 | } |
| 625 | |
| 626 | /* |
| 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,...) |
| 630 | * |
| 631 | * Returns zero-based column number, or -1 if not found or ambiguous. |
| 632 | * |
| 633 | * Note: may modify contents of "arg" string. |
| 634 | */ |
| 635 | static int |
| 636 | indexOfColumn(char *arg, const PGresult *res) |
| 637 | { |
| 638 | int idx; |
| 639 | |
| 640 | if (arg[0] && strspn(arg, "0123456789" ) == strlen(arg)) |
| 641 | { |
| 642 | /* if arg contains only digits, it's a column number */ |
| 643 | idx = atoi(arg) - 1; |
| 644 | if (idx < 0 || idx >= PQnfields(res)) |
| 645 | { |
| 646 | pg_log_error("\\crosstabview: column number %d is out of range 1..%d" , |
| 647 | idx + 1, PQnfields(res)); |
| 648 | return -1; |
| 649 | } |
| 650 | } |
| 651 | else |
| 652 | { |
| 653 | int i; |
| 654 | |
| 655 | /* |
| 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. |
| 659 | */ |
| 660 | dequote_downcase_identifier(arg, true, pset.encoding); |
| 661 | |
| 662 | /* Now look for match(es) among res' column names */ |
| 663 | idx = -1; |
| 664 | for (i = 0; i < PQnfields(res); i++) |
| 665 | { |
| 666 | if (strcmp(arg, PQfname(res, i)) == 0) |
| 667 | { |
| 668 | if (idx >= 0) |
| 669 | { |
| 670 | /* another idx was already found for the same name */ |
| 671 | pg_log_error("\\crosstabview: ambiguous column name: \"%s\"" , arg); |
| 672 | return -1; |
| 673 | } |
| 674 | idx = i; |
| 675 | } |
| 676 | } |
| 677 | if (idx == -1) |
| 678 | { |
| 679 | pg_log_error("\\crosstabview: column name not found: \"%s\"" , arg); |
| 680 | return -1; |
| 681 | } |
| 682 | } |
| 683 | |
| 684 | return idx; |
| 685 | } |
| 686 | |
| 687 | /* |
| 688 | * Value comparator for vertical and horizontal headers |
| 689 | * used for deduplication only. |
| 690 | * - null values are considered equal |
| 691 | * - non-null < null |
| 692 | * - non-null values are compared with strcmp() |
| 693 | */ |
| 694 | static int |
| 695 | pivotFieldCompare(const void *a, const void *b) |
| 696 | { |
| 697 | const pivot_field *pa = (const pivot_field *) a; |
| 698 | const pivot_field *pb = (const pivot_field *) b; |
| 699 | |
| 700 | /* test null values */ |
| 701 | if (!pb->name) |
| 702 | return pa->name ? -1 : 0; |
| 703 | else if (!pa->name) |
| 704 | return 1; |
| 705 | |
| 706 | /* non-null values */ |
| 707 | return strcmp(pa->name, pb->name); |
| 708 | } |
| 709 | |
| 710 | static int |
| 711 | rankCompare(const void *a, const void *b) |
| 712 | { |
| 713 | return *((const int *) a) - *((const int *) b); |
| 714 | } |
| 715 | |