This commit is contained in:
Philipp Holzer 2019-04-12 08:38:03 +02:00
parent f7ac20e151
commit c97f07f370
No known key found for this signature in database
GPG Key ID: 517BE60E2CE5C8A5
7 changed files with 198 additions and 166 deletions

View File

@ -6,11 +6,6 @@ use Friendica\Core\Config\Cache\IConfigCache;
use Friendica\Core\Logger; use Friendica\Core\Logger;
use Friendica\Database\Driver\IDriver; use Friendica\Database\Driver\IDriver;
use Friendica\Util\DateTimeFormat; use Friendica\Util\DateTimeFormat;
use Friendica\Util\Profiler;
use mysqli_result;
use mysqli_stmt;
use PDO;
use PDOStatement;
/** /**
* @class MySQL database class * @class MySQL database class
@ -34,22 +29,11 @@ class DBA
* @var IConfigCache * @var IConfigCache
*/ */
private static $configCache; private static $configCache;
/**
* @var Profiler
*/
private static $profiler;
/**
* @var string
*/
private static $basePath;
private static $connection; private static $connection;
private static $driver; private static $driver;
private static $error = false; private static $error = false;
private static $errorno = 0; private static $errorno = 0;
private static $affected_rows = 0;
private static $in_transaction = false; private static $in_transaction = false;
private static $in_retrial = false;
private static $relation = [];
/** /**
* @var IDatabase * @var IDatabase
@ -183,55 +167,19 @@ class DBA
return self::$db->getDriver()->isConnected(true); 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) 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; return $retval;
} }
/** public static function affectedRows()
* @brief Returns the number of affected rows of the last statement {
* return self::$db->getAffectedRows();
* @return int Number of rows
*/
public static function affectedRows() {
return self::$affected_rows;
} }
/** /**

View File

@ -48,6 +48,18 @@ class Database implements IDatabase, IDatabaseLock
*/ */
private $dbRelation; 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) public function __construct(IDriver $driver, IConfigCache $configCache, Profiler $profiler, LoggerInterface $logger)
{ {
$this->configCache = $configCache; $this->configCache = $configCache;
@ -84,6 +96,14 @@ class Database implements IDatabase, IDatabaseLock
return $data[0]['db']; return $data[0]['db'];
} }
/**
* {@inheritDoc}
*/
public function getAffectedRows()
{
return $this->currNumRows;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@ -263,7 +283,7 @@ class Database implements IDatabase, IDatabaseLock
if ((count($command['conditions']) > 1) || is_int($first_key)) { if ((count($command['conditions']) > 1) || is_int($first_key)) {
$sql = "DELETE FROM `" . $command['table'] . "`" . $condition_string; $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 (!$this->execute($sql, $conditions)) {
if ($do_transaction) { if ($do_transaction) {
@ -293,7 +313,7 @@ class Database implements IDatabase, IDatabaseLock
$sql = "DELETE FROM `" . $table . "` WHERE `" . $field . "` IN (" . $sql = "DELETE FROM `" . $table . "` WHERE `" . $field . "` IN (" .
substr(str_repeat("?, ", count($field_values)), 0, -2) . ");"; 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 (!$this->execute($sql, $field_values)) {
if ($do_transaction) { if ($do_transaction) {
@ -424,8 +444,10 @@ class Database implements IDatabase, IDatabaseLock
/** /**
* {@inheritDoc} * {@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; $logger = $this->logger;
$profiler = $this->profiler; $profiler = $this->profiler;
@ -434,8 +456,6 @@ class Database implements IDatabase, IDatabaseLock
$stamp1 = microtime(true); $stamp1 = microtime(true);
$params = $this->getParameters(func_get_args());
// Renumber the array keys to be sure that they fit // Renumber the array keys to be sure that they fit
$i = 0; $i = 0;
$args = []; $args = [];
@ -463,9 +483,8 @@ class Database implements IDatabase, IDatabaseLock
$sql = "/*".System::callstack()." */ ".$sql; $sql = "/*".System::callstack()." */ ".$sql;
} }
self::$error = ''; $this->currDriverException = null;
self::$errorno = 0; $this->currNumRows = 0;
self::$affected_rows = 0;
// We have to make some things different if this function is called from "e" // We have to make some things different if this function is called from "e"
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
@ -482,40 +501,51 @@ class Database implements IDatabase, IDatabaseLock
try { try {
$retval = $driver->executePrepared($sql, $args); $retval = $driver->executePrepared($sql, $args);
self::$affected_rows = $driver->getNumRows($retval); $this->currNumRows = $driver->getNumRows($retval);
} catch (DriverException $exception) { } catch (DriverException $exception) {
// We are having an own error logging in the function "e" // We are having an own error logging in the function "e"
if (($exception->getCode() != 0) && !$called_from_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" . // We have to preserve the error code, somewhere in the logging it get lost
System::callstack(8) . "\n" . self::replaceParameters($sql, $args)); $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. // On a lost connection we try to reconnect - but only once.
if ($errorno == 2006) { if ($exception->getCode() == 2006) {
if (self::$in_retrial || !$driver->reconnect()) { if ($retried || !$driver->reconnect()) {
// It doesn't make sense to continue when the database connection was lost // It doesn't make sense to continue when the database connection was lost
if (self::$in_retrial) { if ($retried) {
$logger->notice('Giving up retrial because of database error ' . $errorno . ': ' . $error); $logger->notice('Giving up retrial because of database error',
[
'code' => $exception->getCode(),
'error' => $exception->getMessage(),
]);
} else { } 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); exit(1);
} else { } else {
// We try it again // We try it again
$logger->notice('Reconnected after database error ' . $errorno . ': ' . $error); $logger->notice('Reconnected after database error',
self::$in_retrial = true; [
$ret = $this->prepared($sql, $args); 'code' => $exception->getCode(),
self::$in_retrial = false; 'error' => $exception->getMessage(),
return $ret; ]);
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" . @file_put_contents($config->get('system', 'db_log'), DateTimeFormat::utcNow() . $duration . "\t" .
basename($backtrace[1]["file"])."\t" . basename($backtrace[1]["file"])."\t" .
$backtrace[1]["line"]."\t".$backtrace[2]["function"]."\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} * {@inheritDoc}
*/ */
public function execute($sql) public function execute($sql, array $params = [])
{ {
$profiler = $this->profiler; $profiler = $this->profiler;
$logger = $this->logger; $logger = $this->logger;
$stamp = microtime(true); $stamp = microtime(true);
$params = self::getParameters(func_get_args());
// In a case of a deadlock we are repeating the query 20 times // In a case of a deadlock we are repeating the query 20 times
$timeout = 20; $timeout = 20;
$errorno = 0; $errorno = 0;
$retval = false;
do { do {
try {
$stmt = $this->prepared($sql, $params); $stmt = $this->prepared($sql, $params);
if (is_bool($stmt)) { if (is_bool($stmt)) {
@ -568,29 +596,32 @@ class Database implements IDatabase, IDatabaseLock
} }
$this->close($stmt); $this->close($stmt);
} catch (DriverException $exception) {
$errorno = $exception->getCode(); $errorno = isset($this->currDriverException) ? $this->currDriverException->getCode() : 0;
}
} while (($errorno == 1213) && (--$timeout > 0)); } while (($errorno == 1213) && (--$timeout > 0));
if ($errorno != 0) { if ($errorno != 0) {
// We have to preserve the error code, somewhere in the logging it get lost // We have to preserve the error code, somewhere in the logging it get lost
$error = self::$error; $exception = $this->currDriverException;
$errorno = self::$errorno;
$logger->error('DB Error ' . self::$errorno . ': ' . self::$error . "\n" . $this->logger->error('DB Error', [
System::callstack(8)."\n".self::replaceParameters($sql, $params)); 'code' => $exception->getCode(),
'error' => $exception->getMessage(),
'callstack' => System::callstack(8),
'param ' => $this->driver->replaceParameters($sql, $params),
]);
// On a lost connection we simply quit. // 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) { 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); exit(1);
} }
self::$error = $error;
self::$errorno = $errorno;
} }
$profiler->saveTimestamp($stamp, "database_write", System::callstack()); $profiler->saveTimestamp($stamp, "database_write", System::callstack());
@ -598,54 +629,6 @@ class Database implements IDatabase, IDatabaseLock
return $retval; 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 * @brief Returns the SQL condition string built from the provided condition array
* *

View File

@ -53,4 +53,28 @@ abstract class AbstractDriver implements IDriver
// fallback, if no explicit escaping is set for a connection // fallback, if no explicit escaping is set for a connection
return str_replace("'", "\\'", $sql); 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;
}
} }

View File

@ -50,6 +50,16 @@ interface IDriver
*/ */
function escape($sql); 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 * Closes the current statement
* *

View File

@ -2,7 +2,6 @@
namespace Friendica\Database\Driver; namespace Friendica\Database\Driver;
use Friendica\Database\Database;
use mysqli; use mysqli;
use mysqli_result; use mysqli_result;
use mysqli_stmt; 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 // The fallback routine is called as well when there are no arguments
if (!$can_be_prepared || (count($args) == 0)) { 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) { if ($this->connection->errno) {
throw new DriverException($this->connection->error, $this->connection->errno); throw new DriverException($this->connection->error, $this->connection->errno);

View File

@ -26,6 +26,13 @@ interface IDatabase
*/ */
function getDatabaseName(); 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 * Executes a prepared statement that returns data
* *
@ -34,13 +41,12 @@ interface IDatabase
* Please only use it with complicated queries. * Please only use it with complicated queries.
* For all regular queries please use DBA::select or DBA::exists * 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 * @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 * 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 * Please use DBA::delete, DBA::insert, DBA::update, ... instead
* *
* @param string $sql SQL statement * @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 * @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 * Check if data exists

65
src/Database/Utils.php Normal file
View File

@ -0,0 +1,65 @@
<?php
namespace Friendica\Database;
class Utils
{
/**
* 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 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;
}
}