Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -8584,7 +8584,8 @@ public function find(string $collection, array $queries = [], string $forPermiss
* @param string|null $namespace
* @return bool
*/
public function purgeCachedQueries(string $collection, ?string $namespace = null): bool
/** @phpstan-ignore-next-line exercised through query cache tests */
private function purgeCachedQueries(string $collection, ?string $namespace = null): bool
{
$collectionDocument = $this->silent(fn () => $this->getCollection($collection));
$collection = $collectionDocument->isEmpty() ? $collection : $collectionDocument->getId();
Expand Down Expand Up @@ -9719,7 +9720,7 @@ public function getCacheKeys(string $collectionId, ?string $documentId = null, a
* @param string|null $namespace

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Breaking public API change

purgeCachedQueries, getQueryCacheKey, and getQueryCacheField were all previously public. This is a semver-breaking change: any downstream code that calls these methods directly on a Database instance will receive a fatal Call to private method error at runtime after upgrading. As a distributed library (utopia-php/database), this warrants a major-version bump or at minimum a deprecation cycle before removal from the public surface.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Database/Database.php
Line: 9719

Comment:
**Breaking public API change**

`purgeCachedQueries`, `getQueryCacheKey`, and `getQueryCacheField` were all previously `public`. This is a semver-breaking change: any downstream code that calls these methods directly on a `Database` instance will receive a fatal `Call to private method` error at runtime after upgrading. As a distributed library (`utopia-php/database`), this warrants a major-version bump or at minimum a deprecation cycle before removal from the public surface.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

* @return string
*/
public function getQueryCacheKey(string $collectionId, ?string $namespace = null): string
private function getQueryCacheKey(string $collectionId, ?string $namespace = null): string
{
$hostname = $this->adapter->getSupportForHostname()
? $this->adapter->getHostname()
Expand All @@ -9744,7 +9745,8 @@ public function getQueryCacheKey(string $collectionId, ?string $namespace = null
* @param string $forPermission
* @return string|null
*/
public function getQueryCacheField(
/** @phpstan-ignore-next-line exercised through query cache tests */
private function getQueryCacheField(
?Document $collection = null,
array $queries = [],
string $field = 'documents',
Expand Down
38 changes: 21 additions & 17 deletions tests/unit/CacheKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

class CacheKeyTest extends TestCase
{
use QueryCacheTestHelpers;

/**
* @param array<string, array{encode: callable, decode: callable}> $instanceFilters
*/
Expand Down Expand Up @@ -147,7 +149,7 @@ public function testQueryCacheKeyUsesQueryCacheShape(): void

$this->assertSame(
'default-cache-mysql-console:_39::collection:ttl_cache_table:query',
$db->getQueryCacheKey('ttl_cache_table'),
$this->getQueryCacheKey($db, 'ttl_cache_table'),
);
}

Expand All @@ -163,7 +165,7 @@ public function testQueryCacheKeyCanOverrideNamespaceSegment(): void

$this->assertSame(
'default-cache-mysql-console:_39::collection:wafrules:query',
$db->getQueryCacheKey('wafrules', '_39'),
$this->getQueryCacheKey($db, 'wafrules', '_39'),
);
}

Expand Down Expand Up @@ -192,7 +194,7 @@ public function testQueryCacheFieldUsesQueryCacheShape(): void
. (\json_encode($collection->getAttribute('$permissions', [])) ?: '')
. (\json_encode($collection->getAttribute('documentSecurity', false)) ?: '')
);
$field = $db->getQueryCacheField($collection, $queries);
$field = $this->getQueryCacheField($db, $collection, $queries);

$this->assertStringStartsWith("{$schemaHash}:", $field);
$this->assertStringEndsWith(':documents', $field);
Expand All @@ -203,7 +205,8 @@ public function testQueryCacheFieldChangesWithInputs(): void
{
$db = $this->createDatabase();

$field = $db->getQueryCacheField(
$field = $this->getQueryCacheField(
$db,
new Document([
'attributes' => [new Document(['$id' => 'name', 'type' => Database::VAR_STRING])],
'indexes' => [],
Expand All @@ -213,56 +216,57 @@ public function testQueryCacheFieldChangesWithInputs(): void

$this->assertNotSame(
$field,
$db->getQueryCacheField(
$this->getQueryCacheField(
$db,
new Document([
'attributes' => [new Document(['$id' => 'status', 'type' => Database::VAR_STRING])],
'indexes' => [],
]),
[Query::limit(10)],
),
);
$this->assertNotSame($field, $db->getQueryCacheField(null, [Query::limit(20)]));
$this->assertStringEndsWith(':total', $db->getQueryCacheField(null, [Query::limit(10)], 'total'));
$this->assertNotSame($field, $this->getQueryCacheField($db, null, [Query::limit(20)]));
$this->assertStringEndsWith(':total', $this->getQueryCacheField($db, null, [Query::limit(10)], 'total'));
}

public function testQueryCacheFieldChangesWithActiveAuthorizationContext(): void
{
$db = $this->createDatabase();

$field = $db->getQueryCacheField(null, [Query::limit(10)]);
$field = $this->getQueryCacheField($db, null, [Query::limit(10)]);

$this->assertNotSame(
$field,
$db->getAuthorization()->skip(fn () => $db->getQueryCacheField(null, [Query::limit(10)])),
$db->getAuthorization()->skip(fn () => $this->getQueryCacheField($db, null, [Query::limit(10)])),
);

$db->getAuthorization()->addRole('user:1');

$this->assertNotSame(
$field,
$db->getQueryCacheField(null, [Query::limit(10)]),
$this->getQueryCacheField($db, null, [Query::limit(10)]),
);
}

public function testQueryCacheFieldReturnsNullForNonReadPermission(): void
{
$db = $this->createDatabase();

$this->assertNull($db->getQueryCacheField(forPermission: Database::PERMISSION_UPDATE));
$this->assertNull($this->getQueryCacheField($db, forPermission: Database::PERMISSION_UPDATE));
}

public function testQueryCacheFieldIncludesCursorDocumentPayload(): void
{
$db = $this->createDatabase();

$fieldA = $db->getQueryCacheField(null, [
$fieldA = $this->getQueryCacheField($db, null, [
Query::orderAsc('name'),
Query::cursorAfter(new Document([
'$id' => 'cursor',
'name' => 'alpha',
])),
]);
$fieldB = $db->getQueryCacheField(null, [
$fieldB = $this->getQueryCacheField($db, null, [
Query::orderAsc('name'),
Query::cursorAfter(new Document([
'$id' => 'cursor',
Expand All @@ -277,15 +281,15 @@ public function testQueryCacheFieldIncludesAmbientState(): void
{
$db = $this->createDatabase();

$field = $db->getQueryCacheField(null, [Query::limit(10)]);
$field = $this->getQueryCacheField($db, null, [Query::limit(10)]);

$this->assertNotSame(
$field,
$db->skipFilters(fn () => $db->getQueryCacheField(null, [Query::limit(10)]), ['json']),
$db->skipFilters(fn () => $this->getQueryCacheField($db, null, [Query::limit(10)]), ['json']),
);
$this->assertNotSame(
$field,
$db->skipRelationships(fn () => $db->getQueryCacheField(null, [Query::limit(10)])),
$db->skipRelationships(fn () => $this->getQueryCacheField($db, null, [Query::limit(10)])),
);
}

Expand All @@ -297,7 +301,7 @@ public function testQueryCacheFieldValidatesQueryTypes(): void
$queries = ['invalid'];

/** @phpstan-ignore-next-line intentionally passing invalid query type */
$db->getQueryCacheField(null, $queries);
$this->getQueryCacheField($db, null, $queries);
}


Expand Down
34 changes: 18 additions & 16 deletions tests/unit/QueryCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

class QueryCacheTest extends TestCase
{
use QueryCacheTestHelpers;

/**
* @param array<string, array{encode: callable, decode: callable}> $filters
*/
Expand Down Expand Up @@ -46,8 +48,8 @@ private function findWithCache(
}

$collectionDocument = $database->getCollection($collection);
$cacheKey = $database->getQueryCacheKey($collectionDocument->getId(), $namespace);
$cacheHash = $database->getQueryCacheField($collectionDocument, $queries);
$cacheKey = $this->getQueryCacheKey($database, $collectionDocument->getId(), $namespace);
$cacheHash = $this->getQueryCacheField($database, $collectionDocument, $queries);

return $database->withCache(
key: $cacheKey,
Expand Down Expand Up @@ -326,8 +328,8 @@ public function testWithCacheCachesSingleDocument(): void

$callbackCalls = 0;
$collection = $database->getCollection('wafRules');
$key = $database->getQueryCacheKey($collection->getId(), '_39');
$hash = $database->getQueryCacheField($collection, field: 'document');
$key = $this->getQueryCacheKey($database, $collection->getId(), '_39');
$hash = $this->getQueryCacheField($database, $collection, field: 'document');

$first = $database->withCache(
key: $key,
Expand Down Expand Up @@ -361,8 +363,8 @@ public function testWithCacheCachesStaticQueryValues(): void

$callbackCalls = 0;
$collection = $database->getCollection('wafRules');
$key = $database->getQueryCacheKey($collection->getId(), '_39');
$hash = $database->getQueryCacheField($collection, field: 'count');
$key = $this->getQueryCacheKey($database, $collection->getId(), '_39');
$hash = $this->getQueryCacheField($database, $collection, field: 'count');

$first = $database->withCache(
key: $key,
Expand Down Expand Up @@ -429,7 +431,7 @@ public function testQueryCacheUsesCacheUntilPurged(): void
$this->assertCount(1, $cached);
$this->assertSame('rule-a', $cached[0]->getId());

$this->assertTrue($database->purgeCachedQueries('wafRules', '_39'));
$this->assertTrue($this->purgeCachedQueries($database, 'wafRules', '_39'));

$fresh = $this->findWithCache($database, 'wafRules', $queries, '_39');
$this->assertCount(2, $fresh);
Expand Down Expand Up @@ -659,7 +661,7 @@ public function testQueryCacheReliesOnPurgeForDocumentSecurityCollections(): voi

$this->assertCount(1, $cached);

$this->assertTrue($database->purgeCachedQueries('secureRules', '_39'));
$this->assertTrue($this->purgeCachedQueries($database, 'secureRules', '_39'));

$fresh = $this->findWithCache($database, 'secureRules', $queries, '_39');
$this->assertSame([], $fresh);
Expand All @@ -678,8 +680,8 @@ public function testQueryCacheFiltersDocumentSecurityPayloadsOnHit(): void
$database->getAuthorization()->addRole(Role::user('user-1')->toString());

$collection = $database->getCollection('secureRules');
$key = $database->getQueryCacheKey($collection->getId(), '_39');
$hash = $database->getQueryCacheField($collection);
$key = $this->getQueryCacheKey($database, $collection->getId(), '_39');
$hash = $this->getQueryCacheField($database, $collection);
$database->getCache()->save($key, [
'collection' => $collection->getId(),
'type' => 'documents',
Expand Down Expand Up @@ -725,7 +727,7 @@ public function testQueryCacheRehydratesNestedDocumentPayloads(): void

$collection = $database->getCollection('parents');
$cache->save(
$database->getQueryCacheKey($collection->getId(), '_39'),
$this->getQueryCacheKey($database, $collection->getId(), '_39'),
[
'collection' => $collection->getId(),
'type' => 'documents',
Expand All @@ -740,7 +742,7 @@ public function testQueryCacheRehydratesNestedDocumentPayloads(): void
],
],
],
$database->getQueryCacheField($collection, $queries),
$this->getQueryCacheField($database, $collection, $queries),
);

$parents = $this->findWithCache($database, 'parents', $queries, '_39');
Expand Down Expand Up @@ -782,13 +784,13 @@ public function testQueryCacheRefreshesInvalidPayload(): void

$collection = $database->getCollection('wafRules');
$cache->save(
$database->getQueryCacheKey($collection->getId(), '_39'),
$this->getQueryCacheKey($database, $collection->getId(), '_39'),
[
'collection' => $collection->getId(),
'type' => 'documents',
'value' => 'invalid',
],
$database->getQueryCacheField($collection, $queries),
$this->getQueryCacheField($database, $collection, $queries),
);

$rules = $this->findWithCache($database, 'wafRules', $queries, '_39');
Expand Down Expand Up @@ -833,7 +835,7 @@ public function testQueryCacheRefreshesInvalidPayloadEntry(): void

$collection = $database->getCollection('wafRules');
$cache->save(
$database->getQueryCacheKey($collection->getId(), '_39'),
$this->getQueryCacheKey($database, $collection->getId(), '_39'),
[
'collection' => $collection->getId(),
'type' => 'documents',
Expand All @@ -845,7 +847,7 @@ public function testQueryCacheRefreshesInvalidPayloadEntry(): void
'invalid',
],
],
$database->getQueryCacheField($collection, $queries),
$this->getQueryCacheField($database, $collection, $queries),
);

$rules = $this->findWithCache($database, 'wafRules', $queries, '_39');
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/QueryCacheTestHelpers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Tests\Unit;

use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;

trait QueryCacheTestHelpers
{
private function purgeCachedQueries(Database $database, string $collection, ?string $namespace = null): bool
{
$method = new \ReflectionMethod(Database::class, 'purgeCachedQueries');

return $method->invoke($database, $collection, $namespace);
}

private function getQueryCacheKey(Database $database, string $collectionId, ?string $namespace = null): string
{
$method = new \ReflectionMethod(Database::class, 'getQueryCacheKey');

return $method->invoke($database, $collectionId, $namespace);
}

/**
* @param array<Query> $queries
*/
private function getQueryCacheField(
Database $database,
?Document $collection = null,
array $queries = [],
string $field = 'documents',
string $forPermission = Database::PERMISSION_READ,
): ?string {
$method = new \ReflectionMethod(Database::class, 'getQueryCacheField');

return $method->invoke($database, $collection, $queries, $field, $forPermission);
}
}
Comment thread
premtsd-code marked this conversation as resolved.
4 changes: 3 additions & 1 deletion tests/unit/WithCacheLeaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

class WithCacheLeaseTest extends TestCase
{
use QueryCacheTestHelpers;

private LeasableMemoryCache $cacheAdapter;

private Database $database;
Expand All @@ -39,7 +41,7 @@ protected function setUp(): void
'name' => 'fresh',
]));

$this->key = $this->database->getQueryCacheKey('projects');
$this->key = $this->getQueryCacheKey($this->database, 'projects');
}

public function testStaleListWriteAfterConcurrentPurgeIsRejected(): void
Expand Down
Loading