Skip to content

Conversation

@fogelito
Copy link
Contributor

@fogelito fogelito commented Sep 4, 2025

Summary by CodeRabbit

  • New Features

    • Join support with aliases and cross-collection relational operators; per-field select/filter/order driven by a query context.
  • Breaking Changes

    • Public APIs now use an explicit query context; internal metadata (ids, sequences, timestamps, permissions) appear only when explicitly selected.
  • Improvements

    • New V2 context-aware query validator and richer multi-table, alias-aware query handling; permissions can be decoded into results when selected.
  • Tests

    • End-to-end and unit tests updated to the new select/context APIs; some legacy validators/tests disabled or replaced.
  • Bug Fixes / Regressions

    • Additional debug output was introduced and may surface in logs.

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: 31

Caution

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

⚠️ Outside diff range comments (15)
tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (1)

800-803: Bug: wrong variable asserted; should reference $account1, not $account2.

After deleting customer1, the null check must be on account1’s newCustomer. This currently tests the unrelated account2 and will fail or mask the intended behavior.

Apply this diff:

-        $account1 = $database->getDocument('account', 'account1');
-        $this->assertEquals(null, $account2->getAttribute('newCustomer', ''));
+        $account1 = $database->getDocument('account', 'account1');
+        $this->assertEquals(null, $account1->getAttribute('newCustomer', ''));
src/Database/Validator/Query/Order.php (1)

3-75: Delete dead validator file
This file only contains commented-out code and no live references to Utopia\Database\Validator\Query\Order; remove src/Database/Validator/Query/Order.php.

tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (2)

1691-1693: Same issue: array-form select should be split.

Apply:

-        $artists = $database->find('artists', [
-            Query::select(['name', 'albums.name', 'albums.tracks.title'])
-        ]);
+        $artists = $database->find('artists', [
+            Query::select('name'),
+            Query::select('albums.name'),
+            Query::select('albums.tracks.title'),
+        ]);

1587-1589: Refactor all array-form Query::select() calls to string-only arguments
PHP 8+ throws a TypeError for Query::select([...]); search for Query::select([ and replace each with individual Query::select('field') calls in tests (SpatialTests, RelationshipTests, ManyToManyTests, DocumentTests) and in README.md.

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

4887-4899: Fix: Query::select() now accepts a single attribute, not an array.

Passing [...] will break with the new API. Mirror the approach used above with array_map.

-                    Query::select([...$selects, '$createdAt']),
+                    Query::select('$createdAt'),
+                    ...array_map(fn ($f) => Query::select($f), $selects),
                     Query::lessThan('$createdAt', '1800-01-01'),
                     Query::orderAsc('$createdAt'),
                     Query::orderAsc(),
                     Query::limit(1),

5152-5155: Replace array-style Query::select calls

  • In tests/e2e/Adapter/Scopes/DocumentTests.php (around line 5143), change
    [Query::select(['*'])] // Mongo bug with Integer UID
    to
    [Query::select('*')] // Mongo bug with Integer UID
  • Likewise, update any Query::select([...]) in SpatialTests.php, RelationshipTests.php, etc., to either multiple per‐field calls (Query::select('name'), Query::select('location'), …) or a single wildcard string.
  • To find all remaining array-style selects, run:
    rg -nP "Query::select\(\s*\[" -g '!vendor/**' -C2
tests/unit/Validator/QueryTest.php (1)

176-176: Remove debug output from validator

There's a var_dump statement in the V2 validator that should be removed.

This debug statement appears to be from the V2.php file based on the relevant code snippets provided. It should be removed from production code.

tests/unit/Validator/Query/FilterTest.php (3)

137-138: Incorrect validator usage for notContains

The validator is being called incorrectly - the isValid() method expects an array of Query objects, but a single Query is being passed.

Apply this diff to fix the validation calls:

-        $this->assertTrue($this->validator->isValid(Query::notContains('string', ['unwanted'])));
-        $this->assertTrue($this->validator->isValid(Query::notContains('string_array', ['spam', 'unwanted'])));
-        $this->assertTrue($this->validator->isValid(Query::notContains('integer_array', [100, 200])));
+        $this->assertTrue($this->validator->isValid([Query::notContains('string', ['unwanted'])]));
+        $this->assertTrue($this->validator->isValid([Query::notContains('string_array', ['spam', 'unwanted'])]));
+        $this->assertTrue($this->validator->isValid([Query::notContains('integer_array', [100, 200])]));

142-143: Inconsistent validator usage pattern

The validator is being called with a single Query instead of an array, which is inconsistent with the expected signature.

Apply this diff:

-        $this->assertFalse($this->validator->isValid(Query::notContains('string', [])));
+        $this->assertFalse($this->validator->isValid([Query::notContains('string', [])]));

149-149: Inconsistent validator method signature usage throughout test methods

Multiple test methods are passing single Query objects instead of arrays to the isValid() method.

Apply this diff to fix all occurrences:

-        $this->assertTrue($this->validator->isValid(Query::notSearch('string', 'unwanted')));
+        $this->assertTrue($this->validator->isValid([Query::notSearch('string', 'unwanted')]));

-        $this->assertFalse($this->validator->isValid(Query::notSearch('string_array', 'unwanted')));
+        $this->assertFalse($this->validator->isValid([Query::notSearch('string_array', 'unwanted')]));

-        $this->assertTrue($this->validator->isValid(Query::notStartsWith('string', 'temp')));
+        $this->assertTrue($this->validator->isValid([Query::notStartsWith('string', 'temp')]));

-        $this->assertFalse($this->validator->isValid(Query::notStartsWith('string_array', 'temp')));
+        $this->assertFalse($this->validator->isValid([Query::notStartsWith('string_array', 'temp')]));

-        $this->assertTrue($this->validator->isValid(Query::notEndsWith('string', '.tmp')));
+        $this->assertTrue($this->validator->isValid([Query::notEndsWith('string', '.tmp')]));

-        $this->assertFalse($this->validator->isValid(Query::notEndsWith('string_array', '.tmp')));
+        $this->assertFalse($this->validator->isValid([Query::notEndsWith('string_array', '.tmp')]));

-        $this->assertTrue($this->validator->isValid(Query::notBetween('integer', 0, 50)));
+        $this->assertTrue($this->validator->isValid([Query::notBetween('integer', 0, 50)]));

-        $this->assertFalse($this->validator->isValid(Query::notBetween('integer_array', 1, 10)));
+        $this->assertFalse($this->validator->isValid([Query::notBetween('integer_array', 1, 10)]));

Also applies to: 163-163, 177-177, 191-191

src/Database/Database.php (5)

3465-3474: Fix caching condition — don’t disable cache for all collections with relationships.

Current check disables cache whenever the collection has any relationship attribute. Only skip cache when this fetch is part of a relationship traversal.

-        // Don't save to cache if it's part of a relationship
-        if (empty($relationships)) {
+        // Don't save to cache if this fetch is part of a relationship traversal
+        if (empty($this->relationshipFetchStack)) {
             try {
                 $this->cache->save($documentKey, $document->getArrayCopy(), $hashKey);
                 $this->cache->save($collectionKey, 'empty', $documentKey);
             } catch (Exception $e) {
                 Console::warning('Failed to save document to cache: ' . $e->getMessage());
             }
         }

6365-6373: Avoid leaking internal $perms in find().

decode()/casting() preserve “$perms”. Remove it from the final payload, like in getDocument().

             $node = $this->casting($context, $node, $selects);
             $node = $this->decode($context, $node, $selects);
+            $node->removeAttribute('$perms');

4617-4621: Batch update requires $permissions and $sequence — auto-inject selects.

Instead of failing later, add the required selects here to make the API resilient.

-            $batch = $this->silent(fn () => $this->find(
+            $requiredSelects = [Query::select('$permissions'), Query::select('$sequence')];
+            $batch = $this->silent(fn () => $this->find(
                 $collection->getId(),
-                array_merge($new, $queries),
+                array_merge($requiredSelects, $new, $queries),
                 forPermission: Database::PERMISSION_UPDATE
             ));

6096-6101: Batch delete requires $permissions and $sequence — auto-inject selects.

Same robustness fix for deletes.

-            $batch = $this->silent(fn () => $this->find(
+            $requiredSelects = [Query::select('$permissions'), Query::select('$sequence')];
+            $batch = $this->silent(fn () => $this->find(
                 $collection->getId(),
-                array_merge($new, $queries),
+                array_merge($requiredSelects, $new, $queries),
                 forPermission: Database::PERMISSION_DELETE
             ));

6723-6743: decodeOriginal(): undefined variable and unused parameter.

$selections is undefined and $selects is unused, producing incorrect decoding and static-analysis noise. Use $selects to build the selected names and gate assignments.

-            foreach ($value as $index => $node) {
+            foreach ($value as $index => $node) {
                 if (is_string($node) && in_array($type, Database::SPATIAL_TYPES)) {
                     $node = $this->decodeSpatialData($node);
                 }
                 foreach (array_reverse($filters) as $filter) {
                     $node = $this->decodeAttribute($filter, $node, $document, $key);
                 }
                 $value[$index] = $node;
             }
-
-            if (
-                empty($selections)
-                || \in_array($key, $selections)
-                || \in_array('*', $selections)
-            ) {
-                $document->setAttribute($key, ($array) ? $value : $value[0]);
-            }
+            $selectedNames = array_map(
+                fn (Query $q) => $q->getAs() !== '' ? $q->getAs() : $q->getAttribute(),
+                $selects
+            );
+            if (empty($selects) || \in_array($key, $selectedNames, true) || \in_array('*', $selectedNames, true)) {
+                $document->setAttribute($key, $array ? $value : $value[0]);
+            }
🧹 Nitpick comments (28)
phpunit.xml (1)

10-10: Avoid committing stop-on-first-failure to the shared config

Flipping stopOnFailure="true" is great for local debugging but hides subsequent failures in CI and slows down triage. Prefer keeping it false in the repo and using the CLI flag ad hoc or a CI-specific config.

Apply this diff if you agree:

-    stopOnFailure="true"
+    stopOnFailure="false"
src/Database/Validator/Query/Base.php (1)

7-16: Consider hardening method types (optional)

To avoid stringly-typed drift, consider centralizing method types via a dedicated final class or native enum (if PHP ≥ 8.1), and referencing it across validators.

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

40-48: Optional: enforce integer-only offsets

Numeric() may accept numeric strings/floats. If offsets must be non-negative integers, validate with FILTER_VALIDATE_INT (and range) or an Integer validator to prevent “10.5” or "007" slipping through.

Example:

-        $validator = new Numeric();
-        if (!$validator->isValid($offset)) {
-            $this->message = 'Invalid offset: ' . $validator->getDescription();
-            return false;
-        }
+        if (filter_var($offset, FILTER_VALIDATE_INT) === false) {
+            $this->message = 'Invalid offset: must be an integer.';
+            return false;
+        }
src/Database/Validator/AsQuery.php (3)

46-47: Tighten identifier rules to avoid SQL identifier pitfalls (optional).

Allowing a leading digit can force quoting in SQL identifiers across adapters. Consider enforcing a leading letter/underscore and validating length in one pass.

-        if (! preg_match('/^[a-zA-Z0-9_]+$/', $value)) {
+        if (! preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $value)) {
             return false;
         }
-
-        if (\mb_strlen($value) > 64) {
-            return false;
-        }
+        if (\mb_strlen($value) > 64) { // keep for clarity or drop since regex already permits any length; up to you
+            return false;
+        }

Also applies to: 50-52


38-44: Non-string path should set a descriptive message.

Right now a non-string returns false but the message remains about length/charset. Set a clearer message for this path.

-        if (! \is_string($value)) {
-            return false;
-        }
+        if (! \is_string($value)) {
+            $this->message = '"as" must be a string.';
+            return false;
+        }

54-57: Confirm spec: disallowing alias on "*"

Blocking AS with * seems intentional. If so, consider harmonizing the default description with this specific message or elevate it earlier in docs/errors for clarity.

src/Database/Query.php (4)

704-708: Unused $function parameter in select

$function isn’t used. Remove it now or persist it as a new property to avoid PHPMD noise.

Minimal removal:

-    public static function select(string $attribute, string $alias = '', string $as = '', string $function = '', bool $system = false): self
+    public static function select(string $attribute, string $alias = '', string $as = '', bool $system = false): self
     {
-        return new self(self::TYPE_SELECT, $attribute, [], alias: $alias, as: $as, system: $system);
+        return new self(self::TYPE_SELECT, $attribute, [], alias: $alias, as: $as, system: $system);
     }

1104-1104: Remove unused $joins local

Declared but never used in groupByType.

-        $joins = [];

301-326: Throw QueryException instead of generic Exception

Aligns with the rest of the query error surface.

-        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');

403-409: Type-hint the parameter

isSpatialQuery should accept a string $method.

-    public static function isSpatialQuery($method): bool
+    public static function isSpatialQuery(string $method): bool
src/Database/Adapter/Postgres.php (1)

1565-1568: Alias handling improved; consider defensive default.

Filtering and quoting alias is correct. Optional: default empty alias to Query::DEFAULT_ALIAS to avoid "" if ever unset.

Apply locally within this block:

-        $alias = $query->getAlias();
+        $alias = $query->getAlias() ?: Query::DEFAULT_ALIAS;
         $alias = $this->filter($alias);
         $alias = $this->quote($alias);
tests/unit/Validator/QueriesTest.php (1)

3-80: Don’t leave an entire test suite commented out—either remove it or turn it into an explicit skipped placeholder.

Commented code silently reduces coverage and confuses contributors. Replace with a tiny skipped test to document the deprecation/migration, or delete the file.

Apply this replacement:

-//
-//namespace Tests\Unit\Validator;
-// ...
-//class QueriesTest extends TestCase
-//{
-//    ...
-//}
+<?php
+namespace Tests\Unit\Validator;
+
+use PHPUnit\Framework\TestCase;
+
+final class QueriesTest extends TestCase
+{
+    public function testLegacyQueriesValidatorDeprecated(): void
+    {
+        $this->markTestSkipped('Legacy Queries validator removed; coverage migrated to V2/QueryContext tests.');
+    }
+}
tests/e2e/Adapter/Base.php (1)

11-11: Avoid importing disabled traits; either enable JoinsTests (recommended for this PR) or drop the import.

Having JoinsTests imported but commented reduces clarity and leaves join coverage out of CI, which is risky for a joins-focused PR.

Apply to enable Joins tests now:

-use Tests\E2E\Adapter\Scopes\JoinsTests;
+use Tests\E2E\Adapter\Scopes\JoinsTests;
@@
-    //use JoinsTests;
+    use JoinsTests;
-    //use CollectionTests;
+    //use CollectionTests;

If joins are still unstable on some adapters, consider guarding via an env flag:

if (getenv('RUN_JOINS_TESTS')) { class_alias(\Tests\E2E\Adapter\Scopes\JoinsTests::class, 'JoinTestsEnabler'); }

Also applies to: 22-23

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

16-18: Drop unused imports.

QueryContext and Authorization aren’t used in this adapter file.

Apply:

 use Utopia\Database\Query;
-use Utopia\Database\QueryContext;
-use Utopia\Database\Validator\Authorization;
tests/unit/Validator/Query/SelectTest.php (1)

22-29: Collection metadata doc id looks off.

Consider setting '$id' to the collection id ('movies') and '$collection' to Database::METADATA for clarity.

-        $collection = new Document([
-            '$id' => Database::METADATA,
+        $collection = new Document([
+            '$id' => 'movies',
             '$collection' => Database::METADATA,
             'name' => 'movies',
             'attributes' => [],
             'indexes' => [],
         ]);
tests/unit/Validator/DocumentQueriesTest.php (1)

24-26: Prefer the constant for $collection over ID::custom() for consistency.

Minor consistency tweak; both work, but other tests use the constant directly.

-            '$collection' => ID::custom(Database::METADATA),
+            '$collection' => Database::METADATA,
tests/unit/Validator/DocumentsQueriesTest.php (1)

154-158: Order with no attribute may be invalid in V2 — verify or specify field.

If V2 doesn’t default to $sequence, these will fail. Prefer explicit fields to reduce ambiguity.

-            Query::orderAsc('title'),
+            Query::orderAsc('title'),
             Query::limit(10),
             Query::offset(10),
-            Query::orderDesc(),
+            Query::orderDesc('$sequence'),
src/Database/QueryContext.php (2)

48-55: Avoid O(n) scans on every alias lookup.

Maintain a map of collectionId ➝ Document to make getCollectionByAlias() O(1).

 class QueryContext
 {
+    /**
+     * @var array<string, Document>
+     */
+    protected array $collectionsById = [];
@@
     public function add(Document $collection, string $alias = Query::DEFAULT_ALIAS): void
     {
         if (! empty($this->aliases[$alias])) {
             throw new QueryException('Ambiguous alias for collection "'.$collection->getId().'".');
         }
 
         $this->collections[] = $collection;
         $this->aliases[$alias] = $collection->getId();
+        $this->collectionsById[$collection->getId()] = $collection;
     }
@@
-        foreach ($this->collections as $collection) {
-            if ($collection->getId() === $collectionId) {
-                return $collection;
-            }
-        }
-
-        return new Document();
+        return $this->collectionsById[$collectionId] ?? new Document();

75-86: Use explicit existence check instead of empty() for boolean flags.

empty() conflates “unset” with false. Use isset() to read the stored boolean.

     public function skipAuth(string $collection, string $permission): bool
     {
         if (!Authorization::$status) { // for Authorization::disable();
             return true;
         }
 
-        if (empty($this->skipAuthCollections[$collection][$permission])) {
-            return false;
-        }
-
-        return true;
+        if (!isset($this->skipAuthCollections[$collection][$permission])) {
+            return false;
+        }
+        return (bool) $this->skipAuthCollections[$collection][$permission];
     }
tests/unit/Validator/Query/OrderTest.php (1)

55-58: Unqualified order should likely specify a field — verify or target $sequence.

If V2 doesn’t default an empty attribute, make these explicit to avoid flaky expectations.

-        $this->assertTrue($this->validator->isValid([Query::orderAsc()]));
+        $this->assertTrue($this->validator->isValid([Query::orderAsc('$sequence')]));
@@
-        $this->assertTrue($this->validator->isValid([Query::orderDesc()]));
+        $this->assertTrue($this->validator->isValid([Query::orderDesc('$sequence')]));
tests/unit/Validator/IndexedQueriesTest.php (1)

23-31: Optional: make the collection document’s $id the logical name.
Using '$id' => Database::METADATA can confuse validators/readers; prefer '$id' => 'movies' and keep '$collection' => Database::METADATA.

-        $collection = new Document([
-            '$id' => Database::METADATA,
+        $collection = new Document([
+            '$id' => 'movies',
             '$collection' => Database::METADATA,
             'name' => 'movies',
             'attributes' => [],
             'indexes' => [],
         ]);
src/Database/Adapter.php (1)

791-802: Docblock matches the new context-based API; flag $queries as legacy.
Since adapters now consume explicit selects/filters/joins/orderQueries, consider marking $queries as deprecated in the doc to steer users away from it.

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

62-66: Silence PHPMD: drop or underscore unused variables.

$sessionNoPermissions and $session3 are not used after creation. Create without assigning or use a leading underscore to indicate intentional discard.

-        $sessionNoPermissions = $db->createDocument('__sessions', new Document([
+        $db->createDocument('__sessions', new Document([
             'user_id' => $user1->getId(),
             '$permissions' => [],
         ]));
...
-        $session3 = $db->createDocument('__sessions', new Document([
+        $db->createDocument('__sessions', new Document([
             'user_id' => $user2->getId(),
             '$permissions' => [
                 Permission::read(Role::any()),
             ],
             'boolean' => true,
             'float' => 5.5,
         ]));

Also applies to: 100-107

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

3532-3536: Minor: prefer consistent $database usage.

This block uses static::getDatabase() while surrounding code uses $database. Consider consistency for readability.

-        $documents = static::getDatabase()->find('movies', [
+        $documents = $database->find('movies', [
             Query::select('name'),
             Query::select('year'),
             Query::select('$createdAt')
         ]);

2929-2938: Optional: add a positive assertion on selected field.

Since only director is selected, assert it exists to tighten the test.

         $documents = $database->find('movies', $queries);
         $this->assertCount(1, $documents);
+        $this->assertArrayHasKey('director', $documents[0]);
         $this->assertArrayNotHasKey('name', $documents[0]);
src/Database/Database.php (3)

3381-3385: Duplicate computation of selects.

$selects is computed twice. Keep the earlier one and drop this block.

-        //$selects = Query::groupByType($queries)['selections'];
-        $selects = Query::getSelectQueries($queries);
+        // $selects already computed above

6300-6315: Prefer one path for grouping queries.

You compute groups via groupByType() and then recompute with Query::get*() calls. Keep one approach to avoid drift.


7249-7251: Cache-key stability.

md5(serialize($selects)) couples to object internals and order. Consider hashing stable tuples like [alias, attribute, as] for each select to avoid accidental cache fragmentation.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e4a03ba and 22f92e3.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • phpunit.xml (2 hunks)
  • src/Database/Adapter.php (2 hunks)
  • src/Database/Adapter/MariaDB.php (4 hunks)
  • src/Database/Adapter/Pool.php (3 hunks)
  • src/Database/Adapter/Postgres.php (4 hunks)
  • src/Database/Adapter/SQL.php (11 hunks)
  • src/Database/Database.php (49 hunks)
  • src/Database/Query.php (20 hunks)
  • src/Database/QueryContext.php (1 hunks)
  • src/Database/Validator/Alias.php (1 hunks)
  • src/Database/Validator/AsQuery.php (1 hunks)
  • src/Database/Validator/IndexedQueries.php (1 hunks)
  • src/Database/Validator/Queries.php (1 hunks)
  • src/Database/Validator/Queries/Document.php (1 hunks)
  • src/Database/Validator/Queries/Documents.php (1 hunks)
  • src/Database/Validator/Queries/V2.php (1 hunks)
  • src/Database/Validator/Query/Base.php (1 hunks)
  • src/Database/Validator/Query/Filter.php (1 hunks)
  • src/Database/Validator/Query/Offset.php (1 hunks)
  • src/Database/Validator/Query/Order.php (1 hunks)
  • src/Database/Validator/Query/Select.php (1 hunks)
  • tests/e2e/Adapter/Base.php (2 hunks)
  • tests/e2e/Adapter/Scopes/AttributeTests.php (3 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (13 hunks)
  • tests/e2e/Adapter/Scopes/JoinsTests.php (1 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (5 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php (5 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (6 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php (6 hunks)
  • tests/unit/QueryTest.php (4 hunks)
  • tests/unit/Validator/DocumentQueriesTest.php (4 hunks)
  • tests/unit/Validator/DocumentsQueriesTest.php (5 hunks)
  • tests/unit/Validator/IndexedQueriesTest.php (6 hunks)
  • tests/unit/Validator/QueriesTest.php (1 hunks)
  • tests/unit/Validator/Query/CursorTest.php (1 hunks)
  • tests/unit/Validator/Query/FilterTest.php (3 hunks)
  • tests/unit/Validator/Query/OrderTest.php (1 hunks)
  • tests/unit/Validator/Query/SelectTest.php (1 hunks)
  • tests/unit/Validator/QueryTest.php (14 hunks)
🧰 Additional context used
🧬 Code graph analysis (28)
tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php (1)
src/Database/Query.php (2)
  • Query (8-1379)
  • select (704-707)
src/Database/Validator/Query/Offset.php (1)
src/Database/Validator/Query/Base.php (1)
  • getDescription (26-29)
tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (2)
src/Database/Query.php (3)
  • Query (8-1379)
  • select (704-707)
  • limit (737-740)
src/Database/Database.php (1)
  • find (6243-6378)
tests/e2e/Adapter/Scopes/AttributeTests.php (2)
src/Database/Database.php (1)
  • getCollection (1413-1429)
src/Database/Query.php (3)
  • getCollection (260-263)
  • Query (8-1379)
  • select (704-707)
tests/e2e/Adapter/Scopes/JoinsTests.php (4)
src/Database/Database.php (2)
  • Database (35-7466)
  • find (6243-6378)
src/Database/Query.php (8)
  • Query (8-1379)
  • join (885-888)
  • relationEqual (923-926)
  • equal (554-557)
  • orderAsc (726-729)
  • orderDesc (715-718)
  • select (704-707)
  • getAttribute (218-221)
src/Database/Helpers/Permission.php (2)
  • Permission (9-264)
  • read (186-195)
src/Database/Helpers/Role.php (3)
  • Role (5-178)
  • any (159-162)
  • user (115-118)
tests/unit/Validator/Query/CursorTest.php (3)
src/Database/Document.php (1)
  • Document (12-470)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Query.php (3)
  • Query (8-1379)
  • cursorAfter (759-762)
  • cursorBefore (770-773)
src/Database/QueryContext.php (2)
src/Database/Database.php (1)
  • Database (35-7466)
src/Database/Validator/Authorization.php (1)
  • Authorization (7-225)
tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (3)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (38-38)
src/Database/Database.php (2)
  • getDatabase (687-690)
  • find (6243-6378)
src/Database/Query.php (2)
  • Query (8-1379)
  • select (704-707)
src/Database/Validator/Alias.php (1)
src/Database/Validator/Queries/V2.php (3)
  • getDescription (127-130)
  • isArray (137-140)
  • getType (147-150)
tests/unit/Validator/Query/SelectTest.php (3)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Validator/Queries/V2.php (3)
  • V2 (23-696)
  • isValid (472-695)
  • getDescription (127-130)
src/Database/Query.php (3)
  • Query (8-1379)
  • select (704-707)
  • limit (737-740)
src/Database/Adapter/MariaDB.php (5)
src/Database/QueryContext.php (1)
  • QueryContext (8-89)
src/Database/Validator/Authorization.php (1)
  • Authorization (7-225)
src/Database/Query.php (5)
  • getAttributeRight (250-253)
  • getAttribute (218-221)
  • getAlias (240-243)
  • Query (8-1379)
  • getRightAlias (245-248)
src/Database/Adapter/SQL.php (1)
  • getInternalKeyForAttribute (1891-1903)
src/Database/Adapter.php (2)
  • filter (1164-1173)
  • quote (468-468)
tests/unit/Validator/Query/OrderTest.php (4)
src/Database/Database.php (1)
  • Database (35-7466)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Validator/Queries/V2.php (3)
  • V2 (23-696)
  • isValid (472-695)
  • getDescription (127-130)
src/Database/Query.php (6)
  • Query (8-1379)
  • orderAsc (726-729)
  • orderDesc (715-718)
  • limit (737-740)
  • offset (748-751)
  • equal (554-557)
src/Database/Adapter/Pool.php (3)
src/Database/QueryContext.php (1)
  • QueryContext (8-89)
src/Database/Adapter/SQL.php (2)
  • find (2330-2532)
  • getAttributeProjection (1842-1889)
src/Database/Adapter.php (2)
  • find (805-817)
  • getAttributeProjection (1155-1155)
tests/unit/Validator/DocumentQueriesTest.php (3)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Validator/Queries/V2.php (2)
  • V2 (23-696)
  • isValid (472-695)
src/Database/Query.php (2)
  • Query (8-1379)
  • select (704-707)
tests/unit/Validator/DocumentsQueriesTest.php (3)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Validator/Queries/V2.php (2)
  • V2 (23-696)
  • getDescription (127-130)
tests/unit/Validator/DocumentQueriesTest.php (1)
  • setUp (21-57)
tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php (3)
src/Database/Query.php (2)
  • Query (8-1379)
  • select (704-707)
src/Database/Database.php (1)
  • getDocument (3335-3480)
src/Database/Adapter.php (1)
  • getDocument (691-691)
src/Database/Validator/Queries/V2.php (12)
src/Database/Database.php (1)
  • Database (35-7466)
src/Database/Document.php (3)
  • Document (12-470)
  • getId (63-66)
  • getArrayCopy (423-458)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Query.php (11)
  • Query (8-1379)
  • getAttribute (218-221)
  • getAlias (240-243)
  • getMethod (210-213)
  • getRightAlias (245-248)
  • getFilterQueries (1081-1084)
  • getValues (226-229)
  • isSpatialQuery (408-425)
  • isFilter (1211-1214)
  • getAs (255-258)
  • parse (434-447)
src/Database/QueryContext.php (3)
  • QueryContext (8-89)
  • getCollections (32-35)
  • getCollectionByAlias (37-55)
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-85)
  • getDescription (25-28)
  • isArray (69-72)
  • getType (81-84)
  • isValid (36-60)
src/Database/Validator/Datetime.php (1)
  • Datetime (7-152)
src/Database/Validator/Query/Cursor.php (1)
  • Cursor (9-51)
src/Database/Validator/Query/Limit.php (1)
  • Limit (9-63)
src/Database/Validator/Query/Offset.php (2)
  • Offset (9-59)
  • isValid (25-53)
src/Database/Validator/Sequence.php (1)
  • Sequence (9-61)
tests/unit/Validator/Query/FilterTest.php (2)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Validator/Queries/V2.php (3)
  • V2 (23-696)
  • isValid (472-695)
  • getDescription (127-130)
src/Database/Adapter/Postgres.php (5)
src/Database/QueryContext.php (1)
  • QueryContext (8-89)
src/Database/Validator/Authorization.php (1)
  • Authorization (7-225)
src/Database/Query.php (6)
  • setAttributeRight (294-299)
  • getAttributeRight (250-253)
  • getAttribute (218-221)
  • getAlias (240-243)
  • Query (8-1379)
  • getRightAlias (245-248)
src/Database/Adapter/SQL.php (1)
  • getInternalKeyForAttribute (1891-1903)
src/Database/Adapter.php (2)
  • filter (1164-1173)
  • quote (468-468)
src/Database/Validator/AsQuery.php (1)
src/Database/Validator/Queries/V2.php (4)
  • getDescription (127-130)
  • isValid (472-695)
  • isArray (137-140)
  • getType (147-150)
src/Database/Adapter.php (4)
src/Database/Adapter/SQL.php (2)
  • find (2330-2532)
  • getAttributeProjection (1842-1889)
src/Database/Database.php (2)
  • find (6243-6378)
  • Database (35-7466)
src/Database/Adapter/Pool.php (2)
  • find (264-279)
  • getAttributeProjection (486-489)
src/Database/QueryContext.php (1)
  • QueryContext (8-89)
src/Database/Adapter/SQL.php (4)
src/Database/Database.php (3)
  • Database (35-7466)
  • find (6243-6378)
  • getCollection (1413-1429)
src/Database/QueryContext.php (3)
  • QueryContext (8-89)
  • getCollections (32-35)
  • skipAuth (75-86)
src/Database/Adapter.php (3)
  • getAttributeProjection (1155-1155)
  • filter (1164-1173)
  • find (805-817)
src/Database/Query.php (10)
  • Query (8-1379)
  • getFilterQueries (1081-1084)
  • select (704-707)
  • getAttribute (218-221)
  • getAlias (240-243)
  • getAs (255-258)
  • limit (737-740)
  • offset (748-751)
  • join (885-888)
  • getCollection (260-263)
tests/unit/Validator/QueryTest.php (3)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Validator/Queries/V2.php (2)
  • V2 (23-696)
  • isValid (472-695)
src/Database/Query.php (4)
  • Query (8-1379)
  • select (704-707)
  • getCursorQueries (1050-1062)
  • getMethod (210-213)
src/Database/Database.php (6)
src/Database/Validator/Queries/V2.php (2)
  • V2 (23-696)
  • isValid (472-695)
src/Database/Query.php (20)
  • getAttribute (218-221)
  • Query (8-1379)
  • getSelectQueries (952-957)
  • select (704-707)
  • getJoinQueries (963-970)
  • join (885-888)
  • getCollection (260-263)
  • getAlias (240-243)
  • getFilterQueries (1081-1084)
  • limit (737-740)
  • getLimitQuery (992-1001)
  • offset (748-751)
  • getOffsetQuery (1023-1032)
  • getOrderQueries (1038-1044)
  • orderAsc (726-729)
  • getAs (255-258)
  • isNested (1188-1195)
  • isJoin (1200-1209)
  • getValues (226-229)
  • setValues (332-337)
src/Database/Document.php (9)
  • getAttribute (224-231)
  • Document (12-470)
  • removeAttribute (287-293)
  • getId (63-66)
  • getRead (101-104)
  • getCollection (85-88)
  • getPermissionsByType (145-157)
  • find (304-322)
  • getArrayCopy (423-458)
src/Database/Adapter/SQL.php (6)
  • count (2544-2606)
  • getIdAttributeType (1733-1736)
  • getDocument (353-421)
  • find (2330-2532)
  • sum (2619-2682)
  • getSupportForCasting (1436-1439)
src/Database/Adapter.php (9)
  • count (840-840)
  • getIdAttributeType (905-905)
  • getMinDateTime (898-898)
  • getMaxDateTime (912-915)
  • getDocument (691-691)
  • filter (1164-1173)
  • find (805-817)
  • sum (829-829)
  • getSupportForCasting (986-986)
src/Database/QueryContext.php (6)
  • QueryContext (8-89)
  • add (60-68)
  • getCollections (32-35)
  • skipAuth (75-86)
  • addSkipAuth (70-73)
  • getCollectionByAlias (37-55)
tests/unit/Validator/IndexedQueriesTest.php (3)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Validator/Queries/V2.php (2)
  • V2 (23-696)
  • isValid (472-695)
src/Database/Document.php (2)
  • Document (12-470)
  • setAttribute (244-261)
src/Database/Query.php (2)
src/Database/Database.php (3)
  • __construct (398-475)
  • getCollection (1413-1429)
  • Database (35-7466)
src/Database/QueryContext.php (1)
  • __construct (25-27)
tests/e2e/Adapter/Scopes/DocumentTests.php (4)
src/Database/Database.php (4)
  • Database (35-7466)
  • getDocument (3335-3480)
  • find (6243-6378)
  • decode (6757-6845)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Query.php (2)
  • Query (8-1379)
  • select (704-707)
src/Database/Adapter.php (2)
  • getDocument (691-691)
  • find (805-817)
tests/unit/QueryTest.php (1)
src/Database/Query.php (17)
  • Query (8-1379)
  • equal (554-557)
  • getMethod (210-213)
  • getAttribute (218-221)
  • getValues (226-229)
  • getAlias (240-243)
  • orderDesc (715-718)
  • limit (737-740)
  • parse (434-447)
  • select (704-707)
  • toString (538-545)
  • getAs (255-258)
  • join (885-888)
  • relationEqual (923-926)
  • getCollection (260-263)
  • getRightAlias (245-248)
  • getAttributeRight (250-253)
🪛 PHPMD (2.15.0)
tests/e2e/Adapter/Scopes/JoinsTests.php

62-62: Avoid unused local variables such as '$sessionNoPermissions'. (Unused Code Rules)

(UnusedLocalVariable)


100-100: Avoid unused local variables such as '$session3'. (Unused Code Rules)

(UnusedLocalVariable)

src/Database/Adapter/SQL.php

1842-1842: Avoid unused parameters such as '$spatialAttributes'. (Unused Code Rules)

(UnusedFormalParameter)

src/Database/Database.php

6673-6673: Avoid unused parameters such as '$selects'. (Unused Code Rules)

(UnusedFormalParameter)

src/Database/Query.php

704-704: Avoid unused parameters such as '$function'. (Unused Code Rules)

(UnusedFormalParameter)


1104-1104: Avoid unused local variables such as '$joins'. (Unused Code Rules)

(UnusedLocalVariable)

🔇 Additional comments (58)
src/Database/Validator/Query/Base.php (1)

15-15: LGTM: add join method type constant

The constant aligns validators with the new join flow. Ensure all join validators’ getMethodType() return this value and that any switch/dispatch in V2 includes it.

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

42-42: LGTM: corrected error message

Message now correctly references “offset”, matching the validator’s purpose and the later range check.

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

3-116: Remove IndexedQueries file and document BC break
No active references remain (only commented-out mentions), so delete src/Database/Validator/IndexedQueries.php and add a CHANGELOG entry noting its removal as a breaking change.

tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (8)

119-122: Switch to string-based select API: looks good.

Query::select('name') with separate Query::limit(1) aligns with the new API and expected projection behavior (no relationships).


151-153: Composed selections are correct.

Using Query::select('*') plus Query::select('albums.name') to project nested attributes while excluding others (e.g., price) matches intent.


163-165: Duplicate selection pattern.

Same pattern as above; no additional feedback.


589-591: Nested projection usage is correct.

Query::select('*') + Query::select('accounts.name') is consistent with the new selection semantics.


600-603: Duplicate selection pattern.

Same as prior block; no additional feedback.


925-927: Root-only projection with name: OK.

Ensures relationships aren’t included unintentionally.


932-934: Using * to select root attributes only: OK.

Expectation that relationships are excluded is clear and tested.


939-942: Multi-level wildcard projections: OK.

'*', 'cities.*', and 'cities.mayor.*' together validate deep selection behavior.

src/Database/Validator/Alias.php (2)

32-34: Confirm empty alias is intended.

Returning true for empty string implies “no alias” is valid. Verify all call sites treat '' as absence of alias and won’t serialize it as an actual alias token.


7-70: Overall validator implementation is solid.

Good regex constraint and type reporting; minimal surface for injection.

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

15-16: composer.json already enforces PHP >= 8.1—readonly properties are supported, no changes needed.

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

2-289: Safe to remove Filter: no active references
Only commented-out imports/instantiations of Utopia\Database\Validator\Query\Filter remain (in tests/unit/Validator/QueriesTest.php, src/Database/Validator/Queries/Documents.php, src/Database/Validator/Queries/V2.php). Removing this class causes no BC breaks.

Likely an incorrect or invalid review comment.

tests/unit/QueryTest.php (2)

22-37: LGTM: alias-aware builders covered

Good additions validating alias handling for equal, orderDesc, and limit.

Also applies to: 29-35


424-459: Joins test shape looks good

Assertions validate join method, collection, aliases, and nested queries correctly (left defaults to DEFAULT_ALIAS; right preserves 'u').

Ensure adapters (MariaDB/Postgres) consume TYPE_RELATION_EQUAL consistently with these alias semantics.

src/Database/Query.php (1)

176-186: Good defaulting of aliases

Falling back to DEFAULT_ALIAS for empty left/right aliases prevents surprising empty-context bugs.

tests/e2e/Adapter/Scopes/AttributeTests.php (3)

66-70: Good addition: assert initial collection metadata.

These checks help catch regressions in defaults after createCollection.


246-247: API-aligned select usage.

Switching to string-based Query::select matches the new signature.


287-288: API-aligned select usage.

Using Query::select('*') as a scalar is correct with the new API.

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

18-20: Imports look fine.

Adding QueryContext and Authorization is OK even if future-facing.


1561-1561: Right-hand attribute internal-key mapping: LGTM.

Normalizing the right attribute with getInternalKeyForAttribute keeps parity with the left side.

tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php (5)

148-150: Use multiple string-based selects.

This aligns with the updated Query::select API and keeps projections explicit.


179-182: Projection update: OK.

Explicit selects for '*' and movie.name are correct.


191-194: Projection update in getDocument: OK.

Matches new API.


562-564: Projection update in findOne: OK.

Matches new API.


573-576: Projection update in getDocument: OK.

Matches new API.

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

1459-1467: Good: consistent internal-key mapping and safe alias quoting.

Mapping both sides via getInternalKeyForAttribute and quoting + filtering the left alias reduces SQL injection surface and aligns with internal column names.


1456-1456: Ensure getAttributeRight() never returns null
Non-relation queries may leave attributeRight unset, causing getAttributeRight() to return null and pass null into getInternalKeyForAttribute(). Confirm that getAttributeRight() always returns a string (e.g. '') or add a fallback before calling getInternalKeyForAttribute().

tests/unit/Validator/Query/CursorTest.php (2)

20-21: Good switch to the new cursor API.

Using Query::cursorAfter/Before with a Document aligns with the updated builder methods.


24-36: Negative cases remain valid.

The validator rejecting non-cursor queries is appropriate here.

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

3-44: Remove src/Database/Validator/Queries/Document.php — this file only contains commented-out code and the class it would define is never referenced; it can be deleted.

tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (3)

111-114: Consistent database accessor and string-based select.

static::getDatabase() and Query::select('name') look good.


142-144: Projection style LGTM.

Splitting selects into multiple string-based Query::select calls matches the new API.

Also applies to: 153-156


528-531: Projection style LGTM.

Same here—string-based selects per attribute are correct.

Also applies to: 540-543

tests/unit/Validator/Query/SelectTest.php (2)

54-57: Happy-path coverage LGTM.

Covers wildcard, scalar, relationship dot-path, and limit.


62-63: Failure-path LGTM.

Invalid dot order ('name.artist') should be rejected.

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

76-77: Selecting price.relation on a non-relation attribute — confirm intent.

If dot-path is intended only for relations, switch to a relation field or just 'price'. Otherwise this masks invalid usage.

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

9-9: Import added matches the new API surface.
The QueryContext import is correct and consistent with the updated abstract signature.


486-489: Delegation signature is safe—no subclass requires a non-optional parameter. SQL adapter’s extra $spatialAttributes parameter has a default value, so the proxy’s single-argument call won’t break it.


264-276: Deprecate $queries and re-verify call sites
Mark the $queries parameter as deprecated in the find() phpdoc and confirm no callers pass a non-default value. The previous ripgrep failed because the pattern begins with -; please re-run with:

rg -nP -- '->find\(\s*\$?context[^,]*,\s*\$?queries' -C2 src/

—or simply list all invocations—

rg -n '->find\(' -C2 src/

to manually ensure no non-empty $queries usage.

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

78-143: LGTM: schema and index setup cover key paths.
Creating KEY and FULLTEXT indexes and asserting cursor/order/limit/offset/search cases matches the V2 validator behavior.

src/Database/Adapter.php (2)

1152-1156: Projection API signatures unified and compliant
All adapters match the new abstract signature—Pool uses only array $selects, and SQL’s extra array $spatialAttributes = [] is optional, preserving LSP. No changes needed.


805-817: Ensure find() overrides match the new abstract signature.
Confirmed both Pool.php and SQL.php declare public function find( with the full parameter list—including ?int $limit = 25, ?int $offset = null, array $cursor = [], string $cursorDirection = Database::CURSOR_AFTER, string $forPermission = Database::PERMISSION_READ, and the remaining array… parameters—and return array. No further updates needed.

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

318-329: Confirm aliasing of main.*.

You're selecting '*' with alias 'main'. Ensure the adapter defines/uses 'main' as the base alias; otherwise, prefer default alias ('') for the root selection to avoid “Unknown Alias context” during decode.

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

1308-1310: Migration to per-field selects looks good.


1321-1327: Assertion updates align with selective getDocument behavior.


3453-3569: Select semantics for find(): assertions look inconsistent.

  • With only name and year selected (Lines 3453–3469), the test expects all internal fields present.
  • With name, year, and $updatedAt selected (Lines 3552–3570), the test expects most internals absent.

Please verify intended behavior for find() when any select is provided: do we always include all internals, or only selected internals? Align both blocks accordingly.

Would you like a patch once you confirm the intended contract?


4678-4685: Nice: per-field select expansion for bulk delete queries.


3940-3944: Decode via QueryContext: LGTM.

The context-based decode matches the new API.

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

3-75: Add backward‐compatibility shim or alias for external consumers

  • Internal ripgrep search found no references to Utopia\Database\Validator\Queries\Documents in the repo.
  • To avoid fatal errors for any external users still relying on the legacy class, provide either:
    • a small proxy that throws a clear deprecation error with upgrade guidance (Option A), or
    • a class_alias to your new V2 validator if it’s fully compatible (Option B).

Example proxy implementation (Option A):

-//
-//class Documents extends IndexedQueries
-//{
-//    ...
-//}
+namespace Utopia\Database\Validator\Queries;
+
+final class Documents
+{
+    public function __construct()
+    {
+        throw new \Error('Validator\Queries\Documents has been removed. Use the context-based validator (V2) instead.');
+    }
+}
tests/unit/Validator/Query/FilterTest.php (1)

155-157: Valid usage of Query constructor with array for isValid

These lines correctly wrap the Query objects in arrays when calling isValid(). This is the correct pattern that should be used consistently throughout the test file.

Also applies to: 170-171, 184-185, 198-202

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

2369-2370: Cursor direction logic reversal

The cursor direction reversal logic for ORDER BY looks correct - when using CURSOR_BEFORE, the sort direction is inverted to fetch previous results.

The implementation correctly handles bidirectional cursor pagination.


2421-2437: Well-implemented JOIN support with permission checks

The new JOIN implementation properly:

  1. Applies permission checks per joined table when authorization is enabled
  2. Handles tenant queries for multi-tenant scenarios
  3. Uses proper ON conditions from the join query values

This is a solid implementation of the context-driven join functionality.


1768-1769: Pre-filtering queries before building conditions

Good practice to filter queries using Query::getFilterQueries() before building SQL conditions, ensuring only filter-type queries are processed.


365-369: The script to inspect getDocument in SQL.php has been dispatched. Once it runs, we can determine if consumers correctly handle the new $perms field.

src/Database/Database.php (1)

3355-3360: getDocument(): rejecting non-select queries — verify callers.

Enforcing “only select queries” is sensible, but it’s breaking if any caller still passes filters/limits. Please confirm tests and public docs reflect this stricter contract.


$sql = $this->trigger(Database::EVENT_DATABASE_DELETE, $sql);

var_dump($sql);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove debug var_dump in production path.

Leaking SQL to stdout/stderr from delete() is a correctness and security issue; it also pollutes test output.

Apply:

-        var_dump($sql);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var_dump($sql);
🤖 Prompt for AI Agents
In src/Database/Adapter/MariaDB.php around line 61 there is a leftover
var_dump($sql) used for debugging; remove that var_dump call from the delete()
path and, if visibility is required for diagnostics, replace it with a
conditional debug log via the project's logger at debug level (or nothing) so no
SQL is printed to stdout/stderr in production; run relevant tests to ensure no
test output is polluted.

Comment on lines +1510 to +1515
case Query::TYPE_RELATION_EQUAL:
$attributeRight = $this->quote($this->filter($query->getAttributeRight()));
$aliasRight = $this->quote($query->getRightAlias());

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

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Sanitize right-hand alias in relation-equality SQL.

aliasRight is quoted but not filtered; filter it like the left alias to prevent malformed aliases or injection.

Apply:

-            case Query::TYPE_RELATION_EQUAL:
-                $attributeRight = $this->quote($this->filter($query->getAttributeRight()));
-                $aliasRight = $this->quote($query->getRightAlias());
-                return "{$alias}.{$attribute}={$aliasRight}.{$attributeRight}";
+            case Query::TYPE_RELATION_EQUAL:
+                $attributeRight = $this->quote($this->filter($query->getAttributeRight()));
+                $aliasRight = $this->quote($this->filter($query->getRightAlias()));
+                return "{$alias}.{$attribute}={$aliasRight}.{$attributeRight}";
🤖 Prompt for AI Agents
In src/Database/Adapter/MariaDB.php around lines 1510 to 1515 the right-hand
relation alias ($aliasRight) is quoted but not passed through the filter method
like the left alias, leaving a risk of malformed aliases or injection; update
the code to first filter the right alias (e.g.,
$this->filter($query->getRightAlias())) and then quote it, mirroring the
left-alias handling so the returned SQL uses the filtered-and-quoted right
alias.

Comment on lines +1607 to +1612
case Query::TYPE_RELATION_EQUAL:
$attributeRight = $this->quote($this->filter($query->getAttributeRight()));
$aliasRight = $this->quote($query->getRightAlias());

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

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Sanitize right alias to prevent malformed identifiers.

getRightAlias() is quoted but not filtered; filter() should precede quote() as with the left alias.

Apply this 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}";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case Query::TYPE_RELATION_EQUAL:
$attributeRight = $this->quote($this->filter($query->getAttributeRight()));
$aliasRight = $this->quote($query->getRightAlias());
return "{$alias}.{$attribute}={$aliasRight}.{$attributeRight}";
case Query::TYPE_RELATION_EQUAL:
$attributeRight = $this->quote($this->filter($query->getAttributeRight()));
$aliasRight = $this->quote($this->filter($query->getRightAlias()));
return "{$alias}.{$attribute}={$aliasRight}.{$attributeRight}";
🤖 Prompt for AI Agents
In src/Database/Adapter/Postgres.php around lines 1607-1612, the right alias is
quoted but not filtered which can allow malformed identifiers; change the
assignment to filter the right alias before quoting it (i.e. call
$this->filter($query->getRightAlias()) and then $this->quote(...) around that)
so it mirrors the left-alias handling and prevents unsafe identifiers.


class Alias extends Validator
{
protected string $message = 'Alias must contain at most 64 chars. Valid chars are a-z, A-Z, 0-9, and underscore.';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Length limit mismatch: message says 64 chars, code enforces max 63.

Current check mb_strlen($value) >= 64 rejects 64-char aliases. Align code with message.

Apply this diff:

-    protected string $message = 'Alias must contain at most 64 chars. Valid chars are a-z, A-Z, 0-9, and underscore.';
+    protected string $message = 'Alias must contain at most 64 chars. Valid chars are a-z, A-Z, 0-9, and underscore.';
@@
-        if (\mb_strlen($value) >= 64) {
+        if (\mb_strlen($value) > 64) {
             return false;
         }

Also applies to: 40-41

🤖 Prompt for AI Agents
In src/Database/Validator/Alias.php around line 9 (and also at lines 40-41), the
validation currently rejects 64-character aliases even though the error message
states "at most 64 chars"; change the length check to allow 64 characters by
replacing the condition that uses mb_strlen($value) >= 64 with a check that
fails only when mb_strlen($value) > 64 (or equivalently >= 65), and apply the
same fix to the check at lines 40-41 so the code behavior matches the message.

Comment on lines +168 to 171
$validator = new DocumentsValidator(
$this->context,
Database::VAR_INTEGER
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same here: pass Database::VAR_STRING as $id type.

-        $validator = new DocumentsValidator(
-            $this->context,
-            Database::VAR_INTEGER
-        );
+        $validator = new DocumentsValidator(
+            $this->context,
+            Database::VAR_STRING
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$validator = new DocumentsValidator(
$this->context,
Database::VAR_INTEGER
);
$validator = new DocumentsValidator(
$this->context,
Database::VAR_STRING
);
🤖 Prompt for AI Agents
In tests/unit/Validator/DocumentsQueriesTest.php around lines 168 to 171, the
DocumentsValidator is being constructed with Database::VAR_INTEGER for the $id
type but the test expects string IDs; change the second argument to
Database::VAR_STRING so the validator treats IDs as strings; update the
instantiation to pass Database::VAR_STRING and run the tests to confirm
behavior.

Comment on lines +40 to 44
$context = new QueryContext();
$context->add($this->collection);

$validator = new DocumentsValidator($context);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix: pass required id-attribute type to DocumentsValidator constructor.
V2::__construct(QueryContext $context, string $idAttributeType, ...) requires the second argument; these calls would fatally error. Use a consistent default (string) in tests that don’t care about PK type.

Apply:

-        $validator = new DocumentsValidator($context);
+        $validator = new DocumentsValidator($context, Database::VAR_STRING);
@@
-        $validator = new DocumentsValidator($context);
+        $validator = new DocumentsValidator($context, Database::VAR_STRING);
@@
-        $validator = new DocumentsValidator($context);
+        $validator = new DocumentsValidator($context, Database::VAR_STRING);
@@
-        $validator = new DocumentsValidator($context);
+        $validator = new DocumentsValidator($context, Database::VAR_STRING);
@@
-        $validator = new DocumentsValidator($context);
+        $validator = new DocumentsValidator($context, Database::VAR_STRING);
@@
-        $validator = new DocumentsValidator($context);
+        $validator = new DocumentsValidator($context, Database::VAR_STRING);

Also applies to: 50-54, 60-64, 70-74, 164-169, 213-218

🤖 Prompt for AI Agents
In tests/unit/Validator/IndexedQueriesTest.php around lines 40-44 (and also at
50-54, 60-64, 70-74, 164-169, 213-218), the DocumentsValidator is being
constructed without the required second parameter id-attribute type
(V2::__construct requires string $idAttributeType). Update each
DocumentsValidator instantiation to pass a consistent default id attribute type
(e.g., 'string') as the second argument so the constructor receives the required
parameter.


$context->add($collection);

$this->validator = new DocumentsValidator($context);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix constructor call: missing required $idAttributeType.

-        $this->validator = new DocumentsValidator($context);
+        $this->validator = new DocumentsValidator($context, Database::VAR_STRING);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->validator = new DocumentsValidator($context);
$this->validator = new DocumentsValidator($context, Database::VAR_STRING);
🤖 Prompt for AI Agents
In tests/unit/Validator/Query/OrderTest.php at line 49, the DocumentsValidator
constructor is called with only $context but the required second parameter
$idAttributeType is missing; update the call to pass the required
$idAttributeType (for example the test-suite's expected constant or string such
as the AttributeType/ID constant used elsewhere in tests) — e.g. new
DocumentsValidator($context, $idAttributeType) or inline the appropriate
constant so the validator is constructed with both $context and the correct id
attribute type.

Comment on lines +45 to +49
$context = new QueryContext();

$context->add($collection);

$this->validator = new DocumentsValidator($context);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Validator construction missing required id-attribute type.

V2::__construct requires $idAttributeType (e.g., Database::VAR_STRING). Without it, this will fatal.

-        $this->validator = new DocumentsValidator($context);
+        $this->validator = new DocumentsValidator($context, Database::VAR_STRING);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$context = new QueryContext();
$context->add($collection);
$this->validator = new DocumentsValidator($context);
$context = new QueryContext();
$context->add($collection);
$this->validator = new DocumentsValidator($context, Database::VAR_STRING);
🤖 Prompt for AI Agents
In tests/unit/Validator/Query/SelectTest.php around lines 45 to 49, the
DocumentsValidator is being constructed without the required $idAttributeType
parameter (V2::__construct requires it), causing a fatal error; update the
constructor call to pass the appropriate id attribute type (e.g.,
Database::VAR_STRING or the constant used in your test suite) when creating new
DocumentsValidator($context, $idAttributeType) so the validator is initialized
with the correct id type.

public function testQueryLimit(): void
{
$validator = new Documents($this->attributes, [], Database::VAR_INTEGER);
$validator = new DocumentsValidator($this->context, Database::VAR_INTEGER);
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Debug output left in production code

There's a var_dump() statement that should be removed before merging.

Apply this diff to remove the debug statement:

-            var_dump($document);

🏁 Script executed:

#!/bin/bash
# Search for other var_dump statements in the codebase that might have been added
rg -n "var_dump\(" --type php

Length of output: 1369


Remove all debug var_dump calls
Multiple active var_dump statements remain in both production and test code. Remove them before merging:

  • src/Database/Database.php:1523
  • src/Database/Validator/Queries/V2.php:180, 688–689
  • src/Database/Adapter/SQL.php:195, 374, 2476
  • src/Database/Adapter/MariaDB.php:61
  • tests/e2e/Adapter/Scopes/JoinsTests.php:333, 375

@utopia-php utopia-php deleted a comment from coderabbitai bot Sep 7, 2025
@utopia-php utopia-php deleted a comment from coderabbitai bot Sep 7, 2025
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: 7

Caution

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

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

2357-2413: ORDER BY is fragile: missing alias, no natural-order handling, and breaks when orders are empty.

  • Qualify columns with the correct alias to avoid ambiguity in joins.
  • Treat empty attribute ('') as natural order by primary key (_id).
  • Use each prior sort’s alias when building cursor tie-breakers.
  • Guard against empty $orders when composing SQL.
-        foreach ($orderQueries as $i => $order) {
-            $orderAlias = $order->getAlias();
-            $attribute  = $order->getAttribute();
-            $originalAttribute = $attribute;
+        foreach ($orderQueries as $i => $order) {
+            $orderAlias = $order->getAlias() ?: Query::DEFAULT_ALIAS;
+            $attribute  = $order->getAttribute();
+            $originalAttribute = $attribute;
+
+            // Natural order -> primary key
+            if ($originalAttribute === '' || $originalAttribute === null) {
+                $originalAttribute = '$sequence';
+                $attribute = '_id';
+            }

             $attribute = $this->getInternalKeyForAttribute($originalAttribute);
             $attribute = $this->filter($attribute);

             $direction = $order->getOrderDirection();

             if ($cursorDirection === Database::CURSOR_BEFORE) {
                 $direction = ($direction === Database::ORDER_ASC) ? Database::ORDER_DESC : Database::ORDER_ASC;
             }

-            $orders[] = "{$this->quote($attribute)} {$direction}";
+            $orders[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$direction}";

             // Build pagination WHERE clause only if we have a cursor
             if (!empty($cursor)) {
                 // Special case: No tie breaks. only 1 attribute and it's a unique primary key
-                if (count($orderQueries) === 1 && $i === 0 && $originalAttribute === '$sequence') {
+                if (count($orderQueries) === 1 && $i === 0 && $originalAttribute === '$sequence') {
                     $operator = ($direction === Database::ORDER_DESC)
                         ? Query::TYPE_LESSER
                         : Query::TYPE_GREATER;

                     $bindName = ":cursor_pk";
-                    $binds[$bindName] = $cursor[$originalAttribute];
+                    $binds[$bindName] = $cursor['$sequence'];

-                    $cursorWhere[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}";
+                    $cursorWhere[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}";
                     break;
                 }

                 $conditions = [];

                 // Add equality conditions for previous attributes
                 for ($j = 0; $j < $i; $j++) {
-                    $prevQuery = $orderQueries[$j];
-                    $prevOriginal = $prevQuery->getAttribute();
-                    $prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal));
+                    $prevQuery = $orderQueries[$j];
+                    $prevAlias = $prevQuery->getAlias() ?: Query::DEFAULT_ALIAS;
+                    $prevOriginal = $prevQuery->getAttribute();
+                    if ($prevOriginal === '' || $prevOriginal === null) {
+                        $prevOriginal = '$sequence';
+                    }
+                    $prevAttr = $this->filter($this->getInternalKeyForAttribute($prevOriginal));

                     $bindName = ":cursor_{$j}";
-                    $binds[$bindName] = $cursor[$prevOriginal];
+                    $binds[$bindName] = $cursor[$prevOriginal];

-                    $conditions[] = "{$this->quote($orderAlias)}.{$this->quote($prevAttr)} = {$bindName}";
+                    $conditions[] = "{$this->quote($prevAlias)}.{$this->quote($prevAttr)} = {$bindName}";
                 }

                 // Add comparison for current attribute
                 $operator = ($direction === Database::ORDER_DESC)
                     ? Query::TYPE_LESSER
                     : Query::TYPE_GREATER;

                 $bindName = ":cursor_{$i}";
-                $binds[$bindName] = $cursor[$originalAttribute];
+                $binds[$bindName] = $cursor[$originalAttribute];

-                $conditions[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}";
+                $conditions[] = "{$this->quote($orderAlias)}.{$this->quote($attribute)} {$this->getSQLOperator($operator)} {$bindName}";

                 $cursorWhere[] = '(' . implode(' AND ', $conditions) . ')';
             }
         }
@@
-        $sqlOrder = 'ORDER BY ' . implode(', ', $orders);
+        $sqlOrder = empty($orders) ? '' : 'ORDER BY ' . implode(', ', $orders);

Also applies to: 2452-2454


521-525: Spatial check bug when binding update values.

isset on a numeric-keyed array always fails here. Use in_array to avoid incorrectly JSON-encoding spatial values.

-        foreach ($attributes as $attributeName => $value) {
-            if (!isset($spatialAttributes[$attributeName]) && is_array($value)) {
+        foreach ($attributes as $attributeName => $value) {
+            if (!in_array($attributeName, $spatialAttributes, true) && is_array($value)) {
                 $value = json_encode($value);
             }
src/Database/Database.php (1)

6721-6727: Fix undefined variable in decodeOriginal().

Uses $selections which is undefined; should use $selects.

-            if (
-                empty($selections)
-                || \in_array($key, $selections)
-                || \in_array('*', $selections)
-            ) {
+            if (
+                empty($selects)
+                || \in_array($key, $selects)
+                || \in_array('*', $selects)
+            ) {
♻️ Duplicate comments (9)
src/Database/Adapter/SQL.php (1)

1840-1847: $spatialAttributes parameter remains unused.

This was flagged previously; either remove it or implement spatial projection handling.

src/Database/Database.php (1)

1523-1523: Remove debug output from production path (duplicate of prior review).

var_dump() will corrupt API responses and leak internals.

Apply:

-        var_dump($collection->getAttribute('attributes'));
+        // no-op
src/Database/Validator/Queries/V2.php (6)

385-387: Allow selecting internal attributes without extra checks (duplicate of prior review).

Return early for internal keys.

-        if (\in_array($attribute, $internalKeys)) {
-            //return;
-        }
+        if (\in_array($attribute, $internalKeys, true)) {
+            return;
+        }

180-180: Remove debug var_dump (duplicate of prior review).

Debug prints break output and leak internals.

-        var_dump('=== validateAttributeExist');
+        // no-op

503-506: Call instance method, not static.

Use $this->isValid(...) for recursion.

-                    if (! self::isValid($query->getValues(), $scope)) {
+                    if (! $this->isValid($query->getValues(), $scope)) {
                         throw new \Exception($this->message);
                     }
@@
-                        if (! self::isValid($query->getValues(), 'joins')) {
+                        if (! $this->isValid($query->getValues(), 'joins')) {
                             throw new \Exception($this->message);
                         }

Also applies to: 586-588


674-683: Fix default branch: wrong variables used and missing spatial handling.

Reference the current $query and validate properly.

-                    default:
-                        if ($value->isSpatialQuery()) {
-                            if ($this->isEmpty($value->getValues())) {
-                                $this->message = \ucfirst($method) . ' queries require at least one value.';
-                                return false;
-                            }
-                            return $this->isValidAttributeAndValues($attribute, $value->getValues(), $method);
-                        }
-
-                        throw new \Exception('Invalid query: Method not found ');
+                    default:
+                        if (Query::isSpatialQuery($method)) {
+                            if ($this->isEmpty($query->getValues())) {
+                                throw new \Exception(\ucfirst($method).' queries require at least one value.');
+                            }
+                            $this->validateAttributeExist($query->getAttribute(), $query->getAlias());
+                            $this->validateValues($query->getAttribute(), $query->getAlias(), $query->getValues(), $method);
+                            break;
+                        }
+                        throw new \Exception('Invalid query: Method not found');

688-690: Remove debug var_dump in catch (duplicate of prior review).

Do not echo exceptions.

-            var_dump($this->message);
-            var_dump($e);
+            // logged upstream

271-301: Fix ID/sequence detection and add ObjectId support.

  • Wrong comparison when flagging sequence.
  • Missing VAR_OBJECT_ID handling.
-                case Database::VAR_ID:
-                    $validator = new Sequence($this->idAttributeType, $attribute === '$sequence');
+                case Database::VAR_ID:
+                    $validator = new Sequence($this->idAttributeType, $attributeId === '$sequence');
                     break;
+                case Database::VAR_OBJECT_ID:
+                    $validator = new Sequence(Database::VAR_OBJECT_ID, false);
+                    break;
@@
-                case Database::VAR_RELATIONSHIP:
-                    $validator = new Text(255, 0); // The query is always on uid
+                case Database::VAR_RELATIONSHIP:
+                    $validator = new Text(0, 255); // The query is always on uid
                     break;
src/Database/Query.php (1)

44-45: Broken round‑trip for joins/alias metadata; missing TYPES/isMethod entries

Joins and relationEqual can’t be parsed/serialized; alias/as/collection/aliasRight/system are dropped; join values aren’t recursively serialized. Add types, persist metadata, and recurse for joins.

@@
     public const TYPES = [
@@
-        self::TYPE_SELECT,
+        self::TYPE_SELECT,
+        self::TYPE_RELATION_EQUAL,
+        self::TYPE_INNER_JOIN,
+        self::TYPE_LEFT_JOIN,
+        self::TYPE_RIGHT_JOIN,
@@
     public static function isMethod(string $value): bool
     {
         return match ($value) {
@@
-            self::TYPE_SELECT => true,
+            self::TYPE_SELECT,
+            self::TYPE_RELATION_EQUAL,
+            self::TYPE_INNER_JOIN,
+            self::TYPE_LEFT_JOIN,
+            self::TYPE_RIGHT_JOIN => true,
             default => false,
         };
     }
@@ public function toArray(): array
-        if (\in_array($array['method'], self::LOGICAL_TYPES)) {
+        // Persist extended metadata (omit defaults)
+        if ($this->alias !== self::DEFAULT_ALIAS) {
+            $array['alias'] = $this->alias;
+        }
+        if ($this->as !== '') {
+            $array['as'] = $this->as;
+        }
+        if ($this->collection !== '') {
+            $array['collection'] = $this->collection;
+        }
+        if ($this->attributeRight !== '') {
+            $array['attributeRight'] = $this->attributeRight;
+        }
+        if ($this->aliasRight !== self::DEFAULT_ALIAS) {
+            $array['aliasRight'] = $this->aliasRight;
+        }
+        if ($this->system === true) {
+            $array['system'] = true;
+        }
+
+        // Logical and Join queries nest other queries
+        if (\in_array($array['method'], self::LOGICAL_TYPES, true) || $this->isJoin()) {
             foreach ($this->values as $index => $value) {
                 $array['values'][$index] = $value->toArray();
             }
         } else {
@@ public static function parseQuery(array $query): self
-        $method = $query['method'] ?? '';
-        $attribute = $query['attribute'] ?? '';
-        $values = $query['values'] ?? [];
+        $method = $query['method'] ?? '';
+        $attribute = $query['attribute'] ?? '';
+        $values = $query['values'] ?? [];
+        $alias = $query['alias'] ?? '';
+        $as = $query['as'] ?? '';
+        $collection = $query['collection'] ?? '';
+        $attributeRight = $query['attributeRight'] ?? '';
+        $aliasRight = $query['aliasRight'] ?? '';
+        $system = $query['system'] ?? false;
@@
-        if (\in_array($method, self::LOGICAL_TYPES)) {
+        if (\in_array($method, self::LOGICAL_TYPES, true) || \in_array($method, [self::TYPE_INNER_JOIN, self::TYPE_LEFT_JOIN, self::TYPE_RIGHT_JOIN], true)) {
             foreach ($values as $index => $value) {
                 $values[$index] = self::parseQuery($value);
             }
         }
 
-        return new self($method, $attribute, $values);
+        if (!\is_string($alias)) { throw new QueryException('Invalid query alias. Must be a string, got ' . \gettype($alias)); }
+        if (!\is_string($as)) { throw new QueryException('Invalid query as. Must be a string, got ' . \gettype($as)); }
+        if (!\is_string($collection)) { throw new QueryException('Invalid query collection. Must be a string, got ' . \gettype($collection)); }
+        if (!\is_string($attributeRight)) { throw new QueryException('Invalid query attributeRight. Must be a string, got ' . \gettype($attributeRight)); }
+        if (!\is_string($aliasRight)) { throw new QueryException('Invalid query aliasRight. Must be a string, got ' . \gettype($aliasRight)); }
+        if (!\is_bool($system)) { throw new QueryException('Invalid query system. Must be a boolean, got ' . \gettype($system)); }
+
+        return new self(
+            $method,
+            $attribute,
+            $values,
+            alias: $alias,
+            attributeRight: $attributeRight,
+            aliasRight: $aliasRight,
+            collection: $collection,
+            as: $as,
+            system: $system,
+        );

Also applies to: 64-70, 73-113, 363-407, 462-491, 512-538

🧹 Nitpick comments (13)
src/Database/Adapter/SQL.php (3)

1854-1856: Default alias safeguard in projection.

Avoid generating ````.col`` when alias is empty.

-            $alias = $select->getAlias();
-            $alias = $this->filter($alias);
+            $alias = $select->getAlias() ?: Query::DEFAULT_ALIAS;
+            $alias = $this->filter($alias);

2341-2344: Nit: unsetting an unused parameter is unnecessary.

Drop the unset; it adds noise.

-    ): array {
-        unset($queries); // remove this since we pass explicit queries
+    ): array {

393-416: Optional: map _collection to $collection for symmetry.

Results already map other internals; consider mapping _collection too for consistency.

         if (\array_key_exists('_permissions', $document)) {
             $document['$permissions'] = json_decode($document['_permissions'] ?? '[]', true);
             unset($document['_permissions']);
         }
+        if (\array_key_exists('_collection', $document)) {
+            $document['$collection'] = $document['_collection'];
+            unset($document['_collection']);
+        }

And inside find() loop:

         if (\array_key_exists('_permissions', $document)) {
             $results[$index]['$permissions'] = \json_decode($document['_permissions'] ?? '[]', true);
             unset($results[$index]['_permissions']);
         }
+        if (\array_key_exists('_collection', $document)) {
+            $results[$index]['$collection'] = $document['_collection'];
+            unset($results[$index]['_collection']);
+        }

Also applies to: 2496-2523

src/Database/Database.php (3)

6285-6300: Avoid recomputing grouped data; pick one source of truth.

Values from groupByType() are immediately overwritten by manual getters; keep one path to reduce drift.

-        $grouped = Query::groupByType($queries);
-        $filters = $grouped['filters'];
-        $selects = $grouped['selections'];
-        $limit = $grouped['limit'];
-        $offset = $grouped['offset'];
-        $orderAttributes = $grouped['orderAttributes'];
-        $orderTypes = $grouped['orderTypes'];
-        $cursor = $grouped['cursor'];
-        $cursorDirection = $grouped['cursorDirection'] ?? Database::CURSOR_AFTER;
-
-        $filters = Query::getFilterQueries($queries);
-        $selects = Query::getSelectQueries($queries);
-        $limit = Query::getLimitQuery($queries, 25);
-        $offset = Query::getOffsetQuery($queries, 0);
-        $orders = Query::getOrderQueries($queries);
+        $grouped = Query::groupByType($queries);
+        $filters = $grouped['filters'];
+        $selects = $grouped['selections'];
+        $limit = $grouped['limit'] ?? 25;
+        $offset = $grouped['offset'] ?? 0;
+        $orders = $grouped['orderQueries'] ?? Query::getOrderQueries($queries);
+        $cursor = $grouped['cursor'] ?? [];
+        $cursorDirection = $grouped['cursorDirection'] ?? Database::CURSOR_AFTER;

4471-4475: Return pipeline consistency: cast before decode in updateDocument().

Other paths cast then decode; do the same here for consistent types.

-        $context = new QueryContext();
-        $context->add($collection);
-
-        $document = $this->decode($context, $document);
+        $context = new QueryContext();
+        $context->add($collection);
+        $document = $this->casting($context, $document);
+        $document = $this->decode($context, $document);

7229-7231: Stabilize cache key hashing for selects.

md5(serialize($selects)) can be brittle across PHP versions/object state. Normalize to a deterministic string (method|alias|attr|as).

-            if (!empty($selects)) {
-                $documentHashKey = $documentKey . ':' . \md5(\serialize($selects));
-            }
+            if (!empty($selects)) {
+                $norm = array_map(fn($q) => implode('|', [
+                    $q->getMethod(), $q->getAlias(), $q->getAttribute(), $q->getAs() ?? ''
+                ]), $selects);
+                $documentHashKey = $documentKey . ':' . \md5(\implode(';', $norm));
+            }
src/Database/Validator/Queries/V2.php (2)

263-265: Fix spatial error message to reference the right attribute.

Use $attributeId and include actual type for clarity.

-        if (Query::isSpatialQuery($method) && !in_array($attribute['type'], Database::SPATIAL_TYPES, true)) {
-            throw new \Exception('Invalid query: Spatial query "' . $method . '" cannot be applied on non-spatial attribute: ' . $attribute);
+        if (Query::isSpatialQuery($method) && !in_array($attribute['type'], Database::SPATIAL_TYPES, true)) {
+            throw new \Exception(
+                'Invalid query: Spatial query "'.$method.'" cannot be applied on non-spatial attribute "'.$attributeId.'" (type '.$attribute['type'].').'
+            );
         }

391-407: Tighten dotted-attribute handling.

Remove duplicated str_contains and keep the try/catch logic.

-        if (\str_contains($attribute, '.')) {
-            if (\str_contains($attribute, '.')) {
+        if (\str_contains($attribute, '.')) {
src/Database/Query.php (5)

710-714: select(): unused $function parameter

$function is accepted but ignored. Either persist it (e.g., into values/metadata) or remove it for now to avoid dead API.

Do you want me to wire this into toArray()/parseQuery as a "function" field?


1194-1201: Make isNested honor join queries

Callers expecting “container” queries will likely want joins considered nested.

-        if (in_array($this->getMethod(), self::LOGICAL_TYPES)) {
+        if (in_array($this->getMethod(), self::LOGICAL_TYPES, true) || $this->isJoin()) {
             return true;
         }

Also applies to: 1204-1215


1217-1221: Use strict in_array for method checks

Avoid accidental truthiness collisions.

-        return in_array($method, self::FILTER_TYPES);
+        return in_array($method, self::FILTER_TYPES, true);

307-331: Throw QueryException, not generic Exception

Aligns with the rest of the class’ error handling.

-        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');

409-416: Tighten param type for isSpatialQuery

Signature/docblock mismatch; accept string explicitly.

-    /**
-     * Check if method is a spatial-only query method
-     * @param $method
-     * @return bool
-     */
-    public static function isSpatialQuery($method): bool
+    /**
+     * Check if method is a spatial-only query method
+     * @param string $method
+     * @return bool
+     */
+    public static function isSpatialQuery(string $method): bool
📜 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 22f92e3 and c418511.

📒 Files selected for processing (5)
  • src/Database/Adapter/SQL.php (10 hunks)
  • src/Database/Database.php (50 hunks)
  • src/Database/Query.php (21 hunks)
  • src/Database/Validator/Queries/V2.php (1 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (16 hunks)
🔇 Additional comments (8)
tests/e2e/Adapter/Scopes/DocumentTests.php (6)

21-21: QueryContext import aligns with new decode/selection API.

Import looks correct and consistent with the new flow.


1308-1339: Per-field select assertions are clear and correct.

Good verification of inclusion/exclusion for user and internal fields with/without selecting $id.


2929-2946: Scoped projection before nested OR reduces payload and matches new exposure rules.

Selecting only 'director' here is appropriate given the assertions.


3454-3590: Comprehensive coverage of select() for special fields.

Nice matrix of $id/$sequence/$collection/$createdAt/$updatedAt/$permissions variants—this will catch regressions in projection and internal-field exposure.


3940-3944: Decode now uses QueryContext.

Looks good; ensure all call sites are updated to pass QueryContext consistently.


4678-4685: Short-selects in bulk delete are well-formed.

Using array_map to build Query::select() list is concise and readable.

Also applies to: 4887-4917

src/Database/Query.php (2)

560-575: API surface additions look good

Alias-aware builders and join/relation helpers are consistent and useful. Once round-trip is fixed, these will integrate cleanly.

Also applies to: 584-623, 657-660, 721-735, 885-927, 929-933


120-144: ```shell
#!/bin/bash

Verify duplicate TYPE_GREATER_EQUAL, FILTER_TYPES, getFilterQueries, and TYPE_AND/TYPE_OR usage

Show occurrences of TYPE_GREATER_EQUAL in Query.php

rg -n "TYPE_GREATER_EQUAL" -g "src/Database/Query.php"

Display FILTER_TYPES definition

rg -n "protected const FILTER_TYPES" -g "src/Database/Query.php" -C3

Locate getFilterQueries method to see how FILTER_TYPES is used

rg -nP --type=php "function getFilterQueries" -g "src/Database/Query.php" -C5

Check where TYPE_AND and TYPE_OR constants are referenced

rg -n "TYPE_AND" -g "src/Database/Query.php"
rg -n "TYPE_OR" -g "src/Database/Query.php"


</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +195 to 196
var_dump($document);
} catch (PDOException $e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove debug output in exists().

Leftover var_dump will leak internals to stdout and break consumers.

Apply this diff:

-            $stmt->closeCursor();
-            var_dump($document);
+            $stmt->closeCursor();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var_dump($document);
} catch (PDOException $e) {
$stmt->closeCursor();
} catch (PDOException $e) {
🤖 Prompt for AI Agents
In src/Database/Adapter/SQL.php around lines 195-196 there is a leftover
var_dump($document) inside the exists() method; remove that debug output so
internals are not printed to stdout and instead either do nothing or log via the
project's logger (e.g. $this->logger->debug(...)) if diagnostic information is
required, ensuring no var_dump or echo statements remain in production code.

Comment on lines +374 to 375
var_dump($sql);
$stmt = $this->getPDO()->prepare($sql);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove debug output in getDocument().

var_dump($sql) should not ship.

-        if ($this->getSupportForUpdateLock()) {
-            $sql .= " {$forUpdate}";
-        }
-var_dump($sql);
+        if ($this->getSupportForUpdateLock()) {
+            $sql .= " {$forUpdate}";
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var_dump($sql);
$stmt = $this->getPDO()->prepare($sql);
if ($this->getSupportForUpdateLock()) {
$sql .= " {$forUpdate}";
}
$stmt = $this->getPDO()->prepare($sql);
🤖 Prompt for AI Agents
In src/Database/Adapter/SQL.php around lines 374 to 375, remove the debug
var_dump($sql) left in getDocument() so SQL is not printed to output; simply
delete that var_dump call so the code proceeds directly to preparing the
statement (keep the $stmt = $this->getPDO()->prepare($sql); line intact).

Comment on lines +2474 to 2475
var_dump($sql);
$sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove debug output in find().

Same issue as above.

-        ";
-var_dump($sql);
+        ";

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/Database/Adapter/SQL.php around lines 2474-2475, remove the leftover
debug var_dump($sql) call in find(); replace it by simply proceeding to trigger
the event and return as intended. Ensure no debug output remains (remove the
var_dump line) so the method does not print SQL to stdout, and keep the
subsequent $sql = $this->trigger(Database::EVENT_DOCUMENT_FIND, $sql); line
intact.

Comment on lines 1109 to 1111
$filters = [];
$joins = [];
$selections = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

groupByType bugs: wrong fallbacks; joins misclassified as filters

  • $offset uses $limit as fallback; should use $offset.
  • $cursor uses $limit as fallback; should use $cursor.
  • Join methods fall into default and are added to filters.
                 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;
+                case Query::TYPE_INNER_JOIN:
+                case Query::TYPE_LEFT_JOIN:
+                case Query::TYPE_RIGHT_JOIN:
+                    // Do not mix joins into filters; consumers can use Query::getJoinQueries($queries)
+                    break;

Also applies to: 1128-1175

🤖 Prompt for AI Agents
In src/Database/Query.php around lines 1109-1111 (and similarly 1128-1175), the
groupByType logic wrongly uses $limit as the fallback for $offset and $cursor
and it treats join-related methods as default filters; update the switch/case
(or mapping) so $offset falls back to $offset (not $limit) and $cursor falls
back to $cursor, and explicitly handle join method names so they are appended to
the $joins array instead of being placed in $filters; ensure default still adds
true filters only and adjust any variable names used in fallbacks and
assignments accordingly.

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

🧹 Nitpick comments (3)
tests/e2e/Adapter/Scopes/RelationshipTests.php (1)

334-337: Nit: collapse consecutive selects into a single variadic call (if supported).
Minor readability win; same behavior with fewer Query objects.

-            Query::select('*'),
-            Query::select('votes.*'),
-            Query::select('votes.animals.*'),
+            Query::select('*', 'votes.*', 'votes.animals.*'),
tests/e2e/Adapter/Base.php (2)

11-11: Unused import: JoinsTests.
Either enable the trait below or drop this import to avoid dead code.

-use Tests\E2E\Adapter\Scopes\JoinsTests;
+// use Tests\E2E\Adapter\Scopes\JoinsTests;

22-27: Big test coverage drop — confirm intent and timeline to re-enable.
Commenting out core suites (Collection/Document/Attribute/Index/Permission) hides regressions. If this is temporary for Joins/QueryContext work, add a TODO and an issue link, or gate with @group to keep code visible in-base.

I can propose a plan to re-enable incrementally or annotate with @group; want me to open a tracking ticket and prep diffs?

📜 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 c418511 and f51ca0f.

📒 Files selected for processing (2)
  • tests/e2e/Adapter/Base.php (2 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (3 hunks)
🔇 Additional comments (2)
tests/e2e/Adapter/Scopes/RelationshipTests.php (2)

323-325: LGTM: additive Query::select() usage is clear and aligns with intent.
Keeps base fields while resolving votes; assertions match expected shape.


349-349: LGTM: '*' alone should not resolve relations.
The following assertions correctly verify non-resolution of nested relations.

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/Database.php (1)

6685-6685: Fix undefined variable in decodeOriginal(): use $selects (Query[]) not $selections.

Current code references $selections, causing notices and incorrect filtering.

     foreach ($this->getInternalAttributes() as $attribute) {
         $attributes[] = $attribute;
     }
 
+    // Normalize selects (support both Query[] and string[] for backward compatibility)
+    $selected = \array_map(
+        fn ($s) => $s instanceof Query ? $s->getAttribute() : $s,
+        $selects
+    );
+
     foreach ($attributes as $attribute) {
         $key = $attribute['$id'] ?? '';
@@
-            if (
-                empty($selections)
-                || \in_array($key, $selections)
-                || \in_array('*', $selections)
-            ) {
+            if (
+                empty($selected)
+                || \in_array($key, $selected, true)
+                || \in_array('*', $selected, true)
+            ) {
                 $document->setAttribute($key, ($array) ? $value : $value[0]);
             }

Also applies to: 6722-6729

♻️ Duplicate comments (3)
src/Database/Database.php (3)

1523-1523: Remove debug output from production path.

Leftover var_dump leaks internals and corrupts responses. Delete it.

-        var_dump($collection->getAttribute('attributes'));
+        // removed debug output

7157-7157: Remove stray var_dump in convertQuery().

Debug print in hot path breaks responses and logs sensitive data.

-                var_dump($values);
+                // removed debug output

6303-6311: Cursor safety: require unique order or fallback to $id; avoid empty ORDER BY.

Query::orderAsc() without an attribute is invalid and will also make cursor paging unsafe. Enforce unique order when a cursor is present; otherwise, add a deterministic fallback on $id only when not using joins.

-        if ($uniqueOrderBy === false) {
-            $orders[] = Query::orderAsc(); // In joins we should not add a default order, we should validate when using a cursor we should have a unique order
-        }
+        if ($uniqueOrderBy === false) {
+            if (!empty($cursor)) {
+                throw new OrderException("Cursor requires unique order by '\$id' or '\$sequence'.");
+            }
+            if (empty($joins)) {
+                $orders[] = Query::orderAsc('$id');
+            }
+        }
 
         if (!empty($cursor)) {
             foreach ($orders as $order) {
                 if ($cursor->getAttribute($order->getAttribute()) === null) {
                     throw new OrderException(
-                        message: "Order attribute '{$order->getAttribute()}' is empty",
+                        message: "Order attribute '{$order->getAttribute()}' is empty",
                         attribute: $order->getAttribute()
                     );
                 }
             }
         }

Also applies to: 6313-6321

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

6285-6300: Avoid double-parsing queries; use one source of truth.

You compute grouped fields via groupByType() then immediately re-derive them via Query::get*(); this risks divergence and increases maintenance.

-        $filters = Query::getFilterQueries($queries);
-        $selects = Query::getSelectQueries($queries);
-        $limit = Query::getLimitQuery($queries, 25);
-        $offset = Query::getOffsetQuery($queries, 0);
-        $orders = Query::getOrderQueries($queries);
+        // Prefer using either $grouped[...] or the Query::get* helpers, not both.
+        $orders = Query::getOrderQueries($queries);

6784-6786: Throw QueryException for unknown alias context.

Use the project’s query exception type for consistency and better handling.

-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Invalid query: Unknown alias context');
+            }

Also applies to: 6881-6883


7230-7232: Stabilize cache key hashing for selects.

md5(serialize($selects)) on objects can be brittle. Hash a normalized tuple of (alias|attribute|as) to avoid serialization quirks.

-            if (!empty($selects)) {
-                $documentHashKey = $documentKey . ':' . \md5(\serialize($selects));
-            }
+            if (!empty($selects)) {
+                $selParts = \array_map(
+                    fn ($s) => $s instanceof Query
+                        ? ($s->getAlias() . '|' . $s->getAttribute() . '|' . ($s->getAs() ?? ''))
+                        : (string)$s,
+                    $selects
+                );
+                $documentHashKey = $documentKey . ':' . \md5(\implode(',', $selParts));
+            }

4471-4475: Consistency: apply casting before decode in updateDocument().

Other paths call casting() then decode(). Do the same here to keep output typing consistent.

-        $context = new QueryContext();
-        $context->add($collection);
-
-        $document = $this->decode($context, $document);
+        $context = new QueryContext();
+        $context->add($collection);
+        $document = $this->casting($context, $document);
+        $document = $this->decode($context, $document);

7035-7041: validateSelections() currently mixes Query objects with strings.

If this helper will be revived, normalize inputs to strings before diffing, or operate directly on Query instances.

-        foreach ($queries as $query) {
+        foreach ($queries as $query) {
             if ($query->getMethod() == Query::TYPE_SELECT) {
-                if (\str_contains($query->getValue(), '.')) {
+                $values = $query->getValues();
+                if (\count($values) === 1 && \str_contains($values[0], '.')) {
                     $relationshipSelections[] = $query;
                     continue;
                 }
-                $selections[] = $query;
+                $selections = \array_merge($selections, $values);
             }
         }
@@
-        $invalid = \array_diff($selections, $keys);
+        $invalid = \array_diff($selections, $keys);

Also applies to: 7057-7071

📜 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 f51ca0f and f42c411.

📒 Files selected for processing (2)
  • src/Database/Database.php (51 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php
🔇 Additional comments (2)
src/Database/Database.php (2)

3355-3365: QueryContext threading and V2 validator integration look solid.

Context-aware validation, casting/decoding, and join-aware selections are wired coherently across get/find/create/update/batch ops.

Also applies to: 3361-3365, 3375-3381, 3436-3438, 3823-3828, 3870-3872, 3929-3931, 4528-4536, 4676-4676, 5153-5155, 6033-6045, 6237-6261, 6351-6353, 6526-6554, 6465-6487


4613-4621: Batch ops: required attribute presence checks are good.

Enforcing $permissions and $sequence in selects prevents downstream errors and permission drift.

Also applies to: 6092-6100

@utopia-php utopia-php deleted a comment from coderabbitai bot Sep 7, 2025
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: 5

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 (3)

6659-6732: Fix undefined variable and selection handling in decodeOriginal().

Uses undefined $selections and ignores Query objects. This will trigger notices and incorrect filtering.

-    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
+    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
     {
+        // Normalize selected attributes from Query[] or string[]
+        $selectedAttributes = [];
+        foreach ($selects as $s) {
+            if ($s instanceof Query && $s->getMethod() === Query::TYPE_SELECT) {
+                $selectedAttributes[] = $s->getAttribute();
+            } elseif (is_string($s)) {
+                $selectedAttributes[] = $s;
+            }
+        }
...
-            if (
-                empty($selections)
-                || \in_array($key, $selections)
-                || \in_array('*', $selections)
-            ) {
+            if (
+                empty($selectedAttributes)
+                || \in_array($key, $selectedAttributes, true)
+                || \in_array('*', $selectedAttributes, true)
+            ) {
                 $document->setAttribute($key, ($array) ? $value : $value[0]);
             }

4595-4607: Batch update requires $permissions and $sequence in select; add them automatically.

Avoids surprising QueryException when callers don't specify these internals.

-            $batch = $this->silent(fn () => $this->find(
+            // Ensure required internals are selected
+            [$queries] = Query::addSelect($queries, Query::select('$permissions', system: true));
+            [$queries] = Query::addSelect($queries, Query::select('$sequence', system: true));
+
+            $batch = $this->silent(fn () => $this->find(
                 $collection->getId(),
                 array_merge($new, $queries),
                 forPermission: Database::PERMISSION_UPDATE
             ));

6079-6086: Batch delete requires $permissions and $sequence in select; add them automatically.

Same requirement as updates.

-            $batch = $this->silent(fn () => $this->find(
+            // Ensure required internals are selected
+            [$queries] = Query::addSelect($queries, Query::select('$permissions', system: true));
+            [$queries] = Query::addSelect($queries, Query::select('$sequence', system: true));
+
+            $batch = $this->silent(fn () => $this->find(
                 $collection->getId(),
                 array_merge($new, $queries),
                 forPermission: Database::PERMISSION_DELETE
             ));
♻️ Duplicate comments (2)
src/Database/Database.php (2)

1523-1523: Remove debug var_dump from production path.

This dumps internals and corrupts responses.

-        var_dump($collection->getAttribute('attributes'));
+        // removed debug output

7155-7158: Remove stray var_dump($values) in convertQuery().

Leaking values during query conversion breaks API responses.

-            if ($attribute->getAttribute('type') == Database::VAR_DATETIME) {
-                $values = $query->getValues();
-                var_dump($values);
+            if ($attribute->getAttribute('type') == Database::VAR_DATETIME) {
+                $values = $query->getValues();
🧹 Nitpick comments (7)
tests/e2e/Adapter/Scopes/RelationshipTests.php (2)

360-361: Make the test deterministic by filtering the subject document.

Without a filter, findOne('veterinarians', [...]) depends on insertion order. Add an $id filter to avoid ordering-related flakes.

Apply:

-            [
-                Query::select('animals.*')
-            ]
+            [
+                Query::select('animals.*'),
+                Query::equal('$id', ['dr.pol']),
+            ]

1342-1345: Clean up stray commented code and migrate legacy Query::select calls

  • Remove the commented line in tests/e2e/Adapter/Scopes/RelationshipTests.php (around lines 1342–1345):
-            Query::select('name'),
-            //Query::select('*'),
-            Query::select('models.name'),
+            Query::select('name'),
+            Query::select('models.name'),
  • Migrate the 22 remaining Query::select([...]) usages (per your rg output) to the additive API (->select('a')->select('b')) or wrap them in Query::select_old([...]) during transition.
src/Database/Database.php (5)

7228-7233: Make cache key hashing stable across PHP versions.

serialize($selects) on objects can be brittle. Normalize selects (alias|attribute|as|function|system) before hashing.

-            if (!empty($selects)) {
-                $documentHashKey = $documentKey . ':' . \md5(\serialize($selects));
-            }
+            if (!empty($selects)) {
+                $documentHashKey = $documentKey . ':' . $this->hashSelects($selects);
+            }

Add helper:

private function hashSelects(array $selects): string
{
    $parts = [];
    foreach ($selects as $s) {
        if ($s instanceof Query && $s->getMethod() === Query::TYPE_SELECT) {
            $parts[] = implode('|', [
                $s->getAlias(),
                $s->getAttribute(),
                $s->getAs(),
                $s->getFunction(),
                $s->isSystem() ? '1' : '0',
            ]);
        }
    }
    sort($parts);
    return md5(implode(';', $parts));
}

6285-6294: Avoid duplicated query parsing.

You compute grouped and then re-derive filters/selects/limit/offset/orders. Keep one code path.

Also applies to: 6295-6300


6492-6497: Initialize $skipAuth for clarity.

Prevents reliance on null coalescing and improves readability.

-        $authorization = new Authorization(self::PERMISSION_READ);
+        $skipAuth = false;
+        $authorization = new Authorization(self::PERMISSION_READ);
         if ($authorization->isValid($collection->getRead())) {
-            $skipAuth = true;
+            $skipAuth = true;
         }

Also applies to: 6529-6533


6766-6781: Avoid O(n×m) scanning of selects during decode/casting.

Pre-index selects by as/attribute to remove per-field scans.

Example:

$selectIndex = [];
foreach ($selects as $s) {
    if ($s->getMethod() === Query::TYPE_SELECT) {
        $selectIndex[$s->getAs() ?: $this->adapter->filter($s->getAttribute())] = $s;
    }
}
// then look up $selectIndex[$key] instead of looping $selects each time

Also applies to: 6864-6878


7261-7266: Docblock mismatch: not passed by reference.

Update the comment or change signature; currently, $queries is not by reference.

📜 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 f42c411 and dd15243.

📒 Files selected for processing (2)
  • src/Database/Database.php (51 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/e2e/Adapter/Scopes/RelationshipTests.php (1)
src/Database/Query.php (2)
  • Query (8-1426)
  • select (710-713)
src/Database/Database.php (7)
src/Database/Validator/Queries/V2.php (3)
  • V2 (23-696)
  • isEmpty (155-166)
  • isValid (472-695)
src/Database/Query.php (15)
  • getAttribute (224-227)
  • Query (8-1426)
  • getSelectQueries (958-963)
  • addSelect (1392-1425)
  • select (710-713)
  • getJoinQueries (969-976)
  • join (891-894)
  • getCollection (266-269)
  • getFilterQueries (1087-1090)
  • limit (743-746)
  • getLimitQuery (998-1007)
  • offset (754-757)
  • getOffsetQuery (1029-1038)
  • getOrderQueries (1044-1050)
  • orderAsc (732-735)
src/Database/Document.php (9)
  • getAttribute (224-231)
  • getId (63-66)
  • isEmpty (396-399)
  • removeAttribute (287-293)
  • Document (12-470)
  • getCollection (85-88)
  • getPermissionsByType (145-157)
  • find (304-322)
  • getArrayCopy (423-458)
src/Database/QueryContext.php (6)
  • QueryContext (8-89)
  • add (60-68)
  • getCollections (32-35)
  • skipAuth (75-86)
  • addSkipAuth (70-73)
  • getCollectionByAlias (37-55)
src/Database/Adapter/SQL.php (5)
  • getIdAttributeType (1731-1734)
  • getDocument (353-419)
  • find (2328-2530)
  • getSupportForCasting (1434-1437)
  • count (2542-2604)
src/Database/Adapter.php (6)
  • getIdAttributeType (905-905)
  • getDocument (691-691)
  • filter (1164-1173)
  • find (805-817)
  • getSupportForCasting (986-986)
  • count (840-840)
src/Database/Validator/Authorization.php (3)
  • Authorization (7-225)
  • isValid (55-77)
  • skip (160-170)
🪛 PHPMD (2.15.0)
src/Database/Database.php

6659-6659: Avoid unused parameters such as '$selects'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (3)
tests/e2e/Adapter/Scopes/RelationshipTests.php (2)

349-351: Confirm semantics: select('*') implies no relationship resolving.

The test relies on adapters consistently treating Query::select('*') as "no resolving." Please verify behavior across all adapters (MySQL, MariaDB, etc.) to avoid adapter-specific flakes.


378-381: LGTM: additive, nested selects are clear and precise.

This sequence cleanly expresses required expansions.

src/Database/Database.php (1)

3361-3365: Good upgrade to V2 validator with context.

Stronger, context-aware validation path for getDocument().

Comment on lines +6782 to +6786
$collection = $context->getCollectionByAlias($alias);
if ($collection->isEmpty()) {
throw new \Exception('Invalid query: Unknown Alias context');
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Throw QueryException for unknown alias context.

Aligns with database/query error semantics and avoids leaking generic \Exception.

-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }

Also applies to: 6881-6884

🤖 Prompt for AI Agents
In src/Database/Database.php around lines 6782-6786, the code throws a generic
\Exception when the alias context is unknown; replace that with throwing the
appropriate QueryException (use the existing QueryException class/namespace used
elsewhere) and provide a clear message like "Unknown alias context" so it fits
database/query error semantics; also make the identical change at the other
occurrence around lines 6881-6884 to ensure consistent error typing across both
sites.

Comment on lines +334 to 337
Query::select('*'),
Query::select('votes.*'),
Query::select('votes.animals.*'),
Query::equal('$id', ['trump'])
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same ordering hazard here; place most-specific first.

To ensure 'votes.animals.*' is honored regardless of addSelect() behavior, order from most- to least-specific.

Apply:

-            Query::select('*'),
-            Query::select('votes.*'),
-            Query::select('votes.animals.*'),
+            Query::select('votes.animals.*'),
+            Query::select('votes.*'),
+            Query::select('*'),
🤖 Prompt for AI Agents
In tests/e2e/Adapter/Scopes/RelationshipTests.php around lines 334 to 337, the
Query::select items are ordered least- to most-specific so 'votes.animals.*' can
be overridden by addSelect behavior; reorder the selects so the most-specific
comes first (e.g., Query::select('votes.animals.*'), then
Query::select('votes.*'), then Query::select('*')), leaving the
Query::equal('$id', ['trump']) unchanged, to ensure 'votes.animals.*' is
honored.

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: 4

Caution

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

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

1-7447: Remove all debug prints and replace generic Exceptions

  • Remove var_dump calls at lines 1523, 3489, 6351–6353, and 7163, as well as the echo PHP_EOL… at line 6350.
  • Replace the three occurrences of
    throw new \Exception('Invalid query: Unknown Alias context');
    at lines 6790, 6888, and 7138 with throw new QueryException('Invalid query: Unknown Alias context');.

3355-3462: Ensure system fields are auto-included for batch ops

  • In updateDocuments() and deleteDocuments(), you validate that each batch entry has ‘$permissions’ and ‘$sequence’, but the internal find() call uses Query::getSelectQueries() without adding those system selects. As a result, calls without explicit selects will throw at runtime.
  • Fix by auto-including Query::select('$permissions', system: true) and Query::select('$sequence', system: true) in the find() invocation for update/delete, or centralize in find() when forPermission ≠ PERMISSION_READ. Document this policy clearly.
♻️ Duplicate comments (3)
src/Database/Database.php (3)

1523-1523: Remove debug output from production path.

Leftover var_dump will break responses and leak internals.

-        var_dump($collection->getAttribute('attributes'));
+        // no-op

7163-7164: Delete leftover var_dump in convertQuery().

Leaks internal data and breaks output.

-                var_dump($values);

6310-6312: Cursor + default order: require unique order when using a cursor; default only when no cursor.

Prevents unstable pagination and invalid cursor checks.

-        if ($uniqueOrderBy === false) {
-            $orders[] = Query::orderAsc(); // In joins we should not add a default order, we should validate when using a cursor we should have a unique order
-        }
+        if ($uniqueOrderBy === false && empty($cursor)) {
+            $orders[] = Query::orderAsc('$id');
+        }
 
-        if (!empty($cursor)) {
+        if (!empty($cursor)) {
+            if ($uniqueOrderBy === false) {
+                throw new OrderException("A unique order by '\$id' or '\$sequence' is required when using a cursor.");
+            }
             foreach ($orders as $order) {
                 if ($cursor->getAttribute($order->getAttribute()) === null) {
                     throw new OrderException(
                         message: "Order attribute '{$order->getAttribute()}' is empty",
                         attribute: $order->getAttribute()
                     );
                 }
             }
         }

Also applies to: 6314-6323

🧹 Nitpick comments (2)
src/Database/Database.php (2)

6285-6303: Redundant query grouping vs. getters.

You compute grouped values then recompute filters/selects/limit/offset/orders from $queries. Prefer one source to avoid drift; keep $grouped (needed for cursor) and drop the re-derivations.

If you agree, remove the re-derivation block and use $grouped['filters'], $grouped['selections'], etc., or keep the getters and only use $grouped for $cursor/$cursorDirection.


6749-6951: DRY up decode() and casting() pipelines.

Both duplicate alias resolution, schema lookup, and iteration. Extract a shared internal helper to reduce maintenance surface.

📜 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 dd15243 and d0d0e10.

📒 Files selected for processing (2)
  • src/Database/Database.php (52 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Database.php (5)
src/Database/Validator/Queries/V2.php (3)
  • V2 (23-696)
  • isEmpty (155-166)
  • isValid (472-695)
src/Database/Query.php (20)
  • getAttribute (224-227)
  • Query (8-1426)
  • getSelectQueries (958-963)
  • addSelect (1392-1425)
  • select (710-713)
  • getJoinQueries (969-976)
  • join (891-894)
  • getCollection (266-269)
  • getAlias (246-249)
  • getFilterQueries (1087-1090)
  • limit (743-746)
  • getLimitQuery (998-1007)
  • offset (754-757)
  • getOffsetQuery (1029-1038)
  • getOrderQueries (1044-1050)
  • getAs (261-264)
  • getMethod (216-219)
  • getValue (241-244)
  • isNested (1194-1201)
  • getValues (232-235)
src/Database/QueryContext.php (6)
  • QueryContext (8-89)
  • add (60-68)
  • getCollections (32-35)
  • skipAuth (75-86)
  • addSkipAuth (70-73)
  • getCollectionByAlias (37-55)
src/Database/Adapter/SQL.php (4)
  • getIdAttributeType (1731-1734)
  • getDocument (353-419)
  • find (2328-2530)
  • getSupportForCasting (1434-1437)
src/Database/Adapter.php (5)
  • getIdAttributeType (905-905)
  • getDocument (691-691)
  • filter (1164-1173)
  • find (805-817)
  • getSupportForCasting (986-986)
🪛 PHPMD (2.15.0)
src/Database/Database.php

6665-6665: Avoid unused parameters such as '$selects'. (undefined)

(UnusedFormalParameter)

* @throws DatabaseException
*/
public function decode(Document $collection, Document $document, array $selections = []): Document
public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix undefined variable in decodeOriginal() and actually honor $selects.

$selections undefined => notices and wrong filtering; also resolves PHPMD UnusedFormalParameter.

-    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
+    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
     {
+        // Normalize selects -> attribute names
+        $selections = array_map(
+            fn ($q) => $q instanceof Query ? $q->getAttribute() : (string)$q,
+            $selects
+        );
@@
-            if (
-                empty($selections)
-                || \in_array($key, $selections)
-                || \in_array('*', $selections)
-            ) {
+            if (
+                empty($selections)
+                || \in_array($key, $selections, true)
+                || \in_array('*', $selections, true)
+            ) {
                 $document->setAttribute($key, ($array) ? $value : $value[0]);
             }

Also applies to: 6728-6735

🧰 Tools
🪛 PHPMD (2.15.0)

6665-6665: Avoid unused parameters such as '$selects'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
In src/Database/Database.php around lines 6665 and 6728-6735, the
decodeOriginal() implementation uses an undefined $selections variable and
ignores the $selects parameter, causing notices and incorrect filtering; replace
the undefined variable with the provided $selects and actually apply it when
building the returned Document: if $selects is empty return the original
Document unchanged, otherwise convert $selects to a lookup (e.g. array_flip) and
construct a new Document that only includes keys present in the lookup (or
remove keys not selected) so no undefined-variable notices occur and the method
honors the selection list; also remove any leftover unused-parameter
workarounds.

Comment on lines +6789 to +6792
if ($collection->isEmpty()) {
throw new \Exception('Invalid query: Unknown Alias context');
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use QueryException instead of generic \Exception for unknown alias context.

Aligns with error semantics and improves catchability.

@@
-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }
@@
-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }
@@
-        if ($collection->isEmpty()) {
-            throw new \Exception('Unknown Alias context');
-        }
+        if ($collection->isEmpty()) {
+            throw new QueryException('Unknown alias context in query.');
+        }

Also applies to: 6887-6889, 7135-7139

🤖 Prompt for AI Agents
In src/Database/Database.php around lines 6789-6792 (and likewise at 6887-6889
and 7135-7139), replace the thrown generic \Exception('Invalid query: Unknown
Alias context') with a QueryException so callers can catch DB-specific errors;
update the file to import or fully-qualify the QueryException class (e.g. add a
use statement or prefix with its namespace), and throw new QueryException with
the same message (and include any existing SQL/bindings/context parameters if
the QueryException constructor requires them).

@utopia-php utopia-php deleted a comment from coderabbitai bot Sep 8, 2025
@utopia-php utopia-php deleted a comment from coderabbitai bot Sep 8, 2025
@utopia-php utopia-php deleted a comment from coderabbitai bot Sep 8, 2025
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: 2

Caution

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

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

3401-3415: Strip auto-added system attributes on cache hit before returning

Currently only non-cached path removes $permissions; ensure consistency on cached reads as well.

Apply:

         if ($cached) {
             $document = new Document($cached);
@@
             if ($collection->getId() !== self::METADATA) {
                 if (!$validator->isValid([
                     ...$collection->getRead(),
                     ...($documentSecurity ? $document->getRead() : [])
                 ])) {
                     return new Document();
                 }
             }
+
+            if (isset($permissionsAdded) && $permissionsAdded) {
+                $document->removeAttribute('$permissions');
+            }
+            if (isset($idAdded) && $idAdded) {
+                $document->removeAttribute('$id');
+            }
+            if (isset($sequenceAdded) && $sequenceAdded) {
+                $document->removeAttribute('$sequence');
+            }
 
             $this->trigger(self::EVENT_DOCUMENT_READ, $document);
 
             return $document;
         }

6295-6359: find(): auto-select required system attributes and clean them from outputs when not requested

Guarantees update/delete batch checks, relationship population, and consistent behavior.

Apply:

-        $result = $this->processRelationshipQueries($relationships, $selects);
+        $result = $this->processRelationshipQueries($relationships, $selects);
         $selects = $result['queries'];
         $nestedSelections = $result['nestedSelections'];
+
+        // Ensure required system attributes are projected
+        [$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));
+        [$selects, $idAdded]         = Query::addSelect($selects, Query::select('$id',        system: true));
+        [$selects, $sequenceAdded]   = Query::addSelect($selects, Query::select('$sequence',  system: true));
@@
         $results = $this->adapter->find(
@@
             orderQueries: $orders
         );
@@
-            $node = $this->casting($context, $node, $selects);
-            $node = $this->decode($context, $node, $selects);
+            $node = $this->casting($context, $node, $selects);
+            $node = $this->decode($context, $node, $selects);
@@
             if (!$node->isEmpty()) {
                 $node->setAttribute('$collection', $collection->getId());
             }
+
+            // Strip auto-added attributes if caller didn't request them
+            if ($permissionsAdded) {
+                $node->removeAttribute('$permissions');
+            }
+            if ($idAdded) {
+                $node->removeAttribute('$id');
+            }
+            if ($sequenceAdded) {
+                $node->removeAttribute('$sequence');
+            }

6678-6751: Fix undefined $selections in decodeOriginal() and honor $selects

Currently uses an undefined variable, causing notices and incorrect filtering.

Apply:

-    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
+    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
     {
+        // Normalize selects -> attribute names
+        $selections = array_map(
+            fn ($q) => $q instanceof Query ? $q->getAttribute() : (string)$q,
+            $selects
+        );
@@
-            if (
-                empty($selections)
-                || \in_array($key, $selections)
-                || \in_array('*', $selections)
-            ) {
+            if (
+                empty($selections)
+                || \in_array($key, $selections, true)
+                || \in_array('*', $selections, true)
+            ) {
                 $document->setAttribute($key, ($array) ? $value : $value[0]);
             }

1-7463: Remove all debug prints (var_dump/echo) from Database.php

  • src/Database/Database.php:1523 → var_dump($collection->getAttribute('attributes'));
  • src/Database/Database.php:3611–3613 → var_dump($relationships);, var_dump($side);, var_dump($document);
  • src/Database/Database.php:6363–6367 → echo PHP_EOL…; var_dump('populateDocumentRelationships');, var_dump($collection->getId());, var_dump($nestedSelections);
  • src/Database/Database.php:7176 → var_dump($values);
♻️ Duplicate comments (5)
src/Database/Database.php (5)

1523-1523: Remove debug var_dump from deleteCollection()

This will corrupt responses and leak internals.

Apply:

-        var_dump($collection->getAttribute('attributes'));
+        // no-op

3611-3616: Remove stray debug output in relationship fetch

Leaking internals and breaking responses.

Apply:

-                    var_dump($relationships);
-                    var_dump($side);
-                    var_dump($document);
-                    /**
-                     * How to force $document->getId() , not to be empty?
-                     */
+                    // no-op

6363-6366: Remove stray echo/var_dump in find() loop

Pollutes output/logs.

Apply:

-                echo PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL.PHP_EOL;
-                var_dump('populateDocumentRelationships');
-                var_dump($collection->getId());
-                var_dump($nestedSelections);
+                // no-op

6801-6805: Use QueryException instead of generic \Exception for unknown alias contexts

Aligns with DB error semantics and improves catchability.

Apply:

-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }
-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }
-        if ($collection->isEmpty()) {
-            throw new \Exception('Unknown Alias context');
-        }
+        if ($collection->isEmpty()) {
+            throw new QueryException('Unknown alias context in query.');
+        }

Also applies to: 6901-6902, 7150-7153


7174-7178: Remove stray var_dump in convertQuery()

Leaks internals and breaks responses.

Apply:

-                var_dump($values);
                 foreach ($values as $valueIndex => $value) {
🧹 Nitpick comments (5)
src/Database/Database.php (5)

6321-6322: Avoid empty orderBy; use a deterministic unique order

Empty attribute in orderAsc() is ambiguous. Prefer ordering by $id (or $sequence if that’s your canonical unique).

Apply (if acceptable):

-            $orders[] = Query::orderAsc(); // In joins we should not add a default order, we should validate when using a cursor we should have a unique order
+            $orders[] = Query::orderAsc('$id'); // Ensures deterministic ordering for cursor pagination

If you rely on $sequence for uniqueness, use Query::orderAsc('$sequence') instead.


7223-7251: Cache key hashing for selects: good improvement

Hashing by serialize($selects) differentiates projections. Works as long as Query serialization is deterministic.

If you hit cache misses due to non-deterministic object serialization, consider hashing a normalized tuple like [$alias,$attribute,$as,$function,$system].


7292-7292: Remove unused local variable

$count is computed but never used.

Apply:

-        $count = \count($queries);
+        // no-op

(or remove the line entirely)


6361-6379: Position of casting/decode is correct; minor after-cleanup suggested

After stripping auto-added system attributes (see earlier comment), the rest of the loop is fine.


6250-6360: Optional: remove duplicated grouping logic

You compute groupByType earlier, then re-derive filters/selects/limit/offset/orders again. Prefer one source to avoid drift.

Apply:

-        $filters = Query::getFilterQueries($queries);
-        $selects = Query::getSelectQueries($queries);
-        $limit = Query::getLimitQuery($queries, 25);
-        $offset = Query::getOffsetQuery($queries, 0);
-        $orders = Query::getOrderQueries($queries);
+        // use values from $grouped calculated above
📜 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 d0d0e10 and 679c278.

📒 Files selected for processing (2)
  • src/Database/Database.php (53 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Database.php (6)
src/Database/Validator/Queries/V2.php (2)
  • V2 (23-696)
  • isValid (472-695)
src/Database/Query.php (16)
  • getAttribute (224-227)
  • Query (8-1426)
  • getSelectQueries (958-963)
  • addSelect (1392-1425)
  • select (710-713)
  • getJoinQueries (969-976)
  • join (891-894)
  • getCollection (266-269)
  • getAlias (246-249)
  • getFilterQueries (1087-1090)
  • limit (743-746)
  • getLimitQuery (998-1007)
  • offset (754-757)
  • getOffsetQuery (1029-1038)
  • getOrderQueries (1044-1050)
  • orderAsc (732-735)
src/Database/Document.php (6)
  • getAttribute (224-231)
  • getId (63-66)
  • removeAttribute (287-293)
  • Document (12-470)
  • getCollection (85-88)
  • find (304-322)
src/Database/QueryContext.php (6)
  • QueryContext (8-89)
  • add (60-68)
  • getCollections (32-35)
  • skipAuth (75-86)
  • addSkipAuth (70-73)
  • getCollectionByAlias (37-55)
src/Database/Adapter.php (7)
  • getIdAttributeType (905-905)
  • getMaxDateTime (912-915)
  • getDocument (691-691)
  • filter (1164-1173)
  • find (805-817)
  • getSupportForCasting (986-986)
  • count (840-840)
src/Database/Adapter/SQL.php (5)
  • getIdAttributeType (1731-1734)
  • getDocument (353-419)
  • find (2328-2530)
  • getSupportForCasting (1434-1437)
  • count (2542-2604)
🪛 PHPMD (2.15.0)
src/Database/Database.php

6678-6678: Avoid unused parameters such as '$selects'. (undefined)

(UnusedFormalParameter)


7292-7292: Avoid unused local variables such as '$count'. (undefined)

(UnusedLocalVariable)

🔇 Additional comments (6)
src/Database/Database.php (6)

32-32: LGTM: V2 validator alias import

Importing Queries\V2 as DocumentsValidator is correct and aligns with the new context-aware validation.


3480-3483: Relationship population requires $id; avoid brittle throws by ensuring upstream selects include it

Given the upstream fix to auto-select $id, this guard should never trip in normal flows. Keeping it is fine, but if you see this tripping in tests, the issue is missing $id in the adapter projection.

Do you want me to add an assertion in tests to verify $id presence whenever nested relationship selections exist?


4625-4633: Batch update requires $permissions and $sequence — ensure find() always selects them

This throws if callers didn’t request these attributes. The find() fix below auto-adds them and removes from outputs when not explicitly requested.


6250-6272: Join context initialization looks good

Adding joined collections to QueryContext and auth flags is appropriate.


6484-6495: Count/Sum: context-aware conversion + validation looks correct

Use of convertQueries with QueryContext and V2 validator is consistent.

Also applies to: 6539-6573


7174-7189: DateTime value normalization in convertQuery(): good, but avoid side-effects

Once var_dump is removed (above), this block is solid.

Comment on lines 3375 to 3391
$selects = Query::getSelectQueries($queries);
[$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));

//$selects = $this->validateSelections($collection, $selects);

$result = $this->processRelationshipQueries($relationships, $selects);
$selects = $result['queries'];
$nestedSelections = $result['nestedSelections'];

$validator = new Authorization(self::PERMISSION_READ);
$documentSecurity = $collection->getAttribute('documentSecurity', false);

[$collectionKey, $documentKey, $hashKey] = $this->getCacheKeys(
$collection->getId(),
$id,
$selections
$selects
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Always include system selects needed for relationships, caching, and batch ops

Populate + caching paths require $id/$sequence/$permissions even if callers didn’t select them. Add them here and track what was auto-added.

Apply:

         $selects = Query::getSelectQueries($queries);
-        [$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions',  system: true));
+        [$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));
+        [$selects, $idAdded]         = Query::addSelect($selects, Query::select('$id',        system: true));
+        [$selects, $sequenceAdded]   = Query::addSelect($selects, Query::select('$sequence',  system: true));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$selects = Query::getSelectQueries($queries);
[$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));
//$selects = $this->validateSelections($collection, $selects);
$result = $this->processRelationshipQueries($relationships, $selects);
$selects = $result['queries'];
$nestedSelections = $result['nestedSelections'];
$validator = new Authorization(self::PERMISSION_READ);
$documentSecurity = $collection->getAttribute('documentSecurity', false);
[$collectionKey, $documentKey, $hashKey] = $this->getCacheKeys(
$collection->getId(),
$id,
$selections
$selects
);
$selects = Query::getSelectQueries($queries);
[$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));
[$selects, $idAdded] = Query::addSelect($selects, Query::select('$id', system: true));
[$selects, $sequenceAdded] = Query::addSelect($selects, Query::select('$sequence', system: true));
//$selects = $this->validateSelections($collection, $selects);
$result = $this->processRelationshipQueries($relationships, $selects);
$selects = $result['queries'];
$nestedSelections = $result['nestedSelections'];
$validator = new Authorization(self::PERMISSION_READ);
$documentSecurity = $collection->getAttribute('documentSecurity', false);
[$collectionKey, $documentKey, $hashKey] = $this->getCacheKeys(
$collection->getId(),
$id,
$selects
);
🤖 Prompt for AI Agents
In src/Database/Database.php around lines 3375-3391, ensure the code always
injects system selects for $id, $sequence and $permissions (system: true) into
$selects even when callers didn't request them, and record which selects were
auto-added; call Query::addSelect for each required system field, merge the
returned selects and a flag/list of auto-added fields into
$selects/$permissionsAdded before passing $selects into
processRelationshipQueries and getCacheKeys, so caching, relationship processing
and batch/populate flows have $id/$sequence/$permissions available and you can
later strip or account for auto-added fields when forming the final response.

Comment on lines 3462 to 3465
if($permissionsAdded){
$document->removeAttribute('$permissions');
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Also remove auto-added $id and $sequence on non-cached path

Mirror the cache-hit behavior.

Apply:

-        if($permissionsAdded){
-            $document->removeAttribute('$permissions');
-        }
+        if ($permissionsAdded) {
+            $document->removeAttribute('$permissions');
+        }
+        if (isset($idAdded) && $idAdded) {
+            $document->removeAttribute('$id');
+        }
+        if (isset($sequenceAdded) && $sequenceAdded) {
+            $document->removeAttribute('$sequence');
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if($permissionsAdded){
$document->removeAttribute('$permissions');
}
if ($permissionsAdded) {
$document->removeAttribute('$permissions');
}
if (isset($idAdded) && $idAdded) {
$document->removeAttribute('$id');
}
if (isset($sequenceAdded) && $sequenceAdded) {
$document->removeAttribute('$sequence');
}
🤖 Prompt for AI Agents
In src/Database/Database.php around lines 3462-3465, the non-cached response
path currently only removes the auto-added $permissions attribute; update it to
mirror the cache-hit behavior by also removing the auto-added $id and $sequence
attributes when permissionsAdded is true (or the same condition used for
$permissions removal). Ensure you call removeAttribute for '$id' and '$sequence'
in the same block so the non-cached output matches the cached output.

@utopia-php utopia-php deleted a comment from coderabbitai bot Sep 8, 2025
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: 4

Caution

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

⚠️ Outside diff range comments (2)
tests/e2e/Adapter/Scopes/DocumentTests.php (1)

5145-5146: Replace array-based Query::select calls with string arguments
All Query::select([...]) usages must be updated to pass a string (or separate calls) per the new signature. Update the following:

  • tests/e2e/Adapter/Scopes/DocumentTests.php:5145
  • tests/e2e/Adapter/Scopes/SpatialTests.php:1521, 1526, 1531
src/Database/Database.php (1)

6665-6735: Fix undefined $selections in decodeOriginal() and honor $selects.

Prevents notices and ensures proper filtering.

-    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
+    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
     {
+        // Normalize selects to attribute names (supports Query instances or raw strings)
+        $selections = array_map(
+            fn ($q) => $q instanceof Query ? $q->getAttribute() : (string)$q,
+            $selects
+        );
@@
-            if (
-                empty($selections)
-                || \in_array($key, $selections)
-                || \in_array('*', $selections)
-            ) {
+            if (
+                empty($selections)
+                || \in_array($key, $selections, true)
+                || \in_array('*', $selections, true)
+            ) {
                 $document->setAttribute($key, ($array) ? $value : $value[0]);
             }
♻️ Duplicate comments (4)
tests/e2e/Adapter/Scopes/RelationshipTests.php (2)

322-326: Reorder selects: placing “*” first swallows later specific selects (duplicate).

Known issue: Query::addSelect blocks merging when a prior select is “”, so subsequent “votes.”/“votes.animals.*” may be ignored. Put most-specific first to make tests robust.

Apply:

@@
-            Query::select('*'),
-            Query::select('votes.*'),
+            Query::select('votes.*'),
+            Query::select('*'),
@@
-            Query::select('*'),
-            Query::select('votes.*'),
-            Query::select('votes.animals.*'),
+            Query::select('votes.animals.*'),
+            Query::select('votes.*'),
+            Query::select('*'),

Also applies to: 334-338


1466-1470: Refactor addSelect merge logic to allow specific selects after “*”
In src/Database/Query.php ::addSelect (around line 1397), change:

-    if ($q->getAttribute() === '*'){
-        $merge = false;
-    }
+    if ($q->getAttribute() === '*' && $query->getAttribute() === '*'){
+        $merge = false;
+    }

This blocks only duplicate “*” entries while permitting child-specific selects regardless of order, so tests no longer need to swap their Query::select calls.

src/Database/Database.php (2)

6789-6792: Use QueryException instead of generic \Exception for unknown alias.

Keeps error semantics consistent and improves catchability.

-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }
-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }
-        if ($collection->isEmpty()) {
-            throw new \Exception('Unknown Alias context');
-        }
+        if ($collection->isEmpty()) {
+            throw new QueryException('Unknown alias context in query.');
+        }

Also applies to: 6887-6890, 7135-7139


7162-7164: Remove stray var_dump() in convertQuery().

Leaks data and corrupts responses.

-                $values = $query->getValues();
-                var_dump($values);
+                $values = $query->getValues();
🧹 Nitpick comments (4)
tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (1)

111-113: Prefer using the existing $database instance for consistency

Avoid mixing static::getDatabase() and the local $database within the same test.

-        $documents = static::getDatabase()->find('playlist', [
+        $documents = $database->find('playlist', [
             Query::select('name'),
             Query::limit(1)
         ]);
tests/e2e/Adapter/Scopes/DocumentTests.php (1)

3532-3536: Use the local $database for consistency

Minor style nit to avoid mixing static::getDatabase() amid an existing $database.

-        $documents = static::getDatabase()->find('movies', [
+        $documents = $database->find('movies', [
             Query::select('name'),
             Query::select('year'),
             Query::select('$createdAt')
         ]);
src/Database/Database.php (2)

6291-6305: Remove duplicate grouping code after convertQueries().

You compute groupByType and then re-derive filters/limits below; keep one path to avoid drift.

-        $grouped = Query::groupByType($queries);
-        $filters = $grouped['filters'];
-        $selects = $grouped['selections'];
-        $limit = $grouped['limit'];
-        $offset = $grouped['offset'];
-        $orderAttributes = $grouped['orderAttributes'];
-        $orderTypes = $grouped['orderTypes'];
-        $cursor = $grouped['cursor'];
-        $cursorDirection = $grouped['cursorDirection'] ?? Database::CURSOR_AFTER;
-
-        $filters = Query::getFilterQueries($queries);
-        $selects = Query::getSelectQueries($queries);
-        $limit = Query::getLimitQuery($queries, 25);
-        $offset = Query::getOffsetQuery($queries, 0);
-        $orders = Query::getOrderQueries($queries);
+        $grouped = Query::groupByType($queries);
+        $filters         = $grouped['filters'];
+        $selects         = $grouped['selections'];
+        $limit           = $grouped['limit'] ?? 25;
+        $offset          = $grouped['offset'] ?? 0;
+        $orders          = $grouped['orders'] ?? Query::getOrderQueries($queries);
+        $cursor          = $grouped['cursor'];
+        $cursorDirection = $grouped['cursorDirection'] ?? Database::CURSOR_AFTER;

7341-7344: Drop unused $idAdded in processRelationshipQueries().

Cleans up static analysis warning without behavior change.

-        if (\count($queries) > 0) {
-            [$queries, $idAdded] = Query::addSelect($queries, Query::select('$id',  system: true));
-        }
+        if (\count($queries) > 0) {
+            [$queries] = Query::addSelect($queries, Query::select('$id', system: true));
+        }
📜 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 679c278 and 7085327.

📒 Files selected for processing (5)
  • src/Database/Database.php (51 hunks)
  • tests/e2e/Adapter/Base.php (2 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (16 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (22 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/Adapter/Base.php
🧰 Additional context used
🧬 Code graph analysis (4)
src/Database/Database.php (4)
src/Database/Validator/Queries/V2.php (2)
  • V2 (23-696)
  • isEmpty (155-166)
src/Database/QueryContext.php (6)
  • QueryContext (8-89)
  • add (60-68)
  • getCollections (32-35)
  • skipAuth (75-86)
  • addSkipAuth (70-73)
  • getCollectionByAlias (37-55)
src/Database/Adapter.php (7)
  • getIdAttributeType (905-905)
  • getDocument (691-691)
  • filter (1164-1173)
  • find (805-817)
  • sum (829-829)
  • getSupportForCasting (986-986)
  • count (840-840)
src/Database/Query.php (16)
  • Query (8-1426)
  • getSelectQueries (958-963)
  • addSelect (1392-1425)
  • select (710-713)
  • getAttribute (224-227)
  • join (891-894)
  • getCollection (266-269)
  • getAlias (246-249)
  • getFilterQueries (1087-1090)
  • limit (743-746)
  • getLimitQuery (998-1007)
  • offset (754-757)
  • getOrderQueries (1044-1050)
  • getAs (261-264)
  • getMethod (216-219)
  • getValue (241-244)
tests/e2e/Adapter/Scopes/DocumentTests.php (3)
src/Database/Database.php (3)
  • Database (35-7448)
  • getDocument (3334-3467)
  • decode (6749-6832)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Query.php (2)
  • Query (8-1426)
  • select (710-713)
tests/e2e/Adapter/Scopes/RelationshipTests.php (3)
src/Database/Query.php (2)
  • Query (8-1426)
  • select (710-713)
src/Database/Database.php (1)
  • getDocument (3334-3467)
src/Database/Adapter/SQL.php (1)
  • getDocument (353-419)
tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (3)
src/Database/Database.php (1)
  • getDatabase (687-690)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (38-38)
src/Database/Query.php (3)
  • Query (8-1426)
  • select (710-713)
  • getAttribute (224-227)
🪛 PHPMD (2.15.0)
src/Database/Database.php

6665-6665: Avoid unused parameters such as '$selects'. (undefined)

(UnusedFormalParameter)


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

(UnusedLocalVariable)

🔇 Additional comments (30)
tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (6)

142-144: Per-field select refactor: OK

Using multiple Query::select(...) entries aligns with the new API.


154-156: Per-field select refactor: OK

Consistent with new selection semantics.


529-531: Per-field select refactor: OK

Matches new API for nested selections.


541-543: Per-field select refactor: OK


1588-1590: Per-field select refactor: OK


1693-1696: Per-field select refactor: OK

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

21-21: Import QueryContext: OK

Required for the new decode signature.


1308-1310: Per-field select refactor: OK


1323-1326: Updated internal-attribute expectations: OK

Asserting absence of $sequence/$createdAt/$updatedAt/$permissions matches new exposure rules.


1329-1332: Explicit $id selection: OK


1335-1339: Internal attributes gating: OK


3453-3456: Per-field select in find: OK


3465-3470: Internal attributes: assertions match new behavior


3473-3476: Selecting $id explicitly: OK


3485-3490: Assertions for internal fields: OK


3493-3496: Selecting $sequence explicitly: OK


3513-3516: Selecting $collection explicitly: OK


3545-3550: Assertions with $createdAt: OK


3553-3556: Selecting $updatedAt explicitly: OK


3565-3570: Assertions with $updatedAt: OK


3572-3576: Selecting $permissions explicitly: OK


3585-3590: Assertions with $permissions: OK


2929-2929: Per-field select inside nested OR query: OK


3940-3944: Switch to context-aware decode: OK

Constructing QueryContext and calling Database::decode($context, ...) matches the new API.


4678-4685: Short-selects in bulk delete: OK

Selecting internals for callbacks is aligned with the new projection model.


4887-4893: Short-selects with callback error path: OK


4910-4917: Short-selects + pagination in delete: OK

src/Database/Database.php (3)

6308-6315: Verify default order invocation.

Query::orderAsc() without an attribute may be invalid or non-unique for cursor use. If unsupported, default to '$id'.

-            $orders[] = Query::orderAsc(); // In joins we should not add a default order, we should validate when using a cursor we should have a unique order
+            $orders[] = Query::orderAsc('$id'); // ensure deterministic ordering

Would you like me to adjust based on the actual Query API?


3478-3480: Good guard: require $id when populating relationships.

Prevents ambiguous lookups and unexpected cascades.


7212-7239: Cache hash improvement LGTM.

Switching to md5(serialize($selects)) correctly disambiguates select variants.

Comment on lines +3374 to 3381
$selects = Query::getSelectQueries($queries);

//$selects = $this->validateSelections($collection, $selects);

[$selects, $nestedSelections] = $this->processRelationshipQueries($relationships, $selects);

[$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Always include and later strip system selects ($permissions, $id, $sequence) in getDocument().

Needed for auth checks, relationship population, and cache hashing; avoid leaking them to callers when auto-added.

-        [$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions',  system: true));
+        [$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));
+        [$selects, $idAdded]         = Query::addSelect($selects, Query::select('$id',       system: true));
+        [$selects, $sequenceAdded]   = Query::addSelect($selects, Query::select('$sequence', system: true));
         if ($cached) {
             $document = new Document($cached);
@@
-            $this->trigger(self::EVENT_DOCUMENT_READ, $document);
-            return $document;
+            // Strip auto-added system attributes before returning
+            if (!empty($permissionsAdded)) { $document->removeAttribute('$permissions'); }
+            if (!empty($idAdded))         { $document->removeAttribute('$id'); }
+            if (!empty($sequenceAdded))   { $document->removeAttribute('$sequence'); }
+
+            $this->trigger(self::EVENT_DOCUMENT_READ, $document);
+            return $document;
         }
-        if($permissionsAdded){ // Or remove all queries added by system
-            $document->removeAttribute('$permissions');
-        }
+        // Remove auto-added system attributes on non-cached path
+        if (!empty($permissionsAdded)) { $document->removeAttribute('$permissions'); }
+        if (!empty($idAdded))         { $document->removeAttribute('$id'); }
+        if (!empty($sequenceAdded))   { $document->removeAttribute('$sequence'); }

Also applies to: 3398-3413, 3459-3463

Comment on lines +6242 to +6265
$context = new QueryContext();
$context->add($collection);

$joins = Query::getJoinQueries($queries);

foreach ($joins as $join) {
$context->add(
$this->silent(fn () => $this->getCollection($join->getCollection())),
$join->getAlias()
);
}

$authorization = new Authorization($forPermission);

foreach ($context->getCollections() as $_collection) {
$documentSecurity = $_collection->getAttribute('documentSecurity', false);
$skipAuth = $authorization->isValid($_collection->getPermissionsByType($forPermission));

if (!$skipAuth && !$documentSecurity && $_collection->getId() !== self::METADATA) {
throw new AuthorizationException($authorization->getDescription());
}

$context->addSkipAuth($this->adapter->filter($_collection->getId()), $forPermission, $skipAuth);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure update/delete batch flows receive required system fields via find().

updateDocuments/deleteDocuments require $permissions and $sequence; inject them when forPermission != read. Avoid exposing them to read callers.

-        $queries = self::convertQueries($context, $queries);
+        $queries = self::convertQueries($context, $queries);
@@
-        $filters = Query::getFilterQueries($queries);
-        $selects = Query::getSelectQueries($queries);
+        $filters = Query::getFilterQueries($queries);
+        $selects = Query::getSelectQueries($queries);
+        // Auto-include system selects for non-read operations
+        $permissionsAdded = $idAdded = $sequenceAdded = false;
+        if ($forPermission !== Database::PERMISSION_READ) {
+            [$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));
+            [$selects, $idAdded]         = Query::addSelect($selects, Query::select('$id',         system: true));
+            [$selects, $sequenceAdded]   = Query::addSelect($selects, Query::select('$sequence',   system: true));
+        }
@@
-            $node = $this->casting($context, $node, $selects);
-            $node = $this->decode($context, $node, $selects);
+            $node = $this->casting($context, $node, $selects);
+            $node = $this->decode($context, $node, $selects);
+            // Hide auto-added attributes on read responses
+            if ($forPermission === Database::PERMISSION_READ) {
+                if (!empty($permissionsAdded)) { $node->removeAttribute('$permissions'); }
+                if (!empty($idAdded))         { $node->removeAttribute('$id'); }
+                if (!empty($sequenceAdded))   { $node->removeAttribute('$sequence'); }
+            }

Also applies to: 6336-6350, 6357-6359

throw new Exception('Playlist not found');
}

var_dump($playlist);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove stray debug output

var_dump in tests is noisy and may break CI output.

-var_dump($playlist);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var_dump($playlist);
🤖 Prompt for AI Agents
In tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php around line 149,
there is a stray debugging statement var_dump($playlist) that produces noisy
output and can break CI; remove that var_dump call and, if needed, replace it
with an appropriate assertion or logging via the test framework (e.g.,
assertNotNull/assertEquals) so the test remains silent on success and only
reports failures.

Comment on lines +1508 to +1513
$make = $database->getDocument('make', 'ford', [
Query::select('models.*'),
]);
var_dump($make);
if ($make->isEmpty()) {
throw new Exception('Make not found');
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove debug output from tests (and adapter).

var_dump in tests will pollute PHPUnit output; drop it. Also remove var_dump($sql) from SQL adapter.

@@
-        $make = $database->getDocument('make', 'ford', [
-            Query::select('models.*'),
-        ]);
-var_dump($make);
+        $make = $database->getDocument('make', 'ford', [
+            Query::select('models.*'),
+        ]);

And in src/Database/Adapter/SQL.php:

@@
-  var_dump($sql);
+  // debug print removed
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$make = $database->getDocument('make', 'ford', [
Query::select('models.*'),
]);
var_dump($make);
if ($make->isEmpty()) {
throw new Exception('Make not found');
// File: tests/e2e/Adapter/Scopes/RelationshipTests.php
$make = $database->getDocument('make', 'ford', [
Query::select('models.*'),
]);
if ($make->isEmpty()) {
throw new Exception('Make not found');
Suggested change
$make = $database->getDocument('make', 'ford', [
Query::select('models.*'),
]);
var_dump($make);
if ($make->isEmpty()) {
throw new Exception('Make not found');
// File: src/Database/Adapter/SQL.php
// debug print removed
🤖 Prompt for AI Agents
In tests/e2e/Adapter/Scopes/RelationshipTests.php around lines 1508–1513 and in
src/Database/Adapter/SQL.php (search for var_dump($sql)), there are stray
var_dump debug calls; remove the var_dump($make) in the test and any
var_dump($sql) in the SQL adapter so PHPUnit output is not polluted, replace
with proper logging if needed, and run the test suite to ensure no leftover
debug statements remain.

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

Caution

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

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

6666-6736: Fix undefined $selections and honor $selects in decodeOriginal().

$selections is undefined; $selects is ignored. Causes notices and wrong filtering.

Apply:

-    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
+    public function decodeOriginal(Document $collection, Document $document, array $selects = []): Document
     {
+        // Normalize selects -> attribute names
+        $selections = array_map(
+            fn ($q) => $q instanceof Query ? $q->getAttribute() : (string)$q,
+            $selects
+        );
@@
-            if (
-                empty($selections)
-                || \in_array($key, $selections)
-                || \in_array('*', $selections)
-            ) {
+            if (
+                empty($selections)
+                || \in_array($key, $selections, true)
+                || \in_array('*', $selections, true)
+            ) {
                 $document->setAttribute($key, ($array) ? $value : $value[0]);
             }
♻️ Duplicate comments (3)
src/Database/Database.php (3)

7136-7176: Remove stray var_dump and throw QueryException in convertQuery().

var_dump($values) leaks data and breaks responses; generic Exception harms error typing.

-        $collection = clone $context->getCollectionByAlias($query->getAlias());
+        $collection = clone $context->getCollectionByAlias($query->getAlias());
         if ($collection->isEmpty()) {
-            throw new \Exception('Unknown Alias context');
+            throw new QueryException('Unknown alias context in query.');
         }
@@
             if ($attribute->getAttribute('type') == Database::VAR_DATETIME) {
                 $values = $query->getValues();
-                var_dump($values);
                 foreach ($values as $valueIndex => $value) {
                     try {
                         $values[$valueIndex] = DateTime::setTimezone($value);

6789-6793: Throw QueryException instead of generic Exception (decode).
Aligns with DB/query error semantics and improves catchability.

-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }

6887-6890: Throw QueryException instead of generic Exception (casting).
Same issue as decode().

-            if ($collection->isEmpty()) {
-                throw new \Exception('Invalid query: Unknown Alias context');
-            }
+            if ($collection->isEmpty()) {
+                throw new QueryException('Unknown alias context in query.');
+            }
🧹 Nitpick comments (6)
tests/e2e/Adapter/Base.php (1)

11-11: Unused import (JoinsTests) if the trait remains disabled

If you don’t enable the trait, drop this import to keep the file clean.

-use Tests\E2E\Adapter\Scopes\JoinsTests;
+// use Tests\E2E\Adapter\Scopes\JoinsTests;

(Prefer enabling the trait per the previous comment.)

src/Database/Database.php (5)

6291-6306: Avoid recomputing grouped pieces; use one source of truth.

You compute via groupByType, then recompute filters/selects/limit/offset/orders again. Simplify to use the grouped result only.

-        $grouped = Query::groupByType($queries);
-        $filters = $grouped['filters'];
-        $selects = $grouped['selections'];
-        $limit = $grouped['limit'];
-        $offset = $grouped['offset'];
-        $orderAttributes = $grouped['orderAttributes'];
-        $orderTypes = $grouped['orderTypes'];
-        $cursor = $grouped['cursor'];
-        $cursorDirection = $grouped['cursorDirection'] ?? Database::CURSOR_AFTER;
-
-        $filters = Query::getFilterQueries($queries);
-        $selects = Query::getSelectQueries($queries);
-        $limit = Query::getLimitQuery($queries, 25);
-        $offset = Query::getOffsetQuery($queries, 0);
-        $orders = Query::getOrderQueries($queries);
+        $grouped = Query::groupByType($queries);
+        $filters = $grouped['filters'];
+        $selects = $grouped['selections'];
+        $limit = $grouped['limit'] ?? 25;
+        $offset = $grouped['offset'] ?? 0;
+        $orders = $grouped['orders'] ?? Query::getOrderQueries($queries);
+        $cursor = $grouped['cursor'];
+        $cursorDirection = $grouped['cursorDirection'] ?? Database::CURSOR_AFTER;

7339-7346: Silence unused $idAdded from addSelect().

PHPMD warning (unused local).

-        if (\count($queries) > 0) {
-            [$queries, $idAdded] = Query::addSelect($queries, Query::select('$id',  system: true));
-        }
+        if (\count($queries) > 0) {
+            [$queries, $idAdded] = Query::addSelect($queries, Query::select('$id', system: true));
+            unset($idAdded);
+        }

6330-6331: Nit: error text.
“Cursor document must be from the same collection.” (lowercase “collection”).

-            throw new DatabaseException("cursor Document must be from the same Collection.");
+            throw new DatabaseException('Cursor document must be from the same collection.');

7030-7079: Dead code: validateSelections() isn’t used.

Either remove or mark @deprecated to avoid drift.


6309-6316: Specify a unique attribute in default orderAsc call
Query::orderAsc() defaults to an empty attribute (''), which can produce an invalid sort order for cursor queries. Pass '$id' to guarantee a unique sort key.

-        if ($uniqueOrderBy === false) {
-            $orders[] = Query::orderAsc(); // In joins we should not add a default order, we should validate when using a cursor we should have a unique order
-        }
+        if ($uniqueOrderBy === false) {
+            $orders[] = Query::orderAsc('$id');
+        }
📜 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 7085327 and 39d6689.

📒 Files selected for processing (2)
  • src/Database/Database.php (51 hunks)
  • tests/e2e/Adapter/Base.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Database.php (6)
src/Database/Validator/Queries/V2.php (3)
  • V2 (23-696)
  • isEmpty (155-166)
  • isValid (472-695)
src/Database/QueryContext.php (6)
  • QueryContext (8-89)
  • add (60-68)
  • getCollections (32-35)
  • skipAuth (75-86)
  • addSkipAuth (70-73)
  • getCollectionByAlias (37-55)
src/Database/Adapter/SQL.php (6)
  • getIdAttributeType (1731-1734)
  • getDocument (353-419)
  • find (2328-2530)
  • sum (2617-2680)
  • getSupportForCasting (1434-1437)
  • count (2542-2604)
src/Database/Adapter.php (9)
  • getIdAttributeType (905-905)
  • getMinDateTime (898-898)
  • getMaxDateTime (912-915)
  • getDocument (691-691)
  • filter (1164-1173)
  • find (805-817)
  • sum (829-829)
  • getSupportForCasting (986-986)
  • count (840-840)
src/Database/Query.php (17)
  • Query (8-1426)
  • getSelectQueries (958-963)
  • addSelect (1392-1425)
  • select (710-713)
  • getAttribute (224-227)
  • getJoinQueries (969-976)
  • join (891-894)
  • getCollection (266-269)
  • getFilterQueries (1087-1090)
  • limit (743-746)
  • getLimitQuery (998-1007)
  • offset (754-757)
  • getOffsetQuery (1029-1038)
  • getOrderQueries (1044-1050)
  • orderAsc (732-735)
  • isNested (1194-1201)
  • isJoin (1206-1215)
src/Database/Document.php (8)
  • getAttribute (224-231)
  • getId (63-66)
  • isEmpty (396-399)
  • removeAttribute (287-293)
  • Document (12-470)
  • getCollection (85-88)
  • find (304-322)
  • getArrayCopy (423-458)
🪛 PHPMD (2.15.0)
src/Database/Database.php

6666-6666: Avoid unused parameters such as '$selects'. (undefined)

(UnusedFormalParameter)


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

(UnusedLocalVariable)

🔇 Additional comments (2)
src/Database/Database.php (2)

3375-3383: Always include and later strip system selects ($permissions, $id, $sequence) in getDocument().

Needed for auth checks, relationship population, and cache hashing; avoid leaking them when auto-added.
[ suggest_essential_refactor ]

         $selects = Query::getSelectQueries($queries);
@@
-        [$selects, $nestedSelections] = $this->processRelationshipQueries($relationships, $selects);
-        [$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions',  system: true));
+        [$selects, $nestedSelections] = $this->processRelationshipQueries($relationships, $selects);
+        [$selects, $permissionsAdded] = Query::addSelect($selects, Query::select('$permissions', system: true));
+        [$selects, $idAdded]         = Query::addSelect($selects, Query::select('$id',        system: true));
+        [$selects, $sequenceAdded]   = Query::addSelect($selects, Query::select('$sequence',  system: true));
@@
         if ($cached) {
             $document = new Document($cached);
@@
-            $this->trigger(self::EVENT_DOCUMENT_READ, $document);
-            return $document;
+            // Strip auto-added system attributes before returning
+            if (!empty($permissionsAdded)) { $document->removeAttribute('$permissions'); }
+            if (!empty($idAdded))         { $document->removeAttribute('$id'); }
+            if (!empty($sequenceAdded))   { $document->removeAttribute('$sequence'); }
+            $this->trigger(self::EVENT_DOCUMENT_READ, $document);
+            return $document;
         }
@@
-        if($permissionsAdded){ // Or remove all queries added by system
-            $document->removeAttribute('$permissions');
-        }
+        // Remove auto-added system attributes on non-cached path
+        if (!empty($permissionsAdded)) { $document->removeAttribute('$permissions'); }
+        if (!empty($idAdded))         { $document->removeAttribute('$id'); }
+        if (!empty($sequenceAdded))   { $document->removeAttribute('$sequence'); }

Also applies to: 3400-3414, 3461-3463


3830-3834: LGTM: context-aware casting/decode after mutations.

Good consistency using QueryContext in create/createDocuments/upsert paths.

Also applies to: 3935-3937, 5330-5331

Comment on lines +6335 to 6351
//$selects = $this->validateSelections($collection, $selects);

$selections = $this->validateSelections($collection, $selects);
$nestedSelections = $this->processRelationshipQueries($relationships, $queries);
[$selects, $nestedSelections] = $this->processRelationshipQueries($relationships, $selects);

$getResults = fn () => $this->adapter->find(
$collection,
$results = $this->adapter->find(
$context,
$queries,
$limit ?? 25,
$offset ?? 0,
$orderAttributes,
$orderTypes,
$cursor,
$cursorDirection,
$forPermission
$forPermission,
selects: $selects,
filters: $filters,
joins: $joins,
orderQueries: $orders
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Inject required system selects for non-read flows (find → update/delete batches).

updateDocuments/deleteDocuments require $permissions and $sequence but find() doesn’t auto-select them, causing immediate exceptions. Add them when $forPermission !== read.

-        [$selects, $nestedSelections] = $this->processRelationshipQueries($relationships, $selects);
+        [$selects, $nestedSelections] = $this->processRelationshipQueries($relationships, $selects);
+        // Ensure batch ops receive required system fields
+        if ($forPermission !== Database::PERMISSION_READ) {
+            [$selects, $pAdded] = Query::addSelect($selects, Query::select('$permissions', system: true)); unset($pAdded);
+            [$selects, $iAdded] = Query::addSelect($selects, Query::select('$id',         system: true)); unset($iAdded);
+            [$selects, $sAdded] = Query::addSelect($selects, Query::select('$sequence',   system: true)); unset($sAdded);
+        }

This change satisfies the required-attributes checks at:

  • updateDocuments: Lines 4622-4626,
  • deleteDocuments: Lines 6101-6106.

Also applies to: 4619-4626, 6101-6106

🤖 Prompt for AI Agents
In src/Database/Database.php around lines 6335 to 6351, find() is called with
$selects produced by processRelationshipQueries but does not include required
system fields used by non-read flows; when $forPermission !== 'read' inject the
system selects (at minimum 'permissions' and 'sequence') into the $selects array
before calling adapter->find so updateDocuments/deleteDocuments will have those
attributes available; ensure you preserve any existing selects and
nestedSelections and only add these system selects when not already present.

Comment on lines +7164 to 7165
var_dump($values);
foreach ($values as $valueIndex => $value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Scan for remaining debug output in repo.

To ensure no stdout leaks remain.


🏁 Script executed:

#!/bin/bash
rg -nP "var_dump\s*\(|print_r\s*\(|echo\s+[^\$]" -g '!**/vendor/**' -C2

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Search for any remaining debug output in all PHP files (excluding vendor)
rg -nP "var_dump\s*\(|print_r\s*\(|echo\s+[^\$]" -C2 --type php

Length of output: 5804


Remove all debug output statements
Debug calls (var_dump, print_r, raw echo) remain across source and test files (e.g., src/Database/Database.php:7164, Validator/Queries/V2.php, Adapter/SQL.php, bin scripts, tests). Remove them to prevent unintended stdout leaks.

🤖 Prompt for AI Agents
In src/Database/Database.php around lines 7164-7165, remove the debug output
call var_dump($values) (and any adjacent raw echo/print_r) so no debugging data
is written to stdout; if structured debug is required replace it with a proper
logger call (e.g., $this->logger->debug(...)) or remove entirely, and then grep
the repository for var_dump, print_r, echo usages in source and test files and
remove/convert them to proper logging where appropriate to prevent unintended
stdout leaks.

Comment on lines +22 to +28
// use JoinsTests;
// use CollectionTests;
// use DocumentTests;
// use AttributeTests;
// use IndexTests;
// use PermissionTests;
// use RelationshipTests;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Join e2e tests are not running — trait usage is commented out

JoinsTests is imported but disabled here, so the new join suite won’t execute in CI. Enable it at least, even if other suites remain temporarily disabled.

Apply:

-//    use JoinsTests;
+    use JoinsTests;

Optional: add a TODO above the remaining commented traits with an issue link and expected re-enable date to avoid silently losing coverage.

To verify current state and test discovery:


🏁 Script executed:

#!/bin/bash
# Show commented trait usage in Base
rg -n '^\s*//\s*use\s+.*Tests;' tests/e2e/Adapter/Base.php -n -C0
# Ensure the JoinsTests trait exists and has tests
rg -n 'trait\s+JoinsTests' tests/e2e/Adapter/Scopes/JoinsTests.php
rg -nP 'function\s+test\w+\s*\(' tests/e2e/Adapter/Scopes/JoinsTests.php

Length of output: 258


Enable Joins e2e tests

  • Uncomment use JoinsTests; in tests/e2e/Adapter/Base.php so the join suite runs in CI.
  • Optional: add a TODO above the other commented traits with an issue link and target re-enable date to avoid losing coverage.
-//    use JoinsTests;
+    use JoinsTests;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// use JoinsTests;
// use CollectionTests;
// use DocumentTests;
// use AttributeTests;
// use IndexTests;
// use PermissionTests;
// use RelationshipTests;
use JoinsTests;
// use CollectionTests;
// use DocumentTests;
// use AttributeTests;
// use IndexTests;
// use PermissionTests;
// use RelationshipTests;
🤖 Prompt for AI Agents
In tests/e2e/Adapter/Base.php around lines 22 to 28, the Joins e2e tests are
disabled; uncomment the line "use JoinsTests;" so the join test suite runs in
CI, and (optionally) add a single-line TODO comment above the remaining
commented trait uses with a link to the tracking issue and a target re-enable
date to preserve coverage context.

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)
tests/e2e/Adapter/Scopes/SpatialTests.php (1)

231-232: Re-enable cleanup to avoid leaking collections between tests.

Commented deletion can cause “collection already exists” on subsequent runs and cross-test flakiness.

-            // $database->deleteCollection($collectionName);
+            $database->deleteCollection($collectionName);
🧹 Nitpick comments (3)
tests/e2e/Adapter/Scopes/SpatialTests.php (3)

167-169: Normalize class casing: use Query:: not query::

Class names should be consistently cased (PSR-1/PSR-12). While PHP resolves class names case-insensitively, keeping the canonical casing avoids surprises.

-                'equals' => query::equal('lineAttr', [[[1.0, 2.0], [3.0, 4.0]]]), // Exact same linestring
+                'equals' => Query::equal('lineAttr', [[[1.0, 2.0], [3.0, 4.0]]]), // Exact same linestring
-                'notEquals' => query::notEqual('lineAttr', [[[5.0, 6.0], [7.0, 8.0]]]), // Different linestring
+                'notEquals' => Query::notEqual('lineAttr', [[[5.0, 6.0], [7.0, 8.0]]]), // Different linestring
-                'equals' => query::equal('polyAttr', [[[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [0.0, 0.0]]]]), // Exact same polygon
+                'equals' => Query::equal('polyAttr', [[[[0.0, 0.0], [0.0, 10.0], [10.0, 10.0], [0.0, 0.0]]]]), // Exact same polygon
-                'notEquals' => query::notEqual('polyAttr', [[[[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [20.0, 20.0]]]]), // Different polygon
+                'notEquals' => Query::notEqual('polyAttr', [[[[20.0, 20.0], [20.0, 30.0], [30.0, 30.0], [20.0, 20.0]]]]), // Different polygon
-            $differentSquare = $database->find($collectionName, [
-                query::notEqual('square', [[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]]) // Different square
+            $differentSquare = $database->find($collectionName, [
+                Query::notEqual('square', [[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]]) // Different square
             ], Database::PERMISSION_READ);

Also applies to: 203-204, 1068-1069


1712-1712: Fix test name typo: testSptialAggregation → testSpatialAggregation

Improves readability and discoverability.

-    public function testSptialAggregation(): void
+    public function testSpatialAggregation(): void

89-96: Also strip global debug prints from the runtime (SQL adapter).

The adapter currently var_dump’s built SQL (see src/Database/Adapter/SQL.php in this PR). That will spam CI logs when these tests run. Please remove or gate behind a debug flag/env.

📜 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 39d6689 and 2ba2962.

📒 Files selected for processing (1)
  • tests/e2e/Adapter/Scopes/SpatialTests.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/e2e/Adapter/Scopes/SpatialTests.php (2)
src/Database/Adapter/SQL.php (1)
  • find (2328-2530)
src/Database/Database.php (1)
  • find (6235-6371)

Comment on lines +141 to +156
$result = $database->find($collectionName);

function decodePoint(string $wkb): array {
// WKB format: 1-byte byte order + 4-byte type + 8-byte X + 8-byte Y
// Skip first 5 bytes (1 byte byte order + 4 bytes type)
$coords = unpack('dX/dY', substr($wkb, 5, 16));
return ['lon' => $coords['X'], 'lat' => $coords['Y']];
}

// Example usage:
$pointBinary = "\000\000\000\000\000\000\000\000\000\000\000\000\000@\000\000\000\000\000\000@";
$coords = decodePoint($pointBinary);
print_r($coords);

var_dump($result);
$this->assertEquals(999,888);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove debug scaffolding and the deliberate failing assertion.

Unconditional find/printouts and the hardcoded failing assertion will halt the suite (stop-on-failure) and pollute output. The nested named function can also trigger redeclaration if this test runs more than once.

Apply:

-            $result = $database->find($collectionName);
-
-            function decodePoint(string $wkb): array {
-                // WKB format: 1-byte byte order + 4-byte type + 8-byte X + 8-byte Y
-                // Skip first 5 bytes (1 byte byte order + 4 bytes type)
-                $coords = unpack('dX/dY', substr($wkb, 5, 16));
-                return ['lon' => $coords['X'], 'lat' => $coords['Y']];
-            }
-
-// Example usage:
-            $pointBinary = "\000\000\000\000\000\000\000\000\000\000\000\000\000@\000\000\000\000\000\000@";
-            $coords = decodePoint($pointBinary);
-            print_r($coords);
-
-            var_dump($result);
-            $this->assertEquals(999,888);
+            // Debug scaffolding removed.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$result = $database->find($collectionName);
function decodePoint(string $wkb): array {
// WKB format: 1-byte byte order + 4-byte type + 8-byte X + 8-byte Y
// Skip first 5 bytes (1 byte byte order + 4 bytes type)
$coords = unpack('dX/dY', substr($wkb, 5, 16));
return ['lon' => $coords['X'], 'lat' => $coords['Y']];
}
// Example usage:
$pointBinary = "\000\000\000\000\000\000\000\000\000\000\000\000\000@\000\000\000\000\000\000@";
$coords = decodePoint($pointBinary);
print_r($coords);
var_dump($result);
$this->assertEquals(999,888);
// Debug scaffolding removed.
🤖 Prompt for AI Agents
In tests/e2e/Adapter/Scopes/SpatialTests.php around lines 141 to 156, remove the
temporary debug scaffolding: delete the nested named function decodePoint (or
convert it to a private helper method on the test class or an anonymous closure
to avoid redeclaration), remove the example WKB usage, print_r and var_dump
calls, and remove the hardcoded failing assertion
($this->assertEquals(999,888)); instead, either assert the actual expected
values from $result or omit the ad-hoc check until a proper assertion is
implemented so the test suite won’t be polluted or halted.

@fogelito fogelito closed this Oct 12, 2025
@fogelito fogelito deleted the joins3 branch October 12, 2025 07:37
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.

2 participants