web-base/Core/Driver/SQL/MySQL.class.php

550 lines
17 KiB
PHP
Raw Normal View History

2020-04-02 00:02:51 +02:00
<?php
2022-11-18 18:06:46 +01:00
namespace Core\Driver\SQL;
2020-04-02 00:02:51 +02:00
2022-11-18 18:06:46 +01:00
use Core\API\Parameter\Parameter;
2020-04-02 00:02:51 +02:00
2023-01-16 21:47:23 +01:00
use Core\Driver\Logger\Logger;
2023-01-09 20:27:01 +01:00
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondLike;
use Core\Driver\SQL\Expression\Count;
2021-01-07 14:59:36 +01:00
use DateTime;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Column\IntColumn;
use Core\Driver\SQL\Column\NumericColumn;
use Core\Driver\SQL\Column\SerialColumn;
use Core\Driver\SQL\Column\StringColumn;
use Core\Driver\SQL\Column\EnumColumn;
use Core\Driver\SQL\Column\DateTimeColumn;
use Core\Driver\SQL\Column\BoolColumn;
use Core\Driver\SQL\Column\JsonColumn;
2020-04-02 00:02:51 +02:00
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Expression\Add;
use Core\Driver\SQL\Expression\CurrentTimeStamp;
use Core\Driver\SQL\Expression\Expression;
use Core\Driver\SQL\Query\CreateProcedure;
use Core\Driver\SQL\Query\CreateTrigger;
use Core\Driver\SQL\Query\Query;
use Core\Driver\SQL\Strategy\Strategy;
use Core\Driver\SQL\Strategy\UpdateStrategy;
use Core\Driver\SQL\Type\CurrentColumn;
use Core\Driver\SQL\Type\CurrentTable;
use Core\Driver\SQL\Type\Trigger;
2020-04-02 00:02:51 +02:00
class MySQL extends SQL {
public function __construct($connectionData) {
parent::__construct($connectionData);
2020-04-02 01:48:46 +02:00
}
public function checkRequirements() {
return function_exists('mysqli_connect');
}
2020-04-02 15:08:14 +02:00
public function getDriverName() {
2020-04-02 01:48:46 +02:00
return 'mysqli';
2020-04-02 00:02:51 +02:00
}
2020-04-03 18:09:01 +02:00
// Connection Management
2020-04-02 00:02:51 +02:00
public function connect() {
if (!is_null($this->connection)) {
2020-04-02 00:02:51 +02:00
return true;
}
try {
$this->connection = @mysqli_connect(
$this->connectionData->getHost(),
$this->connectionData->getLogin(),
$this->connectionData->getPassword(),
$this->connectionData->getProperty('database'),
$this->connectionData->getPort()
);
if (mysqli_connect_errno()) {
$this->lastError = $this->logger->severe("Failed to connect to MySQL: " . mysqli_connect_error());
$this->connection = NULL;
return false;
}
mysqli_set_charset($this->connection, $this->connectionData->getProperty('encoding', 'UTF8'));
return true;
} catch (\Exception $ex) {
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->severe("Failed to connect to MySQL: " . mysqli_connect_error());
2020-04-02 00:02:51 +02:00
$this->connection = NULL;
return false;
}
}
public function disconnect() {
if (is_null($this->connection)) {
2020-04-02 00:02:51 +02:00
return true;
}
mysqli_close($this->connection);
2020-04-03 17:39:58 +02:00
return true;
2020-04-02 00:02:51 +02:00
}
2021-04-08 18:29:47 +02:00
public function getLastError(): string {
2020-04-02 00:02:51 +02:00
$lastError = parent::getLastError();
if (empty($lastError)) {
$lastError = mysqli_error($this->connection);
}
return $lastError;
}
2022-02-20 23:17:17 +01:00
private function getPreparedParams($values): array {
2020-04-02 00:02:51 +02:00
$sqlParams = array('');
foreach ($values as $value) {
$paramType = Parameter::parseType($value, true); // TODO: is strict type checking really correct here?
switch ($paramType) {
2020-04-02 00:02:51 +02:00
case Parameter::TYPE_BOOLEAN:
$value = $value ? 1 : 0;
2020-04-03 17:39:58 +02:00
$sqlParams[0] .= 'i';
break;
2020-04-02 00:02:51 +02:00
case Parameter::TYPE_INT:
$sqlParams[0] .= 'i';
break;
case Parameter::TYPE_FLOAT:
$sqlParams[0] .= 'd';
break;
case Parameter::TYPE_DATE:
2021-01-07 14:59:36 +01:00
if ($value instanceof DateTime) {
$value = $value->format('Y-m-d');
}
2020-04-02 00:02:51 +02:00
$sqlParams[0] .= 's';
break;
case Parameter::TYPE_TIME:
2021-01-07 14:59:36 +01:00
if ($value instanceof DateTime) {
$value = $value->format('H:i:s');
}
2020-04-02 00:02:51 +02:00
$sqlParams[0] .= 's';
break;
case Parameter::TYPE_DATE_TIME:
2021-01-07 14:59:36 +01:00
if ($value instanceof DateTime) {
$value = $value->format('Y-m-d H:i:s');
}
2020-04-02 00:02:51 +02:00
$sqlParams[0] .= 's';
break;
2020-06-27 01:18:10 +02:00
case Parameter::TYPE_ARRAY:
$value = json_encode($value);
$sqlParams[0] .= 's';
break;
2020-04-02 00:02:51 +02:00
case Parameter::TYPE_EMAIL:
default:
$sqlParams[0] .= 's';
}
$sqlParams[] = $value;
}
return $sqlParams;
}
/**
* @return mixed
*/
2023-01-16 21:47:23 +01:00
protected function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE, int $logLevel = Logger::LOG_LEVEL_ERROR) {
2020-04-02 00:02:51 +02:00
2022-06-08 18:37:08 +02:00
$result = null;
2020-04-02 00:02:51 +02:00
$this->lastError = "";
2022-02-20 23:17:17 +01:00
$stmt = null;
$res = null;
$success = false;
2023-01-16 21:47:23 +01:00
if ($logLevel === Logger::LOG_LEVEL_DEBUG) {
$this->logger->debug("query: " . $query . ", args: " . json_encode($values), false);
}
2022-02-20 23:17:17 +01:00
try {
if (empty($values)) {
$res = mysqli_query($this->connection, $query);
2022-06-08 18:37:08 +02:00
$success = ($res !== FALSE);
if ($success) {
switch ($fetchType) {
case self::FETCH_NONE:
$result = true;
break;
case self::FETCH_ONE:
$result = $res->fetch_assoc();
break;
case self::FETCH_ALL:
$result = $res->fetch_all(MYSQLI_ASSOC);
break;
case self::FETCH_ITERATIVE:
$result = new RowIteratorMySQL($res);
break;
2022-02-20 23:17:17 +01:00
}
2020-04-02 00:02:51 +02:00
}
2022-02-20 23:17:17 +01:00
} else if ($stmt = $this->connection->prepare($query)) {
$sqlParams = $this->getPreparedParams($values);
if ($stmt->bind_param(...$sqlParams)) {
if ($stmt->execute()) {
2022-06-08 18:37:08 +02:00
if ($fetchType === self::FETCH_NONE) {
$result = true;
$success = true;
} else {
2022-02-20 23:17:17 +01:00
$res = $stmt->get_result();
if ($res) {
2022-06-08 18:37:08 +02:00
switch ($fetchType) {
case self::FETCH_ONE:
$result = $res->fetch_assoc();
break;
case self::FETCH_ALL:
$result = $res->fetch_all(MYSQLI_ASSOC);
break;
case self::FETCH_ITERATIVE:
$result = new RowIteratorMySQL($res);
break;
2022-02-20 23:17:17 +01:00
}
$success = true;
} else {
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->error("PreparedStatement::get_result failed: $stmt->error ($stmt->errno)");
2020-04-02 00:02:51 +02:00
}
}
} else {
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->error("PreparedStatement::execute failed: $stmt->error ($stmt->errno)");
2020-04-02 00:02:51 +02:00
}
} else {
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->error("PreparedStatement::prepare failed: $stmt->error ($stmt->errno)");
2020-04-02 00:02:51 +02:00
}
2022-02-20 23:17:17 +01:00
}
} catch (\mysqli_sql_exception $exception) {
2023-01-16 21:47:23 +01:00
if ($logLevel >= Logger::LOG_LEVEL_ERROR) {
$this->lastError = $this->logger->error("MySQL::execute failed: " .
($stmt !== null
? "$stmt->error ($stmt->errno)"
: $exception->getMessage()));
}
2022-02-20 23:17:17 +01:00
} finally {
2022-06-08 18:37:08 +02:00
if ($res !== null && !is_bool($res) && $fetchType !== self::FETCH_ITERATIVE) {
2022-02-20 23:17:17 +01:00
$res->close();
2020-04-02 00:02:51 +02:00
}
2022-02-20 23:17:17 +01:00
if ($stmt !== null && !is_bool($stmt)) {
$stmt->close();
}
2022-06-08 18:37:08 +02:00
2020-04-02 00:02:51 +02:00
}
2022-06-08 18:37:08 +02:00
return $success ? $result : false;
2020-04-02 00:02:51 +02:00
}
2021-04-08 18:29:47 +02:00
public function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string {
2020-04-03 17:39:58 +02:00
if (is_null($strategy)) {
return "";
} else if ($strategy instanceof UpdateStrategy) {
$updateValues = array();
foreach ($strategy->getValues() as $key => $value) {
2020-04-03 17:39:58 +02:00
$leftColumn = $this->columnName($key);
if ($value instanceof Column) {
$columnName = $this->columnName($value->getName());
2020-06-25 16:54:58 +02:00
$updateValues[] = "$leftColumn=VALUES($columnName)";
2022-02-20 23:17:17 +01:00
} else if ($value instanceof Add) {
2021-04-08 19:48:04 +02:00
$columnName = $this->columnName($value->getColumn());
2020-06-17 23:50:08 +02:00
$operator = $value->getOperator();
$value = $value->getValue();
$updateValues[] = "$leftColumn=$columnName$operator" . $this->addValue($value, $params);
2020-04-03 17:39:58 +02:00
} else {
2020-06-17 23:50:08 +02:00
$updateValues[] = "$leftColumn=" . $this->addValue($value, $params);
2020-04-02 00:02:51 +02:00
}
}
2020-04-03 17:39:58 +02:00
return " ON DUPLICATE KEY UPDATE " . implode(",", $updateValues);
} else {
$strategyClass = get_class($strategy);
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->error("ON DUPLICATE Strategy $strategyClass is not supported yet.");
2021-04-08 18:29:47 +02:00
return null;
2020-04-02 00:02:51 +02:00
}
2020-04-03 17:39:58 +02:00
}
2020-04-02 00:02:51 +02:00
2020-04-03 17:39:58 +02:00
protected function fetchReturning($res, string $returningCol) {
$this->lastInsertId = mysqli_insert_id($this->connection);
2020-04-02 00:02:51 +02:00
}
2021-04-08 18:29:47 +02:00
public function getColumnType(Column $column): ?string {
2020-04-02 00:02:51 +02:00
if ($column instanceof StringColumn) {
$maxSize = $column->getMaxSize();
if ($maxSize) {
2021-04-08 18:29:47 +02:00
return "VARCHAR($maxSize)";
2020-04-02 00:02:51 +02:00
} else {
2021-04-08 18:29:47 +02:00
return "TEXT";
2020-04-02 00:02:51 +02:00
}
} else if ($column instanceof SerialColumn) {
2021-04-08 18:29:47 +02:00
return "INTEGER AUTO_INCREMENT";
} else if ($column instanceof IntColumn) {
2021-12-08 16:53:43 +01:00
$unsigned = $column->isUnsigned() ? " UNSIGNED" : "";
return $column->getType() . $unsigned;
} else if ($column instanceof DateTimeColumn) {
2021-04-08 18:29:47 +02:00
return "DATETIME";
} else if ($column instanceof BoolColumn) {
2021-04-08 18:29:47 +02:00
return "BOOLEAN";
} else if ($column instanceof JsonColumn) {
2021-04-08 18:29:47 +02:00
return "LONGTEXT"; # some maria db setups don't allow JSON here…
2022-06-17 20:53:35 +02:00
} else if ($column instanceof NumericColumn) {
$digitsTotal = $column->getTotalDigits();
$digitsDecimal = $column->getDecimalDigits();
$type = $column->getTypeName();
if ($digitsTotal !== null) {
if ($digitsDecimal !== null) {
return "$type($digitsTotal,$digitsDecimal)";
} else {
return "$type($digitsTotal)";
}
} else {
return $type;
}
2020-04-02 00:02:51 +02:00
} else {
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->error("Unsupported Column Type: " . get_class($column));
2020-04-02 00:02:51 +02:00
return NULL;
}
2021-04-08 18:29:47 +02:00
}
public function getColumnDefinition(Column $column): ?string {
$columnName = $this->columnName($column->getName());
$defaultValue = $column->getDefaultValue();
2022-05-31 16:14:49 +02:00
if ($column instanceof EnumColumn) { // check this, shouldn't it be in getColumnType?
$values = array();
foreach ($column->getValues() as $value) {
2022-05-31 16:14:49 +02:00
$values[] = $this->getValueDefinition($value);
}
2021-04-08 18:29:47 +02:00
2022-05-31 16:14:49 +02:00
$values = implode(",", $values);
$type = "ENUM($values)";
} else {
$type = $this->getColumnType($column);
if (!$type) {
2021-04-08 18:29:47 +02:00
return null;
}
}
if ($type === "LONGTEXT") {
$defaultValue = NULL; # must be null :(
}
2020-04-02 00:02:51 +02:00
$notNull = $column->notNull() ? " NOT NULL" : "";
2020-04-02 21:32:26 +02:00
if (!is_null($defaultValue) || !$column->notNull()) {
2020-06-27 22:53:36 +02:00
$defaultValue = " DEFAULT " . $this->getValueDefinition($defaultValue);
2020-04-02 21:32:26 +02:00
} else {
$defaultValue = "";
2020-04-02 01:48:46 +02:00
}
2020-04-02 15:08:14 +02:00
2020-04-03 14:46:29 +02:00
return "$columnName $type$notNull$defaultValue";
2020-04-02 00:02:51 +02:00
}
public function getValueDefinition($value) {
2020-04-02 21:19:06 +02:00
if (is_numeric($value)) {
2020-04-02 00:02:51 +02:00
return $value;
} else if (is_bool($value)) {
2020-04-02 21:19:06 +02:00
return $value ? "TRUE" : "FALSE";
} else if (is_null($value)) {
2020-04-02 00:02:51 +02:00
return "NULL";
} else if ($value instanceof Keyword) {
2020-04-02 00:02:51 +02:00
return $value->getValue();
2021-04-08 19:08:05 +02:00
} else if ($value instanceof CurrentTimeStamp) {
return "CURRENT_TIMESTAMP";
2020-04-02 00:02:51 +02:00
} else {
$str = addslashes($value);
return "'$str'";
}
}
public function addValue($val, &$params = NULL, bool $unsafe = false) {
if ($val instanceof Expression) {
return $val->getExpression($this, $params);
2020-04-02 01:48:46 +02:00
} else {
if ($unsafe) {
return $this->getUnsafeValue($val);
} else {
$params[] = $val;
return "?";
}
2020-04-02 01:48:46 +02:00
}
}
2021-04-08 18:29:47 +02:00
public function tableName($table): string {
2020-04-02 21:19:06 +02:00
if (is_array($table)) {
$tables = array();
foreach ($table as $t) $tables[] = $this->tableName($t);
2020-04-02 21:19:06 +02:00
return implode(",", $tables);
} else {
2022-02-20 16:53:26 +01:00
$parts = explode(" ", $table);
if (count($parts) === 2) {
list ($name, $alias) = $parts;
return "`$name` $alias";
} else {
$parts = explode(".", $table);
return implode(".", array_map(function ($n) {
return "`$n`";
}, $parts));
2022-02-20 16:53:26 +01:00
}
2020-04-02 21:19:06 +02:00
}
2020-04-02 15:08:14 +02:00
}
2021-04-08 18:29:47 +02:00
public function columnName($col): string {
2020-04-02 16:16:58 +02:00
if ($col instanceof Keyword) {
2020-04-02 15:08:14 +02:00
return $col->getValue();
} elseif (is_array($col)) {
2020-04-02 21:19:06 +02:00
$columns = array();
foreach ($col as $c) $columns[] = $this->columnName($c);
2020-04-02 21:19:06 +02:00
return implode(",", $columns);
2020-04-02 15:08:14 +02:00
} else {
2020-04-02 16:16:58 +02:00
if (($index = strrpos($col, ".")) !== FALSE) {
$tableName = $this->tableName(substr($col, 0, $index));
$columnName = $this->columnName(substr($col, $index + 1));
return "$tableName.$columnName";
} else if (($index = stripos($col, " as ")) !== FALSE) {
2020-04-02 16:16:58 +02:00
$columnName = $this->columnName(trim(substr($col, 0, $index)));
$alias = trim(substr($col, $index + 4));
return "$columnName as $alias";
} else {
return "`$col`";
}
2020-04-02 15:08:14 +02:00
}
}
2020-06-19 13:13:13 +02:00
public function getStatus() {
return mysqli_stat($this->connection);
}
2021-04-08 18:29:47 +02:00
2022-02-20 16:53:26 +01:00
public function createTriggerBody(CreateTrigger $trigger, array $parameters = []): ?string {
2021-04-08 18:29:47 +02:00
$values = array();
2022-02-20 16:53:26 +01:00
foreach ($parameters as $paramValue) {
if ($paramValue instanceof CurrentTable) {
2021-04-08 18:29:47 +02:00
$values[] = $this->getUnsafeValue($trigger->getTable());
2022-02-20 16:53:26 +01:00
} elseif ($paramValue instanceof CurrentColumn) {
2021-04-08 19:24:17 +02:00
$prefix = ($trigger->getEvent() !== "DELETE" ? "NEW." : "OLD.");
2022-02-20 16:53:26 +01:00
$values[] = $this->columnName($prefix . $paramValue->getName());
} else {
$values[] = $paramValue;
2021-04-08 18:29:47 +02:00
}
}
$procName = $trigger->getProcedure()->getName();
$procParameters = implode(",", $values);
return "CALL $procName($procParameters)";
}
private function getParameterDefinition(Column $parameter, bool $out = false): string {
$out = ($out ? "OUT" : "IN");
$name = $parameter->getName();
$type = $this->getColumnType($parameter);
return "$out $name $type";
}
public function getProcedureHead(CreateProcedure $procedure): ?string {
$name = $procedure->getName();
$returns = $procedure->getReturns();
$paramDefs = [];
2023-01-09 15:59:53 +01:00
foreach ($procedure->getParameters() as $parameter) {
if ($parameter instanceof Column) {
$paramDefs[] = $this->getParameterDefinition($parameter);
} else if ($parameter instanceof CurrentTable) {
$paramDefs[] = $this->getParameterDefinition($parameter->toColumn());
2021-04-08 18:29:47 +02:00
} else {
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->error("PROCEDURE parameter type " . gettype($returns) . " is not implemented yet");
2021-04-08 18:29:47 +02:00
return null;
}
}
if ($returns) {
if ($returns instanceof Column) {
$paramDefs[] = $this->getParameterDefinition($returns, true);
} else if (!($returns instanceof Trigger)) { // mysql does not need to return triggers here
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->error("PROCEDURE RETURN type " . gettype($returns) . " is not implemented yet");
2021-04-08 18:29:47 +02:00
return null;
}
}
$paramDefs = implode(",", $paramDefs);
return "CREATE PROCEDURE $name($paramDefs)";
}
protected function buildUnsafe(Query $statement): string {
$params = [];
$query = $statement->build($params);
foreach ($params as $value) {
$query = preg_replace("?", $this->getUnsafeValue($value), $query, 1);
}
return $query;
}
2023-01-09 20:27:01 +01:00
2023-01-15 00:32:17 +01:00
// FIXME: access mysql database instead of configured one
2023-01-09 20:27:01 +01:00
public function tableExists(string $tableName): bool {
$tableSchema = $this->connectionData->getProperty("database");
$res = $this->select(new Count())
->from("information_schema.TABLES")
->where(new Compare("TABLE_NAME", $tableName, "=", true))
->where(new Compare("TABLE_SCHEMA", $tableSchema, "=", true))
->where(new CondLike(new Column("TABLE_TYPE"), "BASE TABLE"))
->execute();
return $res && $res[0]["count"] > 0;
}
public function listTables(): ?array {
$tableSchema = $this->connectionData->getProperty("database");
$res = $this->select("TABLE_NAME")
->from("information_schema.TABLES")
->where(new Compare("TABLE_SCHEMA", $tableSchema, "=", true))
->where(new CondLike(new Column("TABLE_TYPE"), "BASE TABLE"))
->execute();
if ($res !== false) {
$tableNames = [];
foreach ($res as $row) {
$tableNames[] = $row["TABLE_NAME"];
}
return $tableNames;
}
return null;
}
2020-04-03 17:39:58 +02:00
}
2022-06-08 18:37:08 +02:00
class RowIteratorMySQL extends RowIterator {
public function __construct($resultSet, bool $useCache = false) {
parent::__construct($resultSet, $useCache);
}
protected function getNumRows(): int {
return $this->resultSet->num_rows;
}
protected function fetchRow(int $index): array {
// check if we already fetched that row
if (!$this->useCache || $index >= count($this->fetchedRows)) {
// if not, fetch it from the result set
$row = $this->resultSet->fetch_assoc();
if ($this->useCache) {
$this->fetchedRows[] = $row;
}
// close result set, after everything's fetched
if ($index >= $this->numRows - 1) {
$this->resultSet->close();
}
} else {
$row = $this->fetchedRows[$index];
}
return $row;
}
2024-04-04 12:46:58 +02:00
public function rewind(): void {
2022-06-08 18:37:08 +02:00
if ($this->useCache) {
$this->rowIndex = 0;
} else if ($this->rowIndex !== 0) {
throw new \Exception("RowIterator::rewind() not supported, when caching is disabled");
}
}
}