<?php
/**
- * @copyright Copyright (C) 2020, Friendica
+ * @copyright Copyright (C) 2010-2022, the Friendica project
*
* @license GNU AGPL version 3 or any later version
*
namespace Friendica\Database;
-use Friendica\Core\Config\Cache;
+use Friendica\Core\Config\ValueObject\Cache;
use Friendica\Core\System;
-use Friendica\Network\HTTPException\InternalServerErrorException;
+use Friendica\Network\HTTPException\ServiceUnavailableException;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Profiler;
use mysqli;
protected $connected = false;
/**
- * @var Cache
+ * @var \Friendica\Core\Config\ValueObject\Cache
*/
protected $configCache;
/**
$pass = trim($this->configCache->get('database', 'password'));
$db = trim($this->configCache->get('database', 'database'));
$charset = trim($this->configCache->get('database', 'charset'));
+ $socket = trim($this->configCache->get('database', 'socket'));
if (!(strlen($server) && strlen($user))) {
return false;
$connect .= ";charset=" . $charset;
}
+ if ($socket) {
+ $connect .= ";$unix_socket=" . $socket;
+ }
+
try {
$this->connection = @new PDO($connect, $user, $pass, [PDO::ATTR_PERSISTENT => $persistent]);
$this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, $this->pdo_emulate_prepares);
+ $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$this->connected = true;
} catch (PDOException $e) {
$this->connected = false;
if ($charset) {
$this->connection->set_charset($charset);
}
+
+ if ($socket) {
+ $this->connection->set_socket($socket);
+ }
+
}
}
private function replaceParameters($sql, $args)
{
$offset = 0;
- foreach ($args AS $param => $value) {
+ foreach ($args as $param => $value) {
if (is_int($args[$param]) || is_float($args[$param]) || is_bool($args[$param])) {
$replace = intval($args[$param]);
} elseif (is_null($args[$param])) {
public function p($sql)
{
+ $this->profiler->startRecording('database');
$stamp1 = microtime(true);
$params = DBA::getParam(func_get_args());
// Renumber the array keys to be sure that they fit
$i = 0;
$args = [];
- foreach ($params AS $param) {
+ foreach ($params as $param) {
// Avoid problems with some MySQL servers and boolean values. See issue #3645
if (is_bool($param)) {
$param = (int)$param;
$called_from_e = ($called_from['function'] == 'e');
if (!isset($this->connection)) {
- throw new InternalServerErrorException('The Connection is empty, although connected is set true.');
+ throw new ServiceUnavailableException('The Connection is empty, although connected is set true.');
}
switch ($this->driver) {
break;
}
- foreach ($args AS $param => $value) {
+ foreach (array_keys($args) as $param) {
+ $data_type = PDO::PARAM_STR;
if (is_int($args[$param])) {
$data_type = PDO::PARAM_INT;
- } else {
- $data_type = PDO::PARAM_STR;
+ } elseif ($args[$param] !== null) {
+ $args[$param] = (string)$args[$param];
}
+
$stmt->bindParam($param, $args[$param], $data_type);
}
$param_types = '';
$values = [];
- foreach ($args AS $param => $value) {
+ foreach (array_keys($args) as $param) {
if (is_int($args[$param])) {
$param_types .= 'i';
} elseif (is_float($args[$param])) {
$param_types .= 'd';
} elseif (is_string($args[$param])) {
$param_types .= 's';
+ } elseif (is_object($args[$param]) && method_exists($args[$param], '__toString')) {
+ $param_types .= 's';
+ $args[$param] = (string)$args[$param];
} else {
$param_types .= 'b';
}
$this->errorno = $errorno;
}
- $this->profiler->saveTimestamp($stamp1, 'database');
+ $this->profiler->stopRecording();
if ($this->configCache->get('system', 'db_log')) {
$stamp2 = microtime(true);
public function e($sql)
{
- $stamp = microtime(true);
+ $this->profiler->startRecording('database_write');
$params = DBA::getParam(func_get_args());
$this->errorno = $errorno;
}
- $this->profiler->saveTimestamp($stamp, "database_write");
+ $this->profiler->stopRecording();
return $retval;
}
/**
* Fetch a single row
*
- * @param PDOStatement|mysqli_stmt $stmt statement object
+ * @param bool|PDOStatement|mysqli_stmt $stmt statement object
*
* @return array|false current row
*/
public function fetch($stmt)
{
- $stamp1 = microtime(true);
+ $this->profiler->startRecording('database');
$columns = [];
$result = $stmt->result_metadata();
$fields = $result->fetch_fields();
- foreach ($cols_num AS $param => $col) {
+ foreach ($cols_num as $param => $col) {
$columns[$fields[$param]->name] = $col;
}
}
- $this->profiler->saveTimestamp($stamp1, 'database');
+ $this->profiler->stopRecording();
return $columns;
}
/**
- * Insert a row into a table
+ * Insert a row into a table. Field value objects will be cast as string.
*
* @param string|array $table Table name or array [schema => table]
* @param array $param parameter array
}
/**
- * Updates rows
+ * Updates rows in the database. Field value objects will be cast as string.
*
- * Updates rows in the database. When $old_fields is set to an array,
+ * When $old_fields is set to an array,
* the system will only do an update if the fields in that array changed.
*
* Attention:
* @param string|array $table Table name or array [schema => table]
* @param array $fields contains the fields that are updated
* @param array $condition condition array with the key values
- * @param array|boolean $old_fields array with the old field values that are about to be replaced (true = update on duplicate)
+ * @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)
*
* @return boolean was the update successfull?
* @throws \Exception
}
}
- foreach ($old_fields AS $fieldname => $content) {
+ foreach ($old_fields as $fieldname => $content) {
if (isset($fields[$fieldname]) && !is_null($content) && ($fields[$fieldname] == $content)) {
unset($fields[$fieldname]);
}
return $this->toArray($this->select($table, $fields, $condition, $params));
}
+ /**
+ * Escape fields, adding special treatment for "group by" handling
+ *
+ * @param array $fields
+ * @param array $options
+ * @return array
+ */
+ private function escapeFields(array $fields, array $options)
+ {
+ // In the case of a "GROUP BY" we have to add all the ORDER fields to the fieldlist.
+ // This needs to done to apply the "ANY_VALUE(...)" treatment from below to them.
+ // Otherwise MySQL would report errors.
+ if (!empty($options['group_by']) && !empty($options['order'])) {
+ foreach ($options['order'] as $key => $field) {
+ if (!is_int($key)) {
+ if (!in_array($key, $fields)) {
+ $fields[] = $key;
+ }
+ } else {
+ if (!in_array($field, $fields)) {
+ $fields[] = $field;
+ }
+ }
+ }
+ }
+
+ array_walk($fields, function(&$value, $key) use ($options)
+ {
+ $field = $value;
+ $value = '`' . str_replace('`', '``', $value) . '`';
+
+ if (!empty($options['group_by']) && !in_array($field, $options['group_by'])) {
+ $value = 'ANY_VALUE(' . $value . ') AS ' . $value;
+ }
+ });
+
+ return $fields;
+ }
+
/**
* Select rows from a table
*
}
if (count($fields) > 0) {
- $select_string = implode(', ', array_map([DBA::class, 'quoteIdentifier'], $fields));
+ $fields = $this->escapeFields($fields, $params);
+ $select_string = implode(', ', $fields);
} else {
$select_string = '*';
}
$row = $this->fetchFirst($sql, $condition);
- // Ensure to always return either a "null" or a numeric value
- return is_numeric($row['count']) ? (int)$row['count'] : $row['count'];
+ if (!isset($row['count'])) {
+ $this->logger->notice('Invalid count.', ['table' => $table, 'row' => $row, 'expression' => $expression, 'condition' => $condition_string, 'callstack' => System::callstack()]);
+ return 0;
+ } else {
+ return (int)$row['count'];
+ }
}
/**
}
}
- return $fields;
+ return $fields;
}
-
+
/**
* Returns the error number of the last query
*
public function close($stmt)
{
- $stamp1 = microtime(true);
+ $this->profiler->startRecording('database');
if (!is_object($stmt)) {
return false;
break;
}
- $this->profiler->saveTimestamp($stamp1, 'database');
+ $this->profiler->stopRecording();
return $ret;
}