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

510 lines
15 KiB
PHP
Raw Permalink Normal View History

2020-04-02 01:48:46 +02:00
<?php
2022-11-18 18:06:46 +01:00
namespace Core\Driver\SQL;
2020-04-02 01:48:46 +02:00
2022-11-18 18:06:46 +01:00
use Core\API\Parameter\Parameter;
2020-04-02 01:48:46 +02:00
2023-01-16 21:47:23 +01:00
use Core\Driver\Logger\Logger;
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 01:48:46 +02:00
2023-01-09 20:27:01 +01:00
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondLike;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Condition\CondRegex;
use Core\Driver\SQL\Expression\Add;
2023-01-09 20:27:01 +01:00
use Core\Driver\SQL\Expression\Count;
2022-11-18 18:06:46 +01:00
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\Insert;
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 01:48:46 +02:00
class PostgreSQL extends SQL {
public function __construct($connectionData) {
2022-06-17 20:53:35 +02:00
parent::__construct($connectionData);
2020-04-02 01:48:46 +02:00
}
public function checkRequirements() {
return function_exists('pg_connect');
}
public function getDriverName() {
return 'pgsql';
}
2020-04-03 18:09:01 +02:00
// Connection Management
2020-04-02 01:48:46 +02:00
public function connect() {
2022-06-17 20:53:35 +02:00
if (!is_null($this->connection)) {
2020-04-02 01:48:46 +02:00
return true;
}
$config = array(
"host" => $this->connectionData->getHost(),
"port" => $this->connectionData->getPort(),
"dbname" => $this->connectionData->getProperty('database', 'public'),
"user" => $this->connectionData->getLogin(),
"password" => $this->connectionData->getPassword()
);
$connectionString = array();
2022-06-17 20:53:35 +02:00
foreach ($config as $key => $val) {
2020-04-02 01:48:46 +02:00
if (!empty($val)) {
$connectionString[] = "$key=$val";
}
}
2021-04-06 20:31:52 +02:00
$this->connection = @pg_connect(implode(" ", $connectionString), PGSQL_CONNECT_FORCE_NEW);
2020-04-02 01:48:46 +02:00
if (!$this->connection) {
2022-05-31 16:14:49 +02:00
$this->lastError = $this->logger->severe("Failed to connect to Database");
2020-04-02 01:48:46 +02:00
$this->connection = NULL;
return false;
}
pg_set_client_encoding($this->connection, $this->connectionData->getProperty('encoding', 'UTF-8'));
return true;
}
public function disconnect() {
2022-06-17 20:53:35 +02:00
if (is_null($this->connection))
2020-04-02 01:48:46 +02:00
return;
2020-06-27 01:37:20 +02:00
@pg_close($this->connection);
2020-04-02 01:48:46 +02:00
}
2021-04-08 18:29:47 +02:00
public function getLastError(): string {
2020-04-02 21:19:06 +02:00
$lastError = parent::getLastError();
if (empty($lastError)) {
2020-06-25 21:53:33 +02:00
$lastError = trim(pg_last_error($this->connection) . " " . pg_last_error($this->connection));
2020-04-02 21:19:06 +02:00
}
return $lastError;
}
/**
* @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 01:48:46 +02:00
$this->lastError = "";
$stmt_name = uniqid();
$pgParams = array();
2023-01-16 21:47:23 +01:00
if ($logLevel === Logger::LOG_LEVEL_DEBUG) {
$this->logger->debug("query: " . $query . ", args: " . json_encode($values), false);
}
2020-04-02 01:48:46 +02:00
if (!is_null($values)) {
2022-06-17 20:53:35 +02:00
foreach ($values as $value) {
2020-04-02 01:48:46 +02:00
$paramType = Parameter::parseType($value);
2022-06-17 20:53:35 +02:00
switch ($paramType) {
2020-04-02 01:48:46 +02:00
case Parameter::TYPE_DATE:
$value = $value->format("Y-m-d");
break;
case Parameter::TYPE_TIME:
$value = $value->format("H:i:s");
break;
case Parameter::TYPE_DATE_TIME:
$value = $value->format("Y-m-d H:i:s");
break;
2020-06-27 01:37:20 +02:00
case Parameter::TYPE_ARRAY:
$value = json_encode($value);
break;
2020-04-02 01:48:46 +02:00
default:
break;
}
$pgParams[] = $value;
}
}
$stmt = @pg_prepare($this->connection, $stmt_name, $query);
if ($stmt === FALSE) {
return false;
}
$result = @pg_execute($this->connection, $stmt_name, $pgParams);
if ($result === FALSE) {
return false;
}
2022-06-08 18:37:08 +02:00
switch ($fetchType) {
case self::FETCH_NONE:
return true;
case self::FETCH_ONE:
return pg_fetch_assoc($result);
case self::FETCH_ALL:
$rows = pg_fetch_all($result);
if ($rows === FALSE) {
if (empty(trim($this->getLastError()))) {
$rows = array();
}
2020-04-02 01:48:46 +02:00
}
2022-06-08 18:37:08 +02:00
return $rows;
case self::FETCH_ITERATIVE:
return new RowIteratorPostgreSQL($result);
2020-04-02 01:48:46 +02:00
}
}
2021-04-08 18:29:47 +02:00
public function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string {
2022-06-17 20:53:35 +02:00
if (!is_null($strategy)) {
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=EXCLUDED.$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, $parameters);
2020-06-18 15:08:09 +02:00
}
2021-04-08 18:29:47 +02:00
}
2022-06-17 20:53:35 +02:00
$conflictingColumns = $this->columnName($strategy->getConflictingColumns());
$updateValues = implode(",", $updateValues);
return " ON CONFLICT ($conflictingColumns) DO UPDATE SET $updateValues";
2020-04-03 17:39:58 +02:00
} else {
2022-06-17 20:53:35 +02:00
$strategyClass = get_class($strategy);
$this->lastError = $this->logger->error("ON DUPLICATE Strategy $strategyClass is not supported yet.");
return null;
2020-04-02 01:48:46 +02:00
}
2022-06-17 20:53:35 +02:00
} else {
return "";
}
2020-04-03 17:39:58 +02:00
}
2020-04-02 01:48:46 +02:00
2021-04-08 18:29:47 +02:00
public function getReturning(?string $columns): string {
2020-04-03 17:39:58 +02:00
return $columns ? (" RETURNING " . $this->columnName($columns)) : "";
}
2020-04-02 01:48:46 +02:00
2022-06-08 18:37:08 +02:00
public function executeQuery(Query $query, int $fetchType = self::FETCH_NONE) {
if ($query instanceof Insert && !empty($query->getReturning())) {
$fetchType = self::FETCH_ONE;
}
return parent::executeQuery($query, $fetchType);
2021-04-08 19:28:05 +02:00
}
2020-04-03 17:39:58 +02:00
protected function fetchReturning($res, string $returningCol) {
$this->lastInsertId = $res[0][$returningCol];
2020-04-02 01:48:46 +02:00
}
// UGLY but.. what should i do?
2021-04-08 18:29:47 +02:00
private function createEnum(EnumColumn $enumColumn, string $typeName): string {
2020-04-02 01:48:46 +02:00
$values = array();
2022-06-17 20:53:35 +02:00
foreach ($enumColumn->getValues() as $value) {
2020-04-02 01:48:46 +02:00
$values[] = $this->getValueDefinition($value);
}
$values = implode(",", $values);
$query =
"DO $$ BEGIN
CREATE TYPE \"$typeName\" AS ENUM ($values);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;";
2021-04-08 18:29:47 +02:00
return $this->execute($query);
2020-04-02 01:48:46 +02:00
}
2021-04-08 18:29:47 +02:00
public function getColumnType(Column $column): ?string {
2020-04-02 01:48:46 +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 01:48:46 +02:00
} else {
2021-04-08 18:29:47 +02:00
return "TEXT";
2020-04-02 01:48:46 +02:00
}
2022-06-17 20:53:35 +02:00
} else if ($column instanceof SerialColumn) {
2021-04-08 18:29:47 +02:00
return "SERIAL";
2022-06-17 20:53:35 +02:00
} else if ($column instanceof IntColumn) {
2021-12-08 16:53:43 +01:00
return $column->getType();
2022-06-17 20:53:35 +02:00
} else if ($column instanceof DateTimeColumn) {
2021-04-08 18:29:47 +02:00
return "TIMESTAMP";
2022-06-17 20:53:35 +02:00
} else if ($column instanceof EnumColumn) {
2021-04-08 18:29:47 +02:00
$typeName = $column->getName();
2022-06-17 20:53:35 +02:00
if (!endsWith($typeName, "_type")) {
2021-04-08 18:29:47 +02:00
$typeName = "${typeName}_type";
}
return $typeName;
2022-06-17 20:53:35 +02:00
} else if ($column instanceof BoolColumn) {
2021-04-08 18:29:47 +02:00
return "BOOLEAN";
2022-06-17 20:53:35 +02:00
} else if ($column instanceof JsonColumn) {
2021-04-08 18:29:47 +02:00
return "JSON";
2022-06-17 20:53:35 +02:00
} else if ($column instanceof NumericColumn) {
$digitsDecimal = $column->getDecimalDigits();
$type = $column->getTypeName();
if ($digitsDecimal !== null) {
if ($type === "double") {
$type = "float"; // postgres doesn't know about double :/
}
return "$type($digitsDecimal)";
} else {
return $type;
}
2020-04-02 01:48:46 +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 01:48:46 +02:00
return NULL;
}
2021-04-08 18:29:47 +02:00
}
public function getColumnDefinition($column): ?string {
$columnName = $this->columnName($column->getName());
$type = $this->getColumnType($column);
if (!$type) {
return null;
} else if ($column instanceof EnumColumn) {
if (!$this->createEnum($column, $type)) {
return null;
}
}
2020-04-02 01:48:46 +02:00
$notNull = $column->notNull() ? " NOT NULL" : "";
$defaultValue = "";
if (!is_null($column->getDefaultValue()) || !$column->notNull()) {
$defaultValue = " DEFAULT " . $this->getValueDefinition($column->getDefaultValue());
}
2020-04-02 16:16:58 +02:00
return "$columnName $type$notNull$defaultValue";
2020-04-02 01:48:46 +02:00
}
protected function getValueDefinition($value) {
if (is_numeric($value)) {
return $value;
2022-06-17 20:53:35 +02:00
} else if (is_bool($value)) {
2020-04-02 01:48:46 +02:00
return $value ? "TRUE" : "FALSE";
2022-06-17 20:53:35 +02:00
} else if (is_null($value)) {
2020-04-02 01:48:46 +02:00
return "NULL";
2021-04-08 19:08:05 +02:00
} else if ($value instanceof Keyword) {
2020-04-02 01:48:46 +02:00
return $value->getValue();
2021-04-08 19:08:05 +02:00
} else if ($value instanceof CurrentTimeStamp) {
return "CURRENT_TIMESTAMP";
2020-04-02 01:48:46 +02:00
} else {
$str = str_replace("'", "''", $value);
return "'$str'";
}
}
public function addValue($val, &$params = NULL, bool $unsafe = false) {
// I don't remember we need this here?
/*if ($val instanceof CurrentTable) {
2021-04-08 18:29:47 +02:00
return "TG_TABLE_NAME";
} else if ($val instanceof CurrentColumn) {
return "NEW." . $this->columnName($val->getName());
} else */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[] = is_bool($val) ? ($val ? "TRUE" : "FALSE") : $val;
return '$' . count($params);
}
2020-04-02 01:48:46 +02:00
}
}
2021-04-08 18:29:47 +02:00
public function tableName($table): string {
2020-04-02 16:16:58 +02:00
if (is_array($table)) {
$tables = array();
2022-06-17 20:53:35 +02:00
foreach ($table as $t) $tables[] = $this->tableName($t);
2020-04-02 16:16:58 +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 16:16:58 +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 15:08:14 +02:00
if ($col instanceof KeyWord) {
return $col->getValue();
2022-06-17 20:53:35 +02:00
} elseif (is_array($col)) {
$columns = array_map(function ($c) {
return $this->columnName($c);
}, $col);
2020-04-02 16:16:58 +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) {
2020-04-02 15:08:14 +02:00
$tableName = $this->tableName(substr($col, 0, $index));
$columnName = $this->columnName(substr($col, $index + 1));
return "$tableName.$columnName";
2022-06-17 20:53:35 +02:00
} else if (($index = stripos($col, " as ")) !== FALSE) {
2020-04-02 16:16:58 +02:00
$columnName = $this->columnName(trim(substr($col, 0, $index)));
$alias = $this->columnName(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() {
$version = pg_version($this->connection)["client"] ?? "??";
$status = pg_connection_status($this->connection);
static $statusTexts = array(
PGSQL_CONNECTION_OK => "PGSQL_CONNECTION_OK",
PGSQL_CONNECTION_BAD => "PGSQL_CONNECTION_BAD",
);
return ($statusTexts[$status] ?? "Unknown") . " (v$version)";
}
2020-06-20 15:49:53 +02:00
2021-11-11 14:25:26 +01:00
public function buildCondition($condition, &$params) {
2022-06-17 20:53:35 +02:00
if ($condition instanceof CondRegex) {
2020-06-20 15:49:53 +02:00
$left = $condition->getLeftExp();
$right = $condition->getRightExp();
$left = ($left instanceof Column) ? $this->columnName($left->getName()) : $this->addValue($left, $params);
$right = ($right instanceof Column) ? $this->columnName($right->getName()) : $this->addValue($right, $params);
return $left . " ~ " . $right;
} else {
return parent::buildCondition($condition, $params);
}
}
2021-04-08 18:29:47 +02:00
private function createTriggerProcedure(string $name, array $statements) {
$params = [];
$query = "CREATE OR REPLACE FUNCTION $name() RETURNS TRIGGER AS \$table\$ BEGIN ";
2022-06-17 20:53:35 +02:00
foreach ($statements as $stmt) {
2021-04-08 18:29:47 +02:00
if ($stmt instanceof Keyword) {
$query .= $stmt->getValue() . ";";
} else {
$query .= $stmt->build($this, $params) . ";";
}
}
$query .= "END;";
$query .= "\$table\$ LANGUAGE plpgsql;";
return $this->execute($query, $params);
}
2022-02-20 16:53:26 +01:00
public function createTriggerBody(CreateTrigger $trigger, array $params = []): ?string {
2021-04-08 18:29:47 +02:00
$procName = $this->tableName($trigger->getProcedure()->getName());
return "EXECUTE PROCEDURE $procName()";
}
public function getProcedureHead(CreateProcedure $procedure): ?string {
$name = $this->tableName($procedure->getName());
$returns = $procedure->getReturns() ?? "";
$paramDefs = [];
if (!($procedure->getReturns() instanceof Trigger)) {
foreach ($procedure->getParameters() as $parameter) {
$paramDefs[] = $parameter->getName() . " " . $this->getColumnType($parameter);
}
}
$paramDefs = implode(",", $paramDefs);
if ($returns) {
if ($returns instanceof Column) {
$returns = " RETURNS " . $this->getColumnType($returns);
} else if ($returns instanceof Keyword) {
$returns = " RETURNS " . $returns->getValue();
}
}
return "CREATE OR REPLACE FUNCTION $name($paramDefs)$returns AS $$";
}
public function getProcedureTail(): string {
return "$$ LANGUAGE plpgsql;";
}
public function getProcedureBody(CreateProcedure $procedure): string {
$statements = parent::getProcedureBody($procedure);
if ($procedure->getReturns() instanceof Trigger) {
$statements .= "RETURN NEW;";
}
return $statements;
}
protected function buildUnsafe(Query $statement): string {
$params = [];
$query = $statement->build($params);
foreach ($params as $index => $value) {
$value = $this->getUnsafeValue($value);
$query = preg_replace("\$$index", $value, $query, 1);
}
return $query;
}
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("pg_tables")
->whereEq("tablename", $tableName)
->whereEq("schemaname", $tableSchema)
->execute();
return $res && $res[0]["count"] > 0;
}
public function listTables(): ?array {
$tableSchema = $this->connectionData->getProperty("database");
$res = $this->select("tablename")
->from("pg_tables")
->where(new Compare("schemaname", $tableSchema))
->execute();
if ($res !== false) {
$tableNames = [];
foreach ($res as $row) {
$tableNames[] = $row["tablename"];
}
return $tableNames;
}
return null;
}
2022-06-08 18:37:08 +02:00
}
class RowIteratorPostgreSQL extends RowIterator {
public function __construct($resultSet, bool $useCache = false) {
parent::__construct($resultSet, false); // caching not needed
}
protected function getNumRows(): int {
return pg_num_rows($this->resultSet);
}
public function rewind() {
$this->rowIndex = 0;
}
protected function fetchRow(int $index): array {
return pg_fetch_assoc($this->resultSet, $index);
}
2020-04-03 17:39:58 +02:00
}