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 | |