Skip to content

Commit 656258b

Browse files
committed
(fix): translate getDocument PDO exceptions
1 parent a1796fd commit 656258b

2 files changed

Lines changed: 176 additions & 7 deletions

File tree

‎src/Database/Adapter/SQL.php‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -397,17 +397,36 @@ public function getDocument(Document $collection, string $id, array $queries = [
397397

398398
$sql = $this->trigger(Database::EVENT_DOCUMENT_READ, $sql);
399399

400-
$stmt = $this->getPDO()->prepare($sql);
400+
$stmt = null;
401+
$document = [];
402+
$exception = null;
401403

402-
$stmt->bindValue(':_uid', $id);
404+
try {
405+
$stmt = $this->getPDO()->prepare($sql);
403406

404-
if ($this->sharedTables) {
405-
$stmt->bindValue(':_tenant', $this->getTenant());
407+
$stmt->bindValue(':_uid', $id);
408+
409+
if ($this->sharedTables) {
410+
$stmt->bindValue(':_tenant', $this->getTenant());
411+
}
412+
413+
$this->execute($stmt);
414+
$document = $stmt->fetchAll();
415+
} catch (PDOException $e) {
416+
$exception = $e;
417+
} finally {
418+
if ($stmt !== null) {
419+
try {
420+
$stmt->closeCursor();
421+
} catch (PDOException $e) {
422+
$exception ??= $e;
423+
}
424+
}
406425
}
407426

408-
$stmt->execute();
409-
$document = $stmt->fetchAll();
410-
$stmt->closeCursor();
427+
if ($exception !== null) {
428+
throw $this->processException($exception);
429+
}
411430

412431
if (empty($document)) {
413432
return new Document([]);

‎tests/unit/SQLGetDocumentTest.php‎

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
namespace Tests\Unit;
4+
5+
use Exception;
6+
use PDOException;
7+
use PHPUnit\Framework\TestCase;
8+
use ReflectionProperty;
9+
use Utopia\Database\Adapter\MySQL;
10+
use Utopia\Database\Adapter\Postgres;
11+
use Utopia\Database\Document;
12+
use Utopia\Database\Exception\Timeout as TimeoutException;
13+
14+
final class SQLGetDocumentTest extends TestCase
15+
{
16+
public function testTranslatesExecuteTimeoutClosesCursorAndPreservesOriginalWhenCleanupFails(): void
17+
{
18+
$exception = $this->createTimeoutException();
19+
20+
$statement = $this->getMockBuilder(\PDOStatement::class)
21+
->disableOriginalConstructor()
22+
->getMock();
23+
$statement->expects($this->once())
24+
->method('bindValue')
25+
->with(':_uid', 'document')
26+
->willReturn(true);
27+
$statement->expects($this->once())
28+
->method('execute')
29+
->willThrowException($exception);
30+
$statement->expects($this->never())
31+
->method('fetchAll');
32+
$statement->expects($this->once())
33+
->method('closeCursor')
34+
->willThrowException(new PDOException('Failed to close cursor'));
35+
36+
$this->assertTimeout($this->createMySQL($statement), $exception);
37+
}
38+
39+
public function testTranslatesFetchTimeoutAndClosesCursor(): void
40+
{
41+
$exception = $this->createTimeoutException();
42+
43+
$statement = $this->getMockBuilder(\PDOStatement::class)
44+
->disableOriginalConstructor()
45+
->getMock();
46+
$statement->expects($this->once())
47+
->method('bindValue')
48+
->with(':_uid', 'document')
49+
->willReturn(true);
50+
$statement->expects($this->once())
51+
->method('execute')
52+
->willReturn(true);
53+
$statement->expects($this->once())
54+
->method('fetchAll')
55+
->willThrowException($exception);
56+
$statement->expects($this->once())
57+
->method('closeCursor')
58+
->willReturn(true);
59+
60+
$this->assertTimeout($this->createMySQL($statement), $exception);
61+
}
62+
63+
public function testUsesPostgresExecuteHook(): void
64+
{
65+
$statement = $this->getMockBuilder(\PDOStatement::class)
66+
->disableOriginalConstructor()
67+
->getMock();
68+
$statement->expects($this->once())
69+
->method('bindValue')
70+
->with(':_uid', 'document')
71+
->willReturn(true);
72+
$statement->expects($this->once())
73+
->method('execute')
74+
->willReturn(true);
75+
$statement->expects($this->once())
76+
->method('fetchAll')
77+
->willReturn([]);
78+
$statement->expects($this->once())
79+
->method('closeCursor')
80+
->willReturn(true);
81+
82+
$pdo = $this->getMockBuilder(\PDO::class)
83+
->disableOriginalConstructor()
84+
->getMock();
85+
$pdo->expects($this->once())
86+
->method('prepare')
87+
->willReturn($statement);
88+
$pdo->expects($this->exactly(2))
89+
->method('exec')
90+
->withConsecutive(
91+
["SET statement_timeout = '25ms'"],
92+
['RESET statement_timeout']
93+
)
94+
->willReturnOnConsecutiveCalls(0, 0);
95+
96+
$adapter = new Postgres($pdo);
97+
$adapter->setDatabase('database');
98+
$adapter->setNamespace('namespace');
99+
$adapter->setTimeout(25);
100+
101+
$document = $adapter->getDocument(
102+
new Document(['$id' => 'collection']),
103+
'document'
104+
);
105+
106+
$this->assertSame([], $document->getArrayCopy());
107+
}
108+
109+
private function assertTimeout(MySQL $adapter, PDOException $exception): void
110+
{
111+
try {
112+
$adapter->getDocument(
113+
new Document(['$id' => 'collection']),
114+
'document'
115+
);
116+
} catch (TimeoutException $timeout) {
117+
$this->assertSame($exception, $timeout->getPrevious());
118+
119+
return;
120+
}
121+
122+
$this->fail('Expected a timeout exception.');
123+
}
124+
125+
private function createMySQL(\PDOStatement $statement): MySQL
126+
{
127+
$pdo = $this->getMockBuilder(\PDO::class)
128+
->disableOriginalConstructor()
129+
->getMock();
130+
$pdo->expects($this->once())
131+
->method('prepare')
132+
->willReturn($statement);
133+
134+
$adapter = new MySQL($pdo);
135+
$adapter->setDatabase('database');
136+
$adapter->setNamespace('namespace');
137+
138+
return $adapter;
139+
}
140+
141+
private function createTimeoutException(): PDOException
142+
{
143+
$exception = new PDOException('Query execution was interrupted');
144+
$code = new ReflectionProperty(Exception::class, 'code');
145+
$code->setValue($exception, 'HY000');
146+
$exception->errorInfo = ['HY000', 3024, 'Query execution was interrupted'];
147+
148+
return $exception;
149+
}
150+
}

0 commit comments

Comments
 (0)