Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aggregation storages #44

Merged
merged 15 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 111 additions & 5 deletions src/Array/ArrayStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

namespace Shopware\Storage\Array;

use Shopware\Storage\Common\Aggregation\AggregationAware;
use Shopware\Storage\Common\Aggregation\AggregationCaster;
use Shopware\Storage\Common\Aggregation\Type\Aggregation;
use Shopware\Storage\Common\Aggregation\Type\Avg;
use Shopware\Storage\Common\Aggregation\Type\Count;
use Shopware\Storage\Common\Aggregation\Type\Distinct;
use Shopware\Storage\Common\Aggregation\Type\Max;
use Shopware\Storage\Common\Aggregation\Type\Min;
use Shopware\Storage\Common\Aggregation\Type\Sum;
use Shopware\Storage\Common\Document\Document;
use Shopware\Storage\Common\Document\Documents;
use Shopware\Storage\Common\Filter\FilterAware;
Expand All @@ -26,22 +35,23 @@
use Shopware\Storage\Common\Filter\Type\Not;
use Shopware\Storage\Common\Filter\Type\Prefix;
use Shopware\Storage\Common\Filter\Type\Suffix;
use Shopware\Storage\Common\KeyValue\KeyAware;
use Shopware\Storage\Common\Schema\FieldType;
use Shopware\Storage\Common\Schema\Schema;
use Shopware\Storage\Common\Schema\SchemaUtil;
use Shopware\Storage\Common\Storage;
use Shopware\Storage\Common\StorageContext;
use Shopware\Storage\Common\Total;

class ArrayStorage extends ArrayKeyStorage implements FilterAware
class ArrayStorage extends ArrayKeyStorage implements FilterAware, AggregationAware
{
/**
* @var array<string, Document>
*/
private array $storage = [];

public function __construct(private readonly Schema $schema) {}
public function __construct(
private readonly AggregationCaster $caster,
private readonly Schema $schema
) {}

public function setup(): void
{
Expand Down Expand Up @@ -82,6 +92,43 @@ public function store(Documents $documents): void
}
}

public function aggregate(array $aggregations, Criteria $criteria, StorageContext $context): array
{
$filtered = $this->storage;

if ($criteria->primaries) {
$filtered = array_filter($filtered, fn($key) => in_array($key, $criteria->primaries, true), ARRAY_FILTER_USE_KEY);
}

if ($criteria->filters) {
$filtered = array_filter($filtered, fn(Document $document): bool => $this->match($document, $criteria->filters, $context));
}

$result = [];
foreach ($aggregations as $aggregation) {
// reset to root filter to not apply aggregations specific filters
$documents = $filtered;

if ($aggregation->filters) {
$documents = array_filter($documents, fn(Document $document): bool => $this->match($document, $aggregation->filters, $context));
}

$value = $this->parseAggregation(
filtered: $documents,
aggregation: $aggregation,
context: $context
);

$result[$aggregation->name] = $this->caster->cast(
schema: $this->schema,
aggregation: $aggregation,
data: $value
);
}

return $result;
}

public function filter(Criteria $criteria, StorageContext $context): Result
{
$filtered = $this->storage;
Expand Down Expand Up @@ -118,6 +165,66 @@ public function filter(Criteria $criteria, StorageContext $context): Result
return new Result(elements: $filtered, total: $total);
}

/**
* @param array<Document> $filtered
*/
private function parseAggregation(array $filtered, Aggregation $aggregation, StorageContext $context): mixed
{
$values = [];
foreach ($filtered as $document) {
$nested = $this->getDocValue(
document: $document,
accessor: $aggregation->field,
context: $context
);
if (!is_array($nested)) {
$values[] = $nested;
continue;
}
foreach ($nested as $item) {
$values[] = $item;
}
}

if ($aggregation instanceof Min) {
return min($values);
}

if ($aggregation instanceof Max) {
return max($values);
}

if ($aggregation instanceof Sum) {
return array_sum($values);
}

if ($aggregation instanceof Avg) {
return array_sum($values) / count($values);
}

if ($aggregation instanceof Distinct) {
return array_unique($values);
}

if ($aggregation instanceof Count) {
$mapped = [];
assert(is_array($values), 'Count aggregation must return an array');
foreach ($values as $value) {
$key = (string) $value;

if (!isset($mapped[$key])) {
$mapped[$key] = ['key' => $value, 'count' => 0];
}

$mapped[$key]['count']++;
}

return $mapped;
}

throw new \LogicException(sprintf('Unknown aggregation type %s', get_class($aggregation)));
}

/**
* @param array<Operator|Filter> $filters
*/
Expand Down Expand Up @@ -458,5 +565,4 @@ private function lte(mixed $docValue, mixed $value): bool
default => $docValue <= $value,
};
}

}
8 changes: 0 additions & 8 deletions src/Common/Aggregation/Aggregation.php

This file was deleted.

7 changes: 3 additions & 4 deletions src/Common/Aggregation/AggregationAware.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@

namespace Shopware\Storage\Common\Aggregation;

use Shopware\Storage\Common\Aggregation\Type\Aggregation;
use Shopware\Storage\Common\Filter\Criteria;
use Shopware\Storage\Common\StorageContext;

interface AggregationAware
{
/**
* @param Aggregation[] $aggregations
* @param Criteria $criteria
* @param StorageContext $context
* @return Aggregations
* @return array<string, mixed>
*/
public function aggregate(
array $aggregations,
Criteria $criteria,
StorageContext $context
): Aggregations;
): array;
}
72 changes: 72 additions & 0 deletions src/Common/Aggregation/AggregationCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace Shopware\Storage\Common\Aggregation;

use Shopware\Storage\Common\Aggregation\Type\Aggregation;
use Shopware\Storage\Common\Aggregation\Type\Count;
use Shopware\Storage\Common\Aggregation\Type\Distinct;
use Shopware\Storage\Common\Schema\FieldType;
use Shopware\Storage\Common\Schema\Schema;
use Shopware\Storage\Common\Schema\SchemaUtil;

class AggregationCaster
{
public function cast(Schema $schema, Aggregation $aggregation, mixed $data): mixed
{
$type = SchemaUtil::type(schema: $schema, accessor: $aggregation->field);

switch ($type) {
case FieldType::INT:
// cast to float, because of avg aggregation
$caster = fn($value) => (float) $value;
break;
case FieldType::FLOAT:
$caster = fn($value) => round((float) $value, 6);
break;
case FieldType::BOOL:
$caster = function ($value) {
return match (true) {
$value === '1', $value === 'true' => true,
$value === '0', $value === 'false' => false,
default => (bool) $value
};
};
break;
case FieldType::DATETIME:

$caster = function ($value) {
return match (true) {
is_string($value) => (new \DateTimeImmutable($value))->format('Y-m-d H:i:s.v'),
is_int($value) => (new \DateTimeImmutable('@' . $value))->format('Y-m-d H:i:s.v'),
default => $value
};
};
break;
default:
$caster = fn($value) => $value;
}

if ($aggregation instanceof Distinct) {
assert(is_array($data), 'Distinct aggregation must return an array');

$values = array_map($caster, $data);
sort($values);
return $values;
}

if ($aggregation instanceof Count) {
assert(is_array($data), 'Count aggregation must return an array');

$values = array_map(fn($value) => [
'key' => $caster($value['key']),
'count' => (int) $value['count']
], $data);

usort($values, fn($a, $b) => $a['key'] <=> $b['key']);

return $values;
}

return $caster($data);
}
}
17 changes: 17 additions & 0 deletions src/Common/Aggregation/Type/Aggregation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Shopware\Storage\Common\Aggregation\Type;

use Shopware\Storage\Common\Filter\Type\Filter;

abstract class Aggregation
{
/**
* @param array<Filter> $filters
*/
public function __construct(
public readonly string $name,
public readonly string $field,
public readonly array $filters = [],
) {}
}
5 changes: 5 additions & 0 deletions src/Common/Aggregation/Type/Avg.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Shopware\Storage\Common\Aggregation\Type;

class Avg extends Aggregation {}
5 changes: 5 additions & 0 deletions src/Common/Aggregation/Type/Count.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Shopware\Storage\Common\Aggregation\Type;

class Count extends Aggregation {}
5 changes: 5 additions & 0 deletions src/Common/Aggregation/Type/Distinct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Shopware\Storage\Common\Aggregation\Type;

class Distinct extends Aggregation {}
5 changes: 5 additions & 0 deletions src/Common/Aggregation/Type/Max.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Shopware\Storage\Common\Aggregation\Type;

class Max extends Aggregation {}
5 changes: 5 additions & 0 deletions src/Common/Aggregation/Type/Min.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Shopware\Storage\Common\Aggregation\Type;

class Min extends Aggregation {}
5 changes: 5 additions & 0 deletions src/Common/Aggregation/Type/Sum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Shopware\Storage\Common\Aggregation\Type;

class Sum extends Aggregation {}
1 change: 0 additions & 1 deletion src/Common/KeyValue/KeyAware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Shopware\Storage\Common\Document\Document;
use Shopware\Storage\Common\Document\Documents;
use Shopware\Storage\Common\Storage;

interface KeyAware
{
Expand Down
4 changes: 3 additions & 1 deletion src/Common/Schema/SchemaUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ public static function fieldSchema(Schema $schema, string $accessor): Field
$field = $field->fields[$part] ?? null;

if (!$field instanceof Field) {
throw new \RuntimeException(sprintf('Field %s not found in schema', $part));
throw new \RuntimeException(
sprintf('Unable to get nested field part %s of accessor %s in schema', $part, $accessor)
);
}
}

Expand Down
Loading