Skip to content

Commit 35c978d

Browse files
authored
Merge pull request #732 from utopia-php/fix-bulk-update-encode
Fix bulk update encoding with defaults
2 parents a91e040 + b1f51f1 commit 35c978d

File tree

3 files changed

+120
-5
lines changed

3 files changed

+120
-5
lines changed

src/Database/Database.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5013,7 +5013,12 @@ public function updateDocuments(
50135013
$updatedAt = $updates->getUpdatedAt();
50145014
$updates['$updatedAt'] = ($updatedAt === null || !$this->preserveDates) ? DateTime::now() : $updatedAt;
50155015

5016-
$updates = $this->encode($collection, $updates);
5016+
$updates = $this->encode(
5017+
$collection,
5018+
$updates,
5019+
applyDefaults: false
5020+
);
5021+
50175022
// Check new document structure
50185023
$validator = new PartialStructure(
50195024
$collection,
@@ -7051,11 +7056,12 @@ public static function addFilter(string $name, callable $encode, callable $decod
70517056
*
70527057
* @param Document $collection
70537058
* @param Document $document
7059+
* @param bool $applyDefaults Whether to apply default values to null attributes
70547060
*
70557061
* @return Document
70567062
* @throws DatabaseException
70577063
*/
7058-
public function encode(Document $collection, Document $document): Document
7064+
public function encode(Document $collection, Document $document, bool $applyDefaults = true): Document
70597065
{
70607066
$attributes = $collection->getAttribute('attributes', []);
70617067
$internalDateAttributes = ['$createdAt', '$updatedAt'];
@@ -7088,6 +7094,10 @@ public function encode(Document $collection, Document $document): Document
70887094
// False positive "Call to function is_null() with mixed will always evaluate to false"
70897095
// @phpstan-ignore-next-line
70907096
if (is_null($value) && !is_null($default)) {
7097+
// Skip applying defaults during updates to avoid resetting unspecified attributes
7098+
if (!$applyDefaults) {
7099+
continue;
7100+
}
70917101
$value = ($array) ? $default : [$default];
70927102
} else {
70937103
$value = ($array) ? $value : [$value];

tests/e2e/Adapter/Scopes/DocumentTests.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6433,4 +6433,109 @@ function (mixed $value) { return str_replace('prefix_', '', $value); }
64336433

64346434
$database->deleteCollection($collectionId);
64356435
}
6436+
6437+
public function testUpdateDocumentsSuccessiveCallsDoNotResetDefaults(): void
6438+
{
6439+
/** @var Database $database */
6440+
$database = static::getDatabase();
6441+
6442+
if (!$database->getAdapter()->getSupportForBatchOperations()) {
6443+
$this->expectNotToPerformAssertions();
6444+
return;
6445+
}
6446+
6447+
$collectionId = 'successive_updates';
6448+
Authorization::cleanRoles();
6449+
Authorization::setRole(Role::any()->toString());
6450+
6451+
// Create collection with two attributes that have default values
6452+
$database->createCollection($collectionId);
6453+
$database->createAttribute($collectionId, 'attrA', Database::VAR_STRING, 50, false, 'defaultA');
6454+
$database->createAttribute($collectionId, 'attrB', Database::VAR_STRING, 50, false, 'defaultB');
6455+
6456+
// Create a document without setting attrA or attrB (should use defaults)
6457+
$doc = $database->createDocument($collectionId, new Document([
6458+
'$id' => 'testdoc',
6459+
'$permissions' => [
6460+
Permission::read(Role::any()),
6461+
Permission::update(Role::any()),
6462+
],
6463+
]));
6464+
6465+
// Verify initial defaults
6466+
$this->assertEquals('defaultA', $doc->getAttribute('attrA'));
6467+
$this->assertEquals('defaultB', $doc->getAttribute('attrB'));
6468+
6469+
// First update: set attrA to a new value
6470+
$count = $database->updateDocuments($collectionId, new Document([
6471+
'attrA' => 'updatedA',
6472+
]));
6473+
$this->assertEquals(1, $count);
6474+
6475+
// Verify attrA was updated
6476+
$doc = $database->getDocument($collectionId, 'testdoc');
6477+
$this->assertEquals('updatedA', $doc->getAttribute('attrA'));
6478+
$this->assertEquals('defaultB', $doc->getAttribute('attrB'));
6479+
6480+
// Second update: set attrB to a new value
6481+
$count = $database->updateDocuments($collectionId, new Document([
6482+
'attrB' => 'updatedB',
6483+
]));
6484+
$this->assertEquals(1, $count);
6485+
6486+
// Verify attrB was updated AND attrA is still 'updatedA' (not reset to 'defaultA')
6487+
$doc = $database->getDocument($collectionId, 'testdoc');
6488+
$this->assertEquals('updatedA', $doc->getAttribute('attrA'), 'attrA should not be reset to default');
6489+
$this->assertEquals('updatedB', $doc->getAttribute('attrB'));
6490+
6491+
$database->deleteCollection($collectionId);
6492+
}
6493+
6494+
public function testUpdateDocumentSuccessiveCallsDoNotResetDefaults(): void
6495+
{
6496+
/** @var Database $database */
6497+
$database = static::getDatabase();
6498+
6499+
$collectionId = 'successive_update_single';
6500+
Authorization::cleanRoles();
6501+
Authorization::setRole(Role::any()->toString());
6502+
6503+
// Create collection with two attributes that have default values
6504+
$database->createCollection($collectionId);
6505+
$database->createAttribute($collectionId, 'attrA', Database::VAR_STRING, 50, false, 'defaultA');
6506+
$database->createAttribute($collectionId, 'attrB', Database::VAR_STRING, 50, false, 'defaultB');
6507+
6508+
// Create a document without setting attrA or attrB (should use defaults)
6509+
$doc = $database->createDocument($collectionId, new Document([
6510+
'$id' => 'testdoc',
6511+
'$permissions' => [
6512+
Permission::read(Role::any()),
6513+
Permission::update(Role::any()),
6514+
],
6515+
]));
6516+
6517+
// Verify initial defaults
6518+
$this->assertEquals('defaultA', $doc->getAttribute('attrA'));
6519+
$this->assertEquals('defaultB', $doc->getAttribute('attrB'));
6520+
6521+
// First update: set attrA to a new value
6522+
$doc = $database->updateDocument($collectionId, 'testdoc', new Document([
6523+
'$id' => 'testdoc',
6524+
'attrA' => 'updatedA',
6525+
]));
6526+
$this->assertEquals('updatedA', $doc->getAttribute('attrA'));
6527+
$this->assertEquals('defaultB', $doc->getAttribute('attrB'));
6528+
6529+
// Second update: set attrB to a new value
6530+
$doc = $database->updateDocument($collectionId, 'testdoc', new Document([
6531+
'$id' => 'testdoc',
6532+
'attrB' => 'updatedB',
6533+
]));
6534+
6535+
// Verify attrB was updated AND attrA is still 'updatedA' (not reset to 'defaultA')
6536+
$this->assertEquals('updatedA', $doc->getAttribute('attrA'), 'attrA should not be reset to default');
6537+
$this->assertEquals('updatedB', $doc->getAttribute('attrB'));
6538+
6539+
$database->deleteCollection($collectionId);
6540+
}
64366541
}

tests/e2e/Adapter/Scopes/RelationshipTests.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4355,7 +4355,7 @@ public function testCountAndSumWithRelationshipQueries(): void
43554355
Query::lessThan('author.age', 30),
43564356
Query::equal('published', [true]),
43574357
]);
4358-
$this->assertEquals(1, $count); // Only Bob's published post
4358+
$this->assertEquals(1, $count);
43594359

43604360
// Count posts by author name (different author)
43614361
$count = $database->count('postsCount', [
@@ -4380,13 +4380,13 @@ public function testCountAndSumWithRelationshipQueries(): void
43804380
Query::lessThan('author.age', 30),
43814381
Query::equal('published', [true]),
43824382
]);
4383-
$this->assertEquals(150, $sum); // Only Bob's published post
4383+
$this->assertEquals(150, $sum);
43844384

43854385
// Sum views for Bob's posts
43864386
$sum = $database->sum('postsCount', 'views', [
43874387
Query::equal('author.name', ['Bob']),
43884388
]);
4389-
$this->assertEquals(225, $sum); // 150 + 75
4389+
$this->assertEquals(225, $sum);
43904390

43914391
// Sum with no matches
43924392
$sum = $database->sum('postsCount', 'views', [

0 commit comments

Comments
 (0)