PathPrefixOptimizer.php 3.3 KB

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