Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/dav/lib/Files/FileSearchBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,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,
Expand Down
18 changes: 13 additions & 5 deletions apps/dav/tests/unit/Files/FileSearchBackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,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) . ')');
}
}
20 changes: 20 additions & 0 deletions lib/private/DB/SQLiteSessionInit.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,30 @@ public function postConnect(ConnectionEventArgs $args) {
/** @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 '';
} else {
$string = (string)$string;
$pattern = str_replace('#', '\#', (string)$pattern);
}

$matches = [];
$result = preg_match("#$pattern#", $string, $matches);
if ($result === 0 || $result === false) {
return '';
} 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);
}
}

Expand Down
56 changes: 54 additions & 2 deletions lib/private/Files/Cache/QuerySearchHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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;
Expand Down Expand Up @@ -98,7 +100,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())) {
Expand All @@ -113,7 +115,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())),
));
}

Expand Down Expand Up @@ -148,6 +150,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(
Expand All @@ -172,6 +176,9 @@ public function searchInCaches(ISearchQuery $searchQuery, array $caches): array
if (in_array('owner', $requestedFields) || in_array('share_with', $requestedFields) || in_array('share_type', $requestedFields)) {
$this->equipQueryForShares($query);
}
if (in_array('mount_point_name', $requestedFields, true)) {
$this->equipQueryForMounts($query, $this->requireUser($searchQuery));
}

$metadataQuery = $query->selectMetadata();

Expand Down Expand Up @@ -246,4 +253,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;
}
}
}
14 changes: 10 additions & 4 deletions lib/private/Files/Cache/SearchBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,6 +67,7 @@ class SearchBuilder {
'owner' => 'string',
'creation_time' => 'integer',
'upload_time' => 'integer',
'mount_point_name' => 'string',
];

/** @var array<string, int|string> */
Expand Down Expand Up @@ -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])) {
Expand All @@ -177,13 +179,13 @@ private function searchComparisonToDBExpr(
* @param ISearchComparison $operator
* @return list{string, 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);
}

/**
Expand All @@ -199,7 +201,7 @@ private function getOperatorFieldAndValueInner(string $field, mixed $value, stri
$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;
}
Expand Down Expand Up @@ -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('m.mount_point', $builder->createNamedParameter('[^/]+/$'));
$value = $value . '/';
}
return [$field, $value, $type, $paramType];
}
Expand All @@ -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::$fieldTypes[$operator->getField()])) {
Expand Down
20 changes: 19 additions & 1 deletion lib/private/Files/Node/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,25 @@ private function queryFromOperator(ISearchOperator $operator, ?string $uid = nul
*/
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
Expand Down
11 changes: 11 additions & 0 deletions lib/public/DB/QueryBuilder/IFunctionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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
*
Expand Down
28 changes: 28 additions & 0 deletions tests/lib/DB/QueryBuilder/FunctionBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Server;
use PHPUnit\Framework\Attributes\DataProvider;
use Test\TestCase;

/**
Expand Down Expand Up @@ -486,4 +487,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);
}
}
1 change: 1 addition & 0 deletions tests/lib/Files/Node/FolderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use OCP\Files\Storage\IStorage;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use Test\Traits\UserTrait;

/**
* Class FolderTest
Expand Down
Loading