@@ -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