MergeDistributiveOperations.php 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OC\Files\Search\QueryOptimizer;
  7. use OC\Files\Search\SearchBinaryOperator;
  8. use OC\Files\Search\SearchComparison;
  9. use OCP\Files\Search\ISearchBinaryOperator;
  10. use OCP\Files\Search\ISearchOperator;
  11. /**
  12. * Attempt to transform
  13. *
  14. * (A AND B) OR (A AND C) OR (A AND D AND E) into A AND (B OR C OR (D AND E))
  15. *
  16. * This is always valid because logical 'AND' and 'OR' are distributive[1].
  17. *
  18. * [1]: https://en.wikipedia.org/wiki/Distributive_property
  19. */
  20. class MergeDistributiveOperations extends ReplacingOptimizerStep {
  21. public function processOperator(ISearchOperator &$operator): bool {
  22. if ($operator instanceof SearchBinaryOperator) {
  23. // either 'AND' or 'OR'
  24. $topLevelType = $operator->getType();
  25. // split the arguments into groups that share a first argument
  26. $groups = $this->groupBinaryOperatorsByChild($operator->getArguments(), 0);
  27. $outerOperations = array_map(function (array $operators) use ($topLevelType) {
  28. // no common operations, no need to change anything
  29. if (count($operators) === 1) {
  30. return $operators[0];
  31. }
  32. // for groups with size >1 we know they are binary operators with at least 1 child
  33. /** @var ISearchBinaryOperator $firstArgument */
  34. $firstArgument = $operators[0];
  35. // we already checked that all arguments have the same type, so this type applies for all, either 'AND' or 'OR'
  36. $innerType = $firstArgument->getType();
  37. // the common operation we move out ('A' from the example)
  38. $extractedLeftHand = $firstArgument->getArguments()[0];
  39. // for each argument we remove the extracted operation to get the leftovers ('B', 'C' and '(D AND E)' in the example)
  40. // note that we leave them inside the "inner" binary operation for when the "inner" operation contained more than two parts
  41. // in the (common) case where the "inner" operation only has 1 item left it will be cleaned up in a follow up step
  42. $rightHandArguments = array_map(function (ISearchOperator $inner) {
  43. /** @var ISearchBinaryOperator $inner */
  44. $arguments = $inner->getArguments();
  45. array_shift($arguments);
  46. if (count($arguments) === 1) {
  47. return $arguments[0];
  48. }
  49. return new SearchBinaryOperator($inner->getType(), $arguments);
  50. }, $operators);
  51. // combine the extracted operation ('A') with the remaining bit ('(B OR C OR (D AND E))')
  52. // note that because of how distribution work, we use the "outer" type "inside" and the "inside" type "outside".
  53. $extractedRightHand = new SearchBinaryOperator($topLevelType, $rightHandArguments);
  54. return new SearchBinaryOperator(
  55. $innerType,
  56. [$extractedLeftHand, $extractedRightHand]
  57. );
  58. }, $groups);
  59. // combine all groups again
  60. $operator = new SearchBinaryOperator($topLevelType, $outerOperations);
  61. parent::processOperator($operator);
  62. return true;
  63. }
  64. return parent::processOperator($operator);
  65. }
  66. /**
  67. * Group a list of binary search operators that have a common argument
  68. *
  69. * Non-binary operators, or empty binary operators will each get their own 1-sized group
  70. *
  71. * @param ISearchOperator[] $operators
  72. * @return ISearchOperator[][]
  73. */
  74. private function groupBinaryOperatorsByChild(array $operators, int $index = 0): array {
  75. $result = [];
  76. foreach ($operators as $operator) {
  77. if ($operator instanceof ISearchBinaryOperator && count($operator->getArguments()) > 0) {
  78. /** @var SearchBinaryOperator|SearchComparison $child */
  79. $child = $operator->getArguments()[$index];
  80. $childKey = (string)$child;
  81. $result[$childKey][] = $operator;
  82. } else {
  83. $result[] = [$operator];
  84. }
  85. }
  86. return array_values($result);
  87. }
  88. }