Skip to content
34 changes: 19 additions & 15 deletions lib/private/FilesMetadata/Service/MetadataRequestService.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,25 +107,29 @@ public function getMetadataFromFileId(int $fileId): IFilesMetadata {
* @psalm-return array<int, IFilesMetadata>
*/
public function getMetadataFromFileIds(array $fileIds): array {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('file_id', 'json', 'sync_token')
->from(self::TABLE_METADATA)
->where($qb->expr()->in('file_id', $qb->createNamedParameter($fileIds, IQueryBuilder::PARAM_INT_ARRAY)))
->runAcrossAllShards();
$chunks = array_chunk($fileIds, IQueryBuilder::MAX_IN_PARAMETERS);

$list = [];
$result = $qb->executeQuery();
while ($data = $result->fetchAssociative()) {
$fileId = (int)$data['file_id'];
$metadata = new FilesMetadata($fileId);
try {
$metadata->importFromDatabase($data);
} catch (FilesMetadataNotFoundException) {
continue;
foreach ($chunks as $chunk) {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('file_id', 'json', 'sync_token')
->from(self::TABLE_METADATA)
->where($qb->expr()->in('file_id', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)))
->runAcrossAllShards();

$result = $qb->executeQuery();
while ($data = $result->fetchAssociative()) {
$fileId = (int)$data['file_id'];
$metadata = new FilesMetadata($fileId);
try {
$metadata->importFromDatabase($data);
} catch (FilesMetadataNotFoundException) {
continue;
}
$list[$fileId] = $metadata;
}
$list[$fileId] = $metadata;
$result->closeCursor();
}
$result->closeCursor();

return $list;
}
Expand Down
71 changes: 71 additions & 0 deletions tests/lib/FilesMetadata/FilesMetadataManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OC\FilesMetadata\FilesMetadataManager;
use OC\FilesMetadata\Service\IndexRequestService;
use OC\FilesMetadata\Service\MetadataRequestService;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\Event;
Expand Down Expand Up @@ -129,4 +130,74 @@ public function testDropMetadataForFilesChunking(): void {

$this->assertSame($expectedChunks, $boundChunks);
}

public function testGetMetadataFromFileIdsChunking(): void {
$connection = $this->createMock(IDBConnection::class);
$qb = $this->createMock(IQueryBuilder::class);
$expr = $this->createMock(IExpressionBuilder::class);
$result = $this->createMock(IResult::class);

$connection->method('getQueryBuilder')->willReturn($qb);
$qb->method('expr')->willReturn($expr);
$qb->method('select')->willReturnSelf();
$qb->method('from')->willReturnSelf();
$qb->method('where')->willReturnSelf();
$qb->method('runAcrossAllShards')->willReturnSelf();
$qb->method('executeQuery')->willReturn($result);
$result->method('fetchAssociative')->willReturn(false);

$fileIds = range(1, IQueryBuilder::MAX_IN_PARAMETERS * 2 + 1);
$expectedChunks = array_chunk($fileIds, IQueryBuilder::MAX_IN_PARAMETERS);
$boundChunks = [];

$qb->expects($this->exactly(count($expectedChunks)))
->method('createNamedParameter')
->willReturnCallback(function (array $chunk, $type) use (&$boundChunks): string {
$this->assertSame(IQueryBuilder::PARAM_INT_ARRAY, $type);
$boundChunks[] = $chunk;
return ':param';
});

$service = new MetadataRequestService($connection, $this->logger);
$service->getMetadataFromFileIds($fileIds);

$this->assertSame($expectedChunks, $boundChunks);
}

public function testGetMetadataFromFileIdsMergesResultsAcrossChunks(): void {
$connection = $this->createMock(IDBConnection::class);
$qb = $this->createMock(IQueryBuilder::class);
$expr = $this->createMock(IExpressionBuilder::class);
$result = $this->createMock(IResult::class);

$connection->method('getQueryBuilder')->willReturn($qb);
$qb->method('expr')->willReturn($expr);
$qb->method('select')->willReturnSelf();
$qb->method('from')->willReturnSelf();
$qb->method('where')->willReturnSelf();
$qb->method('runAcrossAllShards')->willReturnSelf();
$qb->method('createNamedParameter')->willReturn(':param');
$qb->method('executeQuery')->willReturn($result);

$fileIds = range(1, IQueryBuilder::MAX_IN_PARAMETERS + 2);
$firstId = $fileIds[0];
$lastId = end($fileIds);
$row = static fn (int $fileId): array => [
'file_id' => (string)$fileId,
'json' => '{}',
'sync_token' => 'token',
];

// one row then end-of-result per chunk, so both chunks contribute
$fetches = [$row($firstId), false, $row($lastId), false];
$result->method('fetchAssociative')
->willReturnCallback(function () use (&$fetches) {
return array_shift($fetches);
});

$service = new MetadataRequestService($connection, $this->logger);
$metadata = $service->getMetadataFromFileIds($fileIds);

$this->assertSame([$firstId, $lastId], array_keys($metadata));
}
}