Skip to content

Unique Exception - #733

Merged
abnegate merged 2 commits into
mainfrom
unique-exception
Aug 13, 2026
Merged

Unique Exception#733
abnegate merged 2 commits into
mainfrom
unique-exception

Conversation

@fogelito

@fogelitofogelito commented Oct 12, 2025

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

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

    • Database adapters now distinguish primary-identifier duplicates from other unique-index conflicts.
    • Non-primary unique-index conflicts now provide clearer, more specific error reporting.
    • Existing primary-identifier duplicate handling remains unchanged.
  • Tests

    • Updated end-to-end coverage to verify both duplicate and unique-index error outcomes.

@coderabbitai

coderabbitaiBot commented Oct 12, 2025

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds UniqueException as a subtype of DuplicateException. Database adapters now use it for unique-index violations while retaining DuplicateException for _uid conflicts. End-to-end tests verify both exception types and the updated message.

Changes

Unique constraint exception mapping

Layer / File(s)Summary
Exception contract and adapter error mapping
src/Database/Exception/Unique.php, src/Database/Adapter/{MariaDB,Postgres,SQLite,Mongo,Redis}.php
Adds UniqueException. Adapters map non-_uid duplicate or unique-index conflicts to this exception and retain document-ID duplicate handling.
In-memory uniqueness errors
src/Database/Adapter/Memory.php
Batch conflicts, hash collisions, and unique-signature validation now throw UniqueException.
End-to-end exception assertions
tests/e2e/Adapter/Scopes/DocumentTests.php
Tests distinguish document-ID duplicates from unique-index violations and verify the UniqueException type and Unique index violation message.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers:abnegate

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 75.00% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly identifies the main change: adding and using a distinct unique-constraint exception.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch unique-exception

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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/Database/Adapter/SQLite.php Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/Database/Adapter/SQLite.php Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 notUniqueException. 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.

Comment threadsrc/Database/Adapter/MariaDB.php
Comment threadsrc/Database/Adapter/MariaDB.php Outdated
Comment threadsrc/Database/Adapter/MariaDB.php Outdated
Comment threadsrc/Database/Adapter/MariaDB.php Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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

Comment threadsrc/Database/Adapter/MariaDB.php Outdated
Comment threadtests/e2e/Adapter/Scopes/DocumentTests.php
Comment threadtests/e2e/Adapter/Scopes/DocumentTests.php

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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.

Comment threadsrc/Database/Adapter/SQLite.php Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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 threadsrc/Database/Adapter/Postgres.php Outdated
Comment threadsrc/Database/Adapter/MariaDB.php Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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)

Comment threadsrc/Database/Adapter/MariaDB.php Outdated
@coderabbitaicoderabbitaiBot mentioned this pull request Oct 28, 2025
Comment threadsrc/Database/Exception/Unique.php
@greptile-apps

greptile-appsBot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR introduces a distinct Unique subtype for non-identifier unique-index conflicts while preserving compatibility through Duplicate.

  • Classifies native constraint failures using the key or columns reported by MariaDB/MySQL, Postgres, SQLite, and Mongo.
  • Updates Memory and Redis to throw the new subtype for unique-index conflicts.
  • Adds unit and end-to-end regression coverage for identifier conflicts, composite indexes, unparseable messages, and _uid appearing in unrelated values or index names.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

FilenameOverview
src/Database/Adapter/MariaDB.phpParses the violated key and distinguishes document identifiers, permission keys, and user unique indexes while retaining a conservative fallback.
src/Database/Adapter/Postgres.phpParses and normalizes DETAIL columns, exactly recognizes document-identifier constraints, and falls back to Duplicate when parsing fails.
src/Database/Adapter/SQLite.phpReplaces broad substring matching with normalized exact-column comparison, resolving the reported composite-index misclassification.
src/Database/Adapter/Mongo.phpClassifies duplicate-key errors from the reported index name rather than unrelated message substrings.
src/Database/Adapter/Memory.phpThrows Unique for each in-memory user unique-index collision path.
src/Database/Adapter/Redis.phpThrows Unique when Redis detects a conflicting unique-index signature.
src/Database/Exception/Unique.phpDefines Unique as a backward-compatible Duplicate subtype, resolving the prior hierarchy concern.
tests/unit/UniqueViolationTest.phpCovers adapter classification, parser fallbacks, composite indexes, and misleading _uid substrings.
tests/e2e/Adapter/Scopes/DocumentTests.phpVerifies the exception identities and messages through document create and update flows.

Reviews (4): Last reviewed commit: "(fix): classify unique violations by vio..." | Re-trigger Greptile

Comment threadsrc/Database/Adapter/SQLite.php Outdated
Comment threadsrc/Database/Adapter/Postgres.php
Comment threadsrc/Database/Exception/Unique.php Outdated
@ChiragAgg5k

Copy link
Copy Markdown
Member

Rebased this PR onto current main and squashed it down to the actual UniqueException change so it is mergeable/reviewable again.

What changed:

  • UniqueException now extends DuplicateException for backwards compatibility.
  • SQL adapters, Mongo, Redis, and Memory now throw UniqueException for non-_uid unique constraint violations.
  • Duplicate document IDs still throw plain DuplicateException.
  • E2E assertions verify unique-index violations are both DuplicateException and UniqueException, while duplicate IDs are not UniqueException.

Local checks:

composer lint src/Database/Exception/Unique.php src/Database/Adapter/MariaDB.php src/Database/Adapter/Postgres.php src/Database/Adapter/SQLite.php src/Database/Adapter/Mongo.php src/Database/Adapter/Memory.php src/Database/Adapter/Redis.php tests/e2e/Adapter/Scopes/DocumentTests.php
composer check

Note: a local targeted PHPUnit run requires service containers (redis host was unavailable locally), so I left full e2e to CI.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Database/Adapter/Postgres.php`:
- Around line 2163-2166: Update the unique-violation classification in the
Postgres adapter to parse the PostgreSQL “DETAIL: Key (...)” column list and
compare those columns with the document ID key, rather than searching the full
exception message for “_uid”. Return UniqueException for violations of other
unique indexes and preserve DuplicateException only when the extracted key
columns identify the document ID.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 954c30f7-6805-42a0-a063-7634d821a09f

📥 Commits

Reviewing files that changed from the base of the PR and between 968fff6 and e10f682.

📒 Files selected for processing (7)
  • src/Database/Adapter/MariaDB.php
  • src/Database/Adapter/Memory.php
  • src/Database/Adapter/Mongo.php
  • src/Database/Adapter/Postgres.php
  • src/Database/Adapter/Redis.php
  • src/Database/Adapter/SQLite.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
💤 Files with no reviewable changes (5)
  • src/Database/Adapter/Redis.php
  • src/Database/Adapter/Memory.php
  • src/Database/Adapter/SQLite.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
  • src/Database/Adapter/Mongo.php

Comment threadsrc/Database/Adapter/Postgres.php Outdated
Searching the whole driver message for `_uid` misreads any conflict whose
index name or conflicting value happens to contain that substring, so a
plain unique-index violation was reported as a document-ID duplicate.
Each adapter now parses the key the driver actually names — MariaDB/MySQL
the key name, Postgres the DETAIL column list, SQLite the failed column
list, Mongo the index name — and falls back to DuplicateException when the
message cannot be parsed, preserving pre-existing behaviour rather than
silently downgrading a document-ID duplicate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abnegate

Copy link
Copy Markdown
Member

Pushed 23eaafb addressing the open review threads.

What changed

Every adapter was classifying the conflict by searching the whole driver message for _uid. That misreads any conflict whose index name or conflicting value happens to contain that substring, so a plain unique-index violation was reported as a document-ID duplicate. Each adapter now parses the key the driver actually names:

AdapterParsed fromDocument-ID key
MariaDB / MySQLfor key 'tbl.<name>'_uid, PRIMARY
PostgresDETAIL: Key (<cols>)={_uid} / {_uid, _tenant}
SQLiteUNIQUE constraint failed: tbl.<col>, …{_uid} / {_uid, _tenant}
Mongoindex: <name> dup key_uid

When the message cannot be parsed, all four fall back to DuplicateException — the pre-PR behaviour — rather than silently downgrading a document-ID duplicate to UniqueException.

Unique stays a subclass of Duplicate so existing call sites keep working; that also means the existing @throws DuplicateException docblocks remain accurate and were left alone.

Verification

  • New tests/unit/UniqueViolationTest.php — 15 cases across all four adapters. 9 of them fail on the previous head and pass on this one (verified by reverting src/ and re-running).
  • New e2e case in testDuplicateExceptionMessages: a conflicting attribute value containing _uid must raise Unique, not a document-ID Duplicate.
  • composer lint, composer check (PHPStan level 7), and the full unit suite (443 tests) pass locally. e2e left to CI.

@greptileai review

@abnegate
abnegate merged commit a8e8386 into mainAug 13, 2026
22 checks passed
@abnegate
abnegate deleted the unique-exception branch August 13, 2026 00:53
Sign up for freeto 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.

4 participants

@fogelito@ChiragAgg5k@abnegate