From 66e10718c6109d0c02d603a5612fe4acc34c711f Mon Sep 17 00:00:00 2001 From: Robin Appelman <robin@icewind.nl> Date: Wed, 26 May 2021 15:50:35 +0200 Subject: split of query building bits from searchhelper Signed-off-by: Robin Appelman <robin@icewind.nl> --- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + lib/private/Files/Cache/QuerySearchHelper.php | 203 +------------------- lib/private/Files/Cache/SearchBuilder.php | 235 ++++++++++++++++++++++++ tests/lib/Files/Cache/QuerySearchHelperTest.php | 230 ----------------------- tests/lib/Files/Cache/SearchBuilderTest.php | 219 ++++++++++++++++++++++ 6 files changed, 463 insertions(+), 426 deletions(-) create mode 100644 lib/private/Files/Cache/SearchBuilder.php delete mode 100644 tests/lib/Files/Cache/QuerySearchHelperTest.php create mode 100644 tests/lib/Files/Cache/SearchBuilderTest.php diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index ef7085cd5ed..64db044d38e 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1061,6 +1061,7 @@ return array( 'OC\\Files\\Cache\\Propagator' => $baseDir . '/lib/private/Files/Cache/Propagator.php', 'OC\\Files\\Cache\\QuerySearchHelper' => $baseDir . '/lib/private/Files/Cache/QuerySearchHelper.php', 'OC\\Files\\Cache\\Scanner' => $baseDir . '/lib/private/Files/Cache/Scanner.php', + 'OC\\Files\\Cache\\SearchBuilder' => $baseDir . '/lib/private/Files/Cache/SearchBuilder.php', 'OC\\Files\\Cache\\Storage' => $baseDir . '/lib/private/Files/Cache/Storage.php', 'OC\\Files\\Cache\\StorageGlobal' => $baseDir . '/lib/private/Files/Cache/StorageGlobal.php', 'OC\\Files\\Cache\\Updater' => $baseDir . '/lib/private/Files/Cache/Updater.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index b89068acbeb..4f9314d088a 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1090,6 +1090,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Files\\Cache\\Propagator' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Propagator.php', 'OC\\Files\\Cache\\QuerySearchHelper' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/QuerySearchHelper.php', 'OC\\Files\\Cache\\Scanner' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Scanner.php', + 'OC\\Files\\Cache\\SearchBuilder' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/SearchBuilder.php', 'OC\\Files\\Cache\\Storage' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Storage.php', 'OC\\Files\\Cache\\StorageGlobal' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/StorageGlobal.php', 'OC\\Files\\Cache\\Updater' => __DIR__ . '/../../..' . '/lib/private/Files/Cache/Updater.php', diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index c16d660051e..963c964aa61 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -27,41 +27,15 @@ namespace OC\Files\Cache; use OC\Files\Search\SearchBinaryOperator; use OC\SystemConfig; -use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\ICache; use OCP\Files\Cache\ICacheEntry; use OCP\Files\IMimeTypeLoader; use OCP\Files\Search\ISearchBinaryOperator; -use OCP\Files\Search\ISearchComparison; -use OCP\Files\Search\ISearchOperator; -use OCP\Files\Search\ISearchOrder; use OCP\Files\Search\ISearchQuery; use OCP\IDBConnection; use OCP\ILogger; -/** - * Tools for transforming search queries into database queries - */ class QuerySearchHelper { - protected static $searchOperatorMap = [ - ISearchComparison::COMPARE_LIKE => 'iLike', - ISearchComparison::COMPARE_EQUAL => 'eq', - ISearchComparison::COMPARE_GREATER_THAN => 'gt', - ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte', - ISearchComparison::COMPARE_LESS_THAN => 'lt', - ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte', - ]; - - protected static $searchOperatorNegativeMap = [ - ISearchComparison::COMPARE_LIKE => 'notLike', - ISearchComparison::COMPARE_EQUAL => 'neq', - ISearchComparison::COMPARE_GREATER_THAN => 'lte', - ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt', - ISearchComparison::COMPARE_LESS_THAN => 'gte', - ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt', - ]; - - public const TAG_FAVORITE = '_$!<Favorite>!$_'; /** @var IMimeTypeLoader */ private $mimetypeLoader; @@ -71,6 +45,8 @@ class QuerySearchHelper { private $systemConfig; /** @var ILogger */ private $logger; + /** @var SearchBuilder */ + private $searchBuilder; public function __construct( IMimeTypeLoader $mimetypeLoader, @@ -82,172 +58,7 @@ class QuerySearchHelper { $this->connection = $connection; $this->systemConfig = $systemConfig; $this->logger = $logger; - } - - /** - * Whether or not the tag tables should be joined to complete the search - * - * @param ISearchOperator $operator - * @return boolean - */ - public function shouldJoinTags(ISearchOperator $operator) { - if ($operator instanceof ISearchBinaryOperator) { - return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) { - return $shouldJoin || $this->shouldJoinTags($operator); - }, false); - } elseif ($operator instanceof ISearchComparison) { - return $operator->getField() === 'tagname' || $operator->getField() === 'favorite'; - } - return false; - } - - /** - * @param IQueryBuilder $builder - * @param ISearchOperator $operator - */ - public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) { - return array_filter(array_map(function ($operator) use ($builder) { - return $this->searchOperatorToDBExpr($builder, $operator); - }, $operators)); - } - - public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) { - $expr = $builder->expr(); - if ($operator instanceof ISearchBinaryOperator) { - if (count($operator->getArguments()) === 0) { - return null; - } - - switch ($operator->getType()) { - case ISearchBinaryOperator::OPERATOR_NOT: - $negativeOperator = $operator->getArguments()[0]; - if ($negativeOperator instanceof ISearchComparison) { - return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap); - } else { - throw new \InvalidArgumentException('Binary operators inside "not" is not supported'); - } - // no break - case ISearchBinaryOperator::OPERATOR_AND: - return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments())); - case ISearchBinaryOperator::OPERATOR_OR: - return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments())); - default: - throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType()); - } - } elseif ($operator instanceof ISearchComparison) { - return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap); - } else { - throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator)); - } - } - - private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) { - $this->validateComparison($comparison); - - [$field, $value, $type] = $this->getOperatorFieldAndValue($comparison); - if (isset($operatorMap[$type])) { - $queryOperator = $operatorMap[$type]; - return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value)); - } else { - throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType()); - } - } - - private function getOperatorFieldAndValue(ISearchComparison $operator) { - $field = $operator->getField(); - $value = $operator->getValue(); - $type = $operator->getType(); - if ($field === 'mimetype') { - if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) { - $value = (int)$this->mimetypeLoader->getId($value); - } elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) { - // transform "mimetype='foo/%'" to "mimepart='foo'" - if (preg_match('|(.+)/%|', $value, $matches)) { - $field = 'mimepart'; - $value = (int)$this->mimetypeLoader->getId($matches[1]); - $type = ISearchComparison::COMPARE_EQUAL; - } elseif (strpos($value, '%') !== false) { - throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported'); - } else { - $field = 'mimetype'; - $value = (int)$this->mimetypeLoader->getId($value); - $type = ISearchComparison::COMPARE_EQUAL; - } - } - } elseif ($field === 'favorite') { - $field = 'tag.category'; - $value = self::TAG_FAVORITE; - } elseif ($field === 'tagname') { - $field = 'tag.category'; - } elseif ($field === 'fileid') { - $field = 'file.fileid'; - } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL) { - $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', - 'favorite' => 'boolean', - 'fileid' => 'integer', - 'storage' => 'integer', - ]; - $comparisons = [ - 'mimetype' => ['eq', 'like'], - 'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'], - 'name' => ['eq', 'like'], - 'path' => ['eq', 'like'], - 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'], - 'tagname' => ['eq', 'like'], - 'favorite' => ['eq'], - 'fileid' => ['eq'], - 'storage' => ['eq'], - ]; - - if (!isset($types[$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()); - } - 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) { - if ($value instanceof \DateTime) { - $value = $value->getTimestamp(); - } - if (is_numeric($value)) { - $type = IQueryBuilder::PARAM_INT; - } else { - $type = IQueryBuilder::PARAM_STR; - } - return $builder->createNamedParameter($value, $type); - } - - /** - * @param IQueryBuilder $query - * @param ISearchOrder[] $orders - */ - public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) { - foreach ($orders as $order) { - $field = $order->getField(); - if ($field === 'fileid') { - $field = 'file.fileid'; - } - $query->addOrderBy($field, $order->getDirection()); - } + $this->searchBuilder = new SearchBuilder($this->mimetypeLoader); } protected function getQueryBuilder() { @@ -288,7 +99,7 @@ class QuerySearchHelper { $query = $builder->selectFileCache('file'); - if ($this->shouldJoinTags($searchQuery->getSearchOperation())) { + if ($this->searchBuilder->shouldJoinTags($searchQuery->getSearchOperation())) { $user = $searchQuery->getUser(); if ($user === null) { throw new \InvalidArgumentException("Searching by tag requires the user to be set in the query"); @@ -303,7 +114,7 @@ class QuerySearchHelper { ->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($user->getUID()))); } - $searchExpr = $this->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation()); + $searchExpr = $this->searchBuilder->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation()); if ($searchExpr) { $query->andWhere($searchExpr); } @@ -311,9 +122,9 @@ class QuerySearchHelper { $storageFilters = array_values(array_map(function (ICache $cache) { return $cache->getQueryFilterForStorage(); }, $caches)); - $query->andWhere($this->searchOperatorToDBExpr($builder, new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters))); + $query->andWhere($this->searchBuilder->searchOperatorToDBExpr($builder, new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, $storageFilters))); - $this->addSearchOrdersToQuery($query, $searchQuery->getOrder()); + $this->searchBuilder->addSearchOrdersToQuery($query, $searchQuery->getOrder()); if ($searchQuery->getLimit()) { $query->setMaxResults($searchQuery->getLimit()); diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php new file mode 100644 index 00000000000..7e1fbe08492 --- /dev/null +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -0,0 +1,235 @@ +<?php +/** + * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> + * + * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * @author Robin Appelman <robin@icewind.nl> + * @author Roeland Jago Douma <roeland@famdouma.nl> + * @author Tobias Kaminsky <tobias@kaminsky.me> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Files\Cache; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\IMimeTypeLoader; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Search\ISearchOrder; + +/** + * Tools for transforming search queries into database queries + */ +class SearchBuilder { + protected static $searchOperatorMap = [ + ISearchComparison::COMPARE_LIKE => 'iLike', + ISearchComparison::COMPARE_EQUAL => 'eq', + ISearchComparison::COMPARE_GREATER_THAN => 'gt', + ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte', + ISearchComparison::COMPARE_LESS_THAN => 'lt', + ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte', + ]; + + protected static $searchOperatorNegativeMap = [ + ISearchComparison::COMPARE_LIKE => 'notLike', + ISearchComparison::COMPARE_EQUAL => 'neq', + ISearchComparison::COMPARE_GREATER_THAN => 'lte', + ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt', + ISearchComparison::COMPARE_LESS_THAN => 'gte', + ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lt', + ]; + + public const TAG_FAVORITE = '_$!<Favorite>!$_'; + + /** @var IMimeTypeLoader */ + private $mimetypeLoader; + + public function __construct( + IMimeTypeLoader $mimetypeLoader + ) { + $this->mimetypeLoader = $mimetypeLoader; + } + + /** + * Whether or not the tag tables should be joined to complete the search + * + * @param ISearchOperator $operator + * @return boolean + */ + public function shouldJoinTags(ISearchOperator $operator) { + if ($operator instanceof ISearchBinaryOperator) { + return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) { + return $shouldJoin || $this->shouldJoinTags($operator); + }, false); + } elseif ($operator instanceof ISearchComparison) { + return $operator->getField() === 'tagname' || $operator->getField() === 'favorite'; + } + return false; + } + + /** + * @param IQueryBuilder $builder + * @param ISearchOperator $operator + */ + public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) { + return array_filter(array_map(function ($operator) use ($builder) { + return $this->searchOperatorToDBExpr($builder, $operator); + }, $operators)); + } + + public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) { + $expr = $builder->expr(); + if ($operator instanceof ISearchBinaryOperator) { + if (count($operator->getArguments()) === 0) { + return null; + } + + switch ($operator->getType()) { + case ISearchBinaryOperator::OPERATOR_NOT: + $negativeOperator = $operator->getArguments()[0]; + if ($negativeOperator instanceof ISearchComparison) { + return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap); + } else { + throw new \InvalidArgumentException('Binary operators inside "not" is not supported'); + } + // no break + case ISearchBinaryOperator::OPERATOR_AND: + return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments())); + case ISearchBinaryOperator::OPERATOR_OR: + return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments())); + default: + throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType()); + } + } elseif ($operator instanceof ISearchComparison) { + return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap); + } else { + throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator)); + } + } + + private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) { + $this->validateComparison($comparison); + + [$field, $value, $type] = $this->getOperatorFieldAndValue($comparison); + if (isset($operatorMap[$type])) { + $queryOperator = $operatorMap[$type]; + return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value)); + } else { + throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType()); + } + } + + private function getOperatorFieldAndValue(ISearchComparison $operator) { + $field = $operator->getField(); + $value = $operator->getValue(); + $type = $operator->getType(); + if ($field === 'mimetype') { + $value = (string)$value; + if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) { + $value = (int)$this->mimetypeLoader->getId($value); + } elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) { + // transform "mimetype='foo/%'" to "mimepart='foo'" + if (preg_match('|(.+)/%|', $value, $matches)) { + $field = 'mimepart'; + $value = (int)$this->mimetypeLoader->getId($matches[1]); + $type = ISearchComparison::COMPARE_EQUAL; + } elseif (strpos($value, '%') !== false) { + throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported'); + } else { + $field = 'mimetype'; + $value = (int)$this->mimetypeLoader->getId($value); + $type = ISearchComparison::COMPARE_EQUAL; + } + } + } elseif ($field === 'favorite') { + $field = 'tag.category'; + $value = self::TAG_FAVORITE; + } elseif ($field === 'tagname') { + $field = 'tag.category'; + } elseif ($field === 'fileid') { + $field = 'file.fileid'; + } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL) { + $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', + 'favorite' => 'boolean', + 'fileid' => 'integer', + 'storage' => 'integer', + ]; + $comparisons = [ + 'mimetype' => ['eq', 'like'], + 'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'], + 'name' => ['eq', 'like'], + 'path' => ['eq', 'like'], + 'size' => ['eq', 'gt', 'lt', 'gte', 'lte'], + 'tagname' => ['eq', 'like'], + 'favorite' => ['eq'], + 'fileid' => ['eq'], + 'storage' => ['eq'], + ]; + + if (!isset($types[$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()); + } + 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) { + if ($value instanceof \DateTime) { + $value = $value->getTimestamp(); + } + if (is_numeric($value)) { + $type = IQueryBuilder::PARAM_INT; + } else { + $type = IQueryBuilder::PARAM_STR; + } + return $builder->createNamedParameter($value, $type); + } + + /** + * @param IQueryBuilder $query + * @param ISearchOrder[] $orders + */ + public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) { + foreach ($orders as $order) { + $field = $order->getField(); + if ($field === 'fileid') { + $field = 'file.fileid'; + } + $query->addOrderBy($field, $order->getDirection()); + } + } +} diff --git a/tests/lib/Files/Cache/QuerySearchHelperTest.php b/tests/lib/Files/Cache/QuerySearchHelperTest.php deleted file mode 100644 index 46aa5a55493..00000000000 --- a/tests/lib/Files/Cache/QuerySearchHelperTest.php +++ /dev/null @@ -1,230 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -namespace Test\Files\Cache; - -use OC\DB\QueryBuilder\Literal; -use OC\Files\Cache\QuerySearchHelper; -use OC\Files\Search\SearchBinaryOperator; -use OC\Files\Search\SearchComparison; -use OC\SystemConfig; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\Files\IMimeTypeLoader; -use OCP\Files\Search\ISearchBinaryOperator; -use OCP\Files\Search\ISearchComparison; -use OCP\Files\Search\ISearchOperator; -use OCP\IDBConnection; -use OCP\ILogger; -use Test\TestCase; - -/** - * @group DB - */ -class QuerySearchHelperTest extends TestCase { - /** @var IQueryBuilder */ - private $builder; - - /** @var IMimeTypeLoader|\PHPUnit\Framework\MockObject\MockObject */ - private $mimetypeLoader; - - /** @var QuerySearchHelper */ - private $querySearchHelper; - - /** @var integer */ - private $numericStorageId; - - protected function setUp(): void { - parent::setUp(); - $this->builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); - $this->mimetypeLoader = $this->createMock(IMimeTypeLoader::class); - - $this->mimetypeLoader->expects($this->any()) - ->method('getId') - ->willReturnMap([ - ['text', 1], - ['text/plain', 2], - ['text/xml', 3], - ['image/jpg', 4], - ['image/png', 5], - ['image', 6], - ]); - - $this->mimetypeLoader->expects($this->any()) - ->method('getMimetypeById') - ->willReturnMap([ - [1, 'text'], - [2, 'text/plain'], - [3, 'text/xml'], - [4, 'image/jpg'], - [5, 'image/png'], - [6, 'image'] - ]); - - $systemConfig = $this->createMock(SystemConfig::class); - $logger = $this->createMock(ILogger::class); - - $this->querySearchHelper = new QuerySearchHelper( - $this->mimetypeLoader, - \OC::$server->get(IDBConnection::class), - $systemConfig, - $logger - ); - $this->numericStorageId = 10000; - - $this->builder->select(['fileid']) - ->from('filecache') - ->where($this->builder->expr()->eq('storage', new Literal($this->numericStorageId))); - } - - protected function tearDown(): void { - parent::tearDown(); - - $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); - - $builder->delete('filecache') - ->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->numericStorageId, IQueryBuilder::PARAM_INT))); - - $builder->execute(); - } - - private function addCacheEntry(array $data) { - $data['storage'] = $this->numericStorageId; - $data['etag'] = 'unimportant'; - $data['storage_mtime'] = $data['mtime']; - if (!isset($data['path'])) { - $data['path'] = 'random/' . $this->getUniqueID(); - } - $data['path_hash'] = md5($data['path']); - if (!isset($data['mtime'])) { - $data['mtime'] = 100; - } - if (!isset($data['size'])) { - $data['size'] = 100; - } - $data['name'] = basename($data['path']); - $data['parent'] = -1; - if (isset($data['mimetype'])) { - [$mimepart,] = explode('/', $data['mimetype']); - $data['mimepart'] = $this->mimetypeLoader->getId($mimepart); - $data['mimetype'] = $this->mimetypeLoader->getId($data['mimetype']); - } else { - $data['mimepart'] = 1; - $data['mimetype'] = 1; - } - - $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); - - $values = []; - foreach ($data as $key => $value) { - $values[$key] = $builder->createNamedParameter($value); - } - - $builder->insert('filecache') - ->values($values) - ->execute(); - - return $builder->getLastInsertId(); - } - - private function search(ISearchOperator $operator) { - $dbOperator = $this->querySearchHelper->searchOperatorToDBExpr($this->builder, $operator); - $this->builder->andWhere($dbOperator); - - $result = $this->builder->execute(); - $rows = $result->fetchAll(\PDO::FETCH_COLUMN); - $result->closeCursor(); - - return $rows; - } - - public function comparisonProvider() { - return [ - [new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), [1]], - [new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [0]], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 125), []], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), [0, 1]], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foobar'), [0]], - [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), [0, 1]], - [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpg'), [0]], - [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'image/%'), [0, 1]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), - new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125) - ]), [0]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), - new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'text/%') - ]), []], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 100), - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), - ]), [0, 1]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), - ]), [0]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), - ]), [0]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), - ]), [1]], - [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ - new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%bar'), - ]), [1]], - - ]; - } - - /** - * @dataProvider comparisonProvider - * - * @param ISearchOperator $operator - * @param array $fileIds - */ - public function testComparison(ISearchOperator $operator, array $fileIds) { - $fileId = []; - $fileId[] = $this->addCacheEntry([ - 'path' => 'foobar', - 'mtime' => 100, - 'size' => 50, - 'mimetype' => 'image/jpg' - ]); - - $fileId[] = $this->addCacheEntry([ - 'path' => 'fooasd', - 'mtime' => 150, - 'size' => 50, - 'mimetype' => 'image/png' - ]); - - $fileIds = array_map(function ($i) use ($fileId) { - return $fileId[$i]; - }, $fileIds); - - $results = $this->search($operator); - - sort($fileIds); - sort($results); - - $this->assertEquals($fileIds, $results); - } -} diff --git a/tests/lib/Files/Cache/SearchBuilderTest.php b/tests/lib/Files/Cache/SearchBuilderTest.php new file mode 100644 index 00000000000..82c4dbaa27f --- /dev/null +++ b/tests/lib/Files/Cache/SearchBuilderTest.php @@ -0,0 +1,219 @@ +<?php +/** + * @copyright Copyright (c) 2017 Robin Appelman <robin@icewind.nl> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Test\Files\Cache; + +use OC\DB\QueryBuilder\Literal; +use OC\Files\Cache\SearchBuilder; +use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\IMimeTypeLoader; +use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; +use Test\TestCase; + +/** + * @group DB + */ +class SearchBuilderTest extends TestCase { + /** @var IQueryBuilder */ + private $builder; + + /** @var IMimeTypeLoader|\PHPUnit\Framework\MockObject\MockObject */ + private $mimetypeLoader; + + /** @var SearchBuilder */ + private $searchBuilder; + + /** @var integer */ + private $numericStorageId; + + protected function setUp(): void { + parent::setUp(); + $this->builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + $this->mimetypeLoader = $this->createMock(IMimeTypeLoader::class); + + $this->mimetypeLoader->expects($this->any()) + ->method('getId') + ->willReturnMap([ + ['text', 1], + ['text/plain', 2], + ['text/xml', 3], + ['image/jpg', 4], + ['image/png', 5], + ['image', 6], + ]); + + $this->mimetypeLoader->expects($this->any()) + ->method('getMimetypeById') + ->willReturnMap([ + [1, 'text'], + [2, 'text/plain'], + [3, 'text/xml'], + [4, 'image/jpg'], + [5, 'image/png'], + [6, 'image'] + ]); + + $this->searchBuilder = new SearchBuilder($this->mimetypeLoader); + $this->numericStorageId = 10000; + + $this->builder->select(['fileid']) + ->from('filecache') + ->where($this->builder->expr()->eq('storage', new Literal($this->numericStorageId))); + } + + protected function tearDown(): void { + parent::tearDown(); + + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + + $builder->delete('filecache') + ->where($builder->expr()->eq('storage', $builder->createNamedParameter($this->numericStorageId, IQueryBuilder::PARAM_INT))); + + $builder->execute(); + } + + private function addCacheEntry(array $data) { + $data['storage'] = $this->numericStorageId; + $data['etag'] = 'unimportant'; + $data['storage_mtime'] = $data['mtime']; + if (!isset($data['path'])) { + $data['path'] = 'random/' . $this->getUniqueID(); + } + $data['path_hash'] = md5($data['path']); + if (!isset($data['mtime'])) { + $data['mtime'] = 100; + } + if (!isset($data['size'])) { + $data['size'] = 100; + } + $data['name'] = basename($data['path']); + $data['parent'] = -1; + if (isset($data['mimetype'])) { + [$mimepart,] = explode('/', $data['mimetype']); + $data['mimepart'] = $this->mimetypeLoader->getId($mimepart); + $data['mimetype'] = $this->mimetypeLoader->getId($data['mimetype']); + } else { + $data['mimepart'] = 1; + $data['mimetype'] = 1; + } + + $builder = \OC::$server->getDatabaseConnection()->getQueryBuilder(); + + $values = []; + foreach ($data as $key => $value) { + $values[$key] = $builder->createNamedParameter($value); + } + + $builder->insert('filecache') + ->values($values) + ->execute(); + + return $builder->getLastInsertId(); + } + + private function search(ISearchOperator $operator) { + $dbOperator = $this->searchBuilder->searchOperatorToDBExpr($this->builder, $operator); + $this->builder->andWhere($dbOperator); + + $result = $this->builder->execute(); + $rows = $result->fetchAll(\PDO::FETCH_COLUMN); + $result->closeCursor(); + + return $rows; + } + + public function comparisonProvider() { + return [ + [new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), [1]], + [new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), [0]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 125), []], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), [0, 1]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'name', 'foobar'), [0]], + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', 'foo%'), [0, 1]], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', 'image/jpg'), [0]], + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'image/%'), [0, 1]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), + new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125) + ]), [0]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'size', 50), + new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), + new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', 'text/%') + ]), []], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 100), + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), + ]), [0, 1]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mtime', 150), + ]), [0]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_GREATER_THAN, 'mtime', 125), + ]), [0]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_LESS_THAN, 'mtime', 125), + ]), [1]], + [new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ + new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%bar'), + ]), [1]], + + ]; + } + + /** + * @dataProvider comparisonProvider + * + * @param ISearchOperator $operator + * @param array $fileIds + */ + public function testComparison(ISearchOperator $operator, array $fileIds) { + $fileId = []; + $fileId[] = $this->addCacheEntry([ + 'path' => 'foobar', + 'mtime' => 100, + 'size' => 50, + 'mimetype' => 'image/jpg' + ]); + + $fileId[] = $this->addCacheEntry([ + 'path' => 'fooasd', + 'mtime' => 150, + 'size' => 50, + 'mimetype' => 'image/png' + ]); + + $fileIds = array_map(function ($i) use ($fileId) { + return $fileId[$i]; + }, $fileIds); + + $results = $this->search($operator); + + sort($fileIds); + sort($results); + + $this->assertEquals($fileIds, $results); + } +} -- cgit v1.2.3