Merge pull request #10393 from appwrite/fix-nested-filter-selects

Fix nested filter selects
This commit is contained in:
Jake Barnby 2025-08-28 00:53:10 +12:00 committed by GitHub
commit 05f34c2485
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 84 additions and 76 deletions

View file

@ -5146,7 +5146,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"tags": [
"databases"
@ -7800,7 +7800,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"tags": [
"tablesDB"

View file

@ -9360,7 +9360,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"tags": [
"databases"
@ -36595,7 +36595,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"tags": [
"tablesDB"

View file

@ -8842,7 +8842,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"tags": [
"databases"
@ -26960,7 +26960,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"tags": [
"tablesDB"

View file

@ -5146,7 +5146,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"tags": [
"databases"
@ -7800,7 +7800,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"tags": [
"tablesDB"

View file

@ -9360,7 +9360,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"tags": [
"databases"
@ -36595,7 +36595,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"tags": [
"tablesDB"

View file

@ -8842,7 +8842,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"tags": [
"databases"
@ -26960,7 +26960,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"tags": [
"tablesDB"

View file

@ -5273,7 +5273,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"consumes": [
"application\/json"
@ -7867,7 +7867,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"consumes": [
"application\/json"

View file

@ -9462,7 +9462,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"consumes": [
"application\/json"
@ -36702,7 +36702,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"consumes": [
"application\/json"

View file

@ -8934,7 +8934,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"consumes": [
"application\/json"
@ -27127,7 +27127,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"consumes": [
"application\/json"

View file

@ -5273,7 +5273,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"consumes": [
"application\/json"
@ -7867,7 +7867,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"consumes": [
"application\/json"

View file

@ -9462,7 +9462,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"consumes": [
"application\/json"
@ -36702,7 +36702,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"consumes": [
"application\/json"

View file

@ -8934,7 +8934,7 @@
]
},
"put": {
"summary": "Create or update a document",
"summary": "Upsert a document",
"operationId": "databasesUpsertDocument",
"consumes": [
"application\/json"
@ -27127,7 +27127,7 @@
]
},
"put": {
"summary": "Create or update a row",
"summary": "Upsert a row",
"operationId": "tablesDBUpsertRow",
"consumes": [
"application\/json"

View file

@ -810,12 +810,6 @@ App::shutdown()
}
if (!empty($queueForDatabase->getType())) {
Console::info("Triggering database event: \n" . \json_encode([
'projectId' => $project->getId(),
'databaseId' => $queueForDatabase->getDatabase()?->getId(),
'tableId' => $queueForDatabase->getTable()?->getId() ?? $queueForDatabase->getCollection()?->getId(),
'rowId' => $queueForDatabase->getRow()?->getId() ?? $queueForDatabase->getDocument()?->getId(),
]));
$queueForDatabase->trigger();
}

12
composer.lock generated
View file

@ -3557,16 +3557,16 @@
},
{
"name": "utopia-php/database",
"version": "1.2.1",
"version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "99beaf1dd6dc3561c8332f9893325777553644a4"
"reference": "8a536fead840d9da6ee819fe6b80e0f047997f69"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/99beaf1dd6dc3561c8332f9893325777553644a4",
"reference": "99beaf1dd6dc3561c8332f9893325777553644a4",
"url": "https://api.github.com/repos/utopia-php/database/zipball/8a536fead840d9da6ee819fe6b80e0f047997f69",
"reference": "8a536fead840d9da6ee819fe6b80e0f047997f69",
"shasum": ""
},
"require": {
@ -3607,9 +3607,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/1.2.1"
"source": "https://github.com/utopia-php/database/tree/1.2.3"
},
"time": "2025-08-26T16:05:26+00:00"
"time": "2025-08-27T11:47:04+00:00"
},
{
"name": "utopia-php/detector",

View file

@ -45,7 +45,7 @@ class Upsert extends Action
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/databases/:databaseId/collections/:collectionId/documents/:documentId')
->desc('Create or update a document')
->desc('Upsert a document')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].collections.[collectionId].documents.[documentId].upsert')
->label('scope', 'documents.write')

View file

@ -31,7 +31,7 @@ class Upsert extends DocumentUpsert
$this
->setHttpMethod(self::HTTP_REQUEST_METHOD_PUT)
->setHttpPath('/v1/tablesdb/:databaseId/tables/:tableId/rows/:rowId')
->desc('Create or update a row')
->desc('Upsert a row')
->groups(['api', 'database'])
->label('event', 'databases.[databaseId].tables.[tableId].rows.[rowId].upsert')
->label('scope', ['rows.write', 'documents.write'])

View file

@ -64,14 +64,6 @@ class Databases extends Action
$collection = new Document($payload['table'] ?? $payload['collection'] ?? []);
$database = new Document($payload['database'] ?? []);
Console::info("Processing database operation: \n" . \json_encode([
'type' => $type,
'projectId' => $project->getId(),
'databaseId' => $database->getId(),
'collectionId' => $collection->getId(),
'documentId' => $document->getId(),
], JSON_PRETTY_PRINT));
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);

View file

@ -32,59 +32,67 @@ class V20 extends Filter
*/
protected function manageSelectQueries(array $content): array
{
$hasWildcard = false;
if (!isset($content['queries'])) {
$hasWildcard = true;
// only query, make it json encoded!
$content['queries'] = [Query::select(['*'])->toString()];
$content['queries'] = [];
}
// Handle case where queries is an array but empty
if (\is_array($content['queries'])) {
$content['queries'] = \array_filter($content['queries'], function ($q) {
if (\is_object($q) && empty((array)$q)) {
return false;
}
if (\is_string($q) && \trim($q) === '') {
return false;
}
if (empty($q)) {
return false;
}
return true;
});
}
try {
$parsed = Query::parseQueries($content['queries']);
} catch (QueryException) {
// don't crash!
return $content;
}
$selections = Query::groupByType($parsed)['selections'] ?? [];
// If there are no select queries at all, add wildcard
if (empty($selections)) {
$hasWildcard = true;
$parsed[] = Query::select(['*']);
} elseif (!$hasWildcard) {
// check if any select includes a wildcard as we added one above
// Check if we need to add wildcard + relationships
// This happens when:
// 1. No select queries exist, OR
// 2. A wildcard select exists
$needsRelationships = empty($selections);
if (!$needsRelationships) {
foreach ($selections as $select) {
if (\in_array('*', $select->getValues(), true)) {
$hasWildcard = true;
$needsRelationships = true;
break;
}
}
}
/**
* Add `keys.*` for all model types!
* Add wildcard and relationship selects for backward compatibility
*/
if ($hasWildcard) {
if ($needsRelationships) {
$relatedKeys = $this->getRelatedCollectionKeys();
$selects = \array_values(\array_unique(\array_merge(['*'], $relatedKeys)));
if (! empty($relatedKeys)) {
$selects = \array_values(\array_unique(\array_merge(['*'], $relatedKeys)));
// Remove any existing select queries
$parsed = \array_filter(
$parsed,
fn ($query) => $query->getMethod() !== Query::TYPE_SELECT
);
// remove previous select queries
$parsed = \array_filter(
$parsed,
fn ($query) => $query->getMethod() !== Query::TYPE_SELECT
);
// add wildcard + relationship(s) selects
$parsed[] = Query::select($selects);
}
// Add wildcard + relationship(s) selects
$parsed[] = Query::select($selects);
}
$resolvedQueries = [];
foreach ($parsed as $query) {
// make em json encoded!
$resolvedQueries[] = $query->toString();
}
@ -95,12 +103,15 @@ class V20 extends Filter
/**
* Returns all relationship attribute keys in `key.*` format for use with `Query::select`.
* Recursively includes nested relationships up to 3 levels deep.
* Prevents infinite loops by tracking all visited collections in the current path.
*/
private function getRelatedCollectionKeys(
?string $databaseId = null,
?string $collectionId = null,
?string $prefix = null,
int $depth = 1,
array $visited = []
): array {
$databaseId ??= $this->getParamValue('databaseId');
$collectionId ??= $this->getParamValue('collectionId');
@ -113,6 +124,13 @@ class V20 extends Filter
return [];
}
// Check if we've already visited this collection in the current path to prevent cycles
if (in_array($collectionId, $visited)) {
return [];
}
$visited[] = $collectionId;
$dbForProject = $this->getDbForProject();
if ($dbForProject === null) {
return [];
@ -144,20 +162,24 @@ class V20 extends Filter
$key = $attr['key'];
$fullKey = $prefix ? $prefix . '.' . $key : $key;
$relatedCollectionId = $attr['relatedCollection'] ?? null;
// Skip this relationship entirely if it points to an already visited collection
if ($relatedCollectionId && in_array($relatedCollectionId, $visited)) {
continue;
}
// Add the wildcard select for this relationship
$relationshipKeys[] = $fullKey . '.*';
// Get the related collection for nested relationships
$relatedCollectionId = $attr['relatedCollection'] ?? null;
// Continue recursively if we have a related collection
if ($relatedCollectionId) {
// Recursively get nested relationship keys
$nestedKeys = $this->getRelatedCollectionKeys(
$databaseId,
$relatedCollectionId,
$fullKey,
$depth + 1,
$visited
);
$relationshipKeys = \array_merge($relationshipKeys, $nestedKeys);