Commit 5091c9d5 authored by Théo Fidry's avatar Théo Fidry Committed by Bernhard Schussek

Added support for composite fields on multiple aggregates

parent eae95a2c
......@@ -16,6 +16,7 @@ namespace Cwd\DataDoctrineORMBundle\OffsetStrategy;
use Cwd\DataBundle\Specification\Offset;
use Cwd\DataBundle\Specification\OrderDirection;
use Doctrine\ORM\QueryBuilder;
use function strpos;
use Webmozart\Assert\Assert;
class CompositeFieldsOffsetStrategy implements OffsetStrategy
......@@ -36,6 +37,11 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
*/
private $defaultDirections;
/**
* @var string[]
*/
private $joinedAliases;
/**
* @var string[]
*/
......@@ -46,7 +52,7 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
*/
private $separator;
public function __construct(array $fieldNames, array $defaultDirections, array $explicitCasts = [], string $separator = self::DEFAULT_SEPARATOR)
public function __construct(array $fieldNames, array $defaultDirections, array $joinedAliases = [], array $explicitCasts = [], string $separator = self::DEFAULT_SEPARATOR)
{
Assert::minCount($fieldNames, 1);
Assert::count($fieldNames, count($defaultDirections));
......@@ -55,6 +61,7 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
$this->fieldNames = $fieldNames;
$this->defaultDirections = $defaultDirections;
$this->joinedAliases = $joinedAliases;
$this->explicitCasts = $explicitCasts;
$this->separator = $separator;
}
......@@ -90,17 +97,20 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
$fieldValues = $this->split($fromOffset);
$nextConditionPrefixes = [];
$i = 0;
$lastIndex = count($fieldValues) - 1;
foreach ($fieldValues as $fieldValue) {
$conditionPrefix = count($nextConditionPrefixes) > 0
? '('.implode(' AND ', $nextConditionPrefixes).') OR '
: '';
$qb->andWhere($conditionPrefix.sprintf(
'%s.%s %s :fromOffset%s',
$alias,
$this->fieldNames[$i],
OrderDirection::ASCENDING === $sortDirection ? '>=' : '<=',
? implode(' AND ', $nextConditionPrefixes).' AND '
: ''
;
$qb->orWhere($conditionPrefix.sprintf(
'%s %s :fromOffset%s',
$this->addAlias($this->fieldNames[$i], $alias),
$i === $lastIndex
? (OrderDirection::ASCENDING === $sortDirection ? '>=' : '<=')
: (OrderDirection::ASCENDING === $sortDirection ? '>' : '<'),
$i
));
......@@ -110,9 +120,8 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
);
$nextConditionPrefixes[] = sprintf(
'%s.%s <> :fromOffset%s',
$alias,
$this->fieldNames[$i],
'%s = :fromOffset%s',
$this->addAlias($this->fieldNames[$i], $alias),
$i
);
......@@ -131,13 +140,13 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
foreach ($fieldValues as $fieldValue) {
$conditionPrefix = count($nextConditionPrefixes) > 0
? '('.implode(' AND ', $nextConditionPrefixes).') OR '
: '';
? implode(' AND ', $nextConditionPrefixes).' AND '
: ''
;
$qb->andWhere($conditionPrefix.sprintf(
'%s.%s %s :afterOffset%s',
$alias,
$this->fieldNames[$i],
$qb->orWhere($conditionPrefix.sprintf(
'%s %s :afterOffset%s',
$this->addAlias($this->fieldNames[$i], $alias),
OrderDirection::ASCENDING === $sortDirection ? '>' : '<',
$i
));
......@@ -148,9 +157,8 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
);
$nextConditionPrefixes[] = sprintf(
'%s.%s <> :afterOffset%s',
$alias,
$this->fieldNames[$i],
'%s = :afterOffset%s',
$this->addAlias($this->fieldNames[$i], $alias),
$i
);
......@@ -167,7 +175,7 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
foreach ($this->defaultDirections as $i => $defaultDirection) {
$qb->addOrderBy(
sprintf('%s.%s', $alias, $this->fieldNames[$i]),
$this->addAlias($this->fieldNames[$i], $alias),
$invertDirections
? OrderDirection::invert($defaultDirection)
: $defaultDirection
......@@ -191,8 +199,24 @@ class CompositeFieldsOffsetStrategy implements OffsetStrategy
private function generateFieldName($fieldName, $alias)
{
return in_array($fieldName, $this->explicitCasts, true)
? sprintf('CAST(%s.%s AS TEXT)', $alias, $fieldName)
: sprintf('%s.%s', $alias, $fieldName);
? sprintf('CAST(%s AS TEXT)', $this->addAlias($fieldName, $alias))
: $this->addAlias($fieldName, $alias)
;
}
private function addAlias(string $fieldName, string $alias): string
{
if (false !== ($pos = strpos($fieldName, '.'))) {
$givenAlias = substr($fieldName, 0, $pos);
if (in_array($givenAlias, $this->joinedAliases, true)) {
// Don't add an alias if the name already includes the alias
// of a joined table
return $fieldName;
}
}
return $alias.'.'.$fieldName;
}
private function split(Offset $offset): array
......
......@@ -17,6 +17,10 @@ use Cwd\DataBundle\Specification\OrderBy;
use Cwd\DataBundle\Specification\OrderDirection;
use Cwd\DataBundle\Specification\Specifications;
use Doctrine\ORM\Mapping\ClassMetadata;
use function iter\rewindable\filter;
use function iter\rewindable\map;
use function iter\toArray;
use function substr;
final class OffsetStrategyFactory
{
......@@ -25,34 +29,68 @@ final class OffsetStrategyFactory
/* @var OrderBy $orderBy */
$orderBy = $specifications->get(OrderBy::class);
$fieldMappings = array_combine(
$orderBy->getFieldNames(),
array_map(
[$classMetadata, 'getFieldMapping'],
$orderBy->getFieldNames()
)
);
$idField = current($classMetadata->getIdentifierFieldNames());
$requiresAdditionUniqueField = true;
$uniqueFieldNames = array_merge(
[$idField],
array_keys(array_column($fieldMappings, 'unique', 'fieldName'))
);
$fieldNames = $orderBy->getFieldNames();
$directions = $orderBy->getDirections();
$requiresAdditionalUniqueField = true;
$joinedAliases = self::retrieveJoinAliases($fieldNames, $classMetadata);
foreach ($fieldNames as $fieldName) {
if (in_array($fieldName, $uniqueFieldNames, true)) {
$requiresAdditionUniqueField = false;
// When there are joined columns involved, we always need to add
// a unique field (=the ID) to make the cursor unambiguous
// This is because we don't know how many joins are needed to reach
// the class corresponding to the joined alias.
// In addition, all join columns in between as well as the column
// in question on the joined table would need to be unique, which
// is unlikely and impractical to check
if ([] === $joinedAliases) {
$fieldMappings = array_combine(
$orderBy->getFieldNames(),
array_map(
[$classMetadata, 'getFieldMapping'],
$orderBy->getFieldNames()
)
);
$uniqueFieldNames = array_merge(
[$idField],
array_keys(array_column($fieldMappings, 'unique', 'fieldName'))
);
foreach ($fieldNames as $fieldName) {
if (in_array($fieldName, $uniqueFieldNames, true)) {
$requiresAdditionalUniqueField = false;
}
}
}
if ($requiresAdditionUniqueField) {
if ($requiresAdditionalUniqueField) {
$fieldNames[] = $idField;
$directions[] = OrderDirection::ASCENDING;
}
return new CompositeFieldsOffsetStrategy($fieldNames, $directions);
return new CompositeFieldsOffsetStrategy($fieldNames, $directions, $joinedAliases);
}
/**
* @return string[]
*/
private static function retrieveJoinAliases(array $fieldNames, ClassMetadata $classMetadata): array
{
$containsDot = static function (string $string): bool {
return false !== strpos($string, '.');
};
$returnPortionBeforeDot = static function (string $fieldName): string {
return substr($fieldName, 0, strpos($fieldName, '.'));
};
return toArray(filter(
static function ($alias) use ($classMetadata) {
// Exclude embeddables from the list
return !$classMetadata->hasField($alias);
},
map($returnPortionBeforeDot, filter($containsDot, $fieldNames))
));
}
private function __construct()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment