Skip to content

Conversation

@fogelito
Copy link
Contributor

@fogelito fogelito commented Oct 28, 2024

Summary by CodeRabbit

  • New Features

    • Introduced advanced support for SQL-like join operations, aliasing, and relation queries in database queries.
    • Added a new query context abstraction for managing collections, aliases, and permission checks within queries.
    • Implemented new validators for query structure, aliasing, and schema-based validation.
    • Enhanced query selection and projection with support for aliasing and internal attributes.
  • Bug Fixes

    • Improved error messages for query validation and offset handling.
  • Refactor

    • Standardized query construction and validation to use single-field select calls and a unified query context.
    • Replaced legacy validators with a new, context-aware validation system.
    • Updated document processing and decoding to utilize the new query context and selection logic.
  • Tests

    • Added comprehensive end-to-end and unit tests for join queries, aliasing, and advanced query validation.
    • Refactored existing tests to align with new query construction and validation approaches.
  • Chores

    • Commented out deprecated or replaced validator classes and related tests to streamline the codebase.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 8, 2025

Walkthrough

This update introduces advanced join and aliasing support to the Utopia Database abstraction, refactors query validation to use a new QueryContext class, and overhauls how queries are constructed, validated, and executed. Numerous legacy validator classes are commented out in favor of a new schema-aware validator. Test suites are updated to reflect the new query construction and validation approach, with extensive join and aliasing tests added.

Changes

File(s) Change Summary
phpunit.xml Changed stopOnFailure to true to halt tests on first failure.
src/Database/Adapter.php Refactored find and getAttributeProjection signatures to use QueryContext and richer query objects; old methods commented out.
src/Database/Adapter/MariaDB.php, src/Database/Adapter/Postgres.php, src/Database/Adapter/Pool.php, src/Database/Adapter/SQL.php Refactored query handling for joins, aliasing, and context-aware validation; updated internal SQL generation, attribute projection, and debugging output.
src/Database/Database.php Overhauled document and query processing to use QueryContext; replaced old validators; improved selection, join, and permission handling.
src/Database/Query.php Added join, alias, and relation query support; introduced query type filtering and new static factory methods; enhanced query utilities.
src/Database/QueryContext.php New: Implements a context class for managing collections, aliases, and permission skipping in queries.
src/Database/Validator/Alias.php, src/Database/Validator/AsQuery.php New: Validators for alias and "as" query parameters, enforcing naming rules.
src/Database/Validator/Queries/V2.php New: Comprehensive validator for queries using QueryContext, enforcing schema, type, and logical constraints.
src/Database/Validator/Query/Base.php Added METHOD_TYPE_JOIN constant for join query categorization.
src/Database/Validator/Query/Offset.php Corrected error message for offset validation.
src/Database/Validator/IndexedQueries.php, src/Database/Validator/Queries.php, src/Database/Validator/Queries/Document.php, src/Database/Validator/Queries/Documents.php, src/Database/Validator/Query/Filter.php, src/Database/Validator/Query/Order.php, src/Database/Validator/Query/Select.php Entire classes commented out, disabling legacy validators.
tests/e2e/Adapter/Base.php Added join tests trait to base E2E test class (imported but not yet activated).
tests/e2e/Adapter/Scopes/JoinsTests.php New: Comprehensive E2E trait for join and aliasing tests.
tests/e2e/Adapter/Scopes/*.php Updated all select queries to use single-field Query::select() calls; adjusted assertions for new metadata expectations.
tests/unit/QueryTest.php Added join query tests; updated to use new factory methods and alias checks.
tests/unit/Validator/* Refactored all validator tests to use QueryContext and new DocumentsValidator; updated assertions and test logic accordingly.
tests/unit/Validator/QueriesTest.php Entire test class commented out, disabling legacy validator tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Database
    participant Adapter
    participant QueryContext
    participant Validator

    Client->>Database: find(collection, queries, forPermission)
    Database->>QueryContext: new QueryContext()
    Database->>QueryContext: add(collection, alias)
    Database->>Validator: validate(queries, context)
    Validator-->>Database: validation result
    Database->>Adapter: find(context, queries, ..., selects, filters, joins, orderQueries)
    Adapter->>QueryContext: getCollectionByAlias(alias)
    Adapter->>Adapter: Build SQL with joins/aliases
    Adapter-->>Database: array of Document
    Database-->>Client: array of Document
Loading

Possibly related PRs

  • Fix cursor #615: Both PRs modify the find method in MariaDB and Postgres adapters to enhance cursor pagination logic with multi-attribute ordering and improved cursor WHERE clauses.
  • Default alias getDocument #606: Related changes to getAttributeProjection method signatures in Adapter and subclasses, though this PR introduces more extensive incompatible signature changes.

Suggested reviewers

  • abnegate

Poem

🐇✨
Joins and aliases hop in with glee,
Contexts and queries, now schema-aware be!
Old validators nap, new ones stand tall,
With single selects and joins, we answer the call.
Tests leap ahead, the suite’s in full bloom—
Database rabbits, there’s plenty of room!
🥕

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

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

172-178: ⚠️ Potential issue

Remove stray var_dump($document) debug statement

Raw debug output will leak internal schema details in production logs and can break JSON / CLI consumers.

-            $document = $stmt->fetchAll();
-            $stmt->closeCursor();
-            var_dump($document);
+            $document = $stmt->fetchAll();
+            $stmt->closeCursor();

1644-1691: 🛠️ Refactor suggestion

Dead code: getAttributeProjection_original() should be deleted

The new projection logic supersedes this method, yet the legacy implementation remains, increasing maintenance cost and binary size. If kept for reference, move it behind a // @deprecated comment or drop entirely.

-    protected function getAttributeProjection_original(array $selections, string $prefix = ''): mixed
-    {
-        …  // 250+ lines
-    }
+    // Legacy V1 projection kept only for git history – safe to delete.
src/Database/Adapter/MariaDB.php (1)

2075-2088: ⚠️ Potential issue

OR queries joined with AND – logical bug

Inside getSQLCondition, for Query::TYPE_OR you concatenate sub-conditions with ' AND '; this flips semantics. Use OR when method is OR.

-                $method = strtoupper($query->getMethod());
-                return empty($conditions) ? '' : ' '. $method .' (' . implode(' AND ', $conditions) . ')';
+                $method = strtoupper($query->getMethod());
+                $glue   = $method === 'OR' ? ' OR ' : ' AND ';
+                return empty($conditions) ? '' : " {$method} (" . implode($glue, $conditions) . ')';
♻️ Duplicate comments (1)
tests/e2e/Adapter/Scopes/JoinsTests.php (1)

417-418: Second stray var_dump()

Same issue as above – remove to keep CI output clean.

🧹 Nitpick comments (20)
src/Database/Validator/Alias.php (1)

26-45: Consider consistency with AsQuery validator.

Both Alias and AsQuery validators have identical validation logic but AsQuery has additional handling for the "*" case. Consider if this validation should be unified or if the differences are intentional.

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

62-62: Consider verifying the error message is set correctly.

The error description is being checked before the validation is performed. This might return a stale or default error message.

 public function testValueFailure(): void
 {
-    $this->assertEquals('Invalid query', $this->validator->getDescription());
     $this->assertFalse($this->validator->isValid([Query::select('name.artist')]));
+    $this->assertEquals('Invalid query', $this->validator->getDescription());
 }
tests/unit/Validator/IndexedQueriesTest.php (1)

39-76: Consider reducing redundant QueryContext initialization.

Each test method creates a new QueryContext and adds the same collection, even when the collection hasn't been modified. This could be optimized to reduce redundancy.

Consider creating a helper method to reduce duplication:

+    /**
+     * Create a fresh QueryContext with the collection
+     */
+    protected function createContext(Document $collection = null): QueryContext
+    {
+        $context = new QueryContext();
+        $context->add($collection ?? $this->collection);
+        return $context;
+    }

     public function testEmptyQueries(): void
     {
-        $context = new QueryContext();
-        $context->add($this->collection);
-
-        $validator = new DocumentsValidator($context);
+        $validator = new DocumentsValidator($this->createContext());

         $this->assertEquals(true, $validator->isValid([]));
     }

Apply similar changes to other test methods for cleaner code.

src/Database/QueryContext.php (2)

37-55: Consider optimizing collection lookup performance.

The current linear search through collections could be inefficient with many collections. Consider maintaining a direct mapping from alias to Document reference for O(1) lookup.

 class QueryContext
 {
     /**
      * @var array<Document>
      */
     protected array $collections = [];

     /**
-     * @var array<string>
+     * @var array<string, string>
      */
     protected array $aliases = [];

+    /**
+     * @var array<string, Document>
+     */
+    protected array $aliasToDocument = [];

     public function getCollectionByAlias(string $alias): Document
     {
-        /**
-         * $alias can be an empty string
-         */
-        $collectionId = $this->aliases[$alias] ?? null;
-
-        if (is_null($collectionId)) {
-            return new Document();
-        }
-
-        foreach ($this->collections as $collection) {
-            if ($collection->getId() === $collectionId) {
-                return $collection;
-            }
-        }
-
-        return new Document();
+        return $this->aliasToDocument[$alias] ?? new Document();
     }

57-68: Good duplicate alias prevention, but consider additional validation.

The method correctly prevents alias collisions and provides clear error messages.

Consider validating that the collection ID is unique within the context to prevent having the same collection added multiple times with different aliases:

 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().'".');
     }

+    // Prevent adding the same collection multiple times
+    foreach ($this->collections as $existingCollection) {
+        if ($existingCollection->getId() === $collection->getId()) {
+            throw new QueryException('Collection "'.$collection->getId().'" already exists in context.');
+        }
+    }

     $this->collections[] = $collection;
     $this->aliases[$alias] = $collection->getId();
 }
src/Database/Adapter/Pool.php (1)

460-463: Missing @param annotation causes loss of generics

getAttributeProjection() overrides the abstract definition but drops the @param array<Query> $selects annotation that exists in the parent. Please add it to preserve static-analysis accuracy.

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

44-46: Remove commented-out dead code

//Authorization::setRole('user:bob'); looks like a local debug artefact. Delete or convert to an explanatory comment to avoid confusion.


484-559: Large block of commented code – delete or move to dedicated test

The 70-line commented section adds noise and risks divergence with real tests. Keeping historical experiments in VCS is redundant.

src/Database/Adapter.php (2)

745-777: Signature inflation – consider consolidating parameters

The abstract find() now requires 10+ parameters, several of which overlap ($queries vs $selects|$filters|$joins|$orderQueries). Call-sites will quickly become error-prone. Two possible approaches:

  1. Accept only QueryContext $context plus a single array<Query> (preferred – simpler).
  2. Wrap the optional lists into a small DTO/struct to avoid a “parameter train”.

Either way, document the expected precedence between $queries and the specialised arrays if both are supplied.


1081-1085: Visibility & doc discrepancy

getAttributeProjection() is declared protected here but Pool keeps the same visibility – good. However, the docblock was reduced to two lines; please restore the @param array<Query> and @return string annotations for consistency with concrete adapters.

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

121-149: Prefer assertTrue / assertFalse over assertEquals for boolean expectations

Using the generic assertEquals(true, …) / assertEquals(false, …) obscures intent and produces less helpful diff output when a test fails. The PHPUnit-idiomatic helpers make the expected type explicit and generate clearer messages.

- $this->assertEquals(true, $validator->isValid([...]));
+ $this->assertTrue($validator->isValid([...]));

Apply the same replacement everywhere boolean equality is asserted.


156-176: Redundant validator instantiation inside each test

$this->context is prepared in setUp(), yet every test re-creates new DocumentsValidator($this->context). That adds ~3 ms per test and duplicates code.

- $validator = new DocumentsValidator($this->context);
+ $validator = $this->validator; // initialise once in setUp()

Consider defining $this->validator in setUp() and re-using it across tests.

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

79-86: Large limit() / offset() values may exceed adapter caps

The success section treats limit(5000) and offset(5000) as universally valid. Several adapters cap limit to 1000 and offset to 10000 (see Query::MAX_LIMIT). If those caps change, this test will break silently.

Either:

  1. Replace hard-coded numbers with Query::MAX_LIMIT / a derived constant, or
  2. Add an explanatory comment stating the chosen values are deliberate.
src/Database/Adapter/SQL.php (1)

1558-1580: addHiddenAttribute() prepends a leading comma – might yield SELECT *, ,

When getAttributeProjection() is invoked with no explicit selects it returns
alias.* concatenated with addHiddenAttribute() which already starts with , .
Result: alias.* , alias._uid … (note the space-comma sequence). While syntactically valid, the extra whitespace/comma style diverges from the rest of the SQL builder.

Minor but easy to normalise:

-        return ', '.implode(', ', $strings);
+        return implode(', ', $strings);

and in getAttributeProjection():

-            return Query::DEFAULT_ALIAS.'.*'.$this->addHiddenAttribute($selects);
+            return Query::DEFAULT_ALIAS.'.*, '.$this->addHiddenAttribute($selects);
src/Database/Validator/Queries/V2.php (1)

413-424: Alias validation runs on empty strings

$query->getRightAlias() can be empty for non-relation queries, yet AliasValidator will still be invoked and fail. Guard with !== ''.

-        if (! $validator->isValid($query->getRightAlias())) {
+        if ($query->getRightAlias() !== '' && ! $validator->isValid($query->getRightAlias())) {
src/Database/Adapter/MariaDB.php (1)

1998-1999: Duplicate assignment to $alias

Redundant line adds cognitive noise and could hide copy-paste errors later.

-        $alias = Query::DEFAULT_ALIAS;
src/Database/Adapter/Postgres.php (1)

1825-1827: Duplicate assignment of $alias

$alias = Query::DEFAULT_ALIAS; appears twice back-to-back – harmless but noisy.

-        $alias = Query::DEFAULT_ALIAS;
-        $alias = Query::DEFAULT_ALIAS;
+        $alias = Query::DEFAULT_ALIAS;
src/Database/Query.php (3)

590-593: Unused parameter $function in select() — dead code / false API surface

$function is accepted but never referenced. Either wire it into the Query (e.g. aggregate functions) or drop it to avoid confusion.

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

265-276: Throw domain-specific exception instead of generic \Exception

getCursorDirection() / getOrderDirection() currently throw \Exception.
Using QueryException (already imported) provides clearer context and consistent error handling.

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

Same change for getOrderDirection().

Also applies to: 278-289


900-907: Doc-block & type inconsistency in getCursorDocument()

The PHPDoc states @param Query $query while the signature allows ?Query. Update the doc-block for accuracy.
Also, importing Utopia\Database\Document at the top would make the return type explicit in IDEs.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72b2e2c and 16c822e.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (40)
  • phpunit.xml (1 hunks)
  • src/Database/Adapter.php (3 hunks)
  • src/Database/Adapter/MariaDB.php (10 hunks)
  • src/Database/Adapter/Pool.php (3 hunks)
  • src/Database/Adapter/Postgres.php (15 hunks)
  • src/Database/Adapter/SQL.php (6 hunks)
  • src/Database/Database.php (43 hunks)
  • src/Database/Query.php (22 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 (15 hunks)
  • tests/e2e/Adapter/Scopes/JoinsTests.php (1 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (11 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 (2 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 (4)
tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php (1)
src/Database/Query.php (2)
  • Query (8-1116)
  • select (590-593)
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 (22-649)
  • isValid (123-321)
  • getDescription (328-331)
src/Database/Query.php (3)
  • Query (8-1116)
  • select (590-593)
  • limit (623-626)
tests/e2e/Adapter/Scopes/RelationshipTests.php (1)
src/Database/Query.php (2)
  • Query (8-1116)
  • select (590-593)
tests/e2e/Adapter/Scopes/DocumentTests.php (4)
src/Database/Database.php (3)
  • Database (32-6690)
  • getDocument (3000-3204)
  • decode (6264-6345)
src/Database/QueryContext.php (2)
  • QueryContext (8-89)
  • add (60-68)
src/Database/Query.php (2)
  • Query (8-1116)
  • select (590-593)
src/Database/Adapter/SQL.php (1)
  • getDocument (214-280)
🔇 Additional comments (65)
phpunit.xml (1)

10-10: Consider the impact on CI/CD pipelines.

Changing stopOnFailure to true will halt test execution on the first failure, which may be helpful during development but could negatively impact CI/CD workflows where seeing all test failures is valuable for comprehensive debugging.

Is this change intentional for the development phase, or should it remain false for production CI/CD pipelines?

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

42-42: LGTM! Error message consistency fix.

Good catch fixing the error message from "Invalid limit" to "Invalid offset" to match the actual validation context.

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

15-15: LGTM! Proper constant addition for join support.

The new METHOD_TYPE_JOIN constant follows the established naming pattern and supports the new join functionality being introduced.

tests/e2e/Adapter/Base.php (1)

11-11:

✅ Verification successful

LGTM! Proper integration of join tests.

The JoinsTests trait is correctly imported and used, following the established pattern for test scopes.

Verify that the JoinsTests trait exists and doesn't have method conflicts with other traits:

Also applies to: 21-21


🏁 Script executed:

#!/bin/bash
# Description: Verify JoinsTests trait exists and check for potential method conflicts

# Check if JoinsTests trait exists
fd "JoinsTests.php" tests/

# Look for method definitions in JoinsTests to check for conflicts
ast-grep --pattern 'trait JoinsTests {
  $$$
  public function $METHOD($_) {
    $$$
  }
  $$$
}'

# Check for potential method conflicts with other traits
rg -A 3 "public function test" tests/e2e/Adapter/Scopes/ | head -20

Length of output: 1830


🏁 Script executed:

#!/bin/bash
# List all test methods in JoinsTests trait
echo "=== Methods in JoinsTests ==="
rg --no-filename --only-matching "public function test[^(]+" tests/e2e/Adapter/Scopes/JoinsTests.php | sed 's/public function //'

# Find any duplicate test method names across all scope traits
echo
echo "=== Duplicate test methods across all Scopes traits ==="
rg --no-filename --only-matching "public function test[^(]+" tests/e2e/Adapter/Scopes/*.php \
  | sed 's/public function //' \
  | sort \
  | uniq -d || echo "None found"

Length of output: 793


✅ JoinsTests Trait Validated

The JoinsTests trait was found in tests/e2e/Adapter/Scopes/JoinsTests.php and defines a single method:

  • testJoin

A scan across all scope traits shows no duplicate test method names.

No further action required.

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

3-80:

✅ Verification successful

Ensure test coverage is maintained during validator migration.

The entire QueriesTest class has been commented out, which raises concerns about test coverage during the migration to the new validator system.

Verify that:

  1. The test scenarios covered by this file are adequately tested in the new validator tests
  2. No regression in functionality occurs due to missing test coverage

Consider either:

  • Updating these tests to work with the new validator system, or
  • Removing them if equivalent coverage exists elsewhere

🏁 Script executed:

#!/bin/bash
# Description: Check if equivalent test coverage exists for the new validator system
# Expected: Find tests for V2 validator or DocumentsValidator covering similar scenarios

echo "Looking for V2 validator tests..."
rg -A 5 "class.*Test.*V2|V2.*Test" --type php

echo ""
echo "Looking for DocumentsValidator tests..."
rg -A 5 "DocumentsValidator" tests/ --type php

echo ""
echo "Checking test methods that might replace QueriesTest scenarios..."
rg -A 3 "testEmptyQueries|testInvalidMethod|testInvalidValue|testValid" tests/ --type php

Length of output: 19120


Remove legacy QueriesTest.php—coverage confirmed in V2 tests

All scenarios from the old QueriesTest are now covered by the V2 validator suite:

  • IndexedQueriesTest: implements testEmptyQueries, testInvalidMethod, testInvalidValue, and testValid
  • DocumentQueriesTest & DocumentsQueriesTest: validate select, limit, order, search, and error conditions
  • QueryTest, FilterTest, OrderTest, SelectTest: cover individual query methods (equal, greaterThan, between, etc.) against schema contexts

Since no gaps remain in test coverage, please delete or remove the commented-out tests/unit/Validator/QueriesTest.php to prevent dead code accumulation.

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

263-263: LGTM: Consistent API standardization.

Another consistent application of the Query::select API change from array to string parameter.


1601-1603: Good documentation of behavioral context.

The comment clarifies that the ConvertQueries method will handle date format normalization, which provides useful context for understanding why various date formats are tested without expecting failures.


222-222:

✅ Verification successful

LGTM: Query API standardization.

The change from Query::select(['dots.name']) to Query::select('dots.name') aligns with the broader API standardization mentioned in the summary, where Query::select() now accepts single string arguments instead of arrays.

Verify that this API change is consistently applied across all test files:


🏁 Script executed:

#!/bin/bash
# Description: Check for any remaining array-style Query::select calls that might need updating
# Expected: Find any inconsistent usage patterns

echo "Searching for remaining array-style Query::select calls..."
rg "Query::select\(\[" --type php

echo ""
echo "Checking for mixed usage patterns..."
rg -A 2 -B 2 "Query::select" tests/ --type php | grep -E "(Query::select\(\[|Query::select\('[^']*'\))"

Length of output: 11619


Approved: Consistent Query::select usage verified

A global search confirms there are no remaining array-style calls to Query::select([…]); all instances now use the single-string signature. LGTM.

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

169-169: LGTM: Query selection syntax updated correctly.

The change from array-based selection to single string parameter aligns with the new query handling approach.


179-180: LGTM: Multiple select calls replace array syntax.

The pattern of using separate Query::select() calls for each field instead of passing an array is consistent with the enhanced query context and aliasing support mentioned in the summary.


191-193: LGTM: Consistent application of new select syntax.

The multiple Query::select() calls properly replace the previous array-based approach, maintaining the same functionality while supporting the new query validation approach.


202-202: LGTM: Single field selection updated correctly.

The transition to single string parameter for Query::select() is applied consistently.


208-208: LGTM: Wildcard selection syntax updated.

The change maintains the wildcard selection functionality with the new single-parameter approach.


213-213: LGTM: Relationship field selection updated.

The dot-notation field selection (library.*) works correctly with the new single string parameter format.


658-659: LGTM: Two-way relationship test updated consistently.

The same pattern of separate Query::select() calls is correctly applied to the two-way relationship tests.


670-671: LGTM: Final select syntax updates are consistent.

All Query::select() calls throughout the test file follow the new single-parameter pattern consistently.

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

107-110: Align Query::select usage with new API
Replaced the array‐based select(['name']) with a single-field Query::select('name') invocation to match the updated QueryContext method signature.


136-141: Split selection of root and nested fields into separate calls
Converted the deprecated array form (['*', 'songs.name']) into two discrete calls: Query::select('*') and Query::select('songs.name'), ensuring compatibility with the new query builder.


149-153: Apply single-field select chain to getDocument
Updated the getDocument call to use individual Query::select('*') and Query::select('songs.name'), aligning with the refactored query API.


521-525: Adopt separate select invocations in two-way findOne test
Replaced the multi-field array argument with separate Query::select('*') and Query::select('classes.name') calls to comply with the new signature.


533-537: Update getDocument in two-way test to new select syntax
Switched from an array argument to individual Query::select('*') and Query::select('classes.name') calls to reflect the revised query builder design.

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

116-116: LGTM! The query construction pattern is consistently updated.

The change from array-based to single-field Query::select() calls aligns with the new query construction standard introduced in this PR.


915-916: LGTM! The pattern is consistently applied across different query scenarios.

The updates correctly handle:

  • Simple field selection (line 915)
  • Wildcard selection (line 922)
  • Complex nested selections with wildcards (lines 929-931)

This demonstrates proper support for various selection patterns in the new query system.

Also applies to: 922-923, 929-931

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

1-96:

❓ Verification inconclusive

Verify that the new DocumentsValidator covers all validation scenarios previously handled by this class.

The entire Select validator class has been commented out as part of the migration to the new DocumentsValidator system. While this aligns with the PR's refactoring goals, ensure that:

  1. All validation logic from this class (internal attributes, schema validation, relationship handling) is properly implemented in the new validator
  2. The special handling for dot notation in attribute names (lines 67-76) is preserved
  3. The wildcard * selection support (line 83) is maintained

Let me verify that the new validator handles all the cases from the old Select validator:


🏁 Script executed:

#!/bin/bash
# Description: Check if the new DocumentsValidator handles all validation scenarios from the old Select validator

# Find the new DocumentsValidator implementation
fd -t f "DocumentsValidator|V2\.php" src/Database/Validator/

# Check if it handles internal attributes validation
rg -A 5 "INTERNAL_ATTRIBUTES|internalId|createdAt|updatedAt|permissions" src/Database/Validator/

# Check if it handles dot notation for relationships
rg -A 5 "str_contains.*\." src/Database/Validator/

# Check if it handles wildcard selection
rg -A 5 "\*|wildcard|TYPE_SELECT" src/Database/Validator/

Length of output: 71920


Please confirm wildcard '*' handling in the new Queries/V2 validator

I’ve verified that:

  • Internal attributes ($internalId, $createdAt, $updatedAt, $permissions, $collection) are seeded in src/Database/Validator/Queries/V2.php and special-cased (if ($attributeId === '$permissions' || $attributeId === '$collection') return;).
  • Dot-notation support is preserved via two str_contains($attribute, '.') checks and a call to validateAttributeExist($attribute, $alias).

However, I did not find an explicit check for the wildcard '*' selection (equivalent to the old

if ($attribute === '*') { … }

block). Please verify that V2 still allows select('*') or has alternative wildcard logic in:

  • src/Database/Validator/Queries/V2.php (look for if ($attribute === '*') or similar)
src/Database/Validator/Query/Filter.php (1)

1-290:

✅ Verification successful

Ensure comprehensive migration of filter validation logic to the new system.

The entire Filter validator has been commented out. This class contained critical validation logic that must be preserved in the new system:

  1. Encrypted attribute filtering prevention (lines 44-49) - Security-critical check
  2. Array attribute query restrictions (lines 185-191) - Important data integrity check
  3. Relationship query validation (lines 146-172) - Complex business logic for different relation types
  4. Type-specific value validation (lines 108-138) - Ensures query values match attribute types
  5. Query method validation (lines 227-283) - Validates correct number of values for each query type

Let me verify that these critical validations are preserved in the new system:


🏁 Script executed:

#!/bin/bash
# Description: Verify that critical validation logic from Filter.php is preserved in the new validator

# Check if encrypted attribute filtering is prevented in the new validator
rg -B 2 -A 5 "encrypt|encrypted.*attribute|Cannot query encrypted" src/Database/

# Check if array attribute query restrictions are maintained
rg -B 2 -A 5 "array.*attribute.*query|Cannot query.*array" src/Database/

# Check if relationship query validation is preserved
rg -B 2 -A 5 "virtual relationship|relationType.*RELATION_" src/Database/

# Check if query method validation is maintained (e.g., BETWEEN requires 2 values)
rg -B 2 -A 5 "TYPE_BETWEEN.*exactly two|queries require.*value" src/Database/

# Look for the new validator implementation to understand how it handles these cases
fd -t f "V2\.php|DocumentsValidator" src/Database/Validator/ -x cat {}

Length of output: 47579


All critical Filter validations have been ported to V2

I’ve verified that the following checks from the old Filter.php are fully re-implemented in src/Database/Validator/Queries/V2.php:

  • Encrypted attribute filtering
    – In validateValues():
    if (Query::isFilter($method) && in_array('encrypt', $filters)) {
        throw new \Exception('Cannot query encrypted attribute: ' . $attributeId);
    }
  • Array vs. string “contains” restrictions
    – In validateValues():
    if (! $array && $method === Query::TYPE_CONTAINS && $attribute['type'] !== Database::VAR_STRING) {
        throw new \Exception('Invalid query: Cannot query contains on attribute "'.$attributeId.'" because it is not an array or string.');
    }
    if ($array && ! in_array($method, [Query::TYPE_CONTAINS, Query::TYPE_IS_NULL, Query::TYPE_IS_NOT_NULL])) {
        throw new \Exception('Invalid query: Cannot query '.$method.' on attribute "'.$attributeId.'" because it is an array.');
    }
  • Relationship query validation
    – In validateValues(): checks for each relationType/side combination and throws Cannot query on virtual relationship attribute
  • Type-specific value validation
    – In validateValues(): switch on Database::VAR_* creating Text, Integer, FloatValidator, Boolean, DatetimeValidator, etc., and throwing on Unknown Data type
  • Query method/value‐count validation
    – In isValid(): switch cases for each Query::TYPE_*, enforcing 1, 2, or ≥1 values as required, e.g.:
    case Query::TYPE_BETWEEN:
        if (count($query->getValues()) != 2) {
            throw new \Exception('Invalid query: Between queries require exactly two values.');
        }
        …

No further action required—all validations are present and consistent.

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

3-44: LGTM - Clean migration approach.

Commenting out the entire legacy Document validator class is a good approach during the migration to the new DocumentsValidator with QueryContext. This preserves the implementation for reference while disabling the functionality.

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

3-115: LGTM - Appropriate migration strategy.

Commenting out the entire legacy IndexedQueries validator is consistent with the broader refactoring effort. This preserves the complex index validation logic for reference during the migration to the new DocumentsValidator system.

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

13-16: Good practice: Added proper exception documentation.

The @throws Exception annotation properly documents the potential exception from the test method.


20-21: Improved type safety with Document objects.

The change from raw string values to Document objects with proper structure (['$id' => 'asb']) enhances type safety and aligns with the new query construction patterns. This ensures cursor queries work with properly structured document references.

tests/unit/QueryTest.php (3)

22-27: Verify alias functionality is properly tested.

The addition of alias parameters to Query::equal() and corresponding assertions for getAlias() looks correct and demonstrates the new aliasing capability.


167-172: Enhanced select query with named parameters.

The use of named parameters (alias: 'alias', as: 'as') and corresponding assertions for getAlias() and getAs() properly test the enhanced select functionality.


296-331: Comprehensive join functionality testing.

The new testJoins() method thoroughly tests join operations including:

  • Join creation with collection and alias
  • Relation equality queries with aliases
  • Proper assertion of join properties and nested query structures

The test correctly verifies the join method type, collection, alias, and validates the nested relation and equality queries.

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

144-145: Consistent migration to single-attribute select queries.

The change from Query::select(['date', 'movie.date']) to separate Query::select('date') and Query::select('movie.date') calls aligns with the enhanced Query class design that supports aliasing and more sophisticated query construction.


176-177: Proper adaptation to new Query API.

The migration maintains the same functionality while adapting to the new Query class interface that expects single string attributes rather than arrays.


555-556: Consistent pattern across relationship tests.

The same migration pattern is consistently applied across different relationship test scenarios, ensuring uniform adoption of the new Query API.

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

11-12: LGTM! Clean migration to the new validation architecture.

The transition from raw arrays to using QueryContext with the new DocumentsValidator aligns well with the broader refactoring across the codebase.


104-109: LGTM! Proper initialization of QueryContext.

The collection is correctly wrapped in a Document and added to the QueryContext, following the expected pattern for the new validator.


170-170:

❌ Incorrect review comment

Minor inconsistency in error message format.

The error message format was changed to remove the "Invalid query: " prefix. While this is a minor change, ensure this is consistent with other error messages in the codebase for better user experience.


🏁 Script executed:

#!/bin/bash
# Description: Check for consistency in error message formats across validator tests

# Search for error message patterns in validator tests to ensure consistency
rg -A 2 -B 2 "Invalid (query|limit):" tests/unit/Validator/

Length of output: 9742


No inconsistency in error message prefixes
All limit‐related errors correctly use the Invalid limit: prefix and all query‐related errors use the Invalid query: prefix across the validator tests. No changes are required; you can ignore this comment.

Likely an incorrect or invalid review comment.

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

11-12: LGTM! Proper migration to the new validator.

Successfully migrated from the old Select validator to the new DocumentsValidator with QueryContext.


54-56: Test now validates arrays of queries as expected.

The validator correctly expects arrays of Query objects, which aligns with the new validation architecture supporting multiple query types together.

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

11-12: LGTM! Consistent migration pattern.

Successfully migrated to use QueryContext and DocumentsValidator, maintaining consistency with other test files.


82-89: LGTM! Proper collection attribute setup.

The test correctly modifies the collection's attributes and indexes before validation, demonstrating the flexibility of the new approach.

Also applies to: 91-100

src/Database/QueryContext.php (2)

8-27: LGTM! Clean class structure with well-documented properties.

The class provides a clear separation of concerns for managing collections, aliases, and authorization context.


70-86: Authorization skip logic is sound.

The implementation correctly prioritizes global authorization status and provides granular collection-permission control.

Note: If this code runs in a multi-threaded environment, consider that Authorization::$status could change between calls, though this is likely not an issue in typical PHP web request contexts.

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

10-15: Correct imports for the new validation architecture.

The transition from individual query validators to the unified DocumentsValidator approach is well-implemented.


20-50: Excellent refactoring to use realistic collection schema.

The new setUp method creates a proper Document structure with collection metadata, making the tests more representative of actual usage. The QueryContext integration is well-implemented.


52-73: Comprehensive test coverage with improved validation.

The tests now validate realistic query arrays and include broader coverage of query types. The updated error message assertions correctly reflect the new validator's behavior.

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

951-953: Correct adoption of new Query::select API pattern.

The changes properly align with the updated Query::select() method signature that accepts single string arguments. The pattern of using multiple Query::select() calls for multiple attributes is consistent and correct.

The addition of Query::select('*') ensures comprehensive attribute selection for test validation.

Also applies to: 975-976, 991-992, 1007-1008, 1023-1024, 1039-1040, 1055-1056, 1072-1073, 1088-1090, 1107-1107, 1123-1123

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

11-16: Consistent adoption of new validation architecture.

The imports and property changes align with the broader refactoring to use QueryContext-based validation.


21-57: Well-structured test setup using QueryContext.

The setUp method correctly creates a Document schema and integrates it with QueryContext, maintaining consistency with the new validation approach.


71-71: Correct usage of updated Query::select API.

The tests properly use single string arguments with Query::select(), aligning with the new API signature.

Also applies to: 76-76


87-96:

✅ Verification successful

Resolve the commented assertion and clarify expected behavior.

The commented assertion with the question "Think what to do about this?" indicates uncertainty about the expected validation behavior. This should be resolved before merging.

Please clarify whether Query::limit(1) should be considered valid or invalid by the new DocumentsValidator, and update the test accordingly. The current state with a commented assertion and contradictory behavior could lead to incorrect test validation.


🏁 Script executed:

#!/bin/bash
# Search for other limit query validations to understand expected behavior
rg -A 5 -B 5 "Query::limit" --type php

Length of output: 76084


🏁 Script executed:

#!/bin/bash
# Search for DocumentsValidator class definition
rg --pretty --heading --line-number "class DocumentsValidator" -A 5 -B 5 --type php

Length of output: 85


🏁 Script executed:

#!/bin/bash
# List all PHP files under src that include 'Validator' in their name
find src -type f -iname '*Validator.php'

Length of output: 42


Limit queries are valid by design
Positive Query::limit(...) values are accepted by DocumentsValidator (see testQueryLimit in tests/unit/Validator/QueryTest.php). The existing assertion

$this->assertEquals(true, $validator->isValid($queries));

correctly reflects this behavior. You can safely remove the commented-out assertion and merge as is.

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

259-271: Add a PHPDoc block and reconsider redundant parameters

find() now exposes both the generic $queries array and four specialised arrays ($selects, $filters, $joins, $orderQueries).

  1. With the new signature the specialised arrays fully cover what used to be inside $queries; keeping both encourages misuse and complicates call-sites.
  2. Unlike the parent abstract method, this override has no PHPDoc – IDEs and static analysers will lose the type information (e.g. array<Query> generics).

Diff sketch:

+    /**
+     * {@inheritDoc}
+     *
+     * @param array<Query> $queries
+     * @param array<Query> $selects
+     * @param array<Query> $filters
+     * @param array<Query> $joins
+     * @param array<Query> $orderQueries
+     * @return array<Document>
+     */
     public function find(
-        QueryContext $context,
-        array $queries = [],
+        QueryContext $context,
+        array $queries = [],   // ⚠️ consider deprecating / removing
         ...
     ): array {

[SUGGEST_ESSENTIAL_REFACTOR]

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

223-236: orderAsc('') and orderAsc() silently allow an empty attribute – verify this is intentional

Allowing ordering with no column (empty string or null) may mask configuration mistakes and can generate vendor-specific SQL errors (ORDER BY ""). If the validator intentionally permits it, add an explicit assertion in the validator or a doc-block comment explaining why this is safe; otherwise, mark these two assertions as failures.

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

21-61: $collection loses required attribute metadata

Attributes added with setAttribute('attributes', …) omit size, required, signed, etc., even though the validator uses those fields for type checks (e.g. integer bounds). Tests might pass but diverge from production data.

Ensure each mock attribute contains the full schema or use the helper that the production code employs to avoid false-positive test results.


92-103: Cursor tests use non-standard $uid key

Query::cursorAfter/Before() expect a Document carrying the primary key in $id, yet the failure-case assertions here build a document with $uid. If the intent is to fail due to wrong key, add a comment; otherwise, switch to $id to accurately test cursor validation logic.

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

19-19: LGTM: Required import for new QueryContext functionality.

The import is necessary for the QueryContext usage in the testEncodeDecode method and aligns with the database abstraction refactor.


776-777: Approve the query construction standardization.

The change from array-based to individual Query::select() calls improves API consistency and aligns with the new query construction pattern mentioned in the summary.


794-795: Correct assertion updates for new metadata behavior.

The assertions now properly expect $permissions and $collection fields to be present, reflecting the changes in the database adapter that includes these metadata fields by default in selected documents.


2886-2889: Proper implementation of new QueryContext pattern.

The code correctly creates a QueryContext, adds the collection metadata, and passes it to the decode method. This aligns with the broader refactor to use QueryContext for managing collections and aliases throughout the database layer.


1062-1063: Consistent query construction standardization.

The change maintains consistency with the new query construction pattern using individual Query::select() calls for each attribute.


1071-1074: Appropriate test assertions for new document structure.

The assertions correctly reflect the new document structure where $collection is included by default while other fields like $permissions may be absent depending on the selection.


3498-3498: Clean implementation for mandatory field selection.

The use of array_map to create Query::select() calls for mandatory fields is an elegant functional programming approach that maintains consistency with the new query construction pattern.

Also applies to: 3503-3504

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

1157-1166: Verify existence of getInternalIds()

createDocuments() calls $this->getInternalIds(...), but the method is not defined in this class. Ensure it exists (likely in the parent SQL class) and returns the expected mapping, otherwise a fatal error will occur when inserting documents without preset internal IDs.

src/Database/Database.php (1)

5826-5836: addSkipAuth() receives filtered id, not the alias

$context->addSkipAuth($this->adapter->filter($_collection->getId()), …) passes a
sanitised collection id whereas addSkipAuth() is later looked-up through
Query::getAlias() (default alias is _).
If a query uses the default alias, the skip-auth entry will never match, causing
unnecessary permission checks on the main collection.

Recommend passing the alias actually used in the query:

- $context->addSkipAuth($this->adapter->filter($_collection->getId()), $forPermission, $skipAuth);
+ $context->addSkipAuth(Query::DEFAULT_ALIAS, $forPermission, $skipAuth);

(or extend addSkipAuth to accept both).

Please review to ensure permission-skipping works as intended for the root collection.

Comment on lines +3 to +74
//
//namespace Utopia\Database\Validator\Queries;
//
//use Exception;
//use Utopia\Database\Database;
//use Utopia\Database\Document;
//use Utopia\Database\Validator\IndexedQueries;
//use Utopia\Database\Validator\Query\Cursor;
//use Utopia\Database\Validator\Query\Filter;
//use Utopia\Database\Validator\Query\Limit;
//use Utopia\Database\Validator\Query\Offset;
//use Utopia\Database\Validator\Query\Order;
//use Utopia\Database\Validator\Query\Select;
//
//class Documents extends IndexedQueries
//{
// /**
// * Expression constructor
// *
// * @param array<mixed> $attributes
// * @param array<mixed> $indexes
// * @throws Exception
// */
// public function __construct(
// array $attributes,
// array $indexes,
// int $maxValuesCount = 100,
// \DateTime $minAllowedDate = new \DateTime('0000-01-01'),
// \DateTime $maxAllowedDate = new \DateTime('9999-12-31'),
// ) {
// $attributes[] = new Document([
// '$id' => '$id',
// 'key' => '$id',
// 'type' => Database::VAR_STRING,
// 'array' => false,
// ]);
// $attributes[] = new Document([
// '$id' => '$internalId',
// 'key' => '$internalId',
// 'type' => Database::VAR_STRING,
// 'array' => false,
// ]);
// $attributes[] = new Document([
// '$id' => '$createdAt',
// 'key' => '$createdAt',
// 'type' => Database::VAR_DATETIME,
// 'array' => false,
// ]);
// $attributes[] = new Document([
// '$id' => '$updatedAt',
// 'key' => '$updatedAt',
// 'type' => Database::VAR_DATETIME,
// 'array' => false,
// ]);
//
// $validators = [
// new Limit(),
// new Offset(),
// new Cursor(),
// new Filter(
// $attributes,
// $maxValuesCount,
// $minAllowedDate,
// $maxAllowedDate,
// ),
// new Order($attributes),
// new Select($attributes),
// ];
//
// parent::__construct($attributes, $indexes, $validators);
// }
//}
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

Consider removing commented code instead of keeping it.

The entire Documents validator class has been commented out as part of the migration to the new V2 validator system. While this approach might be useful during development, keeping large blocks of commented code can lead to:

  • Code bloat and reduced readability
  • Confusion about what code is actually active
  • Maintenance overhead

Consider removing this commented code if the migration is complete and stable. If you need to keep it temporarily, add a clear comment explaining the timeline for removal.


🏁 Script executed:

#!/bin/bash
# Description: Verify that the Documents validator is no longer used anywhere in the codebase
# Expected: Only references should be in tests and the new V2 validator

echo "Searching for remaining usage of Documents validator..."
rg -A 3 "use.*Validator.*Documents" --type php
echo ""
echo "Searching for 'new Documents' instantiations..."
rg -A 3 "new Documents" --type php
echo ""
echo "Checking if V2 validator is properly replacing Documents..."
rg -A 3 "DocumentsValidator.*V2" --type php

Length of output: 12282


Remove the commented-out Documents validator class

The src/Database/Validator/Queries/Documents.php file is fully commented out and no longer referenced—tests and runtime use the V2 validator (aliased as DocumentsValidator). Keeping it only adds dead code and potential confusion.

• Delete src/Database/Validator/Queries/Documents.php from the repository
• If you need a temporary fallback during migration, add a clear deprecation comment at the top with a removal timeline (e.g. “to be deleted in vX.Y”)

🤖 Prompt for AI Agents
In src/Database/Validator/Queries/Documents.php from lines 3 to 74, the entire
Documents validator class is commented out and no longer used, which adds dead
code and reduces clarity. Remove this file completely from the repository to
eliminate unused code. If you need to keep it temporarily during migration, add
a clear deprecation comment at the top specifying when it will be removed.

Comment on lines +3 to +74
//
//namespace Utopia\Database\Validator\Query;
//
//use Utopia\Database\Document;
//use Utopia\Database\Query;
//
//class Order extends Base
//{
// /**
// * @var array<int|string, mixed>
// */
// protected array $schema = [];
//
// /**
// * @param array<Document> $attributes
// */
// public function __construct(array $attributes = [])
// {
// foreach ($attributes as $attribute) {
// $this->schema[$attribute->getAttribute('key', $attribute->getAttribute('$id'))] = $attribute->getArrayCopy();
// }
// }
//
// /**
// * @param string $attribute
// * @return bool
// */
// protected function isValidAttribute(string $attribute): bool
// {
// // Search for attribute in schema
// if (!isset($this->schema[$attribute])) {
// $this->message = 'Attribute not found in schema: ' . $attribute;
// return false;
// }
//
// return true;
// }
//
// /**
// * Is valid.
// *
// * Returns true if method is ORDER_ASC or ORDER_DESC and attributes are valid
// *
// * Otherwise, returns false
// *
// * @param Query $value
// * @return bool
// */
// public function isValid($value): bool
// {
// if (!$value instanceof Query) {
// return false;
// }
//
// $method = $value->getMethod();
// $attribute = $value->getAttribute();
//
// if ($method === Query::TYPE_ORDER_ASC || $method === Query::TYPE_ORDER_DESC) {
// if ($attribute === '') {
// return true;
// }
// return $this->isValidAttribute($attribute);
// }
//
// return false;
// }
//
// public function getMethodType(): string
// {
// return self::METHOD_TYPE_ORDER;
// }
//}
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

Verify that the new DocumentsValidator provides equivalent order validation functionality.

The entire Order validator class has been commented out as part of the migration to the new DocumentsValidator and QueryContext system. Ensure that:

  1. Order query validation is properly handled by the new DocumentsValidator class
  2. All existing order validation logic (schema attribute checking, order type validation) is preserved
  3. Error messages for invalid order queries remain informative
  4. All tests pass with the new validation system

🏁 Script executed:

#!/bin/bash
# Description: Verify that order query validation is handled by the new DocumentsValidator

# Search for DocumentsValidator usage and order validation logic
ast-grep --pattern $'class DocumentsValidator {
  $$$
}'

# Check for order-related validation in the new validator
rg -A 10 -B 5 "order|Order|ORDER_ASC|ORDER_DESC" src/Database/Validator/Queries/V2.php

# Look for tests that verify order query validation still works
rg -A 5 -B 5 "order.*valid" tests/

Length of output: 2318


Let’s inspect how validateAttributeExist handles empty attributes and find any new tests for order queries:


🏁 Script executed:

#!/bin/bash
# Locate validateAttributeExist implementation
rg -n "function validateAttributeExist" -n src/Database/Validator/Queries

# Show its full context
rg -A 10 -B 2 "function validateAttributeExist" -n src/Database/Validator/Queries

# Search for any tests covering Query::orderAsc or ::orderDesc
rg -n "orderAsc" -n tests
rg -n "orderDesc" -n tests

Length of output: 10672


Inspecting the full validateAttributeExist implementation and class context:


🏁 Script executed:

#!/bin/bash
# Show class declaration and context
sed -n '1,80p' src/Database/Validator/Queries/V2.php

# Print the complete validateAttributeExist method
rg -A 20 -B 2 "protected function validateAttributeExist" -n src/Database/Validator/Queries/V2.php

Length of output: 3434


Implement missing attribute-existence checks in V2.validateAttributeExist

The new validateAttributeExist method in src/Database/Validator/Queries/V2.php is currently a stub (with only a var_dump and commented‐out logic) and does not throw on unknown attributes—breaking the old Order validator’s schema checks.

Please:

  • Complete validateAttributeExist(string $attributeId, string $alias): void (around lines 372–383):
    • Remove the var_dump
    • Look up $attributeId in the cloned schema from $this->context
    • Throw an Exception with a clear message when the attribute isn’t found
  • Mirror the original isValidAttribute behavior (including empty‐string handling)
  • Ensure the following tests still pass or are updated:
    • tests/unit/Validator/Query/OrderTest.php
    • Any e2e Document/Joins tests covering Query::orderAsc and Query::orderDesc
  • Verify error messages remain informative for invalid order queries
🤖 Prompt for AI Agents
In src/Database/Validator/Queries/V2.php around lines 372 to 383, the
validateAttributeExist method is incomplete and currently only contains a
var_dump. Remove the var_dump and implement logic to check if the given
attributeId exists in the cloned schema from $this->context. If the attribute is
not found or is an empty string, throw an Exception with a clear message
indicating the attribute is not found, replicating the original isValidAttribute
behavior from the old Order validator. After implementing this, ensure that all
existing tests related to order validation, including
tests/unit/Validator/Query/OrderTest.php and any e2e tests covering
Query::orderAsc and Query::orderDesc, pass without errors and that error
messages for invalid order queries remain informative.

Comment on lines +147 to +148
Query::select('*'),
Query::select('albums.name')
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

Verify that multi-field selections work correctly with the new pattern.

The change from Query::select(['*', 'albums.name']) to separate Query::select('*') and Query::select('albums.name') calls should preserve the same functionality. Ensure that the order of these calls doesn't matter for the query execution.

Let me verify that the new query pattern maintains the same behavior:


🏁 Script executed:

#!/bin/bash
# Description: Check if there are any tests that validate the order independence of Query::select() calls

# Search for test cases that use multiple Query::select() calls to ensure order doesn't matter
rg -B 5 -A 10 "Query::select\('.*'\).*Query::select\('.*'\)" tests/

Length of output: 1365


Add tests to verify order independence of multi-field Query::select calls

I didn’t find any existing tests that exercise calling Query::select('albums.name') before Query::select('*'). To ensure the new pattern truly preserves functionality regardless of call order, please:

  • In tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (or a new e2e file), add a scenario where you reverse the selects:
    Query::select('albums.name'),
    Query::select('*'),
    and assert that the resulting SQL (or fetched data) matches the original order.
  • In tests/unit/Validator/Query/SelectTest.php (or a new unit test), add a case for:
    $this->assertTrue($this->validator->isValid([Query::select('attr'), Query::select('*')]));
    to mirror the reversed ordering in the validator.

These additions will confirm that splitting the array into separate select() calls does not introduce any order-dependent behavior.

🤖 Prompt for AI Agents
In tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php around lines
147-148, add a new test scenario that reverses the order of Query::select calls
to Query::select('albums.name') followed by Query::select('*') and assert that
the resulting SQL or fetched data matches the original order to verify order
independence. Additionally, in tests/unit/Validator/Query/SelectTest.php, add a
unit test case asserting that the validator accepts the reversed order of
selects, such as Query::select('attr') followed by Query::select('*'), to ensure
the validator also treats the order as irrelevant.


class AsQuery extends Validator
{
protected string $message = '"as" 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

Fix the inconsistency between error message and validation logic.

The error message claims "at most 64 chars" but the validation logic mb_strlen($value) >= 64 actually enforces a 63-character limit.

-    protected string $message = '"as" must contain at most 64 chars. Valid chars are a-z, A-Z, 0-9, and underscore.';
+    protected string $message = '"as" must contain at most 63 chars. Valid chars are a-z, A-Z, 0-9, and underscore.';

Or if 64 characters should be allowed:

-        if (\mb_strlen($value) >= 64) {
+        if (\mb_strlen($value) > 64) {
            return false;
        }

Also applies to: 50-52

🤖 Prompt for AI Agents
In src/Database/Validator/AsQuery.php at line 9 and also lines 50-52, the error
message states that the "as" value must contain at most 64 characters, but the
validation logic uses a condition that enforces a maximum length of 63
characters (mb_strlen($value) >= 64). To fix this inconsistency, either update
the validation logic to allow 64 characters by changing the condition to
mb_strlen($value) > 64, or revise the error message to say "at most 63 chars" to
match the current validation. Ensure both the message and validation logic are
aligned.

Comment on lines +54 to +57
if($this->attribute === '*'){
$this->message = 'Invalid "as" on attribute "*"';
return false;
}
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 the logic error in the wildcard attribute check.

The condition checks $this->attribute === '*' but should check $value === '*' since you're validating the alias value, not the attribute name.

-        if($this->attribute === '*'){
+        if($value === '*'){
            $this->message = 'Invalid "as" on attribute "*"';
            return false;
        }
🤖 Prompt for AI Agents
In src/Database/Validator/AsQuery.php around lines 54 to 57, the condition
incorrectly checks if $this->attribute equals '*', but it should check if $value
equals '*' since the validation is for the alias value. Update the condition to
compare $value to '*' instead of $this->attribute.

Comment on lines +1934 to +1939
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

Filter/validate getRightAlias() to prevent SQL-injection via aliases

$aliasRight = $this->quote($query->getRightAlias()); quotes but does not pass through $this->filter() unlike the left alias. A malicious alias ("; DROP TABLE …) injected upstream would still be accepted inside the quotes.

-$aliasRight = $this->quote($query->getRightAlias());
+$aliasRight = $this->quote($this->filter($query->getRightAlias()));
📝 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 1934 to 1939, the alias
obtained from getRightAlias() is only quoted but not filtered, which risks SQL
injection via malicious aliases. To fix this, pass the result of getRightAlias()
through the filter() method before quoting it, similar to how the left alias is
handled. This ensures the alias is properly validated and sanitized before use
in the SQL statement.

Comment on lines +6606 to +6611
$values = $query->getValues();
foreach ($values as $valueIndex => $value) {
try {
$values[$valueIndex] = DateTime::setTimezone($value);
} catch (\Throwable $e) {
throw new QueryException($e->getMessage(), $e->getCode(), $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

Potential undefined class reference – qualify DateTime::setTimezone()

DateTime::setTimezone() is invoked without a leading back-slash or use import.
Inside the Utopia\Database namespace this resolves to Utopia\Database\DateTime.
If the helper lives in Utopia\Database\Helpers\DateTime, this call will fatally error
(Class "Utopia\Database\DateTime" not found) at runtime.

-        $values[$valueIndex] = DateTime::setTimezone($value);
+        $values[$valueIndex] = \Utopia\Database\Helpers\DateTime::setTimezone($value);

Either add an explicit use Utopia\Database\Helpers\DateTime; at the top of the file
or prefix the call with its fully-qualified name.
Please verify the correct namespace before shipping.

🤖 Prompt for AI Agents
In src/Database/Database.php around lines 6606 to 6611, the call to
DateTime::setTimezone() is unqualified and resolves incorrectly to
Utopia\Database\DateTime, causing a fatal error. Fix this by either adding a
'use Utopia\Database\Helpers\DateTime;' statement at the top of the file or
prefixing the call with the fully qualified class name
'\Utopia\Database\Helpers\DateTime::setTimezone()' to ensure the correct class
is referenced.

Comment on lines +3020 to +3026
/**
* Auth requires $permissions
*/
//$selects[] = Query::select('$id'); // Do we need this?
//$selects[] = Query::select('$permissions', system: true);
$queries = Query::addSelect($queries, Query::select('$permissions', system: true));
// $queries = Query::add($queries, Query::select('$id'));
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

$id must always be included in the projection

populateDocumentRelationships() and several other downstream code paths rely on Document::getId() being populated.
Currently we only force–add $permissions; $id (and $collection) are commented-out.
If the caller supplies a selective projection without $id, the returned Document
will have an empty id → broken relationship resolution, cache keys, conflict detection, etc.

-//$selects[] = Query::select('$id'); // Do we need this?
-//$selects[] = Query::select('$permissions',  system: true);
-$queries = Query::addSelect($queries, Query::select('$permissions',  system: true));
+// Always project internal columns required by the runtime
+$queries = Query::addSelect($queries, Query::select('$id',          system: true));
+$queries = Query::addSelect($queries, Query::select('$permissions', system: true));
+// Optionally expose $collection as well – several callers assume its presence.
+// $queries = Query::addSelect($queries, Query::select('$collection', system: true));

Without this, any call such as getDocument('posts', 'abc', [Query::select('title')])
returns a Document whose $id === '', silently breaking logic.
This is a correctness issue – please restore the mandatory $id select.

📝 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
/**
* Auth requires $permissions
*/
//$selects[] = Query::select('$id'); // Do we need this?
//$selects[] = Query::select('$permissions', system: true);
$queries = Query::addSelect($queries, Query::select('$permissions', system: true));
// $queries = Query::add($queries, Query::select('$id'));
/**
* Auth requires $permissions
*/
// Always project internal columns required by the runtime
$queries = Query::addSelect($queries, Query::select('$id', system: true));
$queries = Query::addSelect($queries, Query::select('$permissions', system: true));
// Optionally expose $collection as well – several callers assume its presence.
// $queries = Query::addSelect($queries, Query::select('$collection', system: true));
// $queries = Query::add($queries, Query::select('$id'));
🤖 Prompt for AI Agents
In src/Database/Database.php around lines 3020 to 3026, the code currently only
forces the addition of the $permissions field in the query projection, while $id
is commented out. To fix the issue where Document::getId() is empty and breaks
downstream logic, ensure that $id is always included in the projection by
uncommenting and restoring the line that adds $id to the selects. This
guarantees that $id is present in all query results, maintaining correct
relationship resolution and other dependent functionality.

Comment on lines +25 to +26
public const TYPE_RELATION_EQUAL = 'relationEqual';

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

New query types are missing from TYPES and isMethod() – breaks JSON parsing/validation

TYPE_RELATION_EQUAL, TYPE_INNER_JOIN, TYPE_LEFT_JOIN, and TYPE_RIGHT_JOIN were added as constants, but

  1. they are not appended to the TYPES whitelist, and
  2. isMethod() still returns false for them.

As a result, any join / relation-equality query coming from JSON (or being re-serialised) will be rejected with “Invalid query method”. This blocks round-tripping through Query::parse() / toString() and can surface as runtime 400 errors from the SDK.

@@ public const TYPES = [
     self::TYPE_ENDS_WITH,
+    self::TYPE_RELATION_EQUAL,
+    self::TYPE_INNER_JOIN,
+    self::TYPE_LEFT_JOIN,
+    self::TYPE_RIGHT_JOIN,
@@
 ];
...
@@     public static function isMethod(string $value): bool
         self::TYPE_SELECT => true,
+        self::TYPE_RELATION_EQUAL,
+        self::TYPE_INNER_JOIN,
+        self::TYPE_LEFT_JOIN,
+        self::TYPE_RIGHT_JOIN => true,
         default => false,

Also applies to: 45-51, 54-77, 321-348

🤖 Prompt for AI Agents
In src/Database/Query.php around lines 25-26, 45-51, 54-77, and 321-348, the new
query type constants TYPE_RELATION_EQUAL, TYPE_INNER_JOIN, TYPE_LEFT_JOIN, and
TYPE_RIGHT_JOIN are missing from the TYPES whitelist array and the isMethod()
function. To fix this, add these constants to the TYPES array to whitelist them
and update isMethod() to return true for these new types, ensuring JSON parsing
and validation accept these query methods without errors.

Comment on lines +1083 to +1114
public static function addSelect(array $queries, Query $query): array
{
$merge = true;
$found = false;

foreach ($queries as $q) {
if ($q->getMethod() === self::TYPE_SELECT){
$found = true;

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

if ($q->getAttribute() === $query->getAttribute()){
if ($q->getAs() === $query->getAs()){
$merge = false;
}
}
}
}
}

if ($found && $merge){
$queries = [
...$queries,
$query
];
}

return $queries;
}
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

addSelect() never inserts when the list is empty

When $queries contains no existing TYPE_SELECT, $found stays false, so the new select is silently discarded. This prevents callers from building the initial projection list.

-if ($found && $merge){
+if ((! $found) || $merge) {
     $queries = [
         ...$queries,
         $query
     ];
 }

Also consider early-returning if $merge is false to avoid needless array spread.

📝 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
public static function addSelect(array $queries, Query $query): array
{
$merge = true;
$found = false;
foreach ($queries as $q) {
if ($q->getMethod() === self::TYPE_SELECT){
$found = true;
if ($q->getAlias() === $query->getAlias()){
if ($q->getAttribute() === '*'){
$merge = false;
}
if ($q->getAttribute() === $query->getAttribute()){
if ($q->getAs() === $query->getAs()){
$merge = false;
}
}
}
}
}
if ($found && $merge){
$queries = [
...$queries,
$query
];
}
return $queries;
}
public static function addSelect(array $queries, Query $query): array
{
$merge = true;
$found = false;
foreach ($queries as $q) {
if ($q->getMethod() === self::TYPE_SELECT){
$found = true;
if ($q->getAlias() === $query->getAlias()){
if ($q->getAttribute() === '*'){
$merge = false;
}
if ($q->getAttribute() === $query->getAttribute()){
if ($q->getAs() === $query->getAs()){
$merge = false;
}
}
}
}
}
// add when there are no existing selects, or when merge is still true
if ((! $found) || $merge) {
$queries = [
...$queries,
$query
];
}
return $queries;
}
🤖 Prompt for AI Agents
In src/Database/Query.php around lines 1083 to 1114, the addSelect() method
fails to add the new query if the $queries array has no existing TYPE_SELECT
queries because $found remains false, causing the new select to be discarded.
Fix this by adding the new query when $found is false, ensuring the initial
projection list can be built. Also, implement an early return when $merge is
false to avoid unnecessary array spreading and improve performance.

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

♻️ Duplicate comments (2)
tests/e2e/Adapter/Scopes/JoinsTests.php (2)

332-334: Debug var_dump() left in committed tests
var_dump($document); will spam test output on every run. Please remove.


375-377: Intentionally failing assertion & stray var_dump()
Both the var_dump($document); and $this->assertEquals('dsdsd', 'ds'); should be deleted or replaced with meaningful checks – they break the suite on purpose.

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

489-564: Large block of commented-out test code clutters the file
The ~75 lines of commented tests make maintenance harder and hide the actual coverage. Either restore them as active tests (fixed) or drop them entirely.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16c822e and 02d269d.

📒 Files selected for processing (2)
  • src/Database/Adapter/SQL.php (6 hunks)
  • tests/e2e/Adapter/Scopes/JoinsTests.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Database/Adapter/SQL.php

@utopia-php utopia-php deleted a comment from coderabbitai bot Jul 6, 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: 0

♻️ Duplicate comments (7)
src/Database/Adapter/MariaDB.php (2)

63-63: Debug output still present after previous review.

This debug statement was already flagged in previous reviews but remains in the code. Remove the var_dump($sql) call to prevent leaking internal DDL statements in production.


1848-1849: Debug output still present after previous review.

These debug statements were already flagged in previous reviews but remain in the code. Remove the echo and var_dump calls to prevent exposing query strings and bindings in production.

src/Database/Database.php (2)

6602-6607: Still unresolved: Qualify the DateTime class reference

The previous review comment about DateTime::setTimezone() requiring proper namespace qualification remains unaddressed. This will cause a fatal error at runtime.

Apply this fix:

-        $values[$valueIndex] = DateTime::setTimezone($value);
+        $values[$valueIndex] = \Utopia\Database\Helpers\DateTime::setTimezone($value);

3021-3027: Critical: $id is still commented out despite past review

The previous review comment correctly identified that $id must always be included in projections, but it remains commented out. This will break downstream functionality that relies on Document::getId().

Restore the mandatory $id select:

-//$selects[] = Query::select('$id'); // Do we need this?
-//$selects[] = Query::select('$permissions',  system: true);
-$queries = Query::addSelect($queries, Query::select('$permissions',  system: true));
+// Always project internal columns required by the runtime
+$queries = Query::addSelect($queries, Query::select('$id',          system: true));
+$queries = Query::addSelect($queries, Query::select('$permissions', system: true));
src/Database/Query.php (3)

54-77: Critical: Missing new query types in TYPES array.

The new query types (TYPE_RELATION_EQUAL, TYPE_INNER_JOIN, TYPE_LEFT_JOIN, TYPE_RIGHT_JOIN) are missing from the TYPES whitelist array, which will cause JSON parsing and validation to fail.

 public const TYPES = [
     self::TYPE_EQUAL,
     self::TYPE_NOT_EQUAL,
     self::TYPE_LESSER,
     self::TYPE_LESSER_EQUAL,
     self::TYPE_GREATER,
     self::TYPE_GREATER_EQUAL,
     self::TYPE_CONTAINS,
     self::TYPE_SEARCH,
     self::TYPE_IS_NULL,
     self::TYPE_IS_NOT_NULL,
     self::TYPE_BETWEEN,
     self::TYPE_STARTS_WITH,
     self::TYPE_ENDS_WITH,
+    self::TYPE_RELATION_EQUAL,
     self::TYPE_SELECT,
     self::TYPE_ORDER_DESC,
     self::TYPE_ORDER_ASC,
     self::TYPE_LIMIT,
     self::TYPE_OFFSET,
     self::TYPE_CURSOR_AFTER,
     self::TYPE_CURSOR_BEFORE,
     self::TYPE_AND,
     self::TYPE_OR,
+    self::TYPE_INNER_JOIN,
+    self::TYPE_LEFT_JOIN,
+    self::TYPE_RIGHT_JOIN,
 ];

321-348: Critical: Missing new query types in isMethod() function.

The isMethod() function is missing the new query types, which will cause validation failures when these query types are used.

 public static function isMethod(string $value): bool
 {
     return match ($value) {
         self::TYPE_EQUAL,
         self::TYPE_NOT_EQUAL,
         self::TYPE_LESSER,
         self::TYPE_LESSER_EQUAL,
         self::TYPE_GREATER,
         self::TYPE_GREATER_EQUAL,
         self::TYPE_CONTAINS,
         self::TYPE_SEARCH,
         self::TYPE_ORDER_ASC,
         self::TYPE_ORDER_DESC,
         self::TYPE_LIMIT,
         self::TYPE_OFFSET,
         self::TYPE_CURSOR_AFTER,
         self::TYPE_CURSOR_BEFORE,
         self::TYPE_IS_NULL,
         self::TYPE_IS_NOT_NULL,
         self::TYPE_BETWEEN,
         self::TYPE_STARTS_WITH,
         self::TYPE_ENDS_WITH,
         self::TYPE_OR,
         self::TYPE_AND,
+        self::TYPE_RELATION_EQUAL,
+        self::TYPE_INNER_JOIN,
+        self::TYPE_LEFT_JOIN,
+        self::TYPE_RIGHT_JOIN,
         self::TYPE_SELECT => true,
         default => false,
     };
 }

1076-1107: Critical: addSelect() method fails to add queries when list is empty.

The method has a logic error where it only adds the new query if an existing TYPE_SELECT is found AND $merge is true. This prevents adding the first select query to an empty list.

-if ($found && $merge){
+if ((!$found) || $merge) {
     $queries = [
         ...$queries,
         $query
     ];
 }
🧹 Nitpick comments (5)
src/Database/Adapter/MariaDB.php (1)

1983-1983: Remove duplicate variable assignment.

The $alias variable is assigned twice consecutively. Remove the duplicate assignment for cleaner code.

-        $alias = Query::DEFAULT_ALIAS;
src/Database/Database.php (1)

6614-6635: Clean separation in query conversion logic

The separation between collections and context provides good organization, though the exception throwing could be more informative about which alias is unknown.

Consider improving the error message:

-            throw new \Exception('Unknown Alias context');
+            throw new \Exception("Unknown alias '{$query->getAlias()}' in query context");
src/Database/Query.php (3)

29-29: Remove the commented out code.

The commented TYPE_SELECTION constant should be removed to keep the code clean.

-    //public const TYPE_SELECTION = 'selection';

590-593: Remove unused parameter from select method.

The $function parameter is declared but never used in the method.

-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

936-937: Remove unused variable.

The $joins variable is declared but never used in the groupByType method.

 public static function groupByType(array $queries): array
 {
     $filters = [];
-    $joins = [];
     $selections = [];
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02d269d and f7ae73c.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • src/Database/Adapter.php (2 hunks)
  • src/Database/Adapter/MariaDB.php (9 hunks)
  • src/Database/Database.php (44 hunks)
  • src/Database/Query.php (20 hunks)
  • tests/e2e/Adapter/Base.php (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/e2e/Adapter/Base.php
  • src/Database/Adapter.php
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/Database/Adapter/MariaDB.php (7)
src/Database/Database.php (3)
  • Database (33-6686)
  • find (5804-5962)
  • getCollection (1363-1379)
src/Database/QueryContext.php (3)
  • QueryContext (8-89)
  • getCollections (32-35)
  • skipAuth (75-86)
src/Database/Adapter/Pool.php (4)
  • quote (119-122)
  • find (259-273)
  • getTenantQuery (485-488)
  • getAttributeProjection (460-463)
src/Database/Query.php (8)
  • limit (623-626)
  • offset (634-637)
  • Query (8-1109)
  • getAlias (204-207)
  • getAttribute (182-185)
  • join (717-720)
  • getCollection (224-227)
  • getValues (190-193)
src/Database/Validator/Authorization.php (2)
  • Authorization (7-225)
  • getRoles (101-104)
src/Database/Adapter/SQL.php (7)
  • getInternalKeyForAttribute (1698-1710)
  • getSQLOperator (1304-1330)
  • getSQLPermissionsCondition (1370-1390)
  • getSQLTable (1399-1402)
  • getSQLConditions (1482-1497)
  • getTenantQuery (1517-1549)
  • getAttributeProjection (1595-1640)
src/Database/Adapter/SQLite.php (2)
  • getSQLPermissionsCondition (1032-1042)
  • getSQLTable (1050-1053)
src/Database/Query.php (3)
src/Database/QueryContext.php (1)
  • __construct (25-27)
src/Database/Database.php (3)
  • __construct (380-457)
  • getCollection (1363-1379)
  • Database (33-6686)
src/Database/Exception/Query.php (1)
  • Query (7-9)
🪛 PHPMD (2.15.0)
src/Database/Query.php

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

(UnusedFormalParameter)


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

(UnusedLocalVariable)

🔇 Additional comments (34)
src/Database/Adapter/MariaDB.php (6)

18-18: LGTM: Clean import addition for QueryContext.

The QueryContext import is properly added and necessary for the updated find() method signature.


1014-1014: Good: Standardized column quoting.

Using the quote() method instead of manual backticks improves consistency and security across the adapter.


1676-1704: Excellent: Major architectural improvement to support QueryContext and joins.

The updated method signature properly separates different query types (selects, filters, joins, orders) and introduces QueryContext for better alias and collection management. This enables more sophisticated query construction with join support.


1721-1777: Sophisticated cursor pagination implementation.

The logic correctly handles multi-attribute cursor pagination with tie-break conditions and includes a performance optimization for single unique key scenarios. The direction reversal for CURSOR_BEFORE is properly implemented.


1783-1799: Robust JOIN implementation with proper security checks.

The INNER JOIN logic correctly implements per-join permission checks, respects skipAuth flags from QueryContext, and maintains tenant isolation. The use of getSQLConditions for join conditions ensures consistency with filter handling.


2051-2089: Good: Added support for relation queries in JOIN conditions.

The updates properly handle TYPE_RELATION_EQUAL queries for generating JOIN ON clauses. The use of query-specific aliases and proper attribute mapping enables flexible join conditions between different table aliases.

src/Database/Database.php (17)

3037-3038: Good architectural improvement with QueryContext

The introduction of QueryContext provides a clean abstraction for managing collections and aliases, improving the separation of concerns.


3041-3041: Consistent validator usage with new V2 system

The migration to DocumentsValidator with QueryContext provides better schema-aware validation compared to the previous approach.


3054-3082: Robust handling of nested selections in relationships

The logic for processing nested selections based on relationship types (many-to-many, one-to-many vs many-to-one, one-to-one) is well thought out and handles the different projection requirements correctly.


3148-3149: Consistent parameter passing to decode methods

The updated method signatures for casting() and decode() now properly accept QueryContext and selects, enabling attribute filtering and alias resolution.


3533-3536: Verify QueryContext initialization in createDocument

The QueryContext creation and population looks correct for single collection operations.


4215-4217: Improved validation with schema awareness

The new validator accepts QueryContext and supports complex nested queries, which is a significant improvement over the previous validation approach.


4232-4240: Enhanced cursor handling with proper validation

The cursor document validation ensuring it belongs to the same collection adds important data integrity checks.


5812-5835: Comprehensive authorization handling for joins

The enhanced authorization logic that considers document security and permissions per collection within the context is well-implemented and maintains proper access control.


5853-5853: Verify convertQueries method implementation

The modularization of query conversion into static methods improves code organization and reusability.


5860-5942: Solid adapter integration with new query structure

The adapter method signature changes to accept QueryContext and explicit arrays of selects, filters, joins, and orders provide better separation of concerns and enable more sophisticated query processing.


5976-5982: Appropriate restriction on cursor direction

Disallowing "before" direction in foreach is a reasonable limitation that simplifies the implementation while covering the most common use cases.


6074-6075: QueryContext properly initialized for count operations

The QueryContext setup for count operations follows the established pattern correctly.


6097-6097: Clean separation of concerns in query filtering

Extracting only filter queries for count operations maintains the method's focus while working with the new query infrastructure.


6675-6676: Improved cache key generation

Serializing the selects array before hashing provides more reliable cache key generation compared to simpler approaches.


6254-6341: Enhanced decode method with alias support

The decode method now properly handles aliased attributes and QueryContext, enabling correct attribute resolution in join scenarios. The logic for handling $perms and attribute mapping is well-structured.


6352-6458: Consistent casting implementation

The casting method mirrors the decode method's approach with QueryContext and alias handling, maintaining consistency across the codebase.


3031-3035: No changes needed: getDocument is intentionally restricted to select-only queries
The getDocument method is designed for projecting specific fields via Query::select only. All existing call sites pass only Query::select(...), and any filtering or sorting should be done through find(). You can safely ignore the suggestion to allow other query types here.

Likely an incorrect or invalid review comment.

src/Database/Query.php (11)

25-26: New query type constant added correctly.

The TYPE_RELATION_EQUAL constant is properly defined and will be used for join conditions.


45-51: Join type constants added correctly.

The join type constants are properly defined and follow the existing naming convention.


84-102: FILTER_TYPES array correctly includes TYPE_RELATION_EQUAL.

The FILTER_TYPES array properly includes the new TYPE_RELATION_EQUAL constant along with existing filter types.


104-110: New properties added correctly.

The new properties support the enhanced functionality for joins and aliasing. The property declarations are properly typed and follow the existing conventions.


125-160: Enhanced constructor with proper parameter handling.

The constructor correctly handles the new parameters with appropriate default values and fallback logic for aliases.


204-228: New getter methods implemented correctly.

The getter methods follow the existing pattern and provide access to the new properties.


255-289: Direction getter methods with proper validation.

The getCursorDirection() and getOrderDirection() methods correctly validate the query type before returning the direction, throwing exceptions for invalid usage.


477-615: Static factory methods enhanced with alias support.

The existing static factory methods have been properly enhanced to accept optional alias parameters while maintaining backward compatibility.


711-759: New join and relation factory methods implemented correctly.

The join factory methods (join, innerJoin, leftJoin, rightJoin) and relationEqual method are properly implemented with correct parameter handling.


767-916: Query filtering methods implemented correctly.

The new static methods to filter queries by type are well-implemented and follow consistent patterns. The methods properly handle cloning to avoid side effects.


1032-1069: New utility methods implemented correctly.

The isJoin(), isFilter(), and isSystem() methods are properly implemented with correct logic and type checking.

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

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

6260-6458: Significant code duplication between decode() and casting() methods

Both decode() and casting() methods share nearly identical logic for:

  • Building internals and schema arrays
  • Iterating through document attributes
  • Resolving aliases from selects
  • Looking up attributes in collections

This violates the DRY principle and makes maintenance harder.

Consider extracting the common logic into a shared helper method:

private function processDocumentWithContext(
    QueryContext $context,
    Document $document,
    array $selects,
    callable $attributeProcessor
): Document {
    $internals = [];
    $schema = [];

    foreach (Database::INTERNAL_ATTRIBUTES as $attribute) {
        $internals[$attribute['$id']] = $attribute;
    }

    foreach ($context->getCollections() as $collection) {
        foreach ($collection->getAttribute('attributes', []) as $attribute) {
            $key = $attribute->getAttribute('key', $attribute->getAttribute('$id'));
            $key = $this->adapter->filter($key);
            $schema[$collection->getId()][$key] = $attribute->getArrayCopy();
        }
    }

    $new = new Document();

    foreach ($document as $key => $value) {
        if($key === '$perms'){
            $new->setAttribute($key, $value);
            continue;
        }

        // Alias resolution logic...
        $result = $this->resolveAttributeContext($key, $selects, $context, $internals, $schema);
        if ($result === null) {
            continue;
        }

        [$attributeKey, $attribute, $alias] = $result;
        
        // Process the attribute value
        $processedValue = $attributeProcessor($attribute, $value, $attributeKey);
        $new->setAttribute($attributeKey, $processedValue);
    }

    return $new;
}

Then refactor both methods to use this shared logic with specific attribute processors.

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

3024-3026: Missing $id in projection needs to be addressed

The $id attribute should always be included in the projection to ensure proper document functionality. Uncommenting the line that adds $id to selects is critical for relationship resolution and other downstream operations.


6605-6606: Unqualified DateTime::setTimezone() call needs namespace qualification

The DateTime::setTimezone() call is unqualified and will resolve incorrectly within the Utopia\Database namespace.

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

3037-3038: Consider extracting QueryContext initialization into a helper method

The pattern of creating and populating QueryContext is repeated across multiple methods. Consider extracting this into a helper method to ensure consistency and reduce code duplication.

Example helper method:

private function createQueryContext(Document|string $collection): QueryContext {
    $context = new QueryContext();
    if (is_string($collection)) {
        $collection = $this->silent(fn () => $this->getCollection($collection));
    }
    $context->add($collection);
    return $context;
}

Also applies to: 3533-3535, 3579-3581, 4160-4162, 4215-4217


4233-4240: Extract duplicated cursor validation logic

The cursor validation logic checking if the cursor document belongs to the same collection is duplicated. Consider extracting this into a reusable method.

+private function validateCursor(?Query $cursorQuery, Document $collection): Document {
+    if (is_null($cursorQuery)) {
+        return new Document();
+    }
+    
+    $cursor = $cursorQuery->getCursorDocument($cursorQuery);
+    if ($cursor->getCollection() !== $collection->getId()) {
+        throw new DatabaseException("cursor Document must be from the same Collection.");
+    }
+    
+    return $cursor;
+}

Then use it:

-$cursor = new Document();
-$cursorQuery = Query::getCursorQueries($queries);
-if (! is_null($cursorQuery)) {
-    $cursor = $cursorQuery->getCursorDocument($cursorQuery);
-    if ($cursor->getCollection() !== $collection->getId()) {
-        throw new DatabaseException("cursor Document must be from the same Collection.");
-    }
-}
+$cursorQuery = Query::getCursorQueries($queries);
+$cursor = $this->validateCursor($cursorQuery, $collection);

Also applies to: 5639-5646

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7ae73c and cb814c1.

📒 Files selected for processing (1)
  • src/Database/Database.php (44 hunks)
🔇 Additional comments (2)
src/Database/Database.php (2)

6676-6676: Cache key generation change may impact performance

Changing from implode() to serialize() for generating cache keys is more robust for complex query structures but could impact performance for frequently accessed documents.

Consider the performance implications and ensure cache invalidation works correctly with the new key format.


5824-5835: Well-structured authorization handling for joined collections

The implementation properly handles authorization checks for each collection in the join context, tracking skipAuth status independently per collection. This ensures security is maintained across complex queries.

Comment on lines +6573 to +6578
public static function convertQuery(QueryContext $context, Query $query): Query
{
$collection = clone $context->getCollectionByAlias($query->getAlias());

if ($collection->isEmpty()) {
throw new \Exception('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.

⚠️ Potential issue

Generic exception type reduces debuggability

Using generic \Exception makes it harder to handle specific error cases and reduces API clarity.

-if ($collection->isEmpty()) {
-    throw new \Exception('Unknown Alias context');
-}
+if ($collection->isEmpty()) {
+    throw new QueryException('Unknown alias "' . $query->getAlias() . '" in query context');
+}
📝 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
public static function convertQuery(QueryContext $context, Query $query): Query
{
$collection = clone $context->getCollectionByAlias($query->getAlias());
if ($collection->isEmpty()) {
throw new \Exception('Unknown Alias context');
public static function convertQuery(QueryContext $context, Query $query): Query
{
$collection = clone $context->getCollectionByAlias($query->getAlias());
if ($collection->isEmpty()) {
throw new QueryException('Unknown alias "' . $query->getAlias() . '" in query context');
}
🤖 Prompt for AI Agents
In src/Database/Database.php around lines 6573 to 6578, replace the generic
\Exception thrown when the collection is empty with a more specific custom
exception class that clearly indicates the error condition, such as
UnknownAliasException. Define this exception if it does not exist, and throw it
instead to improve error handling and API clarity.

@fogelito fogelito closed this Oct 12, 2025
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