mirror of
https://github.com/nextcloud/server.git
synced 2026-06-10 09:13:19 -04:00
Merge pull request #40555 from nextcloud/query-optimize-distribute
optimize query pattern used by storage filter
This commit is contained in:
commit
bb87232882
20 changed files with 877 additions and 54 deletions
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
|
|
|||
|
|
@ -1406,9 +1406,15 @@ return array(
|
|||
'OC\\Files\\ObjectStore\\Swift' => $baseDir . '/lib/private/Files/ObjectStore/Swift.php',
|
||||
'OC\\Files\\ObjectStore\\SwiftFactory' => $baseDir . '/lib/private/Files/ObjectStore/SwiftFactory.php',
|
||||
'OC\\Files\\ObjectStore\\SwiftV2CachingAuthService' => $baseDir . '/lib/private/Files/ObjectStore/SwiftV2CachingAuthService.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\FlattenNestedBool' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/FlattenNestedBool.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\FlattenSingleArgumentBinaryOperation' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\MergeDistributiveOperations' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/MergeDistributiveOperations.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\OrEqualsToIn' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\PathPrefixOptimizer' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\QueryOptimizer' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/QueryOptimizer.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\QueryOptimizerStep' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/QueryOptimizerStep.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\ReplacingOptimizerStep' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/ReplacingOptimizerStep.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\SplitLargeIn' => $baseDir . '/lib/private/Files/Search/QueryOptimizer/SplitLargeIn.php',
|
||||
'OC\\Files\\Search\\SearchBinaryOperator' => $baseDir . '/lib/private/Files/Search/SearchBinaryOperator.php',
|
||||
'OC\\Files\\Search\\SearchComparison' => $baseDir . '/lib/private/Files/Search/SearchComparison.php',
|
||||
'OC\\Files\\Search\\SearchOrder' => $baseDir . '/lib/private/Files/Search/SearchOrder.php',
|
||||
|
|
|
|||
|
|
@ -1439,9 +1439,15 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OC\\Files\\ObjectStore\\Swift' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/Swift.php',
|
||||
'OC\\Files\\ObjectStore\\SwiftFactory' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/SwiftFactory.php',
|
||||
'OC\\Files\\ObjectStore\\SwiftV2CachingAuthService' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/SwiftV2CachingAuthService.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\FlattenNestedBool' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/FlattenNestedBool.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\FlattenSingleArgumentBinaryOperation' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/FlattenSingleArgumentBinaryOperation.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\MergeDistributiveOperations' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/MergeDistributiveOperations.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\OrEqualsToIn' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\PathPrefixOptimizer' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/PathPrefixOptimizer.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\QueryOptimizer' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/QueryOptimizer.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\QueryOptimizerStep' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/QueryOptimizerStep.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\ReplacingOptimizerStep' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/ReplacingOptimizerStep.php',
|
||||
'OC\\Files\\Search\\QueryOptimizer\\SplitLargeIn' => __DIR__ . '/../../..' . '/lib/private/Files/Search/QueryOptimizer/SplitLargeIn.php',
|
||||
'OC\\Files\\Search\\SearchBinaryOperator' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchBinaryOperator.php',
|
||||
'OC\\Files\\Search\\SearchComparison' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchComparison.php',
|
||||
'OC\\Files\\Search\\SearchOrder' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchOrder.php',
|
||||
|
|
|
|||
|
|
@ -37,8 +37,12 @@ use OCP\FilesMetadata\IMetadataQuery;
|
|||
|
||||
/**
|
||||
* Tools for transforming search queries into database queries
|
||||
*
|
||||
* @psalm-import-type ParamSingleValue from ISearchComparison
|
||||
* @psalm-import-type ParamValue from ISearchComparison
|
||||
*/
|
||||
class SearchBuilder {
|
||||
/** @var array<string, string> */
|
||||
protected static $searchOperatorMap = [
|
||||
ISearchComparison::COMPARE_LIKE => 'iLike',
|
||||
ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'like',
|
||||
|
|
@ -48,8 +52,10 @@ class SearchBuilder {
|
|||
ISearchComparison::COMPARE_LESS_THAN => 'lt',
|
||||
ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte',
|
||||
ISearchComparison::COMPARE_DEFINED => 'isNotNull',
|
||||
ISearchComparison::COMPARE_IN => 'in',
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected static $searchOperatorNegativeMap = [
|
||||
ISearchComparison::COMPARE_LIKE => 'notLike',
|
||||
ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'notLike',
|
||||
|
|
@ -59,6 +65,38 @@ class SearchBuilder {
|
|||
ISearchComparison::COMPARE_LESS_THAN => 'gte',
|
||||
ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'gt',
|
||||
ISearchComparison::COMPARE_DEFINED => 'isNull',
|
||||
ISearchComparison::COMPARE_IN => 'notIn',
|
||||
];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected static $fieldTypes = [
|
||||
'mimetype' => 'string',
|
||||
'mtime' => 'integer',
|
||||
'name' => 'string',
|
||||
'path' => 'string',
|
||||
'size' => 'integer',
|
||||
'tagname' => 'string',
|
||||
'systemtag' => 'string',
|
||||
'favorite' => 'boolean',
|
||||
'fileid' => 'integer',
|
||||
'storage' => 'integer',
|
||||
'share_with' => 'string',
|
||||
'share_type' => 'integer',
|
||||
'owner' => 'string',
|
||||
];
|
||||
|
||||
/** @var array<string, int> */
|
||||
protected static $paramTypeMap = [
|
||||
'string' => IQueryBuilder::PARAM_STR,
|
||||
'integer' => IQueryBuilder::PARAM_INT,
|
||||
'boolean' => IQueryBuilder::PARAM_BOOL,
|
||||
];
|
||||
|
||||
/** @var array<string, int> */
|
||||
protected static $paramArrayTypeMap = [
|
||||
'string' => IQueryBuilder::PARAM_STR_ARRAY,
|
||||
'integer' => IQueryBuilder::PARAM_INT_ARRAY,
|
||||
'boolean' => IQueryBuilder::PARAM_INT_ARRAY,
|
||||
];
|
||||
|
||||
public const TAG_FAVORITE = '_$!<Favorite>!$_';
|
||||
|
|
@ -142,31 +180,56 @@ class SearchBuilder {
|
|||
?IMetadataQuery $metadataQuery = null
|
||||
) {
|
||||
if ($comparison->getExtra()) {
|
||||
[$field, $value, $type] = $this->getExtraOperatorField($comparison, $metadataQuery);
|
||||
[$field, $value, $type, $paramType] = $this->getExtraOperatorField($comparison, $metadataQuery);
|
||||
} else {
|
||||
[$field, $value, $type] = $this->getOperatorFieldAndValue($comparison);
|
||||
[$field, $value, $type, $paramType] = $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, $paramType));
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
|
||||
}
|
||||
}
|
||||
|
||||
private function getOperatorFieldAndValue(ISearchComparison $operator) {
|
||||
/**
|
||||
* @param ISearchComparison $operator
|
||||
* @return list{string, ParamValue, string, string}
|
||||
*/
|
||||
private function getOperatorFieldAndValue(ISearchComparison $operator): array {
|
||||
$this->validateComparison($operator);
|
||||
|
||||
$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 ParamValue $value
|
||||
* @param string $type
|
||||
* @return list{string, ParamValue, string, string}
|
||||
*/
|
||||
private function getOperatorFieldAndValueInner(string $field, mixed $value, string $type, bool $pathEqHash): array {
|
||||
$paramType = self::$fieldTypes[$field];
|
||||
if ($type === ISearchComparison::COMPARE_IN) {
|
||||
$resultField = $field;
|
||||
$values = [];
|
||||
foreach ($value as $arrayValue) {
|
||||
/** @var ParamSingleValue $arrayValue */
|
||||
[$arrayField, $arrayValue] = $this->getOperatorFieldAndValueInner($field, $arrayValue, ISearchComparison::COMPARE_EQUAL, $pathEqHash);
|
||||
$resultField = $arrayField;
|
||||
$values[] = $arrayValue;
|
||||
}
|
||||
return [$resultField, $values, ISearchComparison::COMPARE_IN, $paramType];
|
||||
}
|
||||
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';
|
||||
|
|
@ -183,6 +246,7 @@ class SearchBuilder {
|
|||
} elseif ($field === 'favorite') {
|
||||
$field = 'tag.category';
|
||||
$value = self::TAG_FAVORITE;
|
||||
$paramType = 'string';
|
||||
} elseif ($field === 'name') {
|
||||
$field = 'file.name';
|
||||
} elseif ($field === 'tagname') {
|
||||
|
|
@ -191,53 +255,49 @@ class SearchBuilder {
|
|||
$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);
|
||||
} elseif ($field === 'owner') {
|
||||
$field = 'uid_owner';
|
||||
}
|
||||
return [$field, $value, $type];
|
||||
return [$field, $value, $type, $paramType];
|
||||
}
|
||||
|
||||
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',
|
||||
'share_with' => 'string',
|
||||
'share_type' => 'integer',
|
||||
'owner' => 'string',
|
||||
];
|
||||
$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'],
|
||||
'share_with' => ['eq'],
|
||||
'share_type' => ['eq'],
|
||||
'owner' => ['eq'],
|
||||
];
|
||||
|
||||
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());
|
||||
|
|
@ -246,6 +306,7 @@ class SearchBuilder {
|
|||
|
||||
|
||||
private function getExtraOperatorField(ISearchComparison $operator, IMetadataQuery $metadataQuery): array {
|
||||
$paramType = self::$fieldTypes[$operator->getField()];
|
||||
$field = $operator->getField();
|
||||
$value = $operator->getValue();
|
||||
$type = $operator->getType();
|
||||
|
|
@ -259,17 +320,17 @@ class SearchBuilder {
|
|||
throw new \InvalidArgumentException('Invalid extra type: ' . $operator->getExtra());
|
||||
}
|
||||
|
||||
return [$field, $value, $type];
|
||||
return [$field, $value, $type, $paramType];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace OC\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
|
||||
class FlattenNestedBool extends QueryOptimizerStep {
|
||||
public function processOperator(ISearchOperator &$operator) {
|
||||
if (
|
||||
$operator instanceof SearchBinaryOperator && (
|
||||
$operator->getType() === ISearchBinaryOperator::OPERATOR_OR ||
|
||||
$operator->getType() === ISearchBinaryOperator::OPERATOR_AND
|
||||
)
|
||||
) {
|
||||
$newArguments = [];
|
||||
foreach ($operator->getArguments() as $oldArgument) {
|
||||
if ($oldArgument instanceof SearchBinaryOperator && $oldArgument->getType() === $operator->getType()) {
|
||||
$newArguments = array_merge($newArguments, $oldArgument->getArguments());
|
||||
} else {
|
||||
$newArguments[] = $oldArgument;
|
||||
}
|
||||
}
|
||||
$operator->setArguments($newArguments);
|
||||
}
|
||||
parent::processOperator($operator);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace OC\Files\Search\QueryOptimizer;
|
||||
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
|
||||
/**
|
||||
* replace single argument AND and OR operations with their single argument
|
||||
*/
|
||||
class FlattenSingleArgumentBinaryOperation extends ReplacingOptimizerStep {
|
||||
public function processOperator(ISearchOperator &$operator): bool {
|
||||
parent::processOperator($operator);
|
||||
if (
|
||||
$operator instanceof ISearchBinaryOperator &&
|
||||
count($operator->getArguments()) === 1 &&
|
||||
(
|
||||
$operator->getType() === ISearchBinaryOperator::OPERATOR_OR ||
|
||||
$operator->getType() === ISearchBinaryOperator::OPERATOR_AND
|
||||
)
|
||||
) {
|
||||
$operator = $operator->getArguments()[0];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace OC\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
|
||||
/**
|
||||
* Attempt to transform
|
||||
*
|
||||
* (A AND B) OR (A AND C) OR (A AND D AND E) into A AND (B OR C OR (D AND E))
|
||||
*
|
||||
* This is always valid because logical 'AND' and 'OR' are distributive[1].
|
||||
*
|
||||
* [1]: https://en.wikipedia.org/wiki/Distributive_property
|
||||
*/
|
||||
class MergeDistributiveOperations extends ReplacingOptimizerStep {
|
||||
public function processOperator(ISearchOperator &$operator): bool {
|
||||
if (
|
||||
$operator instanceof SearchBinaryOperator &&
|
||||
$this->isAllSameBinaryOperation($operator->getArguments())
|
||||
) {
|
||||
// either 'AND' or 'OR'
|
||||
$topLevelType = $operator->getType();
|
||||
|
||||
// split the arguments into groups that share a first argument
|
||||
// (we already know that all arguments are binary operators with at least 1 child)
|
||||
$groups = $this->groupBinaryOperatorsByChild($operator->getArguments(), 0);
|
||||
$outerOperations = array_map(function (array $operators) use ($topLevelType) {
|
||||
// no common operations, no need to change anything
|
||||
if (count($operators) === 1) {
|
||||
return $operators[0];
|
||||
}
|
||||
/** @var ISearchBinaryOperator $firstArgument */
|
||||
$firstArgument = $operators[0];
|
||||
|
||||
// we already checked that all arguments have the same type, so this type applies for all, either 'AND' or 'OR'
|
||||
$innerType = $firstArgument->getType();
|
||||
|
||||
// the common operation we move out ('A' from the example)
|
||||
$extractedLeftHand = $firstArgument->getArguments()[0];
|
||||
|
||||
// for each argument we remove the extracted operation to get the leftovers ('B', 'C' and '(D AND E)' in the example)
|
||||
// note that we leave them inside the "inner" binary operation for when the "inner" operation contained more than two parts
|
||||
// in the (common) case where the "inner" operation only has 1 item left it will be cleaned up in a follow up step
|
||||
$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);
|
||||
|
||||
// combine the extracted operation ('A') with the remaining bit ('(B OR C OR (D AND E))')
|
||||
// note that because of how distribution work, we use the "outer" type "inside" and the "inside" type "outside".
|
||||
$extractedRightHand = new SearchBinaryOperator($topLevelType, $rightHandArguments);
|
||||
return new SearchBinaryOperator(
|
||||
$innerType,
|
||||
[$extractedLeftHand, $extractedRightHand]
|
||||
);
|
||||
}, $groups);
|
||||
|
||||
// combine all groups again
|
||||
$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 SearchBinaryOperator[] $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 SearchBinaryOperator[] $operators
|
||||
* @return SearchBinaryOperator[][]
|
||||
*/
|
||||
private function groupBinaryOperatorsByChild(array $operators, int $index = 0): array {
|
||||
$result = [];
|
||||
foreach ($operators as $operator) {
|
||||
/** @var SearchBinaryOperator|SearchComparison $child */
|
||||
$child = $operator->getArguments()[$index];
|
||||
$childKey = (string) $child;
|
||||
$result[$childKey][] = $operator;
|
||||
}
|
||||
return array_values($result);
|
||||
}
|
||||
}
|
||||
70
lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php
Normal file
70
lib/private/Files/Search/QueryOptimizer/OrEqualsToIn.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace OC\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
|
||||
/**
|
||||
* transform (field == A OR field == B ...) into field IN (A, B, ...)
|
||||
*/
|
||||
class OrEqualsToIn extends ReplacingOptimizerStep {
|
||||
public function processOperator(ISearchOperator &$operator): bool {
|
||||
if (
|
||||
$operator instanceof ISearchBinaryOperator &&
|
||||
$operator->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) {
|
||||
/** @var string|integer|bool|\DateTime $value */
|
||||
$value = $comparison->getValue();
|
||||
return $value;
|
||||
}, $group);
|
||||
$in = new SearchComparison(ISearchComparison::COMPARE_IN, $field, $values);
|
||||
$pathEqHash = array_reduce($group, function ($pathEqHash, ISearchComparison $comparison) {
|
||||
return $comparison->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true) && $pathEqHash;
|
||||
}, true);
|
||||
$in->setQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, $pathEqHash);
|
||||
return $in;
|
||||
} else {
|
||||
return $group[0];
|
||||
}
|
||||
}, $groups);
|
||||
if (count($newParts) === 1) {
|
||||
$operator = $newParts[0];
|
||||
} else {
|
||||
$operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $newParts);
|
||||
}
|
||||
parent::processOperator($operator);
|
||||
return true;
|
||||
}
|
||||
parent::processOperator($operator);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$result[$operator->getField()][] = $operator;
|
||||
} else {
|
||||
$result[] = [$operator];
|
||||
}
|
||||
}
|
||||
return array_values($result);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,15 +29,20 @@ class QueryOptimizer {
|
|||
/** @var QueryOptimizerStep[] */
|
||||
private $steps = [];
|
||||
|
||||
public function __construct(
|
||||
PathPrefixOptimizer $pathPrefixOptimizer
|
||||
) {
|
||||
public function __construct() {
|
||||
// note that the order here is relevant
|
||||
$this->steps = [
|
||||
$pathPrefixOptimizer
|
||||
new PathPrefixOptimizer(),
|
||||
new MergeDistributiveOperations(),
|
||||
new FlattenSingleArgumentBinaryOperation(),
|
||||
new FlattenNestedBool(),
|
||||
new OrEqualsToIn(),
|
||||
new FlattenNestedBool(),
|
||||
new SplitLargeIn(),
|
||||
];
|
||||
}
|
||||
|
||||
public function processOperator(ISearchOperator $operator) {
|
||||
public function processOperator(ISearchOperator &$operator) {
|
||||
foreach ($this->steps as $step) {
|
||||
$step->inspectOperator($operator);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace OC\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
|
||||
/**
|
||||
* Optimizer step that can replace the $operator altogether instead of just modifying it
|
||||
* These steps need some extra logic to properly replace the arguments of binary operators
|
||||
*/
|
||||
class ReplacingOptimizerStep extends QueryOptimizerStep {
|
||||
/**
|
||||
* Allow optimizer steps to modify query operators
|
||||
*
|
||||
* Returns true if the reference $operator points to a new value
|
||||
*/
|
||||
public function processOperator(ISearchOperator &$operator): bool {
|
||||
if ($operator instanceof SearchBinaryOperator) {
|
||||
$modified = false;
|
||||
$arguments = $operator->getArguments();
|
||||
foreach ($arguments as &$argument) {
|
||||
$modified = $modified || $this->processOperator($argument);
|
||||
}
|
||||
if ($modified) {
|
||||
$operator->setArguments($arguments);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
32
lib/private/Files/Search/QueryOptimizer/SplitLargeIn.php
Normal file
32
lib/private/Files/Search/QueryOptimizer/SplitLargeIn.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace OC\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use OCP\Files\Search\ISearchOperator;
|
||||
|
||||
/**
|
||||
* transform IN (1000+ element) into (IN (1000 elements) OR IN(...))
|
||||
*/
|
||||
class SplitLargeIn extends ReplacingOptimizerStep {
|
||||
public function processOperator(ISearchOperator &$operator): bool {
|
||||
if (
|
||||
$operator instanceof ISearchComparison &&
|
||||
$operator->getType() === ISearchComparison::COMPARE_IN &&
|
||||
count($operator->getValue()) > 1000
|
||||
) {
|
||||
$chunks = array_chunk($operator->getValue(), 1000);
|
||||
$chunkComparisons = array_map(function (array $values) use ($operator) {
|
||||
return new SearchComparison(ISearchComparison::COMPARE_IN, $operator->getField(), $values);
|
||||
}, $chunks);
|
||||
|
||||
$operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $chunkComparisons);
|
||||
return true;
|
||||
}
|
||||
parent::processOperator($operator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ use OCP\Files\Search\ISearchOperator;
|
|||
class SearchBinaryOperator implements ISearchBinaryOperator {
|
||||
/** @var string */
|
||||
private $type;
|
||||
/** @var ISearchOperator[] */
|
||||
/** @var (SearchBinaryOperator|SearchComparison)[] */
|
||||
private $arguments;
|
||||
private $hints = [];
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ class SearchBinaryOperator implements ISearchBinaryOperator {
|
|||
* SearchBinaryOperator constructor.
|
||||
*
|
||||
* @param string $type
|
||||
* @param ISearchOperator[] $arguments
|
||||
* @param (SearchBinaryOperator|SearchComparison)[] $arguments
|
||||
*/
|
||||
public function __construct($type, array $arguments) {
|
||||
$this->type = $type;
|
||||
|
|
@ -57,6 +57,14 @@ class SearchBinaryOperator implements ISearchBinaryOperator {
|
|||
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 @@ class SearchBinaryOperator implements ISearchBinaryOperator {
|
|||
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) . ')';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,13 +27,17 @@ namespace OC\Files\Search;
|
|||
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
|
||||
/**
|
||||
* @psalm-import-type ParamValue from ISearchComparison
|
||||
*/
|
||||
class SearchComparison implements ISearchComparison {
|
||||
private array $hints = [];
|
||||
|
||||
public function __construct(
|
||||
private string $type,
|
||||
private string $field,
|
||||
private \DateTime|int|string|bool $value,
|
||||
/** @var ParamValue $value */
|
||||
private \DateTime|int|string|bool|array $value,
|
||||
private string $extra = ''
|
||||
) {
|
||||
}
|
||||
|
|
@ -52,10 +56,7 @@ class SearchComparison implements ISearchComparison {
|
|||
return $this->field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|int|string|bool
|
||||
*/
|
||||
public function getValue(): string|int|bool|\DateTime {
|
||||
public function getValue(): string|int|bool|\DateTime|array {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
|
|
@ -78,4 +79,8 @@ class SearchComparison implements ISearchComparison {
|
|||
public static function escapeLikeParameter(string $param): string {
|
||||
return addcslashes($param, '\\_%');
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return $this->field . ' ' . $this->type . ' ' . json_encode($this->value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ namespace OCP\Files\Search;
|
|||
|
||||
/**
|
||||
* @since 12.0.0
|
||||
*
|
||||
* @psalm-type ParamSingleValue = \DateTime|int|string|bool
|
||||
* @psalm-type ParamValue = ParamSingleValue|list<ParamSingleValue>
|
||||
*/
|
||||
interface ISearchComparison extends ISearchOperator {
|
||||
/**
|
||||
|
|
@ -67,6 +70,11 @@ interface ISearchComparison extends ISearchOperator {
|
|||
* @since 28.0.0
|
||||
*/
|
||||
public const COMPARE_DEFINED = 'is-defined';
|
||||
|
||||
/**
|
||||
* @since 29.0.0
|
||||
*/
|
||||
public const COMPARE_IN = 'in';
|
||||
|
||||
/**
|
||||
* @since 23.0.0
|
||||
|
|
@ -102,8 +110,8 @@ interface ISearchComparison extends ISearchOperator {
|
|||
/**
|
||||
* Get the value to compare the field with
|
||||
*
|
||||
* @return string|integer|bool|\DateTime
|
||||
* @return ParamValue
|
||||
* @since 12.0.0
|
||||
*/
|
||||
public function getValue(): string|int|bool|\DateTime;
|
||||
public function getValue(): string|int|bool|\DateTime|array;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ class SearchBuilderTest extends TestCase {
|
|||
[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)
|
||||
|
|
|
|||
45
tests/lib/Files/Search/QueryOptimizer/CombinedTests.php
Normal file
45
tests/lib/Files/Search/QueryOptimizer/CombinedTests.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace Test\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\QueryOptimizer\QueryOptimizer;
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use Test\TestCase;
|
||||
|
||||
class CombinedTests extends TestCase {
|
||||
private QueryOptimizer $optimizer;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->optimizer = new QueryOptimizer();
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Test\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\QueryOptimizer\FlattenNestedBool;
|
||||
use OC\Files\Search\QueryOptimizer\FlattenSingleArgumentBinaryOperation;
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use Test\TestCase;
|
||||
|
||||
class FlattenNestedBoolTest extends TestCase {
|
||||
private $optimizer;
|
||||
private $simplifier;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->optimizer = new FlattenNestedBool();
|
||||
$this->simplifier = new FlattenSingleArgumentBinaryOperation();
|
||||
}
|
||||
|
||||
public function testOrs() {
|
||||
$operator = new SearchBinaryOperator(
|
||||
ISearchBinaryOperator::OPERATOR_OR,
|
||||
[
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"),
|
||||
new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [
|
||||
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 eq "foo" or path eq "bar" or path eq "asd")', $operator->__toString());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
namespace Test\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\QueryOptimizer\FlattenSingleArgumentBinaryOperation;
|
||||
use OC\Files\Search\QueryOptimizer\MergeDistributiveOperations;
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use Test\TestCase;
|
||||
|
||||
class MergeDistributiveOperationsTest extends TestCase {
|
||||
private $optimizer;
|
||||
private $simplifier;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->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());
|
||||
}
|
||||
|
||||
public function testMoveInnerOperations() {
|
||||
$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"),
|
||||
new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, "size", "100"),
|
||||
])
|
||||
]
|
||||
);
|
||||
$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 size gt "100"))', $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 size gt "100")))', $operator->__toString());
|
||||
}
|
||||
}
|
||||
120
tests/lib/Files/Search/QueryOptimizer/OrEqualsToInTest.php
Normal file
120
tests/lib/Files/Search/QueryOptimizer/OrEqualsToInTest.php
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
namespace Test\Files\Search\QueryOptimizer;
|
||||
|
||||
use OC\Files\Search\QueryOptimizer\FlattenSingleArgumentBinaryOperation;
|
||||
use OC\Files\Search\QueryOptimizer\OrEqualsToIn;
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use Test\TestCase;
|
||||
|
||||
class OrEqualsToInTest extends TestCase {
|
||||
private $optimizer;
|
||||
private $simplifier;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->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));
|
||||
}
|
||||
|
||||
public function testOrSomeEq() {
|
||||
$operator = new SearchBinaryOperator(
|
||||
ISearchBinaryOperator::OPERATOR_OR,
|
||||
[
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "foo"),
|
||||
new SearchComparison(ISearchComparison::COMPARE_LIKE, "path", "foo%"),
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, "path", "bar"),
|
||||
]
|
||||
);
|
||||
$this->assertEquals('(path eq "foo" or path like "foo%" or path eq "bar")', $operator->__toString());
|
||||
|
||||
$this->optimizer->processOperator($operator);
|
||||
$this->simplifier->processOperator($operator);
|
||||
|
||||
$this->assertEquals('(path in ["foo","bar"] or path like "foo%")', $operator->__toString());
|
||||
}
|
||||
|
||||
public function testOrsInside() {
|
||||
$operator = new SearchBinaryOperator(
|
||||
ISearchBinaryOperator::OPERATOR_AND,
|
||||
[
|
||||
new SearchComparison(ISearchComparison::COMPARE_EQUAL, "mimetype", "text"),
|
||||
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('(mimetype eq "text" and (path eq "foo" or path eq "bar" or path eq "asd"))', $operator->__toString());
|
||||
|
||||
$this->optimizer->processOperator($operator);
|
||||
$this->simplifier->processOperator($operator);
|
||||
|
||||
$this->assertEquals('(mimetype eq "text" and path in ["foo","bar","asd"])', $operator->__toString());
|
||||
}
|
||||
}
|
||||
44
tests/lib/Files/Search/SearchIntegrationTest.php
Normal file
44
tests/lib/Files/Search/SearchIntegrationTest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Test\Files\Search;
|
||||
|
||||
use OC\Files\Search\SearchBinaryOperator;
|
||||
use OC\Files\Search\SearchComparison;
|
||||
use OC\Files\Search\SearchQuery;
|
||||
use OC\Files\Storage\Temporary;
|
||||
use OCP\Files\Search\ISearchBinaryOperator;
|
||||
use OCP\Files\Search\ISearchComparison;
|
||||
use Test\TestCase;
|
||||
|
||||
/**
|
||||
* @group DB
|
||||
*/
|
||||
class SearchIntegrationTest extends TestCase {
|
||||
private $cache;
|
||||
private $storage;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->storage = new Temporary([]);
|
||||
$this->cache = $this->storage->getCache();
|
||||
$this->storage->getScanner()->scan('');
|
||||
}
|
||||
|
||||
|
||||
public function testThousandAndOneFilters() {
|
||||
$id = $this->cache->put("file10", ['size' => 1, 'mtime' => 50, 'mimetype' => 'foo/folder']);
|
||||
|
||||
$comparisons = [];
|
||||
for($i = 1; $i <= 1001; $i++) {
|
||||
$comparisons[] = new SearchComparison(ISearchComparison::COMPARE_EQUAL, "name", "file$i");
|
||||
}
|
||||
$operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $comparisons);
|
||||
$query = new SearchQuery($operator, 10, 0, []);
|
||||
|
||||
$results = $this->cache->searchQuery($query);
|
||||
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertEquals($id, $results[0]->getId());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue