PostgreSQL (#2599)
* dev * postgres update set * position in list * discard charset * binary => blob * rollback transaction * insert on conflict * fix POSITION_IN_LIST * TIMESTAMPDIFF * functions * functions * set UTC time zone * functions and operators * function * fulltext * fix details provider * full text config usage * fix param * full text index rebuild * full text and round fix * add uuid db type * if function * tests * delete with joins order limit * update * alias max length
This commit is contained in:
@@ -33,6 +33,8 @@ use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\PDO\PgSQL\Driver as PostgreSQLDriver;
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Espo\Core\Utils\Database\Dbal\ConnectionFactory;
|
||||
use Espo\Core\Utils\Database\Dbal\Platforms\PostgresqlPlatform;
|
||||
use Espo\Core\Utils\Database\Helper;
|
||||
use Espo\ORM\DatabaseParams;
|
||||
use Espo\ORM\PDO\Options as PdoOptions;
|
||||
|
||||
@@ -42,7 +44,8 @@ use RuntimeException;
|
||||
class PostgresqlConnectionFactory implements ConnectionFactory
|
||||
{
|
||||
public function __construct(
|
||||
private PDO $pdo
|
||||
private PDO $pdo,
|
||||
private Helper $helper
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -56,7 +59,11 @@ class PostgresqlConnectionFactory implements ConnectionFactory
|
||||
throw new RuntimeException("No required database params.");
|
||||
}
|
||||
|
||||
$platform = new PostgresqlPlatform();
|
||||
$platform->setTextSearchConfig($this->helper->getParam('default_text_search_config'));
|
||||
|
||||
$params = [
|
||||
'platform' => $platform,
|
||||
'pdo' => $this->pdo,
|
||||
'host' => $databaseParams->getHost(),
|
||||
'dbname' => $databaseParams->getName(),
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Dbal\Platforms;
|
||||
|
||||
use Doctrine\DBAL\Schema\Index;
|
||||
use Doctrine\DBAL\Schema\PostgreSQLSchemaManager as BasePostgreSQLSchemaManager;
|
||||
|
||||
class PostgreSQLSchemaManager extends BasePostgreSQLSchemaManager
|
||||
{
|
||||
/**
|
||||
* DBAL does not add the 'fulltext' flag on reverse engineering.
|
||||
*/
|
||||
protected function _getPortableTableIndexesList($tableIndexes, $tableName = null)
|
||||
{
|
||||
$indexes = parent::_getPortableTableIndexesList($tableIndexes, $tableName);
|
||||
|
||||
foreach ($tableIndexes as $row) {
|
||||
$key = $row['relname'];
|
||||
|
||||
if ($key === "idx_{$tableName}_system_full_text_search") {
|
||||
$sql = "SELECT indexdef FROM pg_indexes WHERE indexname = '{$key}'";
|
||||
|
||||
$rows = $this->_conn->fetchAllAssociative($sql);
|
||||
|
||||
if (!$rows) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columns = self::parseColumnsIndexFromDeclaration($rows[0]['indexdef']);
|
||||
|
||||
$indexes[$key] = new Index(
|
||||
$key,
|
||||
$columns,
|
||||
false,
|
||||
false,
|
||||
['fulltext']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private static function parseColumnsIndexFromDeclaration(string $string): array
|
||||
{
|
||||
preg_match('/to_tsvector\((.*),(.*)\)/i', $string, $matches);
|
||||
|
||||
if (!$matches || count($matches) < 3) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$part = $matches[2];
|
||||
|
||||
$part = str_replace("|| ' '::text", '', $part);
|
||||
$part = str_replace("::text", '', $part);
|
||||
$part = str_replace(" ", '', $part);
|
||||
$part = str_replace("||", ' ', $part);
|
||||
$part = str_replace("(", '', $part);
|
||||
$part = str_replace(")", '', $part);
|
||||
|
||||
$list = array_map(
|
||||
fn ($item) => trim($item),
|
||||
explode(' ', $part)
|
||||
);
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Dbal\Platforms;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\PostgreSQL100Platform;
|
||||
use Doctrine\DBAL\Schema\Index;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
|
||||
class PostgresqlPlatform extends PostgreSQL100Platform
|
||||
{
|
||||
private const TEXT_SEARCH_CONFIG = 'pg_catalog.simple';
|
||||
|
||||
private ?string $textSearchConfig;
|
||||
|
||||
public function setTextSearchConfig(?string $textSearchConfig): void
|
||||
{
|
||||
$this->textSearchConfig = $textSearchConfig;
|
||||
}
|
||||
|
||||
public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager
|
||||
{
|
||||
return new PostgreSQLSchemaManager($connection, $this);
|
||||
}
|
||||
|
||||
public function getCreateIndexSQL(Index $index, $table)
|
||||
{
|
||||
if (!$index->hasFlag('fulltext')) {
|
||||
return parent::getCreateIndexSQL($index, $table);
|
||||
}
|
||||
|
||||
if ($table instanceof Table) {
|
||||
$table = $table->getQuotedName($this);
|
||||
}
|
||||
|
||||
$name = $index->getQuotedName($this);
|
||||
$columns = $index->getColumns();
|
||||
|
||||
if (count($columns) === 0) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Incomplete or invalid index definition %s on table %s',
|
||||
$name,
|
||||
$table,
|
||||
));
|
||||
}
|
||||
|
||||
$columnsPart = implode(" || ' ' || ", $index->getQuotedColumns($this));
|
||||
$partialPart = $this->getPartialIndexSQL($index);
|
||||
|
||||
$textSearchConfig = $this->textSearchConfig ?? self::TEXT_SEARCH_CONFIG;
|
||||
$textSearchConfig = preg_replace('/[^A-Za-z0-9_.\-]+/', '', $textSearchConfig) ?? '';
|
||||
$configPart = $this->quoteStringLiteral($textSearchConfig);
|
||||
|
||||
return "CREATE INDEX {$name} ON {$table} USING GIN (TO_TSVECTOR({$configPart}, {$columnsPart})) {$partialPart}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\DetailsProviders;
|
||||
|
||||
use Espo\Core\Utils\Database\DetailsProvider;
|
||||
use PDO;
|
||||
|
||||
class PostgresqlDetailsProvider implements DetailsProvider
|
||||
{
|
||||
private const TYPE_POSTGRESQL = 'PostgreSQL';
|
||||
|
||||
public function __construct(private PDO $pdo)
|
||||
{}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_POSTGRESQL;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
$fullVersion = $this->getFullDatabaseVersion() ?? '';
|
||||
|
||||
if (preg_match('/[0-9]+\.[0-9]+/', $fullVersion, $match)) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
return '0.0';
|
||||
}
|
||||
|
||||
public function getServerVersion(): string
|
||||
{
|
||||
return (string) $this->getParam('version');
|
||||
}
|
||||
|
||||
public function getParam(string $name): ?string
|
||||
{
|
||||
$name = preg_replace('/[^A-Za-z0-9_]+/', '', $name);;
|
||||
|
||||
$sql = "SHOW {$name}";
|
||||
|
||||
$sth = $this->pdo->query($sql);
|
||||
|
||||
if ($sth === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $sth->fetch(PDO::FETCH_NUM);
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $row[0] ?: null;
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private function getFullDatabaseVersion(): ?string
|
||||
{
|
||||
$sql = "select version()";
|
||||
|
||||
$sth = $this->pdo->prepare($sql);
|
||||
$sth->execute();
|
||||
|
||||
/** @var string|null|false $result */
|
||||
$result = $sth->fetchColumn();
|
||||
|
||||
if ($result === false || $result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Orm\IndexHelpers;
|
||||
|
||||
use Espo\Core\Utils\Database\Orm\IndexHelper;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Defs\IndexDefs;
|
||||
|
||||
class PostgresqlIndexHelper implements IndexHelper
|
||||
{
|
||||
private const MAX_LENGTH = 59;
|
||||
|
||||
public function composeKey(IndexDefs $defs, string $entityType): string
|
||||
{
|
||||
$name = $defs->getName();
|
||||
$prefix = $defs->isUnique() ? 'UNIQ' : 'IDX';
|
||||
|
||||
$parts = [
|
||||
$prefix,
|
||||
strtoupper(Util::toUnderScore($entityType)),
|
||||
strtoupper(Util::toUnderScore($name)),
|
||||
];
|
||||
|
||||
$key = implode('_', $parts);
|
||||
|
||||
return self::decreaseLength($key);
|
||||
}
|
||||
|
||||
private static function decreaseLength(string $key): string
|
||||
{
|
||||
if (strlen($key) <= self::MAX_LENGTH) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$list = explode('_', $key);
|
||||
|
||||
$maxItemLength = 0;
|
||||
foreach ($list as $item) {
|
||||
if (strlen($item) > $maxItemLength) {
|
||||
$maxItemLength = strlen($item);
|
||||
}
|
||||
}
|
||||
$maxItemLength--;
|
||||
|
||||
$list = array_map(
|
||||
fn ($item) => substr($item, 0, min($maxItemLength, strlen($item))),
|
||||
$list
|
||||
);
|
||||
|
||||
$key = implode('_', $list);
|
||||
|
||||
return self::decreaseLength($key);
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\Utils\Database\Schema\ColumnPreparators;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Espo\Core\Utils\Database\Schema\Column;
|
||||
use Espo\Core\Utils\Database\Schema\ColumnPreparator;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Defs\AttributeDefs;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
class PostgresqlColumnPreparator implements ColumnPreparator
|
||||
{
|
||||
private const PARAM_DB_TYPE = 'dbType';
|
||||
private const PARAM_DEFAULT = 'default';
|
||||
private const PARAM_NOT_NULL = 'notNull';
|
||||
private const PARAM_AUTOINCREMENT = 'autoincrement';
|
||||
private const PARAM_PRECISION = 'precision';
|
||||
private const PARAM_SCALE = 'scale';
|
||||
|
||||
/** @var string[] */
|
||||
private array $textTypeList = [
|
||||
Entity::TEXT,
|
||||
Entity::JSON_OBJECT,
|
||||
Entity::JSON_ARRAY,
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
private array $columnTypeMap = [
|
||||
Entity::BOOL => Types::BOOLEAN,
|
||||
Entity::INT => Types::INTEGER,
|
||||
Entity::VARCHAR => Types::STRING,
|
||||
// DBAL reverse engineers as blob.
|
||||
Types::BINARY => Types::BLOB,
|
||||
];
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function prepare(AttributeDefs $defs): Column
|
||||
{
|
||||
$dbType = $defs->getParam(self::PARAM_DB_TYPE);
|
||||
$type = $defs->getType();
|
||||
$length = $defs->getLength();
|
||||
$default = $defs->getParam(self::PARAM_DEFAULT);
|
||||
$notNull = $defs->getParam(self::PARAM_NOT_NULL);
|
||||
$autoincrement = $defs->getParam(self::PARAM_AUTOINCREMENT);
|
||||
$precision = $defs->getParam(self::PARAM_PRECISION);
|
||||
$scale = $defs->getParam(self::PARAM_SCALE);
|
||||
|
||||
$columnType = $dbType ?? $type;
|
||||
|
||||
if (in_array($type, $this->textTypeList) && !$dbType) {
|
||||
$columnType = Types::TEXT;
|
||||
}
|
||||
|
||||
$columnType = $this->columnTypeMap[$columnType] ?? $columnType;
|
||||
|
||||
$columnName = Util::toUnderScore($defs->getName());
|
||||
|
||||
$column = Column::create($columnName, strtolower($columnType));
|
||||
|
||||
if ($length !== null) {
|
||||
$column = $column->withLength($length);
|
||||
}
|
||||
|
||||
if ($default !== null) {
|
||||
$column = $column->withDefault($default);
|
||||
}
|
||||
|
||||
if ($notNull !== null) {
|
||||
$column = $column->withNotNull($notNull);
|
||||
}
|
||||
|
||||
if ($autoincrement !== null) {
|
||||
$column = $column->withAutoincrement($autoincrement);
|
||||
}
|
||||
|
||||
if ($precision !== null) {
|
||||
$column = $column->withPrecision($precision);
|
||||
}
|
||||
|
||||
if ($scale !== null) {
|
||||
$column = $column->withScale($scale);
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case Entity::TEXT:
|
||||
case Entity::JSON_ARRAY:
|
||||
$column = $column->withDefault(null);
|
||||
|
||||
break;
|
||||
|
||||
case Entity::BOOL:
|
||||
$default = intval($default ?? false);
|
||||
|
||||
$column = $column->withDefault($default);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($type !== Entity::ID && $autoincrement) {
|
||||
$column = $column
|
||||
->withNotNull()
|
||||
->withUnsigned();
|
||||
}
|
||||
|
||||
return $column;
|
||||
|
||||
// @todo Revise. Comparator would detect the column as changed if charset is set.
|
||||
/*if (
|
||||
!in_array($columnType, [
|
||||
Types::STRING,
|
||||
Types::TEXT,
|
||||
])
|
||||
) {
|
||||
return $column;
|
||||
}
|
||||
|
||||
return $column->withCharset('UTF8');*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\PDO;
|
||||
|
||||
use Espo\ORM\DatabaseParams;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
|
||||
class PostgresqlPDOFactory implements PDOFactory
|
||||
{
|
||||
public function create(DatabaseParams $databaseParams): PDO
|
||||
{
|
||||
$platform = strtolower($databaseParams->getPlatform() ?? '');
|
||||
|
||||
$host = $databaseParams->getHost();
|
||||
$port = $databaseParams->getPort();
|
||||
$dbname = $databaseParams->getName();
|
||||
//$charset = $databaseParams->getCharset();
|
||||
$username = $databaseParams->getUsername();
|
||||
$password = $databaseParams->getPassword();
|
||||
|
||||
if (!$platform) {
|
||||
throw new RuntimeException("No 'platform' parameter.");
|
||||
}
|
||||
|
||||
if (!$host) {
|
||||
throw new RuntimeException("No 'host' parameter.");
|
||||
}
|
||||
|
||||
$dsn = 'pgsql:' . 'host=' . $host;
|
||||
|
||||
if ($port) {
|
||||
$dsn .= ';' . 'port=' . (string) $port;
|
||||
}
|
||||
|
||||
if ($dbname) {
|
||||
$dsn .= ';' . 'dbname=' . $dbname;
|
||||
}
|
||||
|
||||
/*if ($charset) {
|
||||
$dsn .= ';' . 'options=' . "'--client_encoding={$charset}'";
|
||||
}*/
|
||||
|
||||
$options = Options::getOptionsFromDatabaseParams($databaseParams);
|
||||
|
||||
$pdo = new PDO($dsn, $username, $password, $options);
|
||||
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$pdo->query("SET time zone 'UTC'");
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\Query\Delete as DeleteQuery;
|
||||
use Espo\ORM\Query\DeleteBuilder;
|
||||
use Espo\ORM\Query\Insert as InsertQuery;
|
||||
use Espo\ORM\Query\LockTable as LockTableQuery;
|
||||
|
||||
use Espo\ORM\Query\Part\Condition as Cond;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\ORM\Query\Update as UpdateQuery;
|
||||
use Espo\ORM\Query\UpdateBuilder;
|
||||
use LogicException;
|
||||
use RuntimeException;
|
||||
|
||||
class PostgresqlQueryComposer extends BaseQueryComposer
|
||||
{
|
||||
protected string $identifierQuoteCharacter = '"';
|
||||
protected bool $indexHints = false;
|
||||
protected int $aliasMaxLength = 128;
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected array $comparisonOperators = [
|
||||
'!=s' => 'NOT IN',
|
||||
'=s' => 'IN',
|
||||
'!=' => '<>',
|
||||
'!*' => 'NOT ILIKE',
|
||||
'*' => 'ILIKE',
|
||||
'>=' => '>=',
|
||||
'<=' => '<=',
|
||||
'>' => '>',
|
||||
'<' => '<',
|
||||
'=' => '=',
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected array $comparisonFunctionOperatorMap = [
|
||||
'LIKE' => 'ILIKE',
|
||||
'NOT_LIKE' => 'NOT ILIKE',
|
||||
'EQUAL' => '=',
|
||||
'NOT_EQUAL' => '<>',
|
||||
'GREATER_THAN' => '>',
|
||||
'LESS_THAN' => '<',
|
||||
'GREATER_THAN_OR_EQUAL' => '>=',
|
||||
'LESS_THAN_OR_EQUAL' => '<=',
|
||||
'IS_NULL' => 'IS NULL',
|
||||
'IS_NOT_NULL' => 'IS NOT NULL',
|
||||
'IN' => 'IN',
|
||||
'NOT_IN' => 'NOT IN',
|
||||
];
|
||||
|
||||
protected function quoteColumn(string $column): string
|
||||
{
|
||||
$list = explode('.', $column);
|
||||
$list = array_map(fn ($item) => '"' . $item . '"', $list);
|
||||
|
||||
return implode('.', $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Make protected.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function quote($value): string
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_int($value)) {
|
||||
return strval($value);
|
||||
}
|
||||
|
||||
if (is_float($value)) {
|
||||
return strval($value);
|
||||
}
|
||||
|
||||
return $this->pdo->quote($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $argumentPartList
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
protected function getFunctionPart(
|
||||
string $function,
|
||||
string $part,
|
||||
array $params,
|
||||
string $entityType,
|
||||
bool $distinct,
|
||||
array $argumentPartList = []
|
||||
): string {
|
||||
|
||||
if (in_array($function, ['MATCH_BOOLEAN', 'MATCH_NATURAL_LANGUAGE'])) {
|
||||
if (count($argumentPartList) < 2) {
|
||||
throw new RuntimeException("Not enough arguments for MATCH function.");
|
||||
}
|
||||
|
||||
$queryPart = end($argumentPartList);
|
||||
$columnsPart = implode(
|
||||
" || ' ' || ",
|
||||
array_map(
|
||||
fn ($item) => "COALESCE({$item}, '')",
|
||||
array_slice($argumentPartList, 0, -1)
|
||||
)
|
||||
);
|
||||
|
||||
return "TS_RANK_CD(TO_TSVECTOR({$columnsPart}), PLAINTO_TSQUERY({$queryPart}))";
|
||||
}
|
||||
|
||||
if ($function === 'IF') {
|
||||
if (count($argumentPartList) < 3) {
|
||||
throw new RuntimeException("Not enough arguments for IF function.");
|
||||
}
|
||||
|
||||
$conditionPart = $argumentPartList[0];
|
||||
$thenPart = $argumentPartList[1];
|
||||
$elsePart = $argumentPartList[2];
|
||||
|
||||
return "CASE WHEN {$conditionPart} THEN {$thenPart} ELSE {$elsePart} END";
|
||||
}
|
||||
|
||||
if ($function === 'ROUND') {
|
||||
if (count($argumentPartList) === 2 && $argumentPartList[1] === '0') {
|
||||
$argumentPartList = array_slice($argumentPartList, 0, -1);
|
||||
|
||||
return "ROUND({$argumentPartList[0]})";
|
||||
}
|
||||
}
|
||||
|
||||
if ($function === 'UNIX_TIMESTAMP') {
|
||||
$arg = $argumentPartList[0] ?? 'NOW()';
|
||||
|
||||
return "FLOOR(EXTRACT(EPOCH FROM {$arg}))";
|
||||
}
|
||||
|
||||
if ($function === 'BINARY') {
|
||||
// Not supported.
|
||||
return $argumentPartList[0] ?? '0';
|
||||
}
|
||||
|
||||
if ($function === 'TZ') {
|
||||
if (count($argumentPartList) < 2) {
|
||||
throw new RuntimeException("Not enough arguments for function TZ.");
|
||||
}
|
||||
|
||||
$offsetHoursString = $argumentPartList[1];
|
||||
if (str_starts_with($offsetHoursString, '\'') && str_ends_with($offsetHoursString, '\'')) {
|
||||
$offsetHoursString = substr($offsetHoursString, 1, -1);
|
||||
}
|
||||
|
||||
return "{$argumentPartList[0]} + INTERVAL 'HOUR {$offsetHoursString}'";
|
||||
}
|
||||
|
||||
if ($function === 'POSITION_IN_LIST') {
|
||||
if (count($argumentPartList) <= 1) {
|
||||
return $this->quote(1);
|
||||
}
|
||||
|
||||
$field = $argumentPartList[0];
|
||||
|
||||
$pairs = array_map(
|
||||
fn($i) => [$i, $argumentPartList[$i]],
|
||||
array_keys($argumentPartList)
|
||||
);
|
||||
|
||||
$whenParts = array_map(function ($item) use ($field) {
|
||||
$resolution = intval($item[0]);
|
||||
$value = $item[1];
|
||||
|
||||
return " WHEN {$field} = {$value} THEN {$resolution}";
|
||||
}, array_slice($pairs, 1));
|
||||
|
||||
return "CASE" . implode('', $whenParts) . " ELSE 0 END";
|
||||
}
|
||||
|
||||
if ($function === 'IFNULL') {
|
||||
$function = 'COALESCE';
|
||||
}
|
||||
|
||||
if (str_starts_with($function, 'YEAR_') && $function !== 'YEAR_NUMBER') {
|
||||
$fiscalShift = substr($function, 5);
|
||||
|
||||
if (is_numeric($fiscalShift)) {
|
||||
$fiscalShift = (int) $fiscalShift;
|
||||
$fiscalFirstMonth = $fiscalShift + 1;
|
||||
|
||||
return
|
||||
"CASE WHEN EXTRACT(MONTH FROM {$part}) >= {$fiscalFirstMonth} THEN ".
|
||||
"EXTRACT(YEAR FROM {$part}) ".
|
||||
"ELSE EXTRACT(YEAR FROM {$part}) - 1 END";
|
||||
}
|
||||
}
|
||||
|
||||
if (str_starts_with($function, 'QUARTER_') && $function !== 'QUARTER_NUMBER') {
|
||||
$fiscalShift = substr($function, 8);
|
||||
|
||||
if (is_numeric($fiscalShift)) {
|
||||
$fiscalShift = (int) $fiscalShift;
|
||||
$fiscalFirstMonth = $fiscalShift + 1;
|
||||
$fiscalDistractedMonth = $fiscalFirstMonth < 4 ?
|
||||
12 - $fiscalFirstMonth :
|
||||
12 - $fiscalFirstMonth + 1;
|
||||
|
||||
return
|
||||
"CASE WHEN EXTRACT(MONTH FROM {$part}) >= {$fiscalFirstMonth} " .
|
||||
"THEN " .
|
||||
"CONCAT(EXTRACT(YEAR FROM {$part}), '_', " .
|
||||
"FLOOR((EXTRACT(MONTH FROM {$part}) - {$fiscalFirstMonth}) / 3) + 1) " .
|
||||
"ELSE CONCAT(EXTRACT(YEAR FROM {$part}) - 1, '_', " .
|
||||
"CEIL(EXTRACT(MONTH FROM {$part}) + {$fiscalDistractedMonth}) / 3)) " .
|
||||
"END";
|
||||
}
|
||||
}
|
||||
|
||||
switch ($function) {
|
||||
case 'MONTH':
|
||||
return "TO_CHAR({$part}, 'YYYY-MM')";
|
||||
|
||||
case 'DAY':
|
||||
return "TO_CHAR({$part}, 'YYYY-MM-DD')";
|
||||
|
||||
case 'WEEK':
|
||||
case 'WEEK_0':
|
||||
case 'WEEK_1':
|
||||
return "CONCAT(TO_CHAR({$part}, 'YYYY'), '/', TRIM(LEADING '0' FROM TO_CHAR({$part}, 'IW')))";
|
||||
|
||||
case 'QUARTER':
|
||||
return "CONCAT(TO_CHAR({$part}, 'YYYY'), '_', TO_CHAR({$part}, 'Q'))";
|
||||
|
||||
case 'WEEK_NUMBER_0':
|
||||
case 'WEEK_NUMBER':
|
||||
case 'WEEK_NUMBER_1':
|
||||
// Monday week-start not implemented.
|
||||
return "TO_CHAR({$part}, 'IW')::INTEGER";
|
||||
|
||||
case 'HOUR_NUMBER':
|
||||
case 'HOUR':
|
||||
return "EXTRACT(HOUR FROM {$part})";
|
||||
|
||||
case 'MINUTE_NUMBER':
|
||||
case 'MINUTE':
|
||||
return "EXTRACT(MINUTE FROM {$part})";
|
||||
|
||||
case 'SECOND_NUMBER':
|
||||
case 'SECOND':
|
||||
return "FLOOR(EXTRACT(SECOND FROM {$part}))";
|
||||
|
||||
case 'DATE_NUMBER':
|
||||
case 'DAYOFMONTH':
|
||||
return "EXTRACT(DAY FROM {$part})";
|
||||
|
||||
case 'DAYOFWEEK_NUMBER':
|
||||
case 'DAYOFWEEK':
|
||||
return "EXTRACT(DOW FROM {$part})";
|
||||
|
||||
case 'MONTH_NUMBER':
|
||||
return "EXTRACT(MONTH FROM {$part})";
|
||||
|
||||
case 'YEAR_NUMBER':
|
||||
case 'YEAR':
|
||||
return "EXTRACT(YEAR FROM {$part})";
|
||||
|
||||
case 'QUARTER_NUMBER':
|
||||
return "EXTRACT(QUARTER FROM {$part})";
|
||||
}
|
||||
|
||||
if (str_starts_with($function, 'TIMESTAMPDIFF_')) {
|
||||
$from = $argumentPartList[0] ?? $this->quote(0);
|
||||
$to = $argumentPartList[1] ?? $this->quote(0);
|
||||
|
||||
switch ($function) {
|
||||
case 'TIMESTAMPDIFF_YEAR':
|
||||
return "EXTRACT(YEAR FROM {$to} - {$from})";
|
||||
|
||||
case 'TIMESTAMPDIFF_MONTH':
|
||||
return "EXTRACT(MONTH FROM {$to}) - {$from})";
|
||||
|
||||
case 'TIMESTAMPDIFF_WEEK':
|
||||
return "FLOOR(EXTRACT(DAY FROM {$to} - {$from}) / 7)";
|
||||
|
||||
case 'TIMESTAMPDIFF_DAY':
|
||||
return "EXTRACT(DAY FROM ({$to}) - {$from})";
|
||||
|
||||
case 'TIMESTAMPDIFF_HOUR':
|
||||
return "EXTRACT(HOUR FROM {$to} - {$from})";
|
||||
|
||||
case 'TIMESTAMPDIFF_MINUTE':
|
||||
return "EXTRACT(MINUTE FROM {$to} - {$from})";
|
||||
|
||||
case 'TIMESTAMPDIFF_SECOND':
|
||||
return "FLOOR(EXTRACT(SECOND FROM {$to} - {$from}))";
|
||||
}
|
||||
}
|
||||
|
||||
return parent::getFunctionPart(
|
||||
$function,
|
||||
$part,
|
||||
$params,
|
||||
$entityType,
|
||||
$distinct,
|
||||
$argumentPartList
|
||||
);
|
||||
}
|
||||
|
||||
public function composeDelete(DeleteQuery $query): string
|
||||
{
|
||||
if (
|
||||
$query->getJoins() !== [] ||
|
||||
$query->getLeftJoins() !== [] ||
|
||||
$query->getLimit() !== null ||
|
||||
$query->getOrder() !== []
|
||||
) {
|
||||
$subQueryBuilder = SelectBuilder::create()
|
||||
->select('id')
|
||||
->from($query->getFrom())
|
||||
->order($query->getOrder());
|
||||
|
||||
foreach ($query->getJoins() as $join) {
|
||||
$subQueryBuilder->join($join);
|
||||
}
|
||||
|
||||
foreach ($query->getLeftJoins() as $join) {
|
||||
$subQueryBuilder->leftJoin($join);
|
||||
}
|
||||
|
||||
if ($query->getWhere()) {
|
||||
$subQueryBuilder->where($query->getWhere());
|
||||
}
|
||||
|
||||
if ($query->getLimit() !== null) {
|
||||
$subQueryBuilder->limit(null, $query->getLimit());
|
||||
}
|
||||
|
||||
$builder = DeleteBuilder::create()
|
||||
->from($query->getFrom(), $query->getFromAlias())
|
||||
->where(
|
||||
Cond::in(
|
||||
Cond::column('id'),
|
||||
$subQueryBuilder->build()
|
||||
)
|
||||
);
|
||||
|
||||
$query = $builder->build();
|
||||
}
|
||||
|
||||
return parent::composeDelete($query);
|
||||
}
|
||||
|
||||
public function composeUpdate(UpdateQuery $query): string
|
||||
{
|
||||
if (
|
||||
$query->getJoins() !== [] ||
|
||||
$query->getLeftJoins() !== [] ||
|
||||
$query->getLimit() !== null ||
|
||||
$query->getOrder() !== []
|
||||
) {
|
||||
$subQueryBuilder = SelectBuilder::create()
|
||||
->select('id')
|
||||
->from($query->getIn())
|
||||
->order($query->getOrder())
|
||||
->forUpdate();
|
||||
|
||||
foreach ($query->getJoins() as $join) {
|
||||
$subQueryBuilder->join($join);
|
||||
}
|
||||
|
||||
foreach ($query->getLeftJoins() as $join) {
|
||||
$subQueryBuilder->leftJoin($join);
|
||||
}
|
||||
|
||||
if ($query->getWhere()) {
|
||||
$subQueryBuilder->where($query->getWhere());
|
||||
}
|
||||
|
||||
if ($query->getLimit() !== null) {
|
||||
$subQueryBuilder->limit(null, $query->getLimit());
|
||||
}
|
||||
|
||||
$builder = UpdateBuilder::create()
|
||||
->in($query->getIn())
|
||||
->set($query->getSet())
|
||||
->where(
|
||||
Cond::in(
|
||||
Cond::column('id'),
|
||||
$subQueryBuilder->build()
|
||||
)
|
||||
);
|
||||
|
||||
$query = $builder->build();
|
||||
}
|
||||
|
||||
return parent::composeUpdate($query);
|
||||
}
|
||||
|
||||
public function composeInsert(InsertQuery $query): string
|
||||
{
|
||||
$params = $query->getRaw();
|
||||
$params = $this->normalizeInsertParams($params);
|
||||
|
||||
$entityType = $params['into'];
|
||||
$columns = $params['columns'];
|
||||
$updateSet = $params['updateSet'];
|
||||
|
||||
$columnsPart = $this->getInsertColumnsPart($columns);
|
||||
$valuesPart = $this->getInsertValuesPart($entityType, $params);
|
||||
$updatePart = $updateSet ? $this->getInsertUpdatePart($updateSet) : null;
|
||||
|
||||
$table = $this->toDb($entityType);
|
||||
|
||||
$sql = "INSERT INTO " . $this->quoteIdentifier($table) . " ({$columnsPart}) {$valuesPart}";
|
||||
|
||||
if ($updatePart) {
|
||||
$updateColumnsPart = implode(', ',
|
||||
array_map(fn ($item) => $this->quoteIdentifier($this->toDb($this->sanitize($item))),
|
||||
$this->getEntityUniqueColumns($entityType)
|
||||
)
|
||||
);
|
||||
|
||||
$sql .= " ON CONFLICT({$updateColumnsPart}) DO UPDATE SET " . $updatePart;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getEntityUniqueColumns(string $entityType): array
|
||||
{
|
||||
$indexes = $this->metadata
|
||||
->getDefs()
|
||||
->getEntity($entityType)
|
||||
->getIndexList();
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
if ($index->isUnique()) {
|
||||
return $index->getColumnList();
|
||||
}
|
||||
}
|
||||
|
||||
return ['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $values
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
protected function getSetPart(Entity $entity, array $values, array $params): string
|
||||
{
|
||||
if (!count($values)) {
|
||||
throw new RuntimeException("ORM Query: No SET values for update query.");
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($values as $attribute => $value) {
|
||||
$isNotValue = false;
|
||||
|
||||
if (str_ends_with($attribute, ':')) {
|
||||
$attribute = substr($attribute, 0, -1);
|
||||
$isNotValue = true;
|
||||
}
|
||||
|
||||
if (strpos($attribute, '.') > 0) {
|
||||
[$alias, $attribute] = explode('.', $attribute);
|
||||
|
||||
$alias = $this->sanitize($alias);
|
||||
$column = $this->toDb($this->sanitize($attribute));
|
||||
|
||||
$left = $this->quoteColumn("{$alias}.{$column}");
|
||||
}
|
||||
else {
|
||||
$column = $this->toDb($this->sanitize($attribute));
|
||||
|
||||
$left = $this->quoteColumn("{$column}"); // Diff.
|
||||
}
|
||||
|
||||
$right = $isNotValue ?
|
||||
$this->convertComplexExpression($entity, $value, false, $params) :
|
||||
$this->quote($value);
|
||||
|
||||
$list[] = $left . " = " . $right;
|
||||
}
|
||||
|
||||
return implode(', ', $list);
|
||||
}
|
||||
|
||||
public function composeRollbackToSavepoint(string $savepointName): string
|
||||
{
|
||||
return 'ROLLBACK TRANSACTION TO SAVEPOINT ' . $this->sanitize($savepointName);
|
||||
}
|
||||
|
||||
public function composeLockTable(LockTableQuery $query): string
|
||||
{
|
||||
$params = $query->getRaw();
|
||||
|
||||
$table = $this->toDb($this->sanitize($params['table']));
|
||||
|
||||
$mode = $params['mode'];
|
||||
|
||||
if (empty($table)) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
if (!in_array($mode, [LockTableQuery::MODE_SHARE, LockTableQuery::MODE_EXCLUSIVE])) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$sql = "LOCK TABLE " . $this->quoteIdentifier($table) . " IN ";
|
||||
|
||||
$modeMap = [
|
||||
LockTableQuery::MODE_SHARE => 'SHARE',
|
||||
LockTableQuery::MODE_EXCLUSIVE => 'EXCLUSIVE',
|
||||
];
|
||||
|
||||
$sql .= $modeMap[$mode] . " MODE";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
protected function limit(string $sql, ?int $offset = null, ?int $limit = null): string
|
||||
{
|
||||
if (!is_null($offset) && !is_null($limit)) {
|
||||
$offset = intval($offset);
|
||||
$limit = intval($limit);
|
||||
|
||||
$sql .= " LIMIT {$limit} OFFSET {$offset}";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
if (!is_null($limit)) {
|
||||
$limit = intval($limit);
|
||||
|
||||
$sql .= " LIMIT {$limit}";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,12 @@
|
||||
}
|
||||
},
|
||||
"Postgresql": {
|
||||
"dbalConnectionFactoryClassName": "Espo\\Core\\Utils\\Database\\Dbal\\Factories\\PostgresqlConnectionFactory"
|
||||
"detailsProviderClassName": "Espo\\Core\\Utils\\Database\\DetailsProviders\\PostgresqlDetailsProvider",
|
||||
"dbalConnectionFactoryClassName": "Espo\\Core\\Utils\\Database\\Dbal\\Factories\\PostgresqlConnectionFactory",
|
||||
"indexHelperClassName": "Espo\\Core\\Utils\\Database\\Orm\\IndexHelpers\\PostgresqlIndexHelper",
|
||||
"columnPreparatorClassName": "Espo\\Core\\Utils\\Database\\Schema\\ColumnPreparators\\PostgresqlColumnPreparator",
|
||||
"dbalTypeClassNameMap": {
|
||||
"uuid": "Espo\\Core\\Utils\\Database\\Dbal\\Types\\UuidType"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,15 @@
|
||||
"functionConverterClassNameMap_Mysql": {
|
||||
"ABS": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\Abs"
|
||||
},
|
||||
"functionConverterClassNameMap_Postgresql": {
|
||||
"ABS": "Espo\\Core\\ORM\\QueryComposer\\Part\\FunctionConverters\\Abs"
|
||||
},
|
||||
"queryComposerClassNameMap": {
|
||||
"Mysql": "Espo\\ORM\\QueryComposer\\MysqlQueryComposer"
|
||||
"Mysql": "Espo\\ORM\\QueryComposer\\MysqlQueryComposer",
|
||||
"Postgresql": "Espo\\ORM\\QueryComposer\\PostgresqlQueryComposer"
|
||||
},
|
||||
"pdoFactoryClassNameMap": {
|
||||
"Mysql": "Espo\\ORM\\PDO\\MysqlPDOFactory"
|
||||
"Mysql": "Espo\\ORM\\PDO\\MysqlPDOFactory",
|
||||
"Postgresql": "Espo\\ORM\\PDO\\PostgresqlPDOFactory"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace tests\unit\Espo\ORM;
|
||||
|
||||
use Espo\ORM\EntityFactory;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Metadata;
|
||||
use Espo\ORM\MetadataDataProvider;
|
||||
use Espo\ORM\Query\DeleteBuilder;
|
||||
use Espo\ORM\Query\InsertBuilder;
|
||||
use Espo\ORM\Query\UpdateBuilder;
|
||||
use Espo\ORM\QueryBuilder;
|
||||
use Espo\ORM\QueryComposer\PostgresqlQueryComposer as QueryComposer;
|
||||
|
||||
require_once 'tests/unit/testData/DB/Entities.php';
|
||||
require_once 'tests/unit/testData/DB/MockPDO.php';
|
||||
require_once 'tests/unit/testData/DB/MockDBResult.php';
|
||||
|
||||
class PostgresqlQueryComposerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private ?QueryComposer $queryComposer;
|
||||
private ?EntityManager $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$ormMetadata = include('tests/unit/testData/DB/ormMetadata.php');
|
||||
|
||||
$metadataDataProvider = $this->createMock(MetadataDataProvider::class);
|
||||
|
||||
$metadataDataProvider
|
||||
->expects($this->any())
|
||||
->method('get')
|
||||
->willReturn($ormMetadata);
|
||||
|
||||
$metadata = new Metadata($metadataDataProvider);
|
||||
|
||||
$this->queryBuilder = new QueryBuilder();
|
||||
|
||||
$pdo = $this->createMock('MockPDO');
|
||||
$pdo
|
||||
->expects($this->any())
|
||||
->method('quote')
|
||||
->will($this->returnCallback(function() {
|
||||
$args = func_get_args();
|
||||
|
||||
return "'" . $args[0] . "'";
|
||||
}));
|
||||
|
||||
$this->entityManager = $this->createMock(EntityManager::class);
|
||||
$entityFactory = $this->createMock(EntityFactory::class);
|
||||
|
||||
$entityFactory
|
||||
->expects($this->any())
|
||||
->method('create')
|
||||
->will(
|
||||
$this->returnCallback(function () use ($metadata) {
|
||||
$args = func_get_args();
|
||||
$className = "tests\\unit\\testData\\DB\\" . $args[0];
|
||||
$defs = $metadata->get($args[0]) ?? [];
|
||||
|
||||
return new $className($args[0], $defs, $this->entityManager);
|
||||
})
|
||||
);
|
||||
|
||||
$this->queryComposer = new QueryComposer($pdo, $entityFactory, $metadata);
|
||||
|
||||
$this->post = $entityFactory->create('Post');
|
||||
$this->comment = $entityFactory->create('Comment');
|
||||
$this->tag = $entityFactory->create('Tag');
|
||||
$this->note = $entityFactory->create('Note');
|
||||
$this->contact = $entityFactory->create('Contact');
|
||||
$this->account = $entityFactory->create('Account');
|
||||
}
|
||||
|
||||
public function testUpdate1(): void
|
||||
{
|
||||
$query = UpdateBuilder::create()
|
||||
->in('Comment')
|
||||
->set(['name' => '1'])
|
||||
->where(['name' => 'post.name'])
|
||||
->join('post')
|
||||
->limit(1)
|
||||
->order('name')
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->composeUpdate($query);
|
||||
|
||||
$expectedSql =
|
||||
'UPDATE "comment" SET "name" = \'1\' WHERE "comment"."id" IN ' .
|
||||
'(SELECT "comment"."id" AS "id" FROM "comment" ' .
|
||||
'JOIN "post" AS "post" ON "comment"."post_id" = "post"."id" ' .
|
||||
'WHERE "comment"."name" = \'post.name\' AND "comment"."deleted" = false ' .
|
||||
'ORDER BY "comment"."name" ASC LIMIT 1 OFFSET 0 FOR UPDATE)';
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testDelete1(): void
|
||||
{
|
||||
$query = DeleteBuilder::create()
|
||||
->from('Account')
|
||||
->where(['name' => 'test'])
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->composeDelete($query);
|
||||
|
||||
$expectedSql =
|
||||
"DELETE FROM \"account\" " .
|
||||
"WHERE \"account\".\"name\" = 'test'";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testDelete2(): void
|
||||
{
|
||||
$query = DeleteBuilder::create()
|
||||
->from('Comment')
|
||||
->join('post')
|
||||
->where(['name' => 'post.name'])
|
||||
->limit(1)
|
||||
->order('name')
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->composeDelete($query);
|
||||
|
||||
$expectedSql =
|
||||
'DELETE FROM "comment" WHERE "comment"."id" IN ' .
|
||||
'(SELECT "comment"."id" AS "id" FROM "comment" ' .
|
||||
'JOIN "post" AS "post" ON "comment"."post_id" = "post"."id" ' .
|
||||
'WHERE "comment"."name" = \'post.name\' AND "comment"."deleted" = false ' .
|
||||
'ORDER BY "comment"."name" ASC LIMIT 1 OFFSET 0)';
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testInsertUpdate1(): void
|
||||
{
|
||||
$query = InsertBuilder::create()
|
||||
->into('PostTag')
|
||||
->columns(['id', 'postId', 'tagId'])
|
||||
->values([
|
||||
'id' => '1',
|
||||
'postId' => 'post-id',
|
||||
'tagId' => 'tag-id',
|
||||
])
|
||||
->updateSet([
|
||||
'deleted' => 0
|
||||
])
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->composeInsert($query);
|
||||
|
||||
$expectedSql =
|
||||
"INSERT INTO \"post_tag\" (\"id\", \"post_id\", \"tag_id\") VALUES ('1', 'post-id', 'tag-id') " .
|
||||
"ON CONFLICT(\"post_id\", \"tag_id\") DO UPDATE SET \"deleted\" = 0";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
|
||||
public function testInsertUpdate2(): void
|
||||
{
|
||||
$query = InsertBuilder::create()
|
||||
->into('Account')
|
||||
->columns(['id', 'name'])
|
||||
->values([
|
||||
'id' => '1',
|
||||
'name' => 'name',
|
||||
])
|
||||
->updateSet([
|
||||
'deleted' => 0
|
||||
])
|
||||
->build();
|
||||
|
||||
$sql = $this->queryComposer->composeInsert($query);
|
||||
|
||||
$expectedSql =
|
||||
"INSERT INTO \"account\" (\"id\", \"name\") VALUES ('1', 'name') " .
|
||||
"ON CONFLICT(\"id\") DO UPDATE SET \"deleted\" = 0";
|
||||
|
||||
$this->assertEquals($expectedSql, $sql);
|
||||
}
|
||||
}
|
||||
@@ -306,6 +306,13 @@ return [
|
||||
'relations' => [
|
||||
|
||||
],
|
||||
'indexes' => [
|
||||
'postIdTagId' => [
|
||||
'key' => 'UNIQ_POST_ID_TAG_ID',
|
||||
'type' => 'unique',
|
||||
'columns' => ['postId', 'tagId'],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'Note' => [
|
||||
|
||||
Reference in New Issue
Block a user