Skip to content

Conversation

@fogelito
Copy link
Contributor

@fogelito fogelito commented Nov 20, 2025

Summary by CodeRabbit

  • New Features

    • Join support added (inner, left, right) with relation-equal queries and cross-collection ordering.
    • QueryContext introduced to drive authorization, selections and multi-collection operations.
  • Refactoring

    • Selection API now uses single-field selects (compose multi-field projections via repeated selects).
    • Projections tightened — internal fields are returned only when explicitly selected; cursor and ordering/pagination revamped.
  • Tests

    • Expanded end-to-end tests covering joins, projections, ordering and permission scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

# Conflicts:
#	composer.lock
#	src/Database/Adapter/Mongo.php
#	src/Database/Database.php
#	src/Database/Query.php
#	tests/unit/Validator/QueriesTest.php
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/Database/Query.php (1)

1224-1306: groupByType() assigns the wrong fallback variables for offset and cursor.
$offset and $cursor currently fall back to $limit, which can silently corrupt pagination.

Proposed fix
 case Query::TYPE_OFFSET:
@@
-    $offset = $values[0] ?? $limit;
+    $offset = $values[0] ?? $offset;
     break;
@@
 case Query::TYPE_CURSOR_AFTER:
 case Query::TYPE_CURSOR_BEFORE:
@@
-    $cursor = $values[0] ?? $limit;
+    $cursor = $values[0] ?? $cursor;
     $cursorDirection = $method === Query::TYPE_CURSOR_AFTER ? Database::CURSOR_AFTER : Database::CURSOR_BEFORE;
     break;
src/Database/Adapter/Mongo.php (2)

1128-1159: Projection bug: getDocument() passes mixed $queries into getAttributeProjection()

getAttributeProjection() assumes a list of SELECT queries, but getDocument() passes $queries (potentially mixed: filters/orders/etc.). If any non-select query is present, projection can unintentionally exclude fields and return incomplete documents.

Fix either by (a) passing only selects into getAttributeProjection(), or (b) making getAttributeProjection() ignore non-TYPE_SELECT entries.

Suggested guard (option b)
 protected function getAttributeProjection(array $selects): array
 {
     $projection = [];

     if (empty($selects)) {
         return [];
     }

     foreach ($selects as $select) {
+        if ($select->getMethod() !== Query::TYPE_SELECT) {
+            continue;
+        }
         if ($select->getAttribute() === '$collection') {
             continue;
         }
         // ...
     }

     return $projection;
 }

2042-2074: Inconsistent result normalization between first batch and getMore

First batch does new Document($this->convertStdClassToArray($record)), but getMore path does new Document($record) (no deep conversion). This will produce inconsistent shapes/types across pages.

Also: unlike getDocument(), find() is not applying castingAfter($collection, $doc), so type casting may silently regress.

Unify both paths (convert + cast in both).

Proposed fix
-                $doc = new Document($this->convertStdClassToArray($record));
+                $doc = new Document($this->convertStdClassToArray($record));
                 if ($removeSequence) {
                     $doc->removeAttribute('$sequence');
                 }
+                $doc = $this->castingAfter($collection, $doc);
                 $found[] = $doc;

@@
-                    $doc = new Document($record);
+                    $doc = new Document($this->convertStdClassToArray($record));
                     if ($removeSequence) {
                         $doc->removeAttribute('$sequence');
                     }
+                    $doc = $this->castingAfter($collection, $doc);
                     $found[] = $doc;
🤖 Fix all issues with AI agents
In `@src/Database/Adapter/SQL.php`:
- Around line 374-379: In getDocument() (where the SQL SELECT is built) the
WHERE clause mixes a quoted alias with an unquoted column
(`{$this->quote($alias)}._uid`); change the column to be quoted as well so
identifier quoting is consistent — replace `{$this->quote($alias)}._uid` with
`{$this->quote($alias)}.{$this->quote('_uid')}` (keep the surrounding use of
getAttributeProjection, getSQLTable, quote($alias) and getTenantQuery as-is).

In `@src/Database/Database.php`:
- Around line 7858-7867: The comment in find() contradicts the code: either
update the comment to state that when no unique order on $id or $sequence is
found the code appends Query::orderAsc() to $orders, or change the logic so the
default order is only appended when not in a join context (e.g., add/check an
$isJoin or similar flag before pushing Query::orderAsc()); locate the
$uniqueOrderBy variable, the foreach over $orders, and the Query::orderAsc()
append and implement one of these fixes to make behavior and comment consistent.
- Around line 8383-8388: The early-return in the casting method is inverted:
change the condition in Database::casting(QueryContext $context, Document
$document, array $selects = []) so the method returns immediately when the
adapter reports it DOES support casting (adapter->getSupportForCasting() ===
true), and only performs manual casting when getSupportForCasting() is false
(e.g., Mongo). Locate the casting method and flip the boolean check on
$this->adapter->getSupportForCasting(), preserving the existing return
$document; behavior and ensuring manual casting logic runs for adapters that do
not support casting.
♻️ Duplicate comments (11)
src/Database/Database.php (4)

5794-5798: Post-write paths still skip casting() before decode() (type normalization inconsistency).
This appears to repeat the earlier review concern: updateDocument() and updateDocuments() decode without casting, and upsertDocumentsWithIncrease() decodes only in one branch.

Also applies to: 6024-6030, 6847-6851


5950-5957: Bulk update/delete: either auto-add required system selects or document the requirement.
These runtime checks (“Missing required attribute … in select query”) are easy to trip if callers use any explicit selects. Consider auto-injecting $permissions and $sequence similarly to other system-select logic.

Also applies to: 7632-7640


8327-8330: Throw QueryException (not generic \\Exception) for unknown alias context in decode()/casting().
Keeps error handling consistent with the rest of the query pipeline.

Also applies to: 8426-8429


8869-8875: Fix unused $idAdded and avoid auto-adding $id when selects are empty (same projection-mode risk).
Also matches the prior guidance: only inject $id for relationship-bearing collections, and don’t capture the unused flag. Based on learnings, keep $id injection relationship-only and remove $idAdded.

Proposed fix
-        if (!empty($relationships)) {
-            [$queries, $idAdded] = QueryContext::addSelect($queries, Query::select('$id', system: true));
-        }
+        if (!empty($relationships) && !empty($queries)) { // only when in projection mode
+            [$queries] = QueryContext::addSelect($queries, Query::select('$id', system: true));
+        }
src/Database/Query.php (2)

79-127: Query::TYPES is now out of sync with isMethod() (join/relation types missing).
If any validators/consumers rely on Query::TYPES for “allowed methods”, they’ll reject join/relation queries even though parseQuery() accepts them via isMethod().

Proposed fix
 public const TYPES = [
@@
         self::TYPE_ELEM_MATCH,
-        self::TYPE_REGEX
+        self::TYPE_REGEX,
+        self::TYPE_RELATION_EQUAL,
+        self::TYPE_INNER_JOIN,
+        self::TYPE_LEFT_JOIN,
+        self::TYPE_RIGHT_JOIN,
     ];

Also applies to: 407-461


525-568: parseQuery() should validate join metadata + nested elements to avoid leaking TypeError.
Right now attributeRight/alias/aliasRight/as/collection can be non-strings and nested values[] can be non-arrays, which will throw a TypeError (or argument type error) instead of a QueryException.

Proposed fix
 public static function parseQuery(array $query): self
 {
     $method = $query['method'] ?? '';
     $attribute = $query['attribute'] ?? '';
     $attributeRight = $query['attributeRight'] ?? '';
     $values = $query['values'] ?? [];
     $alias = $query['alias'] ?? '';
     $aliasRight = $query['aliasRight'] ?? '';
     $as = $query['as'] ?? '';
     $collection = $query['collection'] ?? '';

     if (!\is_string($method)) {
         throw new QueryException('Invalid query method. Must be a string, got ' . \gettype($method));
     }
@@
     if (!\is_array($values)) {
         throw new QueryException('Invalid query values. Must be an array, got ' . \gettype($values));
     }
+
+    foreach ([
+        'attributeRight' => $attributeRight,
+        'alias' => $alias,
+        'aliasRight' => $aliasRight,
+        'as' => $as,
+        'collection' => $collection,
+    ] as $key => $value) {
+        if (!\is_string($value)) {
+            throw new QueryException("Invalid query {$key}. Must be a string, got " . \gettype($value));
+        }
+    }

-    if (\in_array($method, self::LOGICAL_TYPES) || \in_array($method, self::JOINS_TYPES)) {
+    if (\in_array($method, self::LOGICAL_TYPES, true) || \in_array($method, self::JOINS_TYPES, true)) {
         foreach ($values as $index => $value) {
+            if (!\is_array($value)) {
+                throw new QueryException("Invalid query values[{$index}]. Must be an array, got " . \gettype($value));
+            }
             $values[$index] = self::parseQuery($value);
         }
     }
src/Database/Adapter/Postgres.php (1)

1745-1755: Filter getRightAlias() before quoting in TYPE_RELATION_EQUAL (injection/malformed identifier risk).
Left alias is filtered, but right alias isn’t.

Proposed fix
 case Query::TYPE_RELATION_EQUAL:
     $attributeRight = $this->quote($this->filter($query->getAttributeRight()));
-    $aliasRight = $this->quote($query->getRightAlias());
+    $aliasRight = $this->quote($this->filter($query->getRightAlias()));

     return "{$alias}.{$attribute}={$aliasRight}.{$attributeRight}";

Also applies to: 1802-1807

tests/e2e/Adapter/Scopes/DocumentTests.php (1)

4279-4283: Decode input likely should be the encoded $result, not the original $document (still).
This matches the earlier review note; if the intent is to test decode independently, an inline comment would help.

Proposed change
         $context = new QueryContext();
         $context->add($collection);

-        $result = $database->decode($context, $document);
+        $result = $database->decode($context, $result);
src/Database/Adapter/Mongo.php (1)

1910-2024: Guard against ORDER_RANDOM before calling getOrderDirection()
This code still calls $order->getOrderDirection() for every order. If an orderRandom() query reaches Mongo, this can throw and take down the request path; either explicitly reject it here (clear “not supported”) or ensure it’s filtered earlier.

src/Database/Adapter/SQL.php (2)

2357-2395: getAttributeProjection() must ignore non-SELECT queries (especially because getDocument() passes $queries)

Without a TYPE_SELECT guard, any non-select query in $queries can be rendered into the SELECT list, producing invalid SQL or wrong projections.

Proposed fix
 foreach ($selects as $select) {
+    if ($select->getMethod() !== Query::TYPE_SELECT) {
+        continue;
+    }
     if ($select->getAttribute() === '$collection') {
         continue;
     }
     // ...
 }

3091-3124: RIGHT JOIN permission clause uses unquoted {$alias}._uid IS NULL + verify skipAuth() keying

  • The OR {$alias}._uid IS NULL branch should use the same quoting helper as getSQLPermissionsCondition().
  • Also, skipAuth() is called with filtered collection IDs; please confirm QueryContext::addSkipAuth() uses the same (filtered vs raw) keys, otherwise skip-auth may silently fail.

Also applies to: 3253-3286

🧹 Nitpick comments (5)
src/Database/Database.php (1)

8739-8765: Replace md5(serialize($selects)) with a normalized string using stable Query fields.

Object serialization includes internal implementation details (like $onArray, $isObjectAttribute) that aren't part of the public API. If the Query class evolves, the serialization hash changes even when the logical selection hasn't, causing unnecessary cache misses. Instead, build the hash from stable getter methods—getAlias(), getAttribute(), getAs(), getMethod(), isSystem()—to create a deterministic key tied only to the relevant selection parameters.

tests/e2e/Adapter/Scopes/DocumentTests.php (2)

3197-3214: Query::select('director') addition is correct for the new per-field select API.
Optional: consider also asserting the selected field is present (e.g., director) since you already assert name is absent.


3792-3930: Find-select tests are updated correctly for per-field selects + internal-field visibility.
This gives solid coverage across user fields and system fields ($id, $sequence, $createdAt, $updatedAt, $permissions).
Optional: the repeated “internal keys not present” assertions could be extracted into a tiny helper to reduce duplication/noise.

src/Database/Adapter.php (1)

829-841: Avoid the “mega-signature” by introducing a single query DTO (optional)

find() now has many parallel arrays ($selects/$filters/$joins/$vectors/$orderQueries), which is hard to evolve and easy to call incorrectly. Consider a QueryPlan/FindParams value object (or moving more of these into QueryContext) so future additions don’t keep widening the interface.

src/Database/Adapter/Mongo.php (1)

2704-2737: Projection builder should reuse getInternalKeyForAttribute() and ignore non-selects

Right now this re-implements the internal attribute mapping via an inline match, and (as above) it will happily treat non-select queries as projection entries.

  • Prefer $this->getInternalKeyForAttribute($attribute) for consistency.
  • Add a TYPE_SELECT guard to make it safe when callers pass mixed query arrays.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ff480f and b4d14af.

📒 Files selected for processing (12)
  • src/Database/Adapter.php
  • src/Database/Adapter/MariaDB.php
  • src/Database/Adapter/Mongo.php
  • src/Database/Adapter/Pool.php
  • src/Database/Adapter/Postgres.php
  • src/Database/Adapter/SQL.php
  • src/Database/Database.php
  • src/Database/Query.php
  • src/Database/Validator/Queries/V2.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
  • tests/e2e/Adapter/Scopes/ObjectAttributeTests.php
  • tests/e2e/Adapter/Scopes/SchemalessTests.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Database/Adapter/MariaDB.php
  • src/Database/Validator/Queries/V2.php
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
Repo: utopia-php/database PR: 721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • tests/e2e/Adapter/Scopes/ObjectAttributeTests.php
  • src/Database/Adapter.php
  • tests/e2e/Adapter/Scopes/SchemalessTests.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
  • src/Database/Adapter/SQL.php
  • src/Database/Query.php
  • src/Database/Adapter/Mongo.php
  • src/Database/Database.php
📚 Learning: 2025-11-25T10:46:37.666Z
Learnt from: fogelito
Repo: utopia-php/database PR: 763
File: src/Database/Database.php:8671-8751
Timestamp: 2025-11-25T10:46:37.666Z
Learning: In Utopia\Database\Database::processRelationshipQueries, ensure the system select for '$id' is added only when the collection has relationship attributes (i.e., !empty($relationships)), and avoid capturing the unused '$idAdded' from Query::addSelect to satisfy static analysis.

Applied to files:

  • tests/e2e/Adapter/Scopes/ObjectAttributeTests.php
  • src/Database/Adapter/Postgres.php
  • tests/e2e/Adapter/Scopes/SchemalessTests.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
  • src/Database/Adapter/SQL.php
  • src/Database/Query.php
  • src/Database/Database.php
📚 Learning: 2025-07-01T11:31:37.438Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 613
File: src/Database/Adapter/Postgres.php:1254-1319
Timestamp: 2025-07-01T11:31:37.438Z
Learning: In PostgreSQL adapter methods like getUpsertStatement, complexity for database-specific SQL generation is acceptable when the main business logic is properly separated in the parent SQL adapter class, following the adapter pattern where each database adapter handles its own SQL syntax requirements.

Applied to files:

  • src/Database/Adapter/Postgres.php
📚 Learning: 2025-10-29T12:27:57.071Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 747
File: src/Database/Adapter/Mongo.php:1449-1453
Timestamp: 2025-10-29T12:27:57.071Z
Learning: In src/Database/Adapter/Mongo.php, when getSupportForAttributes() returns false (schemaless mode), the updateDocument method intentionally uses a raw document without $set operator for replacement-style updates, as confirmed by the repository maintainer ArnabChatterjee20k.

Applied to files:

  • src/Database/Adapter.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
  • src/Database/Adapter/Mongo.php
📚 Learning: 2025-10-20T05:29:29.487Z
Learnt from: abnegate
Repo: utopia-php/database PR: 731
File: src/Database/Database.php:6987-6988
Timestamp: 2025-10-20T05:29:29.487Z
Learning: In Utopia\Database\Database::convertRelationshipQueries, Many-to-Many filtering does not need the parent collection or a direct junction query: it calls find() on the related collection without skipping relationships, which populates relationship attributes (including the two-way key). Parent IDs are derived from that populated attribute. Therefore, calls should remain convertRelationshipQueries($relationships, $queries).

Applied to files:

  • src/Database/Database.php
📚 Learning: 2025-10-16T09:37:33.531Z
Learnt from: fogelito
Repo: utopia-php/database PR: 733
File: src/Database/Adapter/MariaDB.php:1801-1806
Timestamp: 2025-10-16T09:37:33.531Z
Learning: In the MariaDB adapter (src/Database/Adapter/MariaDB.php), only duplicate `_uid` violations should throw `DuplicateException`. All other unique constraint violations, including `PRIMARY` key collisions on the internal `_id` field, should throw `UniqueException`. This is the intended design to distinguish between user-facing document duplicates and internal/user-defined unique constraint violations.

Applied to files:

  • src/Database/Database.php
📚 Learning: 2025-07-30T19:17:53.630Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 642
File: src/Database/Validator/PartialStructure.php:43-52
Timestamp: 2025-07-30T19:17:53.630Z
Learning: In PartialStructure validator, when filtering for required attributes validation using the $requiredAttributes parameter, $this->attributes should be used instead of the merged $attributes array because this validation is specifically for internal attributes like $createdAt and $updatedAt that are defined in the base Structure class, not collection-specific attributes.

Applied to files:

  • src/Database/Database.php
🧬 Code graph analysis (6)
tests/e2e/Adapter/Scopes/ObjectAttributeTests.php (1)
src/Database/Query.php (2)
  • Query (8-1614)
  • select (800-803)
src/Database/Adapter/Postgres.php (4)
src/Database/Query.php (5)
  • getAttributeRight (296-299)
  • getAttribute (264-267)
  • getAlias (286-289)
  • Query (8-1614)
  • getRightAlias (291-294)
src/Database/Adapter/SQL.php (1)
  • getInternalKeyForAttribute (2397-2409)
src/Database/Adapter.php (1)
  • quote (491-491)
src/Database/Adapter/MariaDB.php (1)
  • quote (1921-1924)
src/Database/Adapter.php (2)
src/Database/Adapter/Mongo.php (2)
  • find (1910-2099)
  • getAttributeProjection (2704-2737)
src/Database/Adapter/SQL.php (2)
  • find (2994-3224)
  • getAttributeProjection (2357-2395)
tests/e2e/Adapter/Scopes/SchemalessTests.php (1)
src/Database/Query.php (3)
  • Query (8-1614)
  • select (800-803)
  • getAttribute (264-267)
src/Database/Adapter/Mongo.php (4)
src/Database/QueryContext.php (3)
  • QueryContext (8-136)
  • getMainCollection (40-43)
  • skipAuth (83-94)
src/Database/Adapter/Postgres.php (1)
  • renameAttribute (533-548)
src/Database/Adapter/SQL.php (4)
  • renameAttribute (310-327)
  • getInternalKeyForAttribute (2397-2409)
  • find (2994-3224)
  • count (3238-3333)
src/Database/Document.php (3)
  • removeAttribute (287-293)
  • getId (63-66)
  • Document (12-470)
src/Database/Database.php (4)
src/Database/Validator/Queries/V2.php (1)
  • V2 (23-719)
src/Database/QueryContext.php (8)
  • QueryContext (8-136)
  • add (68-76)
  • addSelect (102-135)
  • getCollections (32-35)
  • skipAuth (83-94)
  • addSkipAuth (78-81)
  • getCollectionByAlias (45-63)
  • getMainCollection (40-43)
src/Database/Query.php (10)
  • Query (8-1614)
  • getSelectQueries (1082-1087)
  • select (800-803)
  • join (1015-1018)
  • getCollection (306-309)
  • groupByType (1224-1306)
  • limit (843-846)
  • offset (854-857)
  • isNested (1313-1320)
  • isJoin (1325-1328)
src/Database/Adapter.php (3)
  • count (865-870)
  • filter (1272-1281)
  • find (829-841)
🪛 PHPMD (2.15.0)
src/Database/Adapter/Mongo.php

1919-1919: Avoid unused parameters such as '$joins'. (undefined)

(UnusedFormalParameter)


1920-1920: Avoid unused parameters such as '$vectors'. (undefined)

(UnusedFormalParameter)


2174-2174: Avoid unused parameters such as '$joins'. (undefined)

(UnusedFormalParameter)

src/Database/Database.php

8873-8873: Avoid unused local variables such as '$idAdded'. (undefined)

(UnusedLocalVariable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Setup & Build Docker Image
🔇 Additional comments (16)
src/Database/Database.php (3)

34-34: Validator alias swap looks fine, but ensure “V2” is the intended class for this file’s query-validation contract.
No issues in this hunk by itself.


4959-4983: applySelectFiltersToDocuments() looks good for post-fetch projection.
Nice to see internal attributes preserved and wildcard handled early.


4266-4274: This review comment is incorrect. The QueryContext::addSelect() method does not flip empty selects into projection mode.

When $selects is empty, the foreach loop at line 108 does not execute (no SELECT queries to iterate), so $found remains false. The condition if ($found && $merge) at line 125 evaluates to false, causing the method to return the original empty array unchanged with false for the second value. No $permissions select is added when the array is empty.

The current code at lines 4292-4295 is safe and the proposed guard (if (!empty($selects))) is unnecessary.

Likely an incorrect or invalid review comment.

tests/e2e/Adapter/Scopes/ObjectAttributeTests.php (1)

219-239: Per-field Query::select() updates look correct and keep intent readable.

Also applies to: 532-577, 836-866

tests/e2e/Adapter/Scopes/SchemalessTests.php (1)

176-201: Per-field Query::select() updates are consistent with the new selection API.

Also applies to: 919-944

src/Database/Adapter/Pool.php (3)

271-285: LGTM!

The updated find method signature properly adopts the context-first approach with QueryContext, and the delegation via func_get_args() correctly forwards all parameters to the underlying adapter.


292-299: LGTM!

The updated count method signature correctly adopts the context-first approach with QueryContext and separates filters from joins for cleaner query building.


526-529: LGTM!

The simplified signature for getAttributeProjection removing the $prefix parameter aligns with the QueryContext-based alias resolution approach.

tests/e2e/Adapter/Scopes/DocumentTests.php (6)

24-24: Import looks good; keep it consistent with other test files.
QueryContext is now referenced later in testEncodeDecode, so this inclusion is justified.


1441-1477: Select-projection assertions match the new “explicit selects only” behavior.
Good coverage for “internal fields excluded unless explicitly selected” (and $collection still present).


1481-1514: Nice guard test: reject non-select queries in getDocument “select” context.
This is useful to prevent silent misuse of the API; the assertions on message + exception type are sufficient.


1999-2004: Good addition: notSearch behavior exercised alongside search.
The expected count (4 out of 6) aligns with the seeded dataset in this test.


5020-5034: Bulk delete select lists updated cleanly (spread + array_map(Query::select) pattern).
This reads well and keeps the test intent clear while adapting to Query::select(string $field).

Also applies to: 5231-5246, 5254-5274


5509-5512: Query::select('*') is explicitly supported as a special-case "all fields" selector.

The codebase confirms '*' has defined semantics: when used with Query::select('*'), it returns all fields including internal fields (mapped in adapters as _uid, _id, _tenant, _createdAt, _updatedAt, _permissions). This is intentionally implemented in both Mongo and SQL adapters and is widely used throughout tests. No risk of confusion or footgun here.

src/Database/Adapter.php (1)

1260-1264: getAttributeProjection(array $selects) signature change looks consistent

Good direction aligning projection on “select queries” rather than generic selections.

src/Database/Adapter/Mongo.php (1)

720-737: renameAttribute() correctly maps internal attributes before $rename

Using getInternalKeyForAttribute() for $old/$new avoids mismatches for $id/$sequence/....

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Database/Query.php (1)

468-494: Fix instance method calls to static isSpatialQuery method.

The code has two instances where isSpatialQuery() is called as an instance method but it's now a static method requiring a $method parameter. These will fail at runtime:

  1. Line 131 (src/Database/Validator/Query/Filter.php): Change $query->isSpatialQuery() to Query::isSpatialQuery($method) (the $method parameter is already available).

  2. Line 488 (src/Database/Validator/Query/Filter.php): Change $value->isSpatialQuery() to Query::isSpatialQuery($value->getMethod()) (where $value is a Query instance).

♻️ Duplicate comments (1)
src/Database/Query.php (1)

529-550: New metadata fields lack type validation.

While $method and $attribute are validated (lines 536-546), the new fields ($attributeRight, $alias, $aliasRight, $as, $collection) are not type-checked. If malformed JSON provides a non-string value (e.g., "alias": []), the constructor will throw a TypeError instead of a QueryException, making error handling inconsistent.

♻️ Suggested validation
$stringFields = ['attributeRight' => $attributeRight, 'alias' => $alias, 'aliasRight' => $aliasRight, 'as' => $as, 'collection' => $collection];
foreach ($stringFields as $name => $value) {
    if (!\is_string($value)) {
        throw new QueryException("Invalid query {$name}. Must be a string, got " . \gettype($value));
    }
}
🧹 Nitpick comments (5)
src/Database/Query.php (5)

191-192: Missing getter for $system property.

The $system property is set via the constructor and select() factory (line 802), but there's no getSystem(): bool getter. Other metadata properties like $alias, $collection, $as all have corresponding getters. For consistency, consider adding:

public function isSystem(): bool
{
    return $this->system;
}

347-375: Use QueryException instead of generic \Exception.

getCursorDirection() (line 357) and getOrderDirection() (line 374) throw generic \Exception. For consistency with the rest of this class (e.g., parse(), parseQuery()), use QueryException:

♻️ Suggested fix
-        throw new \Exception('Invalid method: Get cursor direction on "'.$this->method.'" Query');
+        throw new QueryException('Invalid method: Get cursor direction on "'.$this->method.'" Query');
-        throw new \Exception('Invalid method: Get order direction on "'.$this->method.'" Query');
+        throw new QueryException('Invalid method: Get order direction on "'.$this->method.'" Query');

1015-1029: join() is redundant with innerJoin().

join() and innerJoin() are identical implementations. Consider either:

  1. Removing join() to avoid confusion, or
  2. Documenting that join() is an alias for innerJoin()

If kept as an alias, add a docblock note:

/**
 * Alias for innerJoin()
 * `@see` innerJoin()
 */

1177-1189: Method naming inconsistency: getCursorQueries returns single Query|null.

getCursorQueries returns ?Query (a single query or null), but the plural "Queries" suffix suggests it returns an array like the other get*Queries methods. Consider renaming to getCursorQuery for clarity:

♻️ Suggested rename
-    public static function getCursorQueries(array $queries): ?Query
+    public static function getCursorQuery(array $queries): ?Query

1325-1333: Minor API inconsistency between isJoin() and isFilter().

isJoin() is an instance method (like isNested()), while isFilter() is static. This creates an inconsistent API:

  • $query->isJoin()
  • Query::isFilter($method)

Consider making isFilter an instance method or adding a static isJoinType(string $method) variant for symmetry. Not blocking, but worth considering for API consistency.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6339269 and bc98c1a.

📒 Files selected for processing (2)
  • src/Database/Query.php
  • tests/e2e/Adapter/Base.php
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
Repo: utopia-php/database PR: 721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • tests/e2e/Adapter/Base.php
  • src/Database/Query.php
📚 Learning: 2025-11-25T10:46:37.666Z
Learnt from: fogelito
Repo: utopia-php/database PR: 763
File: src/Database/Database.php:8671-8751
Timestamp: 2025-11-25T10:46:37.666Z
Learning: In Utopia\Database\Database::processRelationshipQueries, ensure the system select for '$id' is added only when the collection has relationship attributes (i.e., !empty($relationships)), and avoid capturing the unused '$idAdded' from Query::addSelect to satisfy static analysis.

Applied to files:

  • src/Database/Query.php
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Adapter Tests (SharedTables/MongoDB)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (Schemaless/MongoDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (MongoDB)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Unit Test
🔇 Additional comments (4)
tests/e2e/Adapter/Base.php (1)

12-12: LGTM!

The new JoinsTests trait is correctly imported and integrated into the base e2e test class. The import is properly placed in alphabetical order, and the trait usage follows the existing pattern in the class. This cleanly enables the new join-related tests to execute across all adapter implementations.

Also applies to: 27-27

src/Database/Query.php (3)

147-182: Logical types TYPE_AND and TYPE_OR in FILTER_TYPES may cause unexpected behavior.

TYPE_AND and TYPE_OR (lines 178-179) are logical container types, not filter predicates. Including them in FILTER_TYPES means getFilterQueries() will return nested logical queries alongside regular filters, which may not be the expected behavior for callers expecting only leaf filter predicates.

If this is intentional (to support recursive filter traversal), consider documenting this behavior. Otherwise, these should be in LOGICAL_TYPES only.


451-459: LGTM!

Join and relation-equal types are now correctly included in isMethod(), addressing the serialization/parsing concern from previous reviews.


600-632: LGTM!

Serialization now properly includes the new metadata fields and handles join child queries correctly. The conditional inclusion of aliases (only when non-default) keeps payloads minimal.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@utopia-php utopia-php deleted a comment from coderabbitai bot Jan 15, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/Database/Validator/Queries/V2.php`:
- Around line 704-709: The array attribute validation currently throws an
exception for methods not in [Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS,
Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL]; update that allowed-method list
in the block that checks "$array && !in_array($method, ...)" to also include
Query::TYPE_EXISTS and Query::TYPE_NOT_EXISTS so array attributes permit
EXISTS/NOT_EXISTS queries, preserving parity with the Filter.php validator.
- Line 5: Remove the unused import Utopia\CLI\Console from the top of V2.php; it
is only referenced in commented-out debug code (around the block that references
Console on lines ~407-409), so delete the use statement (use Utopia\CLI\Console)
to satisfy the linter and avoid dead code while leaving the commented debug
lines intact if you want to keep them.

In `@tests/unit/Validator/QueryTest.php`:
- Around line 375-387: The comment says this complex multi-key nested structure
is disallowed but the test uses assertTrue; change the expectation to match the
comment by replacing assertTrue(...) with assertFalse(...) for the
validator->isValid([ Query::contains('meta', [...]) ]) assertion so the test
asserts rejection of that structure.
♻️ Duplicate comments (5)
src/Database/Query.php (1)

532-570: Validate new metadata fields in parseQuery() for consistent error handling.

The new fields (alias, aliasRight, attributeRight, as, collection) extracted at lines 532-537 are passed directly to the constructor without type validation. If a serialized query contains a non-string value (e.g., "alias": []), the constructor's strict string parameter types will throw a TypeError instead of a QueryException.

For consistency with the existing validation pattern (lines 539-553), add type checks:

if (!is_string($alias)) {
    throw new QueryException('Invalid query alias. Must be a string, got ' . \gettype($alias));
}
// Similar for aliasRight, attributeRight, as, collection

This keeps parse()/parseQuery() failures predictable and avoids leaking raw type errors from malformed JSON.

src/Database/Validator/Queries/V2.php (1)

198-202: Join-scope alias validation may reject valid non-relation queries.

This check runs for all queries when $scope === 'joins', but getRightAlias() returns 'main' (the default) for non-relation queries like filters or orders. Since 'main' is in $joinsAliasOrder, this should work, but the check feels overly broad.

Consider narrowing to only TYPE_RELATION_EQUAL queries where both aliases are semantically relevant:

-                if ($scope === 'joins') {
-                    if (!in_array($query->getAlias(), $this->joinsAliasOrder) || !in_array($query->getRightAlias(), $this->joinsAliasOrder)) {
+                if ($scope === 'joins' && $query->getMethod() === Query::TYPE_RELATION_EQUAL) {
+                    if (!in_array($query->getAlias(), $this->joinsAliasOrder, true) || !in_array($query->getRightAlias(), $this->joinsAliasOrder, true)) {

This prevents false positives if future changes alter default alias behavior.

src/Database/Validator/Queries.php (1)

1-177: Delete this file or fix the pipeline failure.

The entire class is commented out, which:

  1. Triggers the linter error (blank_line_after_opening_tag - the <?php tag should be followed by a blank line, but instead there's a comment)
  2. Leaves a "zombie" file with no functional code

Since the class has been superseded by the V2 validator, delete this file entirely. If the code must be retained for reference, version control already preserves history.

src/Database/Validator/Query/Filter.php (1)

1-509: Delete this file instead of keeping commented-out code.

The entire Filter validator class is commented out. This triggers the linter error and provides no value—version control preserves history if you need to reference the old implementation.

tests/unit/Validator/QueriesTest.php (1)

1-119: Delete this test file if tests have been migrated to the new validator.

The entire test class is commented out. If test coverage has been migrated to DocumentsQueriesTest.php or similar files using the new V2 validator, delete this file rather than leaving commented-out code. This also resolves the linter error.

🧹 Nitpick comments (4)
src/Database/Query.php (3)

47-77: Join types not added to TYPES array - intentional or oversight?

The new constants TYPE_INNER_JOIN, TYPE_LEFT_JOIN, TYPE_RIGHT_JOIN, and TYPE_RELATION_EQUAL are added to isMethod() (lines 454-457) but are not included in the public TYPES array (lines 79-127).

If joins are intended to be first-class query methods that consumers can enumerate via Query::TYPES, add them:

         self::TYPE_OR,
         self::TYPE_ELEM_MATCH,
-        self::TYPE_REGEX
+        self::TYPE_REGEX,
+        self::TYPE_RELATION_EQUAL,
+        self::TYPE_INNER_JOIN,
+        self::TYPE_LEFT_JOIN,
+        self::TYPE_RIGHT_JOIN,
     ];

If joins are internal-only and should not appear in the public API enumeration, document this decision.


184-184: Minor: Inconsistent self-reference style in FILTER_TYPES.

All other entries use self::TYPE_*, but this line uses Query::TYPE_ELEM_MATCH. For consistency:

-        Query::TYPE_ELEM_MATCH,
+        self::TYPE_ELEM_MATCH,

350-378: Consider using QueryException instead of generic \Exception.

getCursorDirection() and getOrderDirection() throw \Exception when called on an invalid query type. For consistency with the rest of the Query class (which uses QueryException for parsing errors), consider:

-        throw new \Exception('Invalid method: Get cursor direction on "'.$this->method.'" Query');
+        throw new QueryException('Invalid method: Get cursor direction on "'.$this->method.'" Query');

This provides a consistent exception hierarchy for callers to catch.

tests/unit/Validator/QueryTest.php (1)

93-98: Inconsistent attribute definition for meta.

The meta attribute is missing fields (size, required, signed, filters) that all other attributes define. While this may work for VAR_OBJECT, the inconsistency could cause unexpected behavior if validators or other code paths expect these fields.

Consider adding the missing fields for consistency
             [
                 '$id' => 'meta',
                 'key' => 'meta',
                 'type' => Database::VAR_OBJECT,
                 'array' => false,
+                'size' => 0,
+                'required' => false,
+                'signed' => false,
+                'filters' => [],
             ]
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc98c1a and 991c3d1.

📒 Files selected for processing (6)
  • src/Database/Query.php
  • src/Database/Validator/Queries.php
  • src/Database/Validator/Queries/V2.php
  • src/Database/Validator/Query/Filter.php
  • tests/unit/Validator/QueriesTest.php
  • tests/unit/Validator/QueryTest.php
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-07-30T19:17:53.630Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 642
File: src/Database/Validator/PartialStructure.php:43-52
Timestamp: 2025-07-30T19:17:53.630Z
Learning: In PartialStructure validator, when filtering for required attributes validation using the $requiredAttributes parameter, $this->attributes should be used instead of the merged $attributes array because this validation is specifically for internal attributes like $createdAt and $updatedAt that are defined in the base Structure class, not collection-specific attributes.

Applied to files:

  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
Repo: utopia-php/database PR: 721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • src/Database/Validator/Queries/V2.php
  • tests/unit/Validator/QueriesTest.php
  • tests/unit/Validator/QueryTest.php
  • src/Database/Query.php
📚 Learning: 2025-11-25T10:46:37.666Z
Learnt from: fogelito
Repo: utopia-php/database PR: 763
File: src/Database/Database.php:8671-8751
Timestamp: 2025-11-25T10:46:37.666Z
Learning: In Utopia\Database\Database::processRelationshipQueries, ensure the system select for '$id' is added only when the collection has relationship attributes (i.e., !empty($relationships)), and avoid capturing the unused '$idAdded' from Query::addSelect to satisfy static analysis.

Applied to files:

  • src/Database/Validator/Queries/V2.php
  • src/Database/Query.php
📚 Learning: 2025-10-20T05:29:29.487Z
Learnt from: abnegate
Repo: utopia-php/database PR: 731
File: src/Database/Database.php:6987-6988
Timestamp: 2025-10-20T05:29:29.487Z
Learning: In Utopia\Database\Database::convertRelationshipQueries, Many-to-Many filtering does not need the parent collection or a direct junction query: it calls find() on the related collection without skipping relationships, which populates relationship attributes (including the two-way key). Parent IDs are derived from that populated attribute. Therefore, calls should remain convertRelationshipQueries($relationships, $queries).

Applied to files:

  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-10-29T12:27:57.071Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 747
File: src/Database/Adapter/Mongo.php:1449-1453
Timestamp: 2025-10-29T12:27:57.071Z
Learning: In src/Database/Adapter/Mongo.php, when getSupportForAttributes() returns false (schemaless mode), the updateDocument method intentionally uses a raw document without $set operator for replacement-style updates, as confirmed by the repository maintainer ArnabChatterjee20k.

Applied to files:

  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-10-16T09:37:33.531Z
Learnt from: fogelito
Repo: utopia-php/database PR: 733
File: src/Database/Adapter/MariaDB.php:1801-1806
Timestamp: 2025-10-16T09:37:33.531Z
Learning: In the MariaDB adapter (src/Database/Adapter/MariaDB.php), only duplicate `_uid` violations should throw `DuplicateException`. All other unique constraint violations, including `PRIMARY` key collisions on the internal `_id` field, should throw `UniqueException`. This is the intended design to distinguish between user-facing document duplicates and internal/user-defined unique constraint violations.

Applied to files:

  • src/Database/Validator/Queries/V2.php
🧬 Code graph analysis (1)
src/Database/Validator/Queries/V2.php (7)
src/Database/Document.php (3)
  • Document (12-470)
  • getId (63-66)
  • getArrayCopy (423-458)
src/Database/Query.php (9)
  • Query (8-1617)
  • __construct (210-245)
  • getAttribute (267-270)
  • parse (506-519)
  • isNested (1316-1323)
  • getMethod (259-262)
  • getFilterQueries (1198-1201)
  • isSpatialQuery (471-488)
  • isVectorQuery (494-497)
src/Database/QueryContext.php (4)
  • QueryContext (8-136)
  • __construct (25-27)
  • getCollections (32-35)
  • getCollectionByAlias (45-63)
src/Database/Validator/Alias.php (5)
  • Alias (7-70)
  • getDescription (15-18)
  • isArray (54-57)
  • getType (66-69)
  • isValid (26-45)
src/Database/Validator/AsQuery.php (6)
  • AsQuery (7-84)
  • __construct (14-18)
  • getDescription (24-27)
  • isArray (68-71)
  • getType (80-83)
  • isValid (35-59)
src/Database/Validator/Query/Cursor.php (1)
  • Cursor (10-56)
src/Database/Validator/Sequence.php (1)
  • Sequence (9-60)
🪛 GitHub Actions: Linter
src/Database/Validator/Query/Filter.php

[error] 1-1: Pint: PSR-12 issue detected - blank_line_after_opening_tag.

src/Database/Validator/Queries.php

[error] 1-1: Pint: PSR-12 issue detected - blank_line_after_opening_tag in file.

src/Database/Validator/Queries/V2.php

[error] 1-1: Pint: PSR-12 issue detected - statement_indentation.

tests/unit/Validator/QueriesTest.php

[error] 1-1: Pint: PSR-12 issue detected - blank_line_after_opening_tag.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (MongoDB)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (Schemaless/MongoDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SharedTables/MongoDB)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (Postgres)
🔇 Additional comments (12)
src/Database/Query.php (3)

803-806: Well-designed select() factory with alias support.

The updated select() method properly accepts alias, as, and system parameters, enabling both user-facing and internal select queries. The system flag is useful for internal queries that shouldn't be user-modifiable.


1012-1059: Join factory methods are well-structured.

The join(), innerJoin(), leftJoin(), rightJoin(), and relationEqual() factories provide a clean API for constructing join queries with proper aliasing and collection references.


1081-1210: Useful query extraction helpers.

The new static methods (getSelectQueries, getJoinQueries, getLimitQueries, getLimitQuery, getOffsetQueries, getOffsetQuery, getOrderQueries, getCursorQueries, getFilterQueries, getVectorQueries) provide a clean API for extracting specific query types from a query array. These properly clone queries to avoid mutation issues.

src/Database/Validator/Queries/V2.php (4)

96-108: System attributes $id and $sequence typed as VAR_STRING instead of VAR_ID.

The synthetic attributes added in the constructor use Database::VAR_STRING for $id and $sequence:

$attributes[] = new Document([
    '$id' => '$id',
    'key' => '$id',
    'type' => Database::VAR_STRING,  // Should this be VAR_ID?
    'array' => false,
]);

In validateValues(), Database::VAR_ID triggers the Sequence validator (line 585). If $id and $sequence are typed as VAR_STRING, they'll use the Text validator instead.

Verify this is intentional. If $id/$sequence should use Sequence validation:

 $attributes[] = new Document([
     '$id' => '$id',
     'key' => '$id',
-    'type' => Database::VAR_STRING,
+    'type' => Database::VAR_ID,
     'array' => false,
 ]);

437-488: Well-structured attribute existence validation.

The validateAttributeExist() method properly:

  • Allows internal attributes ($permissions, $collection)
  • Validates alias context exists
  • Handles dot-notation for nested/relationship attributes
  • Returns early for schemaless mode (line 469-471)
  • Prevents queries on encrypted attributes
  • Restricts ordering by nested attributes

715-738: Fulltext index validation correctly handles both SEARCH and NOT_SEARCH.

The method now validates both TYPE_SEARCH and TYPE_NOT_SEARCH require a fulltext index on the queried attribute, addressing the previous review concern.


745-762: Clean recursive relation existence check.

The isRelationExist() method correctly traverses nested queries to ensure joins contain at least one TYPE_RELATION_EQUAL query referencing the joined collection's alias.

tests/unit/Validator/QueryTest.php (5)

10-15: LGTM!

The imports and property declaration are properly updated to use the new QueryContext and V2 validator (aliased as DocumentsValidator), maintaining clear naming while adopting the new validation architecture.


101-117: LGTM!

Clean context setup pattern: attributes are properly converted to Document objects, wrapped in a collection, and added to the QueryContext. This provides a solid foundation for context-aware validation testing.


150-153: LGTM!

The Query::select() usage correctly reflects the new per-field selection API, with separate calls for each attribute instead of a single multi-field call.


129-129: LGTM!

Validator instantiation is correctly updated to use DocumentsValidator with the QueryContext parameter. The pattern is consistent across all test methods.


268-273: Method name getCursorQueries (plural) is misleading: it returns a single Query object.

The return type is ?Query, not array<Query>. The implementation filters cursor queries but intentionally returns only the first result via return $queries[0]. While similar methods like getFilterQueries() and getVectorQueries() return array<Query>, this method's plural naming contradicts its singular return type, which is confusing for API users.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/unit/Validator/QueriesTest.php`:
- Around line 3-120: Remove the entirely commented-out test file
Tests\Unit\Validator\QueriesTest.php: the file contains a fully commented class
QueriesTest (methods setUp, tearDown, testEmptyQueries, testInvalidMethod,
testInvalidValue, testValid and references to Queries, Cursor, Filter, Limit,
Offset, Order, Query) and should be deleted from the repository; delete the file
and commit the removal (no code changes required inside other files).
♻️ Duplicate comments (5)
src/Database/Validator/Queries/V2.php (3)

5-5: Remove unused Console import.

The Utopia\CLI\Console import is unused in the active code. The only references are in commented-out debug code (lines 407-409). Remove this import to satisfy linters and reduce dead code.

-use Utopia\CLI\Console;

198-202: Join-scope alias check is overly broad and will reject valid non-relation queries.

This block runs for every query when $scope === 'joins', but non-relation queries (filters, orders) typically have an empty getRightAlias(). This causes in_array('', $this->joinsAliasOrder) to fail, incorrectly rejecting valid filter queries inside joins.

Restrict this check to relation queries only:

-                if ($scope === 'joins') {
-                    if (!in_array($query->getAlias(), $this->joinsAliasOrder) || !in_array($query->getRightAlias(), $this->joinsAliasOrder)) {
+                if ($scope === 'joins' && $query->getMethod() === Query::TYPE_RELATION_EQUAL) {
+                    if (!in_array($query->getAlias(), $this->joinsAliasOrder, true) || !in_array($query->getRightAlias(), $this->joinsAliasOrder, true)) {
                         throw new \Exception('Invalid query: '.\ucfirst($query->getMethod()).' alias reference in join has not been defined.');
                     }
                 }

704-709: Missing TYPE_EXISTS and TYPE_NOT_EXISTS in array attribute allowed methods.

The array attribute validation is missing TYPE_EXISTS and TYPE_NOT_EXISTS from the allowed methods list. The original Filter.php validator included them (line 268). Add them to maintain parity:

         if (
             $array &&
-            !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])
+            !in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_NOT_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL, Query::TYPE_EXISTS, Query::TYPE_NOT_EXISTS])
         ) {
src/Database/Validator/Query/Filter.php (1)

3-510: Remove commented-out code entirely.

The entire Filter validator class (507 lines) remains commented out. If this validator has been superseded by V2, delete the file entirely rather than keeping commented code:

  • Creates confusion about what code is active
  • Makes the codebase harder to maintain
  • Can be recovered from version control if needed
src/Database/Validator/Queries.php (1)

3-178: Breaking change: Queries class is now unavailable.

The Utopia\Database\Validator\Queries class is entirely commented out. Any external code instantiating this class will encounter a fatal error. Either:

  1. Remove the file outright and document this as a major-version breaking change, or
  2. Provide a deprecated stub that throws on construction or proxies to V2

Leaving commented-out code is the worst option for API consumers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48a90b7 and cdc510a.

📒 Files selected for processing (4)
  • src/Database/Validator/Queries.php
  • src/Database/Validator/Queries/V2.php
  • src/Database/Validator/Query/Filter.php
  • tests/unit/Validator/QueriesTest.php
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
Repo: utopia-php/database PR: 721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • tests/unit/Validator/QueriesTest.php
  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-07-30T19:17:53.630Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 642
File: src/Database/Validator/PartialStructure.php:43-52
Timestamp: 2025-07-30T19:17:53.630Z
Learning: In PartialStructure validator, when filtering for required attributes validation using the $requiredAttributes parameter, $this->attributes should be used instead of the merged $attributes array because this validation is specifically for internal attributes like $createdAt and $updatedAt that are defined in the base Structure class, not collection-specific attributes.

Applied to files:

  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-11-25T10:46:37.666Z
Learnt from: fogelito
Repo: utopia-php/database PR: 763
File: src/Database/Database.php:8671-8751
Timestamp: 2025-11-25T10:46:37.666Z
Learning: In Utopia\Database\Database::processRelationshipQueries, ensure the system select for '$id' is added only when the collection has relationship attributes (i.e., !empty($relationships)), and avoid capturing the unused '$idAdded' from Query::addSelect to satisfy static analysis.

Applied to files:

  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-10-20T05:29:29.487Z
Learnt from: abnegate
Repo: utopia-php/database PR: 731
File: src/Database/Database.php:6987-6988
Timestamp: 2025-10-20T05:29:29.487Z
Learning: In Utopia\Database\Database::convertRelationshipQueries, Many-to-Many filtering does not need the parent collection or a direct junction query: it calls find() on the related collection without skipping relationships, which populates relationship attributes (including the two-way key). Parent IDs are derived from that populated attribute. Therefore, calls should remain convertRelationshipQueries($relationships, $queries).

Applied to files:

  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-10-29T12:27:57.071Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 747
File: src/Database/Adapter/Mongo.php:1449-1453
Timestamp: 2025-10-29T12:27:57.071Z
Learning: In src/Database/Adapter/Mongo.php, when getSupportForAttributes() returns false (schemaless mode), the updateDocument method intentionally uses a raw document without $set operator for replacement-style updates, as confirmed by the repository maintainer ArnabChatterjee20k.

Applied to files:

  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-10-16T09:37:33.531Z
Learnt from: fogelito
Repo: utopia-php/database PR: 733
File: src/Database/Adapter/MariaDB.php:1801-1806
Timestamp: 2025-10-16T09:37:33.531Z
Learning: In the MariaDB adapter (src/Database/Adapter/MariaDB.php), only duplicate `_uid` violations should throw `DuplicateException`. All other unique constraint violations, including `PRIMARY` key collisions on the internal `_id` field, should throw `UniqueException`. This is the intended design to distinguish between user-facing document duplicates and internal/user-defined unique constraint violations.

Applied to files:

  • src/Database/Validator/Queries/V2.php
🧬 Code graph analysis (1)
src/Database/Validator/Queries/V2.php (8)
src/Database/Query.php (3)
  • Query (8-1617)
  • parse (506-519)
  • isNested (1316-1323)
src/Database/QueryContext.php (3)
  • QueryContext (8-136)
  • getCollections (32-35)
  • getCollectionByAlias (45-63)
src/Database/Validator/Alias.php (5)
  • Alias (7-70)
  • getDescription (15-18)
  • isArray (54-57)
  • getType (66-69)
  • isValid (26-45)
src/Database/Validator/AsQuery.php (5)
  • AsQuery (7-84)
  • getDescription (24-27)
  • isArray (68-71)
  • getType (80-83)
  • isValid (35-59)
src/Database/Validator/Query/Cursor.php (1)
  • Cursor (10-51)
src/Database/Validator/Query/Limit.php (1)
  • Limit (9-58)
src/Database/Validator/Query/Offset.php (2)
  • Offset (9-54)
  • isValid (25-53)
src/Database/Validator/Sequence.php (1)
  • Sequence (9-60)
🔇 Additional comments (6)
src/Database/Validator/Queries/V2.php (6)

88-128: Schema construction looks correct; system attributes properly added.

The constructor correctly clones collections to avoid mutating the original context, and appends all required system attributes ($id, $sequence, $createdAt, $updatedAt) to each collection's schema. The schema indexing by collection ID and attribute key is appropriate for the lookup patterns used throughout the validator.


166-416: The isValid method structure is well-organized with comprehensive query type coverage.

The switch-case structure provides clear handling for each query type with appropriate validation calls. The nested query recursion via $this->isValid() (instead of the previous self::isValid()) correctly maintains instance state. Error handling with the try-catch block and message propagation is appropriate.


437-488: Attribute existence validation handles schemaless mode correctly.

The early return at line 470 for schemaless mode (when !$this->supportForAttributes) is consistent with the validation pattern in validateValues(). The nested attribute handling via dot notation and the relationship type check are properly implemented.


526-710: Value validation logic is comprehensive with proper type handling.

The validateValues method correctly handles all attribute types including spatial, vector, object, and relationship types. The vector query limit (single vector per request) at lines 639-643 is a sensible constraint. The relationship validation at lines 670-692 properly restricts queries on virtual relationship attributes.


715-738: Fulltext index validation correctly handles both SEARCH and NOT_SEARCH.

The guard at line 717 now properly includes both TYPE_SEARCH and TYPE_NOT_SEARCH, ensuring consistent fulltext index requirements for both query types.


778-806: Object query validation is well-implemented.

The isValidObjectQueryValues method correctly prevents ambiguous mixed-key arrays (both integer and string keys at the same level) while allowing valid object query structures. The recursive validation ensures nested structures are also validated.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Database/Adapter/Mongo.php (1)

1128-1159: Fix projection: getAttributeProjection() must ignore non-SELECT queries (and getDocument shouldn’t pass mixed $queries).
Right now getDocument() passes $queries into getAttributeProjection(), which can include filters/orders; that can unintentionally restrict returned fields.

Proposed fix
--- a/src/Database/Adapter/Mongo.php
+++ b/src/Database/Adapter/Mongo.php
@@
-        $projections = $this->getAttributeProjection($queries);
+        $selects = array_values(array_filter(
+            $queries,
+            static fn (Query $q) => $q->getMethod() === Query::TYPE_SELECT
+        ));
+        $projections = $this->getAttributeProjection($selects);
@@
     protected function getAttributeProjection(array $selects): array
     {
         $projection = [];
@@
         foreach ($selects as $select) {
+            if ($select->getMethod() !== Query::TYPE_SELECT) {
+                continue;
+            }
             if ($select->getAttribute() === '$collection') {
                 continue;
             }

Also applies to: 2704-2737

♻️ Duplicate comments (5)
src/Database/Validator/Queries/V2.php (1)

197-201: Join-scope alias check is overly strict for non-relation queries.

This block runs for all nested queries when $scope === 'joins', but non-relation queries (filters, orders) typically have an empty rightAlias. The in_array($query->getRightAlias(), $this->joinsAliasOrder) check will fail for these valid queries since an empty string is not in the alias order.

Restrict this check to relation queries only:

-                if ($scope === 'joins') {
-                    if (!in_array($query->getAlias(), $this->joinsAliasOrder) || !in_array($query->getRightAlias(), $this->joinsAliasOrder)) {
+                if ($scope === 'joins' && $query->getMethod() === Query::TYPE_RELATION_EQUAL) {
+                    if (!in_array($query->getAlias(), $this->joinsAliasOrder, true) || !in_array($query->getRightAlias(), $this->joinsAliasOrder, true)) {
                         throw new \Exception('Invalid query: '.\ucfirst($query->getMethod()).' alias reference in join has not been defined.');
                     }
                 }
src/Database/Adapter/Mongo.php (1)

1973-1983: Guard against orderRandom before calling getOrderDirection() (still unsafe).
This matches a previously raised concern: Mongo reports getSupportForOrderRandom() === false, and getOrderDirection() can throw for random-order queries.

Proposed defensive guard
--- a/src/Database/Adapter/Mongo.php
+++ b/src/Database/Adapter/Mongo.php
@@
         foreach ($orderQueries as $i => $order) {
+            if ($order->getMethod() === Query::TYPE_ORDER_RANDOM) {
+                throw new DatabaseException('Random ordering is not supported by the Mongo adapter.');
+            }
             $attribute  = $order->getAttribute();
             $originalAttribute = $attribute;
@@
             $direction = $order->getOrderDirection();
src/Database/Adapter/SQL.php (3)

374-379: Quote _uid consistently in getDocument() WHERE clause.
Currently mixes quoted alias with unquoted column.

Proposed fix
--- a/src/Database/Adapter/SQL.php
+++ b/src/Database/Adapter/SQL.php
@@
-            WHERE {$this->quote($alias)}._uid = :_uid 
+            WHERE {$this->quote($alias)}.{$this->quote('_uid')} = :_uid

2357-2395: Add TYPE_SELECT guard to getAttributeProjection() (projection currently treats all queries as selects).
This is especially important since getDocument() passes $queries (mixed types) into this method.

Proposed fix
--- a/src/Database/Adapter/SQL.php
+++ b/src/Database/Adapter/SQL.php
@@
         foreach ($selects as $select) {
+            if ($select->getMethod() !== Query::TYPE_SELECT) {
+                continue;
+            }
             if ($select->getAttribute() === '$collection') {
                 continue;
             }

3117-3124: RIGHT JOIN permission NULL-branch should use consistent quoting.
Currently uses {$alias}._uid IS NULL while other branches quote identifiers.

Proposed fix
--- a/src/Database/Adapter/SQL.php
+++ b/src/Database/Adapter/SQL.php
@@
             $permissionsCondition = $this->getSQLPermissionsCondition($name, $roles, $alias, $forPermission);
             if ($rightJoins) {
-                $permissionsCondition = "($permissionsCondition OR {$alias}._uid IS NULL)";
+                $permissionsCondition = sprintf(
+                    '(%s OR %s.%s IS NULL)',
+                    $permissionsCondition,
+                    $this->quote($alias),
+                    $this->quote('_uid'),
+                );
             }
@@
             $permissionsCondition = $this->getSQLPermissionsCondition($name, $roles, $alias);
             if ($rightJoins) {
-                $permissionsCondition = "($permissionsCondition OR {$alias}._uid IS NULL)";
+                $permissionsCondition = sprintf(
+                    '(%s OR %s.%s IS NULL)',
+                    $permissionsCondition,
+                    $this->quote($alias),
+                    $this->quote('_uid'),
+                );
             }

Also applies to: 3279-3286

🧹 Nitpick comments (5)
src/Database/Validator/Queries/V2.php (1)

665-687: Use Database::VAR_RELATIONSHIP constant instead of string literal.

Line 665 uses the string literal 'relationship' while line 557 correctly uses Database::VAR_RELATIONSHIP. This inconsistency could lead to bugs if the constant value ever changes.

-        if ($attribute['type'] === 'relationship') {
+        if ($attribute['type'] === Database::VAR_RELATIONSHIP) {
tests/unit/Validator/QueryTest.php (1)

93-98: Consider adding required fields to the 'meta' attribute definition.

The 'meta' attribute definition is missing fields like size, required, signed, and filters that other attributes have. While this may work, it's inconsistent with the other attribute definitions in the test setup.

             [
                 '$id' => 'meta',
                 'key' => 'meta',
                 'type' => Database::VAR_OBJECT,
                 'array' => false,
+                'size' => 0,
+                'required' => false,
+                'signed' => false,
+                'filters' => [],
             ]
src/Database/Adapter/Mongo.php (2)

1936-1940: Don’t filter() the collection id used as the QueryContext skipAuth key.
If QueryContext stores skip-auth by raw collection id, filtering here can cause skip-auth to be missed and permissions to be enforced unexpectedly.

Proposed fix
--- a/src/Database/Adapter/Mongo.php
+++ b/src/Database/Adapter/Mongo.php
@@
-        $skipAuth = $context->skipAuth($this->filter($collection->getId()), $forPermission, $this->authorization);
+        $skipAuth = $context->skipAuth($collection->getId(), $forPermission, $this->authorization);
@@
-        $skipAuth = $context->skipAuth($this->filter($collection->getId()), $permission, $this->authorization);
+        $skipAuth = $context->skipAuth($collection->getId(), $permission, $this->authorization);

Also applies to: 2176-2196


2174-2260: count(): avoid swallowing Mongo exceptions (returning 0 hides outages/bugs).
Returning 0 on MongoException can turn “DB failure” into “no results”, which is hard to detect and can break callers.

Proposed fix
--- a/src/Database/Adapter/Mongo.php
+++ b/src/Database/Adapter/Mongo.php
@@
-        } catch (MongoException $e) {
-            return 0;
+        } catch (MongoException $e) {
+            throw $this->processException($e);
         }
src/Database/Adapter/SQL.php (1)

3021-3034: Verify orderRandom handling: guard before calling getOrderDirection().
Right now getOrderDirection() is called unconditionally; if order-random is represented as a distinct query method, this can still throw.

Proposed fix
--- a/src/Database/Adapter/SQL.php
+++ b/src/Database/Adapter/SQL.php
@@
         foreach ($orderQueries as $i => $order) {
             $orderAlias = $order->getAlias();
             $attribute  = $order->getAttribute();
             $originalAttribute = $attribute;
+
+            if ($order->getMethod() === Query::TYPE_ORDER_RANDOM) {
+                $orders[] = $this->getRandomOrder();
+                continue;
+            }
@@
             $direction = $order->getOrderDirection();
-
-            // Handle random ordering specially
-            if ($direction === Database::ORDER_RANDOM) {
-                $orders[] = $this->getRandomOrder();
-                continue;
-            }
#!/bin/bash
# Verify how order-random is represented and whether getOrderDirection can be called safely.
rg -n "function getOrderDirection" -n src/Database/Query.php -A40
rg -n "TYPE_ORDER_RANDOM|ORDER_RANDOM" src/Database/Query.php
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cdc510a and 2c7ee2e.

📒 Files selected for processing (5)
  • src/Database/Adapter/Mongo.php
  • src/Database/Adapter/Pool.php
  • src/Database/Adapter/SQL.php
  • src/Database/Validator/Queries/V2.php
  • tests/unit/Validator/QueryTest.php
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-07-30T19:17:53.630Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 642
File: src/Database/Validator/PartialStructure.php:43-52
Timestamp: 2025-07-30T19:17:53.630Z
Learning: In PartialStructure validator, when filtering for required attributes validation using the $requiredAttributes parameter, $this->attributes should be used instead of the merged $attributes array because this validation is specifically for internal attributes like $createdAt and $updatedAt that are defined in the base Structure class, not collection-specific attributes.

Applied to files:

  • src/Database/Validator/Queries/V2.php
  • tests/unit/Validator/QueryTest.php
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
Repo: utopia-php/database PR: 721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • src/Database/Validator/Queries/V2.php
  • src/Database/Adapter/SQL.php
  • tests/unit/Validator/QueryTest.php
  • src/Database/Adapter/Mongo.php
📚 Learning: 2025-11-25T10:46:37.666Z
Learnt from: fogelito
Repo: utopia-php/database PR: 763
File: src/Database/Database.php:8671-8751
Timestamp: 2025-11-25T10:46:37.666Z
Learning: In Utopia\Database\Database::processRelationshipQueries, ensure the system select for '$id' is added only when the collection has relationship attributes (i.e., !empty($relationships)), and avoid capturing the unused '$idAdded' from Query::addSelect to satisfy static analysis.

Applied to files:

  • src/Database/Validator/Queries/V2.php
  • src/Database/Adapter/SQL.php
📚 Learning: 2025-10-20T05:29:29.487Z
Learnt from: abnegate
Repo: utopia-php/database PR: 731
File: src/Database/Database.php:6987-6988
Timestamp: 2025-10-20T05:29:29.487Z
Learning: In Utopia\Database\Database::convertRelationshipQueries, Many-to-Many filtering does not need the parent collection or a direct junction query: it calls find() on the related collection without skipping relationships, which populates relationship attributes (including the two-way key). Parent IDs are derived from that populated attribute. Therefore, calls should remain convertRelationshipQueries($relationships, $queries).

Applied to files:

  • src/Database/Validator/Queries/V2.php
📚 Learning: 2025-10-29T12:27:57.071Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 747
File: src/Database/Adapter/Mongo.php:1449-1453
Timestamp: 2025-10-29T12:27:57.071Z
Learning: In src/Database/Adapter/Mongo.php, when getSupportForAttributes() returns false (schemaless mode), the updateDocument method intentionally uses a raw document without $set operator for replacement-style updates, as confirmed by the repository maintainer ArnabChatterjee20k.

Applied to files:

  • src/Database/Validator/Queries/V2.php
  • src/Database/Adapter/Mongo.php
📚 Learning: 2025-10-16T09:37:33.531Z
Learnt from: fogelito
Repo: utopia-php/database PR: 733
File: src/Database/Adapter/MariaDB.php:1801-1806
Timestamp: 2025-10-16T09:37:33.531Z
Learning: In the MariaDB adapter (src/Database/Adapter/MariaDB.php), only duplicate `_uid` violations should throw `DuplicateException`. All other unique constraint violations, including `PRIMARY` key collisions on the internal `_id` field, should throw `UniqueException`. This is the intended design to distinguish between user-facing document duplicates and internal/user-defined unique constraint violations.

Applied to files:

  • src/Database/Validator/Queries/V2.php
  • src/Database/Adapter/SQL.php
📚 Learning: 2025-08-14T06:35:30.429Z
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 661
File: tests/e2e/Adapter/Scopes/SpatialTests.php:180-186
Timestamp: 2025-08-14T06:35:30.429Z
Learning: Query::distance method in Utopia\Database\Query expects an array of values parameter, where each value is a [geometry, distance] pair. So the correct format is Query::distance('attribute', [[[lat, lng], distance]]) where the outer array contains the values and each value is [geometry, distance].

Applied to files:

  • src/Database/Validator/Queries/V2.php
🧬 Code graph analysis (4)
src/Database/Adapter/SQL.php (3)
src/Database/QueryContext.php (3)
  • QueryContext (8-136)
  • getMainCollection (40-43)
  • skipAuth (83-94)
src/Database/Query.php (4)
  • Query (8-1617)
  • select (803-806)
  • join (1018-1021)
  • getCollection (309-312)
src/Database/Validator/Authorization.php (1)
  • getRoles (105-108)
tests/unit/Validator/QueryTest.php (2)
src/Database/QueryContext.php (2)
  • QueryContext (8-136)
  • add (68-76)
src/Database/Validator/Queries/V2.php (2)
  • V2 (23-809)
  • getDescription (135-138)
src/Database/Adapter/Mongo.php (2)
src/Database/QueryContext.php (3)
  • QueryContext (8-136)
  • getMainCollection (40-43)
  • skipAuth (83-94)
src/Database/Adapter/SQL.php (3)
  • getInternalKeyForAttribute (2397-2409)
  • getAttributeProjection (2357-2395)
  • find (2994-3224)
src/Database/Adapter/Pool.php (4)
src/Database/Database.php (3)
  • Database (38-9499)
  • find (7793-7945)
  • count (8043-8122)
src/Database/QueryContext.php (1)
  • QueryContext (8-136)
src/Database/Adapter/Mongo.php (3)
  • find (1910-2099)
  • count (2174-2260)
  • getAttributeProjection (2704-2737)
src/Database/Adapter.php (3)
  • find (829-841)
  • count (865-870)
  • getAttributeProjection (1263-1263)
🪛 PHPMD (2.15.0)
src/Database/Adapter/Mongo.php

1919-1919: Avoid unused parameters such as '$joins'. (undefined)

(UnusedFormalParameter)


1920-1920: Avoid unused parameters such as '$vectors'. (undefined)

(UnusedFormalParameter)


2174-2174: Avoid unused parameters such as '$joins'. (undefined)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (Schemaless/MongoDB)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (Mirror)
🔇 Additional comments (13)
src/Database/Validator/Queries/V2.php (3)

56-128: LGTM! Constructor properly builds per-collection schema with system attributes.

The constructor correctly:

  • Clones collections from the context to avoid mutating the original
  • Adds required system attributes ($id, $sequence, $createdAt, $updatedAt)
  • Builds an indexed schema for efficient attribute lookups

780-808: LGTM! Object query validation prevents ambiguous nested structures.

The isValidObjectQueryValues method correctly identifies and rejects arrays that mix integer and string keys (e.g., ['a' => [1, 'b' => [212]]]), which would be ambiguous for object queries.


717-740: LGTM! Fulltext index validation now covers both SEARCH and NOT_SEARCH.

The guard correctly checks for both TYPE_SEARCH and TYPE_NOT_SEARCH, ensuring consistent index requirements for negated search queries.

tests/unit/Validator/QueryTest.php (3)

10-11: LGTM! Test file updated for V2 validator with QueryContext.

The imports and property type correctly reflect the migration to the new context-based validation approach.

Also applies to: 15-15


346-388: LGTM! Object attribute validation tests cover key scenarios.

The new testObjectAttribute method correctly tests:

  1. Valid shape with separate top-level keys (lines 354-362)
  2. Invalid mixed integer/string keys in same array (lines 365-373)
  3. Valid complex nested structure with consistent key types (lines 376-387)

The assertions align with the comments describing expected behavior.


258-274: Test correctly verifies getCursorQueries returns the first cursor type.

The getCursorQueries method filters the input queries using getByType, which preserves the original array order by iterating sequentially. Since cursorBefore appears before cursorAfter in the test's queries array, it is correctly returned as the first element, matching the assertion that expects TYPE_CURSOR_BEFORE.

src/Database/Adapter/Pool.php (2)

241-269: Signatures look aligned with the new Adapter contract; delegation remains correct.
find() / count() now match the context-first shape and correctly forward all args to the pooled adapter.


496-499: getAttributeProjection() delegation is fine.
Pool stays a pure pass-through wrapper with the updated signature.

src/Database/Adapter/Mongo.php (3)

720-737: renameAttribute(): internal-key mapping + transaction options usage looks good.


1825-1828: deleteDocuments(): switching to $sequence-based filter construction is consistent with the new query model.


2044-2051: Normalize getMore batch documents the same way as firstBatch (stdClass handling).
First batch uses convertStdClassToArray(...), later batches don’t—this can yield inconsistent document shapes across pages.

Proposed fix
--- a/src/Database/Adapter/Mongo.php
+++ b/src/Database/Adapter/Mongo.php
@@
-                    $doc = new Document($record);
+                    $doc = new Document($this->convertStdClassToArray($record));
                     if ($removeSequence) {
                         $doc->removeAttribute('$sequence');
                     }

Also applies to: 2067-2074

⛔ Skipped due to learnings
Learnt from: ArnabChatterjee20k
Repo: utopia-php/database PR: 747
File: src/Database/Adapter/Mongo.php:1449-1453
Timestamp: 2025-10-29T12:27:57.071Z
Learning: In src/Database/Adapter/Mongo.php, when getSupportForAttributes() returns false (schemaless mode), the updateDocument method intentionally uses a raw document without $set operator for replacement-style updates, as confirmed by the repository maintainer ArnabChatterjee20k.
Learnt from: abnegate
Repo: utopia-php/database PR: 721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.
src/Database/Adapter/SQL.php (2)

1770-1813: JOIN operator mapping via getSQLOperator(): good direction.
Centralizing join keyword generation makes join assembly cleaner.


2315-2348: getTenantQuery(..., forceIsNull: bool) is a reasonable hook for RIGHT JOIN semantics.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/Database/Database.php (2)

4179-4203: Relationship population can be skipped when only system selects are present.

processRelationshipQueries() adds $id, and you later add $permissions. That makes empty($selects) false even when the caller passed no selects, so relationship population is skipped unless nested selections exist. This effectively disables default relationship resolution for collections with relationships.

Consider tracking explicit user selects before system additions (and use that in the guard), and apply the same logic in find().

🔧 Suggested fix (apply similarly in find())
-        $selects = Query::getSelectQueries($queries);
+        $selects = Query::getSelectQueries($queries);
+        $hasExplicitSelects = !empty($selects);
@@
-        if (!$this->inBatchRelationshipPopulation && $this->resolveRelationships && !empty($relationships) && (empty($selects) || !empty($nestedSelections))) {
+        if (
+            !$this->inBatchRelationshipPopulation
+            && $this->resolveRelationships
+            && !empty($relationships)
+            && (!$hasExplicitSelects || !empty($nestedSelections))
+        ) {

Also applies to: 4266-4271


4865-4889: Select aliases (as) aren’t honored in the post-filtering path.

applySelectFiltersToDocuments() keeps attributes by getAttribute() only. If a select uses as, the alias field is dropped and the original key is retained, which diverges from normal select behavior (especially in relationship batch population).

🔧 Suggested fix
-        $attributesToKeep = [];
+        $attributesToKeep = [];
+        $aliasMap = [];
 
-        foreach ($selectQueries as $selectQuery) {
-            $attributesToKeep[$selectQuery->getAttribute()] = true;
-        }
+        foreach ($selectQueries as $selectQuery) {
+            $attr = $selectQuery->getAttribute();
+            $as = $selectQuery->getAs() ?? $attr;
+            $attributesToKeep[$as] = true;
+            $aliasMap[$attr] = $as;
+        }
@@
-        foreach ($documents as $doc) {
+        foreach ($documents as $doc) {
+            foreach ($aliasMap as $attr => $as) {
+                if ($as !== $attr && $doc->offsetExists($attr)) {
+                    $doc->setAttribute($as, $doc->getAttribute($attr));
+                    $doc->removeAttribute($attr);
+                }
+            }
             $allKeys = \array_keys($doc->getArrayCopy());
             foreach ($allKeys as $attrKey) {
src/Database/Adapter/Mongo.php (1)

2042-2073: Ensure consistent stdClass-to-array conversion across cursor batches.

The first batch uses convertStdClassToArray(), but getMore batches don’t, producing mixed shapes across the result set. Apply the same conversion for all batches.

🐛 Proposed fix
-                    $doc = new Document($record);
+                    $doc = new Document($this->convertStdClassToArray($record));
♻️ Duplicate comments (7)
src/Database/Validator/Queries/V2.php (1)

187-191: Verify getRightAlias() return value for non-relation queries in join scope.

This check runs for all queries within join scope, but non-relation queries (filters, orders) may not have a meaningful rightAlias. If getRightAlias() returns an empty string for such queries, in_array('', $this->joinsAliasOrder) will fail and incorrectly reject valid queries.

The previous review suggested narrowing this check to TYPE_RELATION_EQUAL queries only. If that wasn't applied, consider:

                 if ($scope === 'joins') {
-                    if (!in_array($query->getAlias(), $this->joinsAliasOrder) || !in_array($query->getRightAlias(), $this->joinsAliasOrder)) {
+                    // Only validate alias references for relation queries that actually use rightAlias
+                    if (
+                        $query->getMethod() === Query::TYPE_RELATION_EQUAL &&
+                        (!in_array($query->getAlias(), $this->joinsAliasOrder, true) || !in_array($query->getRightAlias(), $this->joinsAliasOrder, true))
+                    ) {
                         throw new \Exception('Invalid query: '.\ucfirst($query->getMethod()).' alias reference in join has not been defined.');
                     }
                 }
#!/bin/bash
# Check what getRightAlias returns for non-relation queries
ast-grep --pattern $'class Query {
  $$$
  getRightAlias($$$) {
    $$$
  }
  $$$
}'
src/Database/Database.php (1)

8777-8782: Drop unused $idAdded from processRelationshipQueries().

$idAdded is never used; PHPMD already flags it. This can be removed by only capturing the updated $queries from QueryContext::addSelect(...). Based on learnings, this should stay relationship-only and avoid the unused local.

🔧 Suggested fix
-        if (!empty($relationships)) {
-            [$queries, $idAdded] = QueryContext::addSelect($queries, Query::select('$id', system: true));
-        }
+        if (!empty($relationships)) {
+            [$queries] = QueryContext::addSelect($queries, Query::select('$id', system: true));
+        }
src/Database/Adapter/Mongo.php (1)

1973-1980: Still missing guard for TYPE_ORDER_RANDOM before getOrderDirection().

This matches the prior review note; if random order can reach this adapter, it will still throw.

src/Database/Adapter/SQL.php (4)

374-378: Quote _uid consistently in getDocument() WHERE.
The column is currently unquoted while the alias is quoted; keep identifier quoting consistent to avoid edge cases with reserved words or quoting changes.

♻️ Suggested fix
-            WHERE {$this->quote($alias)}._uid = :_uid 
+            WHERE {$this->quote($alias)}.{$this->quote('_uid')} = :_uid 

2370-2405: Guard projection builder against non‑SELECT queries.
getAttributeProjection() is used with $queries in getDocument(). If the array is mixed, non‑SELECT queries can generate invalid projection SQL. Add a method check to skip non‑SELECT entries.

♻️ Suggested fix
         foreach ($selects as $select) {
+            if ($select->getMethod() !== Query::TYPE_SELECT) {
+                continue;
+            }
             if ($select->getAttribute() === '$collection') {
                 continue;
             }

3106-3111: Ensure skipAuth() uses the same key format as QueryContext::addSkipAuth().
Filtering the collection id before skipAuth() may prevent matches if the context stores raw ids.

Also applies to: 3268-3273


3132-3135: Quote alias/column in right‑join NULL permission clause.
The OR {$alias}._uid IS NULL branch mixes unquoted identifiers with quoted ones.

♻️ Suggested fix
-            if ($rightJoins) {
-                $permissionsCondition = "($permissionsCondition OR {$alias}._uid IS NULL)";
-            }
+            if ($rightJoins) {
+                $permissionsCondition = sprintf(
+                    '(%s OR %s.%s IS NULL)',
+                    $permissionsCondition,
+                    $this->quote($alias),
+                    $this->quote('_uid'),
+                );
+            }

Also applies to: 3294-3297

🧹 Nitpick comments (2)
src/Database/Validator/Queries/V2.php (2)

77-79: Minor typo in comment.

The comment has a grammatical error.

         /**
-         * Since $context includes Documents , clone if original data is changes.
+         * Since $context includes Documents, clone if original data is changed.
          */

659-681: Use Database::VAR_RELATIONSHIP constant instead of string literal.

Line 659 uses the string 'relationship' while line 547 and line 604 use Database::VAR_RELATIONSHIP. This inconsistency could cause subtle bugs if the constant value ever changes.

♻️ Suggested fix
-        if ($attribute['type'] === 'relationship') {
+        if ($attribute['type'] === Database::VAR_RELATIONSHIP) {

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.

3 participants