@@ -1155,4 +1155,227 @@ public function testSchemalessDates(): void
11551155
11561156 $ database ->deleteCollection ($ col );
11571157 }
1158+
1159+ public function testSchemalessExists (): void
1160+ {
1161+ /** @var Database $database */
1162+ $ database = static ::getDatabase ();
1163+
1164+ if ($ database ->getAdapter ()->getSupportForAttributes ()) {
1165+ $ this ->expectNotToPerformAssertions ();
1166+ return ;
1167+ }
1168+
1169+ $ colName = uniqid ('schemaless_exists ' );
1170+ $ database ->createCollection ($ colName );
1171+
1172+ $ permissions = [
1173+ Permission::read (Role::any ()),
1174+ Permission::write (Role::any ()),
1175+ Permission::update (Role::any ()),
1176+ Permission::delete (Role::any ())
1177+ ];
1178+
1179+ // Create documents with and without the 'optionalField' attribute
1180+ $ docs = [
1181+ new Document (['$id ' => 'doc1 ' , '$permissions ' => $ permissions , 'optionalField ' => 'value1 ' , 'name ' => 'doc1 ' ]),
1182+ new Document (['$id ' => 'doc2 ' , '$permissions ' => $ permissions , 'optionalField ' => 'value2 ' , 'name ' => 'doc2 ' ]),
1183+ new Document (['$id ' => 'doc3 ' , '$permissions ' => $ permissions , 'name ' => 'doc3 ' ]), // no optionalField
1184+ new Document (['$id ' => 'doc4 ' , '$permissions ' => $ permissions , 'optionalField ' => null , 'name ' => 'doc4 ' ]), // exists but null
1185+ new Document (['$id ' => 'doc5 ' , '$permissions ' => $ permissions , 'name ' => 'doc5 ' ]), // no optionalField
1186+ ];
1187+ $ this ->assertEquals (5 , $ database ->createDocuments ($ colName , $ docs ));
1188+
1189+ // Test exists - should return documents where optionalField exists (even if null)
1190+ $ documents = $ database ->find ($ colName , [
1191+ Query::exists (['optionalField ' ]),
1192+ ]);
1193+
1194+ $ this ->assertEquals (3 , count ($ documents )); // doc1, doc2, doc4
1195+ $ ids = array_map (fn ($ doc ) => $ doc ->getId (), $ documents );
1196+ $ this ->assertContains ('doc1 ' , $ ids );
1197+ $ this ->assertContains ('doc2 ' , $ ids );
1198+ $ this ->assertContains ('doc4 ' , $ ids );
1199+
1200+ // Verify that doc4 is included even though optionalField is null
1201+ $ doc4 = array_filter ($ documents , fn ($ doc ) => $ doc ->getId () === 'doc4 ' );
1202+ $ this ->assertCount (1 , $ doc4 );
1203+ $ doc4Array = array_values ($ doc4 );
1204+ $ this ->assertTrue (array_key_exists ('optionalField ' , $ doc4Array [0 ]->getAttributes ()));
1205+
1206+ // Test exists with another attribute
1207+ $ documents = $ database ->find ($ colName , [
1208+ Query::exists (['name ' ]),
1209+ ]);
1210+ $ this ->assertEquals (5 , count ($ documents )); // All documents have 'name'
1211+
1212+ // Test exists with non-existent attribute
1213+ $ documents = $ database ->find ($ colName , [
1214+ Query::exists (['nonExistentField ' ]),
1215+ ]);
1216+ $ this ->assertEquals (0 , count ($ documents ));
1217+
1218+ // Multiple attributes in a single exists query (OR semantics)
1219+ $ documents = $ database ->find ($ colName , [
1220+ Query::exists (['optionalField ' , 'name ' ]),
1221+ ]);
1222+ // All documents have "name", some also have "optionalField"
1223+ $ this ->assertEquals (5 , count ($ documents ));
1224+
1225+ // Multiple attributes where only one exists on some documents
1226+ $ documents = $ database ->find ($ colName , [
1227+ Query::exists (['optionalField ' , 'nonExistentField ' ]),
1228+ ]);
1229+ // Only documents where optionalField exists should be returned
1230+ $ this ->assertEquals (3 , count ($ documents )); // doc1, doc2, doc4
1231+
1232+ // Multiple attributes where none exist should return empty
1233+ $ documents = $ database ->find ($ colName , [
1234+ Query::exists (['nonExistentField ' , 'alsoMissing ' ]),
1235+ ]);
1236+ $ this ->assertEquals (0 , count ($ documents ));
1237+
1238+ // Multiple attributes including one present on all docs still returns all (OR)
1239+ $ documents = $ database ->find ($ colName , [
1240+ Query::exists (['name ' , 'nonExistentField ' , 'alsoMissing ' ]),
1241+ ]);
1242+ $ this ->assertEquals (5 , count ($ documents ));
1243+
1244+ // Multiple exists queries (AND semantics)
1245+ $ documents = $ database ->find ($ colName , [
1246+ Query::exists (['optionalField ' ]),
1247+ Query::exists (['name ' ]),
1248+ ]);
1249+ // Documents must have both attributes
1250+ $ this ->assertEquals (3 , count ($ documents )); // doc1, doc2, doc4
1251+
1252+ // Nested OR with exists (optionalField OR nonExistentField) AND name
1253+ $ documents = $ database ->find ($ colName , [
1254+ Query::and ([
1255+ Query::or ([
1256+ Query::exists (['optionalField ' ]),
1257+ Query::exists (['nonExistentField ' ]),
1258+ ]),
1259+ Query::exists (['name ' ]),
1260+ ]),
1261+ ]);
1262+ $ this ->assertEquals (3 , count ($ documents )); // doc1, doc2, doc4
1263+
1264+ // Nested OR with only missing attributes should yield empty
1265+ $ documents = $ database ->find ($ colName , [
1266+ Query::or ([
1267+ Query::exists (['nonExistentField ' ]),
1268+ Query::exists (['alsoMissing ' ]),
1269+ ]),
1270+ ]);
1271+ $ this ->assertEquals (0 , count ($ documents ));
1272+
1273+ $ database ->deleteCollection ($ colName );
1274+ }
1275+
1276+ public function testSchemalessNotExists (): void
1277+ {
1278+ /** @var Database $database */
1279+ $ database = static ::getDatabase ();
1280+
1281+ if ($ database ->getAdapter ()->getSupportForAttributes ()) {
1282+ $ this ->expectNotToPerformAssertions ();
1283+ return ;
1284+ }
1285+
1286+ $ colName = uniqid ('schemaless_not_exists ' );
1287+ $ database ->createCollection ($ colName );
1288+
1289+ $ permissions = [
1290+ Permission::read (Role::any ()),
1291+ Permission::write (Role::any ()),
1292+ Permission::update (Role::any ()),
1293+ Permission::delete (Role::any ())
1294+ ];
1295+
1296+ // Create documents with and without the 'optionalField' attribute
1297+ $ docs = [
1298+ new Document (['$id ' => 'doc1 ' , '$permissions ' => $ permissions , 'optionalField ' => 'value1 ' , 'name ' => 'doc1 ' ]),
1299+ new Document (['$id ' => 'doc2 ' , '$permissions ' => $ permissions , 'optionalField ' => 'value2 ' , 'name ' => 'doc2 ' ]),
1300+ new Document (['$id ' => 'doc3 ' , '$permissions ' => $ permissions , 'name ' => 'doc3 ' ]), // no optionalField
1301+ new Document (['$id ' => 'doc4 ' , '$permissions ' => $ permissions , 'optionalField ' => null , 'name ' => 'doc4 ' ]), // exists but null
1302+ new Document (['$id ' => 'doc5 ' , '$permissions ' => $ permissions , 'name ' => 'doc5 ' ]), // no optionalField
1303+ ];
1304+ $ this ->assertEquals (5 , $ database ->createDocuments ($ colName , $ docs ));
1305+
1306+ // Test notExists - should return documents where optionalField does not exist
1307+ $ documents = $ database ->find ($ colName , [
1308+ Query::notExists ('optionalField ' ),
1309+ ]);
1310+
1311+ $ this ->assertEquals (2 , count ($ documents )); // doc3, doc5
1312+ $ ids = array_map (fn ($ doc ) => $ doc ->getId (), $ documents );
1313+ $ this ->assertContains ('doc3 ' , $ ids );
1314+ $ this ->assertContains ('doc5 ' , $ ids );
1315+
1316+ // Verify that doc4 is NOT included (it exists even though null)
1317+ $ this ->assertNotContains ('doc4 ' , $ ids );
1318+
1319+ // Test notExists with another attribute
1320+ $ documents = $ database ->find ($ colName , [
1321+ Query::notExists ('name ' ),
1322+ ]);
1323+ $ this ->assertEquals (0 , count ($ documents )); // All documents have 'name'
1324+
1325+ // Test notExists with non-existent attribute
1326+ $ documents = $ database ->find ($ colName , [
1327+ Query::notExists ('nonExistentField ' ),
1328+ ]);
1329+ $ this ->assertEquals (5 , count ($ documents )); // All documents don't have this field
1330+
1331+ // Multiple attributes in a single notExists query (OR semantics) - both missing
1332+ $ documents = $ database ->find ($ colName , [
1333+ Query::notExists (['nonExistentField ' , 'alsoMissing ' ]),
1334+ ]);
1335+ $ this ->assertEquals (5 , count ($ documents ));
1336+
1337+ // Multiple attributes (OR) where only some documents miss one of them
1338+ $ documents = $ database ->find ($ colName , [
1339+ Query::notExists (['name ' , 'optionalField ' ]),
1340+ ]);
1341+ $ this ->assertEquals (2 , count ($ documents )); // doc3, doc5
1342+
1343+ // Multiple notExists queries (AND semantics) - must miss both
1344+ $ documents = $ database ->find ($ colName , [
1345+ Query::notExists (['optionalField ' ]),
1346+ Query::notExists (['nonExistentField ' ]),
1347+ ]);
1348+ $ this ->assertEquals (2 , count ($ documents )); // doc3, doc5
1349+
1350+ // Test combination of exists and notExists
1351+ $ documents = $ database ->find ($ colName , [
1352+ Query::exists (['name ' ]),
1353+ Query::notExists ('optionalField ' ),
1354+ ]);
1355+ $ this ->assertEquals (2 , count ($ documents )); // doc3, doc5
1356+
1357+ // Nested OR/AND with notExists: (notExists optionalField OR notExists nonExistent) AND name
1358+ $ documents = $ database ->find ($ colName , [
1359+ Query::and ([
1360+ Query::or ([
1361+ Query::notExists (['optionalField ' ]),
1362+ Query::notExists (['nonExistentField ' ]),
1363+ ]),
1364+ Query::exists (['name ' ]),
1365+ ]),
1366+ ]);
1367+ // notExists(nonExistentField) matches all docs, so OR is always true; AND with name returns all
1368+ $ this ->assertEquals (5 , count ($ documents )); // all docs match due to nonExistentField
1369+
1370+ // Nested OR with notExists where all attributes exist => empty
1371+ $ documents = $ database ->find ($ colName , [
1372+ Query::or ([
1373+ Query::notExists (['name ' ]),
1374+ Query::notExists (['optionalField ' ]),
1375+ ]),
1376+ ]);
1377+ $ this ->assertEquals (2 , count ($ documents )); // only ones missing optionalField (doc3, doc5)
1378+
1379+ $ database ->deleteCollection ($ colName );
1380+ }
11581381}
0 commit comments