Skip to content

Conversation

@shimonewman
Copy link
Contributor

@shimonewman shimonewman commented Oct 16, 2025

Summary by CodeRabbit

  • New Features

    • Authorization now operates per-database instance, allowing isolated permission contexts per connection.
  • Refactor

    • Migration from global/static authorization to instance-based authorization and a revised role-management API.
    • Centralized authorization integrated into database adapters and connection pooling.
  • Tests

    • Test suites updated to use instance-scoped authorization and adapted helper access patterns.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

Authorization was converted from a static/global API to an instance-scoped API. Authorization instances are created and propagated via Database/Adapter/Pool/Mirror, an Input descriptor class was added, and tests/examples/tasks were updated to use per-database authorization and instance methods (e.g., addRole).

Changes

Cohort / File(s) Summary
Documentation & Examples
README.md
Updated example to use instance role delegation ($authorization->addRole(...)) instead of static Authorization::setRole(...).
Task Scripts
bin/tasks/load.php, bin/tasks/query.php, bin/tasks/relationships.php
Replaced global/static Authorization usage and top-level helper functions with local closures using per-database authorization ($database->getAuthorization()->addRole(...)) and added JSON result dumping in query.php.
Core Authorization Validator
src/Database/Validator/Authorization.php
Converted static API to instance API (e.g., setRole()addRole(), unsetRole()removeRole(), isRole()hasRole()), moved role/status storage to instance properties, added status controls, and switched validation to accept Input.
Authorization Input
src/Database/Validator/Authorization/Input.php
Added new Input class encapsulating action and permissions for authorization checks.
Database Core
src/Database/Database.php
Added private Authorization $authorization, created getter/setter, initialized instance authorization in constructor, replaced static Authorization checks with $this->authorization->isValid(new Input(...)), and routed skip/status through instance.
Adapters / Pool / Mirror
src/Database/Adapter.php, src/Database/Adapter/SQL.php, src/Database/Adapter/Mongo.php, src/Database/Adapter/Pool.php, src/Database/Mirror.php
Threaded Authorization instance into adapters and pool, added accessor/mutator (setAuthorization/getAuthorization), replaced static Authorization accesses with instance-based calls, and propagated authorization in Pool::delegate and Mirror::setAuthorization.
Tests: Base & Adapter Suites
tests/e2e/Adapter/Base.php, tests/e2e/Adapter/*Test.php (many)
Converted many static helper methods (getDatabase, deleteColumn, deleteIndex) to instance methods, added setAuthorization(self::$authorization) to Database builder chains in tests, and replaced test-side static Authorization calls with instance calls.
Tests: Scopes & Schemaless
tests/e2e/Adapter/Scopes/**, tests/e2e/Adapter/Schemaless/**
Replaced static::getDatabase()$this->getDatabase() and migrated all Authorization usages in tests to ->getAuthorization()->... instance calls; added early returns in some relationship tests.
Unit Tests
tests/unit/Validator/AuthorizationTest.php
Converted tests to use an instance Authorization fixture, switched to Input wrappers, and adapted assertions to instance API.
Results / Artifacts
bin/view/results/mariadb_testing_1000_1760626673.json
Added static JSON test results file.

Sequence Diagram(s)

sequenceDiagram
    participant Test
    participant Database
    participant Adapter
    participant Auth as Authorization (instance)

    Note over Test,Auth: Previous (Static) flow
    Test->>Auth: Authorization::setRole(role)
    Note right of Auth: global roles/status
    Adapter->>Auth: Authorization::getRoles()
    Auth-->>Adapter: roles

    Note over Database,Auth: New (Instance) flow
    Test->>Database: new Database()
    Database->>Auth: new Authorization()
    Database->>Database: setAuthorization(auth)
    Database->>Adapter: new Adapter(...)->setAuthorization(auth)
    Test->>Database: getAuthorization()
    Database-->>Test: auth
    Test->>Auth: addRole(role)
    Auth-->>Adapter: getRoles()
    Adapter->>Database: use auth for permission checks (Input)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas to focus during review:
    • src/Database/Validator/Authorization.php — storage model, Input handling, and signature changes.
    • src/Database/Database.php — many permission check replacements and error messages now using Input.
    • Adapter implementations (SQL, Mongo, Pool) — ensure authorization propagation and no regressions in query logic.
    • Widespread test updates — verify helpers and test harness changes are consistent and not introducing hidden state coupling.

Possibly related PRs

  • Feat relationship updates #724 — overlapping edits to relationship task files and migration to per-database authorization closures.
  • Feat mongo tmp #647 — touches adapter-level code and adapter threading similar to this PR; likely related to authorization propagation.

Suggested reviewers

  • abnegate
  • fogelito

Poem

🐰 From global fences to cozy burrows small,
Each database carries its own auth call.
Roles hop in place, no static snare,
Instance checks guard data with care.
A little rabbit cheers the change — hip, hop, hurrah! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Feat authorization instance' accurately reflects the main objective of this changeset. The primary change involves converting the Authorization class from static methods to instance-based methods, which is consistent with the theme of 'authorization instance'. The title is concise, specific, and clearly communicates the core architectural shift being introduced throughout the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-authorization-instance

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61d0662 and 5464da0.

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

Applied to files:

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

Applied to files:

  • src/Database/Database.php
🧬 Code graph analysis (1)
src/Database/Database.php (5)
src/Database/Validator/Authorization.php (4)
  • Authorization (8-236)
  • skip (171-181)
  • isValid (52-81)
  • getDescription (42-45)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
src/Database/Adapter/Pool.php (1)
  • setAuthorization (630-634)
src/Database/Adapter.php (2)
  • setAuthorization (61-66)
  • getAuthorization (68-71)
src/Database/Mirror.php (1)
  • setAuthorization (1103-1116)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Adapter Tests (Schemaless/MongoDB)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/MongoDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (MongoDB)
  • GitHub Check: Unit Test

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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

Caution

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

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

795-798: Bug: wrong variable in assertion ($account2 vs $account1)

You fetch $account1 then assert on $account2. Assert on $account1 to test the nullified relation.

Apply:

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

9-10: Use instance Authorization; fix linter: remove unused imports + trim blank blank-line whitespace

Good switch to $this->getSource()->getAuthorization()->skip(...). CI fails with PSR-12: unused imports and whitespace. Remove unused Authorization, DuplicateException, and Limit imports, and remove whitespace in the blank line before the return.

Based on pipeline failures

Apply:

@@
-use Utopia\Database\Exception\Duplicate as DuplicateException;
-use Utopia\Database\Exception\Limit;
 use Utopia\Database\Helpers\ID;
 use Utopia\Database\Mirroring\Filter;
-use Utopia\Database\Validator\Authorization;
@@
     protected function getUpgradeStatus(string $collection): ?Document
     {
         if ($collection === 'upgrades' || $collection === Database::METADATA) {
             return new Document();
         }
- 
-        return $this->getSource()->getAuthorization()->skip(function () use ($collection) {
+
+        return $this->getSource()->getAuthorization()->skip(function () use ($collection) {
             try {
                 return $this->source->getDocument('upgrades', $collection);
             } catch (\Throwable) {
                 return;
             }
         });
     }

Also applies to: 1086-1094

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

23-23: Fix linter: remove unused import

The Authorization validator import is unused; remove to satisfy PSR-12 no_unused_imports.

- use Utopia\Database\Validator\Authorization;
tests/e2e/Adapter/Scopes/RelationshipTests.php (1)

1-22: Fix the unused import to resolve the linter error.

The pipeline linter reports a 'no_unused_imports' violation. Since the code now accesses Authorization through $this->getDatabase()->getAuthorization() instead of using the class directly, the use Utopia\Database\Validator\Authorization; import at line 21 is likely no longer needed and should be removed.

Apply this diff to remove the unused import:

 use Utopia\Database\Helpers\Role;
 use Utopia\Database\Query;
-use Utopia\Database\Validator\Authorization;
bin/tasks/query.php (1)

1-22: Fix import ordering to resolve the linter error.

The pipeline linter reports an 'ordered_imports' rule violation. According to PSR-12, imports should be in alphabetical order. The use Utopia\Database\Validator\Authorization; import at line 21 should come before the use Utopia\Validator\Boolean; import.

Apply this diff to fix the import order:

 use Utopia\Database\Query;
+use Utopia\Database\Validator\Authorization;
 use Utopia\Validator\Boolean;
 use Utopia\Validator\Integer;
 use Utopia\Validator\Text;
-use Utopia\Database\Validator\Authorization;
tests/e2e/Adapter/Scopes/DocumentTests.php (1)

21-21: Remove unused import to fix linter.

use Utopia\Database\Validator\Authorization; is unused. Drop it to satisfy PSR-12 no_unused_imports.

- use Utopia\Database\Validator\Authorization;
tests/e2e/Adapter/Scopes/GeneralTests.php (1)

22-22: Remove unused import to satisfy linter.

use Utopia\Database\Validator\Authorization; isn’t used. Remove it to fix PSR‑12 no_unused_imports.

- use Utopia\Database\Validator\Authorization;
🧹 Nitpick comments (20)
bin/tasks/load.php (1)

29-30: Inject the Authorization instance into Database (local $authorization currently unused).

You create and mutate $authorization but never set it on the Database; the DB uses its own Authorization instance. Inject it to align with the instance-based model.

Apply this diff at the two Database construction sites:

@@
-        $createSchema(
-            (new Database(new ($cfg['adapter'])($pdo), $cache))
-                ->setDatabase($name)
-                ->setNamespace($namespace)
-                ->setSharedTables($sharedTables)
-        );
+        $createSchema(
+            (new Database(new ($cfg['adapter'])($pdo), $cache))
+                ->setAuthorization($authorization)
+                ->setDatabase($name)
+                ->setNamespace($namespace)
+                ->setSharedTables($sharedTables)
+        );
@@
-                $database = (new Database(new ($cfg['adapter'])($pdo), $cache))
-                    ->setDatabase($name)
-                    ->setNamespace($namespace)
-                    ->setSharedTables($sharedTables);
+                $database = (new Database(new ($cfg['adapter'])($pdo), $cache))
+                    ->setAuthorization($authorization)
+                    ->setDatabase($name)
+                    ->setNamespace($namespace)
+                    ->setSharedTables($sharedTables);

Also applies to: 42-46, 51-66, 125-130, 151-155

bin/tasks/relationships.php (1)

37-38: Propagate Authorization to Database (and into workers).

$authorization is set and updated but never attached to Database; also not captured in go() closures. Inject it to enforce the intended roles.

@@
-        $database = (new Database(new ($cfg['adapter'])($pdo), $cache))
+        $database = (new Database(new ($cfg['adapter'])($pdo), $cache))
+            ->setAuthorization($authorization)
             ->setDatabase($name)
             ->setNamespace($namespace)
             ->setSharedTables($sharedTables);
@@
-        for ($i = 0; $i < $limit / 1000; $i++) {
-            go(function () use ($cfg, $pool, $name, $namespace, $sharedTables, $cache, $globalDocs) {
+        for ($i = 0; $i < $limit / 1000; $i++) {
+            go(function () use ($cfg, $pool, $name, $namespace, $sharedTables, $cache, $globalDocs, $authorization) {
                 try {
                     $pdo = $pool->get();
 
-                    $database = (new Database(new ($cfg['adapter'])($pdo), $cache))
+                    $database = (new Database(new ($cfg['adapter'])($pdo), $cache))
+                        ->setAuthorization($authorization)
                         ->setDatabase($name)
                         ->setNamespace($namespace)
                         ->setSharedTables($sharedTables);

Also applies to: 47-48, 54-66, 169-175, 195-205

README.md (1)

29-29: Clarify how to add roles with the instance-based API.

Readers need to know where $authorization comes from. Suggest referencing $database->getAuthorization() or showing setAuthorization usage.

-- **Permissions** - Using permissions, you can decide which roles have read, create, update and delete access for a specific document. The special attribute `$permissions` is used to store permission metadata for each document in the collection. A permission role can be any string you want. You can use `$authorization->addRole()` to delegate new roles to your users, once obtained a new role a user would gain read, create, update or delete access to a relevant document.
+- **Permissions** - Using permissions, you can decide which roles have read, create, update and delete access for a specific document. The special attribute `$permissions` is used to store permission metadata for each document in the collection. A permission role can be any string you want. You can use `$database->getAuthorization()->addRole('role')` (or inject a custom Authorization via `$database->setAuthorization($authorization)`) to delegate new roles to your users.
tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (1)

1839-1844: Consistency: avoid mixing $database and $this->getDatabase() within the same test

In this method you create $database = $this->getDatabase(); but then call $this->getDatabase()->... repeatedly. Prefer one access style (use $database) for readability and to simplify future refactors.

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

1349-1350: Typo: test name testSptialAggregation

Consider renaming to testSpatialAggregation for clarity and grep-ability.

Also applies to: 1479-1480, 1781-1782


95-96: Normalize Query casing across test files (style/consistency)

Lowercase query:: static calls found in SpatialTests.php and DocumentTests.php should use correct PascalCase Query:: to comply with PSR naming conventions and improve static analysis resolution.

Fix these 7 occurrences:

  • SpatialTests.php:168, 169, 203, 212, 1124
  • DocumentTests.php:290, 323
src/Database/Adapter/SQL.php (2)

2432-2436: Add fallback ORDER BY to avoid invalid SQL when no sort is provided

If $orders is empty, ORDER BY will be malformed. Default to primary key for stability.

-        $sqlOrder = 'ORDER BY ' . implode(', ', $orders);
+        if (empty($orders)) {
+            $orders[] = $this->quote('_id') . ' ' . Database::ORDER_ASC;
+        }
+        $sqlOrder = 'ORDER BY ' . implode(', ', $orders);

585-606: Remove unused $removeBindKeys scratch array

$removeBindKeys is appended to but never used; drop it to reduce noise.

-                        $bindKey = '_uid_' . $index;
-                        $removeBindKeys[] = ':_uid_' . $index;
-                        $removeBindValues[$bindKey] = $document->getId();
+                        $bindKey = '_uid_' . $index;
+                        $removeBindValues[$bindKey] = $document->getId();
...
-                            $removeBindKeys[] = ':' . $bindKey;
                             $removeBindValues[$bindKey] = $permissionsToRemove[$i];
tests/e2e/Adapter/Scopes/RelationshipTests.php (1)

1748-1750: Consider caching the authorization reference for better readability.

Multiple calls to $this->getDatabase()->getAuthorization() in quick succession could be simplified by storing the reference in a local variable. While the performance impact is minimal in tests, it improves readability.

Example refactor for this test method:

+    $authorization = $this->getDatabase()->getAuthorization();
+    $authorization->cleanRoles();
+    $authorization->addRole(Role::any()->toString());
-    $this->getDatabase()->getAuthorization()->cleanRoles();
-    $this->getDatabase()->getAuthorization()->addRole(Role::any()->toString());

This pattern appears in multiple places throughout the file (e.g., lines 2249-2250, 1826, 1839).

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

60-61: Align table name with SQLite naming (include tenant).

SQLite tables are named with namespace_tenant_collection (per index naming). Include tenant to avoid mismatches when tenant is set.

-        $sqlTable = "`" . $this->getDatabase()->getNamespace() . "_" . $collection . "`";
+        $sqlTable = sprintf(
+            '`%s_%s_%s`',
+            $this->getDatabase()->getNamespace(),
+            $this->getDatabase()->getTenant(),
+            $collection
+        );
src/Database/Database.php (1)

4909-4913: Address PSR‑12 whitespace warnings

There are trailing spaces/formatting nits (example vicinity). Please run your CS fixer (php‑cs‑fixer/PHPCS) to remove trailing whitespace and enforce single‑space around constructs.

tests/e2e/Adapter/SharedTables/PostgresTest.php (2)

32-65: Instance migration + auth injection look good

Swapping to an instance getDatabase() and injecting authorization via setAuthorization(self::$authorization) is aligned with Base and core changes. Minor nit: docblock above still says “@reture Adapter”.

Also applies to: 51-55


77-86: Use adapter’s deleteIndex to avoid quoting/compat pitfalls

Leverage the adapter API instead of constructing SQL manually; it already handles namespacing, tenant, and “not exists” cases consistently.

Apply this diff:

-    protected function deleteIndex(string $collection, string $index): bool
-    {
-        $key = "\"".$this->getDatabase()->getNamespace()."_".$this->getDatabase()->getTenant()."_{$collection}_{$index}\"";
-
-        $sql = "DROP INDEX \"".$this->getDatabase()->getDatabase()."\".{$key}";
-
-        self::$pdo->exec($sql);
-
-        return true;
-    }
+    protected function deleteIndex(string $collection, string $index): bool
+    {
+        return $this->getDatabase()->getAdapter()->deleteIndex($collection, $index);
+    }
tests/e2e/Adapter/MirrorTest.php (3)

96-101: Explicitly set authorization on both source and destination

Mirror’s setAuthorization() updates its (source) adapter; destination isn’t guaranteed unless set earlier. Make it explicit to avoid coupling to cleanup side-effects.

 $database
     ->setDatabase('utopiaTests')
-    ->setAuthorization(self::$authorization)
     ->setNamespace(static::$namespace = 'myapp_' . uniqid());
+
+// Ensure both sides share the same Authorization explicitly
+$database->getSource()->setAuthorization(self::$authorization);
+$database->getDestination()->setAuthorization(self::$authorization);
+
+// Optionally also set on Mirror for symmetry (source will be covered anyway)
+$database->setAuthorization(self::$authorization);

316-329: Avoid reliance on static state; use instance-derived source/destination

Use $this->getDatabase()->getSource()/getDestination() to reduce ordering/initialization risks.

-    protected function deleteColumn(string $collection, string $column): bool
-    {
-        $sqlTable = "`" . self::$source->getDatabase() . "`.`" . self::$source->getNamespace() . "_" . $collection . "`";
+    protected function deleteColumn(string $collection, string $column): bool
+    {
+        $db = $this->getDatabase();
+        $source = $db->getSource();
+        $destination = $db->getDestination();
+
+        $sqlTable = "`" . $source->getDatabase() . "`.`" . $source->getNamespace() . "_" . $collection . "`";
         $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`";
 
-        self::$sourcePdo->exec($sql);
+        self::$sourcePdo->exec($sql);
 
-        $sqlTable = "`" . self::$destination->getDatabase() . "`.`" . self::$destination->getNamespace() . "_" . $collection . "`";
+        $sqlTable = "`" . $destination->getDatabase() . "`.`" . $destination->getNamespace() . "_" . $collection . "`";
         $sql = "ALTER TABLE {$sqlTable} DROP COLUMN `{$column}`";
 
-        self::$destinationPdo->exec($sql);
+        self::$destinationPdo->exec($sql);
 
         return true;
     }

331-344: Same here: prefer instance access and/or adapter API for index drop

Either reuse instance-derived DBs or delegate to adapter deleteIndex for correctness and fewer SQL edge cases.

-    protected function deleteIndex(string $collection, string $index): bool
-    {
-        $sqlTable = "`" . self::$source->getDatabase() . "`.`" . self::$source->getNamespace() . "_" . $collection . "`";
-        $sql = "DROP INDEX `{$index}` ON {$sqlTable}";
-
-        self::$sourcePdo->exec($sql);
-
-        $sqlTable = "`" . self::$destination->getDatabase() . "`.`" . self::$destination->getNamespace() . "_" . $collection . "`";
-        $sql = "DROP INDEX `{$index}` ON {$sqlTable}";
-
-        self::$destinationPdo->exec($sql);
-
-        return true;
-    }
+    protected function deleteIndex(string $collection, string $index): bool
+    {
+        $db = $this->getDatabase();
+        $ok1 = $db->getSource()->getAdapter()->deleteIndex($collection, $index);
+        $ok2 = $db->getDestination()->getAdapter()->deleteIndex($collection, $index);
+        return $ok1 && $ok2;
+    }
tests/e2e/Adapter/SharedTables/SQLiteTest.php (1)

83-91: Delegate index drop to adapter to handle “no such index” gracefully

SQLite adapter already swallows “no such index” and normalizes naming; use it directly.

-    protected function deleteIndex(string $collection, string $index): bool
-    {
-        $index = "`".$this->getDatabase()->getNamespace()."_".$this->getDatabase()->getTenant()."_{$collection}_{$index}`";
-        $sql = "DROP INDEX {$index}";
-
-        self::$pdo->exec($sql);
-
-        return true;
-    }
+    protected function deleteIndex(string $collection, string $index): bool
+    {
+        return $this->getDatabase()->getAdapter()->deleteIndex($collection, $index);
+    }
tests/e2e/Adapter/SharedTables/MySQLTest.php (1)

80-88: Prefer adapter deleteIndex over raw SQL

Lets the adapter own quoting and edge cases.

-    protected function deleteIndex(string $collection, string $index): bool
-    {
-        $sqlTable = "`" . $this->getDatabase()->getDatabase() . "`.`" . $this->getDatabase()->getNamespace() . "_" . $collection . "`";
-        $sql = "DROP INDEX `{$index}` ON {$sqlTable}";
-
-        self::$pdo->exec($sql);
-
-        return true;
-    }
+    protected function deleteIndex(string $collection, string $index): bool
+    {
+        return $this->getDatabase()->getAdapter()->deleteIndex($collection, $index);
+    }
tests/e2e/Adapter/PostgresTest.php (1)

21-21: LGTM on instance method; fix docblock typo

Good migration to instance. Minor: change “@reture Adapter” to “@return Database”.

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

60-65: Prevent role leakage across tests

Clean roles before adding defaults to avoid cross-test state.

Apply:

-        if (is_null(self::$authorization)) {
-            self::$authorization = new Authorization();
-        }
-
-        self::$authorization->addRole('any');
+        if (is_null(self::$authorization)) {
+            self::$authorization = new Authorization();
+        }
+        self::$authorization->cleanRoles();
+        self::$authorization->addRole('any');
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 141338a and 8007d6f.

📒 Files selected for processing (34)
  • README.md (1 hunks)
  • bin/tasks/load.php (3 hunks)
  • bin/tasks/query.php (2 hunks)
  • bin/tasks/relationships.php (3 hunks)
  • src/Database/Adapter.php (2 hunks)
  • src/Database/Adapter/SQL.php (6 hunks)
  • src/Database/Database.php (29 hunks)
  • src/Database/Mirror.php (1 hunks)
  • src/Database/Validator/Authorization.php (5 hunks)
  • src/Database/Validator/Authorization/Input.php (1 hunks)
  • tests/e2e/Adapter/Base.php (1 hunks)
  • tests/e2e/Adapter/MariaDBTest.php (3 hunks)
  • tests/e2e/Adapter/MirrorTest.php (12 hunks)
  • tests/e2e/Adapter/MySQLTest.php (3 hunks)
  • tests/e2e/Adapter/PoolTest.php (4 hunks)
  • tests/e2e/Adapter/PostgresTest.php (3 hunks)
  • tests/e2e/Adapter/SQLiteTest.php (3 hunks)
  • tests/e2e/Adapter/Scopes/AttributeTests.php (41 hunks)
  • tests/e2e/Adapter/Scopes/CollectionTests.php (29 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (126 hunks)
  • tests/e2e/Adapter/Scopes/GeneralTests.php (9 hunks)
  • tests/e2e/Adapter/Scopes/IndexTests.php (11 hunks)
  • tests/e2e/Adapter/Scopes/PermissionTests.php (43 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (36 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (16 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php (15 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (17 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php (18 hunks)
  • tests/e2e/Adapter/Scopes/SpatialTests.php (25 hunks)
  • tests/e2e/Adapter/SharedTables/MariaDBTest.php (3 hunks)
  • tests/e2e/Adapter/SharedTables/MySQLTest.php (3 hunks)
  • tests/e2e/Adapter/SharedTables/PostgresTest.php (3 hunks)
  • tests/e2e/Adapter/SharedTables/SQLiteTest.php (3 hunks)
  • tests/unit/Validator/AuthorizationTest.php (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
PR: utopia-php/database#721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • tests/e2e/Adapter/Scopes/IndexTests.php
  • tests/e2e/Adapter/Scopes/AttributeTests.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
🧬 Code graph analysis (33)
src/Database/Adapter.php (2)
src/Database/Database.php (2)
  • Database (38-8163)
  • setAuthorization (867-872)
src/Database/Validator/Authorization.php (1)
  • Authorization (8-240)
tests/e2e/Adapter/Scopes/IndexTests.php (2)
tests/e2e/Adapter/Base.php (2)
  • getDatabase (40-40)
  • deleteIndex (56-56)
tests/e2e/Adapter/MariaDBTest.php (2)
  • getDatabase (21-53)
  • deleteIndex (65-73)
src/Database/Adapter/SQL.php (1)
src/Database/Validator/Authorization.php (2)
  • getRoles (109-112)
  • getStatus (161-164)
tests/e2e/Adapter/Scopes/GeneralTests.php (6)
src/Database/Database.php (4)
  • getDatabase (786-789)
  • ping (1254-1257)
  • getAuthorization (879-882)
  • getAdapter (1231-1234)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
tests/e2e/Adapter/MirrorTest.php (1)
  • getDatabase (36-104)
src/Database/Adapter.php (3)
  • getDatabase (178-181)
  • ping (492-492)
  • getSupportForCacheSkipOnFailure (1047-1047)
src/Database/Validator/Authorization.php (4)
  • disable (202-205)
  • reset (212-215)
  • cleanRoles (117-120)
  • addRole (91-94)
src/Database/Helpers/Role.php (2)
  • Role (5-178)
  • any (159-162)
tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (1)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
bin/tasks/load.php (4)
src/Database/Validator/Authorization.php (2)
  • Authorization (8-240)
  • addRole (91-94)
src/Database/Database.php (5)
  • Database (38-8163)
  • exists (1303-1308)
  • delete (1331-1345)
  • create (1273-1292)
  • createCollection (1360-1494)
src/Database/Helpers/Role.php (2)
  • Role (5-178)
  • any (159-162)
src/Database/Helpers/Permission.php (2)
  • Permission (9-264)
  • read (186-195)
src/Database/Mirror.php (2)
src/Database/Database.php (1)
  • getAuthorization (879-882)
src/Database/Validator/Authorization.php (1)
  • skip (175-185)
bin/tasks/relationships.php (4)
src/Database/Validator/Authorization.php (2)
  • Authorization (8-240)
  • addRole (91-94)
src/Database/Database.php (3)
  • Database (38-8163)
  • exists (1303-1308)
  • create (1273-1292)
src/Database/Helpers/Role.php (2)
  • Role (5-178)
  • any (159-162)
src/Database/Helpers/Permission.php (3)
  • Permission (9-264)
  • read (186-195)
  • update (220-229)
tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (1)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Validator/Authorization/Input.php (2)
src/Database/Database.php (1)
  • Database (38-8163)
src/Database/Validator/Authorization.php (1)
  • Authorization (8-240)
tests/e2e/Adapter/SQLiteTest.php (3)
src/Database/Database.php (6)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
  • getTenant (1084-1087)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
src/Database/Adapter/SQLite.php (1)
  • deleteIndex (491-510)
tests/e2e/Adapter/Scopes/AttributeTests.php (4)
src/Database/Database.php (2)
  • getDatabase (786-789)
  • getAuthorization (879-882)
tests/e2e/Adapter/Base.php (2)
  • getDatabase (40-40)
  • deleteColumn (48-48)
src/Database/Validator/Authorization.php (1)
  • addRole (91-94)
src/Database/Helpers/Role.php (2)
  • Role (5-178)
  • any (159-162)
tests/e2e/Adapter/SharedTables/MariaDBTest.php (2)
src/Database/Database.php (5)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php (1)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
tests/e2e/Adapter/Scopes/SpatialTests.php (1)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Database.php (4)
src/Database/Validator/Authorization.php (4)
  • Authorization (8-240)
  • isValid (52-85)
  • getDescription (42-45)
  • skip (175-185)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
src/Database/Adapter.php (1)
  • setAuthorization (61-66)
src/Database/Document.php (4)
  • getCreate (109-112)
  • getUpdate (117-120)
  • getDelete (125-128)
  • getPermissionsByType (145-157)
tests/e2e/Adapter/Scopes/RelationshipTests.php (5)
src/Database/Database.php (3)
  • getDatabase (786-789)
  • getAdapter (1231-1234)
  • getAuthorization (879-882)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Adapter.php (2)
  • getDatabase (178-181)
  • getSupportForRelationships (1010-1010)
src/Database/Validator/Authorization.php (2)
  • cleanRoles (117-120)
  • addRole (91-94)
src/Database/Helpers/Role.php (3)
  • Role (5-178)
  • any (159-162)
  • user (115-118)
tests/e2e/Adapter/PostgresTest.php (2)
src/Database/Database.php (6)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
  • getTenant (1084-1087)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
tests/e2e/Adapter/SharedTables/MySQLTest.php (2)
src/Database/Database.php (4)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
tests/e2e/Adapter/MySQLTest.php (5)
src/Database/Database.php (5)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
tests/e2e/Adapter/MariaDBTest.php (3)
  • getDatabase (21-53)
  • deleteColumn (55-63)
  • deleteIndex (65-73)
tests/e2e/Adapter/PoolTest.php (3)
  • getDatabase (35-76)
  • deleteColumn (78-93)
  • deleteIndex (95-110)
src/Database/Adapter.php (4)
  • getDatabase (178-181)
  • setAuthorization (61-66)
  • getNamespace (125-128)
  • deleteIndex (698-698)
tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php (1)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
tests/e2e/Adapter/PoolTest.php (2)
src/Database/Database.php (5)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
tests/e2e/Adapter/Scopes/DocumentTests.php (3)
src/Database/Database.php (3)
  • getDatabase (786-789)
  • getAuthorization (879-882)
  • Database (38-8163)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Validator/Authorization.php (6)
  • disable (202-205)
  • reset (212-215)
  • addRole (91-94)
  • removeRole (101-104)
  • cleanRoles (117-120)
  • skip (175-185)
tests/e2e/Adapter/SharedTables/PostgresTest.php (3)
src/Database/Database.php (4)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
src/Database/Adapter.php (3)
  • getDatabase (178-181)
  • setAuthorization (61-66)
  • deleteIndex (698-698)
tests/e2e/Adapter/Scopes/PermissionTests.php (3)
src/Database/Database.php (3)
  • getDatabase (786-789)
  • getAuthorization (879-882)
  • getAdapter (1231-1234)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Validator/Authorization.php (5)
  • disable (202-205)
  • reset (212-215)
  • cleanRoles (117-120)
  • addRole (91-94)
  • skip (175-185)
tests/e2e/Adapter/Scopes/CollectionTests.php (3)
src/Database/Database.php (2)
  • getDatabase (786-789)
  • getAuthorization (879-882)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Validator/Authorization.php (2)
  • skip (175-185)
  • addRole (91-94)
tests/e2e/Adapter/SharedTables/SQLiteTest.php (3)
src/Database/Database.php (6)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
  • getTenant (1084-1087)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
src/Database/Adapter/SQLite.php (1)
  • deleteIndex (491-510)
src/Database/Validator/Authorization.php (2)
src/Database/Database.php (1)
  • Database (38-8163)
src/Database/Validator/Authorization/Input.php (3)
  • Input (5-49)
  • getPermissions (40-43)
  • getAction (45-48)
tests/unit/Validator/AuthorizationTest.php (2)
src/Database/Validator/Authorization.php (14)
  • Authorization (8-240)
  • addRole (91-94)
  • isValid (52-85)
  • getDescription (42-45)
  • isRole (127-130)
  • cleanRoles (117-120)
  • disable (202-205)
  • reset (212-215)
  • setDefaultStatus (139-143)
  • enable (192-195)
  • getRoles (109-112)
  • removeRole (101-104)
  • skip (175-185)
  • getStatus (161-164)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
tests/e2e/Adapter/MirrorTest.php (3)
src/Database/Database.php (6)
  • getDatabase (786-789)
  • setAuthorization (867-872)
  • setDatabase (771-776)
  • delete (1331-1345)
  • exists (1303-1308)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
src/Database/Mirror.php (7)
  • Mirror (11-1102)
  • getSource (56-59)
  • setDatabase (105-110)
  • delete (192-195)
  • getDestination (61-64)
  • exists (182-185)
  • deleteIndex (514-538)
tests/e2e/Adapter/Base.php (3)
src/Database/Validator/Authorization.php (3)
  • Authorization (8-240)
  • addRole (91-94)
  • setDefaultStatus (139-143)
src/Database/Database.php (2)
  • getDatabase (786-789)
  • Database (38-8163)
tests/e2e/Adapter/MariaDBTest.php (1)
  • getDatabase (21-53)
bin/tasks/query.php (3)
src/Database/Database.php (2)
  • Database (38-8163)
  • count (6951-6996)
src/Database/Validator/Authorization.php (3)
  • Authorization (8-240)
  • addRole (91-94)
  • getRoles (109-112)
src/Database/Adapter/SQL.php (1)
  • count (2524-2586)
tests/e2e/Adapter/MariaDBTest.php (2)
src/Database/Database.php (5)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
🪛 GitHub Actions: CodeQL
src/Database/Database.php

[error] 6991-6991: PHPStan: Variable $skipAuth on left side of ?? always exists and is not nullable.

tests/e2e/Adapter/PostgresTest.php

[error] 56-56: Binary operation "." between '"' and Utopia\Database\Database results in an error.

tests/e2e/Adapter/MySQLTest.php

[error] 63-63: Binary operation "." between '`' and Utopia\Database\Database results in an error.

tests/e2e/Adapter/PoolTest.php

[error] 80-80: Binary operation "." between '`' and Utopia\Database\Database results in an error.


[error] 97-97: Binary operation "." between '`' and Utopia\Database\Database results in an error.

🪛 GitHub Actions: Linter
tests/e2e/Adapter/Scopes/GeneralTests.php

[error] 1-1: PSR 12: 'no_unused_imports' issue.

src/Database/Mirror.php

[error] 1-1: PSR 12: 'no_unused_imports' and 'no_whitespace_in_blank_line' issues.

src/Database/Validator/Authorization/Input.php

[error] 1-1: PSR 12: 'single_blank_line_at_eof' issue.

tests/e2e/Adapter/Scopes/AttributeTests.php

[error] 1-1: PSR 12: 'no_unused_imports' issue.

src/Database/Database.php

[error] 1-1: PSR 12: 'single_space_around_construct' and 'no_trailing_whitespace' issues.

tests/e2e/Adapter/Scopes/RelationshipTests.php

[error] 1-1: PSR 12: 'no_unused_imports' issue.

tests/e2e/Adapter/Scopes/DocumentTests.php

[error] 1-1: PSR 12: 'no_unused_imports' issue.

tests/e2e/Adapter/Scopes/CollectionTests.php

[error] 1-1: PSR 12: 'single_line_after_imports' issue.

src/Database/Validator/Authorization.php

[error] 1-1: PSR 12: 'single_space_around_construct' and related style issues.

tests/unit/Validator/AuthorizationTest.php

[error] 1-1: PSR 12: 'single_blank_line_at_eof' issue.

bin/tasks/query.php

[error] 1-1: PSR 12: 'ordered_imports' rule violation.

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

15-16: Adapter authorization plumbing looks good.

Import, property, and fluent setter are consistent; subclasses can use protected $authorization. No blockers.

Also applies to: 51-66

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

59-61: Switched to instance-based database access in tests — good.

Using $this->getDatabase() and $this->deleteIndex() aligns with the new instance model.

Also applies to: 100-104, 162-168, 303-306, 331-333, 358-361, 369-372, 377-379, 407-410, 441-445, 470-472

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

23-24: Instance-based DB access updates look correct.

All updated calls use $this->getDatabase(), consistent with the refactor.

Also applies to: 448-449, 1010-1011, 1109-1111, 1201-1203, 1302-1304, 1493-1495, 1574-1576, 1658-1660, 1701-1702, 1766-1768, 1834-1836, 1903-1904, 1969-1971, 2294-2296, 2380-2381

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

20-21: Consistent migration to $this->getDatabase() in M2O tests.

Changes align with instance-based initialization.

Also applies to: 354-355, 822-823, 923-925, 1034-1035, 1207-1209, 1290-1291, 1400-1401, 1466-1467, 1533-1534, 1601-1602, 1684-1685, 1766-1767

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

20-21: LGTM: migrated to instance-based DB access

Using $this->getDatabase() is consistent with the instance authorization refactor. No issues spotted in these initializations.

Also applies to: 392-393, 843-844, 1002-1003, 1134-1135, 1227-1228, 1322-1323, 1436-1437, 1528-1529, 1571-1572, 1637-1638, 1705-1706, 1773-1774, 1839-1840, 2022-2023, 2080-2081, 2161-2162

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

21-21: LGTM: instance-based DB access throughout Spatial tests

The transition to $this->getDatabase() looks correct and consistent with the PR’s authorization changes.

Also applies to: 95-96, 247-248, 352-353, 400-401, 512-513, 617-618, 722-723, 919-920, 1349-1350, 1479-1480, 1781-1782, 1868-1869, 1954-1955, 2058-2059, 2163-2164, 2233-2234, 2363-2364, 2446-2447, 2487-2488, 2539-2540, 2575-2576, 2637-2638, 2667-2668, 2758-2759

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

20-20: Instance-based database access LGTM

Good migration to $this->getDatabase(); no behavior changes.

Also applies to: 331-331, 785-785, 896-896, 996-996, 1110-1110, 1214-1214, 1257-1257, 1323-1323, 1391-1391, 1459-1459, 1525-1525, 1603-1603, 1724-1724, 1801-1801, 1886-1886

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

62-62: Instance-based Database/Authorization usage LGTM

Consistent $this->getDatabase() and $database->getAuthorization() adoption across tests looks good.

Also applies to: 161-161, 184-184, 195-195, 205-205, 229-229, 292-292, 346-346, 393-393, 416-416, 450-450, 512-512, 650-650, 774-774, 787-787, 796-796, 948-948, 1026-1026, 1188-1188, 1209-1209, 1243-1247, 1565-1565, 1655-1655, 1712-1712, 1723-1723, 1743-1743, 1768-1768, 1783-1783, 1810-1810, 1871-1871, 1979-1979, 2010-2010, 2039-2039

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

26-26: Instance-based Database/Authorization usage LGTM

Good migration to $this->getDatabase() and auth->skip closures; reads clean and consistent.

Also applies to: 45-45, 80-80, 312-312, 350-350, 363-363, 455-455, 497-497, 539-539, 597-597, 653-653, 727-727, 762-762, 782-782, 795-795, 901-901, 930-930, 947-947, 985-985, 1064-1064, 1248-1248, 1268-1268, 1332-1334, 404-409

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

2339-2339: Authorization wiring confirmed
Adapter declares protected Authorization $authorization and Database’s constructor invokes setAuthorization(new Authorization()), so calls to getRoles()/getStatus() are safe.

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

33-33: LGTM! Consistent conversion to instance-based authorization.

The changes successfully convert from static Authorization::setRole() / Authorization::cleanRoles() to instance-based $this->getDatabase()->getAuthorization()->addRole() / cleanRoles() throughout the file. This aligns well with the PR's objective of moving to instance-based authorization.

The test semantics are preserved, and the conversion is applied consistently across all test methods.

Also applies to: 407-407, 1748-1750

bin/tasks/query.php (1)

28-44: LGTM! Clean instance-based authorization pattern.

The introduction of an Authorization instance and the $setRoles closure that captures it aligns perfectly with the PR's goal of moving to instance-based authorization. The closure correctly adds roles cumulatively to test query performance with increasing role counts, which appears to be the intended benchmark behavior.

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

206-208: LGTM! Consistent instance-based authorization pattern.

The conversion from static Authorization::cleanRoles() / Authorization::setRole() to instance-based $this->getDatabase()->getAuthorization()->cleanRoles() / addRole() is consistent throughout the file and aligns with the PR's objectives.

The test logic is preserved, and the changes correctly implement the new instance-based authorization access pattern.

Note: Similar to RelationshipTests.php, there are multiple repeated calls to $this->getDatabase()->getAuthorization() in some methods (e.g., lines 206-207, 230-236) that could optionally be refactored to use a local variable for better readability, but this is a minor nitpick.

Also applies to: 230-236


322-346: Authorization skip pattern is correctly implemented.

The skip() method in src/Database/Validator/Authorization.php (lines 175–185) properly uses a try-finally block to restore authorization state, guaranteeing restoration even if the callback throws an exception. The implementation is safe for nested calls and exception scenarios. No changes needed.

src/Database/Database.php (2)

420-421: Centralized Authorization instance wiring looks solid

Default auth instantiation + setAuthorization propagation to Adapter is correct and consistent with the new instance-based model.

Also applies to: 867-872


3536-3541: Consistent READ gating via Authorization Input

Good use of Input(action, permissions) combining collection READ and document READ when documentSecurity is enabled. Returning empty Document on denial preserves prior semantics.

Also applies to: 3563-3569

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

21-21: Tests aligned to instance‑based Database and Authorization

Instance getDatabase(), auth injection via setAuthorization(...), and instance helpers for DDL look correct for the new model.

Also applies to: 41-42, 55-58, 65-68

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

110-116: LGTM on instance callsite updates

Switching to $this->getDatabase() across tests is consistent and clear.

Also applies to: 122-129, 136-146, 154-182, 184-196, 205-236, 247-285, 287-315

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

12-21: Solid migration to instance-based Authorization with Input

Coverage of roles, status defaults, reset/enable/disable, and skip (incl. nested) looks good.

Also applies to: 29-101, 106-125

tests/e2e/Adapter/SharedTables/SQLiteTest.php (1)

33-71: Instance migration and auth injection look correct

Initialization, cache reset, and fluent chain are consistent with Base and core.

Also applies to: 55-61

tests/e2e/Adapter/SharedTables/MySQLTest.php (1)

33-68: Instance getDatabase + auth injection look good

Setup aligns with Base and other adapters.

Also applies to: 52-59

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

40-40: Authorization injection looks good

Chaining setAuthorization(self::$authorization) aligns with the new instance-based flow.

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

32-35: Static Authorization holder introduced — OK

Centralizing a shared Authorization for tests is fine.


40-40: Abstract methods migrated to instance — OK

Signature matches new usage across tests.


69-70: Tear down status reset — OK

Status reset to default is appropriate.

tests/e2e/Adapter/SharedTables/MariaDBTest.php (3)

50-57: Authorization injection and init chain — OK

Fluent initialization with setAuthorization(self::$authorization) is correct.


68-76: deleteColumn SQL assembly — OK

Uses database and namespace correctly for MariaDB.


78-86: deleteIndex SQL assembly — OK

Index drop statement is correct for MariaDB.

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

63-65: Authorization injection — OK

Wiring setAuthorization(self::$authorization) looks correct.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
bin/tasks/relationships.php (1)

191-205: Fix PDOPool connection leak on exceptions

On errors, the PDO proxy isn’t returned to the pool, causing resource exhaustion under concurrency. Return it in finally.

Apply:

-            go(function () use ($cfg, $pool, $name, $namespace, $sharedTables, $cache, $globalDocs) {
-                try {
-                    $pdo = $pool->get();
-
-                    $database = (new Database(new ($cfg['adapter'])($pdo), $cache))
-                        ->setDatabase($name)
-                        ->setNamespace($namespace)
-                        ->setSharedTables($sharedTables);
-
-                    createRelationshipDocuments($database, $globalDocs['categories'], $globalDocs['users']);
-                    $pool->put($pdo);
-                } catch (\Throwable $error) {
-                    // Errors caught but documents still created successfully - likely concurrent update race conditions
-                }
-            });
+            go(function () use ($cfg, $pool, $name, $namespace, $sharedTables, $cache, $globalDocs) {
+                $pdo = null;
+                try {
+                    $pdo = $pool->get();
+                    $database = (new Database(new ($cfg['adapter'])($pdo), $cache))
+                        ->setDatabase($name)
+                        ->setNamespace($namespace)
+                        ->setSharedTables($sharedTables);
+                    // Optional: be explicit about auth context
+                    // $database->getAuthorization()->addRole(Role::any()->toString());
+                    createRelationshipDocuments($database, $globalDocs['categories'], $globalDocs['users']);
+                } catch (\Throwable $error) {
+                    // swallow; concurrent races are acceptable in this generator
+                } finally {
+                    if ($pdo !== null) {
+                        $pool->put($pdo);
+                    }
+                }
+            });
♻️ Duplicate comments (2)
src/Database/Database.php (1)

6973-6992: Fix conditional: remove null‑coalescing on $skipAuth (breaks static analysis)

$skipAuth is a bool; ?? false is redundant and flagged by PHPStan. Use a plain ternary.

Apply this diff:

-        $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead()));
+        $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead()));
         ...
-        $getCount = fn () => $this->adapter->count($collection, $queries, $max);
-        $count = $skipAuth ?? false ? $this->authorization->skip($getCount) : $getCount();
+        $getCount = fn () => $this->adapter->count($collection, $queries, $max);
+        $count = $skipAuth ? $this->authorization->skip($getCount) : $getCount();
#!/bin/bash
# Scan for any remaining incorrect patterns like "?? false ? $this->authorization->skip(" across the repo.
rg -n -C2 -P '\?\?\s*false\s*\?\s*\$this->authorization->skip\s*\('
tests/unit/Validator/AuthorizationTest.php (1)

126-126: Fix PSR-12: ensure single blank line at EOF.

This issue was previously flagged and remains unresolved. Add a trailing newline at end of file to satisfy the linter.

-}
+}
+
🧹 Nitpick comments (5)
tests/e2e/Adapter/Scopes/GeneralTests.php (2)

90-93: Consider storing the database reference once.

Lines 90 and 93 both call $this->getDatabase(). While not incorrect, storing the database reference in a local variable once would be more efficient.

Apply this diff to consolidate the calls:

-        $this->getDatabase()->getAuthorization()->disable();
-
-        /** @var Database $database */
-        $database = $this->getDatabase();
+        /** @var Database $database */
+        $database = $this->getDatabase();
+        $database->getAuthorization()->disable();

185-188: Consider storing the database reference once.

Lines 185 and 188 both call $this->getDatabase(). While not incorrect, storing the database reference in a local variable once would be more efficient.

Apply this diff to consolidate the calls:

-        $this->getDatabase()->getAuthorization()->disable();
-
-        /** @var Database $database */
-        $database = $this->getDatabase();
+        /** @var Database $database */
+        $database = $this->getDatabase();
+        $database->getAuthorization()->disable();
src/Database/Database.php (1)

861-872: Return type nit: prefer static for fluent API consistency

Most fluent setters in this class return static. Consider changing setAuthorization(...): self to static for consistency and better subclass chaining. Behavior unchanged.

-    public function setAuthorization(Authorization $authorization): self
+    public function setAuthorization(Authorization $authorization): static
     {
         $this->adapter->setAuthorization($authorization);
         $this->authorization = $authorization;
         return $this;
     }
bin/tasks/query.php (2)

37-42: Make generated roles unique to avoid collisions

Faker numerify can repeat; collisions skew role counts. Use Faker’s unique() (and increase digits) to ensure uniqueness.

Apply:

-        $setRoles = function (Authorization $authorization, $faker, $count): int {
+        $setRoles = function (Authorization $authorization, $faker, $count): int {
             for ($i = 0; $i < $count; $i++) {
-                $authorization->addRole($faker->numerify('user####'));
+                $authorization->addRole($faker->unique()->numerify('user########'));
             }
             return \count($authorization->getRoles());
         };

142-145: Harden results persistence (error handling and atomic write)

Use file_put_contents with LOCK_EX and check JSON encoding errors.

Apply:

-        $results = \fopen("bin/view/results/{$adapter}_{$name}_{$limit}_{$time}.json", 'w');
-        \fwrite($results, \json_encode($report));
-        \fclose($results);
+        $path = "bin/view/results/{$adapter}_{$name}_{$limit}_{$time}.json";
+        $json = \json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+        if ($json === false) {
+            Console::error('Failed to encode report: ' . \json_last_error_msg());
+            return;
+        }
+        if (\file_put_contents($path, $json, LOCK_EX) === false) {
+            Console::error("Failed to write results to {$path}");
+        }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8007d6f and 59153b1.

📒 Files selected for processing (14)
  • bin/tasks/load.php (3 hunks)
  • bin/tasks/query.php (3 hunks)
  • bin/tasks/relationships.php (3 hunks)
  • bin/view/results/mariadb_testing_1000_1760626673.json (1 hunks)
  • src/Database/Database.php (29 hunks)
  • src/Database/Mirror.php (1 hunks)
  • src/Database/Validator/Authorization.php (4 hunks)
  • src/Database/Validator/Authorization/Input.php (1 hunks)
  • tests/e2e/Adapter/Scopes/AttributeTests.php (41 hunks)
  • tests/e2e/Adapter/Scopes/CollectionTests.php (29 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (126 hunks)
  • tests/e2e/Adapter/Scopes/GeneralTests.php (9 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (36 hunks)
  • tests/unit/Validator/AuthorizationTest.php (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • bin/view/results/mariadb_testing_1000_1760626673.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/Database/Mirror.php
  • src/Database/Validator/Authorization/Input.php
  • tests/e2e/Adapter/Scopes/AttributeTests.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
  • bin/tasks/load.php
🧰 Additional context used
🧬 Code graph analysis (8)
tests/e2e/Adapter/Scopes/CollectionTests.php (4)
src/Database/Database.php (2)
  • getDatabase (786-789)
  • getAuthorization (879-882)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Validator/Authorization.php (2)
  • skip (175-185)
  • addRole (91-94)
src/Database/Helpers/Role.php (2)
  • Role (5-178)
  • label (149-152)
tests/unit/Validator/AuthorizationTest.php (3)
src/Database/Database.php (1)
  • Database (38-8163)
src/Database/Validator/Authorization.php (14)
  • Authorization (8-240)
  • addRole (91-94)
  • isValid (52-85)
  • getDescription (42-45)
  • isRole (127-130)
  • cleanRoles (117-120)
  • disable (202-205)
  • reset (212-215)
  • setDefaultStatus (139-143)
  • enable (192-195)
  • getRoles (109-112)
  • removeRole (101-104)
  • skip (175-185)
  • getStatus (161-164)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
tests/e2e/Adapter/Scopes/RelationshipTests.php (5)
src/Database/Database.php (2)
  • getDatabase (786-789)
  • getAuthorization (879-882)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Adapter.php (2)
  • getDatabase (178-181)
  • getSupportForRelationships (1010-1010)
src/Database/Validator/Authorization.php (2)
  • cleanRoles (117-120)
  • addRole (91-94)
src/Database/Helpers/Role.php (3)
  • Role (5-178)
  • any (159-162)
  • user (115-118)
bin/tasks/query.php (3)
src/Database/Validator/Authorization.php (3)
  • Authorization (8-240)
  • addRole (91-94)
  • getRoles (109-112)
src/Database/Database.php (2)
  • count (6951-6996)
  • getAuthorization (879-882)
src/Database/Adapter/SQL.php (1)
  • count (2524-2586)
tests/e2e/Adapter/Scopes/GeneralTests.php (5)
src/Database/Database.php (3)
  • getDatabase (786-789)
  • getAuthorization (879-882)
  • getAdapter (1231-1234)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (40-40)
src/Database/Adapter.php (2)
  • getDatabase (178-181)
  • getSupportForCacheSkipOnFailure (1047-1047)
src/Database/Validator/Authorization.php (4)
  • disable (202-205)
  • reset (212-215)
  • cleanRoles (117-120)
  • addRole (91-94)
src/Database/Helpers/Role.php (2)
  • Role (5-178)
  • any (159-162)
bin/tasks/relationships.php (3)
src/Database/Database.php (2)
  • Database (38-8163)
  • getAuthorization (879-882)
src/Database/Validator/Authorization.php (1)
  • addRole (91-94)
src/Database/Helpers/Role.php (2)
  • Role (5-178)
  • any (159-162)
src/Database/Database.php (3)
src/Database/Validator/Authorization.php (4)
  • Authorization (8-240)
  • isValid (52-85)
  • getDescription (42-45)
  • skip (175-185)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
src/Database/Adapter.php (1)
  • setAuthorization (61-66)
src/Database/Validator/Authorization.php (2)
src/Database/Database.php (1)
  • Database (38-8163)
src/Database/Validator/Authorization/Input.php (3)
  • Input (5-49)
  • getPermissions (40-43)
  • getAction (45-48)
🪛 GitHub Actions: CodeQL
src/Database/Database.php

[error] 6991-6991: PHPStan: Variable $skipAuth on left side of ?? always exists and is not nullable.

🔇 Additional comments (33)
tests/e2e/Adapter/Scopes/GeneralTests.php (7)

27-27: LGTM!

The migration from static::getDatabase() to $this->getDatabase() is correct.


46-46: LGTM!

The migration from static::getDatabase() to $this->getDatabase() is correct.


180-180: LGTM!

The migration to instance-based authorization reset is correct.


285-285: LGTM!

The migration to instance-based authorization reset is correct.


310-310: LGTM!

The migration from static::getDatabase() to $this->getDatabase() is correct.


369-369: LGTM!

The migration from static::getDatabase() to $this->getDatabase() is correct.


424-424: LGTM!

The migration from static::getDatabase() to $this->getDatabase() is correct.

src/Database/Database.php (19)

28-28: Import for Authorization Input is correct

New Input class is properly imported for instance-based checks.


401-405: Private Authorization instance on Database

Good encapsulation; aligns all checks through a single instance.


420-421: Initialize default Authorization and wire to adapter

Constructing and setting the default Authorization in ctor is correct and avoids null access.


3536-3541: Read check on cached document path

Using instance Authorization with Input(action: read, merged collection/document permissions) is correct. Early-return with empty Document on deny is consistent.


3563-3567: Read check on fresh document path

Same as cached path; consistent permission enforcement.


4229-4232: Create permission gate on createDocument

Collection-level create validation via Authorization::isValid(Input) is correct; throwing AuthorizationException with validator description is appropriate.


4336-4338: Create permission gate on createDocuments (batch)

Batch path mirrors single-create semantics; good.


4763-4766: Bypass auth to fetch “old” document for update

Wrapping the for-update read with authorization->skip prevents spurious read-denials while still checking update/read permissions later. Good pattern.


4889-4907: Update vs Read permission check split

Building update/read permission sets and validating only when needed (based on actual changes) is efficient and correct.


4998-5002: Short-circuit when lacking collection-level update and no document security

Correctly blocks mass updates when docSecurity is off and caller lacks collection-level update.


5661-5670: Upsert: old-document fetch under auth skip

Correctly skips authorization to probe existence (including per-tenant variant), deferring actual auth checks to create/update paths.


5701-5709: Upsert: permission checks per operation

Validates create vs update with merged doc permissions when documentSecurity is enabled; correct behavior.


5801-5805: Upsert: wrap adapter call in authorization->skip

Avoids redundant per-row auth during engine upsert; higher-level checks already enforced.


5901-5915: Increase attribute: auth-skip read + update permission check

Pattern matches updateDocument. Good separation of concerns.


5999-6013: Decrease attribute: auth-skip read + update permission check

Mirrors increase path; correct.


6065-6081: Delete: auth-skip read-for-update + delete permission check

Correct to fetch under skip then enforce delete permissions (collection and document when applicable).


6244-6272: Cascade helpers: use authorization->skip for internal maintenance writes

Using skip in deleteRestrict to clear inverse keys avoids requiring user write privileges for system-enforced integrity.


6305-6371: Cascade helpers (setNull): all maintenance updates under auth skip

Consistently skipping authorization for integrity updates is appropriate.


6751-6756: find(): collection-level bypass via skipAuth

Computing skipAuth per requested permission and conditionally wrapping adapter call in authorization->skip is correct and mirrors other paths.

Also applies to: 6831-6832

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

12-12: LGTM! Clean setup for instance-based authorization.

The introduction of the Input import and the protected $authorization property, initialized in setUp, correctly establishes the instance-based authorization pattern. This aligns well with the broader refactoring effort to move away from static authorization state.

Also applies to: 16-21


29-101: LGTM! Thorough migration to instance-based API.

The refactoring correctly replaces all static authorization calls with instance methods:

  • Role management: addRole, removeRole, cleanRoles, isRole, getRoles
  • Status management: disable, enable, reset, setDefaultStatus, getStatus
  • Validation: All isValid calls properly wrap permissions with new Input(action, permissions)
  • Skip functionality: Correctly uses instance method with Input wrapper

The test logic remains comprehensive and validates all authorization behaviors with the new API.


104-125: LGTM! Nested skip test correctly uses instance API.

The nested skip test properly validates that the authorization status is correctly managed across multiple skip levels, using $this->authorization->getStatus() to check the state and $this->authorization->skip() for nesting.

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

1748-1749: LGTM: migrated to instance-based Authorization in tests

Clean, instance-scoped role management via $this->getDatabase()->getAuthorization() matches the new API and improves isolation.

Also applies to: 1825-1826, 1838-1839

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

403-407: LGTM: correct use of Authorization->skip for privileged cleanup

Wrapping deletes in skip avoids false auth failures and keeps tests fast and deterministic.


919-924: LGTM: label role test aligns with per-instance roles

Adding Role::label('reader') on the instance precisely validates label-based access.

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

52-85: LGTM: solid instance-scoped Authorization with Input

  • Type-safe isValid(Input) with clear messages.
  • Per-instance roles/status; safe skip() restore.
  • API (add/remove/clean/getRoles, enable/disable/reset) looks coherent.

Also applies to: 91-120, 151-165, 175-215

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

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

7033-7051: Bug: sum() missing authorization gating and exception path (inconsistent with find()/count())

sum() neither throws when collection has no doc security nor bypasses engine‑level filtering when collection scope exists. This can yield incorrect sums or inconsistent errors.

Apply this diff to align with find()/count():

     public function sum(string $collection, string $attribute, array $queries = [], ?int $max = null): float|int
     {
         $collection = $this->silent(fn () => $this->getCollection($collection));
         $attributes = $collection->getAttribute('attributes', []);
         $indexes = $collection->getAttribute('indexes', []);
@@
         if ($this->validate) {
             $validator = new DocumentsValidator(
                 $attributes,
                 $indexes,
                 $this->adapter->getIdAttributeType(),
                 $this->maxQueryValues,
                 $this->adapter->getMinDateTime(),
                 $this->adapter->getMaxDateTime(),
             );
             if (!$validator->isValid($queries)) {
                 throw new QueryException($validator->getDescription());
             }
         }
 
+        $documentSecurity = $collection->getAttribute('documentSecurity', false);
+        $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead()));
+        if (!$skipAuth && !$documentSecurity && $collection->getId() !== self::METADATA) {
+            throw new AuthorizationException($this->authorization->getDescription());
+        }
+
         $relationships = \array_filter(
             $collection->getAttribute('attributes', []),
             fn (Document $attribute) => $attribute->getAttribute('type') === self::VAR_RELATIONSHIP
         );
 
         $queries = $this->convertQueries($collection, $queries);
@@
-        $sum = $this->adapter->sum($collection, $attribute, $queries, $max);
+        $getSum = fn () => $this->adapter->sum($collection, $attribute, $queries, $max);
+        $sum = $skipAuth ? $this->authorization->skip($getSum) : $getSum();
 
         $this->trigger(self::EVENT_DOCUMENT_SUM, $sum);
 
         return $sum;
     }
♻️ Duplicate comments (1)
tests/e2e/Adapter/PostgresTest.php (1)

66-68: Fix null tenant handling and Postgres DROP INDEX schema qualifier.

Two issues remain unresolved from the previous review:

  1. Line 66: When getTenant() returns null, the key becomes "namespace__collection_index" (double underscore). Handle the null case explicitly:

  2. Line 68: Postgres DROP INDEX expects schema qualification, not database qualification. Use $this->getDatabase()->getNamespace() instead of $this->getDatabase()->getDatabase().

Apply this diff to fix both issues:

-        $key = "\"".$this->getDatabase()->getNamespace()."_".$this->getDatabase()->getTenant()."_{$collection}_{$index}\"";
+        $tenant = $this->getDatabase()->getTenant();
+        $tenantPart = $tenant ? "{$tenant}_" : "";
+        $key = "\"".$this->getDatabase()->getNamespace()."_{$tenantPart}{$collection}_{$index}\"";

-        $sql = "DROP INDEX \"".$this->getDatabase()->getDatabase()."\".{$key}";
+        $sql = "DROP INDEX \"".$this->getDatabase()->getNamespace()."\".{$key}";
🧹 Nitpick comments (2)
src/Database/Database.php (2)

861-883: Unify setter return type: prefer static for fluency

Most setters in this class return static. Align setAuthorization for consistency and better subclass fluency.

-    /**
-     * Sets instance of authorization for permission checks
-     *
-     * @param Authorization $authorization
-     * @return self
-     */
-    public function setAuthorization(Authorization $authorization): self
+    /**
+     * Sets instance of authorization for permission checks
+     *
+     * @param Authorization $authorization
+     * @return static
+     */
+    public function setAuthorization(Authorization $authorization): static
     {
         $this->adapter->setAuthorization($authorization);
         $this->authorization = $authorization;
         return $this;
     }

6245-6245: Set‑null/Restrict operations use authorization->skip appropriately; verify cascade intent

Set‑null/Restrict paths bypass user perms for integrity writes. However, cascade delete path (Lines 6417‑6489) calls deleteDocument(...) without authorization->skip, requiring delete permission on related docs.

Would you like cascades to bypass user perms as well (consistent with set‑null/restrict)? If yes, wrap those deleteDocument calls in authorization->skip.

- $this->deleteDocument(
+ $this->authorization->skip(fn () => $this->deleteDocument(
     $relatedCollection->getId(),
-    ($value instanceof Document) ? $value->getId() : $value
- );
+    ($value instanceof Document) ? $value->getId() : $value
+ ));

Also applies to: 6269-6269, 6305-6305, 6337-6337, 6363-6363

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59153b1 and 85a2e71.

📒 Files selected for processing (4)
  • src/Database/Database.php (29 hunks)
  • tests/e2e/Adapter/MySQLTest.php (3 hunks)
  • tests/e2e/Adapter/PoolTest.php (4 hunks)
  • tests/e2e/Adapter/PostgresTest.php (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/Adapter/PoolTest.php
🧰 Additional context used
🧬 Code graph analysis (3)
tests/e2e/Adapter/PostgresTest.php (3)
src/Database/Database.php (5)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
src/Database/Adapter/Postgres.php (1)
  • deleteIndex (909-922)
src/Database/Database.php (4)
src/Database/Validator/Authorization.php (4)
  • Authorization (8-240)
  • isValid (52-85)
  • getDescription (42-45)
  • skip (175-185)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
src/Database/Adapter.php (8)
  • setAuthorization (61-66)
  • updateDocument (743-743)
  • deleteDocument (791-791)
  • getTenant (234-237)
  • getDocument (709-709)
  • withTransaction (395-442)
  • upsertDocuments (770-774)
  • count (843-843)
src/Database/Adapter/SQL.php (3)
  • getDocument (350-416)
  • upsertDocuments (2109-2266)
  • count (2524-2586)
tests/e2e/Adapter/MySQLTest.php (3)
src/Database/Database.php (5)
  • getDatabase (786-789)
  • Database (38-8163)
  • setAuthorization (867-872)
  • getNamespace (758-761)
  • deleteIndex (3439-3464)
tests/e2e/Adapter/PoolTest.php (3)
  • getDatabase (35-76)
  • deleteColumn (78-93)
  • deleteIndex (95-110)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (40-40)
  • deleteColumn (48-48)
  • deleteIndex (56-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Setup & Build Docker Image
🔇 Additional comments (15)
tests/e2e/Adapter/MySQLTest.php (3)

27-58: LGTM! Instance-based refactoring applied correctly.

The conversion from static to instance method is correct, and the authorization is properly wired through setAuthorization(self::$authorization).


61-69: LGTM! Database concatenation issue resolved.

The string concatenation now correctly calls $this->getDatabase()->getDatabase() to obtain the database name string instead of concatenating the Database object directly. This resolves the critical issue flagged in the previous review.


71-79: LGTM! Database concatenation issue resolved.

Similar to deleteColumn, the string concatenation correctly uses $this->getDatabase()->getDatabase() to obtain the database name string.

tests/e2e/Adapter/PostgresTest.php (2)

21-51: LGTM! Instance-based refactoring applied correctly.

The conversion from static to instance method is correct, and authorization is properly wired through setAuthorization(self::$authorization).


54-62: LGTM! Database concatenation issue resolved.

The string concatenation now correctly calls $this->getDatabase()->getDatabase() to obtain the database name string instead of concatenating the Database object directly. This resolves the critical issue flagged in the previous review.

src/Database/Database.php (10)

400-405: Authorization instance wiring looks good

Private Authorization property, default initialization, and propagation to Adapter are correct and cohesive.

Also applies to: 420-421


3536-3543: Read checks on cached and fresh getDocument are correct

Union of collection/document read scopes via Input and returning empty Document on denial is consistent.

Also applies to: 3563-3569


4229-4232: Create permission checks correctly migrated to instance Authorization

Simple and batch create both validate via Input(PERMISSION_CREATE, $collection->getCreate()) and throw on denial. Good.

Also applies to: 4336-4339


4763-4766: Update flow: safe read for update and proper gate on read/update

  • Reading old doc under authorization->skip avoids requiring read permission just to update.
  • Conditional check for UPDATE vs READ based on actual changes is solid.

Also applies to: 4889-4907


5369-5369: Relationship updates honor internal integrity without leaking permissions

Using authorization->skip for side-effect updates/deletes on related docs/junctions is appropriate.

Also applies to: 5496-5496


5661-5670: Upsert authorization checks and adapter call strategy look correct

  • Old-fetch under skip is safe.
  • Create vs Update checks via Input mirror single-doc flows.
  • Wrapping adapter upsert in authorization->skip avoids redundant engine-level filtering after prechecks.

Also applies to: 5701-5709, 5801-5805


5901-5901: Increase/Decrease attribute: permission and locking behavior is correct

Locks with forUpdate, validates update scopes (collection + optional document), then mutates. Good.

Also applies to: 5910-5915, 5999-5999, 6008-6013


6065-6077: Delete: secure locked read and delete permission gate are correct

Fetching under skip with forUpdate, then checking delete scopes (collection + optional document) is sound.


6751-6756: Find: collection‑level bypass vs document‑level filtering is consistent

  • Throws when no collection scope and no doc security.
  • Uses authorization->skip to avoid engine‑level permission predicates when collection scope exists. Matches new model.

Also applies to: 6831-6831


6973-6992: Count: aligns with find’s authorization semantics

Collection‑scope check + skip around adapter call mirrors find(); good parity.

…ion method in Database Pool class for improved authorization handling.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

573-577: Implementation correctly follows the Adapter pattern.

The setter properly stores the authorization instance and returns $this for method chaining, consistent with the parent class implementation.

For consistency with other imports in the file, consider adding an import statement instead of using the fully qualified class name:

 use Utopia\Database\Adapter;
 use Utopia\Database\Database;
 use Utopia\Database\Document;
 use Utopia\Database\Exception as DatabaseException;
+use Utopia\Database\Validator\Authorization;
 use Utopia\Pools\Pool as UtopiaPool;

Then update the method signature:

-    public function setAuthorization(\Utopia\Database\Validator\Authorization $authorization): self
+    public function setAuthorization(Authorization $authorization): self
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85a2e71 and ae1f07c.

📒 Files selected for processing (2)
  • phpunit.xml (1 hunks)
  • src/Database/Adapter/Pool.php (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • phpunit.xml
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Adapter/Pool.php (3)
src/Database/Database.php (2)
  • setAuthorization (867-872)
  • Database (38-8163)
src/Database/Adapter.php (1)
  • setAuthorization (61-66)
src/Database/Validator/Authorization.php (1)
  • Authorization (8-240)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (Pool)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

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

1826-1826: Fix inconsistent database access pattern.

This line uses static::getDatabase() while all other test methods in this PR use $this->getDatabase(). Update for consistency with the instance-based refactor.

Apply this diff:

-        $database = static::getDatabase();
+        $database = $this->getDatabase();
tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (1)

2221-2221: Fix inconsistent database access pattern.

These lines use static::getDatabase() while all other test methods in this PR use $this->getDatabase(). Update for consistency with the instance-based refactor.

Apply this diff to all five occurrences:

-        $database = static::getDatabase();
+        $database = $this->getDatabase();

Also applies to: 2326-2326, 2424-2424, 2540-2540, 2595-2595

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

585-597: Use try/finally to guarantee authorization reset

If upsertDocuments ever throws, the authorization status stays disabled for everything that follows, which will corrupt this test and subsequent ones. Capture the instance once and reset it in a finally block.

-        $this->getDatabase()->getAuthorization()->disable();
-
-        $results = [];
-        $count = $database->upsertDocuments(
-            __FUNCTION__,
-            $documents,
-            onNext: function ($doc) use (&$results) {
-                $results[] = $doc;
-            }
-        );
-
-        $this->getDatabase()->getAuthorization()->reset();
+        $authorization = $database->getAuthorization();
+        $authorization->disable();
+
+        $results = [];
+        $count = 0;
+
+        try {
+            $count = $database->upsertDocuments(
+                __FUNCTION__,
+                $documents,
+                onNext: function ($doc) use (&$results) {
+                    $results[] = $doc;
+                }
+            );
+        } finally {
+            $authorization->reset();
+        }
tests/e2e/Adapter/Scopes/RelationshipTests.php (1)

3260-3260: Update for consistency with instance-based pattern.

Line 3260 uses static::getDatabase() while all other test methods in this file now use $this->getDatabase(). For consistency with the instance-based architecture introduced in this PR, consider updating this line.

Apply this diff:

-        $database = static::getDatabase();
+        $database = $this->getDatabase();
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63ba415 and 1196010.

📒 Files selected for processing (16)
  • src/Database/Adapter.php (2 hunks)
  • src/Database/Adapter/Pool.php (2 hunks)
  • src/Database/Adapter/SQL.php (6 hunks)
  • src/Database/Database.php (29 hunks)
  • tests/e2e/Adapter/Base.php (1 hunks)
  • tests/e2e/Adapter/PostgresTest.php (3 hunks)
  • tests/e2e/Adapter/Scopes/CollectionTests.php (29 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (126 hunks)
  • tests/e2e/Adapter/Scopes/IndexTests.php (11 hunks)
  • tests/e2e/Adapter/Scopes/PermissionTests.php (43 hunks)
  • tests/e2e/Adapter/Scopes/RelationshipTests.php (36 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php (16 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php (15 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (17 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php (18 hunks)
  • tests/e2e/Adapter/Scopes/SpatialTests.php (25 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/Database/Adapter/Pool.php
  • tests/e2e/Adapter/Scopes/IndexTests.php
  • tests/e2e/Adapter/Scopes/Relationships/ManyToManyTests.php
  • src/Database/Adapter.php
  • tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php
  • src/Database/Adapter/SQL.php
  • tests/e2e/Adapter/Scopes/SpatialTests.php
  • tests/e2e/Adapter/PostgresTest.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
PR: utopia-php/database#721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • tests/e2e/Adapter/Scopes/DocumentTests.php
🧬 Code graph analysis (8)
tests/e2e/Adapter/Base.php (3)
src/Database/Validator/Authorization.php (3)
  • Authorization (8-240)
  • addRole (91-94)
  • setDefaultStatus (139-143)
src/Database/Database.php (2)
  • getDatabase (837-840)
  • Database (38-8296)
tests/e2e/Adapter/PostgresTest.php (1)
  • getDatabase (21-52)
tests/e2e/Adapter/Scopes/Relationships/OneToManyTests.php (1)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (42-42)
tests/e2e/Adapter/Scopes/DocumentTests.php (3)
src/Database/Database.php (3)
  • getDatabase (837-840)
  • getAuthorization (930-933)
  • Database (38-8296)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (42-42)
src/Database/Validator/Authorization.php (6)
  • disable (202-205)
  • reset (212-215)
  • addRole (91-94)
  • removeRole (101-104)
  • cleanRoles (117-120)
  • skip (175-185)
tests/e2e/Adapter/Scopes/RelationshipTests.php (3)
src/Database/Database.php (2)
  • getDatabase (837-840)
  • getAuthorization (930-933)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (42-42)
src/Database/Validator/Authorization.php (2)
  • cleanRoles (117-120)
  • addRole (91-94)
tests/e2e/Adapter/Scopes/Relationships/ManyToOneTests.php (1)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (42-42)
tests/e2e/Adapter/Scopes/PermissionTests.php (6)
src/Database/Database.php (2)
  • getDatabase (837-840)
  • getAuthorization (930-933)
tests/e2e/Adapter/PostgresTest.php (1)
  • getDatabase (21-52)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (42-42)
tests/e2e/Adapter/MySQLTest.php (1)
  • getDatabase (27-59)
tests/e2e/Adapter/SQLiteTest.php (1)
  • getDatabase (21-56)
src/Database/Validator/Authorization.php (5)
  • disable (202-205)
  • reset (212-215)
  • cleanRoles (117-120)
  • addRole (91-94)
  • skip (175-185)
src/Database/Database.php (6)
src/Database/Validator/Authorization.php (4)
  • Authorization (8-240)
  • isValid (52-85)
  • getDescription (42-45)
  • skip (175-185)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
src/Database/Adapter/Pool.php (1)
  • setAuthorization (578-582)
src/Database/Mirror.php (1)
  • setAuthorization (1103-1116)
src/Database/Adapter.php (1)
  • setAuthorization (61-66)
src/Database/Document.php (1)
  • getPermissionsByType (145-157)
tests/e2e/Adapter/Scopes/CollectionTests.php (4)
src/Database/Database.php (2)
  • getDatabase (837-840)
  • getAuthorization (930-933)
tests/e2e/Adapter/PostgresTest.php (1)
  • getDatabase (21-52)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (42-42)
src/Database/Validator/Authorization.php (2)
  • skip (175-185)
  • addRole (91-94)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (MySQL)
🔇 Additional comments (11)
tests/e2e/Adapter/Scopes/CollectionTests.php (4)

25-25: LGTM! Database retrieval correctly migrated from static to instance method.

The consistent replacement of static::getDatabase() with $this->getDatabase() throughout all test methods aligns with the PR's refactoring objective to move from static global authorization to per-instance objects.

Also applies to: 44-44, 79-79, 311-311, 349-349, 362-362, 454-454, 495-495, 537-537, 596-596, 652-652, 726-726, 761-761, 781-781, 793-793, 900-900, 929-929, 945-945, 983-983, 1063-1063, 1247-1247, 1267-1267, 1462-1462, 1487-1487, 1506-1506, 1533-1533


403-407: LGTM! Authorization skip pattern correctly migrated.

The change from Authorization::skip(...) to $this->getDatabase()->getAuthorization()->skip(...) properly delegates authorization control to the database instance, maintaining the same callback-based skip logic.


919-919: LGTM! Role addition correctly uses instance-based API.

The migration from Authorization::setRole(...) to $this->getDatabase()->getAuthorization()->addRole(Role::label('reader')->toString()) correctly:

  • Obtains the authorization instance from the database
  • Calls the new addRole() API (replacing the old setRole())
  • Passes the role string obtained from Role::label('reader')->toString()

1331-1332: LGTM! Nested authorization skip and database retrieval correctly refactored.

The authorization skip wrapper now correctly uses the instance-based API, and the database is retrieved within the callback using the instance method. The nesting and scoping are appropriate.

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

32-32: LGTM! Consistent migration to instance-based database access.

The refactoring from static::getDatabase() to $this->getDatabase() is consistent throughout the test methods and aligns with the PR's objective of moving to instance-based architecture.

Also applies to: 406-406, 478-478, 644-644, 1100-1100, 1134-1134, 1234-1234, 1293-1293, 1368-1368, 1662-1662, 1741-1741, 1900-1900, 1921-1921, 1944-1944, 1975-1975, 2000-2000, 2018-2018, 2050-2050, 2072-2072, 2100-2100, 2202-2202, 2356-2356, 2662-2662, 2882-2882, 3017-3017, 3506-3506, 3673-3673, 3927-3927, 4036-4036, 4141-4141, 4242-4242, 4498-4498, 4659-4659


1747-1748: LGTM! Proper migration to instance-based Authorization API.

The changes correctly refactor authorization state management:

  • Authorization::cleanRoles()$this->getDatabase()->getAuthorization()->cleanRoles()
  • Authorization::setRole(...)$this->getDatabase()->getAuthorization()->addRole(...)

The API method name change from setRole() to addRole() better reflects the additive nature of role assignment, and routing through the database instance properly scopes authorization to the test instance.

Also applies to: 1825-1825, 1838-1838, 2248-2249

src/Database/Database.php (1)

918-923: Nice instance-level authorization hook.

Moving the adapter wiring into setAuthorization() keeps the Database and Adapter in sync without relying on globals, which makes swapping authorization contexts per request much cleaner.

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

19-19: LGTM! Authorization refactoring is consistent.

The migration from static to instance-based authorization is systematic and correct throughout this test file. Database access and authorization state management now properly route through per-database instances.

Also applies to: 157-159, 173-173, 206-207, 210-210, 230-230, 236-236, 244-244, 303-303, 322-346, 358-360, 389-389, 404-406, 427-427, 457-458, 461-461, 478-479, 482-482, 498-499, 503-503, 522-523, 526-526, 550-551, 556-556, 572-573, 576-576, 587-587, 603-604, 609-609, 623-624, 627-627, 652-653, 656-656, 675-676, 679-679, 697-697, 777-778, 781-781, 789-790, 798-799, 816-817, 821-821, 841-842, 847-847, 863-864, 867-867, 919-920, 923-923, 939-940, 943-943, 958-960, 974-975, 993-994, 997-997, 1016-1017, 1020-1020, 1033-1034, 1058-1059, 1064-1064, 1082-1083, 1086-1086, 1096-1097, 1118-1119, 1123-1123, 1141-1142, 1145-1145, 1164-1164, 1173-1174, 1239-1239, 1248-1248, 1255-1256

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

34-37: LGTM! Test base properly supports instance-based authorization.

The Authorization instance is correctly initialized at the test base level and propagated to database instances. The lazy initialization pattern in setUp() ensures the instance is reused across test methods.

Also applies to: 42-42, 50-50, 58-58, 62-66, 71-71

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

20-20: LGTM! Database access properly uses instance method.

These changes correctly migrate from static to instance-based database access, consistent with the broader authorization refactor.

Also applies to: 354-354, 822-822, 923-923, 1034-1034, 1136-1136, 1207-1207, 1290-1290, 1334-1334, 1400-1400, 1466-1466, 1533-1533, 1601-1601, 1684-1684, 1766-1766

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

20-20: LGTM! Database access properly uses instance method.

These changes correctly migrate from static to instance-based database access, consistent with the broader authorization refactor.

Also applies to: 392-392, 843-843, 1002-1002, 1134-1134, 1227-1227, 1322-1322, 1436-1436, 1528-1528, 1571-1571, 1637-1637, 1705-1705, 1773-1773, 1839-1839, 2022-2022, 2079-2079, 2161-2161

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

70-70: Uninitialized property access remains unresolved.

The critical issue previously identified still exists: $this->authorization is accessed without being initialized in the Pool constructor. If any method that calls delegate() is invoked before setAuthorization() is called on the Pool instance, it will cause an uninitialized property error.

Required fix: Initialize the authorization property before it's accessed. Options include:

  1. Require an Authorization parameter in Pool's constructor and either call parent::__construct($authorization) or $this->setAuthorization($authorization)
  2. Make the property nullable and add a null check: if (isset($this->authorization)) { $adapter->setAuthorization($this->authorization); }
  3. Ensure all Pool instantiation sites call setAuthorization() immediately after construction (fragile and error-prone)

Option 1 or 2 is recommended for robustness.

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

579-583: Consider calling parent method for consistency.

The implementation is functionally correct, but for consistency with the Mirror class pattern (which calls parent::setAuthorization() before additional propagation), consider refactoring to call the parent method.

 public function setAuthorization(Authorization $authorization): self
 {
-    $this->authorization = $authorization;
+    parent::setAuthorization($authorization);
     return $this;
 }

This makes the inheritance chain explicit and ensures any future logic added to Adapter::setAuthorization() will be automatically inherited.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cd048e and 5096b38.

📒 Files selected for processing (1)
  • src/Database/Adapter/Pool.php (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Adapter/Pool.php (4)
src/Database/Database.php (2)
  • Database (38-8296)
  • setAuthorization (918-923)
src/Database/Validator/Authorization.php (1)
  • Authorization (8-240)
src/Database/Mirror.php (1)
  • setAuthorization (1103-1116)
src/Database/Adapter.php (1)
  • setAuthorization (61-66)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Unit Test
🔇 Additional comments (1)
src/Database/Adapter/Pool.php (1)

9-9: LGTM!

The import is necessary for the new instance-based authorization functionality.

…rization-instance

# Conflicts:
#	src/Database/Adapter/Pool.php
#	tests/e2e/Adapter/Scopes/AttributeTests.php
#	tests/e2e/Adapter/Scopes/DocumentTests.php
#	tests/e2e/Adapter/Scopes/GeneralTests.php
#	tests/e2e/Adapter/Scopes/IndexTests.php
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

7239-7261: Fix sum(): use instance Authorization + Input; remove static call and null‑coalesce

Current code instantiates Authorization with a ctor arg, calls static skip, and uses ?? false. This violates the new instance model and breaks CI.

Apply:

-        $authorization = new Authorization(self::PERMISSION_READ);
-        if ($authorization->isValid($collection->getRead())) {
-            $skipAuth = true;
-        }
+        $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead()));
@@
-        $getSum = fn () => $this->adapter->sum($collection, $attribute, $queries, $max);
-        $sum = $skipAuth ?? false ? Authorization::skip($getSum) : $getSum();
+        $getSum = fn () => $this->adapter->sum($collection, $attribute, $queries, $max);
+        $sum = $skipAuth ? $this->authorization->skip($getSum) : $getSum();

This aligns with find()/count() and resolves the CodeQL error.

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

1899-1929: Fix method name casing: getSupportForFulltextWildcardIndex() requires lowercase 'c'.

The canonical method name across all adapters is getSupportForFulltextWildcardIndex(). Two calls in the test file incorrectly use uppercase 'C' (getSupportForFulltextWildCardIndex()), which will cause fatal errors at runtime.

Location 1 (line 1921):

-            if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildCardIndex()) {
+            if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildcardIndex()) {

Location 2 (line 3409):

-            if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildCardIndex()) {
+            if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildcardIndex()) {
♻️ Duplicate comments (2)
src/Database/Database.php (1)

4927-4930: Update path auth is correct; prefer spread over array_merge (prior feedback)

  • Using authorization->skip(...) to fetch the old doc for update is right.
  • Minor: build permission lists with array spread for consistency with the rest of the file.

Example:

-                $updatePermissions = array_merge(
-                    $collection->getUpdate(),
-                    $documentSecurity ? $old->getUpdate() : []
-                );
-                $readPermissions = array_merge(
-                    $collection->getRead(),
-                    $documentSecurity ? $old->getRead() : []
-                );
+                $updatePermissions = [
+                    ...$collection->getUpdate(),
+                    ...($documentSecurity ? $old->getUpdate() : []),
+                ];
+                $readPermissions = [
+                    ...$collection->getRead(),
+                    ...($documentSecurity ? $old->getRead() : []),
+                ];

Also applies to: 5053-5071

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

56-63: Verify adapter method overrides are non-static (same concern as Base).

Avoid static implementations for getDatabase/deleteIndex/deleteColumn in adapter tests; matches Base’s protected instance signatures.

#!/bin/bash
# Re-check in this scope specifically
rg -nP '^\s*(public|protected)\s+static\s+function\s+(getDatabase|deleteColumn|deleteIndex)\b' tests/e2e/Adapter/Scopes -C1

Also applies to: 331-339

🧹 Nitpick comments (5)
tests/e2e/Adapter/Base.php (2)

36-39: Docblock type is not nullable while property is.

Adjust the annotation to match ?Authorization.

-    /**
-     * @var Authorization
-     */
+    /**
+     * @var Authorization|null
+     */

62-69: Avoid auth state leakage between tests; reset roles and status.

tearDown() only resets status default; roles added during tests can persist. Normalize both roles and status per test for isolation.

     public function setUp(): void
     {
-        if (is_null(self::$authorization)) {
-            self::$authorization = new Authorization();
-        }
-
-        self::$authorization->addRole('any');
+        if (is_null(self::$authorization)) {
+            self::$authorization = new Authorization();
+        }
+        // Ensure a clean baseline
+        self::$authorization->setDefaultStatus(true);
+        self::$authorization->cleanRoles();
+        self::$authorization->addRole('any');
     }

     public function tearDown(): void
     {
-        self::$authorization->setDefaultStatus(true);
+        // Restore baseline for next test
+        self::$authorization->setDefaultStatus(true);
+        self::$authorization->cleanRoles();
+        self::$authorization->addRole('any');
     }

Also applies to: 71-75

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

618-631: Reduce boilerplate and guard against auth-state drift with small helpers.

Introduce Base helpers to scope authorization changes, e.g., withAuthDisabled(callable) and withRoles(array $roles, callable), to centralize disable/reset and role setup/restore.

I can open a follow-up PR to add these helpers and refactor the tests.

Also applies to: 4491-4501, 4514-4531

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

403-407: Consider caching the database instance.

Within the skip callback, $this->getDatabase() is called multiple times. For better readability and to avoid redundant calls, consider assigning it to a local variable at the start of the callback.

Apply this diff:

-        $this->getDatabase()->getAuthorization()->skip(function () use ($loopCount) {
+        $database = $this->getDatabase();
+        $database->getAuthorization()->skip(function () use ($database, $loopCount) {
             for ($i = 0; $i < $loopCount; $i++) {
-                $this->getDatabase()->deleteDocument('sizeTest2', 'doc' . $i);
+                $database->deleteDocument('sizeTest2', 'doc' . $i);
             }
         });
tests/e2e/Adapter/Scopes/PermissionTests.php (1)

206-207: Consider caching the authorization instance for readability.

Throughout the file, the authorization object is accessed repeatedly via $this->getDatabase()->getAuthorization() for consecutive operations like cleanRoles() followed by addRole(). While functionally correct, caching the authorization instance in a local variable would improve readability.

Example refactor:

-        $this->getDatabase()->getAuthorization()->cleanRoles();
-        $this->getDatabase()->getAuthorization()->addRole(Role::any()->toString());
+        $authorization = $this->getDatabase()->getAuthorization();
+        $authorization->cleanRoles();
+        $authorization->addRole(Role::any()->toString());

Also applies to: 230-230, 236-236, 457-458, 478-479, 498-499, 522-523, 550-551, 572-573, 603-604, 623-624, 632-633, 652-653, 675-676

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5096b38 and 46312fc.

📒 Files selected for processing (12)
  • src/Database/Adapter.php (2 hunks)
  • src/Database/Adapter/Pool.php (3 hunks)
  • src/Database/Adapter/SQL.php (6 hunks)
  • src/Database/Database.php (29 hunks)
  • tests/e2e/Adapter/Base.php (1 hunks)
  • tests/e2e/Adapter/Scopes/AttributeTests.php (41 hunks)
  • tests/e2e/Adapter/Scopes/CollectionTests.php (29 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (126 hunks)
  • tests/e2e/Adapter/Scopes/GeneralTests.php (9 hunks)
  • tests/e2e/Adapter/Scopes/IndexTests.php (13 hunks)
  • tests/e2e/Adapter/Scopes/PermissionTests.php (43 hunks)
  • tests/e2e/Adapter/Scopes/SpatialTests.php (25 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/Database/Adapter/SQL.php
  • src/Database/Adapter/Pool.php
  • src/Database/Adapter.php
  • tests/e2e/Adapter/Scopes/AttributeTests.php
  • tests/e2e/Adapter/Scopes/SpatialTests.php
  • tests/e2e/Adapter/Scopes/GeneralTests.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
PR: utopia-php/database#721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • tests/e2e/Adapter/Scopes/DocumentTests.php
🧬 Code graph analysis (6)
tests/e2e/Adapter/Scopes/DocumentTests.php (5)
src/Database/Database.php (4)
  • getDatabase (837-840)
  • getAuthorization (930-933)
  • getAdapter (1282-1285)
  • Database (38-8376)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (44-44)
tests/e2e/Adapter/MongoDBTest.php (1)
  • getDatabase (32-66)
tests/e2e/Adapter/SharedTables/MongoDBTest.php (1)
  • getDatabase (33-68)
src/Database/Validator/Authorization.php (6)
  • disable (202-205)
  • reset (212-215)
  • addRole (91-94)
  • removeRole (101-104)
  • cleanRoles (117-120)
  • skip (175-185)
tests/e2e/Adapter/Scopes/IndexTests.php (2)
tests/e2e/Adapter/Base.php (2)
  • getDatabase (44-44)
  • deleteIndex (60-60)
tests/e2e/Adapter/MongoDBTest.php (2)
  • getDatabase (32-66)
  • deleteIndex (105-108)
tests/e2e/Adapter/Base.php (7)
src/Database/Validator/Authorization.php (3)
  • Authorization (8-240)
  • addRole (91-94)
  • setDefaultStatus (139-143)
src/Database/Database.php (3)
  • getDatabase (837-840)
  • Database (38-8376)
  • deleteIndex (3593-3618)
tests/e2e/Adapter/MySQLTest.php (3)
  • getDatabase (27-59)
  • deleteColumn (61-69)
  • deleteIndex (71-79)
tests/e2e/Adapter/PostgresTest.php (3)
  • getDatabase (21-52)
  • deleteColumn (54-62)
  • deleteIndex (64-73)
tests/e2e/Adapter/PoolTest.php (2)
  • getDatabase (35-76)
  • deleteColumn (78-93)
src/Database/Adapter.php (2)
  • getDatabase (178-181)
  • deleteIndex (698-698)
tests/e2e/Adapter/SQLiteTest.php (3)
  • getDatabase (21-56)
  • deleteColumn (58-66)
  • deleteIndex (68-76)
src/Database/Database.php (6)
src/Database/Validator/Authorization.php (4)
  • Authorization (8-240)
  • isValid (52-85)
  • getDescription (42-45)
  • skip (175-185)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
src/Database/Adapter/Pool.php (6)
  • setAuthorization (615-619)
  • updateDocument (240-243)
  • deleteDocument (255-258)
  • getDocument (225-228)
  • upsertDocuments (250-253)
  • count (275-278)
src/Database/Mirror.php (5)
  • setAuthorization (1103-1116)
  • silent (172-175)
  • updateDocument (658-704)
  • deleteDocument (845-886)
  • upsertDocuments (774-843)
src/Database/Adapter.php (7)
  • setAuthorization (61-66)
  • updateDocument (743-743)
  • deleteDocument (791-791)
  • getTenant (234-237)
  • getDocument (709-709)
  • upsertDocuments (770-774)
  • count (843-843)
src/Database/Document.php (9)
  • getRead (101-104)
  • getCreate (109-112)
  • getUpdate (117-120)
  • getId (63-66)
  • getCollection (85-88)
  • getTenant (178-187)
  • isEmpty (396-399)
  • getDelete (125-128)
  • getPermissionsByType (145-157)
tests/e2e/Adapter/Scopes/CollectionTests.php (3)
src/Database/Database.php (2)
  • getDatabase (837-840)
  • getAuthorization (930-933)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (44-44)
src/Database/Validator/Authorization.php (2)
  • skip (175-185)
  • addRole (91-94)
tests/e2e/Adapter/Scopes/PermissionTests.php (4)
src/Database/Database.php (2)
  • getDatabase (837-840)
  • getAuthorization (930-933)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (44-44)
tests/e2e/Adapter/MySQLTest.php (1)
  • getDatabase (27-59)
src/Database/Validator/Authorization.php (5)
  • disable (202-205)
  • reset (212-215)
  • cleanRoles (117-120)
  • addRole (91-94)
  • skip (175-185)
🪛 GitHub Actions: CodeQL
src/Database/Database.php

[error] 7239-7260: Class Utopia\Database\Validator\Authorization does not have a constructor and must be instantiated without any parameters; static call to Utopia\Database\Validator\Authorization::skip().

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

414-419: Authorization instance wiring looks correct

Private property, constructor init via setAuthorization(new Authorization()), and adapter propagation via setAuthorization()/getAuthorization() are consistent.

Also applies to: 434-435, 912-934


3691-3696: Read checks on cached and fresh document paths

Using $this->authorization->isValid(new Input(...)) with collection and (optional) document permissions is correct. Returns empty Document on denial.

Also applies to: 3719-3726


4386-4389: Create permission checks migrated to instance model

Collection-level create checks via Input before create/createDocuments are correct.

Also applies to: 4496-4499


5169-5173: Batch update permission gating and per-batch checks are solid

Pre-check for collection update when doc security is off; then find(... forPermission: update) and per-doc merging with $skipPermissionsUpdate are correct.

Also applies to: 5258-5263, 5311-5316


5839-5847: Upsert paths: correct use of skip() for reads/writes and per-case create/update checks

Auth checks for create vs update with documentSecurity considered; wrapping adapter upsert with authorization->skip is appropriate.

Also applies to: 5880-5887, 5983-5987


6091-6104: Atomic increase/decrease: correct skip-for-read and update permission checks

Fetching with forUpdate under skip(), then validating update permission against collection/doc is correct.

Also applies to: 6191-6204


6256-6272: Delete: skip-for-read on lock + delete permission check is correct

Pattern mirrors update; good.


6435-6463: Relationship cleanup: using authorization->skip to avoid requiring user read/update during cascading ops

Good defensive use of skip() for internal maintenance writes.

Also applies to: 6495-6520, 6528-6535, 6553-6561


6948-6953: find(): proper Input-based skipAuth and instance skip() usage

Matches the new model; no null‑coalescing.

Also applies to: 7034-7035


7178-7197: count(): fixed ternary and instance skip()

Matches prior guidance to remove ?? false and static calls.

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

59-60: LGTM: migrate to instance-based DB accessor.

Using $this->getDatabase() in traits is correct and aligns with per-instance authorization propagation.


100-104: Pre-dropping index via adapter hook is appropriate.

Calling $this->deleteIndex(...) before $database->deleteIndex(...) correctly validates idempotent deletion behavior.

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

25-25: LGTM! Migration to instance-based database access.

The systematic replacement of static::getDatabase() with $this->getDatabase() is correct and aligns with the PR's objective of moving from static to instance-based access patterns.

Also applies to: 44-44, 79-79, 311-311, 349-349, 362-362, 454-454, 495-495, 537-537, 596-596, 652-652, 726-726, 761-761, 781-781, 793-793, 900-900, 929-929, 945-945, 983-983, 1063-1063, 1247-1247, 1267-1267, 1332-1332, 1462-1462, 1487-1487, 1506-1506, 1533-1533

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

19-19: LGTM! Consistent migration to instance-based database access.

The replacement of static::getDatabase() with $this->getDatabase() throughout the file is correct and aligns with the instance-based authorization architecture.

Also applies to: 173-173, 210-210, 244-244, 303-303, 358-358, 427-427, 461-461, 482-482, 503-503, 526-526, 556-556, 576-576, 587-587, 609-609, 627-627, 656-656, 679-679, 697-697, 781-781, 821-821, 847-847, 867-867, 923-923, 943-943, 1002-1002, 1025-1025, 1074-1074, 1096-1096, 1133-1133, 1155-1155, 1174-1174, 1184-1184, 1258-1258


322-346: LGTM! Well-implemented authorization skip callbacks.

The skip callback implementations correctly pass the necessary variables through the use clause and properly utilize instance-based authorization. The mix of traditional closures and arrow functions is appropriate for their respective use cases.

Also applies to: 360-362, 404-406, 1249-1249

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

115-116: Fix assertion attribute: use bigint_unsigned.

Second assertion’s type check targets bigint_signed again. It should assert on bigint_unsigned before comparing its value.

Apply this diff:

-        $this->assertIsInt($document->getAttribute('bigint_signed'));
+        $this->assertIsInt($document->getAttribute('bigint_unsigned'));

464-475: Initialize $hash to avoid notices.

$hash is first used via indexed assignment. Initialize to an empty array to prevent E_NOTICE in stricter environments.

-        $offset = 1000000;
+        $offset = 1000000;
+        $hash = [];

1921-1927: Method name mismatch: Fulltext wildcard capability check.

Inconsistent method names: getSupportForFulltextWildCardIndex vs getSupportForFulltextWildcardIndex. Use one valid name across the file to avoid fatal errors.

-            if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildCardIndex()) {
+            if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildcardIndex()) {

Also confirm the same spelling is used consistently elsewhere.

Also applies to: 1970-1974

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

119-125: Schemaless invalid internal attributes: don’t expect exceptions

When getSupportForAttributes() === false, schemaless adapters shouldn’t throw on “invalid” internal attributes during bulk create. Remove try/catch and keep at least one assertion to ensure the call returns without error.

Apply:

-        try {
-            $docs = [
-                new Document(['$id' => true, 'freeA' => 'doc1']),
-                new Document(['$id' => true, 'freeB' => 'test']),
-                new Document(['$id' => true]),
-            ];
-            $database->createDocuments($colName, $docs);
-        } catch (\Throwable $e) {
-            $this->assertInstanceOf(StructureException::class, $e);
-        }
-
-        try {
-            $docs = [
-                new Document(['$createdAt' => true, 'freeA' => 'doc1']),
-                new Document(['$updatedAt' => true, 'freeB' => 'test']),
-                new Document(['$permissions' => 12]),
-            ];
-            $database->createDocuments($colName, $docs);
-        } catch (\Throwable $e) {
-            $this->assertInstanceOf(StructureException::class, $e);
-        }
+        $docs = [
+            new Document(['$id' => true, 'freeA' => 'doc1']),
+            new Document(['$id' => true, 'freeB' => 'test']),
+            new Document(['$id' => true]),
+        ];
+        $database->createDocuments($colName, $docs);
+
+        $docs = [
+            new Document(['$createdAt' => true, 'freeA' => 'doc1']),
+            new Document(['$updatedAt' => true, 'freeB' => 'test']),
+            new Document(['$permissions' => 12]),
+        ];
+        $database->createDocuments($colName, $docs);
+        $this->assertTrue(true); // ensure at least one assertion

Based on learnings

Also applies to: 129-149

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

1709-1715: Min/max checks clobber and skip zero; combine and use null-checks

Using truthy checks skips 0 and second assignment overwrites the first.

-        if ($max) {
-            $filters[$attribute] = ['$lte' => $max];
-        }
-
-        if ($min) {
-            $filters[$attribute] = ['$gte' => $min];
-        }
+        if ($max !== null) {
+            $filters[$attribute]['$lte'] = $max;
+        }
+        if ($min !== null) {
+            $filters[$attribute]['$gte'] = $min;
+        }
tests/e2e/Adapter/Schemaless/MongoDBTest.php (1)

32-36: Fix stale static call to getDatabase()

getDatabase() is now an instance method; calling it statically will error on PHP 8+.

-        $this->assertNotNull(static::getDatabase()->create());
+        $this->assertNotNull($this->getDatabase()->create());

Also applies to: 75-80

src/Database/Database.php (1)

2529-2533: Method casing nit: use setAttribute (capital “A”)

One occurrence uses setattribute which can trip static analysis/IDE tooling.

Apply this diff:

-                ->setattribute('key', $newKey ?? $id)
+                ->setAttribute('key', $newKey ?? $id)
🧹 Nitpick comments (14)
tests/e2e/Adapter/Scopes/DocumentTests.php (4)

216-236: Schemaless adapters: don’t expect exceptions; assert success instead.

When getSupportForAttributes() is false, this test should not expect an exception. Branch explicitly and keep an assertion in the schemaless path.

-        try {
-            $database->createDocument('documents', new Document([
-                'string' => '',
-                'integer_signed' => 0,
-                'integer_unsigned' => 0,
-                'bigint_signed' => 0,
-                'bigint_unsigned' => 0,
-                'float_signed' => 0,
-                'float_unsigned' => -5.55,
-                'boolean' => true,
-                'colors' => [],
-                'empty' => [],
-            ]));
-            $this->fail('Failed to throw exception');
-        } catch (Throwable $e) {
-            if ($database->getAdapter()->getSupportForAttributes()) {
-                $this->assertTrue($e instanceof StructureException);
-                $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage());
-            }
-        }
+        if ($database->getAdapter()->getSupportForAttributes()) {
+            try {
+                $database->createDocument('documents', new Document([
+                    'string' => '',
+                    'integer_signed' => 0,
+                    'integer_unsigned' => 0,
+                    'bigint_signed' => 0,
+                    'bigint_unsigned' => 0,
+                    'float_signed' => 0,
+                    'float_unsigned' => -5.55,
+                    'boolean' => true,
+                    'colors' => [],
+                    'empty' => [],
+                ]));
+                $this->fail('Failed to throw exception');
+            } catch (Throwable $e) {
+                $this->assertInstanceOf(StructureException::class, $e);
+                $this->assertStringContainsString('Invalid document structure: Attribute "float_unsigned" has invalid type. Value must be a valid range between 0 and', $e->getMessage());
+            }
+        } else {
+            $doc = $database->createDocument('documents', new Document([
+                'string' => '',
+                'integer_signed' => 0,
+                'integer_unsigned' => 0,
+                'bigint_signed' => 0,
+                'bigint_unsigned' => 0,
+                'float_signed' => 0,
+                'float_unsigned' => -5.55,
+                'boolean' => true,
+                'colors' => [],
+                'empty' => [],
+            ]));
+            $this->assertNotEmpty($doc->getId());
+        }

Based on learnings.


237-256: Schemaless adapters: don’t expect exceptions; assert success instead.

Same branching pattern should be applied here for bigint_unsigned negative value.

-        try {
-            $database->createDocument('documents', new Document([
-                'string' => '',
-                'integer_signed' => 0,
-                'integer_unsigned' => 0,
-                'bigint_signed' => 0,
-                'bigint_unsigned' => -10,
-                'float_signed' => 0,
-                'float_unsigned' => 0,
-                'boolean' => true,
-                'colors' => [],
-                'empty' => [],
-            ]));
-            $this->fail('Failed to throw exception');
-        } catch (Throwable $e) {
-            if ($database->getAdapter()->getSupportForAttributes()) {
-                $this->assertTrue($e instanceof StructureException);
-                $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage());
-            }
-        }
+        if ($database->getAdapter()->getSupportForAttributes()) {
+            try {
+                $database->createDocument('documents', new Document([
+                    'string' => '',
+                    'integer_signed' => 0,
+                    'integer_unsigned' => 0,
+                    'bigint_signed' => 0,
+                    'bigint_unsigned' => -10,
+                    'float_signed' => 0,
+                    'float_unsigned' => 0,
+                    'boolean' => true,
+                    'colors' => [],
+                    'empty' => [],
+                ]));
+                $this->fail('Failed to throw exception');
+            } catch (Throwable $e) {
+                $this->assertInstanceOf(StructureException::class, $e);
+                $this->assertEquals('Invalid document structure: Attribute "bigint_unsigned" has invalid type. Value must be a valid range between 0 and 9,223,372,036,854,775,807', $e->getMessage());
+            }
+        } else {
+            $doc = $database->createDocument('documents', new Document([
+                'string' => '',
+                'integer_signed' => 0,
+                'integer_unsigned' => 0,
+                'bigint_signed' => 0,
+                'bigint_unsigned' => -10,
+                'float_signed' => 0,
+                'float_unsigned' => 0,
+                'boolean' => true,
+                'colors' => [],
+                'empty' => [],
+            ]));
+            $this->assertNotEmpty($doc->getId());
+        }

Based on learnings.


258-280: Schemaless adapters: don’t expect exceptions; assert success instead.

Same for invalid $sequence value when attributes aren’t enforced.

-        try {
-            $database->createDocument('documents', new Document([
-                '$sequence' => '0',
-                '$permissions' => [],
-                'string' => '',
-                'integer_signed' => 1,
-                'integer_unsigned' => 1,
-                'bigint_signed' => 1,
-                'bigint_unsigned' => 1,
-                'float_signed' => 1,
-                'float_unsigned' => 1,
-                'boolean' => true,
-                'colors' => [],
-                'empty' => [],
-                'with-dash' => '',
-            ]));
-            $this->fail('Failed to throw exception');
-        } catch (Throwable $e) {
-            if ($database->getAdapter()->getSupportForAttributes()) {
-                $this->assertTrue($e instanceof StructureException);
-                $this->assertEquals('Invalid document structure: Attribute "$sequence" has invalid type. Invalid sequence value', $e->getMessage());
-            }
-        }
+        if ($database->getAdapter()->getSupportForAttributes())) {
+            try {
+                $database->createDocument('documents', new Document([
+                    '$sequence' => '0',
+                    '$permissions' => [],
+                    'string' => '',
+                    'integer_signed' => 1,
+                    'integer_unsigned' => 1,
+                    'bigint_signed' => 1,
+                    'bigint_unsigned' => 1,
+                    'float_signed' => 1,
+                    'float_unsigned' => 1,
+                    'boolean' => true,
+                    'colors' => [],
+                    'empty' => [],
+                    'with-dash' => '',
+                ]));
+                $this->fail('Failed to throw exception');
+            } catch (Throwable $e) {
+                $this->assertInstanceOf(StructureException::class, $e);
+                $this->assertEquals('Invalid document structure: Attribute "$sequence" has invalid type. Invalid sequence value', $e->getMessage());
+            }
+        } else {
+            $doc = $database->createDocument('documents', new Document([
+                '$sequence' => '0',
+                '$permissions' => [],
+                'string' => '',
+                'integer_signed' => 1,
+                'integer_unsigned' => 1,
+                'bigint_signed' => 1,
+                'bigint_unsigned' => 1,
+                'float_signed' => 1,
+                'float_unsigned' => 1,
+                'boolean' => true,
+                'colors' => [],
+                'empty' => [],
+                'with-dash' => '',
+            ]));
+            $this->assertNotEmpty($doc->getId());
+        }

Based on learnings.


309-311: Consistent class casing for Query.

Class names are case-insensitive in PHP, but using consistent casing improves readability.

-            query::isNull('id')
+            Query::isNull('id')
-            query::equal('id', [$sequence])
+            Query::equal('id', [$sequence])

Also applies to: 347-349

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

2106-2134: Preserve maxTimeMS for count aggregation

$options are overwritten, dropping timeout. Wrap existing options with getTransactionOptions($options).

-        $options = $this->getTransactionOptions();
+        $options = $this->getTransactionOptions($options);

2235-2237: Add aggregation timeout in sum

Parity with find/count; include maxTimeMS before transaction options.

-        $options = $this->getTransactionOptions();
+        $options = [];
+        if ($this->timeout) {
+            $options['maxTimeMS'] = $this->timeout;
+        }
+        $options = $this->getTransactionOptions($options);
tests/e2e/Adapter/SharedTables/MongoDBTest.php (1)

103-111: Unused parameters in stubs

Tiny nit: add unset($collection, $column); and unset($collection, $index); to silence analyzers, or suppress via annotations.

tests/e2e/Adapter/Schemaless/MongoDBTest.php (1)

102-110: Unused parameters in stubs

Same nit as other Mongo tests: consider unset(...) in stub methods to quiet static analysis.

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

45-47: Use assertTrue/assertFalse and expected-first order for clarity.

Current assertions pass but reverse the expected/actual order and compare to booleans via assertEquals. Prefer assertTrue/assertFalse and assertSame for strings to improve failure messages and strictness.

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false);
-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, [])), false);
-        $this->assertEquals($object->getDescription(), 'No permissions provided for action \'read\'');
+        $this->assertFalse($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));
+        $this->assertFalse($object->isValid(new Input(Database::PERMISSION_READ, [])));
+        $this->assertSame('No permissions provided for action \'read\'', $object->getDescription());

-        $this->assertEquals($this->authorization->hasRole(Role::user('456')->toString()), true);
-        $this->assertEquals($this->authorization->hasRole(Role::user('457')->toString()), false);
-        $this->assertEquals($this->authorization->hasRole(''), false);
-        $this->assertEquals($this->authorization->hasRole(Role::any()->toString()), true);
+        $this->assertTrue($this->authorization->hasRole(Role::user('456')->toString()));
+        $this->assertFalse($this->authorization->hasRole(Role::user('457')->toString()));
+        $this->assertFalse($this->authorization->hasRole(''));
+        $this->assertTrue($this->authorization->hasRole(Role::any()->toString()));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true);
+        $this->assertTrue($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false);
+        $this->assertFalse($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true);
+        $this->assertTrue($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true);
+        $this->assertTrue($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false);
+        $this->assertFalse($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true);
+        $this->assertTrue($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), true);
+        $this->assertTrue($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false);
+        $this->assertFalse($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())), false);
+        $this->assertFalse($object->isValid(new Input(Database::PERMISSION_READ, $document->getRead())));

-        $this->assertEquals($this->authorization->skip(function () use ($object, $document) {
-            return $object->isValid(new Input(Database::PERMISSION_READ, $document->getRead()));
-        }), true);
+        $this->assertTrue($this->authorization->skip(function () use ($object, $document) {
+            return $object->isValid(new Input(Database::PERMISSION_READ, $document->getRead()));
+        }));

-        $this->assertEquals(true, $this->authorization->getStatus());
+        $this->assertTrue($this->authorization->getStatus());

-            $this->assertEquals(false, $this->authorization->getStatus());
+            $this->assertFalse($this->authorization->getStatus());

-                $this->assertEquals(false, $this->authorization->getStatus());
+                $this->assertFalse($this->authorization->getStatus());

-                $this->authorization->skip(function () {
-                    $this->assertEquals(false, $this->authorization->getStatus());
-                });
+                $this->authorization->skip(function () {
+                    $this->assertFalse($this->authorization->getStatus());
+                });

-                $this->assertEquals(false, $this->authorization->getStatus());
+                $this->assertFalse($this->authorization->getStatus());

-            $this->assertEquals(false, $this->authorization->getStatus());
+            $this->assertFalse($this->authorization->getStatus());

-        $this->assertEquals(true, $this->authorization->getStatus());
+        $this->assertTrue($this->authorization->getStatus());

Also applies to: 52-57, 57-57, 61-61, 65-65, 70-70, 74-74, 79-79, 83-83, 87-87, 98-101, 106-106, 109-116, 118-118, 121-121, 124-124


29-29: Redundant addRole(any).

Authorization initializes with role 'any' by default, so this call is unnecessary. Either remove it or assert the default explicitly.

-        $this->authorization->addRole(Role::any()->toString());
+        // Default includes 'any'; no need to add explicitly.

43-44: Inline the $object alias.

Using $this->authorization directly reduces noise in assertions.

-        $object = $this->authorization;
+        // Use $this->authorization directly for readability.
tests/e2e/Adapter/MongoDBTest.php (1)

101-104: Silence PHPMD UnusedFormalParameter in no-op adapter helpers.

Parameters are required by the abstract signature but unused here. Rename with underscores and add a suppress annotation.

-    protected function deleteColumn(string $collection, string $column): bool
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    protected function deleteColumn(string $_collection, string $_column): bool
     {
         return true;
     }

-    protected function deleteIndex(string $collection, string $index): bool
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    protected function deleteIndex(string $_collection, string $_index): bool
     {
         return true;
     }

Also applies to: 106-109

src/Database/Database.php (1)

3691-3696: DRY the READ permission check in getDocument

Same permission array and isValid logic is duplicated for cache hit and DB path. Extract a tiny private helper (e.g., canReadDocument(Document $collection, Document $doc): bool) and invoke it in both spots.

Also applies to: 3721-3725

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

71-81: Consider improving the error message clarity.

Line 79 uses $permission which is the last permission from the loop. If multiple permissions are checked and none match, the error message says Missing "action" permission for role "role3" (showing only the last one), which could be misleading during debugging.

While the full permissions array is shown in the "was given" part, consider either:

  1. Removing the for role "$permission" part since the full array is already displayed
  2. Initializing $permission to a more meaningful value

Example refactor:

-        $permission = '-';
-
         foreach ($permissions as $permission) {
             if (\array_key_exists($permission, $this->roles)) {
                 return true;
             }
         }
 
-        $this->message = 'Missing "'.$action.'" permission for role "'.$permission.'". Only "'.\json_encode($this->getRoles()).'" scopes are allowed and "'.\json_encode($permissions).'" was given.';
+        $this->message = 'Missing "'.$action.'" permission. Only "'.\json_encode($this->getRoles()).'" scopes are allowed and "'.\json_encode($permissions).'" was given.';
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 46312fc and 0a4d6f3.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • composer.json (1 hunks)
  • src/Database/Adapter.php (2 hunks)
  • src/Database/Adapter/Mongo.php (3 hunks)
  • src/Database/Adapter/Pool.php (4 hunks)
  • src/Database/Database.php (31 hunks)
  • src/Database/Validator/Authorization.php (4 hunks)
  • src/Database/Validator/Authorization/Input.php (1 hunks)
  • tests/e2e/Adapter/MongoDBTest.php (4 hunks)
  • tests/e2e/Adapter/Schemaless/MongoDBTest.php (4 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (126 hunks)
  • tests/e2e/Adapter/Scopes/GeneralTests.php (9 hunks)
  • tests/e2e/Adapter/Scopes/SchemalessTests.php (19 hunks)
  • tests/e2e/Adapter/SharedTables/MongoDBTest.php (4 hunks)
  • tests/unit/Validator/AuthorizationTest.php (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/Database/Adapter/Pool.php
  • src/Database/Validator/Authorization/Input.php
  • tests/e2e/Adapter/Scopes/GeneralTests.php
  • src/Database/Adapter.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-03T02:04:17.803Z
Learnt from: abnegate
PR: utopia-php/database#721
File: tests/e2e/Adapter/Scopes/DocumentTests.php:6418-6439
Timestamp: 2025-10-03T02:04:17.803Z
Learning: In tests/e2e/Adapter/Scopes/DocumentTests::testSchemalessDocumentInvalidInteralAttributeValidation (PHP), when the adapter reports getSupportForAttributes() === false (schemaless), the test should not expect exceptions from createDocuments for “invalid” internal attributes; remove try/catch and ensure the test passes without exceptions, keeping at least one assertion.

Applied to files:

  • tests/e2e/Adapter/Scopes/SchemalessTests.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
🧬 Code graph analysis (9)
tests/e2e/Adapter/Schemaless/MongoDBTest.php (3)
src/Database/Adapter.php (6)
  • getDatabase (182-185)
  • setAuthorization (61-66)
  • delete (537-537)
  • create (510-510)
  • setDatabase (167-172)
  • deleteIndex (702-702)
src/Database/Database.php (7)
  • getDatabase (838-841)
  • Database (38-8379)
  • setAuthorization (919-924)
  • delete (1383-1397)
  • create (1325-1344)
  • setDatabase (823-828)
  • deleteIndex (3594-3619)
tests/e2e/Adapter/MongoDBTest.php (3)
  • getDatabase (32-67)
  • deleteColumn (101-104)
  • deleteIndex (106-109)
tests/e2e/Adapter/SharedTables/MongoDBTest.php (3)
src/Database/Database.php (7)
  • getDatabase (838-841)
  • Database (38-8379)
  • setAuthorization (919-924)
  • create (1325-1344)
  • delete (1383-1397)
  • setDatabase (823-828)
  • deleteIndex (3594-3619)
tests/e2e/Adapter/MongoDBTest.php (3)
  • getDatabase (32-67)
  • deleteColumn (101-104)
  • deleteIndex (106-109)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (44-44)
  • deleteColumn (52-52)
  • deleteIndex (60-60)
tests/e2e/Adapter/Scopes/SchemalessTests.php (4)
tests/e2e/Adapter/Schemaless/MongoDBTest.php (1)
  • getDatabase (32-68)
src/Database/Database.php (2)
  • getDatabase (838-841)
  • getAuthorization (931-934)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (44-44)
src/Database/Validator/Authorization.php (3)
  • cleanRoles (113-116)
  • addRole (87-90)
  • skip (171-181)
tests/e2e/Adapter/Scopes/DocumentTests.php (4)
tests/e2e/Adapter/SharedTables/MongoDBTest.php (1)
  • getDatabase (33-69)
src/Database/Database.php (3)
  • getDatabase (838-841)
  • getAuthorization (931-934)
  • Database (38-8379)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (44-44)
src/Database/Validator/Authorization.php (6)
  • disable (198-201)
  • reset (208-211)
  • addRole (87-90)
  • removeRole (97-100)
  • cleanRoles (113-116)
  • skip (171-181)
src/Database/Database.php (3)
src/Database/Validator/Authorization.php (4)
  • Authorization (8-236)
  • isValid (52-81)
  • getDescription (42-45)
  • skip (171-181)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
src/Database/Adapter.php (2)
  • setAuthorization (61-66)
  • getAuthorization (68-71)
tests/e2e/Adapter/MongoDBTest.php (5)
tests/e2e/Adapter/SharedTables/MongoDBTest.php (3)
  • getDatabase (33-69)
  • deleteColumn (103-106)
  • deleteIndex (108-111)
tests/e2e/Adapter/Schemaless/MongoDBTest.php (2)
  • getDatabase (32-68)
  • deleteColumn (102-105)
src/Database/Database.php (7)
  • getDatabase (838-841)
  • Database (38-8379)
  • setAuthorization (919-924)
  • create (1325-1344)
  • delete (1383-1397)
  • setDatabase (823-828)
  • deleteIndex (3594-3619)
tests/e2e/Adapter/MySQLTest.php (3)
  • getDatabase (27-59)
  • deleteColumn (61-69)
  • deleteIndex (71-79)
tests/e2e/Adapter/Base.php (3)
  • getDatabase (44-44)
  • deleteColumn (52-52)
  • deleteIndex (60-60)
src/Database/Adapter/Mongo.php (1)
src/Database/Validator/Authorization.php (2)
  • getStatus (157-160)
  • getRoles (105-108)
src/Database/Validator/Authorization.php (2)
src/Database/Database.php (1)
  • Database (38-8379)
src/Database/Validator/Authorization/Input.php (3)
  • Input (5-49)
  • getPermissions (40-43)
  • getAction (45-48)
tests/unit/Validator/AuthorizationTest.php (2)
src/Database/Validator/Authorization.php (14)
  • Authorization (8-236)
  • addRole (87-90)
  • isValid (52-81)
  • getDescription (42-45)
  • hasRole (123-126)
  • cleanRoles (113-116)
  • disable (198-201)
  • reset (208-211)
  • setDefaultStatus (135-139)
  • enable (188-191)
  • getRoles (105-108)
  • removeRole (97-100)
  • skip (171-181)
  • getStatus (157-160)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
🪛 PHPMD (2.15.0)
tests/e2e/Adapter/Schemaless/MongoDBTest.php

102-102: Avoid unused parameters such as '$collection'. (undefined)

(UnusedFormalParameter)


102-102: Avoid unused parameters such as '$column'. (undefined)

(UnusedFormalParameter)


107-107: Avoid unused parameters such as '$collection'. (undefined)

(UnusedFormalParameter)


107-107: Avoid unused parameters such as '$index'. (undefined)

(UnusedFormalParameter)

tests/e2e/Adapter/SharedTables/MongoDBTest.php

103-103: Avoid unused parameters such as '$collection'. (undefined)

(UnusedFormalParameter)


103-103: Avoid unused parameters such as '$column'. (undefined)

(UnusedFormalParameter)


108-108: Avoid unused parameters such as '$collection'. (undefined)

(UnusedFormalParameter)


108-108: Avoid unused parameters such as '$index'. (undefined)

(UnusedFormalParameter)

tests/e2e/Adapter/MongoDBTest.php

101-101: Avoid unused parameters such as '$collection'. (undefined)

(UnusedFormalParameter)


101-101: Avoid unused parameters such as '$column'. (undefined)

(UnusedFormalParameter)


106-106: Avoid unused parameters such as '$collection'. (undefined)

(UnusedFormalParameter)


106-106: Avoid unused parameters such as '$index'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (20)
tests/e2e/Adapter/Scopes/DocumentTests.php (2)

619-631: Instance-based authorization usage looks consistent.

disable/reset/skip/addRole/removeRole now flow through $this->getDatabase()->getAuthorization(), matching the instance-based design.

Also applies to: 3897-3910, 4491-4500, 4514-4531, 4992-4995


5467-5471: Permission::write() exists and is properly supported—no changes needed.

Verification confirms that Permission::write() is a fully supported aggregate permission in the database layer. It is explicitly documented as an "Aggregate permission" (Database.php line 119), validated in the permission system (Permissions.php:25 and Permission.php:93), and tested successfully. The method exists, is honored by database permission checks, and all usages at the cited lines are correct.

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

22-22: Instance access: good migration

Switch to $this->getDatabase() aligns tests with instance-based authorization propagation.


779-790: Authorization calls migrated correctly

Use of $database->getAuthorization()->cleanRoles()/addRole()/skip() matches the new instance API and restores state via skip() finally-block.
Please confirm no tests rely on global Authorization state anymore (e.g., static role leakage across tests).

Also applies to: 798-806, 811-823, 838-839, 931-933

tests/e2e/Adapter/SharedTables/MongoDBTest.php (1)

33-69: Instance migration looks correct

getDatabase() is now an instance method and used consistently in tests.

composer.json (1)

43-44: Manual test execution and lockfile update needed

The dependency change from 0.11.0 to 0.11.* is compatible with no breaking changes in utopia-php/mongo 0.11.0 (only a readConcern bug fix). Composer resolution shows no conflicts. However, verify by:

  1. Running composer update utopia-php/mongo to update composer.lock
  2. Executing the test suite to ensure MongoDB adapter tests pass
tests/unit/Validator/AuthorizationTest.php (1)

12-12: Good shift to instance-based Authorization and Input wrapper.

Import and property initialization align with the new API and improve test isolation.

Also applies to: 16-21

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

32-32: Instance-based database and per-instance Authorization: LGTM.

Visibility widening on getDatabase() and propagating Authorization via setAuthorization() matches the refactor across adapters and tests.

Also applies to: 56-56, 72-79

src/Database/Database.php (3)

415-420: Per‑instance Authorization: injection and propagation look correct

Private Authorization property, default initialization in the constructor, and Adapter propagation via setAuthorization/getAuthorization are consistent. Good foundation for instance‑scoped auth across Database/Adapter.

Also applies to: 435-436, 913-935


5548-5553: Good use of Authorization::skip for relationship side‑effects

Wrapping cross‑document updates/deletes under skip avoids unnecessary READ checks while preserving outer UPDATE/DELETE authorization gates. LGTM.

Also applies to: 5676-5677


6101-6106: Increase/Decrease permission checks are correct

UPDATE checks incorporate collection+document ACLs; read bypass for row locking is done via skip before auth gate. Solid.

Also applies to: 6201-6206

src/Database/Validator/Authorization.php (9)

5-5: LGTM!

The import is necessary for the new Input DTO-based validation approach.


26-28: Verify the security implications of the default 'any' role.

The default role 'any' => true means authorization checks will pass for any permission that includes 'any'. Ensure this is the intended behavior and that it aligns with your security model.

If this is intentional (e.g., for public resources or backward compatibility), consider adding a comment explaining why 'any' is included by default to prevent confusion.


52-57: LGTM!

The type check is correct and necessary given the mixed signature. The instanceof check properly validates the input type before proceeding with validation logic.


59-64: LGTM!

The extraction of permissions and action from the Input object is clean, and the status check correctly short-circuits validation when authorization is disabled.


66-69: LGTM!

The empty permissions check is correct, and the error message appropriately includes the action for better debugging context.


87-126: LGTM!

The role management methods are correctly implemented as instance methods, and the method renames (addRole, removeRole, hasRole) improve semantic clarity.


135-139: Verify the intended behavior of setDefaultStatus.

The method sets both statusDefault (line 137) and the current status (line 138). This means calling setDefaultStatus(false) immediately disables authorization, not just changing what reset() will restore to.

If this is intentional, consider updating the docblock to clarify that this method affects both the default status and the current status immediately.

Suggested docblock update:

     /**
-     * Change default status.
-     * This will be used for the
-     *  value set on the $this->reset() method
+     * Change default status and current status.
+     * Sets both the current authorization status and the default
+     * status that will be restored when reset() is called.
+     *
      * @param bool $status
      * @return void
      */

147-160: LGTM!

The status getter and setter methods are straightforward and correctly implemented.


171-211: LGTM!

The status control methods are correctly implemented. The skip() method properly uses try/finally to ensure the status is always restored, even if the callback throws an exception.

…r authorization initialization in Pool adapter
…rization-instance

# Conflicts:
#	src/Database/Adapter/Pool.php
#	tests/e2e/Adapter/Scopes/DocumentTests.php
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

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

2813-2813: Inconsistent database access pattern.

This line still uses static::getDatabase() while all other test methods in this file were updated to use $this->getDatabase(). This inconsistency should be corrected to align with the PR's migration to instance-based database access.

Apply this diff to fix the inconsistency:

-        $database = static::getDatabase();
+        $database = $this->getDatabase();
src/Database/Database.php (1)

745-747: Critical: Static call to Authorization::skip — use instance method

This triggers PHPStan/CodeQL error and breaks the new instance API.

Apply:

-        $refetched = Authorization::skip(fn () => $this->silent(
+        $refetched = $this->authorization->skip(fn () => $this->silent(
             fn () => $this->find($collection->getId(), [Query::equal('$id', $docIds)])
         ));
tests/e2e/Adapter/Schemaless/MongoDBTest.php (1)

74-81: Fix non-static call used statically.

getDatabase() is an instance method; calling static::getDatabase() will fail. Use $this->getDatabase() consistently.

-        $this->assertNotNull(static::getDatabase()->create());
+        $this->assertNotNull($this->getDatabase()->create());
tests/e2e/Adapter/Scopes/DocumentTests.php (1)

1922-1929: Fix method name case mismatch: getSupportForFulltextWildCardIndex()getSupportForFulltextWildcardIndex()

The adapter API uses lowercase c in Wildcard, but the test file calls it with uppercase C in WildCard. This causes a runtime "Call to undefined method" error.

Incorrect usages found:

  • Line 1922
  • Line 3410

Both must change from getSupportForFulltextWildCardIndex() to getSupportForFulltextWildcardIndex().

-            if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildCardIndex()) {
+            if ($this->getDatabase()->getAdapter()->getSupportForFulltextWildcardIndex()) {
♻️ Duplicate comments (5)
src/Database/Database.php (3)

7314-7333: count(): also guard skip() with documentSecurity

Without this, aggregates can leak/inflate results when doc‑level ACLs are enabled.

-        $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead()));
+        $documentSecurity = $collection->getAttribute('documentSecurity', false);
+        $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead()));
@@
-        $count = $skipAuth ? $this->authorization->skip($getCount) : $getCount();
+        $count = ($skipAuth && !$documentSecurity) ? $this->authorization->skip($getCount) : $getCount();

Based on learnings


7376-7395: sum(): also guard skip() with documentSecurity

Mirror the fix from find()/count() to keep doc‑level filters in place.

-        $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead()));
+        $documentSecurity = $collection->getAttribute('documentSecurity', false);
+        $skipAuth = $this->authorization->isValid(new Input(self::PERMISSION_READ, $collection->getRead()));
@@
-        $sum = $skipAuth ? $this->authorization->skip($getSum) : $getSum();
+        $sum = ($skipAuth && !$documentSecurity) ? $this->authorization->skip($getSum) : $getSum();

Based on learnings


7169-7170: Don’t bypass doc‑level ACLs in find() when documentSecurity=true

Guard skip() with !$documentSecurity so adapter filters remain active for documentSecurity collections. Previously flagged.

-            $results = $skipAuth ? $this->authorization->skip($getResults) : $getResults();
+            // Only skip all auth when document-level security is disabled
+            $results = ($skipAuth && !$documentSecurity) ? $this->authorization->skip($getResults) : $getResults();
#!/bin/bash
# Verify no unsafe skips remain in find/count/sum
rg -nP -C2 '\$results\s*=\s*\$skipAuth\s*\?' src/Database/Database.php
rg -nP -C2 '\$count\s*=\s*\$skipAuth\s*\?' src/Database/Database.php
rg -nP -C2 '\$sum\s*=\s*\$skipAuth\s*\?' src/Database/Database.php
# Audit any lingering static calls
rg -n 'Authorization::skip\s*\('

Based on learnings

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

3946-3947: Nit resolved: role string corrected to 'user:x'.

Previous review flagged userx; current code uses removeRole('user:x'). Looks good.

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

1871-1873: Critical: Authorization bypass via empty roles and unescaped regex.

This security issue was previously flagged. When authorization is enabled but getRoles() returns an empty array, the regex pattern becomes {$forPermission}\(".*(?:).*"\) which matches any permission string, effectively bypassing authorization. Additionally, role identifiers are not escaped before regex interpolation, allowing potential regex injection if roles contain special characters.

Apply the fix previously suggested:

-        if ($this->authorization->getStatus()) {
-            $roles = \implode('|', $this->authorization->getRoles());
-            $filters['_permissions']['$in'] = [new Regex("{$forPermission}\\(\".*(?:{$roles}).*\"\\)", 'i')];
-        }
+        if ($this->authorization->getStatus()) {
+            $roleList = $this->authorization->getRoles();
+            if (!empty($roleList)) {
+                $escaped = \array_map(static fn ($r) => \preg_quote($r, '/'), $roleList);
+                $pattern = \sprintf('%s\(".*(?:%s).*"\)', $forPermission, \implode('|', $escaped));
+                $filters['_permissions']['$in'] = [new Regex($pattern, 'i')];
+            } else {
+                // No roles => no access
+                $filters['_uid'] = ['$in' => []];
+            }
+        }

Apply similar fixes at lines 2118-2120 and 2208-2210.

Also applies to: 2118-2120, 2208-2210

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

2564-2576: Nit: normalize method casing to setAttribute

Keep API usage consistent; PHP allows case‑insensitivity, but consistency helps readability.

-                ->setattribute('key', $newKey ?? $id)
+                ->setAttribute('key', $newKey ?? $id)
tests/e2e/Adapter/Scopes/DocumentTests.php (2)

2176-2181: Prefer assertCount over assertEmpty(count(...)) for clarity.

Replace assertEmpty(count($documents)) with assertCount(0, $documents) here and similarly elsewhere for zero-count checks.

-        $this->assertEmpty(count($documents));
+        $this->assertCount(0, $documents);

3938-3941: Remove duplicate sum assertion.

The sum('price', [year=2019]) assertion is repeated twice back-to-back with identical expectations. Drop one to reduce noise.

-        $sum = $database->sum('movies', 'price', [Query::equal('year', [2019]),]);
-        $this->assertEquals(round(39.50 + 25.99, 2), round($sum, 2));
tests/e2e/Adapter/Schemaless/MongoDBTest.php (1)

103-111: Silence unused parameter warnings in stubbed trait methods.

Parameters are intentionally unused in these adapter-specific stubs. Prefix with underscores to satisfy PHPMD without changing behavior.

-protected function deleteColumn(string $collection, string $column): bool
+protected function deleteColumn(string $_collection, string $_column): bool
 {
     return true;
 }
 
-protected function deleteIndex(string $collection, string $index): bool
+protected function deleteIndex(string $_collection, string $_index): bool
 {
     return true;
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22123ad and 61d0662.

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

Applied to files:

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

Applied to files:

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

Applied to files:

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

Applied to files:

  • tests/e2e/Adapter/Scopes/SpatialTests.php
  • src/Database/Database.php
🧬 Code graph analysis (6)
tests/e2e/Adapter/Scopes/DocumentTests.php (3)
src/Database/Database.php (3)
  • getDatabase (874-877)
  • getAuthorization (967-970)
  • Database (38-8521)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (46-46)
src/Database/Validator/Authorization.php (6)
  • disable (198-201)
  • reset (208-211)
  • addRole (87-90)
  • removeRole (97-100)
  • cleanRoles (113-116)
  • skip (171-181)
tests/e2e/Adapter/Scopes/SpatialTests.php (2)
tests/e2e/Adapter/Base.php (1)
  • getDatabase (46-46)
tests/e2e/Adapter/MariaDBTest.php (1)
  • getDatabase (21-53)
src/Database/Adapter/SQL.php (1)
src/Database/Validator/Authorization.php (2)
  • getRoles (105-108)
  • getStatus (157-160)
src/Database/Database.php (7)
src/Database/Validator/Authorization.php (4)
  • Authorization (8-236)
  • isValid (52-81)
  • getDescription (42-45)
  • skip (171-181)
src/Database/Validator/Authorization/Input.php (1)
  • Input (5-49)
src/Database/Adapter/Pool.php (3)
  • setAuthorization (630-634)
  • count (280-283)
  • sum (275-278)
src/Database/Adapter.php (4)
  • setAuthorization (61-66)
  • getAuthorization (68-71)
  • count (847-847)
  • sum (836-836)
src/Database/Mirror.php (1)
  • setAuthorization (1103-1116)
src/Database/Adapter/Mongo.php (2)
  • count (2093-2180)
  • sum (2195-2237)
src/Database/Adapter/SQL.php (2)
  • count (3172-3234)
  • sum (3247-3310)
src/Database/Adapter/Mongo.php (1)
src/Database/Validator/Authorization.php (2)
  • getStatus (157-160)
  • getRoles (105-108)
tests/e2e/Adapter/Schemaless/MongoDBTest.php (3)
src/Database/Database.php (7)
  • getDatabase (874-877)
  • Database (38-8521)
  • setAuthorization (955-960)
  • delete (1419-1433)
  • create (1361-1380)
  • setDatabase (859-864)
  • deleteIndex (3630-3655)
src/Database/Adapter.php (6)
  • getDatabase (182-185)
  • setAuthorization (61-66)
  • delete (537-537)
  • create (510-510)
  • setDatabase (167-172)
  • deleteIndex (702-702)
tests/e2e/Adapter/MongoDBTest.php (3)
  • getDatabase (32-67)
  • deleteColumn (101-104)
  • deleteIndex (106-109)
🪛 GitHub Actions: CodeQL
src/Database/Database.php

[error] 745-745: PHPStan: Static call to instance method Utopia\Database\Validator\Authorization::skip().

🪛 PHPMD (2.15.0)
tests/e2e/Adapter/Schemaless/MongoDBTest.php

103-103: Avoid unused parameters such as '$collection'. (undefined)

(UnusedFormalParameter)


103-103: Avoid unused parameters such as '$column'. (undefined)

(UnusedFormalParameter)


108-108: Avoid unused parameters such as '$collection'. (undefined)

(UnusedFormalParameter)


108-108: Avoid unused parameters such as '$index'. (undefined)

(UnusedFormalParameter)

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

21-21: LGTM! Consistent migration to instance-based database access.

All test methods correctly updated from static::getDatabase() to $this->getDatabase(), aligning with the PR's refactoring of the authorization system to instance-based patterns. The changes are uniform and maintain the existing test logic.

Also applies to: 96-96, 249-249, 354-354, 402-402, 514-514, 619-619, 724-724, 921-921, 1352-1352, 1483-1483, 1786-1786, 1874-1874, 1961-1961, 2066-2066, 2172-2172, 2243-2243, 2375-2375, 2460-2460, 2502-2502, 2554-2554, 2589-2589, 2651-2651, 2683-2683, 2775-2775

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

2960-2960: LGTM! Instance-based authorization properly implemented.

The migration from static to instance-based authorization access is correct. The roles retrieved via $this->authorization->getRoles() are safely used in getSQLPermissionsCondition() where they're properly quoted via PDO::quote() (line 1841), preventing SQL injection.

Also applies to: 3057-3058, 3177-3177, 3195-3196, 3253-3253, 3271-3272

@abnegate abnegate merged commit da9d021 into main Nov 4, 2025
18 checks passed
@abnegate abnegate deleted the feat-authorization-instance branch November 4, 2025 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants