Skip to content

Commit

Permalink
optimize query pattern used by storage filter
Browse files Browse the repository at this point in the history
Signed-off-by: Robin Appelman <robin@icewind.nl>
  • Loading branch information
icewind1991 committed Sep 22, 2023
1 parent ef87ff1 commit 4e08186
Show file tree
Hide file tree
Showing 15 changed files with 723 additions and 39 deletions.
111 changes: 81 additions & 30 deletions lib/private/Files/Cache/SearchBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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 = '_$!<Favorite>!$_';
Expand Down Expand Up @@ -129,21 +155,47 @@ 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 = $field;
$values = [];
foreach ($value as $arrayValue) {
/** @var string|integer|\DateTime $arrayValue */
[$arrayField, $arrayValue] = $this->getOperatorFieldAndValueInner($field, $arrayValue, ISearchComparison::COMPARE_EQUAL, $pathEqHash);
$resultField = $arrayField;
$values[] = $arrayValue;
}
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';
Expand All @@ -168,59 +220,58 @@ 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);
}
return [$field, $value, $type];
}

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);
}
Expand Down
30 changes: 30 additions & 0 deletions lib/private/Files/Search/QueryOptimizer/FlattenNestedBool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?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);
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?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) into A AND (B OR C)
*/
class MergeDistributiveOperations extends ReplacingOptimizerStep {
public function processOperator(ISearchOperator &$operator): bool {
if (
$operator instanceof SearchBinaryOperator &&
$this->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 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): array {
$result = [];
foreach ($operators as $operator) {
/** @var SearchBinaryOperator|SearchComparison $child */
$child = $operator->getArguments()[0];
;
$childKey = (string) $child;
$result[$childKey][] = $operator;
}
return array_values($result);
}
}
Loading

0 comments on commit 4e08186

Please sign in to comment.