Skip to content

Commit

Permalink
feat(Datastore): Enable SUM / AVG Aggregations (#6817)
Browse files Browse the repository at this point in the history
  • Loading branch information
yash30201 authored Dec 1, 2023
1 parent c0243c9 commit b43313e
Show file tree
Hide file tree
Showing 14 changed files with 909 additions and 237 deletions.
8 changes: 7 additions & 1 deletion Datastore/src/DatastoreClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Google\Cloud\Datastore\Connection\Grpc;
use Google\Cloud\Datastore\Connection\Rest;
use Google\Cloud\Datastore\Query\AggregationQuery;
use Google\Cloud\Datastore\Query\AggregationQueryResult;
use Google\Cloud\Datastore\Query\GqlQuery;
use Google\Cloud\Datastore\Query\Query;
use Google\Cloud\Datastore\Query\QueryInterface;
Expand Down Expand Up @@ -175,7 +176,12 @@ public function __construct(array $config = [])

// The second parameter here should change to a variable
// when gRPC support is added for variable encoding.
$this->entityMapper = new EntityMapper($this->projectId, true, $config['returnInt64AsObject']);
$this->entityMapper = new EntityMapper(
$this->projectId,
true,
$config['returnInt64AsObject'],
$connectionType
);
$this->operation = new Operation(
$this->connection,
$this->projectId,
Expand Down
47 changes: 45 additions & 2 deletions Datastore/src/EntityMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ class EntityMapper
*/
private $returnInt64AsObject;

/**
* The connection type of the client. Required while mapping
* `INF`, `-INF` and `NAN` to datastore equivalent values.
*
* @var string
*/
private $connectionType;

/**
* Create an Entity Mapper
*
Expand All @@ -57,12 +65,19 @@ class EntityMapper
* @param bool $returnInt64AsObject If true, 64 bit integers will be
* returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility.
* @param string $connectionType [optional] The connection type of the client.
* Can be `rest` or `grpc`, defaults to `grpc`.
*/
public function __construct($projectId, $encode, $returnInt64AsObject)
{
public function __construct(
$projectId,
$encode,
$returnInt64AsObject,
$connectionType = 'grpc'
) {
$this->projectId = $projectId;
$this->encode = $encode;
$this->returnInt64AsObject = $returnInt64AsObject;
$this->connectionType = $connectionType;
}

/**
Expand Down Expand Up @@ -189,6 +204,22 @@ public function convertValue($type, $value, $className = Entity::class)
break;

case 'doubleValue':
// Flow will enter this logic only when REST transport is used
// because gRPC response values are always parsed correctly. Therefore
// the default $connectionType is set to 'grpc'
if (is_string($value)) {
switch ($value) {
case 'Infinity':
$value = INF;
break;
case '-Infinity':
$value = -INF;
break;
case 'NaN':
$value = NAN;
break;
}
}
$result = (float) $value;

break;
Expand Down Expand Up @@ -328,6 +359,18 @@ public function valueObject($value, $exclude = false, $meaning = null)
break;

case 'double':
// The mappings happen automatically for grpc hence
// this is required only incase of rest as grpc
// doesn't recognises 'Infinity', '-Infinity' and 'NaN'.
if ($this->connectionType == 'rest') {
if ($value == INF) {
$value = 'Infinity';
} elseif ($value == -INF) {
$value = '-Infinity';
} elseif (is_nan($value)) {
$value = 'NaN';
}
}
$propertyValue = [
'doubleValue' => $value
];
Expand Down
2 changes: 1 addition & 1 deletion Datastore/src/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ public function runAggregationQuery(AggregationQuery $runQueryObj, array $option
] + $requestQueryArr + $this->readOptions($options) + $options;

$res = $this->connection->runAggregationQuery($request);
return new AggregationQueryResult($res);
return new AggregationQueryResult($res, $this->entityMapper);
}

/**
Expand Down
74 changes: 70 additions & 4 deletions Datastore/src/Query/Aggregation.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
namespace Google\Cloud\Datastore\Query;

/**
* Represents Count Aggregation properties.
* Represents Aggregation properties.
*
* Example:
* ```
Expand All @@ -28,6 +28,8 @@
*
* echo json_encode($count->getProps());
* ```
*
* Aggregations considers non existing property name as an empty query set
*/
class Aggregation
{
Expand All @@ -36,6 +38,16 @@ class Aggregation
*/
private const TYPE_COUNT = 'count';

/**
* Default placeholder for all sum aggregation props.
*/
private const TYPE_SUM = 'sum';

/**
* Default placeholder for all average aggregation props.
*/
private const TYPE_AVG = 'avg';

/**
* @var array Properties for an aggregation query.
*/
Expand Down Expand Up @@ -67,9 +79,63 @@ private function __construct($aggregationType)
*/
public static function count()
{
$count = new Aggregation(self::TYPE_COUNT);
$count->props[$count->aggregationType] = [];
return $count;
return self::createAggregation(self::TYPE_COUNT);
}

/**
* Creates sum aggregation properties.
*
* Example:
* ```
* $sum = Aggregate::sum('property_to_aggregate_upon');
* ```
* Result of SUM aggregation can be a integer or a float.
* Sum of integers which exceed maxinum integer value returns a float.
* Sum of numbers exceeding max float value returns `INF`.
* Sum of data which contains `NaN` returns `NaN`.
* Non numeric values are ignored.
*
* @param string $property The relative path of the field to aggregate upon.
* @return Aggregation
*/
public static function sum($property)
{
return self::createAggregation(self::TYPE_SUM, $property);
}

/**
* Creates average aggregation properties.
*
* Example:
* ```
* $avg = Aggregate::avg('property_to_aggregate_upon');
* ```
* Result of AVG aggregation can be a float or a null.
* Average of empty valid data set return `null`.
* Average of numbers exceeding max float value returns `INF`.
* Average of data which contains `NaN` returns `NaN`.
* Non numeric values are ignored.
*
* @param string $property The relative path of the field to aggregate upon.
* @return Aggregation
*/
public static function avg($property)
{
return self::createAggregation(self::TYPE_AVG, $property);
}

private static function createAggregation(string $type, $property = null)
{
$aggregation = new Aggregation($type);
$aggregation->props[$aggregation->aggregationType] = [];
if (!is_null($property)) {
$aggregation->props[$aggregation->aggregationType] = [
'property' => [
'name' => $property
]
];
}
return $aggregation;
}

/**
Expand Down
23 changes: 20 additions & 3 deletions Datastore/src/Query/AggregationQueryResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

use Google\Cloud\Core\Timestamp;
use Google\Cloud\Core\TimestampTrait;
use Google\Cloud\Datastore\EntityMapper;
use InvalidArgumentException;

/**
Expand Down Expand Up @@ -68,13 +69,20 @@ class AggregationQueryResult
*/
private $transaction;

/**
* @var EntityMapper
*/
private $mapper;

/**
* Create AggregationQueryResult object.
*
* @param array $result Response of
* [RunAggregationQuery](https://cloud.google.com/datastore/docs/reference/data/rest/v1/projects/runAggregationQuery)
* @param EntityMapper $mapper [Optional] Entity mapper to map datastore values
* to their equivalent php values incase of `null`, `NAN`, `INF` and `-INF`
*/
public function __construct($result = [])
public function __construct($result = [], $mapper = null)
{
// When executing an Agggregation query nested with GqlQuery, the server will return
// the parsed query with the first response batch.
Expand All @@ -93,6 +101,14 @@ public function __construct($result = [])
if (isset($result['batch']['readTime'])) {
$this->readTime = $result['batch']['readTime'];
}

// Though the client always passes an entity mapper object, we need to
// re-instantiate an entity mapper incase a user creates an instance of
// AggregationQueryResult manually without supplying the `$entityMapper`.
// Here, entity mapper is used to parse response +/- 'Infinity' to +/- `INF`,
// 'NaN' to `NAN` and ['nullValue' => 0] to ['nullValue' => null]. Therefore the
// usage is independent of `$projectId` or other arguments.
$this->mapper = $mapper ?? new EntityMapper('', true, false);
}

/**
Expand All @@ -109,7 +125,7 @@ public function __construct($result = [])
* ```
*
* @param string $alias The aggregation alias.
* @return int
* @return mixed
* @throws InvalidArgumentException If provided alias does not exist in result.
*/
public function get($alias)
Expand All @@ -119,7 +135,8 @@ public function get($alias)
}
$result = $this->aggregationResults[0]['aggregateProperties'][$alias];
if (is_array($result)) {
return $result['integerValue'];
$key = array_key_first($result);
return $this->mapper->convertValue($key, $result[$key]);
}
return $result;
}
Expand Down
Loading

0 comments on commit b43313e

Please sign in to comment.