YugabyteDB (2.13.0.0-b42, bfc6a6643e7399ac8a0e81d06a3ee6d6571b33ab)

Coverage Report

Created: 2022-03-09 17:30

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