From dcaa28dcd15c52bfb2728df12b4158cbcaf94030 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 9 Sep 2026 00:01:31 +0200 Subject: [PATCH 1/4] feat: add `regexSubstring` to query function builder Signed-off-by: Robin Appelman --- .../FunctionBuilder/FunctionBuilder.php | 5 ++++ .../FunctionBuilder/PgSqlFunctionBuilder.php | 5 ++++ lib/private/DB/SQLiteSessionInit.php | 20 +++++++++++++ .../DB/QueryBuilder/IFunctionBuilder.php | 11 ++++++++ .../DB/QueryBuilder/FunctionBuilderTest.php | 28 +++++++++++++++++++ 5 files changed, 69 insertions(+) diff --git a/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php b/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php index c0598dd134a5d..e5727df324060 100644 --- a/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php +++ b/lib/private/DB/QueryBuilder/FunctionBuilder/FunctionBuilder.php @@ -55,6 +55,11 @@ public function substring($input, $start, $length = null): IQueryFunction { } } + #[\Override] + public function regexSubstring($input, $pattern): IQueryFunction { + return new QueryFunction('REGEXP_SUBSTR(' . $this->helper->quoteColumnName($input) . ', ' . $this->helper->quoteColumnName($pattern) . ')'); + } + #[\Override] public function sum($field): IQueryFunction { return new QueryFunction('SUM(' . $this->helper->quoteColumnName($field) . ')'); diff --git a/lib/private/DB/QueryBuilder/FunctionBuilder/PgSqlFunctionBuilder.php b/lib/private/DB/QueryBuilder/FunctionBuilder/PgSqlFunctionBuilder.php index 7bc3433b4be6c..8fbbd09af0d99 100644 --- a/lib/private/DB/QueryBuilder/FunctionBuilder/PgSqlFunctionBuilder.php +++ b/lib/private/DB/QueryBuilder/FunctionBuilder/PgSqlFunctionBuilder.php @@ -33,4 +33,9 @@ public function groupConcat($expr, ?string $separator = ','): IQueryFunction { $separator = $this->connection->quote($separator); return new QueryFunction('string_agg(' . $castedExpression . ', ' . $separator . ')'); } + + #[\Override] + public function regexSubstring($input, $pattern): IQueryFunction { + return new QueryFunction('substring(' . $this->helper->quoteColumnName($input) . ' from ' . $this->helper->quoteColumnName($pattern) . ')'); + } } diff --git a/lib/private/DB/SQLiteSessionInit.php b/lib/private/DB/SQLiteSessionInit.php index 8fd15cd90c375..0497df79e1afa 100644 --- a/lib/private/DB/SQLiteSessionInit.php +++ b/lib/private/DB/SQLiteSessionInit.php @@ -30,10 +30,30 @@ public function postConnect(ConnectionEventArgs $args): void { /** @var \Doctrine\DBAL\Driver\PDO\Connection $connection */ $connection = $args->getConnection()->getWrappedConnection(); $pdo = $connection->getWrappedConnection(); + + $regexSubstr = function ($string, $pattern): ?string { + if (is_null($string) || is_null($pattern)) { + return null; + } else { + $string = (string)$string; + $pattern = str_replace('#', '\#', (string)$pattern); + } + + $matches = []; + $result = preg_match("#$pattern#", $string, $matches); + if ($result === 0 || $result === false) { + return null; + } else { + return $matches[0]; + } + }; + if (PHP_VERSION_ID >= 80500 && method_exists($pdo, 'createFunction')) { $pdo->createFunction('md5', 'md5', 1); + $pdo->createFunction('regexp_substr', $regexSubstr, 2); } else { $pdo->sqliteCreateFunction('md5', 'md5', 1); + $pdo->sqliteCreateFunction('regexp_substr', $regexSubstr, 2); } } diff --git a/lib/public/DB/QueryBuilder/IFunctionBuilder.php b/lib/public/DB/QueryBuilder/IFunctionBuilder.php index 31edd96dc6e88..1cabd356defdc 100644 --- a/lib/public/DB/QueryBuilder/IFunctionBuilder.php +++ b/lib/public/DB/QueryBuilder/IFunctionBuilder.php @@ -63,6 +63,17 @@ public function groupConcat($expr, ?string $separator = ','): IQueryFunction; */ public function substring($input, $start, $length = null): IQueryFunction; + /** + * Takes a substring from the input string using a regex pattern + * + * @param string|ILiteral|IParameter|IQueryFunction $input The input string + * @param string|ILiteral|IParameter|IQueryFunction $pattern The pattern to match and return + * + * @return IQueryFunction + * @since 36.0.0 + */ + public function regexSubstring($input, $pattern): IQueryFunction; + /** * Takes the sum of all rows in a column * diff --git a/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php b/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php index f52b4f3bda1ff..8f9dcc1ed2872 100644 --- a/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php +++ b/tests/lib/DB/QueryBuilder/FunctionBuilderTest.php @@ -11,6 +11,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\Server; +use PHPUnit\Framework\Attributes\DataProvider; use Test\TestCase; /** @@ -487,4 +488,31 @@ public function testLeast(): void { $result->closeCursor(); $this->assertEquals(1, $row); } + + public static function regexSubstringData(): array { + return [ + ['foobar', 'foo', 'foo'], + ['foobar', 'b.+$', 'bar'], + ['foo#bar', 'ba.+r$', null], + ['foo#bar', 'o#.', 'o#b'], + ['a/file/path', '[^/]+$', 'path'], + ]; + } + + #[DataProvider('regexSubstringData')] + public function testRegexSubstring(string $input, string $pattern, ?string $expected): void { + $query = $this->connection->getQueryBuilder(); + + $query->select($query->func()->regexSubstring( + $query->createNamedParameter($input), + $query->createNamedParameter($pattern), + )); + $query->from('appconfig') + ->setMaxResults(1); + + $result = $query->executeQuery(); + $row = $result->fetchOne(); + $result->closeCursor(); + $this->assertEquals($expected, $row); + } } From 7a2ec4b449772ae128294a4432c8a7088a39f37c Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 9 Sep 2026 00:02:22 +0200 Subject: [PATCH 2/4] fix: also include mountpoint name in search filter Signed-off-by: Robin Appelman --- apps/dav/lib/Files/FileSearchBackend.php | 17 ++++++++++++++++ .../unit/Files/FileSearchBackendTest.php | 18 ++++++++++++----- lib/private/DB/SQLiteSessionInit.php | 6 +++--- lib/private/Files/Cache/QuerySearchHelper.php | 11 ++++++++++ lib/private/Files/Cache/SearchBuilder.php | 20 ++++++++++++------- lib/private/Files/Node/Folder.php | 20 ++++++++++++++++++- tests/lib/Files/Node/FolderTest.php | 10 ++++++++++ 7 files changed, 86 insertions(+), 16 deletions(-) diff --git a/apps/dav/lib/Files/FileSearchBackend.php b/apps/dav/lib/Files/FileSearchBackend.php index 6b6e0e50a20ed..7c6a50fcdc32c 100644 --- a/apps/dav/lib/Files/FileSearchBackend.php +++ b/apps/dav/lib/Files/FileSearchBackend.php @@ -458,6 +458,23 @@ private function transformSearchOperation(Operator $operator) { throw new \InvalidArgumentException('Invalid property value for ' . $property->name, previous: $e); } + if ($field === 'name') { + return new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ + new SearchComparison( + $trimmedType, + $field, + $castedValue, + $extra ?? '' + ), + new SearchComparison( + $trimmedType, + 'mount_point_name', + $castedValue, + $extra ?? '' + ) + ]); + } + return new SearchComparison( $trimmedType, $field, diff --git a/apps/dav/tests/unit/Files/FileSearchBackendTest.php b/apps/dav/tests/unit/Files/FileSearchBackendTest.php index 5d57ca4bd035e..19d97e4179295 100644 --- a/apps/dav/tests/unit/Files/FileSearchBackendTest.php +++ b/apps/dav/tests/unit/Files/FileSearchBackendTest.php @@ -8,6 +8,7 @@ namespace OCA\DAV\Tests\unit\Files; +use OC\Files\Search\SearchBinaryOperator; use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchQuery; use OC\Files\View; @@ -92,11 +93,18 @@ public function testSearchFilename(): void { $this->searchFolder->expects($this->once()) ->method('search') ->with(new SearchQuery( - new SearchComparison( - ISearchComparison::COMPARE_EQUAL, - 'name', - 'foo' - ), + new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'name', + 'foo' + ), + new SearchComparison( + ISearchComparison::COMPARE_EQUAL, + 'mount_point_name', + 'foo' + ), + ]), 100, 0, [], diff --git a/lib/private/DB/SQLiteSessionInit.php b/lib/private/DB/SQLiteSessionInit.php index 0497df79e1afa..9a2e0ed4633be 100644 --- a/lib/private/DB/SQLiteSessionInit.php +++ b/lib/private/DB/SQLiteSessionInit.php @@ -31,9 +31,9 @@ public function postConnect(ConnectionEventArgs $args): void { $connection = $args->getConnection()->getWrappedConnection(); $pdo = $connection->getWrappedConnection(); - $regexSubstr = function ($string, $pattern): ?string { + $regexSubstr = function ($string, $pattern): string { if (is_null($string) || is_null($pattern)) { - return null; + return ''; } else { $string = (string)$string; $pattern = str_replace('#', '\#', (string)$pattern); @@ -42,7 +42,7 @@ public function postConnect(ConnectionEventArgs $args): void { $matches = []; $result = preg_match("#$pattern#", $string, $matches); if ($result === 0 || $result === false) { - return null; + return ''; } else { return $matches[0]; } diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 9287dcb005593..1accbf473fcf5 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -118,6 +118,14 @@ protected function equipQueryForDavTags(CacheQueryBuilder $query, IUser $user): )); } + protected function equipQueryForMounts(CacheQueryBuilder $query, IUser $user): void { + $query + ->leftJoin('file', 'mounts', 'm', $query->expr()->andX( + $query->expr()->eq('m.root_id', 'file.fileid'), + $query->expr()->eq('m.user_id', $query->createNamedParameter($user->getUID())) + )); + } + protected function equipQueryForShares(CacheQueryBuilder $query): void { $query->join('file', 'share', 's', $query->expr()->eq('file.fileid', 's.file_source')); } @@ -172,6 +180,9 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array if (in_array('owner', $requestedFields, true) || in_array('share_with', $requestedFields, true) || in_array('share_type', $requestedFields, true)) { $this->equipQueryForShares($query); } + if (in_array('mount_point_name', $requestedFields, true)) { + $this->equipQueryForMounts($query, $this->requireUser($searchQuery)); + } $metadataQuery = $query->selectMetadata(); diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index 04763ae804408..0672813705f46 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -8,6 +8,7 @@ namespace OC\Files\Cache; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\QueryBuilder\IQueryFunction; use OCP\Files\IMimeTypeLoader; use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchComparison; @@ -66,6 +67,7 @@ class SearchBuilder { 'owner' => 'string', 'creation_time' => 'integer', 'upload_time' => 'integer', + 'mount_point_name' => 'string', ]; /** @var array */ @@ -162,7 +164,7 @@ private function searchComparisonToDBExpr( if ($comparison->getExtra()) { [$field, $value, $type, $paramType] = $this->getExtraOperatorField($comparison, $metadataQuery); } else { - [$field, $value, $type, $paramType] = $this->getOperatorFieldAndValue($comparison); + [$field, $value, $type, $paramType] = $this->getOperatorFieldAndValue($builder, $comparison); } if (isset($operatorMap[$type])) { @@ -175,31 +177,31 @@ private function searchComparisonToDBExpr( /** * @param ISearchComparison $operator - * @return list{string, ParamValue, string, string} + * @return list{string|IQueryFunction, ParamValue, string, string} */ - private function getOperatorFieldAndValue(ISearchComparison $operator): array { + private function getOperatorFieldAndValue(IQueryBuilder $builder, ISearchComparison $operator): array { $this->validateComparison($operator); $field = $operator->getField(); $value = $operator->getValue(); $type = $operator->getType(); $pathEqHash = $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true); - return $this->getOperatorFieldAndValueInner($field, $value, $type, $pathEqHash); + return $this->getOperatorFieldAndValueInner($builder, $field, $value, $type, $pathEqHash); } /** * @param string $field * @param ParamValue $value * @param string $type - * @return list{string, ParamValue, string, string} + * @return list{string|IQueryFunction, ParamValue, string, string} */ - private function getOperatorFieldAndValueInner(string $field, mixed $value, string $type, bool $pathEqHash): array { + private function getOperatorFieldAndValueInner(IQueryBuilder $builder, string $field, mixed $value, string $type, bool $pathEqHash): array { $paramType = self::FIELD_TYPES[$field]; if ($type === ISearchComparison::COMPARE_IN) { $resultField = $field; $values = []; foreach ($value as $arrayValue) { /** @var ParamSingleValue $arrayValue */ - [$arrayField, $arrayValue] = $this->getOperatorFieldAndValueInner($field, $arrayValue, ISearchComparison::COMPARE_EQUAL, $pathEqHash); + [$arrayField, $arrayValue] = $this->getOperatorFieldAndValueInner($builder, $field, $arrayValue, ISearchComparison::COMPARE_EQUAL, $pathEqHash); $resultField = $arrayField; $values[] = $arrayValue; } @@ -240,6 +242,9 @@ private function getOperatorFieldAndValueInner(string $field, mixed $value, stri $value = md5((string)$value); } elseif ($field === 'owner') { $field = 'uid_owner'; + } elseif ($field === 'mount_point_name') { + $field = $builder->func()->regexSubstring('mount_point', $builder->createNamedParameter('[^/]+/$')); + $value = $value . '/'; } return [$field, $value, $type, $paramType]; } @@ -261,6 +266,7 @@ private function validateComparison(ISearchComparison $operator) { 'owner' => ['eq'], 'creation_time' => ['eq', 'gt', 'lt', 'gte', 'lte'], 'upload_time' => ['eq', 'gt', 'lt', 'gte', 'lte'], + 'mount_point_name' => ['eq', 'like', 'clike', 'in'], ]; if (!isset(self::FIELD_TYPES[$operator->getField()])) { diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index b6774987193c8..98a2ad488a3fd 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -214,7 +214,25 @@ private function queryFromOperator(ISearchOperator $operator, ?string $uid = nul #[\Override] public function search($query) { if (is_string($query)) { - $query = $this->queryFromOperator(new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', '%' . $query . '%')); + $operator = new SearchComparison( + ISearchComparison::COMPARE_LIKE, + 'name', + '%' . $query . '%', + ); + $parts = explode('/', $this->path); + $uid = null; + if (count($parts) > 2) { + [, $uid] = $parts; + $operator = new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR, [ + $operator, + new SearchComparison( + ISearchComparison::COMPARE_LIKE, + 'mount_point_name', + '%' . $query . '%', + ) + ]); + } + $query = $this->queryFromOperator($operator, $uid); } // search is handled by a single query covering all caches that this folder contains diff --git a/tests/lib/Files/Node/FolderTest.php b/tests/lib/Files/Node/FolderTest.php index d67f25f622b7e..6bc32dc97102b 100644 --- a/tests/lib/Files/Node/FolderTest.php +++ b/tests/lib/Files/Node/FolderTest.php @@ -39,8 +39,10 @@ use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOrder; use OCP\Files\Storage\IStorage; +use OCP\IUser; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; +use Test\Traits\UserTrait; /** * Class FolderTest @@ -50,6 +52,14 @@ */ #[\PHPUnit\Framework\Attributes\Group('DB')] class FolderTest extends NodeTestCase { + use UserTrait; + + protected function setUp(): void { + parent::setUp(); + + $this->createUser('bar', 'bar'); + } + #[\Override] protected function createTestNode(IRootFolder $root, View&MockObject $view, string $path, array $data = [], string $internalPath = '', ?IStorage $storage = null): Folder { $view->expects($this->any()) From 2b178760a2b99ceb28722e74d67c7b5fb796be13 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 9 Sep 2026 18:55:11 +0200 Subject: [PATCH 3/4] test: add test for searching for mount name Signed-off-by: Robin Appelman --- lib/private/Files/Cache/SearchBuilder.php | 2 +- tests/lib/Files/Node/FolderTest.php | 1 - .../Files/Search/SearchIntegrationTest.php | 52 +++++++++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/lib/private/Files/Cache/SearchBuilder.php b/lib/private/Files/Cache/SearchBuilder.php index 0672813705f46..cacd8cfcaf829 100644 --- a/lib/private/Files/Cache/SearchBuilder.php +++ b/lib/private/Files/Cache/SearchBuilder.php @@ -243,7 +243,7 @@ private function getOperatorFieldAndValueInner(IQueryBuilder $builder, string $f } elseif ($field === 'owner') { $field = 'uid_owner'; } elseif ($field === 'mount_point_name') { - $field = $builder->func()->regexSubstring('mount_point', $builder->createNamedParameter('[^/]+/$')); + $field = $builder->func()->regexSubstring('m.mount_point', $builder->createNamedParameter('[^/]+/$')); $value = $value . '/'; } return [$field, $value, $type, $paramType]; diff --git a/tests/lib/Files/Node/FolderTest.php b/tests/lib/Files/Node/FolderTest.php index 6bc32dc97102b..55632711b1883 100644 --- a/tests/lib/Files/Node/FolderTest.php +++ b/tests/lib/Files/Node/FolderTest.php @@ -39,7 +39,6 @@ use OCP\Files\Search\ISearchComparison; use OCP\Files\Search\ISearchOrder; use OCP\Files\Storage\IStorage; -use OCP\IUser; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use Test\Traits\UserTrait; diff --git a/tests/lib/Files/Search/SearchIntegrationTest.php b/tests/lib/Files/Search/SearchIntegrationTest.php index 80397c4272dda..2118edc615131 100644 --- a/tests/lib/Files/Search/SearchIntegrationTest.php +++ b/tests/lib/Files/Search/SearchIntegrationTest.php @@ -11,22 +11,45 @@ use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchQuery; use OC\Files\Storage\Temporary; +use OCP\Files\Cache\ICache; +use OCP\Files\Config\IUserMountCache; use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; +use OCP\Files\Storage\IStorage; +use OCP\IUser; +use OCP\Server; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; use Test\TestCase; -#[\PHPUnit\Framework\Attributes\Group('DB')] +#[Group('DB')] class SearchIntegrationTest extends TestCase { - private $cache; - private $storage; + private ICache $cache; + private IStorage $storage; + private string $mountPoint; + private IUserMountCache $mountCache; + private IUser $user; #[\Override] protected function setUp(): void { parent::setUp(); + $this->user = $this->createMock(IUser::class); + $this->user->method('getUID') + ->willReturn('user'); $this->storage = new Temporary([]); $this->cache = $this->storage->getCache(); $this->storage->getScanner()->scan(''); + $this->mountCache = Server::get(IUserMountCache::class); + $this->mountPoint = '/user/files/search_test/'; + $this->mountCache->addMount($this->user, $this->mountPoint, $this->cache->get(''), 'dummy'); + } + + protected function tearDown(): void { + $this->mountCache->removeMount($this->mountPoint); + + parent::tearDown(); } public function testThousandAndOneFilters(): void { @@ -44,4 +67,27 @@ public function testThousandAndOneFilters(): void { $this->assertCount(1, $results); $this->assertEquals($id, $results[0]->getId()); } + + public static function searchMountNameProvider(): array { + return [ + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%search%'), ''], + [new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mount_point_name', 'search_test'), ''], + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%search_test%'), ''], + [new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mount_point_name', '%files%'), null], + ]; + } + + #[DataProvider('searchMountNameProvider')] + public function testSearchMountName(ISearchOperator $operator, ?string $resultPath): void { + $query = new SearchQuery($operator, 10, 0, [], $this->user); + + $results = $this->cache->searchQuery($query); + + if (is_null($resultPath)) { + $this->assertCount(0, $results); + } else { + $this->assertCount(1, $results); + $this->assertEquals($this->cache->getId($resultPath), $results[0]->getId()); + } + } } From dc0ddb54c59a4cc77e305a59cc2fdc6d9bca0efa Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Tue, 22 Sep 2026 12:06:05 +0200 Subject: [PATCH 4/4] fix: make mount point name compatible with sharding by switching it to the list of root ids Signed-off-by: Robin Appelman --- lib/private/Files/Cache/QuerySearchHelper.php | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/lib/private/Files/Cache/QuerySearchHelper.php b/lib/private/Files/Cache/QuerySearchHelper.php index 1accbf473fcf5..4bfec8a26402f 100644 --- a/lib/private/Files/Cache/QuerySearchHelper.php +++ b/lib/private/Files/Cache/QuerySearchHelper.php @@ -10,6 +10,8 @@ use OC\Files\Cache\Wrapper\CacheJail; use OC\Files\Search\QueryOptimizer\QueryOptimizer; use OC\Files\Search\SearchBinaryOperator; +use OC\Files\Search\SearchComparison; +use OC\Files\Search\SearchQuery; use OC\SystemConfig; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Cache\ICache; @@ -18,6 +20,8 @@ use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountPoint; use OCP\Files\Search\ISearchBinaryOperator; +use OCP\Files\Search\ISearchComparison; +use OCP\Files\Search\ISearchOperator; use OCP\Files\Search\ISearchQuery; use OCP\FilesMetadata\IFilesMetadataManager; use OCP\FilesMetadata\IMetadataQuery; @@ -99,7 +103,7 @@ public function findUsedTagsInCaches(ISearchQuery $searchQuery, array $caches): protected function equipQueryForSystemTags(CacheQueryBuilder $query, IUser $user): void { $query->leftJoin('file', 'systemtag_object_mapping', 'systemtagmap', $query->expr()->andX( $query->expr()->eq('file.fileid', $query->expr()->castColumn('systemtagmap.objectid', IQueryBuilder::PARAM_INT)), - $query->expr()->eq('systemtagmap.objecttype', $query->createNamedParameter('files')) + $query->expr()->eq('systemtagmap.objecttype', $query->createNamedParameter('files')), )); $on = $query->expr()->andX($query->expr()->eq('systemtag.id', 'systemtagmap.systemtagid')); if (!$this->groupManager->isAdmin($user->getUID())) { @@ -114,7 +118,7 @@ protected function equipQueryForDavTags(CacheQueryBuilder $query, IUser $user): ->leftJoin('tagmap', 'vcategory', 'tag', $query->expr()->andX( $query->expr()->eq('tagmap.categoryid', 'tag.id'), $query->expr()->eq('tag.type', $query->createNamedParameter('files')), - $query->expr()->eq('tag.uid', $query->createNamedParameter($user->getUID())) + $query->expr()->eq('tag.uid', $query->createNamedParameter($user->getUID())), )); } @@ -122,7 +126,7 @@ protected function equipQueryForMounts(CacheQueryBuilder $query, IUser $user): v $query ->leftJoin('file', 'mounts', 'm', $query->expr()->andX( $query->expr()->eq('m.root_id', 'file.fileid'), - $query->expr()->eq('m.user_id', $query->createNamedParameter($user->getUID())) + $query->expr()->eq('m.user_id', $query->createNamedParameter($user->getUID())), )); } @@ -156,6 +160,8 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array // while the resulting rows don't have a way to tell what storage they came from (multiple storages/caches can share storage_id) // we can just ask every cache if the row belongs to them and give them the cache to do any post processing on the result. + $searchQuery = $this->preProcessQuery($searchQuery); + $builder = $this->getQueryBuilder(); $requestedFields = array_merge( @@ -257,4 +263,49 @@ public function getCachesAndMountPointsForSearch(IRootFolder $root, string $path return [$caches, $mountByMountPoint]; } + + private function preProcessQuery(ISearchQuery $searchQuery): ISearchQuery { + // when sharding is enabled, we can't join on the mounts table + // so instead we need to fetch the matching mount root ids and filter on those + if ($this->connection->getShardDefinition('filecache') !== null) { + $operation = $this->replaceMountNameWithRootIds($searchQuery->getSearchOperation()); + return new SearchQuery( + $operation, + $searchQuery->getLimit(), + $searchQuery->getOffset(), + $searchQuery->getOrder(), + $searchQuery->getUser(), + $searchQuery->limitToHome(), + $searchQuery->getSelectFields(), + ); + } else { + return $searchQuery; + } + } + + private function replaceMountNameWithRootIds(ISearchOperator $searchOperator): ISearchOperator { + if ($searchOperator instanceof ISearchBinaryOperator) { + return new SearchBinaryOperator( + $searchOperator->getType(), + array_map($this->replaceMountNameWithRootIds(...), $searchOperator->getArguments()) + ); + } elseif ($searchOperator instanceof ISearchComparison && $searchOperator->getField() === 'mount_point_name') { + if (!in_array($searchOperator->getType(), [ + ISearchComparison::COMPARE_LIKE, + ISearchComparison::COMPARE_EQUAL, + ISearchComparison::COMPARE_IN, + ], true)) { + throw new \InvalidArgumentException('Filtering mount name with ' . $searchOperator->getType() . ' is not supported'); + } + + $query = $this->connection->getQueryBuilder(); + $query->select('root_id') + ->from('mounts', 'm') + ->where($this->searchBuilder->searchOperatorToDBExpr($query, $searchOperator)); + $rootIds = $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + return new SearchComparison(ISearchComparison::COMPARE_IN, 'fileid', $rootIds); + } else { + return $searchOperator; + } + } }