Skip to content

Commit 8795a7f

Browse files
committed
Merge remote-tracking branch 'origin/3.x'
2 parents 4f0be24 + 3a8d259 commit 8795a7f

File tree

2 files changed

+337
-0
lines changed

2 files changed

+337
-0
lines changed

src/Database/Adapter/Mongo.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,7 @@ public function upsertDocuments(Document $collection, string $attribute, array $
15621562
$operations = [];
15631563
foreach ($changes as $change) {
15641564
$document = $change->getNew();
1565+
$oldDocument = $change->getOld();
15651566
$attributes = $document->getAttributes();
15661567
$attributes['_uid'] = $document->getId();
15671568
$attributes['_createdAt'] = $document['$createdAt'];
@@ -1587,6 +1588,9 @@ public function upsertDocuments(Document $collection, string $attribute, array $
15871588

15881589
unset($record['_id']); // Don't update _id
15891590

1591+
// Get fields to unset for schemaless mode
1592+
$unsetFields = $this->getUpsertAttributeRemovals($oldDocument, $document, $record);
1593+
15901594
if (!empty($attribute)) {
15911595
// Get the attribute value before removing it from $set
15921596
$attributeValue = $record[$attribute] ?? 0;
@@ -1595,17 +1599,28 @@ public function upsertDocuments(Document $collection, string $attribute, array $
15951599
// it is requierd to mimic the behaver of SQL on duplicate key update
15961600
unset($record[$attribute]);
15971601

1602+
// Also remove from unset if it was there
1603+
unset($unsetFields[$attribute]);
1604+
15981605
// Increment the specific attribute and update all other fields
15991606
$update = [
16001607
'$inc' => [$attribute => $attributeValue],
16011608
'$set' => $record
16021609
];
1610+
1611+
if (!empty($unsetFields)) {
1612+
$update['$unset'] = $unsetFields;
1613+
}
16031614
} else {
16041615
// Update all fields
16051616
$update = [
16061617
'$set' => $record
16071618
];
16081619

1620+
if (!empty($unsetFields)) {
1621+
$update['$unset'] = $unsetFields;
1622+
}
1623+
16091624
// Add UUID7 _id for new documents in upsert operations
16101625
if (empty($document->getSequence())) {
16111626
$update['$setOnInsert'] = [
@@ -1634,6 +1649,43 @@ public function upsertDocuments(Document $collection, string $attribute, array $
16341649
return \array_map(fn ($change) => $change->getNew(), $changes);
16351650
}
16361651

1652+
/**
1653+
* Get fields to unset for schemaless upsert operations
1654+
*
1655+
* @param Document $oldDocument
1656+
* @param Document $newDocument
1657+
* @param array<string, mixed> $record
1658+
* @return array<string, string>
1659+
*/
1660+
private function getUpsertAttributeRemovals(Document $oldDocument, Document $newDocument, array $record): array
1661+
{
1662+
$unsetFields = [];
1663+
1664+
if ($this->getSupportForAttributes() || $oldDocument->isEmpty()) {
1665+
return $unsetFields;
1666+
}
1667+
1668+
$oldUserAttributes = $oldDocument->getAttributes();
1669+
$newUserAttributes = $newDocument->getAttributes();
1670+
1671+
$protectedFields = ['_uid', '_id', '_createdAt', '_updatedAt', '_permissions', '_tenant'];
1672+
1673+
foreach ($oldUserAttributes as $originalKey => $originalValue) {
1674+
if (in_array($originalKey, $protectedFields) || array_key_exists($originalKey, $newUserAttributes)) {
1675+
continue;
1676+
}
1677+
1678+
$transformed = $this->replaceChars('$', '_', [$originalKey => $originalValue]);
1679+
$dbKey = array_key_first($transformed);
1680+
1681+
if ($dbKey && !array_key_exists($dbKey, $record) && !in_array($dbKey, $protectedFields)) {
1682+
$unsetFields[$dbKey] = '';
1683+
}
1684+
}
1685+
1686+
return $unsetFields;
1687+
}
1688+
16371689
/**
16381690
* Get sequences for documents that were created
16391691
*

tests/e2e/Adapter/Scopes/SchemalessTests.php

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1864,4 +1864,289 @@ public function testSchemalessNestedObjectAttributeQueries(): void
18641864

18651865
$database->deleteCollection($col);
18661866
}
1867+
1868+
public function testUpsertFieldRemoval(): void
1869+
{
1870+
/** @var Database $database */
1871+
$database = $this->getDatabase();
1872+
1873+
if ($database->getAdapter()->getSupportForAttributes()) {
1874+
$this->markTestSkipped('Adapter supports attributes (schemaful mode). Field removal in upsert is tested in schemaful tests.');
1875+
}
1876+
1877+
$collectionName = ID::unique();
1878+
$database->createCollection($collectionName, permissions: [
1879+
Permission::create(Role::any()),
1880+
Permission::read(Role::any()),
1881+
Permission::update(Role::any()),
1882+
Permission::delete(Role::any()),
1883+
]);
1884+
1885+
$permissions = [
1886+
Permission::read(Role::any()),
1887+
Permission::create(Role::any()),
1888+
Permission::update(Role::any()),
1889+
Permission::delete(Role::any()),
1890+
];
1891+
1892+
// Test 1: Basic field removal with upsertDocument
1893+
// Create a document with multiple fields
1894+
$doc1 = $database->createDocument($collectionName, new Document([
1895+
'$id' => 'doc1',
1896+
'$permissions' => $permissions,
1897+
'title' => 'Original Title',
1898+
'description' => 'Original Description',
1899+
'category' => 'tech',
1900+
'tags' => ['php', 'mongodb'],
1901+
'metadata' => [
1902+
'author' => 'John Doe',
1903+
'version' => 1
1904+
]
1905+
]));
1906+
1907+
$this->assertEquals('Original Title', $doc1->getAttribute('title'));
1908+
$this->assertEquals('Original Description', $doc1->getAttribute('description'));
1909+
$this->assertEquals('tech', $doc1->getAttribute('category'));
1910+
$this->assertArrayHasKey('tags', $doc1->getArrayCopy());
1911+
$this->assertArrayHasKey('metadata', $doc1->getArrayCopy());
1912+
1913+
// Upsert with fewer fields - removed fields should be deleted
1914+
$upserted = $database->upsertDocument($collectionName, new Document([
1915+
'$id' => 'doc1',
1916+
'$permissions' => $permissions,
1917+
'title' => 'Updated Title',
1918+
'category' => 'science',
1919+
// description, tags, and metadata are removed
1920+
]));
1921+
1922+
$this->assertEquals('Updated Title', $upserted->getAttribute('title'));
1923+
$this->assertEquals('science', $upserted->getAttribute('category'));
1924+
1925+
// Verify removed fields are actually deleted
1926+
$retrieved = $database->getDocument($collectionName, 'doc1');
1927+
$this->assertEquals('Updated Title', $retrieved->getAttribute('title'));
1928+
$this->assertEquals('science', $retrieved->getAttribute('category'));
1929+
$this->assertArrayNotHasKey('description', $retrieved->getArrayCopy());
1930+
$this->assertArrayNotHasKey('tags', $retrieved->getArrayCopy());
1931+
$this->assertArrayNotHasKey('metadata', $retrieved->getArrayCopy());
1932+
1933+
// Test 2: Remove all custom fields except one
1934+
$doc2 = $database->createDocument($collectionName, new Document([
1935+
'$id' => 'doc2',
1936+
'$permissions' => $permissions,
1937+
'field1' => 'value1',
1938+
'field2' => 'value2',
1939+
'field3' => 'value3',
1940+
'field4' => 'value4',
1941+
]));
1942+
1943+
// Upsert keeping only field1
1944+
$database->upsertDocument($collectionName, new Document([
1945+
'$id' => 'doc2',
1946+
'$permissions' => $permissions,
1947+
'field1' => 'updated_value1',
1948+
]));
1949+
1950+
$retrieved2 = $database->getDocument($collectionName, 'doc2');
1951+
$this->assertEquals('updated_value1', $retrieved2->getAttribute('field1'));
1952+
$this->assertArrayNotHasKey('field2', $retrieved2->getArrayCopy());
1953+
$this->assertArrayNotHasKey('field3', $retrieved2->getArrayCopy());
1954+
$this->assertArrayNotHasKey('field4', $retrieved2->getArrayCopy());
1955+
1956+
// Test 3: Remove nested object fields
1957+
$doc3 = $database->createDocument($collectionName, new Document([
1958+
'$id' => 'doc3',
1959+
'$permissions' => $permissions,
1960+
'name' => 'Product',
1961+
'details' => [
1962+
'color' => 'red',
1963+
'size' => 'large',
1964+
'weight' => 10
1965+
],
1966+
'specs' => [
1967+
'cpu' => 'Intel',
1968+
'ram' => '8GB'
1969+
]
1970+
]));
1971+
1972+
// Upsert removing details but keeping specs
1973+
$database->upsertDocument($collectionName, new Document([
1974+
'$id' => 'doc3',
1975+
'$permissions' => $permissions,
1976+
'name' => 'Updated Product',
1977+
'specs' => [
1978+
'cpu' => 'AMD',
1979+
'ram' => '16GB'
1980+
],
1981+
// details is removed
1982+
]));
1983+
1984+
$retrieved3 = $database->getDocument($collectionName, 'doc3');
1985+
$this->assertEquals('Updated Product', $retrieved3->getAttribute('name'));
1986+
$this->assertArrayHasKey('specs', $retrieved3->getArrayCopy());
1987+
$this->assertEquals('AMD', $retrieved3->getAttribute('specs')['cpu']);
1988+
$this->assertArrayNotHasKey('details', $retrieved3->getArrayCopy());
1989+
1990+
// Test 4: Remove array fields
1991+
$doc4 = $database->createDocument($collectionName, new Document([
1992+
'$id' => 'doc4',
1993+
'$permissions' => $permissions,
1994+
'title' => 'Article',
1995+
'tags' => ['tag1', 'tag2', 'tag3'],
1996+
'categories' => ['cat1', 'cat2'],
1997+
'comments' => ['comment1', 'comment2']
1998+
]));
1999+
2000+
// Upsert removing tags and comments but keeping categories
2001+
$database->upsertDocument($collectionName, new Document([
2002+
'$id' => 'doc4',
2003+
'$permissions' => $permissions,
2004+
'title' => 'Updated Article',
2005+
'categories' => ['cat3'],
2006+
]));
2007+
2008+
$retrieved4 = $database->getDocument($collectionName, 'doc4');
2009+
$this->assertEquals('Updated Article', $retrieved4->getAttribute('title'));
2010+
$this->assertArrayHasKey('categories', $retrieved4->getArrayCopy());
2011+
$this->assertEquals(['cat3'], $retrieved4->getAttribute('categories'));
2012+
$this->assertArrayNotHasKey('tags', $retrieved4->getArrayCopy());
2013+
$this->assertArrayNotHasKey('comments', $retrieved4->getArrayCopy());
2014+
2015+
// Test 5: upsertDocuments with field removal (bulk upsert)
2016+
$docs5 = [
2017+
new Document([
2018+
'$id' => 'bulk1',
2019+
'$permissions' => $permissions,
2020+
'fieldA' => 'valueA',
2021+
'fieldB' => 'valueB',
2022+
'fieldC' => 'valueC',
2023+
]),
2024+
new Document([
2025+
'$id' => 'bulk2',
2026+
'$permissions' => $permissions,
2027+
'fieldX' => 'valueX',
2028+
'fieldY' => 'valueY',
2029+
'fieldZ' => 'valueZ',
2030+
]),
2031+
];
2032+
$database->createDocuments($collectionName, $docs5);
2033+
2034+
// Upsert removing some fields from each
2035+
$upsertDocs5 = [
2036+
new Document([
2037+
'$id' => 'bulk1',
2038+
'$permissions' => $permissions,
2039+
'fieldA' => 'updatedA',
2040+
// fieldB and fieldC removed
2041+
]),
2042+
new Document([
2043+
'$id' => 'bulk2',
2044+
'$permissions' => $permissions,
2045+
'fieldX' => 'updatedX',
2046+
'fieldZ' => 'updatedZ',
2047+
// fieldY removed
2048+
]),
2049+
];
2050+
$database->upsertDocuments($collectionName, $upsertDocs5);
2051+
2052+
$retrievedBulk1 = $database->getDocument($collectionName, 'bulk1');
2053+
$this->assertEquals('updatedA', $retrievedBulk1->getAttribute('fieldA'));
2054+
$this->assertArrayNotHasKey('fieldB', $retrievedBulk1->getArrayCopy());
2055+
$this->assertArrayNotHasKey('fieldC', $retrievedBulk1->getArrayCopy());
2056+
2057+
$retrievedBulk2 = $database->getDocument($collectionName, 'bulk2');
2058+
$this->assertEquals('updatedX', $retrievedBulk2->getAttribute('fieldX'));
2059+
$this->assertEquals('updatedZ', $retrievedBulk2->getAttribute('fieldZ'));
2060+
$this->assertArrayNotHasKey('fieldY', $retrievedBulk2->getArrayCopy());
2061+
2062+
// Test 6: Upsert creating new document (should not unset anything)
2063+
$newDoc = $database->upsertDocument($collectionName, new Document([
2064+
'$id' => 'newDoc',
2065+
'$permissions' => $permissions,
2066+
'newField' => 'newValue',
2067+
]));
2068+
2069+
$this->assertEquals('newValue', $newDoc->getAttribute('newField'));
2070+
$retrievedNew = $database->getDocument($collectionName, 'newDoc');
2071+
$this->assertEquals('newValue', $retrievedNew->getAttribute('newField'));
2072+
$this->assertArrayHasKey('newField', $retrievedNew->getArrayCopy());
2073+
2074+
// Test 7: Remove all custom fields (keep only system fields)
2075+
$doc7 = $database->createDocument($collectionName, new Document([
2076+
'$id' => 'doc7',
2077+
'$permissions' => $permissions,
2078+
'custom1' => 'value1',
2079+
'custom2' => 'value2',
2080+
'custom3' => 'value3',
2081+
]));
2082+
2083+
// Upsert with only system fields (no custom fields)
2084+
$database->upsertDocument($collectionName, new Document([
2085+
'$id' => 'doc7',
2086+
'$permissions' => $permissions,
2087+
// No custom fields
2088+
]));
2089+
2090+
$retrieved7 = $database->getDocument($collectionName, 'doc7');
2091+
$this->assertArrayNotHasKey('custom1', $retrieved7->getArrayCopy());
2092+
$this->assertArrayNotHasKey('custom2', $retrieved7->getArrayCopy());
2093+
$this->assertArrayNotHasKey('custom3', $retrieved7->getArrayCopy());
2094+
// System fields should still exist
2095+
$this->assertEquals('doc7', $retrieved7->getId());
2096+
$this->assertNotNull($retrieved7->getCreatedAt());
2097+
$this->assertNotNull($retrieved7->getUpdatedAt());
2098+
2099+
// Test 8: Mixed scenario - add new fields while removing others
2100+
$doc8 = $database->createDocument($collectionName, new Document([
2101+
'$id' => 'doc8',
2102+
'$permissions' => $permissions,
2103+
'oldField1' => 'old1',
2104+
'oldField2' => 'old2',
2105+
'keepField' => 'keep',
2106+
]));
2107+
2108+
// Upsert removing oldField1 and oldField2, keeping keepField, adding newField
2109+
$database->upsertDocument($collectionName, new Document([
2110+
'$id' => 'doc8',
2111+
'$permissions' => $permissions,
2112+
'keepField' => 'updatedKeep',
2113+
'newField' => 'newValue',
2114+
]));
2115+
2116+
$retrieved8 = $database->getDocument($collectionName, 'doc8');
2117+
$this->assertEquals('updatedKeep', $retrieved8->getAttribute('keepField'));
2118+
$this->assertEquals('newValue', $retrieved8->getAttribute('newField'));
2119+
$this->assertArrayNotHasKey('oldField1', $retrieved8->getArrayCopy());
2120+
$this->assertArrayNotHasKey('oldField2', $retrieved8->getArrayCopy());
2121+
2122+
// Test 9: Verify internal/system fields are never removed
2123+
$doc9 = $database->createDocument($collectionName, new Document([
2124+
'$id' => 'doc9',
2125+
'$permissions' => $permissions,
2126+
'data' => 'test',
2127+
]));
2128+
2129+
$originalCreatedAt = $doc9->getCreatedAt();
2130+
$originalUpdatedAt = $doc9->getUpdatedAt();
2131+
2132+
// Upsert - internal fields should be preserved
2133+
$database->upsertDocument($collectionName, new Document([
2134+
'$id' => 'doc9',
2135+
'$permissions' => $permissions,
2136+
'newData' => 'newTest',
2137+
]));
2138+
2139+
$retrieved9 = $database->getDocument($collectionName, 'doc9');
2140+
// System fields should still exist
2141+
$this->assertEquals('doc9', $retrieved9->getId());
2142+
$this->assertEquals($originalCreatedAt, $retrieved9->getCreatedAt());
2143+
// UpdatedAt should be different (document was updated)
2144+
$this->assertNotEquals($originalUpdatedAt, $retrieved9->getUpdatedAt());
2145+
$this->assertEquals('newTest', $retrieved9->getAttribute('newData'));
2146+
// Old field should be removed
2147+
$this->assertArrayNotHasKey('data', $retrieved9->getArrayCopy());
2148+
2149+
// Clean up
2150+
$database->deleteCollection($collectionName);
2151+
}
18672152
}

0 commit comments

Comments
 (0)