Skip to content

Commit 53bfc16

Browse files
replacement style update documents for schemaless
1 parent 0598fe7 commit 53bfc16

3 files changed

Lines changed: 146 additions & 17 deletions

File tree

src/Database/Adapter/Mongo.php

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,38 +1474,95 @@ public function updateDocument(Document $collection, string $id, Document $docum
14741474
public function updateDocuments(Document $collection, Document $updates, array $documents): int
14751475
{
14761476
$name = $this->getNamespace() . '_' . $this->filter($collection->getId());
1477-
14781477
$options = $this->getTransactionOptions();
1478+
1479+
// Build filter for all documents in this batch
14791480
$queries = [
1480-
Query::equal('$sequence', \array_map(fn ($document) => $document->getSequence(), $documents))
1481+
Query::equal('$sequence', array_map(fn ($doc) => $doc->getSequence(), $documents))
14811482
];
1482-
14831483
$filters = $this->buildFilters($queries);
14841484

14851485
if ($this->sharedTables) {
14861486
$filters['_tenant'] = $this->getTenantFilters($collection->getId());
14871487
}
14881488

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

14961491
try {
1497-
return $this->client->update(
1498-
$name,
1499-
$filters,
1500-
$updateQuery,
1501-
options: $options,
1502-
multi: true,
1503-
);
1492+
/**
1493+
* Case 1: Normal attribute-supported mode — bulk update
1494+
*/
1495+
if ($this->getSupportForAttributes()) {
1496+
$updateQuery = [
1497+
'$set' => $record,
1498+
];
1499+
1500+
return $this->client->update(
1501+
$name,
1502+
$filters,
1503+
$updateQuery,
1504+
options: $options,
1505+
multi: true
1506+
);
1507+
}
1508+
1509+
/**
1510+
* Case 2: Schemaless mode — emulate replacement with $set + $unset per document
1511+
*/
1512+
$modified = 0;
1513+
foreach ($documents as $doc) {
1514+
$seq = $doc->getSequence();
1515+
$perDocFilters = ['_id' => $seq];
1516+
unset($doc['$skipPermissionsUpdate']);
1517+
if ($this->sharedTables) {
1518+
$perDocFilters['_tenant'] = $this->getTenantFilters($collection->getId());
1519+
}
1520+
1521+
// Fetch current persisted document
1522+
$currentCursor = $this->client->find($name, $perDocFilters, $options)->cursor->firstBatch ?? [];
1523+
$currentArray = !empty($currentCursor)
1524+
? $this->client->toArray($currentCursor[0])
1525+
: [];
1526+
1527+
// Prepare new document body
1528+
$newDocArray = $this->replaceChars('$', '_', (array) $doc);
1529+
1530+
// Preserve internal keys (never unset)
1531+
$internalKeys = ['_id', '_uid', '_collection', '_createdAt', '_updatedAt', '_permissions', '_tenant'];
1532+
1533+
// Determine fields to unset (removed ones only)
1534+
$unsetKeys = array_diff(
1535+
array_keys($currentArray),
1536+
array_keys($newDocArray),
1537+
$internalKeys
1538+
);
1539+
1540+
// Build update query
1541+
$updateQuery = [
1542+
'$set' => $newDocArray,
1543+
];
1544+
1545+
if (!empty($unsetKeys)) {
1546+
$updateQuery['$unset'] = array_fill_keys($unsetKeys, 1);
1547+
}
1548+
$this->client->update(
1549+
$name,
1550+
$perDocFilters,
1551+
$updateQuery,
1552+
options: $options,
1553+
multi: false
1554+
);
1555+
1556+
$modified++;
1557+
}
1558+
1559+
return $modified;
15041560
} catch (MongoException $e) {
15051561
throw $this->processException($e);
15061562
}
15071563
}
15081564

1565+
15091566
/**
15101567
* @param Document $collection
15111568
* @param string $attribute

src/Database/Database.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5274,7 +5274,15 @@ public function updateDocuments(
52745274

52755275
$document->setAttribute('$skipPermissionsUpdate', $skipPermissionsUpdate);
52765276

5277-
$new = new Document(\array_merge($document->getArrayCopy(), $updates->getArrayCopy()));
5277+
if ($this->adapter->getSupportForAttributes()) {
5278+
$new = new Document(\array_merge($document->getArrayCopy(), $updates->getArrayCopy()));
5279+
} else {
5280+
$oldArray = $document->getArrayCopy();
5281+
$newArray = $updates->getArrayCopy();
5282+
$internalKeys = array_map(fn ($attr) => $attr['$id'], self::INTERNAL_ATTRIBUTES);
5283+
$internalAttrs = array_intersect_key($oldArray, array_flip($internalKeys));
5284+
$new = new Document(array_merge($internalAttrs, $newArray));
5285+
}
52785286

52795287
if ($this->resolveRelationships) {
52805288
$this->silent(fn () => $this->updateDocumentRelationships($collection, $document, $new));

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)