Skip to content

Conversation

@fogelito
Copy link
Contributor

@fogelito fogelito commented Oct 12, 2025

Summary by CodeRabbit

  • New Features

    • Added a distinct Unique error type to represent non-primary unique-index violations.
  • Bug Fixes

    • Adapters now distinguish primary-identifier duplicates from other unique-index conflicts; non-primary conflicts surface as Unique errors.
    • Duplicate/error handling consolidated into a common processor for more consistent and clearer error reporting across databases.
    • Public API signatures unchanged.
  • Tests

    • End-to-end tests updated to accept and assert both Duplicate and Unique error outcomes.

@fogelito fogelito requested a review from abnegate October 12, 2025 06:28
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 12, 2025

Walkthrough

Adds a new Utopia\Database\Exception\Unique class and updates MariaDB, Postgres, and SQLite adapters to parse DB-specific duplicate/unique constraint errors and map them to either DuplicateException or UniqueException; updates docblocks and e2e tests to accept both exception types.

Changes

Cohort / File(s) Summary
Exception: New type
src/Database/Exception/Unique.php
Adds Unique exception class in Utopia\Database\Exception, extending Duplicate.
Adapter: MariaDB
src/Database/Adapter/MariaDB.php
Imports Unique as UniqueException; updates docblocks (deleteIndex, createDocument, updateDocument) to include @throws UniqueException; processException inspects duplicate codes (23000/1062) and maps to DuplicateException for _uid/PRIMARY key names, otherwise to UniqueException.
Adapter: Postgres
src/Database/Adapter/Postgres.php
Imports Unique as UniqueException; processException parses duplicate constraint column names (23505 or translated 23000/19), compares against expected key sets (_uid or _tenant+_uid), and returns DuplicateException or UniqueException accordingly.
Adapter: SQLite
src/Database/Adapter/SQLite.php
Imports Unique as UniqueException; createDocument/updateDocument delegate PDOExceptions to processException($e); processException preserves timeout mapping and maps constraint violations to DuplicateException when targeting _tenant+_uid, otherwise to UniqueException.
Tests: e2e document scopes
tests/e2e/Adapter/Scopes/DocumentTests.php
Imports Unique as UniqueException; updates tests to accept either DuplicateException or UniqueException for duplicate/unique constraint failures, adjusting assertions accordingly.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Adapter as DB Adapter\n(MariaDB / Postgres / SQLite)
  participant DB as Database
  participant Handler as processException

  Client->>Adapter: createDocument/updateDocument(...)
  Adapter->>DB: INSERT/UPDATE
  DB-->>Adapter: PDOException / SQL error (duplicate)
  Adapter->>Handler: processException(error)
  alt Duplicate/Unique constraint detected
    note right of Handler #f0f4c3: Adapter-specific parsing\nMariaDB: key name (23000/1062)\nPostgres: column list (23505 or 23000/19)\nSQLite: constraint detail (23000/19)
    Handler-->>Adapter: DuplicateException or UniqueException
    Adapter-->>Client: throw mapped exception
  else Timeout or other mapped error
    Handler-->>Adapter: rethrow original or mapped timeout exception
    Adapter-->>Client: throw exception
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • abnegate

Poem

I hop through rows and index trails,
A new Unique joins Duplicate's tales.
Maria, Postgres, SQLite debate,
Which constraint was truly at stake.
Tests nod along — the rabbit celebrates 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Unique Exception" directly reflects the primary objective of the changeset, which is to introduce a new Unique exception class and update multiple database adapters (MariaDB, Postgres, SQLite) to distinguish between DuplicateException (for document ID duplicates) and UniqueException (for other unique constraint violations). The title is concise, clear, and accurately summarizes the main change without being vague or misleading. A developer scanning the git history would understand that this PR addresses exception handling for unique constraints.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ 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 unique-exception

📜 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 6a9c42a and 968fff6.

📒 Files selected for processing (2)
  • src/Database/Adapter/Postgres.php (2 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Database/Adapter/Postgres.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (MariaDB)

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

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

1928-1928: Refresh PHPDoc to reflect UniqueException.

Now that processException() returns UniqueException for duplicate rows, the @throws DuplicateException annotations on methods such as createDocument() and updateDocument() are stale. Please swap them to @throws UniqueException (and keep Duplicate where still valid) so downstream callers see the correct type.

-     * @throws DuplicateException
+     * @throws UniqueException

Apply the same adjustment to other affected docblocks in this class.

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

1799-1799: Synchronize PHPDoc with new exception type.

Since duplicate-key violations now surface as UniqueException, please update the affected @throws DuplicateException annotations (e.g., on createDocument() / updateDocument()) to mention UniqueException, keeping any remaining Duplicate mentions only where they’re still thrown.

-     * @throws DuplicateException
+     * @throws UniqueException

Update the parallel spots in this adapter as well.

📜 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 35c978d and f87ec3d.

📒 Files selected for processing (5)
  • src/Database/Adapter/MariaDB.php (2 hunks)
  • src/Database/Adapter/Postgres.php (2 hunks)
  • src/Database/Exception/Unique.php (1 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (5 hunks)
  • tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.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/DocumentTests.php
🧬 Code graph analysis (5)
tests/e2e/Adapter/Scopes/Relationships/OneToOneTests.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (7-9)
src/Database/Exception/Unique.php (1)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Adapter/MariaDB.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (7-9)
src/Database/Adapter/Postgres.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (7-9)
tests/e2e/Adapter/Scopes/DocumentTests.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (7-9)
⏰ 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 (7)
src/Database/Exception/Unique.php (1)

7-9: Looks good.

Dedicated Unique exception aligns with the new behavior.

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

13-807: Tests updated appropriately.

Catching UniqueException keeps the one-to-one E2Es aligned with the adapter change.

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

17-17: Import alignment looks good.

Adding the UniqueException alias keeps the test expectations consistent with the new exception class.


4733-4733: Updated expectation matches new behavior.

Switching to UniqueException reflects the adapter change for unique constraint violations.


4798-4798: Good call on aligning the update test.

Expecting UniqueException here mirrors the new adapter semantics during conflict updates.


5303-5303: Consistent duplicate insert expectation.

Updating this assertion to UniqueException keeps the duplicate-create scenario in sync with the new exception contract.


5319-5319: Case-insensitive duplicate now consistent.

Using UniqueException here matches the revised behavior for duplicate IDs regardless of casing.

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

📜 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 f87ec3d and bbd9f4f.

📒 Files selected for processing (1)
  • src/Database/Adapter/SQLite.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Adapter/SQLite.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (7-9)
⏰ 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 (Pool)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (MySQL)
🔇 Additional comments (1)
src/Database/Adapter/SQLite.php (1)

15-15: LGTM!

The import is correctly added and the alias UniqueException is clear and appropriate.

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

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

520-520: Verify PHPDoc comment has been updated.

A previous review noted that the PHPDoc comment at line 520 should be updated from @throws Duplicate to @throws UniqueException. Please ensure this documentation reflects the new exception type. The same applies to line 642.


11-11: Remove the unused Duplicate import.

The Duplicate exception is no longer used after routing errors through processException. This was flagged in a previous review but remains unaddressed.

Apply this diff:

-use Utopia\Database\Exception\Duplicate;
📜 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 bbd9f4f and c9daebf.

📒 Files selected for processing (1)
  • src/Database/Adapter/SQLite.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Adapter/SQLite.php (6)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (7-9)
src/Database/Adapter/MariaDB.php (1)
  • processException (1775-1831)
src/Database/Adapter/Postgres.php (1)
  • processException (1909-1942)
src/Database/Adapter/MySQL.php (1)
  • processException (147-164)
src/Database/Adapter/SQL.php (1)
  • processException (1943-1946)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Unit Test
🔇 Additional comments (2)
src/Database/Adapter/SQLite.php (2)

622-622: LGTM! Exception handling now properly centralized.

Both createDocument and updateDocument now correctly route PDOExceptions through processException, providing consistent error handling across the adapter. This addresses the concerns from the previous review.

Also applies to: 841-841


1247-1250: LGTM! Duplicate row handling correctly updated for SQLite.

The logic correctly identifies SQLite unique constraint violations using SQLSTATE '23000' and driver-specific error code 19 (SQLITE_CONSTRAINT), then throws UniqueException instead of the previous DuplicateException. This aligns with the changes made to the MariaDB and Postgres adapters.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

📜 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 e4412d7 and 53e1a2e.

📒 Files selected for processing (3)
  • src/Database/Adapter/MariaDB.php (4 hunks)
  • src/Database/Exception/Unique.php (1 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (5 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/DocumentTests.php
🧬 Code graph analysis (3)
tests/e2e/Adapter/Scopes/DocumentTests.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
src/Database/Exception/Unique.php (1)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Adapter/MariaDB.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
🪛 GitHub Actions: Linter
src/Database/Adapter/MariaDB.php

[error] 1-1: PSR-12: single_space_around_construct

⏰ 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/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (MySQL)
🔇 Additional comments (1)
tests/e2e/Adapter/Scopes/DocumentTests.php (1)

4738-4767: Double-check expectation for UniqueException on unique indexes

Line 4756 currently asserts the duplicate insert on a unique index bubbles up as DuplicateException but not UniqueException. Given the PR headline (“surface Unique vs Duplicate exceptions based on constraint context”), can you confirm that this scenario is really supposed to stay on the generic DuplicateException path? If the adapters now classify unique-constraint violations as UniqueException, this assertNotInstanceOf will start failing. Please double-check the intended contract here and adjust either the adapters or the test accordingly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

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

928-934: Remove debug code and fix Postgres-specific regex pattern.

This code has critical issues that prevent it from working correctly:

  1. Debug code: Lines 928 and 931 contain var_dump() calls that will pollute production logs and responses.
  2. Wrong regex pattern: The regex /for key '(?:[^.]+\.)?([^']+)'/ is designed for MariaDB error messages ("Duplicate entry '...' for key 'key_name'"), but Postgres uses a different format: "duplicate key value violates unique constraint \"constraint_name\"".

As a result, the regex will never match Postgres errors, and the new UniqueException will never be thrown despite the code being added.

Apply this diff to fix both issues:

-        if ($e->getCode() === '23505' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) {
-            var_dump($e->getMessage());
-            if (preg_match("/for key '(?:[^.]+\.)?([^']+)'/", $e->getMessage(), $m)) {
-                if ($m[1] === '_uid' || $m[1] === 'PRIMARY') {
-                    var_dump($m);
-                    return new UniqueException('Document already exists', $e->getCode(), $e);
-                }
-            }
-
+        if ($e->getCode() === '23505' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) {
+            // Postgres format: "duplicate key value violates unique constraint \"constraint_name\""
+            if (preg_match('/unique constraint "([^"]+)"/', $e->getMessage(), $m)) {
+                // Extract the base constraint name (remove namespace/tenant prefix if present)
+                $constraintName = $m[1];
+                if (preg_match('/_uid$/', $constraintName)) {
+                    return new UniqueException('Document already exists', $e->getCode(), $e);
+                }
+            }
+
             return new DuplicateException('Document already exists', $e->getCode(), $e);
         }
📜 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 53e1a2e and 07f9850.

📒 Files selected for processing (3)
  • src/Database/Adapter/MariaDB.php (4 hunks)
  • src/Database/Adapter/Postgres.php (2 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (5 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/DocumentTests.php
🧬 Code graph analysis (3)
src/Database/Adapter/Postgres.php (2)
src/Database/Database.php (1)
  • Database (37-8144)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
src/Database/Adapter/MariaDB.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
tests/e2e/Adapter/Scopes/DocumentTests.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
⏰ 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 (Pool)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Unit Test
🔇 Additional comments (7)
src/Database/Adapter/Postgres.php (1)

16-16: LGTM!

The import alias for UniqueException is correctly added and follows the existing import pattern.

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

17-17: Import for UniqueException looks good

Alias is correct and aligns with new exception type.


5333-5335: Good fix: clearing $sequence before reusing the Document

This avoids accidental internal ID collisions when creating another document from a previously fetched instance.


5339-5345: Verify assumption: case-insensitive duplicate should not map to Unique

The test enforces Duplicate and not Unique. Ensure this path is consistently handled at the application layer (case-insensitive $id comparison) across all adapters, regardless of DB collation. Otherwise, some adapters may throw Unique or allow both IDs.

Do we have adapter-level guarantees normalizing $id uniqueness case-insensitively before hitting the DB?


4738-4767: Adapt exception assertion to be adapter-agnostic
Assert DuplicateException for all adapters; only assert UniqueException on SQL adapters. For example:

-        } catch (Throwable $e) {
-            $this->assertInstanceOf(UniqueException::class, $e);
-            $this->assertInstanceOf(DuplicateException::class, $e);
-        }
+        } catch (Throwable $e) {
+            $this->assertInstanceOf(DuplicateException::class, $e);
+            if ($database->getAdapter() instanceof SQL) {
+                $this->assertInstanceOf(UniqueException::class, $e);
+            }
+        }

Confirm that all CI adapters now throw UniqueException for unique-constraint violations; otherwise this change is necessary.

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

15-15: LGTM!

The import statement for UniqueException is correctly added and properly namespaced.


820-821: LGTM! Docblocks now correctly document both exception types.

The docblocks for both createDocument() and updateDocument() now properly advertise that they can throw both UniqueException (for unique constraint violations on non-document-ID fields) and DuplicateException (for document ID conflicts on _uid or PRIMARY keys). This accurately reflects the implementation's behavior.

Also applies to: 947-948

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

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

1801-1808: Handle PRIMARY key duplicates as DuplicateException.

MySQL reports PK conflicts as “for key 'PRIMARY'”. Treat these as document duplicates too.

-            if (preg_match("/for key '(?:[^.]+\.)?([^']+)'/", $e->getMessage(), $matches)) {
-                if ($matches[1] === '_uid') {
+            if (preg_match("/for key '(?:[^.]+\.)?([^']+)'/", $e->getMessage(), $matches)) {
+                if ($matches[1] === '_uid' || $matches[1] === 'PRIMARY') {
                     return new DuplicateException('Document already exists', $e->getCode(), $e);
                 }
             }
 
             return new UniqueException('Document already exists', $e->getCode(), $e);
tests/e2e/Adapter/Scopes/DocumentTests.php (2)

4803-4810: Reapply SQL-scoped Unique assertion (still outstanding)

Line 4808 repeats the unconditional UniqueException assertion we previously flagged; it will break non-SQL adapters. Please make the UniqueException check conditional on $database->getAdapter() instanceof SQL, keeping DuplicateException as the baseline. (Same fix as earlier feedback.)


4738-4767: Ensure adapter-agnostic duplicate assertions

Line 4745 currently requires UniqueException for every adapter. Non-SQL adapters only raise DuplicateException, so this assertion will fail those runs. Assert DuplicateException unconditionally and gate the UniqueException check behind an instanceof SQL guard.

         } catch (Throwable $e) {
-            $this->assertInstanceOf(UniqueException::class, $e);
-            $this->assertInstanceOf(DuplicateException::class, $e);
+            $this->assertInstanceOf(DuplicateException::class, $e);
+            if ($database->getAdapter() instanceof SQL) {
+                $this->assertInstanceOf(UniqueException::class, $e);
+            }
         }
🧹 Nitpick comments (4)
src/Database/Adapter/Postgres.php (1)

1928-1938: Also treat primary key (_id) duplicates as DuplicateException.

For parity with MariaDB/SQLite and intent (“document duplicate” vs other unique), map PK conflicts to DuplicateException too.

Apply this within the same block:

         if ($e->getCode() === '23505' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) {
             if (preg_match('/Key \(([^)]+)\)=\(.+\) already exists/', $e->getMessage(), $matches)) {
                 $columns = array_map('trim', explode(',', $matches[1]));
                 sort($columns);
                 $target = $this->sharedTables ? ['_tenant', '_uid'] : ['_uid'];
                 if ($columns == $target) {
                     return new DuplicateException('Document already exists', $e->getCode(), $e);
                 }
+                // Primary key duplicate (_id) also indicates a document-duplicate
+                if ($columns == ['_id']) {
+                    return new DuplicateException('Document already exists', $e->getCode(), $e);
+                }
             }
 
             return new UniqueException('Document already exists', $e->getCode(), $e);
         }

Also consider updating createDocument/updateDocument docblocks to include both UniqueException and DuplicateException to reflect this behavior.

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

520-521: Docblock should also include DuplicateException.

processException can return DuplicateException for _uid/PK conflicts.

  * @throws Exception
  * @throws PDOException
- * @throws UniqueException
+ * @throws UniqueException
+ * @throws DuplicateException

639-640: Docblock should also include DuplicateException.

Mirror createDocument; both paths can bubble DuplicateException.

  * @throws Exception
  * @throws PDOException
- * @throws UniqueException
+ * @throws UniqueException
+ * @throws DuplicateException
tests/e2e/Adapter/Scopes/DocumentTests.php (1)

5345-5345: Drop leftover debug output

var_dump($e); on Line 5345 leaks into the test output and should be removed.

-            var_dump($e);
📜 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 07f9850 and 231d53e.

📒 Files selected for processing (4)
  • src/Database/Adapter/MariaDB.php (4 hunks)
  • src/Database/Adapter/Postgres.php (2 hunks)
  • src/Database/Adapter/SQLite.php (6 hunks)
  • tests/e2e/Adapter/Scopes/DocumentTests.php (5 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/DocumentTests.php
🧬 Code graph analysis (4)
src/Database/Adapter/Postgres.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
src/Database/Adapter/MariaDB.php (3)
src/Database/Database.php (1)
  • Database (37-8144)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
tests/e2e/Adapter/Scopes/DocumentTests.php (3)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
src/Database/Document.php (2)
  • Document (12-470)
  • removeAttribute (287-293)
src/Database/Adapter/SQLite.php (6)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
src/Database/Adapter/MariaDB.php (1)
  • processException (1777-1839)
src/Database/Adapter/Postgres.php (1)
  • processException (1909-1951)
src/Database/Adapter/SQL.php (1)
  • processException (1943-1946)
src/Database/Adapter/MySQL.php (1)
  • processException (147-164)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (MySQL)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SharedTables/SQLite)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Unit Test
🔇 Additional comments (7)
src/Database/Adapter/Postgres.php (1)

16-16: Import looks good.

Consistent with other adapters.

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

15-15: Import looks good.

Aligns with new exception usage.


622-622: Good: delegate to processException.

Consistent, centralized mapping.


841-842: Good: delegate to processException.

Keeps behavior consistent.

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

15-15: Import looks good.

Matches new exception strategy.


820-823: Docblock update looks good.

Both UniqueException and DuplicateException are advertised.


947-950: Docblock update looks good.

Accurately reflects thrown exceptions.

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

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

1244-1261: Remove debug output; tighten duplicate classification to exact keys.

  • var_dump leaks to output; remove it.
  • Current in_array('_uid', $columns) can misclassify other unique constraints. Compare sorted sets to ['_uid'] (or ['_tenant','_uid'] when shared) or ['_id'] only.

Apply this diff:

         // Duplicate row
         if ($e->getCode() === '23000' && ($e->errorInfo[1] ?? null) === 19) {
             $msg = $e->errorInfo[2] ?? $e->getMessage();
 
-            // Match all table.column pairs (handles commas & spaces)
-            if (preg_match_all('/\b([^.]+)\.([^\s,]+)/', $msg, $matches, PREG_SET_ORDER)) {
-                $columns = array_map(fn ($m) => $m[2], $matches);
-                sort($columns);
-
-                var_dump($columns);
-
-                if ($columns === ['_tenant', '_uid'] || in_array('_uid', $columns)) {
-                    return new DuplicateException('Document already exists', $e->getCode(), $e);
-                }
-            }
+            // Extract columns from: "UNIQUE constraint failed: tbl._tenant, tbl._uid" or "tbl._id"
+            if (preg_match('/UNIQUE constraint failed:\s+(.+)/', $msg, $m)) {
+                $parts = array_map('trim', explode(',', $m[1]));
+                $cols = array_map(function (string $p): string {
+                    $dot = strrpos($p, '.');
+                    $col = $dot !== false ? substr($p, $dot + 1) : $p;
+                    return rtrim($col, ', ');
+                }, $parts);
+                sort($cols);
+                $target = $this->sharedTables ? ['_tenant', '_uid'] : ['_uid'];
+                if ($cols === $target || $cols === ['_id']) {
+                    return new DuplicateException('Document already exists', $e->getCode(), $e);
+                }
+            }
 
             return new UniqueException('Document already exists', $e->getCode(), $e);
         }
🧹 Nitpick comments (1)
src/Database/Adapter/SQLite.php (1)

520-521: Docblocks: also declare DuplicateException.

These methods can throw both UniqueException and DuplicateException via processException. Add DuplicateException to the @throws list.

  * @throws Exception
  * @throws PDOException
- * @throws UniqueException
+ * @throws UniqueException
+ * @throws DuplicateException

Also applies to: 639-640

📜 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 231d53e and 2b59a27.

📒 Files selected for processing (2)
  • src/Database/Adapter/Postgres.php (2 hunks)
  • src/Database/Adapter/SQLite.php (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/Database/Adapter/Postgres.php (2)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
src/Database/Adapter/SQLite.php (5)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)
src/Database/Adapter/Postgres.php (1)
  • processException (1909-1953)
src/Database/Adapter/MariaDB.php (1)
  • processException (1777-1839)
src/Database/Adapter/MySQL.php (1)
  • processException (147-164)
src/Database/Adapter/SQL.php (1)
  • processException (1943-1946)
⏰ 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/SQLite)
  • GitHub Check: Adapter Tests (SharedTables/Postgres)
  • GitHub Check: Adapter Tests (SharedTables/MariaDB)
  • GitHub Check: Adapter Tests (SQLite)
  • GitHub Check: Adapter Tests (Pool)
  • GitHub Check: Adapter Tests (Mirror)
  • GitHub Check: Adapter Tests (SharedTables/MySQL)
  • GitHub Check: Adapter Tests (Postgres)
  • GitHub Check: Adapter Tests (MariaDB)
  • GitHub Check: Adapter Tests (MySQL)
🔇 Additional comments (4)
src/Database/Adapter/Postgres.php (1)

16-16: LGTM: added UniqueException import.

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

15-15: LGTM: added UniqueException import.


622-623: LGTM: centralizes exception mapping.

Routing through processException is correct and consistent.


841-842: LGTM: centralizes exception mapping.

Routing through processException is correct and consistent.

Comment on lines 1801 to 1802
if (preg_match("/for key '(?:[^.]+\.)?([^']+)'/", $e->getMessage(), $matches)) {
if ($matches[1] === '_uid') {
Copy link
Member

Choose a reason for hiding this comment

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

Can we just check contains for key '_uid' instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess in MySQL we can, since it prints the name of the index, and not the attribute key

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'CaseSensitive-1' for key '_documents._uid'

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

📜 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 ce620a6 and 6a9c42a.

📒 Files selected for processing (1)
  • src/Database/Adapter/MariaDB.php (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/Database/Adapter/MariaDB.php (3)
src/Database/Database.php (1)
  • Database (37-8144)
src/Database/Exception.php (1)
  • Exception (7-21)
src/Database/Exception/Unique.php (1)
  • Unique (5-7)

…eption

# Conflicts:
#	tests/e2e/Adapter/Scopes/DocumentTests.php
@coderabbitai coderabbitai bot mentioned this pull request Oct 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants