Skip to content

Commit 3db51ec

Browse files
authored
Merge pull request #600 from utopia-php/fix-upsert-mismatch
Fix upsert duplicate IDs
2 parents 550c4d4 + 6d26eee commit 3db51ec

File tree

3 files changed

+81
-3
lines changed

3 files changed

+81
-3
lines changed

src/Database/Adapter/MariaDB.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,12 +1445,14 @@ public function createOrUpdateDocuments(
14451445
if (!empty($toRemove)) {
14461446
$removeQueries[] = "(
14471447
_document = :_uid_{$index}
1448-
{$this->getTenantQuery($collection, tenantCount: \count($toRemove))}
1448+
" . ($this->sharedTables ? " AND _tenant = :_tenant_{$index}" : '') . "
14491449
AND _type = '{$type}'
14501450
AND _permission IN (" . \implode(',', \array_map(fn ($i) => ":remove_{$type}_{$index}_{$i}", \array_keys($toRemove))) . ")
14511451
)";
14521452
$removeBindValues[":_uid_{$index}"] = $document->getId();
1453-
$removeBindValues[":_tenant_{$index}"] = $document->getTenant();
1453+
if ($this->sharedTables) {
1454+
$removeBindValues[":_tenant_{$index}"] = $document->getTenant();
1455+
}
14541456
foreach ($toRemove as $i => $perm) {
14551457
$removeBindValues[":remove_{$type}_{$index}_{$i}"] = $perm;
14561458
}

src/Database/Database.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4920,7 +4920,7 @@ public function createOrUpdateDocumentsWithIncrease(
49204920
$time = DateTime::now();
49214921
$created = 0;
49224922
$updated = 0;
4923-
4923+
$seenIds = [];
49244924
foreach ($documents as $key => $document) {
49254925
if ($this->getSharedTables() && $this->getTenantPerDocument()) {
49264926
$old = Authorization::skip(fn () => $this->withTenant($document->getTenant(), fn () => $this->silent(fn () => $this->getDocument(
@@ -5028,12 +5028,19 @@ public function createOrUpdateDocumentsWithIncrease(
50285028
$document = $this->silent(fn () => $this->createDocumentRelationships($collection, $document));
50295029
}
50305030

5031+
$seenIds[] = $document->getId();
5032+
50315033
$documents[$key] = new Change(
50325034
old: $old,
50335035
new: $document
50345036
);
50355037
}
50365038

5039+
// Required because *some* DBs will allow duplicate IDs for upsert
5040+
if (\count($seenIds) !== \count(\array_unique($seenIds))) {
5041+
throw new DuplicateException('Duplicate document IDs found in the input array.');
5042+
}
5043+
50375044
foreach (\array_chunk($documents, $batchSize) as $chunk) {
50385045
/**
50395046
* @var array<Change> $chunk

tests/e2e/Adapter/Scopes/DocumentTests.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,75 @@ public function testUpsertDocumentsNoop(): void
735735
$this->assertEquals(0, $count);
736736
}
737737

738+
public function testUpsertDuplicateIds(): void
739+
{
740+
$db = static::getDatabase();
741+
if (!$db->getAdapter()->getSupportForUpserts()) {
742+
$this->expectNotToPerformAssertions();
743+
return;
744+
}
745+
746+
$db->createCollection(__FUNCTION__);
747+
$db->createAttribute(__FUNCTION__, 'num', Database::VAR_INTEGER, 0, true);
748+
749+
$doc1 = new Document(['$id' => 'dup', 'num' => 1]);
750+
$doc2 = new Document(['$id' => 'dup', 'num' => 2]);
751+
752+
try {
753+
$db->createOrUpdateDocuments(__FUNCTION__, [$doc1, $doc2]);
754+
$this->fail('Failed to throw exception');
755+
} catch (\Throwable $e) {
756+
$this->assertInstanceOf(DuplicateException::class, $e, $e->getMessage());
757+
}
758+
}
759+
760+
public function testUpsertMixedPermissionDelta(): void
761+
{
762+
$db = static::getDatabase();
763+
if (!$db->getAdapter()->getSupportForUpserts()) {
764+
$this->expectNotToPerformAssertions();
765+
return;
766+
}
767+
768+
$db->createCollection(__FUNCTION__);
769+
$db->createAttribute(__FUNCTION__, 'v', Database::VAR_INTEGER, 0, true);
770+
771+
$d1 = $db->createDocument(__FUNCTION__, new Document([
772+
'$id' => 'a',
773+
'v' => 0,
774+
'$permissions' => [
775+
Permission::update(Role::any())
776+
]
777+
]));
778+
$d2 = $db->createDocument(__FUNCTION__, new Document([
779+
'$id' => 'b',
780+
'v' => 0,
781+
'$permissions' => [
782+
Permission::update(Role::any())
783+
]
784+
]));
785+
786+
// d1 adds write, d2 removes update
787+
$d1->setAttribute('$permissions', [
788+
Permission::read(Role::any()),
789+
Permission::update(Role::any())
790+
]);
791+
$d2->setAttribute('$permissions', [
792+
Permission::read(Role::any())
793+
]);
794+
795+
$db->createOrUpdateDocuments(__FUNCTION__, [$d1, $d2]);
796+
797+
$this->assertEquals([
798+
Permission::read(Role::any()),
799+
Permission::update(Role::any()),
800+
], $db->getDocument(__FUNCTION__, 'a')->getPermissions());
801+
802+
$this->assertEquals([
803+
Permission::read(Role::any()),
804+
], $db->getDocument(__FUNCTION__, 'b')->getPermissions());
805+
}
806+
738807
public function testRespectNulls(): Document
739808
{
740809
static::getDatabase()->createCollection('documents_nulls');

0 commit comments

Comments
 (0)