3 * @copyright Copyright (C) 2020, Friendica
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';
45 public static function connect()
47 return DI::dba()->connect();
51 * Disconnects the current database connection
53 public static function disconnect()
55 DI::dba()->disconnect();
59 * Perform a reconnect of an existing database connection
61 public static function reconnect()
63 return DI::dba()->reconnect();
67 * Return the database object.
70 public static function getConnection()
72 return DI::dba()->getConnection();
76 * Returns the MySQL server version string
78 * This function discriminate between the deprecated mysql API and the current
79 * object-oriented mysqli API. Example of returned string: 5.5.46-0+deb8u1
83 public static function serverInfo()
85 return DI::dba()->serverInfo();
89 * Returns the selected database name
94 public static function databaseName()
96 return DI::dba()->databaseName();
100 * Escape all SQL unsafe data
103 * @return string escaped string
105 public static function escape($str)
107 return DI::dba()->escape($str);
111 * Checks if the database is connected
113 * @return boolean is the database connected?
115 public static function connected()
117 return DI::dba()->connected();
121 * Replaces ANY_VALUE() function by MIN() function,
122 * if the database server does not support ANY_VALUE().
124 * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5).
125 * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html
126 * A standard fall-back is to use MIN().
128 * @param string $sql An SQL string without the values
129 * @return string The input SQL string modified if necessary.
131 public static function anyValueFallback($sql)
133 return DI::dba()->anyValueFallback($sql);
137 * beautifies the query - useful for "SHOW PROCESSLIST"
139 * This is safe when we bind the parameters later.
140 * The parameter values aren't part of the SQL.
142 * @param string $sql An SQL string without the values
143 * @return string The input SQL string modified if necessary.
145 public static function cleanQuery($sql)
147 $search = ["\t", "\n", "\r", " "];
148 $replace = [' ', ' ', ' ', ' '];
151 $sql = str_replace($search, $replace, $sql);
152 } while ($oldsql != $sql);
158 * Convert parameter array to an universal form
159 * @param array $args Parameter array
160 * @return array universalized parameter array
162 public static function getParam($args)
166 // When the second function parameter is an array then use this as the parameter array
167 if ((count($args) > 0) && (is_array($args[1]))) {
175 * Executes a prepared statement that returns data
176 * Example: $r = p("SELECT * FROM `item` WHERE `guid` = ?", $guid);
178 * Please only use it with complicated queries.
179 * For all regular queries please use DBA::select or DBA::exists
181 * @param string $sql SQL statement
182 * @return bool|object statement object or result object
185 public static function p($sql)
187 $params = self::getParam(func_get_args());
189 return DI::dba()->p($sql, $params);
193 * Executes a prepared statement like UPDATE or INSERT that doesn't return data
195 * Please use DBA::delete, DBA::insert, DBA::update, ... instead
197 * @param string $sql SQL statement
198 * @return boolean Was the query successfull? False is returned only if an error occurred
201 public static function e($sql) {
203 $params = self::getParam(func_get_args());
205 return DI::dba()->e($sql, $params);
209 * Check if data exists
211 * @param string|array $table Table name or array [schema => table]
212 * @param array $condition array of fields for condition
214 * @return boolean Are there rows for that condition?
217 public static function exists($table, $condition)
219 return DI::dba()->exists($table, $condition);
223 * Fetches the first row
225 * Please use DBA::selectFirst or DBA::exists whenever this is possible.
227 * @param string $sql SQL statement
228 * @return array first row of query
231 public static function fetchFirst($sql)
233 $params = self::getParam(func_get_args());
235 return DI::dba()->fetchFirst($sql, $params);
239 * Returns the number of affected rows of the last statement
241 * @return int Number of rows
243 public static function affectedRows()
245 return DI::dba()->affectedRows();
249 * Returns the number of columns of a statement
251 * @param object Statement object
252 * @return int Number of columns
254 public static function columnCount($stmt)
256 return DI::dba()->columnCount($stmt);
259 * Returns the number of rows of a statement
261 * @param PDOStatement|mysqli_result|mysqli_stmt Statement object
262 * @return int Number of rows
264 public static function numRows($stmt)
266 return DI::dba()->numRows($stmt);
272 * @param mixed $stmt statement object
273 * @return array current row
275 public static function fetch($stmt)
277 return DI::dba()->fetch($stmt);
281 * Insert a row into a table
283 * @param string|array $table Table name or array [schema => table]
284 * @param array $param parameter array
285 * @param bool $on_duplicate_update Do an update on a duplicate entry
287 * @return boolean was the insert successful?
290 public static function insert($table, $param, $on_duplicate_update = false)
292 return DI::dba()->insert($table, $param, $on_duplicate_update);
296 * Fetch the id of the last insert command
298 * @return integer Last inserted id
300 public static function lastInsertId()
302 return DI::dba()->lastInsertId();
306 * Locks a table for exclusive write access
308 * This function can be extended in the future to accept a table array as well.
310 * @param string|array $table Table name or array [schema => table]
312 * @return boolean was the lock successful?
315 public static function lock($table)
317 return DI::dba()->lock($table);
321 * Unlocks all locked tables
323 * @return boolean was the unlock successful?
326 public static function unlock()
328 return DI::dba()->unlock();
332 * Starts a transaction
334 * @return boolean Was the command executed successfully?
336 public static function transaction()
338 return DI::dba()->transaction();
344 * @return boolean Was the command executed successfully?
346 public static function commit()
348 return DI::dba()->commit();
354 * @return boolean Was the command executed successfully?
356 public static function rollback()
358 return DI::dba()->rollback();
362 * Delete a row from a table
364 * @param string|array $table Table name
365 * @param array $conditions Field condition(s)
366 * @param array $options
367 * - cascade: If true we delete records in other tables that depend on the one we're deleting through
368 * relations (default: true)
370 * @return boolean was the delete successful?
373 public static function delete($table, array $conditions, array $options = [])
375 return DI::dba()->delete($table, $conditions, $options);
379 * Updates rows in the database.
381 * When $old_fields is set to an array,
382 * the system will only do an update if the fields in that array changed.
385 * Only the values in $old_fields are compared.
386 * This is an intentional behaviour.
389 * We include the timestamp field in $fields but not in $old_fields.
390 * Then the row will only get the new timestamp when the other fields had changed.
392 * When $old_fields is set to a boolean value the system will do this compare itself.
393 * When $old_fields is set to "true" the system will do an insert if the row doesn't exists.
396 * Only set $old_fields to a boolean value when you are sure that you will update a single row.
397 * When you set $old_fields to "true" then $fields must contain all relevant fields!
399 * @param string|array $table Table name or array [schema => table]
400 * @param array $fields contains the fields that are updated
401 * @param array $condition condition array with the key values
402 * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate)
404 * @return boolean was the update successfull?
407 public static function update($table, $fields, $condition, $old_fields = [])
409 return DI::dba()->update($table, $fields, $condition, $old_fields);
413 * Retrieve a single record from a table and returns it in an associative array
415 * @param string|array $table Table name or array [schema => table]
416 * @param array $fields
417 * @param array $condition
418 * @param array $params
423 public static function selectFirst($table, array $fields = [], array $condition = [], $params = [])
425 return DI::dba()->selectFirst($table, $fields, $condition, $params);
429 * Select rows from a table and fills an array with the data
431 * @param string|array $table Table name or array [schema => table]
432 * @param array $fields Array of selected fields, empty for all
433 * @param array $condition Array of fields for condition
434 * @param array $params Array of several parameters
436 * @return array Data array
440 public static function selectToArray($table, array $fields = [], array $condition = [], array $params = [])
442 return DI::dba()->selectToArray($table, $fields, $condition, $params);
446 * Select rows from a table
448 * @param string|array $table Table name or array [schema => table]
449 * @param array $fields Array of selected fields, empty for all
450 * @param array $condition Array of fields for condition
451 * @param array $params Array of several parameters
453 * @return boolean|object
457 * $fields = array("id", "uri", "uid", "network");
459 * $condition = array("uid" => 1, "network" => 'dspr');
461 * $condition = array("`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr');
463 * $params = array("order" => array("id", "received" => true), "limit" => 10);
465 * $data = DBA::select($table, $fields, $condition, $params);
468 public static function select($table, array $fields = [], array $condition = [], array $params = [])
470 return DI::dba()->select($table, $fields, $condition, $params);
474 * Counts the rows from a table satisfying the provided condition
476 * @param string|array $table Table name or array [schema => table]
477 * @param array $condition array of fields for condition
478 * @param array $params Array of several parameters
485 * $condition = ["uid" => 1, "network" => 'dspr'];
487 * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
489 * $count = DBA::count($table, $condition);
492 public static function count($table, array $condition = [], array $params = [])
494 return DI::dba()->count($table, $condition, $params);
498 * Build the table query substring from one or more tables, with or without a schema.
502 * - [table1, table2, ...]
503 * - [schema1 => table1, schema2 => table2, table3, ...]
505 * @param string|array $tables
508 public static function buildTableString($tables)
510 if (is_string($tables)) {
516 foreach ($tables as $schema => $table) {
517 if (is_numeric($schema)) {
518 $quotedTables[] = self::quoteIdentifier($table);
520 $quotedTables[] = self::quoteIdentifier($schema) . '.' . self::quoteIdentifier($table);
524 return implode(', ', $quotedTables);
528 * Escape an identifier (table or field name)
533 public static function quoteIdentifier($identifier)
535 return '`' . str_replace('`', '``', $identifier) . '`';
539 * Returns the SQL condition string built from the provided condition array
541 * This function operates with two modes.
542 * - Supplied with a filed/value associative array, it builds simple strict
543 * equality conditions linked by AND.
544 * - Supplied with a flat list, the first element is the condition string and
545 * the following arguments are the values to be interpolated
547 * $condition = ["uid" => 1, "network" => 'dspr'];
549 * $condition = ["`uid` = ? AND `network` IN (?, ?)", 1, 'dfrn', 'dspr'];
551 * In either case, the provided array is left with the parameters only
553 * @param array $condition
556 public static function buildCondition(array &$condition = [])
558 $condition = self::collapseCondition($condition);
560 $condition_string = '';
561 if (count($condition) > 0) {
562 $condition_string = " WHERE (" . array_shift($condition) . ")";
565 return $condition_string;
569 * Collapse an associative array condition into a SQL string + parameters condition array.
571 * ['uid' => 1, 'network' => ['dspr', 'apub']]
573 * gets transformed into
575 * ["`uid` = ? AND `network` IN (?, ?)", 1, 'dspr', 'apub']
577 * @param array $condition
580 public static function collapseCondition(array $condition)
582 // Ensures an always true condition is returned
583 if (count($condition) < 1) {
588 $first_key = key($condition);
590 if (is_int($first_key)) {
596 $condition_string = "";
597 foreach ($condition as $field => $value) {
598 if ($condition_string != "") {
599 $condition_string .= " AND ";
602 if (is_array($value)) {
604 /* Workaround for MySQL Bug #64791.
605 * Never mix data types inside any IN() condition.
606 * In case of mixed types, cast all as string.
607 * Logic needs to be consistent with DBA::p() data types.
611 foreach ($value as $single_value) {
612 if (is_int($single_value)) {
619 if ($is_int && $is_alpha) {
620 foreach ($value as &$ref) {
625 unset($ref); //Prevent accidental re-use.
628 $values = array_merge($values, array_values($value));
629 $placeholders = substr(str_repeat("?, ", count($value)), 0, -2);
630 $condition_string .= self::quoteIdentifier($field) . " IN (" . $placeholders . ")";
632 // Empty value array isn't supported by IN and is logically equivalent to no match
633 $condition_string .= "FALSE";
635 } elseif (is_null($value)) {
636 $condition_string .= self::quoteIdentifier($field) . " IS NULL";
638 $values[$field] = $value;
639 $condition_string .= self::quoteIdentifier($field) . " = ?";
643 $condition = array_merge([$condition_string], array_values($values));
649 * Returns the SQL parameter string built from the provided parameter array
651 * @param array $params
654 public static function buildParameter(array $params = [])
656 $groupby_string = '';
657 if (!empty($params['group_by'])) {
658 $groupby_string = " GROUP BY " . implode(', ', array_map(['self', 'quoteIdentifier'], $params['group_by']));
662 if (isset($params['order'])) {
663 $order_string = " ORDER BY ";
664 foreach ($params['order'] AS $fields => $order) {
665 if ($order === 'RAND()') {
666 $order_string .= "RAND(), ";
667 } elseif (!is_int($fields)) {
668 $order_string .= self::quoteIdentifier($fields) . " " . ($order ? "DESC" : "ASC") . ", ";
670 $order_string .= self::quoteIdentifier($order) . ", ";
673 $order_string = substr($order_string, 0, -2);
677 if (isset($params['limit']) && is_numeric($params['limit'])) {
678 $limit_string = " LIMIT " . intval($params['limit']);
681 if (isset($params['limit']) && is_array($params['limit'])) {
682 $limit_string = " LIMIT " . intval($params['limit'][0]) . ", " . intval($params['limit'][1]);
685 return $groupby_string . $order_string . $limit_string;
689 * Fills an array with data from a query
691 * @param object $stmt statement object
692 * @param bool $do_close
693 * @return array Data array
695 public static function toArray($stmt, $do_close = true)
697 return DI::dba()->toArray($stmt, $do_close);
701 * Returns the error number of the last query
703 * @return string Error number (0 if no error)
705 public static function errorNo()
707 return DI::dba()->errorNo();
711 * Returns the error message of the last query
713 * @return string Error message ('' if no error)
715 public static function errorMessage()
717 return DI::dba()->errorMessage();
721 * Closes the current statement
723 * @param object $stmt statement object
724 * @return boolean was the close successful?
726 public static function close($stmt)
728 return DI::dba()->close($stmt);
732 * Return a list of database processes
735 * 'list' => List of processes, separated in their different states
736 * 'amount' => Number of concurrent database processes
739 public static function processlist()
741 return DI::dba()->processlist();
745 * Fetch a database variable
747 * @param string $name
748 * @return string content
750 public static function getVariable(string $name)
752 return DI::dba()->getVariable($name);
756 * Checks if $array is a filled array with at least one entry.
758 * @param mixed $array A filled array with at least one entry
760 * @return boolean Whether $array is a filled array or an object with rows
762 public static function isResult($array)
764 return DI::dba()->isResult($array);
768 * Escapes a whole array
770 * @param mixed $arr Array with values to be escaped
771 * @param boolean $add_quotation add quotation marks for string values
774 public static function escapeArray(&$arr, $add_quotation = false)
776 DI::dba()->escapeArray($arr, $add_quotation);