From 4b0c0e2aba95a9e18d01f766af9ebaf6b3d06214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 Jul 2022 09:07:47 +0000 Subject: [PATCH 01/27] WIP: Query syntax V2 --- phpunit.xml | 2 +- src/Database/Query.php | 264 +++++++++++---------------------- tests/Database/QueryTestV2.php | 26 ++++ 3 files changed, 116 insertions(+), 176 deletions(-) create mode 100644 tests/Database/QueryTestV2.php diff --git a/phpunit.xml b/phpunit.xml index 7bc4228a4..951a7437f 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,7 +10,7 @@ stopOnFailure="false"> - ./tests/ + ./tests/Database/QueryTestV2.php \ No newline at end of file diff --git a/src/Database/Query.php b/src/Database/Query.php index 142fee8ef..2eef554d6 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -2,134 +2,73 @@ namespace Utopia\Database; +use Error; + class Query { + // Filter methods const TYPE_EQUAL = 'equal'; const TYPE_NOTEQUAL = 'notEqual'; - const TYPE_LESSER = 'lesser'; - const TYPE_LESSEREQUAL = 'lesserEqual'; - const TYPE_GREATER = 'greater'; - const TYPE_GREATEREQUAL = 'greaterEqual'; + const TYPE_LESSER = 'lessThan'; + const TYPE_LESSEREQUAL = 'greaterEqualThan'; + const TYPE_GREATER = 'greaterThan'; + const TYPE_GREATEREQUAL = 'greaterEqualThan'; const TYPE_CONTAINS = 'contains'; const TYPE_SEARCH = 'search'; - /** - * @var string - */ - protected string $attribute = ''; + // Order methods + const TYPE_ORDERDESC = 'orderDesc'; + const TYPE_ORDERASC = 'orderAsc'; - /** - * @var string - */ - protected string $operator = ''; + // Pagination methods + const TYPE_LIMIT = 'limit'; + const TYPE_OFFSET = 'offset'; + const TYPE_CURSORAFTER = 'cursorAfter'; + const TYPE_CURSORBEFORE = 'cursorBefore'; - /** - * @var (mixed)[] - */ - protected array $values; + public static mixed $TYPE_ALIASES; // Filled from constructor - /** - * Construct. - * - * Construct a new query object - * - * @param string $attribute - * @param string $operator - * @param array $values - */ - public function __construct(string $attribute, string $operator, array $values) - { - $this->attribute = $attribute; - $this->operator = $operator; - $this->values = $values; - } + protected string $method = ''; - /** - * Get attribute - * - * @return string - */ - public function getAttribute(): string - { - return $this->attribute; - } + protected array $params = []; /** - * Get operator - * - * @return string + * Construct a new query object */ - public function getOperator(): string + public function __construct(string $method, array $params) { - return $this->operator; + $this->method = $method; + $this->params = $params; } - /** - * Get operand - * - * @return array - */ - public function getValues(): array + public function getMethod(): string { - return $this->values; + return $this->method; } - /** - * Get all query details as array - * - * @return array - */ - public function getQuery(): array + public function getParams(): array { - return [ - 'attribute' => $this->attribute, - 'operator' => $this->operator, - 'values' => $this->values, - ]; + return $this->params; } - /** - * Set attribute - * @param string $attribute - * @return Query - */ - public function setAttribute(string $attribute): self + public function setMethod(string $method): self { - $this->attribute = $attribute; + $this->method = $method; return $this; } - /** - * Set operator - * @param string $operator - * @return Query - */ - public function setOperator(string $operator): self + public function setParams(array $params): self { - $this->operator = $operator; + $this->params = $params; return $this; } /** - * Set operand - * @param array $values - * @return Query + * Check if method is supported */ - public function setValues(array $values): self - { - $this->values = $values; - - return $this; - } - - /** - * Check if operator is supported - * @param string $value - * @return bool - */ - public static function isOperator(string $value): bool + public static function isMethod(string $value): bool { switch ($value) { case self::TYPE_EQUAL: @@ -140,106 +79,81 @@ public static function isOperator(string $value): bool case self::TYPE_GREATEREQUAL: case self::TYPE_CONTAINS: case self::TYPE_SEARCH: + case self::TYPE_ORDERASC: + case self::TYPE_ORDERDESC: + case self::TYPE_LIMIT: + case self::TYPE_OFFSET: + case self::TYPE_CURSORAFTER: + case self::TYPE_CURSORBEFORE: return true; - default: - return false; } + + if(\array_key_exists($value, self::$TYPE_ALIASES)) { + return true; + } + + return false; } /** * Parse query filter - * - * @param string $filter - * - * @return Query * */ public static function parse(string $filter): Query { - $attribute = ''; - $operator = ''; - $values = []; - - // get index of open parentheses - $end = intval(mb_strpos($filter, '(')); - - // count stanzas by only counting '.' that come before open parentheses - $stanzas = mb_substr_count(mb_substr($filter, 0, $end), ".") + 1; - - // TODO@kodumbeats handle relations between collections, e.g. if($stanzas > 2) - switch ($stanzas) { - case 2: - // use limit param to ignore '.' in $expression - $input = explode('.', $filter, $stanzas); - $attribute = $input[0]; - $expression = $input[1]; - [$operator, $values] = self::parseExpression($expression); - break; + // TODO: Support for array [] (there are commas and spaces inside) + $method = ''; + $params = []; + + // Separate method and params + $paramsStart = mb_strpos($filter, '('); + $method = mb_substr($filter, 0, $paramsStart); + + // Remove everything after end of query + $paramsEnd = mb_strpos($filter, ')'); + $overflowChars = \strlen($filter) - 1 - $paramsEnd; + if($overflowChars > 0) { + $filter = substr($filter, 0, -1 * $overflowChars); } - return new Query($attribute, $operator, $values); - } - - /** - * Get attribute key-value from query expression - * $expression: string with format 'operator(value)' - * - * @param string $expression - * - * @return array - */ - protected static function parseExpression(string $expression): array - { - //find location of parentheses in expression - - /** @var int */ - $start = mb_strpos($expression, '('); - /** @var int */ - $end = mb_strrpos($expression, ')'); - - //extract the query method - $operator = mb_substr($expression, 0, $start); - - //grab everything inside parentheses - $value = mb_substr( - $expression, - ($start + 1), /* exclude open paren*/ - ($end - $start - 1) /* exclude closed paren*/ - ); - - // Explode comma-separated values - $values = explode(',', $value); - - // Cast $value type - $values = array_map(function ($value) { + // Check for deprecated query syntax + if(\str_contains($method, '.')) { + throw new Error("Invalid query method"); + } - // Trim whitespace from around $value + // Keep track of what hasn't been processed yet + $unprocessedFilter = substr($filter, $paramsStart + 1); - $value = trim($value); + // While ends when we only have ')' + while(\strlen($unprocessedFilter) > 1 ) { + $paramEnd = mb_strpos($unprocessedFilter, ','); - switch (true) { - // type casted to int or float by "+" operator - case is_numeric($value): - return $value + 0; + if($paramEnd === false) { + $paramEnd = \strlen($unprocessedFilter) - 1; + } - // since (bool)"false" returns true, check bools manually - case $value === 'true': - return true; + $param = mb_substr($unprocessedFilter, 0, $paramEnd); + $params[] = self::parseParam($param); - case $value === 'false': - return false; + $unprocessedFilter = substr($unprocessedFilter, $paramEnd + 1); + } - // need special case to cast (null) as null, not string - case $value === 'null': - return null; + \var_dump($method); + \var_dump($params); - default: - // strip escape characters - $value = stripslashes($value); - // trim leading and tailing quotes - return trim($value, '\'"'); - } - }, $values); + return new Query($method, $params); + } - return [$operator, $values]; + public static function parseParam(string $param) { + $param = \trim($param); + return $param; } } + +Query::$TYPE_ALIASES = [ + 'lt' => fn(array $params) => [new Query(Query::TYPE_LESSER, $params)], + 'lte' => fn(array $params) => [new Query(Query::TYPE_LESSEREQUAL, $params)], + 'gt' => fn(array $params) => [new Query(Query::TYPE_GREATER, $params)], + 'gte' => fn(array $params) => [new Query(Query::TYPE_GREATEREQUAL, $params)], + 'eq' => fn(array $params) => [new Query(Query::TYPE_EQUAL, $params)], + 'page' => fn(array $params) => [new Query(Query::TYPE_LIMIT, [$params[1]]), new Query(Query::TYPE_OFFSET, [($params[0]-1)*$params[1]])], +]; \ No newline at end of file diff --git a/tests/Database/QueryTestV2.php b/tests/Database/QueryTestV2.php new file mode 100644 index 000000000..1373e892d --- /dev/null +++ b/tests/Database/QueryTestV2.php @@ -0,0 +1,26 @@ +assertEquals('equal', $query->getMethod()); + // $this->assertEquals('equal', $query->getParams()); + } +} \ No newline at end of file From b66e0526b2fd1d212094a745908b589b27f5fbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 Jul 2022 16:01:14 +0000 Subject: [PATCH 02/27] Finish Queries V2 implementation --- src/Database/Query.php | 65 +++++++++++++++++++++++++++++----- tests/Database/QueryTestV2.php | 30 ++++++++++++++-- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index 2eef554d6..f7b9901f9 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -2,8 +2,6 @@ namespace Utopia\Database; -use Error; - class Query { // Filter methods @@ -100,7 +98,6 @@ public static function isMethod(string $value): bool * */ public static function parse(string $filter): Query { - // TODO: Support for array [] (there are commas and spaces inside) $method = ''; $params = []; @@ -117,34 +114,84 @@ public static function parse(string $filter): Query // Check for deprecated query syntax if(\str_contains($method, '.')) { - throw new Error("Invalid query method"); + throw new \Exception("Invalid query method"); } // Keep track of what hasn't been processed yet $unprocessedFilter = substr($filter, $paramsStart + 1); // While ends when we only have ')' - while(\strlen($unprocessedFilter) > 1 ) { + while(\strlen($unprocessedFilter) > 1) { + $arrayStart = mb_strpos($unprocessedFilter, '['); $paramEnd = mb_strpos($unprocessedFilter, ','); + // Array parameter support + if($arrayStart !== false && $arrayStart < $paramEnd) { + $paramEnd = mb_strpos($unprocessedFilter, ']') + 1; + } + + // No comma found, this is last param if($paramEnd === false) { $paramEnd = \strlen($unprocessedFilter) - 1; } + // Extract parameter from correct place $param = mb_substr($unprocessedFilter, 0, $paramEnd); - $params[] = self::parseParam($param); + $param = \trim($param); + // Empty parameter means comma without anything after. We ignore such empty parameter + if(!empty($param)) { + $params[] = self::parseParam($param); + } + + // Shorten unprocessed list until it finishes $unprocessedFilter = substr($unprocessedFilter, $paramEnd + 1); } - \var_dump($method); - \var_dump($params); - return new Query($method, $params); } public static function parseParam(string $param) { $param = \trim($param); + + // Array param + if(\str_starts_with($param, '[')) { + $param = substr($param, 1, -1); // Remove [ and ] + + $array = []; + + foreach (\explode(',', $param) as $value) { + $array[] = self::parseParam($value); + } + + return $array; + } + + // Numeric param + if(\is_numeric($param)) { + // Cast to number + return $param + 0; + } + + // Boolean param + if($param === 'false') { + return false; + } else if($param === 'true') { + return true; + } + + // Null param + if($param === 'null') { + return null; + } + + // String param + if(\str_starts_with($param, '"') || \str_starts_with($param, '\'')) { + $param = substr($param, 1, -1); // Remove '' or "" + return $param; + } + + // Unknown format return $param; } } diff --git a/tests/Database/QueryTestV2.php b/tests/Database/QueryTestV2.php index 1373e892d..6c3f3fa13 100644 --- a/tests/Database/QueryTestV2.php +++ b/tests/Database/QueryTestV2.php @@ -18,9 +18,33 @@ public function tearDown(): void public function testParse() { - $query = Query::parse('equal("One", "Two" , 3 ,false, null)'); + $queries = [ + Query::parse('equal("One",3,[55.55,\'Works\',true],false,null)'), + // Same query with random spaces + Query::parse('equal("One" , 3 , [55.55, \'Works\',true], false, null)') + ]; - $this->assertEquals('equal', $query->getMethod()); - // $this->assertEquals('equal', $query->getParams()); + foreach ($queries as $query) { + $this->assertEquals('equal', $query->getMethod()); + $this->assertCount(5, $query->getParams()); + + $this->assertIsString($query->getParams()[0]); + $this->assertEquals('One', $query->getParams()[0]); + + $this->assertIsNumeric($query->getParams()[1]); + $this->assertEquals(3, $query->getParams()[1]); + + $this->assertIsArray($query->getParams()[2]); + $this->assertCount(3, $query->getParams()[2]); + $this->assertIsNumeric($query->getParams()[2][0]); + $this->assertEquals(55.55, $query->getParams()[2][0]); + $this->assertIsString($query->getParams()[2][1]); + $this->assertEquals('Works', $query->getParams()[2][1]); + $this->assertTrue($query->getParams()[2][2]); + + $this->assertFalse($query->getParams()[3]); + + $this->assertNull($query->getParams()[4]); + } } } \ No newline at end of file From 6703819c4c90f35574da086a049a37d347a8ac03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 Jul 2022 16:22:55 +0000 Subject: [PATCH 03/27] WIP: Fix current tests --- phpunit.xml | 2 +- src/Database/Adapter/MariaDB.php | 12 +- src/Database/Adapter/Mongo/MongoDBAdapter.php | 2 +- src/Database/Validator/Queries.php | 2 +- src/Database/Validator/QueryValidator.php | 8 +- tests/Database/QueryTest.php | 209 +++++++++++------- tests/Database/QueryTestV2.php | 50 ----- tests/Database/Validator/QueriesTest.php | 12 +- .../Database/Validator/QueryValidatorTest.php | 20 +- 9 files changed, 153 insertions(+), 164 deletions(-) delete mode 100644 tests/Database/QueryTestV2.php diff --git a/phpunit.xml b/phpunit.xml index 951a7437f..7bc4228a4 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,7 +10,7 @@ stopOnFailure="false"> - ./tests/Database/QueryTestV2.php + ./tests/ \ No newline at end of file diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 45c9b03cb..b5b6a9cb2 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -862,7 +862,7 @@ public function find(string $collection, array $queries = [], int $limit = 25, i $conditions = []; foreach ($query->getValues() as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getOperator(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); + $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); } $condition = implode(' OR ', $conditions); $where[] = empty($condition) ? '' : '(' . $condition . ')'; @@ -888,7 +888,7 @@ public function find(string $collection, array $queries = [], int $limit = 25, i "); foreach ($queries as $i => $query) { - if ($query->getOperator() === Query::TYPE_SEARCH) continue; + if ($query->getMethod() === Query::TYPE_SEARCH) continue; foreach ($query->getValues() as $key => $value) { $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); } @@ -968,7 +968,7 @@ public function count(string $collection, array $queries = [], int $max = 0): in $conditions = []; foreach ($query->getValues() as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getOperator(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); + $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); } $condition = implode(' OR ', $conditions); @@ -991,7 +991,7 @@ public function count(string $collection, array $queries = [], int $max = 0): in "); foreach ($queries as $i => $query) { - if ($query->getOperator() === Query::TYPE_SEARCH) continue; + if ($query->getMethod() === Query::TYPE_SEARCH) continue; foreach ($query->getValues() as $key => $value) { $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); } @@ -1037,7 +1037,7 @@ public function sum(string $collection, string $attribute, array $queries = [], $conditions = []; foreach ($query->getValues() as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getOperator(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); + $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); } $where[] = implode(' OR ', $conditions); @@ -1058,7 +1058,7 @@ public function sum(string $collection, string $attribute, array $queries = [], "); foreach ($queries as $i => $query) { - if ($query->getOperator() === Query::TYPE_SEARCH) continue; + if ($query->getMethod() === Query::TYPE_SEARCH) continue; foreach ($query->getValues() as $key => $value) { $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); } diff --git a/src/Database/Adapter/Mongo/MongoDBAdapter.php b/src/Database/Adapter/Mongo/MongoDBAdapter.php index 6915883e8..6893074f9 100644 --- a/src/Database/Adapter/Mongo/MongoDBAdapter.php +++ b/src/Database/Adapter/Mongo/MongoDBAdapter.php @@ -740,7 +740,7 @@ protected function buildFilters($queries): array $query->setAttribute('_uid'); } $attribute = $query->getAttribute(); - $operator = $this->getQueryOperator($query->getOperator()); + $operator = $this->getQueryOperator($query->getMethod()); $value = (count($query->getValues()) > 1) ? $query->getValues() : $query->getValues()[0]; // TODO@kodumbeats Mongo recommends different methods depending on operator - implement the rest diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index fd53bf603..30793e7ea 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -93,7 +93,7 @@ public function isValid($value): bool foreach ($value as $query) { // [attribute => operator] - $queries[$query->getAttribute()] = $query->getOperator(); + $queries[$query->getAttribute()] = $query->getMethod(); if (!$this->validator->isValid($query)) { $this->message = 'Query not valid: ' . $this->validator->getDescription(); diff --git a/src/Database/Validator/QueryValidator.php b/src/Database/Validator/QueryValidator.php index 53593c2fd..d59860827 100644 --- a/src/Database/Validator/QueryValidator.php +++ b/src/Database/Validator/QueryValidator.php @@ -90,8 +90,8 @@ public function getDescription(): string public function isValid($query): bool { // Validate operator - if (!in_array($query->getOperator(), $this->operators)) { - $this->message = 'Query operator invalid: ' . $query->getOperator(); + if (!in_array($query->getMethod(), $this->operators)) { + $this->message = 'Query operator invalid: ' . $query->getMethod(); return false; } @@ -114,8 +114,8 @@ public function isValid($query): bool } // Contains operator only supports array attributes - if (!$this->schema[$attributeIndex]['array'] && $query->getOperator() === Query::TYPE_CONTAINS) { - $this->message = 'Query operator only supported on array attributes: ' . $query->getOperator(); + if (!$this->schema[$attributeIndex]['array'] && $query->getMethod() === Query::TYPE_CONTAINS) { + $this->message = 'Query operator only supported on array attributes: ' . $query->getMethod(); return false; } diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 6b6fbee4c..fde0c505e 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -18,129 +18,168 @@ public function tearDown(): void public function testCreate(): void { - $query = new Query('title', 'equal', ['Iron Man']); + $query = new Query('equal', ['title', 'Iron Man']); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals('equal', $query->getOperator()); - $this->assertContains('Iron Man', $query->getValues()); + $this->assertEquals('equal', $query->getMethod()); + $this->assertEquals('title', $query->getParams()[0]); + $this->assertEquals('Iron Man', $query->getParams()[1]); } public function testParse() { - $query = Query::parse('title.equal("Iron Man")'); + $query = Query::parse('equal("title", "Iron Man")'); - $this->assertEquals('title', $query->getAttribute()); - $this->assertEquals('equal', $query->getOperator()); - $this->assertContains('Iron Man', $query->getValues()); + $this->assertEquals('equal', $query->getMethod()); + $this->assertEquals('title', $query->getParams()[0]); + $this->assertEquals('Iron Man', $query->getParams()[1]); - $query = Query::parse('year.lesser(2001)'); + $query = Query::parse('lesser("year", 2001)'); - $this->assertEquals('year', $query->getAttribute()); - $this->assertEquals('lesser', $query->getOperator()); - $this->assertContains(2001, $query->getValues()); + $this->assertEquals('lesser', $query->getMethod()); + $this->assertContains('year', $query->getParams()[0]); + $this->assertEquals(2001, $query->getParams()[1]); - $query = Query::parse('published.equal(true)'); + $query = Query::parse('equal("published", true)'); - $this->assertEquals('published', $query->getAttribute()); - $this->assertEquals('equal', $query->getOperator()); - $this->assertContains(true, $query->getValues()); + $this->assertEquals('equal', $query->getMethod()); + $this->assertContains('published', $query->getParams()[0]); + $this->assertEquals(true, $query->getParams()[1]); - $query = Query::parse('published.equal(false)'); + $query = Query::parse('equal("published", false)'); - $this->assertEquals('published', $query->getAttribute()); - $this->assertEquals('equal', $query->getOperator()); - $this->assertContains(false, $query->getValues()); + $this->assertEquals('equal', $query->getMethod()); + $this->assertContains('published', $query->getParams()[0]); + $this->assertEquals(false, $query->getParams()[1]); - $query = Query::parse('actors.notContains( " Johnny Depp ", " Brad Pitt" , "Al Pacino")'); + $query = Query::parse('notContains("actors", [ " Johnny Depp ", " Brad Pitt" , \'Al Pacino \' ])'); - $this->assertEquals('actors', $query->getAttribute()); - $this->assertEquals('notContains', $query->getOperator()); - $this->assertContains(' Johnny Depp ', $query->getValues()); - $this->assertContains(' Brad Pitt', $query->getValues()); - $this->assertContains('Al Pacino', $query->getValues()); + $this->assertEquals('actors', $query->getMethod()); + $this->assertContains('notContains', $query->getParams()[0]); + $this->assertEquals(" Johnny Depp ", $query->getParams()[1][0]); + $this->assertEquals(" Brad Pitt", $query->getParams()[1][1]); + $this->assertEquals("Al Pacino ", $query->getParams()[1][2]); - $query = Query::parse('actors.equal("Brad Pitt", "Johnny Depp")'); + $query = Query::parse('equal("actors", ["Brad Pitt", "Johnny Depp"])'); - $this->assertEquals('actors', $query->getAttribute()); - $this->assertEquals('equal', $query->getOperator()); - $this->assertContains('Brad Pitt', $query->getValues()); - $this->assertContains('Johnny Depp', $query->getValues()); + $this->assertEquals('equal', $query->getMethod()); + $this->assertContains('actors', $query->getParams()[0]); + $this->assertEquals("Brad Pitt", $query->getParams()[1][0]); + $this->assertEquals("Johnny Depp", $query->getParams()[1][1]); - $query = Query::parse('writers.contains("Tim O\'Reilly")'); + $query = Query::parse('contains("writers","Tim O\'Reilly")'); - $this->assertEquals('writers', $query->getAttribute()); - $this->assertEquals('contains', $query->getOperator()); - $this->assertContains("Tim O'Reilly", $query->getValues()); + $this->assertEquals('contains', $query->getMethod()); + $this->assertContains('writers', $query->getParams()[0]); + $this->assertEquals("Tim O'Reilly", $query->getParams()[1]); - $query = Query::parse('score.greater(8.5)'); + $query = Query::parse('greater("score", 8.5)'); - $this->assertEquals('score', $query->getAttribute()); - $this->assertEquals('greater', $query->getOperator()); - $this->assertContains(8.5, $query->getValues()); + $this->assertEquals('greater', $query->getMethod()); + $this->assertContains('score', $query->getParams()[0]); + $this->assertEquals(8.5, $query->getParams()[1]); - $query = Query::parse('director.notEqual("null")'); + $query = Query::parse('notEqual("director", "null")'); - $this->assertEquals('director', $query->getAttribute()); - $this->assertEquals('notEqual', $query->getOperator()); - $this->assertContains('null', $query->getValues()); + $this->assertEquals('notEqual', $query->getMethod()); + $this->assertContains('director', $query->getParams()[0]); + $this->assertEquals('null', $query->getParams()[1]); - $query = Query::parse('director.notEqual(null)'); + $query = Query::parse('notEqual("director", null)'); - $this->assertEquals('director', $query->getAttribute()); - $this->assertEquals('notEqual', $query->getOperator()); - $this->assertContains(null, $query->getValues()); + $this->assertEquals('notEqual', $query->getMethod()); + $this->assertContains('director', $query->getParams()[0]); + $this->assertEquals(null, $query->getParams()[1]); } - public function testGetAttribute() + public function testParseComplex() { - $query = Query::parse('title.equal("Iron Man")'); - - $this->assertEquals('title', $query->getAttribute()); + $queries = [ + Query::parse('equal("One",3,[55.55,\'Works\',true],false,null)'), + // Same query with random spaces + Query::parse('equal("One" , 3 , [55.55, \'Works\',true], false, null)') + ]; + + foreach ($queries as $query) { + $this->assertEquals('equal', $query->getMethod()); + $this->assertCount(5, $query->getParams()); + + $this->assertIsString($query->getParams()[0]); + $this->assertEquals('One', $query->getParams()[0]); + + $this->assertIsNumeric($query->getParams()[1]); + $this->assertEquals(3, $query->getParams()[1]); + + $this->assertIsArray($query->getParams()[2]); + $this->assertCount(3, $query->getParams()[2]); + $this->assertIsNumeric($query->getParams()[2][0]); + $this->assertEquals(55.55, $query->getParams()[2][0]); + $this->assertIsString($query->getParams()[2][1]); + $this->assertEquals('Works', $query->getParams()[2][1]); + $this->assertTrue($query->getParams()[2][2]); + + $this->assertFalse($query->getParams()[3]); + + $this->assertNull($query->getParams()[4]); + } } - public function testGetOperator() + public function testGetAttribute() { - $query = Query::parse('title.equal("Iron Man")'); + $query = Query::parse('equal("title", "Iron Man")'); - $this->assertEquals('equal', $query->getOperator()); + $this->assertIsArray($query->getParams()); + $this->assertCount(2, $query->getParams()); + $this->assertEquals('title', $query->getParams()[0]); + $this->assertEquals('Iron Man', $query->getParams()[1]); } - public function testGetValue() + public function testGetMethod() { - $query = Query::parse('title.equal("Iron Man")'); + $query = Query::parse('equal("title", "Iron Man")'); - $this->assertContains('Iron Man', $query->getValues()); + $this->assertEquals('equal', $query->getMethod()); } - public function testGetQuery() + public function testisMethod() { - $query = Query::parse('title.equal("Iron Man")')->getQuery(); - $this->assertEquals('title', $query['attribute']); - $this->assertEquals('equal', $query['operator']); - $this->assertContains('Iron Man', $query['values']); - } - - public function testIsOperator() - { - $this->assertEquals(true, Query::isOperator('equal')); - $this->assertEquals(true, Query::isOperator('notEqual')); - $this->assertEquals(true, Query::isOperator('lesser')); - $this->assertEquals(true, Query::isOperator('lesserEqual')); - $this->assertEquals(true, Query::isOperator('greater')); - $this->assertEquals(true, Query::isOperator('greaterEqual')); - $this->assertEquals(true, Query::isOperator('contains')); - $this->assertEquals(true, Query::isOperator('search')); - - $this->assertEquals(true, Query::isOperator(Query::TYPE_EQUAL)); - $this->assertEquals(true, Query::isOperator(Query::TYPE_NOTEQUAL)); - $this->assertEquals(true, Query::isOperator(Query::TYPE_LESSER)); - $this->assertEquals(true, Query::isOperator(Query::TYPE_LESSEREQUAL)); - $this->assertEquals(true, Query::isOperator(Query::TYPE_GREATER)); - $this->assertEquals(true, Query::isOperator(Query::TYPE_GREATEREQUAL)); - $this->assertEquals(true, Query::isOperator(Query::TYPE_CONTAINS)); - $this->assertEquals(true, Query::isOperator(QUERY::TYPE_SEARCH)); - - $this->assertEquals(false, Query::isOperator('invalid')); + $this->assertEquals(true, Query::isMethod('equal')); + $this->assertEquals(true, Query::isMethod('notEqual')); + $this->assertEquals(true, Query::isMethod('lesser')); + $this->assertEquals(true, Query::isMethod('lesserEqual')); + $this->assertEquals(true, Query::isMethod('greater')); + $this->assertEquals(true, Query::isMethod('greaterEqual')); + $this->assertEquals(true, Query::isMethod('contains')); + $this->assertEquals(true, Query::isMethod('search')); + $this->assertEquals(true, Query::isMethod('orderDesc')); + $this->assertEquals(true, Query::isMethod('orderAsc')); + $this->assertEquals(true, Query::isMethod('limit')); + $this->assertEquals(true, Query::isMethod('offset')); + $this->assertEquals(true, Query::isMethod('cursorAfter')); + $this->assertEquals(true, Query::isMethod('cursorBefore')); + + $this->assertEquals(true, Query::isMethod(Query::TYPE_EQUAL)); + $this->assertEquals(true, Query::isMethod(Query::TYPE_NOTEQUAL)); + $this->assertEquals(true, Query::isMethod(Query::TYPE_LESSER)); + $this->assertEquals(true, Query::isMethod(Query::TYPE_LESSEREQUAL)); + $this->assertEquals(true, Query::isMethod(Query::TYPE_GREATER)); + $this->assertEquals(true, Query::isMethod(Query::TYPE_GREATEREQUAL)); + $this->assertEquals(true, Query::isMethod(Query::TYPE_CONTAINS)); + $this->assertEquals(true, Query::isMethod(QUERY::TYPE_SEARCH)); + $this->assertEquals(true, Query::isMethod(QUERY::TYPE_ORDERASC)); + $this->assertEquals(true, Query::isMethod(QUERY::TYPE_ORDERDESC)); + $this->assertEquals(true, Query::isMethod(QUERY::TYPE_LIMIT)); + $this->assertEquals(true, Query::isMethod(QUERY::TYPE_OFFSET)); + $this->assertEquals(true, Query::isMethod(QUERY::TYPE_CURSORAFTER)); + $this->assertEquals(true, Query::isMethod(QUERY::TYPE_CURSORBEFORE)); + + $this->assertEquals(true, Query::isMethod('lt')); + $this->assertEquals(true, Query::isMethod('lte')); + $this->assertEquals(true, Query::isMethod('gt')); + $this->assertEquals(true, Query::isMethod('gte')); + $this->assertEquals(true, Query::isMethod('eq')); + $this->assertEquals(true, Query::isMethod('page')); + + $this->assertEquals(false, Query::isMethod('invalid')); } } \ No newline at end of file diff --git a/tests/Database/QueryTestV2.php b/tests/Database/QueryTestV2.php deleted file mode 100644 index 6c3f3fa13..000000000 --- a/tests/Database/QueryTestV2.php +++ /dev/null @@ -1,50 +0,0 @@ -assertEquals('equal', $query->getMethod()); - $this->assertCount(5, $query->getParams()); - - $this->assertIsString($query->getParams()[0]); - $this->assertEquals('One', $query->getParams()[0]); - - $this->assertIsNumeric($query->getParams()[1]); - $this->assertEquals(3, $query->getParams()[1]); - - $this->assertIsArray($query->getParams()[2]); - $this->assertCount(3, $query->getParams()[2]); - $this->assertIsNumeric($query->getParams()[2][0]); - $this->assertEquals(55.55, $query->getParams()[2][0]); - $this->assertIsString($query->getParams()[2][1]); - $this->assertEquals('Works', $query->getParams()[2][1]); - $this->assertTrue($query->getParams()[2][2]); - - $this->assertFalse($query->getParams()[3]); - - $this->assertNull($query->getParams()[4]); - } - } -} \ No newline at end of file diff --git a/tests/Database/Validator/QueriesTest.php b/tests/Database/Validator/QueriesTest.php index 1887893dd..dbff66791 100644 --- a/tests/Database/Validator/QueriesTest.php +++ b/tests/Database/Validator/QueriesTest.php @@ -104,8 +104,8 @@ public function setUp(): void $this->queryValidator = new QueryValidator($attributes); - $query1 = Query::parse('title.notEqual("Iron Man", "Ant Man")'); - $query2 = Query::parse('description.equal("Best movie ever")'); + $query1 = Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'); + $query2 = Query::parse('equal("description", "Best movie ever")'); array_push($this->queries, $query1, $query2); @@ -169,7 +169,7 @@ public function testQueries() $this->assertEquals(true, $validator->isValid($this->queries)); - $this->queries[] = Query::parse('price.lesserEqual(6.50)'); + $this->queries[] = Query::parse('lesserEqual("price", 6.50)'); $this->assertEquals(true, $validator->isValid($this->queries)); @@ -181,8 +181,8 @@ public function testQueries() $this->assertEquals("Index not found: title,description,price,rating", $validator->getDescription()); // test for queued index - $query1 = Query::parse('price.lesserEqual(6.50)'); - $query2 = Query::parse('title.notEqual("Iron Man", "Ant Man")'); + $query1 = Query::parse('lesserEqual("price", 6.50)'); + $query2 = Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'); $this->queries = [$query1, $query2]; $this->assertEquals(false, $validator->isValid($this->queries)); @@ -190,7 +190,7 @@ public function testQueries() // test fulltext - $query3 = Query::parse('description.search("iron")'); + $query3 = Query::parse('search("description", "iron")'); $this->queries = [$query3]; $this->assertEquals(false, $validator->isValid($this->queries)); $this->assertEquals("Search operator requires fulltext index: description", $validator->getDescription()); diff --git a/tests/Database/Validator/QueryValidatorTest.php b/tests/Database/Validator/QueryValidatorTest.php index 1c14efc0a..f3a741b5a 100644 --- a/tests/Database/Validator/QueryValidatorTest.php +++ b/tests/Database/Validator/QueryValidatorTest.php @@ -97,19 +97,19 @@ public function testQuery() { $validator = new QueryValidator($this->schema); - $this->assertEquals(true, $validator->isValid(Query::parse('$id.equal("Iron Man", "Ant Man")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('title.notEqual("Iron Man", "Ant Man")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('description.equal("Best movie ever")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('rating.greater(4)'))); - $this->assertEquals(true, $validator->isValid(Query::parse('price.lesserEqual(6.50)'))); - $this->assertEquals(true, $validator->isValid(Query::parse('tags.contains("action")'))); + $this->assertEquals(true, $validator->isValid(Query::parse('equal("$id", ["Iron Man", "Ant Man"])'))); + $this->assertEquals(true, $validator->isValid(Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'))); + $this->assertEquals(true, $validator->isValid(Query::parse('equal("description", "Best movie ever")'))); + $this->assertEquals(true, $validator->isValid(Query::parse('greater("rating" 4)'))); + $this->assertEquals(true, $validator->isValid(Query::parse('lesserEqual("price", 6.50)'))); + $this->assertEquals(true, $validator->isValid(Query::parse('contains("tags", "action")'))); } public function testInvalidOperator() { $validator = new QueryValidator($this->schema); - $response = $validator->isValid(Query::parse('title.eqqual("Iron Man")')); + $response = $validator->isValid(Query::parse('eqqual("title", "Iron Man")')); $this->assertEquals(false, $response); $this->assertEquals('Query operator invalid: eqqual', $validator->getDescription()); @@ -119,7 +119,7 @@ public function testAttributeNotFound() { $validator = new QueryValidator($this->schema); - $response = $validator->isValid(Query::parse('name.equal("Iron Man")')); + $response = $validator->isValid(Query::parse('equal("name", "Iron Man")')); $this->assertEquals(false, $response); $this->assertEquals('Attribute not found in schema: name', $validator->getDescription()); @@ -129,7 +129,7 @@ public function testAttributeWrongType() { $validator = new QueryValidator($this->schema); - $response = $validator->isValid(Query::parse('title.equal(1776)')); + $response = $validator->isValid(Query::parse('equal("title", 1776)')); $this->assertEquals(false, $response); $this->assertEquals('Query type does not match expected: string', $validator->getDescription()); @@ -139,7 +139,7 @@ public function testOperatorWrongType() { $validator = new QueryValidator($this->schema); - $response = $validator->isValid(Query::parse('title.contains("Iron")')); + $response = $validator->isValid(Query::parse('contains("title", "Iron")')); $this->assertEquals(false, $response); $this->assertEquals('Query operator only supported on array attributes: contains', $validator->getDescription()); From 928fb86fd22733d0f54f1dc0b325077f056f7159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 Jul 2022 16:54:41 +0000 Subject: [PATCH 04/27] Tests fixed --- README.md | 2 +- src/Database/Adapter/MariaDB.php | 36 ++++++------- src/Database/Adapter/Mongo/MongoDBAdapter.php | 10 ++-- src/Database/Query.php | 32 +++++++++++- src/Database/Validator/Queries.php | 4 +- src/Database/Validator/QueryValidator.php | 6 +-- tests/Database/Adapter/MongoDBTest.php | 6 +-- tests/Database/Base.php | 52 +++++++++---------- tests/Database/QueryTest.php | 28 +++++----- tests/Database/Validator/QueriesTest.php | 2 +- .../Database/Validator/QueryValidatorTest.php | 2 +- 11 files changed, 104 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 668728a28..90db471cf 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ static::getDatabase()->createDocument('movies', new Document([ ```php $documents = static::getDatabase()->find('movies', [ - new Query('year', Query::TYPE_EQUAL, [2019]), + new Query(Query::TYPE_EQUAL, ['year', 2019]), ]); ``` diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index b5b6a9cb2..c1a6bace8 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -853,16 +853,16 @@ public function find(string $collection, array $queries = [], int $limit = 25, i } foreach ($queries as $i => $query) { - $query->setAttribute(match ($query->getAttribute()) { + $query->setFirstParam(match ($query->getFirstParam()) { '$id' => '_uid', '$createdAt' => '_createdAt', '$updatedAt' => '_updatedAt', - default => $query->getAttribute() + default => $query->getFirstParam() }); $conditions = []; - foreach ($query->getValues() as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); + foreach ($query->getArrayParam(1) as $key => $value) { + $conditions[] = $this->getSQLCondition('table_main.' . $query->getFirstParam(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value); } $condition = implode(' OR ', $conditions); $where[] = empty($condition) ? '' : '(' . $condition . ')'; @@ -889,8 +889,8 @@ public function find(string $collection, array $queries = [], int $limit = 25, i foreach ($queries as $i => $query) { if ($query->getMethod() === Query::TYPE_SEARCH) continue; - foreach ($query->getValues() as $key => $value) { - $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); + foreach ($query->getArrayParam(1) as $key => $value) { + $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value, $this->getPDOType($value)); } } @@ -959,16 +959,16 @@ public function count(string $collection, array $queries = [], int $max = 0): in $limit = ($max === 0) ? '' : 'LIMIT :max'; foreach ($queries as $i => $query) { - $query->setAttribute(match ($query->getAttribute()) { + $query->setFirstParam(match ($query->getFirstParam()) { '$id' => '_uid', '$createdAt' => '_createdAt', '$updatedAt' => '_updatedAt', - default => $query->getAttribute() + default => $query->getFirstParam() }); $conditions = []; - foreach ($query->getValues() as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); + foreach ($query->getArrayParam(1) as $key => $value) { + $conditions[] = $this->getSQLCondition('table_main.' . $query->getFirstParam(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value); } $condition = implode(' OR ', $conditions); @@ -992,8 +992,8 @@ public function count(string $collection, array $queries = [], int $max = 0): in foreach ($queries as $i => $query) { if ($query->getMethod() === Query::TYPE_SEARCH) continue; - foreach ($query->getValues() as $key => $value) { - $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); + foreach ($query->getArrayParam(1) as $key => $value) { + $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value, $this->getPDOType($value)); } } @@ -1028,16 +1028,16 @@ public function sum(string $collection, string $attribute, array $queries = [], $limit = ($max === 0) ? '' : 'LIMIT :max'; foreach ($queries as $i => $query) { - $query->setAttribute(match ($query->getAttribute()) { + $query->setFirstParam(match ($query->getFirstParam()) { '$id' => '_uid', '$createdAt' => '_createdAt', '$updatedAt' => '_updatedAt', - default => $query->getAttribute() + default => $query->getFirstParam() }); $conditions = []; - foreach ($query->getValues() as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); + foreach ($query->getArrayParam(1) as $key => $value) { + $conditions[] = $this->getSQLCondition('table_main.' . $query->getFirstParam(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value); } $where[] = implode(' OR ', $conditions); @@ -1059,8 +1059,8 @@ public function sum(string $collection, string $attribute, array $queries = [], foreach ($queries as $i => $query) { if ($query->getMethod() === Query::TYPE_SEARCH) continue; - foreach ($query->getValues() as $key => $value) { - $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); + foreach ($query->getArrayParam(1) as $key => $value) { + $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value, $this->getPDOType($value)); } } diff --git a/src/Database/Adapter/Mongo/MongoDBAdapter.php b/src/Database/Adapter/Mongo/MongoDBAdapter.php index 6893074f9..911c03609 100644 --- a/src/Database/Adapter/Mongo/MongoDBAdapter.php +++ b/src/Database/Adapter/Mongo/MongoDBAdapter.php @@ -736,18 +736,18 @@ protected function buildFilters($queries): array $filters = []; foreach ($queries as $i => $query) { - if ($query->getAttribute() === '$id') { - $query->setAttribute('_uid'); + if ($query->getFirstParam() === '$id') { + $query->setFirstParam('_uid'); } - $attribute = $query->getAttribute(); + $attribute = $query->getFirstParam(); $operator = $this->getQueryOperator($query->getMethod()); - $value = (count($query->getValues()) > 1) ? $query->getValues() : $query->getValues()[0]; + $value = (count($query->getArrayParam(1)) > 1) ? $query->getArrayParam(1) : $query->getArrayParam(1)[0]; // TODO@kodumbeats Mongo recommends different methods depending on operator - implement the rest if (is_array($value) && $operator === '$eq') { $filters[$attribute]['$in'] = $value; } elseif ($operator === '$in') { - $filters[$attribute]['$in'] = $query->getValues(); + $filters[$attribute]['$in'] = $query->getArrayParam(1); } elseif ($operator === '$search') { // only one fulltext index per mongo collection, so attribute not necessary $filters['$text'][$operator] = $value; diff --git a/src/Database/Query.php b/src/Database/Query.php index f7b9901f9..0697c6b16 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -8,9 +8,9 @@ class Query const TYPE_EQUAL = 'equal'; const TYPE_NOTEQUAL = 'notEqual'; const TYPE_LESSER = 'lessThan'; - const TYPE_LESSEREQUAL = 'greaterEqualThan'; + const TYPE_LESSEREQUAL = 'lessThanEqual'; const TYPE_GREATER = 'greaterThan'; - const TYPE_GREATEREQUAL = 'greaterEqualThan'; + const TYPE_GREATEREQUAL = 'greaterThanEqual'; const TYPE_CONTAINS = 'contains'; const TYPE_SEARCH = 'search'; @@ -49,6 +49,34 @@ public function getParams(): array return $this->params; } + /** + * Helper method returning first param. In first param we often store attribute + */ + public function getFirstParam(): mixed + { + return $this->params[0]; + } + + /** + * Helper method changing first param. In first param we often store attribute + */ + public function setFirstParam(mixed $value): void + { + $this->params[0] = $value; + } + + /** + * Helper method. Returns param, but in form of array array + */ + public function getArrayParam(int $index): array + { + if(\is_array($this->params[$index])) { + return $this->params[$index]; + } + + return [ $this->params[$index] ]; + } + public function setMethod(string $method): self { $this->method = $method; diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 30793e7ea..3dc405349 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -85,7 +85,7 @@ public function getDescription(): string public function isValid($value): bool { /** - * Array of attributes from Query->getAttribute() + * Array of attributes from query * * @var string[] */ @@ -93,7 +93,7 @@ public function isValid($value): bool foreach ($value as $query) { // [attribute => operator] - $queries[$query->getAttribute()] = $query->getMethod(); + $queries[$query->getFirstParam()] = $query->getMethod(); if (!$this->validator->isValid($query)) { $this->message = 'Query not valid: ' . $this->validator->getDescription(); diff --git a/src/Database/Validator/QueryValidator.php b/src/Database/Validator/QueryValidator.php index d59860827..aab916878 100644 --- a/src/Database/Validator/QueryValidator.php +++ b/src/Database/Validator/QueryValidator.php @@ -96,17 +96,17 @@ public function isValid($query): bool } // Search for attribute in schema - $attributeIndex = array_search($query->getAttribute(), array_column($this->schema, 'key')); + $attributeIndex = array_search($query->getFirstParam(), array_column($this->schema, 'key')); if ($attributeIndex === false) { - $this->message = 'Attribute not found in schema: ' . $query->getAttribute(); + $this->message = 'Attribute not found in schema: ' . $query->getFirstParam(); return false; } // Extract the type of desired attribute from collection $schema $attributeType = $this->schema[$attributeIndex]['type']; - foreach ($query->getValues() as $value) { + foreach ($query->getArrayParam(1) as $value) { if (gettype($value) !== $attributeType) { $this->message = 'Query type does not match expected: ' . $attributeType; return false; diff --git a/tests/Database/Adapter/MongoDBTest.php b/tests/Database/Adapter/MongoDBTest.php index dd750a073..b994673af 100644 --- a/tests/Database/Adapter/MongoDBTest.php +++ b/tests/Database/Adapter/MongoDBTest.php @@ -245,7 +245,7 @@ public function testCount() $count = static::getDatabase()->count('movies'); $this->assertEquals(5, $count); - $count = static::getDatabase()->count('movies', [new Query('year', Query::TYPE_EQUAL, [2019]),]); + $count = static::getDatabase()->count('movies', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); $this->assertEquals(2, $count); Authorization::unsetRole('userx'); @@ -267,8 +267,8 @@ public function testCount() */ Authorization::disable(); $count = static::getDatabase()->count('movies', [ - new Query('director', Query::TYPE_EQUAL, ['TBD', 'Joe Johnston']), - new Query('year', Query::TYPE_EQUAL, [2025]), + new Query(Query::TYPE_EQUAL, ['director', ['TBD', 'Joe Johnston']]), + new Query(Query::TYPE_EQUAL, ['year', 2025]), ]); $this->assertEquals(1, $count); Authorization::reset(); diff --git a/tests/Database/Base.php b/tests/Database/Base.php index f441f7d39..753933e35 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -659,7 +659,7 @@ public function testListDocumentSearch(Document $document) * Allow reserved keywords for search */ $documents = static::getDatabase()->find('documents', [ - new Query('string', Query::TYPE_SEARCH, ['*test+alias@email-provider.com']), + new Query(Query::TYPE_SEARCH, ['string', '*test+alias@email-provider.com']), ]); $this->assertEquals(1, count($documents)); @@ -905,7 +905,7 @@ public function testFind(Document $document) * Check an Integer condition */ $documents = static::getDatabase()->find('movies', [ - new Query('year', Query::TYPE_EQUAL, [2019]), + new Query(Query::TYPE_EQUAL, ['year', 2019]), ]); $this->assertEquals(2, count($documents)); @@ -916,7 +916,7 @@ public function testFind(Document $document) * Boolean condition */ $documents = static::getDatabase()->find('movies', [ - new Query('active', Query::TYPE_EQUAL, [true]), + new Query(Query::TYPE_EQUAL, ['active', true]), ]); $this->assertEquals(4, count($documents)); @@ -925,7 +925,7 @@ public function testFind(Document $document) * String condition */ $documents = static::getDatabase()->find('movies', [ - new Query('director', Query::TYPE_EQUAL, ['TBD']), + new Query(Query::TYPE_EQUAL, ['director', 'TBD']), ]); $this->assertEquals(2, count($documents)); @@ -934,8 +934,8 @@ public function testFind(Document $document) * Float condition */ $documents = static::getDatabase()->find('movies', [ - new Query('price', Query::TYPE_LESSER, [26.00]), - new Query('price', Query::TYPE_GREATER, [25.98]), + new Query(Query::TYPE_LESSER, ['price', 26.00]), + new Query(Query::TYPE_GREATER, ['price', 25.98]), ]); // TODO@kodumbeats hacky way to pass mariadb tests @@ -946,7 +946,7 @@ public function testFind(Document $document) * Array contains condition */ $documents = static::getDatabase()->find('movies', [ - new Query('generes', Query::TYPE_CONTAINS, ['comics']), + new Query(Query::TYPE_CONTAINS, ['generes', 'comics']), ]); $this->assertEquals(2, count($documents)); @@ -955,7 +955,7 @@ public function testFind(Document $document) * Array contains OR condition */ $documents = static::getDatabase()->find('movies', [ - new Query('generes', Query::TYPE_CONTAINS, ['comics', 'kids']), + new Query(Query::TYPE_CONTAINS, ['generes', ['comics', 'kids']]), ]); $this->assertEquals(4, count($documents)); @@ -968,7 +968,7 @@ public function testFind(Document $document) $this->assertEquals(true, $success); $documents = static::getDatabase()->find('movies', [ - new Query('name', Query::TYPE_SEARCH, ['captain']), + new Query(Query::TYPE_SEARCH, ['name', 'captain']), ]); $this->assertEquals(2, count($documents)); @@ -979,7 +979,7 @@ public function testFind(Document $document) // TODO: Looks like the MongoDB implementation is a bit more complex, skipping that for now. if (in_array(static::getAdapterName(), ['mysql', 'mariadb'])) { $documents = static::getDatabase()->find('movies', [ - new Query('name', Query::TYPE_SEARCH, ['cap']), + new Query(Query::TYPE_SEARCH, ['name', 'cap']), ]); $this->assertEquals(2, count($documents)); @@ -989,8 +989,8 @@ public function testFind(Document $document) * Multiple conditions */ $documents = static::getDatabase()->find('movies', [ - new Query('director', Query::TYPE_EQUAL, ['TBD']), - new Query('year', Query::TYPE_EQUAL, [2026]), + new Query(Query::TYPE_EQUAL, ['director', 'TBD']), + new Query(Query::TYPE_EQUAL, ['year', 2026]), ]); $this->assertEquals(1, count($documents)); @@ -999,7 +999,7 @@ public function testFind(Document $document) * Multiple conditions and OR values */ $documents = static::getDatabase()->find('movies', [ - new Query('name', Query::TYPE_EQUAL, ['Frozen II', 'Captain Marvel']), + new Query(Query::TYPE_EQUAL, ['name', ['Frozen II', 'Captain Marvel']]), ]); $this->assertEquals(2, count($documents)); @@ -1010,7 +1010,7 @@ public function testFind(Document $document) * $id condition */ $documents = static::getDatabase()->find('movies', [ - new Query('$id', Query::TYPE_EQUAL, ['frozen']), + new Query(Query::TYPE_EQUAL, ['$id', 'frozen']), ]); $this->assertEquals(1, count($documents)); @@ -1312,8 +1312,8 @@ public function testFind(Document $document) * Test that OR queries are handled correctly */ $documents = static::getDatabase()->find('movies', [ - new Query('director', Query::TYPE_EQUAL, ['TBD', 'Joe Johnston']), - new Query('year', Query::TYPE_EQUAL, [2025]), + new Query(Query::TYPE_EQUAL, ['director', ['TBD', 'Joe Johnston']]), + new Query(Query::TYPE_EQUAL, ['year', 2025]), ]); $this->assertEquals(1, count($documents)); @@ -1349,7 +1349,7 @@ public function testCount() $count = static::getDatabase()->count('movies'); $this->assertEquals(6, $count); - $count = static::getDatabase()->count('movies', [new Query('year', Query::TYPE_EQUAL, [2019]),]); + $count = static::getDatabase()->count('movies', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); $this->assertEquals(2, $count); Authorization::unsetRole('userx'); @@ -1371,8 +1371,8 @@ public function testCount() */ Authorization::disable(); $count = static::getDatabase()->count('movies', [ - new Query('director', Query::TYPE_EQUAL, ['TBD', 'Joe Johnston']), - new Query('year', Query::TYPE_EQUAL, [2025]), + new Query(Query::TYPE_EQUAL, ['director', ['TBD', 'Joe Johnston']]), + new Query(Query::TYPE_EQUAL, ['year', 2025]), ]); $this->assertEquals(1, $count); Authorization::reset(); @@ -1384,26 +1384,26 @@ public function testCount() public function testSum() { Authorization::setRole('userx'); - $sum = static::getDatabase()->sum('movies', 'year', [new Query('year', Query::TYPE_EQUAL, [2019]),]); + $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); $this->assertEquals(2019+2019, $sum); $sum = static::getDatabase()->sum('movies', 'year'); $this->assertEquals(2013+2019+2011+2019+2025+2026, $sum); - $sum = static::getDatabase()->sum('movies', 'price', [new Query('year', Query::TYPE_EQUAL, [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); $this->assertEquals(round(39.50+25.99, 2), round($sum, 2)); - $sum = static::getDatabase()->sum('movies', 'price', [new Query('year', Query::TYPE_EQUAL, [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); $this->assertEquals(round(39.50+25.99, 2), round($sum, 2)); - $sum = static::getDatabase()->sum('movies', 'year', [new Query('year', Query::TYPE_EQUAL, [2019])], 1); + $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, ['year', 2019])], 1); $this->assertEquals(2019, $sum); Authorization::unsetRole('userx'); - $sum = static::getDatabase()->sum('movies', 'year', [new Query('year', Query::TYPE_EQUAL, [2019]),]); + $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); $this->assertEquals(2019+2019, $sum); $sum = static::getDatabase()->sum('movies', 'year'); $this->assertEquals(2013+2019+2011+2019+2025, $sum); - $sum = static::getDatabase()->sum('movies', 'price', [new Query('year', Query::TYPE_EQUAL, [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); $this->assertEquals(round(39.50+25.99, 2), round($sum, 2)); - $sum = static::getDatabase()->sum('movies', 'price', [new Query('year', Query::TYPE_EQUAL, [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); $this->assertEquals(round(39.50+25.99, 2), round($sum, 2)); } diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index fde0c505e..25bee37d7 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -36,25 +36,25 @@ public function testParse() $query = Query::parse('lesser("year", 2001)'); $this->assertEquals('lesser', $query->getMethod()); - $this->assertContains('year', $query->getParams()[0]); + $this->assertEquals('year', $query->getParams()[0]); $this->assertEquals(2001, $query->getParams()[1]); $query = Query::parse('equal("published", true)'); $this->assertEquals('equal', $query->getMethod()); - $this->assertContains('published', $query->getParams()[0]); + $this->assertEquals('published', $query->getParams()[0]); $this->assertEquals(true, $query->getParams()[1]); $query = Query::parse('equal("published", false)'); $this->assertEquals('equal', $query->getMethod()); - $this->assertContains('published', $query->getParams()[0]); + $this->assertEquals('published', $query->getParams()[0]); $this->assertEquals(false, $query->getParams()[1]); $query = Query::parse('notContains("actors", [ " Johnny Depp ", " Brad Pitt" , \'Al Pacino \' ])'); - $this->assertEquals('actors', $query->getMethod()); - $this->assertContains('notContains', $query->getParams()[0]); + $this->assertEquals('notContains', $query->getMethod()); + $this->assertEquals('actors', $query->getParams()[0]); $this->assertEquals(" Johnny Depp ", $query->getParams()[1][0]); $this->assertEquals(" Brad Pitt", $query->getParams()[1][1]); $this->assertEquals("Al Pacino ", $query->getParams()[1][2]); @@ -62,32 +62,32 @@ public function testParse() $query = Query::parse('equal("actors", ["Brad Pitt", "Johnny Depp"])'); $this->assertEquals('equal', $query->getMethod()); - $this->assertContains('actors', $query->getParams()[0]); + $this->assertEquals('actors', $query->getParams()[0]); $this->assertEquals("Brad Pitt", $query->getParams()[1][0]); $this->assertEquals("Johnny Depp", $query->getParams()[1][1]); $query = Query::parse('contains("writers","Tim O\'Reilly")'); $this->assertEquals('contains', $query->getMethod()); - $this->assertContains('writers', $query->getParams()[0]); + $this->assertEquals('writers', $query->getParams()[0]); $this->assertEquals("Tim O'Reilly", $query->getParams()[1]); $query = Query::parse('greater("score", 8.5)'); $this->assertEquals('greater', $query->getMethod()); - $this->assertContains('score', $query->getParams()[0]); + $this->assertEquals('score', $query->getParams()[0]); $this->assertEquals(8.5, $query->getParams()[1]); $query = Query::parse('notEqual("director", "null")'); $this->assertEquals('notEqual', $query->getMethod()); - $this->assertContains('director', $query->getParams()[0]); + $this->assertEquals('director', $query->getParams()[0]); $this->assertEquals('null', $query->getParams()[1]); $query = Query::parse('notEqual("director", null)'); $this->assertEquals('notEqual', $query->getMethod()); - $this->assertContains('director', $query->getParams()[0]); + $this->assertEquals('director', $query->getParams()[0]); $this->assertEquals(null, $query->getParams()[1]); } @@ -145,10 +145,10 @@ public function testisMethod() $this->assertEquals(true, Query::isMethod('equal')); $this->assertEquals(true, Query::isMethod('notEqual')); - $this->assertEquals(true, Query::isMethod('lesser')); - $this->assertEquals(true, Query::isMethod('lesserEqual')); - $this->assertEquals(true, Query::isMethod('greater')); - $this->assertEquals(true, Query::isMethod('greaterEqual')); + $this->assertEquals(true, Query::isMethod('lessThan')); + $this->assertEquals(true, Query::isMethod('lessThanEqual')); + $this->assertEquals(true, Query::isMethod('greaterThan')); + $this->assertEquals(true, Query::isMethod('greaterThanEqual')); $this->assertEquals(true, Query::isMethod('contains')); $this->assertEquals(true, Query::isMethod('search')); $this->assertEquals(true, Query::isMethod('orderDesc')); diff --git a/tests/Database/Validator/QueriesTest.php b/tests/Database/Validator/QueriesTest.php index dbff66791..fc8e57502 100644 --- a/tests/Database/Validator/QueriesTest.php +++ b/tests/Database/Validator/QueriesTest.php @@ -175,7 +175,7 @@ public function testQueries() // test for FAILURE - $this->queries[] = Query::parse('rating.greater(4)'); + $this->queries[] = Query::parse('greater("rating", 4)'); $this->assertEquals(false, $validator->isValid($this->queries)); $this->assertEquals("Index not found: title,description,price,rating", $validator->getDescription()); diff --git a/tests/Database/Validator/QueryValidatorTest.php b/tests/Database/Validator/QueryValidatorTest.php index f3a741b5a..39193cd07 100644 --- a/tests/Database/Validator/QueryValidatorTest.php +++ b/tests/Database/Validator/QueryValidatorTest.php @@ -100,7 +100,7 @@ public function testQuery() $this->assertEquals(true, $validator->isValid(Query::parse('equal("$id", ["Iron Man", "Ant Man"])'))); $this->assertEquals(true, $validator->isValid(Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'))); $this->assertEquals(true, $validator->isValid(Query::parse('equal("description", "Best movie ever")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('greater("rating" 4)'))); + $this->assertEquals(true, $validator->isValid(Query::parse('greater("rating", 4)'))); $this->assertEquals(true, $validator->isValid(Query::parse('lesserEqual("price", 6.50)'))); $this->assertEquals(true, $validator->isValid(Query::parse('contains("tags", "action")'))); } From 847fff172c030f72034c594dd0bc8b7db93941cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 15 Jul 2022 17:03:19 +0000 Subject: [PATCH 05/27] Tests for new helpers --- tests/Database/QueryTest.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 25bee37d7..e1f6c557d 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -133,6 +133,28 @@ public function testGetAttribute() $this->assertEquals('Iron Man', $query->getParams()[1]); } + public function testHelperMethods() + { + $query = Query::parse('equal("title", "Iron Man")'); + + $this->assertEquals('title', $query->getFirstParam()); + $this->assertEquals('title', $query->getParams()[0]); + + $this->assertIsArray($query->getArrayParam(1)); + $this->assertCount(1, $query->getArrayParam(1)); + + + $query->setFirstParam("name"); + + $this->assertEquals('name', $query->getFirstParam()); + $this->assertEquals('name', $query->getParams()[0]); + + $query = Query::parse('equal("title", ["Iron Man", "Spider Man"])'); + + $this->assertIsArray($query->getArrayParam(1)); + $this->assertCount(2, $query->getArrayParam(1)); + } + public function testGetMethod() { $query = Query::parse('equal("title", "Iron Man")'); From 10384f3cc585b693865e2f0e8179a2378b459482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 22 Jul 2022 12:14:32 +0000 Subject: [PATCH 06/27] Refactor Queries parser --- src/Database/Query.php | 174 +++++++++++++++++++++++++++++------ tests/Database/QueryTest.php | 29 ++++++ 2 files changed, 175 insertions(+), 28 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index 0697c6b16..c21328dbb 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -126,62 +126,179 @@ public static function isMethod(string $value): bool * */ public static function parse(string $filter): Query { + // Init empty vars we fill later $method = ''; $params = []; - // Separate method and params + // Separate method from filter $paramsStart = mb_strpos($filter, '('); $method = mb_substr($filter, 0, $paramsStart); - // Remove everything after end of query - $paramsEnd = mb_strpos($filter, ')'); - $overflowChars = \strlen($filter) - 1 - $paramsEnd; - if($overflowChars > 0) { - $filter = substr($filter, 0, -1 * $overflowChars); - } + // Separate params from filter + $paramsEnd = \strlen($filter) - 1; // -1 to ignore ) + $parametersStart = $paramsStart + 1; // +1 to ignore ( // Check for deprecated query syntax if(\str_contains($method, '.')) { throw new \Exception("Invalid query method"); } - // Keep track of what hasn't been processed yet - $unprocessedFilter = substr($filter, $paramsStart + 1); + $currentParam = ""; // We build param here before pushing when it's ended + $currentArrayParam = []; // We build array param here before pushing when it's ended + $stack = []; // Stack of syntactical symbols - // While ends when we only have ')' - while(\strlen($unprocessedFilter) > 1) { - $arrayStart = mb_strpos($unprocessedFilter, '['); - $paramEnd = mb_strpos($unprocessedFilter, ','); + // Utility method to know if we are inside string + $isInStringStack = function() use (&$stack) { + if( + \count($stack) > 0 && // Stack is not empty + ($stack[\count($stack) - 1] === '"' || $stack[\count($stack) - 1] === '\'')) // Stack ends with string symbol + { + return true; + } + + return false; + }; + + // Utility method to know if we are inside array + $isInArrayStack = function() use (&$stack) { + if( + \count($stack) > 0 && // Stack is not empty + $stack[\count($stack) - 1] === '[') // Stack ends with array symbol + { + return true; + } - // Array parameter support - if($arrayStart !== false && $arrayStart < $paramEnd) { - $paramEnd = mb_strpos($unprocessedFilter, ']') + 1; + return false; + }; + + // Utility method to only add symbol is relevant + $addSymbol = function (string $char, int $index) use (&$filter, &$currentParam, $isInStringStack) { + $nextChar = $filter[$index + 1] ?? ''; + if( + $char === '\\' && // Current char might be escaping + ($nextChar === '"' || $nextChar === '\'') // Next char must be string syntax symbol + ) { + return; } - // No comma found, this is last param - if($paramEnd === false) { - $paramEnd = \strlen($unprocessedFilter) - 1; + // Ignore spaces and commas outside of string + if($char === ' ' || $char === ',') { + if(\call_user_func($isInStringStack)) { + $currentParam .= $char; + } + } else { + $currentParam .= $char; + } + }; + + // Loop thorough all characters + for($i = $parametersStart; $i < $paramsEnd; $i++) { + $char = $filter[$i]; + + // String support + escaping support + if( + ($char === '"' || $char === '\'') && // Must be string indicator + $filter[$i - 1] !== '\\') // Must not be escaped; first cant be + { + if(\call_user_func($isInStringStack)) { + // Dont mix-up string symbols. Only allow the same as on start + if($char === $stack[\count($stack) - 1]) { + // End of string + \array_pop($stack); + } + + // Either way, add symbol to builder + \call_user_func($addSymbol, $char, $i); + } else { + // Start of string + $stack[] = $char; + \call_user_func($addSymbol, $char, $i); + } + + continue; } - // Extract parameter from correct place - $param = mb_substr($unprocessedFilter, 0, $paramEnd); - $param = \trim($param); + // Array support + if(!(\call_user_func($isInStringStack))) { + if($char === '[') { + // Start of array + $stack[] = $char; + continue; + } else if($char === ']') { + // End of array + \array_pop($stack); + + if(!empty($currentParam)) { + $currentArrayParam[] = $currentParam; + } + + $params[] = $currentArrayParam; + $currentArrayParam = []; + $currentParam = ""; + + continue; + } + } - // Empty parameter means comma without anything after. We ignore such empty parameter - if(!empty($param)) { - $params[] = self::parseParam($param); + // Params separation support + if($char === ',') { + // Only consider it end of param if stack doesn't end with string + if(!(\call_user_func($isInStringStack))) { + // If in array stack, dont merge yet, just mark in array param builder + if(\call_user_func($isInArrayStack)) { + $currentArrayParam[] = $currentParam; + $currentParam = ""; + } else { + // Append from parap builder. Either value, or array + if(!empty($currentArrayParam)) { + // Do nothing, it's done in ] check + } else { + if(!empty($currentParam)) { + $params[] = $currentParam; + } + + $currentParam = ""; + } + } + + } } - // Shorten unprocessed list until it finishes - $unprocessedFilter = substr($unprocessedFilter, $paramEnd + 1); + // Value, not relevant to syntax + \call_user_func($addSymbol, $char, $i); } - return new Query($method, $params); + if(!empty($currentParam)) { + $params[] = $currentParam; + $currentParam = ""; + } + + $parsedParams = []; + + foreach($params as $param) { + // If array, parse each child separatelly + if(\is_array($param)) { + $arr = []; + + foreach($param as $element) { + $arr[] = self::parseParam($element); + } + + $parsedParams[] = $arr; + } else { + $parsedParams[] = self::parseParam($param); + } + } + + + return new Query($method, $parsedParams); } public static function parseParam(string $param) { $param = \trim($param); + + /* // Array param if(\str_starts_with($param, '[')) { $param = substr($param, 1, -1); // Remove [ and ] @@ -194,6 +311,7 @@ public static function parseParam(string $param) { return $array; } + */ // Numeric param if(\is_numeric($param)) { diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index e1f6c557d..76ddd0d39 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -91,6 +91,35 @@ public function testParse() $this->assertEquals(null, $query->getParams()[1]); } + public function testParseV2() { + $query = Query::parse('equal(1)'); + $this->assertEquals(1, $query->getParams()[0]); + + $query = Query::parse('equal(1, ["[Hello] World"])'); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("[Hello] World", $query->getParams()[1][0]); + + $query = Query::parse('equal(1, ["(Hello) World"])'); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("(Hello) World", $query->getParams()[1][0]); + + $query = Query::parse('equal(1, ["Hello , World"])'); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("Hello , World", $query->getParams()[1][0]); + + $query = Query::parse('equal(1, ["Hello , World"])'); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("Hello , World", $query->getParams()[1][0]); + + $query = Query::parse('equal(1, ["Hello /\ World"])'); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("Hello /\ World", $query->getParams()[1][0]); + + $query = Query::parse('equal(1, ["I\'m [**awesome**], \"Dev\"eloper"])'); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("I'm [**awesome**], \"Dev\"eloper", $query->getParams()[1][0]); + } + public function testParseComplex() { $queries = [ From 091e10c60755391bf93bf2a2db1cb4fe2d4833b5 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 22 Jul 2022 15:37:22 +0200 Subject: [PATCH 07/27] feat: torstens review --- src/Database/Query.php | 232 +++++++++++++++++++---------------- tests/Database/QueryTest.php | 112 +++++++++-------- 2 files changed, 187 insertions(+), 157 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index c21328dbb..1344dc9dc 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -24,8 +24,6 @@ class Query const TYPE_CURSORAFTER = 'cursorAfter'; const TYPE_CURSORBEFORE = 'cursorBefore'; - public static mixed $TYPE_ALIASES; // Filled from constructor - protected string $method = ''; protected array $params = []; @@ -70,11 +68,11 @@ public function setFirstParam(mixed $value): void */ public function getArrayParam(int $index): array { - if(\is_array($this->params[$index])) { + if (\is_array($this->params[$index])) { return $this->params[$index]; } - return [ $this->params[$index] ]; + return [$this->params[$index]]; } public function setMethod(string $method): self @@ -96,7 +94,7 @@ public function setParams(array $params): self */ public static function isMethod(string $value): bool { - switch ($value) { + switch (static::getMethodFromAlias($value)) { case self::TYPE_EQUAL: case self::TYPE_NOTEQUAL: case self::TYPE_LESSER: @@ -114,10 +112,6 @@ public static function isMethod(string $value): bool return true; } - if(\array_key_exists($value, self::$TYPE_ALIASES)) { - return true; - } - return false; } @@ -139,7 +133,7 @@ public static function parse(string $filter): Query $parametersStart = $paramsStart + 1; // +1 to ignore ( // Check for deprecated query syntax - if(\str_contains($method, '.')) { + if (\str_contains($method, '.')) { throw new \Exception("Invalid query method"); } @@ -147,88 +141,47 @@ public static function parse(string $filter): Query $currentArrayParam = []; // We build array param here before pushing when it's ended $stack = []; // Stack of syntactical symbols - // Utility method to know if we are inside string - $isInStringStack = function() use (&$stack) { - if( - \count($stack) > 0 && // Stack is not empty - ($stack[\count($stack) - 1] === '"' || $stack[\count($stack) - 1] === '\'')) // Stack ends with string symbol - { - return true; - } - - return false; - }; - - // Utility method to know if we are inside array - $isInArrayStack = function() use (&$stack) { - if( - \count($stack) > 0 && // Stack is not empty - $stack[\count($stack) - 1] === '[') // Stack ends with array symbol - { - return true; - } - - return false; - }; - - // Utility method to only add symbol is relevant - $addSymbol = function (string $char, int $index) use (&$filter, &$currentParam, $isInStringStack) { - $nextChar = $filter[$index + 1] ?? ''; - if( - $char === '\\' && // Current char might be escaping - ($nextChar === '"' || $nextChar === '\'') // Next char must be string syntax symbol - ) { - return; - } - - // Ignore spaces and commas outside of string - if($char === ' ' || $char === ',') { - if(\call_user_func($isInStringStack)) { - $currentParam .= $char; - } - } else { - $currentParam .= $char; - } - }; + //TODO: make util methods part of the class // Loop thorough all characters - for($i = $parametersStart; $i < $paramsEnd; $i++) { + for ($i = $parametersStart; $i < $paramsEnd; $i++) { $char = $filter[$i]; // String support + escaping support - if( - ($char === '"' || $char === '\'') && // Must be string indicator - $filter[$i - 1] !== '\\') // Must not be escaped; first cant be + if ( + (\in_array($char, ['"', '\''])) && // Must be string indicator + $filter[$i - 1] !== '\\' + ) // Must not be escaped; first cant be { - if(\call_user_func($isInStringStack)) { + if (static::isInStringStack($stack)) { // Dont mix-up string symbols. Only allow the same as on start - if($char === $stack[\count($stack) - 1]) { + if ($char === $stack[\count($stack) - 1]) { // End of string \array_pop($stack); } // Either way, add symbol to builder - \call_user_func($addSymbol, $char, $i); + static::appendSymbol($stack, $char, $i, $filter, $currentParam); } else { // Start of string $stack[] = $char; - \call_user_func($addSymbol, $char, $i); + static::appendSymbol($stack, $char, $i, $filter, $currentParam); } continue; } // Array support - if(!(\call_user_func($isInStringStack))) { - if($char === '[') { + if (!(static::isInStringStack($stack))) { + if ($char === '[') { // Start of array $stack[] = $char; continue; - } else if($char === ']') { + } else if ($char === ']') { // End of array \array_pop($stack); - if(!empty($currentParam)) { + if (!empty($currentParam)) { $currentArrayParam[] = $currentParam; } @@ -241,112 +194,173 @@ public static function parse(string $filter): Query } // Params separation support - if($char === ',') { + if ($char === ',') { // Only consider it end of param if stack doesn't end with string - if(!(\call_user_func($isInStringStack))) { - // If in array stack, dont merge yet, just mark in array param builder - if(\call_user_func($isInArrayStack)) { + if (!static::isInStringStack($stack)) { + // If in array stack, dont merge yet, just mark it in array param builder + if (static::isInArrayStack($stack)) { $currentArrayParam[] = $currentParam; $currentParam = ""; } else { // Append from parap builder. Either value, or array - if(!empty($currentArrayParam)) { - // Do nothing, it's done in ] check - } else { - if(!empty($currentParam)) { + if (empty($currentArrayParam)) { + if (!empty($currentParam)) { $params[] = $currentParam; } $currentParam = ""; } } - } } // Value, not relevant to syntax - \call_user_func($addSymbol, $char, $i); + static::appendSymbol($stack, $char, $i, $filter, $currentParam); } - if(!empty($currentParam)) { + if (!empty($currentParam)) { $params[] = $currentParam; $currentParam = ""; } $parsedParams = []; - foreach($params as $param) { + foreach ($params as $param) { // If array, parse each child separatelly - if(\is_array($param)) { - $arr = []; - - foreach($param as $element) { + if (\is_array($param)) { + foreach ($param as $element) { $arr[] = self::parseParam($element); } - $parsedParams[] = $arr; + $parsedParams[] = $arr ?? []; } else { $parsedParams[] = self::parseParam($param); } } - + $method = static::getMethodFromAlias($method); return new Query($method, $parsedParams); } - public static function parseParam(string $param) { - $param = \trim($param); + /** + * Utility method to know if we are inside String. + * + * @param array $stack + * @return bool + */ + protected static function isInStringStack(array $stack): bool + { + if (\count($stack) > 0 && \in_array($stack[\count($stack) - 1], ['"', '\''])) // Stack ends with string symbol ' or " + { + return true; + } + return false; + } - /* - // Array param - if(\str_starts_with($param, '[')) { - $param = substr($param, 1, -1); // Remove [ and ] + /** + * Utility method to know if we are inside Array. + * + * @param array $stack + * @return bool + */ + protected static function isInArrayStack(array $stack): bool + { + if ( + \count($stack) > 0 && // Stack is not empty + $stack[\count($stack) - 1] === '[' + ) // Stack ends with array symbol + { + return true; + } - $array = []; + return false; + } - foreach (\explode(',', $param) as $value) { - $array[] = self::parseParam($value); - } + /** + * Utility method to only append symbol if relevant. + * + * @param array $stack + * @param string $char + * @param int $index + * @param string $filter + * @param string $currentParam + * @return void + */ + protected static function appendSymbol(array $stack, string $char, int $index, string $filter, string &$currentParam): void + { + $nextChar = $filter[$index + 1] ?? ''; + if ( + $char === '\\' && // Current char might be escaping + (\in_array($nextChar, ['"', '\''])) // Next char must be string syntax symbol + ) { + return; + } - return $array; + // Ignore spaces and commas outside of string + if (\in_array($char, [' ', ','])) { + if (static::isInStringStack($stack)) { + $currentParam .= $char; + } + } else { + $currentParam .= $char; } - */ + } + + /** + * Parses param value. + * + * @param string $param + * @return mixed + */ + protected static function parseParam(string $param) + { + $param = \trim($param); // Numeric param - if(\is_numeric($param)) { + if (\is_numeric($param)) { // Cast to number return $param + 0; } // Boolean param - if($param === 'false') { + if ($param === 'false') { return false; - } else if($param === 'true') { + } else if ($param === 'true') { return true; } // Null param - if($param === 'null') { + if ($param === 'null') { return null; } // String param - if(\str_starts_with($param, '"') || \str_starts_with($param, '\'')) { + if (\str_starts_with($param, '"') || \str_starts_with($param, '\'')) { $param = substr($param, 1, -1); // Remove '' or "" + return $param; } // Unknown format return $param; } -} -Query::$TYPE_ALIASES = [ - 'lt' => fn(array $params) => [new Query(Query::TYPE_LESSER, $params)], - 'lte' => fn(array $params) => [new Query(Query::TYPE_LESSEREQUAL, $params)], - 'gt' => fn(array $params) => [new Query(Query::TYPE_GREATER, $params)], - 'gte' => fn(array $params) => [new Query(Query::TYPE_GREATEREQUAL, $params)], - 'eq' => fn(array $params) => [new Query(Query::TYPE_EQUAL, $params)], - 'page' => fn(array $params) => [new Query(Query::TYPE_LIMIT, [$params[1]]), new Query(Query::TYPE_OFFSET, [($params[0]-1)*$params[1]])], -]; \ No newline at end of file + /** + * Returns Method from Alias. + * + * @param string $method + * @return string + */ + static protected function getMethodFromAlias(string $method): string + { + return match ($method) { + 'lt' => Query::TYPE_LESSER, + 'lte' => Query::TYPE_LESSEREQUAL, + 'gt' => Query::TYPE_GREATER, + 'gte' => Query::TYPE_GREATEREQUAL, + 'eq' => Query::TYPE_EQUAL, + default => $method + }; + } +} diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 76ddd0d39..84f77b038 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -32,8 +32,8 @@ public function testParse() $this->assertEquals('equal', $query->getMethod()); $this->assertEquals('title', $query->getParams()[0]); $this->assertEquals('Iron Man', $query->getParams()[1]); - - $query = Query::parse('lesser("year", 2001)'); + + $query = Query::parse('lesser("year", 2001)'); $this->assertEquals('lesser', $query->getMethod()); $this->assertEquals('year', $query->getParams()[0]); @@ -43,13 +43,13 @@ public function testParse() $this->assertEquals('equal', $query->getMethod()); $this->assertEquals('published', $query->getParams()[0]); - $this->assertEquals(true, $query->getParams()[1]); + $this->assertTrue($query->getParams()[1]); $query = Query::parse('equal("published", false)'); $this->assertEquals('equal', $query->getMethod()); $this->assertEquals('published', $query->getParams()[0]); - $this->assertEquals(false, $query->getParams()[1]); + $this->assertFalse($query->getParams()[1]); $query = Query::parse('notContains("actors", [ " Johnny Depp ", " Brad Pitt" , \'Al Pacino \' ])'); @@ -91,7 +91,8 @@ public function testParse() $this->assertEquals(null, $query->getParams()[1]); } - public function testParseV2() { + public function testParseV2() + { $query = Query::parse('equal(1)'); $this->assertEquals(1, $query->getParams()[0]); @@ -120,6 +121,21 @@ public function testParseV2() { $this->assertEquals("I'm [**awesome**], \"Dev\"eloper", $query->getParams()[1][0]); } + public function testAlias() + { + $query = Query::parse('eq(1)'); + $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); + $query = Query::parse('lt(1)'); + $this->assertEquals(Query::TYPE_LESSER, $query->getMethod()); + $query = Query::parse('lte(1)'); + $this->assertEquals(Query::TYPE_LESSEREQUAL, $query->getMethod()); + $query = Query::parse('gt(1)'); + $this->assertEquals(Query::TYPE_GREATER, $query->getMethod()); + $query = Query::parse('gte(1)'); + $this->assertEquals(Query::TYPE_GREATEREQUAL, $query->getMethod()); + + } + public function testParseComplex() { $queries = [ @@ -131,10 +147,10 @@ public function testParseComplex() foreach ($queries as $query) { $this->assertEquals('equal', $query->getMethod()); $this->assertCount(5, $query->getParams()); - + $this->assertIsString($query->getParams()[0]); $this->assertEquals('One', $query->getParams()[0]); - + $this->assertIsNumeric($query->getParams()[1]); $this->assertEquals(3, $query->getParams()[1]); @@ -145,9 +161,9 @@ public function testParseComplex() $this->assertIsString($query->getParams()[2][1]); $this->assertEquals('Works', $query->getParams()[2][1]); $this->assertTrue($query->getParams()[2][2]); - + $this->assertFalse($query->getParams()[3]); - + $this->assertNull($query->getParams()[4]); } } @@ -194,43 +210,43 @@ public function testGetMethod() public function testisMethod() { - $this->assertEquals(true, Query::isMethod('equal')); - $this->assertEquals(true, Query::isMethod('notEqual')); - $this->assertEquals(true, Query::isMethod('lessThan')); - $this->assertEquals(true, Query::isMethod('lessThanEqual')); - $this->assertEquals(true, Query::isMethod('greaterThan')); - $this->assertEquals(true, Query::isMethod('greaterThanEqual')); - $this->assertEquals(true, Query::isMethod('contains')); - $this->assertEquals(true, Query::isMethod('search')); - $this->assertEquals(true, Query::isMethod('orderDesc')); - $this->assertEquals(true, Query::isMethod('orderAsc')); - $this->assertEquals(true, Query::isMethod('limit')); - $this->assertEquals(true, Query::isMethod('offset')); - $this->assertEquals(true, Query::isMethod('cursorAfter')); - $this->assertEquals(true, Query::isMethod('cursorBefore')); - - $this->assertEquals(true, Query::isMethod(Query::TYPE_EQUAL)); - $this->assertEquals(true, Query::isMethod(Query::TYPE_NOTEQUAL)); - $this->assertEquals(true, Query::isMethod(Query::TYPE_LESSER)); - $this->assertEquals(true, Query::isMethod(Query::TYPE_LESSEREQUAL)); - $this->assertEquals(true, Query::isMethod(Query::TYPE_GREATER)); - $this->assertEquals(true, Query::isMethod(Query::TYPE_GREATEREQUAL)); - $this->assertEquals(true, Query::isMethod(Query::TYPE_CONTAINS)); - $this->assertEquals(true, Query::isMethod(QUERY::TYPE_SEARCH)); - $this->assertEquals(true, Query::isMethod(QUERY::TYPE_ORDERASC)); - $this->assertEquals(true, Query::isMethod(QUERY::TYPE_ORDERDESC)); - $this->assertEquals(true, Query::isMethod(QUERY::TYPE_LIMIT)); - $this->assertEquals(true, Query::isMethod(QUERY::TYPE_OFFSET)); - $this->assertEquals(true, Query::isMethod(QUERY::TYPE_CURSORAFTER)); - $this->assertEquals(true, Query::isMethod(QUERY::TYPE_CURSORBEFORE)); - - $this->assertEquals(true, Query::isMethod('lt')); - $this->assertEquals(true, Query::isMethod('lte')); - $this->assertEquals(true, Query::isMethod('gt')); - $this->assertEquals(true, Query::isMethod('gte')); - $this->assertEquals(true, Query::isMethod('eq')); - $this->assertEquals(true, Query::isMethod('page')); - - $this->assertEquals(false, Query::isMethod('invalid')); + $this->assertTrue(Query::isMethod('equal')); + $this->assertTrue(Query::isMethod('notEqual')); + $this->assertTrue(Query::isMethod('lessThan')); + $this->assertTrue(Query::isMethod('lessThanEqual')); + $this->assertTrue(Query::isMethod('greaterThan')); + $this->assertTrue(Query::isMethod('greaterThanEqual')); + $this->assertTrue(Query::isMethod('contains')); + $this->assertTrue(Query::isMethod('search')); + $this->assertTrue(Query::isMethod('orderDesc')); + $this->assertTrue(Query::isMethod('orderAsc')); + $this->assertTrue(Query::isMethod('limit')); + $this->assertTrue(Query::isMethod('offset')); + $this->assertTrue(Query::isMethod('cursorAfter')); + $this->assertTrue(Query::isMethod('cursorBefore')); + + $this->assertTrue(Query::isMethod(Query::TYPE_EQUAL)); + $this->assertTrue(Query::isMethod(Query::TYPE_NOTEQUAL)); + $this->assertTrue(Query::isMethod(Query::TYPE_LESSER)); + $this->assertTrue(Query::isMethod(Query::TYPE_LESSEREQUAL)); + $this->assertTrue(Query::isMethod(Query::TYPE_GREATER)); + $this->assertTrue(Query::isMethod(Query::TYPE_GREATEREQUAL)); + $this->assertTrue(Query::isMethod(Query::TYPE_CONTAINS)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_SEARCH)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_ORDERASC)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_ORDERDESC)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_LIMIT)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_OFFSET)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSORAFTER)); + $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSORBEFORE)); + + $this->assertTrue(Query::isMethod('lt')); + $this->assertTrue(Query::isMethod('lte')); + $this->assertTrue(Query::isMethod('gt')); + $this->assertTrue(Query::isMethod('gte')); + $this->assertTrue(Query::isMethod('eq')); + + $this->assertFalse(Query::isMethod('invalid')); + $this->assertFalse(Query::isMethod('lte ')); } -} \ No newline at end of file +} From b8689016e783e17f9a1e7163bf1185b550ebc114 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 22 Jul 2022 15:55:23 +0200 Subject: [PATCH 08/27] feat: use const for used characters --- src/Database/Query.php | 73 +++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index 1344dc9dc..ccfa16543 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -24,6 +24,16 @@ class Query const TYPE_CURSORAFTER = 'cursorAfter'; const TYPE_CURSORBEFORE = 'cursorBefore'; + protected const CHAR_ALL_QUOTES = [self::CHAR_SINGLE_QUOTE, self::CHAR_DOUBLE_QUOTE]; + protected const CHAR_SINGLE_QUOTE = '\''; + protected const CHAR_DOUBLE_QUOTE = '"'; + protected const CHAR_COMMA = ','; + protected const CHAR_SPACE = ' '; + protected const CHAR_BRACKET_START = '['; + protected const CHAR_BRACKET_END = ']'; + protected const CHAR_PARENTHESES_START = '('; + protected const CHAR_PARENTHESES_END = ')'; + protected string $method = ''; protected array $params = []; @@ -49,22 +59,32 @@ public function getParams(): array /** * Helper method returning first param. In first param we often store attribute + * + * @return null|string */ - public function getFirstParam(): mixed + public function getFirstParam(): ?string { - return $this->params[0]; + return $this->params[0] ?? null; } /** * Helper method changing first param. In first param we often store attribute + * + * @param string $value + * @return self */ - public function setFirstParam(mixed $value): void + public function setFirstParam(string $value): self { $this->params[0] = $value; + + return $this; } /** - * Helper method. Returns param, but in form of array array + * Helper method. Returns param, but in form of array array. + * + * @param int $index + * @return array */ public function getArrayParam(int $index): array { @@ -75,6 +95,11 @@ public function getArrayParam(int $index): array return [$this->params[$index]]; } + /** + * Sets Method. + * @param string $method + * @return self + */ public function setMethod(string $method): self { $this->method = $method; @@ -82,6 +107,11 @@ public function setMethod(string $method): self return $this; } + /** + * Sets Param. + * @param array $params + * @return self + */ public function setParams(array $params): self { $this->params = $params; @@ -91,6 +121,9 @@ public function setParams(array $params): self /** * Check if method is supported + * + * @param string $value + * @return bool */ public static function isMethod(string $value): bool { @@ -117,15 +150,19 @@ public static function isMethod(string $value): bool /** * Parse query filter - * */ - public static function parse(string $filter): Query + * + * @param string $filter + * @return self + * @throws \Exception + */ + public static function parse(string $filter): self { // Init empty vars we fill later $method = ''; $params = []; // Separate method from filter - $paramsStart = mb_strpos($filter, '('); + $paramsStart = mb_strpos($filter, static::CHAR_PARENTHESES_START); $method = mb_substr($filter, 0, $paramsStart); // Separate params from filter @@ -141,15 +178,13 @@ public static function parse(string $filter): Query $currentArrayParam = []; // We build array param here before pushing when it's ended $stack = []; // Stack of syntactical symbols - //TODO: make util methods part of the class - // Loop thorough all characters for ($i = $parametersStart; $i < $paramsEnd; $i++) { $char = $filter[$i]; // String support + escaping support if ( - (\in_array($char, ['"', '\''])) && // Must be string indicator + (\in_array($char, static::CHAR_ALL_QUOTES)) && // Must be string indicator $filter[$i - 1] !== '\\' ) // Must not be escaped; first cant be { @@ -173,11 +208,11 @@ public static function parse(string $filter): Query // Array support if (!(static::isInStringStack($stack))) { - if ($char === '[') { + if ($char === static::CHAR_BRACKET_START) { // Start of array $stack[] = $char; continue; - } else if ($char === ']') { + } else if ($char === static::CHAR_BRACKET_END) { // End of array \array_pop($stack); @@ -194,7 +229,7 @@ public static function parse(string $filter): Query } // Params separation support - if ($char === ',') { + if ($char === static::CHAR_COMMA) { // Only consider it end of param if stack doesn't end with string if (!static::isInStringStack($stack)) { // If in array stack, dont merge yet, just mark it in array param builder @@ -250,7 +285,7 @@ public static function parse(string $filter): Query */ protected static function isInStringStack(array $stack): bool { - if (\count($stack) > 0 && \in_array($stack[\count($stack) - 1], ['"', '\''])) // Stack ends with string symbol ' or " + if (\count($stack) > 0 && \in_array($stack[\count($stack) - 1], static::CHAR_ALL_QUOTES)) // Stack ends with string symbol ' or " { return true; } @@ -268,7 +303,7 @@ protected static function isInArrayStack(array $stack): bool { if ( \count($stack) > 0 && // Stack is not empty - $stack[\count($stack) - 1] === '[' + $stack[\count($stack) - 1] === static::CHAR_BRACKET_START ) // Stack ends with array symbol { return true; @@ -292,13 +327,13 @@ protected static function appendSymbol(array $stack, string $char, int $index, s $nextChar = $filter[$index + 1] ?? ''; if ( $char === '\\' && // Current char might be escaping - (\in_array($nextChar, ['"', '\''])) // Next char must be string syntax symbol + (\in_array($nextChar, static::CHAR_ALL_QUOTES)) // Next char must be string syntax symbol ) { return; } // Ignore spaces and commas outside of string - if (\in_array($char, [' ', ','])) { + if (\in_array($char, [static::CHAR_SPACE, static::CHAR_COMMA])) { if (static::isInStringStack($stack)) { $currentParam .= $char; } @@ -313,7 +348,7 @@ protected static function appendSymbol(array $stack, string $char, int $index, s * @param string $param * @return mixed */ - protected static function parseParam(string $param) + protected static function parseParam(string $param): mixed { $param = \trim($param); @@ -336,7 +371,7 @@ protected static function parseParam(string $param) } // String param - if (\str_starts_with($param, '"') || \str_starts_with($param, '\'')) { + if (\str_starts_with($param, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($param, static::CHAR_SINGLE_QUOTE)) { $param = substr($param, 1, -1); // Remove '' or "" return $param; From d3e287cc95371f01035742d7c17d5b516e80fa94 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 22 Jul 2022 16:01:44 +0200 Subject: [PATCH 09/27] tests: add more query tests --- tests/Database/QueryTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 84f77b038..9842daf4d 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -94,29 +94,41 @@ public function testParse() public function testParseV2() { $query = Query::parse('equal(1)'); + $this->assertCount(1, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); $query = Query::parse('equal(1, ["[Hello] World"])'); + $this->assertCount(2, $query->getParams()); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("[Hello] World", $query->getParams()[1][0]); + + $query = Query::parse('equal(1, , , ["[Hello] World"], , , )'); + $this->assertCount(2, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("[Hello] World", $query->getParams()[1][0]); $query = Query::parse('equal(1, ["(Hello) World"])'); + $this->assertCount(2, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("(Hello) World", $query->getParams()[1][0]); $query = Query::parse('equal(1, ["Hello , World"])'); + $this->assertCount(2, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("Hello , World", $query->getParams()[1][0]); $query = Query::parse('equal(1, ["Hello , World"])'); + $this->assertCount(2, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("Hello , World", $query->getParams()[1][0]); $query = Query::parse('equal(1, ["Hello /\ World"])'); + $this->assertCount(2, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("Hello /\ World", $query->getParams()[1][0]); $query = Query::parse('equal(1, ["I\'m [**awesome**], \"Dev\"eloper"])'); + $this->assertCount(2, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("I'm [**awesome**], \"Dev\"eloper", $query->getParams()[1][0]); } From 8172d6681433553d33a5615d7c0cc9abd0c06c31 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 22 Jul 2022 16:13:33 +0200 Subject: [PATCH 10/27] tests: fix tests for keywords --- tests/Database/Adapter/MariaDBTest.php | 2 +- tests/Database/Adapter/MongoDBTest.php | 4 ++-- tests/Database/Adapter/MySQLTest.php | 2 +- tests/Database/Base.php | 4 ++-- tests/Database/QueryTest.php | 3 --- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/Database/Adapter/MariaDBTest.php b/tests/Database/Adapter/MariaDBTest.php index d1a7d601d..379c8bd48 100644 --- a/tests/Database/Adapter/MariaDBTest.php +++ b/tests/Database/Adapter/MariaDBTest.php @@ -40,7 +40,7 @@ static function getAdapterRowLimit(): int } /** - * @return Adapter + * @return Database */ static function getDatabase(): Database { diff --git a/tests/Database/Adapter/MongoDBTest.php b/tests/Database/Adapter/MongoDBTest.php index efc3ca145..7842c6a9e 100644 --- a/tests/Database/Adapter/MongoDBTest.php +++ b/tests/Database/Adapter/MongoDBTest.php @@ -50,7 +50,7 @@ static function getAdapterRowLimit(): int } /** - * @return Adapter + * @return Database */ static function getDatabase(): Database { @@ -113,7 +113,7 @@ public function testListDocumentSearch(Document $document) 'empty' => [], ])); - $documents = static::getDatabase()->find('documents', [ new Query('string', Query::TYPE_SEARCH, ['*test+alias@email-provider.com']) ]); + $documents = static::getDatabase()->find('documents', [ new Query(Query::TYPE_SEARCH, ['string', '*test+alias@email-provider.com']) ]); $this->assertEquals(1, count($documents)); diff --git a/tests/Database/Adapter/MySQLTest.php b/tests/Database/Adapter/MySQLTest.php index 405042d66..f49e71faf 100644 --- a/tests/Database/Adapter/MySQLTest.php +++ b/tests/Database/Adapter/MySQLTest.php @@ -49,7 +49,7 @@ static function getUsedIndexes(): int } /** - * @reture Adapter + * @return Database */ static function getDatabase(): Database { diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 7ae06b1c0..7173dec3d 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -17,7 +17,7 @@ abstract class Base extends TestCase { /** - * @return Adapter + * @return Database */ abstract static protected function getDatabase(): Database; @@ -2432,7 +2432,7 @@ public function testReservedKeywords() { $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); $this->assertEquals('Reserved:' . $keyword, $documents[0]->getAttribute($keyword)); - $documents = $database->find($collectionName, [ new Query($keyword, Query::TYPE_EQUAL, ["Reserved:${keyword}"]) ]); + $documents = $database->find($collectionName, [ new Query(Query::TYPE_EQUAL, [$keyword, "Reserved:${keyword}"]) ]); $this->assertCount(1, $documents); $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 9842daf4d..090949972 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -145,7 +145,6 @@ public function testAlias() $this->assertEquals(Query::TYPE_GREATER, $query->getMethod()); $query = Query::parse('gte(1)'); $this->assertEquals(Query::TYPE_GREATEREQUAL, $query->getMethod()); - } public function testParseComplex() @@ -200,7 +199,6 @@ public function testHelperMethods() $this->assertIsArray($query->getArrayParam(1)); $this->assertCount(1, $query->getArrayParam(1)); - $query->setFirstParam("name"); $this->assertEquals('name', $query->getFirstParam()); @@ -221,7 +219,6 @@ public function testGetMethod() public function testisMethod() { - $this->assertTrue(Query::isMethod('equal')); $this->assertTrue(Query::isMethod('notEqual')); $this->assertTrue(Query::isMethod('lessThan')); From 8abcc1602dd3e41ac7b9dcdbb00d6d4132f13dba Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 22 Jul 2022 17:28:38 +0200 Subject: [PATCH 11/27] feat: add bench --- composer.json | 3 +- composer.lock | 727 +++++++++++++++++++++++- phpbench.json | 4 + src/Database/QueryV1.php | 245 ++++++++ tests/Database/Benchmark/QueryBench.php | 44 ++ 5 files changed, 1014 insertions(+), 9 deletions(-) create mode 100644 phpbench.json create mode 100644 src/Database/QueryV1.php create mode 100644 tests/Database/Benchmark/QueryBench.php diff --git a/composer.json b/composer.json index 9dd959a29..f52e8ca09 100755 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ "phpunit/phpunit": "^9.4", "swoole/ide-helper": "4.8.0", "utopia-php/cli": "^0.11.0", - "vimeo/psalm": "4.0.1" + "vimeo/psalm": "4.0.1", + "phpbench/phpbench": "1.2.0" } } diff --git a/composer.lock b/composer.lock index 67478e81f..4e7861f7b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d960db719a704a1cbd556d510e3708d4", + "content-hash": "a88312360d181c46f87ff0a0eb6e7827", "packages": [ { "name": "composer/package-versions-deprecated", @@ -737,6 +737,79 @@ }, "time": "2019-12-04T15:06:13+00:00" }, + { + "name": "doctrine/annotations", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^6.0 || ^8.1", + "phpstan/phpstan": "^1.4.10 || ^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", + "symfony/cache": "^4.4 || ^5.2", + "vimeo/psalm": "^4.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.13.3" + }, + "time": "2022-07-02T10:48:51+00:00" + }, { "name": "doctrine/instantiator", "version": "1.4.1", @@ -807,18 +880,94 @@ ], "time": "2022-03-03T08:28:38+00:00" }, + { + "name": "doctrine/lexer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-02-28T11:07:21+00:00" + }, { "name": "fakerphp/faker", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "d7f08a622b3346766325488aa32ddc93ccdecc75" + "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/d7f08a622b3346766325488aa32ddc93ccdecc75", - "reference": "d7f08a622b3346766325488aa32ddc93ccdecc75", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/37f751c67a5372d4e26353bd9384bc03744ec77b", + "reference": "37f751c67a5372d4e26353bd9384bc03744ec77b", "shasum": "" }, "require": { @@ -845,7 +994,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "v1.19-dev" + "dev-main": "v1.20-dev" } }, "autoload": { @@ -870,9 +1019,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.19.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.20.0" }, - "time": "2022-02-02T17:38:57+00:00" + "time": "2022-07-20T13:12:54+00:00" }, { "name": "felixfbecker/advanced-json-rpc", @@ -1305,6 +1454,197 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpbench/container", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpbench/container.git", + "reference": "6d555ff7174fca13f9b1ec0b4a089ed41d0ab392" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/container/zipball/6d555ff7174fca13f9b1ec0b4a089ed41d0ab392", + "reference": "6d555ff7174fca13f9b1ec0b4a089ed41d0ab392", + "shasum": "" + }, + "require": { + "psr/container": "^1.0|^2.0", + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.52", + "phpunit/phpunit": "^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\DependencyInjection\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "Simple, configurable, service container.", + "support": { + "issues": "https://github.com/phpbench/container/issues", + "source": "https://github.com/phpbench/container/tree/2.2.1" + }, + "time": "2022-01-25T10:17:35+00:00" + }, + { + "name": "phpbench/dom", + "version": "0.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpbench/dom.git", + "reference": "b013b717832ddbaadf2a40984b04bc66af9a7110" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/dom/zipball/b013b717832ddbaadf2a40984b04bc66af9a7110", + "reference": "b013b717832ddbaadf2a40984b04bc66af9a7110", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^7.2||^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.18", + "phpstan/phpstan": "^0.12.83", + "phpunit/phpunit": "^8.0||^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\Dom\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "DOM wrapper to simplify working with the PHP DOM implementation", + "support": { + "issues": "https://github.com/phpbench/dom/issues", + "source": "https://github.com/phpbench/dom/tree/0.3.2" + }, + "time": "2021-09-24T15:26:07+00:00" + }, + { + "name": "phpbench/phpbench", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpbench/phpbench.git", + "reference": "3555dff668e58d25c39d287f3f1bac13a7817b4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/3555dff668e58d25c39d287f3f1bac13a7817b4c", + "reference": "3555dff668e58d25c39d287f3f1bac13a7817b4c", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.13", + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "ext-tokenizer": "*", + "php": "^7.3 || ^8.0", + "phpbench/container": "^2.1", + "phpbench/dom": "~0.3.1", + "psr/log": "^1.1", + "seld/jsonlint": "^1.1", + "symfony/console": "^4.2 || ^5.0", + "symfony/filesystem": "^4.2 || ^5.0", + "symfony/finder": "^4.2 || ^5.0", + "symfony/options-resolver": "^4.2 || ^5.0", + "symfony/process": "^4.2 || ^5.0", + "webmozart/path-util": "^2.3" + }, + "require-dev": { + "dantleech/invoke": "^2.0", + "friendsofphp/php-cs-fixer": "^3.0", + "jangregor/phpstan-prophecy": "^0.8.1", + "phpspec/prophecy": "^1.12", + "phpstan/phpstan": "^0.12.7", + "phpunit/phpunit": "^8.5.8 || ^9.0", + "symfony/error-handler": "^5.2", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "ext-xdebug": "For Xdebug profiling extension." + }, + "bin": [ + "bin/phpbench" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "files": [ + "lib/Report/Func/functions.php" + ], + "psr-4": { + "PhpBench\\": "lib/", + "PhpBench\\Extensions\\XDebug\\": "extensions/xdebug/lib/", + "PhpBench\\Extensions\\Reports\\": "extensions/reports/lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "PHP Benchmarking Framework", + "support": { + "issues": "https://github.com/phpbench/phpbench/issues", + "source": "https://github.com/phpbench/phpbench/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/dantleech", + "type": "github" + } + ], + "time": "2021-11-06T13:52:05+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -1952,6 +2292,55 @@ ], "time": "2022-06-19T12:14:25+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -3019,6 +3408,70 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "seld/jsonlint", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "4211420d25eba80712bff236a98960ef68b866b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/4211420d25eba80712bff236a98960ef68b866b7", + "reference": "4211420d25eba80712bff236a98960ef68b866b7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2022-04-01T13:37:23+00:00" + }, { "name": "swoole/ide-helper", "version": "4.8.0", @@ -3227,6 +3680,202 @@ ], "time": "2022-02-25T11:15:52+00:00" }, + { + "name": "symfony/filesystem", + "version": "v5.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "36a017fa4cce1eff1b8e8129ff53513abcef05ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/36a017fa4cce1eff1b8e8129ff53513abcef05ba", + "reference": "36a017fa4cce1eff1b8e8129ff53513abcef05ba", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-20T13:55:35+00:00" + }, + { + "name": "symfony/finder", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/9b630f3427f3ebe7cd346c277a1408b00249dad9", + "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-15T08:07:45+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/cc1147cb11af1b43f503ac18f31aa3bec213aba8", + "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "~1.0", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.26.0", @@ -3636,6 +4285,68 @@ ], "time": "2022-05-24T11:49:31+00:00" }, + { + "name": "symfony/process", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-08T05:07:18+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.1.1", diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 000000000..664605231 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,4 @@ +{ + "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "vendor/autoload.php" +} diff --git a/src/Database/QueryV1.php b/src/Database/QueryV1.php new file mode 100644 index 000000000..4f9f224a4 --- /dev/null +++ b/src/Database/QueryV1.php @@ -0,0 +1,245 @@ +attribute = $attribute; + $this->operator = $operator; + $this->values = $values; + } + + /** + * Get attribute + * + * @return string + */ + public function getAttribute(): string + { + return $this->attribute; + } + + /** + * Get operator + * + * @return string + */ + public function getOperator(): string + { + return $this->operator; + } + + /** + * Get operand + * + * @return array + */ + public function getValues(): array + { + return $this->values; + } + + /** + * Get all query details as array + * + * @return array + */ + public function getQuery(): array + { + return [ + 'attribute' => $this->attribute, + 'operator' => $this->operator, + 'values' => $this->values, + ]; + } + + /** + * Set attribute + * @param string $attribute + * @return QueryV1 + */ + public function setAttribute(string $attribute): self + { + $this->attribute = $attribute; + + return $this; + } + + /** + * Set operator + * @param string $operator + * @return QueryV1 + */ + public function setOperator(string $operator): self + { + $this->operator = $operator; + + return $this; + } + + /** + * Set operand + * @param array $values + * @return QueryV1 + */ + public function setValues(array $values): self + { + $this->values = $values; + + return $this; + } + + /** + * Check if operator is supported + * @param string $value + * @return bool + */ + public static function isOperator(string $value): bool + { + switch ($value) { + case self::TYPE_EQUAL: + case self::TYPE_NOTEQUAL: + case self::TYPE_LESSER: + case self::TYPE_LESSEREQUAL: + case self::TYPE_GREATER: + case self::TYPE_GREATEREQUAL: + case self::TYPE_CONTAINS: + case self::TYPE_SEARCH: + return true; + default: + return false; + } + } + + /** + * Parse query filter + * + * @param string $filter + * + * @return QueryV1 + * */ + public static function parse(string $filter): QueryV1 + { + $attribute = ''; + $operator = ''; + $values = []; + + // get index of open parentheses + $end = intval(mb_strpos($filter, '(')); + + // count stanzas by only counting '.' that come before open parentheses + $stanzas = mb_substr_count(mb_substr($filter, 0, $end), ".") + 1; + + // TODO@kodumbeats handle relations between collections, e.g. if($stanzas > 2) + switch ($stanzas) { + case 2: + // use limit param to ignore '.' in $expression + $input = explode('.', $filter, $stanzas); + $attribute = $input[0]; + $expression = $input[1]; + [$operator, $values] = self::parseExpression($expression); + break; + } + + return new QueryV1($attribute, $operator, $values); + } + + /** + * Get attribute key-value from query expression + * $expression: string with format 'operator(value)' + * + * @param string $expression + * + * @return array + */ + protected static function parseExpression(string $expression): array + { + //find location of parentheses in expression + + /** @var int */ + $start = mb_strpos($expression, '('); + /** @var int */ + $end = mb_strrpos($expression, ')'); + + //extract the query method + $operator = mb_substr($expression, 0, $start); + + //grab everything inside parentheses + $value = mb_substr( + $expression, + ($start + 1), /* exclude open paren*/ + ($end - $start - 1) /* exclude closed paren*/ + ); + + // Explode comma-separated values + $values = explode(',', $value); + + // Cast $value type + $values = array_map(function ($value) { + + // Trim whitespace from around $value + + $value = trim($value); + + switch (true) { + // type casted to int or float by "+" operator + case is_numeric($value): + return $value + 0; + + // since (bool)"false" returns true, check bools manually + case $value === 'true': + return true; + + case $value === 'false': + return false; + + // need special case to cast (null) as null, not string + case $value === 'null': + return null; + + default: + // strip escape characters + $value = stripslashes($value); + // trim leading and tailing quotes + return trim($value, '\'"'); + } + }, $values); + + return [$operator, $values]; + } +} diff --git a/tests/Database/Benchmark/QueryBench.php b/tests/Database/Benchmark/QueryBench.php new file mode 100644 index 000000000..095648916 --- /dev/null +++ b/tests/Database/Benchmark/QueryBench.php @@ -0,0 +1,44 @@ + ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 1)]; + yield '2 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 2)]; + yield '4 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 4)]; + yield '8 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 8)]; + yield '16 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 16)]; + yield '32 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 32)]; + yield '64 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 64)]; + yield '128 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 128)]; + } + + protected function generateRandomString(int $length = 10) + { + return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length); + } + + #[Bench\Revs(1000)] + #[Bench\Iterations(10)] + #[Bench\ParamProviders('provideAttributes')] + public function benchV1(array $params) + { + QueryV1::parse("actors.equal({$params['attributes']})"); + } + + #[Bench\Revs(1000)] + #[Bench\Iterations(10)] + #[Bench\ParamProviders('provideAttributes')] + public function benchV2(array $params) + { + Query::parse("equal('actors', [{$params['attributes']}])"); + } +} From 7bdd75447063ee97a2d3c27f63d39d78ee3ad686 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Fri, 22 Jul 2022 17:32:47 +0200 Subject: [PATCH 12/27] add typing --- tests/Database/Benchmark/QueryBench.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Database/Benchmark/QueryBench.php b/tests/Database/Benchmark/QueryBench.php index 095648916..bf5460cd4 100644 --- a/tests/Database/Benchmark/QueryBench.php +++ b/tests/Database/Benchmark/QueryBench.php @@ -21,7 +21,7 @@ public function provideAttributes(): Generator yield '128 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 128)]; } - protected function generateRandomString(int $length = 10) + protected function generateRandomString(int $length = 10): string { return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length); } From c500c4e3b48e03fa27bf16d12087edb07f1c6b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 23 Jul 2022 09:33:24 +0000 Subject: [PATCH 13/27] Improve performance of parser --- src/Database/Query.php | 118 +++++++++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index ccfa16543..d20939533 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -24,7 +24,6 @@ class Query const TYPE_CURSORAFTER = 'cursorAfter'; const TYPE_CURSORBEFORE = 'cursorBefore'; - protected const CHAR_ALL_QUOTES = [self::CHAR_SINGLE_QUOTE, self::CHAR_DOUBLE_QUOTE]; protected const CHAR_SINGLE_QUOTE = '\''; protected const CHAR_DOUBLE_QUOTE = '"'; protected const CHAR_COMMA = ','; @@ -182,13 +181,22 @@ public static function parse(string $filter): self for ($i = $parametersStart; $i < $paramsEnd; $i++) { $char = $filter[$i]; + $isStringStack = static::isInStringStack($stack); + + // If not special symbol, continue asap + $isSpecialSymbol = self::isSpecialChar($char); + if(!$isSpecialSymbol) { + static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); + continue; + } + // String support + escaping support if ( - (\in_array($char, static::CHAR_ALL_QUOTES)) && // Must be string indicator - $filter[$i - 1] !== '\\' - ) // Must not be escaped; first cant be + (self::isQuote($char)) && // Must be string indicator + $filter[$i - 1] !== '\\' // Must not be escaped; first cant be + ) { - if (static::isInStringStack($stack)) { + if ($isStringStack) { // Dont mix-up string symbols. Only allow the same as on start if ($char === $stack[\count($stack) - 1]) { // End of string @@ -196,18 +204,18 @@ public static function parse(string $filter): self } // Either way, add symbol to builder - static::appendSymbol($stack, $char, $i, $filter, $currentParam); + static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } else { // Start of string $stack[] = $char; - static::appendSymbol($stack, $char, $i, $filter, $currentParam); + static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } continue; } // Array support - if (!(static::isInStringStack($stack))) { + if (!($isStringStack)) { if ($char === static::CHAR_BRACKET_START) { // Start of array $stack[] = $char; @@ -225,13 +233,7 @@ public static function parse(string $filter): self $currentParam = ""; continue; - } - } - - // Params separation support - if ($char === static::CHAR_COMMA) { - // Only consider it end of param if stack doesn't end with string - if (!static::isInStringStack($stack)) { + } else if ($char === static::CHAR_COMMA) { // Params separation support // If in array stack, dont merge yet, just mark it in array param builder if (static::isInArrayStack($stack)) { $currentArrayParam[] = $currentParam; @@ -242,15 +244,16 @@ public static function parse(string $filter): self if (!empty($currentParam)) { $params[] = $currentParam; } - + $currentParam = ""; } } + continue; } } // Value, not relevant to syntax - static::appendSymbol($stack, $char, $i, $filter, $currentParam); + static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } if (!empty($currentParam)) { @@ -272,6 +275,7 @@ public static function parse(string $filter): self $parsedParams[] = self::parseParam($param); } } + $method = static::getMethodFromAlias($method); return new Query($method, $parsedParams); @@ -285,7 +289,9 @@ public static function parse(string $filter): self */ protected static function isInStringStack(array $stack): bool { - if (\count($stack) > 0 && \in_array($stack[\count($stack) - 1], static::CHAR_ALL_QUOTES)) // Stack ends with string symbol ' or " + $stackLength = \count($stack); + + if ($stackLength > 0 && self::isQuote($stack[$stackLength - 1])) // Stack ends with string symbol ' or " { return true; } @@ -301,10 +307,9 @@ protected static function isInStringStack(array $stack): bool */ protected static function isInArrayStack(array $stack): bool { - if ( - \count($stack) > 0 && // Stack is not empty - $stack[\count($stack) - 1] === static::CHAR_BRACKET_START - ) // Stack ends with array symbol + $stackLength = \count($stack); + + if ($stackLength > 0 && $stack[$stackLength - 1] === static::CHAR_BRACKET_START) // Stack ends with array symbol { return true; } @@ -322,19 +327,27 @@ protected static function isInArrayStack(array $stack): bool * @param string $currentParam * @return void */ - protected static function appendSymbol(array $stack, string $char, int $index, string $filter, string &$currentParam): void + protected static function appendSymbol(bool $isStringStack, string $char, int $index, string $filter, string &$currentParam): void { $nextChar = $filter[$index + 1] ?? ''; if ( $char === '\\' && // Current char might be escaping - (\in_array($nextChar, static::CHAR_ALL_QUOTES)) // Next char must be string syntax symbol + self::isQuote($nextChar) // Next char must be string syntax symbol ) { return; } // Ignore spaces and commas outside of string - if (\in_array($char, [static::CHAR_SPACE, static::CHAR_COMMA])) { - if (static::isInStringStack($stack)) { + $canBeIgnored = false; + + if($char === static::CHAR_SPACE) { + $canBeIgnored = true; + } else if($char === static::CHAR_COMMA) { + $canBeIgnored = true; + } + + if ($canBeIgnored) { + if ($isStringStack) { $currentParam .= $char; } } else { @@ -342,6 +355,32 @@ protected static function appendSymbol(array $stack, string $char, int $index, s } } + protected static function isQuote(string $char) { + if($char === self::CHAR_SINGLE_QUOTE) { + return true; + } else if($char === self::CHAR_DOUBLE_QUOTE) { + return true; + } + + return false; + } + + protected static function isSpecialChar(string $char) { + if($char === static::CHAR_COMMA) { + return true; + } else if($char === static::CHAR_BRACKET_END) { + return true; + } else if($char === static::CHAR_BRACKET_START) { + return true; + } else if($char === static::CHAR_DOUBLE_QUOTE) { + return true; + } else if($char === static::CHAR_SINGLE_QUOTE) { + return true; + } + + return false; + } + /** * Parses param value. * @@ -351,29 +390,18 @@ protected static function appendSymbol(array $stack, string $char, int $index, s protected static function parseParam(string $param): mixed { $param = \trim($param); - - // Numeric param - if (\is_numeric($param)) { - // Cast to number - return $param + 0; - } - - // Boolean param - if ($param === 'false') { + + if ($param === 'false') { // Boolean param return false; } else if ($param === 'true') { return true; - } - - // Null param - if ($param === 'null') { + } else if ($param === 'null') { // Null param return null; - } - - // String param - if (\str_starts_with($param, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($param, static::CHAR_SINGLE_QUOTE)) { - $param = substr($param, 1, -1); // Remove '' or "" - + } else if (\is_numeric($param)) { // Numeric param + // Cast to number + return $param + 0; + } else if (\str_starts_with($param, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($param, static::CHAR_SINGLE_QUOTE)) { // String param + $param = \substr($param, 1, -1); // Remove '' or "" return $param; } From 029dad427afd7bf570251a352c0440931699a87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 23 Jul 2022 11:12:45 +0000 Subject: [PATCH 14/27] Improve performance of query parser again --- src/Database/Query.php | 83 +++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 46 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index d20939533..56824925d 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -177,19 +177,16 @@ public static function parse(string $filter): self $currentArrayParam = []; // We build array param here before pushing when it's ended $stack = []; // Stack of syntactical symbols + $stringStackState = false; + $arrayStackState = false; + // Loop thorough all characters for ($i = $parametersStart; $i < $paramsEnd; $i++) { $char = $filter[$i]; - $isStringStack = static::isInStringStack($stack); - - // If not special symbol, continue asap - $isSpecialSymbol = self::isSpecialChar($char); - if(!$isSpecialSymbol) { - static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); - continue; - } - + $isStringStack = $stringStackState; + $isArrayStack = $arrayStackState; + // String support + escaping support if ( (self::isQuote($char)) && // Must be string indicator @@ -201,6 +198,19 @@ public static function parse(string $filter): self if ($char === $stack[\count($stack) - 1]) { // End of string \array_pop($stack); + + $stackCount = \count($stack); + if ($stackCount > 0) { + $lastChar = $stack[\count($stack) - 1]; + $stringStackState = $lastChar === static::CHAR_SINGLE_QUOTE || $lastChar === static::CHAR_DOUBLE_QUOTE; + + if(!$stringStackState) { + $arrayStackState = $lastChar === static::CHAR_BRACKET_START; + } + } else { + $stringStackState = false; + $arrayStackState = false; + } } // Either way, add symbol to builder @@ -208,6 +218,8 @@ public static function parse(string $filter): self } else { // Start of string $stack[] = $char; + $stringStackState = true; + $arrayStackState = false; static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } @@ -219,11 +231,26 @@ public static function parse(string $filter): self if ($char === static::CHAR_BRACKET_START) { // Start of array $stack[] = $char; + $stringStackState = false; + $arrayStackState = true; continue; } else if ($char === static::CHAR_BRACKET_END) { // End of array \array_pop($stack); + $stackCount = \count($stack); + if ($stackCount > 0) { + $lastChar = $stack[\count($stack) - 1]; + $stringStackState = $lastChar === static::CHAR_SINGLE_QUOTE || $lastChar === static::CHAR_DOUBLE_QUOTE; + + if(!$stringStackState) { + $arrayStackState = $lastChar === static::CHAR_BRACKET_START; + } + } else { + $stringStackState = false; + $arrayStackState = false; + } + if (!empty($currentParam)) { $currentArrayParam[] = $currentParam; } @@ -235,7 +262,7 @@ public static function parse(string $filter): self continue; } else if ($char === static::CHAR_COMMA) { // Params separation support // If in array stack, dont merge yet, just mark it in array param builder - if (static::isInArrayStack($stack)) { + if ($isArrayStack) { $currentArrayParam[] = $currentParam; $currentParam = ""; } else { @@ -281,42 +308,6 @@ public static function parse(string $filter): self return new Query($method, $parsedParams); } - /** - * Utility method to know if we are inside String. - * - * @param array $stack - * @return bool - */ - protected static function isInStringStack(array $stack): bool - { - $stackLength = \count($stack); - - if ($stackLength > 0 && self::isQuote($stack[$stackLength - 1])) // Stack ends with string symbol ' or " - { - return true; - } - - return false; - } - - /** - * Utility method to know if we are inside Array. - * - * @param array $stack - * @return bool - */ - protected static function isInArrayStack(array $stack): bool - { - $stackLength = \count($stack); - - if ($stackLength > 0 && $stack[$stackLength - 1] === static::CHAR_BRACKET_START) // Stack ends with array symbol - { - return true; - } - - return false; - } - /** * Utility method to only append symbol if relevant. * From 1ff03f3b8d96876eb947e1f2053ff0561d96f734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 24 Jul 2022 05:47:19 +0000 Subject: [PATCH 15/27] Rename from operator to method --- composer.lock | 138 ------------------ src/Database/Adapter/MariaDB.php | 34 +++-- src/Database/Adapter/Mongo/MongoDBAdapter.php | 24 +-- src/Database/Validator/OrderAttributes.php | 4 +- src/Database/Validator/Queries.php | 6 +- src/Database/Validator/QueryValidator.php | 12 +- tests/Database/Base.php | 2 +- tests/Database/Validator/QueriesTest.php | 2 +- .../Database/Validator/QueryValidatorTest.php | 8 +- 9 files changed, 47 insertions(+), 183 deletions(-) diff --git a/composer.lock b/composer.lock index 4e7861f7b..2da598925 100644 --- a/composer.lock +++ b/composer.lock @@ -880,82 +880,6 @@ ], "time": "2022-03-03T08:28:38+00:00" }, - { - "name": "doctrine/lexer", - "version": "1.2.3", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", - "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.3" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], - "time": "2022-02-28T11:07:21+00:00" - }, { "name": "fakerphp/faker", "version": "v1.20.0", @@ -4285,68 +4209,6 @@ ], "time": "2022-05-24T11:49:31+00:00" }, - { - "name": "symfony/process", - "version": "v5.4.8", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", - "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.4.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-04-08T05:07:18+00:00" - }, { "name": "symfony/service-contracts", "version": "v3.1.1", diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index e256c0de6..b34668822 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -805,21 +805,21 @@ public function find(string $collection, array $queries = [], int $limit = 25, i // Get most dominant/first order attribute if ($i === 0 && !empty($cursor)) { - $orderOperatorInternalId = Query::TYPE_GREATER; // To preserve natural order - $orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + $orderMethodInternalId = Query::TYPE_GREATER; // To preserve natural order + $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; if ($cursorDirection === Database::CURSOR_BEFORE) { $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - $orderOperatorInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; - $orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + $orderMethodInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; } $where[] = "( - table_main.{$attribute} {$this->getSQLOperator($orderOperator)} :cursor + table_main.{$attribute} {$this->getSQLOperator($orderMethod)} :cursor OR ( table_main.{$attribute} = :cursor AND - table_main._id {$this->getSQLOperator($orderOperatorInternalId)} {$cursor['$internalId']} + table_main._id {$this->getSQLOperator($orderMethodInternalId)} {$cursor['$internalId']} ) )"; } else if ($cursorDirection === Database::CURSOR_BEFORE) { @@ -832,10 +832,10 @@ public function find(string $collection, array $queries = [], int $limit = 25, i // Allow after pagination without any order if (empty($orderAttributes) && !empty($cursor)) { $orderType = $orderTypes[0] ?? Database::ORDER_ASC; - $orderOperator = $cursorDirection === Database::CURSOR_AFTER ? ($orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER + $orderMethod = $cursorDirection === Database::CURSOR_AFTER ? ($orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER ) : ($orderType === Database::ORDER_DESC ? Query::TYPE_GREATER : Query::TYPE_LESSER ); - $where[] = "( table_main._id {$this->getSQLOperator($orderOperator)} {$cursor['$internalId']} )"; + $where[] = "( table_main._id {$this->getSQLOperator($orderMethod)} {$cursor['$internalId']} )"; } // Allow order type without any order attribute, fallback to the natural order (_id) @@ -1364,15 +1364,15 @@ protected function getSQLType(string $type, int $size, bool $signed = true): str * Get SQL Conditions * * @param string $attribute - * @param string $operator + * @param string $method * @param string $placeholder * @param mixed $value * @return string * @throws Exception */ - protected function getSQLCondition(string $attribute, string $operator, string $placeholder, $value): string + protected function getSQLCondition(string $attribute, string $method, string $placeholder, $value): string { - switch ($operator) { + switch ($method) { case Query::TYPE_SEARCH: /** * Replace reserved chars with space. @@ -1387,20 +1387,21 @@ protected function getSQLCondition(string $attribute, string $operator, string $ return 'MATCH(' . $attribute . ') AGAINST(' . $this->getPDO()->quote($value) . ' IN BOOLEAN MODE)'; default: - return $attribute . ' ' . $this->getSQLOperator($operator) . ' ' . $placeholder; // Using `attrubute_` to avoid conflicts with custom names; + return $attribute . ' ' . $this->getSQLOperator($method) . ' ' . $placeholder; // Using `attrubute_` to avoid conflicts with custom names; + break; } } /** * Get SQL Operator * - * @param string $operator + * @param string $method * @return string * @throws Exception */ - protected function getSQLOperator(string $operator): string + protected function getSQLOperator(string $method): string { - switch ($operator) { + switch ($method) { case Query::TYPE_EQUAL: return '='; @@ -1420,7 +1421,8 @@ protected function getSQLOperator(string $operator): string return '>='; default: - throw new Exception('Unknown Operator:' . $operator); + throw new Exception('Unknown method:' . $method); + break; } } diff --git a/src/Database/Adapter/Mongo/MongoDBAdapter.php b/src/Database/Adapter/Mongo/MongoDBAdapter.php index e36830a25..6f8ecee97 100644 --- a/src/Database/Adapter/Mongo/MongoDBAdapter.php +++ b/src/Database/Adapter/Mongo/MongoDBAdapter.php @@ -494,13 +494,13 @@ public function find(string $collection, array $queries = [], int $limit = 25, i // Allow after pagination without any order if (!empty($cursor)) { $orderType = $orderTypes[0] ?? Database::ORDER_ASC; - $orderOperator = $cursorDirection === Database::CURSOR_AFTER ? ($orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER + $orderMethod = $cursorDirection === Database::CURSOR_AFTER ? ($orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER ) : ($orderType === Database::ORDER_DESC ? Query::TYPE_GREATER : Query::TYPE_LESSER ); $filters = array_merge($filters, [ '_id' => [ - $this->getQueryOperator($orderOperator) => new \MongoDB\BSON\ObjectId($cursor['$internalId']) + $this->getQueryOperator($orderMethod) => new \MongoDB\BSON\ObjectId($cursor['$internalId']) ] ]); } @@ -520,26 +520,26 @@ public function find(string $collection, array $queries = [], int $limit = 25, i throw new Exception("Order attribute '{$attribute}' is empty."); } - $orderOperatorInternalId = Query::TYPE_GREATER; + $orderMethodInternalId = Query::TYPE_GREATER; $orderType = $this->filter($orderTypes[0] ?? Database::ORDER_ASC); - $orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; if ($cursorDirection === Database::CURSOR_BEFORE) { $orderType = $orderType === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; - $orderOperatorInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; - $orderOperator = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + $orderMethodInternalId = $orderType === Database::ORDER_ASC ? Query::TYPE_LESSER : Query::TYPE_GREATER; + $orderMethod = $orderType === Database::ORDER_DESC ? Query::TYPE_LESSER : Query::TYPE_GREATER; } $filters = array_merge($filters, [ '$or' => [ [ $attribute => [ - $this->getQueryOperator($orderOperator) => $cursor[$attribute] + $this->getQueryOperator($orderMethod) => $cursor[$attribute] ] ], [ $attribute => $cursor[$attribute], '_id' => [ - $this->getQueryOperator($orderOperatorInternalId) => new \MongoDB\BSON\ObjectId($cursor['$internalId']) + $this->getQueryOperator($orderMethodInternalId) => new \MongoDB\BSON\ObjectId($cursor['$internalId']) ] ] @@ -764,13 +764,13 @@ protected function buildFilters($queries): array /** * Get Query Operator * - * @param string $operator + * @param string $method * * @return string */ - protected function getQueryOperator(string $operator): string + protected function getQueryOperator(string $method): string { - switch ($operator) { + switch ($method) { case Query::TYPE_EQUAL: return '$eq'; break; @@ -804,7 +804,7 @@ protected function getQueryOperator(string $operator): string break; default: - throw new Exception('Unknown Operator:' . $operator); + throw new Exception('Unknown method:' . $method); break; } } diff --git a/src/Database/Validator/OrderAttributes.php b/src/Database/Validator/OrderAttributes.php index 04a3d83e9..c1e9d3dd9 100644 --- a/src/Database/Validator/OrderAttributes.php +++ b/src/Database/Validator/OrderAttributes.php @@ -134,9 +134,9 @@ public function isValid($attributes): bool return false; } - // search operator requires fulltext index + // search method requires fulltext index if (in_array(Query::TYPE_SEARCH, $attributes) && $found['type'] !== Database::INDEX_FULLTEXT) { - $this->message = 'Search operator requires fulltext index: ' . implode(",", $attributes); + $this->message = 'Search method requires fulltext index: ' . implode(",", $attributes); return false; } } diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 3dc405349..7910fab3b 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -92,7 +92,7 @@ public function isValid($value): bool $queries = []; foreach ($value as $query) { - // [attribute => operator] + // [attribute => method] $queries[$query->getFirstParam()] = $query->getMethod(); if (!$this->validator->isValid($query)) { @@ -117,9 +117,9 @@ public function isValid($value): bool return false; } - // search operator requires fulltext index + // search method requires fulltext index if (in_array(Query::TYPE_SEARCH, array_values($queries)) && $found['type'] !== Database::INDEX_FULLTEXT) { - $this->message = 'Search operator requires fulltext index: ' . implode(",", array_keys($queries)); + $this->message = 'Search method requires fulltext index: ' . implode(",", array_keys($queries)); return false; } } diff --git a/src/Database/Validator/QueryValidator.php b/src/Database/Validator/QueryValidator.php index aab916878..a20fc8f07 100644 --- a/src/Database/Validator/QueryValidator.php +++ b/src/Database/Validator/QueryValidator.php @@ -22,7 +22,7 @@ class QueryValidator extends Validator /** * @var array */ - protected $operators = [ + protected $methods = [ 'equal', 'notEqual', 'lesser', @@ -89,9 +89,9 @@ public function getDescription(): string */ public function isValid($query): bool { - // Validate operator - if (!in_array($query->getMethod(), $this->operators)) { - $this->message = 'Query operator invalid: ' . $query->getMethod(); + // Validate method + if (!in_array($query->getMethod(), $this->methods)) { + $this->message = 'Query method invalid: ' . $query->getMethod(); return false; } @@ -113,9 +113,9 @@ public function isValid($query): bool } } - // Contains operator only supports array attributes + // Contains method only supports array attributes if (!$this->schema[$attributeIndex]['array'] && $query->getMethod() === Query::TYPE_CONTAINS) { - $this->message = 'Query operator only supported on array attributes: ' . $query->getMethod(); + $this->message = 'Query method only supported on array attributes: ' . $query->getMethod(); return false; } diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 7173dec3d..8691650cc 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -945,7 +945,7 @@ public function testFind(Document $document) ]); // TODO@kodumbeats hacky way to pass mariadb tests - // Remove when $operator="contains" is supported + // Remove when query method contains() is supported if (static::getAdapterName() === "mongodb") { /** diff --git a/tests/Database/Validator/QueriesTest.php b/tests/Database/Validator/QueriesTest.php index fc8e57502..2c98e9ffc 100644 --- a/tests/Database/Validator/QueriesTest.php +++ b/tests/Database/Validator/QueriesTest.php @@ -193,7 +193,7 @@ public function testQueries() $query3 = Query::parse('search("description", "iron")'); $this->queries = [$query3]; $this->assertEquals(false, $validator->isValid($this->queries)); - $this->assertEquals("Search operator requires fulltext index: description", $validator->getDescription()); + $this->assertEquals("Search method requires fulltext index: description", $validator->getDescription()); } public function testIsStrict() diff --git a/tests/Database/Validator/QueryValidatorTest.php b/tests/Database/Validator/QueryValidatorTest.php index 39193cd07..03c2e432b 100644 --- a/tests/Database/Validator/QueryValidatorTest.php +++ b/tests/Database/Validator/QueryValidatorTest.php @@ -105,14 +105,14 @@ public function testQuery() $this->assertEquals(true, $validator->isValid(Query::parse('contains("tags", "action")'))); } - public function testInvalidOperator() + public function testInvalidMethod() { $validator = new QueryValidator($this->schema); $response = $validator->isValid(Query::parse('eqqual("title", "Iron Man")')); $this->assertEquals(false, $response); - $this->assertEquals('Query operator invalid: eqqual', $validator->getDescription()); + $this->assertEquals('Query method invalid: eqqual', $validator->getDescription()); } public function testAttributeNotFound() @@ -135,13 +135,13 @@ public function testAttributeWrongType() $this->assertEquals('Query type does not match expected: string', $validator->getDescription()); } - public function testOperatorWrongType() + public function testMethodWrongType() { $validator = new QueryValidator($this->schema); $response = $validator->isValid(Query::parse('contains("title", "Iron")')); $this->assertEquals(false, $response); - $this->assertEquals('Query operator only supported on array attributes: contains', $validator->getDescription()); + $this->assertEquals('Query method only supported on array attributes: contains', $validator->getDescription()); } } From fd049f44bf5a77238a06641299a9a17f2d99186e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 24 Jul 2022 08:05:50 +0000 Subject: [PATCH 16/27] PR review changes --- composer.lock | 138 +++++++++++++++++++++++++++++++++++ src/Database/Query.php | 12 +-- src/Database/QueryV1.php | 4 +- tests/Database/QueryTest.php | 10 +++ 4 files changed, 156 insertions(+), 8 deletions(-) diff --git a/composer.lock b/composer.lock index 2da598925..4e7861f7b 100644 --- a/composer.lock +++ b/composer.lock @@ -880,6 +880,82 @@ ], "time": "2022-03-03T08:28:38+00:00" }, + { + "name": "doctrine/lexer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2022-02-28T11:07:21+00:00" + }, { "name": "fakerphp/faker", "version": "v1.20.0", @@ -4209,6 +4285,68 @@ ], "time": "2022-05-24T11:49:31+00:00" }, + { + "name": "symfony/process", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-08T05:07:18+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.1.1", diff --git a/src/Database/Query.php b/src/Database/Query.php index 56824925d..7d80b2602 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -305,7 +305,7 @@ public static function parse(string $filter): self $method = static::getMethodFromAlias($method); - return new Query($method, $parsedParams); + return new self($method, $parsedParams); } /** @@ -409,11 +409,11 @@ protected static function parseParam(string $param): mixed static protected function getMethodFromAlias(string $method): string { return match ($method) { - 'lt' => Query::TYPE_LESSER, - 'lte' => Query::TYPE_LESSEREQUAL, - 'gt' => Query::TYPE_GREATER, - 'gte' => Query::TYPE_GREATEREQUAL, - 'eq' => Query::TYPE_EQUAL, + 'lt' => self::TYPE_LESSER, + 'lte' => self::TYPE_LESSEREQUAL, + 'gt' => self::TYPE_GREATER, + 'gte' => self::TYPE_GREATEREQUAL, + 'eq' => self::TYPE_EQUAL, default => $method }; } diff --git a/src/Database/QueryV1.php b/src/Database/QueryV1.php index 4f9f224a4..4dc575333 100644 --- a/src/Database/QueryV1.php +++ b/src/Database/QueryV1.php @@ -153,7 +153,7 @@ public static function isOperator(string $value): bool * * @return QueryV1 * */ - public static function parse(string $filter): QueryV1 + public static function parse(string $filter): self { $attribute = ''; $operator = ''; @@ -176,7 +176,7 @@ public static function parse(string $filter): QueryV1 break; } - return new QueryV1($attribute, $operator, $values); + return new self($attribute, $operator, $values); } /** diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 090949972..84275f368 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -131,6 +131,16 @@ public function testParseV2() $this->assertCount(2, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("I'm [**awesome**], \"Dev\"eloper", $query->getParams()[1][0]); + + $query = Query::parse('equal(1, "\\\\")'); + $this->assertCount(2, $query->getParams()); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("\\\\", $query->getParams()[1]); + + $query = Query::parse('equal(1, "Hello\\\\World")'); + $this->assertCount(2, $query->getParams()); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("Hello\\\\World", $query->getParams()[1]); } public function testAlias() From 2154dfadb1cee4694f21520f238b2aeefeb916c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 24 Jul 2022 08:59:36 +0000 Subject: [PATCH 17/27] Fix backshash tests --- src/Database/Query.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index 7d80b2602..299648d5c 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -32,6 +32,7 @@ class Query protected const CHAR_BRACKET_END = ']'; protected const CHAR_PARENTHESES_START = '('; protected const CHAR_PARENTHESES_END = ')'; + protected const CHAR_BACKSLASH = '\\'; protected string $method = ''; @@ -190,7 +191,7 @@ public static function parse(string $filter): self // String support + escaping support if ( (self::isQuote($char)) && // Must be string indicator - $filter[$i - 1] !== '\\' // Must not be escaped; first cant be + $filter[$i - 1] !== static::CHAR_BACKSLASH // Must not be escaped; first cant be ) { if ($isStringStack) { @@ -321,9 +322,11 @@ public static function parse(string $filter): self protected static function appendSymbol(bool $isStringStack, string $char, int $index, string $filter, string &$currentParam): void { $nextChar = $filter[$index + 1] ?? ''; + $prevChar = $filter[$index - 1] ?? ''; if ( - $char === '\\' && // Current char might be escaping - self::isQuote($nextChar) // Next char must be string syntax symbol + $char === static::CHAR_BACKSLASH && // Current char might be escaping + self::isQuote($nextChar) && // Next char must be string syntax symbol + $prevChar !== static::CHAR_BACKSLASH // Current \ can't be escaped ) { return; } From b866d3d2e1db96630a0a0813074c95c245f63af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 25 Jul 2022 08:04:55 +0000 Subject: [PATCH 18/27] Escaping bug fix --- src/Database/Query.php | 2 +- tests/Database/QueryTest.php | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index 299648d5c..075aecf0c 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -191,7 +191,7 @@ public static function parse(string $filter): self // String support + escaping support if ( (self::isQuote($char)) && // Must be string indicator - $filter[$i - 1] !== static::CHAR_BACKSLASH // Must not be escaped; first cant be + ($filter[$i - 1] !== static::CHAR_BACKSLASH || $filter[$i - 2] === static::CHAR_BACKSLASH) // Must not be escaped; ) { if ($isStringStack) { diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 84275f368..3e7bb61b3 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -24,7 +24,7 @@ public function testCreate(): void $this->assertEquals('title', $query->getParams()[0]); $this->assertEquals('Iron Man', $query->getParams()[1]); } - + public function testParse() { $query = Query::parse('equal("title", "Iron Man")'); @@ -137,10 +137,16 @@ public function testParseV2() $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("\\\\", $query->getParams()[1]); - $query = Query::parse('equal(1, "Hello\\\\World")'); + $query = Query::parse('equal(1, "Hello\\\\")'); $this->assertCount(2, $query->getParams()); $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("Hello\\\\World", $query->getParams()[1]); + $this->assertEquals("Hello\\\\", $query->getParams()[1]); + + $query = Query::parse('equal(1, "Hello\\\\", "World")'); + $this->assertCount(3, $query->getParams()); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("Hello\\\\", $query->getParams()[1]); + $this->assertEquals("World", $query->getParams()[2]); } public function testAlias() From 1319d56a871c070ef675687cdced11d065c7252c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 25 Jul 2022 08:18:07 +0000 Subject: [PATCH 19/27] Separate string support from array support --- src/Database/Query.php | 48 +++++++++--------------------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/src/Database/Query.php b/src/Database/Query.php index 075aecf0c..72bb1767d 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -176,17 +176,17 @@ public static function parse(string $filter): self $currentParam = ""; // We build param here before pushing when it's ended $currentArrayParam = []; // We build array param here before pushing when it's ended - $stack = []; // Stack of syntactical symbols - $stringStackState = false; - $arrayStackState = false; + $stack = []; // State for stack of parentheses + $stackCount = 0; // Length of stack array. Kept as variable to improve performance + $stringStackState = null; // State for string support // Loop thorough all characters for ($i = $parametersStart; $i < $paramsEnd; $i++) { $char = $filter[$i]; - $isStringStack = $stringStackState; - $isArrayStack = $arrayStackState; + $isStringStack = $stringStackState !== null; + $isArrayStack = !$isStringStack && $stackCount > 0; // String support + escaping support if ( @@ -196,31 +196,16 @@ public static function parse(string $filter): self { if ($isStringStack) { // Dont mix-up string symbols. Only allow the same as on start - if ($char === $stack[\count($stack) - 1]) { + if ($char === $stringStackState) { // End of string - \array_pop($stack); - - $stackCount = \count($stack); - if ($stackCount > 0) { - $lastChar = $stack[\count($stack) - 1]; - $stringStackState = $lastChar === static::CHAR_SINGLE_QUOTE || $lastChar === static::CHAR_DOUBLE_QUOTE; - - if(!$stringStackState) { - $arrayStackState = $lastChar === static::CHAR_BRACKET_START; - } - } else { - $stringStackState = false; - $arrayStackState = false; - } + $stringStackState = null; } // Either way, add symbol to builder static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } else { // Start of string - $stack[] = $char; - $stringStackState = true; - $arrayStackState = false; + $stringStackState = $char; static::appendSymbol($isStringStack, $char, $i, $filter, $currentParam); } @@ -232,25 +217,12 @@ public static function parse(string $filter): self if ($char === static::CHAR_BRACKET_START) { // Start of array $stack[] = $char; - $stringStackState = false; - $arrayStackState = true; + $stackCount++; continue; } else if ($char === static::CHAR_BRACKET_END) { // End of array \array_pop($stack); - - $stackCount = \count($stack); - if ($stackCount > 0) { - $lastChar = $stack[\count($stack) - 1]; - $stringStackState = $lastChar === static::CHAR_SINGLE_QUOTE || $lastChar === static::CHAR_DOUBLE_QUOTE; - - if(!$stringStackState) { - $arrayStackState = $lastChar === static::CHAR_BRACKET_START; - } - } else { - $stringStackState = false; - $arrayStackState = false; - } + $stackCount--; if (!empty($currentParam)) { $currentArrayParam[] = $currentParam; From 33309761b9b26647880ce1be63bb7d68b5891bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 25 Jul 2022 11:50:47 +0000 Subject: [PATCH 20/27] Removed benchmarks --- src/Database/QueryV1.php | 245 ------------------------ tests/Database/Benchmark/QueryBench.php | 44 ----- 2 files changed, 289 deletions(-) delete mode 100644 src/Database/QueryV1.php delete mode 100644 tests/Database/Benchmark/QueryBench.php diff --git a/src/Database/QueryV1.php b/src/Database/QueryV1.php deleted file mode 100644 index 4dc575333..000000000 --- a/src/Database/QueryV1.php +++ /dev/null @@ -1,245 +0,0 @@ -attribute = $attribute; - $this->operator = $operator; - $this->values = $values; - } - - /** - * Get attribute - * - * @return string - */ - public function getAttribute(): string - { - return $this->attribute; - } - - /** - * Get operator - * - * @return string - */ - public function getOperator(): string - { - return $this->operator; - } - - /** - * Get operand - * - * @return array - */ - public function getValues(): array - { - return $this->values; - } - - /** - * Get all query details as array - * - * @return array - */ - public function getQuery(): array - { - return [ - 'attribute' => $this->attribute, - 'operator' => $this->operator, - 'values' => $this->values, - ]; - } - - /** - * Set attribute - * @param string $attribute - * @return QueryV1 - */ - public function setAttribute(string $attribute): self - { - $this->attribute = $attribute; - - return $this; - } - - /** - * Set operator - * @param string $operator - * @return QueryV1 - */ - public function setOperator(string $operator): self - { - $this->operator = $operator; - - return $this; - } - - /** - * Set operand - * @param array $values - * @return QueryV1 - */ - public function setValues(array $values): self - { - $this->values = $values; - - return $this; - } - - /** - * Check if operator is supported - * @param string $value - * @return bool - */ - public static function isOperator(string $value): bool - { - switch ($value) { - case self::TYPE_EQUAL: - case self::TYPE_NOTEQUAL: - case self::TYPE_LESSER: - case self::TYPE_LESSEREQUAL: - case self::TYPE_GREATER: - case self::TYPE_GREATEREQUAL: - case self::TYPE_CONTAINS: - case self::TYPE_SEARCH: - return true; - default: - return false; - } - } - - /** - * Parse query filter - * - * @param string $filter - * - * @return QueryV1 - * */ - public static function parse(string $filter): self - { - $attribute = ''; - $operator = ''; - $values = []; - - // get index of open parentheses - $end = intval(mb_strpos($filter, '(')); - - // count stanzas by only counting '.' that come before open parentheses - $stanzas = mb_substr_count(mb_substr($filter, 0, $end), ".") + 1; - - // TODO@kodumbeats handle relations between collections, e.g. if($stanzas > 2) - switch ($stanzas) { - case 2: - // use limit param to ignore '.' in $expression - $input = explode('.', $filter, $stanzas); - $attribute = $input[0]; - $expression = $input[1]; - [$operator, $values] = self::parseExpression($expression); - break; - } - - return new self($attribute, $operator, $values); - } - - /** - * Get attribute key-value from query expression - * $expression: string with format 'operator(value)' - * - * @param string $expression - * - * @return array - */ - protected static function parseExpression(string $expression): array - { - //find location of parentheses in expression - - /** @var int */ - $start = mb_strpos($expression, '('); - /** @var int */ - $end = mb_strrpos($expression, ')'); - - //extract the query method - $operator = mb_substr($expression, 0, $start); - - //grab everything inside parentheses - $value = mb_substr( - $expression, - ($start + 1), /* exclude open paren*/ - ($end - $start - 1) /* exclude closed paren*/ - ); - - // Explode comma-separated values - $values = explode(',', $value); - - // Cast $value type - $values = array_map(function ($value) { - - // Trim whitespace from around $value - - $value = trim($value); - - switch (true) { - // type casted to int or float by "+" operator - case is_numeric($value): - return $value + 0; - - // since (bool)"false" returns true, check bools manually - case $value === 'true': - return true; - - case $value === 'false': - return false; - - // need special case to cast (null) as null, not string - case $value === 'null': - return null; - - default: - // strip escape characters - $value = stripslashes($value); - // trim leading and tailing quotes - return trim($value, '\'"'); - } - }, $values); - - return [$operator, $values]; - } -} diff --git a/tests/Database/Benchmark/QueryBench.php b/tests/Database/Benchmark/QueryBench.php deleted file mode 100644 index bf5460cd4..000000000 --- a/tests/Database/Benchmark/QueryBench.php +++ /dev/null @@ -1,44 +0,0 @@ - ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 1)]; - yield '2 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 2)]; - yield '4 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 4)]; - yield '8 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 8)]; - yield '16 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 16)]; - yield '32 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 32)]; - yield '64 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 64)]; - yield '128 Attributes' => ['attributes' => str_repeat('"' . $this->generateRandomString() . '",', 128)]; - } - - protected function generateRandomString(int $length = 10): string - { - return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length); - } - - #[Bench\Revs(1000)] - #[Bench\Iterations(10)] - #[Bench\ParamProviders('provideAttributes')] - public function benchV1(array $params) - { - QueryV1::parse("actors.equal({$params['attributes']})"); - } - - #[Bench\Revs(1000)] - #[Bench\Iterations(10)] - #[Bench\ParamProviders('provideAttributes')] - public function benchV2(array $params) - { - Query::parse("equal('actors', [{$params['attributes']}])"); - } -} From f01a300a13e48669135dbe1394cfe7e5f4466286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 25 Jul 2022 11:53:44 +0000 Subject: [PATCH 21/27] Remove more benchmark stuff --- composer.json | 3 +-- phpbench.json | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 phpbench.json diff --git a/composer.json b/composer.json index f52e8ca09..9dd959a29 100755 --- a/composer.json +++ b/composer.json @@ -35,7 +35,6 @@ "phpunit/phpunit": "^9.4", "swoole/ide-helper": "4.8.0", "utopia-php/cli": "^0.11.0", - "vimeo/psalm": "4.0.1", - "phpbench/phpbench": "1.2.0" + "vimeo/psalm": "4.0.1" } } diff --git a/phpbench.json b/phpbench.json deleted file mode 100644 index 664605231..000000000 --- a/phpbench.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", - "runner.bootstrap": "vendor/autoload.php" -} From 88c5772f556616eaabefba3c483e71c8b45e6c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 25 Jul 2022 16:07:55 +0000 Subject: [PATCH 22/27] Fix yet another edge case with backslashes --- composer.lock | 713 +---------------------------------- src/Database/Query.php | 21 +- tests/Database/QueryTest.php | 11 + 3 files changed, 23 insertions(+), 722 deletions(-) diff --git a/composer.lock b/composer.lock index 4e7861f7b..ec28dd9f5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a88312360d181c46f87ff0a0eb6e7827", + "content-hash": "d960db719a704a1cbd556d510e3708d4", "packages": [ { "name": "composer/package-versions-deprecated", @@ -737,79 +737,6 @@ }, "time": "2019-12-04T15:06:13+00:00" }, - { - "name": "doctrine/annotations", - "version": "1.13.3", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "648b0343343565c4a056bfc8392201385e8d89f0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0", - "reference": "648b0343343565c4a056bfc8392201385e8d89f0", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "ext-tokenizer": "*", - "php": "^7.1 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" - }, - "require-dev": { - "doctrine/cache": "^1.11 || ^2.0", - "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^1.4.10 || ^1.8.0", - "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2", - "vimeo/psalm": "^4.10" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.3" - }, - "time": "2022-07-02T10:48:51+00:00" - }, { "name": "doctrine/instantiator", "version": "1.4.1", @@ -880,82 +807,6 @@ ], "time": "2022-03-03T08:28:38+00:00" }, - { - "name": "doctrine/lexer", - "version": "1.2.3", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", - "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.3" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", - "type": "tidelift" - } - ], - "time": "2022-02-28T11:07:21+00:00" - }, { "name": "fakerphp/faker", "version": "v1.20.0", @@ -1454,197 +1305,6 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phpbench/container", - "version": "2.2.1", - "source": { - "type": "git", - "url": "https://github.com/phpbench/container.git", - "reference": "6d555ff7174fca13f9b1ec0b4a089ed41d0ab392" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpbench/container/zipball/6d555ff7174fca13f9b1ec0b4a089ed41d0ab392", - "reference": "6d555ff7174fca13f9b1ec0b4a089ed41d0ab392", - "shasum": "" - }, - "require": { - "psr/container": "^1.0|^2.0", - "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.16", - "phpstan/phpstan": "^0.12.52", - "phpunit/phpunit": "^8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "PhpBench\\DependencyInjection\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Leech", - "email": "daniel@dantleech.com" - } - ], - "description": "Simple, configurable, service container.", - "support": { - "issues": "https://github.com/phpbench/container/issues", - "source": "https://github.com/phpbench/container/tree/2.2.1" - }, - "time": "2022-01-25T10:17:35+00:00" - }, - { - "name": "phpbench/dom", - "version": "0.3.2", - "source": { - "type": "git", - "url": "https://github.com/phpbench/dom.git", - "reference": "b013b717832ddbaadf2a40984b04bc66af9a7110" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpbench/dom/zipball/b013b717832ddbaadf2a40984b04bc66af9a7110", - "reference": "b013b717832ddbaadf2a40984b04bc66af9a7110", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "php": "^7.2||^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.18", - "phpstan/phpstan": "^0.12.83", - "phpunit/phpunit": "^8.0||^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "PhpBench\\Dom\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Leech", - "email": "daniel@dantleech.com" - } - ], - "description": "DOM wrapper to simplify working with the PHP DOM implementation", - "support": { - "issues": "https://github.com/phpbench/dom/issues", - "source": "https://github.com/phpbench/dom/tree/0.3.2" - }, - "time": "2021-09-24T15:26:07+00:00" - }, - { - "name": "phpbench/phpbench", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpbench/phpbench.git", - "reference": "3555dff668e58d25c39d287f3f1bac13a7817b4c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/3555dff668e58d25c39d287f3f1bac13a7817b4c", - "reference": "3555dff668e58d25c39d287f3f1bac13a7817b4c", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.13", - "ext-dom": "*", - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "ext-tokenizer": "*", - "php": "^7.3 || ^8.0", - "phpbench/container": "^2.1", - "phpbench/dom": "~0.3.1", - "psr/log": "^1.1", - "seld/jsonlint": "^1.1", - "symfony/console": "^4.2 || ^5.0", - "symfony/filesystem": "^4.2 || ^5.0", - "symfony/finder": "^4.2 || ^5.0", - "symfony/options-resolver": "^4.2 || ^5.0", - "symfony/process": "^4.2 || ^5.0", - "webmozart/path-util": "^2.3" - }, - "require-dev": { - "dantleech/invoke": "^2.0", - "friendsofphp/php-cs-fixer": "^3.0", - "jangregor/phpstan-prophecy": "^0.8.1", - "phpspec/prophecy": "^1.12", - "phpstan/phpstan": "^0.12.7", - "phpunit/phpunit": "^8.5.8 || ^9.0", - "symfony/error-handler": "^5.2", - "symfony/var-dumper": "^4.0 || ^5.0" - }, - "suggest": { - "ext-xdebug": "For Xdebug profiling extension." - }, - "bin": [ - "bin/phpbench" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "files": [ - "lib/Report/Func/functions.php" - ], - "psr-4": { - "PhpBench\\": "lib/", - "PhpBench\\Extensions\\XDebug\\": "extensions/xdebug/lib/", - "PhpBench\\Extensions\\Reports\\": "extensions/reports/lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Leech", - "email": "daniel@dantleech.com" - } - ], - "description": "PHP Benchmarking Framework", - "support": { - "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.2.0" - }, - "funding": [ - { - "url": "https://github.com/dantleech", - "type": "github" - } - ], - "time": "2021-11-06T13:52:05+00:00" - }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -2292,55 +1952,6 @@ ], "time": "2022-06-19T12:14:25+00:00" }, - { - "name": "psr/cache", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" - }, - "time": "2021-02-03T23:26:27+00:00" - }, { "name": "psr/container", "version": "2.0.2", @@ -3408,70 +3019,6 @@ ], "time": "2020-09-28T06:39:44+00:00" }, - { - "name": "seld/jsonlint", - "version": "1.9.0", - "source": { - "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "4211420d25eba80712bff236a98960ef68b866b7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/4211420d25eba80712bff236a98960ef68b866b7", - "reference": "4211420d25eba80712bff236a98960ef68b866b7", - "shasum": "" - }, - "require": { - "php": "^5.3 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.5", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" - }, - "bin": [ - "bin/jsonlint" - ], - "type": "library", - "autoload": { - "psr-4": { - "Seld\\JsonLint\\": "src/Seld/JsonLint/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "JSON Linter", - "keywords": [ - "json", - "linter", - "parser", - "validator" - ], - "support": { - "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.9.0" - }, - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", - "type": "tidelift" - } - ], - "time": "2022-04-01T13:37:23+00:00" - }, { "name": "swoole/ide-helper", "version": "4.8.0", @@ -3680,202 +3227,6 @@ ], "time": "2022-02-25T11:15:52+00:00" }, - { - "name": "symfony/filesystem", - "version": "v5.4.9", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "36a017fa4cce1eff1b8e8129ff53513abcef05ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/36a017fa4cce1eff1b8e8129ff53513abcef05ba", - "reference": "36a017fa4cce1eff1b8e8129ff53513abcef05ba", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.9" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-05-20T13:55:35+00:00" - }, - { - "name": "symfony/finder", - "version": "v5.4.8", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9b630f3427f3ebe7cd346c277a1408b00249dad9", - "reference": "9b630f3427f3ebe7cd346c277a1408b00249dad9", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-04-15T08:07:45+00:00" - }, - { - "name": "symfony/options-resolver", - "version": "v5.4.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/cc1147cb11af1b43f503ac18f31aa3bec213aba8", - "reference": "cc1147cb11af1b43f503ac18f31aa3bec213aba8", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an improved replacement for the array_replace PHP function", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], - "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.4.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-01-02T09:53:40+00:00" - }, { "name": "symfony/polyfill-ctype", "version": "v1.26.0", @@ -4285,68 +3636,6 @@ ], "time": "2022-05-24T11:49:31+00:00" }, - { - "name": "symfony/process", - "version": "v5.4.8", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", - "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.4.8" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-04-08T05:07:18+00:00" - }, { "name": "symfony/service-contracts", "version": "v3.1.1", diff --git a/src/Database/Query.php b/src/Database/Query.php index 72bb1767d..d69c3a12f 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -187,6 +187,17 @@ public static function parse(string $filter): self $isStringStack = $stringStackState !== null; $isArrayStack = !$isStringStack && $stackCount > 0; + + if($char === static::CHAR_BACKSLASH) { + if(!(static::isSpecialChar($filter[$i + 1]))) { + static::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam); + } + + static::appendSymbol($isStringStack, $filter[$i + 1], $i, $filter, $currentParam); + $i++; + + continue; + } // String support + escaping support if ( @@ -293,16 +304,6 @@ public static function parse(string $filter): self */ protected static function appendSymbol(bool $isStringStack, string $char, int $index, string $filter, string &$currentParam): void { - $nextChar = $filter[$index + 1] ?? ''; - $prevChar = $filter[$index - 1] ?? ''; - if ( - $char === static::CHAR_BACKSLASH && // Current char might be escaping - self::isQuote($nextChar) && // Next char must be string syntax symbol - $prevChar !== static::CHAR_BACKSLASH // Current \ can't be escaped - ) { - return; - } - // Ignore spaces and commas outside of string $canBeIgnored = false; diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 3e7bb61b3..60eaf5193 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -147,6 +147,17 @@ public function testParseV2() $this->assertEquals(1, $query->getParams()[0]); $this->assertEquals("Hello\\\\", $query->getParams()[1]); $this->assertEquals("World", $query->getParams()[2]); + + $query = Query::parse('equal(1, "Hello\\", World")'); + $this->assertCount(2, $query->getParams()); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("Hello\", World", $query->getParams()[1]); + + $query = Query::parse('equal(1, "Hello\\\\\\", ", "World")'); + $this->assertCount(3, $query->getParams()); + $this->assertEquals(1, $query->getParams()[0]); + $this->assertEquals("Hello\\\\\", ", $query->getParams()[1]); + $this->assertEquals("World", $query->getParams()[2]); } public function testAlias() From 74435b57ec8ff416b40658950b8fdcdd83aee6ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 27 Jul 2022 13:12:07 +0000 Subject: [PATCH 23/27] Commented out alias support. --- src/Database/Query.php | 4 ++++ tests/Database/QueryTest.php | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/Database/Query.php b/src/Database/Query.php index d69c3a12f..0f1703677 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -384,6 +384,9 @@ protected static function parseParam(string $param): mixed */ static protected function getMethodFromAlias(string $method): string { + return $method; + /* + Commented out as we didn't consider this important at the moment, since IDE autocomplete should do the job. return match ($method) { 'lt' => self::TYPE_LESSER, 'lte' => self::TYPE_LESSEREQUAL, @@ -392,5 +395,6 @@ static protected function getMethodFromAlias(string $method): string 'eq' => self::TYPE_EQUAL, default => $method }; + */ } } diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 60eaf5193..e3d7a9fae 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -160,6 +160,8 @@ public function testParseV2() $this->assertEquals("World", $query->getParams()[2]); } + /* + Tests for aliases if we enable them: public function testAlias() { $query = Query::parse('eq(1)'); @@ -173,6 +175,7 @@ public function testAlias() $query = Query::parse('gte(1)'); $this->assertEquals(Query::TYPE_GREATEREQUAL, $query->getMethod()); } + */ public function testParseComplex() { @@ -276,11 +279,14 @@ public function testisMethod() $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSORAFTER)); $this->assertTrue(Query::isMethod(QUERY::TYPE_CURSORBEFORE)); + /* + Tests for aliases if we enable them: $this->assertTrue(Query::isMethod('lt')); $this->assertTrue(Query::isMethod('lte')); $this->assertTrue(Query::isMethod('gt')); $this->assertTrue(Query::isMethod('gte')); $this->assertTrue(Query::isMethod('eq')); + */ $this->assertFalse(Query::isMethod('invalid')); $this->assertFalse(Query::isMethod('lte ')); From 97754fb77f422f7bba183b9ba4d9a4b6f89820d3 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 8 Aug 2022 22:02:53 +0000 Subject: [PATCH 24/27] Update Query class --- README.md | 2 +- src/Database/Adapter/MariaDB.php | 52 ++-- src/Database/Adapter/Mongo/MongoDBAdapter.php | 10 +- src/Database/Query.php | 265 +++++++++++++----- src/Database/Validator/Queries.php | 6 +- src/Database/Validator/QueryValidator.php | 22 +- tests/Database/Adapter/MongoDBTest.php | 10 +- tests/Database/Base.php | 197 +++++++------ tests/Database/QueryTest.php | 253 +++++++++-------- tests/Database/Validator/QueriesTest.php | 9 +- .../Database/Validator/QueryValidatorTest.php | 6 +- 11 files changed, 475 insertions(+), 357 deletions(-) diff --git a/README.md b/README.md index 90db471cf..370a1aadf 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ static::getDatabase()->createDocument('movies', new Document([ ```php $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, ['year', 2019]), + Query::equal('year', [2019]), ]); ``` diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index 8ef3aa2f1..c9fd88aab 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -152,7 +152,7 @@ public function createCollection(string $name, array $attributes = [], array $in $indexAttributes = $index->getAttribute('attributes'); foreach ($indexAttributes as $nested => $attribute) { $indexLength = $index->getAttribute('lengths')[$key] ?? ''; - $indexLength = (empty($indexLength)) ? '' : '('.(int)$indexLength.')'; + $indexLength = (empty($indexLength)) ? '' : '(' . (int)$indexLength . ')'; $indexOrder = $index->getAttribute('orders')[$key] ?? ''; $indexAttribute = $this->filter($attribute); @@ -795,8 +795,8 @@ public function find(string $collection, array $queries = [], int $limit = 25, i }, $orderAttributes); $hasIdAttribute = false; - foreach($orderAttributes as $i => $attribute) { - if($attribute === '_uid') { + foreach ($orderAttributes as $i => $attribute) { + if ($attribute === '_uid') { $hasIdAttribute = true; } @@ -839,13 +839,13 @@ public function find(string $collection, array $queries = [], int $limit = 25, i } // Allow order type without any order attribute, fallback to the natural order (_id) - if(!$hasIdAttribute) { + if (!$hasIdAttribute) { if (empty($orderAttributes) && !empty($orderTypes)) { $order = $orderTypes[0] ?? Database::ORDER_ASC; if ($cursorDirection === Database::CURSOR_BEFORE) { $order = $order === Database::ORDER_ASC ? Database::ORDER_DESC : Database::ORDER_ASC; } - + $orders[] = 'table_main._id ' . $this->filter($order); } else { $orders[] = 'table_main._id ' . ($cursorDirection === Database::CURSOR_AFTER ? Database::ORDER_ASC : Database::ORDER_DESC); // Enforce last ORDER by '_id' @@ -853,16 +853,16 @@ public function find(string $collection, array $queries = [], int $limit = 25, i } foreach ($queries as $i => $query) { - $query->setFirstParam(match ($query->getFirstParam()) { + $query->setAttribute(match ($query->getAttribute()) { '$id' => '_uid', '$createdAt' => '_createdAt', '$updatedAt' => '_updatedAt', - default => $query->getFirstParam() + default => $query->getAttribute() }); $conditions = []; - foreach ($query->getArrayParam(1) as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getFirstParam(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value); + foreach ($query->getValues() as $key => $value) { + $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); } $condition = implode(' OR ', $conditions); $where[] = empty($condition) ? '' : '(' . $condition . ')'; @@ -889,8 +889,8 @@ public function find(string $collection, array $queries = [], int $limit = 25, i foreach ($queries as $i => $query) { if ($query->getMethod() === Query::TYPE_SEARCH) continue; - foreach ($query->getArrayParam(1) as $key => $value) { - $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value, $this->getPDOType($value)); + foreach ($query->getValues() as $key => $value) { + $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); } } @@ -959,16 +959,16 @@ public function count(string $collection, array $queries = [], int $max = 0): in $limit = ($max === 0) ? '' : 'LIMIT :max'; foreach ($queries as $i => $query) { - $query->setFirstParam(match ($query->getFirstParam()) { + $query->setAttribute(match ($query->getAttribute()) { '$id' => '_uid', '$createdAt' => '_createdAt', '$updatedAt' => '_updatedAt', - default => $query->getFirstParam() + default => $query->getAttribute() }); $conditions = []; - foreach ($query->getArrayParam(1) as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getFirstParam(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value); + foreach ($query->getValues() as $key => $value) { + $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); } $condition = implode(' OR ', $conditions); @@ -992,8 +992,8 @@ public function count(string $collection, array $queries = [], int $max = 0): in foreach ($queries as $i => $query) { if ($query->getMethod() === Query::TYPE_SEARCH) continue; - foreach ($query->getArrayParam(1) as $key => $value) { - $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value, $this->getPDOType($value)); + foreach ($query->getValues() as $key => $value) { + $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); } } @@ -1028,16 +1028,16 @@ public function sum(string $collection, string $attribute, array $queries = [], $limit = ($max === 0) ? '' : 'LIMIT :max'; foreach ($queries as $i => $query) { - $query->setFirstParam(match ($query->getFirstParam()) { + $query->setAttribute(match ($query->getAttribute()) { '$id' => '_uid', '$createdAt' => '_createdAt', '$updatedAt' => '_updatedAt', - default => $query->getFirstParam() + default => $query->getAttribute() }); $conditions = []; - foreach ($query->getArrayParam(1) as $key => $value) { - $conditions[] = $this->getSQLCondition('table_main.' . $query->getFirstParam(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value); + foreach ($query->getValues() as $key => $value) { + $conditions[] = $this->getSQLCondition('table_main.' . $query->getAttribute(), $query->getMethod(), ':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value); } $where[] = implode(' OR ', $conditions); @@ -1059,8 +1059,8 @@ public function sum(string $collection, string $attribute, array $queries = [], foreach ($queries as $i => $query) { if ($query->getMethod() === Query::TYPE_SEARCH) continue; - foreach ($query->getArrayParam(1) as $key => $value) { - $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getFirstParam(), $value, $this->getPDOType($value)); + foreach ($query->getValues() as $key => $value) { + $stmt->bindValue(':attribute_' . $i . '_' . $key . '_' . $query->getAttribute(), $value, $this->getPDOType($value)); } } @@ -1488,7 +1488,7 @@ protected function getSQLIndex(string $collection, string $id, string $type, ar break; } - return "CREATE {$type} `{$id}` ON `{$this->getDefaultDatabase()}`.`{$this->getNamespace()}_{$collection}` ( ". implode(', ', $attributes) ." )"; + return "CREATE {$type} `{$id}` ON `{$this->getDefaultDatabase()}`.`{$this->getNamespace()}_{$collection}` ( " . implode(', ', $attributes) . " )"; } /** @@ -1584,7 +1584,7 @@ protected function getPDO() /** * Returns default PDO configuration */ - public static function getPdoAttributes():array + public static function getPdoAttributes(): array { return [ PDO::ATTR_TIMEOUT => 3, // Specifies the timeout duration in seconds. Takes a value of type int. @@ -1595,6 +1595,4 @@ public static function getPdoAttributes():array PDO::ATTR_STRINGIFY_FETCHES => true // Returns all fetched data as Strings ]; } - - } diff --git a/src/Database/Adapter/Mongo/MongoDBAdapter.php b/src/Database/Adapter/Mongo/MongoDBAdapter.php index 6f8ecee97..d312d08b6 100644 --- a/src/Database/Adapter/Mongo/MongoDBAdapter.php +++ b/src/Database/Adapter/Mongo/MongoDBAdapter.php @@ -738,18 +738,18 @@ protected function buildFilters($queries): array $filters = []; foreach ($queries as $i => $query) { - if ($query->getFirstParam() === '$id') { - $query->setFirstParam('_uid'); + if ($query->getAttribute() === '$id') { + $query->setAttribute('_uid'); } - $attribute = $query->getFirstParam(); + $attribute = $query->getAttribute(); $operator = $this->getQueryOperator($query->getMethod()); - $value = (count($query->getArrayParam(1)) > 1) ? $query->getArrayParam(1) : $query->getArrayParam(1)[0]; + $value = (count($query->getValues()) > 1) ? $query->getValues() : $query->getValues()[0]; // TODO@kodumbeats Mongo recommends different methods depending on operator - implement the rest if (is_array($value) && $operator === '$eq') { $filters[$attribute]['$in'] = $value; } elseif ($operator === '$in') { - $filters[$attribute]['$in'] = $query->getArrayParam(1); + $filters[$attribute]['$in'] = $query->getValues(); } elseif ($operator === '$search') { // only one fulltext index per mongo collection, so attribute not necessary $filters['$text'][$operator] = $value; diff --git a/src/Database/Query.php b/src/Database/Query.php index 0f1703677..0df5b5275 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -35,16 +35,18 @@ class Query protected const CHAR_BACKSLASH = '\\'; protected string $method = ''; + protected string $attribute = ''; + protected array $values = []; - protected array $params = []; /** * Construct a new query object */ - public function __construct(string $method, array $params) + public function __construct(string $method, string $attribute = '', array $values = []) { $this->method = $method; - $this->params = $params; + $this->attribute = $attribute; + $this->values = $values; } public function getMethod(): string @@ -52,69 +54,48 @@ public function getMethod(): string return $this->method; } - public function getParams(): array + public function getValues(): array { - return $this->params; + return $this->values; } - /** - * Helper method returning first param. In first param we often store attribute - * - * @return null|string - */ - public function getFirstParam(): ?string + public function getAttribute(): string { - return $this->params[0] ?? null; + return $this->attribute; } /** - * Helper method changing first param. In first param we often store attribute - * - * @param string $value + * Sets Method. + * @param string $method * @return self */ - public function setFirstParam(string $value): self + public function setMethod(string $method): self { - $this->params[0] = $value; + $this->method = $method; return $this; } /** - * Helper method. Returns param, but in form of array array. - * - * @param int $index - * @return array - */ - public function getArrayParam(int $index): array - { - if (\is_array($this->params[$index])) { - return $this->params[$index]; - } - - return [$this->params[$index]]; - } - - /** - * Sets Method. - * @param string $method + * Sets Values. + * @param array $values * @return self */ - public function setMethod(string $method): self + public function setValues(array $values): self { - $this->method = $method; + $this->values = $values; return $this; } /** - * Sets Param. - * @param array $params + * Sets Attribute. + * @param string $attribute * @return self */ - public function setParams(array $params): self + public function setAttribute(string $attribute): self { - $this->params = $params; + $this->attribute = $attribute; return $this; } @@ -188,8 +169,8 @@ public static function parse(string $filter): self $isStringStack = $stringStackState !== null; $isArrayStack = !$isStringStack && $stackCount > 0; - if($char === static::CHAR_BACKSLASH) { - if(!(static::isSpecialChar($filter[$i + 1]))) { + if ($char === static::CHAR_BACKSLASH) { + if (!(static::isSpecialChar($filter[$i + 1]))) { static::appendSymbol($isStringStack, $filter[$i], $i, $filter, $currentParam); } @@ -198,13 +179,12 @@ public static function parse(string $filter): self continue; } - + // String support + escaping support if ( (self::isQuote($char)) && // Must be string indicator ($filter[$i - 1] !== static::CHAR_BACKSLASH || $filter[$i - 2] === static::CHAR_BACKSLASH) // Must not be escaped; - ) - { + ) { if ($isStringStack) { // Dont mix-up string symbols. Only allow the same as on start if ($char === $stringStackState) { @@ -255,7 +235,7 @@ public static function parse(string $filter): self if (!empty($currentParam)) { $params[] = $currentParam; } - + $currentParam = ""; } } @@ -278,18 +258,41 @@ public static function parse(string $filter): self // If array, parse each child separatelly if (\is_array($param)) { foreach ($param as $element) { - $arr[] = self::parseParam($element); + $arr[] = self::parseValue($element); } $parsedParams[] = $arr ?? []; } else { - $parsedParams[] = self::parseParam($param); + $parsedParams[] = self::parseValue($param); } } $method = static::getMethodFromAlias($method); - return new self($method, $parsedParams); + switch ($method) { + case self::TYPE_EQUAL: + case self::TYPE_NOTEQUAL: + case self::TYPE_LESSER: + case self::TYPE_LESSEREQUAL: + case self::TYPE_GREATER: + case self::TYPE_GREATEREQUAL: + case self::TYPE_CONTAINS: + case self::TYPE_SEARCH: + return new self($method, $parsedParams[0], \is_array($parsedParams[1]) ? $parsedParams[1] : [$parsedParams[1]]); + + case self::TYPE_ORDERASC: + case self::TYPE_ORDERDESC: + return new self($method, $parsedParams[0]); + + case self::TYPE_LIMIT: + case self::TYPE_OFFSET: + case self::TYPE_CURSORAFTER: + case self::TYPE_CURSORBEFORE: + return new self($method, values: $parsedParams[0]); + + default: + return new self($method); + } } /** @@ -307,9 +310,9 @@ protected static function appendSymbol(bool $isStringStack, string $char, int $i // Ignore spaces and commas outside of string $canBeIgnored = false; - if($char === static::CHAR_SPACE) { + if ($char === static::CHAR_SPACE) { $canBeIgnored = true; - } else if($char === static::CHAR_COMMA) { + } else if ($char === static::CHAR_COMMA) { $canBeIgnored = true; } @@ -322,26 +325,28 @@ protected static function appendSymbol(bool $isStringStack, string $char, int $i } } - protected static function isQuote(string $char) { - if($char === self::CHAR_SINGLE_QUOTE) { + protected static function isQuote(string $char) + { + if ($char === self::CHAR_SINGLE_QUOTE) { return true; - } else if($char === self::CHAR_DOUBLE_QUOTE) { + } else if ($char === self::CHAR_DOUBLE_QUOTE) { return true; } return false; } - protected static function isSpecialChar(string $char) { - if($char === static::CHAR_COMMA) { + protected static function isSpecialChar(string $char) + { + if ($char === static::CHAR_COMMA) { return true; - } else if($char === static::CHAR_BRACKET_END) { + } else if ($char === static::CHAR_BRACKET_END) { return true; - } else if($char === static::CHAR_BRACKET_START) { + } else if ($char === static::CHAR_BRACKET_START) { return true; - } else if($char === static::CHAR_DOUBLE_QUOTE) { + } else if ($char === static::CHAR_DOUBLE_QUOTE) { return true; - } else if($char === static::CHAR_SINGLE_QUOTE) { + } else if ($char === static::CHAR_SINGLE_QUOTE) { return true; } @@ -349,31 +354,31 @@ protected static function isSpecialChar(string $char) { } /** - * Parses param value. + * Parses value. * - * @param string $param + * @param string $value * @return mixed */ - protected static function parseParam(string $param): mixed + protected static function parseValue(string $value): mixed { - $param = \trim($param); - - if ($param === 'false') { // Boolean param + $value = \trim($value); + + if ($value === 'false') { // Boolean value return false; - } else if ($param === 'true') { + } else if ($value === 'true') { return true; - } else if ($param === 'null') { // Null param + } else if ($value === 'null') { // Null value return null; - } else if (\is_numeric($param)) { // Numeric param + } else if (\is_numeric($value)) { // Numeric value // Cast to number - return $param + 0; - } else if (\str_starts_with($param, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($param, static::CHAR_SINGLE_QUOTE)) { // String param - $param = \substr($param, 1, -1); // Remove '' or "" - return $param; + return $value + 0; + } else if (\str_starts_with($value, static::CHAR_DOUBLE_QUOTE) || \str_starts_with($value, static::CHAR_SINGLE_QUOTE)) { // String param + $value = \substr($value, 1, -1); // Remove '' or "" + return $value; } // Unknown format - return $param; + return $value; } /** @@ -397,4 +402,116 @@ static protected function getMethodFromAlias(string $method): string }; */ } + + /** + * Helper method to create Query with equal method + */ + public static function equal(string $attribute, array $values): self + { + return new self(self::TYPE_EQUAL, $attribute, $values); + } + + /** + * Helper method to create Query with notEqual method + */ + public static function notEqual(string $attribute, $value): self + { + return new self(self::TYPE_NOTEQUAL, $attribute, [$value]); + } + + /** + * Helper method to create Query with lessThan method + */ + public static function lessThan(string $attribute, $value): self + { + return new self(self::TYPE_LESSER, $attribute, [$value]); + } + + /** + * Helper method to create Query with lessThanEqual method + */ + public static function lessThanEqual(string $attribute, $value): self + { + return new self(self::TYPE_LESSEREQUAL, $attribute, [$value]); + } + + /** + * Helper method to create Query with greaterThan method + */ + public static function greaterThan(string $attribute, $value): self + { + return new self(self::TYPE_GREATER, $attribute, [$value]); + } + + /** + * Helper method to create Query with greaterThanEqual method + */ + public static function greaterThanEqual(string $attribute, $value): self + { + return new self(self::TYPE_GREATEREQUAL, $attribute, [$value]); + } + + /** + * Helper method to create Query with contains method + */ + public static function contains(string $attribute, $value): self + { + return new self(self::TYPE_CONTAINS, $attribute, [$value]); + } + + /** + * Helper method to create Query with search method + */ + public static function search(string $attribute, $value): self + { + return new self(self::TYPE_SEARCH, $attribute, [$value]); + } + + /** + * Helper method to create Query with orderDesc method + */ + public static function orderDesc(string $attribute): self + { + return new self(self::TYPE_ORDERDESC, $attribute); + } + + /** + * Helper method to create Query with orderAsc method + */ + public static function orderAsc(string $attribute): self + { + return new self(self::TYPE_ORDERASC, $attribute); + } + + /** + * Helper method to create Query with limit method + */ + public static function limit(int $value): self + { + return new self(self::TYPE_LIMIT, values: [$value]); + } + + /** + * Helper method to create Query with offset method + */ + public static function offset(int $value): self + { + return new self(self::TYPE_OFFSET, values: [$value]); + } + + /** + * Helper method to create Query with cursorAfter method + */ + public static function cursorAfter(string $value): self + { + return new self(self::TYPE_CURSORAFTER, values: [$value]); + } + + /** + * Helper method to create Query with cursorBefore method + */ + public static function cursorBefore(string $value): self + { + return new self(self::TYPE_CURSORBEFORE, values: [$value]); + } } diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 7910fab3b..a50336e43 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -93,7 +93,7 @@ public function isValid($value): bool foreach ($value as $query) { // [attribute => method] - $queries[$query->getFirstParam()] = $query->getMethod(); + $queries[$query->getAttribute()] = $query->getMethod(); if (!$this->validator->isValid($query)) { $this->message = 'Query not valid: ' . $this->validator->getDescription(); @@ -108,7 +108,7 @@ public function isValid($value): bool // look for strict match among indexes foreach ($this->indexes as $index) { if ($this->arrayMatch($index['attributes'], array_keys($queries))) { - $found = $index; + $found = $index; } } @@ -121,7 +121,7 @@ public function isValid($value): bool if (in_array(Query::TYPE_SEARCH, array_values($queries)) && $found['type'] !== Database::INDEX_FULLTEXT) { $this->message = 'Search method requires fulltext index: ' . implode(",", array_keys($queries)); return false; - } + } } return true; diff --git a/src/Database/Validator/QueryValidator.php b/src/Database/Validator/QueryValidator.php index 35cbc5b34..7957a4744 100644 --- a/src/Database/Validator/QueryValidator.php +++ b/src/Database/Validator/QueryValidator.php @@ -19,20 +19,6 @@ class QueryValidator extends Validator */ protected $schema = []; - /** - * @var array - */ - protected $methods = [ - 'equal', - 'notEqual', - 'lesser', - 'lesserEqual', - 'greater', - 'greaterEqual', - 'contains', - 'search', - ]; - /** * Expression constructor * @@ -90,16 +76,16 @@ public function getDescription(): string public function isValid($query): bool { // Validate method - if (!in_array($query->getMethod(), $this->methods)) { + if (!Query::isMethod($query->getMethod())) { $this->message = 'Query method invalid: ' . $query->getMethod(); return false; } // Search for attribute in schema - $attributeIndex = array_search($query->getFirstParam(), array_column($this->schema, 'key')); + $attributeIndex = array_search($query->getAttribute(), array_column($this->schema, 'key')); if ($attributeIndex === false) { - $this->message = 'Attribute not found in schema: ' . $query->getFirstParam(); + $this->message = 'Attribute not found in schema: ' . $query->getAttribute(); return false; } @@ -107,7 +93,7 @@ public function isValid($query): bool $attributeType = $this->schema[$attributeIndex]['type']; foreach ($query->getValues() as $value) { - $condition = match($attributeType) { + $condition = match ($attributeType) { Database::VAR_DATETIME => gettype($value) === Database::VAR_STRING, default => gettype($value) === $attributeType }; diff --git a/tests/Database/Adapter/MongoDBTest.php b/tests/Database/Adapter/MongoDBTest.php index 7842c6a9e..1a671db19 100644 --- a/tests/Database/Adapter/MongoDBTest.php +++ b/tests/Database/Adapter/MongoDBTest.php @@ -113,7 +113,7 @@ public function testListDocumentSearch(Document $document) 'empty' => [], ])); - $documents = static::getDatabase()->find('documents', [ new Query(Query::TYPE_SEARCH, ['string', '*test+alias@email-provider.com']) ]); + $documents = static::getDatabase()->find('documents', [new Query(Query::TYPE_SEARCH, 'string', ['*test+alias@email-provider.com'])]); $this->assertEquals(1, count($documents)); @@ -245,7 +245,7 @@ public function testCount() $count = static::getDatabase()->count('movies'); $this->assertEquals(5, $count); - $count = static::getDatabase()->count('movies', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); + $count = static::getDatabase()->count('movies', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); $this->assertEquals(2, $count); Authorization::unsetRole('userx'); @@ -267,8 +267,8 @@ public function testCount() */ Authorization::disable(); $count = static::getDatabase()->count('movies', [ - new Query(Query::TYPE_EQUAL, ['director', ['TBD', 'Joe Johnston']]), - new Query(Query::TYPE_EQUAL, ['year', 2025]), + new Query(Query::TYPE_EQUAL, 'director', ['TBD', 'Joe Johnston']), + new Query(Query::TYPE_EQUAL, 'year', [2025]), ]); $this->assertEquals(1, $count); Authorization::reset(); @@ -309,6 +309,6 @@ public function testCleanupAttributeTests() static function getReservedKeywords(): array { // Mongo does not have concept of reserverd words. We put something here just to run the rests for this adapter too - return [ 'mogno' ]; + return ['mogno']; } } diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 8a4f704a7..0387f1059 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -77,7 +77,6 @@ public function testCreatedAtUpdatedAt() $this->assertNotEmpty($document->getInternalId()); $this->assertNotNull($document->getInternalId()); } - } /** @@ -111,9 +110,9 @@ public function testCreateDeleteAttribute() static::getDatabase()->createCollection('attributes'); $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string1', Database::VAR_STRING, 128, true)); - $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string2', Database::VAR_STRING, 16383+1, true)); - $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string3', Database::VAR_STRING, 65535+1, true)); - $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string4', Database::VAR_STRING, 16777215+1, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string2', Database::VAR_STRING, 16383 + 1, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string3', Database::VAR_STRING, 65535 + 1, true)); + $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'string4', Database::VAR_STRING, 16777215 + 1, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'integer', Database::VAR_INTEGER, 0, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'bigint', Database::VAR_INTEGER, 8, true)); $this->assertEquals(true, static::getDatabase()->createAttribute('attributes', 'float', Database::VAR_FLOAT, 0, true)); @@ -246,7 +245,7 @@ public function testIndexCaseInsensitivity() public function testCleanupAttributeTests() { static::getDatabase()->deleteCollection('attributes'); - $this->assertEquals(1,1); + $this->assertEquals(1, 1); } /** @@ -667,7 +666,7 @@ public function testListDocumentSearch(Document $document) * Allow reserved keywords for search */ $documents = static::getDatabase()->find('documents', [ - new Query(Query::TYPE_SEARCH, ['string', '*test+alias@email-provider.com']), + new Query(Query::TYPE_SEARCH, 'string', ['*test+alias@email-provider.com']), ]); $this->assertEquals(1, count($documents)); @@ -686,8 +685,7 @@ public function testUpdateDocument(Document $document) ->setAttribute('float', 5.56) ->setAttribute('boolean', false) ->setAttribute('colors', 'red', Document::SET_TYPE_APPEND) - ->setAttribute('with-dash', 'Works') - ; + ->setAttribute('with-dash', 'Works'); $new = $this->getDatabase()->updateDocument($document->getCollection(), $document->getId(), $document); @@ -709,8 +707,7 @@ public function testUpdateDocument(Document $document) $new ->setAttribute('$read', 'role:guest', Document::SET_TYPE_APPEND) - ->setAttribute('$write', 'role:guest', Document::SET_TYPE_APPEND) - ; + ->setAttribute('$write', 'role:guest', Document::SET_TYPE_APPEND); $this->getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new, true); @@ -721,8 +718,7 @@ public function testUpdateDocument(Document $document) $new ->setAttribute('$read', $oldRead) - ->setAttribute('$write', $oldWrite) - ; + ->setAttribute('$write', $oldWrite); $this->getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new); @@ -745,8 +741,7 @@ public function testUpdateDocumentDuplicatePermissions(Document $document) ->setAttribute('$read', 'role:guest', Document::SET_TYPE_APPEND) ->setAttribute('$read', 'role:guest', Document::SET_TYPE_APPEND) ->setAttribute('$write', 'role:guest', Document::SET_TYPE_APPEND) - ->setAttribute('$write', 'role:guest', Document::SET_TYPE_APPEND) - ; + ->setAttribute('$write', 'role:guest', Document::SET_TYPE_APPEND); $this->getDatabase()->updateDocument($new->getCollection(), $new->getId(), $new, true); @@ -876,14 +871,14 @@ public function testFind(Document $document) // Alphabetical order $sortedDocuments = $movieDocuments; - \usort($sortedDocuments, function($doc1, $doc2) { + \usort($sortedDocuments, function ($doc1, $doc2) { return strcmp($doc1['$id'], $doc2['$id']); }); $firstDocumentId = $sortedDocuments[0]->getId(); $lastDocumentId = $sortedDocuments[\count($sortedDocuments) - 1]->getId(); - /** + /** * Check $id: Notice, this orders ID names alphabetically, not by internal numeric ID */ $documents = static::getDatabase()->find('movies', [], 25, 0, ['$id'], [Database::ORDER_DESC]); @@ -913,7 +908,7 @@ public function testFind(Document $document) * Check an Integer condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, ['year', 2019]), + new Query(Query::TYPE_EQUAL, 'year', [2019]), ]); $this->assertEquals(2, count($documents)); @@ -924,7 +919,7 @@ public function testFind(Document $document) * Boolean condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, ['active', true]), + new Query(Query::TYPE_EQUAL, 'active', [true]), ]); $this->assertEquals(4, count($documents)); @@ -933,7 +928,7 @@ public function testFind(Document $document) * String condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, ['director', 'TBD']), + new Query(Query::TYPE_EQUAL, 'director', ['TBD']), ]); $this->assertEquals(2, count($documents)); @@ -942,7 +937,7 @@ public function testFind(Document $document) * Not Equal query */ $documents = static::getDatabase()->find('movies', [ - new Query('director', Query::TYPE_NOTEQUAL, ['TBD', 'Joe Johnston']), + new Query(Query::TYPE_NOTEQUAL, 'director', ['TBD', 'Joe Johnston']), ]); $this->assertGreaterThan(0, count($documents)); @@ -956,19 +951,18 @@ public function testFind(Document $document) * Float condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_LESSER, ['price', 26.00]), - new Query(Query::TYPE_GREATER, ['price', 25.98]), + new Query(Query::TYPE_LESSER, 'price', [26.00]), + new Query(Query::TYPE_GREATER, 'price', [25.98]), ]); // TODO@kodumbeats hacky way to pass mariadb tests // Remove when query method contains() is supported - if (static::getAdapterName() === "mongodb") - { + if (static::getAdapterName() === "mongodb") { /** * Array contains condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_CONTAINS, ['generes', 'comics']), + new Query(Query::TYPE_CONTAINS, 'generes', ['comics']), ]); $this->assertEquals(2, count($documents)); @@ -977,7 +971,7 @@ public function testFind(Document $document) * Array contains OR condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_CONTAINS, ['generes', ['comics', 'kids']]), + new Query(Query::TYPE_CONTAINS, 'generes', ['comics', 'kids']), ]); $this->assertEquals(4, count($documents)); @@ -990,7 +984,7 @@ public function testFind(Document $document) $this->assertEquals(true, $success); $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_SEARCH, ['name', 'captain']), + new Query(Query::TYPE_SEARCH, 'name', ['captain']), ]); $this->assertEquals(2, count($documents)); @@ -1001,7 +995,7 @@ public function testFind(Document $document) // TODO: Looks like the MongoDB implementation is a bit more complex, skipping that for now. if (in_array(static::getAdapterName(), ['mysql', 'mariadb'])) { $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_SEARCH, ['name', 'cap']), + new Query(Query::TYPE_SEARCH, 'name', ['cap']), ]); $this->assertEquals(2, count($documents)); @@ -1011,8 +1005,8 @@ public function testFind(Document $document) * Multiple conditions */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, ['director', 'TBD']), - new Query(Query::TYPE_EQUAL, ['year', 2026]), + new Query(Query::TYPE_EQUAL, 'director', ['TBD']), + new Query(Query::TYPE_EQUAL, 'year', [2026]), ]); $this->assertEquals(1, count($documents)); @@ -1021,7 +1015,7 @@ public function testFind(Document $document) * Multiple conditions and OR values */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, ['name', ['Frozen II', 'Captain Marvel']]), + new Query(Query::TYPE_EQUAL, 'name', ['Frozen II', 'Captain Marvel']), ]); $this->assertEquals(2, count($documents)); @@ -1032,7 +1026,7 @@ public function testFind(Document $document) * $id condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, ['$id', 'frozen']), + new Query(Query::TYPE_EQUAL, '$id', ['frozen']), ]); $this->assertEquals(1, count($documents)); @@ -1281,7 +1275,7 @@ public function testFind(Document $document) */ $documentsTest = static::getDatabase()->find('movies', [], 2, 0, ['price'], [Database::ORDER_DESC]); $documents = static::getDatabase()->find('movies', [], 1, 0, ['price'], [Database::ORDER_DESC], $documentsTest[0], Database::CURSOR_AFTER); - + $this->assertEquals($documentsTest[1]['$id'], $documents[0]['$id']); /** @@ -1289,7 +1283,7 @@ public function testFind(Document $document) */ $documentsTest = static::getDatabase()->find('movies', [], 2, 0, ['$id'], [Database::ORDER_DESC]); $documents = static::getDatabase()->find('movies', [], 1, 0, ['$id'], [Database::ORDER_DESC], $documentsTest[0], Database::CURSOR_AFTER); - + $this->assertEquals($documentsTest[1]['$id'], $documents[0]['$id']); /** @@ -1297,7 +1291,7 @@ public function testFind(Document $document) */ $documentsTest = static::getDatabase()->find('movies', [], 2, 0, ['$createdAt'], [Database::ORDER_DESC]); $documents = static::getDatabase()->find('movies', [], 1, 0, ['$createdAt'], [Database::ORDER_DESC], $documentsTest[0], Database::CURSOR_AFTER); - + $this->assertEquals($documentsTest[1]['$id'], $documents[0]['$id']); /** @@ -1305,7 +1299,7 @@ public function testFind(Document $document) */ $documentsTest = static::getDatabase()->find('movies', [], 2, 0, ['$updatedAt'], [Database::ORDER_DESC]); $documents = static::getDatabase()->find('movies', [], 1, 0, ['$updatedAt'], [Database::ORDER_DESC], $documentsTest[0], Database::CURSOR_AFTER); - + $this->assertEquals($documentsTest[1]['$id'], $documents[0]['$id']); /** @@ -1334,8 +1328,8 @@ public function testFind(Document $document) * Test that OR queries are handled correctly */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, ['director', ['TBD', 'Joe Johnston']]), - new Query(Query::TYPE_EQUAL, ['year', 2025]), + new Query(Query::TYPE_EQUAL, 'director', ['TBD', 'Joe Johnston']), + new Query(Query::TYPE_EQUAL, 'year', [2025]), ]); $this->assertEquals(1, count($documents)); @@ -1370,7 +1364,7 @@ public function testCount() { $count = static::getDatabase()->count('movies'); $this->assertEquals(6, $count); - $count = static::getDatabase()->count('movies', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); + $count = static::getDatabase()->count('movies', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); $this->assertEquals(2, $count); Authorization::disable(); @@ -1388,8 +1382,8 @@ public function testCount() */ Authorization::disable(); $count = static::getDatabase()->count('movies', [ - new Query(Query::TYPE_EQUAL, ['director', ['TBD', 'Joe Johnston']]), - new Query(Query::TYPE_EQUAL, ['year', 2025]), + new Query(Query::TYPE_EQUAL, 'director', ['TBD', 'Joe Johnston']), + new Query(Query::TYPE_EQUAL, 'year', [2025]), ]); $this->assertEquals(1, $count); Authorization::reset(); @@ -1401,27 +1395,27 @@ public function testCount() public function testSum() { Authorization::setRole('userx'); - $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); - $this->assertEquals(2019+2019, $sum); + $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $this->assertEquals(2019 + 2019, $sum); $sum = static::getDatabase()->sum('movies', 'year'); - $this->assertEquals(2013+2019+2011+2019+2025+2026, $sum); - $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); - $this->assertEquals(round(39.50+25.99, 2), round($sum, 2)); - $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); - $this->assertEquals(round(39.50+25.99, 2), round($sum, 2)); - - $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, ['year', 2019])], 1); + $this->assertEquals(2013 + 2019 + 2011 + 2019 + 2025 + 2026, $sum); + $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); + $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); + + $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, 'year', [2019])], 1); $this->assertEquals(2019, $sum); Authorization::unsetRole('userx'); - $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); - $this->assertEquals(2019+2019, $sum); + $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $this->assertEquals(2019 + 2019, $sum); $sum = static::getDatabase()->sum('movies', 'year'); - $this->assertEquals(2013+2019+2011+2019+2025, $sum); - $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); - $this->assertEquals(round(39.50+25.99, 2), round($sum, 2)); - $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, ['year', 2019]),]); - $this->assertEquals(round(39.50+25.99, 2), round($sum, 2)); + $this->assertEquals(2013 + 2019 + 2011 + 2019 + 2025, $sum); + $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); + $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); } public function testEncodeDecode() @@ -1628,8 +1622,8 @@ public function testEncodeDecode() $this->assertEquals('[]', $result->getAttribute('sessions')); $this->assertEquals('[]', $result->getAttribute('tokens')); $this->assertEquals('[]', $result->getAttribute('memberships')); - $this->assertEquals(['admin','developer','tester',], $result->getAttribute('roles')); - $this->assertEquals(['{"$id":"1","label":"x"}','{"$id":"2","label":"y"}','{"$id":"3","label":"z"}',], $result->getAttribute('tags')); + $this->assertEquals(['admin', 'developer', 'tester',], $result->getAttribute('roles')); + $this->assertEquals(['{"$id":"1","label":"x"}', '{"$id":"2","label":"y"}', '{"$id":"3","label":"z"}',], $result->getAttribute('tags')); $result = static::getDatabase()->decode($collection, $document); @@ -1648,7 +1642,7 @@ public function testEncodeDecode() $this->assertEquals([], $result->getAttribute('sessions')); $this->assertEquals([], $result->getAttribute('tokens')); $this->assertEquals([], $result->getAttribute('memberships')); - $this->assertEquals(['admin','developer','tester',], $result->getAttribute('roles')); + $this->assertEquals(['admin', 'developer', 'tester',], $result->getAttribute('roles')); $this->assertEquals([ new Document(['$id' => '1', 'label' => 'x']), new Document(['$id' => '2', 'label' => 'y']), @@ -1790,7 +1784,7 @@ public function testExceptionAttributeLimit() if ($this->getDatabase()->getAttributeLimit() > 0) { // load the collection up to the limit $attributes = []; - for ($i=0; $i < $this->getDatabase()->getAttributeLimit(); $i++) { + for ($i = 0; $i < $this->getDatabase()->getAttributeLimit(); $i++) { $attributes[] = new Document([ '$id' => "test{$i}", 'type' => Database::VAR_INTEGER, @@ -1809,7 +1803,7 @@ public function testExceptionAttributeLimit() } // Default assertion for other adapters - $this->assertEquals(1,1); + $this->assertEquals(1, 1); } /** @@ -1822,14 +1816,14 @@ public function testCheckAttributeCountLimit() // create same attribute in testExceptionAttributeLimit $attribute = new Document([ - '$id' => 'breaking', - 'type' => Database::VAR_INTEGER, - 'size' => 0, - 'required' => true, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], + '$id' => 'breaking', + 'type' => Database::VAR_INTEGER, + 'size' => 0, + 'required' => true, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], ]); $this->expectException(LimitException::class); @@ -1837,8 +1831,7 @@ public function testCheckAttributeCountLimit() } // Default assertion for other adapters - $this->assertEquals(1,1); - + $this->assertEquals(1, 1); } /** @@ -1874,7 +1867,7 @@ public function testExceptionWidthLimit($key, $stringSize, $stringCount, $intCou // Load the collection up to the limit // Strings - for ($i=0; $i < $stringCount; $i++) { + for ($i = 0; $i < $stringCount; $i++) { $attributes[] = new Document([ '$id' => "test_string{$i}", 'type' => Database::VAR_STRING, @@ -1888,7 +1881,7 @@ public function testExceptionWidthLimit($key, $stringSize, $stringCount, $intCou } // Integers - for ($i=0; $i < $intCount; $i++) { + for ($i = 0; $i < $intCount; $i++) { $attributes[] = new Document([ '$id' => "test_int{$i}", 'type' => Database::VAR_INTEGER, @@ -1902,7 +1895,7 @@ public function testExceptionWidthLimit($key, $stringSize, $stringCount, $intCou } // Floats - for ($i=0; $i < $floatCount; $i++) { + for ($i = 0; $i < $floatCount; $i++) { $attributes[] = new Document([ '$id' => "test_float{$i}", 'type' => Database::VAR_FLOAT, @@ -1916,7 +1909,7 @@ public function testExceptionWidthLimit($key, $stringSize, $stringCount, $intCou } // Booleans - for ($i=0; $i < $boolCount; $i++) { + for ($i = 0; $i < $boolCount; $i++) { $attributes[] = new Document([ '$id' => "test_bool{$i}", 'type' => Database::VAR_BOOLEAN, @@ -1936,7 +1929,7 @@ public function testExceptionWidthLimit($key, $stringSize, $stringCount, $intCou } // Default assertion for other adapters - $this->assertEquals(1,1); + $this->assertEquals(1, 1); } /** @@ -1950,14 +1943,14 @@ public function testCheckAttributeWidthLimit($key, $stringSize, $stringCount, $i // create same attribute in testExceptionWidthLimit $attribute = new Document([ - '$id' => 'breaking', - 'type' => Database::VAR_STRING, - 'size' => 100, - 'required' => true, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], + '$id' => 'breaking', + 'type' => Database::VAR_STRING, + 'size' => 100, + 'required' => true, + 'default' => null, + 'signed' => true, + 'array' => false, + 'filters' => [], ]); $this->expectException(LimitException::class); @@ -1965,7 +1958,7 @@ public function testCheckAttributeWidthLimit($key, $stringSize, $stringCount, $i } // Default assertion for other adapters - $this->assertEquals(1,1); + $this->assertEquals(1, 1); } public function testExceptionIndexLimit() @@ -1973,13 +1966,13 @@ public function testExceptionIndexLimit() static::getDatabase()->createCollection('indexLimit'); // add unique attributes for indexing - for ($i=0; $i < 64; $i++) { + for ($i = 0; $i < 64; $i++) { $this->assertEquals(true, static::getDatabase()->createAttribute('indexLimit', "test{$i}", Database::VAR_STRING, 16, true)); } // Testing for indexLimit // Add up to the limit, then check if the next index throws IndexLimitException - for ($i=0; $i < ($this->getDatabase()->getIndexLimit()); $i++) { + for ($i = 0; $i < ($this->getDatabase()->getIndexLimit()); $i++) { $this->assertEquals(true, static::getDatabase()->createIndex('indexLimit', "index{$i}", Database::INDEX_KEY, ["test{$i}"], [16])); } $this->expectException(LimitException::class); @@ -2228,7 +2221,8 @@ public function testUpdateAttributeDefault() /** * @depends testUpdateAttributeDefault */ - public function testUpdateAttributeRequired() { + public function testUpdateAttributeRequired() + { $database = static::getDatabase(); $database->updateAttributeRequired('flowers', 'inStock', true); @@ -2245,7 +2239,8 @@ public function testUpdateAttributeRequired() { /** * @depends testUpdateAttributeDefault */ - public function testUpdateAttributeFilter() { + public function testUpdateAttributeFilter() + { $database = static::getDatabase(); $database->createAttribute('flowers', 'cartModel', Database::VAR_STRING, 2000, false); @@ -2274,7 +2269,8 @@ public function testUpdateAttributeFilter() { /** * @depends testUpdateAttributeDefault */ - public function testUpdateAttributeFormat() { + public function testUpdateAttributeFormat() + { $database = static::getDatabase(); $database->createAttribute('flowers', 'price', Database::VAR_INTEGER, 0, false); @@ -2292,7 +2288,7 @@ public function testUpdateAttributeFormat() { $this->assertIsNumeric($doc->getAttribute('price')); $this->assertEquals(500, $doc->getAttribute('price')); - Structure::addFormat('priceRange', function($attribute) { + Structure::addFormat('priceRange', function ($attribute) { $min = $attribute['formatOptions']['min']; $max = $attribute['formatOptions']['max']; @@ -2318,7 +2314,8 @@ public function testUpdateAttributeFormat() { * @depends testUpdateAttributeDefault * @depends testUpdateAttributeFormat */ - public function testUpdateAttributeStructure() { + public function testUpdateAttributeStructure() + { // TODO: When this becomes relevant, add many more tests (from all types to all types, chaging size up&down, switchign between array/non-array... $database = static::getDatabase(); @@ -2351,7 +2348,6 @@ public function testCreatedAtUpdatedAtAssert() $this->assertGreaterThan($document->getCreatedAt(), $document->getUpdatedAt()); $this->expectException(DuplicateException::class); static::getDatabase()->createCollection('created_at'); - } public function testCreateDatetime() @@ -2374,11 +2370,11 @@ public function testCreateDatetime() $this->assertEquals(false, DateTime::isValid($document->getAttribute('date2'))); $documents = static::getDatabase()->find('datetime', [ - new Query('date', Query::TYPE_GREATER, ['1975-12-06 10:00:00+01:00']), - new Query('date', Query::TYPE_LESSER, ['2030-12-06 10:00:00-01:00']), + new Query(Query::TYPE_GREATER, 'date', ['1975-12-06 10:00:00+01:00']), + new Query(Query::TYPE_LESSER, 'date', ['2030-12-06 10:00:00-01:00']), ]); - if (in_array(static::getAdapterName(), ['mysql', 'mariadb'])) {// todo: fix in mongo + if (in_array(static::getAdapterName(), ['mysql', 'mariadb'])) { // todo: fix in mongo $this->assertEquals(1, count($documents)); } @@ -2389,7 +2385,8 @@ public function testCreateDatetime() ])); } - public function testReservedKeywords() { + public function testReservedKeywords() + { $keywords = $this->getReservedKeywords(); $database = static::getDatabase(); @@ -2471,7 +2468,7 @@ public function testReservedKeywords() { $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); $this->assertEquals('Reserved:' . $keyword, $documents[0]->getAttribute($keyword)); - $documents = $database->find($collectionName, [ new Query(Query::TYPE_EQUAL, [$keyword, "Reserved:${keyword}"]) ]); + $documents = $database->find($collectionName, [new Query(Query::TYPE_EQUAL, $keyword, ["Reserved:${keyword}"])]); $this->assertCount(1, $documents); $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); @@ -2488,4 +2485,4 @@ public function testReservedKeywords() { // TODO: Index name tests } -} \ No newline at end of file +} diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index e3d7a9fae..6167b4b14 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -18,146 +18,193 @@ public function tearDown(): void public function testCreate(): void { - $query = new Query('equal', ['title', 'Iron Man']); + $query = new Query(Query::TYPE_EQUAL, 'title', ['Iron Man']); - $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('title', $query->getParams()[0]); - $this->assertEquals('Iron Man', $query->getParams()[1]); + $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); + $this->assertEquals('title', $query->getAttribute()); + $this->assertEquals('Iron Man', $query->getValues()[0]); + + $query = new Query(Query::TYPE_ORDERDESC, 'score'); + + $this->assertEquals(Query::TYPE_ORDERDESC, $query->getMethod()); + $this->assertEquals('score', $query->getAttribute()); + $this->assertEquals([], $query->getValues()); + + $query = new Query(Query::TYPE_LIMIT, values: [10]); + + $this->assertEquals(Query::TYPE_LIMIT, $query->getMethod()); + $this->assertEquals('', $query->getAttribute()); + $this->assertEquals(10, $query->getValues()[0]); + + $query = Query::equal('title', ['Iron Man']); + + $this->assertEquals(Query::TYPE_EQUAL, $query->getMethod()); + $this->assertEquals('title', $query->getAttribute()); + $this->assertEquals('Iron Man', $query->getValues()[0]); + + $query = Query::greaterThan('score', 10); + + $this->assertEquals(Query::TYPE_GREATER, $query->getMethod()); + $this->assertEquals('score', $query->getAttribute()); + $this->assertEquals(10, $query->getValues()[0]); + + $query = Query::search('search', 'John Doe'); + + $this->assertEquals(Query::TYPE_SEARCH, $query->getMethod()); + $this->assertEquals('search', $query->getAttribute()); + $this->assertEquals('John Doe', $query->getValues()[0]); + + $query = Query::orderAsc('score'); + + $this->assertEquals(Query::TYPE_ORDERASC, $query->getMethod()); + $this->assertEquals('score', $query->getAttribute()); + $this->assertEquals([], $query->getValues()); + + $query = Query::limit(10); + + $this->assertEquals(Query::TYPE_LIMIT, $query->getMethod()); + $this->assertEquals('', $query->getAttribute()); + $this->assertEquals([10], $query->getValues()); + + $query = Query::cursorAfter('cursor'); + + $this->assertEquals(Query::TYPE_CURSORAFTER, $query->getMethod()); + $this->assertEquals('', $query->getAttribute()); + $this->assertEquals(['cursor'], $query->getValues()); } - + public function testParse() { $query = Query::parse('equal("title", "Iron Man")'); $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('title', $query->getParams()[0]); - $this->assertEquals('Iron Man', $query->getParams()[1]); + $this->assertEquals('title', $query->getAttribute()); + $this->assertEquals('Iron Man', $query->getValues()[0]); - $query = Query::parse('lesser("year", 2001)'); + $query = Query::parse('lessThan("year", 2001)'); - $this->assertEquals('lesser', $query->getMethod()); - $this->assertEquals('year', $query->getParams()[0]); - $this->assertEquals(2001, $query->getParams()[1]); + $this->assertEquals('lessThan', $query->getMethod()); + $this->assertEquals('year', $query->getAttribute()); + $this->assertEquals(2001, $query->getValues()[0]); $query = Query::parse('equal("published", true)'); $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('published', $query->getParams()[0]); - $this->assertTrue($query->getParams()[1]); + $this->assertEquals('published', $query->getAttribute()); + $this->assertTrue($query->getValues()[0]); $query = Query::parse('equal("published", false)'); $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('published', $query->getParams()[0]); - $this->assertFalse($query->getParams()[1]); + $this->assertEquals('published', $query->getAttribute()); + $this->assertFalse($query->getValues()[0]); - $query = Query::parse('notContains("actors", [ " Johnny Depp ", " Brad Pitt" , \'Al Pacino \' ])'); + $query = Query::parse('equal("actors", [ " Johnny Depp ", " Brad Pitt" , \'Al Pacino \' ])'); - $this->assertEquals('notContains', $query->getMethod()); - $this->assertEquals('actors', $query->getParams()[0]); - $this->assertEquals(" Johnny Depp ", $query->getParams()[1][0]); - $this->assertEquals(" Brad Pitt", $query->getParams()[1][1]); - $this->assertEquals("Al Pacino ", $query->getParams()[1][2]); + $this->assertEquals('equal', $query->getMethod()); + $this->assertEquals('actors', $query->getAttribute()); + $this->assertEquals(" Johnny Depp ", $query->getValues()[0]); + $this->assertEquals(" Brad Pitt", $query->getValues()[1]); + $this->assertEquals("Al Pacino ", $query->getValues()[2]); $query = Query::parse('equal("actors", ["Brad Pitt", "Johnny Depp"])'); $this->assertEquals('equal', $query->getMethod()); - $this->assertEquals('actors', $query->getParams()[0]); - $this->assertEquals("Brad Pitt", $query->getParams()[1][0]); - $this->assertEquals("Johnny Depp", $query->getParams()[1][1]); + $this->assertEquals('actors', $query->getAttribute()); + $this->assertEquals("Brad Pitt", $query->getValues()[0]); + $this->assertEquals("Johnny Depp", $query->getValues()[1]); $query = Query::parse('contains("writers","Tim O\'Reilly")'); $this->assertEquals('contains', $query->getMethod()); - $this->assertEquals('writers', $query->getParams()[0]); - $this->assertEquals("Tim O'Reilly", $query->getParams()[1]); + $this->assertEquals('writers', $query->getAttribute()); + $this->assertEquals("Tim O'Reilly", $query->getValues()[0]); - $query = Query::parse('greater("score", 8.5)'); + $query = Query::parse('greaterThan("score", 8.5)'); - $this->assertEquals('greater', $query->getMethod()); - $this->assertEquals('score', $query->getParams()[0]); - $this->assertEquals(8.5, $query->getParams()[1]); + $this->assertEquals('greaterThan', $query->getMethod()); + $this->assertEquals('score', $query->getAttribute()); + $this->assertEquals(8.5, $query->getValues()[0]); $query = Query::parse('notEqual("director", "null")'); $this->assertEquals('notEqual', $query->getMethod()); - $this->assertEquals('director', $query->getParams()[0]); - $this->assertEquals('null', $query->getParams()[1]); + $this->assertEquals('director', $query->getAttribute()); + $this->assertEquals('null', $query->getValues()[0]); $query = Query::parse('notEqual("director", null)'); $this->assertEquals('notEqual', $query->getMethod()); - $this->assertEquals('director', $query->getParams()[0]); - $this->assertEquals(null, $query->getParams()[1]); + $this->assertEquals('director', $query->getAttribute()); + $this->assertEquals(null, $query->getValues()[0]); } public function testParseV2() { - $query = Query::parse('equal(1)'); - $this->assertCount(1, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); + $query = Query::parse('equal("attr", 1)'); + $this->assertCount(1, $query->getValues()); + $this->assertEquals("attr", $query->getAttribute()); + $this->assertEquals([1], $query->getValues()); $query = Query::parse('equal(1, ["[Hello] World"])'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("[Hello] World", $query->getParams()[1][0]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("[Hello] World", $query->getValues()[0]); $query = Query::parse('equal(1, , , ["[Hello] World"], , , )'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("[Hello] World", $query->getParams()[1][0]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("[Hello] World", $query->getValues()[0]); $query = Query::parse('equal(1, ["(Hello) World"])'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("(Hello) World", $query->getParams()[1][0]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("(Hello) World", $query->getValues()[0]); $query = Query::parse('equal(1, ["Hello , World"])'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("Hello , World", $query->getParams()[1][0]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("Hello , World", $query->getValues()[0]); $query = Query::parse('equal(1, ["Hello , World"])'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("Hello , World", $query->getParams()[1][0]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("Hello , World", $query->getValues()[0]); $query = Query::parse('equal(1, ["Hello /\ World"])'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("Hello /\ World", $query->getParams()[1][0]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("Hello /\ World", $query->getValues()[0]); $query = Query::parse('equal(1, ["I\'m [**awesome**], \"Dev\"eloper"])'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("I'm [**awesome**], \"Dev\"eloper", $query->getParams()[1][0]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("I'm [**awesome**], \"Dev\"eloper", $query->getValues()[0]); $query = Query::parse('equal(1, "\\\\")'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("\\\\", $query->getParams()[1]); - + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("\\\\", $query->getValues()[0]); + $query = Query::parse('equal(1, "Hello\\\\")'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("Hello\\\\", $query->getParams()[1]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("Hello\\\\", $query->getValues()[0]); $query = Query::parse('equal(1, "Hello\\\\", "World")'); - $this->assertCount(3, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("Hello\\\\", $query->getParams()[1]); - $this->assertEquals("World", $query->getParams()[2]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("Hello\\\\", $query->getValues()[0]); $query = Query::parse('equal(1, "Hello\\", World")'); - $this->assertCount(2, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("Hello\", World", $query->getParams()[1]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("Hello\", World", $query->getValues()[0]); $query = Query::parse('equal(1, "Hello\\\\\\", ", "World")'); - $this->assertCount(3, $query->getParams()); - $this->assertEquals(1, $query->getParams()[0]); - $this->assertEquals("Hello\\\\\", ", $query->getParams()[1]); - $this->assertEquals("World", $query->getParams()[2]); + $this->assertCount(1, $query->getValues()); + $this->assertEquals(1, $query->getAttribute()); + $this->assertEquals("Hello\\\\\", ", $query->getValues()[0]); } /* @@ -180,32 +227,25 @@ public function testAlias() public function testParseComplex() { $queries = [ - Query::parse('equal("One",3,[55.55,\'Works\',true],false,null)'), + Query::parse('equal("One",[55.55,\'Works\',true])'), // Same query with random spaces - Query::parse('equal("One" , 3 , [55.55, \'Works\',true], false, null)') + Query::parse('equal("One" , [55.55, \'Works\',true])') ]; foreach ($queries as $query) { $this->assertEquals('equal', $query->getMethod()); - $this->assertCount(5, $query->getParams()); - $this->assertIsString($query->getParams()[0]); - $this->assertEquals('One', $query->getParams()[0]); + $this->assertIsString($query->getAttribute()); + $this->assertEquals('One', $query->getAttribute()); - $this->assertIsNumeric($query->getParams()[1]); - $this->assertEquals(3, $query->getParams()[1]); + $this->assertCount(3, $query->getValues()); - $this->assertIsArray($query->getParams()[2]); - $this->assertCount(3, $query->getParams()[2]); - $this->assertIsNumeric($query->getParams()[2][0]); - $this->assertEquals(55.55, $query->getParams()[2][0]); - $this->assertIsString($query->getParams()[2][1]); - $this->assertEquals('Works', $query->getParams()[2][1]); - $this->assertTrue($query->getParams()[2][2]); + $this->assertIsNumeric($query->getValues()[0]); + $this->assertEquals(55.55, $query->getValues()[0]); - $this->assertFalse($query->getParams()[3]); + $this->assertEquals('Works', $query->getValues()[1]); - $this->assertNull($query->getParams()[4]); + $this->assertTrue($query->getValues()[2]); } } @@ -213,31 +253,10 @@ public function testGetAttribute() { $query = Query::parse('equal("title", "Iron Man")'); - $this->assertIsArray($query->getParams()); - $this->assertCount(2, $query->getParams()); - $this->assertEquals('title', $query->getParams()[0]); - $this->assertEquals('Iron Man', $query->getParams()[1]); - } - - public function testHelperMethods() - { - $query = Query::parse('equal("title", "Iron Man")'); - - $this->assertEquals('title', $query->getFirstParam()); - $this->assertEquals('title', $query->getParams()[0]); - - $this->assertIsArray($query->getArrayParam(1)); - $this->assertCount(1, $query->getArrayParam(1)); - - $query->setFirstParam("name"); - - $this->assertEquals('name', $query->getFirstParam()); - $this->assertEquals('name', $query->getParams()[0]); - - $query = Query::parse('equal("title", ["Iron Man", "Spider Man"])'); - - $this->assertIsArray($query->getArrayParam(1)); - $this->assertCount(2, $query->getArrayParam(1)); + $this->assertIsArray($query->getValues()); + $this->assertCount(1, $query->getValues()); + $this->assertEquals('title', $query->getAttribute()); + $this->assertEquals('Iron Man', $query->getValues()[0]); } public function testGetMethod() diff --git a/tests/Database/Validator/QueriesTest.php b/tests/Database/Validator/QueriesTest.php index 2c98e9ffc..bccd5ab08 100644 --- a/tests/Database/Validator/QueriesTest.php +++ b/tests/Database/Validator/QueriesTest.php @@ -97,7 +97,8 @@ class QueriesTest extends TestCase public function setUp(): void { // Query validator expects Document[] - $attributes = []; /** @var Document[] $attributes */ + $attributes = []; + /** @var Document[] $attributes */ foreach ($this->collection['attributes'] as $attribute) { $attributes[] = new Document($attribute); } @@ -169,19 +170,19 @@ public function testQueries() $this->assertEquals(true, $validator->isValid($this->queries)); - $this->queries[] = Query::parse('lesserEqual("price", 6.50)'); + $this->queries[] = Query::parse('lessThan("price", 6.50)'); $this->assertEquals(true, $validator->isValid($this->queries)); // test for FAILURE - $this->queries[] = Query::parse('greater("rating", 4)'); + $this->queries[] = Query::parse('greaterThan("rating", 4)'); $this->assertEquals(false, $validator->isValid($this->queries)); $this->assertEquals("Index not found: title,description,price,rating", $validator->getDescription()); // test for queued index - $query1 = Query::parse('lesserEqual("price", 6.50)'); + $query1 = Query::parse('lessThan("price", 6.50)'); $query2 = Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'); $this->queries = [$query1, $query2]; diff --git a/tests/Database/Validator/QueryValidatorTest.php b/tests/Database/Validator/QueryValidatorTest.php index 9a44bd508..7db85e53e 100644 --- a/tests/Database/Validator/QueryValidatorTest.php +++ b/tests/Database/Validator/QueryValidatorTest.php @@ -110,8 +110,8 @@ public function testQuery() $this->assertEquals(true, $validator->isValid(Query::parse('equal("$id", ["Iron Man", "Ant Man"])'))); $this->assertEquals(true, $validator->isValid(Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'))); $this->assertEquals(true, $validator->isValid(Query::parse('equal("description", "Best movie ever")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('greater("rating", 4)'))); - $this->assertEquals(true, $validator->isValid(Query::parse('lesserEqual("price", 6.50)'))); + $this->assertEquals(true, $validator->isValid(Query::parse('greaterThan("rating", 4)')), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid(Query::parse('lessThan("price", 6.50)'))); $this->assertEquals(true, $validator->isValid(Query::parse('contains("tags", "action")'))); } @@ -158,7 +158,7 @@ public function testMethodWrongType() public function testQueryDate() { $validator = new QueryValidator($this->schema); - $response = $validator->isValid(Query::parse('birthDay.greater("1960-01-01 10:10:10")')); + $response = $validator->isValid(Query::parse('greaterThan("birthDay", "1960-01-01 10:10:10")')); $this->assertEquals(true, $response); } } From 4348f07d232678452b47b2744a2c281198e0a606 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 9 Aug 2022 22:41:16 +0000 Subject: [PATCH 25/27] Update Database find methods to only use queries --- bin/tasks/query.php | 24 +- src/Database/Adapter.php | 6 +- src/Database/Adapter/MariaDB.php | 1 + src/Database/Database.php | 115 +++--- src/Database/Query.php | 8 +- src/Database/Validator/Queries.php | 82 ++++ tests/Database/Adapter/MongoDBTest.php | 4 +- tests/Database/Base.php | 494 ++++++++++++++++++++----- tests/Database/QueryTest.php | 6 +- 9 files changed, 572 insertions(+), 168 deletions(-) diff --git a/bin/tasks/query.php b/bin/tasks/query.php index 80854a3db..0da48dd3b 100644 --- a/bin/tasks/query.php +++ b/bin/tasks/query.php @@ -3,6 +3,7 @@ /** * @var CLI */ global $cli; + use Faker\Factory; use MongoDB\Client; use Utopia\Cache\Cache; @@ -17,19 +18,21 @@ use Utopia\Database\Validator\Authorization; use Utopia\Validator\Numeric; use Utopia\Validator\Text; + $cli ->task('query') ->desc('Query mock data') ->param('adapter', '', new Text(0), 'Database adapter', false) ->param('name', '', new Text(0), 'Name of created database.', false) ->param('limit', 25, new Numeric(), 'Limit on queried documents', true) - ->action(function ($adapter, $name, $limit) { + ->action(function (string $adapter, string $name, int $limit) { $database = null; switch ($adapter) { case 'mongodb': $options = ["typeMap" => ['root' => 'array', 'document' => 'array', 'array' => 'array']]; - $client = new Client('mongodb://mongo/', + $client = new Client( + 'mongodb://mongo/', [ 'username' => 'root', 'password' => 'example', @@ -130,7 +133,8 @@ fclose($f); }); -function runQueries($database, $limit) { +function runQueries(Database $database, int $limit) +{ $results = []; // Recent travel blogs $query = ["created.greater(1262322000)", "genre.equal('travel')"]; @@ -151,21 +155,23 @@ function runQueries($database, $limit) { return $results; } -function addRoles($faker, $count) { - for ($i=0; $i < $count; $i++) { +function addRoles($faker, $count) +{ + for ($i = 0; $i < $count; $i++) { Authorization::setRole($faker->numerify('user####')); } return count(Authorization::getRoles()); } -function runQuery($query, $database, $limit) { - Console::log('Running query: ['.implode(', ', $query).']'); - $query = array_map(function($q) { +function runQuery(array $query, Database $database, int $limit) +{ + Console::log('Running query: [' . implode(', ', $query) . ']'); + $query = array_map(function ($q) { return Query::parse($q); }, $query); $start = microtime(true); - $documents = $database->find('articles', $query, $limit); + $database->find('articles', array_merge($query, [Query::limit($limit)])); $time = microtime(true) - $start; Console::success("{$time} s"); return $time; diff --git a/src/Database/Adapter.php b/src/Database/Adapter.php index 88cf12759..d6d058cf4 100644 --- a/src/Database/Adapter.php +++ b/src/Database/Adapter.php @@ -318,7 +318,7 @@ abstract public function deleteDocument(string $collection, string $id): bool; * @param int $offset * @param array $orderAttributes * @param array $orderTypes - * @param array $cursor Array copy of document used for before/after pagination + * @param array $cursor * @param string $cursorDirection * * @return Document[] @@ -459,11 +459,11 @@ abstract public function getSupportForCasting(): bool; * @throws Exception * @return string */ - public function filter(string $value):string + public function filter(string $value): string { $value = preg_replace("/[^A-Za-z0-9\_\-]/", '', $value); - if(\is_null($value)) { + if (\is_null($value)) { throw new Exception('Failed to filter key'); } diff --git a/src/Database/Adapter/MariaDB.php b/src/Database/Adapter/MariaDB.php index c9fd88aab..3a9b6a207 100644 --- a/src/Database/Adapter/MariaDB.php +++ b/src/Database/Adapter/MariaDB.php @@ -776,6 +776,7 @@ public function deleteDocument(string $collection, string $id): bool * @param array $orderTypes * @param array $cursor * @param string $cursorDirection + * * @return Document[] * @throws Exception * @throws PDOException diff --git a/src/Database/Database.php b/src/Database/Database.php index 4f253a01f..65c45114a 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -6,6 +6,7 @@ use Utopia\Database\Exception\Duplicate; use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Structure; +use Utopia\Database\Validator\Queries; use Utopia\Database\Exception\Authorization as AuthorizationException; use Utopia\Database\Exception\Duplicate as DuplicateException; use Utopia\Database\Exception\Limit as LimitException; @@ -203,7 +204,6 @@ function (?string $value) { return $value; } ); - } /** @@ -362,7 +362,7 @@ public function delete(string $name): bool public function createCollection(string $id, array $attributes = [], array $indexes = []): Document { $collection = $this->getCollection($id); - if(!$collection->isEmpty() && $id !== self::METADATA){ + if (!$collection->isEmpty() && $id !== self::METADATA) { throw new Duplicate('Collection ' . $id . ' Exists!'); } @@ -431,7 +431,10 @@ public function listCollections($limit = 25, $offset = 0): array { Authorization::disable(); - $result = $this->find(self::METADATA, [], $limit, $offset); + $result = $this->find(self::METADATA, [ + Query::limit($limit), + Query::offset($offset) + ]); Authorization::reset(); @@ -593,9 +596,9 @@ private function updateAttributeMeta(string $collection, string $id, callable $u $attributes = $collection->getAttribute('attributes', []); - $attributeIndex = \array_search($id, \array_map(fn($attribute) => $attribute['$id'], $attributes)); + $attributeIndex = \array_search($id, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); - if($attributeIndex === false) { + if ($attributeIndex === false) { throw new Exception('Attribute not found'); } @@ -621,7 +624,7 @@ private function updateAttributeMeta(string $collection, string $id, callable $u */ public function updateAttributeRequired(string $collection, string $id, bool $required): void { - $this->updateAttributeMeta($collection, $id, function($attribute) use($required) { + $this->updateAttributeMeta($collection, $id, function ($attribute) use ($required) { $attribute->setAttribute('required', $required); }); } @@ -637,7 +640,7 @@ public function updateAttributeRequired(string $collection, string $id, bool $re */ public function updateAttributeFormat(string $collection, string $id, string $format): void { - $this->updateAttributeMeta($collection, $id, function($attribute) use($format) { + $this->updateAttributeMeta($collection, $id, function ($attribute) use ($format) { if (!Structure::hasFormat($format, $attribute->getAttribute('type'))) { throw new Exception('Format ("' . $format . '") not available for this attribute type ("' . $attribute->getAttribute('type') . '")'); } @@ -657,7 +660,7 @@ public function updateAttributeFormat(string $collection, string $id, string $fo */ public function updateAttributeFormatOptions(string $collection, string $id, array $formatOptions): void { - $this->updateAttributeMeta($collection, $id, function($attribute) use($formatOptions) { + $this->updateAttributeMeta($collection, $id, function ($attribute) use ($formatOptions) { $attribute->setAttribute('formatOptions', $formatOptions); }); } @@ -673,7 +676,7 @@ public function updateAttributeFormatOptions(string $collection, string $id, arr */ public function updateAttributeFilters(string $collection, string $id, array $filters): void { - $this->updateAttributeMeta($collection, $id, function($attribute) use($filters) { + $this->updateAttributeMeta($collection, $id, function ($attribute) use ($filters) { $attribute->setAttribute('filters', $filters); }); } @@ -689,7 +692,7 @@ public function updateAttributeFilters(string $collection, string $id, array $fi */ public function updateAttributeDefault(string $collection, string $id, $default = null): void { - $this->updateAttributeMeta($collection, $id, function($attribute) use($default) { + $this->updateAttributeMeta($collection, $id, function ($attribute) use ($default) { if ($attribute->getAttribute('required') === true) { throw new Exception('Cannot set a default value on a required attribute'); } @@ -718,7 +721,7 @@ public function updateAttributeDefault(string $collection, string $id, $default throw new Exception('Unknown attribute type for: ' . $default); break; } - + $attribute->setAttribute('default', $default); }); } @@ -739,20 +742,20 @@ public function updateAttributeDefault(string $collection, string $id, $default */ public function updateAttribute(string $collection, string $id, string $type = null, int $size = null, bool $signed = null, bool $array = null): bool { - $this->updateAttributeMeta($collection, $id, function($attribute, $collectionDoc, $attributeIndex) use($collection, $id, $type, $size, $signed, $array, &$success) { - if($type !== null || $size !== null || $signed !== null || $array !== null) { + $this->updateAttributeMeta($collection, $id, function ($attribute, $collectionDoc, $attributeIndex) use ($collection, $id, $type, $size, $signed, $array, &$success) { + if ($type !== null || $size !== null || $signed !== null || $array !== null) { $type ??= $attribute->getAttribute('type'); $size ??= $attribute->getAttribute('size'); $signed ??= $attribute->getAttribute('signed'); $array ??= $attribute->getAttribute('array'); - + switch ($type) { case self::VAR_STRING: if ($size > $this->adapter->getStringLimit()) { throw new Exception('Max size allowed for string is: ' . number_format($this->adapter->getStringLimit())); } break; - + case self::VAR_INTEGER: $limit = ($signed) ? $this->adapter->getIntLimit() / 2 : $this->adapter->getIntLimit(); if ($size > $limit) { @@ -766,7 +769,7 @@ public function updateAttribute(string $collection, string $id, string $type = n throw new Exception('Unknown attribute type: ' . $type); break; } - + $attribute ->setAttribute('type', $type) ->setAttribute('size', $size) @@ -783,7 +786,7 @@ public function updateAttribute(string $collection, string $id, string $type = n ) { throw new LimitException('Row width limit reached. Cannot create new attribute.'); } - + $this->adapter->updateAttribute($collection, $id, $type, $size, $signed, $array); } }); @@ -871,15 +874,15 @@ public function renameAttribute(string $collection, string $old, string $new): b $attributes = $collection->getAttribute('attributes', []); $indexes = $collection->getAttribute('indexes', []); - $attribute = \in_array($old, \array_map(fn($attribute) => $attribute['$id'], $attributes)); + $attribute = \in_array($old, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); - if($attribute === false) { + if ($attribute === false) { throw new Exception('Attribute not found'); } - $attributeNew = \in_array($new, \array_map(fn($attribute) => $attribute['$id'], $attributes)); + $attributeNew = \in_array($new, \array_map(fn ($attribute) => $attribute['$id'], $attributes)); - if($attributeNew !== false) { + if ($attributeNew !== false) { throw new DuplicateException('Attribute name already used'); } @@ -894,7 +897,7 @@ public function renameAttribute(string $collection, string $old, string $new): b foreach ($indexes as $index) { $indexAttributes = $index->getAttribute('attributes', []); - $indexAttributes = \array_map(fn($attribute) => ($attribute === $old) ? $new : $attribute , $indexAttributes); + $indexAttributes = \array_map(fn ($attribute) => ($attribute === $old) ? $new : $attribute, $indexAttributes); $index->setAttribute('attributes', $indexAttributes); } @@ -924,15 +927,15 @@ public function renameIndex(string $collection, string $old, string $new): bool $indexes = $collection->getAttribute('indexes', []); - $index = \in_array($old, \array_map(fn($index) => $index['$id'], $indexes)); + $index = \in_array($old, \array_map(fn ($index) => $index['$id'], $indexes)); - if($index === false) { + if ($index === false) { throw new Exception('Index not found'); } - $indexNew = \in_array($new, \array_map(fn($index) => $index['$id'], $indexes)); + $indexNew = \in_array($new, \array_map(fn ($index) => $index['$id'], $indexes)); - if($indexNew !== false) { + if ($indexNew !== false) { throw new DuplicateException('Index name already used'); } @@ -1141,8 +1144,7 @@ public function createDocument(string $collection, Document $document): Document ->setAttribute('$id', empty($document->getId()) ? $this->getId() : $document->getId()) ->setAttribute('$collection', $collection->getId()) ->setAttribute('$createdAt', $time) - ->setAttribute('$updatedAt', $time) - ; + ->setAttribute('$updatedAt', $time); $document = $this->encode($collection, $document); @@ -1266,29 +1268,41 @@ public function deleteCachedDocument(string $collection, string $id): bool * * @param string $collection * @param Query[] $queries - * @param int $limit - * @param int $offset - * @param array $orderAttributes - * @param array $orderTypes - * @param Document|null $cursor - * @param string $cursorDirection * * @return Document[] * @throws Exception */ - public function find(string $collection, array $queries = [], int $limit = 25, int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $cursor = null, string $cursorDirection = self::CURSOR_AFTER): array + public function find(string $collection, array $queries = []): array { $collection = $this->getCollection($collection); + $queriesByMethod = Queries::byMethod($queries); + /** @var Query[] */ $filters = $queriesByMethod['filters']; + /** @var int */ $limit = $queriesByMethod['limit']; + /** @var int */ $offset = $queriesByMethod['offset']; + /** @var string[] */ $orderAttributes = $queriesByMethod['orderAttributes']; + /** @var string[] */ $orderTypes = $queriesByMethod['orderTypes']; + /** @var Document */ $cursor = $queriesByMethod['cursor']; + /** @var string */ $cursorDirection = $queriesByMethod['cursorDirection']; + if (!empty($cursor) && $cursor->getCollection() !== $collection->getId()) { throw new Exception("cursor Document must be from the same Collection."); } $cursor = empty($cursor) ? [] : $cursor->getArrayCopy(); - $queries = self::convertQueries($collection, $queries); - - $results = $this->adapter->find($collection->getId(), $queries, $limit, $offset, $orderAttributes, $orderTypes, $cursor, $cursorDirection); + $queries = self::convertQueries($collection, $filters); + + $results = $this->adapter->find( + $collection->getId(), + $queries, + $limit, + $offset, + $orderAttributes, + $orderTypes, + $cursor, + $cursorDirection, + ); foreach ($results as &$node) { $node = $this->casting($collection, $node); @@ -1310,9 +1324,9 @@ public function find(string $collection, array $queries = [], int $limit = 25, i * * @return Document|bool */ - public function findOne(string $collection, array $queries = [], int $offset = 0, array $orderAttributes = [], array $orderTypes = [], Document $cursor = null, string $cursorDirection = Database::CURSOR_AFTER) + public function findOne(string $collection, array $queries = []) { - $results = $this->find($collection, $queries, /*limit*/ 1, $offset, $orderAttributes, $orderTypes, $cursor, $cursorDirection); + $results = $this->find($collection, \array_merge([Query::limit(1)], $queries)); return \reset($results); } @@ -1528,7 +1542,7 @@ public function casting(Document $collection, Document $document): Document $type = $attribute['type'] ?? ''; $array = $attribute['array'] ?? false; $value = $document->getAttribute($key, null); - if(is_null($value)) { + if (is_null($value)) { continue; } @@ -1539,7 +1553,7 @@ public function casting(Document $collection, Document $document): Document } foreach ($value as &$node) { - if(is_null($value)) { + if (is_null($value)) { continue; } switch ($type) { @@ -1585,7 +1599,7 @@ protected function encodeAttribute(string $name, $value, Document $document) } try { - if(array_key_exists($name, $this->instanceFilters)) { + if (array_key_exists($name, $this->instanceFilters)) { $value = $this->instanceFilters[$name]['encode']($value, $document, $this); } else { $value = self::$filters[$name]['encode']($value, $document, $this); @@ -1616,7 +1630,7 @@ protected function decodeAttribute(string $name, $value, Document $document) } try { - if(array_key_exists($name, $this->instanceFilters)) { + if (array_key_exists($name, $this->instanceFilters)) { $value = $this->instanceFilters[$name]['decode']($value, $document, $this); } else { $value = self::$filters[$name]['decode']($value, $document, $this); @@ -1676,18 +1690,18 @@ public function getIndexLimit() * @return Query[] * @throws Exception */ - public static function convertQueries(Document $collection, array $queries):array + public static function convertQueries(Document $collection, array $queries): array { $attributes = $collection->getAttribute('attributes', []); - foreach ($attributes as $v){ + foreach ($attributes as $v) { /* @var $v Document */ switch ($v->getAttribute('type')) { case Database::VAR_DATETIME: - foreach ($queries as $qk => $q){ - if($q->getAttribute() === $v->getId()){ + foreach ($queries as $qk => $q) { + if ($q->getAttribute() === $v->getId()) { $arr = $q->getValues(); - foreach ($arr as $vk => $vv){ + foreach ($arr as $vk => $vv) { $arr[$vk] = DateTime::setTimezone($vv); } $q->setValues($arr); @@ -1699,5 +1713,4 @@ public static function convertQueries(Document $collection, array $queries):arra } return $queries; } - -} \ No newline at end of file +} diff --git a/src/Database/Query.php b/src/Database/Query.php index 0df5b5275..b805f9b56 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -454,9 +454,9 @@ public static function greaterThanEqual(string $attribute, $value): self /** * Helper method to create Query with contains method */ - public static function contains(string $attribute, $value): self + public static function contains(string $attribute, array $values): self { - return new self(self::TYPE_CONTAINS, $attribute, [$value]); + return new self(self::TYPE_CONTAINS, $attribute, $values); } /** @@ -502,7 +502,7 @@ public static function offset(int $value): self /** * Helper method to create Query with cursorAfter method */ - public static function cursorAfter(string $value): self + public static function cursorAfter(Document $value): self { return new self(self::TYPE_CURSORAFTER, values: [$value]); } @@ -510,7 +510,7 @@ public static function cursorAfter(string $value): self /** * Helper method to create Query with cursorBefore method */ - public static function cursorBefore(string $value): self + public static function cursorBefore(Document $value): self { return new self(self::TYPE_CURSORBEFORE, values: [$value]); } diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index a50336e43..5a205da99 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -184,4 +184,86 @@ protected function arrayMatch($indexes, $queries): bool return true; } + + /** + * Iterates through $queries and returns an array with: + * - filters: array of filter queries + * - limit: int + * - offset: int + * - orderAttributes: array of attribute keys + * - orderTypes: array of Database::ORDER_ASC or Database::ORDER_DESC + * - cursor: Document + * - cursorDirection: Database::CURSOR_BEFORE or Database::CURSOR_AFTER + * + * @param Query[] $queries + * @param int $defaultLimit + * @param int $defaultOffset + * @param string $defaultCursorDirection + * + * @return array + */ + public static function byMethod(array $queries, int $defaultLimit = 25, int $defaultOffset = 0, string $defaultCursorDirection = Database::CURSOR_AFTER): array + { + $filters = []; + $limit = null; + $offset = null; + $orderAttributes = []; + $orderTypes = []; + $cursor = null; + $cursorDirection = null; + foreach ($queries as $query) { + if (!$query instanceof Query) continue; + + $method = $query->getMethod(); + $attribute = $query->getAttribute(); + $values = $query->getValues(); + switch ($method) { + case Query::TYPE_ORDERASC: + case Query::TYPE_ORDERDESC: + if (!empty($attribute)) { + $orderAttributes[] = $attribute; + } + + $orderTypes[] = $method === Query::TYPE_ORDERASC ? Database::ORDER_ASC : Database::ORDER_DESC; + break; + + case Query::TYPE_LIMIT: + // keep the 1st limit encountered and ignore the rest + if ($limit !== null) break; + + $limit = $values[0] ?? $limit; + break; + + case Query::TYPE_OFFSET: + // keep the 1st offset encountered and ignore the rest + if ($offset !== null) break; + + $offset = $values[0] ?? $limit; + break; + + case Query::TYPE_CURSORAFTER: + case Query::TYPE_CURSORBEFORE: + // keep the 1st cursor encountered and ignore the rest + if ($cursor !== null) break; + + $cursor = $values[0] ?? $limit; + $cursorDirection = $method === Query::TYPE_CURSORAFTER ? Database::CURSOR_AFTER : Database::CURSOR_BEFORE; + break; + + default: + $filters[] = $query; + break; + } + } + + return [ + 'filters' => $filters, + 'limit' => $limit ?? $defaultLimit, + 'offset' => $offset ?? $defaultOffset, + 'orderAttributes' => $orderAttributes, + 'orderTypes' => $orderTypes, + 'cursor' => $cursor, + 'cursorDirection' => $cursorDirection ?? $defaultCursorDirection, + ]; + } } diff --git a/tests/Database/Adapter/MongoDBTest.php b/tests/Database/Adapter/MongoDBTest.php index 1a671db19..9de9612f4 100644 --- a/tests/Database/Adapter/MongoDBTest.php +++ b/tests/Database/Adapter/MongoDBTest.php @@ -113,7 +113,9 @@ public function testListDocumentSearch(Document $document) 'empty' => [], ])); - $documents = static::getDatabase()->find('documents', [new Query(Query::TYPE_SEARCH, 'string', ['*test+alias@email-provider.com'])]); + $documents = static::getDatabase()->find('documents', [ + Query::search('string', '*test+alias@email-provider.com') + ]); $this->assertEquals(1, count($documents)); diff --git a/tests/Database/Base.php b/tests/Database/Base.php index 0387f1059..d5e9e499a 100644 --- a/tests/Database/Base.php +++ b/tests/Database/Base.php @@ -666,7 +666,7 @@ public function testListDocumentSearch(Document $document) * Allow reserved keywords for search */ $documents = static::getDatabase()->find('documents', [ - new Query(Query::TYPE_SEARCH, 'string', ['*test+alias@email-provider.com']), + Query::search('string', '*test+alias@email-provider.com'), ]); $this->assertEquals(1, count($documents)); @@ -881,17 +881,33 @@ public function testFind(Document $document) /** * Check $id: Notice, this orders ID names alphabetically, not by internal numeric ID */ - $documents = static::getDatabase()->find('movies', [], 25, 0, ['$id'], [Database::ORDER_DESC]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc('$id'), + ]); $this->assertEquals($lastDocumentId, $documents[0]->getId()); - $documents = static::getDatabase()->find('movies', [], 25, 0, ['$id'], [Database::ORDER_ASC]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderAsc('$id'), + ]); $this->assertEquals($firstDocumentId, $documents[0]->getId()); /** * Check internal numeric ID sorting */ - $documents = static::getDatabase()->find('movies', [], 25, 0, [], [Database::ORDER_DESC]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc(''), + ]); $this->assertEquals($movieDocuments[\count($movieDocuments) - 1]->getId(), $documents[0]->getId()); - $documents = static::getDatabase()->find('movies', [], 25, 0, [], [Database::ORDER_ASC]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderAsc(''), + ]); $this->assertEquals($movieDocuments[0]->getId(), $documents[0]->getId()); @@ -908,7 +924,7 @@ public function testFind(Document $document) * Check an Integer condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, 'year', [2019]), + Query::equal('year', [2019]), ]); $this->assertEquals(2, count($documents)); @@ -919,7 +935,7 @@ public function testFind(Document $document) * Boolean condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, 'active', [true]), + Query::equal('active', [true]), ]); $this->assertEquals(4, count($documents)); @@ -928,7 +944,7 @@ public function testFind(Document $document) * String condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, 'director', ['TBD']), + Query::equal('director', ['TBD']), ]); $this->assertEquals(2, count($documents)); @@ -937,22 +953,21 @@ public function testFind(Document $document) * Not Equal query */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_NOTEQUAL, 'director', ['TBD', 'Joe Johnston']), + Query::notEqual('director', 'TBD'), ]); $this->assertGreaterThan(0, count($documents)); foreach ($documents as $document) { - $isAllowed = $document['director'] !== 'TBD' || $document['director'] !== 'Joe Johnston'; - $this->assertTrue($isAllowed); + $this->assertTrue($document['director'] !== 'TBD'); } /** * Float condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_LESSER, 'price', [26.00]), - new Query(Query::TYPE_GREATER, 'price', [25.98]), + Query::lessThan('price', 26.00), + Query::greaterThan('price', 25.98), ]); // TODO@kodumbeats hacky way to pass mariadb tests @@ -962,7 +977,7 @@ public function testFind(Document $document) * Array contains condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_CONTAINS, 'generes', ['comics']), + Query::contains('generes', ['comics']) ]); $this->assertEquals(2, count($documents)); @@ -971,7 +986,7 @@ public function testFind(Document $document) * Array contains OR condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_CONTAINS, 'generes', ['comics', 'kids']), + Query::contains('generes', ['comics', 'kids']), ]); $this->assertEquals(4, count($documents)); @@ -984,7 +999,7 @@ public function testFind(Document $document) $this->assertEquals(true, $success); $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_SEARCH, 'name', ['captain']), + Query::search('name', 'captain'), ]); $this->assertEquals(2, count($documents)); @@ -995,7 +1010,7 @@ public function testFind(Document $document) // TODO: Looks like the MongoDB implementation is a bit more complex, skipping that for now. if (in_array(static::getAdapterName(), ['mysql', 'mariadb'])) { $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_SEARCH, 'name', ['cap']), + Query::search('name', 'cap'), ]); $this->assertEquals(2, count($documents)); @@ -1005,8 +1020,8 @@ public function testFind(Document $document) * Multiple conditions */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, 'director', ['TBD']), - new Query(Query::TYPE_EQUAL, 'year', [2026]), + Query::equal('director', ['TBD']), + Query::equal('year', [2026]), ]); $this->assertEquals(1, count($documents)); @@ -1015,7 +1030,7 @@ public function testFind(Document $document) * Multiple conditions and OR values */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, 'name', ['Frozen II', 'Captain Marvel']), + Query::equal('name', ['Frozen II', 'Captain Marvel']), ]); $this->assertEquals(2, count($documents)); @@ -1026,7 +1041,7 @@ public function testFind(Document $document) * $id condition */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, '$id', ['frozen']), + Query::equal('$id', ['frozen']), ]); $this->assertEquals(1, count($documents)); @@ -1035,7 +1050,12 @@ public function testFind(Document $document) /** * ORDER BY */ - $documents = static::getDatabase()->find('movies', [], 25, 0, ['price', 'name'], [Database::ORDER_DESC]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('name') + ]); $this->assertEquals(6, count($documents)); $this->assertEquals('Frozen', $documents[0]['name']); @@ -1048,8 +1068,15 @@ public function testFind(Document $document) /** * ORDER BY natural */ - $base = array_reverse(static::getDatabase()->find('movies', [], 25, 0)); - $documents = static::getDatabase()->find('movies', [], 25, 0, [], [Database::ORDER_DESC]); + $base = array_reverse(static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + ])); + $documents = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc(''), + ]); $this->assertEquals(6, count($documents)); $this->assertEquals($base[0]['name'], $documents[0]['name']); @@ -1062,7 +1089,12 @@ public function testFind(Document $document) /** * ORDER BY - Multiple attributes */ - $documents = static::getDatabase()->find('movies', [], 25, 0, ['price', 'name'], [Database::ORDER_DESC, Database::ORDER_DESC]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc('price'), + Query::orderDesc('name') + ]); $this->assertEquals(6, count($documents)); $this->assertEquals('Frozen II', $documents[0]['name']); @@ -1075,237 +1107,488 @@ public function testFind(Document $document) /** * ORDER BY - After */ - $movies = static::getDatabase()->find('movies', [], 25, 0, [], []); + $movies = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + ]); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[1]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorAfter($movies[1]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[3]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorAfter($movies[3]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[4]['name'], $documents[0]['name']); $this->assertEquals($movies[5]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[4]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorAfter($movies[4]) + ]); $this->assertEquals(1, count($documents)); $this->assertEquals($movies[5]['name'], $documents[0]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[5]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorAfter($movies[5]) + ]); $this->assertEmpty(count($documents)); /** * ORDER BY - Before */ - $movies = static::getDatabase()->find('movies', [], 25, 0, [], []); + $movies = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + ]); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[5], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorBefore($movies[5]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[3]['name'], $documents[0]['name']); $this->assertEquals($movies[4]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[3], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorBefore($movies[3]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[1]['name'], $documents[0]['name']); $this->assertEquals($movies[2]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[2], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorBefore($movies[2]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); $this->assertEquals($movies[1]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[1], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorBefore($movies[1]) + ]); $this->assertEquals(1, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [], $movies[0], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorBefore($movies[0]) + ]); $this->assertEmpty(count($documents)); /** * ORDER BY - After by natural order */ - $movies = array_reverse(static::getDatabase()->find('movies', [], 25, 0, [], [])); + $movies = array_reverse(static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + ])); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[1]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorAfter($movies[1]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[3]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorAfter($movies[3]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[4]['name'], $documents[0]['name']); $this->assertEquals($movies[5]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[4]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorAfter($movies[4]) + ]); $this->assertEquals(1, count($documents)); $this->assertEquals($movies[5]['name'], $documents[0]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[5]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorAfter($movies[5]) + ]); $this->assertEmpty(count($documents)); /** * ORDER BY - Before by natural order */ - $movies = static::getDatabase()->find('movies', [], 25, 0, [], [Database::ORDER_DESC]); + $movies = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc(''), + ]); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[5], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorBefore($movies[5]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[3]['name'], $documents[0]['name']); $this->assertEquals($movies[4]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[3], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorBefore($movies[3]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[1]['name'], $documents[0]['name']); $this->assertEquals($movies[2]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[2], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorBefore($movies[2]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); $this->assertEquals($movies[1]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[1], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorBefore($movies[1]) + ]); $this->assertEquals(1, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, [], [Database::ORDER_DESC], $movies[0], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc(''), + Query::cursorBefore($movies[0]) + ]); $this->assertEmpty(count($documents)); /** * ORDER BY - Single Attribute After */ - $movies = static::getDatabase()->find('movies', [], 25, 0, ['year'], [Database::ORDER_DESC]); + $movies = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc('year') + ]); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[1]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorAfter($movies[1]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[3]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorAfter($movies[3]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[4]['name'], $documents[0]['name']); $this->assertEquals($movies[5]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[4]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorAfter($movies[4]) + ]); $this->assertEquals(1, count($documents)); $this->assertEquals($movies[5]['name'], $documents[0]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[5]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorAfter($movies[5]) + ]); $this->assertEmpty(count($documents)); /** * ORDER BY - Single Attribute Before */ - $movies = static::getDatabase()->find('movies', [], 25, 0, ['year'], [Database::ORDER_DESC]); + $movies = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc('year') + ]); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[5], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorBefore($movies[5]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[3]['name'], $documents[0]['name']); $this->assertEquals($movies[4]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[3], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorBefore($movies[3]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[1]['name'], $documents[0]['name']); $this->assertEquals($movies[2]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[2], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorBefore($movies[2]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); $this->assertEquals($movies[1]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[1], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorBefore($movies[1]) + ]); $this->assertEquals(1, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['year'], [Database::ORDER_DESC], $movies[0], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('year'), + Query::cursorBefore($movies[0]) + ]); $this->assertEmpty(count($documents)); /** * ORDER BY - Multiple Attribute After */ - $movies = static::getDatabase()->find('movies', [], 25, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC]); + $movies = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year') + ]); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[1]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorAfter($movies[1]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[3]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorAfter($movies[3]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[4]['name'], $documents[0]['name']); $this->assertEquals($movies[5]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[4]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorAfter($movies[4]) + ]); $this->assertEquals(1, count($documents)); $this->assertEquals($movies[5]['name'], $documents[0]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[5]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorAfter($movies[5]) + ]); $this->assertEmpty(count($documents)); /** * ORDER BY - Multiple Attribute Before */ - $movies = static::getDatabase()->find('movies', [], 25, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC]); + $movies = static::getDatabase()->find('movies', [ + Query::limit(25), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year') + ]); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[5], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorBefore($movies[5]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[3]['name'], $documents[0]['name']); $this->assertEquals($movies[4]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[4], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorBefore($movies[4]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[2]['name'], $documents[0]['name']); $this->assertEquals($movies[3]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[2], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorBefore($movies[2]) + ]); $this->assertEquals(2, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); $this->assertEquals($movies[1]['name'], $documents[1]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[1], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorBefore($movies[1]) + ]); $this->assertEquals(1, count($documents)); $this->assertEquals($movies[0]['name'], $documents[0]['name']); - $documents = static::getDatabase()->find('movies', [], 2, 0, ['price', 'year'], [Database::ORDER_DESC, Database::ORDER_ASC], $movies[0], Database::CURSOR_BEFORE); + $documents = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + Query::orderAsc('year'), + Query::cursorBefore($movies[0]) + ]); $this->assertEmpty(count($documents)); /** * ORDER BY + CURSOR */ - $documentsTest = static::getDatabase()->find('movies', [], 2, 0, ['price'], [Database::ORDER_DESC]); - $documents = static::getDatabase()->find('movies', [], 1, 0, ['price'], [Database::ORDER_DESC], $documentsTest[0], Database::CURSOR_AFTER); + $documentsTest = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('price'), + ]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(1), + Query::offset(0), + Query::orderDesc('price'), + Query::cursorAfter($documentsTest[0]) + ]); $this->assertEquals($documentsTest[1]['$id'], $documents[0]['$id']); /** * ORDER BY ID + CURSOR */ - $documentsTest = static::getDatabase()->find('movies', [], 2, 0, ['$id'], [Database::ORDER_DESC]); - $documents = static::getDatabase()->find('movies', [], 1, 0, ['$id'], [Database::ORDER_DESC], $documentsTest[0], Database::CURSOR_AFTER); + $documentsTest = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('$id'), + ]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(1), + Query::offset(0), + Query::orderDesc('$id'), + Query::cursorAfter($documentsTest[0]) + ]); $this->assertEquals($documentsTest[1]['$id'], $documents[0]['$id']); /** * ORDER BY CREATE DATE + CURSOR */ - $documentsTest = static::getDatabase()->find('movies', [], 2, 0, ['$createdAt'], [Database::ORDER_DESC]); - $documents = static::getDatabase()->find('movies', [], 1, 0, ['$createdAt'], [Database::ORDER_DESC], $documentsTest[0], Database::CURSOR_AFTER); + $documentsTest = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('$createdAt'), + ]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(1), + Query::offset(0), + Query::orderDesc('$createdAt'), + Query::cursorAfter($documentsTest[0]) + ]); $this->assertEquals($documentsTest[1]['$id'], $documents[0]['$id']); /** * ORDER BY UPDATE DATE + CURSOR */ - $documentsTest = static::getDatabase()->find('movies', [], 2, 0, ['$updatedAt'], [Database::ORDER_DESC]); - $documents = static::getDatabase()->find('movies', [], 1, 0, ['$updatedAt'], [Database::ORDER_DESC], $documentsTest[0], Database::CURSOR_AFTER); + $documentsTest = static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::orderDesc('$updatedAt'), + ]); + $documents = static::getDatabase()->find('movies', [ + Query::limit(1), + Query::offset(0), + Query::orderDesc('$updatedAt'), + Query::cursorAfter($documentsTest[0]) + ]); $this->assertEquals($documentsTest[1]['$id'], $documents[0]['$id']); /** * Limit */ - $documents = static::getDatabase()->find('movies', [], 4, 0, ['name']); + $documents = static::getDatabase()->find('movies', [ + Query::limit(4), + Query::offset(0), + Query::orderAsc('name') + ]); $this->assertEquals(4, count($documents)); $this->assertEquals('Captain America: The First Avenger', $documents[0]['name']); @@ -1316,7 +1599,11 @@ public function testFind(Document $document) /** * Limit + Offset */ - $documents = static::getDatabase()->find('movies', [], 4, 2, ['name']); + $documents = static::getDatabase()->find('movies', [ + Query::limit(4), + Query::offset(2), + Query::orderAsc('name') + ]); $this->assertEquals(4, count($documents)); $this->assertEquals('Frozen', $documents[0]['name']); @@ -1328,8 +1615,8 @@ public function testFind(Document $document) * Test that OR queries are handled correctly */ $documents = static::getDatabase()->find('movies', [ - new Query(Query::TYPE_EQUAL, 'director', ['TBD', 'Joe Johnston']), - new Query(Query::TYPE_EQUAL, 'year', [2025]), + Query::equal('director', ['TBD', 'Joe Johnston']), + Query::equal('year', [2025]), ]); $this->assertEquals(1, count($documents)); @@ -1342,7 +1629,11 @@ public function testFind(Document $document) ]); $this->expectException(Exception::class); - static::getDatabase()->find('movies', [], 2, 0, [], [], $document); + static::getDatabase()->find('movies', [ + Query::limit(2), + Query::offset(0), + Query::cursorAfter($document) + ]); } /** @@ -1350,10 +1641,15 @@ public function testFind(Document $document) */ public function testFindOne() { - $document = static::getDatabase()->findOne('movies', [], 2, ['name']); + $document = static::getDatabase()->findOne('movies', [ + Query::offset(2), + Query::orderAsc('name') + ], 2, ['name']); $this->assertEquals('Frozen', $document['name']); - $document = static::getDatabase()->findOne('movies', [], 10); + $document = static::getDatabase()->findOne('movies', [ + Query::offset(10) + ]); $this->assertEquals(false, $document); } @@ -1364,7 +1660,7 @@ public function testCount() { $count = static::getDatabase()->count('movies'); $this->assertEquals(6, $count); - $count = static::getDatabase()->count('movies', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $count = static::getDatabase()->count('movies', [Query::equal('year', [2019]),]); $this->assertEquals(2, $count); Authorization::disable(); @@ -1382,8 +1678,8 @@ public function testCount() */ Authorization::disable(); $count = static::getDatabase()->count('movies', [ - new Query(Query::TYPE_EQUAL, 'director', ['TBD', 'Joe Johnston']), - new Query(Query::TYPE_EQUAL, 'year', [2025]), + Query::equal('director', ['TBD', 'Joe Johnston']), + Query::equal('year', [2025]), ]); $this->assertEquals(1, $count); Authorization::reset(); @@ -1395,26 +1691,26 @@ public function testCount() public function testSum() { Authorization::setRole('userx'); - $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'year', [Query::equal('year', [2019]),]); $this->assertEquals(2019 + 2019, $sum); $sum = static::getDatabase()->sum('movies', 'year'); $this->assertEquals(2013 + 2019 + 2011 + 2019 + 2025 + 2026, $sum); - $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); - $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); - $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, 'year', [2019])], 1); + $sum = static::getDatabase()->sum('movies', 'year', [Query::equal('year', [2019])], 1); $this->assertEquals(2019, $sum); Authorization::unsetRole('userx'); - $sum = static::getDatabase()->sum('movies', 'year', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'year', [Query::equal('year', [2019]),]); $this->assertEquals(2019 + 2019, $sum); $sum = static::getDatabase()->sum('movies', 'year'); $this->assertEquals(2013 + 2019 + 2011 + 2019 + 2025, $sum); - $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); - $sum = static::getDatabase()->sum('movies', 'price', [new Query(Query::TYPE_EQUAL, 'year', [2019]),]); + $sum = static::getDatabase()->sum('movies', 'price', [Query::equal('year', [2019]),]); $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2)); } @@ -2370,8 +2666,8 @@ public function testCreateDatetime() $this->assertEquals(false, DateTime::isValid($document->getAttribute('date2'))); $documents = static::getDatabase()->find('datetime', [ - new Query(Query::TYPE_GREATER, 'date', ['1975-12-06 10:00:00+01:00']), - new Query(Query::TYPE_LESSER, 'date', ['2030-12-06 10:00:00-01:00']), + Query::greaterThan('date', '1975-12-06 10:00:00+01:00'), + Query::lessThan('date', '2030-12-06 10:00:00-01:00'), ]); if (in_array(static::getAdapterName(), ['mysql', 'mariadb'])) { // todo: fix in mongo @@ -2468,11 +2764,13 @@ public function testReservedKeywords() $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); $this->assertEquals('Reserved:' . $keyword, $documents[0]->getAttribute($keyword)); - $documents = $database->find($collectionName, [new Query(Query::TYPE_EQUAL, $keyword, ["Reserved:${keyword}"])]); + $documents = $database->find($collectionName, [Query::equal($keyword, ["Reserved:${keyword}"])]); $this->assertCount(1, $documents); $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); - $documents = $database->find($collectionName, orderAttributes: [$keyword], orderTypes: ['DESC']); + $documents = $database->find($collectionName, [ + Query::orderDesc($keyword) + ]); $this->assertCount(1, $documents); $this->assertEquals('reservedKeyDocument', $documents[0]->getId()); diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index 6167b4b14..b8b1c540b 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -2,6 +2,7 @@ namespace Utopia\Tests; +use Utopia\Database\Document; use Utopia\Database\Query; use PHPUnit\Framework\TestCase; @@ -66,11 +67,12 @@ public function testCreate(): void $this->assertEquals('', $query->getAttribute()); $this->assertEquals([10], $query->getValues()); - $query = Query::cursorAfter('cursor'); + $cursor = new Document(); + $query = Query::cursorAfter($cursor); $this->assertEquals(Query::TYPE_CURSORAFTER, $query->getMethod()); $this->assertEquals('', $query->getAttribute()); - $this->assertEquals(['cursor'], $query->getValues()); + $this->assertEquals([$cursor], $query->getValues()); } public function testParse() From 3f7af1688b1a97b3d7aea45e7ee41d286cc476a0 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 11 Aug 2022 01:03:16 +0000 Subject: [PATCH 26/27] Update Queries Validator to validate all queries --- src/Database/Database.php | 2 +- src/Database/Query.php | 42 ++++- src/Database/Validator/Queries.php | 85 ++++++---- src/Database/Validator/QueryValidator.php | 91 ++++++++-- tests/Database/QueryTest.php | 25 +++ tests/Database/Validator/QueriesTest.php | 156 +++++++++--------- .../Database/Validator/QueryValidatorTest.php | 68 ++++++++ 7 files changed, 333 insertions(+), 136 deletions(-) diff --git a/src/Database/Database.php b/src/Database/Database.php index 65c45114a..1ad8e8b7c 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1276,7 +1276,7 @@ public function find(string $collection, array $queries = []): array { $collection = $this->getCollection($collection); - $queriesByMethod = Queries::byMethod($queries); + $queriesByMethod = Queries::groupByType($queries); /** @var Query[] */ $filters = $queriesByMethod['filters']; /** @var int */ $limit = $queriesByMethod['limit']; /** @var int */ $offset = $queriesByMethod['offset']; diff --git a/src/Database/Query.php b/src/Database/Query.php index b805f9b56..40028fda4 100644 --- a/src/Database/Query.php +++ b/src/Database/Query.php @@ -54,14 +54,19 @@ public function getMethod(): string return $this->method; } + public function getAttribute(): string + { + return $this->attribute; + } + public function getValues(): array { return $this->values; } - public function getAttribute(): string + public function getValue($default = null) { - return $this->attribute; + return $this->values[0] ?? $default; } /** @@ -76,6 +81,18 @@ public function setMethod(string $method): self return $this; } + /** + * Sets Attribute. + * @param string $attribute + * @return self + */ + public function setAttribute(string $attribute): self + { + $this->attribute = $attribute; + + return $this; + } + /** * Sets Values. * @param array $values @@ -89,13 +106,13 @@ public function setValues(array $values): self } /** - * Sets Attribute. - * @param string $attribute + * Sets Value. + * @param $value * @return self */ - public function setAttribute(string $attribute): self + public function setValue($value): self { - $this->attribute = $attribute; + $this->values = [$value]; return $this; } @@ -278,17 +295,24 @@ public static function parse(string $filter): self case self::TYPE_GREATEREQUAL: case self::TYPE_CONTAINS: case self::TYPE_SEARCH: - return new self($method, $parsedParams[0], \is_array($parsedParams[1]) ? $parsedParams[1] : [$parsedParams[1]]); + $attribute = $parsedParams[0] ?? ''; + if (count($parsedParams) < 2) { + return new self($method, $attribute); + } + return new self($method, $attribute, \is_array($parsedParams[1]) ? $parsedParams[1] : [$parsedParams[1]]); case self::TYPE_ORDERASC: case self::TYPE_ORDERDESC: - return new self($method, $parsedParams[0]); + return new self($method, $parsedParams[0] ?? ''); case self::TYPE_LIMIT: case self::TYPE_OFFSET: case self::TYPE_CURSORAFTER: case self::TYPE_CURSORBEFORE: - return new self($method, values: $parsedParams[0]); + if (count($parsedParams) > 0) { + return new self($method, values: [$parsedParams[0]]); + } + return new self($method); default: return new self($method); diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 5a205da99..55bccf7e6 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -21,7 +21,12 @@ class Queries extends Validator protected $validator; /** - * @var array + * @var Document[] + */ + protected $attributes = []; + + /** + * @var Document[] */ protected $indexes = []; @@ -34,30 +39,31 @@ class Queries extends Validator * Queries constructor * * @param QueryValidator $validator - * @param Document[] $indexes + * @param Document $collection * @param bool $strict */ - public function __construct($validator, $indexes, $strict = true) + public function __construct($validator, $collection, $strict = true) { $this->validator = $validator; + $this->attributes = $collection->getAttribute('attributes', []); - $this->indexes[] = [ + $this->indexes[] = new Document([ 'type' => Database::INDEX_UNIQUE, 'attributes' => ['$id'] - ]; + ]); - $this->indexes[] = [ + $this->indexes[] = new Document([ 'type' => Database::INDEX_KEY, 'attributes' => ['$createdAt'] - ]; + ]); - $this->indexes[] = [ + $this->indexes[] = new Document([ 'type' => Database::INDEX_KEY, 'attributes' => ['$updatedAt'] - ]; + ]); - foreach ($indexes as $index) { - $this->indexes[] = $index->getArrayCopy(['attributes', 'type']); + foreach ($collection->getAttribute('indexes', []) as $index) { + $this->indexes[] = $index; } $this->strict = $strict; @@ -78,52 +84,69 @@ public function getDescription(): string /** * Is valid. * - * Returns true if all $queries are valid as a set. + * Returns false if: + * 1. any query in $value is invalid based on $validator + * + * In addition, if $strict is true, this returns false if: + * 1. there is no index with an exact match of the filters + * 2. there is no index with an exact mathc of the order attributes + * + * Otherwise, returns true. + * * @param mixed $value as array of Query objects * @return bool */ public function isValid($value): bool { - /** - * Array of attributes from query - * - * @var string[] - */ - $queries = []; - foreach ($value as $query) { - // [attribute => method] - $queries[$query->getAttribute()] = $query->getMethod(); - if (!$this->validator->isValid($query)) { $this->message = 'Query not valid: ' . $this->validator->getDescription(); return false; } } - $found = null; + if (!$this->strict) { + return true; + } + + $queriesByMethod = self::groupByType($value); + /** @var Query[] */ $filters = $queriesByMethod['filters']; + /** @var string[] */ $orderAttributes = $queriesByMethod['orderAttributes']; + + // Check filter queries for exact index match + if (count($filters) > 0) { + $filtersByAttribute = []; + foreach ($filters as $filter) { + $filtersByAttribute[$filter->getAttribute()] = $filter->getMethod(); + } + + $found = null; - // Return false if attributes do not exactly match an index - if ($this->strict) { - // look for strict match among indexes foreach ($this->indexes as $index) { - if ($this->arrayMatch($index['attributes'], array_keys($queries))) { + if ($this->arrayMatch($index->getAttribute('attributes'), array_keys($filtersByAttribute))) { $found = $index; } } if (!$found) { - $this->message = 'Index not found: ' . implode(",", array_keys($queries)); + $this->message = 'Index not found: ' . implode(",", array_keys($filtersByAttribute)); return false; } // search method requires fulltext index - if (in_array(Query::TYPE_SEARCH, array_values($queries)) && $found['type'] !== Database::INDEX_FULLTEXT) { - $this->message = 'Search method requires fulltext index: ' . implode(",", array_keys($queries)); + if (in_array(Query::TYPE_SEARCH, array_values($filtersByAttribute)) && $found['type'] !== Database::INDEX_FULLTEXT) { + $this->message = 'Search method requires fulltext index: ' . implode(",", array_keys($filtersByAttribute)); return false; } } + // Check order attributes for exact index match + $validator = new OrderAttributes($this->attributes, $this->indexes, true); + if (count($orderAttributes) > 0 && !$validator->isValid($orderAttributes)) { + $this->message = $validator->getDescription(); + return false; + } + return true; } /** @@ -202,7 +225,7 @@ protected function arrayMatch($indexes, $queries): bool * * @return array */ - public static function byMethod(array $queries, int $defaultLimit = 25, int $defaultOffset = 0, string $defaultCursorDirection = Database::CURSOR_AFTER): array + public static function groupByType(array $queries, int $defaultLimit = 25, int $defaultOffset = 0, string $defaultCursorDirection = Database::CURSOR_AFTER): array { $filters = []; $limit = null; diff --git a/src/Database/Validator/QueryValidator.php b/src/Database/Validator/QueryValidator.php index 7957a4744..94d37a133 100644 --- a/src/Database/Validator/QueryValidator.php +++ b/src/Database/Validator/QueryValidator.php @@ -19,28 +19,32 @@ class QueryValidator extends Validator */ protected $schema = []; + protected int $maxLimit; + protected int $maxOffset; + protected int $maxValuesCount; + /** * Expression constructor * * @param Document[] $attributes */ - public function __construct(array $attributes) + public function __construct(array $attributes, int $maxLimit = 100, int $maxOffset = 5000, int $maxValuesCount = 100) { - $this->schema[] = [ + $this->schema['$id'] = [ 'key' => '$id', 'array' => false, 'type' => Database::VAR_STRING, 'size' => 512 ]; - $this->schema[] = [ + $this->schema['$createdAt'] = [ 'key' => '$createdAt', 'array' => false, 'type' => Database::VAR_DATETIME, 'size' => 0 ]; - $this->schema[] = [ + $this->schema['$updatedAt'] = [ 'key' => '$updatedAt', 'array' => false, 'type' => Database::VAR_DATETIME, @@ -48,8 +52,12 @@ public function __construct(array $attributes) ]; foreach ($attributes as $attribute) { - $this->schema[] = $attribute->getArrayCopy(); + $this->schema[$attribute->getAttribute('key')] = $attribute->getArrayCopy(); } + + $this->maxLimit = $maxLimit; + $this->maxOffset = $maxOffset; + $this->maxValuesCount = $maxValuesCount; } /** @@ -67,32 +75,85 @@ public function getDescription(): string /** * Is valid. * - * Returns true if query typed according to schema. + * Returns false if: + * 1. $query has an invalid method + * 2. limit value is not a number, less than 0, or greater than $maxLimit + * 3. offset value is not a number, less than 0, or greater than $maxOffset + * 4. attribute does not exist + * 5. count of values is greater than $maxValuesCount + * 6. value type does not match attribute type + * 6. contains method is used on non-array attribute + * + * Otherwise, returns true. * - * @param $query + * @param Query $query * * @return bool */ public function isValid($query): bool { // Validate method - if (!Query::isMethod($query->getMethod())) { - $this->message = 'Query method invalid: ' . $query->getMethod(); + $method = $query->getMethod(); + if (!Query::isMethod($method)) { + $this->message = 'Query method invalid: ' . $method; return false; } + if ($method === Query::TYPE_LIMIT) { + $limit = $query->getValue(); + if ($limit === null || $limit < 0 || $limit > $this->maxLimit) { + $this->message = 'Limit must be between 0 and ' . $this->maxLimit . '(inclusive)'; + return false; + } + return true; + } + + if ($method === Query::TYPE_OFFSET) { + $offset = $query->getValue(); + if ($offset === null || $offset < 0 || $offset > $this->maxOffset) { + $this->message = 'Offset must be between 0 and ' . $this->maxOffset . '(inclusive)'; + return false; + } + return true; + } + + if ($method === Query::TYPE_CURSORAFTER || $method === Query::TYPE_CURSORBEFORE) { + $value = $query->getValue(); + if ($value === null) { + $this->message = 'Cursor must not be null'; + return false; + } + return true; + } + + // Allow empty string for order attribute so we can order by natural order + $attribute = $query->getAttribute(); + if ($attribute === '' && ($method === DatabaseQuery::TYPE_ORDERASC || $method === DatabaseQuery::TYPE_ORDERDESC)) { + return true; + } + // Search for attribute in schema - $attributeIndex = array_search($query->getAttribute(), array_column($this->schema, 'key')); + if (!isset($this->schema[$attribute])) { + $this->message = 'Attribute not found in schema: ' . $attribute; + return false; + } + + if ($method === Query::TYPE_ORDERASC || $method === Query::TYPE_ORDERDESC) { + return true; + } + + $attributeSchema = $this->schema[$attribute]; - if ($attributeIndex === false) { - $this->message = 'Attribute not found in schema: ' . $query->getAttribute(); + $values = $query->getValues(); + 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 = $this->schema[$attributeIndex]['type']; + $attributeType = $attributeSchema['type']; - foreach ($query->getValues() as $value) { + foreach ($values as $value) { $condition = match ($attributeType) { Database::VAR_DATETIME => gettype($value) === Database::VAR_STRING, default => gettype($value) === $attributeType @@ -105,7 +166,7 @@ public function isValid($query): bool } // Contains method only supports array attributes - if (!$this->schema[$attributeIndex]['array'] && $query->getMethod() === Query::TYPE_CONTAINS) { + if (!$attributeSchema['array'] && $query->getMethod() === Query::TYPE_CONTAINS) { $this->message = 'Query method only supported on array attributes: ' . $query->getMethod(); return false; } diff --git a/tests/Database/QueryTest.php b/tests/Database/QueryTest.php index b8b1c540b..6e6dc8203 100644 --- a/tests/Database/QueryTest.php +++ b/tests/Database/QueryTest.php @@ -207,6 +207,31 @@ public function testParseV2() $this->assertCount(1, $query->getValues()); $this->assertEquals(1, $query->getAttribute()); $this->assertEquals("Hello\\\\\", ", $query->getValues()[0]); + + $query = Query::parse('equal()'); + $this->assertCount(0, $query->getValues()); + $this->assertEquals('', $query->getAttribute()); + $this->assertEquals(null, $query->getValue()); + + $query = Query::parse('limit()'); + $this->assertCount(0, $query->getValues()); + $this->assertEquals('', $query->getAttribute()); + $this->assertEquals(null, $query->getValue()); + + $query = Query::parse('offset()'); + $this->assertCount(0, $query->getValues()); + $this->assertEquals('', $query->getAttribute()); + $this->assertEquals(null, $query->getValue()); + + $query = Query::parse('cursorAfter()'); + $this->assertCount(0, $query->getValues()); + $this->assertEquals('', $query->getAttribute()); + $this->assertEquals(null, $query->getValue()); + + $query = Query::parse('orderDesc()'); + $this->assertCount(0, $query->getValues()); + $this->assertEquals('', $query->getAttribute()); + $this->assertEquals(null, $query->getValue()); } /* diff --git a/tests/Database/Validator/QueriesTest.php b/tests/Database/Validator/QueriesTest.php index bccd5ab08..03d53d338 100644 --- a/tests/Database/Validator/QueriesTest.php +++ b/tests/Database/Validator/QueriesTest.php @@ -14,75 +14,7 @@ class QueriesTest extends TestCase /** * @var array */ - protected $collection = [ - '$id' => Database::METADATA, - '$collection' => Database::METADATA, - 'name' => 'movies', - 'attributes' => [ - [ - '$id' => 'title', - 'key' => 'title', - 'type' => Database::VAR_STRING, - 'size' => 256, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'description', - 'key' => 'description', - 'type' => Database::VAR_STRING, - 'size' => 1000000, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'rating', - 'key' => 'rating', - 'type' => Database::VAR_INTEGER, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'price', - 'key' => 'price', - 'type' => Database::VAR_FLOAT, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'published', - 'key' => 'published', - 'type' => Database::VAR_BOOLEAN, - 'size' => 5, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'tags', - 'key' => 'tags', - 'type' => Database::VAR_STRING, - 'size' => 55, - 'required' => true, - 'signed' => true, - 'array' => true, - 'filters' => [], - ], - ], - 'indexes' => [], - ]; - + protected $collection = []; /** * @var Query[] $queries @@ -96,14 +28,76 @@ class QueriesTest extends TestCase public function setUp(): void { - // Query validator expects Document[] - $attributes = []; - /** @var Document[] $attributes */ - foreach ($this->collection['attributes'] as $attribute) { - $attributes[] = new Document($attribute); - } + $this->collection = [ + '$id' => Database::METADATA, + '$collection' => Database::METADATA, + 'name' => 'movies', + 'attributes' => [ + new Document([ + '$id' => 'title', + 'key' => 'title', + 'type' => Database::VAR_STRING, + 'size' => 256, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'description', + 'key' => 'description', + 'type' => Database::VAR_STRING, + 'size' => 1000000, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'rating', + 'key' => 'rating', + 'type' => Database::VAR_INTEGER, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'price', + 'key' => 'price', + 'type' => Database::VAR_FLOAT, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'published', + 'key' => 'published', + 'type' => Database::VAR_BOOLEAN, + 'size' => 5, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ]), + new Document([ + '$id' => 'tags', + 'key' => 'tags', + 'type' => Database::VAR_STRING, + 'size' => 55, + 'required' => true, + 'signed' => true, + 'array' => true, + 'filters' => [], + ]), + ], + 'indexes' => [], + ]; - $this->queryValidator = new QueryValidator($attributes); + $this->queryValidator = new Query($this->collection['attributes']); $query1 = Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'); $query2 = Query::parse('equal("description", "Best movie ever")'); @@ -166,13 +160,15 @@ public function tearDown(): void public function testQueries() { // test for SUCCESS - $validator = new Queries($this->queryValidator, $this->collection['indexes']); + $validator = new Queries($this->queryValidator, new Document($this->collection)); - $this->assertEquals(true, $validator->isValid($this->queries)); + $this->assertEquals(true, $validator->isValid($this->queries), $validator->getDescription()); $this->queries[] = Query::parse('lessThan("price", 6.50)'); $this->assertEquals(true, $validator->isValid($this->queries)); + $queries = [DatabaseQuery::orderDesc('')]; + $this->assertEquals(true, $validator->isValid($queries), $validator->getDescription()); // test for FAILURE @@ -199,11 +195,11 @@ public function testQueries() public function testIsStrict() { - $validator = new Queries($this->queryValidator, $this->collection['indexes']); + $validator = new Queries($this->queryValidator, new Document($this->collection)); $this->assertEquals(true, $validator->isStrict()); - $validator = new Queries($this->queryValidator, $this->collection['indexes'], false); + $validator = new Queries($this->queryValidator, new Document($this->collection), false); $this->assertEquals(false, $validator->isStrict()); } diff --git a/tests/Database/Validator/QueryValidatorTest.php b/tests/Database/Validator/QueryValidatorTest.php index 7db85e53e..3ff9dbc86 100644 --- a/tests/Database/Validator/QueryValidatorTest.php +++ b/tests/Database/Validator/QueryValidatorTest.php @@ -113,6 +113,10 @@ public function testQuery() $this->assertEquals(true, $validator->isValid(Query::parse('greaterThan("rating", 4)')), $validator->getDescription()); $this->assertEquals(true, $validator->isValid(Query::parse('lessThan("price", 6.50)'))); $this->assertEquals(true, $validator->isValid(Query::parse('contains("tags", "action")'))); + $this->assertEquals(true, $validator->isValid(Query::parse('cursorAfter("docId")'))); + $this->assertEquals(true, $validator->isValid(Query::parse('cursorBefore("docId")'))); + $this->assertEquals(true, $validator->isValid(Query::parse('orderAsc("title")'))); + $this->assertEquals(true, $validator->isValid(Query::parse('orderDesc("title")'))); } public function testInvalidMethod() @@ -133,6 +137,11 @@ public function testAttributeNotFound() $this->assertEquals(false, $response); $this->assertEquals('Attribute not found in schema: name', $validator->getDescription()); + + $response = $validator->isValid(Query::parse('orderAsc("name")')); + + $this->assertEquals(false, $response); + $this->assertEquals('Attribute not found in schema: name', $validator->getDescription()); } public function testAttributeWrongType() @@ -161,4 +170,63 @@ public function testQueryDate() $response = $validator->isValid(Query::parse('greaterThan("birthDay", "1960-01-01 10:10:10")')); $this->assertEquals(true, $response); } + + public function testQueryLimit() + { + $validator = new QueryValidator($this->schema); + + $response = $validator->isValid(Query::parse('limit(25)')); + $this->assertEquals(true, $response); + + $response = $validator->isValid(Query::parse('limit()')); + $this->assertEquals(false, $response); + + $response = $validator->isValid(Query::parse('limit(-1)')); + $this->assertEquals(false, $response); + + $response = $validator->isValid(Query::parse('limit(10000)')); + $this->assertEquals(false, $response); + } + + public function testQueryOffset() + { + $validator = new QueryValidator($this->schema); + + $response = $validator->isValid(Query::parse('offset(25)')); + $this->assertEquals(true, $response); + + $response = $validator->isValid(Query::parse('offset()')); + $this->assertEquals(false, $response); + + $response = $validator->isValid(Query::parse('offset(-1)')); + $this->assertEquals(false, $response); + + $response = $validator->isValid(Query::parse('offset(10000)')); + $this->assertEquals(false, $response); + } + + public function testQueryOrder() + { + $validator = new QueryValidator($this->schema); + + $response = $validator->isValid(Query::parse('orderAsc("title")')); + $this->assertEquals(true, $response); + + $response = $validator->isValid(Query::parse('orderAsc("")')); + $this->assertEquals(true, $response); + + $response = $validator->isValid(Query::parse('orderAsc("doesNotExist")')); + $this->assertEquals(false, $response); + } + + public function testQueryCursor() + { + $validator = new QueryValidator($this->schema); + + $response = $validator->isValid(Query::parse('cursorAfter("asdf")')); + $this->assertEquals(true, $response); + + $response = $validator->isValid(Query::parse('cursorAfter()')); + $this->assertEquals(false, $response); + } } From edd8404317cb292c26e9427a0d7a8e247e1b09e3 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 11 Aug 2022 19:26:25 +0000 Subject: [PATCH 27/27] Rename QueryValidator to Query Query matches the naming convention of the other validators. We can use aliasing whenever any conflict arises with Utopia\Database\Query. --- src/Database/Database.php | 8 +- src/Database/Validator/Queries.php | 23 +-- .../{QueryValidator.php => Query.php} | 167 +++++++++++------- tests/Database/Validator/QueriesTest.php | 26 +-- .../{QueryValidatorTest.php => QueryTest.php} | 89 +++++----- 5 files changed, 178 insertions(+), 135 deletions(-) rename src/Database/Validator/{QueryValidator.php => Query.php} (58%) rename tests/Database/Validator/{QueryValidatorTest.php => QueryTest.php} (56%) diff --git a/src/Database/Database.php b/src/Database/Database.php index 1ad8e8b7c..53ed8fa34 100644 --- a/src/Database/Database.php +++ b/src/Database/Database.php @@ -1296,12 +1296,12 @@ public function find(string $collection, array $queries = []): array $results = $this->adapter->find( $collection->getId(), $queries, - $limit, - $offset, + $limit ?? 25, + $offset ?? 0, $orderAttributes, $orderTypes, - $cursor, - $cursorDirection, + $cursor ?? [], + $cursorDirection ?? Database::CURSOR_AFTER, ); foreach ($results as &$node) { diff --git a/src/Database/Validator/Queries.php b/src/Database/Validator/Queries.php index 55bccf7e6..7b77f44c4 100644 --- a/src/Database/Validator/Queries.php +++ b/src/Database/Validator/Queries.php @@ -5,7 +5,7 @@ use Utopia\Validator; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Validator\QueryValidator; +use Utopia\Database\Validator\Query as QueryValidator; use Utopia\Database\Query; class Queries extends Validator @@ -38,14 +38,15 @@ class Queries extends Validator /** * Queries constructor * - * @param QueryValidator $validator - * @param Document $collection + * @param QueryValidator $validator used to validate each query + * @param Document[] $attributes allowed attributes to be queried + * @param Document[] $indexes available for strict query matching * @param bool $strict */ - public function __construct($validator, $collection, $strict = true) + public function __construct($validator, $attributes = [], $indexes = [], $strict = true) { $this->validator = $validator; - $this->attributes = $collection->getAttribute('attributes', []); + $this->attributes = $attributes; $this->indexes[] = new Document([ 'type' => Database::INDEX_UNIQUE, @@ -62,7 +63,7 @@ public function __construct($validator, $collection, $strict = true) 'attributes' => ['$updatedAt'] ]); - foreach ($collection->getAttribute('indexes', []) as $index) { + foreach ($indexes ?? [] as $index) { $this->indexes[] = $index; } @@ -89,7 +90,7 @@ public function getDescription(): string * * In addition, if $strict is true, this returns false if: * 1. there is no index with an exact match of the filters - * 2. there is no index with an exact mathc of the order attributes + * 2. there is no index with an exact match of the order attributes * * Otherwise, returns true. * @@ -225,7 +226,7 @@ protected function arrayMatch($indexes, $queries): bool * * @return array */ - public static function groupByType(array $queries, int $defaultLimit = 25, int $defaultOffset = 0, string $defaultCursorDirection = Database::CURSOR_AFTER): array + public static function groupByType(array $queries): array { $filters = []; $limit = null; @@ -281,12 +282,12 @@ public static function groupByType(array $queries, int $defaultLimit = 25, int $ return [ 'filters' => $filters, - 'limit' => $limit ?? $defaultLimit, - 'offset' => $offset ?? $defaultOffset, + 'limit' => $limit, + 'offset' => $offset, 'orderAttributes' => $orderAttributes, 'orderTypes' => $orderTypes, 'cursor' => $cursor, - 'cursorDirection' => $cursorDirection ?? $defaultCursorDirection, + 'cursorDirection' => $cursorDirection, ]; } } diff --git a/src/Database/Validator/QueryValidator.php b/src/Database/Validator/Query.php similarity index 58% rename from src/Database/Validator/QueryValidator.php rename to src/Database/Validator/Query.php index 94d37a133..c158740cc 100644 --- a/src/Database/Validator/QueryValidator.php +++ b/src/Database/Validator/Query.php @@ -4,10 +4,11 @@ use Utopia\Database\Database; use Utopia\Validator; +use Utopia\Validator\Range; use Utopia\Database\Document; -use Utopia\Database\Query; +use Utopia\Database\Query as DatabaseQuery; -class QueryValidator extends Validator +class Query extends Validator { /** * @var string @@ -24,9 +25,12 @@ class QueryValidator extends Validator protected int $maxValuesCount; /** - * Expression constructor + * Query constructor * * @param Document[] $attributes + * @param int $maxLimit + * @param int $maxOffset + * @param int $maxValuesCount */ public function __construct(array $attributes, int $maxLimit = 100, int $maxOffset = 5000, int $maxValuesCount = 100) { @@ -72,79 +76,45 @@ public function getDescription(): string return $this->message; } - /** - * Is valid. - * - * Returns false if: - * 1. $query has an invalid method - * 2. limit value is not a number, less than 0, or greater than $maxLimit - * 3. offset value is not a number, less than 0, or greater than $maxOffset - * 4. attribute does not exist - * 5. count of values is greater than $maxValuesCount - * 6. value type does not match attribute type - * 6. contains method is used on non-array attribute - * - * Otherwise, returns true. - * - * @param Query $query - * - * @return bool - */ - public function isValid($query): bool - { - // Validate method - $method = $query->getMethod(); - if (!Query::isMethod($method)) { - $this->message = 'Query method invalid: ' . $method; - return false; - } + protected function isValidLimit($limit): bool { + $validator = new Range(0, $this->maxLimit); + if ($validator->isValid($limit)) return true; - if ($method === Query::TYPE_LIMIT) { - $limit = $query->getValue(); - if ($limit === null || $limit < 0 || $limit > $this->maxLimit) { - $this->message = 'Limit must be between 0 and ' . $this->maxLimit . '(inclusive)'; - return false; - } - return true; - } + $this->message = 'Invalid limit: ' . $validator->getDescription(); + return false; + } - if ($method === Query::TYPE_OFFSET) { - $offset = $query->getValue(); - if ($offset === null || $offset < 0 || $offset > $this->maxOffset) { - $this->message = 'Offset must be between 0 and ' . $this->maxOffset . '(inclusive)'; - return false; - } - return true; - } + protected function isValidOffset($offset): bool { + $validator = new Range(0, $this->maxOffset); + if ($validator->isValid($offset)) return true; - if ($method === Query::TYPE_CURSORAFTER || $method === Query::TYPE_CURSORBEFORE) { - $value = $query->getValue(); - if ($value === null) { - $this->message = 'Cursor must not be null'; - return false; - } - return true; - } + $this->message = 'Invalid offset: ' . $validator->getDescription(); + return false; + } - // Allow empty string for order attribute so we can order by natural order - $attribute = $query->getAttribute(); - if ($attribute === '' && ($method === DatabaseQuery::TYPE_ORDERASC || $method === DatabaseQuery::TYPE_ORDERDESC)) { - return true; + protected function isValidCursor($cursor): bool { + if ($cursor === null) { + $this->message = 'Cursor must not be null'; + return false; } + return true; + } + protected function isValidAttribute($attribute): bool { // Search for attribute in schema if (!isset($this->schema[$attribute])) { $this->message = 'Attribute not found in schema: ' . $attribute; return false; } - if ($method === Query::TYPE_ORDERASC || $method === Query::TYPE_ORDERDESC) { - return true; - } + return true; + } - $attributeSchema = $this->schema[$attribute]; + protected function isValidAttributeAndValues(string $attribute, array $values): bool { + if (!$this->isValidAttribute($attribute)) return false; - $values = $query->getValues(); + $attributeSchema = $this->schema[$attribute]; + if (count($values) > $this->maxValuesCount) { $this->message = 'Query on attribute has greater than ' . $this->maxValuesCount . ' values: ' . $attribute; return false; @@ -164,15 +134,84 @@ public function isValid($query): bool return false; } } + + return true; + } + + protected function isValidContains(string $attribute, array $values): bool { + if (!$this->isValidAttributeAndValues($attribute, $values)) return false; + + $attributeSchema = $this->schema[$attribute]; // Contains method only supports array attributes - if (!$attributeSchema['array'] && $query->getMethod() === Query::TYPE_CONTAINS) { - $this->message = 'Query method only supported on array attributes: ' . $query->getMethod(); + if (!$attributeSchema['array']) { + $this->message = 'Query method only supported on array attributes: ' . DatabaseQuery::TYPE_CONTAINS; return false; } return true; } + + /** + * Is valid. + * + * Returns false if: + * 1. $query has an invalid method + * 2. limit value is not a number, less than 0, or greater than $maxLimit + * 3. offset value is not a number, less than 0, or greater than $maxOffset + * 4. attribute does not exist + * 5. count of values is greater than $maxValuesCount + * 6. value type does not match attribute type + * 6. contains method is used on non-array attribute + * + * Otherwise, returns true. + * + * @param DatabaseQuery $query + * + * @return bool + */ + public function isValid($query): bool + { + // Validate method + $method = $query->getMethod(); + if (!DatabaseQuery::isMethod($method)) { + $this->message = 'Query method invalid: ' . $method; + return false; + } + + $attribute = $query->getAttribute(); + + switch ($method) { + case DatabaseQuery::TYPE_LIMIT: + $limit = $query->getValue(); + return $this->isValidLimit($limit); + + case DatabaseQuery::TYPE_OFFSET: + $offset = $query->getValue(); + return $this->isValidOffset($offset); + + case DatabaseQuery::TYPE_CURSORAFTER: + case DatabaseQuery::TYPE_CURSORBEFORE: + $cursor = $query->getValue(); + return $this->isValidCursor($cursor); + + case DatabaseQuery::TYPE_ORDERASC: + case DatabaseQuery::TYPE_ORDERDESC: + // Allow empty string for order attribute so we can order by natural order + if ($attribute === '') return true; + return $this->isValidAttribute($attribute); + + case DatabaseQuery::TYPE_CONTAINS: + $values = $query->getValues(); + return $this->isValidContains($attribute, $values); + + default: + // other filter queries + $values = $query->getValues(); + return $this->isValidAttributeAndValues($attribute, $values); + } + + } /** * Is array * diff --git a/tests/Database/Validator/QueriesTest.php b/tests/Database/Validator/QueriesTest.php index 03d53d338..8df010cde 100644 --- a/tests/Database/Validator/QueriesTest.php +++ b/tests/Database/Validator/QueriesTest.php @@ -2,11 +2,11 @@ namespace Utopia\Tests\Validator; -use Utopia\Database\Validator\QueryValidator; +use Utopia\Database\Validator\Query; use PHPUnit\Framework\TestCase; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Query; +use Utopia\Database\Query as DatabaseQuery; use Utopia\Database\Validator\Queries; class QueriesTest extends TestCase @@ -17,7 +17,7 @@ class QueriesTest extends TestCase protected $collection = []; /** - * @var Query[] $queries + * @var DatabaseQuery[] $queries */ protected $queries = []; @@ -99,8 +99,8 @@ public function setUp(): void $this->queryValidator = new Query($this->collection['attributes']); - $query1 = Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'); - $query2 = Query::parse('equal("description", "Best movie ever")'); + $query1 = DatabaseQuery::parse('notEqual("title", ["Iron Man", "Ant Man"])'); + $query2 = DatabaseQuery::parse('equal("description", "Best movie ever")'); array_push($this->queries, $query1, $query2); @@ -160,11 +160,11 @@ public function tearDown(): void public function testQueries() { // test for SUCCESS - $validator = new Queries($this->queryValidator, new Document($this->collection)); + $validator = new Queries($this->queryValidator, $this->collection['attributes'], $this->collection['indexes']); $this->assertEquals(true, $validator->isValid($this->queries), $validator->getDescription()); - $this->queries[] = Query::parse('lessThan("price", 6.50)'); + $this->queries[] = DatabaseQuery::parse('lessThan("price", 6.50)'); $this->assertEquals(true, $validator->isValid($this->queries)); $queries = [DatabaseQuery::orderDesc('')]; @@ -172,14 +172,14 @@ public function testQueries() // test for FAILURE - $this->queries[] = Query::parse('greaterThan("rating", 4)'); + $this->queries[] = DatabaseQuery::parse('greaterThan("rating", 4)'); $this->assertEquals(false, $validator->isValid($this->queries)); $this->assertEquals("Index not found: title,description,price,rating", $validator->getDescription()); // test for queued index - $query1 = Query::parse('lessThan("price", 6.50)'); - $query2 = Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'); + $query1 = DatabaseQuery::parse('lessThan("price", 6.50)'); + $query2 = DatabaseQuery::parse('notEqual("title", ["Iron Man", "Ant Man"])'); $this->queries = [$query1, $query2]; $this->assertEquals(false, $validator->isValid($this->queries)); @@ -187,7 +187,7 @@ public function testQueries() // test fulltext - $query3 = Query::parse('search("description", "iron")'); + $query3 = DatabaseQuery::parse('search("description", "iron")'); $this->queries = [$query3]; $this->assertEquals(false, $validator->isValid($this->queries)); $this->assertEquals("Search method requires fulltext index: description", $validator->getDescription()); @@ -195,11 +195,11 @@ public function testQueries() public function testIsStrict() { - $validator = new Queries($this->queryValidator, new Document($this->collection)); + $validator = new Queries($this->queryValidator, $this->collection['attributes'], $this->collection['indexes']); $this->assertEquals(true, $validator->isStrict()); - $validator = new Queries($this->queryValidator, new Document($this->collection), false); + $validator = new Queries($this->queryValidator, $this->collection['attributes'], $this->collection['indexes'], false); $this->assertEquals(false, $validator->isStrict()); } diff --git a/tests/Database/Validator/QueryValidatorTest.php b/tests/Database/Validator/QueryTest.php similarity index 56% rename from tests/Database/Validator/QueryValidatorTest.php rename to tests/Database/Validator/QueryTest.php index 3ff9dbc86..4876d6674 100644 --- a/tests/Database/Validator/QueryValidatorTest.php +++ b/tests/Database/Validator/QueryTest.php @@ -2,13 +2,13 @@ namespace Utopia\Tests\Validator; -use Utopia\Database\Validator\QueryValidator; +use Utopia\Database\Validator\Query; use PHPUnit\Framework\TestCase; use Utopia\Database\Database; use Utopia\Database\Document; -use Utopia\Database\Query; +use Utopia\Database\Query as DatabaseQuery; -class QueryValidatorTest extends TestCase +class QueryTest extends TestCase { /** * @var Document[] @@ -105,25 +105,25 @@ public function tearDown(): void public function testQuery() { - $validator = new QueryValidator($this->schema); - - $this->assertEquals(true, $validator->isValid(Query::parse('equal("$id", ["Iron Man", "Ant Man"])'))); - $this->assertEquals(true, $validator->isValid(Query::parse('notEqual("title", ["Iron Man", "Ant Man"])'))); - $this->assertEquals(true, $validator->isValid(Query::parse('equal("description", "Best movie ever")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('greaterThan("rating", 4)')), $validator->getDescription()); - $this->assertEquals(true, $validator->isValid(Query::parse('lessThan("price", 6.50)'))); - $this->assertEquals(true, $validator->isValid(Query::parse('contains("tags", "action")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('cursorAfter("docId")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('cursorBefore("docId")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('orderAsc("title")'))); - $this->assertEquals(true, $validator->isValid(Query::parse('orderDesc("title")'))); + $validator = new Query($this->schema); + + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('equal("$id", ["Iron Man", "Ant Man"])'))); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('notEqual("title", ["Iron Man", "Ant Man"])'))); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('equal("description", "Best movie ever")'))); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('greaterThan("rating", 4)')), $validator->getDescription()); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('lessThan("price", 6.50)'))); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('contains("tags", "action")'))); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('cursorAfter("docId")'))); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('cursorBefore("docId")'))); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('orderAsc("title")'))); + $this->assertEquals(true, $validator->isValid(DatabaseQuery::parse('orderDesc("title")'))); } public function testInvalidMethod() { - $validator = new QueryValidator($this->schema); + $validator = new Query($this->schema); - $response = $validator->isValid(Query::parse('eqqual("title", "Iron Man")')); + $response = $validator->isValid(DatabaseQuery::parse('eqqual("title", "Iron Man")')); $this->assertEquals(false, $response); $this->assertEquals('Query method invalid: eqqual', $validator->getDescription()); @@ -131,14 +131,14 @@ public function testInvalidMethod() public function testAttributeNotFound() { - $validator = new QueryValidator($this->schema); + $validator = new Query($this->schema); - $response = $validator->isValid(Query::parse('equal("name", "Iron Man")')); + $response = $validator->isValid(DatabaseQuery::parse('equal("name", "Iron Man")')); $this->assertEquals(false, $response); $this->assertEquals('Attribute not found in schema: name', $validator->getDescription()); - $response = $validator->isValid(Query::parse('orderAsc("name")')); + $response = $validator->isValid(DatabaseQuery::parse('orderAsc("name")')); $this->assertEquals(false, $response); $this->assertEquals('Attribute not found in schema: name', $validator->getDescription()); @@ -146,9 +146,9 @@ public function testAttributeNotFound() public function testAttributeWrongType() { - $validator = new QueryValidator($this->schema); + $validator = new Query($this->schema); - $response = $validator->isValid(Query::parse('equal("title", 1776)')); + $response = $validator->isValid(DatabaseQuery::parse('equal("title", 1776)')); $this->assertEquals(false, $response); $this->assertEquals('Query type does not match expected: string', $validator->getDescription()); @@ -156,9 +156,9 @@ public function testAttributeWrongType() public function testMethodWrongType() { - $validator = new QueryValidator($this->schema); + $validator = new Query($this->schema); - $response = $validator->isValid(Query::parse('contains("title", "Iron")')); + $response = $validator->isValid(DatabaseQuery::parse('contains("title", "Iron")')); $this->assertEquals(false, $response); $this->assertEquals('Query method only supported on array attributes: contains', $validator->getDescription()); @@ -166,67 +166,70 @@ public function testMethodWrongType() public function testQueryDate() { - $validator = new QueryValidator($this->schema); - $response = $validator->isValid(Query::parse('greaterThan("birthDay", "1960-01-01 10:10:10")')); + $validator = new Query($this->schema); + $response = $validator->isValid(DatabaseQuery::parse('greaterThan("birthDay", "1960-01-01 10:10:10")')); $this->assertEquals(true, $response); } public function testQueryLimit() { - $validator = new QueryValidator($this->schema); + $validator = new Query($this->schema); - $response = $validator->isValid(Query::parse('limit(25)')); + $response = $validator->isValid(DatabaseQuery::parse('limit(25)')); $this->assertEquals(true, $response); - $response = $validator->isValid(Query::parse('limit()')); + $response = $validator->isValid(DatabaseQuery::parse('limit()')); $this->assertEquals(false, $response); - $response = $validator->isValid(Query::parse('limit(-1)')); + $response = $validator->isValid(DatabaseQuery::parse('limit(-1)')); $this->assertEquals(false, $response); - $response = $validator->isValid(Query::parse('limit(10000)')); + $response = $validator->isValid(DatabaseQuery::parse('limit(10000)')); $this->assertEquals(false, $response); } public function testQueryOffset() { - $validator = new QueryValidator($this->schema); + $validator = new Query($this->schema); - $response = $validator->isValid(Query::parse('offset(25)')); + $response = $validator->isValid(DatabaseQuery::parse('offset(25)')); $this->assertEquals(true, $response); - $response = $validator->isValid(Query::parse('offset()')); + $response = $validator->isValid(DatabaseQuery::parse('offset()')); $this->assertEquals(false, $response); - $response = $validator->isValid(Query::parse('offset(-1)')); + $response = $validator->isValid(DatabaseQuery::parse('offset(-1)')); $this->assertEquals(false, $response); - $response = $validator->isValid(Query::parse('offset(10000)')); + $response = $validator->isValid(DatabaseQuery::parse('offset(10000)')); $this->assertEquals(false, $response); } public function testQueryOrder() { - $validator = new QueryValidator($this->schema); + $validator = new Query($this->schema); - $response = $validator->isValid(Query::parse('orderAsc("title")')); + $response = $validator->isValid(DatabaseQuery::parse('orderAsc("title")')); $this->assertEquals(true, $response); - $response = $validator->isValid(Query::parse('orderAsc("")')); + $response = $validator->isValid(DatabaseQuery::parse('orderAsc("")')); $this->assertEquals(true, $response); - $response = $validator->isValid(Query::parse('orderAsc("doesNotExist")')); + $response = $validator->isValid(DatabaseQuery::parse('orderAsc()')); + $this->assertEquals(true, $response); + + $response = $validator->isValid(DatabaseQuery::parse('orderAsc("doesNotExist")')); $this->assertEquals(false, $response); } public function testQueryCursor() { - $validator = new QueryValidator($this->schema); + $validator = new Query($this->schema); - $response = $validator->isValid(Query::parse('cursorAfter("asdf")')); + $response = $validator->isValid(DatabaseQuery::parse('cursorAfter("asdf")')); $this->assertEquals(true, $response); - $response = $validator->isValid(Query::parse('cursorAfter()')); + $response = $validator->isValid(DatabaseQuery::parse('cursorAfter()')); $this->assertEquals(false, $response); } }