(fix): pass collection to getDatabasesDB for zero-query ID mapping

Endpoints pass the collection document they already have to
getDatabasesDB. The factory registers the single collection mapping
on the Metadata decorator — no bulk find queries, no static cache,
no dbForProject dependency.

The decorator is now purely stateless with zero database overhead.
Collection ID resolution uses only the pre-registered mapping from
the endpoint's request parameters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jake Barnby
2026-04-02 02:08:19 +13:00
co-authored by Claude Opus 4.6
parent 080361338c
commit 5740cecbe5
21 changed files with 31 additions and 69 deletions
+10 -29
View File
@@ -707,9 +707,9 @@ Http::setResource('dbForPlatform', function (Group $pools, Cache $cache, Authori
return $database;
}, ['pools', 'cache', 'authorization']);
Http::setResource('getDatabasesDB', function (Group $pools, Database $dbForProject, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
Http::setResource('getDatabasesDB', function (Group $pools, Cache $cache, Document $project, Request $request, UsageContext $usage, Authorization $authorization) {
return function (Document $database) use ($pools, $dbForProject, $cache, $project, $request, $usage, $authorization): Database {
return function (Document $database, ?Document $collection = null) use ($pools, $cache, $project, $request, $usage, $authorization): Database {
$originalDatabase = $database;
$context = str_contains($request->getURI(), '/tablesdb/') ? 'table' : 'collection';
$databaseDSN = $database->getAttribute('database', $project->getAttribute('database', ''));
@@ -774,32 +774,13 @@ Http::setResource('getDatabasesDB', function (Group $pools, Database $dbForProje
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
// Register collection ID mapping if collection document is provided
if ($collection !== null && !$collection->isEmpty()) {
$seq = $collection->getSequence();
if ($seq !== null) {
$dbPrefix = 'database_' . $originalDatabase->getSequence();
$metadata->setCollectionId('collection_' . $seq, $collection->getId());
$metadata->setCollectionId($dbPrefix . '_collection_' . $seq, $collection->getId());
}
}
@@ -821,7 +802,7 @@ Http::setResource('getDatabasesDB', function (Group $pools, Database $dbForProje
return $database;
};
}, ['pools','dbForProject','cache','project','request','usage','authorization']);
}, ['pools','cache','project','request','usage','authorization']);
Http::setResource('getProjectDB', function (Group $pools, Database $dbForPlatform, $cache, Authorization $authorization) {
$databases = [];
@@ -129,7 +129,7 @@ class Create extends Action
/**
* @var Database $dbForDatabases
*/
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$collectionKey = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
$databaseKey = 'database_' . $database->getSequence();
@@ -86,7 +86,7 @@ class Delete extends Action
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Failed to remove $type from DB");
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$dbForDatabases->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
$queueForDatabase
@@ -173,7 +173,7 @@ class Decrement extends Action
return;
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
try {
$document = $dbForDatabases->decreaseDocumentAttribute(
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
@@ -173,7 +173,7 @@ class Increment extends Action
return;
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
try {
$document = $dbForDatabases->increaseDocumentAttribute(
collection: 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence(),
@@ -165,7 +165,7 @@ class Delete extends Action
return;
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$documents = [];
try {
@@ -191,7 +191,7 @@ class Update extends Action
return;
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$documents = [];
try {
@@ -167,7 +167,7 @@ class Upsert extends Action
return;
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$upserted = [];
try {
@@ -358,7 +358,7 @@ class Create extends Action
return;
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
try {
$created = [];
$dbForDatabases->withPreserveDates(
@@ -121,7 +121,7 @@ class Delete extends Action
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
// Read permission should not be required for delete
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
@@ -88,7 +88,7 @@ class Get extends Action
$collection = $authorization->skip(fn () => $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId));
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
if ($collection->isEmpty() || (!$collection->getAttribute('enabled', false) && !$isAPIKey && !$isPrivilegedUser)) {
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
}
@@ -90,7 +90,7 @@ class XList extends Action
throw new Exception($this->getParentNotFoundException(), params: [$collectionId]);
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$document = $dbForDatabases->getDocument('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $documentId);
if ($document->isEmpty()) {
throw new Exception($this->getNotFoundException(), params: [$documentId]);
@@ -122,7 +122,7 @@ class Update extends Action
$data = $this->parseOperators($data, $collection);
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
// Read permission should not be required for update
/** @var Document $document */
$collectionTableId = 'database_' . $database->getSequence() . '_collection_' . $collection->getSequence();
@@ -127,7 +127,7 @@ class Upsert extends Action
$data = $this->parseOperators($data, $collection);
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$allowedPermissions = [
PermissionType::Read,
PermissionType::Update,
@@ -104,7 +104,7 @@ class XList extends Action
throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
}
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$cursor = Query::getCursorQueries($queries, false);
$cursor = \reset($cursor);
@@ -109,7 +109,7 @@ class Create extends Action
Query::equal('databaseInternalId', [$db->getSequence()])
], 61);
$dbForDatabases = $getDatabasesDB($db);
$dbForDatabases = $getDatabasesDB($db, $collection);
$limit = $dbForDatabases->getLimitForIndexes();
@@ -111,7 +111,7 @@ class Update extends Action
->setAttribute('search', \implode(' ', [$collectionId, $searchName]))
);
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$dbForDatabases->updateCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence(), $permissions, $documentSecurity);
$queueForEvents
@@ -77,7 +77,7 @@ class Get extends Action
{
$database = $dbForProject->getDocument('databases', $databaseId);
$collectionDocument = $dbForProject->getDocument('database_' . $database->getSequence(), $collectionId);
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collectionDocument);
$collection = $dbForDatabases->getCollection('database_' . $database->getSequence() . '_collection_' . $collectionDocument->getSequence());
if ($collection->isEmpty()) {
@@ -117,7 +117,7 @@ class Create extends CollectionAction
throw new Exception(Exception::DATABASE_NOT_FOUND);
}
/** @var Database $dbForDatabases */
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$collections = (Config::getParam('collections', [])['vectorsdb'] ?? [])['collections'] ?? [];
$attributes = \array_map(function (Attribute $attribute) use ($dimension) {
@@ -104,7 +104,7 @@ class Update extends CollectionAction
->setAttribute('search', \implode(' ', [$collectionId, $name ?? $collection->getAttribute('name')]))
);
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases = $getDatabasesDB($database, $collection);
$dbForDatabases->updateCollection('database_' . $database->getSequence() . '_collection_' . $updated->getSequence(), $permissions, $documentSecurity);
$queueForEvents
@@ -12,8 +12,8 @@ use Utopia\Query\Schema\ColumnType;
* Stamps database/collection metadata onto every document returned from the database,
* and recursively decorates nested relationship documents.
*
* Collection ID mapping is pre-populated by getDatabasesDB via setCollectionId()
* and cached statically per database sequence for the Swoole worker lifetime.
* Collection ID mappings are registered by endpoint actions via setCollectionId()
* before querying documents. No bulk queries or static caches are needed.
*/
class Metadata implements Decorator
{
@@ -23,9 +23,6 @@ class Metadata implements Decorator
/** @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(
@@ -36,26 +33,10 @@ class Metadata implements Decorator
/**
* 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;
$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