Skip to content

Commit ac657c3

Browse files
authored
Merge branch 'main' into feat-rel-id-test
2 parents cdf2310 + f3c3c8a commit ac657c3

8 files changed

Lines changed: 1134 additions & 101 deletions

File tree

composer.lock

Lines changed: 96 additions & 95 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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
@@ -4601,7 +4601,7 @@ public function testCountAndSumWithRelationshipQueries(): void
46014601
Query::lessThan('author.age', 30),
46024602
Query::equal('published', [true]),
46034603
]);
4604-
$this->assertEquals(1, $count); // Only Bob's published post
4604+
$this->assertEquals(1, $count);
46054605

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

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

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

tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,4 +1939,158 @@ public function testDeleteDocumentsRelationshipErrorDoesNotDeleteParent_ManyToMa
19391939
$database->deleteCollection($parentCollection);
19401940
$database->deleteCollection($childCollection);
19411941
}
1942+
1943+
public function testPartialUpdateManyToManyBothSides(): void
1944+
{
1945+
/** @var Database $database */
1946+
$database = static::getDatabase();
1947+
1948+
if (!$database->getAdapter()->getSupportForRelationships()) {
1949+
$this->expectNotToPerformAssertions();
1950+
return;
1951+
}
1952+
1953+
$database->createCollection('partial_students');
1954+
$database->createCollection('partial_courses');
1955+
1956+
$database->createAttribute('partial_students', 'name', Database::VAR_STRING, 255, true);
1957+
$database->createAttribute('partial_students', 'grade', Database::VAR_STRING, 10, false);
1958+
$database->createAttribute('partial_courses', 'title', Database::VAR_STRING, 255, true);
1959+
$database->createAttribute('partial_courses', 'credits', Database::VAR_INTEGER, 0, false);
1960+
1961+
$database->createRelationship(
1962+
collection: 'partial_students',
1963+
relatedCollection: 'partial_courses',
1964+
type: Database::RELATION_MANY_TO_MANY,
1965+
twoWay: true,
1966+
id: 'partial_courses',
1967+
twoWayKey: 'partial_students'
1968+
);
1969+
1970+
// Create student with courses
1971+
$database->createDocument('partial_students', new Document([
1972+
'$id' => 'student1',
1973+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
1974+
'name' => 'David',
1975+
'grade' => 'A',
1976+
'partial_courses' => [
1977+
['$id' => 'course1', '$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())], 'title' => 'Math', 'credits' => 3],
1978+
['$id' => 'course2', '$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())], 'title' => 'Science', 'credits' => 4],
1979+
],
1980+
]));
1981+
1982+
// Partial update from student side - update grade only, preserve courses
1983+
$database->updateDocument('partial_students', 'student1', new Document([
1984+
'$id' => 'student1',
1985+
'$collection' => 'partial_students',
1986+
'grade' => 'A+',
1987+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
1988+
]));
1989+
1990+
$student = $database->getDocument('partial_students', 'student1');
1991+
$this->assertEquals('David', $student->getAttribute('name'), 'Name should be preserved');
1992+
$this->assertEquals('A+', $student->getAttribute('grade'), 'Grade should be updated');
1993+
$this->assertCount(2, $student->getAttribute('partial_courses'), 'Courses should be preserved');
1994+
1995+
// Partial update from course side - update credits only, preserve students
1996+
$database->updateDocument('partial_courses', 'course1', new Document([
1997+
'$id' => 'course1',
1998+
'$collection' => 'partial_courses',
1999+
'credits' => 5,
2000+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
2001+
]));
2002+
2003+
$course = $database->getDocument('partial_courses', 'course1');
2004+
$this->assertEquals('Math', $course->getAttribute('title'), 'Title should be preserved');
2005+
$this->assertEquals(5, $course->getAttribute('credits'), 'Credits should be updated');
2006+
$this->assertCount(1, $course->getAttribute('partial_students'), 'Students should be preserved');
2007+
2008+
$database->deleteCollection('partial_students');
2009+
$database->deleteCollection('partial_courses');
2010+
}
2011+
2012+
public function testPartialUpdateManyToManyWithStringIdsAndDocuments(): void
2013+
{
2014+
/** @var Database $database */
2015+
$database = static::getDatabase();
2016+
2017+
if (!$database->getAdapter()->getSupportForRelationships()) {
2018+
$this->expectNotToPerformAssertions();
2019+
return;
2020+
}
2021+
2022+
$database->createCollection('tags');
2023+
$database->createCollection('articles');
2024+
2025+
$database->createAttribute('tags', 'name', Database::VAR_STRING, 255, true);
2026+
$database->createAttribute('tags', 'color', Database::VAR_STRING, 50, false);
2027+
$database->createAttribute('articles', 'title', Database::VAR_STRING, 255, true);
2028+
$database->createAttribute('articles', 'published', Database::VAR_BOOLEAN, 0, false);
2029+
2030+
$database->createRelationship(
2031+
collection: 'articles',
2032+
relatedCollection: 'tags',
2033+
type: Database::RELATION_MANY_TO_MANY,
2034+
twoWay: true,
2035+
id: 'tags',
2036+
twoWayKey: 'articles'
2037+
);
2038+
2039+
// Create article with tags
2040+
$database->createDocument('articles', new Document([
2041+
'$id' => 'article1',
2042+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
2043+
'title' => 'Great Article',
2044+
'published' => false,
2045+
'tags' => [
2046+
['$id' => 'tag1', '$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())], 'name' => 'Tech', 'color' => 'blue'],
2047+
],
2048+
]));
2049+
2050+
$database->createDocument('tags', new Document([
2051+
'$id' => 'tag2',
2052+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
2053+
'name' => 'News',
2054+
'color' => 'red',
2055+
]));
2056+
2057+
// Update using STRING IDs
2058+
$database->updateDocument('articles', 'article1', new Document([
2059+
'$id' => 'article1',
2060+
'$collection' => 'articles',
2061+
'tags' => ['tag1', 'tag2'], // String IDs
2062+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
2063+
]));
2064+
2065+
$article = $database->getDocument('articles', 'article1');
2066+
$this->assertEquals('Great Article', $article->getAttribute('title'));
2067+
$this->assertFalse($article->getAttribute('published'));
2068+
$this->assertCount(2, $article->getAttribute('tags'));
2069+
2070+
// Update from tag side using DOCUMENT objects
2071+
$database->createDocument('articles', new Document([
2072+
'$id' => 'article2',
2073+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
2074+
'title' => 'Another Article',
2075+
'published' => true,
2076+
]));
2077+
2078+
$database->updateDocument('tags', 'tag1', new Document([
2079+
'$id' => 'tag1',
2080+
'$collection' => 'tags',
2081+
'articles' => [ // Document objects
2082+
new Document(['$id' => 'article1']),
2083+
new Document(['$id' => 'article2']),
2084+
],
2085+
'$permissions' => [Permission::read(Role::any()), Permission::update(Role::any())],
2086+
]));
2087+
2088+
$tag = $database->getDocument('tags', 'tag1');
2089+
$this->assertEquals('Tech', $tag->getAttribute('name'));
2090+
$this->assertEquals('blue', $tag->getAttribute('color'));
2091+
$this->assertCount(2, $tag->getAttribute('articles'));
2092+
2093+
$database->deleteCollection('tags');
2094+
$database->deleteCollection('articles');
2095+
}
19422096
}

0 commit comments

Comments
 (0)