]> git.mxchange.org Git - friendica.git/blob - src/Database/DBA.php
Merge pull request #12547 from MrPetovan/bug/12545-plink-zindex
[friendica.git] / src / Database / DBA.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Database;
23
24 use Friendica\DI;
25 use mysqli;
26 use mysqli_result;
27 use mysqli_stmt;
28 use PDO;
29 use PDOStatement;
30
31 /**
32  * This class is for the low level database stuff that does driver specific things.
33  */
34 class DBA
35 {
36         /**
37          * Lowest possible date value
38          */
39         const NULL_DATE     = '0001-01-01';
40         /**
41          * Lowest possible datetime value
42          */
43         const NULL_DATETIME = '0001-01-01 00:00:00';
44
45         public static function connect(): bool
46         {
47                 return DI::dba()->connect();
48         }
49
50         /**
51          * Disconnects the current database connection
52          */
53         public static function disconnect()
54         {
55                 DI::dba()->disconnect();
56         }
57
58         /**
59          * Perform a reconnect of an existing database connection
60          */
61         public static function reconnect(): bool
62         {
63                 return DI::dba()->reconnect();
64         }
65
66         /**
67          * Return the database object.
68          * @return PDO|mysqli
69          */
70         public static function getConnection()
71         {
72                 return DI::dba()->getConnection();
73         }
74
75         /**
76          * Return the database driver string
77          *
78          * @return string with either "pdo" or "mysqli"
79          */
80         public static function getDriver(): string
81         {
82                 return DI::dba()->getDriver();
83         }
84
85         /**
86          * Returns the MySQL server version string
87          *
88          * This function discriminate between the deprecated mysql API and the current
89          * object-oriented mysqli API. Example of returned string: 5.5.46-0+deb8u1
90          *
91          * @return string
92          */
93         public static function serverInfo(): string
94         {
95                 return DI::dba()->serverInfo();
96         }
97
98         /**
99          * Returns the selected database name
100          *
101          * @return string
102          * @throws \Exception
103          */
104         public static function databaseName(): string
105         {
106                 return DI::dba()->databaseName();
107         }
108
109         /**
110          * Escape all SQL unsafe data
111          *
112          * @param string $str
113          * @return string escaped string
114          */
115         public static function escape(string $str): string
116         {
117                 return DI::dba()->escape($str);
118         }
119
120         /**
121          * Checks if the database is connected
122          *
123          * @return boolean is the database connected?
124          */
125         public static function connected(): bool
126         {
127                 return DI::dba()->connected();
128         }
129
130         /**
131          * Replaces ANY_VALUE() function by MIN() function,
132          * if the database server does not support ANY_VALUE().
133          *
134          * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5).
135          * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html
136          * A standard fall-back is to use MIN().
137          *
138          * @param string $sql An SQL string without the values
139          * @return string The input SQL string modified if necessary.
140          */
141         public static function anyValueFallback(string $sql): string
142         {
143                 return DI::dba()->anyValueFallback($sql);
144         }
145
146         /**
147          * beautifies the query - useful for "SHOW PROCESSLIST"
148          *
149          * This is safe when we bind the parameters later.
150          * The parameter values aren't part of the SQL.
151          *
152          * @param string $sql An SQL string without the values
153          * @return string The input SQL string modified if necessary.
154          */
155         public static function cleanQuery(string $sql): string
156         {
157                 $search = ["\t", "\n", "\r", "  "];
158                 $replace = [' ', ' ', ' ', ' '];
159                 do {
160                         $oldsql = $sql;
161                         $sql = str_replace($search, $replace, $sql);
162                 } while ($oldsql != $sql);
163
164                 return $sql;
165         }
166
167         /**
168          * Convert parameter array to an universal form
169          * @param array $args Parameter array
170          * @return array universalized parameter array
171          */
172         public static function getParam(array $args): array
173         {
174                 unset($args[0]);
175
176                 // When the second function parameter is an array then use this as the parameter array
177                 if ((count($args) > 0) && (is_array($args[1]))) {
178                         return $args[1];
179                 } else {
180                         return $args;
181                 }
182         }
183
184         /**
185          * Executes a prepared statement that returns data
186          * Example: $r = p("SELECT * FROM `post` WHERE `guid` = ?", $guid);
187          *
188          * Please only use it with complicated queries.
189          * For all regular queries please use DBA::select or DBA::exists
190          *
191          * @param string $sql SQL statement
192          * @return bool|object statement object or result object
193          * @throws \Exception
194          */
195         public static function p(string $sql)
196         {
197                 $params = self::getParam(func_get_args());
198
199                 return DI::dba()->p($sql, $params);
200         }
201
202         /**
203          * Executes a prepared statement like UPDATE or INSERT that doesn't return data
204          *
205          * Please use DBA::delete, DBA::insert, DBA::update, ... instead
206          *
207          * @param string $sql SQL statement
208          * @return boolean Was the query successfull? False is returned only if an error occurred
209          * @throws \Exception
210          */
211         public static function e(string $sql): bool
212         {
213                 $params = self::getParam(func_get_args());
214
215                 return DI::dba()->e($sql, $params);
216         }
217
218         /**
219          * Check if data exists
220          *
221          * @param string $table     Table name in format schema.table (where schema is optional)
222          * @param array  $condition Array of fields for condition
223          * @return boolean Are there rows for that condition?
224          * @throws \Exception
225          */
226         public static function exists(string $table, array $condition): bool
227         {
228                 return DI::dba()->exists($table, $condition);
229         }
230
231         /**
232          * Fetches the first row
233          *
234          * Please use DBA::selectFirst or DBA::exists whenever this is possible.
235          *
236          * @param string $sql SQL statement
237          * @return array first row of query
238          * @throws \Exception
239          */
240         public static function fetchFirst(string $sql)
241         {
242                 $params = self::getParam(func_get_args());
243
244                 return DI::dba()->fetchFirst($sql, $params);
245         }
246
247         /**
248          * Returns the number of affected rows of the last statement
249          *
250          * @return int Number of rows
251          */
252         public static function affectedRows(): int
253         {
254                 return DI::dba()->affectedRows();
255         }
256
257         /**
258          * Returns the number of columns of a statement
259          *
260          * @param object Statement object
261          * @return int Number of columns
262          */
263         public static function columnCount($stmt): int
264         {
265                 return DI::dba()->columnCount($stmt);
266         }
267         /**
268          * Returns the number of rows of a statement
269          *
270          * @param PDOStatement|mysqli_result|mysqli_stmt Statement object
271          * @return int Number of rows
272          */
273         public static function numRows($stmt): int
274         {
275                 return DI::dba()->numRows($stmt);
276         }
277
278         /**
279          * Fetch a single row
280          *
281          * @param mixed $stmt statement object
282          * @return array current row
283          */
284         public static function fetch($stmt)
285         {
286                 return DI::dba()->fetch($stmt);
287         }
288
289         /**
290          * Insert a row into a table
291          *
292          * @param string $table          Table name in format schema.table (where schema is optional)
293          * @param array  $param          parameter array
294          * @param int    $duplicate_mode What to do on a duplicated entry
295          * @return boolean was the insert successful?
296          * @throws \Exception
297          */
298         public static function insert(string $table, array $param, int $duplicate_mode = Database::INSERT_DEFAULT): bool
299         {
300                 return DI::dba()->insert($table, $param, $duplicate_mode);
301         }
302
303         /**
304          * Inserts a row with the provided data in the provided table.
305          * If the data corresponds to an existing row through a UNIQUE or PRIMARY index constraints, it updates the row instead.
306          *
307          * @param string $table Table name in format schema.table (where schema is optional)
308          * @param array  $param parameter array
309          * @return boolean was the insert successful?
310          * @throws \Exception
311          */
312         public static function replace(string $table, array $param): bool
313         {
314                 return DI::dba()->replace($table, $param);
315         }
316
317         /**
318          * Fetch the id of the last insert command
319          *
320          * @return integer Last inserted id
321          */
322         public static function lastInsertId(): int
323         {
324                 return DI::dba()->lastInsertId();
325         }
326
327         /**
328          * Locks a table for exclusive write access
329          *
330          * This function can be extended in the future to accept a table array as well.
331          *
332          * @param string $table Table name in format schema.table (where schema is optional)
333          * @return boolean was the lock successful?
334          * @throws \Exception
335          */
336         public static function lock(string $table): bool
337         {
338                 return DI::dba()->lock($table);
339         }
340
341         /**
342          * Unlocks all locked tables
343          *
344          * @return boolean was the unlock successful?
345          * @throws \Exception
346          */
347         public static function unlock(): bool
348         {
349                 return DI::dba()->unlock();
350         }
351
352         /**
353          * Starts a transaction
354          *
355          * @return boolean Was the command executed successfully?
356          */
357         public static function transaction(): bool
358         {
359                 return DI::dba()->transaction();
360         }
361
362         /**
363          * Does a commit
364          *
365          * @return boolean Was the command executed successfully?
366          */
367         public static function commit(): bool
368         {
369                 return DI::dba()->commit();
370         }
371
372         /**
373          * Does a rollback
374          *
375          * @return boolean Was the command executed successfully?
376          */
377         public static function rollback(): bool
378         {
379                 return DI::dba()->rollback();
380         }
381
382         /**
383          * Delete a row from a table
384          *
385          * @param string $table      Table name
386          * @param array  $conditions Field condition(s)
387          *
388          * @return boolean was the delete successful?
389          * @throws \Exception
390          */
391         public static function delete(string $table, array $conditions, array $options = []): bool
392         {
393                 return DI::dba()->delete($table, $conditions, $options);
394         }
395
396         /**
397          * Updates rows in the database.
398          *
399          * When $old_fields is set to an array,
400          * the system will only do an update if the fields in that array changed.
401          *
402          * Attention:
403          * Only the values in $old_fields are compared.
404          * This is an intentional behaviour.
405          *
406          * Example:
407          * We include the timestamp field in $fields but not in $old_fields.
408          * Then the row will only get the new timestamp when the other fields had changed.
409          *
410          * When $old_fields is set to a boolean value the system will do this compare itself.
411          * When $old_fields is set to "true" the system will do an insert if the row doesn't exists.
412          *
413          * Attention:
414          * Only set $old_fields to a boolean value when you are sure that you will update a single row.
415          * When you set $old_fields to "true" then $fields must contain all relevant fields!
416          *
417          * @param string        $table      Table name in format schema.table (where schema is optional)
418          * @param array         $fields     contains the fields that are updated
419          * @param array         $condition  condition array with the key values
420          * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate, false = don't update identical fields)
421          * @param array         $params     Parameters: "ignore" If set to "true" then the update is done with the ignore parameter
422          *
423          * @return boolean was the update successfull?
424          * @throws \Exception
425          */
426         public static function update(string $table, array $fields, array $condition, $old_fields = [], array $params = []): bool
427         {
428                 return DI::dba()->update($table, $fields, $condition, $old_fields, $params);
429         }
430
431         /**
432          * Retrieve a single record from a table and returns it in an associative array
433          *
434          * @param string|array $table     Table name in format schema.table (where schema is optional)
435          * @param array        $fields
436          * @param array        $condition
437          * @param array        $params
438          * @return bool|array
439          * @throws \Exception
440          * @see   self::select
441          */
442         public static function selectFirst($table, array $fields = [], array $condition = [], array $params = [])
443         {
444                 return DI::dba()->selectFirst($table, $fields, $condition, $params);
445         }
446
447         /**
448          * Select rows from a table and fills an array with the data
449          *
450          * @param string $table     Table name in format schema.table (where schema is optional)
451          * @param array  $fields    Array of selected fields, empty for all
452          * @param array  $condition Array of fields for condition
453          * @param array  $params    Array of several parameters
454          *
455          * @return array Data array
456          * @throws \Exception
457          * @see   self::select
458          */
459         public static function selectToArray(string $table, array $fields = [], array $condition = [], array $params = [])
460         {
461                 return DI::dba()->selectToArray($table, $fields, $condition, $params);
462         }
463
464         /**
465          * Select rows from a table
466          *
467          * @param string $table     Table name in format schema.table (where schema is optional)
468          * @param array  $fields    Array of selected fields, empty for all
469          * @param array  $condition Array of fields for condition
470          * @param array  $params    Array of several parameters
471          *
472          * @return boolean|object
473          *
474          * Example:
475          * $table = "post";
476          * $fields = array("id", "uri", "uid", "network");
477          *
478          * $condition = array("uid" => 1, "network" => 'dspr');
479          * or:
480          * $condition = array("`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr');
481          *
482          * $params = array("order" => array("id", "received" => true), "limit" => 10);
483          *
484          * $data = DBA::select($table, $fields, $condition, $params);
485          * @throws \Exception
486          */
487         public static function select(string $table, array $fields = [], array $condition = [], array $params = [])
488         {
489                 return DI::dba()->select($table, $fields, $condition, $params);
490         }
491
492         /**
493          * Counts the rows from a table satisfying the provided condition
494          *
495          * @param string $table     Table name in format schema.table (where schema is optional)
496          * @param array  $condition array of fields for condition
497          * @param array  $params    Array of several parameters
498          *
499          * @return int
500          *
501          * Example:
502          * $table = "post";
503          *
504          * $condition = ["uid" => 1, "network" => 'dspr'];
505          * or:
506          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
507          *
508          * $count = DBA::count($table, $condition);
509          * @throws \Exception
510          */
511         public static function count(string $table, array $condition = [], array $params = []): int
512         {
513                 return DI::dba()->count($table, $condition, $params);
514         }
515
516         /**
517          * Build the table query substring from one or more tables, with or without a schema.
518          *
519          * Expected formats:
520          * - table
521          * - [table1, table2, ...]
522          * - [schema1 => table1, schema2 => table2, table3, ...]
523          *
524          * @param array $tables Table names
525          * @return string
526          */
527         public static function buildTableString(array $tables): string
528         {
529                 // Quote each entry
530                 return implode(',', array_map(['self', 'quoteIdentifier'], $tables));
531         }
532
533         /**
534          * Escape an identifier (table or field name) optional with a schema like ((schema.)table.)field
535          *
536          * @param string $identifier Table, field name
537          * @return string Quotes table or field name
538          */
539         public static function quoteIdentifier(string $identifier): string
540         {
541                 return implode(
542                         '.',
543                         array_map(
544                                 function (string $identifier) { return '`' . str_replace('`', '``', $identifier) . '`'; },
545                                 explode('.', $identifier)
546                         )
547                 );
548         }
549
550         /**
551          * Returns the SQL condition string built from the provided condition array
552          *
553          * This function operates with two modes.
554          * - Supplied with a field/value associative array, it builds simple strict
555          *   equality conditions linked by AND.
556          * - Supplied with a flat list, the first element is the condition string and
557          *   the following arguments are the values to be interpolated
558          *
559          * $condition = ["uid" => 1, "network" => 'dspr'];
560          * or:
561          * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
562          *
563          * In either case, the provided array is left with the parameters only
564          *
565          * @param array $condition
566          * @return string
567          */
568         public static function buildCondition(array &$condition = []): string
569         {
570                 $condition = self::collapseCondition($condition);
571                 
572                 $condition_string = '';
573                 if (count($condition) > 0) {
574                         $condition_string = " WHERE (" . array_shift($condition) . ")";
575                 }
576
577                 return $condition_string;
578         }
579
580         /**
581          * Collapse an associative array condition into a SQL string + parameters condition array.
582          *
583          * ['uid' => 1, 'network' => ['dspr', 'apub']]
584          *
585          * gets transformed into
586          *
587          * ["`uid` = ? AND `network` IN (?, ?)", 1, 'dspr', 'apub']
588          *
589          * @param array $condition
590          * @return array
591          */
592         public static function collapseCondition(array $condition): array
593         {
594                 // Ensures an always true condition is returned
595                 if (count($condition) < 1) {
596                         return ['1'];
597                 }
598
599                 reset($condition);
600                 $first_key = key($condition);
601
602                 if (is_int($first_key)) {
603                         // Already collapsed
604                         return $condition;
605                 }
606
607                 $values = [];
608                 $condition_string = "";
609                 foreach ($condition as $field => $value) {
610                         if ($condition_string != "") {
611                                 $condition_string .= " AND ";
612                         }
613
614                         if (is_array($value)) {
615                                 if (count($value)) {
616                                         /* Workaround for MySQL Bug #64791.
617                                          * Never mix data types inside any IN() condition.
618                                          * In case of mixed types, cast all as string.
619                                          * Logic needs to be consistent with DBA::p() data types.
620                                          */
621                                         $is_int = false;
622                                         $is_alpha = false;
623                                         foreach ($value as $single_value) {
624                                                 if (is_int($single_value)) {
625                                                         $is_int = true;
626                                                 } else {
627                                                         $is_alpha = true;
628                                                 }
629                                         }
630
631                                         if ($is_int && $is_alpha) {
632                                                 foreach ($value as &$ref) {
633                                                         if (is_int($ref)) {
634                                                                 $ref = (string)$ref;
635                                                         }
636                                                 }
637                                                 unset($ref); //Prevent accidental re-use.
638                                         }
639
640                                         $values = array_merge($values, array_values($value));
641                                         $placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
642                                         $condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")";
643                                 } else {
644                                         // Empty value array isn't supported by IN and is logically equivalent to no match
645                                         $condition_string .= "FALSE";
646                                 }
647                         } elseif (is_null($value)) {
648                                 $condition_string .= self::quoteIdentifier($field) . " IS NULL";
649                         } else {
650                                 $values[$field] = $value;
651                                 $condition_string .= self::quoteIdentifier($field) . " = ?";
652                         }
653                 }
654
655                 $condition = array_merge([$condition_string], array_values($values));
656
657                 return $condition;
658         }
659
660         /**
661          * Merges the provided conditions into a single collapsed one
662          *
663          * @param array ...$conditions One or more condition arrays
664          * @return array A collapsed condition
665          * @see DBA::collapseCondition() for the condition array formats
666          */
667         public static function mergeConditions(array ...$conditions): array
668         {
669                 if (count($conditions) == 1) {
670                         return current($conditions);
671                 }
672
673                 $conditionStrings = [];
674                 $result = [];
675
676                 foreach ($conditions as $key => $condition) {
677                         if (!$condition) {
678                                 continue;
679                         }
680
681                         $condition = self::collapseCondition($condition);
682
683                         $conditionStrings[] = array_shift($condition);
684                         // The result array holds the eventual parameter values
685                         $result = array_merge($result, $condition);
686                 }
687
688                 if (count($conditionStrings)) {
689                         // We prepend the condition string at the end to form a collapsed condition array again
690                         array_unshift($result, implode(' AND ', $conditionStrings));
691                 }
692
693                 return $result;
694         }
695
696         /**
697          * Returns the SQL parameter string built from the provided parameter array
698          *
699          * Expected format for each key:
700          *
701          * group_by:
702          *  - list of column names
703          *
704          * order:
705          *  - numeric keyed column name => ASC
706          *  - associative element with boolean value => DESC (true), ASC (false)
707          *  - associative element with string value => 'ASC' or 'DESC' literally
708          *
709          * limit:
710          *  - single numeric value => count
711          *  - list with two numeric values => offset, count
712          *
713          * @param array $params
714          * @return string
715          */
716         public static function buildParameter(array $params = []): string
717         {
718                 $groupby_string = '';
719                 if (!empty($params['group_by'])) {
720                         $groupby_string = " GROUP BY " . implode(', ', array_map(['self', 'quoteIdentifier'], $params['group_by']));
721                 }
722
723                 $order_string = '';
724                 if (isset($params['order'])) {
725                         $order_string = " ORDER BY ";
726                         foreach ($params['order'] as $fields => $order) {
727                                 if ($order === 'RAND()') {
728                                         $order_string .= "RAND(), ";
729                                 } elseif (!is_int($fields)) {
730                                         if ($order !== 'DESC' && $order !== 'ASC') {
731                                                 $order = $order ? 'DESC' : 'ASC';
732                                         }
733
734                                         $order_string .= self::quoteIdentifier($fields) . " " . $order . ", ";
735                                 } else {
736                                         $order_string .= self::quoteIdentifier($order) . ", ";
737                                 }
738                         }
739                         $order_string = substr($order_string, 0, -2);
740                 }
741
742                 $limit_string = '';
743                 if (isset($params['limit']) && is_numeric($params['limit'])) {
744                         $limit_string = " LIMIT " . intval($params['limit']);
745                 }
746
747                 if (isset($params['limit']) && is_array($params['limit'])) {
748                         $limit_string = " LIMIT " . intval($params['limit'][0]) . ", " . intval($params['limit'][1]);
749                 }
750
751                 return $groupby_string . $order_string . $limit_string;
752         }
753
754         /**
755          * Fills an array with data from a query
756          *
757          * @param object $stmt     statement object
758          * @param bool   $do_close Close database connection after last row
759          * @param int    $count    maximum number of rows to be fetched
760          *
761          * @return array Data array
762          */
763         public static function toArray($stmt, bool $do_close = true, int $count = 0): array
764         {
765                 return DI::dba()->toArray($stmt, $do_close, $count);
766         }
767
768         /**
769          * Cast field types according to the table definition
770          *
771          * @param string $table
772          * @param array  $fields
773          * @return array casted fields
774          */
775         public static function castFields(string $table, array $fields): array
776         {
777                 return DI::dba()->castFields($table, $fields);
778         }
779
780         /**
781          * Returns the error number of the last query
782          *
783          * @return string Error number (0 if no error)
784          */
785         public static function errorNo(): int
786         {
787                 return DI::dba()->errorNo();
788         }
789
790         /**
791          * Returns the error message of the last query
792          *
793          * @return string Error message ('' if no error)
794          */
795         public static function errorMessage(): string
796         {
797                 return DI::dba()->errorMessage();
798         }
799
800         /**
801          * Closes the current statement
802          *
803          * @param object $stmt statement object
804          * @return boolean was the close successful?
805          */
806         public static function close($stmt): bool
807         {
808                 return DI::dba()->close($stmt);
809         }
810
811         /**
812          * Return a list of database processes
813          *
814          * @return array
815          *      'list' => List of processes, separated in their different states
816          *      'amount' => Number of concurrent database processes
817          * @throws \Exception
818          */
819         public static function processlist(): array
820         {
821                 return DI::dba()->processlist();
822         }
823
824         /**
825          * Fetch a database variable
826          *
827          * @param string $name
828          * @return string content
829          */
830         public static function getVariable(string $name)
831         {
832                 return DI::dba()->getVariable($name);
833         }
834
835         /**
836          * Checks if $array is a filled array with at least one entry.
837          *
838          * @param mixed $array A filled array with at least one entry
839          * @return boolean Whether $array is a filled array or an object with rows
840          */
841         public static function isResult($array): bool
842         {
843                 return DI::dba()->isResult($array);
844         }
845
846         /**
847          * Escapes a whole array
848          *
849          * @param mixed   $arr           Array with values to be escaped
850          * @param boolean $add_quotation add quotation marks for string values
851          * @return void
852          */
853         public static function escapeArray(&$arr, bool $add_quotation = false)
854         {
855                 DI::dba()->escapeArray($arr, $add_quotation);
856         }
857 }