From 13beed1169bbd072a67d41d00f308ff4221b9f3d Mon Sep 17 00:00:00 2001 From: fogelito Date: Fri, 11 Oct 2024 14:18:26 +0300 Subject: [PATCH 1/8] joins --- phpunit.xml | 2 +- src/Database/Query.php | 51 ++++++++++++++++++++++++++++++++++++++++ tests/unit/QueryTest.php | 33 +++++++++++++++++++------- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index ccdaa969e..783265d80 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false"> + stopOnFailure="true"> ./tests/unit diff --git a/src/Database/Query.php b/src/Database/Query.php index 6af553415..3d35d7086 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -21,6 +21,7 @@ class Query public const TYPE_BETWEEN = 'between'; public const TYPE_STARTS_WITH = 'startsWith'; public const TYPE_ENDS_WITH = 'endsWith'; + public const TYPE_RELATION = 'relation'; public const TYPE_SELECT = 'select'; @@ -38,6 +39,11 @@ class Query public const TYPE_AND = 'and'; public const TYPE_OR = 'or'; + // Join methods + public const TYPE_INNER_JOIN = 'join'; + public const TYPE_LEFT_JOIN = 'leftJoin'; + public const TYPE_RIGHT_JOIN = 'rightJoin'; + public const TYPES = [ self::TYPE_EQUAL, self::TYPE_NOT_EQUAL, @@ -71,6 +77,7 @@ class Query protected string $method = ''; protected string $attribute = ''; protected bool $onArray = false; + protected bool $isRelation = false; /** * @var array @@ -567,6 +574,50 @@ public static function and(array $queries): self return new self(self::TYPE_AND, '', $queries); } + /** + * @param string $collection + * @param string $alias + * @param array $conditions + * @return Query + */ + public static function join(string $collection, string $alias, array $conditions = []): self + { + $value = [ + 'collection' => $collection, + 'alias' => $alias, + 'conditions' => $conditions, + ]; + + return new self(self::TYPE_INNER_JOIN, '', $value); + } + + /** + * @param string $collection + * @param string $alias + * @param array $conditions + * @return Query + */ + public static function relation(string $leftColumn, string $method, string $rightColumn): self + { + if (in_array($method, [ + self::TYPE_EQUAL, + self::TYPE_NOT_EQUAL, + self::TYPE_GREATER, + self::TYPE_GREATER_EQUAL, + self::TYPE_LESSER, + self::TYPE_LESSER_EQUAL, + ])) { + throw new QueryException('Invalid query method: ' . $method); + } + + $value = [ + 'operator' => $method, + 'rightColumn' => $rightColumn, + ]; + + return new self(self::TYPE_RELATION, $leftColumn, $value); + } + /** * Filters $queries for $types * diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index 9666ebf3a..c77818f97 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -9,13 +9,9 @@ class QueryTest extends TestCase { - public function setUp(): void - { - } + public function setUp(): void {} - public function tearDown(): void - { - } + public function tearDown(): void {} public function testCreate(): void { @@ -67,7 +63,7 @@ public function testCreate(): void $this->assertEquals('', $query->getAttribute()); $this->assertEquals([10], $query->getValues()); - $cursor = new Document(); + $cursor = new Document; $query = Query::cursorAfter($cursor); $this->assertEquals(Query::TYPE_CURSOR_AFTER, $query->getMethod()); @@ -88,7 +84,6 @@ public function testCreate(): void } /** - * @return void * @throws QueryException */ public function testParse(): void @@ -200,7 +195,7 @@ public function testParse(): void $json = Query::or([ Query::equal('actors', ['Brad Pitt']), - Query::equal('actors', ['Johnny Depp']) + Query::equal('actors', ['Johnny Depp']), ])->toString(); $query = Query::parse($json); @@ -275,4 +270,24 @@ public function testIsMethod(): void $this->assertFalse(Query::isMethod('invalid')); $this->assertFalse(Query::isMethod('lte ')); } + + /** + * @throws QueryException + */ + public function testJoins(): void + { + $query = + Query::join( + 'users', + 'u', + [ + Query::relation('u.id', Query::TYPE_EQUAL, 'd.user_id'), + Query::equal('u.id', ['usa']), + ] + ); + + $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); + $this->assertEquals('title', $query->getAttribute()); + $this->assertEquals('Iron Man', $query->getValues()[0]); + } } From 966631b05e54fca41bc9f60f50751343d8511f94 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 14 Oct 2024 19:22:11 +0300 Subject: [PATCH 2/8] query test --- composer.lock | 58 ++++++++++++++++++++-------------------- src/Database/Query.php | 9 ++++--- tests/unit/QueryTest.php | 19 ++++++++++--- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/composer.lock b/composer.lock index b98610e74..2f3e7358a 100644 --- a/composer.lock +++ b/composer.lock @@ -136,20 +136,20 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -196,7 +196,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -212,7 +212,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "utopia-php/cache", @@ -506,16 +506,16 @@ }, { "name": "laravel/pint", - "version": "v1.17.2", + "version": "v1.17.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110" + "reference": "9d77be916e145864f10788bb94531d03e1f7b482" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/e8a88130a25e3f9d4d5785e6a1afca98268ab110", - "reference": "e8a88130a25e3f9d4d5785e6a1afca98268ab110", + "url": "https://api.github.com/repos/laravel/pint/zipball/9d77be916e145864f10788bb94531d03e1f7b482", + "reference": "9d77be916e145864f10788bb94531d03e1f7b482", "shasum": "" }, "require": { @@ -526,13 +526,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.61.1", - "illuminate/view": "^10.48.18", + "friendsofphp/php-cs-fixer": "^3.64.0", + "illuminate/view": "^10.48.20", "larastan/larastan": "^2.9.8", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.0" + "pestphp/pest": "^2.35.1" }, "bin": [ "builds/pint" @@ -568,7 +568,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-08-06T15:11:54+00:00" + "time": "2024-09-03T15:00:28+00:00" }, { "name": "myclabs/deep-copy", @@ -632,16 +632,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.19.1", + "version": "v4.19.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", - "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/715f4d25e225bc47b293a8b997fe6ce99bf987d2", + "reference": "715f4d25e225bc47b293a8b997fe6ce99bf987d2", "shasum": "" }, "require": { @@ -650,7 +650,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "bin": [ "bin/php-parse" @@ -682,9 +682,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.4" }, - "time": "2024-03-17T08:10:35+00:00" + "time": "2024-09-29T15:01:53+00:00" }, { "name": "pcov/clobber", @@ -1217,16 +1217,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.20", + "version": "9.6.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "49d7820565836236411f5dc002d16dd689cde42f" + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f", - "reference": "49d7820565836236411f5dc002d16dd689cde42f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", "shasum": "" }, "require": { @@ -1241,7 +1241,7 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.31", + "phpunit/php-code-coverage": "^9.2.32", "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.4", @@ -1300,7 +1300,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.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" }, "funding": [ { @@ -1316,7 +1316,7 @@ "type": "tidelift" } ], - "time": "2024-07-10T11:45:39+00:00" + "time": "2024-09-19T10:50:18+00:00" }, { "name": "psr/container", diff --git a/src/Database/Query.php b/src/Database/Query.php index 3d35d7086..9e11b181a 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -597,9 +597,9 @@ public static function join(string $collection, string $alias, array $conditions * @param array $conditions * @return Query */ - public static function relation(string $leftColumn, string $method, string $rightColumn): self + public static function relation($leftAlias, string $leftColumn, string $method, $rightAlias, string $rightColumn): self { - if (in_array($method, [ + if (!in_array($method, [ self::TYPE_EQUAL, self::TYPE_NOT_EQUAL, self::TYPE_GREATER, @@ -611,7 +611,10 @@ public static function relation(string $leftColumn, string $method, string $righ } $value = [ - 'operator' => $method, + 'leftAlias' => $leftAlias, + //'leftColumn' => $leftColumn, // this is attribute + 'method' => $method, + 'rightAlias' => $rightAlias, 'rightColumn' => $rightColumn, ]; diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index c77818f97..c5a1f50d8 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -281,13 +281,24 @@ public function testJoins(): void 'users', 'u', [ - Query::relation('u.id', Query::TYPE_EQUAL, 'd.user_id'), + Query::relation('u', 'id', Query::TYPE_EQUAL, 'u', 'user_id'), Query::equal('u.id', ['usa']), ] ); - $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals('Iron Man', $query->getValues()[0]); + $this->assertEquals(Query::TYPE_INNER_JOIN, $query->getMethod()); + $this->assertEquals('users', $query->getValues()['collection']); + $this->assertEquals('u', $query->getValues()['alias']); + + /** + * @var $conditions array + */ + $conditions = $query->getValues()['conditions']; + $this->assertEquals(Query::TYPE_RELATION, $conditions[0]->getMethod()); + $this->assertEquals('id', $conditions[0]->getAttribute()); + $this->assertEquals('u', $conditions[0]->getValues()['leftAlias']); + $this->assertEquals(Query::TYPE_EQUAL, $conditions[0]->getValues()['method']); + $this->assertEquals('u', $conditions[0]->getValues()['rightAlias']); + $this->assertEquals('user_id', $conditions[0]->getValues()['rightColumn']); } } From 80040147bd63ca739c46fe2f15b8f31672392e82 Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 28 Oct 2024 12:55:53 +0200 Subject: [PATCH 3/8] Add constructor params --- src/Database/Query.php | 83 ++++++++++++++++++++++++++-------------- tests/unit/QueryTest.php | 42 ++++++++++++-------- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index 9e11b181a..5b2f33816 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -75,7 +75,14 @@ class Query ]; protected string $method = ''; + protected string $as = ''; + protected string $collection = ''; + protected string $function = ''; + protected string $alias = ''; protected string $attribute = ''; + protected string $aliasRight = ''; + protected string $attributeRight = ''; + protected bool $onArray = false; protected bool $isRelation = false; @@ -91,11 +98,27 @@ class Query * @param string $attribute * @param array $values */ - public function __construct(string $method, string $attribute = '', array $values = []) + protected function __construct( + string $method, + string $attribute = '', + array $values = [], + string $alias = '', + string $attributeRight = '', + string $aliasRight = '', + string $as = '', + string $collection = '', + string $function = '' + ) { $this->method = $method; + $this->alias = $alias; $this->attribute = $attribute; $this->values = $values; + $this->function = $function; + $this->aliasRight = $aliasRight; + $this->attributeRight = $attributeRight; + $this->as = $as; + $this->collection = $collection; } public function __clone(): void @@ -140,6 +163,26 @@ public function getValue(mixed $default = null): mixed return $this->values[0] ?? $default; } + public function getAlias(): string + { + return $this->alias; + } + + public function getRightAlias(): string + { + return $this->aliasRight; + } + + public function getAttributeRight(): string + { + return $this->attributeRight; + } + + public function getCollection(): string + { + return $this->collection; + } + /** * Sets method * @@ -345,9 +388,9 @@ public function toString(): string * @param array $values * @return Query */ - public static function equal(string $attribute, array $values): self + public static function equal(string $attribute, array $values, string $alias = ''): self { - return new self(self::TYPE_EQUAL, $attribute, $values); + return new self(self::TYPE_EQUAL, $attribute, $values, alias: $alias); } /** @@ -464,9 +507,9 @@ public static function select(array $attributes): self * @param string $attribute * @return Query */ - public static function orderDesc(string $attribute = ''): self + public static function orderDesc(string $attribute = '', string $alias = ''): self { - return new self(self::TYPE_ORDER_DESC, $attribute); + return new self(self::TYPE_ORDER_DESC, $attribute, alias: $alias); } /** @@ -580,15 +623,12 @@ public static function and(array $queries): self * @param array $conditions * @return Query */ - public static function join(string $collection, string $alias, array $conditions = []): self + public static function join(string $collection, string $alias, array $queries = []): self { - $value = [ - 'collection' => $collection, - 'alias' => $alias, - 'conditions' => $conditions, - ]; + //$conditions = Query::groupByType($queries)['filters']; + //$conditions = Query::groupByType($queries)['relations']; - return new self(self::TYPE_INNER_JOIN, '', $value); + return new self(self::TYPE_INNER_JOIN, '', $queries, alias: $alias, collection: $collection); } /** @@ -597,28 +637,13 @@ public static function join(string $collection, string $alias, array $conditions * @param array $conditions * @return Query */ - public static function relation($leftAlias, string $leftColumn, string $method, $rightAlias, string $rightColumn): self + public static function relation($leftAlias, string $leftColumn, string $method, string $rightAlias, string $rightColumn): self { - if (!in_array($method, [ - self::TYPE_EQUAL, - self::TYPE_NOT_EQUAL, - self::TYPE_GREATER, - self::TYPE_GREATER_EQUAL, - self::TYPE_LESSER, - self::TYPE_LESSER_EQUAL, - ])) { - throw new QueryException('Invalid query method: ' . $method); - } - $value = [ - 'leftAlias' => $leftAlias, - //'leftColumn' => $leftColumn, // this is attribute 'method' => $method, - 'rightAlias' => $rightAlias, - 'rightColumn' => $rightColumn, ]; - return new self(self::TYPE_RELATION, $leftColumn, $value); + return new self(self::TYPE_RELATION, $leftColumn, $value, alias: $leftAlias, attributeRight: $rightColumn, aliasRight: $rightAlias); } /** diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index c5a1f50d8..e7e57768d 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -15,19 +15,21 @@ public function tearDown(): void {} public function testCreate(): void { - $query = new Query(Query::TYPE_EQUAL, 'title', ['Iron Man']); + $query = Query::equal('title', ['Iron Man'], 'users'); $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); $this->assertEquals('title', $query->getAttribute()); $this->assertEquals('Iron Man', $query->getValues()[0]); + $this->assertEquals('users', $query->getAlias()); - $query = new Query(Query::TYPE_ORDER_DESC, 'score'); + $query = Query::orderDesc('score', 'users'); $this->assertEquals(Query::TYPE_ORDER_DESC, $query->getMethod()); $this->assertEquals('score', $query->getAttribute()); $this->assertEquals([], $query->getValues()); + $this->assertEquals('users', $query->getAlias()); - $query = new Query(Query::TYPE_LIMIT, values: [10]); + $query = Query::limit(10); $this->assertEquals(Query::TYPE_LIMIT, $query->getMethod()); $this->assertEquals('', $query->getAttribute()); @@ -281,24 +283,34 @@ public function testJoins(): void 'users', 'u', [ - Query::relation('u', 'id', Query::TYPE_EQUAL, 'u', 'user_id'), - Query::equal('u.id', ['usa']), + Query::relation('main', 'id', Query::TYPE_EQUAL, 'u', 'user_id'), + Query::equal('id', ['usa'], 'u'), ] ); $this->assertEquals(Query::TYPE_INNER_JOIN, $query->getMethod()); - $this->assertEquals('users', $query->getValues()['collection']); - $this->assertEquals('u', $query->getValues()['alias']); + $this->assertEquals('users', $query->getCollection()); + $this->assertEquals('u', $query->getAlias()); + $this->assertCount(2, $query->getValues()); + + /** + * @var $query0 Query + */ + $query0 = $query->getValues()[0]; + $this->assertEquals(Query::TYPE_RELATION, $query0->getMethod()); + $this->assertEquals('main', $query0->getAlias()); + $this->assertEquals('id', $query0->getAttribute()); + $this->assertEquals('u', $query0->getRightAlias()); + $this->assertEquals('user_id', $query0->getAttributeRight()); /** - * @var $conditions array + * @var $query0 Query */ - $conditions = $query->getValues()['conditions']; - $this->assertEquals(Query::TYPE_RELATION, $conditions[0]->getMethod()); - $this->assertEquals('id', $conditions[0]->getAttribute()); - $this->assertEquals('u', $conditions[0]->getValues()['leftAlias']); - $this->assertEquals(Query::TYPE_EQUAL, $conditions[0]->getValues()['method']); - $this->assertEquals('u', $conditions[0]->getValues()['rightAlias']); - $this->assertEquals('user_id', $conditions[0]->getValues()['rightColumn']); + $query1 = $query->getValues()[1]; + $this->assertEquals(Query::TYPE_EQUAL, $query1->getMethod()); + $this->assertEquals('u', $query1->getAlias()); + $this->assertEquals('id', $query1->getAttribute()); + $this->assertEquals('', $query1->getRightAlias()); + $this->assertEquals('', $query1->getAttributeRight()); } } From c7af565224e998d53f65495116a383e93c1d52dd Mon Sep 17 00:00:00 2001 From: fogelito Date: Mon, 28 Oct 2024 14:34:08 +0200 Subject: [PATCH 4/8] Join validator --- src/Database/Query.php | 15 +++++-- src/Database/Validator/Queries.php | 9 ++++- src/Database/Validator/Queries/Documents.php | 2 + src/Database/Validator/Query/Base.php | 1 + src/Database/Validator/Query/Join.php | 42 ++++++++++++++++++++ tests/e2e/Adapter/Base.php | 22 ++++++++++ tests/unit/QueryTest.php | 3 +- 7 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 src/Database/Validator/Query/Join.php diff --git a/src/Database/Query.php b/src/Database/Query.php index 5b2f33816..c8567ba34 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -40,7 +40,8 @@ class Query public const TYPE_OR = 'or'; // Join methods - public const TYPE_INNER_JOIN = 'join'; + public const TYPE_JOIN = 'join'; + public const TYPE_INNER_JOIN = 'innerJoin'; public const TYPE_LEFT_JOIN = 'leftJoin'; public const TYPE_RIGHT_JOIN = 'rightJoin'; @@ -77,6 +78,7 @@ class Query protected string $method = ''; protected string $as = ''; protected string $collection = ''; + protected string $type = ''; protected string $function = ''; protected string $alias = ''; protected string $attribute = ''; @@ -107,7 +109,8 @@ protected function __construct( string $aliasRight = '', string $as = '', string $collection = '', - string $function = '' + string $function = '', + string $type = '' ) { $this->method = $method; @@ -119,6 +122,7 @@ protected function __construct( $this->attributeRight = $attributeRight; $this->as = $as; $this->collection = $collection; + $this->type = $type; } public function __clone(): void @@ -183,6 +187,11 @@ public function getCollection(): string return $this->collection; } + public function getType(): string + { + return $this->type; + } + /** * Sets method * @@ -628,7 +637,7 @@ public static function join(string $collection, string $alias, array $queries = //$conditions = Query::groupByType($queries)['filters']; //$conditions = Query::groupByType($queries)['relations']; - return new self(self::TYPE_INNER_JOIN, '', $queries, alias: $alias, collection: $collection); + return new self(self::TYPE_JOIN, '', $queries, alias: $alias, collection: $collection, type: self::TYPE_INNER_JOIN); } /** diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 2e4aac71a..5072a262d 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -101,14 +101,21 @@ public function isValid($value): bool Query::TYPE_ENDS_WITH, Query::TYPE_AND, Query::TYPE_OR => Base::METHOD_TYPE_FILTER, + Query::TYPE_JOIN => Base::METHOD_TYPE_JOIN, default => '', }; - + var_dump('____________________________________'); $methodIsValid = false; foreach ($this->validators as $validator) { + var_dump('---'); + var_dump($method); + var_dump($methodType); + var_dump($validator->getMethodType()); + var_dump('---'); if ($validator->getMethodType() !== $methodType) { continue; } + if (!$validator->isValid($query)) { $this->message = 'Invalid query: ' . $validator->getDescription(); return false; diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php index 0d1dc2384..a150edd86 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -8,6 +8,7 @@ use Utopia\Database\Validator\IndexedQueries; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Filter; +use Utopia\Database\Validator\Query\Join; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Query\Order; @@ -56,6 +57,7 @@ public function __construct(array $attributes, array $indexes) new Filter($attributes), new Order($attributes), new Select($attributes), + new Join($attributes), ]; parent::__construct($attributes, $indexes, $validators); diff --git a/src/Database/Validator/Query/Base.php b/src/Database/Validator/Query/Base.php index a37fdd65a..6b40c37af 100644 --- a/src/Database/Validator/Query/Base.php +++ b/src/Database/Validator/Query/Base.php @@ -12,6 +12,7 @@ abstract class Base extends Validator public const METHOD_TYPE_ORDER = 'order'; public const METHOD_TYPE_FILTER = 'filter'; public const METHOD_TYPE_SELECT = 'select'; + public const METHOD_TYPE_JOIN = 'join'; protected string $message = 'Invalid query'; diff --git a/src/Database/Validator/Query/Join.php b/src/Database/Validator/Query/Join.php new file mode 100644 index 000000000..57c5be02f --- /dev/null +++ b/src/Database/Validator/Query/Join.php @@ -0,0 +1,42 @@ +getMethod(); + + if ($method === Query::TYPE_JOIN) { + if(!in_array($value->getType(), $this->types)) { + $this->message = 'Invalid join type'; + return false; + } + + return true; + } + + return false; + } + + public function getMethodType(): string + { + return self::METHOD_TYPE_JOIN; + } +} diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 72587d44a..a9bca1455 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -2251,6 +2251,28 @@ public function testListDocumentSearch(): void $this->assertEquals(1, count($documents)); } + public function testJoin() + { + $documents = static::getDatabase()->find( + 'documents', + [ + Query::join( + 'users', + 'u', + [ + Query::relation('main', 'id', Query::TYPE_EQUAL, 'u', 'user_id'), + Query::equal('id', ['usa'], 'u'), + ] + ) + ] + ); + + var_dump($documents); + + $this->assertEquals('shmuel', 'fogel'); + + } + public function testEmptyTenant(): void { if(static::getDatabase()->getAdapter()->getSharedTables()) { diff --git a/tests/unit/QueryTest.php b/tests/unit/QueryTest.php index e7e57768d..c11fe57cf 100644 --- a/tests/unit/QueryTest.php +++ b/tests/unit/QueryTest.php @@ -288,7 +288,8 @@ public function testJoins(): void ] ); - $this->assertEquals(Query::TYPE_INNER_JOIN, $query->getMethod()); + $this->assertEquals(Query::TYPE_JOIN, $query->getMethod()); + $this->assertEquals(Query::TYPE_INNER_JOIN, $query->getType()); $this->assertEquals('users', $query->getCollection()); $this->assertEquals('u', $query->getAlias()); $this->assertCount(2, $query->getValues()); From a87eed3bb6da7eefd78dbf69548140e4b8df419d Mon Sep 17 00:00:00 2001 From: fogelito Date: Tue, 29 Oct 2024 17:28:48 +0200 Subject: [PATCH 5/8] Init V2 validators --- src/Database/Database.php | 16 +- src/Database/Query.php | 6 + src/Database/Validator/Queries/Documents.php | 60 +-- src/Database/Validator/Queries/V2.php | 396 +++++++++++++++++++ src/Database/Validator/Query/Join.php | 10 +- tests/e2e/Adapter/Base.php | 6 +- 6 files changed, 452 insertions(+), 42 deletions(-) create mode 100644 src/Database/Validator/Queries/V2.php diff --git a/src/Database/Database.php b/src/Database/Database.php index 437f18881..5ab48992e 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -21,7 +21,7 @@ use Utopia\Database\Validator\Index as IndexValidator; use Utopia\Database\Validator\Permissions; use Utopia\Database\Validator\Queries\Document as DocumentValidator; -use Utopia\Database\Validator\Queries\Documents as DocumentsValidator; +use Utopia\Database\Validator\Queries\V2 as DocumentsValidator; use Utopia\Database\Validator\Structure; class Database @@ -5006,11 +5006,17 @@ public function find(string $collection, array $queries = []): array throw new DatabaseException('Collection not found'); } - $attributes = $collection->getAttribute('attributes', []); - $indexes = $collection->getAttribute('indexes', []); - if ($this->validate) { - $validator = new DocumentsValidator($attributes, $indexes); + $collections[] = $collection; + + $joins = Query::getByType($queries, [Query::TYPE_JOIN]); + var_dump($joins); + $collections = []; + foreach ($joins as $join) { + $collections[] = $this->silent(fn () => $this->getCollection($join->getCollection())); + } + + $validator = new DocumentsValidator($collections); if (!$validator->isValid($queries)) { throw new QueryException($validator->getDescription()); } diff --git a/src/Database/Query.php b/src/Database/Query.php index c8567ba34..f62386ed8 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -693,6 +693,7 @@ public static function getByType(array $queries, array $types): array public static function groupByType(array $queries): array { $filters = []; + $joins = []; $selections = []; $limit = null; $offset = null; @@ -753,6 +754,10 @@ public static function groupByType(array $queries): array $selections[] = clone $query; break; + case Query::TYPE_JOIN: + $joins[] = clone $query; + break; + default: $filters[] = clone $query; break; @@ -768,6 +773,7 @@ public static function groupByType(array $queries): array 'orderTypes' => $orderTypes, 'cursor' => $cursor, 'cursorDirection' => $cursorDirection, + 'join' => $joins, ]; } diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php index a150edd86..4b7baf3cb 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -23,43 +23,43 @@ class Documents extends IndexedQueries * @param array $indexes * @throws Exception */ - public function __construct(array $attributes, array $indexes) + public function __construct(array $collections) { - $attributes[] = new Document([ - '$id' => '$id', - 'key' => '$id', - 'type' => Database::VAR_STRING, - 'array' => false, - ]); - $attributes[] = new Document([ - '$id' => '$internalId', - 'key' => '$internalId', - 'type' => Database::VAR_STRING, - 'array' => false, - ]); - $attributes[] = new Document([ - '$id' => '$createdAt', - 'key' => '$createdAt', - 'type' => Database::VAR_DATETIME, - 'array' => false, - ]); - $attributes[] = new Document([ - '$id' => '$updatedAt', - 'key' => '$updatedAt', - 'type' => Database::VAR_DATETIME, - 'array' => false, - ]); +// $attributes[] = new Document([ +// '$id' => '$id', +// 'key' => '$id', +// 'type' => Database::VAR_STRING, +// 'array' => false, +// ]); +// $attributes[] = new Document([ +// '$id' => '$internalId', +// 'key' => '$internalId', +// 'type' => Database::VAR_STRING, +// 'array' => false, +// ]); +// $attributes[] = new Document([ +// '$id' => '$createdAt', +// 'key' => '$createdAt', +// 'type' => Database::VAR_DATETIME, +// 'array' => false, +// ]); +// $attributes[] = new Document([ +// '$id' => '$updatedAt', +// 'key' => '$updatedAt', +// 'type' => Database::VAR_DATETIME, +// 'array' => false, +// ]); $validators = [ new Limit(), new Offset(), new Cursor(), - new Filter($attributes), - new Order($attributes), - new Select($attributes), - new Join($attributes), + new Filter($collections), + new Order($collections), + new Select($collections), + new Join($collections), ]; - parent::__construct($attributes, $indexes, $validators); + parent::__construct($collections, $validators); } } diff --git a/src/Database/Validator/Queries/V2.php b/src/Database/Validator/Queries/V2.php new file mode 100644 index 000000000..64f93ec07 --- /dev/null +++ b/src/Database/Validator/Queries/V2.php @@ -0,0 +1,396 @@ + $collections + * @throws Exception + */ + public function __construct(array $collections, int $length = 0, int $maxValuesCount = 100) + { + foreach ($collections as $collection) { + $this->collections[$collection->getId()] = $collection->getArrayCopy(); + + $attributes = $collection->getAttribute('attributes', []); + foreach ($attributes as $attribute) { + // todo: Add internal id's? + $this->schema[$collection->getId()][$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); + } + } + + $this->length = $length; + $this->maxValuesCount = $maxValuesCount; + +// $attributes[] = new Document([ +// '$id' => '$id', +// 'key' => '$id', +// 'type' => Database::VAR_STRING, +// 'array' => false, +// ]); +// $attributes[] = new Document([ +// '$id' => '$internalId', +// 'key' => '$internalId', +// 'type' => Database::VAR_STRING, +// 'array' => false, +// ]); +// $attributes[] = new Document([ +// '$id' => '$createdAt', +// 'key' => '$createdAt', +// 'type' => Database::VAR_DATETIME, +// 'array' => false, +// ]); +// $attributes[] = new Document([ +// '$id' => '$updatedAt', +// 'key' => '$updatedAt', +// 'type' => Database::VAR_DATETIME, +// 'array' => false, +// ]); + +// $validators = [ +// new Limit(), +// new Offset(), +// new Cursor(), +// new Filter($collections), +// new Order($collections), +// new Select($collections), +// new Join($collections), +// ]; + } + + /** + * @param array $value + * @return bool + * @throws \Utopia\Database\Exception\Query + */ + public function isValid($value): bool + { + if (!is_array($value)) { + $this->message = 'Queries must be an array'; + return false; + } + + if ($this->length && \count($value) > $this->length) { + return false; + } + + var_dump("ininininininininininininininin"); + + foreach ($value as $query) { + if (!$query instanceof Query) { + try { + $query = Query::parse($query); + } catch (\Throwable $e) { + $this->message = 'Invalid query: ' . $e->getMessage(); + return false; + } + } + + if($query->isNested()) { + if(!self::isValid($query->getValues())) { + return false; + } + } + + $method = $query->getMethod(); + $attribute = $query->getAttribute(); + + switch ($method) { + case Query::TYPE_EQUAL: + case Query::TYPE_CONTAINS: + if ($this->isEmpty($query->getValues())) { + $this->message = \ucfirst($method) . ' queries require at least one value.'; + return false; + } + + return $this->isValidAttributeAndValues($attribute, $query->getValues(), $method); + + case Query::TYPE_NOT_EQUAL: + case Query::TYPE_LESSER: + case Query::TYPE_LESSER_EQUAL: + case Query::TYPE_GREATER: + case Query::TYPE_GREATER_EQUAL: + case Query::TYPE_SEARCH: + case Query::TYPE_STARTS_WITH: + case Query::TYPE_ENDS_WITH: + if (count($query->getValues()) != 1) { + $this->message = \ucfirst($method) . ' queries require exactly one value.'; + return false; + } + + return $this->isValidAttributeAndValues($attribute, $query->getValues(), $method); + + case Query::TYPE_BETWEEN: + if (count($query->getValues()) != 2) { + $this->message = \ucfirst($method) . ' queries require exactly two values.'; + return false; + } + + return $this->isValidAttributeAndValues($attribute, $query->getValues(), $method); + + case Query::TYPE_IS_NULL: + case Query::TYPE_IS_NOT_NULL: + return $this->isValidAttributeAndValues($attribute, $query->getValues(), $method); + + case Query::TYPE_OR: + case Query::TYPE_AND: + $filters = Query::groupByType($query->getValues())['filters']; + + if(count($query->getValues()) !== count($filters)) { + $this->message = \ucfirst($method) . ' queries can only contain filter queries'; + return false; + } + + if(count($filters) < 2) { + $this->message = \ucfirst($method) . ' queries require at least two queries'; + return false; + } + + return true; + + case Query::TYPE_RELATION: + echo "Hello TYPE_RELATION"; + break; + + default: + return false; + } + } + + return false; + } + + /** + * Get Description. + * + * Returns validator description + * + * @return string + */ + public function getDescription(): string + { + return $this->message; + } + + /** + * Is array + * + * Function will return true if object is array. + * + * @return bool + */ + public function isArray(): bool + { + return true; + } + + /** + * Get Type + * + * Returns validator type. + * + * @return string + */ + public function getType(): string + { + return self::TYPE_OBJECT; + } + + /** + * @param array $values + * @return bool + */ + protected function isEmpty(array $values): bool + { + if (count($values) === 0) { + return true; + } + + if (is_array($values[0]) && count($values[0]) === 0) { + return true; + } + + return false; + } + + /** + * @param string $attribute + * @return bool + */ + protected function isValidAttribute(string $attribute): bool + { + if (\str_contains($attribute, '.')) { + // Check for special symbol `.` + if (isset($this->schema[$attribute])) { + return true; + } + + // For relationships, just validate the top level. + // will validate each nested level during the recursive calls. + $attribute = \explode('.', $attribute)[0]; + + if (isset($this->schema[$attribute])) { + $this->message = 'Cannot query nested attribute on: ' . $attribute; + return false; + } + } + + // Search for attribute in schema + if (!isset($this->schema[$attribute])) { + $this->message = 'Attribute not found in schema: ' . $attribute; + return false; + } + + return true; + } + + /** + * @param string $attribute + * @param array $values + * @return bool + */ + protected function isValidAttributeAndValues(string $attribute, array $values, string $method): bool + { + if (!$this->isValidAttribute($attribute)) { + return false; + } + + // isset check if for special symbols "." in the attribute name + if (\str_contains($attribute, '.') && !isset($this->schema[$attribute])) { + // For relationships, just validate the top level. + // Utopia will validate each nested level during the recursive calls. + $attribute = \explode('.', $attribute)[0]; + } + + $attributeSchema = $this->schema[$attribute]; + + if (count($values) > $this->maxValuesCount) { + $this->message = 'Query on attribute has greater than ' . $this->maxValuesCount . ' values: ' . $attribute; + return false; + } + + // Extract the type of desired attribute from collection $schema + $attributeType = $attributeSchema['type']; + + foreach ($values as $value) { + + $validator = null; + + switch ($attributeType) { + case Database::VAR_STRING: + $validator = new Text(0, 0); + break; + + case Database::VAR_INTEGER: + $validator = new Integer(); + break; + + case Database::VAR_FLOAT: + $validator = new FloatValidator(); + break; + + case Database::VAR_BOOLEAN: + $validator = new Boolean(); + break; + + case Database::VAR_DATETIME: + $validator = new DatetimeValidator(); + break; + + case Database::VAR_RELATIONSHIP: + $validator = new Text(255, 0); // The query is always on uid + break; + default: + $this->message = 'Unknown Data type'; + return false; + } + + if (!$validator->isValid($value)) { + $this->message = 'Query value is invalid for attribute "' . $attribute . '"'; + return false; + } + } + + if($attributeSchema['type'] === 'relationship') { + /** + * We can not disable relationship query since we have logic that use it, + * so instead we validate against the relation type + */ + $options = $attributeSchema['options']; + + if($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { + $this->message = 'Cannot query on virtual relationship attribute'; + return false; + } + + if($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { + $this->message = 'Cannot query on virtual relationship attribute'; + return false; + } + + if($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { + $this->message = 'Cannot query on virtual relationship attribute'; + return false; + } + + if($options['relationType'] === Database::RELATION_MANY_TO_MANY) { + $this->message = 'Cannot query on virtual relationship attribute'; + return false; + } + } + + $array = $attributeSchema['array'] ?? false; + + if( + !$array && + $method === Query::TYPE_CONTAINS && + $attributeSchema['type'] !== Database::VAR_STRING + ) { + $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array or string.'; + return false; + } + + if( + $array && + !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) + ) { + $this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array.'; + return false; + } + + return true; + } +} diff --git a/src/Database/Validator/Query/Join.php b/src/Database/Validator/Query/Join.php index 57c5be02f..82e2f5543 100644 --- a/src/Database/Validator/Query/Join.php +++ b/src/Database/Validator/Query/Join.php @@ -10,22 +10,24 @@ class Join extends Base /** * Is valid. - * @param Query $value - * @return bool + * + * @param Query $value */ public function isValid($value): bool { var_dump('Validating join'); + var_dump($value); - if (!$value instanceof Query) { + if (! $value instanceof Query) { return false; } $method = $value->getMethod(); if ($method === Query::TYPE_JOIN) { - if(!in_array($value->getType(), $this->types)) { + if (! in_array($value->getType(), $this->types)) { $this->message = 'Invalid join type'; + return false; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index a9bca1455..73a613830 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -307,7 +307,9 @@ public function testVirtualRelationsAttributes(): void } catch (Exception $e) { $this->assertTrue($e instanceof RelationshipException); } - + static::getDatabase()->find('v2', [ + Query::equal('v1', ['virtual_attribute']), + ]); try { static::getDatabase()->find('v2', [ Query::equal('v1', ['virtual_attribute']), @@ -2267,8 +2269,6 @@ public function testJoin() ] ); - var_dump($documents); - $this->assertEquals('shmuel', 'fogel'); } From 93d414edc480f6221f5aa53ea594a238d2a67d83 Mon Sep 17 00:00:00 2001 From: fogelito Date: Wed, 30 Oct 2024 20:26:41 +0200 Subject: [PATCH 6/8] validate values --- src/Database/Database.php | 4 +- src/Database/Validator/Queries/V2.php | 315 +++++++++++++++----------- tests/e2e/Adapter/Base.php | 4 +- 3 files changed, 184 insertions(+), 139 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 5ab48992e..374f2505d 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -5007,11 +5007,9 @@ public function find(string $collection, array $queries = []): array } if ($this->validate) { + $collections = []; $collections[] = $collection; - $joins = Query::getByType($queries, [Query::TYPE_JOIN]); - var_dump($joins); - $collections = []; foreach ($joins as $join) { $collections[] = $this->silent(fn () => $this->getCollection($join->getCollection())); } diff --git a/src/Database/Validator/Queries/V2.php b/src/Database/Validator/Queries/V2.php index 64f93ec07..3cf67744c 100644 --- a/src/Database/Validator/Queries/V2.php +++ b/src/Database/Validator/Queries/V2.php @@ -7,8 +7,6 @@ use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; -use Utopia\Database\Validator\Queries; -use Utopia\Database\Validator\Query\Base; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Filter; use Utopia\Database\Validator\Query\Join; @@ -26,7 +24,9 @@ class V2 extends Validator { protected string $message = 'Invalid queries'; - protected array $collections = []; + //protected string $collectionId = ''; + + //protected array $collections = []; protected array $schema = []; @@ -34,20 +34,27 @@ class V2 extends Validator private int $maxValuesCount; + private array $aliases = []; + /** * Expression constructor * - * @param array $collections + * @param array $collections + * * @throws Exception */ public function __construct(array $collections, int $length = 0, int $maxValuesCount = 100) { - foreach ($collections as $collection) { - $this->collections[$collection->getId()] = $collection->getArrayCopy(); + foreach ($collections as $i => $collection) { + if($i === 0){ + $this->aliases[''] = $collection->getId(); + } + + //$this->collections[$collection->getId()] = $collection->getArrayCopy(); $attributes = $collection->getAttribute('attributes', []); foreach ($attributes as $attribute) { - // todo: Add internal id's? + // todo: internal id's? $this->schema[$collection->getId()][$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy(); } } @@ -55,51 +62,52 @@ public function __construct(array $collections, int $length = 0, int $maxValuesC $this->length = $length; $this->maxValuesCount = $maxValuesCount; -// $attributes[] = new Document([ -// '$id' => '$id', -// 'key' => '$id', -// 'type' => Database::VAR_STRING, -// 'array' => false, -// ]); -// $attributes[] = new Document([ -// '$id' => '$internalId', -// 'key' => '$internalId', -// 'type' => Database::VAR_STRING, -// 'array' => false, -// ]); -// $attributes[] = new Document([ -// '$id' => '$createdAt', -// 'key' => '$createdAt', -// 'type' => Database::VAR_DATETIME, -// 'array' => false, -// ]); -// $attributes[] = new Document([ -// '$id' => '$updatedAt', -// 'key' => '$updatedAt', -// 'type' => Database::VAR_DATETIME, -// 'array' => false, -// ]); - -// $validators = [ -// new Limit(), -// new Offset(), -// new Cursor(), -// new Filter($collections), -// new Order($collections), -// new Select($collections), -// new Join($collections), -// ]; + // $attributes[] = new Document([ + // '$id' => '$id', + // 'key' => '$id', + // 'type' => Database::VAR_STRING, + // 'array' => false, + // ]); + // $attributes[] = new Document([ + // '$id' => '$internalId', + // 'key' => '$internalId', + // 'type' => Database::VAR_STRING, + // 'array' => false, + // ]); + // $attributes[] = new Document([ + // '$id' => '$createdAt', + // 'key' => '$createdAt', + // 'type' => Database::VAR_DATETIME, + // 'array' => false, + // ]); + // $attributes[] = new Document([ + // '$id' => '$updatedAt', + // 'key' => '$updatedAt', + // 'type' => Database::VAR_DATETIME, + // 'array' => false, + // ]); + + // $validators = [ + // new Limit(), + // new Offset(), + // new Cursor(), + // new Filter($collections), + // new Order($collections), + // new Select($collections), + // new Join($collections), + // ]; } /** - * @param array $value - * @return bool + * @param array $value + * * @throws \Utopia\Database\Exception\Query */ public function isValid($value): bool { - if (!is_array($value)) { + if (! is_array($value)) { $this->message = 'Queries must be an array'; + return false; } @@ -107,7 +115,9 @@ public function isValid($value): bool return false; } - var_dump("ininininininininininininininin"); + var_dump('in isValid '); + var_dump($this->aliases); + $queries = []; foreach ($value as $query) { if (!$query instanceof Query) { @@ -115,28 +125,45 @@ public function isValid($value): bool $query = Query::parse($query); } catch (\Throwable $e) { $this->message = 'Invalid query: ' . $e->getMessage(); + return false; } } - if($query->isNested()) { - if(!self::isValid($query->getValues())) { + if($query->getMethod() === Query::TYPE_JOIN) { + $this->aliases[$query->getAlias()] = $query->getCollection(); + } + + var_dump($query); + $queries[] = $query; + } + + foreach ($queries as $query) { + if ($query->isNested()) { + if (! self::isValid($query->getValues())) { return false; } } $method = $query->getMethod(); - $attribute = $query->getAttribute(); switch ($method) { case Query::TYPE_EQUAL: case Query::TYPE_CONTAINS: if ($this->isEmpty($query->getValues())) { - $this->message = \ucfirst($method) . ' queries require at least one value.'; + $this->message = \ucfirst($method).' queries require at least one value.'; + return false; + } + + if(!$this->isAttributeExist($query->getAttribute(), $query->getAlias())){ + return false; + } + + if(!$this->isValidValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method)){ return false; } - return $this->isValidAttributeAndValues($attribute, $query->getValues(), $method); + return true; case Query::TYPE_NOT_EQUAL: case Query::TYPE_LESSER: @@ -147,42 +174,71 @@ public function isValid($value): bool case Query::TYPE_STARTS_WITH: case Query::TYPE_ENDS_WITH: if (count($query->getValues()) != 1) { - $this->message = \ucfirst($method) . ' queries require exactly one value.'; + $this->message = \ucfirst($method).' queries require exactly one value.'; + + return false; + } + + if(!$this->isAttributeExist($query->getAttribute(), $query->getAlias())){ return false; } - return $this->isValidAttributeAndValues($attribute, $query->getValues(), $method); + if(!$this->isValidValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method)){ + return false; + } + + return true; case Query::TYPE_BETWEEN: if (count($query->getValues()) != 2) { - $this->message = \ucfirst($method) . ' queries require exactly two values.'; + $this->message = \ucfirst($method).' queries require exactly two values.'; + return false; } - return $this->isValidAttributeAndValues($attribute, $query->getValues(), $method); + if(!$this->isAttributeExist($query->getAttribute(), $query->getAlias())){ + return false; + } + + if(!$this->isValidValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method)){ + return false; + } + + return true; case Query::TYPE_IS_NULL: case Query::TYPE_IS_NOT_NULL: - return $this->isValidAttributeAndValues($attribute, $query->getValues(), $method); + if(!$this->isAttributeExist($query->getAttribute(), $query->getAlias())){ + return false; + } + + if(!$this->isValidValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method)){ + return false; + } + + return true; case Query::TYPE_OR: case Query::TYPE_AND: $filters = Query::groupByType($query->getValues())['filters']; - if(count($query->getValues()) !== count($filters)) { - $this->message = \ucfirst($method) . ' queries can only contain filter queries'; + if (count($query->getValues()) !== count($filters)) { + $this->message = \ucfirst($method).' queries can only contain filter queries'; + return false; } - if(count($filters) < 2) { - $this->message = \ucfirst($method) . ' queries require at least two queries'; + if (count($filters) < 2) { + $this->message = \ucfirst($method).' queries require at least two queries'; + return false; } return true; case Query::TYPE_RELATION: - echo "Hello TYPE_RELATION"; + // Check attributes right & left + echo 'Hello TYPE_RELATION'; break; default: @@ -197,8 +253,6 @@ public function isValid($value): bool * Get Description. * * Returns validator description - * - * @return string */ public function getDescription(): string { @@ -209,8 +263,6 @@ public function getDescription(): string * Is array * * Function will return true if object is array. - * - * @return bool */ public function isArray(): bool { @@ -221,8 +273,6 @@ public function isArray(): bool * Get Type * * Returns validator type. - * - * @return string */ public function getType(): string { @@ -230,8 +280,7 @@ public function getType(): string } /** - * @param array $values - * @return bool + * @param array $values */ protected function isEmpty(array $values): bool { @@ -246,88 +295,80 @@ protected function isEmpty(array $values): bool return false; } - /** - * @param string $attribute - * @return bool - */ - protected function isValidAttribute(string $attribute): bool + protected function isAttributeExist(string $attributeId, string $alias): bool { - if (\str_contains($attribute, '.')) { - // Check for special symbol `.` - if (isset($this->schema[$attribute])) { - return true; - } - - // For relationships, just validate the top level. - // will validate each nested level during the recursive calls. - $attribute = \explode('.', $attribute)[0]; - - if (isset($this->schema[$attribute])) { - $this->message = 'Cannot query nested attribute on: ' . $attribute; - return false; - } - } + var_dump("=== isAttributeExist"); + +// if (\str_contains($attributeId, '.')) { +// // Check for special symbol `.` +// if (isset($this->schema[$attributeId])) { +// return true; +// } +// +// // For relationships, just validate the top level. +// // will validate each nested level during the recursive calls. +// $attributeId = \explode('.', $attributeId)[0]; +// +// if (isset($this->schema[$attributeId])) { +// $this->message = 'Cannot query nested attribute on: '.$attributeId; +// +// return false; +// } +// } + + $collectionId = $this->aliases[$alias]; + var_dump("=== attribute === " . $attributeId); + var_dump("=== alias === " . $alias); + var_dump("=== collectionId === " . $collectionId); + + var_dump($this->schema[$collectionId][$attributeId]); + + if (! isset($this->schema[$collectionId][$attributeId])) { + $this->message = 'Attribute not found in schema: '.$attributeId; - // Search for attribute in schema - if (!isset($this->schema[$attribute])) { - $this->message = 'Attribute not found in schema: ' . $attribute; return false; } return true; } - /** - * @param string $attribute - * @param array $values - * @return bool - */ - protected function isValidAttributeAndValues(string $attribute, array $values, string $method): bool + protected function isValidValues(string $attributeId, string $alias, array $values, string $method): bool { - if (!$this->isValidAttribute($attribute)) { - return false; - } - - // isset check if for special symbols "." in the attribute name - if (\str_contains($attribute, '.') && !isset($this->schema[$attribute])) { - // For relationships, just validate the top level. - // Utopia will validate each nested level during the recursive calls. - $attribute = \explode('.', $attribute)[0]; - } - - $attributeSchema = $this->schema[$attribute]; + var_dump("=== isValidValues"); if (count($values) > $this->maxValuesCount) { - $this->message = 'Query on attribute has greater than ' . $this->maxValuesCount . ' values: ' . $attribute; + $this->message = 'Query on attribute has greater than '.$this->maxValuesCount.' values: '.$attributeId; + return false; } - // Extract the type of desired attribute from collection $schema - $attributeType = $attributeSchema['type']; + $collectionId = $this->aliases[$alias]; + + $attribute = $this->schema[$collectionId][$attributeId]; foreach ($values as $value) { $validator = null; - switch ($attributeType) { + switch ($attribute['type']) { case Database::VAR_STRING: $validator = new Text(0, 0); break; case Database::VAR_INTEGER: - $validator = new Integer(); + $validator = new Integer; break; case Database::VAR_FLOAT: - $validator = new FloatValidator(); + $validator = new FloatValidator; break; case Database::VAR_BOOLEAN: - $validator = new Boolean(); + $validator = new Boolean; break; case Database::VAR_DATETIME: - $validator = new DatetimeValidator(); + $validator = new DatetimeValidator; break; case Database::VAR_RELATIONSHIP: @@ -335,59 +376,67 @@ protected function isValidAttributeAndValues(string $attribute, array $values, s break; default: $this->message = 'Unknown Data type'; + return false; } - if (!$validator->isValid($value)) { - $this->message = 'Query value is invalid for attribute "' . $attribute . '"'; + if (! $validator->isValid($value)) { + $this->message = 'Query value is invalid for attribute "'.$attributeId.'"'; + return false; } } - if($attributeSchema['type'] === 'relationship') { + if ($attribute['type'] === 'relationship') { /** * We can not disable relationship query since we have logic that use it, * so instead we validate against the relation type */ - $options = $attributeSchema['options']; + $options = $attribute['options']; - if($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { + if ($options['relationType'] === Database::RELATION_ONE_TO_ONE && $options['twoWay'] === false && $options['side'] === Database::RELATION_SIDE_CHILD) { $this->message = 'Cannot query on virtual relationship attribute'; + return false; } - if($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { + if ($options['relationType'] === Database::RELATION_ONE_TO_MANY && $options['side'] === Database::RELATION_SIDE_PARENT) { $this->message = 'Cannot query on virtual relationship attribute'; + return false; } - if($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { + if ($options['relationType'] === Database::RELATION_MANY_TO_ONE && $options['side'] === Database::RELATION_SIDE_CHILD) { $this->message = 'Cannot query on virtual relationship attribute'; + return false; } - if($options['relationType'] === Database::RELATION_MANY_TO_MANY) { + if ($options['relationType'] === Database::RELATION_MANY_TO_MANY) { $this->message = 'Cannot query on virtual relationship attribute'; + return false; } } - $array = $attributeSchema['array'] ?? false; + $array = $attribute['array'] ?? false; - if( - !$array && + if ( + ! $array && $method === Query::TYPE_CONTAINS && - $attributeSchema['type'] !== Database::VAR_STRING + $attribute['type'] !== Database::VAR_STRING ) { - $this->message = 'Cannot query contains on attribute "' . $attribute . '" because it is not an array or string.'; + $this->message = 'Cannot query contains on attribute "'.$attributeId.'" because it is not an array or string.'; + return false; } - if( + if ( $array && - !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) + ! in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]) ) { - $this->message = 'Cannot query '. $method .' on attribute "' . $attribute . '" because it is an array.'; + $this->message = 'Cannot query '.$method.' on attribute "'.$attributeId.'" because it is an array.'; + return false; } diff --git a/tests/e2e/Adapter/Base.php b/tests/e2e/Adapter/Base.php index 73a613830..1f40da090 100644 --- a/tests/e2e/Adapter/Base.php +++ b/tests/e2e/Adapter/Base.php @@ -307,9 +307,7 @@ public function testVirtualRelationsAttributes(): void } catch (Exception $e) { $this->assertTrue($e instanceof RelationshipException); } - static::getDatabase()->find('v2', [ - Query::equal('v1', ['virtual_attribute']), - ]); + try { static::getDatabase()->find('v2', [ Query::equal('v1', ['virtual_attribute']), From 35f73e6d291da8138f0d26951289bcf3951e1cf3 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 31 Oct 2024 10:58:32 +0200 Subject: [PATCH 7/8] Revert Documents validator --- src/Database/Validator/Queries/Documents.php | 60 ++++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/src/Database/Validator/Queries/Documents.php b/src/Database/Validator/Queries/Documents.php index 4b7baf3cb..0d1dc2384 100644 --- a/src/Database/Validator/Queries/Documents.php +++ b/src/Database/Validator/Queries/Documents.php @@ -8,7 +8,6 @@ use Utopia\Database\Validator\IndexedQueries; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\Query\Filter; -use Utopia\Database\Validator\Query\Join; use Utopia\Database\Validator\Query\Limit; use Utopia\Database\Validator\Query\Offset; use Utopia\Database\Validator\Query\Order; @@ -23,43 +22,42 @@ class Documents extends IndexedQueries * @param array $indexes * @throws Exception */ - public function __construct(array $collections) + public function __construct(array $attributes, array $indexes) { -// $attributes[] = new Document([ -// '$id' => '$id', -// 'key' => '$id', -// 'type' => Database::VAR_STRING, -// 'array' => false, -// ]); -// $attributes[] = new Document([ -// '$id' => '$internalId', -// 'key' => '$internalId', -// 'type' => Database::VAR_STRING, -// 'array' => false, -// ]); -// $attributes[] = new Document([ -// '$id' => '$createdAt', -// 'key' => '$createdAt', -// 'type' => Database::VAR_DATETIME, -// 'array' => false, -// ]); -// $attributes[] = new Document([ -// '$id' => '$updatedAt', -// 'key' => '$updatedAt', -// 'type' => Database::VAR_DATETIME, -// 'array' => false, -// ]); + $attributes[] = new Document([ + '$id' => '$id', + 'key' => '$id', + 'type' => Database::VAR_STRING, + 'array' => false, + ]); + $attributes[] = new Document([ + '$id' => '$internalId', + 'key' => '$internalId', + 'type' => Database::VAR_STRING, + 'array' => false, + ]); + $attributes[] = new Document([ + '$id' => '$createdAt', + 'key' => '$createdAt', + 'type' => Database::VAR_DATETIME, + 'array' => false, + ]); + $attributes[] = new Document([ + '$id' => '$updatedAt', + 'key' => '$updatedAt', + 'type' => Database::VAR_DATETIME, + 'array' => false, + ]); $validators = [ new Limit(), new Offset(), new Cursor(), - new Filter($collections), - new Order($collections), - new Select($collections), - new Join($collections), + new Filter($attributes), + new Order($attributes), + new Select($attributes), ]; - parent::__construct($collections, $validators); + parent::__construct($attributes, $indexes, $validators); } } From fc83d9b68329a33e4b64121ab61d59801c366a70 Mon Sep 17 00:00:00 2001 From: fogelito Date: Thu, 31 Oct 2024 16:05:18 +0200 Subject: [PATCH 8/8] Limit Offset validators --- src/Database/Validator/Queries/V2.php | 102 +++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/src/Database/Validator/Queries/V2.php b/src/Database/Validator/Queries/V2.php index 3cf67744c..259b17db1 100644 --- a/src/Database/Validator/Queries/V2.php +++ b/src/Database/Validator/Queries/V2.php @@ -18,6 +18,8 @@ use Utopia\Validator\Boolean; use Utopia\Validator\FloatValidator; use Utopia\Validator\Integer; +use Utopia\Validator\Numeric; +use Utopia\Validator\Range; use Utopia\Validator\Text; class V2 extends Validator @@ -34,6 +36,10 @@ class V2 extends Validator private int $maxValuesCount; + protected int $maxLimit; + + protected int $maxOffset; + private array $aliases = []; /** @@ -43,7 +49,7 @@ class V2 extends Validator * * @throws Exception */ - public function __construct(array $collections, int $length = 0, int $maxValuesCount = 100) + public function __construct(array $collections, int $length = 0, int $maxValuesCount = 100, int $maxLimit = PHP_INT_MAX, int $maxOffset = PHP_INT_MAX) { foreach ($collections as $i => $collection) { if($i === 0){ @@ -59,6 +65,8 @@ public function __construct(array $collections, int $length = 0, int $maxValuesC } } + $this->maxLimit = $maxLimit; + $this->maxOffset = $maxOffset; $this->length = $length; $this->maxValuesCount = $maxValuesCount; @@ -237,10 +245,18 @@ public function isValid($value): bool return true; case Query::TYPE_RELATION: - // Check attributes right & left echo 'Hello TYPE_RELATION'; break; + case Query::TYPE_LIMIT: + return $this->isValidLimit($query); + + case Query::TYPE_OFFSET: + return $this->isValidOffset($query); + + case Query::TYPE_SELECT: + return $this->isValidSelect($query); + default: return false; } @@ -442,4 +458,86 @@ protected function isValidValues(string $attributeId, string $alias, array $valu return true; } + + public function isValidLimit(Query $query): bool + { + $limit = $query->getValue(); + + $validator = new Numeric(); + if (!$validator->isValid($limit)) { + $this->message = 'Invalid limit: ' . $validator->getDescription(); + return false; + } + + $validator = new Range(1, $this->maxLimit); + if (!$validator->isValid($limit)) { + $this->message = 'Invalid limit: ' . $validator->getDescription(); + return false; + } + + return true; + } + + public function isValidOffset(Query $query): bool + { + $offset = $query->getValue(); + + $validator = new Numeric(); + if (!$validator->isValid($offset)) { + $this->message = 'Invalid limit: ' . $validator->getDescription(); + return false; + } + + $validator = new Range(0, $this->maxOffset); + if (!$validator->isValid($offset)) { + $this->message = 'Invalid offset: ' . $validator->getDescription(); + return false; + } + + return true; + } + + public function isValidSelect(Query $query): bool + { + $internalKeys = \array_map( + fn ($attr) => $attr['$id'], + Database::INTERNAL_ATTRIBUTES + ); + + foreach ($query->getValues() as $attribute) { + + if(is_string()){ + + } + else if($this->isArray()){ + + } + + if($this->isAttributeExist()){ + + } + +// if (\str_contains($attribute, '.')) { +// //special symbols with `dots` +// if (isset($this->schema[$attribute])) { +// continue; +// } +// +// // For relationships, just validate the top level. +// // Will validate each nested level during the recursive calls. +// $attribute = \explode('.', $attribute)[0]; +// } + + if (\in_array($attribute, $internalKeys)) { + continue; + } + + if (!isset($this->schema[$attribute]) && $attribute !== '*') { + $this->message = 'Attribute not found in schema: ' . $attribute; + return false; + } + } + return true; + } + }