diff --git a/application/Espo/ORM/QueryComposer/BaseQueryComposer.php b/application/Espo/ORM/QueryComposer/BaseQueryComposer.php index 72727678af..f338f25ae5 100644 --- a/application/Espo/ORM/QueryComposer/BaseQueryComposer.php +++ b/application/Espo/ORM/QueryComposer/BaseQueryComposer.php @@ -136,13 +136,6 @@ abstract class BaseQueryComposer implements QueryComposer 'MOD' => '%', ]; - /** @var array */ - protected array $matchFunctionMap = [ - 'MATCH_BOOLEAN' => 'IN BOOLEAN MODE', - 'MATCH_NATURAL_LANGUAGE' => 'IN NATURAL LANGUAGE MODE', - 'MATCH_QUERY_EXPANSION' => 'WITH QUERY EXPANSION', - ]; - protected const SELECT_METHOD = 'SELECT'; protected const DELETE_METHOD = 'DELETE'; protected const UPDATE_METHOD = 'UPDATE'; @@ -952,6 +945,19 @@ abstract class BaseQueryComposer implements QueryComposer throw new RuntimeException("ORM Query: Not allowed function '{$function}'."); } + 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_splice($argumentPartList, 0, -1)); + $modePart = $function === 'MATCH_BOOLEAN' ? + 'IN BOOLEAN MODE' : 'IN NATURAL LANGUAGE MODE'; + + return "MATCH ({$columnsPart}) AGAINST ({$queryPart} {$modePart})"; + } + if (str_starts_with($function, 'YEAR_') && $function !== 'YEAR_NUMBER') { $fiscalShift = substr($function, 5); @@ -989,7 +995,7 @@ abstract class BaseQueryComposer implements QueryComposer if (in_array($function, Functions::COMPARISON_FUNCTION_LIST)) { if (count($argumentPartList) < 2) { - throw new RuntimeException("ORM Query: Not enough arguments for function '{$function}'."); + throw new RuntimeException("Not enough arguments for function '{$function}'."); } $operator = $this->comparisonFunctionOperatorMap[$function]; @@ -1181,68 +1187,6 @@ abstract class BaseQueryComposer implements QueryComposer return "CONVERT_TZ(". $argumentPartList[0]. ", '+00:00', " . $this->quote($offsetString) . ")"; } - /** - * @param array $params - */ - protected function convertMatchExpression(Entity $entity, string $expression, array $params): string - { - $delimiterPosition = strpos($expression, ':'); - - if ($delimiterPosition === false) { - throw new RuntimeException("ORM Query: Bad MATCH usage."); - } - - $function = substr($expression, 0, $delimiterPosition); - $rest = substr($expression, $delimiterPosition + 1); - - if (empty($rest)) { - throw new RuntimeException("ORM Query: Empty MATCH parameters."); - } - - if (str_starts_with($rest, '(') && str_ends_with($rest, ')')) { - $rest = substr($rest, 1, -1); - - $argumentList = Util::parseArgumentListFromFunctionContent($rest); - - if (count($argumentList) < 2) { - throw new RuntimeException("ORM Query: Bad MATCH usage."); - } - - $columnList = []; - - for ($i = 0; $i < count($argumentList) - 1; $i++) { - $columnList[] = $argumentList[$i]; - } - - $query = $argumentList[count($argumentList) - 1]; - } - else { - throw new RuntimeException("ORM Query: Bad MATCH usage."); - } - - $fromAlias = $this->getFromAlias($params, $entity->getEntityType()); - - foreach ($columnList as $i => $column) { - $columnList[$i] = $this->quoteColumn($fromAlias . '.' . $this->sanitize($this->toDb($column))); - } - - if (!Util::isArgumentString($query)) { - throw new RuntimeException("ORM Query: Bad MATCH usage. The last argument should be a string."); - } - - $query = mb_substr($query, 1, -1); - - $query = $this->quote($query); - - if (!in_array($function, Functions::MATCH_FUNCTION_LIST)) { - throw new RuntimeException("ORM Query: Not allowed MATCH usage."); - } - - $modePart = ' ' . $this->matchFunctionMap[$function]; - - return "MATCH (" . implode(',', $columnList) . ") AGAINST (" . $query . $modePart . ")"; - } - /** * @param array $params */ @@ -1265,11 +1209,6 @@ abstract class BaseQueryComposer implements QueryComposer /** @var int $delimiterPosition */ $delimiterPosition = strpos($attribute, ':'); $function = substr($attribute, 0, $delimiterPosition); - - if (in_array($function, Functions::MATCH_FUNCTION_LIST)) { - return $this->convertMatchExpression($entity, $attribute, $params); - } - $attribute = substr($attribute, $delimiterPosition + 1); if (str_starts_with($attribute, '(') && str_ends_with($attribute, ')')) { @@ -2530,7 +2469,7 @@ abstract class BaseQueryComposer implements QueryComposer ): ?string { if (is_int($leftKey) && is_string($value)) { - return $this->convertMatchExpression($entity, $value, $params); + return $this->convertComplexExpression($entity, $value, false, $params); } $field = $leftKey; diff --git a/application/Espo/ORM/QueryComposer/Functions.php b/application/Espo/ORM/QueryComposer/Functions.php index 7bb6d832a0..642b6a305a 100644 --- a/application/Espo/ORM/QueryComposer/Functions.php +++ b/application/Espo/ORM/QueryComposer/Functions.php @@ -130,6 +130,8 @@ class Functions 'TIMESTAMPDIFF_MINUTE', 'TIMESTAMPDIFF_SECOND', 'POSITION_IN_LIST', + 'MATCH_BOOLEAN', + 'MATCH_NATURAL_LANGUAGE', ]; public const COMPARISON_FUNCTION_LIST = [ @@ -150,10 +152,4 @@ class Functions 'DIV', 'MOD', ]; - - public const MATCH_FUNCTION_LIST = [ - 'MATCH_BOOLEAN', - 'MATCH_NATURAL_LANGUAGE', - 'MATCH_QUERY_EXPANSION', - ]; } diff --git a/tests/unit/Espo/ORM/MysqlQueryComposerTest.php b/tests/unit/Espo/ORM/MysqlQueryComposerTest.php index ba6e0e22b5..9ca0472710 100644 --- a/tests/unit/Espo/ORM/MysqlQueryComposerTest.php +++ b/tests/unit/Espo/ORM/MysqlQueryComposerTest.php @@ -1306,7 +1306,7 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase $expectedSql = "SELECT DISTINCT article.id AS `id`, article.name AS `name`, article.description AS `description` " . "FROM `article` WHERE article.deleted = 0 ". - "ORDER BY MATCH (article.name,article.description) AGAINST ('test' IN BOOLEAN MODE) DESC"; + "ORDER BY MATCH (article.name, article.description) AGAINST ('test' IN BOOLEAN MODE) DESC"; $sql = $this->query->compose($select); @@ -1326,7 +1326,7 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase $expectedSql = "SELECT DISTINCT article.id AS `id`, article.name AS `name`, article.description AS `description` " . "FROM `article` WHERE article.deleted = 0 ". - "ORDER BY MATCH (article.name,article.description) AGAINST ('test' IN BOOLEAN MODE) DESC"; + "ORDER BY MATCH (article.name, article.description) AGAINST ('test' IN BOOLEAN MODE) DESC"; $sql = $this->query->compose($select); @@ -1801,7 +1801,7 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase $expectedSql = "SELECT article.id AS `id`, article.name AS `name` FROM `article` " . - "WHERE MATCH (article.name,article.description) AGAINST " . + "WHERE MATCH (article.name, article.description) AGAINST " . "('test +hello' IN BOOLEAN MODE) AND article.id IS NOT NULL AND article.deleted = 0"; $this->assertEquals($expectedSql, $sql);