3 * @copyright Copyright (C) 2010-2023, the Friendica project
5 * @license GNU AGPL version 3 or any later version
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.
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.
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/>.
22 namespace Friendica\Database;
32 * This class is for the low level database stuff that does driver specific things.
37 * Lowest possible date value
39 const NULL_DATE = '0001-01-01';
41 * Lowest possible datetime value
43 const NULL_DATETIME = '0001-01-01 00:00:00';
46 * Lowest possible datetime(6) value
48 const NULL_DATETIME6 = '0001-01-01 00:00:00.000000';
50 public static function connect(): bool
52 return DI::dba()->connect();
56 * Disconnects the current database connection
58 public static function disconnect()
60 DI::dba()->disconnect();
64 * Perform a reconnect of an existing database connection
66 public static function reconnect(): bool
68 return DI::dba()->reconnect();
72 * Return the database object.
75 public static function getConnection()
77 return DI::dba()->getConnection();
81 * Return the database driver string
83 * @return string with either "pdo" or "mysqli"
85 public static function getDriver(): string
87 return DI::dba()->getDriver();
91 * Returns the MySQL server version string
93 * This function discriminate between the deprecated mysql API and the current
94 * object-oriented mysqli API. Example of returned string: 5.5.46-0+deb8u1
98 public static function serverInfo(): string
100 return DI::dba()->serverInfo();
104 * Returns the selected database name
109 public static function databaseName(): string
111 return DI::dba()->databaseName();
115 * Escape all SQL unsafe data
118 * @return string escaped string
120 public static function escape(string $str): string
122 return DI::dba()->escape($str);
126 * Checks if the database is connected
128 * @return boolean is the database connected?
130 public static function connected(): bool
132 return DI::dba()->connected();
136 * Replaces ANY_VALUE() function by MIN() function,
137 * if the database server does not support ANY_VALUE().
139 * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5).
140 * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html
141 * A standard fall-back is to use MIN().
143 * @param string $sql An SQL string without the values
144 * @return string The input SQL string modified if necessary.
146 public static function anyValueFallback(string $sql): string
148 return DI::dba()->anyValueFallback($sql);
152 * beautifies the query - useful for "SHOW PROCESSLIST"
154 * This is safe when we bind the parameters later.
155 * The parameter values aren't part of the SQL.
157 * @param string $sql An SQL string without the values
158 * @return string The input SQL string modified if necessary.
160 public static function cleanQuery(string $sql): string
162 $search = ["\t", "\n", "\r", " "];
163 $replace = [' ', ' ', ' ', ' '];
166 $sql = str_replace($search, $replace, $sql);
167 } while ($oldsql != $sql);
173 * Convert parameter array to an universal form
174 * @param array $args Parameter array
175 * @return array universalized parameter array
177 public static function getParam(array $args): array
181 // When the second function parameter is an array then use this as the parameter array
182 if ((count($args) > 0) && (is_array($args[1]))) {
190 * Executes a prepared statement that returns data
191 * Example: $r = p("SELECT * FROM `post` WHERE `guid` = ?", $guid);
193 * Please only use it with complicated queries.
194 * For all regular queries please use DBA::select or DBA::exists
196 * @param string $sql SQL statement
197 * @return bool|object statement object or result object
200 public static function p(string $sql)
202 $params = self::getParam(func_get_args());
204 return DI::dba()->p($sql, $params);
208 * Executes a prepared statement like UPDATE or INSERT that doesn't return data
210 * Please use DBA::delete, DBA::insert, DBA::update, ... instead
212 * @param string $sql SQL statement
213 * @return boolean Was the query successful? False is returned only if an error occurred
216 public static function e(string $sql): bool
218 $params = self::getParam(func_get_args());
220 return DI::dba()->e($sql, $params);
224 * Check if data exists
226 * @param string $table Table name in format schema.table (where schema is optional)
227 * @param array $condition Array of fields for condition
228 * @return boolean Are there rows for that condition?
231 public static function exists(string $table, array $condition): bool
233 return DI::dba()->exists($table, $condition);
237 * Fetches the first row
239 * Please use DBA::selectFirst or DBA::exists whenever this is possible.
241 * @param string $sql SQL statement
242 * @return array first row of query
245 public static function fetchFirst(string $sql)
247 $params = self::getParam(func_get_args());
249 return DI::dba()->fetchFirst($sql, $params);
253 * Returns the number of affected rows of the last statement
255 * @return int Number of rows
257 public static function affectedRows(): int
259 return DI::dba()->affectedRows();
263 * Returns the number of columns of a statement
265 * @param object Statement object
266 * @return int Number of columns
268 public static function columnCount($stmt): int
270 return DI::dba()->columnCount($stmt);
273 * Returns the number of rows of a statement
275 * @param PDOStatement|mysqli_result|mysqli_stmt Statement object
276 * @return int Number of rows
278 public static function numRows($stmt): int
280 return DI::dba()->numRows($stmt);
286 * @param mixed $stmt statement object
287 * @return array current row
289 public static function fetch($stmt)
291 return DI::dba()->fetch($stmt);
295 * Insert a row into a table
297 * @param string $table Table name in format schema.table (where schema is optional)
298 * @param array $param parameter array
299 * @param int $duplicate_mode What to do on a duplicated entry
300 * @return boolean was the insert successful?
303 public static function insert(string $table, array $param, int $duplicate_mode = Database::INSERT_DEFAULT): bool
305 return DI::dba()->insert($table, $param, $duplicate_mode);
309 * Inserts a row with the provided data in the provided table.
310 * If the data corresponds to an existing row through a UNIQUE or PRIMARY index constraints, it updates the row instead.
312 * @param string $table Table name in format schema.table (where schema is optional)
313 * @param array $param parameter array
314 * @return boolean was the insert successful?
317 public static function replace(string $table, array $param): bool
319 return DI::dba()->replace($table, $param);
323 * Fetch the id of the last insert command
325 * @return integer Last inserted id
327 public static function lastInsertId(): int
329 return DI::dba()->lastInsertId();
333 * Locks a table for exclusive write access
335 * This function can be extended in the future to accept a table array as well.
337 * @param string $table Table name in format schema.table (where schema is optional)
338 * @return boolean was the lock successful?
341 public static function lock(string $table): bool
343 return DI::dba()->lock($table);
347 * Unlocks all locked tables
349 * @return boolean was the unlock successful?
352 public static function unlock(): bool
354 return DI::dba()->unlock();
358 * Starts a transaction
360 * @return boolean Was the command executed successfully?
362 public static function transaction(): bool
364 return DI::dba()->transaction();
370 * @return boolean Was the command executed successfully?
372 public static function commit(): bool
374 return DI::dba()->commit();
380 * @return boolean Was the command executed successfully?
382 public static function rollback(): bool
384 return DI::dba()->rollback();
388 * Delete a row from a table
390 * @param string $table Table name
391 * @param array $conditions Field condition(s)
393 * @return boolean was the delete successful?
396 public static function delete(string $table, array $conditions, array $options = []): bool
398 return DI::dba()->delete($table, $conditions, $options);
402 * Updates rows in the database.
404 * When $old_fields is set to an array,
405 * the system will only do an update if the fields in that array changed.
408 * Only the values in $old_fields are compared.
409 * This is an intentional behaviour.
412 * We include the timestamp field in $fields but not in $old_fields.
413 * Then the row will only get the new timestamp when the other fields had changed.
415 * When $old_fields is set to a boolean value the system will do this compare itself.
416 * When $old_fields is set to "true" the system will do an insert if the row doesn't exists.
419 * Only set $old_fields to a boolean value when you are sure that you will update a single row.
420 * When you set $old_fields to "true" then $fields must contain all relevant fields!
422 * @param string $table Table name in format schema.table (where schema is optional)
423 * @param array $fields contains the fields that are updated
424 * @param array $condition condition array with the key values
425 * @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)
426 * @param array $params Parameters: "ignore" If set to "true" then the update is done with the ignore parameter
428 * @return boolean was the update successful?
431 public static function update(string $table, array $fields, array $condition, $old_fields = [], array $params = []): bool
433 return DI::dba()->update($table, $fields, $condition, $old_fields, $params);
437 * Retrieve a single record from a table and returns it in an associative array
439 * @param string|array $table Table name in format schema.table (where schema is optional)
440 * @param array $fields
441 * @param array $condition
442 * @param array $params
447 public static function selectFirst($table, array $fields = [], array $condition = [], array $params = [])
449 return DI::dba()->selectFirst($table, $fields, $condition, $params);
453 * Select rows from a table and fills an array with the data
455 * @param string $table Table name in format schema.table (where schema is optional)
456 * @param array $fields Array of selected fields, empty for all
457 * @param array $condition Array of fields for condition
458 * @param array $params Array of several parameters
460 * @return array Data array
464 public static function selectToArray(string $table, array $fields = [], array $condition = [], array $params = [])
466 return DI::dba()->selectToArray($table, $fields, $condition, $params);
470 * Select rows from a table
472 * @param string $table Table name in format schema.table (where schema is optional)
473 * @param array $fields Array of selected fields, empty for all
474 * @param array $condition Array of fields for condition
475 * @param array $params Array of several parameters
477 * @return boolean|object
481 * $fields = array("id", "uri", "uid", "network");
483 * $condition = array("uid" => 1, "network" => 'dspr');
485 * $condition = array("`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr');
487 * $params = array("order" => array("id", "received" => true), "limit" => 10);
489 * $data = DBA::select($table, $fields, $condition, $params);
492 public static function select(string $table, array $fields = [], array $condition = [], array $params = [])
494 return DI::dba()->select($table, $fields, $condition, $params);
498 * Counts the rows from a table satisfying the provided condition
500 * @param string $table Table name in format schema.table (where schema is optional)
501 * @param array $condition array of fields for condition
502 * @param array $params Array of several parameters
509 * $condition = ["uid" => 1, "network" => 'dspr'];
511 * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
513 * $count = DBA::count($table, $condition);
516 public static function count(string $table, array $condition = [], array $params = []): int
518 return DI::dba()->count($table, $condition, $params);
522 * Build the table query substring from one or more tables, with or without a schema.
526 * - [table1, table2, ...]
527 * - [schema1 => table1, schema2 => table2, table3, ...]
529 * @param array $tables Table names
532 public static function buildTableString(array $tables): string
535 return implode(',', array_map([self::class, 'quoteIdentifier'], $tables));
539 * Escape an identifier (table or field name) optional with a schema like ((schema.)table.)field
541 * @param string $identifier Table, field name
542 * @return string Quotes table or field name
544 public static function quoteIdentifier(string $identifier): string
549 function (string $identifier) { return '`' . str_replace('`', '``', $identifier) . '`'; },
550 explode('.', $identifier)
556 * Returns the SQL condition string built from the provided condition array
558 * This function operates with two modes.
559 * - Supplied with a field/value associative array, it builds simple strict
560 * equality conditions linked by AND.
561 * - Supplied with a flat list, the first element is the condition string and
562 * the following arguments are the values to be interpolated
564 * $condition = ["uid" => 1, "network" => 'dspr'];
566 * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
568 * In either case, the provided array is left with the parameters only
570 * @param array $condition
573 public static function buildCondition(array &$condition = []): string
575 $condition = self::collapseCondition($condition);
577 $condition_string = '';
578 if (count($condition) > 0) {
579 $condition_string = " WHERE (" . array_shift($condition) . ")";
582 return $condition_string;
586 * Collapse an associative array condition into a SQL string + parameters condition array.
588 * ['uid' => 1, 'network' => ['dspr', 'apub']]
590 * gets transformed into
592 * ["`uid` = ? AND `network` IN (?, ?)", 1, 'dspr', 'apub']
594 * @param array $condition
597 public static function collapseCondition(array $condition): array
599 // Ensures an always true condition is returned
600 if (count($condition) < 1) {
605 $first_key = key($condition);
607 if (is_int($first_key)) {
613 $condition_string = "";
614 foreach ($condition as $field => $value) {
615 if ($condition_string != "") {
616 $condition_string .= " AND ";
619 if (is_array($value)) {
621 /* Workaround for MySQL Bug #64791.
622 * Never mix data types inside any IN() condition.
623 * In case of mixed types, cast all as string.
624 * Logic needs to be consistent with DBA::p() data types.
628 foreach ($value as $single_value) {
629 if (is_int($single_value)) {
636 if ($is_int && $is_alpha) {
637 foreach ($value as &$ref) {
642 unset($ref); //Prevent accidental re-use.
645 $values = array_merge($values, array_values($value));
646 $placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
647 $condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")";
649 // Empty value array isn't supported by IN and is logically equivalent to no match
650 $condition_string .= "FALSE";
652 } elseif (is_null($value)) {
653 $condition_string .= self::quoteIdentifier($field) . " IS NULL";
655 $values[$field] = $value;
656 $condition_string .= self::quoteIdentifier($field) . " = ?";
660 $condition = array_merge([$condition_string], array_values($values));
666 * Merges the provided conditions into a single collapsed one
668 * @param array ...$conditions One or more condition arrays
669 * @return array A collapsed condition
670 * @see DBA::collapseCondition() for the condition array formats
672 public static function mergeConditions(array ...$conditions): array
674 if (count($conditions) == 1) {
675 return current($conditions);
678 $conditionStrings = [];
681 foreach ($conditions as $key => $condition) {
686 $condition = self::collapseCondition($condition);
688 $conditionStrings[] = array_shift($condition);
689 // The result array holds the eventual parameter values
690 $result = array_merge($result, $condition);
693 if (count($conditionStrings)) {
694 // We prepend the condition string at the end to form a collapsed condition array again
695 array_unshift($result, implode(' AND ', $conditionStrings));
702 * Returns the SQL parameter string built from the provided parameter array
704 * Expected format for each key:
707 * - list of column names
710 * - numeric keyed column name => ASC
711 * - associative element with boolean value => DESC (true), ASC (false)
712 * - associative element with string value => 'ASC' or 'DESC' literally
715 * - single numeric value => count
716 * - list with two numeric values => offset, count
718 * @param array $params
721 public static function buildParameter(array $params = []): string
723 $groupby_string = '';
724 if (!empty($params['group_by'])) {
725 $groupby_string = " GROUP BY " . implode(', ', array_map([self::class, 'quoteIdentifier'], $params['group_by']));
729 if (isset($params['order'])) {
730 $order_string = " ORDER BY ";
731 foreach ($params['order'] as $fields => $order) {
732 if ($order === 'RAND()') {
733 $order_string .= "RAND(), ";
734 } elseif (!is_int($fields)) {
735 if ($order !== 'DESC' && $order !== 'ASC') {
736 $order = $order ? 'DESC' : 'ASC';
739 $order_string .= self::quoteIdentifier($fields) . " " . $order . ", ";
741 $order_string .= self::quoteIdentifier($order) . ", ";
744 $order_string = substr($order_string, 0, -2);
748 if (isset($params['limit']) && is_numeric($params['limit'])) {
749 $limit_string = " LIMIT " . intval($params['limit']);
752 if (isset($params['limit']) && is_array($params['limit'])) {
753 $limit_string = " LIMIT " . intval($params['limit'][0]) . ", " . intval($params['limit'][1]);
756 return $groupby_string . $order_string . $limit_string;
760 * Fills an array with data from a query
762 * @param object $stmt statement object
763 * @param bool $do_close Close database connection after last row
764 * @param int $count maximum number of rows to be fetched
766 * @return array Data array
768 public static function toArray($stmt, bool $do_close = true, int $count = 0): array
770 return DI::dba()->toArray($stmt, $do_close, $count);
774 * Cast field types according to the table definition
776 * @param string $table
777 * @param array $fields
778 * @return array casted fields
780 public static function castFields(string $table, array $fields): array
782 return DI::dba()->castFields($table, $fields);
786 * Returns the error number of the last query
788 * @return string Error number (0 if no error)
790 public static function errorNo(): int
792 return DI::dba()->errorNo();
796 * Returns the error message of the last query
798 * @return string Error message ('' if no error)
800 public static function errorMessage(): string
802 return DI::dba()->errorMessage();
806 * Closes the current statement
808 * @param object $stmt statement object
809 * @return boolean was the close successful?
811 public static function close($stmt): bool
813 return DI::dba()->close($stmt);
817 * Return a list of database processes
820 * 'list' => List of processes, separated in their different states
821 * 'amount' => Number of concurrent database processes
824 public static function processlist(): array
826 return DI::dba()->processlist();
832 * @param string $table a given table
834 * @return bool True, if successfully optimized, otherwise false
837 public static function optimizeTable(string $table): bool
839 return DI::dba()->optimizeTable($table);
843 * Kill sleeping database processes
845 public static function deleteSleepingProcesses()
847 DI::dba()->deleteSleepingProcesses();
851 * Fetch a database variable
853 * @param string $name
854 * @return string content
856 public static function getVariable(string $name)
858 return DI::dba()->getVariable($name);
862 * Checks if $array is a filled array with at least one entry.
864 * @param mixed $array A filled array with at least one entry
865 * @return boolean Whether $array is a filled array or an object with rows
867 public static function isResult($array): bool
869 return DI::dba()->isResult($array);
873 * Escapes a whole array
875 * @param mixed $arr Array with values to be escaped
876 * @param boolean $add_quotation add quotation marks for string values
879 public static function escapeArray(&$arr, bool $add_quotation = false)
881 DI::dba()->escapeArray($arr, $add_quotation);