Passed
Push — master ( dbc0d6...5024f2 )
by Robin
17:08 queued 12s
created

PathPrefixOptimizer::processOperator()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 3
nc 2
nop 1
dl 0
loc 6
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * @copyright Copyright (c) 2021 Robin Appelman <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
24
namespace OC\Files\Search\QueryOptimizer;
25
26
use OC\Files\Search\SearchComparison;
27
use OCP\Files\Search\ISearchBinaryOperator;
28
use OCP\Files\Search\ISearchComparison;
29
use OCP\Files\Search\ISearchOperator;
30
31
class PathPrefixOptimizer extends QueryOptimizerStep {
32
	private bool $useHashEq = true;
33
34
	public function inspectOperator(ISearchOperator $operator): void {
35
		// normally any `path = "$path"` search filter would be generated as an `path_hash = md5($path)` sql query
36
		// since the `path_hash` sql column usually provides much faster querying that selecting on the `path` sql column
37
		//
38
		// however, if we're already doing a filter on the `path` column in the form of `path LIKE "$prefix/%"`
39
		// generating a `path = "$prefix"` sql query lets the database handle use the same column for both expressions and potentially use the same index
40
		//
41
		// If there is any operator in the query that matches this pattern, we change all `path = "$path"` instances to not the `path_hash` equality,
42
		// otherwise mariadb has a tendency of ignoring the path_prefix index
43
		if ($this->useHashEq && $this->isPathPrefixOperator($operator)) {
44
			$this->useHashEq = false;
45
		}
46
47
		parent::inspectOperator($operator);
48
	}
49
50
	public function processOperator(ISearchOperator &$operator) {
51
		if (!$this->useHashEq && $operator instanceof ISearchComparison && $operator->getField() === 'path' && $operator->getType() === ISearchComparison::COMPARE_EQUAL) {
52
			$operator->setQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, false);
53
		}
54
55
		parent::processOperator($operator);
56
	}
57
58
	private function isPathPrefixOperator(ISearchOperator $operator): bool {
59
		if ($operator instanceof ISearchBinaryOperator && $operator->getType() === ISearchBinaryOperator::OPERATOR_OR && count($operator->getArguments()) == 2) {
60
			$a = $operator->getArguments()[0];
61
			$b = $operator->getArguments()[1];
62
			if ($this->operatorPairIsPathPrefix($a, $b) || $this->operatorPairIsPathPrefix($b, $a)) {
63
				return true;
64
			}
65
		}
66
		return false;
67
	}
68
69
	private function operatorPairIsPathPrefix(ISearchOperator $like, ISearchOperator $equal): bool {
70
		return (
71
			$like instanceof ISearchComparison && $equal instanceof ISearchComparison &&
72
			$like->getField() === 'path' && $equal->getField() === 'path' &&
73
			$like->getType() === ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE && $equal->getType() === ISearchComparison::COMPARE_EQUAL
74
			&& $like->getValue() === SearchComparison::escapeLikeParameter($equal->getValue()) . '/%'
75
		);
76
	}
77
}
78