diff --git a/src/Database/DBA.php b/src/Database/DBA.php index 3d813318a2..4477d13963 100644 --- a/src/Database/DBA.php +++ b/src/Database/DBA.php @@ -6,11 +6,6 @@ use Friendica\Core\Config\Cache\IConfigCache; use Friendica\Core\Logger; use Friendica\Database\Driver\IDriver; use Friendica\Util\DateTimeFormat; -use Friendica\Util\Profiler; -use mysqli_result; -use mysqli_stmt; -use PDO; -use PDOStatement; /** * @class MySQL database class @@ -34,22 +29,11 @@ class DBA * @var IConfigCache */ private static $configCache; - /** - * @var Profiler - */ - private static $profiler; - /** - * @var string - */ - private static $basePath; private static $connection; private static $driver; private static $error = false; private static $errorno = 0; - private static $affected_rows = 0; private static $in_transaction = false; - private static $in_retrial = false; - private static $relation = []; /** * @var IDatabase @@ -183,55 +167,19 @@ class DBA return self::$db->getDriver()->isConnected(true); } - /** - * @brief Replaces ANY_VALUE() function by MIN() function, - * if the database server does not support ANY_VALUE(). - * - * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5). - * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html - * A standard fall-back is to use MIN(). - * - * @param string $sql An SQL string without the values - * @return string The input SQL string modified if necessary. - */ - public static function anyValueFallback($sql) { - $server_info = self::serverInfo(); - if (version_compare($server_info, '5.7.5', '<') || - (stripos($server_info, 'MariaDB') !== false)) { - $sql = str_ireplace('ANY_VALUE(', 'MIN(', $sql); - } - return $sql; - } - - /** - * @brief beautifies the query - useful for "SHOW PROCESSLIST" - * - * This is safe when we bind the parameters later. - * The parameter values aren't part of the SQL. - * - * @param string $sql An SQL string without the values - * @return string The input SQL string modified if necessary. - */ - public static function cleanQuery($sql) { - $search = ["\t", "\n", "\r", " "]; - $replace = [' ', ' ', ' ', ' ']; - do { - $oldsql = $sql; - $sql = str_replace($search, $replace, $sql); - } while ($oldsql != $sql); - - return $sql; - } - public static function p($sql) { - return self::$db->prepared($sql); + $params = Utils::getParameters(func_get_args()); + + return self::$db->prepared($sql, $params); } - public static function e($sql) { - + public static function e($sql) + { + $params = Utils::getParameters(func_get_args()); + return self::$db->execute($sql, $params); } /** @@ -299,13 +247,9 @@ class DBA return $retval; } - /** - * @brief Returns the number of affected rows of the last statement - * - * @return int Number of rows - */ - public static function affectedRows() { - return self::$affected_rows; + public static function affectedRows() + { + return self::$db->getAffectedRows(); } /** diff --git a/src/Database/Database.php b/src/Database/Database.php index c41cf1961b..baeef0a7b6 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -48,6 +48,18 @@ class Database implements IDatabase, IDatabaseLock */ private $dbRelation; + /** + * A possible driver exception of a current call + * @var DriverException + */ + private $currDriverException; + + /** + * The number of affected rows of a current call + * @var int + */ + private $currNumRows; + public function __construct(IDriver $driver, IConfigCache $configCache, Profiler $profiler, LoggerInterface $logger) { $this->configCache = $configCache; @@ -84,6 +96,14 @@ class Database implements IDatabase, IDatabaseLock return $data[0]['db']; } + /** + * {@inheritDoc} + */ + public function getAffectedRows() + { + return $this->currNumRows; + } + /** * {@inheritDoc} */ @@ -263,7 +283,7 @@ class Database implements IDatabase, IDatabaseLock if ((count($command['conditions']) > 1) || is_int($first_key)) { $sql = "DELETE FROM `" . $command['table'] . "`" . $condition_string; - $logger->debug(self::replaceParameters($sql, $conditions)); + $logger->debug($driver->replaceParameters($sql, $conditions)); if (!$this->execute($sql, $conditions)) { if ($do_transaction) { @@ -293,7 +313,7 @@ class Database implements IDatabase, IDatabaseLock $sql = "DELETE FROM `" . $table . "` WHERE `" . $field . "` IN (" . substr(str_repeat("?, ", count($field_values)), 0, -2) . ");"; - $logger->debug(self::replaceParameters($sql, $field_values)); + $logger->debug($driver->replaceParameters($sql, $field_values)); if (!$this->execute($sql, $field_values)) { if ($do_transaction) { @@ -424,8 +444,10 @@ class Database implements IDatabase, IDatabaseLock /** * {@inheritDoc} + * + * @param bool $retried if true, this is a retry of the current call */ - public function prepared($sql) + public function prepared($sql, array $params = [], $retried = false) { $logger = $this->logger; $profiler = $this->profiler; @@ -434,8 +456,6 @@ class Database implements IDatabase, IDatabaseLock $stamp1 = microtime(true); - $params = $this->getParameters(func_get_args()); - // Renumber the array keys to be sure that they fit $i = 0; $args = []; @@ -463,9 +483,8 @@ class Database implements IDatabase, IDatabaseLock $sql = "/*".System::callstack()." */ ".$sql; } - self::$error = ''; - self::$errorno = 0; - self::$affected_rows = 0; + $this->currDriverException = null; + $this->currNumRows = 0; // We have to make some things different if this function is called from "e" $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); @@ -482,40 +501,51 @@ class Database implements IDatabase, IDatabaseLock try { $retval = $driver->executePrepared($sql, $args); - self::$affected_rows = $driver->getNumRows($retval); + $this->currNumRows = $driver->getNumRows($retval); } catch (DriverException $exception) { // We are having an own error logging in the function "e" if (($exception->getCode() != 0) && !$called_from_e) { - // We have to preserve the error code, somewhere in the logging it get lost - $error = $exception->getMessage(); - $errorno = $exception->getCode(); - $this->logger->error('DB Error ' . self::$errorno . ': ' . self::$error . "\n" . - System::callstack(8) . "\n" . self::replaceParameters($sql, $args)); + // We have to preserve the error code, somewhere in the logging it get lost + $this->currDriverException = $exception; + + $this->logger->error('DB Error', [ + 'code' => $exception->getCode(), + 'error' => $exception->getMessage(), + 'callstack' => System::callstack(8), + 'param ' => $driver->replaceParameters($sql, $args), + ]); // On a lost connection we try to reconnect - but only once. - if ($errorno == 2006) { - if (self::$in_retrial || !$driver->reconnect()) { + if ($exception->getCode() == 2006) { + if ($retried || !$driver->reconnect()) { // It doesn't make sense to continue when the database connection was lost - if (self::$in_retrial) { - $logger->notice('Giving up retrial because of database error ' . $errorno . ': ' . $error); + if ($retried) { + $logger->notice('Giving up retrial because of database error', + [ + 'code' => $exception->getCode(), + 'error' => $exception->getMessage(), + ]); } else { - $logger->notice("Couldn't reconnect after database error " . $errorno . ': ' . $error); + $logger->notice("Couldn't reconnect after database error", + [ + 'code' => $exception->getCode(), + 'error' => $exception->getMessage(), + ]); } + exit(1); } else { // We try it again - $logger->notice('Reconnected after database error ' . $errorno . ': ' . $error); - self::$in_retrial = true; - $ret = $this->prepared($sql, $args); - self::$in_retrial = false; - return $ret; + $logger->notice('Reconnected after database error', + [ + 'code' => $exception->getCode(), + 'error' => $exception->getMessage(), + ]); + return $this->prepared($sql, $args, true); } } - - self::$error = $error; - self::$errorno = $errorno; } } @@ -532,7 +562,7 @@ class Database implements IDatabase, IDatabaseLock @file_put_contents($config->get('system', 'db_log'), DateTimeFormat::utcNow() . $duration . "\t" . basename($backtrace[1]["file"])."\t" . $backtrace[1]["line"]."\t".$backtrace[2]["function"]."\t" . - substr(self::replaceParameters($sql, $args), 0, 2000)."\n", FILE_APPEND); + substr($driver->replaceParameters($sql, $args), 0, 2000)."\n", FILE_APPEND); } } @@ -542,21 +572,19 @@ class Database implements IDatabase, IDatabaseLock /** * {@inheritDoc} */ - public function execute($sql) + public function execute($sql, array $params = []) { $profiler = $this->profiler; $logger = $this->logger; $stamp = microtime(true); - $params = self::getParameters(func_get_args()); - // In a case of a deadlock we are repeating the query 20 times $timeout = 20; $errorno = 0; + $retval = false; do { - try { $stmt = $this->prepared($sql, $params); if (is_bool($stmt)) { @@ -568,29 +596,32 @@ class Database implements IDatabase, IDatabaseLock } $this->close($stmt); - } catch (DriverException $exception) { - $errorno = $exception->getCode(); - } + + $errorno = isset($this->currDriverException) ? $this->currDriverException->getCode() : 0; } while (($errorno == 1213) && (--$timeout > 0)); if ($errorno != 0) { // We have to preserve the error code, somewhere in the logging it get lost - $error = self::$error; - $errorno = self::$errorno; + $exception = $this->currDriverException; - $logger->error('DB Error ' . self::$errorno . ': ' . self::$error . "\n" . - System::callstack(8)."\n".self::replaceParameters($sql, $params)); + $this->logger->error('DB Error', [ + 'code' => $exception->getCode(), + 'error' => $exception->getMessage(), + 'callstack' => System::callstack(8), + 'param ' => $this->driver->replaceParameters($sql, $params), + ]); // On a lost connection we simply quit. - // A reconnect like in self::p could be dangerous with modifications + // A reconnect like in $this->prepared() could be dangerous with modifications if ($errorno == 2006) { - $logger->notice('Giving up because of database error '.$errorno.': '.$error); + $logger->notice('Giving up retrial because of database error', + [ + 'code' => $exception->getCode(), + 'error' => $exception->getMessage(), + ]); exit(1); } - - self::$error = $error; - self::$errorno = $errorno; } $profiler->saveTimestamp($stamp, "database_write", System::callstack()); @@ -598,54 +629,6 @@ class Database implements IDatabase, IDatabaseLock return $retval; } - /** - * @brief Replaces the ? placeholders with the parameters in the $args array - * - * @param string $sql SQL query - * @param array $args The parameters that are to replace the ? placeholders - * - * @return string The replaced SQL query - */ - public static function replaceParameters($sql, array $args) - { - $offset = 0; - - foreach ($args AS $param => $value) { - if (is_int($args[$param]) || is_float($args[$param])) { - $replace = intval($args[$param]); - } else { - $replace = "'" . $this->escape($args[$param]) . "'"; - } - - $pos = strpos($sql, '?', $offset); - if ($pos !== false) { - $sql = substr_replace($sql, $replace, $pos, 1); - } - $offset = $pos + strlen($replace); - } - - return $sql; - } - - /** - * Convert parameter array to an universal form - * - * @param array $args Parameter array - * - * @return array universalized parameter array - */ - public static function getParameters(array $args) - { - unset($args[0]); - - // When the second function parameter is an array then use this as the parameter array - if ((count($args) > 0) && (is_array($args[1]))) { - return $args[1]; - } else { - return $args; - } - } - /** * @brief Returns the SQL condition string built from the provided condition array * diff --git a/src/Database/Driver/AbstractDriver.php b/src/Database/Driver/AbstractDriver.php index 0e6670b5f4..476e49dc8f 100644 --- a/src/Database/Driver/AbstractDriver.php +++ b/src/Database/Driver/AbstractDriver.php @@ -53,4 +53,28 @@ abstract class AbstractDriver implements IDriver // fallback, if no explicit escaping is set for a connection return str_replace("'", "\\'", $sql); } + + /** + * {@inheritDoc} + */ + public function replaceParameters($sql, array $args = []) + { + $offset = 0; + + foreach ($args AS $param => $value) { + if (is_int($args[$param]) || is_float($args[$param])) { + $replace = intval($args[$param]); + } else { + $replace = "'" . $this->escape($args[$param]) . "'"; + } + + $pos = strpos($sql, '?', $offset); + if ($pos !== false) { + $sql = substr_replace($sql, $replace, $pos, 1); + } + $offset = $pos + strlen($replace); + } + + return $sql; + } } diff --git a/src/Database/Driver/IDriver.php b/src/Database/Driver/IDriver.php index af9d835607..a8772fa59d 100644 --- a/src/Database/Driver/IDriver.php +++ b/src/Database/Driver/IDriver.php @@ -50,6 +50,16 @@ interface IDriver */ function escape($sql); + /** + * Replaces the ? placeholders with the parameters in the $args array + * + * @param string $sql SQL query + * @param array $args The parameters that are to replace the ? placeholders + * + * @return string The replaced SQL query + */ + function replaceParameters($sql, array $args = []); + /** * Closes the current statement * diff --git a/src/Database/Driver/MySQLiDriver.php b/src/Database/Driver/MySQLiDriver.php index a17c4c3531..5b35a08e2b 100644 --- a/src/Database/Driver/MySQLiDriver.php +++ b/src/Database/Driver/MySQLiDriver.php @@ -2,7 +2,6 @@ namespace Friendica\Database\Driver; -use Friendica\Database\Database; use mysqli; use mysqli_result; use mysqli_stmt; @@ -190,7 +189,7 @@ class MySQLiDriver extends AbstractDriver implements IDriver // The fallback routine is called as well when there are no arguments if (!$can_be_prepared || (count($args) == 0)) { - $retval = $this->connection->query(Database::replaceParameters($sql, $args)); + $retval = $this->connection->query($this->replaceParameters($sql, $args)); if ($this->connection->errno) { throw new DriverException($this->connection->error, $this->connection->errno); diff --git a/src/Database/IDatabase.php b/src/Database/IDatabase.php index d5dbbe3db7..bb377e5837 100644 --- a/src/Database/IDatabase.php +++ b/src/Database/IDatabase.php @@ -26,6 +26,13 @@ interface IDatabase */ function getDatabaseName(); + /** + * Returns the number of affected rows of the last statement + * + * @return int Number of rows + */ + function getAffectedRows(); + /** * Executes a prepared statement that returns data * @@ -34,13 +41,12 @@ interface IDatabase * Please only use it with complicated queries. * For all regular queries please use DBA::select or DBA::exists * - * @param string $sql SQL statement + * @param string $sql SQL statement + * @param array $params The parameters of the current SQL statement * * @return bool|object statement object or result object - * - * @throws \Exception */ - function prepared($sql); + function prepared($sql, array $params = []); /** * Executes a prepared statement like UPDATE or INSERT that doesn't return data @@ -48,10 +54,11 @@ interface IDatabase * Please use DBA::delete, DBA::insert, DBA::update, ... instead * * @param string $sql SQL statement + * @param array $params The parameters of the current SQL statement + * * @return boolean Was the query successfull? False is returned only if an error occurred - * @throws \Exception */ - function execute($sql); + function execute($sql, array $params = []); /** * Check if data exists diff --git a/src/Database/Utils.php b/src/Database/Utils.php new file mode 100644 index 0000000000..e449187a74 --- /dev/null +++ b/src/Database/Utils.php @@ -0,0 +1,65 @@ + 0) && (is_array($args[1]))) { + return $args[1]; + } else { + return $args; + } + } + + /** + * @brief Replaces ANY_VALUE() function by MIN() function, + * if the database server does not support ANY_VALUE(). + * + * Considerations for Standard SQL, or MySQL with ONLY_FULL_GROUP_BY (default since 5.7.5). + * ANY_VALUE() is available from MySQL 5.7.5 https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html + * A standard fall-back is to use MIN(). + * + * @param string $sql An SQL string without the values + * @return string The input SQL string modified if necessary. + */ + public static function anyValueFallback($sql) { + $server_info = self::serverInfo(); + if (version_compare($server_info, '5.7.5', '<') || + (stripos($server_info, 'MariaDB') !== false)) { + $sql = str_ireplace('ANY_VALUE(', 'MIN(', $sql); + } + return $sql; + } + + /** + * @brief beautifies the query - useful for "SHOW PROCESSLIST" + * + * This is safe when we bind the parameters later. + * The parameter values aren't part of the SQL. + * + * @param string $sql An SQL string without the values + * @return string The input SQL string modified if necessary. + */ + public static function cleanQuery($sql) { + $search = ["\t", "\n", "\r", " "]; + $replace = [' ', ' ', ' ', ' ']; + do { + $oldsql = $sql; + $sql = str_replace($search, $replace, $sql); + } while ($oldsql != $sql); + + return $sql; + } +}