Skip to content

Conversation

@fogelito
Copy link
Contributor

@fogelito fogelito commented Nov 20, 2025

Summary by CodeRabbit

  • New Features

    • Join support (inner/left/right) with relation-equal semantics and per-query context for multi-collection queries and permission gating.
  • Refactor

    • Selection API moved to per-field selects and context-driven projections; cursor, ordering and pagination rebuilt around this flow.
  • Tests

    • Expanded e2e/unit coverage for joins, projections, ordering, permissions and context-aware validation.
  • Breaking Changes

    • Query and validation APIs are now context-driven; many public signatures and selection/validator behaviors changed.
  • Chores

    • Test runner now stops on first failure.

✏️ Tip: You can customize this high-level summary 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) {

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

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)

1264-1280: Copy-paste bugs: fallback values use $limit instead of correct variables.

Lines 1270 and 1279 incorrectly fall back to $limit instead of $offset and $cursor respectively. While the null-coalescing operator makes these fallbacks rarely triggered (values should always exist for valid queries), the code is misleading and could cause subtle bugs if a malformed query slips through.

Proposed fix
                 case Query::TYPE_OFFSET:
                     // Keep the 1st offset encountered and ignore the rest
                     if ($offset !== null) {
                         break;
                     }

-                    $offset = $values[0] ?? $limit;
+                    $offset = $values[0] ?? null;
                     break;
                 case Query::TYPE_CURSOR_AFTER:
                 case Query::TYPE_CURSOR_BEFORE:
                     // Keep the 1st cursor encountered and ignore the rest
                     if ($cursor !== null) {
                         break;
                     }

-                    $cursor = $values[0] ?? $limit;
+                    $cursor = $values[0] ?? null;
                     $cursorDirection = $method === Query::TYPE_CURSOR_AFTER ? Database::CURSOR_AFTER : Database::CURSOR_BEFORE;
                     break;
🤖 Fix all issues with AI agents
In `@tests/unit/Validator/QueryTest.php`:
- Around line 268-275: Replace the unsafe use of reset() and the wrong
assertion: instead of $cursor = reset($queries) (which can be false), assign
$cursor = $queries[0] after asserting the array has two elements; and change the
incorrect assertNotEquals on $queries[1]->getMethod() to
assertEquals($queries[1]->getMethod(), Query::TYPE_CURSOR_AFTER) so the second
query is asserted to be the cursor-after type; keep references to
Query::getCursorQueries, $cursor, getMethod, Query::TYPE_CURSOR_BEFORE and
Query::TYPE_CURSOR_AFTER to locate the lines to change.
🧹 Nitpick comments (1)
src/Database/Query.php (1)

350-378: Use QueryException instead of generic \Exception for consistency.

The getCursorDirection() and getOrderDirection() methods throw \Exception, but the rest of the class uses QueryException for error handling. This inconsistency could make error handling more difficult for consumers.

Suggested fix
     public function getCursorDirection(): string
     {
         if ($this->method === self::TYPE_CURSOR_AFTER) {
             return Database::CURSOR_AFTER;
         }

         if ($this->method === self::TYPE_CURSOR_BEFORE) {
             return Database::CURSOR_BEFORE;
         }

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

     public function getOrderDirection(): string
     {
         if ($this->method === self::TYPE_ORDER_ASC) {
             return Database::ORDER_ASC;
         }

         if ($this->method === self::TYPE_ORDER_DESC) {
             return Database::ORDER_DESC;
         }

         if ($this->method === self::TYPE_ORDER_RANDOM) {
             return Database::ORDER_RANDOM;
         }

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

# Conflicts:
#	src/Database/Query.php
#	tests/unit/Validator/QueryTest.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: 0

Caution

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

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

1069-1080: Breaking change: getByType visibility reduced to protected.

Changing getByType from public to protected is a breaking API change. External consumers and tests that call this method directly will fail. The pipeline failures confirm this affects the test suite.

If this is intentional, consider:

  1. Providing public alternatives (which you've done with getCursorQueries, getSelectQueries, etc.)
  2. Documenting this breaking change in release notes

The test file needs to be updated to use the new public methods instead.

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

258-324: Pipeline failure: Calling protected method Query::getByType().

Lines 266 and 288 call Query::getByType() which is now protected, causing the pipeline to fail. The test is meant to verify the behavior of filtering queries by type, but must use the public API.

Since this test specifically validates the clone vs reference behavior of getByType, you could:

  1. Remove the direct getByType calls and rely on the specialized public methods
  2. Keep one test using getCursorQueries to verify the same behavior
Proposed fix using public getCursorQueries method
     public function testQueryGetByType(): void
     {
         $queries = [
             Query::equal('key', ['value']),
             Query::cursorBefore(new Document([])),
             Query::cursorAfter(new Document([])),
         ];

-        $queries1 = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]);
+        // Test with clone (default behavior)
+        $queries1 = Query::getCursorQueries($queries);

         $this->assertCount(2, $queries1);
         foreach ($queries1 as $query) {
             $this->assertEquals(true, in_array($query->getMethod(), [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE]));
         }

         $cursor = reset($queries1);

         $this->assertInstanceOf(Query::class, $cursor);

         $cursor->setValue(new Document(['$id' => 'hello1']));

         $query1 = $queries[1];

         $this->assertEquals(Query::TYPE_CURSOR_BEFORE, $query1->getMethod());
         $this->assertInstanceOf(Document::class, $query1->getValue());
         $this->assertTrue($query1->getValue()->isEmpty()); // Cursor Document is not updated

         /**
          * Using reference $queries2 => $queries
          */
-        $queries2 = Query::getByType($queries, [Query::TYPE_CURSOR_AFTER, Query::TYPE_CURSOR_BEFORE], false);
+        $queries2 = Query::getCursorQueries($queries, false);

         $cursor = reset($queries2);
         $this->assertInstanceOf(Query::class, $cursor);
🧹 Nitpick comments (2)
src/Database/Query.php (2)

350-378: Direction helpers could throw unclear exceptions.

getCursorDirection() and getOrderDirection() throw generic \Exception with a message, but the rest of the class uses QueryException. Consider using QueryException for consistency, or at minimum add @throws docblocks.

Suggested improvement for exception consistency
+    /**
+     * `@throws` QueryException
+     */
     public function getCursorDirection(): string
     {
         if ($this->method === self::TYPE_CURSOR_AFTER) {
             return Database::CURSOR_AFTER;
         }
 
         if ($this->method === self::TYPE_CURSOR_BEFORE) {
             return Database::CURSOR_BEFORE;
         }
 
-        throw new \Exception('Invalid method: Get cursor direction on "'.$this->method.'" Query');
+        throw new QueryException('Invalid method: Get cursor direction on "'.$this->method.'" Query');
     }

+    /**
+     * `@throws` QueryException
+     */
     public function getOrderDirection(): string
     {
         // ... similar change for the throw statement
     }

532-570: New metadata fields extracted but not type-validated.

The new fields (alias, aliasRight, as, collection, attributeRight) are extracted from the decoded array but not validated to be strings before passing to the constructor. If malformed JSON provides a non-string value (e.g., "alias": []), it will cause a TypeError instead of a QueryException, which is inconsistent with the error handling for method, attribute, and values.

Add type validation for new metadata fields
         $alias = $query['alias'] ?? '';
         $aliasRight = $query['aliasRight'] ?? '';
         $as = $query['as'] ?? '';
         $collection = $query['collection'] ?? '';
 
+        foreach (['alias' => $alias, 'aliasRight' => $aliasRight, 'as' => $as, 'collection' => $collection, 'attributeRight' => $attributeRight] as $key => $value) {
+            if (!\is_string($value)) {
+                throw new QueryException("Invalid query {$key}. Must be a string, got " . \gettype($value));
+            }
+        }
+
         if (!\is_string($method)) {

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 `@src/Database/Validator/Queries/V2.php`:
- Around line 298-300: Update the exception message in the validation branch
that checks count($filters) (in class V2, where the code throws new
\Exception('Invalid query: '.\ucfirst($method).' queries require at least one
queries')); replace the grammatically incorrect phrase "at least one queries"
with "at least one query" so the message reads something like 'Invalid query:
'.\ucfirst($method).' queries require at least one query'.
♻️ Duplicate comments (1)
tests/e2e/Adapter/Scopes/DocumentTests.php (1)

4289-4293: Decode should use the encoded document.

Right now the test decodes the raw $document, so the encode→decode path isn’t actually exercised.

🐛 Proposed fix
-        $result = $database->decode($context, $document);
+        $result = $database->decode($context, $result);
🧹 Nitpick comments (2)
src/Database/Validator/Queries/V2.php (2)

77-79: Minor typo in comment.

The comment has a grammatical error: "if original data is changes" should be "if original data changes".

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

663-685: Use constant instead of string literal for type comparison.

Line 663 uses the string literal 'relationship' while the switch case on line 608 uses Database::VAR_RELATIONSHIP. For consistency and to avoid potential bugs if the constant value ever changes, use the constant:

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

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

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)

2094-2125: Inconsistent convertStdClassToArray usage between batches.

The first batch processing (line 2097) uses convertStdClassToArray($record) to handle stdClass objects in results, but subsequent batch processing (line 2120) creates the Document directly from $record without this conversion. This inconsistency could cause documents to contain unconverted stdClass objects when query results span multiple batches.

Proposed fix
                 foreach ($moreResults as $result) {
                     $record = $this->replaceChars('_', '$', (array)$result);

-                    $doc = new Document($record);
+                    $doc = new Document($this->convertStdClassToArray($record));
                     if ($removeSequence) {
                         $doc->removeAttribute('$sequence');
                     }

                     $found[] = $doc;
                 }
🤖 Fix all issues with AI agents
In `@src/Database/Database.php`:
- Around line 4891-4896: In applySelectFiltersToDocuments(), change the
attributesToKeep construction to preserve aliases and relationship roots: for
each $selectQuery use its getAttribute() but also, if $selectQuery has an alias
(e.g., getAlias() or similar), add that alias as a key to $attributesToKeep, and
if the attribute contains a dot (e.g., "rel.field"), explode on '.' and add the
first segment as a key as well; this ensures selects like "foo AS bar" and
"rel.field" keep "bar" and "rel" so nested relationship data isn't stripped
while still using getAttribute() for normal selects.
♻️ Duplicate comments (6)
tests/e2e/Adapter/Scopes/DocumentTests.php (1)

4427-4431: Decode should use encoded output (or clarify intent).

Right now decode receives the original $document, which bypasses the encode→decode round‑trip. If the goal is to validate the pipeline, pass $result instead (or add a short comment if intentional).

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

2025-2040: Guard against unsupported ORDER_RANDOM queries.

The code calls $order->getOrderDirection() at line 2031 without guarding against TYPE_ORDER_RANDOM. Since getOrderDirection() throws an exception for random-order queries and this adapter reports getSupportForOrderRandom() === false, an orderRandom() query reaching this code would crash with an unclear error message. Add an explicit check similar to the SQL adapter's handling.

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

5728-5732: Type normalization is skipped on update/upsert return paths.

These paths call decode() without casting(), which leaves unnormalized scalar types for adapters without native casting and makes update/upsert inconsistent with create/get/find. Please add casting($context, …) before decode() in each path.

Also applies to: 5958-5963, 6784-6788


8264-8267: Throw QueryException for unknown alias context.

These blocks still throw a generic \Exception, which bypasses query-specific handling. Replace with QueryException for consistent error flow.

Also applies to: 8363-8366


8322-8324: Casting guard is inverted.

The early-return should skip casting when the adapter does support casting, not when it doesn’t.


8809-8811: Remove unused $idAdded from relationship select injection.

$idAdded is never used and triggers PHPMD. You can safely drop it while keeping the relationship-only $id injection.

🛠️ Proposed 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));
+        }

Based on learnings, ...

🧹 Nitpick comments (1)
src/Database/Adapter/Mongo.php (1)

1971-1972: Unused parameters $joins and $vectors are intentional for interface compatibility.

The static analyzer flags these as unused. This is expected since MongoDB doesn't support joins or vector operations, but the parameters are required to match the abstract Adapter interface. Consider adding a PHPDoc annotation to suppress the warning.

      * `@param` array<Query> $vectors
+     *
+     * `@SuppressWarnings`(PHPMD.UnusedFormalParameter) - Interface compliance; Mongo doesn't support joins/vectors
      *
      * `@return` array<Document>

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