Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -5013,7 +5013,12 @@ public function updateDocuments(
$updatedAt = $updates->getUpdatedAt();
$updates['$updatedAt'] = ($updatedAt === null || !$this->preserveDates) ? DateTime::now() : $updatedAt;

$updates = $this->encode($collection, $updates);
$updates = $this->encode(
$collection,
$updates,
applyDefaults: false
);

// Check new document structure
$validator = new PartialStructure(
$collection,
Expand Down Expand Up @@ -7051,11 +7056,12 @@ public static function addFilter(string $name, callable $encode, callable $decod
*
* @param Document $collection
* @param Document $document
* @param bool $applyDefaults Whether to apply default values to null attributes
*
* @return Document
* @throws DatabaseException
*/
public function encode(Document $collection, Document $document): Document
public function encode(Document $collection, Document $document, bool $applyDefaults = true): Document
{
$attributes = $collection->getAttribute('attributes', []);
$internalDateAttributes = ['$createdAt', '$updatedAt'];
Expand Down Expand Up @@ -7088,6 +7094,10 @@ public function encode(Document $collection, Document $document): Document
// False positive "Call to function is_null() with mixed will always evaluate to false"
// @phpstan-ignore-next-line
if (is_null($value) && !is_null($default)) {
// Skip applying defaults during updates to avoid resetting unspecified attributes
if (!$applyDefaults) {
continue;
}
$value = ($array) ? $default : [$default];
} else {
$value = ($array) ? $value : [$value];
Expand Down
105 changes: 105 additions & 0 deletions tests/e2e/Adapter/Scopes/DocumentTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -6433,4 +6433,109 @@ function (mixed $value) { return str_replace('prefix_', '', $value); }

$database->deleteCollection($collectionId);
}

public function testUpdateDocumentsSuccessiveCallsDoNotResetDefaults(): void
{
/** @var Database $database */
$database = static::getDatabase();

if (!$database->getAdapter()->getSupportForBatchOperations()) {
$this->expectNotToPerformAssertions();
return;
}

$collectionId = 'successive_updates';
Authorization::cleanRoles();
Authorization::setRole(Role::any()->toString());

// Create collection with two attributes that have default values
$database->createCollection($collectionId);
$database->createAttribute($collectionId, 'attrA', Database::VAR_STRING, 50, false, 'defaultA');
$database->createAttribute($collectionId, 'attrB', Database::VAR_STRING, 50, false, 'defaultB');

// Create a document without setting attrA or attrB (should use defaults)
$doc = $database->createDocument($collectionId, new Document([
'$id' => 'testdoc',
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
],
]));

// Verify initial defaults
$this->assertEquals('defaultA', $doc->getAttribute('attrA'));
$this->assertEquals('defaultB', $doc->getAttribute('attrB'));

// First update: set attrA to a new value
$count = $database->updateDocuments($collectionId, new Document([
'attrA' => 'updatedA',
]));
$this->assertEquals(1, $count);

// Verify attrA was updated
$doc = $database->getDocument($collectionId, 'testdoc');
$this->assertEquals('updatedA', $doc->getAttribute('attrA'));
$this->assertEquals('defaultB', $doc->getAttribute('attrB'));

// Second update: set attrB to a new value
$count = $database->updateDocuments($collectionId, new Document([
'attrB' => 'updatedB',
]));
$this->assertEquals(1, $count);

// Verify attrB was updated AND attrA is still 'updatedA' (not reset to 'defaultA')
$doc = $database->getDocument($collectionId, 'testdoc');
$this->assertEquals('updatedA', $doc->getAttribute('attrA'), 'attrA should not be reset to default');
$this->assertEquals('updatedB', $doc->getAttribute('attrB'));

$database->deleteCollection($collectionId);
}

public function testUpdateDocumentSuccessiveCallsDoNotResetDefaults(): void
{
/** @var Database $database */
$database = static::getDatabase();

$collectionId = 'successive_update_single';
Authorization::cleanRoles();
Authorization::setRole(Role::any()->toString());

// Create collection with two attributes that have default values
$database->createCollection($collectionId);
$database->createAttribute($collectionId, 'attrA', Database::VAR_STRING, 50, false, 'defaultA');
$database->createAttribute($collectionId, 'attrB', Database::VAR_STRING, 50, false, 'defaultB');

// Create a document without setting attrA or attrB (should use defaults)
$doc = $database->createDocument($collectionId, new Document([
'$id' => 'testdoc',
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
],
]));

// Verify initial defaults
$this->assertEquals('defaultA', $doc->getAttribute('attrA'));
$this->assertEquals('defaultB', $doc->getAttribute('attrB'));

// First update: set attrA to a new value
$doc = $database->updateDocument($collectionId, 'testdoc', new Document([
'$id' => 'testdoc',
'attrA' => 'updatedA',
]));
$this->assertEquals('updatedA', $doc->getAttribute('attrA'));
$this->assertEquals('defaultB', $doc->getAttribute('attrB'));

// Second update: set attrB to a new value
$doc = $database->updateDocument($collectionId, 'testdoc', new Document([
'$id' => 'testdoc',
'attrB' => 'updatedB',
]));

// Verify attrB was updated AND attrA is still 'updatedA' (not reset to 'defaultA')
$this->assertEquals('updatedA', $doc->getAttribute('attrA'), 'attrA should not be reset to default');
$this->assertEquals('updatedB', $doc->getAttribute('attrB'));

$database->deleteCollection($collectionId);
}
}
6 changes: 3 additions & 3 deletions tests/e2e/Adapter/Scopes/RelationshipTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -4355,7 +4355,7 @@ public function testCountAndSumWithRelationshipQueries(): void
Query::lessThan('author.age', 30),
Query::equal('published', [true]),
]);
$this->assertEquals(1, $count); // Only Bob's published post
$this->assertEquals(1, $count);

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

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

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