Skip to content
11 changes: 10 additions & 1 deletion src/Database/Adapter/MariaDB.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Utopia\Database\Exception\Query as QueryException;
use Utopia\Database\Exception\Timeout as TimeoutException;
use Utopia\Database\Exception\Truncate as TruncateException;
use Utopia\Database\Exception\Unique as UniqueException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;

Expand Down Expand Up @@ -816,6 +817,7 @@ public function deleteIndex(string $collection, string $id): bool
* @return Document
* @throws Exception
* @throws PDOException
* @throws UniqueException
* @throws DuplicateException
* @throws \Throwable
*/
Expand Down Expand Up @@ -942,6 +944,7 @@ public function createDocument(Document $collection, Document $document): Docume
* @return Document
* @throws Exception
* @throws PDOException
* @throws UniqueException
* @throws DuplicateException
* @throws \Throwable
*/
Expand Down Expand Up @@ -1795,7 +1798,13 @@ protected function processException(PDOException $e): \Exception

// Duplicate row
if ($e->getCode() === '23000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1062) {
return new DuplicateException('Document already exists', $e->getCode(), $e);
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'

return new DuplicateException('Document already exists', $e->getCode(), $e);
}
}

return new UniqueException('Document already exists', $e->getCode(), $e);
}

// Data is too big for column resize
Expand Down
12 changes: 11 additions & 1 deletion src/Database/Adapter/Postgres.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Utopia\Database\Exception\Timeout as TimeoutException;
use Utopia\Database\Exception\Transaction as TransactionException;
use Utopia\Database\Exception\Truncate as TruncateException;
use Utopia\Database\Exception\Unique as UniqueException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;

Expand Down Expand Up @@ -1924,7 +1925,16 @@ protected function processException(PDOException $e): \Exception

// Duplicate row
if ($e->getCode() === '23505' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 7) {
return new DuplicateException('Document already exists', $e->getCode(), $e);
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);
}
}

return new UniqueException('Document already exists', $e->getCode(), $e);
}

// Data is too big for column resize
Expand Down
29 changes: 14 additions & 15 deletions src/Database/Adapter/SQLite.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Exception\Duplicate as DuplicateException;
use Utopia\Database\Exception\NotFound as NotFoundException;
use Utopia\Database\Exception\Timeout as TimeoutException;
use Utopia\Database\Exception\Transaction as TransactionException;
use Utopia\Database\Exception\Unique as UniqueException;
use Utopia\Database\Helpers\ID;

/**
Expand Down Expand Up @@ -517,7 +517,7 @@ public function deleteIndex(string $collection, string $id): bool
* @return Document
* @throws Exception
* @throws PDOException
* @throws Duplicate
* @throws UniqueException
*/
public function createDocument(Document $collection, Document $document): Document
{
Expand Down Expand Up @@ -619,10 +619,7 @@ public function createDocument(Document $collection, Document $document): Docume
$stmtPermissions->execute();
}
} catch (PDOException $e) {
throw match ($e->getCode()) {
"1062", "23000" => new Duplicate('Duplicated document: ' . $e->getMessage()),
default => $e,
};
throw $this->processException($e);
}


Expand All @@ -639,7 +636,7 @@ public function createDocument(Document $collection, Document $document): Docume
* @return Document
* @throws Exception
* @throws PDOException
* @throws Duplicate
* @throws UniqueException
*/
public function updateDocument(Document $collection, string $id, Document $document, bool $skipPermissions): Document
{
Expand Down Expand Up @@ -841,11 +838,7 @@ public function updateDocument(Document $collection, string $id, Document $docum
$stmtAddPermissions->execute();
}
} catch (PDOException $e) {
throw match ($e->getCode()) {
'1062',
'23000' => new Duplicate('Duplicated document: ' . $e->getMessage()),
default => $e,
};
throw $this->processException($e);
}

return $document;
Expand Down Expand Up @@ -1248,9 +1241,15 @@ protected function processException(PDOException $e): \Exception
return new TimeoutException('Query timed out', $e->getCode(), $e);
}

// Duplicate
if ($e->getCode() === 'HY000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 1) {
return new DuplicateException('Document already exists', $e->getCode(), $e);
// Duplicate row
if ($e->getCode() === '23000' && isset($e->errorInfo[1]) && $e->errorInfo[1] === 19) {
if (preg_match('/UNIQUE constraint failed:\s+([^.]+)\.([^\s]+)/', $e->getMessage(), $m)) {
if ($m[2] === '_uid') {
return new DuplicateException('Document already exists', $e->getCode(), $e);
}
}

return new UniqueException('Document already exists', $e->getCode(), $e);
}

return $e;
Expand Down
7 changes: 7 additions & 0 deletions src/Database/Exception/Unique.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Utopia\Database\Exception;

class Unique extends Duplicate
{
}
95 changes: 61 additions & 34 deletions tests/e2e/Adapter/Scopes/DocumentTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Utopia\Database\Exception\Limit as LimitException;
use Utopia\Database\Exception\Structure as StructureException;
use Utopia\Database\Exception\Type as TypeException;
use Utopia\Database\Exception\Unique as UniqueException;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
Expand Down Expand Up @@ -4729,36 +4730,41 @@ public function testWritePermissionsUpdateFailure(Document $document): Document
*/
public function testUniqueIndexDuplicate(): void
{
$this->expectException(DuplicateException::class);

/** @var Database $database */
$database = static::getDatabase();

$this->assertEquals(true, $database->createIndex('movies', 'uniqueIndex', Database::INDEX_UNIQUE, ['name'], [128], [Database::ORDER_ASC]));

$database->createDocument('movies', new Document([
'$permissions' => [
Permission::read(Role::any()),
Permission::read(Role::user('1')),
Permission::read(Role::user('2')),
Permission::create(Role::any()),
Permission::create(Role::user('1x')),
Permission::create(Role::user('2x')),
Permission::update(Role::any()),
Permission::update(Role::user('1x')),
Permission::update(Role::user('2x')),
Permission::delete(Role::any()),
Permission::delete(Role::user('1x')),
Permission::delete(Role::user('2x')),
],
'name' => 'Frozen',
'director' => 'Chris Buck & Jennifer Lee',
'year' => 2013,
'price' => 39.50,
'active' => true,
'genres' => ['animation', 'kids'],
'with-dash' => 'Works4'
]));
try {
$database->createDocument('movies', new Document([
'$permissions' => [
Permission::read(Role::any()),
Permission::read(Role::user('1')),
Permission::read(Role::user('2')),
Permission::create(Role::any()),
Permission::create(Role::user('1x')),
Permission::create(Role::user('2x')),
Permission::update(Role::any()),
Permission::update(Role::user('1x')),
Permission::update(Role::user('2x')),
Permission::delete(Role::any()),
Permission::delete(Role::user('1x')),
Permission::delete(Role::user('2x')),
],
'name' => 'Frozen',
'director' => 'Chris Buck & Jennifer Lee',
'year' => 2013,
'price' => 39.50,
'active' => true,
'genres' => ['animation', 'kids'],
'with-dash' => 'Works4'
]));

$this->fail('Failed to throw exception');
} catch (Throwable $e) {
$this->assertInstanceOf(UniqueException::class, $e);
$this->assertInstanceOf(DuplicateException::class, $e);
}
}
/**
* @depends testUniqueIndexDuplicate
Expand Down Expand Up @@ -4794,9 +4800,14 @@ public function testUniqueIndexDuplicateUpdate(): void
'with-dash' => 'Works4'
]));

$this->expectException(DuplicateException::class);
try {
$database->updateDocument('movies', $document->getId(), $document->setAttribute('name', 'Frozen'));

$database->updateDocument('movies', $document->getId(), $document->setAttribute('name', 'Frozen'));
$this->fail('Failed to throw exception');
} catch (Throwable $e) {
$this->assertInstanceOf(UniqueException::class, $e);
$this->assertInstanceOf(DuplicateException::class, $e);
}
}

public function propagateBulkDocuments(string $collection, int $amount = 10, bool $documentSecurity = false): void
Expand Down Expand Up @@ -5298,9 +5309,18 @@ public function testExceptionDuplicate(Document $document): void
$database = static::getDatabase();

$document->setAttribute('$id', 'duplicated');
$document->removeAttribute('$sequence');

$database->createDocument($document->getCollection(), $document);
$this->expectException(DuplicateException::class);
$database->createDocument($document->getCollection(), $document);
$document->removeAttribute('$sequence');

try {
$database->createDocument($document->getCollection(), $document);
$this->fail('Failed to throw exception');
} catch (Throwable $e) {
$this->assertInstanceOf(DuplicateException::class, $e);
$this->assertNotInstanceOf(UniqueException::class, $e);
}
}

/**
Expand All @@ -5312,13 +5332,20 @@ public function testExceptionCaseInsensitiveDuplicate(Document $document): Docum
$database = static::getDatabase();

$document->setAttribute('$id', 'caseSensitive');
$document->setAttribute('$sequence', '200');
$database->createDocument($document->getCollection(), $document);
$document->removeAttribute('$sequence');

$document->setAttribute('$id', 'CaseSensitive');

$this->expectException(DuplicateException::class);
$database->createDocument($document->getCollection(), $document);
$document->setAttribute('$id', 'caseSensitive'); // Revert this
$document->removeAttribute('$sequence');

try {
$database->createDocument($document->getCollection(), $document);
$this->fail('Failed to throw exception');
} catch (Throwable $e) {
var_dump($e);
$this->assertInstanceOf(DuplicateException::class, $e);
$this->assertNotInstanceOf(UniqueException::class, $e);
}

return $document;
}
Expand Down
Loading