Skip to content

Commit 9a1496e

Browse files
added bulk update support for document replacement in the updateDocuments
1 parent 0598fe7 commit 9a1496e

File tree

4 files changed

+163
-22
lines changed

4 files changed

+163
-22
lines changed

src/Database/Adapter/Mongo.php

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,11 +1445,12 @@ public function updateDocument(Document $collection, string $id, Document $docum
14451445

14461446
try {
14471447
unset($record['_id']); // Don't update _id
1448-
14491448
$options = $this->getTransactionOptions();
1450-
$updateQuery = $this->getSupportForAttributes() ? [
1451-
'$set' => $record,
1452-
] : $record;
1449+
$updates = match (true) {
1450+
$this->getSupportForAttributes() => ['$set' => $record],
1451+
default => $record,
1452+
};
1453+
$updateQuery = $updates;
14531454
$this->client->update($name, $filters, $updateQuery, $options);
14541455
} catch (MongoException $e) {
14551456
throw $this->processException($e);
@@ -1486,20 +1487,23 @@ public function updateDocuments(Document $collection, Document $updates, array $
14861487
$filters['_tenant'] = $this->getTenantFilters($collection->getId());
14871488
}
14881489

1489-
$record = $updates->getArrayCopy();
1490-
$record = $this->replaceChars('$', '_', $record);
1491-
1492-
$updateQuery = [
1493-
'$set' => $record,
1494-
];
1490+
$record = $this->replaceChars('$', '_', $updates->getArrayCopy());
14951491

14961492
try {
1493+
if (!$this->getSupportForAttributes()) {
1494+
return $this->updateDocumentsForSchemaless($name, $documents, $record, $filters, $options);
1495+
}
1496+
1497+
$updateQuery = [
1498+
'$set' => $record,
1499+
];
1500+
14971501
return $this->client->update(
14981502
$name,
14991503
$filters,
15001504
$updateQuery,
15011505
options: $options,
1502-
multi: true,
1506+
multi: true
15031507
);
15041508
} catch (MongoException $e) {
15051509
throw $this->processException($e);
@@ -3189,4 +3193,50 @@ public function getTenantQuery(string $collection, string $alias = ''): string
31893193
{
31903194
return '';
31913195
}
3196+
3197+
/**
3198+
* @param string $collection
3199+
* @param array<Document> $oldDocuments
3200+
* @param array<mixed> $updates
3201+
* @param array<mixed> $filters
3202+
* @param array<mixed> $options
3203+
* @return int
3204+
*/
3205+
private function updateDocumentsForSchemaless(string $collection, array $oldDocuments, array $updates, array $filters, array $options): int
3206+
{
3207+
$internalKeys = array_map(
3208+
fn ($attr) => str_replace('$', '_', $attr['$id']),
3209+
Database::INTERNAL_ATTRIBUTES
3210+
);
3211+
$unsetUnion = [];
3212+
3213+
foreach ($oldDocuments as $doc) {
3214+
if ($doc->offsetExists('$skipPermissionsUpdate')) {
3215+
$doc->removeAttribute('$skipPermissionsUpdate');
3216+
}
3217+
$attrs = $doc->getAttributes();
3218+
$attributes = $this->replaceChars('$', '_', $attrs);
3219+
foreach (array_keys($attributes) as $attributeKey) {
3220+
if (!array_key_exists($attributeKey, $updates) && !in_array($attributeKey, $internalKeys, true)) {
3221+
$unsetUnion[$attributeKey] = 1;
3222+
}
3223+
}
3224+
}
3225+
3226+
$updateQuery = [
3227+
'$set' => $updates,
3228+
];
3229+
3230+
if (!empty($unsetUnion)) {
3231+
$updateQuery['$unset'] = $unsetUnion;
3232+
}
3233+
3234+
return $this->client->update(
3235+
$collection,
3236+
$filters,
3237+
$updateQuery,
3238+
options: $options,
3239+
multi: true
3240+
);
3241+
}
31923242
}

src/Database/Database.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4913,15 +4913,7 @@ public function updateDocument(string $collection, string $id, Document $documen
49134913
$skipPermissionsUpdate = ($originalPermissions === $currentPermissions);
49144914
}
49154915
$createdAt = $document->getCreatedAt();
4916-
if ($this->adapter->getSupportForAttributes()) {
4917-
$document = \array_merge($old->getArrayCopy(), $document->getArrayCopy());
4918-
} else {
4919-
$oldArray = $old->getArrayCopy();
4920-
$newArray = $document->getArrayCopy();
4921-
$internalKeys = array_map(fn ($attr) => $attr['$id'], self::INTERNAL_ATTRIBUTES);
4922-
$internalAttrs = array_intersect_key($oldArray, array_flip($internalKeys));
4923-
$document = array_merge($internalAttrs, $newArray);
4924-
}
4916+
$document = $this->mergeDocuments($old, $document);
49254917
$document['$collection'] = $old->getAttribute('$collection'); // Make sure user doesn't switch collection ID
49264918
$document['$createdAt'] = ($createdAt === null || !$this->preserveDates) ? $old->getCreatedAt() : $createdAt;
49274919

@@ -8378,4 +8370,27 @@ protected function encodeSpatialData(mixed $value, string $type): string
83788370
throw new DatabaseException('Unknown spatial type: ' . $type);
83798371
}
83808372
}
8373+
8374+
/**
8375+
* @param Document $old
8376+
* @param Document $new
8377+
* @return array<mixed>
8378+
*/
8379+
protected function mergeDocuments(Document $old, Document $new): array
8380+
{
8381+
switch (true) {
8382+
case $this->adapter->getSupportForAttributes():
8383+
return \array_merge($old->getArrayCopy(), $new->getArrayCopy());
8384+
8385+
// schemaless behaviour
8386+
default:
8387+
$oldArray = $old->getArrayCopy();
8388+
$newArray = $new->getArrayCopy();
8389+
8390+
$internalKeys = array_map(fn ($attr) => $attr['$id'], self::INTERNAL_ATTRIBUTES);
8391+
$internalAttrs = array_intersect_key($oldArray, array_flip($internalKeys));
8392+
8393+
return array_merge($internalAttrs, $newArray);
8394+
}
8395+
}
83818396
}

tests/e2e/Adapter/Scopes/DocumentTests.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6170,14 +6170,26 @@ public function testCreateUpdateDocumentsMismatch(): void
61706170
$this->assertEquals(2, $database->updateDocuments($colName, new Document(['key' => 'new doc'])));
61716171
$doc = $database->getDocument($colName, 'doc4');
61726172
$this->assertEquals('doc4', $doc->getId());
6173-
$this->assertEquals('value', $doc->getAttribute('value'));
6173+
if (!$database->getAdapter()->getSupportForAttributes()) {
6174+
// not assertArrayNotHasKey as createAttribute is done previously
6175+
// value would be returned in attributes but schemaless updateDocuments do replace the old document with new document
6176+
// so null
6177+
$this->assertNull($doc->getAttribute('value'));
6178+
} else {
6179+
$this->assertEquals('value', $doc->getAttribute('value'));
6180+
6181+
}
61746182

61756183
$addedDocs = $database->find($colName);
61766184
$this->assertCount(2, $addedDocs);
61776185
foreach ($addedDocs as $doc) {
61786186
$this->assertNotEmpty($doc->getPermissions());
61796187
$this->assertCount(3, $doc->getPermissions());
6180-
$this->assertEquals('value', $doc->getAttribute('value'));
6188+
if (!$database->getAdapter()->getSupportForAttributes()) {
6189+
$this->assertNull($doc->getAttribute('value'));
6190+
} else {
6191+
$this->assertEquals('value', $doc->getAttribute('value'));
6192+
}
61816193
}
61826194
$database->deleteCollection($colName);
61836195
}

tests/e2e/Adapter/Scopes/SchemalessTests.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,70 @@ public function testSchemalessRemoveAttributesByUpdate(): void
11961196
$this->assertEquals('single2', $doc->getAttribute('key'));
11971197
$this->assertArrayNotHasKey('extra', $doc);
11981198

1199+
// Test updateDocuments - bulk update with attribute removal
1200+
$docs = [
1201+
new Document(['$id' => 'docA', 'key' => 'keepA', 'extra' => 'removeA', '$permissions' => $permissions]),
1202+
new Document(['$id' => 'docB', 'key' => 'keepB', 'extra' => 'removeB', 'more' => 'data', '$permissions' => $permissions]),
1203+
new Document(['$id' => 'docC', 'key' => 'keepC', 'extra' => 'removeC', '$permissions' => $permissions]),
1204+
];
1205+
$this->assertEquals(3, $database->createDocuments($col, $docs));
1206+
1207+
// Verify all documents have both 'key' and 'extra'
1208+
$allDocs = $database->find($col);
1209+
$this->assertCount(4, $allDocs); // 3 new + 1 old (docS)
1210+
1211+
foreach ($allDocs as $doc) {
1212+
if (in_array($doc->getId(), ['docA', 'docB', 'docC'])) {
1213+
$this->assertArrayHasKey('extra', $doc->getAttributes());
1214+
}
1215+
}
1216+
1217+
// Bulk update all new docs: keep 'key', remove 'extra' and 'more'
1218+
$updatedCount = $database->updateDocuments($col, new Document(['key' => 'updatedBulk']), [Query::startsWith('$id', 'doc'),Query::notEqual('$id', 'docS')]);
1219+
$this->assertEquals(3, $updatedCount);
1220+
1221+
// Verify 'extra' and 'more' fields are removed from all updated docs
1222+
$updatedDocs = $database->find($col, [Query::startsWith('$id', 'doc'),Query::notEqual('$id', 'docS')]);
1223+
$this->assertCount(3, $updatedDocs);
1224+
1225+
foreach ($updatedDocs as $doc) {
1226+
$this->assertEquals('updatedBulk', $doc->getAttribute('key'));
1227+
$this->assertArrayNotHasKey('extra', $doc->getAttributes());
1228+
if ($doc->getId() === 'docB') {
1229+
$this->assertArrayNotHasKey('more', $doc->getAttributes());
1230+
}
1231+
}
1232+
1233+
// Verify docS (not in update query) is unchanged
1234+
$docS = $database->getDocument($col, 'docS');
1235+
$this->assertEquals('single2', $docS->getAttribute('key'));
1236+
$this->assertArrayNotHasKey('extra', $docS);
1237+
1238+
// Update documents with query filter - partial update
1239+
$database->updateDocuments(
1240+
$col,
1241+
new Document(['label' => 'tagged']),
1242+
[Query::equal('$id', ['docA', 'docC'])]
1243+
);
1244+
1245+
$docA = $database->getDocument($col, 'docA');
1246+
$docB = $database->getDocument($col, 'docB');
1247+
$docC = $database->getDocument($col, 'docC');
1248+
1249+
$this->assertEquals('tagged', $docA->getAttribute('label'));
1250+
$this->assertArrayNotHasKey('label', $docB->getAttributes());
1251+
$this->assertEquals('tagged', $docC->getAttribute('label'));
1252+
1253+
// Verify 'key' is still preserved in docB and not in others
1254+
$this->assertEquals('updatedBulk', $docB->getAttribute('key'));
1255+
1256+
foreach (['docA','docC'] as $doc) {
1257+
$this->assertArrayNotHasKey('key', $database->getDocument($col, $doc)->getAttributes());
1258+
}
1259+
1260+
// verify docs is still preserved(untouched)
1261+
$docS = $database->getDocument($col, 'docS');
1262+
$this->assertEquals('single2', $docS->getAttribute('key'));
11991263
$database->deleteCollection($col);
12001264
}
12011265
}

0 commit comments

Comments
 (0)