web-base/core/Driver/SQL/MySQL.class.php
2021-12-08 16:54:53 +01:00

429 lines
13 KiB
PHP

<?php
namespace Driver\SQL;
use \Api\Parameter\Parameter;
use DateTime;
use \Driver\SQL\Column\Column;
use \Driver\SQL\Column\IntColumn;
use \Driver\SQL\Column\SerialColumn;
use \Driver\SQL\Column\StringColumn;
use \Driver\SQL\Column\EnumColumn;
use \Driver\SQL\Column\DateTimeColumn;
use Driver\SQL\Column\BoolColumn;
use Driver\SQL\Column\JsonColumn;
use Driver\SQL\Expression\Add;
use Driver\SQL\Expression\CurrentTimeStamp;
use Driver\SQL\Expression\DateAdd;
use Driver\SQL\Expression\DateSub;
use Driver\SQL\Expression\Expression;
use Driver\SQL\Expression\JsonArrayAgg;
use Driver\SQL\Query\CreateProcedure;
use Driver\SQL\Query\CreateTrigger;
use Driver\SQL\Query\Query;
use Driver\SQL\Strategy\Strategy;
use \Driver\SQL\Strategy\UpdateStrategy;
use Driver\SQL\Type\CurrentColumn;
use Driver\SQL\Type\CurrentTable;
use Driver\SQL\Type\Trigger;
class MySQL extends SQL {
public function __construct($connectionData) {
parent::__construct($connectionData);
}
public function checkRequirements() {
return function_exists('mysqli_connect');
}
public function getDriverName() {
return 'mysqli';
}
// Connection Management
public function connect() {
if(!is_null($this->connection)) {
return true;
}
@$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 = "Failed to connect to MySQL: " . mysqli_connect_error();
$this->connection = NULL;
return false;
}
mysqli_set_charset($this->connection, $this->connectionData->getProperty('encoding', 'UTF-8'));
return true;
}
public function disconnect() {
if(is_null($this->connection)) {
return true;
}
mysqli_close($this->connection);
return true;
}
public function getLastError(): string {
$lastError = parent::getLastError();
if (empty($lastError)) {
$lastError = mysqli_error($this->connection);
}
return $lastError;
}
private function getPreparedParams($values) {
$sqlParams = array('');
foreach($values as $value) {
$paramType = Parameter::parseType($value);
switch($paramType) {
case Parameter::TYPE_BOOLEAN:
$value = $value ? 1 : 0;
$sqlParams[0] .= 'i';
break;
case Parameter::TYPE_INT:
$sqlParams[0] .= 'i';
break;
case Parameter::TYPE_FLOAT:
$sqlParams[0] .= 'd';
break;
case Parameter::TYPE_DATE:
if ($value instanceof DateTime) {
$value = $value->format('Y-m-d');
}
$sqlParams[0] .= 's';
break;
case Parameter::TYPE_TIME:
if ($value instanceof DateTime) {
$value = $value->format('H:i:s');
}
$sqlParams[0] .= 's';
break;
case Parameter::TYPE_DATE_TIME:
if ($value instanceof DateTime) {
$value = $value->format('Y-m-d H:i:s');
}
$sqlParams[0] .= 's';
break;
case Parameter::TYPE_ARRAY:
$value = json_encode($value);
$sqlParams[0] .= 's';
break;
case Parameter::TYPE_EMAIL:
default:
$sqlParams[0] .= 's';
}
$sqlParams[] = $value;
}
return $sqlParams;
}
protected function execute($query, $values = NULL, $returnValues = false) {
$resultRows = array();
$this->lastError = "";
if (is_null($values) || empty($values)) {
$res = mysqli_query($this->connection, $query);
$success = $res !== FALSE;
if ($success && $returnValues) {
while($row = $res->fetch_assoc()) {
$resultRows[] = $row;
}
$res->close();
}
} else if($stmt = $this->connection->prepare($query)) {
$success = false;
$sqlParams = $this->getPreparedParams($values);
$tmp = array();
foreach($sqlParams as $key => $value) $tmp[$key] = &$sqlParams[$key];
if(call_user_func_array(array($stmt, "bind_param"), $tmp)) {
if($stmt->execute()) {
if ($returnValues) {
$res = $stmt->get_result();
if($res) {
while($row = $res->fetch_assoc()) {
$resultRows[] = $row;
}
$res->close();
$success = true;
} else {
$this->lastError = "PreparedStatement::get_result failed: $stmt->error ($stmt->errno)";
}
} else {
$success = true;
}
} else {
$this->lastError = "PreparedStatement::execute failed: $stmt->error ($stmt->errno)";
}
} else {
$this->lastError = "PreparedStatement::prepare failed: $stmt->error ($stmt->errno)";
}
$stmt->close();
} else {
$success = false;
}
return ($success && $returnValues) ? $resultRows : $success;
}
public function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string {
if (is_null($strategy)) {
return "";
} else if ($strategy instanceof UpdateStrategy) {
$updateValues = array();
foreach($strategy->getValues() as $key => $value) {
$leftColumn = $this->columnName($key);
if ($value instanceof Column) {
$columnName = $this->columnName($value->getName());
$updateValues[] = "$leftColumn=VALUES($columnName)";
} else if($value instanceof Add) {
$columnName = $this->columnName($value->getColumn());
$operator = $value->getOperator();
$value = $value->getValue();
$updateValues[] = "$leftColumn=$columnName$operator" . $this->addValue($value, $params);
} else {
$updateValues[] = "$leftColumn=" . $this->addValue($value, $params);
}
}
return " ON DUPLICATE KEY UPDATE " . implode(",", $updateValues);
} else {
$strategyClass = get_class($strategy);
$this->lastError = "ON DUPLICATE Strategy $strategyClass is not supported yet.";
return null;
}
}
protected function fetchReturning($res, string $returningCol) {
$this->lastInsertId = mysqli_insert_id($this->connection);
}
public function getColumnType(Column $column): ?string {
if ($column instanceof StringColumn) {
$maxSize = $column->getMaxSize();
if ($maxSize) {
return "VARCHAR($maxSize)";
} else {
return "TEXT";
}
} else if($column instanceof SerialColumn) {
return "INTEGER AUTO_INCREMENT";
} else if($column instanceof IntColumn) {
$unsigned = $column->isUnsigned() ? " UNSIGNED" : "";
return $column->getType() . $unsigned;
} else if($column instanceof DateTimeColumn) {
return "DATETIME";
} else if($column instanceof BoolColumn) {
return "BOOLEAN";
} else if($column instanceof JsonColumn) {
return "LONGTEXT"; # some maria db setups don't allow JSON here…
} else {
$this->lastError = "Unsupported Column Type: " . get_class($column);
return NULL;
}
}
public function getColumnDefinition(Column $column): ?string {
$columnName = $this->columnName($column->getName());
$defaultValue = $column->getDefaultValue();
$type = $this->getColumnType($column);
if (!$type) {
if ($column instanceof EnumColumn) {
$values = array();
foreach($column->getValues() as $value) {
$values[] = $this->getValueDefinition($value);
}
$values = implode(",", $values);
$type = "ENUM($values)";
} else {
return null;
}
}
if ($type === "LONGTEXT") {
$defaultValue = NULL; # must be null :(
}
$notNull = $column->notNull() ? " NOT NULL" : "";
if (!is_null($defaultValue) || !$column->notNull()) {
$defaultValue = " DEFAULT " . $this->getValueDefinition($defaultValue);
} else {
$defaultValue = "";
}
return "$columnName $type$notNull$defaultValue";
}
public function getValueDefinition($value) {
if (is_numeric($value)) {
return $value;
} else if(is_bool($value)) {
return $value ? "TRUE" : "FALSE";
} else if(is_null($value)) {
return "NULL";
} else if($value instanceof Keyword) {
return $value->getValue();
} else if ($value instanceof CurrentTimeStamp) {
return "CURRENT_TIMESTAMP";
} else {
$str = addslashes($value);
return "'$str'";
}
}
public function addValue($val, &$params = NULL, bool $unsafe = false) {
if ($val instanceof Keyword) {
return $val->getValue();
} else if ($val instanceof CurrentColumn) {
return $val->getName();
} else if ($val instanceof Column) {
return $this->columnName($val->getName());
} else if ($val instanceof Expression) {
return $this->createExpression($val, $params);
} else {
if ($unsafe) {
return $this->getUnsafeValue($val);
} else {
$params[] = $val;
return "?";
}
}
}
public function tableName($table): string {
if (is_array($table)) {
$tables = array();
foreach($table as $t) $tables[] = $this->tableName($t);
return implode(",", $tables);
} else {
return "`$table`";
}
}
public function columnName($col): string {
if ($col instanceof Keyword) {
return $col->getValue();
} elseif(is_array($col)) {
$columns = array();
foreach($col as $c) $columns[] = $this->columnName($c);
return implode(",", $columns);
} else {
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) {
$columnName = $this->columnName(trim(substr($col, 0, $index)));
$alias = trim(substr($col, $index + 4));
return "$columnName as $alias";
} else {
return "`$col`";
}
}
}
public function getStatus() {
return mysqli_stat($this->connection);
}
public function createTriggerBody(CreateTrigger $trigger): ?string {
$values = array();
foreach ($trigger->getProcedure()->getParameters() as $param) {
if ($param instanceof CurrentTable) {
$values[] = $this->getUnsafeValue($trigger->getTable());
} else {
$prefix = ($trigger->getEvent() !== "DELETE" ? "NEW." : "OLD.");
$values[] = $this->columnName($prefix . $param->getName());
}
}
$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 = [];
foreach ($procedure->getParameters() as $param) {
if ($param instanceof Column) {
$paramDefs[] = $this->getParameterDefinition($param);
} else {
$this->setLastError("PROCEDURE parameter type " . gettype($returns) . " is not implemented yet");
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
$this->setLastError("PROCEDURE RETURN type " . gettype($returns) . " is not implemented yet");
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;
}
protected function createExpression(Expression $exp, array &$params): ?string {
if ($exp instanceof DateAdd || $exp instanceof DateSub) {
$lhs = $this->addValue($exp->getLHS(), $params);
$rhs = $this->addValue($exp->getRHS(), $params);
$unit = $exp->getUnit();
$dateFunction = ($exp instanceof DateAdd ? "DATE_ADD" : "DATE_SUB");
return "$dateFunction($lhs, INTERVAL $rhs $unit)";
} else if ($exp instanceof CurrentTimeStamp) {
return "NOW()";
} else if ($exp instanceof JsonArrayAgg) {
$value = $this->addValue($exp->getValue(), $params);
$alias = $this->columnName($exp->getAlias());
return "JSON_ARRAYAGG($value) as $alias";
} else {
return parent::createExpression($exp, $params);
}
}
}