mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
(fix): move collection mapping query from decorator to getDatabasesDB with static cache
The decorator no longer queries dbForProject directly. Instead, getDatabasesDB pre-populates the mapping via setCollectionId() and uses a static cache (Metadata::getCachedMap) so the query runs at most ONCE per database per Swoole worker process. The decorator is now stateless — no database dependencies, just a pre-set map. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
99394e6dfa
commit
d7b61e6afc
+35
-6
@@ -769,6 +769,40 @@ Http::setResource('getDatabasesDB', function (Group $pools, Database $dbForProje
|
||||
$databaseIdCollectionIdDocumentsMetric = $databaseType . '.' . $databaseIdCollectionIdDocumentsMetric;
|
||||
}
|
||||
|
||||
$metadata = new Metadata(
|
||||
database: $originalDatabase,
|
||||
context: $context,
|
||||
);
|
||||
|
||||
// Pre-populate collection ID mapping from static cache or single query
|
||||
$databaseSequence = $originalDatabase->getSequence();
|
||||
$cachedMap = Metadata::getCachedMap($databaseSequence);
|
||||
if ($cachedMap !== null) {
|
||||
foreach ($cachedMap as $k => $v) {
|
||||
$metadata->setCollectionId($k, $v);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$dbPrefix = 'database_' . $databaseSequence;
|
||||
$collections = $authorization->skip(
|
||||
fn () => $dbForProject->silent(
|
||||
fn () => $dbForProject->find($dbPrefix, [
|
||||
\Utopia\Database\Query::limit(5000),
|
||||
])
|
||||
)
|
||||
);
|
||||
foreach ($collections as $col) {
|
||||
$seq = $col->getSequence();
|
||||
if ($seq !== null) {
|
||||
$metadata->setCollectionId('collection_' . $seq, $col->getId());
|
||||
$metadata->setCollectionId($dbPrefix . '_collection_' . $seq, $col->getId());
|
||||
}
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Database may not have collections yet
|
||||
}
|
||||
}
|
||||
|
||||
$database
|
||||
->addHook(new DocumentUsage(
|
||||
$usage,
|
||||
@@ -778,12 +812,7 @@ Http::setResource('getDatabasesDB', function (Group $pools, Database $dbForProje
|
||||
))
|
||||
->addHook(new Permissions())
|
||||
->addHook(new Relationships($database))
|
||||
->addHook(new Metadata(
|
||||
database: $originalDatabase,
|
||||
dbForProject: $dbForProject,
|
||||
authorization: $authorization,
|
||||
context: $context,
|
||||
));
|
||||
->addHook($metadata);
|
||||
|
||||
if ($database->getSharedTables() && ($database->getTenant() !== null)) {
|
||||
$database->addHook(new Tenancy($database->getTenant()));
|
||||
|
||||
@@ -6,50 +6,56 @@ use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Event;
|
||||
use Utopia\Database\Hook\Decorator;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
use Utopia\Query\Schema\ColumnType;
|
||||
|
||||
/**
|
||||
* Stamps database/collection metadata onto every document returned from the database,
|
||||
* and recursively decorates nested relationship documents.
|
||||
*
|
||||
* Lazily loads the collection mapping from dbForProject on first use to resolve
|
||||
* internal collection names (database_N_collection_M) to user-facing collection IDs.
|
||||
* Collection ID mapping is pre-populated by getDatabasesDB via setCollectionId()
|
||||
* and cached statically per database sequence for the Swoole worker lifetime.
|
||||
*/
|
||||
class Metadata implements Decorator
|
||||
{
|
||||
/** @var array<string, array<Document>> */
|
||||
private array $relationshipCache = [];
|
||||
|
||||
/** @var array<string, array<string, string>> static cache keyed by database sequence */
|
||||
private static array $collectionIdMaps = [];
|
||||
|
||||
/** @var bool whether this instance has initialized its map reference */
|
||||
private bool $mapInitialized = false;
|
||||
|
||||
/** @var array<string, string> reference to the map for this database */
|
||||
/** @var array<string, string> internal collection name -> user-facing collection ID */
|
||||
private array $collectionIdMap = [];
|
||||
|
||||
/** @var array<string, array<string, string>> static cache keyed by database sequence */
|
||||
private static array $staticMaps = [];
|
||||
|
||||
private int $operations = 0;
|
||||
|
||||
public function __construct(
|
||||
private Document $database,
|
||||
private Database $dbForProject,
|
||||
private Authorization $authorization,
|
||||
private string $context = 'collection',
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a mapping from internal collection name to user-facing collection ID.
|
||||
* Also updates the static cache for subsequent requests.
|
||||
*/
|
||||
public function setCollectionId(string $internalName, string $externalId): void
|
||||
{
|
||||
$this->collectionIdMap[$internalName] = $externalId;
|
||||
$databaseSequence = $this->database->getSequence();
|
||||
if (isset(self::$collectionIdMaps[$databaseSequence])) {
|
||||
self::$collectionIdMaps[$databaseSequence][$internalName] = $externalId;
|
||||
$seq = $this->database->getSequence();
|
||||
self::$staticMaps[$seq][$internalName] = $externalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cached map for a database sequence, or null if not cached.
|
||||
*
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
public static function getCachedMap(?string $sequence): ?array
|
||||
{
|
||||
if ($sequence === null) {
|
||||
return null;
|
||||
}
|
||||
return self::$staticMaps[$sequence] ?? null;
|
||||
}
|
||||
|
||||
public function decorate(Event $event, Document $collection, Document $document): Document
|
||||
@@ -60,8 +66,6 @@ class Metadata implements Decorator
|
||||
|
||||
$this->operations++;
|
||||
|
||||
$this->ensureMapLoaded();
|
||||
|
||||
$collectionId = $this->collectionIdMap[$collection->getId()] ?? $collection->getId();
|
||||
$document->setAttribute('$databaseId', $this->database->getId());
|
||||
$document->setAttribute('$' . $this->context . 'Id', $collectionId);
|
||||
@@ -81,51 +85,6 @@ class Metadata implements Decorator
|
||||
$this->operations = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily load collection mapping from dbForProject.
|
||||
* Queries are wrapped in authorization->skip() and dbForProject->silent()
|
||||
* to prevent lifecycle hooks from firing and avoid permission checks.
|
||||
*/
|
||||
private function ensureMapLoaded(): void
|
||||
{
|
||||
if ($this->mapInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->mapInitialized = true;
|
||||
$databaseSequence = $this->database->getSequence();
|
||||
|
||||
if (isset(self::$collectionIdMaps[$databaseSequence])) {
|
||||
$this->collectionIdMap = self::$collectionIdMaps[$databaseSequence];
|
||||
return;
|
||||
}
|
||||
|
||||
$metadataCollection = 'database_' . $databaseSequence;
|
||||
|
||||
try {
|
||||
$collections = $this->authorization->skip(
|
||||
fn () => $this->dbForProject->silent(
|
||||
fn () => $this->dbForProject->find($metadataCollection)
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($collections as $collection) {
|
||||
$externalId = $collection->getId();
|
||||
$sequence = $collection->getSequence();
|
||||
|
||||
$relativeKey = 'collection_' . $sequence;
|
||||
$fullKey = 'database_' . $databaseSequence . '_collection_' . $sequence;
|
||||
|
||||
$this->collectionIdMap[$relativeKey] = $externalId;
|
||||
$this->collectionIdMap[$fullKey] = $externalId;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Silently fail — fall back to internal names
|
||||
}
|
||||
|
||||
self::$collectionIdMaps[$databaseSequence] = $this->collectionIdMap;
|
||||
}
|
||||
|
||||
private function decorateRelationships(Document $collection, Document $document, int $depth = 0): void
|
||||
{
|
||||
if ($depth >= Database::RELATION_MAX_DEPTH) {
|
||||
|
||||
Reference in New Issue
Block a user