feat: joins v2 support#12476
Conversation
- Add Types validator replacing Queries/Limit/Offset pattern - Add QueryContext for collection-aware query validation - Add Request/Filter/V22 version filter - Add Response/Filter/ListSelection filter - Add empty sequence guards to database filters - Add user attributes (authenticators, challenges, targets) - Add teams.php controller placeholder - Bump APP_VERSION_STABLE to 1.10.0 - Convert Query::select array syntax to individual strings - Fix various use statement imports and type hints - Update tests for new query API
Greptile SummaryThis PR introduces joins v2 support by migrating
Confidence Score: 3/5The core joins v2 logic and internal query migration look sound, but the backward-compatibility filters (V17 and V20) appear to generate SELECT queries in the old multi-value format while all downstream consumers now read only the single-attribute property, which could silently strip response fields for clients on older API versions. The request filters V17 and V20 both use Query::parseQuery with values arrays to produce SELECT queries, yet ListSelection::parse() and every other consumer introduced by this PR call getAttribute() which reads the attribute property. If the library now stores the selected field in attribute for single-string selects (consistent with the test migrations from select(['']) to select('')), these filter-generated queries would return an empty attribute, causing the response filter to drop all non-$-prefixed fields for pre-v18 and pre-v21 clients. src/Appwrite/Utopia/Request/Filters/V17.php and src/Appwrite/Utopia/Request/Filters/V20.php — both generate backward-compat SELECT queries that may be incompatible with how ListSelection and RuntimeQuery now read query attributes. Important Files Changed
|
|
|
||
| case Query::TYPE_SELECT: | ||
| return new Query($method, values: $parsedParams[0]); | ||
| return Query::parseQuery([ | ||
| 'method' => $method, | ||
| 'values' => $parsedParams[0], |
There was a problem hiding this comment.
SELECT backward-compat conversion uses old multi-value format
The V17 filter converts old select(["field1","field2"]) into a single query via Query::parseQuery(['method' => TYPE_SELECT, 'values' => $parsedParams[0]]), producing one Query object with the fields stored in the values array. However, all new code in this PR — ListSelection::parse(), RuntimeQuery::isSelectAll(), and RuntimeQuery::compile() — exclusively calls $query->getAttribute() to read the selected field, which reads from the attribute property (not values). If the library at v5.9.0 now stores the selected field in attribute for single-string selects (as evidenced by the test changes from select(['*']) to select('*') and the getAttribute() === '*' checks), then a query built with parseQuery(['values' => ['f1','f2']]) would have attribute = '', silently breaking field selection for any client on API version ≤ 17. Each old field should become a separate Query::select('field') call instead.
| $queries[] = Query::select($select); | ||
| } | ||
|
|
||
| // Delete Usage stats from logsDB | ||
| $this->deleteByGroup('stats', [ | ||
| Query::select($selects), | ||
| Query::equal('period', ['1h']), | ||
| Query::lessThan('time', $hourlyUsageRetentionDatetime), | ||
| Query::orderDesc('time'), | ||
| Query::orderDesc(), | ||
| ], $dbForLogs); | ||
| // Delete Usage stats from projectDB | ||
| $this->deleteByGroup('stats', $queries, $dbForProject); | ||
|
|
||
| if ($project->getId() !== 'console') { | ||
| /** @var Database $dbForLogs */ | ||
| $dbForLogs = call_user_func($getLogsDB, $project); | ||
|
|
||
| $queries = [ | ||
| Query::equal('period', ['1h']), | ||
| Query::lessThan('time', $hourlyUsageRetentionDatetime), | ||
| Query::orderDesc('time'), | ||
| Query::orderDesc(), | ||
| ]; | ||
|
|
||
| foreach ($selects as $select) { | ||
| $queries[] = Query::select($select); | ||
| } | ||
|
|
||
| // Delete Usage stats from logsDB |
There was a problem hiding this comment.
Console project logsDB stats skipped silently — potential data accumulation
The new if ($project->getId() !== 'console') guard prevents deleteByGroup from running against logsDB for the console project. The original code always ran this deletion for every project. If the console project has hourly usage stats stored in logsDB, those stats will now accumulate indefinitely. A comment explaining why the console project has no logsDB (or a guard that checks availability rather than the project ID) would make the intent clearer.
| if ($query->isNested()) { | ||
| if (!$this->isValid($query->getValues())) { | ||
| throw new \Exception('Invalid queries'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Nested validation failure discards the specific error message
When $this->isValid($query->getValues()) returns false, the recursive call has already stored the specific reason in $this->message. The immediately following throw new \Exception('Invalid queries') then replaces that context with a generic string. The outer catch (\Throwable $e) at line 100 captures only this generic message and sets it back into $this->message, so callers see "Invalid queries" rather than the root cause.
Changes
Core Library Changes
API Changes
Test Changes
Closes #11121