Skip to content

Commit 48b0cf1

Browse files
added unset for schemaless upsert
1 parent e491ed2 commit 48b0cf1

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-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->getUnsetForSchemalessUpsert($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'] = [
@@ -3395,4 +3410,41 @@ public function getSupportForTrigramIndex(): bool
33953410
{
33963411
return false;
33973412
}
3413+
3414+
/**
3415+
* Get fields to unset for schemaless upsert operations
3416+
*
3417+
* @param Document $oldDocument
3418+
* @param Document $newDocument
3419+
* @param array<string, mixed> $record
3420+
* @return array<string, string>
3421+
*/
3422+
private function getUnsetForSchemalessUpsert(Document $oldDocument, Document $newDocument, array $record): array
3423+
{
3424+
$unsetFields = [];
3425+
3426+
if ($this->getSupportForAttributes() || $oldDocument->isEmpty()) {
3427+
return $unsetFields;
3428+
}
3429+
3430+
$oldUserAttributes = $oldDocument->getAttributes();
3431+
$newUserAttributes = $newDocument->getAttributes();
3432+
3433+
$protectedFields = ['_uid', '_id', '_createdAt', '_updatedAt', '_permissions', '_tenant'];
3434+
3435+
foreach ($oldUserAttributes as $originalKey => $originalValue) {
3436+
if (in_array($originalKey, $protectedFields) || array_key_exists($originalKey, $newUserAttributes)) {
3437+
continue;
3438+
}
3439+
3440+
$transformed = $this->replaceChars('$', '_', [$originalKey => $originalValue]);
3441+
$dbKey = array_key_first($transformed);
3442+
3443+
if ($dbKey && !array_key_exists($dbKey, $record) && !in_array($dbKey, $protectedFields)) {
3444+
$unsetFields[$dbKey] = '';
3445+
}
3446+
}
3447+
3448+
return $unsetFields;
3449+
}
33983450
}

tests/e2e/Adapter/Scopes/SchemalessTests.php

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,4 +1865,290 @@ public function testSchemalessNestedObjectAttributeQueries(): void
18651865

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

0 commit comments

Comments
 (0)