Skip to content

Commit 204410e

Browse files
authored
Merge pull request #63346 from nextcloud/backport/63286/stable33
[stable33] fix(preview): qualify the ambiguous columns in PreviewMapper queries
2 parents 6c0408a + b5222a1 commit 204410e

2 files changed

Lines changed: 102 additions & 6 deletions

File tree

lib/private/Preview/Db/PreviewMapper.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ class PreviewMapper extends QBMapper {
2828
private const LOCATION_TABLE_NAME = 'preview_locations';
2929
private const VERSION_TABLE_NAME = 'preview_versions';
3030

31+
// Columns selected by joinLocation() that do not belong to the previews table
32+
private const JOINED_COLUMN_ALIASES = [
33+
'version' => 'v',
34+
'bucket_name' => 'l',
35+
'object_store_name' => 'l',
36+
];
37+
3138
public function __construct(
3239
IDBConnection $db,
3340
private readonly IMimeTypeLoader $mimeTypeLoader,
@@ -130,7 +137,7 @@ public function getAvailablePreviews(array $fileIds): array {
130137
public function getByFileId(int $fileId): \Generator {
131138
$selectQb = $this->db->getQueryBuilder();
132139
$this->joinLocation($selectQb)
133-
->where($selectQb->expr()->eq('file_id', $selectQb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
140+
->where($selectQb->expr()->eq('p.file_id', $selectQb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)));
134141
yield from $this->yieldEntities($selectQb);
135142
}
136143

@@ -238,7 +245,19 @@ public function getPreviewForSpecification(array $parameters): ?Preview {
238245
$this->joinLocation($qb);
239246

240247
foreach ($parameters as $key => $value) {
241-
$qb->andWhere($qb->expr()->eq($key, $qb->createNamedParameter($value)));
248+
// The previews table is joined with preview_versions, which shares
249+
// the file_id column name, so plain column names have to be aliased.
250+
$column = str_contains($key, '.')
251+
? $key
252+
: (self::JOINED_COLUMN_ALIASES[$key] ?? 'p') . '.' . $key;
253+
// An untyped false binds as an empty string, which PostgreSQL
254+
// rejects for a boolean column.
255+
$type = match (true) {
256+
is_bool($value) => IQueryBuilder::PARAM_BOOL,
257+
is_int($value) => IQueryBuilder::PARAM_INT,
258+
default => IQueryBuilder::PARAM_STR,
259+
};
260+
$qb->andWhere($qb->expr()->eq($column, $qb->createNamedParameter($value, $type)));
242261
}
243262

244263
try {

tests/lib/Preview/PreviewMapperTest.php

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use OC\Preview\Db\Preview;
1313
use OC\Preview\Db\PreviewMapper;
14+
use OCP\Files\IMimeTypeLoader;
1415
use OCP\IDBConnection;
1516
use OCP\Server;
1617
use OCP\Snowflake\ISnowflakeGenerator;
@@ -21,12 +22,15 @@ class PreviewMapperTest extends TestCase {
2122
private PreviewMapper $previewMapper;
2223
private IDBConnection $connection;
2324
private ISnowflakeGenerator $snowflake;
25+
private IMimeTypeLoader $mimeTypeLoader;
2426

27+
#[\Override]
2528
public function setUp(): void {
2629
parent::setUp();
2730
$this->previewMapper = Server::get(PreviewMapper::class);
2831
$this->connection = Server::get(IDBConnection::class);
2932
$this->snowflake = Server::get(ISnowflakeGenerator::class);
33+
$this->mimeTypeLoader = Server::get(IMimeTypeLoader::class);
3034

3135
$qb = $this->connection->getQueryBuilder();
3236
$qb->delete('preview_locations')->executeStatement();
@@ -38,6 +42,7 @@ public function setUp(): void {
3842
$qb->delete('previews')->executeStatement();
3943
}
4044

45+
#[\Override]
4146
public function tearDown(): void {
4247
$this->previewMapper->deleteAll();
4348
parent::tearDown();
@@ -64,7 +69,7 @@ public function testGetAvailablePreviews(): void {
6469
$this->assertEquals('default', $previews[43][0]->getObjectStoreName());
6570
}
6671

67-
private function createPreviewForFileId(int $fileId, ?int $bucket = null): void {
72+
private function createPreviewForFileId(int $fileId, ?int $bucket = null, int $size = 100, ?string $version = null, bool $cropped = true): string {
6873
$locationId = null;
6974
if ($bucket) {
7075
$qb = $this->connection->getQueryBuilder();
@@ -81,19 +86,91 @@ private function createPreviewForFileId(int $fileId, ?int $bucket = null): void
8186
$preview->generateId();
8287
$preview->setFileId($fileId);
8388
$preview->setStorageId(1);
84-
$preview->setCropped(true);
89+
$preview->setCropped($cropped);
8590
$preview->setMax(true);
86-
$preview->setWidth(100);
91+
$preview->setWidth($size);
8792
$preview->setSourceMimeType('image/jpeg');
88-
$preview->setHeight(100);
93+
$preview->setHeight($size);
8994
$preview->setSize(100);
9095
$preview->setMtime(time());
9196
$preview->setMimetype('image/jpeg');
9297
$preview->setEtag('abcdefg');
98+
$preview->setVersion($version);
9399

94100
if ($locationId !== null) {
95101
$preview->setLocationId($locationId);
96102
}
97103
$this->previewMapper->insert($preview);
104+
105+
return $preview->id;
106+
}
107+
108+
/**
109+
* The previews table is joined with preview_versions, which also has a
110+
* file_id column, so the condition has to be qualified with the alias.
111+
*/
112+
public function testGetByFileId(): void {
113+
$fileId = 4242;
114+
$this->createPreviewForFileId($fileId);
115+
$this->createPreviewForFileId($fileId, size: 256);
116+
$this->createPreviewForFileId(4243);
117+
118+
$previews = iterator_to_array($this->previewMapper->getByFileId($fileId));
119+
120+
$this->assertCount(2, $previews);
121+
foreach ($previews as $preview) {
122+
$this->assertSame($fileId, $preview->getFileId());
123+
}
124+
}
125+
126+
/**
127+
* Same ambiguity, reached through the specification lookup that
128+
* Generator::savePreview() uses to recover from a unique constraint
129+
* violation. It passes the cropped flag as a PHP bool, and false is the
130+
* common case, so both values have to be covered.
131+
*/
132+
#[\PHPUnit\Framework\Attributes\TestWith([false])]
133+
#[\PHPUnit\Framework\Attributes\TestWith([true])]
134+
public function testGetPreviewForSpecification(bool $cropped): void {
135+
$fileId = 4244;
136+
$previewId = $this->createPreviewForFileId($fileId, cropped: $cropped);
137+
138+
$preview = $this->previewMapper->getPreviewForSpecification([
139+
'file_id' => $fileId,
140+
'width' => 100,
141+
'height' => 100,
142+
'mimetype_id' => $this->mimeTypeLoader->getId('image/jpeg'),
143+
'cropped' => $cropped,
144+
'version_id' => '-1',
145+
]);
146+
147+
$this->assertNotNull($preview);
148+
$this->assertEquals($previewId, $preview->getId());
149+
}
150+
151+
/**
152+
* version lives in the joined preview_versions table, so it has to keep
153+
* resolving to that alias rather than to the previews table.
154+
*/
155+
public function testGetPreviewForSpecificationOnJoinedColumn(): void {
156+
$fileId = 4245;
157+
$previewId = $this->createPreviewForFileId($fileId, version: '1000');
158+
159+
$preview = $this->previewMapper->getPreviewForSpecification([
160+
'file_id' => $fileId,
161+
'version' => '1000',
162+
]);
163+
164+
$this->assertNotNull($preview);
165+
$this->assertEquals($previewId, $preview->getId());
166+
}
167+
168+
public function testLargeIdInsertRetrieve(): void {
169+
$fileId = PHP_INT_MAX;
170+
$originalPreviewId = $this->createPreviewForFileId($fileId);
171+
172+
$dbPreview = $this->previewMapper->getAvailablePreviews([$fileId])[$fileId][0];
173+
$this->assertEquals($originalPreviewId, $dbPreview->id);
174+
$this->assertEquals($fileId, $dbPreview->getFileId());
98175
}
99176
}

0 commit comments

Comments
 (0)