Passed
Push — main ( 759879...de5ba5 )
by Thierry
01:57
created

TableSelectAdmin::getSelectEntity()   B

Complexity

Conditions 10
Paths 18

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 18
c 1
b 0
f 0
nc 18
nop 10
dl 0
loc 30
rs 7.6666

How to fix   Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace Lagdo\DbAdmin\DbAdmin;
4
5
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
6
use Lagdo\DbAdmin\Driver\Entity\TableSelectEntity;
7
8
use Exception;
9
10
use function intval;
11
use function count;
12
use function str_replace;
13
use function compact;
14
use function preg_match;
15
use function microtime;
16
use function html_entity_decode;
17
use function strip_tags;
18
use function array_flip;
19
use function in_array;
20
use function trim;
21
use function md5;
22
use function strlen;
23
use function strpos;
24
25
/**
26
 * Admin table select functions
27
 */
28
class TableSelectAdmin extends AbstractAdmin
29
{
30
    /**
31
     * Print columns box in select
32
     * @param array $select Result of processSelectColumns()[0]
33
     * @param array $columns Selectable columns
34
     * @param array $options
35
     * @return array
36
     */
37
    private function getColumnsOptions(array $select, array $columns, array $options): array
38
    {
39
        return [
40
            'select' => $select,
41
            'values' => (array)$options["columns"],
42
            'columns' => $columns,
43
            'functions' => $this->driver->functions(),
44
            'grouping' => $this->driver->grouping(),
45
        ];
46
    }
47
48
    /**
49
     * Print search box in select
50
     *
51
     * @param array $columns Selectable columns
52
     * @param array $indexes
53
     * @param array $options
54
     *
55
     * @return array
56
     */
57
    private function getFiltersOptions(array $columns, array $indexes, array $options): array
58
    {
59
        $fulltexts = [];
60
        foreach ($indexes as $i => $index) {
61
            $fulltexts[$i] = $index->type == "FULLTEXT" ? $this->util->html($options["fulltext"][$i]) : '';
62
        }
63
        return [
64
            // 'where' => $where,
65
            'values' => (array)$options["where"],
66
            'columns' => $columns,
67
            'indexes' => $indexes,
68
            'operators' => $this->driver->operators(),
69
            'fulltexts' => $fulltexts,
70
        ];
71
    }
72
73
    /**
74
     * Print order box in select
75
     *
76
     * @param array $columns Selectable columns
77
     * @param array $options
78
     *
79
     * @return array
80
     */
81
    private function getSortingOptions(array $columns, array $options): array
82
    {
83
        $values = [];
84
        $descs = (array)$options["desc"];
85
        foreach ((array)$options["order"] as $key => $value) {
86
            $values[] = [
87
                'col' => $value,
88
                'desc' => $descs[$key] ?? 0,
89
            ];
90
        }
91
        return [
92
            // 'order' => $order,
93
            'values' => $values,
94
            'columns' => $columns,
95
        ];
96
    }
97
98
    /**
99
     * Print limit box in select
100
     *
101
     * @param string $limit Result of processSelectLimit()
102
     *
103
     * @return array
104
     */
105
    private function getLimitOptions(string $limit): array
106
    {
107
        return ['value' => $this->util->html($limit)];
108
    }
109
110
    /**
111
     * Print text length box in select
112
     *
113
     * @param int $textLength Result of processSelectLength()
114
     *
115
     * @return array
116
     */
117
    private function getLengthOptions($textLength): array
118
    {
119
        return [
120
            'value' => $textLength === 0 ? 0 : $this->util->html($textLength),
121
        ];
122
    }
123
124
    /**
125
     * Print action box in select
126
     *
127
     * @param array $indexes
128
     *
129
     * @return array
130
     */
131
    // private function getActionOptions(array $indexes)
132
    // {
133
    //     $columns = [];
134
    //     foreach ($indexes as $index) {
135
    //         $current_key = \reset($index->columns);
136
    //         if ($index->type != "FULLTEXT" && $current_key) {
137
    //             $columns[$current_key] = 1;
138
    //         }
139
    //     }
140
    //     $columns[""] = 1;
141
    //     return ['columns' => $columns];
142
    // }
143
144
    /**
145
     * Print command box in select
146
     *
147
     * @return bool whether to print default commands
148
     */
149
    // private function getCommandOptions()
150
    // {
151
    //     return !$this->driver->isInformationSchema($this->driver->database());
152
    // }
153
154
    /**
155
     * Print import box in select
156
     *
157
     * @return bool whether to print default import
158
     */
159
    // private function getImportOptions()
160
    // {
161
    //     return !$this->driver->isInformationSchema($this->driver->database());
162
    // }
163
164
    /**
165
     * Print extra text in the end of a select form
166
     *
167
     * @param array $emailFields Fields holding e-mails
168
     * @param array $columns Selectable columns
169
     *
170
     * @return array
171
     */
172
    // private function getEmailOptions(array $emailFields, array $columns)
173
    // {
174
    // }
175
176
    /**
177
     * @param array $queryOptions
178
     *
179
     * @return int
180
     */
181
    private function setDefaultOptions(array &$queryOptions): int
182
    {
183
        $defaultOptions = [
184
            'columns' => [],
185
            'where' => [],
186
            'order' => [],
187
            'desc' => [],
188
            'fulltext' => [],
189
            'limit' => '50',
190
            'text_length' => '100',
191
            'page' => '1',
192
        ];
193
        foreach ($defaultOptions as $name => $value) {
194
            if (!isset($queryOptions[$name])) {
195
                $queryOptions[$name] = $value;
196
            }
197
        }
198
        $page = intval($queryOptions['page']);
199
        if ($page > 0) {
200
            $page -= 1; // Page numbers start at 0 here, instead of 1.
201
        }
202
        $queryOptions['page'] = $page;
203
        return $page;
204
    }
205
206
    /**
207
     * @param array $fields
208
     *
209
     * @return array
210
     */
211
    private function getFieldsOptions(array $fields): array
212
    {
213
        $rights = []; // privilege => 0
214
        $columns = []; // selectable columns
215
        $textLength = 0;
216
        foreach ($fields as $key => $field) {
217
            $name = $this->util->fieldName($field);
218
            if (isset($field->privileges["select"]) && $name != "") {
219
                $columns[$key] = html_entity_decode(strip_tags($name), ENT_QUOTES);
220
                if ($this->util->isShortable($field)) {
221
                    $textLength = $this->util->processSelectLength();
222
                }
223
            }
224
            $rights[] = $field->privileges;
225
        }
226
        return [$rights, $columns, $textLength];
227
    }
228
229
    /**
230
     * @param array $indexes
231
     * @param array $select
232
     * @param mixed $tableStatus
233
     *
234
     * @return array|null
235
     */
236
    private function setPrimaryKey(array &$indexes, array $select, $tableStatus)
237
    {
238
        $primary = $unselected = null;
239
        foreach ($indexes as $index) {
240
            if ($index->type == "PRIMARY") {
241
                $primary = array_flip($index->columns);
242
                $unselected = ($select ? $primary : []);
243
                foreach ($unselected as $key => $val) {
244
                    if (in_array($this->driver->escapeId($key), $select)) {
245
                        unset($unselected[$key]);
246
                    }
247
                }
248
                break;
249
            }
250
        }
251
252
        $oid = $tableStatus->oid;
253
        if ($oid && !$primary) {
254
            /*$primary = */$unselected = [$oid => 0];
255
            $indexes[] = ["type" => "PRIMARY", "columns" => [$oid]];
256
        }
257
258
        return $unselected;
259
    }
260
261
    /**
262
     * @param array $select
263
     * @param array $columns
264
     * @param array $indexes
265
     * @param int $limit
266
     * @param int $textLength
267
     * @param array $queryOptions
268
     *
269
     * @return array
270
     */
271
    private function getAllOptions(array $select, array $columns, array $indexes,
272
        int $limit, int $textLength, array $queryOptions): array
273
    {
274
        return [
275
            'columns' => $this->getColumnsOptions($select, $columns, $queryOptions),
276
            'filters' => $this->getFiltersOptions($columns, $indexes, $queryOptions),
277
            'sorting' => $this->getSortingOptions($columns, $queryOptions),
278
            'limit' => $this->getLimitOptions($limit),
279
            'length' => $this->getLengthOptions($textLength),
280
            // 'action' => $this->getActionOptions($indexes),
281
        ];
282
    }
283
284
    /**
285
     * @param string $table
286
     * @param array $columns
287
     * @param array $fields
288
     * @param array $select
289
     * @param array $group
290
     * @param array $where
291
     * @param array $order
292
     * @param array $unselected
293
     * @param int $limit
294
     * @param int $page
295
     *
296
     * @return TableSelectEntity
297
     */
298
    private function getSelectEntity(string $table, array $columns, array $fields, array $select,
299
        array $group, array $where, array $order, array $unselected, int $limit, int $page): TableSelectEntity
300
    {
301
        $select2 = $select;
302
        $group2 = $group;
303
        if (!$select2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $select2 of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
304
            $select2[] = "*";
305
            $convert_fields = $this->driver->convertFields($columns, $fields, $select);
306
            if ($convert_fields) {
307
                $select2[] = \substr($convert_fields, 2);
308
            }
309
        }
310
        foreach ($select as $key => $val) {
311
            $field = $fields[$this->driver->unescapeId($val)] ?? null;
312
            if ($field && ($as = $this->driver->convertField($field))) {
313
                $select2[$key] = "$as AS $val";
314
            }
315
        }
316
        $isGroup = count($group) < count($select);
317
        if (!$isGroup && $unselected) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unselected of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
318
            foreach ($unselected as $key => $val) {
319
                $select2[] = $this->driver->escapeId($key);
320
                if ($group2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $group2 of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
321
                    $group2[] = $this->driver->escapeId($key);
322
                }
323
            }
324
        }
325
326
        // From driver.inc.php
327
        return new TableSelectEntity($table, $select2, $where, $group2, $order, $limit, $page);
328
    }
329
330
    /**
331
     * Get required data for create/update on tables
332
     *
333
     * @param string $table The table name
334
     * @param array $queryOptions The query options
335
     *
336
     * @return array
337
     * @throws Exception
338
     */
339
    private function prepareSelect(string $table, array &$queryOptions = []): array
340
    {
341
        $page = $this->setDefaultOptions($queryOptions);
342
        $this->util->input()->setValues($queryOptions);
0 ignored issues
show
Bug introduced by
The method setValues() does not exist on Lagdo\DbAdmin\Driver\InputInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

342
        $this->util->input()->/** @scrutinizer ignore-call */ setValues($queryOptions);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
343
344
        // From select.inc.php
345
        $fields = $this->driver->fields($table);
346
        list(, $columns, $textLength) = $this->getFieldsOptions($fields);
347
        if (!$columns && $this->driver->support("table")) {
348
            throw new Exception($this->trans->lang('Unable to select the table') .
349
                ($fields ? "." : ": " . $this->driver->error()));
350
        }
351
352
        $indexes = $this->driver->indexes($table);
353
        $foreignKeys = $this->admin->columnForeignKeys($table);
354
        list($select, $group) = $this->util->processSelectColumns();
355
        $where = $this->util->processSelectSearch($fields, $indexes);
356
        $order = $this->util->processSelectOrder();
357
        $limit = $this->util->processSelectLimit();
358
        $tableStatus = $this->driver->tableStatusOrName($table);
359
        $unselected = $this->setPrimaryKey($indexes, $select, $tableStatus);
360
        $tableName = $this->util->tableName($tableStatus);
361
362
        // $set = null;
363
        // if(isset($rights["insert"]) || !support("table")) {
364
        //     $set = "";
365
        //     foreach((array) $queryOptions["where"] as $val) {
366
        //         if($foreignKeys[$val["col"]] && count($foreignKeys[$val["col"]]) == 1 && ($val["op"] == "="
367
        //             || (!$val["op"] && !preg_match('~[_%]~', $val["val"])) // LIKE in Editor
368
        //         )) {
369
        //             $set .= "&set" . urlencode("[" . $this->util->bracketEscape($val["col"]) . "]") . "=" . urlencode($val["val"]);
370
        //         }
371
        //     }
372
        // }
373
        // $this->util->selectLinks($tableStatus, $set);
374
375
        // if($page == "last")
376
        // {
377
        //     $isGroup = count($group) < count($select);
378
        //     $found_rows = $this->driver->result($this->driver->sqlForRowCount($table, $where, $isGroup, $group));
379
        //     $page = \floor(\max(0, $found_rows - 1) / $limit);
380
        // }
381
382
        $options = $this->getAllOptions($select, $columns, $indexes, $limit, $textLength, $queryOptions);
383
        $entity = $this->getSelectEntity($table, $columns, $fields, $select, $group, $where, $order, $unselected, $limit, $page);
0 ignored issues
show
Bug introduced by
It seems like $unselected can also be of type null; however, parameter $unselected of Lagdo\DbAdmin\DbAdmin\Ta...dmin::getSelectEntity() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

383
        $entity = $this->getSelectEntity($table, $columns, $fields, $select, $group, $where, $order, /** @scrutinizer ignore-type */ $unselected, $limit, $page);
Loading history...
384
        $query = $this->driver->buildSelectQuery($entity);
385
        // From adminer.inc.php
386
        $query = str_replace("\n", " ", $query);
387
388
        return [$options, $query, $select, $fields, $foreignKeys, $columns, $indexes,
389
            $where, $group, $order, $limit, $page, $textLength, $tableName, $unselected];
390
    }
391
392
    /**
393
     * Get required data for create/update on tables
394
     *
395
     * @param string $table The table name
396
     * @param array $queryOptions The query options
397
     *
398
     * @return array
399
     * @throws Exception
400
     */
401
    public function getSelectData(string $table, array $queryOptions = []): array
402
    {
403
        list($options, $query) = $this->prepareSelect($table, $queryOptions);
404
405
        $query = $this->util->html($query);
406
        $mainActions = [
407
            'select-exec' => $this->trans->lang('Execute'),
408
            'insert-table' => $this->trans->lang('New item'),
409
            'select-back' => $this->trans->lang('Back'),
410
        ];
411
412
        return compact('mainActions', 'options', 'query');
413
    }
414
415
    /**
416
     * @param string $query
417
     * @param int $page
418
     *
419
     * @return array
420
     */
421
    private function executeQuery(string $query, int $page): array
422
    {
423
        // From driver.inc.php
424
        $start = microtime(true);
425
        $statement = $this->driver->execute($query);
426
        // From adminer.inc.php
427
        $duration = $this->trans->formatTime($start); // Compute and format the duration
428
429
        if (!$statement) {
430
            return ['error' => $this->driver->error()];
431
        }
432
        // From select.inc.php
433
        $rows = [];
434
        while (($row = $statement->fetchAssoc())) {
435
            if ($page && $this->driver->jush() == "oracle") {
436
                unset($row["RNUM"]);
437
            }
438
            $rows[] = $row;
439
        }
440
441
        return [$rows, $duration];
442
    }
443
444
    /**
445
     * @param array $rows
446
     * @param array $select
447
     * @param array $fields
448
     * @param array $unselected
449
     *
450
     * @return array
451
     */
452
    private function getResultHeaders(array $rows, array $select, array $fields, array $unselected): array
453
    {
454
        // Results headers
455
        $headers = [
456
            '', // !$group && $select ? '' : lang('Modify');
457
        ];
458
        $names = [];
459
        // $functions = [];
460
        reset($select);
461
        $rank = 1;
462
        foreach ($rows[0] as $key => $value) {
463
            $header = [];
464
            if (!isset($unselected[$key])) {
465
                $value = $queryOptions["columns"][key($select)] ?? [];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $queryOptions seems to never exist and therefore isset should always be false.
Loading history...
466
                $fun = $value["fun"] ?? '';
467
                $field = $fields[$select ? ($value ? $value["col"] : current($select)) : $key];
468
                $name = ($field ? $this->util->fieldName($field, $rank) : ($fun ? "*" : $key));
469
                $header = compact('value', 'field', 'name');
470
                if ($name != "") {
471
                    $rank++;
472
                    $names[$key] = $name;
473
                    $column = $this->driver->escapeId($key);
474
                    // $href = remove_from_uri('(order|desc)[^=]*|page') . '&order%5B0%5D=' . urlencode($key);
475
                    // $desc = "&desc%5B0%5D=1";
476
                    $header['column'] = $column;
477
                    $header['key'] = $this->util->html($this->util->bracketEscape($key));
478
                    $header['sql'] = $this->util->applySqlFunction($fun, $name); //! columns looking like functions
479
                }
480
                // $functions[$key] = $fun;
481
                next($select);
482
            }
483
            $headers[] = $header;
484
        }
485
        return [$headers, $names];
486
    }
487
488
    /**
489
     * @param array $rows
490
     * @param array $queryOptions
491
     *
492
     * @return array
493
     */
494
    /*private function getValuesLengths(array $rows, array $queryOptions): array
495
    {
496
         $lengths = [];
497
         if($queryOptions["modify"])
498
         {
499
             foreach($rows as $row)
500
             {
501
                 foreach($row as $key => $value)
502
                 {
503
                     $lengths[$key] = \max($lengths[$key], \min(40, strlen(\utf8_decode($value))));
504
                 }
505
             }
506
         }
507
         return $lengths;
508
    }*/
509
510
    /**
511
     * @param array $row
512
     * @param array $indexes
513
     *
514
     * @return array
515
     */
516
    private function getUniqueIds(array $row, array $indexes): array
517
    {
518
        $uniqueIds = $this->util->uniqueIds($row, $indexes);
519
        if (empty($uniqueIds)) {
520
            $pattern = '~^(COUNT\((\*|(DISTINCT )?`(?:[^`]|``)+`)\)|(AVG|GROUP_CONCAT|MAX|MIN|SUM)\(`(?:[^`]|``)+`\))$~';
521
            foreach ($row as $key => $value) {
522
                if (!preg_match($pattern, $key)) {
523
                    //! columns looking like functions
524
                    $uniqueIds[$key] = $value;
525
                }
526
            }
527
        }
528
        return $uniqueIds;
529
    }
530
531
    /**
532
     * @param array $row
533
     * @param array $indexes
534
     *
535
     * @return array
536
     */
537
    private function getRowIds(array $row, array $indexes): array
538
    {
539
        $uniqueIds = $this->getUniqueIds($row, $indexes);
540
        // Unique identifier to edit returned data.
541
        // $unique_idf = "";
542
        $rowIds = ['where' => [], 'null' => []];
543
        foreach ($uniqueIds as $key => $value) {
544
            $key = trim($key);
545
            $type = '';
546
            $collation = '';
547
            if (isset($fields[$key])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fields seems to never exist and therefore isset should always be false.
Loading history...
548
                $type = $fields[$key]->type;
549
                $collation = $fields[$key]->collation;
550
            }
551
            if (($this->driver->jush() == "sql" || $this->driver->jush() == "pgsql") &&
552
                preg_match('~char|text|enum|set~', $type) && strlen($value) > 64) {
553
                $key = (strpos($key, '(') ? $key : $this->driver->escapeId($key)); //! columns looking like functions
554
                $key = "MD5(" . ($this->driver->jush() != 'sql' || preg_match("~^utf8~", $collation) ?
555
                        $key : "CONVERT($key USING " . $this->driver->charset() . ")") . ")";
556
                $value = md5($value);
557
            }
558
            if ($value !== null) {
559
                $rowIds['where'][$this->util->bracketEscape($key)] = $value;
560
            } else {
561
                $rowIds['null'][] = $this->util->bracketEscape($key);
562
            }
563
            // $unique_idf .= "&" . ($value !== null ? \urlencode("where[" . $this->util->bracketEscape($key) . "]") .
564
            //     "=" . \urlencode($value) : \urlencode("null[]") . "=" . \urlencode($key));
565
        }
566
        return $rowIds;
567
    }
568
569
    /**
570
     * @param array $row
571
     * @param array $names
572
     * @param int $textLength
573
     *
574
     * @return array
575
     */
576
    private function getRowColumns(array $row, array $names, int $textLength): array
577
    {
578
        $cols = [];
579
        foreach ($row as $key => $value) {
580
            if (isset($names[$key])) {
581
                $field = $fields[$key] ?? new TableFieldEntity();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fields does not exist. Did you maybe mean $email_fields?
Loading history...
582
                $value = $this->driver->value($value, $field);
583
                if ($value != "" && (!isset($email_fields[$key]) || $email_fields[$key] != "")) {
584
                    //! filled e-mails can be contained on other pages
585
                    $email_fields[$key] = ($this->util->isMail($value) ? $names[$key] : "");
586
                }
587
588
                $link = "";
589
590
                $value = $this->util->selectValue($value, $link, $field, $textLength);
591
                $text = preg_match('~text|lob~', $field->type);
592
593
                $cols[] = compact(/*'id', */'text', 'value'/*, 'editable'*/);
594
            }
595
        }
596
        return $cols;
597
    }
598
599
    /**
600
     * Get required data for create/update on tables
601
     *
602
     * @param string $table The table name
603
     * @param array $queryOptions The query options
604
     *
605
     * @return array
606
     * @throws Exception
607
     */
608
    public function execSelect(string $table, array $queryOptions): array
609
    {
610
        list(, $query, $select, $fields, , , $indexes, $where, $group, , $limit, $page,
611
            $textLength, , $unselected) = $this->prepareSelect($table, $queryOptions);
612
613
        list($rows, $duration) = $this->executeQuery($query, $page);
614
        if (!$rows) {
615
            return ['error' => $this->trans->lang('No rows.')];
616
        }
617
        // $backward_keys = $this->driver->backwardKeys($table, $tableName);
618
        // lengths = $this->getValuesLengths($rows, $queryOptions);
619
620
        list($headers, $names) = $this->getResultHeaders($rows, $select, $fields, $unselected);
621
622
        $results = [];
623
        foreach ($rows as $row) {
624
            // Unique identifier to edit returned data.
625
            $rowIds = $this->getRowIds($row, $indexes);
626
            $cols = $this->getRowColumns($row, $names, $textLength);
627
            $results[] = ['ids' => $rowIds, 'cols' => $cols];
628
        }
629
630
        $isGroup = count($group) < count($select);
631
        $total = $this->driver->result($this->driver->sqlForRowCount($table, $where, $isGroup, $group));
632
633
        $rows = $results;
634
        $error = null;
635
        return compact('duration', 'headers', 'query', 'rows', 'limit', 'total', 'error');
636
    }
637
}
638