@@ -148,69 +148,79 @@ public function testRandomOrderAlwaysBypassesQueryCache(): void
148148 $ this ->assertSame (2 , $ adapter ->getObservedFinds ());
149149 }
150150
151- public function testSetQueryCacheInstallsOneInvalidatorAndRemovesIt (): void
151+ public function testSetQueryCacheInvalidatesWhileInstalledAndStopsOnceRemoved (): void
152152 {
153153 [$ database , $ queryAdapter ] = $ this ->createDatabase (queryCache: false );
154- $ database ->createCollection (new Collection (id: 'users ' , permissions: $ this -> permissions (), documentSecurity: false ));
154+ $ database ->createCollection (new Collection (id: 'users ' , permissions: self :: permissions (), documentSecurity: false ));
155155
156156 $ queryCache = new QueryCache (new Cache ($ queryAdapter ));
157157 $ database ->setQueryCache ($ queryCache );
158158 $ database ->setQueryCache ($ queryCache );
159159
160- $ queryAdapter ->resetPurges ();
161- $ database ->createDocument ('users ' , new Document (['$id ' => 'a ' ]));
162- $ this ->assertSame (2 , $ queryAdapter ->getWrites ($ this ->collectionKey ($ database , 'users ' ).'#epoch ' ));
160+ $ this ->assertMutationRetiresQueries (
161+ $ database ,
162+ static fn (Database $ database ) => $ database ->createDocument ('users ' , new Document (['$id ' => 'a ' ])),
163+ );
163164
164165 $ database ->setQueryCache (null );
165- $ queryAdapter ->resetPurges ();
166+ $ scope = $ this ->scope ($ database );
167+ $ before = $ queryCache ->getEntry ($ scope , 'users ' , []);
168+ $ this ->assertNotNull ($ before );
169+ $ this ->assertTrue ($ queryCache ->set ($ before , [new Document (['$id ' => 'detached ' ])], $ queryCache ->getGeneration ($ before )));
170+
166171 $ database ->createDocument ('users ' , new Document (['$id ' => 'b ' ]));
167- $ this ->assertSame (0 , $ queryAdapter ->getWrites ($ this ->collectionKey ($ database , 'users ' ).'#epoch ' ));
172+
173+ $ after = $ queryCache ->getEntry ($ scope , 'users ' , []);
174+ $ this ->assertNotNull ($ after );
175+ $ this ->assertSame (['detached ' ], $ this ->ids ($ queryCache ->get ($ after ) ?? []), 'A removed query cache must no longer be invalidated by writes ' );
168176 }
169177
170- public function testSchemaAndCollectionMutationsInvalidateQueries (): void
178+ /**
179+ * @return array<string, array{callable(Database): mixed}>
180+ */
181+ public static function schemaAndCollectionMutations (): array
171182 {
172- [$ database , $ queryAdapter ] = $ this ->createDatabase ();
173- $ database ->createCollection (new Collection (id: 'users ' , permissions: $ this ->permissions (), documentSecurity: false ));
174- $ started = $ this ->collectionKey ($ database , 'users ' ).'#started ' ;
175-
176- $ queryAdapter ->resetPurges ();
177- $ database ->updateCollection ('users ' , $ this ->permissions (), false );
178- $ this ->assertGreaterThan (0 , $ queryAdapter ->getPurges ($ started ));
179-
180- $ queryAdapter ->resetPurges ();
181- $ database ->createAttribute ('users ' , Attribute ::string (key: 'name ' ));
182- $ this ->assertGreaterThan (0 , $ queryAdapter ->getPurges ($ started ));
183-
184- $ queryAdapter ->resetPurges ();
185- $ database ->updateAttribute ('users ' , 'name ' , size: 128 );
186- $ this ->assertGreaterThan (0 , $ queryAdapter ->getPurges ($ started ));
187-
188- $ queryAdapter ->resetPurges ();
189- $ database ->createIndex ('users ' , Index ::key (key: 'name ' , attributes: ['name ' ]));
190- $ this ->assertGreaterThan (0 , $ queryAdapter ->getPurges ($ started ));
191-
192- $ queryAdapter ->resetPurges ();
193- $ database ->renameIndex ('users ' , 'name ' , 'renamed ' );
194- $ this ->assertGreaterThan (0 , $ queryAdapter ->getPurges ($ started ));
183+ return [
184+ 'update collection ' => [static fn (Database $ database ) => $ database ->updateCollection ('users ' , self ::permissions (), false )],
185+ 'create attribute ' => [static fn (Database $ database ) => $ database ->createAttribute ('users ' , Attribute ::string (key: 'email ' ))],
186+ 'update attribute ' => [static fn (Database $ database ) => $ database ->updateAttribute ('users ' , 'name ' , size: 128 )],
187+ 'delete attribute ' => [static fn (Database $ database ) => $ database ->deleteAttribute ('users ' , 'title ' )],
188+ 'create index ' => [static fn (Database $ database ) => $ database ->createIndex ('users ' , Index ::key (key: 'title ' , attributes: ['title ' ]))],
189+ 'rename index ' => [static fn (Database $ database ) => $ database ->renameIndex ('users ' , 'name ' , 'renamed ' )],
190+ 'delete index ' => [static fn (Database $ database ) => $ database ->deleteIndex ('users ' , 'name ' )],
191+ ];
192+ }
195193
196- $ queryAdapter ->resetPurges ();
197- $ database ->deleteIndex ('users ' , 'renamed ' );
198- $ this ->assertGreaterThan (0 , $ queryAdapter ->getPurges ($ started ));
194+ /**
195+ * @param callable(Database): mixed $mutation
196+ */
197+ #[DataProvider('schemaAndCollectionMutations ' )]
198+ public function testSchemaAndCollectionMutationsInvalidateQueries (callable $ mutation ): void
199+ {
200+ [$ database ] = $ this ->createDatabase ();
201+ $ database ->createCollection (new Collection (
202+ id: 'users ' ,
203+ attributes: [
204+ Attribute ::string (key: 'name ' ),
205+ Attribute ::string (key: 'title ' ),
206+ ],
207+ indexes: [Index ::key (key: 'name ' , attributes: ['name ' ])],
208+ permissions: self ::permissions (),
209+ documentSecurity: false ,
210+ ));
199211
200- $ queryAdapter ->resetPurges ();
201- $ database ->deleteAttribute ('users ' , 'name ' );
202- $ this ->assertGreaterThan (0 , $ queryAdapter ->getPurges ($ started ));
212+ $ this ->assertMutationRetiresQueries ($ database , $ mutation );
203213 }
204214
205215 public function testDeleteAndRecreateCannotReuseOldCollectionResults (): void
206216 {
207217 [$ database ] = $ this ->createDatabase ();
208- $ database ->createCollection (new Collection (id: 'users ' , permissions: $ this -> permissions (), documentSecurity: false ));
218+ $ database ->createCollection (new Collection (id: 'users ' , permissions: self :: permissions (), documentSecurity: false ));
209219 $ database ->createDocument ('users ' , new Document (['$id ' => 'old ' ]));
210220 $ this ->assertSame (['old ' ], $ this ->ids ($ database ->find ('users ' )));
211221
212222 $ database ->deleteCollection ('users ' );
213- $ database ->createCollection (new Collection (id: 'users ' , permissions: $ this -> permissions (), documentSecurity: false ));
223+ $ database ->createCollection (new Collection (id: 'users ' , permissions: self :: permissions (), documentSecurity: false ));
214224 $ database ->createDocument ('users ' , new Document (['$id ' => 'new ' ]));
215225
216226 $ this ->assertSame (['new ' ], $ this ->ids ($ database ->find ('users ' )));
@@ -219,7 +229,7 @@ public function testDeleteAndRecreateCannotReuseOldCollectionResults(): void
219229 public function testRolledBackTransactionCannotPoisonQueryCache (): void
220230 {
221231 [$ database ] = $ this ->createDatabase ();
222- $ database ->createCollection (new Collection (id: 'users ' , permissions: $ this -> permissions (), documentSecurity: false ));
232+ $ database ->createCollection (new Collection (id: 'users ' , permissions: self :: permissions (), documentSecurity: false ));
223233 $ database ->createDocument ('users ' , new Document (['$id ' => 'committed ' ]));
224234 $ this ->assertSame (['committed ' ], $ this ->ids ($ database ->find ('users ' )));
225235
@@ -246,7 +256,7 @@ public function testMetadataReadStartedBeforePurgeCannotPublish(): void
246256 {
247257 $ adapter = new ObservedMemory ();
248258 [$ database ] = $ this ->createDatabase ($ adapter , queryCache: false , dataAdapter: new None ());
249- $ database ->createCollection (new Collection (id: 'users ' , permissions: $ this -> permissions (), documentSecurity: false ));
259+ $ database ->createCollection (new Collection (id: 'users ' , permissions: self :: permissions (), documentSecurity: false ));
250260 $ database ->purgeCachedCollection ('users ' );
251261
252262 $ adapter ->observeMetadata ('users ' , fn () => $ database ->purgeCachedCollection ('users ' ));
@@ -262,7 +272,7 @@ public function testValidatorBuildStartedBeforePurgeCannotPublish(): void
262272 [$ database ] = $ this ->createDatabase ($ adapter , queryCache: false );
263273 $ database ->createCollection (new Collection (id: 'users ' , attributes: [
264274 Attribute ::string (key: 'name ' ),
265- ], permissions: $ this -> permissions (), documentSecurity: false ));
275+ ], permissions: self :: permissions (), documentSecurity: false ));
266276 $ database ->getCollection ('users ' );
267277
268278 $ byName = static fn (string $ name ): array => [Query ::select (['name ' ]), Query ::equal ('name ' , [$ name ])];
@@ -286,7 +296,7 @@ public function testANarrowValidationStraddlingAPurgeKeepsNothing(): void
286296 [$ database ] = $ this ->createDatabase ($ adapter , queryCache: false );
287297 $ database ->createCollection (new Collection (id: 'users ' , attributes: [
288298 Attribute ::string (key: 'name ' ),
289- ], permissions: $ this -> permissions (), documentSecurity: false ));
299+ ], permissions: self :: permissions (), documentSecurity: false ));
290300
291301 $ adapter ->observeMetadata ('users ' , fn () => $ database ->createAttribute ('users ' , Attribute ::integer (key: 'age ' )));
292302 $ this ->assertSame ([], $ database ->find ('users ' , [Query ::equal ('name ' , ['first ' ])]));
@@ -358,7 +368,7 @@ public function testMemoryCacheSeparatesPointSelectionVariants(): void
358368 $ database ->createCollection (new Collection (id: 'users ' , attributes: [
359369 Attribute ::string (key: 'name ' ),
360370 Attribute ::string (key: 'email ' ),
361- ], permissions: $ this -> permissions (), documentSecurity: false ));
371+ ], permissions: self :: permissions (), documentSecurity: false ));
362372 $ database ->createDocument ('users ' , new Document ([
363373 '$id ' => 'user ' ,
364374 'name ' => 'Alice ' ,
@@ -440,8 +450,8 @@ public function testJoinQueriesAlwaysBypassCacheAfterRelatedMutation(): void
440450 {
441451 $ adapter = new JoinMemory ();
442452 [$ database ] = $ this ->createDatabase ($ adapter );
443- $ database ->createCollection (new Collection (id: 'parents ' , permissions: $ this -> permissions (), documentSecurity: false ));
444- $ database ->createCollection (new Collection (id: 'children ' , permissions: $ this -> permissions (), documentSecurity: false ));
453+ $ database ->createCollection (new Collection (id: 'parents ' , permissions: self :: permissions (), documentSecurity: false ));
454+ $ database ->createCollection (new Collection (id: 'children ' , permissions: self :: permissions (), documentSecurity: false ));
445455
446456 $ queries = [Query ::join ('children ' , '$id ' , '$id ' )];
447457 $ database ->find ('parents ' , $ queries );
@@ -456,7 +466,7 @@ public function testMandatoryInvalidationFailureEscapesSilentScope(): void
456466 $ cache = new FailingMemory ();
457467 [$ database ] = $ this ->createDatabase (queryCache: false );
458468 $ database ->setQueryCache (new QueryCache (new Cache ($ cache )));
459- $ database ->createCollection (new Collection (id: 'users ' , permissions: $ this -> permissions (), documentSecurity: false ));
469+ $ database ->createCollection (new Collection (id: 'users ' , permissions: self :: permissions (), documentSecurity: false ));
460470 $ database ->find ('users ' );
461471 $ cache ->failBlocks ();
462472
@@ -515,7 +525,7 @@ public function testDeleteRollsBackWhenMandatoryInvalidationFails(): void
515525
516526 public function testSharedBlockedEpochPreventsPreCommitStaleFill (): void
517527 {
518- [$ writer , $ reader , $ writerAdapter , $ readerAdapter , $ cache , $ path ] = $ this ->createSharedSQLiteDatabases ();
528+ [$ writer , $ reader , $ writerAdapter , $ readerAdapter , , $ path ] = $ this ->createSharedSQLiteDatabases ();
519529
520530 try {
521531 $ this ->assertSame (
@@ -525,10 +535,7 @@ public function testSharedBlockedEpochPreventsPreCommitStaleFill(): void
525535 $ readerAdapter ->observeFinds ('users ' );
526536
527537 $ duringCommit = [];
528- $ writerAdapter ->pauseNextCommit (function () use ($ reader , $ cache , &$ duringCommit ): void {
529- $ epoch = $ cache ->load ($ this ->collectionKey ($ reader , 'users ' ).'#epoch ' , 3600 );
530- $ this ->assertIsString ($ epoch );
531- $ this ->assertStringStartsWith ('blocked: ' , $ epoch );
538+ $ writerAdapter ->pauseNextCommit (function () use ($ reader , &$ duringCommit ): void {
532539 $ duringCommit = $ this ->names ($ reader ->find ('users ' , [Query ::orderAsc ('$id ' )]));
533540 });
534541
@@ -568,10 +575,6 @@ public function testActivationFailureAfterCommitLeavesSharedEpochBlocked(): void
568575 $ this ->assertStringContainsString ('activate query cache ' , $ exception ->getMessage ());
569576 }
570577
571- $ epoch = $ cache ->load ($ this ->collectionKey ($ reader , 'users ' ).'#epoch ' , 3600 );
572- $ this ->assertIsString ($ epoch );
573- $ this ->assertStringStartsWith ('blocked: ' , $ epoch );
574-
575578 $ readerAdapter ->observeFinds ('users ' );
576579 $ this ->assertSame (
577580 ['existing ' => 'updated ' ],
@@ -603,7 +606,7 @@ public function testPooledRollbackCannotPoisonPointOrQueryCaches(): void
603606 $ database ->getAuthorization ()->addRole (Role ::any ()->toString ());
604607 $ database ->createCollection (new Collection (id: 'users ' , attributes: [
605608 Attribute ::string (key: 'name ' ),
606- ], permissions: $ this -> permissions (), documentSecurity: false ));
609+ ], permissions: self :: permissions (), documentSecurity: false ));
607610 $ database ->createDocument ('users ' , new Document ([
608611 '$id ' => 'user ' ,
609612 'name ' => 'committed ' ,
@@ -653,7 +656,7 @@ private function createDatabase(
653656
654657 private function createUsers (Database $ database ): void
655658 {
656- $ database ->createCollection (new Collection (id: 'users ' , permissions: $ this -> permissions (), documentSecurity: false ));
659+ $ database ->createCollection (new Collection (id: 'users ' , permissions: self :: permissions (), documentSecurity: false ));
657660 foreach (['a ' , 'b ' , 'c ' ] as $ id ) {
658661 $ database ->createDocument ('users ' , new Document (['$id ' => $ id ]));
659662 }
@@ -675,7 +678,7 @@ private function assertMutationRollsBackOnInvalidationFailure(callable $mutation
675678 $ database ->getAuthorization ()->addRole (Role ::any ()->toString ());
676679 $ database ->createCollection (new Collection (id: 'users ' , attributes: [
677680 Attribute ::string (key: 'name ' , required: true ),
678- ], permissions: $ this -> permissions (), documentSecurity: false ));
681+ ], permissions: self :: permissions (), documentSecurity: false ));
679682 $ database ->createDocument ('users ' , new Document ([
680683 '$id ' => 'existing ' ,
681684 'name ' => 'original ' ,
@@ -745,7 +748,7 @@ private function createSharedSQLiteDatabases(): array
745748 $ reader ->getAuthorization ()->addRole (Role ::any ()->toString ());
746749 $ writer ->createCollection (new Collection (id: 'users ' , attributes: [
747750 Attribute ::string (key: 'name ' , required: true ),
748- ], permissions: $ this -> permissions (), documentSecurity: false ));
751+ ], permissions: self :: permissions (), documentSecurity: false ));
749752 $ writer ->createDocument ('users ' , new Document ([
750753 '$id ' => 'existing ' ,
751754 'name ' => 'original ' ,
@@ -767,20 +770,42 @@ private function removeSQLiteFiles(string $path): void
767770 }
768771 }
769772
770- private function collectionKey (Database $ database, string $ collection ): string
773+ private function scope (Database $ database ): Scope
771774 {
772775 $ adapter = $ database ->getAdapter ();
773776
774- return ( new QueryCache ( new Cache ( new None ())))-> getCollectionKey ( new Scope (
777+ return new Scope (
775778 hostname: $ adapter ->supports (Capability::Hostname) ? $ adapter ->getHostname () : '' ,
776779 database: $ adapter ->getDatabase (),
777780 namespace: $ adapter ->getNamespace (),
778781 tenant: $ adapter ->getTenant (),
779- ), $ collection );
782+ );
783+ }
784+
785+ /**
786+ * @param callable(Database): mixed $mutation
787+ */
788+ private function assertMutationRetiresQueries (Database $ database , callable $ mutation ): void
789+ {
790+ $ queryCache = $ database ->getQueryCache ();
791+ $ this ->assertNotNull ($ queryCache );
792+ $ scope = $ this ->scope ($ database );
793+ $ before = $ queryCache ->getEntry ($ scope , 'users ' , []);
794+ $ this ->assertNotNull ($ before );
795+ $ this ->assertTrue ($ queryCache ->set ($ before , [new Document (['$id ' => 'stale ' ])], $ queryCache ->getGeneration ($ before )));
796+ $ this ->assertSame (['stale ' ], $ this ->ids ($ queryCache ->get ($ before ) ?? []));
797+
798+ $ mutation ($ database );
799+
800+ $ after = $ queryCache ->getEntry ($ scope , 'users ' , []);
801+ $ this ->assertNotNull ($ after , 'The mutation must leave the query cache usable ' );
802+ $ this ->assertNull ($ queryCache ->get ($ after ), 'The mutation must retire what was cached before it ' );
803+ $ this ->assertTrue ($ queryCache ->set ($ after , [new Document (['$id ' => 'fresh ' ])], $ queryCache ->getGeneration ($ after )));
804+ $ this ->assertSame (['fresh ' ], $ this ->ids ($ queryCache ->get ($ after ) ?? []), 'The mutation must publish a fresh epoch ' );
780805 }
781806
782807 /** @return array<string> */
783- private function permissions (): array
808+ private static function permissions (): array
784809 {
785810 return [
786811 Permission ::read (Role ::any ()),
0 commit comments