Initial commit
This commit is contained in:
3799
application/Espo/ORM/QueryComposer/BaseQueryComposer.php
Normal file
3799
application/Espo/ORM/QueryComposer/BaseQueryComposer.php
Normal file
File diff suppressed because it is too large
Load Diff
165
application/Espo/ORM/QueryComposer/Functions.php
Normal file
165
application/Espo/ORM/QueryComposer/Functions.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Functions
|
||||
{
|
||||
public const FUNCTION_LIST = [
|
||||
'ROW',
|
||||
'COUNT',
|
||||
'SUM',
|
||||
'AVG',
|
||||
'MAX',
|
||||
'MIN',
|
||||
'DATE',
|
||||
'MONTH',
|
||||
'DAY',
|
||||
'YEAR',
|
||||
'WEEK',
|
||||
'WEEK_0',
|
||||
'WEEK_1',
|
||||
'QUARTER',
|
||||
'DAYOFMONTH',
|
||||
'DAYOFWEEK',
|
||||
'DAYOFWEEK_NUMBER',
|
||||
'MONTH_NUMBER',
|
||||
'DATE_NUMBER',
|
||||
'YEAR_NUMBER',
|
||||
'HOUR_NUMBER',
|
||||
'HOUR',
|
||||
'MINUTE_NUMBER',
|
||||
'MINUTE',
|
||||
'QUARTER_NUMBER',
|
||||
'WEEK_NUMBER',
|
||||
'WEEK_NUMBER_0',
|
||||
'WEEK_NUMBER_1',
|
||||
'LOWER',
|
||||
'UPPER',
|
||||
'TRIM',
|
||||
'REPLACE',
|
||||
'LENGTH',
|
||||
'CHAR_LENGTH',
|
||||
'YEAR_0',
|
||||
'YEAR_1',
|
||||
'YEAR_2',
|
||||
'YEAR_3',
|
||||
'YEAR_4',
|
||||
'YEAR_5',
|
||||
'YEAR_6',
|
||||
'YEAR_7',
|
||||
'YEAR_8',
|
||||
'YEAR_9',
|
||||
'YEAR_10',
|
||||
'YEAR_11',
|
||||
'QUARTER_0',
|
||||
'QUARTER_1',
|
||||
'QUARTER_2',
|
||||
'QUARTER_3',
|
||||
'QUARTER_4',
|
||||
'QUARTER_5',
|
||||
'QUARTER_6',
|
||||
'QUARTER_7',
|
||||
'QUARTER_8',
|
||||
'QUARTER_9',
|
||||
'QUARTER_10',
|
||||
'QUARTER_11',
|
||||
'CONCAT',
|
||||
'LEFT',
|
||||
'TZ',
|
||||
'NOW',
|
||||
'ADD',
|
||||
'SUB',
|
||||
'MUL',
|
||||
'DIV',
|
||||
'MOD',
|
||||
'FLOOR',
|
||||
'CEIL',
|
||||
'ROUND',
|
||||
'GREATEST',
|
||||
'LEAST',
|
||||
'COALESCE',
|
||||
'IF',
|
||||
'LIKE',
|
||||
'NOT_LIKE',
|
||||
'EQUAL',
|
||||
'NOT_EQUAL',
|
||||
'GREATER_THAN',
|
||||
'LESS_THAN',
|
||||
'GREATER_THAN_OR_EQUAL',
|
||||
'LESS_THAN_OR_EQUAL',
|
||||
'IS_NULL',
|
||||
'IS_NOT_NULL',
|
||||
'OR',
|
||||
'AND',
|
||||
'NOT',
|
||||
'IN',
|
||||
'NOT_IN',
|
||||
'IFNULL',
|
||||
'NULLIF',
|
||||
'SWITCH',
|
||||
'MAP',
|
||||
'BINARY',
|
||||
'MD5',
|
||||
'UNIX_TIMESTAMP',
|
||||
'TIMESTAMPDIFF_DAY',
|
||||
'TIMESTAMPDIFF_MONTH',
|
||||
'TIMESTAMPDIFF_YEAR',
|
||||
'TIMESTAMPDIFF_WEEK',
|
||||
'TIMESTAMPDIFF_HOUR',
|
||||
'TIMESTAMPDIFF_MINUTE',
|
||||
'TIMESTAMPDIFF_SECOND',
|
||||
'POSITION_IN_LIST',
|
||||
'MATCH_BOOLEAN',
|
||||
'MATCH_NATURAL_LANGUAGE',
|
||||
'ANY_VALUE',
|
||||
];
|
||||
|
||||
public const COMPARISON_FUNCTION_LIST = [
|
||||
'LIKE',
|
||||
'NOT_LIKE',
|
||||
'EQUAL',
|
||||
'NOT_EQUAL',
|
||||
'GREATER_THAN',
|
||||
'LESS_THAN',
|
||||
'GREATER_THAN_OR_EQUAL',
|
||||
'LESS_THAN_OR_EQUAL',
|
||||
];
|
||||
|
||||
public const MATH_OPERATION_FUNCTION_LIST = [
|
||||
'ADD',
|
||||
'SUB',
|
||||
'MUL',
|
||||
'DIV',
|
||||
'MOD',
|
||||
];
|
||||
}
|
||||
101
application/Espo/ORM/QueryComposer/MysqlQueryComposer.php
Normal file
101
application/Espo/ORM/QueryComposer/MysqlQueryComposer.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer;
|
||||
|
||||
use Espo\ORM\Query\LockTable as LockTableQuery;
|
||||
|
||||
use LogicException;
|
||||
|
||||
class MysqlQueryComposer extends BaseQueryComposer
|
||||
{
|
||||
public function composeLockTable(LockTableQuery $query): string
|
||||
{
|
||||
$params = $query->getRaw();
|
||||
|
||||
$entityType = $this->sanitize($params['table']);
|
||||
|
||||
$table = $this->toDb($entityType);
|
||||
|
||||
$mode = $params['mode'];
|
||||
|
||||
if (empty($table)) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
if (!in_array($mode, [LockTableQuery::MODE_SHARE, LockTableQuery::MODE_EXCLUSIVE])) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$sql = "LOCK TABLES " . $this->quoteIdentifier($table) . " ";
|
||||
|
||||
$modeMap = [
|
||||
LockTableQuery::MODE_SHARE => 'READ',
|
||||
LockTableQuery::MODE_EXCLUSIVE => 'WRITE',
|
||||
];
|
||||
|
||||
$sql .= $modeMap[$mode];
|
||||
|
||||
if (str_contains($table, '_')) {
|
||||
// MySQL has an issue that aliased tables must be locked with alias.
|
||||
$sql .= ", " .
|
||||
$this->quoteIdentifier($table) . " AS " .
|
||||
$this->quoteIdentifier(lcfirst($entityType)) . " " . $modeMap[$mode];
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
public function composeUnlockTables(): string
|
||||
{
|
||||
return "UNLOCK TABLES";
|
||||
}
|
||||
|
||||
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 $offset, $limit";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
if (!is_null($limit)) {
|
||||
$limit = intval($limit);
|
||||
|
||||
$sql .= " LIMIT $limit";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer\Part;
|
||||
|
||||
interface FunctionConverter
|
||||
{
|
||||
public function convert(string ...$argumentList): string;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer\Part;
|
||||
|
||||
interface FunctionConverterFactory
|
||||
{
|
||||
public function create(string $name): FunctionConverter;
|
||||
|
||||
public function isCreatable(string $name): bool;
|
||||
}
|
||||
656
application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php
Normal file
656
application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php
Normal file
@@ -0,0 +1,656 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero 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\Name\Attribute;
|
||||
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 bool $skipForeignIfForUpdate = true;
|
||||
protected int $aliasMaxLength = 128;
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected array $comparisonOperatorMap = [
|
||||
'!=s' => 'NOT IN',
|
||||
'=s' => 'IN',
|
||||
'!=' => '<>',
|
||||
'!*' => 'NOT ILIKE',
|
||||
'*' => 'ILIKE',
|
||||
'>=any' => '>= ANY',
|
||||
'<=any' => '<= ANY',
|
||||
'>any' => '> ANY',
|
||||
'<any' => '< ANY',
|
||||
'!=any' => '<> ANY',
|
||||
'=any' => '= ANY',
|
||||
'>=all' => '>= ALL',
|
||||
'<=all' => '<= ALL',
|
||||
'>all' => '> ALL',
|
||||
'<all' => '< ALL',
|
||||
'!=all' => '<> ALL',
|
||||
'=all' => '= ALL',
|
||||
];
|
||||
|
||||
/** @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);
|
||||
}
|
||||
|
||||
if (str_contains($offsetHoursString, '.')) {
|
||||
$minutes = (int) (floatval($offsetHoursString) * 60);
|
||||
$minutesString = (string) $minutes;
|
||||
|
||||
return "$argumentPartList[0] + INTERVAL '$minutesString MINUTE'";
|
||||
}
|
||||
|
||||
return "$argumentPartList[0] + INTERVAL '$offsetHoursString HOUR'";
|
||||
}
|
||||
|
||||
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':
|
||||
if (str_starts_with($part, "'")) {
|
||||
$part = "DATE " . $part;
|
||||
}
|
||||
|
||||
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(Attribute::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(Attribute::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(Attribute::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(Attribute::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 [Attribute::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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
protected function getSelectTailPart(array $params): ?string
|
||||
{
|
||||
$forShare = $params['forShare'] ?? null;
|
||||
$forUpdate = $params['forUpdate'] ?? null;
|
||||
|
||||
if ($forShare) {
|
||||
return "FOR SHARE";
|
||||
}
|
||||
|
||||
if ($forUpdate) {
|
||||
return "FOR UPDATE";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function composeDeleteQuery(
|
||||
string $table,
|
||||
?string $alias,
|
||||
string $where,
|
||||
?string $joins,
|
||||
?string $order,
|
||||
?int $limit
|
||||
): string {
|
||||
|
||||
$sql = "DELETE ";
|
||||
|
||||
$sql .= "FROM " . $this->quoteIdentifier($table);
|
||||
|
||||
if ($alias) {
|
||||
$sql .= " AS " . $this->quoteIdentifier($alias);
|
||||
}
|
||||
|
||||
if ($joins) {
|
||||
$sql .= " $joins";
|
||||
}
|
||||
|
||||
if ($where) {
|
||||
$sql .= " WHERE $where";
|
||||
}
|
||||
|
||||
if ($order) {
|
||||
$sql .= " ORDER BY $order";
|
||||
}
|
||||
|
||||
if ($limit !== null) {
|
||||
$sql = $this->limit($sql, null, $limit);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
58
application/Espo/ORM/QueryComposer/QueryComposer.php
Normal file
58
application/Espo/ORM/QueryComposer/QueryComposer.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer;
|
||||
|
||||
use Espo\ORM\Query\Select as SelectQuery;
|
||||
use Espo\ORM\Query\Update as UpdateQuery;
|
||||
use Espo\ORM\Query\Insert as InsertQuery;
|
||||
use Espo\ORM\Query\Delete as DeleteQuery;
|
||||
use Espo\ORM\Query\Union as UnionQuery;
|
||||
use Espo\ORM\Query\LockTable as LockTableQuery;
|
||||
|
||||
interface QueryComposer
|
||||
{
|
||||
public function composeSelect(SelectQuery $query): string;
|
||||
|
||||
public function composeUpdate(UpdateQuery $query): string;
|
||||
|
||||
public function composeDelete(DeleteQuery $query): string;
|
||||
|
||||
public function composeInsert(InsertQuery $query): string;
|
||||
|
||||
public function composeUnion(UnionQuery $query): string;
|
||||
|
||||
public function composeLockTable(LockTableQuery $query): string;
|
||||
|
||||
public function composeCreateSavepoint(string $savepointName): string;
|
||||
|
||||
public function composeReleaseSavepoint(string $savepointName): string;
|
||||
|
||||
public function composeRollbackToSavepoint(string $savepointName): string;
|
||||
}
|
||||
35
application/Espo/ORM/QueryComposer/QueryComposerFactory.php
Normal file
35
application/Espo/ORM/QueryComposer/QueryComposerFactory.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer;
|
||||
|
||||
interface QueryComposerFactory
|
||||
{
|
||||
public function create(string $platform): QueryComposer;
|
||||
}
|
||||
127
application/Espo/ORM/QueryComposer/QueryComposerWrapper.php
Normal file
127
application/Espo/ORM/QueryComposer/QueryComposerWrapper.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer;
|
||||
|
||||
use Espo\ORM\Query\Query as Query;
|
||||
use Espo\ORM\Query\Select as SelectQuery;
|
||||
use Espo\ORM\Query\Update as UpdateQuery;
|
||||
use Espo\ORM\Query\Insert as InsertQuery;
|
||||
use Espo\ORM\Query\Delete as DeleteQuery;
|
||||
use Espo\ORM\Query\Union as UnionQuery;
|
||||
use Espo\ORM\Query\LockTable as LockTableQuery;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class QueryComposerWrapper implements QueryComposer
|
||||
{
|
||||
private QueryComposer $queryComposer;
|
||||
|
||||
public function __construct(QueryComposer $queryComposer)
|
||||
{
|
||||
$this->queryComposer = $queryComposer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose an SQL query.
|
||||
*/
|
||||
public function compose(Query $query): string
|
||||
{
|
||||
if ($query instanceof SelectQuery) {
|
||||
return $this->composeSelect($query);
|
||||
}
|
||||
|
||||
if ($query instanceof UpdateQuery) {
|
||||
return $this->composeUpdate($query);
|
||||
}
|
||||
|
||||
if ($query instanceof InsertQuery) {
|
||||
return $this->composeInsert($query);
|
||||
}
|
||||
|
||||
if ($query instanceof DeleteQuery) {
|
||||
return $this->composeDelete($query);
|
||||
}
|
||||
|
||||
if ($query instanceof UnionQuery) {
|
||||
return $this->composeUnion($query);
|
||||
}
|
||||
|
||||
if ($query instanceof LockTableQuery) {
|
||||
return $this->composeLockTable($query);
|
||||
}
|
||||
|
||||
throw new RuntimeException("ORM Query: Unknown query type passed.");
|
||||
}
|
||||
|
||||
public function composeSelect(SelectQuery $query): string
|
||||
{
|
||||
return $this->queryComposer->composeSelect($query);
|
||||
}
|
||||
|
||||
public function composeUpdate(UpdateQuery $query): string
|
||||
{
|
||||
return $this->queryComposer->composeUpdate($query);
|
||||
}
|
||||
|
||||
public function composeDelete(DeleteQuery $query): string
|
||||
{
|
||||
return $this->queryComposer->composeDelete($query);
|
||||
}
|
||||
|
||||
public function composeInsert(InsertQuery $query): string
|
||||
{
|
||||
return $this->queryComposer->composeInsert($query);
|
||||
}
|
||||
|
||||
public function composeUnion(UnionQuery $query): string
|
||||
{
|
||||
return $this->queryComposer->composeUnion($query);
|
||||
}
|
||||
|
||||
public function composeLockTable(LockTableQuery $query): string
|
||||
{
|
||||
return $this->queryComposer->composeLockTable($query);
|
||||
}
|
||||
|
||||
public function composeCreateSavepoint(string $savepointName): string
|
||||
{
|
||||
return $this->queryComposer->composeCreateSavepoint($savepointName);
|
||||
}
|
||||
|
||||
public function composeReleaseSavepoint(string $savepointName): string
|
||||
{
|
||||
return $this->queryComposer->composeReleaseSavepoint($savepointName);
|
||||
}
|
||||
|
||||
public function composeRollbackToSavepoint(string $savepointName): string
|
||||
{
|
||||
return $this->queryComposer->composeRollbackToSavepoint($savepointName);
|
||||
}
|
||||
}
|
||||
199
application/Espo/ORM/QueryComposer/Util.php
Normal file
199
application/Espo/ORM/QueryComposer/Util.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2025 EspoCRM, Inc.
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://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 Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\ORM\QueryComposer;
|
||||
|
||||
class Util
|
||||
{
|
||||
public static function isComplexExpression(string $string): bool
|
||||
{
|
||||
if (
|
||||
self::isArgumentString($string) ||
|
||||
self::isArgumentNumeric($string) ||
|
||||
self::isArgumentBoolOrNull($string)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_contains($string, '.')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_contains($string, ':')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_starts_with($string, '#')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function isArgumentString(string $argument): bool
|
||||
{
|
||||
return
|
||||
str_starts_with($argument, '\'') && str_ends_with($argument, '\'')
|
||||
||
|
||||
str_starts_with($argument, '"') && str_ends_with($argument, '"');
|
||||
}
|
||||
|
||||
public static function isArgumentNumeric(string $argument): bool
|
||||
{
|
||||
return is_numeric($argument);
|
||||
}
|
||||
|
||||
public static function isArgumentBoolOrNull(string $argument): bool
|
||||
{
|
||||
return in_array(strtoupper($argument), ['NULL', 'TRUE', 'FALSE']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $expression
|
||||
* @return string[]
|
||||
*/
|
||||
public static function getAllAttributesFromComplexExpression(string $expression): array
|
||||
{
|
||||
return self::getAllAttributesFromComplexExpressionImplementation($expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[]|null $list
|
||||
* @return string[]
|
||||
*/
|
||||
private static function getAllAttributesFromComplexExpressionImplementation(
|
||||
string $expression,
|
||||
?array &$list = null
|
||||
): array {
|
||||
|
||||
if (!$list) {
|
||||
$list = [];
|
||||
}
|
||||
|
||||
if (!strpos($expression, ':')) {
|
||||
if (
|
||||
!self::isArgumentString($expression) &&
|
||||
!self::isArgumentNumeric($expression) &&
|
||||
!self::isArgumentBoolOrNull($expression) &&
|
||||
!str_contains($expression, '#')
|
||||
) {
|
||||
$list[] = $expression;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
$delimiterPosition = strpos($expression, ':');
|
||||
$arguments = substr($expression, $delimiterPosition + 1);
|
||||
|
||||
if (str_starts_with($arguments, '(') && str_ends_with($arguments, ')')) {
|
||||
$arguments = substr($arguments, 1, -1);
|
||||
}
|
||||
|
||||
$argumentList = self::parseArgumentListFromFunctionContent($arguments);
|
||||
|
||||
foreach ($argumentList as $argument) {
|
||||
self::getAllAttributesFromComplexExpressionImplementation($argument, $list);
|
||||
}
|
||||
|
||||
return $list ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
static public function parseArgumentListFromFunctionContent(string $functionContent): array
|
||||
{
|
||||
$functionContent = trim($functionContent);
|
||||
|
||||
$isString = false;
|
||||
$isSingleQuote = false;
|
||||
|
||||
if ($functionContent === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$commaIndexList = [];
|
||||
$braceCounter = 0;
|
||||
|
||||
for ($i = 0; $i < strlen($functionContent); $i++) {
|
||||
if ($functionContent[$i] === "'" && ($i === 0 || $functionContent[$i - 1] !== "\\")) {
|
||||
if (!$isString) {
|
||||
$isString = true;
|
||||
$isSingleQuote = true;
|
||||
} else {
|
||||
if ($isSingleQuote) {
|
||||
$isString = false;
|
||||
}
|
||||
}
|
||||
} else if ($functionContent[$i] === "\"" && ($i === 0 || $functionContent[$i - 1] !== "\\")) {
|
||||
if (!$isString) {
|
||||
$isString = true;
|
||||
$isSingleQuote = false;
|
||||
} else {
|
||||
if (!$isSingleQuote) {
|
||||
$isString = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isString) {
|
||||
if ($functionContent[$i] === '(') {
|
||||
$braceCounter++;
|
||||
} else if ($functionContent[$i] === ')') {
|
||||
$braceCounter--;
|
||||
}
|
||||
}
|
||||
|
||||
if ($braceCounter === 0 && !$isString && $functionContent[$i] === ',') {
|
||||
$commaIndexList[] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
$commaIndexList[] = strlen($functionContent);
|
||||
|
||||
$argumentList = [];
|
||||
|
||||
for ($i = 0; $i < count($commaIndexList); $i++) {
|
||||
if ($i > 0) {
|
||||
$previousCommaIndex = $commaIndexList[$i - 1] + 1;
|
||||
} else {
|
||||
$previousCommaIndex = 0;
|
||||
}
|
||||
|
||||
$argument = trim(
|
||||
substr($functionContent, $previousCommaIndex, $commaIndexList[$i] - $previousCommaIndex)
|
||||
);
|
||||
|
||||
$argumentList[] = $argument;
|
||||
}
|
||||
|
||||
return $argumentList;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user