From 8cd134123136f2b048379e5128dd436e2a6e2faf Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 21 Sep 2023 13:49:16 +0200 Subject: [PATCH] optimize query pattern used by storage filter Signed-off-by: Robin Appelman --- lib/private/Files/Cache/SearchBuilder.php | 112 +++++++++++---- .../FlattenSingleArgumentBinaryOperation.php | 27 ++++ .../MergeDistributiveOperations.php | 92 ++++++++++++ .../Search/QueryOptimizer/OrEqualsToIn.php | 78 ++++++++++ .../Search/QueryOptimizer/QueryOptimizer.php | 13 +- .../QueryOptimizer/ReplacingOptimizerStep.php | 31 ++++ .../Files/Search/SearchBinaryOperator.php | 15 ++ lib/private/Files/Search/SearchComparison.php | 10 +- lib/public/Files/Search/ISearchComparison.php | 3 +- tests/lib/Files/Cache/SearchBuilderTest.php | 1 + .../Search/QueryOptimizer/CombinedTests.php | 54 +++++++ .../MergeDistributiveOperationsTest.php | 133 ++++++++++++++++++ .../QueryOptimizer/OrEqualsToInTest.php | 81 +++++++++++ 13 files changed, 612 insertions(+), 38 deletions(-) create mode 100644 lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php create mode 100644 lib/private/Files/Search/QueryOptimizer/MergeDistributiveOperations.php create mode 100644 lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php create mode 100644 lib/private/Files/Search/QueryOptimizer/ReplacingOptimizerStep.php create mode 100644 tests/lib/Files/Search/QueryOptimizer/CombinedTests.php create mode 100644 tests/lib/Files/Search/QueryOptimizer/MergeDistributiveOperationsTest.php create mode 100644 tests/lib/Files/Search/QueryOptimizer/OrEqualsToInTest.php diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index b9a70bbd39b2c..05eb1f87e9a26 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -45,6 +45,7 @@ class SearchBuilder { ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte', ISearchComparison::COMPARE_LESS_THAN => 'lt', ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte', + ISearchComparison::COMPARE_IN => 'in', ]; protected static $searchOperatorNegativeMap = [ @@ -55,6 +56,31 @@ class SearchBuilder { ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt', ISearchComparison::COMPARE_LESS_THAN => 'gte', ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'gt', + ISearchComparison::COMPARE_IN => 'notIn', + ]; + + protected static $fieldTypes = [ + 'mimetype' => 'string', + 'mtime' => 'integer', + 'name' => 'string', + 'path' => 'string', + 'size' => 'integer', + 'tagname' => 'string', + 'systemtag' => 'string', + 'favorite' => 'boolean', + 'fileid' => 'integer', + 'storage' => 'integer', + ]; + + protected static $paramTypeMap = [ + 'string' => IQueryBuilder::PARAM_STR, + 'integer' => IQueryBuilder::PARAM_INT, + 'boolean' => IQueryBuilder::PARAM_INT, + ]; + protected static $paramArrayTypeMap = [ + 'string' => IQueryBuilder::PARAM_STR_ARRAY, + 'integer' => IQueryBuilder::PARAM_INT_ARRAY, + 'boolean' => IQueryBuilder::PARAM_INT_ARRAY, ]; public const TAG_FAVORITE = '_$!!$_'; @@ -108,7 +134,7 @@ public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $ } else { throw new \InvalidArgumentException('Binary operators inside "not" is not supported'); } - // no break + // no break case ISearchBinaryOperator::OPERATOR_AND: return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments())); case ISearchBinaryOperator::OPERATOR_OR: @@ -129,21 +155,46 @@ private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchCompari [$field, $value, $type] = $this->getOperatorFieldAndValue($comparison); if (isset($operatorMap[$type])) { $queryOperator = $operatorMap[$type]; - return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value)); + return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value, self::$fieldTypes[$comparison->getField()])); } else { throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType()); } } - private function getOperatorFieldAndValue(ISearchComparison $operator) { + /** + * @param ISearchComparison $operator + * @return list{string, string|integer|\DateTime|(\DateTime|int|string)[], string} + */ + private function getOperatorFieldAndValue(ISearchComparison $operator): array { $field = $operator->getField(); $value = $operator->getValue(); $type = $operator->getType(); + $pathEqHash = $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true); + return $this->getOperatorFieldAndValueInner($field, $value, $type, $pathEqHash); + } + + /** + * @param string $field + * @param string|integer|\DateTime|(\DateTime|int|string)[] $value + * @param string $type + * @return list{string, string|integer|\DateTime|(\DateTime|int|string)[], string} + */ + private function getOperatorFieldAndValueInner(string $field, mixed $value, string $type, bool $pathEqHash): array { + if ($type === ISearchComparison::COMPARE_IN) { + $resultField = null; + $values = []; + foreach($value as $arrayValue) { + $result = $this->getOperatorFieldAndValueInner($field, $arrayValue, ISearchComparison::COMPARE_EQUAL, $pathEqHash); + $resultField = $result[0]; + $values[] = $result[1]; + } + return [$resultField, $values, ISearchComparison::COMPARE_IN]; + } if ($field === 'mimetype') { $value = (string)$value; - if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) { + if ($type === ISearchComparison::COMPARE_EQUAL) { $value = (int)$this->mimetypeLoader->getId($value); - } elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) { + } elseif ($type === ISearchComparison::COMPARE_LIKE) { // transform "mimetype='foo/%'" to "mimepart='foo'" if (preg_match('|(.+)/%|', $value, $matches)) { $field = 'mimepart'; @@ -168,7 +219,7 @@ private function getOperatorFieldAndValue(ISearchComparison $operator) { $field = 'systemtag.name'; } elseif ($field === 'fileid') { $field = 'file.fileid'; - } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) { + } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $pathEqHash) { $field = 'path_hash'; $value = md5((string)$value); } @@ -176,51 +227,50 @@ private function getOperatorFieldAndValue(ISearchComparison $operator) { } private function validateComparison(ISearchComparison $operator) { - $types = [ - 'mimetype' => 'string', - 'mtime' => 'integer', - 'name' => 'string', - 'path' => 'string', - 'size' => 'integer', - 'tagname' => 'string', - 'systemtag' => 'string', - 'favorite' => 'boolean', - 'fileid' => 'integer', - 'storage' => 'integer', - ]; $comparisons = [ - 'mimetype' => ['eq', 'like'], + 'mimetype' => ['eq', 'like', 'in'], 'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'], - 'name' => ['eq', 'like', 'clike'], - 'path' => ['eq', 'like', 'clike'], + 'name' => ['eq', 'like', 'clike', 'in'], + 'path' => ['eq', 'like', 'clike', 'in'], 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'], 'tagname' => ['eq', 'like'], 'systemtag' => ['eq', 'like'], 'favorite' => ['eq'], - 'fileid' => ['eq'], - 'storage' => ['eq'], + 'fileid' => ['eq', 'in'], + 'storage' => ['eq', 'in'], ]; - if (!isset($types[$operator->getField()])) { + if (!isset(self::$fieldTypes[$operator->getField()])) { throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField()); } - $type = $types[$operator->getField()]; - if (gettype($operator->getValue()) !== $type) { - throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField()); + $type = self::$fieldTypes[$operator->getField()]; + if ($operator->getType() === ISearchComparison::COMPARE_IN) { + if (!is_array($operator->getValue())) { + throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField()); + } + foreach ($operator->getValue() as $arrayValue) { + if (gettype($arrayValue) !== $type) { + throw new \InvalidArgumentException('Invalid type in array for field ' . $operator->getField()); + } + } + } else { + if (gettype($operator->getValue()) !== $type) { + throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField()); + } } if (!in_array($operator->getType(), $comparisons[$operator->getField()])) { throw new \InvalidArgumentException('Unsupported comparison for field ' . $operator->getField() . ': ' . $operator->getType()); } } - private function getParameterForValue(IQueryBuilder $builder, $value) { + private function getParameterForValue(IQueryBuilder $builder, $value, string $paramType) { if ($value instanceof \DateTime) { $value = $value->getTimestamp(); } - if (is_numeric($value)) { - $type = IQueryBuilder::PARAM_INT; + if (is_array($value)) { + $type = self::$paramArrayTypeMap[$paramType]; } else { - $type = IQueryBuilder::PARAM_STR; + $type = self::$paramTypeMap[$paramType]; } return $builder->createNamedParameter($value, $type); } diff --git a/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php b/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php new file mode 100644 index 0000000000000..2c32f2e01746a --- /dev/null +++ b/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php @@ -0,0 +1,27 @@ +getArguments()) === 1 && + ( + $operator->getType() === ISearchBinaryOperator::OPERATOR_OR || + $operator->getType() === ISearchBinaryOperator::OPERATOR_AND + ) + ) { + $operator = $operator->getArguments()[0]; + return true; + } + return false; + } +} diff --git a/lib/private/Files/Search/QueryOptimizer/MergeDistributiveOperations.php b/lib/private/Files/Search/QueryOptimizer/MergeDistributiveOperations.php new file mode 100644 index 0000000000000..3f33db2439b6c --- /dev/null +++ b/lib/private/Files/Search/QueryOptimizer/MergeDistributiveOperations.php @@ -0,0 +1,92 @@ +isAllSameBinaryOperation($operator->getArguments()) + ) { + $topLevelType = $operator->getType(); + + $groups = $this->groupBinaryOperatorsByChild($operator->getArguments(), 0); + $outerOperations = array_map(function(array $operators) use ($topLevelType) { + /** @var ISearchBinaryOperator $firstArgument */ + $firstArgument = $operators[0]; + $outerType = $firstArgument->getType(); + $extractedLeftHand = $firstArgument->getArguments()[0]; + + $rightHandArguments = array_map(function(ISearchOperator $inner) { + /** @var ISearchBinaryOperator $inner */ + $arguments = $inner->getArguments(); + array_shift($arguments); + if (count($arguments) === 1){ + return $arguments[0]; + } + return new SearchBinaryOperator($inner->getType(), $arguments); + }, $operators); + $extractedRightHand = new SearchBinaryOperator($topLevelType, $rightHandArguments); + return new SearchBinaryOperator( + $outerType, + [$extractedLeftHand, $extractedRightHand] + ); + }, $groups); + $operator = new SearchBinaryOperator($topLevelType, $outerOperations); + parent::processOperator($operator); + return true; + } + return parent::processOperator($operator); + } + + /** + * Check that a list of operators is all the same type of (non-empty) binary operators + * + * @param ISearchOperator[] $operators + * @return bool + * @psalm-assert-if-true ISearchBinaryOperator[] $operators + */ + private function isAllSameBinaryOperation(array $operators): bool { + $operation = null; + foreach ($operators as $operator) { + if (!$operator instanceof SearchBinaryOperator) { + return false; + } + if (!$operator->getArguments()) { + return false; + } + if ($operation === null) { + $operation = $operator->getType(); + } else { + if ($operation !== $operator->getType()) { + return false; + } + } + } + return true; + } + + /** + * Group a list of binary search operators that have a common argument + * + * @param ISearchBinaryOperator[] $operators + * @return ISearchBinaryOperator[][] + */ + private function groupBinaryOperatorsByChild(array $operators, int $index): array { + $result = []; + foreach($operators as $operator) { + $childKey = (string) $operator->getArguments()[0]; + $result[$childKey][] = $operator; + } + return array_values($result); + } +} diff --git a/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php b/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php new file mode 100644 index 0000000000000..2f2fa1a707b15 --- /dev/null +++ b/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php @@ -0,0 +1,78 @@ +getType() == ISearchBinaryOperator::OPERATOR_OR + ) { + $groups = $this->groupEqualsComparisonsByField($operator->getArguments()); + $newParts = array_map(function(array $group) { + if (count($group) > 1) { + // because of the logic from `groupEqualsComparisonsByField` we now that group is all comparisons on the same field + /** @var ISearchComparison[] $group */ + $field = $group[0]->getField(); + $values = array_map(function(ISearchComparison $comparison) { + return $comparison->getValue(); + }, $group); + $in = new SearchComparison(ISearchComparison::COMPARE_IN, $field, $values); + $in->setQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, $group[0]->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)); + return $in; + } else { + return $group[0]; + } + }, $groups); + if (count($newParts) === 1) { + $operator = $newParts[0]; + } else { + $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $newParts); + } + return true; + } + parent::processOperator($operator); + return false; + } + + /** + * @param array $operators + * @return string|null + */ + private function isAllEquals(array $operators): bool { + foreach ($operators as $operator) { + if (!$operator instanceof ISearchComparison || $operator->getType() !== ISearchComparison::COMPARE_EQUAL) { + return false; + } + } + return $field; + } + + /** + * Non-equals operators are put in a separate group for each + * + * @param ISearchOperator $operators + * @return ISearchOperator[][] + */ + private function groupEqualsComparisonsByField(array $operators): array { + $result = []; + foreach ($operators as $operator) { + if ($operator instanceof ISearchComparison && $operator->getType() === ISearchComparison::COMPARE_EQUAL) { + $key = $operator->getField() . $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true); + $result[$key][] = $operator; + } else { + $result[] = [$operator]; + } + } + return array_values($result); + } +} diff --git a/lib/private/Files/Search/QueryOptimizer/QueryOptimizer.php b/lib/private/Files/Search/QueryOptimizer/QueryOptimizer.php index 160b27b7f11a8..4ab32362541b3 100644 --- a/lib/private/Files/Search/QueryOptimizer/QueryOptimizer.php +++ b/lib/private/Files/Search/QueryOptimizer/QueryOptimizer.php @@ -30,14 +30,21 @@ class QueryOptimizer { private $steps = []; public function __construct( - PathPrefixOptimizer $pathPrefixOptimizer + PathPrefixOptimizer $pathPrefixOptimizer, + MergeDistributiveOperations $mergeDistributiveOperations, + FlattenSingleArgumentBinaryOperation $flattenSingleArgumentBinaryOperation, + OrEqualsToIn $orEqualsToIn ) { + // note that the order here is relevant $this->steps = [ - $pathPrefixOptimizer + $pathPrefixOptimizer, + $mergeDistributiveOperations, + $flattenSingleArgumentBinaryOperation, + $orEqualsToIn, ]; } - public function processOperator(ISearchOperator $operator) { + public function processOperator(ISearchOperator &$operator) { foreach ($this->steps as $step) { $step->inspectOperator($operator); } diff --git a/lib/private/Files/Search/QueryOptimizer/ReplacingOptimizerStep.php b/lib/private/Files/Search/QueryOptimizer/ReplacingOptimizerStep.php new file mode 100644 index 0000000000000..546061522bc22 --- /dev/null +++ b/lib/private/Files/Search/QueryOptimizer/ReplacingOptimizerStep.php @@ -0,0 +1,31 @@ +getArguments(); + foreach ($arguments as &$argument) { + $modified = $modified || $this->processOperator($argument); + } + if ($modified) { + $operator->setArguments($arguments); + } + } + return false; + } +} diff --git a/lib/private/Files/Search/SearchBinaryOperator.php b/lib/private/Files/Search/SearchBinaryOperator.php index d7bba8f1b4edb..e9deebc3dc657 100644 --- a/lib/private/Files/Search/SearchBinaryOperator.php +++ b/lib/private/Files/Search/SearchBinaryOperator.php @@ -57,6 +57,14 @@ public function getArguments() { return $this->arguments; } + /** + * @param ISearchOperator[] $arguments + * @return void + */ + public function setArguments(array $arguments): void { + $this->arguments = $arguments; + } + public function getQueryHint(string $name, $default) { return $this->hints[$name] ?? $default; } @@ -64,4 +72,11 @@ public function getQueryHint(string $name, $default) { public function setQueryHint(string $name, $value): void { $this->hints[$name] = $value; } + + public function __toString(): string { + if ($this->type === ISearchBinaryOperator::OPERATOR_NOT) { + return '(not ' . $this->arguments[0] . ')'; + } + return '(' . implode(' ' . $this->type . ' ', $this->arguments) . ')'; + } } diff --git a/lib/private/Files/Search/SearchComparison.php b/lib/private/Files/Search/SearchComparison.php index 122a1f730b403..bd83911201ea1 100644 --- a/lib/private/Files/Search/SearchComparison.php +++ b/lib/private/Files/Search/SearchComparison.php @@ -29,7 +29,7 @@ class SearchComparison implements ISearchComparison { private $type; /** @var string */ private $field; - /** @var string|integer|\DateTime */ + /** @var string|integer|\DateTime|(\DateTime|int|string)[] */ private $value; private $hints = []; @@ -38,7 +38,7 @@ class SearchComparison implements ISearchComparison { * * @param string $type * @param string $field - * @param \DateTime|int|string $value + * @param \DateTime|int|string|(\DateTime|int|string)[] $value */ public function __construct($type, $field, $value) { $this->type = $type; @@ -61,7 +61,7 @@ public function getField() { } /** - * @return \DateTime|int|string + * @return \DateTime|int|string|(\DateTime|int|string)[] */ public function getValue() { return $this->value; @@ -78,4 +78,8 @@ public function setQueryHint(string $name, $value): void { public static function escapeLikeParameter(string $param): string { return addcslashes($param, '\\_%'); } + + public function __toString(): string { + return $this->field . ' ' . $this->type . ' ' . json_encode($this->value); + } } diff --git a/lib/public/Files/Search/ISearchComparison.php b/lib/public/Files/Search/ISearchComparison.php index 8ebaeced304a1..9e49f1fcd7315 100644 --- a/lib/public/Files/Search/ISearchComparison.php +++ b/lib/public/Files/Search/ISearchComparison.php @@ -34,6 +34,7 @@ interface ISearchComparison extends ISearchOperator { public const COMPARE_LESS_THAN_EQUAL = 'lte'; public const COMPARE_LIKE = 'like'; public const COMPARE_LIKE_CASE_SENSITIVE = 'clike'; + public const COMPARE_IN = 'in'; public const HINT_PATH_EQ_HASH = 'path_eq_hash'; // transform `path = "$path"` into `path_hash = md5("$path")`, on by default @@ -58,7 +59,7 @@ public function getField(); /** * Get the value to compare the field with * - * @return string|integer|\DateTime + * @return string|integer|\DateTime|(\DateTime|int|string)[] * @since 12.0.0 */ public function getValue(); diff --git a/tests/lib/Files/Cache/SearchBuilderTest.php b/tests/lib/Files/Cache/SearchBuilderTest.php index 5eb1a0252f0d3..45fa17bd2270a 100644 --- a/tests/lib/Files/Cache/SearchBuilderTest.php +++ b/tests/lib/Files/Cache/SearchBuilderTest.php @@ -154,6 +154,7 @@ public function comparisonProvider() { [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), [0, 1]], [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpg'), [0]], [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'image/%'), [0, 1]], + [new SearchComparison(ISearchComparison::COMPARE_IN, 'mimetype', ['image/jpg', 'image/png']), [0, 1]], [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125) diff --git a/tests/lib/Files/Search/QueryOptimizer/CombinedTests.php b/tests/lib/Files/Search/QueryOptimizer/CombinedTests.php new file mode 100644 index 0000000000000..5aaf0cda3a385 --- /dev/null +++ b/tests/lib/Files/Search/QueryOptimizer/CombinedTests.php @@ -0,0 +1,54 @@ +optimizer = new QueryOptimizer( + new PathPrefixOptimizer(), + new MergeDistributiveOperations(), + new FlattenSingleArgumentBinaryOperation(), + new OrEqualsToIn() + ); + } + + public function testBasicOrOfAnds() { + $operator = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "asd"), + ]) + ] + ); + $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 1 and path eq "asd"))', $operator->__toString()); + + $this->optimizer->processOperator($operator); + + $this->assertEquals('(storage eq 1 and path in ["foo","bar","asd"])', $operator->__toString()); + } +} diff --git a/tests/lib/Files/Search/QueryOptimizer/MergeDistributiveOperationsTest.php b/tests/lib/Files/Search/QueryOptimizer/MergeDistributiveOperationsTest.php new file mode 100644 index 0000000000000..2e386a16a7954 --- /dev/null +++ b/tests/lib/Files/Search/QueryOptimizer/MergeDistributiveOperationsTest.php @@ -0,0 +1,133 @@ +optimizer = new MergeDistributiveOperations(); + $this->simplifier = new FlattenSingleArgumentBinaryOperation(); + } + + public function testBasicOrOfAnds() { + $operator = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "asd"), + ]) + ] + ); + $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 1 and path eq "asd"))', $operator->__toString()); + + $this->optimizer->processOperator($operator); + $this->simplifier->processOperator($operator); + + $this->assertEquals('(storage eq 1 and (path eq "foo" or path eq "bar" or path eq "asd"))', $operator->__toString()); + } + + public function testDontTouchIfNotSame() { + $operator = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 2), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 3), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "asd"), + ]) + ] + ); + $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 2 and path eq "bar") or (storage eq 3 and path eq "asd"))', $operator->__toString()); + + $this->optimizer->processOperator($operator); + $this->simplifier->processOperator($operator); + + $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 2 and path eq "bar") or (storage eq 3 and path eq "asd"))', $operator->__toString()); + } + + public function testMergePartial() { + $operator = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 2), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "asd"), + ]) + ] + ); + $this->assertEquals('((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 2 and path eq "asd"))', $operator->__toString()); + + $this->optimizer->processOperator($operator); + $this->simplifier->processOperator($operator); + + $this->assertEquals('((storage eq 1 and (path eq "foo" or path eq "bar")) or (storage eq 2 and path eq "asd"))', $operator->__toString()); + } + + public function testOptimizeInside() { + $operator = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_AND, + [ + new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"), + ]), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "storage", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "asd"), + ]) + ] + ), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "mimetype", "text") + ] + ); + $this->assertEquals('(((storage eq 1 and path eq "foo") or (storage eq 1 and path eq "bar") or (storage eq 1 and path eq "asd")) and mimetype eq "text")', $operator->__toString()); + + $this->optimizer->processOperator($operator); + $this->simplifier->processOperator($operator); + + $this->assertEquals('((storage eq 1 and (path eq "foo" or path eq "bar" or path eq "asd")) and mimetype eq "text")', $operator->__toString()); + } +} diff --git a/tests/lib/Files/Search/QueryOptimizer/OrEqualsToInTest.php b/tests/lib/Files/Search/QueryOptimizer/OrEqualsToInTest.php new file mode 100644 index 0000000000000..453c220ac375b --- /dev/null +++ b/tests/lib/Files/Search/QueryOptimizer/OrEqualsToInTest.php @@ -0,0 +1,81 @@ +optimizer = new OrEqualsToIn(); + $this->simplifier = new FlattenSingleArgumentBinaryOperation(); + } + + public function testOrs() { + $operator = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "asd"), + ] + ); + $this->assertEquals('(path eq "foo" or path eq "bar" or path eq "asd")', $operator->__toString()); + + $this->optimizer->processOperator($operator); + $this->simplifier->processOperator($operator); + + $this->assertEquals('path in ["foo","bar","asd"]', $operator->__toString()); + } + + public function testOrsMultipleFields() { + $operator = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "fileid", 1), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "fileid", 2), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "mimetype", "asd"), + ] + ); + $this->assertEquals('(path eq "foo" or path eq "bar" or fileid eq 1 or fileid eq 2 or mimetype eq "asd")', $operator->__toString()); + + $this->optimizer->processOperator($operator); + $this->simplifier->processOperator($operator); + + $this->assertEquals('(path in ["foo","bar"] or fileid in [1,2] or mimetype eq "asd")', $operator->__toString()); + } + + public function testPreserveHints() { + $operator = new SearchBinaryOperator( + ISearchBinaryOperator::OPERATOR_OR, + [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "asd"), + ] + ); + foreach ($operator->getArguments() as $argument) { + $argument->setQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, false); + } + $this->assertEquals('(path eq "foo" or path eq "bar" or path eq "asd")', $operator->__toString()); + + $this->optimizer->processOperator($operator); + $this->simplifier->processOperator($operator); + + $this->assertEquals('path in ["foo","bar","asd"]', $operator->__toString()); + $this->assertEquals(false, $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)); + } +}