1
0

PathPrefixOptimizer.php 3.4 KB

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