@@ -1260,6 +1260,78 @@ function (mixed $value) {
12601260 }
12611261 }
12621262
1263+ /**
1264+ * A filter can build its value by querying rather than transforming the stored one — that is
1265+ * what the subQuery filters in Appwrite do, listing a child collection per document. Reading a
1266+ * document without selecting such an attribute must not run it: the value is dropped anyway,
1267+ * and it is the filter, not the value, that costs the query.
1268+ */
1269+ public function testFilterNotAppliedWhenAttributeNotSelected (): void
1270+ {
1271+ /** @var Database $database */
1272+ $ database = $ this ->getDatabase ();
1273+
1274+ $ calls = 0 ;
1275+
1276+ $ database ->addFilter (
1277+ 'subQueryProbe ' ,
1278+ fn (mixed $ value ) => null , // stores nothing, like a subQuery filter
1279+ function (mixed $ value ) use (&$ calls ) {
1280+ $ calls ++;
1281+ return ['fanned ' , 'out ' ];
1282+ }
1283+ );
1284+
1285+ $ database ->createCollection ('filterSelect ' );
1286+ $ database ->createAttribute ('filterSelect ' , 'plain ' , Database::VAR_STRING , 128 , false );
1287+ $ database ->createAttribute ('filterSelect ' , 'kids ' , Database::VAR_STRING , 128 , false , filters: ['subQueryProbe ' ]);
1288+
1289+ $ database ->createDocument ('filterSelect ' , new Document ([
1290+ '$id ' => 'doc1 ' ,
1291+ '$permissions ' => [
1292+ Permission::read (Role::any ()),
1293+ Permission::update (Role::any ()),
1294+ Permission::delete (Role::any ()),
1295+ ],
1296+ 'plain ' => 'x ' ,
1297+ ]));
1298+
1299+ $ calls = 0 ;
1300+ $ document = $ database ->getDocument ('filterSelect ' , 'doc1 ' );
1301+ $ this ->assertEquals (1 , $ calls );
1302+ $ this ->assertEquals (['fanned ' , 'out ' ], $ document ->getAttribute ('kids ' ));
1303+
1304+ $ calls = 0 ;
1305+ $ document = $ database ->getDocument ('filterSelect ' , 'doc1 ' , [Query::select (['$id ' , 'plain ' ])]);
1306+ $ this ->assertEquals (0 , $ calls );
1307+ $ this ->assertNull ($ document ->getAttribute ('kids ' ));
1308+ $ this ->assertEquals ('x ' , $ document ->getAttribute ('plain ' ));
1309+
1310+ // Selecting it explicitly, and selecting everything, both still decode it.
1311+ $ calls = 0 ;
1312+ $ document = $ database ->getDocument ('filterSelect ' , 'doc1 ' , [Query::select (['$id ' , 'kids ' ])]);
1313+ $ this ->assertEquals (1 , $ calls );
1314+ $ this ->assertEquals (['fanned ' , 'out ' ], $ document ->getAttribute ('kids ' ));
1315+
1316+ $ calls = 0 ;
1317+ $ document = $ database ->getDocument ('filterSelect ' , 'doc1 ' , [Query::select (['* ' ])]);
1318+ $ this ->assertEquals (1 , $ calls );
1319+ $ this ->assertEquals (['fanned ' , 'out ' ], $ document ->getAttribute ('kids ' ));
1320+
1321+ // find() decodes through the same path, once per document returned.
1322+ $ calls = 0 ;
1323+ $ documents = $ database ->find ('filterSelect ' , [Query::select (['$id ' , 'plain ' ])]);
1324+ $ this ->assertCount (1 , $ documents );
1325+ $ this ->assertEquals (0 , $ calls );
1326+ $ this ->assertNull ($ documents [0 ]->getAttribute ('kids ' ));
1327+
1328+ $ calls = 0 ;
1329+ $ documents = $ database ->find ('filterSelect ' );
1330+ $ this ->assertCount (1 , $ documents );
1331+ $ this ->assertEquals (1 , $ calls );
1332+ $ this ->assertEquals (['fanned ' , 'out ' ], $ documents [0 ]->getAttribute ('kids ' ));
1333+ }
1334+
12631335 public function updateStringAttributeSize (int $ size , Document $ document ): Document
12641336 {
12651337 /** @var Database $database */
0 commit comments