Skip to content

Commit 59d1ae9

Browse files
authored
Merge pull request #262 from oboeglen/fix/insertOrUpdate-missing-id
fix(ContentManager): look up existing row before insertOrUpdate
2 parents 2a30822 + 27a789c commit 59d1ae9

3 files changed

Lines changed: 100 additions & 7 deletions

File tree

lib/Db/QueueContentItemMapper.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,33 @@ public function countLocked() : array {
123123
return $stats;
124124
}
125125

126+
/**
127+
* Finds the id of the queue item for a given (app_id, provider_id, item_id)
128+
* triple, matching the table's unique index. Used to look up the id of an
129+
* already-queued item before updating it, since update() needs the id and
130+
* every other column is overwritten anyway.
131+
*
132+
* @throws Exception
133+
*/
134+
public function findIdByUniqueKey(string $appId, string $providerId, string $itemId): ?int {
135+
$qb = $this->db->getQueryBuilder();
136+
$qb->select('id')
137+
->from($this->getTableName())
138+
->where($qb->expr()->eq('app_id', $qb->createNamedParameter($appId)))
139+
->andWhere($qb->expr()->eq('provider_id', $qb->createNamedParameter($providerId)))
140+
->andWhere($qb->expr()->eq('item_id', $qb->createNamedParameter($itemId)))
141+
->setMaxResults(1);
142+
143+
$result = $qb->executeQuery();
144+
$id = $result->fetchOne();
145+
$result->closeCursor();
146+
147+
if ($id === false) {
148+
return null;
149+
}
150+
return (int)$id;
151+
}
152+
126153
/**
127154
* @throws Exception
128155
*/

lib/Public/ContentManager.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,23 @@ public function submitContent(string $appId, array $items): void {
137137
$dbItem->setUsers(implode(',', $item->users));
138138

139139
try {
140-
$this->mapper->insertOrUpdate($dbItem);
140+
// insertOrUpdate() can only fall back to update() when the entity's
141+
// id is already known, which a freshly built entity never has, so it
142+
// fails with "Entity which should be updated has no id" whenever the
143+
// unique index on (app_id, provider_id, item_id) is hit. Look up the
144+
// id of the already-queued row first and update that row instead.
145+
$id = $this->mapper->findIdByUniqueKey($appId, $item->providerId, $item->itemId);
146+
if ($id === null) {
147+
$this->mapper->insert($dbItem);
148+
} else {
149+
$dbItem->setId($id);
150+
$this->mapper->update($dbItem);
151+
}
141152
} catch (Exception $e) {
142-
$this->logger->error($e->getMessage(), ['exception' => $e]);
153+
$this->logger->error(
154+
"Error adding content item id {$item->itemId} from app {$appId}: {$e->getMessage()}",
155+
['exception' => $e],
156+
);
143157
}
144158
}
145159
}

tests/integration/ContentManagerTest.php

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
use DateTime;
1313
use OCA\ContextChat\AppInfo\Application;
1414
use OCA\ContextChat\BackgroundJobs\InitialContentImportJob;
15-
use OCA\ContextChat\BackgroundJobs\SubmitContentJob;
15+
use OCA\ContextChat\Db\QueueContentItem;
1616
use OCA\ContextChat\Db\QueueContentItemMapper;
1717
use OCA\ContextChat\Event\ContentProviderRegisterEvent;
1818
use OCA\ContextChat\Logger;
@@ -21,6 +21,7 @@
2121
use OCA\ContextChat\Public\IContentProvider;
2222
use OCA\ContextChat\Service\ActionScheduler;
2323
use OCA\ContextChat\Service\ProviderConfigService;
24+
use OCP\AppFramework\Services\IAppConfig;
2425
use OCP\BackgroundJob\IJobList;
2526
use OCP\EventDispatcher\IEventDispatcher;
2627
use OCP\IServerContainer;
@@ -31,6 +32,8 @@
3132
use Test\TestCase;
3233

3334
class ContentManagerTest extends TestCase {
35+
/** @var MockObject | IAppConfig */
36+
private IAppConfig $appConfig;
3437
/** @var MockObject | QueueContentItemMapper */
3538
private QueueContentItemMapper $mapper;
3639
/** @var MockObject | ProviderConfigService */
@@ -52,6 +55,7 @@ public function setUp(): void {
5255
$this->jobList = Server::get(IJobList::class);
5356
$this->logger = Server::get(LoggerInterface::class);
5457

58+
$this->appConfig = $this->createMock(IAppConfig::class);
5559
$this->mapper = $this->createMock(QueueContentItemMapper::class);
5660
$this->providerConfig = $this->createMock(ProviderConfigService::class);
5761
$this->actionService = $this->createMock(ActionScheduler::class);
@@ -92,6 +96,7 @@ public function setUp(): void {
9296

9397
$this->contentManager = new ContentManager(
9498
$this->jobList,
99+
$this->appConfig,
95100
$this->providerConfig,
96101
$this->mapper,
97102
$this->actionService,
@@ -167,17 +172,64 @@ public function testSubmitContent(): void {
167172
),
168173
];
169174

175+
$this->mapper
176+
->expects($this->once())
177+
->method('findIdByUniqueKey')
178+
->with($appId, 'provider-id', 'item-id')
179+
->willReturn(null);
180+
170181
$this->mapper
171182
->expects($this->once())
172183
->method('insert');
173184

174-
$this->jobList->remove(SubmitContentJob::class, null);
175-
$this->assertFalse($this->jobList->has(SubmitContentJob::class, null));
185+
$this->mapper
186+
->expects($this->never())
187+
->method('update');
176188

177189
$this->contentManager->submitContent($appId, $items);
190+
}
191+
192+
public function testSubmitContentUpdatesAlreadyQueuedItem(): void {
193+
$appId = 'test';
194+
$items = [
195+
new ContentItem(
196+
'item-id',
197+
'provider-id',
198+
'new title',
199+
'new content',
200+
'email-file',
201+
new DateTime(),
202+
['user1', 'user2'],
203+
),
204+
];
178205

179-
$this->assertTrue($this->jobList->has(SubmitContentJob::class, null));
180-
$this->jobList->remove(SubmitContentJob::class, null);
206+
$this->mapper
207+
->expects($this->once())
208+
->method('findIdByUniqueKey')
209+
->with($appId, 'provider-id', 'item-id')
210+
->willReturn(42);
211+
212+
$this->mapper
213+
->expects($this->never())
214+
->method('insert');
215+
216+
$this->mapper
217+
->expects($this->once())
218+
->method('update')
219+
->with($this->callback(function (QueueContentItem $dbItem) use ($appId) {
220+
// update() needs the id of the already queued row, otherwise
221+
// QBMapper throws "Entity which should be updated has no id"
222+
$this->assertSame(42, $dbItem->getId());
223+
$this->assertSame($appId, $dbItem->getAppId());
224+
$this->assertSame('provider-id', $dbItem->getProviderId());
225+
$this->assertSame('item-id', $dbItem->getItemId());
226+
$this->assertSame('new title', $dbItem->getTitle());
227+
$this->assertSame('new content', $dbItem->getContent());
228+
$this->assertSame('user1,user2', $dbItem->getUsers());
229+
return true;
230+
}));
231+
232+
$this->contentManager->submitContent($appId, $items);
181233
}
182234
}
183235

0 commit comments

Comments
 (0)