Skip to content

feat: joins v2 support#12476

Open
deepshekhardas wants to merge 1 commit into
appwrite:1.9.xfrom
deepshekhardas:feat/joins-v2
Open

feat: joins v2 support#12476
deepshekhardas wants to merge 1 commit into
appwrite:1.9.xfrom
deepshekhardas:feat/joins-v2

Conversation

@deepshekhardas
Copy link
Copy Markdown

Changes

Core Library Changes

  • Add \Types\ validator class to replace \Queries([new Limit(), new Offset()])\ pattern across all endpoints
  • Add \QueryContext\ for collection-aware query validation in document listing endpoints
  • Convert \Query::select\ array syntax to individual string arguments

API Changes

  • Add new request version filter V22 (\�pp/init/constants.php: \APP_VERSION_STABLE\ → 1.10.0)
  • Add \Response/Filters/ListSelection\ filter
  • Add empty sequence guards to all database filter callbacks
  • Add \�uthenticators, \challenges, \ argets\ fields to user document creation
  • Add \ eams.php\ controller placeholder
  • Update migration map with 1.10.0 → V24

Test Changes

  • Update \RuntimeQueryTest\ for new query API
  • Update \ProjectsConsoleClientTest, \RealtimeCustomClientQueryTest\
  • Update \FunctionsCustomServerTest, \SitesCustomServerTest, \UsersBase\ etc.

Closes #11121

- 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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 3, 2026

Greptile Summary

This PR introduces joins v2 support by migrating Query::select() from the old array syntax (select([\"field1\",\"field2\"]) to individual string arguments (select(\"field1\"), select(\"field2\")), introducing a new Types validator that wraps QueryContext-aware validation, and adding join resolution logic in the document listing endpoint.

  • Query API migration: All internal usages of Query::select([...]) are replaced with one Query::select('field') call per field across workers, adapters, and event processors. ListSelection, RuntimeQuery, and Base validator are updated to use getAttribute() (new API) instead of getValues().
  • Types validator: New Types class provides a reusable, type-restricted validator; Base now extends it and builds a QueryContext from the collection schema for native join validation.
  • Joins v2 endpoint: Documents/XList.php adds a join resolution pass that maps user-facing collection IDs to internal table IDs and registers them with the DB layer before executing the query. Several user-creation paths also receive missing challenges and targets null-initializations for schema consistency.

Confidence Score: 3/5

The 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

Filename Overview
src/Appwrite/Utopia/Request/Filters/V17.php SELECT case still creates a single multi-value query via parseQuery while all downstream code now expects individual single-attribute select queries, risking silent field-selection breakage for pre-v18 clients
src/Appwrite/Utopia/Request/Filters/V20.php Wildcard check uses getValues() while new API stores '*' in attribute; backward-compat SELECT is generated with parseQuery/values format incompatible with ListSelection's getAttribute() calls
src/Appwrite/Utopia/Database/Validator/Queries/Types.php New validator wrapping DocumentsValidator with type and attribute allow-listing; nested query error message is swallowed and replaced with generic string
src/Appwrite/Utopia/Response/Filters/ListSelection.php Correctly migrated from iterating getValues() to a single getAttribute() call per query; safe when queries come from new-format select() calls but may mishandle old-format multi-value queries
src/Appwrite/Platform/Workers/Deletes.php Correctly migrated Query::select() from array to individual calls throughout; new console-project guard in deleteStats() skips logsDB cleanup without explanation, risking silent stat accumulation
src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php Adds join query resolution block: resolves user-facing collection IDs to internal table IDs, checks enabled status, and registers with DB layer before find(); looks correct
src/Appwrite/Platform/Modules/Projects/Http/Projects/XList.php Removes the application-level subquery-filter-skipping optimization; now delegates directly to dbForPlatform.find() relying on library-level joins v2 for efficiency
src/Appwrite/Utopia/Database/RuntimeQuery.php Correctly migrated isSelectAll() and compile() to use getAttribute() === '*' instead of iterating getValues(); logic is clean
src/Appwrite/Utopia/Database/Validator/Queries/Base.php Refactored to use QueryContext and new Types parent class; correctly adds all collection attributes to context and internal attributes to allowedAttributes
app/init/database/filters.php Adds empty-sequence guards to all subquery filter callbacks to avoid unnecessary DB queries when documents lack a sequence; straightforward defensive addition
app/controllers/api/account.php Adds missing challenges and targets null fields to several user-creation paths, and replaces legacy Queries([Limit, Offset]) validator with new Types([TYPE_LIMIT, TYPE_OFFSET])
src/Appwrite/Migration/Migration.php Adds 1.10.0 to V24 migration mapping entry; straightforward one-liner addition

Comments Outside Diff (1)

  1. src/Appwrite/Utopia/Request/Filters/V20.php, line 80-106 (link)

    P1 V20 wildcard detection and SELECT generation inconsistent with new API

    Two related issues exist here. First, the wildcard check on line 82 uses $select->getValues() to search for '*', but all other places in this PR (e.g., RuntimeQuery::isSelectAll, ListSelection::parse) detect wildcards via $query->getAttribute() === '*'. If incoming old-format SELECT queries are now parsed such that '*' is stored in attribute rather than values, this check silently fails and $needsRelationships is never set to true.

    Second, line 103 creates the backward-compat SELECT via Query::parseQuery(['method' => TYPE_SELECT, 'values' => $selects]), which stores fields in values (not attribute). ListSelection::parse() calls $query->getAttribute() on these queries, which would return '' rather than '*', so the early-return wildcard path in ListSelection is never triggered. The V20-generated wildcard select would then produce $selections = ['' => true], filtering the response to $-prefixed fields only and dropping all document content for pre-1.8 clients.

Reviews (1): Last reviewed commit: "feat: joins v2 support (PR #11121)" | Re-trigger Greptile

Comment on lines +264 to +268

case Query::TYPE_SELECT:
return new Query($method, values: $parsedParams[0]);
return Query::parseQuery([
'method' => $method,
'values' => $parsedParams[0],
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 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.

Comment on lines +619 to +640
$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
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.

P2 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.

Comment on lines +72 to +76
if ($query->isNested()) {
if (!$this->isValid($query->getValues())) {
throw new \Exception('Invalid queries');
}
}
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.

P2 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant