diff --git a/application/Espo/ORM/Query/Part/Where/Comparison.php b/application/Espo/ORM/Query/Part/Where/Comparison.php index 564ecebd43..439f93ecd4 100644 --- a/application/Espo/ORM/Query/Part/Where/Comparison.php +++ b/application/Espo/ORM/Query/Part/Where/Comparison.php @@ -52,6 +52,16 @@ class Comparison implements WhereItem private const OPERATOR_NOT_LIKE = '!*'; private const OPERATOR_IN_SUB_QUERY = '=s'; private const OPERATOR_NOT_IN_SUB_QUERY = '!=s'; + private const OPERATOR_NOT_EQUAL_ANY = '!=any'; + private const OPERATOR_GREATER_ANY = '>any'; + private const OPERATOR_GREATER_OR_EQUAL_ANY = '>=any'; + private const OPERATOR_LESS_ANY = ' ANY' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function greaterAny(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_GREATER_ANY, $argument, $subQuery); + } + + /** + * Create '< ANY' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function lessAny(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_LESS_ANY, $argument, $subQuery); + } + + /** + * Create '>= ANY' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function greaterOrEqualAny(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_GREATER_OR_EQUAL_ANY, $argument, $subQuery); + } + + /** + * Create '<= ANY' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function lessOrEqualAny(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_LESS_OR_EQUAL_ANY, $argument, $subQuery); + } + + /** + * Create '= ALL' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function equalAll(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_EQUAL_ALL, $argument, $subQuery); + } + + /** + * Create '> ALL' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function greaterAll(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_GREATER_ALL, $argument, $subQuery); + } + + /** + * Create '< ALL' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function lessAll(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_LESS_ALL, $argument, $subQuery); + } + + /** + * Create '>= ALL' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function greaterOrEqualAll(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_GREATER_OR_EQUAL_ALL, $argument, $subQuery); + } + + /** + * Create '<= ALL' comparison. + * + * @param Expression $argument An expression. + * @param Select $subQuery A sub-query. + * @return self + */ + public static function lessOrEqualAll(Expression $argument, Select $subQuery): self + { + return self::createComparison(self::OPERATOR_LESS_OR_EQUAL_ALL, $argument, $subQuery); + } + private static function createComparison( string $operator, Expression|string $argument1, diff --git a/application/Espo/ORM/QueryComposer/BaseQueryComposer.php b/application/Espo/ORM/QueryComposer/BaseQueryComposer.php index 2ca5195b80..8ca5cfd769 100644 --- a/application/Espo/ORM/QueryComposer/BaseQueryComposer.php +++ b/application/Espo/ORM/QueryComposer/BaseQueryComposer.php @@ -108,6 +108,18 @@ abstract class BaseQueryComposer implements QueryComposer '>', '<', '=', + '>=any', + '<=any', + '>any', + '=all', + '<=all', + '>all', + ' */ @@ -117,6 +129,18 @@ abstract class BaseQueryComposer implements QueryComposer '!=' => '<>', '!*' => 'NOT LIKE', '*' => 'LIKE', + '>=any' => '>= ANY', + '<=any' => '<= ANY', + '>any' => '> ANY', + ' '< ANY', + '!=any' => '<> ANY', + '=any' => '= ANY', + '>=all' => '>= ALL', + '<=all' => '<= ALL', + '>all' => '> ALL', + ' '< ALL', + '!=all' => '<> ALL', + '=all' => '= ALL', ]; /** @var array */ @@ -2424,13 +2448,13 @@ abstract class BaseQueryComposer implements QueryComposer } if ($field === self::EXISTS_OPERATOR) { - if ($value instanceof Select) { - $subQueryPart = $this->composeSelect($value); - } - else { + if (!$value instanceof Select) { throw new RuntimeException("Bad EXISTS usage in where-clause."); + } + $subQueryPart = $this->composeSelect($value); + return "EXISTS ({$subQueryPart})"; } @@ -2482,7 +2506,6 @@ abstract class BaseQueryComposer implements QueryComposer return $this->quote(false); } - // @todo Operators (s, <=s, =>s) producing 'operator ANY (sub-query)'. if ($operatorOrm === '=s' || $operatorOrm === '!=s') { if ($value instanceof Select) { $subSql = $this->composeSelect($value); @@ -2525,6 +2548,10 @@ abstract class BaseQueryComposer implements QueryComposer return "{$leftPart} {$operator} ({$subQueryPart})"; } + if (str_ends_with($operatorOrm, 'any') || str_ends_with($operatorOrm, 'all')) { + throw new RuntimeException("ANY/ALL operators can be used only with sub-query."); + } + if ($value instanceof Expression) { $isNotValue = true; diff --git a/application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php b/application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php index afe15418ea..402ced13c3 100644 --- a/application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php +++ b/application/Espo/ORM/QueryComposer/PostgresqlQueryComposer.php @@ -55,6 +55,18 @@ class PostgresqlQueryComposer extends BaseQueryComposer '!=' => '<>', '!*' => 'NOT ILIKE', '*' => 'ILIKE', + '>=any' => '>= ANY', + '<=any' => '<= ANY', + '>any' => '> ANY', + ' '< ANY', + '!=any' => '<> ANY', + '=any' => '= ANY', + '>=all' => '>= ALL', + '<=all' => '<= ALL', + '>all' => '> ALL', + ' '< ALL', + '!=all' => '<> ALL', + '=all' => '= ALL', ]; /** @var array */ diff --git a/tests/unit/Espo/ORM/MysqlQueryComposerTest.php b/tests/unit/Espo/ORM/MysqlQueryComposerTest.php index 64d36afeb6..d245bf2326 100644 --- a/tests/unit/Espo/ORM/MysqlQueryComposerTest.php +++ b/tests/unit/Espo/ORM/MysqlQueryComposerTest.php @@ -1210,6 +1210,58 @@ class MysqlQueryComposerTest extends \PHPUnit\Framework\TestCase $this->assertEquals($expectedSql, $sql); } + public function testSelectWithSubquery7() + { + $subQuery = SelectBuilder::create() + ->from('Post') + ->select('id') + ->where(['name' => 'test']) + ->build(); + + $sql = $this->query->composeSelect( + SelectBuilder::create() + ->from('Post') + ->select('id') + ->where( + Comparison::greaterOrEqualAny(Expression::value('1'), $subQuery) + ) + ->build() + ); + + $expectedSql = + "SELECT post.id AS `id` FROM `post` ". + "WHERE '1' >= ANY (SELECT post.id AS `id` FROM `post` ". + "WHERE post.name = 'test' AND post.deleted = 0) AND post.deleted = 0"; + + $this->assertEquals($expectedSql, $sql); + } + + public function testSelectWithSubquery8() + { + $subQuery = SelectBuilder::create() + ->from('Post') + ->select('id') + ->where(['name' => 'test']) + ->build(); + + $sql = $this->query->composeSelect( + SelectBuilder::create() + ->from('Post') + ->select('id') + ->where( + Comparison::greaterOrEqualAll(Expression::value('1'), $subQuery) + ) + ->build() + ); + + $expectedSql = + "SELECT post.id AS `id` FROM `post` ". + "WHERE '1' >= ALL (SELECT post.id AS `id` FROM `post` ". + "WHERE post.name = 'test' AND post.deleted = 0) AND post.deleted = 0"; + + $this->assertEquals($expectedSql, $sql); + } + public function testSelectExists1(): void { $expectedSql =