Skip to content

Commit

Permalink
feat(Datastore): Query OR (#6010)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yash30201 committed Apr 19, 2023
1 parent 37d75e5 commit 25254f4
Show file tree
Hide file tree
Showing 9 changed files with 680 additions and 152 deletions.
9 changes: 7 additions & 2 deletions Datastore/src/Connection/Grpc.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
127 changes: 127 additions & 0 deletions Datastore/src/Query/Filter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php
/**
* Copyright 2023 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Cloud\Datastore\Query;

use Google\Cloud\Datastore\Query\Query;

/**
* Represents an interface to create composite and property filters for
* Google\Cloud\Datastore\Query\Query via static methods.
*
* Each method returns an array representation of respective filter which is
* consumed by other filters or Query object.
*
* Example:
* ```
* $filter = Filter::where('CompanyName', '=', 'Google');
* $query = $datastore->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;
}
}
84 changes: 70 additions & 14 deletions Datastore/src/Query/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
];

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
114 changes: 114 additions & 0 deletions Datastore/tests/Snippet/FilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

namespace Google\Cloud\Datastore\Tests\Snippet;

use Google\Cloud\Core\Testing\DatastoreOperationRefreshTrait;
use Google\Cloud\Core\Testing\Snippet\SnippetTestCase;
use Google\Cloud\Core\Testing\TestHelpers;
use Google\Cloud\Datastore\Connection\ConnectionInterface;
use Google\Cloud\Datastore\DatastoreClient;
use Google\Cloud\Datastore\EntityMapper;
use Google\Cloud\Datastore\Operation;
use Google\Cloud\Datastore\Query\Filter;
use Google\Cloud\Datastore\Query\Query;
use Google\Cloud\Datastore\Query\QueryInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;

class FilterTest extends SnippetTestCase
{
use DatastoreOperationRefreshTrait;
use ProphecyTrait;

private const PROJECT = 'alpha-project';
private $connection;
private $datastore;
private $operation;
private $query;
private $filter;

public function setUp(): void
{
$entityMapper = new EntityMapper(self::PROJECT, false, false);

$this->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
]);
}
}
Loading

0 comments on commit 25254f4

Please sign in to comment.