From 25254f41ae44769b270269d76a0b58d82cc5dcbb Mon Sep 17 00:00:00 2001 From: Yash Sahu <54198301+yash30201@users.noreply.github.com> Date: Wed, 19 Apr 2023 16:11:44 +0530 Subject: [PATCH] feat(Datastore): Query OR (#6010) Notable points: * Added support for `OR` queries. * Added support for nested `AND` and `OR` queries. * New Filter class under `Datastore/Query` namespace. * `Query::filter()` method now allows two different types of invocations. --- Datastore/src/Connection/Grpc.php | 9 +- Datastore/src/Query/Filter.php | 127 ++++++++++ Datastore/src/Query/Query.php | 84 +++++-- Datastore/tests/Snippet/FilterTest.php | 114 +++++++++ Datastore/tests/Snippet/Query/QueryTest.php | 16 +- Datastore/tests/System/FilterTest.php | 220 ++++++++++++++++++ Datastore/tests/Unit/Query/FilterTest.php | 79 +++++++ Datastore/tests/Unit/Query/QueryTest.php | 182 ++++----------- .../tests/Unit/data/QueryApiArrayFormat.json | 1 + 9 files changed, 680 insertions(+), 152 deletions(-) create mode 100644 Datastore/src/Query/Filter.php create mode 100644 Datastore/tests/Snippet/FilterTest.php create mode 100644 Datastore/tests/System/FilterTest.php create mode 100644 Datastore/tests/Unit/Query/FilterTest.php create mode 100644 Datastore/tests/Unit/data/QueryApiArrayFormat.json diff --git a/Datastore/src/Connection/Grpc.php b/Datastore/src/Connection/Grpc.php index 937b2f44942..62ff0923a06 100644 --- a/Datastore/src/Connection/Grpc.php +++ b/Datastore/src/Connection/Grpc.php @@ -467,8 +467,13 @@ private function convertFilterProps(array $filter) } if (isset($filter['compositeFilter'])) { - $filter['compositeFilter']['op'] = CompositeFilterOperator::PBAND; - + if ($filter['compositeFilter']['op'] == 'AND') { + $filter['compositeFilter']['op'] = CompositeFilterOperator::PBAND; + } elseif ($filter['compositeFilter']['op'] == 'OR') { + $filter['compositeFilter']['op'] = CompositeFilterOperator::PBOR; + } else { + $filter['compositeFilter']['op'] = CompositeFilterOperator::OPERATOR_UNSPECIFIED; + } foreach ($filter['compositeFilter']['filters'] as &$nested) { $nested = $this->convertFilterProps($nested); } diff --git a/Datastore/src/Query/Filter.php b/Datastore/src/Query/Filter.php new file mode 100644 index 00000000000..a435b07b2a8 --- /dev/null +++ b/Datastore/src/Query/Filter.php @@ -0,0 +1,127 @@ +query(); + * $query->kind('Companies'); + * $query->filter($filter); + * $results = $datastore->runQuery($query); + * $finalResult = []; + * foreach ($results as $result) { + * $finalResult[] = $result['companyName']; + * } + * ``` + * + * Composite filters can be created by using other composite/property + * filters. + * ``` + * // Or filter + * $filterType = 'or'; + * $filterOr = Filter::or([$filter, ...$filters]); + * $query = $datastore->query(); + * $query->kind('Companies'); + * $query->filter($filter); + * $results = $datastore->runQuery($query); + * $finalResult = []; + * foreach ($results as $result) { + * $finalResult[] = $result['companyName']; + * } + * ``` + * + * Similaryly, `AND` filter can be created using `Filter::and` method. + */ +class Filter +{ + /** + * Creates a property filter in array format. + * + * @param string $property Property name + * @param string $operator Operator, one of ('=', '<', '<=', '>', '>=', + * '!=', 'IN', 'NOT IN') + * @param mixed $value Value for operation on property + * @return array Returns array representation of a property filter. + */ + public static function where($property, $operator, $value) + { + return self::propertyFilter($property, $operator, $value); + } + + /** + * Creates an AND composite filter in array format. + * + * @param array $filters An array of filters(array representations) to AND + * upon. + * @return array Returns array representation of AND composite filter. + */ + public static function and(array $filters) + { + return self::compositeFilter('AND', $filters); + } + + /** + * Creates a OR composite filter in array format. + * + * @param array $filters An array of filters(array representations) to OR + * upon. + * @return array Returns array representation of OR composite filter. + */ + public static function or(array $filters) + { + return self::compositeFilter('OR', $filters); + } + + private static function propertyFilter($property, $operator, $value) + { + $filter = [ + 'propertyFilter' => [ + 'property' => $property, + 'value' => $value, + 'op' => $operator + ] + ]; + return $filter; + } + + /** + * @param string $type Type of Composite Filter, i.e. `AND` / `OR`. + * There values are checked in `Query::filter()` method. + * @param array $filters Filter array to operator on. + */ + private static function compositeFilter($type, $filters) + { + $filter = [ + 'compositeFilter' => [ + 'op' => $type, + 'filters' => $filters + ] + ]; + return $filter; + } +} diff --git a/Datastore/src/Query/Query.php b/Datastore/src/Query/Query.php index e08eb37e28f..341f0f16005 100644 --- a/Datastore/src/Query/Query.php +++ b/Datastore/src/Query/Query.php @@ -117,7 +117,9 @@ class Query implements QueryInterface '>' => self::OP_GREATER_THAN, '>=' => self::OP_GREATER_THAN_OR_EQUAL, '=' => self::OP_EQUALS, - '!=' => self::OP_NOT_EQUALS + '!=' => self::OP_NOT_EQUALS, + 'IN' => self::OP_IN, + 'NOT IN' => self::OP_NOT_IN ]; /** @@ -237,37 +239,68 @@ public function kind($kinds) * If the top-level filter is specified as a propertyFilter, it will be replaced. * Any composite filters will be preserved and the new filter will be added. * + * Filters can be added either by supplying three arguments + * `(string $property, string $operator, mixed $value)` to add a property + * filter to the root `AND` filter or by using a single argument invocation + * `(array $filter)` to add an array representation of Composite / Property + * Filter to the root `AND` filter. They can also be mixed and used together. + * * Example: * ``` + * // Using (string $property, string $operator, mixed $value) invocation + * // to add property filter. * $query->filter('firstName', '=', 'Bob') * ->filter('lastName', '=', 'Testguy'); * ``` * + * Using (array $filter) invocation to add composite/property filter. + * ``` + * use Google\Cloud\Datastore\Query\Filter; + * $filterA = Filter::or([$testFilter, ...$testFilters]); // OR filter + * $filterB = Filter::and([$testFilter, ...$testFilters]); // AND filter + * $filterC = Filter::where('foo', 'NOT IN', ['bar']); // Property filter + * $query->filter($filterA) + * ->filter($filterB) + * ->filter($filterC) + * ->filter('foo', '<', 'bar'); + * ``` + * * @see https://cloud.google.com/datastore/reference/rest/v1/projects/runQuery#operator_1 Allowed Operators * - * @param string $property The property to filter. - * @param string $operator The operator to use in the filter. A list of + * @param string|array $filterOrProperty Either a string property name or + * an array representation of Property/Composite filter returned + * by Filter::and(), Filter::or() and Filter::where(). + * @param string|null $operator [optional] The operator to use in the filter + * if property name is used in the first argument. A list of * allowed operators may be found * [here](https://cloud.google.com/datastore/reference/rest/v1/projects/runQuery#operator_1). * Short comparison operators are provided for convenience and are * mapped to their datastore-compatible equivalents. Available short - * operators are `=`, `!=`, `<`, `<=`, `>`, and `>=`. - * @param mixed $value The value to check. + * operators are `=`, `!=`, `<`, `<=`, `>`, `>=`, `IN` and `NOT IN`. + * @param mixed $value [optional] The value to check if property name is + * used in the first argument. * @return Query */ - public function filter($property, $operator, $value) + public function filter($filterOrProperty, $operator = null, $value = null) { - if (!isset($this->query['filter']) || !isset($this->query['filter']['compositeFilter'])) { + if (!isset($this->query['filter']) || + !isset($this->query['filter']['compositeFilter']) + ) { $this->initializeFilter(); } - $this->query['filter']['compositeFilter']['filters'][] = [ - 'propertyFilter' => [ - 'property' => $this->propertyName($property), - 'value' => $this->entityMapper->valueObject($value), - 'op' => $this->mapOperator($operator) - ] - ]; + if (is_string($filterOrProperty)) { + $this->query['filter']['compositeFilter']['filters'][] = [ + 'propertyFilter' => [ + 'property' => $this->propertyName($filterOrProperty), + 'value' => $this->entityMapper->valueObject($value), + 'op' => $this->mapOperator($operator) + ] + ]; + } else { + $this->query['filter']['compositeFilter']['filters'][] = + $this->convertFilterToApiFormat($filterOrProperty); + } return $this; } @@ -545,4 +578,27 @@ private function mapOperator($operator) return $operator; } + + /** + * Converts the filter array data to proper API format recursively. + */ + private function convertFilterToApiFormat($filterArray) + { + if (array_key_exists('propertyFilter', $filterArray)) { + $propertyFilter = $filterArray['propertyFilter']; + $filterArray['propertyFilter'] = [ + 'property' => $this->propertyName($propertyFilter['property']), + 'value' => $this->entityMapper->valueObject($propertyFilter['value']), + 'op' => $this->mapOperator($propertyFilter['op']) + ]; + } else { + $filters = $filterArray['compositeFilter']['filters']; + foreach ($filters as &$filter) { + $filter = $this->convertFilterToApiFormat($filter); + } + $filterArray['compositeFilter']['filters'] = $filters; + } + + return $filterArray; + } } diff --git a/Datastore/tests/Snippet/FilterTest.php b/Datastore/tests/Snippet/FilterTest.php new file mode 100644 index 00000000000..a4156b002d3 --- /dev/null +++ b/Datastore/tests/Snippet/FilterTest.php @@ -0,0 +1,114 @@ +connection = $this->prophesize(ConnectionInterface::class); + + $this->datastore = TestHelpers::stub( + DatastoreClient::class, + [], + ['operation'] + ); + + $this->query = TestHelpers::stub(Query::class, [$entityMapper]); + + $this->filter = Filter::where('CompanyName', '=', 'Google'); + } + + public function testFilter() + { + $this->createConnectionProphecy(); + + $snippet = $this->snippetFromClass(Filter::class, 0); + $snippet->addLocal('datastore', $this->datastore); + $snippet->addLocal('query', $this->query); + $snippet->addLocal('filter', $this->filter); + $snippet->addUse(Filter::class); + + $res = $snippet->invoke('finalResult'); + $this->assertEquals(['Google'], $res->returnVal()); + } + + /** + * @dataProvider getCompositeFilterTypes + */ + public function testOrFilter($compositeFilterType) + { + $this->createConnectionProphecy(); + + $snippet = $this->snippetFromClass(Filter::class, 1); + $snippet->addLocal('filterType', $compositeFilterType); + $snippet->addLocal('datastore', $this->datastore); + $snippet->addLocal('query', $this->query); + $snippet->addLocal('filter', $this->filter); + $snippet->addLocal('filters', []); + $snippet->addUse(Filter::class); + + $res = $snippet->invoke('finalResult'); + $this->assertEquals(['Google'], $res->returnVal()); + } + + public function getCompositeFilterTypes() + { + return [ + ['or'], + ['and'] + ]; + } + + private function createConnectionProphecy() + { + $this->connection->runQuery(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'batch' => [ + 'entityResults' => [ + [ + 'entity' => [ + 'key' => ['path' => []], + 'properties' => [ + 'companyName' => [ + 'stringValue' => 'Google' + ] + ] + ] + ] + ], + 'moreResults' => 'no' + ] + ]); + + $this->refreshOperation($this->datastore, $this->connection->reveal(), [ + 'projectId' => self::PROJECT + ]); + } +} diff --git a/Datastore/tests/Snippet/Query/QueryTest.php b/Datastore/tests/Snippet/Query/QueryTest.php index 5a54e63d9d7..8c6c5bb7af6 100644 --- a/Datastore/tests/Snippet/Query/QueryTest.php +++ b/Datastore/tests/Snippet/Query/QueryTest.php @@ -25,6 +25,7 @@ use Google\Cloud\Datastore\EntityMapper; use Google\Cloud\Datastore\Key; use Google\Cloud\Datastore\Operation; +use Google\Cloud\Datastore\Query\Filter; use Google\Cloud\Datastore\Query\Query; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -136,7 +137,20 @@ public function testKind() $this->assertEquals($this->propertyName('Person'), $this->query->queryObject()['kind'][0]); } - public function testFilter() + public function testFilterAsFilterAdder() + { + $filter = Filter::where('foo', '=', 'bar'); + $snippet = $this->snippetFromMethod(Query::class, 'filter', 1); + $snippet->addLocal('query', $this->query); + $snippet->addLocal('testFilter', $filter); + $snippet->addLocal('testFilters', []); + + $snippet->invoke(); + + $this->assertCount(4, $this->query->queryObject()['filter']['compositeFilter']['filters']); + } + + public function testFilterAsPropertyFilterAdder() { $snippet = $this->snippetFromMethod(Query::class, 'filter'); $snippet->addLocal('query', $this->query); diff --git a/Datastore/tests/System/FilterTest.php b/Datastore/tests/System/FilterTest.php new file mode 100644 index 00000000000..dd971aab50e --- /dev/null +++ b/Datastore/tests/System/FilterTest.php @@ -0,0 +1,220 @@ +key(self::$kind, $element['Name']); + $entities[] = self::$restClient->entity($key, $element); + } + self::$restClient->insertBatch($entities); + + // on rare occasions the queries below are returning no results when + // triggered immediately after an insert operation. the sleep here + // is intended to help alleviate this issue. + sleep(1); + + foreach ($entities as $entity) { + self::$localDeletionQueue->add($entity->key()); + } + } + + public static function tearDownAfterClass(): void + { + self::tearDownFixtures(); + } + + /** + * @dataProvider defaultDbClientProvider + */ + public function testOrRunQuery(DatastoreClient $client) + { + $this->skipEmulatorTests(); + $filter = Filter::or([ + Filter::where('Name', '=', 'Hersch'), + Filter::where('Name', '=', 'Davy') + ]); + $query = $client->query() + ->kind(self::$kind) + ->filter($filter); + + $results = $this->runQueryAndSortResults($client, $query); + $this->assertEquals(count($results), 2); + $this->assertEquals($results[0]['Name'], 'Hersch'); + $this->assertEquals($results[1]['Name'], 'Davy'); + } + + /** + * @dataProvider defaultDbClientProvider + */ + public function testMixOfOrAndRunQuery(DatastoreClient $client) + { + $this->skipEmulatorTests(); + $filter = Filter::and([ + Filter::where('Age', '<', 26), + Filter::or([ + Filter::and([ + Filter::where('Age', '>', 23), + Filter::where('Age', '<', 30) + ]), + Filter::and([ + Filter::where('Age', '>', 25), + Filter::where('Age', '<', 31) + ]), + ]) + ]); + $query = $client->query() + ->kind(self::$kind) + ->filter($filter); + + $results = $this->runQueryAndSortResults($client, $query); + $this->assertEquals(count($results), 1); + $this->assertEquals($results[0]['Name'], 'Eldredge'); + } + + /** + * @dataProvider defaultDbClientProvider + */ + public function testOrQueryViaTransaction(DatastoreClient $client) + { + $this->skipEmulatorTests(); + $filter = Filter::or([ + Filter::where('Name', '=', 'Hersch'), + Filter::where('Name', '=', 'Davy') + ]); + $query = $client->query() + ->kind(self::$kind) + ->filter($filter); + + $transaction = $client->transaction(); + $results = $this->runQueryAndSortResults($transaction, $query); + $this->assertEquals(count($results), 2); + $this->assertEquals($results[0]['Name'], 'Hersch'); + $this->assertEquals($results[1]['Name'], 'Davy'); + $transaction->commit(); + } + + /** + * @dataProvider defaultDbClientProvider + */ + public function testMixOfOrAndViaTransaction(DatastoreClient $client) + { + $this->skipEmulatorTests(); + $filter = Filter::and([ + Filter::where('Age', '<', 26), + Filter::or([ + Filter::and([ + Filter::where('Age', '>', 23), + Filter::where('Age', '<', 30) + ]), + Filter::and([ + Filter::where('Age', '>', 25), + Filter::where('Age', '<', 31) + ]), + ]) + ]); + $query = $client->query() + ->kind(self::$kind) + ->filter($filter); + + $transaction = $client->transaction(); + $results = $this->runQueryAndSortResults($transaction, $query); + $this->assertEquals(count($results), 1); + $this->assertEquals($results[0]['Name'], 'Eldredge'); + $transaction->commit(); + } + + private static function getInitialData() + { + return [[ + "Name" => "Hersch", + "Age" => 21, + "FavouriteColor" => "Purple", + "Gender" => "Male" + ], [ + "Name" => "Davy", + "Age" => 23, + "FavouriteColor" => "Turquoise", + "Gender" => "Male" + ], [ + "Name" => "Eldredge", + "Age" => 25, + "FavouriteColor" => "Violet", + "Gender" => "Male" + ], [ + "Name" => "Rickey", + "Age" => 26, + "FavouriteColor" => "Crimson", + "Gender" => "Bigender" + ], [ + "Name" => "Arron", + "Age" => 30, + "FavouriteColor" => "Red", + "Gender" => "Male" + ], [ + "Name" => "Hi", + "Age" => 31, + "FavouriteColor" => "Teal", + "Gender" => "Male" + ], [ + "Name" => "Chandler", + "Age" => 33, + "FavouriteColor" => "Turquoise", + "Gender" => "Male" + ], [ + "Name" => "Juliette", + "Age" => 36, + "FavouriteColor" => "Purple", + "Gender" => "Female" + ], [ + "Name" => "Mufi", + "Age" => 36, + "FavouriteColor" => "Fuscia", + "Gender" => "Female" + ], [ + "Name" => "Karrah", + "Age" => 39, + "FavouriteColor" => "Violet", + "Gender" => "Female" + ]]; + } + + private function runQueryAndSortResults($client, $query) + { + $results = iterator_to_array($client->runQuery($query)); + usort($results, function ($a, $b) { + return $a['Name'] < $b['Name']; + }); + + return $results; + } +} diff --git a/Datastore/tests/Unit/Query/FilterTest.php b/Datastore/tests/Unit/Query/FilterTest.php new file mode 100644 index 00000000000..8015d6a1a5c --- /dev/null +++ b/Datastore/tests/Unit/Query/FilterTest.php @@ -0,0 +1,79 @@ +assertArrayHasKey('compositeFilter', $filter); + $compositeFilter = $filter['compositeFilter']; + + $this->assertEquals($compositeFilter['filters'], $filters); + + $this->assertEquals($compositeFilter['op'], strtoupper($methodName)); + } + + /** + * @dataProvider getWhereCases + */ + public function testWhere($value) + { + $filter = Filter::where('foo', 'test_op', $value); + $this->assertArrayHasKey('propertyFilter', $filter); + $propertyFilter = $filter['propertyFilter']; + + $this->assertEquals($propertyFilter['property'], 'foo'); + $this->assertEquals($propertyFilter['op'], 'test_op'); + $this->assertEquals($propertyFilter['value'], $value); + } + + public function getWhereCases() + { + $cases = [ + // Since $operator can be (=, <, >, <=, >=, !=), so string should be + // accepted. + ['value'], + // Also, $operator can be ('IN', 'NOT IN'), so array should also be + // allowed as value. + [['value']] + ]; + return $cases; + } + + public function getCompositeFilterCases() + { + $cases = [ + ['and', [['foo' => 'bar1'], ['foo' => 'bar2']]], + ['or', [['foo' => 'bar1'], ['foo' => 'bar2']]] + ]; + return $cases; + } +} diff --git a/Datastore/tests/Unit/Query/QueryTest.php b/Datastore/tests/Unit/Query/QueryTest.php index 2ea582a0709..d05241c9785 100644 --- a/Datastore/tests/Unit/Query/QueryTest.php +++ b/Datastore/tests/Unit/Query/QueryTest.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Datastore\Tests\Unit\Query; use Google\Cloud\Datastore\EntityMapper; +use Google\Cloud\Datastore\Query\Filter; use Google\Cloud\Datastore\Key; use Google\Cloud\Datastore\Query\Query; use InvalidArgumentException; @@ -127,7 +128,7 @@ public function testFilter() 'name' => 'propname' ], 'value' => [ - 'stringValue' =>'value' + 'stringValue' => 'value' ], 'op' => Query::OP_DEFAULT ]); @@ -160,148 +161,35 @@ public function testFilterInvalidOperator() $this->query->filter('propname', 'foo', 12); } - public function testOperatorConstantsDefault() + public function testFilterWithFilterArrayArgument() { - $this->query->filter('propName', Query::OP_DEFAULT, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('EQUAL', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsLessThan() - { - $this->query->filter('propName', Query::OP_LESS_THAN, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('LESS_THAN', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsLessThanOrEqual() - { - $this->query->filter('propName', Query::OP_LESS_THAN_OR_EQUAL, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('LESS_THAN_OR_EQUAL', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsGreaterThan() - { - $this->query->filter('propName', Query::OP_GREATER_THAN, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('GREATER_THAN', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsGreaterThanOrEqual() - { - $this->query->filter('propName', Query::OP_GREATER_THAN_OR_EQUAL, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('GREATER_THAN_OR_EQUAL', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsEquals() - { - $this->query->filter('propName', Query::OP_EQUALS, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('EQUAL', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsNotEquals() - { - $this->query->filter('propName', Query::OP_NOT_EQUALS, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('NOT_EQUAL', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsIn() - { - $this->query->filter('propName', Query::OP_IN, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('IN', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsNotIn() - { - $this->query->filter('propName', Query::OP_NOT_IN, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('NOT_IN', $filters[0]['propertyFilter']['op']); - } - - public function testOperatorConstantsHasAncestor() - { - $this->query->filter('propName', Query::OP_HAS_ANCESTOR, 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('HAS_ANCESTOR', $filters[0]['propertyFilter']['op']); - } - - public function testShortOperatorLessThan() - { - $this->query->filter('propName', '<', 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('LESS_THAN', $filters[0]['propertyFilter']['op']); - } - - public function testShortOperatorLessThanOrEqual() - { - $this->query->filter('propName', '<=', 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('LESS_THAN_OR_EQUAL', $filters[0]['propertyFilter']['op']); - } - - public function testShortOperatorGreaterThan() - { - $this->query->filter('propName', '>', 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('GREATER_THAN', $filters[0]['propertyFilter']['op']); - } - - public function testShortOperatorGreaterThanOrEqual() - { - $this->query->filter('propName', '>=', 'val'); - $res = $this->query->queryObject(); - - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('GREATER_THAN_OR_EQUAL', $filters[0]['propertyFilter']['op']); - } - - public function testShortOperatorEquals() - { - $this->query->filter('propName', '=', 'val'); - $res = $this->query->queryObject(); + $filter = Filter::or([ + Filter::and([ + Filter::where('bar', '>', 1), + Filter::where('bar', '<', 4), + ]), + Filter::where('bar', '>', 8) + ]); + $expected = json_decode(file_get_contents( + __DIR__ . '/../data/QueryApiArrayFormat.json' + ), true); - $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('EQUAL', $filters[0]['propertyFilter']['op']); + $query = new Query($this->mapper); + $query->kind('Foo')->filter($filter); + $queryObject = $query->queryObject(); + $this->assertEquals($expected, $queryObject); } - public function testShortOperatorNotEquals() + /** + * @dataProvider getOperatorCases + */ + public function testOperators($operator, $resultingOperator) { - $this->query->filter('propName', '!=', 'val'); + $this->query->filter('propName', $operator, 'val'); $res = $this->query->queryObject(); $filters = $res['filter']['compositeFilter']['filters']; - $this->assertEquals('NOT_EQUAL', $filters[0]['propertyFilter']['op']); + $this->assertEquals($resultingOperator, $filters[0]['propertyFilter']['op']); } public function testOrder() @@ -430,4 +318,28 @@ public function testLimit() $this->assertEquals($res['limit'], 2); } + + public function getOperatorCases() + { + return [ + ['=', 'EQUAL'], + [Query::OP_EQUALS, 'EQUAL'], + [Query::OP_DEFAULT, 'EQUAL'], + ['<', 'LESS_THAN'], + [Query::OP_LESS_THAN, 'LESS_THAN'], + ['<=', 'LESS_THAN_OR_EQUAL'], + [Query::OP_LESS_THAN_OR_EQUAL, 'LESS_THAN_OR_EQUAL'], + ['>', 'GREATER_THAN'], + [Query::OP_GREATER_THAN, 'GREATER_THAN'], + ['>=', 'GREATER_THAN_OR_EQUAL'], + [Query::OP_GREATER_THAN_OR_EQUAL, 'GREATER_THAN_OR_EQUAL'], + ['IN', 'IN'], + [Query::OP_IN, 'IN'], + ['NOT IN', 'NOT_IN'], + [Query::OP_NOT_IN, 'NOT_IN'], + ['!=', 'NOT_EQUAL'], + [Query::OP_NOT_EQUALS, 'NOT_EQUAL'], + [Query::OP_HAS_ANCESTOR, 'HAS_ANCESTOR'] + ]; + } } diff --git a/Datastore/tests/Unit/data/QueryApiArrayFormat.json b/Datastore/tests/Unit/data/QueryApiArrayFormat.json new file mode 100644 index 00000000000..4dc691d9f0b --- /dev/null +++ b/Datastore/tests/Unit/data/QueryApiArrayFormat.json @@ -0,0 +1 @@ +{"kind":[{"name":"Foo"}],"filter":{"compositeFilter":{"filters":[{"compositeFilter":{"op":"OR","filters":[{"compositeFilter":{"op":"AND","filters":[{"propertyFilter":{"property":{"name":"bar"},"value":{"integerValue":1},"op":"GREATER_THAN"}},{"propertyFilter":{"property":{"name":"bar"},"value":{"integerValue":4},"op":"LESS_THAN"}}]}},{"propertyFilter":{"property":{"name":"bar"},"value":{"integerValue":8},"op":"GREATER_THAN"}}]}}],"op":"AND"}}}