diff --git a/composer.lock b/composer.lock index 8f3d208b3..e55fd9b4f 100644 --- a/composer.lock +++ b/composer.lock @@ -513,16 +513,16 @@ }, { "name": "laravel/pint", - "version": "v1.13.5", + "version": "v1.13.6", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "df105cf8ce7a8f0b8a9425ff45cd281a5448e423" + "reference": "3e3d2ab01c7d8b484c18e6100ecf53639c744fa7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/df105cf8ce7a8f0b8a9425ff45cd281a5448e423", - "reference": "df105cf8ce7a8f0b8a9425ff45cd281a5448e423", + "url": "https://api.github.com/repos/laravel/pint/zipball/3e3d2ab01c7d8b484c18e6100ecf53639c744fa7", + "reference": "3e3d2ab01c7d8b484c18e6100ecf53639c744fa7", "shasum": "" }, "require": { @@ -533,13 +533,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.34.1", - "illuminate/view": "^10.26.2", - "laravel-zero/framework": "^10.1.2", + "friendsofphp/php-cs-fixer": "^3.38.0", + "illuminate/view": "^10.30.1", + "laravel-zero/framework": "^10.3.0", "mockery/mockery": "^1.6.6", "nunomaduro/larastan": "^2.6.4", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.20.0" + "pestphp/pest": "^2.24.2" }, "bin": [ "builds/pint" @@ -575,7 +575,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2023-10-26T09:26:10+00:00" + "time": "2023-11-07T17:59:57+00:00" }, { "name": "myclabs/deep-copy", @@ -839,16 +839,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.40", + "version": "1.10.47", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d" + "reference": "84dbb33b520ea28b6cf5676a3941f4bae1c1ff39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/93c84b5bf7669920d823631e39904d69b9c7dc5d", - "reference": "93c84b5bf7669920d823631e39904d69b9c7dc5d", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/84dbb33b520ea28b6cf5676a3941f4bae1c1ff39", + "reference": "84dbb33b520ea28b6cf5676a3941f4bae1c1ff39", "shasum": "" }, "require": { @@ -897,7 +897,7 @@ "type": "tidelift" } ], - "time": "2023-10-30T14:48:31+00:00" + "time": "2023-12-01T15:19:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1220,16 +1220,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.13", + "version": "9.6.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be" + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f3d767f7f9e191eab4189abe41ab37797e30b1be", - "reference": "f3d767f7f9e191eab4189abe41ab37797e30b1be", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1", + "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1", "shasum": "" }, "require": { @@ -1303,7 +1303,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.13" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.15" }, "funding": [ { @@ -1319,7 +1319,7 @@ "type": "tidelift" } ], - "time": "2023-09-19T05:39:22+00:00" + "time": "2023-12-01T16:55:19+00:00" }, { "name": "psr/container", @@ -2428,7 +2428,7 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.3.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", @@ -2475,7 +2475,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" }, "funding": [ { @@ -2495,16 +2495,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", + "reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "shasum": "" }, "require": { @@ -2533,7 +2533,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + "source": "https://github.com/theseer/tokenizer/tree/1.2.2" }, "funding": [ { @@ -2541,7 +2541,7 @@ "type": "github" } ], - "time": "2021-07-28T10:34:58+00:00" + "time": "2023-11-20T00:12:19+00:00" }, { "name": "utopia-php/cli", diff --git a/docker-compose.yml b/docker-compose.yml index af5a18be8..672e3a95d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: - ./src:/usr/src/code/src - ./tests:/usr/src/code/tests - ./phpunit.xml:/usr/src/code/phpunit.xml + #- ./vendor/utopia-php/mongo:/usr/src/code/vendor/utopia-php/mongo ports: - "8708:8708" diff --git a/phpunit.xml b/phpunit.xml index 31b947dd6..3833748e0 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="true"> ./tests/ diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 4ee36bafe..904a196dd 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -1471,14 +1471,11 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } } - foreach ($queries as $query) { - if ($query->getMethod() === Query::TYPE_SELECT) { - continue; - } - $where[] = $this->getSQLCondition($query); + $conditions = $this->getSQLConditions($queries); + if(!empty($conditions)) { + $where[] = $conditions; } - if (Authorization::$status) { $where[] = $this->getSQLPermissionsCondition($name, $roles); } @@ -1588,8 +1585,9 @@ public function count(string $collection, array $queries = [], ?int $max = null) $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; - foreach ($queries as $query) { - $where[] = $this->getSQLCondition($query); + $conditions = $this->getSQLConditions($queries); + if(!empty($conditions)) { + $where[] = $conditions; } if (Authorization::$status) { @@ -1743,7 +1741,7 @@ protected function getAttributeProjection(array $selections, string $prefix = '' return \implode(', ', $selections); } - /* + /** * Get SQL Condition * * @param Query $query @@ -1764,6 +1762,17 @@ protected function getSQLCondition(Query $query): string $placeholder = $this->getSQLPlaceholder($query); switch ($query->getMethod()) { + case Query::TYPE_OR: + case Query::TYPE_AND: + $conditions = []; + /* @var $q Query */ + foreach ($query->getValue() as $q) { + $conditions[] = $this->getSQLCondition($q); + } + + $method = strtoupper($query->getMethod()); + return empty($conditions) ? '' : ' '. $method .' (' . implode(' AND ', $conditions) . ')'; + case Query::TYPE_SEARCH: return "MATCH(table_main.{$attribute}) AGAINST (:{$placeholder}_0 IN BOOLEAN MODE)"; @@ -1779,8 +1788,7 @@ protected function getSQLCondition(Query $query): string foreach ($query->getValues() as $key => $value) { $conditions[] = $attribute . ' ' . $this->getSQLOperator($query->getMethod()) . ' :' . $placeholder . '_' . $key; } - $condition = implode(' OR ', $conditions); - return empty($condition) ? '' : '(' . $condition . ')'; + return empty($conditions) ? '' : '(' . implode(' OR ', $conditions) . ')'; } } diff --git a/src/Database/Adapter/Mongo.php b/src/Database/Adapter/Mongo.php index 451913461..bae2cb661 100644 --- a/src/Database/Adapter/Mongo.php +++ b/src/Database/Adapter/Mongo.php @@ -977,7 +977,7 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, $orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; } - $filter_ext = [ + $cursorFilters = [ [ $attribute => [ $this->getQueryOperator($orderOperator) => $cursor[$attribute] @@ -988,18 +988,16 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, '_id' => [ $this->getQueryOperator($orderOperatorInternalId) => new ObjectId($cursor['$internalId']) ] - ], ]; $filters = [ - '$and' => [$filters, ['$or' => $filter_ext]] + '$and' => [$filters, ['$or' => $cursorFilters]] ]; } - $filters = $this->recursiveReplace($filters, '$', '_', $this->operators); + $filters = $this->replaceInternalIdsKeys($filters, '$', '_', $this->operators); $filters = $this->timeFilter($filters); - /** * @var array */ @@ -1114,7 +1112,7 @@ private function toMongoDatetime(string $dt): UTCDateTime * @param array $exclude * @return array */ - private function recursiveReplace(array $array, string $from, string $to, array $exclude = []): array + private function replaceInternalIdsKeys(array $array, string $from, string $to, array $exclude = []): array { $result = []; @@ -1124,7 +1122,7 @@ private function recursiveReplace(array $array, string $from, string $to, array } $result[$key] = is_array($value) - ? $this->recursiveReplace($value, $from, $to, $exclude) + ? $this->replaceInternalIdsKeys($value, $from, $to, $exclude) : $value; } @@ -1296,70 +1294,82 @@ protected function replaceChars(string $from, string $to, array $array): array } /** - * Build mongo filters from array of $queries - * * @param array $queries - * - * @return array + * @param string $separator + * @return array * @throws Exception */ - protected function buildFilters(array $queries): array + protected function buildFilters(array $queries, string $separator = '$and'): array { $filters = []; - + $queries = Query::groupByType($queries)['filters']; foreach ($queries as $query) { - if ($query->getMethod() === Query::TYPE_SELECT) { - continue; + /* @var $query Query */ + if($query->isNested()) { + $operator = $this->getQueryOperator($query->getMethod()); + $filters[$separator][] = $this->buildFilters($query->getValues(), $operator); + } else { + $filters[$separator][] = $this->buildFilter($query); } + } - if ($query->getAttribute() === '$id') { - $query->setAttribute('_uid'); - } elseif ($query->getAttribute() === '$internalId') { - $query->setAttribute('_id'); - $values = $query->getValues(); - foreach ($values as &$value) { - $value = new ObjectId($value); - } - $query->setValues($values); - } elseif ($query->getAttribute() === '$createdAt') { - $query->setAttribute('_createdAt'); - } elseif ($query->getAttribute() === '$updatedAt') { - $query->setAttribute('_updatedAt'); - } + return $filters; + } - $attribute = $query->getAttribute(); - $operator = $this->getQueryOperator($query->getMethod()); - - unset($value); - - $value = match ($query->getMethod()) { - Query::TYPE_IS_NULL, - Query::TYPE_IS_NOT_NULL => null, - default => $this->getQueryValue( - $query->getMethod(), - count($query->getValues()) > 1 - ? $query->getValues() - : $query->getValues()[0] - ), - }; - - if ($operator == '$eq' && \is_array($value)) { - $filters[$attribute]['$in'] = $value; - } elseif ($operator == '$ne' && \is_array($value)) { - $filters[$attribute]['$nin'] = $value; - } elseif ($operator == '$in') { - $filters[$attribute]['$in'] = $query->getValues(); - } elseif ($operator == '$search') { - $filters['$text'][$operator] = $value; - } elseif ($operator === Query::TYPE_BETWEEN) { - $filters[$attribute]['$lte'] = $value[1]; - $filters[$attribute]['$gte'] = $value[0]; - } else { - $filters[$attribute][$operator] = $value; + /** + * @param Query $query + * @return array + * @throws Exception + */ + protected function buildFilter(Query $query): array + { + if ($query->getAttribute() === '$id') { + $query->setAttribute('_uid'); + } elseif ($query->getAttribute() === '$internalId') { + $query->setAttribute('_id'); + $values = $query->getValues(); + foreach ($values as $k => $v) { + $values[$k] = new ObjectId($v); } + $query->setValues($values); + } elseif ($query->getAttribute() === '$createdAt') { + $query->setAttribute('_createdAt'); + } elseif ($query->getAttribute() === '$updatedAt') { + $query->setAttribute('_updatedAt'); } - return $filters; + $attribute = $query->getAttribute(); + $operator = $this->getQueryOperator($query->getMethod()); + + $value = match ($query->getMethod()) { + Query::TYPE_IS_NULL, + Query::TYPE_IS_NOT_NULL => null, + default => $this->getQueryValue( + $query->getMethod(), + count($query->getValues()) > 1 + ? $query->getValues() + : $query->getValues()[0] + ), + }; + + $filter = []; + + if ($operator == '$eq' && \is_array($value)) { + $filter[$attribute]['$in'] = $value; + } elseif ($operator == '$ne' && \is_array($value)) { + $filter[$attribute]['$nin'] = $value; + } elseif ($operator == '$in') { + $filter[$attribute]['$in'] = $query->getValues(); + } elseif ($operator == '$search') { + $filter['$text'][$operator] = $value; + } elseif ($operator === Query::TYPE_BETWEEN) { + $filter[$attribute]['$lte'] = $value[1]; + $filter[$attribute]['$gte'] = $value[0]; + } else { + $filter[$attribute][$operator] = $value; + } + + return $filter; } /** @@ -1386,6 +1396,8 @@ protected function getQueryOperator(string $operator): string Query::TYPE_BETWEEN => 'between', Query::TYPE_STARTS_WITH, Query::TYPE_ENDS_WITH => '$regex', + Query::TYPE_OR => '$or', + Query::TYPE_AND => '$and', default => throw new DatabaseException('Unknown operator:' . $operator . '. Must be one of ' . Query::TYPE_EQUAL . ', ' . Query::TYPE_NOT_EQUAL . ', ' . Query::TYPE_LESSER . ', ' . Query::TYPE_LESSER_EQUAL . ', ' . Query::TYPE_GREATER . ', ' . Query::TYPE_GREATER_EQUAL . ', ' . Query::TYPE_IS_NULL . ', ' . Query::TYPE_IS_NOT_NULL . ', ' . Query::TYPE_BETWEEN . ', ' . Query::TYPE_CONTAINS . ', ' . Query::TYPE_SEARCH . ', ' . Query::TYPE_SELECT), }; } diff --git a/src/Database/Adapter/Postgres.php b/src/Database/Adapter/Postgres.php index d065b9850..7b5bea1b9 100644 --- a/src/Database/Adapter/Postgres.php +++ b/src/Database/Adapter/Postgres.php @@ -1461,14 +1461,11 @@ public function find(string $collection, array $queries = [], ?int $limit = 25, } } - foreach ($queries as $query) { - if ($query->getMethod() === Query::TYPE_SELECT) { - continue; - } - $where[] = $this->getSQLCondition($query); + $conditions = $this->getSQLConditions($queries); + if(!empty($conditions)) { + $where[] = $conditions; } - if (Authorization::$status) { $where[] = $this->getSQLPermissionsCondition($name, $roles); } @@ -1578,8 +1575,9 @@ public function count(string $collection, array $queries = [], ?int $max = null) $where = []; $limit = \is_null($max) ? '' : 'LIMIT :max'; - foreach ($queries as $query) { - $where[] = $this->getSQLCondition($query); + $conditions = $this->getSQLConditions($queries); + if(!empty($conditions)) { + $where[] = $conditions; } if (Authorization::$status) { diff --git a/src/Database/Adapter/SQL.php b/src/Database/Adapter/SQL.php index 344b74b72..51faae6d4 100644 --- a/src/Database/Adapter/SQL.php +++ b/src/Database/Adapter/SQL.php @@ -690,6 +690,13 @@ protected function bindConditionValue(mixed $stmt, Query $query): void return; } + if($query->isNested()) { + foreach ($query->getValues() as $value) { + $this->bindConditionValue($stmt, $value); + } + return; + } + foreach ($query->getValues() as $key => $value) { $value = match ($query->getMethod()) { Query::TYPE_STARTS_WITH => $this->escapeWildcards($value) . '%', @@ -911,4 +918,38 @@ public function getMaxIndexLength(): int { return 768; } + + /** + * @param Query $query + * @return string + * @throws Exception + */ + abstract protected function getSQLCondition(Query $query): string; + + /** + * @param array $queries + * @param string $separator + * @return string + * @throws Exception + */ + public function getSQLConditions(array $queries = [], string $separator = 'and'): string + { + $conditions = []; + foreach ($queries as $query) { + + if ($query->getMethod() === Query::TYPE_SELECT) { + continue; + } + + /* @var $query Query */ + if($query->isNested()) { + $conditions[] = $this->getSQLConditions($query->getValues(), $query->getMethod()); + } else { + $conditions[] = $this->getSQLCondition($query); + } + } + + $tmp = implode(' '. $separator .' ', $conditions); + return empty($tmp) ? '' : '(' . $tmp . ')'; + } } diff --git a/src/Database/Database.php b/src/Database/Database.php index d6b741d55..6f0675abd 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -4414,6 +4414,8 @@ public function find(string $collection, array $queries = []): array } } + unset($query); // It is used previously as reference + // Remove internal attributes which are not queried foreach ($queries as $query) { if ($query->getMethod() === Query::TYPE_SELECT) { diff --git a/src/Database/Query.php b/src/Database/Query.php index f2ed42416..6b527e5d9 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -22,6 +22,8 @@ class Query public const TYPE_ENDS_WITH = 'endsWith'; public const TYPE_SELECT = 'select'; + public const TYPE_OR = 'or'; + public const TYPE_AND = 'and'; // Order methods public const TYPE_ORDERDESC = 'orderDesc'; @@ -178,6 +180,8 @@ public static function isMethod(string $value): bool self::TYPE_BETWEEN, self::TYPE_STARTS_WITH, self::TYPE_ENDS_WITH, + self::TYPE_OR, + self::TYPE_AND, self::TYPE_SELECT => true, default => false, }; @@ -694,6 +698,24 @@ public static function endsWith(string $attribute, string $value): self return new self(self::TYPE_ENDS_WITH, $attribute, [$value]); } + /** + * @param array $queries + * @return Query + */ + public static function or(array $queries): self + { + return new self(self::TYPE_OR, '', $queries); + } + + /** + * @param array $queries + * @return Query + */ + public static function and(array $queries): self + { + return new self(self::TYPE_AND, '', $queries); + } + /** * Filters $queries for $types * @@ -828,4 +850,20 @@ public static function parseQueries(array $queries): array return $parsed; } + + /** + * Is isNested + * + * Function will return true if nested method + * + * @return bool + */ + public function isNested(): bool + { + if(in_array($this->getMethod(), [self::TYPE_OR, self::TYPE_AND])) { + return true; + } + + return false; + } } diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index f184b86bb..4787fa4bb 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -71,6 +71,12 @@ public function isValid($value): bool } } + if($query->isNested()) { + if(!self::isValid($query->getValues())) { + return false; + } + } + $method = $query->getMethod(); $methodType = match ($method) { Query::TYPE_SELECT => Base::METHOD_TYPE_SELECT, @@ -92,7 +98,9 @@ public function isValid($value): bool Query::TYPE_BETWEEN, Query::TYPE_STARTS_WITH, Query::TYPE_CONTAINS, - Query::TYPE_ENDS_WITH => Base::METHOD_TYPE_FILTER, + Query::TYPE_ENDS_WITH, + Query::TYPE_AND, + Query::TYPE_OR => Base::METHOD_TYPE_FILTER, default => '', }; diff --git a/src/Database/Validator/Query/Filter.php b/src/Database/Validator/Query/Filter.php index 9eab6b28e..c38995b15 100644 --- a/src/Database/Validator/Query/Filter.php +++ b/src/Database/Validator/Query/Filter.php @@ -172,6 +172,22 @@ public function isValid($value): bool case Query::TYPE_IS_NOT_NULL: return $this->isValidAttributeAndValues($attribute, $value->getValues()); + case Query::TYPE_OR: + case Query::TYPE_AND: + $filters = Query::groupByType($value->getValues())['filters']; + + if(count($value->getValues()) !== count($filters)) { + $this->message = \ucfirst($method) . ' queries requires only filters'; + return false; + } + + if(count($filters) < 2) { + $this->message = \ucfirst($method) . ' queries require at least two queries'; + return false; + } + + return true; + default: return false; } diff --git a/tests/Database/Base.php b/tests/Database/Base.php index f38f70b25..56b122cb3 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -2854,6 +2854,111 @@ public function testFindEdgeCases(Document $document): void } } + public function testOrSingleQuery(): void + { + try { + static::getDatabase()->find('movies', [ + Query::or([ + Query::equal('active', [true]) + ]) + ]); + $this->fail('Failed to throw exception'); + } catch(Exception $e) { + $this->assertEquals('Invalid query: Or queries require at least two queries', $e->getMessage()); + } + } + + public function testOrMultipleQueries(): void + { + $queries = [ + Query::or([ + Query::equal('active', [true]), + Query::equal('name', ['Frozen II']) + ]) + ]; + $this->assertCount(4, static::getDatabase()->find('movies', $queries)); + $this->assertEquals(4, static::getDatabase()->count('movies', $queries)); + + $queries = [ + Query::equal('active', [true]), + Query::or([ + Query::equal('name', ['Frozen']), + Query::equal('name', ['Frozen II']), + Query::equal('director', ['Joe Johnston']) + ]) + ]; + + $this->assertCount(3, static::getDatabase()->find('movies', $queries)); + $this->assertEquals(3, static::getDatabase()->count('movies', $queries)); + } + + public function testOrNested(): void + { + $queries = [ + Query::select(['director']), + Query::equal('director', ['Joe Johnston']), + Query::or([ + Query::equal('name', ['Frozen']), + Query::or([ + Query::equal('active', [true]), + Query::equal('active', [false]), + ]) + ]) + ]; + + $documents = static::getDatabase()->find('movies', $queries); + $this->assertCount(1, $documents); + $this->assertArrayNotHasKey('name', $documents[0]); + + $count = static::getDatabase()->count('movies', $queries); + $this->assertEquals(1, $count); + } + + public function testAndSingleQuery(): void + { + try { + static::getDatabase()->find('movies', [ + Query::and([ + Query::equal('active', [true]) + ]) + ]); + $this->fail('Failed to throw exception'); + } catch(Exception $e) { + $this->assertEquals('Invalid query: And queries require at least two queries', $e->getMessage()); + } + } + + public function testAndMultipleQueries(): void + { + $queries = [ + Query::and([ + Query::equal('active', [true]), + Query::equal('name', ['Frozen II']) + ]) + ]; + $this->assertCount(1, static::getDatabase()->find('movies', $queries)); + $this->assertEquals(1, static::getDatabase()->count('movies', $queries)); + } + + public function testAndNested(): void + { + $queries = [ + Query::or([ + Query::equal('active', [false]), + Query::and([ + Query::equal('active', [true]), + Query::equal('name', ['Frozen']), + ]) + ]) + ]; + + $documents = static::getDatabase()->find('movies', $queries); + $this->assertCount(3, $documents); + + $count = static::getDatabase()->count('movies', $queries); + $this->assertEquals(3, $count); + } + /** * @depends testFind */ diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index a7e497959..9ed5cb1eb 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -453,6 +453,7 @@ public function testisMethod(): void $this->assertTrue(Query::isMethod('isNotNull')); $this->assertTrue(Query::isMethod('between')); $this->assertTrue(Query::isMethod('select')); + $this->assertTrue(Query::isMethod('or')); $this->assertTrue(Query::isMethod(Query::TYPE_EQUAL)); $this->assertTrue(Query::isMethod(Query::TYPE_NOT_EQUAL)); @@ -472,6 +473,7 @@ public function testisMethod(): void $this->assertTrue(Query::isMethod(QUERY::TYPE_IS_NOT_NULL)); $this->assertTrue(Query::isMethod(QUERY::TYPE_BETWEEN)); $this->assertTrue(Query::isMethod(QUERY::TYPE_SELECT)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_OR)); /* Tests for aliases if we enable them: diff --git a/tests/Database/Validator/QueryTest.php b/tests/Database/Validator/QueryTest.php index 76f78c1a5..f05175be8 100644 --- a/tests/Database/Validator/QueryTest.php +++ b/tests/Database/Validator/QueryTest.php @@ -311,4 +311,45 @@ public function testQueryEmpty(): void $response = $validator->isValid([Query::isNull('price')]); $this->assertEquals(true, $response); } + + /** + * @throws Exception + */ + public function testOrQuery(): void + { + $validator = new Documents($this->attributes, []); + + $this->assertFalse($validator->isValid( + [Query::or( + [Query::equal('title', [''])] + )] + )); + + $this->assertEquals('Invalid query: Or queries require at least two queries', $validator->getDescription()); + + $this->assertFalse($validator->isValid( + [ + Query::or( + [ + Query::equal('price', [0]), + Query::equal('not_found', ['']) + ] + )] + )); + + $this->assertEquals('Invalid query: Attribute not found in schema: not_found', $validator->getDescription()); + + $this->assertFalse($validator->isValid( + [ + Query::equal('price', [10]), + Query::or( + [ + Query::select(['price']), + Query::limit(1) + ] + )] + )); + + $this->assertEquals('Invalid query: Or queries requires only filters', $validator->getDescription()); + } }