1<?php
2// dbl.php -- database interface layer
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5class Dbl_Result {
6    public $affected_rows;
7    public $insert_id;
8    public $warning_count;
9
10    function __construct(mysqli $dblink) {
11        $this->affected_rows = $dblink->affected_rows;
12        $this->insert_id = $dblink->insert_id;
13        $this->warning_count = $dblink->warning_count;
14    }
15}
16
17class Dbl_MultiResult {
18    private $dblink;
19    private $flags;
20    private $qstr;
21    private $more;
22
23    function __construct(mysqli $dblink, $flags, $qstr, $result) {
24        $this->dblink = $dblink;
25        $this->flags = $flags;
26        $this->qstr = $qstr;
27        $this->more = $result;
28    }
29    function next() {
30        // XXX does processing stop at first error?
31        if ($this->more === null)
32            $this->more = $this->dblink->more_results() ? $this->dblink->next_result() : -1;
33        if ($this->more === -1)
34            return false;
35        else if ($this->more) {
36            $result = $this->dblink->store_result();
37            $this->more = null;
38        } else
39            $result = false;
40        return Dbl::do_result($this->dblink, $this->flags, $this->qstr, $result);
41    }
42    function free_all() {
43        while (($result = $this->next()))
44            Dbl::free($result);
45    }
46}
47
48class Dbl {
49    const F_RAW = 1;
50    const F_APPLY = 2;
51    const F_LOG = 4;
52    const F_ERROR = 8;
53    const F_ALLOWERROR = 16;
54    const F_MULTI = 32;
55    const F_ECHO = 64;
56    const F_NOEXEC = 128;
57
58    static public $nerrors = 0;
59    static public $default_dblink;
60    static private $error_handler = "Dbl::default_error_handler";
61    static private $query_log = false;
62    static private $query_log_key = false;
63    static private $query_log_file = null;
64    static public $check_warnings = true;
65    static public $landmark_sanitizer = "/^Dbl::/";
66
67    static function has_error() {
68        return self::$nerrors > 0;
69    }
70
71    static function make_dsn($opt) {
72        if (isset($opt["dsn"])) {
73            if (is_string($opt["dsn"]))
74                return $opt["dsn"];
75        } else {
76            list($user, $password, $host, $name) =
77                [get($opt, "dbUser"), get($opt, "dbPassword"), get($opt, "dbHost"), get($opt, "dbName")];
78            $user = ($user !== null ? $user : $name);
79            $password = ($password !== null ? $password : $name);
80            $host = ($host !== null ? $host : "localhost");
81            if (is_string($user) && is_string($password) && is_string($host) && is_string($name))
82                return "mysql://" . urlencode($user) . ":" . urlencode($password) . "@" . urlencode($host) . "/" . urlencode($name);
83        }
84        return null;
85    }
86
87    static function sanitize_dsn($dsn) {
88        return preg_replace('{\A(\w+://[^/:]*:)[^\@/]+([\@/])}', '$1PASSWORD$2', $dsn);
89    }
90
91    static function connect_dsn($dsn) {
92        global $Opt;
93
94        $dbhost = $dbuser = $dbpass = $dbname = $dbport = null;
95        if ($dsn && preg_match('|^mysql://([^:@/]*)/(.*)|', $dsn, $m)) {
96            $dbhost = urldecode($m[1]);
97            $dbname = urldecode($m[2]);
98        } else if ($dsn && preg_match('|^mysql://([^:@/]*)@([^/]*)/(.*)|', $dsn, $m)) {
99            $dbhost = urldecode($m[2]);
100            $dbuser = urldecode($m[1]);
101            $dbname = urldecode($m[3]);
102        } else if ($dsn && preg_match('|^mysql://([^:@/]*):([^@/]*)@([^/]*)/(.*)|', $dsn, $m)) {
103            $dbhost = urldecode($m[3]);
104            $dbuser = urldecode($m[1]);
105            $dbpass = urldecode($m[2]);
106            $dbname = urldecode($m[4]);
107        }
108        if (!$dbname || $dbname === "mysql" || substr($dbname, -7) === "_schema")
109            return array(null, null);
110
111        $dbsock = get($Opt, "dbSocket");
112        if ($dbsock && $dbport === null)
113            $dbport = ini_get("mysqli.default_port");
114        if ($dbpass === null)
115            $dbpass = ini_get("mysqli.default_pw");
116        if ($dbuser === null)
117            $dbuser = ini_get("mysqli.default_user");
118        if ($dbhost === null)
119            $dbhost = ini_get("mysqli.default_host");
120
121        if ($dbsock)
122            $dblink = new mysqli($dbhost, $dbuser, $dbpass, "", $dbport, $dbsock);
123        else if ($dbport !== null)
124            $dblink = new mysqli($dbhost, $dbuser, $dbpass, "", $dbport);
125        else
126            $dblink = new mysqli($dbhost, $dbuser, $dbpass);
127
128        if ($dblink && !mysqli_connect_errno() && $dblink->select_db($dbname)) {
129            // We send binary strings to MySQL, so we don't want warnings
130            // about non-UTF-8 data
131            $dblink->set_charset("binary");
132            // The necessity of the following line is explosively terrible
133            // (the default is 1024/!?))(U#*@$%&!U
134            $dblink->query("set group_concat_max_len=4294967295");
135        } else if ($dblink) {
136            $dblink->close();
137            $dblink = null;
138        }
139        return array($dblink, $dbname);
140    }
141
142    static function set_default_dblink($dblink) {
143        self::$default_dblink = $dblink;
144    }
145
146    static function set_error_handler($callable) {
147        self::$error_handler = $callable ? : "Dbl::default_error_handler";
148    }
149
150    static function landmark() {
151        return caller_landmark(1, self::$landmark_sanitizer);
152    }
153
154    static function default_error_handler($dblink, $query) {
155        trigger_error(self::landmark() . ": database error: $dblink->error in $query");
156    }
157
158    static private function query_args($args, $flags, $log_location) {
159        $argpos = 0;
160        $dblink = self::$default_dblink;
161        if (is_object($args[0])) {
162            $argpos = 1;
163            $dblink = $args[0];
164        } else if ($args[0] === null && count($args) > 1)
165            $argpos = 1;
166        if ((($flags & self::F_RAW) && count($args) != $argpos + 1)
167            || (($flags & self::F_APPLY) && count($args) > $argpos + 2))
168            trigger_error(self::landmark() . ": wrong number of arguments");
169        else if (($flags & self::F_APPLY) && isset($args[$argpos + 1])
170                 && !is_array($args[$argpos + 1]))
171            trigger_error(self::landmark() . ": argument is not array");
172        $q = $args[$argpos];
173        if (($flags & self::F_MULTI) && is_array($q))
174            $q = join(";", $q);
175        if ($log_location && self::$query_log !== false) {
176            self::$query_log_key = $qx = simplify_whitespace($q);
177            if (isset(self::$query_log[$qx]))
178                ++self::$query_log[$qx][1];
179            else
180                self::$query_log[$qx] = [0, 1, self::landmark()];
181        }
182        if (count($args) === $argpos + 1)
183            return array($dblink, $q, array());
184        else if ($flags & self::F_APPLY)
185            return array($dblink, $q, $args[$argpos + 1]);
186        else
187            return array($dblink, $q, array_slice($args, $argpos + 1));
188    }
189
190    static private function format_query_args($dblink, $qstr, $argv) {
191        $original_qstr = $qstr;
192        $strpos = $argpos = 0;
193        $usedargs = [];
194        $simpleargs = true;
195        while (($strpos = strpos($qstr, "?", $strpos)) !== false) {
196            // argument name
197            $nextpos = $strpos + 1;
198            $nextch = substr($qstr, $nextpos, 1);
199            if ($nextch === "?") {
200                $qstr = substr($qstr, 0, $strpos + 1) . substr($qstr, $strpos + 2);
201                $strpos = $strpos + 1;
202                continue;
203            } else if ($nextch === "{"
204                       && ($rbracepos = strpos($qstr, "}", $nextpos + 1)) !== false) {
205                $thisarg = substr($qstr, $nextpos + 1, $rbracepos - $nextpos - 1);
206                if ($thisarg === (string) (int) $thisarg)
207                    --$thisarg;
208                $nextpos = $rbracepos + 1;
209                $nextch = substr($qstr, $nextpos, 1);
210                $simpleargs = false;
211            } else {
212                do {
213                    $thisarg = $argpos;
214                    ++$argpos;
215                } while (isset($usedargs[$thisarg]));
216            }
217            if (!array_key_exists($thisarg, $argv))
218                trigger_error(self::landmark() . ": query '$original_qstr' argument " . (is_int($thisarg) ? $thisarg + 1 : $thisarg) . " not set");
219            $usedargs[$thisarg] = true;
220            // argument format
221            $arg = get($argv, $thisarg);
222            if ($nextch === "e" || $nextch === "E") {
223                if ($arg === null)
224                    $arg = ($nextch === "e" ? " IS NULL" : " IS NOT NULL");
225                else if (is_int($arg) || is_float($arg))
226                    $arg = ($nextch === "e" ? "=" : "!=") . $arg;
227                else
228                    $arg = ($nextch === "e" ? "='" : "!='") . $dblink->real_escape_string($arg) . "'";
229                ++$nextpos;
230            } else if ($nextch === "a" || $nextch === "A") {
231                if ($arg === null)
232                    $arg = array();
233                else if (is_int($arg) || is_float($arg) || is_string($arg))
234                    $arg = array($arg);
235                foreach ($arg as $x)
236                    if (!is_int($x) && !is_float($x)) {
237                        reset($arg);
238                        foreach ($arg as &$y)
239                            $y = "'" . $dblink->real_escape_string($y) . "'";
240                        unset($y);
241                        break;
242                    }
243                if (empty($arg)) {
244                    // We want `foo IN ()` and `foo NOT IN ()`.
245                    // That is, we want `false` and `true`. We compromise. The
246                    // statement `foo=NULL` is always NULL -- which is falsy
247                    // -- even if `foo IS NULL`. The statement `foo IS NOT
248                    // NULL` is true unless `foo IS NULL`.
249                    $arg = ($nextch === "a" ? "=NULL" : " IS NOT NULL");
250                } else if (count($arg) === 1) {
251                    reset($arg);
252                    $arg = ($nextch === "a" ? "=" : "!=") . current($arg);
253                } else
254                    $arg = ($nextch === "a" ? " IN (" : " NOT IN (") . join(", ", $arg) . ")";
255                ++$nextpos;
256            } else if ($nextch === "s") {
257                $arg = $dblink->real_escape_string($arg);
258                ++$nextpos;
259            } else if ($nextch === "l") {
260                $arg = sqlq_for_like($arg);
261                ++$nextpos;
262                if (substr($qstr, $nextpos + 1, 1) === "s")
263                    ++$nextpos;
264                else
265                    $arg = "'" . $arg . "'";
266            } else if ($nextch === "v") {
267                ++$nextpos;
268                if (!is_array($arg) || empty($arg)) {
269                    trigger_error(self::landmark() . ": query '$original_qstr' argument " . (is_int($thisarg) ? $thisarg + 1 : $thisarg) . " should be nonempty array");
270                    $arg = "NULL";
271                } else {
272                    $alln = -1;
273                    $vs = [];
274                    foreach ($arg as $x) {
275                        if (!is_array($x))
276                            $x = [$x];
277                        $n = count($x);
278                        if ($alln === -1)
279                            $alln = $n;
280                        if ($alln !== $n && $alln !== -2) {
281                            trigger_error(self::landmark() . ": query '$original_qstr' argument " . (is_int($thisarg) ? $thisarg + 1 : $thisarg) . " has components of different lengths");
282                            $alln = -2;
283                        }
284                        foreach ($x as &$y)
285                            if ($y === null)
286                                $y = "NULL";
287                            else if (!is_int($y) && !is_float($y))
288                                $y = "'" . $dblink->real_escape_string($y) . "'";
289                        unset($y);
290                        $vs[] = "(" . join(",", $x) . ")";
291                    }
292                    $arg = join(", ", $vs);
293                }
294            } else {
295                if ($arg === null)
296                    $arg = "NULL";
297                else if (!is_int($arg) && !is_float($arg))
298                    $arg = "'" . $dblink->real_escape_string($arg) . "'";
299            }
300            // combine
301            $suffix = substr($qstr, $nextpos);
302            $qstr = substr($qstr, 0, $strpos) . $arg . $suffix;
303            $strpos = strlen($qstr) - strlen($suffix);
304        }
305        if ($simpleargs && $argpos !== count($argv))
306            trigger_error(self::landmark() . ": query '$original_qstr' unused arguments");
307        return $qstr;
308    }
309
310    static function format_query(/* [$dblink,] $qstr, ... */) {
311        list($dblink, $qstr, $argv) = self::query_args(func_get_args(), 0, false);
312        return self::format_query_args($dblink, $qstr, $argv);
313    }
314
315    static function format_query_apply(/* [$dblink,] $qstr, [$argv] */) {
316        list($dblink, $qstr, $argv) = self::query_args(func_get_args(), self::F_APPLY, false);
317        return self::format_query_args($dblink, $qstr, $argv);
318    }
319
320    static private function call_query($dblink, $flags, $qfunc, $qstr) {
321        if ($flags & self::F_ECHO)
322            error_log($qstr);
323        if ($flags & self::F_NOEXEC)
324            return null;
325        if (self::$query_log_key) {
326            $time = microtime(true);
327            $result = $dblink->$qfunc($qstr);
328            self::$query_log[self::$query_log_key][0] += microtime(true) - $time;
329            self::$query_log_key = false;
330        } else
331            $result = $dblink->$qfunc($qstr);
332        return $result;
333    }
334
335    static private function do_query_with($dblink, $qstr, $argv, $flags) {
336        if (!($flags & self::F_RAW))
337            $qstr = self::format_query_args($dblink, $qstr, $argv);
338        if (!$qstr) {
339            error_log(self::landmark() . ": empty query");
340            return false;
341        }
342        return self::do_result($dblink, $flags, $qstr, self::call_query($dblink, $flags, "query", $qstr));
343    }
344
345    static private function do_query($args, $flags) {
346        list($dblink, $qstr, $argv) = self::query_args($args, $flags, true);
347        return self::do_query_with($dblink, $qstr, $argv, $flags);
348    }
349
350    static function do_query_on($dblink, $args, $flags) {
351        list($ignored_dblink, $qstr, $argv) = self::query_args($args, $flags, true);
352        return self::do_query_with($dblink, $qstr, $argv, $flags);
353    }
354
355    static public function do_result($dblink, $flags, $qstr, $result) {
356        if ($result === false && $dblink->errno) {
357            if (!($flags & self::F_ALLOWERROR))
358                ++self::$nerrors;
359            if ($flags & self::F_ERROR)
360                call_user_func(self::$error_handler, $dblink, $qstr);
361            else if ($flags & self::F_LOG)
362                error_log(self::landmark() . ": database error: " . $dblink->error . " in $qstr");
363        } else if ($result === false || $result === true)
364            $result = new Dbl_Result($dblink);
365        if (self::$check_warnings && !($flags & self::F_ALLOWERROR)
366            && $dblink->warning_count) {
367            $wresult = $dblink->query("show warnings");
368            while ($wresult && ($wrow = $wresult->fetch_row()))
369                error_log(self::landmark() . ": database warning: $wrow[0] ($wrow[1]) $wrow[2]");
370            $wresult && $wresult->close();
371        }
372        return $result;
373    }
374
375    static private function do_multi_query($args, $flags) {
376        list($dblink, $qstr, $argv) = self::query_args($args, $flags, true);
377        if (!($flags & self::F_RAW))
378            $qstr = self::format_query_args($dblink, $qstr, $argv);
379        return new Dbl_MultiResult($dblink, $flags, $qstr, self::call_query($dblink, $flags, "multi_query", $qstr));
380    }
381
382    static function query(/* [$dblink,] $qstr, ... */) {
383        return self::do_query(func_get_args(), 0);
384    }
385
386    static function query_raw(/* [$dblink,] $qstr */) {
387        return self::do_query(func_get_args(), self::F_RAW);
388    }
389
390    static function query_apply(/* [$dblink,] $qstr, [$argv] */) {
391        return self::do_query(func_get_args(), self::F_APPLY);
392    }
393
394    static function q(/* [$dblink,] $qstr, ... */) {
395        return self::do_query(func_get_args(), 0);
396    }
397
398    static function q_raw(/* [$dblink,] $qstr */) {
399        return self::do_query(func_get_args(), self::F_RAW);
400    }
401
402    static function q_apply(/* [$dblink,] $qstr, [$argv] */) {
403        return self::do_query(func_get_args(), self::F_APPLY);
404    }
405
406    static function qx(/* [$dblink,] $qstr, ... */) {
407        return self::do_query(func_get_args(), self::F_ALLOWERROR);
408    }
409
410    static function qx_raw(/* [$dblink,] $qstr */) {
411        return self::do_query(func_get_args(), self::F_RAW | self::F_ALLOWERROR);
412    }
413
414    static function qx_apply(/* [$dblink,] $qstr, [$argv] */) {
415        return self::do_query(func_get_args(), self::F_APPLY | self::F_ALLOWERROR);
416    }
417
418    static function ql(/* [$dblink,] $qstr, ... */) {
419        return self::do_query(func_get_args(), self::F_LOG);
420    }
421
422    static function ql_raw(/* [$dblink,] $qstr */) {
423        return self::do_query(func_get_args(), self::F_RAW | self::F_LOG);
424    }
425
426    static function ql_apply(/* [$dblink,] $qstr, [$argv] */) {
427        return self::do_query(func_get_args(), self::F_APPLY | self::F_LOG);
428    }
429
430    static function qe(/* [$dblink,] $qstr, ... */) {
431        return self::do_query(func_get_args(), self::F_ERROR);
432    }
433
434    static function qe_raw(/* [$dblink,] $qstr */) {
435        return self::do_query(func_get_args(), self::F_RAW | self::F_ERROR);
436    }
437
438    static function qe_apply(/* [$dblink,] $qstr, [$argv] */) {
439        return self::do_query(func_get_args(), self::F_APPLY | self::F_ERROR);
440    }
441
442    static function multi_q(/* [$dblink,] $qstr, ... */) {
443        return self::do_multi_query(func_get_args(), self::F_MULTI);
444    }
445
446    static function multi_q_raw(/* [$dblink,] $qstr */) {
447        return self::do_multi_query(func_get_args(), self::F_MULTI | self::F_RAW);
448    }
449
450    static function multi_q_apply(/* [$dblink,] $qstr, [$argv] */) {
451        return self::do_multi_query(func_get_args(), self::F_MULTI | self::F_APPLY);
452    }
453
454    static function multi_ql(/* [$dblink,] $qstr, ... */) {
455        return self::do_multi_query(func_get_args(), self::F_MULTI | self::F_LOG);
456    }
457
458    static function multi_ql_raw(/* [$dblink,] $qstr */) {
459        return self::do_multi_query(func_get_args(), self::F_MULTI | self::F_RAW | self::F_LOG);
460    }
461
462    static function multi_ql_apply(/* [$dblink,] $qstr, [$argv] */) {
463        return self::do_multi_query(func_get_args(), self::F_MULTI | self::F_APPLY | self::F_LOG);
464    }
465
466    static function multi_qe(/* [$dblink,] $qstr, ... */) {
467        return self::do_multi_query(func_get_args(), self::F_MULTI | self::F_ERROR);
468    }
469
470    static function multi_qe_raw(/* [$dblink,] $qstr */) {
471        return self::do_multi_query(func_get_args(), self::F_MULTI | self::F_RAW | self::F_ERROR);
472    }
473
474    static function multi_qe_apply(/* [$dblink,] $qstr, [$argv] */) {
475        return self::do_multi_query(func_get_args(), self::F_MULTI | self::F_APPLY | self::F_ERROR);
476    }
477
478    static function make_multi_query_stager($dblink, $flags) {
479        $qs = $qvs = [];
480        return function ($q, $qv = []) use ($dblink, $flags, &$qs, &$qvs) {
481            if ($q && $q !== true) {
482                $qs[] = $q;
483                $qvs = array_merge($qvs, $qv);
484            }
485            if ((!$q || $q === true || count($qs) >= 50 || count($qv) >= 1000)
486                && !empty($qs)) {
487                $mresult = Dbl::do_multi_query([$dblink, join("; ", $qs), $qvs], self::F_MULTI | self::F_APPLY | $flags);
488                $mresult->free_all();
489                $qs = $qvs = [];
490            }
491        };
492    }
493
494    static function make_multi_ql_stager($dblink = null) {
495        return self::make_multi_query_stager($dblink ? : self::$default_dblink, self::F_LOG);
496    }
497
498    static function make_multi_qe_stager($dblink = null) {
499        return self::make_multi_query_stager($dblink ? : self::$default_dblink, self::F_ERROR);
500    }
501
502    static function free($result) {
503        if ($result && $result instanceof mysqli_result)
504            $result->close();
505    }
506
507    // array of all first columns
508    static private function do_make_result($args, $flags = self::F_ERROR) {
509        if (count($args) == 1 && !is_string($args[0]))
510            return $args[0];
511        else
512            return self::do_query($args, $flags);
513    }
514
515    static function fetch_value(/* $result | [$dblink,] $query, ... */) {
516        $result = self::do_make_result(func_get_args());
517        $x = $result ? $result->fetch_row() : null;
518        $result && $result->close();
519        return $x ? $x[0] : null;
520    }
521
522    static function fetch_ivalue(/* $result | [$dblink,] $query, ... */) {
523        $result = self::do_make_result(func_get_args());
524        $x = $result ? $result->fetch_row() : null;
525        $result && $result->close();
526        return $x ? (int) $x[0] : null;
527    }
528
529    static function fetch_rows(/* $result | [$dblink,] $query, ... */) {
530        $result = self::do_make_result(func_get_args());
531        $x = [];
532        while (($row = ($result ? $result->fetch_row() : null)))
533            $x[] = $row;
534        $result && $result->close();
535        return $x;
536    }
537
538    static function fetch_objects(/* $result | [$dblink,] $query, ... */) {
539        $result = self::do_make_result(func_get_args());
540        $x = [];
541        while (($row = ($result ? $result->fetch_object() : null)))
542            $x[] = $row;
543        $result && $result->close();
544        return $x;
545    }
546
547    static function fetch_first_row(/* $result | [$dblink,] $query, ... */) {
548        $result = self::do_make_result(func_get_args());
549        $x = $result ? $result->fetch_row() : null;
550        $result && $result->close();
551        return $x;
552    }
553
554    static function fetch_first_object(/* $result | [$dblink,] $query, ... */) {
555        $result = self::do_make_result(func_get_args());
556        $x = $result ? $result->fetch_object() : null;
557        $result && $result->close();
558        return $x;
559    }
560
561    static function fetch_first_columns(/* $result | [$dblink,] $query, ... */) {
562        $result = self::do_make_result(func_get_args());
563        $x = array();
564        while ($result && ($row = $result->fetch_row()))
565            $x[] = $row[0];
566        $result && $result->close();
567        return $x;
568    }
569
570    static function fetch_map(/* $result | [$dblink,] $query, ... */) {
571        $result = self::do_make_result(func_get_args());
572        $x = array();
573        while ($result && ($row = $result->fetch_row()))
574            $x[$row[0]] = count($row) == 2 ? $row[1] : array_slice($row, 1);
575        $result && $result->close();
576        return $x;
577    }
578
579    static function fetch_iimap(/* $result | [$dblink,] $query, ... */) {
580        $result = self::do_make_result(func_get_args());
581        $x = array();
582        while ($result && ($row = $result->fetch_row())) {
583            assert(count($row) == 2);
584            $x[(int) $row[0]] = ($row[1] === null ? null : (int) $row[1]);
585        }
586        $result && $result->close();
587        return $x;
588    }
589
590    static function compare_and_swap($dblink, $value_query, $value_query_args,
591                                     $callback, $update_query, $update_query_args) {
592        while (1) {
593            $result = self::qe_apply($dblink, $value_query, $value_query_args);
594            $value = self::fetch_value($result);
595            $new_value = call_user_func($callback, $value);
596            if ($new_value === $value)
597                return $new_value;
598            $update_query_args["expected"] = $value;
599            $update_query_args["desired"] = $new_value;
600            $result = self::qe_apply($dblink, $update_query, $update_query_args);
601            if ($result->affected_rows)
602                return $new_value;
603        }
604    }
605
606    static function log_queries($limit, $file = false) {
607        if (is_float($limit))
608            $limit = $limit >= 1 || ($limit > 0 && mt_rand() < $limit * mt_getrandmax());
609        if (!$limit)
610            self::$query_log = false;
611        else if (self::$query_log === false) {
612            register_shutdown_function("Dbl::shutdown");
613            self::$query_log = [];
614            self::$query_log_file = $file;
615        }
616    }
617
618    static function shutdown() {
619        if (self::$query_log) {
620            uasort(self::$query_log, function ($a, $b) {
621                return $b[0] < $a[0] ? -1 : $b[0] > $a[0];
622            });
623            $self = Navigation::self();
624            $i = 1;
625            $n = count(self::$query_log);
626            $t = [0, 0];
627            $qlog = "";
628            foreach (self::$query_log as $where => $what) {
629                $a = [$what[0], $what[1], $what[2], $where];
630                $qlog .= "query_log: $self #$i/$n: " . json_encode($a) . "\n";
631                ++$i;
632                $t[0] += $what[0];
633                $t[1] += $what[1];
634            }
635            $qlog .= "query_log: total: " . json_encode($t) . "\n";
636            if (self::$query_log_file)
637                @file_put_contents(self::$query_log_file, $qlog, FILE_APPEND);
638            else
639                error_log($qlog);
640        }
641        self::$query_log = false;
642    }
643
644    static function utf8(/* [$dblink,] $qstr */) {
645        $args = func_get_args();
646        $dblink = count($args) > 1 ? $args[0] : self::$default_dblink;
647        $utf8 = $dblink->server_version >= 50503 ? "utf8mb4" : "utf8";
648        $qstr = count($args) > 1 ? $args[1] : $args[0];
649        return "_" . $utf8 . $qstr;
650    }
651
652    static function utf8ci(/* [$dblink,] $qstr */) {
653        $args = func_get_args();
654        $dblink = count($args) > 1 ? $args[0] : self::$default_dblink;
655        $utf8 = $dblink->server_version >= 50503 ? "utf8mb4" : "utf8";
656        $qstr = count($args) > 1 ? $args[1] : $args[0];
657        return "_" . $utf8 . $qstr . " collate " . $utf8 . "_general_ci";
658    }
659}
660
661// number of rows returned by a select query, or 'false' if result is an error
662function edb_nrows($result) {
663    return $result ? $result->num_rows : false;
664}
665
666// next row as an array, or 'false' if no more rows or result is an error
667function edb_row($result) {
668    return $result ? $result->fetch_row() : false;
669}
670
671// array of all rows as arrays
672function edb_rows($result) {
673    $x = array();
674    while ($result && ($row = $result->fetch_row()))
675        $x[] = $row;
676    Dbl::free($result);
677    return $x;
678}
679
680// array of all first columns as arrays
681function edb_first_columns($result) {
682    $x = array();
683    while ($result && ($row = $result->fetch_row()))
684        $x[] = $row[0];
685    Dbl::free($result);
686    return $x;
687}
688
689// map of all rows
690function edb_map($result) {
691    $x = array();
692    while ($result && ($row = $result->fetch_row()))
693        $x[$row[0]] = (count($row) == 2 ? $row[1] : array_slice($row, 1));
694    Dbl::free($result);
695    return $x;
696}
697
698// next row as an object, or 'false' if no more rows or result is an error
699function edb_orow($result) {
700    return $result ? $result->fetch_object() : false;
701}
702
703// array of all rows as objects
704function edb_orows($result) {
705    $x = array();
706    while ($result && ($row = $result->fetch_object()))
707        $x[] = $row;
708    Dbl::free($result);
709    return $x;
710}
711
712// quoting for SQL
713function sqlq($value) {
714    return Dbl::$default_dblink->escape_string($value);
715}
716
717function sqlq_for_like($value) {
718    return preg_replace("/(?=[%_\\\\'\"\\x00\\n\\r\\x1a])/", "\\", $value);
719}
720
721function sql_in_numeric_set($set) {
722    if (empty($set))
723        return "=-1";
724    else if (count($set) == 1)
725        return "=" . $set[0];
726    else
727        return " in (" . join(", ", $set) . ")";
728}
729
730function sql_not_in_numeric_set($set) {
731    $sql = sql_in_numeric_set($set);
732    return ($sql[0] === "=" ? "!" : " not") . $sql;
733}
734